Compare commits

..

6 Commits

Author SHA1 Message Date
Kyaru
5e8a464f51 Merge branch 'main' of https://chochox.asia/ChoChoX/backmanager-server 2026-06-30 23:12:21 +08:00
Kyaru
11cb335507 push 2026-06-30 23:11:19 +08:00
d25cf862d8 最后小修 2026-06-29 15:07:53 +08:00
Kyaru
ccb4c32614 离职联动禁用账号 2026-06-29 14:49:24 +08:00
Kyaru
59ea8d1e77 Merge branch 'main' of https://chochox.asia/ChoChoX/backmanager-server 2026-06-29 14:43:24 +08:00
Kyaru
faf94d696d 联动部门更改 2026-06-29 14:43:21 +08:00
8 changed files with 338 additions and 116 deletions

97
db.js
View File

@@ -42,6 +42,7 @@ async function initDB() {
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用', is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
department_id INT DEFAULT NULL COMMENT '部门ID (关联departments表)', department_id INT DEFAULT NULL COMMENT '部门ID (关联departments表)',
employee_id INT DEFAULT NULL COMMENT '员工ID (关联employees表)', employee_id INT DEFAULT NULL COMMENT '员工ID (关联employees表)',
role_level TINYINT NOT NULL DEFAULT 4 COMMENT '角色级别: 1-总经理, 2-部门经理, 3-部门副经理, 4-一般专员',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id), PRIMARY KEY (id),
@@ -176,6 +177,14 @@ async function initDB() {
// 列已删除则忽略 // 列已删除则忽略
} }
// users 表添加 role_level角色级别
try {
await pool.query(`ALTER TABLE users ADD COLUMN role_level TINYINT NOT NULL DEFAULT 4 COMMENT '角色级别: 1-总经理, 2-部门经理, 3-部门副经理, 4-一般专员' AFTER employee_id`)
console.log('[init] users 表已添加 role_level 字段')
} catch (e) {
// 字段已存在则忽略
}
console.log('[init] 数据表初始化完成') console.log('[init] 数据表初始化完成')
// ============ 2.3 部门与权限相关表 ============ // ============ 2.3 部门与权限相关表 ============
@@ -235,25 +244,25 @@ async function initDB() {
// ============ 2.4 种子数据 ============ // ============ 2.4 种子数据 ============
await seedDepartmentsAndPermissions() await seedDepartmentsAndPermissions()
await seedDefaultUsers() await seedDefaultUsers()
await seedBusinessData() // await seedBusinessData()
// ============ 2.5 数据库迁移(兼容已有数据的旧库) ============ // // ============ 2.5 数据库迁移(兼容已有数据的旧库) ============
// 为财务部补上 product:read 权限 // // 为财务部补上 product:read 权限
try { // try {
const [financeDept] = await pool.query("SELECT id FROM departments WHERE name = 'finance' LIMIT 1") // const [financeDept] = await pool.query("SELECT id FROM departments WHERE name = 'finance' LIMIT 1")
if (financeDept.length > 0) { // if (financeDept.length > 0) {
const [prodPerm] = await pool.query("SELECT id FROM permissions WHERE name = 'product:read' LIMIT 1") // const [prodPerm] = await pool.query("SELECT id FROM permissions WHERE name = 'product:read' LIMIT 1")
if (prodPerm.length > 0) { // if (prodPerm.length > 0) {
await pool.query( // await pool.query(
'INSERT IGNORE INTO department_permissions (department_id, permission_id) VALUES (?, ?)', // 'INSERT IGNORE INTO department_permissions (department_id, permission_id) VALUES (?, ?)',
[financeDept[0].id, prodPerm[0].id] // [financeDept[0].id, prodPerm[0].id]
) // )
console.log('[migrate] 财务部已补上 product:read 权限') // console.log('[migrate] 财务部已补上 product:read 权限')
} // }
} // }
} catch (e) { // } catch (e) {
console.log('[migrate] 财务部权限迁移跳过:', e.message) // console.log('[migrate] 财务部权限迁移跳过:', e.message)
} // }
} }
// ============ 3. 种子数据:部门与权限 ============ // ============ 3. 种子数据:部门与权限 ============
@@ -314,8 +323,7 @@ async function seedDepartmentsAndPermissions() {
const deptPermMap = { const deptPermMap = {
admin: [ admin: [
'user:manage', 'user:manage',
'customer:read', 'contract:read', 'after_sale:read', 'employee:read',
'product:read', 'supplier:read', 'employee:read',
], ],
general_manager: [ general_manager: [
'customer:read', 'contract:read', 'after_sale:read', 'customer:read', 'contract:read', 'after_sale:read',
@@ -330,7 +338,6 @@ async function seedDepartmentsAndPermissions() {
], ],
operations_manager: [ operations_manager: [
'customer:read', 'customer:read',
'contract:read',
'after_sale:read', 'after_sale:create', 'after_sale:update', 'after_sale:delete', 'after_sale:read', 'after_sale:create', 'after_sale:update', 'after_sale:delete',
'employee:read', 'employee:read',
], ],
@@ -348,7 +355,6 @@ async function seedDepartmentsAndPermissions() {
hr: [ hr: [
'employee:read', 'employee:create', 'employee:update', 'employee:delete', 'employee:read', 'employee:create', 'employee:update', 'employee:delete',
'user:manage', 'user:manage',
'customer:read',
], ],
} }
@@ -368,7 +374,7 @@ async function seedDepartmentsAndPermissions() {
console.log('[init] 已初始化部门-权限映射') console.log('[init] 已初始化部门-权限映射')
} }
// ============ 4. 种子数据:默认用户 ============ // ============ 4. 种子数据:默认用户(仅保留总经理账号) ============
async function seedDefaultUsers() { async function seedDefaultUsers() {
const hash = await bcrypt.hash('123456', 10) const hash = await bcrypt.hash('123456', 10)
@@ -386,13 +392,13 @@ async function seedDefaultUsers() {
] ]
const seedUsers = [ const seedUsers = [
{ username: 'zhangchao', deptName: 'general_manager' }, { username: 'zhangchao', deptName: 'general_manager', role_level: 1 },
{ username: 'liming', deptName: 'franchise_manager' }, { username: 'liming', deptName: 'franchise_manager', role_level: 2 },
{ username: 'wangli', deptName: 'operations_manager' }, { username: 'wangli', deptName: 'operations_manager', role_level: 2 },
{ username: 'zhaoqiang', deptName: 'procurement_manager' }, { username: 'zhaoqiang', deptName: 'procurement_manager',role_level: 2 },
{ username: 'chenfang', deptName: 'finance' }, { username: 'chenfang', deptName: 'finance', role_level: 2 },
{ username: 'admin', deptName: 'admin' }, { username: 'admin', deptName: 'admin', role_level: 2 },
{ username: 'yangfan', deptName: 'hr' }, { username: 'yangfan', deptName: 'hr', role_level: 2 },
] ]
for (let i = 0; i < seedUsers.length; i++) { for (let i = 0; i < seedUsers.length; i++) {
@@ -409,15 +415,12 @@ async function seedDefaultUsers() {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
[emp.name, emp.gender, emp.age, emp.education, emp.department, emp.entry_date, emp.position, emp.salary, emp.phone, emp.email] [emp.name, emp.gender, emp.age, emp.education, emp.department, emp.entry_date, emp.position, emp.salary, emp.phone, emp.email]
) )
const empId = empResult.insertId
// 再插入 users关联 employee 和 department
await pool.query( await pool.query(
`INSERT INTO users (username, password, is_active, department_id, employee_id) `INSERT INTO users (username, password, is_active, department_id, employee_id, role_level)
VALUES (?, ?, 1, ?, ?)`, VALUES (?, ?, 1, ?, ?, ?)`,
[usr.username, hash, deptIdMap[usr.deptName], empId] [usr.username, hash, deptIdMap[usr.deptName], empId, usr.role_level]
) )
console.log(`[init] 已创建用户 ${usr.username} (${emp.name} - ${emp.department})`) console.log(`[init] 已创建用户 ${usr.username} (${emp.name})`)
} }
} }
@@ -452,14 +455,14 @@ async function seedBusinessData() {
const deptIdMap2 = Object.fromEntries(deptRows.map(d => [d.name, d.id])) const deptIdMap2 = Object.fromEntries(deptRows.map(d => [d.name, d.id]))
const extraUsers = [ const extraUsers = [
{ empName: '刘洋', username: 'liuyang', deptName: 'franchise_manager' }, { empName: '刘洋', username: 'liuyang', deptName: 'franchise_manager', role_level: 4 },
{ empName: '孙婷', username: 'sunting', deptName: 'franchise_manager' }, { empName: '孙婷', username: 'sunting', deptName: 'franchise_manager', role_level: 4 },
{ empName: '周杰', username: 'zhoujie', deptName: 'operations_manager' }, { empName: '周杰', username: 'zhoujie', deptName: 'operations_manager', role_level: 4 },
{ empName: '吴敏', username: 'wumin', deptName: 'operations_manager' }, { empName: '吴敏', username: 'wumin', deptName: 'operations_manager', role_level: 4 },
{ empName: '郑伟', username: 'zhengwei', deptName: 'operations_manager' }, { empName: '郑伟', username: 'zhengwei', deptName: 'operations_manager', role_level: 4 },
{ empName: '黄磊', username: 'huanglei', deptName: 'procurement_manager' }, { empName: '黄磊', username: 'huanglei', deptName: 'procurement_manager',role_level: 4 },
{ empName: '马丽', username: 'mali', deptName: 'finance' }, { empName: '马丽', username: 'mali', deptName: 'finance', role_level: 4 },
{ empName: '林峰', username: 'linfeng', deptName: 'general_manager' }, { empName: '林峰', username: 'linfeng', deptName: 'general_manager', role_level: 2 },
] ]
const [newEmps] = await pool.query("SELECT id, name, department FROM employees WHERE name IN ('刘洋','孙婷','周杰','吴敏','郑伟','黄磊','马丽','林峰')") const [newEmps] = await pool.query("SELECT id, name, department FROM employees WHERE name IN ('刘洋','孙婷','周杰','吴敏','郑伟','黄磊','马丽','林峰')")
@@ -471,8 +474,8 @@ async function seedBusinessData() {
const [userCheck] = await pool.query('SELECT id FROM users WHERE username = ?', [u.username]) const [userCheck] = await pool.query('SELECT id FROM users WHERE username = ?', [u.username])
if (userCheck.length > 0) continue if (userCheck.length > 0) continue
await pool.query( await pool.query(
'INSERT INTO users (username, password, is_active, department_id, employee_id) VALUES (?, ?, 1, ?, ?)', 'INSERT INTO users (username, password, is_active, department_id, employee_id, role_level) VALUES (?, ?, 1, ?, ?, ?)',
[u.username, hash, deptIdMap2[u.deptName], emp.id] [u.username, hash, deptIdMap2[u.deptName], emp.id, u.role_level]
) )
console.log(`[init] 已创建用户 ${u.username} (${u.empName} - ${emp.department})`) console.log(`[init] 已创建用户 ${u.username} (${u.empName} - ${emp.department})`)
} }

View File

@@ -4,7 +4,7 @@
* 允许在创建员工时一并创建系统用户账号(需 user:manage 权限) * 允许在创建员工时一并创建系统用户账号(需 user:manage 权限)
* 将用户创建参数挂到 req._createUser由路由在写入 employee 后处理 * 将用户创建参数挂到 req._createUser由路由在写入 employee 后处理
*/ */
function allowUserCreation(req, res, next) { async function allowUserCreation(req, res, next) {
const { username, password, department_id } = req.body || {} const { username, password, department_id } = req.body || {}
// 没传用户相关字段 → 只创建员工,跳过 // 没传用户相关字段 → 只创建员工,跳过
@@ -27,8 +27,37 @@ function allowUserCreation(req, res, next) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' }) return res.status(400).json({ code: 400, message: '密码至少 6 位' })
} }
// 检查是否是添加总经理办公室的用户
try {
const { pool } = require('../db')
const [dept] = await pool.query('SELECT name FROM departments WHERE id = ?', [department_id])
if (dept.length > 0 && dept[0].name === 'general_manager') {
const operatorLevel = req.user.role_level || 4
const operatorDept = req.user.departmentName
// 只有信息技术部和人力资源部的部门经理才能添加总经理办公室的用户
if (operatorLevel > 2 || !['admin', 'hr'].includes(operatorDept)) {
return res.status(403).json({ code: 403, message: '只有信息技术部和人力资源部的部门经理才能添加总经理办公室的用户' })
}
// 一般科员不能添加总经理办公室的用户
if (operatorLevel === 4) {
return res.status(403).json({ code: 403, message: '一般科员不能添加总经理办公室的用户' })
}
// 只能添加副总经理,总经理是系统预设的
// 这个检查在员工创建时通过 position 字段控制
}
} catch (e) {
console.error('[allowUserCreation] error:', e)
return res.status(500).json({ code: 500, message: e.message })
}
// 获取角色级别
const { role_level } = req.body || {}
// 挂到 req供路由在 INSERT employee 后使用 // 挂到 req供路由在 INSERT employee 后使用
req._createUser = { username, password, department_id } req._createUser = { username, password, department_id, role_level: role_level || 4 }
next() next()
} }

View File

@@ -1,9 +1,7 @@
// middleware/permissions.js —— 接口权限 + 数据范围 + 合同类型校验 // middleware/permissions.js —— 接口权限 + 数据范围 + 合同类型校验 + 层级检查
/** /**
* 检查当前用户是否拥有指定权限 * 检查当前用户是否拥有指定权限
* @param {string} resource - 资源名,如 'customer'
* @param {string} action - 操作名,如 'read'
*/ */
function checkPermission(resource, action) { function checkPermission(resource, action) {
return (req, res, next) => { return (req, res, next) => {
@@ -16,50 +14,105 @@ function checkPermission(resource, action) {
} }
/** /**
* 根据用户部门计算数据范围,挂到 req.scope * 根据用户角色级别和部门计算数据范围,挂到 req.scope
* @param {string} resource - 资源名customers / contracts / after_sales / employees *
* 角色层级:
* - 总经理(level=1): 全量数据
* - 部门经理(level=2): 本部门数据
* - 部门副经理(level=3): 本部门内自己及以下的数据
* - 一般专员(level=4): 仅自己的数据
*/ */
function dataScope(resource) { function dataScope(resource) {
return (req, res, next) => { return async (req, res, next) => {
const deptName = req.user.departmentName // 如 'franchise_manager' const { pool } = require('../db')
const roleLevel = req.user.role_level || 4
const deptName = req.user.departmentName
const deptDesc = req.user.departmentDesc
// 信息技术部和总经理办公室:全量数据 // 总经理:全量数据
if (deptName === 'admin' || deptName === 'general_manager') { if (roleLevel === 1) {
return next() return next()
} }
let sql = null, params = [] let sql = null
let params = []
switch (resource) { switch (resource) {
case 'customers': case 'employees':
// 过渡期:招商部和运营部也能看全量加盟商 if (roleLevel === 1) {
// 总经理:全量数据(已在上面处理)
} else if (roleLevel === 2) {
if (deptName === 'hr') {
// 人力资源部经理:查看所有员工
// 无过滤
} else {
// 其他部门经理:本部门所有员工
sql = 'AND department = ?'
params = [deptDesc]
}
} else if (roleLevel === 3) {
// 部门副经理:本部门内非经理级别的员工 + 自己
sql = 'AND (department = ? AND position NOT LIKE ?) OR id = (SELECT employee_id FROM users WHERE id = ?)'
params = [deptDesc, '%经理%', req.user.id]
} else {
// 一般专员:不能看到总经理的信息,但能看到其他员工
sql = 'AND position NOT LIKE ?'
params = ['%总经理%']
}
break break
case 'contracts': case 'contracts':
if (deptName === 'franchise_manager') { if (roleLevel === 2) {
sql = 'AND type = ?' // 部门经理:本部门人员负责的合同
params = ['franchise'] // 先查本部门的用户ID列表
} else if (deptName === 'procurement_manager') { const [deptUsers] = await pool.query(
sql = 'AND type = ?' 'SELECT u.id FROM users u LEFT JOIN departments d ON u.department_id = d.id WHERE d.name = ?',
params = ['supply'] [deptName]
)
const deptUserIds = deptUsers.map(u => u.id)
if (deptUserIds.length > 0) {
sql = `AND responsible_user_id IN (${deptUserIds.map(() => '?').join(',')})`
params = deptUserIds
} else {
sql = 'AND 1=0' // 无数据
}
} else if (roleLevel >= 3) {
// 副经理和专员:自己负责的合同
sql = 'AND responsible_user_id = ?'
params = [req.user.id]
} }
// finance 全量看operations_manager 无合同权限checkPermission 已拦截) // 招商部/采购部的类型限制仍然通过 validateContractType 处理
break break
case 'after_sales': case 'after_sales':
// 过渡期:运营部看全量售后 if (roleLevel === 2) {
// 部门经理:本部门人员负责的售后
const [deptUsersAS] = await pool.query(
'SELECT u.id FROM users u LEFT JOIN departments d ON u.department_id = d.id WHERE d.name = ?',
[deptName]
)
const deptUserIdsAS = deptUsersAS.map(u => u.id)
if (deptUserIdsAS.length > 0) {
sql = `AND responsible_user_id IN (${deptUserIdsAS.map(() => '?').join(',')})`
params = deptUserIdsAS
} else {
sql = 'AND 1=0'
}
} else if (roleLevel >= 3) {
// 副经理和专员:自己负责的售后
sql = 'AND responsible_user_id = ?'
params = [req.user.id]
}
break break
case 'employees': case 'customers':
if (deptName === 'hr') { // 加盟商是公共资源,所有有权限的人都能看
// 人力资源部可查看全量员工 // 但一般专员只能看自己关联的(通过合同/售后间接关联)
break if (roleLevel === 4) {
// 一般专员:只能看到与自己负责的合同/售后关联的客户
sql = 'AND id IN (SELECT customer_id FROM contracts WHERE responsible_user_id = ? UNION SELECT customer_id FROM after_sales WHERE responsible_user_id = ?)'
params = [req.user.id, req.user.id]
} }
if (['franchise_manager', 'operations_manager', 'procurement_manager'].includes(deptName)) {
sql = 'AND department = ?'
params = [req.user.departmentDesc] // 如 '招商部'
}
// admin / general_manager / finance 全量看员工
break break
} }
@@ -72,24 +125,23 @@ function dataScope(resource) {
/** /**
* 合同创建/修改时校验类型是否匹配部门 * 合同创建/修改时校验类型是否匹配部门
* 招商部只能操作加盟合同,采购部只能操作采购合同
*/ */
function validateContractType(req, res, next) { function validateContractType(req, res, next) {
const deptName = req.user.departmentName const deptName = req.user.departmentName
const type = req.body.type const type = req.body.type
// 未传 type 则跳过(路由会默认 'franchise'
if (type === undefined) return next() if (type === undefined) return next()
// 管理员/总经理/财务不限制 // 总经理财务不限制
if (['admin', 'general_manager', 'finance'].includes(deptName)) return next() if (['general_manager', 'finance'].includes(deptName)) return next()
// 信息技术部也不再是超级管理员,按部门权限走
if (deptName === 'admin') return next()
// 采购部只能操作采购合同
if (deptName === 'procurement_manager' && type !== 'supply') { if (deptName === 'procurement_manager' && type !== 'supply') {
return res.status(403).json({ code: 403, message: '采购部只能操作采购合同' }) return res.status(403).json({ code: 403, message: '采购部只能操作采购合同' })
} }
// 招商部只能操作加盟合同
if (deptName === 'franchise_manager' && type !== 'franchise') { if (deptName === 'franchise_manager' && type !== 'franchise') {
return res.status(403).json({ code: 403, message: '招商部只能操作加盟合同' }) return res.status(403).json({ code: 403, message: '招商部只能操作加盟合同' })
} }
@@ -97,4 +149,41 @@ function validateContractType(req, res, next) {
next() next()
} }
module.exports = { checkPermission, dataScope, validateContractType } /**
* 层级检查:确保操作者不能操作比自己级别高的用户
* 用于用户管理的删除/修改操作
*/
function checkHierarchy(req, res, next) {
return async (req, res, next) => {
try {
const { pool } = require('../db')
const operatorLevel = req.user.role_level || 4
const targetId = Number(req.params.id)
// 查询目标用户的role_level
const [rows] = await pool.query('SELECT role_level FROM users WHERE id = ?', [targetId])
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
const targetLevel = rows[0].role_level || 4
// 不能操作比自己级别高的用户
if (targetLevel < operatorLevel) {
return res.status(403).json({ code: 403, message: '不能操作比自己级别高的用户' })
}
// 最高只能操作同级
if (targetLevel < operatorLevel) {
return res.status(403).json({ code: 403, message: '权限不足,无法操作该用户' })
}
next()
} catch (e) {
console.error('[checkHierarchy] error:', e)
return res.status(500).json({ code: 500, message: e.message })
}
}
}
module.exports = { checkPermission, dataScope, validateContractType, checkHierarchy }

View File

@@ -20,29 +20,52 @@ function protectSelfUpdate(req, res, next) {
} }
/** /**
* 删除用户时的保护: * 删除用户时的保护:
* - 不能删除自己 * - 不能删除自己
* - 不能删除最后一个管理员 * - 不能删除比自己级别高的用户
* - 最高只能删除同级用户
* - 不能删除最后一个总经理
*/ */
async function protectUserDelete(req, res, next) { async function protectUserDelete(req, res, next) {
if (Number(req.params.id) === req.user.id) { const targetId = Number(req.params.id)
const operatorLevel = req.user.role_level || 4
// 不能删除自己
if (targetId === req.user.id) {
return res.status(400).json({ code: 400, message: '不能删除自己' }) return res.status(400).json({ code: 400, message: '不能删除自己' })
} }
const [rows] = await pool.query('SELECT department_id FROM users WHERE id = ?', [req.params.id]) try {
if (rows.length > 0) { const [rows] = await pool.query('SELECT role_level, department_id FROM users WHERE id = ?', [targetId])
const [adminDept] = await pool.query('SELECT id FROM departments WHERE name = ?', ['admin']) if (rows.length === 0) {
if (adminDept.length > 0 && rows[0].department_id === adminDept[0].id) { return res.status(404).json({ code: 404, message: '用户不存在' })
}
const targetLevel = rows[0].role_level || 4
// 不能删除比自己级别高的用户(数字越小级别越高)
if (targetLevel < operatorLevel) {
return res.status(403).json({ code: 403, message: '不能删除比自己级别高的用户' })
}
// 最高只能删除同级
// 这个逻辑已经在上面的检查中覆盖了targetLevel >= operatorLevel 才能继续)
// 不能删除最后一个总经理
if (targetLevel === 1) {
const [[{ cnt }]] = await pool.query( const [[{ cnt }]] = await pool.query(
'SELECT COUNT(*) AS cnt FROM users WHERE department_id = ?', 'SELECT COUNT(*) AS cnt FROM users WHERE role_level = 1'
[adminDept[0].id]
) )
if (cnt <= 1) { if (cnt <= 1) {
return res.status(400).json({ code: 400, message: '不能删除最后一个管理员' }) return res.status(400).json({ code: 400, message: '不能删除最后一个总经理' })
} }
} }
next()
} catch (e) {
console.error('[protectUserDelete] error:', e)
return res.status(500).json({ code: 500, message: e.message })
} }
next()
} }
module.exports = { protectSelfUpdate, protectUserDelete } module.exports = { protectSelfUpdate, protectUserDelete }

View File

@@ -241,7 +241,7 @@ async function update(req, res) {
let checkSql = 'SELECT id, employee_id FROM contracts WHERE id = ?' let checkSql = 'SELECT id, employee_id FROM contracts WHERE id = ?'
let checkParams = [id] let checkParams = [id]
if (req.scope && req.scope.sql) { if (req.scope && req.scope.sql) {
checkSql += ' ' + req.scope.sql checkSql += ' ' + req.scope.sql.replace(/\btype\b/g, 'c.type')
checkParams.push(...req.scope.params) checkParams.push(...req.scope.params)
} }
const [existing] = await pool.query(checkSql, checkParams) const [existing] = await pool.query(checkSql, checkParams)
@@ -320,7 +320,7 @@ async function remove(req, res) {
let checkSql = 'SELECT id FROM contracts WHERE id = ?' let checkSql = 'SELECT id FROM contracts WHERE id = ?'
let checkParams = [id] let checkParams = [id]
if (req.scope && req.scope.sql) { if (req.scope && req.scope.sql) {
checkSql += ' ' + req.scope.sql checkSql += ' ' + req.scope.sql.replace(/\btype\b/g, 'c.type')
checkParams.push(...req.scope.params) checkParams.push(...req.scope.params)
} }
const [existing] = await pool.query(checkSql, checkParams) const [existing] = await pool.query(checkSql, checkParams)

View File

@@ -120,6 +120,27 @@ async function create(req, res) {
return res.status(400).json({ code: 400, message: '员工姓名必填' }) return res.status(400).json({ code: 400, message: '员工姓名必填' })
} }
// 检查是否是添加总经理办公室的员工
if (department === '总经理办公室') {
const roleLevel = req.user.role_level || 4
const deptName = req.user.departmentName
// 只有信息技术部和人力资源部的部门经理才能添加总经理办公室的员工
if (roleLevel > 2 || !['admin', 'hr'].includes(deptName)) {
return res.status(403).json({ code: 403, message: '只有信息技术部和人力资源部的部门经理才能添加总经理办公室的员工' })
}
// 一般科员不能添加员工到总经理办公室
if (roleLevel === 4) {
return res.status(403).json({ code: 403, message: '一般科员不能添加总经理办公室的员工' })
}
// 只能添加副总经理,总经理是系统预设的
if (position && position !== '副总经理') {
return res.status(400).json({ code: 400, message: '总经理办公室只能添加副总经理,总经理是系统预设的' })
}
}
try { try {
const [result] = await pool.query( const [result] = await pool.query(
`INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status, remark) `INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status, remark)
@@ -141,11 +162,12 @@ async function create(req, res) {
// 如果中间件 allowUserCreation 要求同步创建用户账号 // 如果中间件 allowUserCreation 要求同步创建用户账号
if (req._createUser) { if (req._createUser) {
const { username, password, department_id } = req._createUser const { username, password, department_id, role_level } = req._createUser
const hash = await bcrypt.hash(password, 10) const hash = await bcrypt.hash(password, 10)
const validRoleLevel = [1, 2, 3, 4].includes(Number(role_level)) ? Number(role_level) : 4
await pool.query( await pool.query(
'INSERT INTO users (username, password, is_active, department_id, employee_id) VALUES (?, ?, 1, ?, ?)', 'INSERT INTO users (username, password, is_active, department_id, employee_id, role_level) VALUES (?, ?, 1, ?, ?, ?)',
[username, hash, department_id, result.insertId] [username, hash, department_id, result.insertId, validRoleLevel]
) )
delete req._createUser // 清理,避免影响后续中间件 delete req._createUser // 清理,避免影响后续中间件
} }
@@ -198,6 +220,20 @@ async function update(req, res) {
params.push(id) params.push(id)
await pool.query(`UPDATE employees SET ${sets.join(', ')} WHERE id = ?`, params) await pool.query(`UPDATE employees SET ${sets.join(', ')} WHERE id = ?`, params)
// 部门联动:员工部门变更时同步更新关联用户的部门
if (req.body.department !== undefined) {
const [[dept]] = await pool.query('SELECT id FROM departments WHERE description = ?', [req.body.department])
if (dept) {
await pool.query('UPDATE users SET department_id = ? WHERE employee_id = ?', [dept.id, id])
}
}
// 状态联动:员工离职时同步禁用关联用户,复职时同步启用
if (req.body.status !== undefined) {
const isActive = req.body.status === 1 ? 1 : 0
await pool.query('UPDATE users SET is_active = ? WHERE employee_id = ?', [isActive, id])
}
const [rows] = await pool.query('SELECT * FROM employees WHERE id = ?', [id]) const [rows] = await pool.query('SELECT * FROM employees WHERE id = ?', [id])
res.json({ code: 0, message: 'ok', data: rows[0] }) res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) { } catch (e) {
@@ -241,14 +277,23 @@ async function remove(req, res) {
// GET /api/employees/simple —— 简易员工列表(无数据范围限制,用于下拉选择) // GET /api/employees/simple —— 简易员工列表(无数据范围限制,用于下拉选择)
async function simpleList(req, res) { async function simpleList(req, res) {
try { try {
const { department } = req.query const { department, max_role_level } = req.query
let sql = 'SELECT id, name, department, position FROM employees WHERE status = 1' // 关联users表获取role_level信息
let sql = `SELECT e.id, e.name, e.department, e.position, COALESCE(u.role_level, 4) as role_level
FROM employees e
LEFT JOIN users u ON e.id = u.employee_id
WHERE e.status = 1`
const params = [] const params = []
if (department) { if (department) {
sql += ' AND department = ?' sql += ' AND e.department = ?'
params.push(department) params.push(department)
} }
sql += ' ORDER BY id' // 过滤角色级别(不能选择比自己职位高的员工)
if (max_role_level) {
sql += ' AND COALESCE(u.role_level, 4) >= ?'
params.push(Number(max_role_level))
}
sql += ' ORDER BY e.id'
const [rows] = await pool.query(sql, params) const [rows] = await pool.query(sql, params)
res.json({ code: 0, message: 'ok', data: rows }) res.json({ code: 0, message: 'ok', data: rows })
} catch (e) { } catch (e) {

View File

@@ -5,7 +5,7 @@ const { pool } = require('../db')
// 安全字段:关联查询 departments 和 employees // 安全字段:关联查询 departments 和 employees
const USER_LIST_SQL = ` const USER_LIST_SQL = `
SELECT u.id, u.username, u.is_active, u.department_id, u.employee_id, SELECT u.id, u.username, u.is_active, u.department_id, u.employee_id, u.role_level,
u.created_at, u.updated_at, u.created_at, u.updated_at,
d.name AS dept_name, d.description AS dept_desc, d.name AS dept_name, d.description AS dept_desc,
e.name AS real_name e.name AS real_name
@@ -32,7 +32,7 @@ async function login(req, res) {
try { try {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT u.id, u.username, u.password, u.is_active, u.department_id, u.employee_id, `SELECT u.id, u.username, u.password, u.is_active, u.department_id, u.employee_id, u.role_level,
d.name AS dept_name, d.description AS dept_desc, e.name AS real_name d.name AS dept_name, d.description AS dept_desc, e.name AS real_name
FROM users u FROM users u
LEFT JOIN departments d ON u.department_id = d.id LEFT JOIN departments d ON u.department_id = d.id
@@ -74,6 +74,7 @@ async function login(req, res) {
department_id: user.department_id, department_id: user.department_id,
departmentName: user.dept_name, departmentName: user.dept_name,
departmentDesc: user.dept_desc, departmentDesc: user.dept_desc,
role_level: user.role_level || 4,
permissions, permissions,
}, },
process.env.JWT_SECRET, process.env.JWT_SECRET,
@@ -93,6 +94,7 @@ async function login(req, res) {
department_id: user.department_id, department_id: user.department_id,
departmentName: user.dept_name, departmentName: user.dept_name,
departmentDesc: user.dept_desc, departmentDesc: user.dept_desc,
role_level: user.role_level || 4,
permissions, permissions,
}, },
}, },
@@ -253,7 +255,7 @@ async function detail(req, res) {
// POST /api/users —— 创建用户 // POST /api/users —— 创建用户
async function create(req, res) { async function create(req, res) {
const { username, password, department_id, employee_id, is_active = 1 } = req.body || {} const { username, password, department_id, employee_id, is_active = 1, role_level = 4 } = req.body || {}
if (!username || !password) { if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' }) return res.status(400).json({ code: 400, message: '用户名和密码必填' })
@@ -267,10 +269,31 @@ async function create(req, res) {
// 校验 department_id 是否存在 // 校验 department_id 是否存在
if (department_id) { if (department_id) {
const [dept] = await pool.query('SELECT id FROM departments WHERE id = ?', [department_id]) const [dept] = await pool.query('SELECT id, name FROM departments WHERE id = ?', [department_id])
if (dept.length === 0) { if (dept.length === 0) {
return res.status(400).json({ code: 400, message: '指定的部门不存在' }) return res.status(400).json({ code: 400, message: '指定的部门不存在' })
} }
// 检查是否是添加总经理办公室的用户
if (dept[0].name === 'general_manager') {
const operatorLevel = req.user.role_level || 4
const operatorDept = req.user.departmentName
// 只有信息技术部和人力资源部的部门经理才能添加总经理办公室的用户
if (operatorLevel > 2 || !['admin', 'hr'].includes(operatorDept)) {
return res.status(403).json({ code: 403, message: '只有信息技术部和人力资源部的部门经理才能添加总经理办公室的用户' })
}
// 一般科员不能添加总经理办公室的用户
if (operatorLevel === 4) {
return res.status(403).json({ code: 403, message: '一般科员不能添加总经理办公室的用户' })
}
// 只能添加副总经理role_level=2总经理是系统预设的
if (role_level && Number(role_level) !== 2) {
return res.status(400).json({ code: 400, message: '总经理办公室只能添加副总经理(角色级别2),总经理是系统预设的' })
}
}
} }
// 校验 employee_id 是否存在 // 校验 employee_id 是否存在
@@ -283,9 +306,11 @@ async function create(req, res) {
try { try {
const hash = await bcrypt.hash(password, 10) const hash = await bcrypt.hash(password, 10)
// 验证role_level范围
const validRoleLevel = [1, 2, 3, 4].includes(Number(role_level)) ? Number(role_level) : 4
const [result] = await pool.query( const [result] = await pool.query(
'INSERT INTO users (username, password, is_active, department_id, employee_id) VALUES (?, ?, ?, ?, ?)', 'INSERT INTO users (username, password, is_active, department_id, employee_id, role_level) VALUES (?, ?, ?, ?, ?, ?)',
[username, hash, is_active, department_id || null, employee_id || null] [username, hash, is_active, department_id || null, employee_id || null, validRoleLevel]
) )
const [rows] = await pool.query( const [rows] = await pool.query(
@@ -305,10 +330,10 @@ async function create(req, res) {
// PUT /api/users/:id —— 更新用户 // PUT /api/users/:id —— 更新用户
async function update(req, res) { async function update(req, res) {
const { id } = req.params const { id } = req.params
const fields = ['username', 'department_id', 'employee_id', 'is_active'] const fields = ['username', 'department_id', 'employee_id', 'is_active', 'role_level']
try { try {
const [existing] = await pool.query('SELECT id, department_id, is_active FROM users WHERE id = ?', [id]) const [existing] = await pool.query('SELECT id, department_id, is_active, employee_id FROM users WHERE id = ?', [id])
if (existing.length === 0) { if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' }) return res.status(404).json({ code: 404, message: '用户不存在' })
} }
@@ -358,6 +383,14 @@ async function update(req, res) {
params.push(id) params.push(id)
await pool.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, params) 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( const [rows] = await pool.query(
`${USER_LIST_SQL} WHERE u.id = ?`, `${USER_LIST_SQL} WHERE u.id = ?`,
[id] [id]

View File

@@ -13,7 +13,7 @@ const products = require('./routes/products')
const suppliers = require('./routes/suppliers') const suppliers = require('./routes/suppliers')
// 导入中间件 // 导入中间件
const { checkPermission, dataScope, validateContractType } = require('./middleware/permissions') const { checkPermission, dataScope, validateContractType, checkHierarchy } = require('./middleware/permissions')
const { protectSelfUpdate, protectUserDelete } = require('./middleware/users') const { protectSelfUpdate, protectUserDelete } = require('./middleware/users')
const { allowUserCreation } = require('./middleware/employees') const { allowUserCreation } = require('./middleware/employees')
@@ -47,8 +47,8 @@ app.put('/api/user/password', auth, users.changePassword)
app.get('/api/users', auth, checkPermission('user', 'manage'), users.list) app.get('/api/users', auth, checkPermission('user', 'manage'), users.list)
app.get('/api/users/:id', auth, checkPermission('user', 'manage'), users.detail) app.get('/api/users/:id', auth, checkPermission('user', 'manage'), users.detail)
app.post('/api/users', auth, checkPermission('user', 'manage'), users.create) app.post('/api/users', auth, checkPermission('user', 'manage'), users.create)
app.put('/api/users/:id', auth, checkPermission('user', 'manage'), protectSelfUpdate, users.update) app.put('/api/users/:id', auth, checkPermission('user', 'manage'), checkHierarchy, protectSelfUpdate, users.update)
app.delete('/api/users/:id', auth, checkPermission('user', 'manage'), protectUserDelete, users.remove) app.delete('/api/users/:id', auth, checkPermission('user', 'manage'), checkHierarchy, protectUserDelete, users.remove)
// ============ 加盟商管理 /api/customers ============ // ============ 加盟商管理 /api/customers ============
app.get('/api/customers/simple', auth, checkPermission('customer', 'read'), customers.simpleList) app.get('/api/customers/simple', auth, checkPermission('customer', 'read'), customers.simpleList)