This commit is contained in:
Kyaru
2026-06-30 23:11:19 +08:00
parent ccb4c32614
commit 11cb335507
8 changed files with 295 additions and 92 deletions

View File

@@ -4,7 +4,7 @@
* 允许在创建员工时一并创建系统用户账号(需 user:manage 权限)
* 将用户创建参数挂到 req._createUser由路由在写入 employee 后处理
*/
function allowUserCreation(req, res, next) {
async function allowUserCreation(req, res, next) {
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 位' })
}
// 检查是否是添加总经理办公室的用户
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._createUser = { username, password, department_id }
req._createUser = { username, password, department_id, role_level: role_level || 4 }
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) {
return (req, res, next) => {
@@ -16,50 +14,105 @@ function checkPermission(resource, action) {
}
/**
* 根据用户部门计算数据范围,挂到 req.scope
* @param {string} resource - 资源名customers / contracts / after_sales / employees
* 根据用户角色级别和部门计算数据范围,挂到 req.scope
*
* 角色层级:
* - 总经理(level=1): 全量数据
* - 部门经理(level=2): 本部门数据
* - 部门副经理(level=3): 本部门内自己及以下的数据
* - 一般专员(level=4): 仅自己的数据
*/
function dataScope(resource) {
return (req, res, next) => {
const deptName = req.user.departmentName // 如 'franchise_manager'
return async (req, res, next) => {
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()
}
let sql = null, params = []
let sql = null
let params = []
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
case 'contracts':
if (deptName === 'franchise_manager') {
sql = 'AND type = ?'
params = ['franchise']
} else if (deptName === 'procurement_manager') {
sql = 'AND type = ?'
params = ['supply']
if (roleLevel === 2) {
// 部门经理:本部门人员负责的合同
// 先查本部门的用户ID列表
const [deptUsers] = await pool.query(
'SELECT u.id FROM users u LEFT JOIN departments d ON u.department_id = d.id WHERE d.name = ?',
[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
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
case 'employees':
if (deptName === 'hr') {
// 人力资源部可查看全量员工
break
case 'customers':
// 加盟商是公共资源,所有有权限的人都能看
// 但一般专员只能看自己关联的(通过合同/售后间接关联)
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
}
@@ -72,24 +125,23 @@ function dataScope(resource) {
/**
* 合同创建/修改时校验类型是否匹配部门
* 招商部只能操作加盟合同,采购部只能操作采购合同
*/
function validateContractType(req, res, next) {
const deptName = req.user.departmentName
const type = req.body.type
// 未传 type 则跳过(路由会默认 'franchise'
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') {
return res.status(403).json({ code: 403, message: '采购部只能操作采购合同' })
}
// 招商部只能操作加盟合同
if (deptName === 'franchise_manager' && type !== 'franchise') {
return res.status(403).json({ code: 403, message: '招商部只能操作加盟合同' })
}
@@ -97,4 +149,41 @@ function validateContractType(req, res, 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) {
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: '不能删除自己' })
}
const [rows] = await pool.query('SELECT department_id FROM users WHERE id = ?', [req.params.id])
if (rows.length > 0) {
const [adminDept] = await pool.query('SELECT id FROM departments WHERE name = ?', ['admin'])
if (adminDept.length > 0 && rows[0].department_id === adminDept[0].id) {
try {
const [rows] = await pool.query('SELECT role_level, department_id 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: '不能删除比自己级别高的用户' })
}
// 最高只能删除同级
// 这个逻辑已经在上面的检查中覆盖了targetLevel >= operatorLevel 才能继续)
// 不能删除最后一个总经理
if (targetLevel === 1) {
const [[{ cnt }]] = await pool.query(
'SELECT COUNT(*) AS cnt FROM users WHERE department_id = ?',
[adminDept[0].id]
'SELECT COUNT(*) AS cnt FROM users WHERE role_level = 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 }