Files
backmanager-server/routes/users.js
2026-06-29 14:43:21 +08:00

402 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// routes/users.js —— 用户登录 / 信息 / CRUD
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const { pool } = require('../db')
// 安全字段:关联查询 departments 和 employees
const USER_LIST_SQL = `
SELECT u.id, u.username, u.is_active, u.department_id, u.employee_id,
u.created_at, u.updated_at,
d.name AS dept_name, d.description AS dept_desc,
e.name AS real_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN employees e ON u.employee_id = e.id
`
function pagination(query) {
const page = Math.max(Number(query.page) || 1, 1)
const pageSize = Math.min(Math.max(Number(query.pageSize) || 10, 1), 100)
const offset = (page - 1) * pageSize
return { page, pageSize, offset }
}
// ========== 登录 / 登出 / 个人信息 ==========
// POST /api/user/login —— 登录(无需 token
async function login(req, res) {
const { username, password } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
try {
const [rows] = await pool.query(
`SELECT u.id, u.username, u.password, u.is_active, u.department_id, u.employee_id,
d.name AS dept_name, d.description AS dept_desc, e.name AS real_name
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
LEFT JOIN employees e ON u.employee_id = e.id
WHERE u.username = ?`,
[username]
)
if (rows.length === 0) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
const user = rows[0]
// 检查账号是否被禁用
if (user.is_active === 0) {
return res.status(403).json({ code: 403, message: '账号已被禁用,请联系管理员' })
}
const ok = await bcrypt.compare(password, user.password)
if (!ok) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
// 查询该部门的所有权限标识
const [perms] = await pool.query(
`SELECT p.name FROM permissions p
JOIN department_permissions dp ON p.id = dp.permission_id
WHERE dp.department_id = ?`,
[user.department_id]
)
const permissions = perms.map(p => p.name)
const token = jwt.sign(
{
id: user.id,
username: user.username,
name: user.real_name || user.username,
employee_id: user.employee_id,
department_id: user.department_id,
departmentName: user.dept_name,
departmentDesc: user.dept_desc,
permissions,
},
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '2h' }
)
res.status(200).json({
code: 200,
message: 'ok',
data: {
token,
userInfo: {
id: user.id,
username: user.username,
name: user.real_name || user.username,
employee_id: user.employee_id,
department_id: user.department_id,
departmentName: user.dept_name,
departmentDesc: user.dept_desc,
permissions,
},
},
})
} catch (e) {
console.error('[login] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/user/info —— 获取当前登录用户信息(需要 token
async function info(req, res) {
try {
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[req.user.id]
)
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[info] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/user/list —— 简易用户列表(用于下拉选择负责人,仅需登录)
async function simpleList(req, res) {
try {
const [rows] = await pool.query(
`SELECT u.id, u.username, e.name AS real_name
FROM users u LEFT JOIN employees e ON u.employee_id = e.id
WHERE u.is_active = 1 ORDER BY u.id`
)
res.json({ code: 0, message: 'ok', data: rows })
} catch (e) {
console.error('[user simpleList] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/user/logout —— 登出(需要 token仅做应答
async function logout(req, res) {
res.json({ code: 0, message: 'ok' })
}
// PUT /api/user/password —— 当前用户修改自己的密码(需要 token
async function changePassword(req, res) {
const { oldPassword, newPassword } = req.body || {}
if (!oldPassword || !newPassword) {
return res.status(400).json({ code: 400, message: '旧密码和新密码必填' })
}
if (typeof newPassword !== 'string' || newPassword.length < 6) {
return res.status(400).json({ code: 400, message: '新密码至少 6 位' })
}
if (oldPassword === newPassword) {
return res.status(400).json({ code: 400, message: '新密码不能与旧密码相同' })
}
try {
const [rows] = await pool.query('SELECT password FROM users WHERE id = ?', [req.user.id])
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
const ok = await bcrypt.compare(oldPassword, rows[0].password)
if (!ok) {
return res.status(400).json({ code: 400, message: '旧密码错误' })
}
const hash = await bcrypt.hash(newPassword, 10)
await pool.query('UPDATE users SET password = ? WHERE id = ?', [hash, req.user.id])
res.json({ code: 0, message: '密码修改成功' })
} catch (e) {
console.error('[changePassword] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// ========== 用户管理 CRUD管理员 ==========
// GET /api/users —— 用户列表
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { username, real_name, id, department_id, is_active } = req.query
let where = 'WHERE 1=1'
const params = []
if (username) {
where += ' AND u.username LIKE ?'
params.push(`%${username}%`)
}
if (real_name) {
where += ' AND e.name LIKE ?'
params.push(`%${real_name}%`)
}
if (id) {
where += ' AND u.id = ?'
params.push(Number(id))
}
if (department_id) {
where += ' AND u.department_id = ?'
params.push(Number(department_id))
}
if (is_active !== undefined && is_active !== '') {
where += ' AND u.is_active = ?'
params.push(Number(is_active))
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM users u
LEFT JOIN employees e ON u.employee_id = e.id
${where}`,
params
)
const [rows] = await pool.query(
`${USER_LIST_SQL} ${where} ORDER BY u.id ASC LIMIT ? OFFSET ?`,
[...params, pageSize, offset]
)
res.json({
code: 0,
message: 'ok',
data: {
list: rows,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
})
} catch (e) {
console.error('[users list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/users/:id —— 用户详情
async function detail(req, res) {
try {
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[req.params.id]
)
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[users detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/users —— 创建用户
async function create(req, res) {
const { username, password, department_id, employee_id, is_active = 1 } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
if (typeof username !== 'string' || username.length < 3 || username.length > 50) {
return res.status(400).json({ code: 400, message: '用户名长度需在 3-50 之间' })
}
if (typeof password !== 'string' || password.length < 6) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
}
// 校验 department_id 是否存在
if (department_id) {
const [dept] = await pool.query('SELECT id FROM departments WHERE id = ?', [department_id])
if (dept.length === 0) {
return res.status(400).json({ code: 400, message: '指定的部门不存在' })
}
}
// 校验 employee_id 是否存在
if (employee_id) {
const [emp] = await pool.query('SELECT id FROM employees WHERE id = ?', [employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, message: '指定的员工不存在' })
}
}
try {
const hash = await bcrypt.hash(password, 10)
const [result] = await pool.query(
'INSERT INTO users (username, password, is_active, department_id, employee_id) VALUES (?, ?, ?, ?, ?)',
[username, hash, is_active, department_id || null, employee_id || null]
)
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[result.insertId]
)
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[users create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/users/:id —— 更新用户
async function update(req, res) {
const { id } = req.params
const fields = ['username', 'department_id', 'employee_id', 'is_active']
try {
const [existing] = await pool.query('SELECT id, department_id, is_active, employee_id FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
// 不允许修改自己的部门或禁用自己(此检查已移至中间件 protectSelfUpdate
// 校验 department_id
if (req.body.department_id) {
const [dept] = await pool.query('SELECT id FROM departments WHERE id = ?', [req.body.department_id])
if (dept.length === 0) {
return res.status(400).json({ code: 400, message: '指定的部门不存在' })
}
}
// 校验 employee_id
if (req.body.employee_id) {
const [emp] = await pool.query('SELECT id FROM employees WHERE id = ?', [req.body.employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, message: '指定的员工不存在' })
}
}
const sets = []
const params = []
for (const f of fields) {
if (req.body[f] !== undefined) {
sets.push(`${f} = ?`)
params.push(req.body[f])
}
}
// 密码单独处理
if (req.body.password !== undefined) {
if (typeof req.body.password !== 'string' || req.body.password.length < 6) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
}
const hash = await bcrypt.hash(req.body.password, 10)
sets.push('password = ?')
params.push(hash)
}
if (sets.length === 0) {
return res.status(400).json({ code: 400, message: '没有需要更新的字段' })
}
params.push(id)
await pool.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, params)
// 部门联动:用户部门变更时同步更新关联员工的部门
if (req.body.department_id !== undefined && existing[0].employee_id) {
const [[dept]] = await pool.query('SELECT description FROM departments WHERE id = ?', [req.body.department_id])
if (dept) {
await pool.query('UPDATE employees SET department = ? WHERE id = ?', [dept.description, existing[0].employee_id])
}
}
const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`,
[id]
)
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[users update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/users/:id —— 删除用户
async function remove(req, res) {
const { id } = req.params
try {
// 不能删除自己/最后一个管理员(此检查已移至中间件 protectUserDelete
const [existing] = await pool.query('SELECT id FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
await pool.query('DELETE FROM users WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[users delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { login, info, simpleList, logout, changePassword, list, detail, create, update, remove }