2026-05-23 14:15:45 +08:00
|
|
|
const { query } = require('../config/database')
|
|
|
|
|
const crypto = require('crypto')
|
2026-05-26 13:37:55 +08:00
|
|
|
const { paginate } = require('../utils/pagination')
|
|
|
|
|
const { DEFAULT_PASSWORD } = require('../config/constants')
|
2026-05-23 14:15:45 +08:00
|
|
|
|
|
|
|
|
function md5(str) {
|
|
|
|
|
return crypto.createHash('md5').update(str).digest('hex')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
2026-05-26 13:37:55 +08:00
|
|
|
await query('UPDATE users SET token = ? WHERE id = ?', [token, user.id])
|
2026-05-23 14:15:45 +08:00
|
|
|
|
|
|
|
|
const userInfo = {
|
|
|
|
|
id: user.id,
|
|
|
|
|
phone: user.phone,
|
|
|
|
|
name: user.name,
|
|
|
|
|
avatar: user.avatar,
|
|
|
|
|
points: user.points,
|
|
|
|
|
role: user.role,
|
|
|
|
|
token
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
code: 200,
|
|
|
|
|
data: userInfo
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 用户注册(普通用户)
|
|
|
|
|
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-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 (?, ?, ?, ?, ?, ?)',
|
|
|
|
|
[phone, md5(password), name, '', 0, 0]
|
|
|
|
|
)
|
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 }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requireStaffAuth(ctx) {
|
|
|
|
|
|
|
|
|
|
async function requireStaffAuth(ctx) {
|
|
|
|
|
const authHeader = ctx.headers.authorization || ''
|
|
|
|
|
const token = authHeader.replace('Bearer ', '')
|
|
|
|
|
if (!token) {
|
|
|
|
|
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: '权限不足,仅店员可操作' }
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return operators[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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, md5(DEFAULT_PASSWORD), name, '', 0, role]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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-05-26 13:37: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
|
|
|
|
|
|
|
|
const operator = await requireStaffAuth(ctx)
|
|
|
|
|
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: '用户不存在'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取用户列表
|
|
|
|
|
async function getUsers(ctx) {
|
|
|
|
|
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'
|
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
const result = await paginate(query, sql, params, ctx.query.page, ctx.query.pageSize)
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新用户信息
|
|
|
|
|
async function updateUser(ctx) {
|
|
|
|
|
const userId = parseInt(ctx.params.id)
|
|
|
|
|
const { name, avatar, points, status } = ctx.request.body
|
|
|
|
|
|
|
|
|
|
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 userId = parseInt(ctx.params.id)
|
|
|
|
|
|
|
|
|
|
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 (user.password !== md5(oldPassword)) {
|
|
|
|
|
ctx.body = {
|
|
|
|
|
code: 400,
|
|
|
|
|
message: '原密码错误'
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await query('UPDATE users SET password = ? WHERE id = ?', [md5(newPassword), user.id])
|
|
|
|
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: '密码修改成功'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重置密码(店员操作)
|
|
|
|
|
async function resetPassword(ctx) {
|
|
|
|
|
const { userId } = ctx.request.body
|
|
|
|
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
ctx.body = {
|
|
|
|
|
code: 400,
|
|
|
|
|
message: '请指定用户ID'
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 13:37:55 +08:00
|
|
|
const defaultPassword = DEFAULT_PASSWORD
|
|
|
|
|
|
2026-05-23 14:15:45 +08:00
|
|
|
const result = await query('UPDATE users SET password = ? WHERE id = ?', [md5(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: '用户不存在'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 调整积分
|
|
|
|
|
async function addPoints(ctx) {
|
|
|
|
|
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 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 = 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 || '管理员调整积分']
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ctx.body = {
|
|
|
|
|
code: 200,
|
|
|
|
|
message: '积分调整成功',
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
oldPoints: user.points,
|
|
|
|
|
newPoints,
|
|
|
|
|
change: pointsInt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取积分记录
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
login,
|
|
|
|
|
register,
|
|
|
|
|
registerStaff,
|
|
|
|
|
registerByStaff,
|
|
|
|
|
getUserInfo,
|
|
|
|
|
getUsers,
|
|
|
|
|
updateUser,
|
|
|
|
|
deleteUser,
|
|
|
|
|
changePassword,
|
|
|
|
|
resetPassword,
|
|
|
|
|
addPoints,
|
|
|
|
|
getPointsLogs
|
|
|
|
|
}
|