2026-06-03 14:15:55 +08:00
|
|
|
|
const { query, transaction } = require('../config/database')
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const crypto = require('crypto')
|
2026-05-26 13:37:55 +08:00
|
|
|
|
const { paginate } = require('../utils/pagination')
|
|
|
|
|
|
const { DEFAULT_PASSWORD } = require('../config/constants')
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const { hashPassword, verifyPassword, needsRehash } = require('../utils/password')
|
2026-06-04 11:10:51 +08:00
|
|
|
|
const { signAccess, signRefresh, verify, ACCESS_TTL, REFRESH_TTL } = require('../utils/jwt')
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const { toLegacyToken } = require('../utils/legacy-token')
|
|
|
|
|
|
const { extractToken, getUserByToken } = require('../middleware/auth')
|
2026-05-23 14:15:45 +08:00
|
|
|
|
|
|
|
|
|
|
function generateToken() {
|
|
|
|
|
|
return crypto.randomBytes(32).toString('hex')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
async function issueTokenPair(user) {
|
|
|
|
|
|
const access = signAccess(user)
|
|
|
|
|
|
const refresh = signRefresh(user)
|
|
|
|
|
|
const dbToken = generateToken()
|
|
|
|
|
|
await query('UPDATE users SET token = ? WHERE id = ?', [dbToken, user.id])
|
|
|
|
|
|
return { access, refresh, legacy: toLegacyToken(dbToken), accessTtl: ACCESS_TTL, refreshTtl: REFRESH_TTL }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function publicUser(user, tokenObj) {
|
2026-06-04 08:20:49 +08:00
|
|
|
|
const roleMap = { 0: 'customer', 1: 'staff', 2: 'admin' }
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const base = {
|
|
|
|
|
|
id: user.id,
|
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
|
name: user.name,
|
|
|
|
|
|
avatar: user.avatar,
|
|
|
|
|
|
points: user.points,
|
2026-06-04 08:20:49 +08:00
|
|
|
|
role: roleMap[user.role] || 'customer',
|
|
|
|
|
|
roleCode: user.role
|
2026-06-03 14:15:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (tokenObj) {
|
|
|
|
|
|
return { ...base, token: tokenObj.access, accessToken: tokenObj.access, refreshToken: tokenObj.refresh, legacyToken: tokenObj.legacy, accessTtl: tokenObj.accessTtl, refreshTtl: tokenObj.refreshTtl }
|
|
|
|
|
|
}
|
|
|
|
|
|
return base
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
// 用户登录(支持双重身份)
|
|
|
|
|
|
async function login(ctx) {
|
|
|
|
|
|
const { phone, password, loginType } = ctx.request.body
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (!phone || !password) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '请输入手机号和密码' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const users = await query('SELECT * FROM users WHERE phone = ?', [phone])
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (users.length === 0) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 401, message: '用户不存在' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const user = users[0]
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (user.status === 0) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 401, message: '账号已被禁用' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
if (!verifyPassword(password, user.password)) {
|
|
|
|
|
|
ctx.body = { code: 401, message: '密码错误' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-06-04 08:20:49 +08:00
|
|
|
|
if (loginType === 'admin' && user.role !== 1 && user.role !== 2) {
|
|
|
|
|
|
ctx.body = { code: 401, message: '该账号不是管理员或店员账号' }
|
2026-06-03 14:15:55 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (loginType === 'staff' && user.role !== 1) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 401, message: '该账号不是店员账号' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
if (loginType === 'customer' && user.role !== 0) {
|
|
|
|
|
|
ctx.body = { code: 401, message: '该账号不是普通用户账号' }
|
|
|
|
|
|
return
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
let tokenObj
|
|
|
|
|
|
if (needsRehash(user.password)) {
|
|
|
|
|
|
const upgraded = hashPassword(password)
|
|
|
|
|
|
await query('UPDATE users SET password = ? WHERE id = ?', [upgraded, user.id])
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
tokenObj = await issueTokenPair(user)
|
|
|
|
|
|
|
|
|
|
|
|
ctx.body = { code: 200, data: publicUser(user, tokenObj) }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户注册(普通用户)
|
2026-06-04 08:20:49 +08:00
|
|
|
|
// 注册频率限制(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)
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function register(ctx) {
|
|
|
|
|
|
const { phone, password, name } = ctx.request.body
|
|
|
|
|
|
if (!phone || !password || !name) {
|
2026-05-26 13:37:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '请填写完整信息' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-06-04 08:20:49 +08:00
|
|
|
|
// 注册频率限制
|
|
|
|
|
|
const clientIp = ctx.ip || 'unknown'
|
|
|
|
|
|
if (!checkRegisterLimit(clientIp, phone)) {
|
|
|
|
|
|
ctx.body = { code: 429, message: '注册过于频繁,请稍后再试' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
if (password.length < 8) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '密码至少8位' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '密码需包含字母和数字' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const existing = await query('SELECT * FROM users WHERE phone = ?', [phone])
|
|
|
|
|
|
if (existing.length > 0) {
|
2026-05-26 13:37:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '该手机号已注册' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const result = await query(
|
|
|
|
|
|
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
|
2026-06-03 14:15:55 +08:00
|
|
|
|
[phone, hashPassword(password), name, '', 0, 0]
|
2026-05-23 14:15:45 +08:00
|
|
|
|
)
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
message: '注册成功',
|
2026-05-26 13:37:55 +08:00
|
|
|
|
data: { id: result.insertId, phone, name, avatar: '', points: 0, role: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
async function requireAuth(ctx) {
|
|
|
|
|
|
const user = await getUserByToken(extractToken(ctx))
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
ctx.status = 401
|
|
|
|
|
|
ctx.body = { code: 401, message: '未登录或登录已失效' }
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return user
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
|
async function requireStaffAuth(ctx) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const user = await getUserByToken(extractToken(ctx))
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
ctx.status = 401
|
|
|
|
|
|
ctx.body = { code: 401, message: '未登录或登录已失效' }
|
2026-05-26 13:37:55 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
if (user.role !== 1 && user.role !== 2) {
|
|
|
|
|
|
ctx.status = 403
|
|
|
|
|
|
ctx.body = { code: 403, message: '权限不足,仅店员或管理员可操作' }
|
2026-05-26 13:37:55 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
return user
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function requireAdminAuth(ctx) {
|
|
|
|
|
|
const user = await getUserByToken(extractToken(ctx))
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
|
ctx.status = 401
|
|
|
|
|
|
ctx.body = { code: 401, message: '未登录或登录已失效' }
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (user.role !== 2) {
|
|
|
|
|
|
ctx.status = 403
|
|
|
|
|
|
ctx.body = { code: 403, message: '权限不足,仅管理员可操作' }
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return user
|
2026-05-26 13:37:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createUser(phone, name, role) {
|
|
|
|
|
|
const existing = await query('SELECT * FROM users WHERE phone = ?', [phone])
|
|
|
|
|
|
if (existing.length > 0) return { conflict: true }
|
|
|
|
|
|
|
|
|
|
|
|
const result = await query(
|
|
|
|
|
|
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
|
2026-06-03 14:15:55 +08:00
|
|
|
|
[phone, hashPassword(DEFAULT_PASSWORD), name, '', 0, role]
|
2026-05-26 13:37:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conflict: false,
|
2026-05-23 14:15:45 +08:00
|
|
|
|
data: {
|
|
|
|
|
|
id: result.insertId,
|
|
|
|
|
|
phone,
|
|
|
|
|
|
name,
|
|
|
|
|
|
avatar: '',
|
|
|
|
|
|
points: 0,
|
2026-05-26 13:37:55 +08:00
|
|
|
|
role
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
// 店员注册(需要管理员权限)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function registerStaff(ctx) {
|
|
|
|
|
|
const { phone, name } = ctx.request.body
|
|
|
|
|
|
if (!phone || !name) {
|
2026-05-26 13:37:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '请填写手机号和姓名' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const operator = await requireAdminAuth(ctx)
|
2026-05-26 13:37:55 +08:00
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
|
|
|
|
|
const result = await createUser(phone, name, 1)
|
|
|
|
|
|
if (result.conflict) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '该手机号已注册' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
2026-05-26 13:37:55 +08:00
|
|
|
|
message: `店员注册成功,默认密码为${DEFAULT_PASSWORD}`,
|
|
|
|
|
|
data: result.data
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
|
// 店员帮助用户注册(需要店员权限)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function registerByStaff(ctx) {
|
|
|
|
|
|
const { phone, name } = ctx.request.body
|
|
|
|
|
|
if (!phone || !name) {
|
2026-05-26 13:37:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '请填写手机号和姓名' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
|
|
|
|
|
const operator = await requireStaffAuth(ctx)
|
|
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
|
|
|
|
|
const result = await createUser(phone, name, 0)
|
|
|
|
|
|
if (result.conflict) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '该手机号已注册' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
2026-05-26 13:37:55 +08:00
|
|
|
|
message: `用户注册成功,默认密码为${DEFAULT_PASSWORD}`,
|
|
|
|
|
|
data: result.data
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户信息
|
|
|
|
|
|
async function getUserInfo(ctx) {
|
|
|
|
|
|
const userId = parseInt(ctx.query.id)
|
|
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
2026-05-26 13:37:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '缺少用户ID' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [userId])
|
|
|
|
|
|
|
|
|
|
|
|
if (users.length > 0) {
|
|
|
|
|
|
const user = users[0]
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
data: {
|
|
|
|
|
|
id: user.id,
|
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
|
name: user.name,
|
|
|
|
|
|
avatar: user.avatar,
|
|
|
|
|
|
points: user.points,
|
|
|
|
|
|
role: user.role
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 404,
|
|
|
|
|
|
message: '用户不存在'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 16:24:40 +08:00
|
|
|
|
// 获取用户列表(店员 / 管理员可操作)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function getUsers(ctx) {
|
2026-06-05 16:24:40 +08:00
|
|
|
|
const operator = await requireStaffAuth(ctx)
|
2026-06-03 14:15:55 +08:00
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
let sql = 'SELECT id, phone, name, points, role, status, created_at FROM users WHERE status = 1'
|
|
|
|
|
|
const params = []
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (ctx.query.role !== undefined) {
|
|
|
|
|
|
sql += ' AND role = ?'
|
|
|
|
|
|
params.push(parseInt(ctx.query.role))
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (ctx.query.keyword) {
|
|
|
|
|
|
sql += ' AND (phone LIKE ? OR name LIKE ?)'
|
|
|
|
|
|
params.push(`%${ctx.query.keyword}%`, `%${ctx.query.keyword}%`)
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
sql += ' ORDER BY created_at DESC'
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
|
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
2026-05-26 13:37:55 +08:00
|
|
|
|
...result
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 08:20:49 +08:00
|
|
|
|
const USER_UPDATEABLE_FIELDS = ['name', 'avatar', 'points', 'status', 'role']
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新用户信息(管理员可改任意人;本人仅可改 name/avatar)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function updateUser(ctx) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const operator = await requireAuth(ctx)
|
|
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const userId = parseInt(ctx.params.id)
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const body = ctx.request.body
|
|
|
|
|
|
|
|
|
|
|
|
let allowedFields = USER_UPDATEABLE_FIELDS
|
|
|
|
|
|
if (operator.role !== 2) {
|
|
|
|
|
|
if (userId !== operator.id) {
|
|
|
|
|
|
ctx.body = { code: 403, message: '无权修改他人信息' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-04 08:20:49 +08:00
|
|
|
|
if ('role' in ctx.request.body) {
|
|
|
|
|
|
ctx.body = { code: 403, message: '无权修改角色' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
allowedFields = ['name', 'avatar']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filtered = {}
|
|
|
|
|
|
for (const key of allowedFields) {
|
|
|
|
|
|
if (key in body) {
|
|
|
|
|
|
filtered[key] = body[key]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 08:20:49 +08:00
|
|
|
|
const { name, avatar, points, status, role } = filtered
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const updateFields = []
|
|
|
|
|
|
const updateParams = []
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (name !== undefined) {
|
|
|
|
|
|
updateFields.push('name = ?')
|
|
|
|
|
|
updateParams.push(name)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (avatar !== undefined) {
|
|
|
|
|
|
updateFields.push('avatar = ?')
|
|
|
|
|
|
updateParams.push(avatar)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (points !== undefined) {
|
|
|
|
|
|
updateFields.push('points = ?')
|
|
|
|
|
|
updateParams.push(parseInt(points))
|
|
|
|
|
|
}
|
|
|
|
|
|
if (status !== undefined) {
|
|
|
|
|
|
updateFields.push('status = ?')
|
|
|
|
|
|
updateParams.push(parseInt(status))
|
|
|
|
|
|
}
|
2026-06-04 08:20:49 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-05-23 14:15:45 +08:00
|
|
|
|
|
|
|
|
|
|
if (updateFields.length === 0) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 400,
|
|
|
|
|
|
message: '没有需要更新的字段'
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateParams.push(userId)
|
|
|
|
|
|
|
|
|
|
|
|
const result = await query(`UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`, updateParams)
|
|
|
|
|
|
|
|
|
|
|
|
if (result.affectedRows > 0) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
message: '更新成功'
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 404,
|
|
|
|
|
|
message: '用户不存在'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
// 删除/禁用用户(管理员操作)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function deleteUser(ctx) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const operator = await requireAdminAuth(ctx)
|
|
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const userId = parseInt(ctx.params.id)
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
if (userId === operator.id) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '不能禁用自己' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const result = await query('UPDATE users SET status = 0 WHERE id = ?', [userId])
|
|
|
|
|
|
|
|
|
|
|
|
if (result.affectedRows > 0) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
message: '禁用成功'
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 404,
|
|
|
|
|
|
message: '用户不存在'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 修改密码
|
|
|
|
|
|
async function changePassword(ctx) {
|
|
|
|
|
|
const { phone, oldPassword, newPassword } = ctx.request.body
|
|
|
|
|
|
|
|
|
|
|
|
if (!phone || !oldPassword || !newPassword) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 400,
|
|
|
|
|
|
message: '请填写完整信息'
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const users = await query('SELECT * FROM users WHERE phone = ? AND status = 1', [phone])
|
|
|
|
|
|
|
|
|
|
|
|
if (users.length === 0) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 404,
|
|
|
|
|
|
message: '用户不存在'
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const user = users[0]
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
if (!verifyPassword(oldPassword, user.password)) {
|
2026-05-23 14:15:45 +08:00
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 400,
|
|
|
|
|
|
message: '原密码错误'
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果是旧版MD5哈希,登录时已自动升级;此处确保修改密码后使用scrypt
|
|
|
|
|
|
await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(newPassword), user.id])
|
2026-05-23 14:15:45 +08:00
|
|
|
|
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
message: '密码修改成功'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
// 重置密码(管理员操作)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function resetPassword(ctx) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const operator = await requireAdminAuth(ctx)
|
|
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const { userId } = ctx.request.body
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (!userId) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 400,
|
|
|
|
|
|
message: '请指定用户ID'
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
|
const defaultPassword = DEFAULT_PASSWORD
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const result = await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(defaultPassword), userId])
|
2026-05-26 13:37:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (result.affectedRows > 0) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
2026-05-26 13:37:55 +08:00
|
|
|
|
message: `密码已重置为${DEFAULT_PASSWORD}`
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 404,
|
|
|
|
|
|
message: '用户不存在'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
// 调整积分(店员或管理员可操作)
|
2026-05-23 14:15:45 +08:00
|
|
|
|
async function addPoints(ctx) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const operator = await requireStaffAuth(ctx)
|
|
|
|
|
|
if (!operator) return
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const { userId, points, description } = ctx.request.body
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
if (!userId || points === undefined) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '请指定用户ID和积分变动值' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const pointsInt = parseInt(points)
|
|
|
|
|
|
if (isNaN(pointsInt)) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 400, message: '积分值无效' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
|
|
|
|
|
const MAX_DELTA = 100000
|
|
|
|
|
|
if (Math.abs(pointsInt) > MAX_DELTA) {
|
|
|
|
|
|
ctx.body = { code: 400, message: `单次积分变动不能超过 ${MAX_DELTA}` }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pointsInt === 0) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '积分变动值不能为0' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pointsInt < 0) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '不支持直接扣除积分,请使用专门的积分扣除接口' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [userId])
|
|
|
|
|
|
if (users.length === 0) {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 404, message: '用户不存在' }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
const user = users[0]
|
2026-06-03 14:15:55 +08:00
|
|
|
|
const newPoints = user.points + pointsInt
|
|
|
|
|
|
|
|
|
|
|
|
await transaction(async (conn) => {
|
|
|
|
|
|
await conn.execute('UPDATE users SET points = ? WHERE id = ?', [newPoints, userId])
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
|
|
|
|
|
|
[userId, 'earn', Math.abs(pointsInt), description || '店员/管理员增加积分']
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
2026-06-03 14:15:55 +08:00
|
|
|
|
message: '积分增加成功',
|
|
|
|
|
|
data: { userId, oldPoints: user.points, newPoints, change: pointsInt }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 退出登录(使服务端 token 失效)
|
|
|
|
|
|
async function logout(ctx) {
|
|
|
|
|
|
const user = await getUserByToken(extractToken(ctx))
|
|
|
|
|
|
if (user) {
|
|
|
|
|
|
await query('UPDATE users SET token = NULL WHERE id = ?', [user.id])
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
2026-06-03 14:15:55 +08:00
|
|
|
|
ctx.body = { code: 200, message: '已退出登录' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新 access token
|
|
|
|
|
|
async function refreshToken(ctx) {
|
|
|
|
|
|
const { refreshToken: token } = ctx.request.body
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
ctx.status = 400
|
|
|
|
|
|
ctx.body = { code: 400, message: '缺少 refreshToken' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-04 11:10:51 +08:00
|
|
|
|
const payload = verify(token)
|
2026-06-03 14:15:55 +08:00
|
|
|
|
if (!payload || payload.type !== 'refresh') {
|
|
|
|
|
|
ctx.status = 401
|
|
|
|
|
|
ctx.body = { code: 401, message: 'refreshToken 无效或已过期' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [payload.sub])
|
|
|
|
|
|
if (users.length === 0) {
|
|
|
|
|
|
ctx.status = 401
|
|
|
|
|
|
ctx.body = { code: 401, message: '账号不存在或已禁用' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const user = users[0]
|
|
|
|
|
|
const tokenObj = await issueTokenPair(user)
|
|
|
|
|
|
ctx.body = { code: 200, data: publicUser(user, tokenObj) }
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取积分记录
|
|
|
|
|
|
async function getPointsLogs(ctx) {
|
|
|
|
|
|
const userId = parseInt(ctx.query.userId)
|
|
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 400,
|
|
|
|
|
|
message: '请指定用户ID'
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const logs = await query(
|
|
|
|
|
|
'SELECT * FROM points_logs WHERE user_id = ? ORDER BY created_at DESC',
|
|
|
|
|
|
[userId]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
|
code: 200,
|
|
|
|
|
|
data: logs
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:15:55 +08:00
|
|
|
|
// 微信一键登录
|
|
|
|
|
|
async function wechatLogin(ctx) {
|
|
|
|
|
|
const { code } = ctx.request.body
|
|
|
|
|
|
if (!code) {
|
|
|
|
|
|
ctx.body = { code: 400, message: '缺少微信登录code' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const fetch = require('node-fetch')
|
|
|
|
|
|
const APPID = process.env.WECHAT_APPID
|
|
|
|
|
|
const SECRET = process.env.WECHAT_SECRET
|
|
|
|
|
|
|
|
|
|
|
|
if (!APPID || !SECRET) {
|
|
|
|
|
|
ctx.body = { code: 500, message: '微信登录未配置' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用微信接口获取 openid 和 session_key
|
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
|
`https://api.weixin.qq.com/sns/jscode2session?appid=${APPID}&secret=${SECRET}&js_code=${code}&grant_type=authorization_code`
|
|
|
|
|
|
)
|
|
|
|
|
|
const data = await res.json()
|
|
|
|
|
|
|
|
|
|
|
|
if (data.errcode) {
|
|
|
|
|
|
ctx.body = { code: 400, message: `微信登录失败: ${data.errmsg}` }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { openid } = data
|
|
|
|
|
|
|
|
|
|
|
|
// 查找是否已有绑定该 openid 的用户
|
|
|
|
|
|
const users = await query('SELECT * FROM users WHERE openid = ? AND status = 1', [openid])
|
|
|
|
|
|
|
|
|
|
|
|
if (users.length > 0) {
|
|
|
|
|
|
// 已有用户,直接登录
|
|
|
|
|
|
const user = users[0]
|
|
|
|
|
|
const tokenObj = await issueTokenPair(user)
|
2026-06-04 08:20:49 +08:00
|
|
|
|
ctx.body = { code: 200, data: { ...publicUser(user, tokenObj), isNewUser: false } }
|
2026-06-03 14:15:55 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 新用户,自动注册
|
|
|
|
|
|
const phone = `wx_${openid.slice(0, 10)}`
|
|
|
|
|
|
const name = `微信用户`
|
|
|
|
|
|
const result = await query(
|
|
|
|
|
|
'INSERT INTO users (phone, password, name, avatar, points, role, openid) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
|
|
|
|
[phone, hashPassword(Math.random().toString(36)), name, '', 0, 0, openid]
|
|
|
|
|
|
)
|
|
|
|
|
|
const newUser = { id: result.insertId, phone, name, avatar: '', points: 0, role: 0, openid }
|
|
|
|
|
|
const tokenObj = await issueTokenPair(newUser)
|
2026-06-04 08:20:49 +08:00
|
|
|
|
ctx.body = { code: 200, data: { ...publicUser(newUser, tokenObj), isNewUser: true } }
|
2026-06-03 14:15:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('微信登录异常:', err)
|
|
|
|
|
|
ctx.body = { code: 500, message: '微信登录失败' }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 08:20:49 +08:00
|
|
|
|
// ============ 忘记密码 - 验证码相关 ============
|
|
|
|
|
|
|
|
|
|
|
|
// 内存中存储验证码(生产环境应使用 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: '用户不存在' }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
|
module.exports = {
|
2026-06-03 14:15:55 +08:00
|
|
|
|
requireAuth,
|
|
|
|
|
|
requireStaffAuth,
|
|
|
|
|
|
requireAdminAuth,
|
2026-05-23 14:15:45 +08:00
|
|
|
|
login,
|
2026-06-03 14:15:55 +08:00
|
|
|
|
wechatLogin,
|
2026-05-23 14:15:45 +08:00
|
|
|
|
register,
|
|
|
|
|
|
registerStaff,
|
|
|
|
|
|
registerByStaff,
|
|
|
|
|
|
getUserInfo,
|
|
|
|
|
|
getUsers,
|
|
|
|
|
|
updateUser,
|
|
|
|
|
|
deleteUser,
|
|
|
|
|
|
changePassword,
|
|
|
|
|
|
resetPassword,
|
2026-06-04 08:20:49 +08:00
|
|
|
|
sendResetCode,
|
|
|
|
|
|
verifyResetCode,
|
|
|
|
|
|
resetPasswordWithCode,
|
2026-05-23 14:15:45 +08:00
|
|
|
|
addPoints,
|
2026-06-03 14:15:55 +08:00
|
|
|
|
getPointsLogs,
|
|
|
|
|
|
logout,
|
|
|
|
|
|
refreshToken
|
2026-05-23 14:15:45 +08:00
|
|
|
|
}
|