更新完善页面

This commit is contained in:
董海洋
2026-06-03 14:15:55 +08:00
parent 4b7ae9c933
commit 1675662537
57 changed files with 7625 additions and 883 deletions
+312 -132
View File
@@ -1,82 +1,90 @@
const { query } = require('../config/database')
const { query, transaction } = require('../config/database')
const crypto = require('crypto')
const { paginate } = require('../utils/pagination')
const { DEFAULT_PASSWORD } = require('../config/constants')
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex')
}
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 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 (user.password !== md5(password)) {
ctx.body = {
code: 401,
message: '密码错误'
}
return
}
// 店员登录需要验证角色
if (loginType === 'staff' && user.role !== 1) {
ctx.body = {
code: 401,
message: '该账号不是店员账号'
}
return
}
const token = generateToken()
await query('UPDATE users SET token = ? WHERE id = ?', [token, user.id])
const userInfo = {
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,
token
role: user.role
}
ctx.body = {
code: 200,
data: userInfo
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) }
}
// 用户注册(普通用户)
@@ -87,6 +95,15 @@ async function register(ctx) {
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: '该手机号已注册' }
@@ -95,7 +112,7 @@ async function register(ctx) {
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(password), name, '', 0, 0]
[phone, hashPassword(password), name, '', 0, 0]
)
ctx.body = {
@@ -106,19 +123,44 @@ async function register(ctx) {
}
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 authHeader = ctx.headers.authorization || ''
const token = authHeader.replace('Bearer ', '')
if (!token) {
ctx.body = { code: 401, message: '未登录,请先登录店员账号' }
const user = await getUserByToken(extractToken(ctx))
if (!user) {
ctx.status = 401
ctx.body = { code: 401, message: '未登录或登录已失效' }
return null
}
const operators = await query('SELECT * FROM users WHERE token = ? AND role = 1 AND status = 1', [token])
if (operators.length === 0) {
ctx.body = { code: 401, message: '权限不足,仅店员可操作' }
if (user.role !== 1 && user.role !== 2) {
ctx.status = 403
ctx.body = { code: 403, message: '权限不足,仅店员或管理员可操作' }
return null
}
return operators[0]
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) {
@@ -127,7 +169,7 @@ async function createUser(phone, name, role) {
const result = await query(
'INSERT INTO users (phone, password, name, avatar, points, role) VALUES (?, ?, ?, ?, ?, ?)',
[phone, md5(DEFAULT_PASSWORD), name, '', 0, role]
[phone, hashPassword(DEFAULT_PASSWORD), name, '', 0, role]
)
return {
@@ -143,7 +185,7 @@ async function createUser(phone, name, role) {
}
}
// 店员注册(需要员权限)
// 店员注册(需要管理员权限)
async function registerStaff(ctx) {
const { phone, name } = ctx.request.body
if (!phone || !name) {
@@ -151,7 +193,7 @@ async function registerStaff(ctx) {
return
}
const operator = await requireStaffAuth(ctx)
const operator = await requireAdminAuth(ctx)
if (!operator) return
const result = await createUser(phone, name, 1)
@@ -223,39 +265,65 @@ async function getUserInfo(ctx) {
}
}
// 获取用户列表
// 获取用户列表(管理员操作)
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 { name, avatar, points, status } = ctx.request.body
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)
@@ -298,10 +366,18 @@ async function updateUser(ctx) {
}
}
// 删除用户
// 删除/禁用用户(管理员操作)
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) {
@@ -341,15 +417,16 @@ async function changePassword(ctx) {
const user = users[0]
if (user.password !== md5(oldPassword)) {
if (!verifyPassword(oldPassword, user.password)) {
ctx.body = {
code: 400,
message: '原密码错误'
}
return
}
await query('UPDATE users SET password = ? WHERE id = ?', [md5(newPassword), user.id])
// 如果是旧版MD5哈希,登录时已自动升级;此处确保修改密码后使用scrypt
await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(newPassword), user.id])
ctx.body = {
code: 200,
@@ -357,10 +434,13 @@ async function changePassword(ctx) {
}
}
// 重置密码(员操作)
// 重置密码(管理员操作)
async function resetPassword(ctx) {
const operator = await requireAdminAuth(ctx)
if (!operator) return
const { userId } = ctx.request.body
if (!userId) {
ctx.body = {
code: 400,
@@ -368,10 +448,10 @@ async function resetPassword(ctx) {
}
return
}
const defaultPassword = DEFAULT_PASSWORD
const result = await query('UPDATE users SET password = ? WHERE id = ?', [md5(defaultPassword), userId])
const result = await query('UPDATE users SET password = ? WHERE id = ?', [hashPassword(defaultPassword), userId])
if (result.affectedRows > 0) {
ctx.body = {
@@ -386,61 +466,98 @@ async function resetPassword(ctx) {
}
}
// 调整积分
// 调整积分(店员或管理员可操作)
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和积分变动值'
}
ctx.body = { code: 400, message: '请指定用户ID和积分变动值' }
return
}
const pointsInt = parseInt(points)
if (isNaN(pointsInt)) {
ctx.body = {
code: 400,
message: '积分值无效'
}
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: '用户不存在'
}
ctx.body = { code: 404, message: '用户不存在' }
return
}
const user = users[0]
const newPoints = Math.max(0, user.points + pointsInt)
// 更新用户积分
await query('UPDATE users SET points = ? WHERE id = ?', [newPoints, userId])
// 记录积分变动日志
await query(
'INSERT INTO points_logs (user_id, type, amount, description) VALUES (?, ?, ?, ?)',
[userId, pointsInt >= 0 ? 'earn' : 'spend', Math.abs(pointsInt), description || '管理员调整积分']
)
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
}
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)
@@ -464,8 +581,69 @@ async function getPointsLogs(ctx) {
}
}
// 微信一键登录
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,
@@ -476,5 +654,7 @@ module.exports = {
changePassword,
resetPassword,
addPoints,
getPointsLogs
getPointsLogs,
logout,
refreshToken
}