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
+205 -7
View File
@@ -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,