API 报错

This commit is contained in:
董海洋
2026-06-04 08:20:49 +08:00
parent 21d0277a77
commit 5300551e21
7 changed files with 332 additions and 9 deletions
+1
View File
@@ -51,6 +51,7 @@ const errorHandler = async (ctx, next) => {
code: 404, code: 404,
message: '接口不存在' message: '接口不存在'
} }
ctx.status = 404
} }
} catch (error) { } catch (error) {
console.error('Server error:', error) console.error('Server error:', error)
+77 -1
View File
@@ -361,8 +361,84 @@ async function refundPayment(ctx) {
} }
} }
/**
* 查询支付结果
* 调用微信支付订单查询API,确认支付状态
*/
async function queryPayment(ctx) {
const { orderId } = ctx.params
if (!orderId) {
ctx.body = { code: 400, message: '订单ID不能为空' }
return
}
// 查询本地订单
const orders = await query('SELECT * FROM orders WHERE id = ?', [orderId])
if (orders.length === 0) {
ctx.body = { code: 404, message: '订单不存在' }
return
}
const order = orders[0]
// 如果本地已标记为已支付,直接返回
if (order.status === 'paid') {
ctx.body = { code: 200, data: { orderId: order.id, status: 'paid', transactionId: order.transaction_id } }
return
}
// 检查支付配置
if (!APPID || !MCH_ID || !API_KEY) {
ctx.body = { code: 200, data: { orderId: order.id, status: order.status } }
return
}
// 调用微信订单查询API
const params = {
appid: APPID,
mch_id: MCH_ID,
out_trade_no: orderId,
nonce_str: generateNonceStr()
}
params.sign = generateSign(params)
try {
const xmlData = buildXML(params)
const response = await fetch(ORDER_QUERY_URL, {
method: 'POST',
body: xmlData,
headers: { 'Content-Type': 'application/xml' }
})
const resultXml = await response.text()
const result = await parseXML(resultXml)
if (result.return_code !== 'SUCCESS') {
ctx.body = { code: 500, message: '查询支付状态通信失败' }
return
}
if (result.trade_state === 'SUCCESS') {
// 微信确认已支付,更新本地订单状态
const transactionId = result.transaction_id || null
await query(
'UPDATE orders SET status = ?, transaction_id = ? WHERE id = ? AND status != ?',
['paid', transactionId, orderId, 'paid']
)
ctx.body = { code: 200, data: { orderId: order.id, status: 'paid', transactionId } }
} else {
ctx.body = { code: 200, data: { orderId: order.id, status: order.status, tradeState: result.trade_state } }
}
} catch (error) {
console.error('查询支付状态失败:', error)
ctx.body = { code: 200, data: { orderId: order.id, status: order.status } }
}
}
module.exports = { module.exports = {
createPayment, createPayment,
paymentNotify, paymentNotify,
refundPayment refundPayment,
queryPayment
} }
+42 -1
View File
@@ -139,8 +139,49 @@ async function getStockByGoodsId(ctx) {
} }
} }
// 获取库存变动日志
async function getStockLogs(ctx) {
const goodsId = ctx.query.goods_id
const page = parseInt(ctx.query.page) || 1
const pageSize = parseInt(ctx.query.pageSize) || 20
await ensureStockLogTable()
let sql = `
SELECT
sl.id,
sl.goods_id,
g.name as goods_name,
sl.change_type,
sl.delta,
sl.quantity_after,
sl.operator_id,
sl.remark,
sl.created_at
FROM stock_logs sl
LEFT JOIN goods g ON sl.goods_id = g.id
WHERE 1=1
`
const params = []
if (goodsId) {
sql += ' AND sl.goods_id = ?'
params.push(goodsId)
}
sql += ' ORDER BY sl.created_at DESC'
const result = await paginate(query, sql, params, page, pageSize)
ctx.body = {
code: 200,
...result
}
}
module.exports = { module.exports = {
getStockList, getStockList,
adjustStock, adjustStock,
getStockByGoodsId getStockByGoodsId,
getStockLogs
} }
+205 -7
View File
@@ -20,13 +20,15 @@ async function issueTokenPair(user) {
} }
function publicUser(user, tokenObj) { function publicUser(user, tokenObj) {
const roleMap = { 0: 'customer', 1: 'staff', 2: 'admin' }
const base = { const base = {
id: user.id, id: user.id,
phone: user.phone, phone: user.phone,
name: user.name, name: user.name,
avatar: user.avatar, avatar: user.avatar,
points: user.points, points: user.points,
role: user.role role: roleMap[user.role] || 'customer',
roleCode: user.role
} }
if (tokenObj) { if (tokenObj) {
return { ...base, token: tokenObj.access, accessToken: tokenObj.access, refreshToken: tokenObj.refresh, legacyToken: tokenObj.legacy, accessTtl: tokenObj.accessTtl, refreshTtl: tokenObj.refreshTtl } return { ...base, token: tokenObj.access, accessToken: tokenObj.access, refreshToken: tokenObj.refresh, legacyToken: tokenObj.legacy, accessTtl: tokenObj.accessTtl, refreshTtl: tokenObj.refreshTtl }
@@ -62,8 +64,8 @@ async function login(ctx) {
return return
} }
if (loginType === 'admin' && user.role !== 2) { if (loginType === 'admin' && user.role !== 1 && user.role !== 2) {
ctx.body = { code: 401, message: '该账号不是管理员账号' } ctx.body = { code: 401, message: '该账号不是管理员或店员账号' }
return return
} }
@@ -88,6 +90,39 @@ async function login(ctx) {
} }
// 用户注册(普通用户) // 用户注册(普通用户)
// 注册频率限制(IP+手机号维度)
const registerLimiter = new Map()
const REGISTER_LIMIT_WINDOW = 60 * 1000 // 1分钟
const REGISTER_LIMIT_MAX = 3 // 每分钟最多3次
function checkRegisterLimit(ip, phone) {
const key = `${ip}:${phone}`
const now = Date.now()
const record = registerLimiter.get(key)
if (!record || now - record.time > REGISTER_LIMIT_WINDOW) {
registerLimiter.set(key, { time: now, count: 1 })
return true
}
if (record.count >= REGISTER_LIMIT_MAX) {
return false
}
record.count++
return true
}
// 定期清理过期记录
setInterval(() => {
const now = Date.now()
for (const [key, record] of registerLimiter) {
if (now - record.time > REGISTER_LIMIT_WINDOW) {
registerLimiter.delete(key)
}
}
}, 60000)
async function register(ctx) { async function register(ctx) {
const { phone, password, name } = ctx.request.body const { phone, password, name } = ctx.request.body
if (!phone || !password || !name) { if (!phone || !password || !name) {
@@ -95,6 +130,13 @@ async function register(ctx) {
return return
} }
// 注册频率限制
const clientIp = ctx.ip || 'unknown'
if (!checkRegisterLimit(clientIp, phone)) {
ctx.body = { code: 429, message: '注册过于频繁,请稍后再试' }
return
}
if (password.length < 8) { if (password.length < 8) {
ctx.body = { code: 400, message: '密码至少8位' } ctx.body = { code: 400, message: '密码至少8位' }
return return
@@ -293,7 +335,7 @@ async function getUsers(ctx) {
} }
} }
const USER_UPDATEABLE_FIELDS = ['name', 'avatar', 'points', 'status'] const USER_UPDATEABLE_FIELDS = ['name', 'avatar', 'points', 'status', 'role']
// 更新用户信息(管理员可改任意人;本人仅可改 name/avatar // 更新用户信息(管理员可改任意人;本人仅可改 name/avatar
async function updateUser(ctx) { async function updateUser(ctx) {
@@ -309,6 +351,10 @@ async function updateUser(ctx) {
ctx.body = { code: 403, message: '无权修改他人信息' } ctx.body = { code: 403, message: '无权修改他人信息' }
return return
} }
if ('role' in ctx.request.body) {
ctx.body = { code: 403, message: '无权修改角色' }
return
}
allowedFields = ['name', 'avatar'] allowedFields = ['name', 'avatar']
} }
@@ -319,7 +365,7 @@ async function updateUser(ctx) {
} }
} }
const { name, avatar, points, status } = filtered const { name, avatar, points, status, role } = filtered
const updateFields = [] const updateFields = []
const updateParams = [] const updateParams = []
@@ -340,6 +386,15 @@ async function updateUser(ctx) {
updateFields.push('status = ?') updateFields.push('status = ?')
updateParams.push(parseInt(status)) updateParams.push(parseInt(status))
} }
if (role !== undefined) {
const roleVal = parseInt(role)
if (![0, 1, 2].includes(roleVal)) {
ctx.body = { code: 400, message: '角色值无效,必须为 0(用户)/1(店员)/2(管理员)' }
return
}
updateFields.push('role = ?')
updateParams.push(roleVal)
}
if (updateFields.length === 0) { if (updateFields.length === 0) {
ctx.body = { ctx.body = {
@@ -619,7 +674,7 @@ async function wechatLogin(ctx) {
// 已有用户,直接登录 // 已有用户,直接登录
const user = users[0] const user = users[0]
const tokenObj = await issueTokenPair(user) const tokenObj = await issueTokenPair(user)
ctx.body = { code: 200, data: publicUser(user, tokenObj) } ctx.body = { code: 200, data: { ...publicUser(user, tokenObj), isNewUser: false } }
} else { } else {
// 新用户,自动注册 // 新用户,自动注册
const phone = `wx_${openid.slice(0, 10)}` const phone = `wx_${openid.slice(0, 10)}`
@@ -630,7 +685,7 @@ async function wechatLogin(ctx) {
) )
const newUser = { id: result.insertId, phone, name, avatar: '', points: 0, role: 0, openid } const newUser = { id: result.insertId, phone, name, avatar: '', points: 0, role: 0, openid }
const tokenObj = await issueTokenPair(newUser) const tokenObj = await issueTokenPair(newUser)
ctx.body = { code: 200, data: publicUser(newUser, tokenObj), isNewUser: true } ctx.body = { code: 200, data: { ...publicUser(newUser, tokenObj), isNewUser: true } }
} }
} catch (err) { } catch (err) {
console.error('微信登录异常:', err) console.error('微信登录异常:', err)
@@ -638,6 +693,146 @@ async function wechatLogin(ctx) {
} }
} }
// ============ 忘记密码 - 验证码相关 ============
// 内存中存储验证码(生产环境应使用 Redis)
const resetCodeStore = new Map()
// 生成6位数字验证码
function generateVerifyCode() {
return String(Math.floor(100000 + Math.random() * 900000))
}
// 发送重置密码验证码
async function sendResetCode(ctx) {
const { phone } = ctx.request.body
if (!phone || phone.length !== 11) {
ctx.body = { code: 400, message: '请输入正确的手机号' }
return
}
// 检查用户是否存在
const users = await query('SELECT id FROM users WHERE phone = ?', [phone])
if (users.length === 0) {
ctx.body = { code: 404, message: '该手机号未注册' }
return
}
// 检查发送频率(60秒内不可重复发送)
const existing = resetCodeStore.get(phone)
if (existing && Date.now() - existing.createdAt < 60000) {
ctx.body = { code: 429, message: '验证码发送过于频繁,请稍后再试' }
return
}
const code = generateVerifyCode()
resetCodeStore.set(phone, {
code,
createdAt: Date.now(),
verified: false,
attempts: 0
})
// TODO: 接入短信服务发送验证码,目前仅输出到日志
console.log(`[忘记密码] 手机号 ${phone} 的验证码: ${code}`)
ctx.body = { code: 200, message: '验证码已发送' }
}
// 验证重置密码验证码
async function verifyResetCode(ctx) {
const { phone, code } = ctx.request.body
if (!phone || !code) {
ctx.body = { code: 400, message: '请输入手机号和验证码' }
return
}
const stored = resetCodeStore.get(phone)
if (!stored) {
ctx.body = { code: 400, message: '请先获取验证码' }
return
}
// 验证码5分钟过期
if (Date.now() - stored.createdAt > 5 * 60 * 1000) {
resetCodeStore.delete(phone)
ctx.body = { code: 400, message: '验证码已过期,请重新获取' }
return
}
// 最多尝试5次
if (stored.attempts >= 5) {
resetCodeStore.delete(phone)
ctx.body = { code: 400, message: '验证码错误次数过多,请重新获取' }
return
}
if (stored.code !== code) {
stored.attempts += 1
ctx.body = { code: 400, message: '验证码错误' }
return
}
// 标记为已验证
stored.verified = true
ctx.body = { code: 200, message: '验证成功' }
}
// 通过验证码重置密码
async function resetPasswordWithCode(ctx) {
const { phone, code, newPassword } = ctx.request.body
if (!phone || !code || !newPassword) {
ctx.body = { code: 400, message: '参数不完整' }
return
}
if (newPassword.length < 8) {
ctx.body = { code: 400, message: '密码至少8位' }
return
}
if (!/[a-zA-Z]/.test(newPassword) || !/[0-9]/.test(newPassword)) {
ctx.body = { code: 400, message: '密码需包含字母和数字' }
return
}
const stored = resetCodeStore.get(phone)
if (!stored || !stored.verified) {
ctx.body = { code: 400, message: '请先验证手机号' }
return
}
// 验证码已验证但需确认code一致
if (stored.code !== code) {
ctx.body = { code: 400, message: '验证码不正确' }
return
}
// 验证通过后10分钟内有效
if (Date.now() - stored.createdAt > 10 * 60 * 1000) {
resetCodeStore.delete(phone)
ctx.body = { code: 400, message: '验证已过期,请重新操作' }
return
}
// 更新密码
const result = await query('UPDATE users SET password = ? WHERE phone = ?', [hashPassword(newPassword), phone])
// 清除验证码记录
resetCodeStore.delete(phone)
if (result.affectedRows > 0) {
ctx.body = { code: 200, message: '密码重置成功' }
} else {
ctx.body = { code: 404, message: '用户不存在' }
}
}
module.exports = { module.exports = {
requireAuth, requireAuth,
requireStaffAuth, requireStaffAuth,
@@ -653,6 +848,9 @@ module.exports = {
deleteUser, deleteUser,
changePassword, changePassword,
resetPassword, resetPassword,
sendResetCode,
verifyResetCode,
resetPasswordWithCode,
addPoints, addPoints,
getPointsLogs, getPointsLogs,
logout, logout,
+3
View File
@@ -7,6 +7,9 @@ const router = new Router()
// 创建支付(需要用户登录) // 创建支付(需要用户登录)
router.post('/create', requireAuth(), paymentController.createPayment) router.post('/create', requireAuth(), paymentController.createPayment)
// 查询支付结果(需要用户登录)
router.get('/query/:orderId', requireAuth(), paymentController.queryPayment)
// 微信支付回调(无需登录) // 微信支付回调(无需登录)
router.post('/notify', paymentController.paymentNotify) router.post('/notify', paymentController.paymentNotify)
+1
View File
@@ -5,6 +5,7 @@ const { requireStaffAuth } = require('../middleware/auth')
const router = new Router() const router = new Router()
router.get('/', requireStaffAuth(), stockController.getStockList) router.get('/', requireStaffAuth(), stockController.getStockList)
router.get('/logs', requireStaffAuth(), stockController.getStockLogs)
router.get('/:id', stockController.getStockByGoodsId) router.get('/:id', stockController.getStockByGoodsId)
router.post('/:id/adjust', requireStaffAuth(), stockController.adjustStock) router.post('/:id/adjust', requireStaffAuth(), stockController.adjustStock)
+3
View File
@@ -8,6 +8,9 @@ router.post('/login', userController.login)
router.post('/wechat-login', userController.wechatLogin) router.post('/wechat-login', userController.wechatLogin)
router.post('/register', userController.register) router.post('/register', userController.register)
router.post('/change-password', userController.changePassword) router.post('/change-password', userController.changePassword)
router.post('/send-reset-code', userController.sendResetCode)
router.post('/verify-reset-code', userController.verifyResetCode)
router.post('/reset-password-with-code', userController.resetPasswordWithCode)
router.post('/refresh-token', userController.refreshToken) router.post('/refresh-token', userController.refreshToken)
router.get('/info', userController.getUserInfo) router.get('/info', userController.getUserInfo)