API 报错
This commit is contained in:
@@ -51,6 +51,7 @@ const errorHandler = async (ctx, next) => {
|
||||
code: 404,
|
||||
message: '接口不存在'
|
||||
}
|
||||
ctx.status = 404
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Server error:', error)
|
||||
|
||||
+77
-1
@@ -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 = {
|
||||
createPayment,
|
||||
paymentNotify,
|
||||
refundPayment
|
||||
refundPayment,
|
||||
queryPayment
|
||||
}
|
||||
|
||||
+42
-1
@@ -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 = {
|
||||
getStockList,
|
||||
adjustStock,
|
||||
getStockByGoodsId
|
||||
getStockByGoodsId,
|
||||
getStockLogs
|
||||
}
|
||||
|
||||
+205
-7
@@ -20,13 +20,15 @@ async function issueTokenPair(user) {
|
||||
}
|
||||
|
||||
function publicUser(user, tokenObj) {
|
||||
const roleMap = { 0: 'customer', 1: 'staff', 2: 'admin' }
|
||||
const base = {
|
||||
id: user.id,
|
||||
phone: user.phone,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
points: user.points,
|
||||
role: user.role
|
||||
role: roleMap[user.role] || 'customer',
|
||||
roleCode: user.role
|
||||
}
|
||||
if (tokenObj) {
|
||||
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
|
||||
}
|
||||
|
||||
if (loginType === 'admin' && user.role !== 2) {
|
||||
ctx.body = { code: 401, message: '该账号不是管理员账号' }
|
||||
if (loginType === 'admin' && user.role !== 1 && user.role !== 2) {
|
||||
ctx.body = { code: 401, message: '该账号不是管理员或店员账号' }
|
||||
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) {
|
||||
const { phone, password, name } = ctx.request.body
|
||||
if (!phone || !password || !name) {
|
||||
@@ -95,6 +130,13 @@ async function register(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
// 注册频率限制
|
||||
const clientIp = ctx.ip || 'unknown'
|
||||
if (!checkRegisterLimit(clientIp, phone)) {
|
||||
ctx.body = { code: 429, message: '注册过于频繁,请稍后再试' }
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
ctx.body = { code: 400, message: '密码至少8位' }
|
||||
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)
|
||||
async function updateUser(ctx) {
|
||||
@@ -309,6 +351,10 @@ async function updateUser(ctx) {
|
||||
ctx.body = { code: 403, message: '无权修改他人信息' }
|
||||
return
|
||||
}
|
||||
if ('role' in ctx.request.body) {
|
||||
ctx.body = { code: 403, message: '无权修改角色' }
|
||||
return
|
||||
}
|
||||
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 updateParams = []
|
||||
@@ -340,6 +386,15 @@ async function updateUser(ctx) {
|
||||
updateFields.push('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) {
|
||||
ctx.body = {
|
||||
@@ -619,7 +674,7 @@ async function wechatLogin(ctx) {
|
||||
// 已有用户,直接登录
|
||||
const user = users[0]
|
||||
const tokenObj = await issueTokenPair(user)
|
||||
ctx.body = { code: 200, data: publicUser(user, tokenObj) }
|
||||
ctx.body = { code: 200, data: { ...publicUser(user, tokenObj), isNewUser: false } }
|
||||
} else {
|
||||
// 新用户,自动注册
|
||||
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 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) {
|
||||
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 = {
|
||||
requireAuth,
|
||||
requireStaffAuth,
|
||||
@@ -653,6 +848,9 @@ module.exports = {
|
||||
deleteUser,
|
||||
changePassword,
|
||||
resetPassword,
|
||||
sendResetCode,
|
||||
verifyResetCode,
|
||||
resetPasswordWithCode,
|
||||
addPoints,
|
||||
getPointsLogs,
|
||||
logout,
|
||||
|
||||
@@ -7,6 +7,9 @@ const router = new Router()
|
||||
// 创建支付(需要用户登录)
|
||||
router.post('/create', requireAuth(), paymentController.createPayment)
|
||||
|
||||
// 查询支付结果(需要用户登录)
|
||||
router.get('/query/:orderId', requireAuth(), paymentController.queryPayment)
|
||||
|
||||
// 微信支付回调(无需登录)
|
||||
router.post('/notify', paymentController.paymentNotify)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const { requireStaffAuth } = require('../middleware/auth')
|
||||
const router = new Router()
|
||||
|
||||
router.get('/', requireStaffAuth(), stockController.getStockList)
|
||||
router.get('/logs', requireStaffAuth(), stockController.getStockLogs)
|
||||
router.get('/:id', stockController.getStockByGoodsId)
|
||||
router.post('/:id/adjust', requireStaffAuth(), stockController.adjustStock)
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ router.post('/login', userController.login)
|
||||
router.post('/wechat-login', userController.wechatLogin)
|
||||
router.post('/register', userController.register)
|
||||
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.get('/info', userController.getUserInfo)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user