const { query, transaction } = require('../config/database') const crypto = require('crypto') const { paginate } = require('../utils/pagination') const { DEFAULT_PASSWORD } = require('../config/constants') const { hashPassword, verifyPassword, needsRehash } = require('../utils/password') const { signAccess, signRefresh, verifyToken, ACCESS_TTL, REFRESH_TTL } = require('../utils/jwt') const { toLegacyToken } = require('../utils/legacy-token') const { extractToken, getUserByToken } = require('../middleware/auth') function generateToken() { return crypto.randomBytes(32).toString('hex') } 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) { const base = { id: user.id, phone: user.phone, name: user.name, avatar: user.avatar, points: user.points, role: user.role } if (tokenObj) { return { ...base, token: tokenObj.access, accessToken: tokenObj.access, refreshToken: tokenObj.refresh, legacyToken: tokenObj.legacy, accessTtl: tokenObj.accessTtl, refreshTtl: tokenObj.refreshTtl } } return base } // 用户登录(支持双重身份) async function login(ctx) { const { phone, password, loginType } = ctx.request.body if (!phone || !password) { ctx.body = { code: 400, message: '请输入手机号和密码' } return } const users = await query('SELECT * FROM users WHERE phone = ?', [phone]) if (users.length === 0) { ctx.body = { code: 401, message: '用户不存在' } return } const user = users[0] if (user.status === 0) { ctx.body = { code: 401, message: '账号已被禁用' } return } if (!verifyPassword(password, user.password)) { ctx.body = { code: 401, message: '密码错误' } return } if (loginType === 'admin' && user.role !== 2) { ctx.body = { code: 401, message: '该账号不是管理员账号' } return } if (loginType === 'staff' && user.role !== 1) { ctx.body = { code: 401, message: '该账号不是店员账号' } return } if (loginType === 'customer' && user.role !== 0) { ctx.body = { code: 401, message: '该账号不是普通用户账号' } return } let tokenObj if (needsRehash(user.password)) { const upgraded = hashPassword(password) await query('UPDATE users SET password = ? WHERE id = ?', [upgraded, user.id]) } tokenObj = await issueTokenPair(user) ctx.body = { code: 200, data: publicUser(user, tokenObj) } } // 用户注册(普通用户) async function register(ctx) { const { phone, password, name } = ctx.request.body if (!phone || !password || !name) { ctx.body = { code: 400, message: '请填写完整信息' } return } 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 } const existing = await query('SELECT * FROM users WHERE phone = ?', [phone]) if (existing.length > 0) { ctx.body = { code: 400, message: '该手机号已注册' } return } const result = await query( 'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)', [phone, hashPassword(password), name, '', 0, 0] ) ctx.body = { code: 200, message: '注册成功', data: { id: result.insertId, phone, name, avatar: '', points: 0, role: 0 } } } async function requireAuth(ctx) { const user = await getUserByToken(extractToken(ctx)) if (!user) { ctx.status = 401 ctx.body = { code: 401, message: '未登录或登录已失效' } return null } return user } async function requireStaffAuth(ctx) { const user = await getUserByToken(extractToken(ctx)) if (!user) { ctx.status = 401 ctx.body = { code: 401, message: '未登录或登录已失效' } return null } if (user.role !== 1 && user.role !== 2) { ctx.status = 403 ctx.body = { code: 403, message: '权限不足,仅店员或管理员可操作' } return null } 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 } 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 (?, ?, ?, ?, ?, ?)', [phone, hashPassword(DEFAULT_PASSWORD), name, '', 0, role] ) return { conflict: false, data: { id: result.insertId, phone, name, avatar: '', points: 0, role } } } // 店员注册(需要管理员权限) async function registerStaff(ctx) { const { phone, name } = ctx.request.body if (!phone || !name) { ctx.body = { code: 400, message: '请填写手机号和姓名' } return } const operator = await requireAdminAuth(ctx) if (!operator) return const result = await createUser(phone, name, 1) if (result.conflict) { ctx.body = { code: 400, message: '该手机号已注册' } return } ctx.body = { code: 200, message: `店员注册成功,默认密码为${DEFAULT_PASSWORD}`, data: result.data } } // 店员帮助用户注册(需要店员权限) async function registerByStaff(ctx) { const { phone, name } = ctx.request.body if (!phone || !name) { ctx.body = { code: 400, message: '请填写手机号和姓名' } return } const operator = await requireStaffAuth(ctx) if (!operator) return const result = await createUser(phone, name, 0) if (result.conflict) { ctx.body = { code: 400, message: '该手机号已注册' } return } ctx.body = { code: 200, message: `用户注册成功,默认密码为${DEFAULT_PASSWORD}`, data: result.data } } // 获取用户信息 async function getUserInfo(ctx) { const userId = parseInt(ctx.query.id) if (!userId) { ctx.body = { code: 400, message: '缺少用户ID' } 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: '用户不存在' } } } // 获取用户列表(管理员操作) async function getUsers(ctx) { const operator = await requireAdminAuth(ctx) if (!operator) return let sql = 'SELECT id, phone, name, points, role, status, created_at FROM users WHERE status = 1' const params = [] if (ctx.query.role !== undefined) { sql += ' AND role = ?' params.push(parseInt(ctx.query.role)) } if (ctx.query.keyword) { sql += ' AND (phone LIKE ? OR name LIKE ?)' params.push(`%${ctx.query.keyword}%`, `%${ctx.query.keyword}%`) } sql += ' ORDER BY created_at DESC' const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize) ctx.body = { code: 200, ...result } } const USER_UPDATEABLE_FIELDS = ['name', 'avatar', 'points', 'status'] // 更新用户信息(管理员可改任意人;本人仅可改 name/avatar) async function updateUser(ctx) { const operator = await requireAuth(ctx) if (!operator) return const userId = parseInt(ctx.params.id) const body = ctx.request.body let allowedFields = USER_UPDATEABLE_FIELDS if (operator.role !== 2) { if (userId !== operator.id) { ctx.body = { code: 403, message: '无权修改他人信息' } return } allowedFields = ['name', 'avatar'] } const filtered = {} for (const key of allowedFields) { if (key in body) { filtered[key] = body[key] } } const { name, avatar, points, status } = filtered const updateFields = [] const updateParams = [] 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)) } 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: '用户不存在' } } } // 删除/禁用用户(管理员操作) async function deleteUser(ctx) { const operator = await requireAdminAuth(ctx) if (!operator) return const userId = parseInt(ctx.params.id) if (userId === operator.id) { ctx.body = { code: 400, message: '不能禁用自己' } return } 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] if (!verifyPassword(oldPassword, user.password)) { ctx.body = { code: 400, message: '原密码错误' } return } // 如果是旧版MD5哈希,登录时已自动升级;此处确保修改密码后使用scrypt await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(newPassword), user.id]) ctx.body = { code: 200, message: '密码修改成功' } } // 重置密码(管理员操作) async function resetPassword(ctx) { const operator = await requireAdminAuth(ctx) if (!operator) return const { userId } = ctx.request.body if (!userId) { ctx.body = { code: 400, message: '请指定用户ID' } return } const defaultPassword = DEFAULT_PASSWORD const result = await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(defaultPassword), userId]) if (result.affectedRows > 0) { ctx.body = { code: 200, message: `密码已重置为${DEFAULT_PASSWORD}` } } else { ctx.body = { code: 404, message: '用户不存在' } } } // 调整积分(店员或管理员可操作) async function addPoints(ctx) { const operator = await requireStaffAuth(ctx) if (!operator) return const { userId, points, description } = ctx.request.body if (!userId || points === undefined) { ctx.body = { code: 400, message: '请指定用户ID和积分变动值' } return } const pointsInt = parseInt(points) if (isNaN(pointsInt)) { ctx.body = { code: 400, message: '积分值无效' } return } 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 } const users = await query('SELECT * FROM users WHERE id = ? AND status = 1', [userId]) if (users.length === 0) { ctx.body = { code: 404, message: '用户不存在' } return } const user = users[0] 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 || '店员/管理员增加积分'] ) }) ctx.body = { code: 200, 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]) } 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 } const payload = verifyToken(token) 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) } } // 获取积分记录 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 } } // 微信一键登录 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) ctx.body = { code: 200, data: publicUser(user, tokenObj) } } 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) ctx.body = { code: 200, data: publicUser(newUser, tokenObj), isNewUser: true } } } catch (err) { console.error('微信登录异常:', err) ctx.body = { code: 500, message: '微信登录失败' } } } module.exports = { requireAuth, requireStaffAuth, requireAdminAuth, login, wechatLogin, register, registerStaff, registerByStaff, getUserInfo, getUsers, updateUser, deleteUser, changePassword, resetPassword, addPoints, getPointsLogs, logout, refreshToken }