Files
services/controllers/users.js
T

859 lines
22 KiB
JavaScript
Raw Normal View History

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
}