更新完善页面
This commit is contained in:
+312
-132
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user