Compare commits

..

2 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
8 changed files with 762 additions and 106 deletions

535
db.js
View File

@@ -42,6 +42,7 @@ async function initDB() {
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
department_id INT DEFAULT NULL COMMENT '部门ID (关联departments表)',
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 '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
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] 数据表初始化完成')
// ============ 2.3 部门与权限相关表 ============
@@ -314,8 +323,7 @@ async function seedDepartmentsAndPermissions() {
const deptPermMap = {
admin: [
'user:manage',
'customer:read', 'contract:read', 'after_sale:read',
'product:read', 'supplier:read', 'employee:read',
'employee:read',
],
general_manager: [
'customer:read', 'contract:read', 'after_sale:read',
@@ -330,7 +338,6 @@ async function seedDepartmentsAndPermissions() {
],
operations_manager: [
'customer:read',
'contract:read',
'after_sale:read', 'after_sale:create', 'after_sale:update', 'after_sale:delete',
'employee:read',
],
@@ -348,7 +355,6 @@ async function seedDepartmentsAndPermissions() {
hr: [
'employee:read', 'employee:create', 'employee:update', 'employee:delete',
'user:manage',
'customer:read',
],
}
@@ -375,51 +381,504 @@ async function seedDefaultUsers() {
const [deptRows] = await pool.query('SELECT id, name FROM departments')
const deptIdMap = Object.fromEntries(deptRows.map(d => [d.name, d.id]))
// 总经理:张超
const emp = { name: '张超', gender: '男', age: 42, education: '硕士', department: '总经理办公室', entry_date: '2015-03-01', position: '总经理', salary: 50000, phone: '13800001001', email: 'zhangchao@mixue.com' }
const usr = { username: 'zhangchao', deptName: 'general_manager' }
const seedEmployees = [
{ name: '张超', gender: '男', age: 42, education: '硕士', department: '总经理办公室', entry_date: '2015-03-01', position: '总经理', salary: 50000, phone: '13800001001', email: 'zhangchao@mixue.com' },
{ name: '李明', gender: '男', age: 35, education: '本科', department: '招商部', entry_date: '2018-06-15', position: '招商经理', salary: 18000, phone: '13800001002', email: 'liming@mixue.com' },
{ name: '王丽', gender: '女', age: 33, education: '本科', department: '运营部', entry_date: '2019-01-10', position: '运营经理', salary: 18000, phone: '13800001003', email: 'wangli@mixue.com' },
{ name: '赵强', gender: '男', age: 36, education: '本科', department: '采购部', entry_date: '2017-09-20', position: '采购经理', salary: 18000, phone: '13800001004', email: 'zhaoqiang@mixue.com' },
{ name: '陈芳', gender: '女', age: 30, education: '本科', department: '财务部', entry_date: '2020-03-05', position: '财务主管', salary: 15000, phone: '13800001005', email: 'chenfang@mixue.com' },
{ name: '系统管理员', gender: '男', age: 28, education: '本科', department: '信息技术部', entry_date: '2022-07-01', position: '系统管理员', salary: 12000, phone: '13800001006', email: 'admin@mixue.com' },
{ name: '杨帆', gender: '女', age: 34, education: '本科', department: '人力资源部', entry_date: '2019-05-01', position: '人力资源经理', salary: 15000, phone: '13800001007', email: 'yangfan@mixue.com' },
]
const [userCheck] = await pool.query('SELECT id FROM users WHERE username = ?', [usr.username])
if (userCheck.length === 0) {
const seedUsers = [
{ username: 'zhangchao', deptName: 'general_manager', role_level: 1 },
{ username: 'liming', deptName: 'franchise_manager', role_level: 2 },
{ username: 'wangli', deptName: 'operations_manager', role_level: 2 },
{ username: 'zhaoqiang', deptName: 'procurement_manager',role_level: 2 },
{ username: 'chenfang', deptName: 'finance', role_level: 2 },
{ username: 'admin', deptName: 'admin', role_level: 2 },
{ username: 'yangfan', deptName: 'hr', role_level: 2 },
]
for (let i = 0; i < seedUsers.length; i++) {
const emp = seedEmployees[i]
const usr = seedUsers[i]
// 逐个检查用户是否存在(不依赖全局检查,避免部分用户被误删后跳过全部重建)
const [userCheck] = await pool.query('SELECT id FROM users WHERE username = ?', [usr.username])
if (userCheck.length > 0) continue
// 先插入 employees
const [empResult] = await pool.query(
`INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
[emp.name, emp.gender, emp.age, emp.education, emp.department, emp.entry_date, emp.position, emp.salary, emp.phone, emp.email]
)
await pool.query(
`INSERT INTO users (username, password, is_active, department_id, employee_id)
VALUES (?, ?, 1, ?, ?)`,
[usr.username, hash, deptIdMap[usr.deptName], empResult.insertId]
`INSERT INTO users (username, password, is_active, department_id, employee_id, role_level)
VALUES (?, ?, 1, ?, ?, ?)`,
[usr.username, hash, deptIdMap[usr.deptName], empId, usr.role_level]
)
console.log(`[init] 已创建用户 ${usr.username} (${emp.name})`)
}
}
// ============ 5. 种子数据:业务数据(已注释,仅保留总经理账号) ============
// async function seedBusinessData() {
// // ---- 5.1 新增员工 ----
// const [empCheck] = await pool.query("SELECT id FROM employees WHERE name = '刘洋' LIMIT 1")
// if (empCheck.length === 0) {
// const extraEmployees = [
// { name: '刘洋', gender: '男', age: 28, education: '本科', department: '招商部', entry_date: '2021-03-15', position: '招商专员', salary: 8000, phone: '13800002001', email: 'liuyang@mixue.com' },
// { name: '孙婷', gender: '女', age: 26, education: '本科', department: '招商部', entry_date: '2022-02-20', position: '招商专员', salary: 7500, phone: '13800002002', email: 'sunting@mixue.com' },
// { name: '周杰', gender: '男', age: 30, education: '本科', department: '运营部', entry_date: '2021-06-01', position: '运营专员', salary: 8500, phone: '13800002003', email: 'zhoujie@mixue.com' },
// { name: '吴敏', gender: '女', age: 25, education: '本科', department: '运营部', entry_date: '2023-01-10', position: '运营专员', salary: 7000, phone: '13800002004', email: 'wumin@mixue.com' },
// { name: '郑伟', gender: '男', age: 32, education: '大专', department: '运营部', entry_date: '2020-08-15', position: '售后工程师', salary: 9000, phone: '13800002005', email: 'zhengwei@mixue.com' },
// { name: '黄磊', gender: '男', age: 29, education: '本科', department: '采购部', entry_date: '2022-04-01', position: '采购专员', salary: 8000, phone: '13800002006', email: 'huanglei@mixue.com' },
// { name: '马丽', gender: '女', age: 27, education: '本科', department: '财务部', entry_date: '2021-09-01', position: '会计', salary: 7500, phone: '13800002007', email: 'mali@mixue.com' },
// { name: '林峰', gender: '男', age: 45, education: '硕士', department: '总经理办公室', entry_date: '2016-05-01', position: '副总经理', salary: 35000, phone: '13800002008', email: 'linfeng@mixue.com' },
// ]
// for (const e of extraEmployees) {
// await pool.query(
// `INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status)
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
// [e.name, e.gender, e.age, e.education, e.department, e.entry_date, e.position, e.salary, e.phone, e.email]
// )
// }
// console.log('[init] 已初始化 8 条员工数据')
// }
// // ... 其余业务种子数据代码
// }
// ============ 5. 种子数据:业务数据 ============
async function seedBusinessData() {
// ---- 5.1 新增员工 ----
const [empCheck] = await pool.query("SELECT id FROM employees WHERE name = '刘洋' LIMIT 1")
if (empCheck.length === 0) {
const extraEmployees = [
{ name: '刘洋', gender: '男', age: 28, education: '本科', department: '招商部', entry_date: '2021-03-15', position: '招商专员', salary: 8000, phone: '13800002001', email: 'liuyang@mixue.com' },
{ name: '孙婷', gender: '女', age: 26, education: '本科', department: '招商部', entry_date: '2022-02-20', position: '招商专员', salary: 7500, phone: '13800002002', email: 'sunting@mixue.com' },
{ name: '周杰', gender: '男', age: 30, education: '本科', department: '运营部', entry_date: '2021-06-01', position: '运营专员', salary: 8500, phone: '13800002003', email: 'zhoujie@mixue.com' },
{ name: '吴敏', gender: '女', age: 25, education: '本科', department: '运营部', entry_date: '2023-01-10', position: '运营专员', salary: 7000, phone: '13800002004', email: 'wumin@mixue.com' },
{ name: '郑伟', gender: '男', age: 32, education: '大专', department: '运营部', entry_date: '2020-08-15', position: '售后工程师', salary: 9000, phone: '13800002005', email: 'zhengwei@mixue.com' },
{ name: '黄磊', gender: '男', age: 29, education: '本科', department: '采购部', entry_date: '2022-04-01', position: '采购专员', salary: 8000, phone: '13800002006', email: 'huanglei@mixue.com' },
{ name: '马丽', gender: '女', age: 27, education: '本科', department: '财务部', entry_date: '2021-09-01', position: '会计', salary: 7500, phone: '13800002007', email: 'mali@mixue.com' },
{ name: '林峰', gender: '男', age: 45, education: '硕士', department: '总经理办公室', entry_date: '2016-05-01', position: '副总经理', salary: 35000, phone: '13800002008', email: 'linfeng@mixue.com' },
]
for (const e of extraEmployees) {
await pool.query(
`INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
[e.name, e.gender, e.age, e.education, e.department, e.entry_date, e.position, e.salary, e.phone, e.email]
)
}
console.log('[init] 已初始化 8 条员工数据')
}
// ---- 5.1b 为业务员工创建用户账号 ----
const hash = await bcrypt.hash('123456', 10)
const [deptRows] = await pool.query('SELECT id, name FROM departments')
const deptIdMap2 = Object.fromEntries(deptRows.map(d => [d.name, d.id]))
const extraUsers = [
{ empName: '刘洋', username: 'liuyang', deptName: 'franchise_manager', role_level: 4 },
{ empName: '孙婷', username: 'sunting', deptName: 'franchise_manager', role_level: 4 },
{ empName: '周杰', username: 'zhoujie', deptName: 'operations_manager', role_level: 4 },
{ empName: '吴敏', username: 'wumin', deptName: 'operations_manager', role_level: 4 },
{ empName: '郑伟', username: 'zhengwei', deptName: 'operations_manager', role_level: 4 },
{ empName: '黄磊', username: 'huanglei', deptName: 'procurement_manager',role_level: 4 },
{ empName: '马丽', username: 'mali', deptName: 'finance', role_level: 4 },
{ empName: '林峰', username: 'linfeng', deptName: 'general_manager', role_level: 2 },
]
const [newEmps] = await pool.query("SELECT id, name, department FROM employees WHERE name IN ('刘洋','孙婷','周杰','吴敏','郑伟','黄磊','马丽','林峰')")
const newEmpMap = Object.fromEntries(newEmps.map(e => [e.name, e]))
for (const u of extraUsers) {
const emp = newEmpMap[u.empName]
if (!emp) continue
const [userCheck] = await pool.query('SELECT id FROM users WHERE username = ?', [u.username])
if (userCheck.length > 0) continue
await pool.query(
'INSERT INTO users (username, password, is_active, department_id, employee_id, role_level) VALUES (?, ?, 1, ?, ?, ?)',
[u.username, hash, deptIdMap2[u.deptName], emp.id, u.role_level]
)
console.log(`[init] 已创建用户 ${u.username} (${u.empName} - ${emp.department})`)
}
// 查询员工 ID 映射(用于合同和售后单)
const [allEmps] = await pool.query('SELECT id, name FROM employees')
const empIdMap = Object.fromEntries(allEmps.map(e => [e.name, e.id]))
// ---- 5.2 供应商 ----
const [supCheck] = await pool.query("SELECT id FROM suppliers WHERE name = '郑州瑞丰食品原料有限公司' LIMIT 1")
if (supCheck.length === 0) {
const suppliers = [
{ name: '郑州瑞丰食品原料有限公司', contact: '张建国', phone: '0371-66881234', address: '郑州市经开区食品工业园A区12号', type: '原料', content: '主要供应茶叶、椰浆、果酱等茶饮原料合作5年以上' },
{ name: '河南优品包装科技有限公司', contact: '李文华', phone: '0371-66885678', address: '郑州市高新区包装产业园B栋', type: '包装', content: '提供PET杯、PP杯、杯盖、吸管等包装材料' },
{ name: '广州冰泉制冰设备有限公司', contact: '陈志强', phone: '020-88661234', address: '广州市番禺区工业路88号', type: '设备', content: '制冰机、冷藏柜等冷链设备供应商' },
{ name: '武汉华源茶叶有限公司', contact: '王秀英', phone: '027-85661234', address: '武汉市江汉区茶业批发市场C区', type: '原料', content: '优质茶叶供应商,提供茉莉绿茶、红茶、乌龙茶等' },
{ name: '浙江鑫达纸杯制品有限公司', contact: '赵明', phone: '0571-88771234', address: '杭州市萧山区纸品工业园区6号', type: '包装', content: '定制印刷纸杯和包装袋' },
{ name: '山东齐鲁冷链物流有限公司', contact: '刘大海', phone: '0531-88661234', address: '济南市历城区物流产业园东区', type: '原料', content: '冷链物流服务,负责奶粉、椰浆等冷链原料配送' },
]
for (const s of suppliers) {
await pool.query(
'INSERT INTO suppliers (name, contact, phone, address, type, content, status) VALUES (?, ?, ?, ?, ?, ?, 1)',
[s.name, s.contact, s.phone, s.address, s.type, s.content]
)
}
console.log('[init] 已初始化 6 家供应商')
}
// 查询供应商名称(供货合同引用)
const [supRows] = await pool.query('SELECT id, name FROM suppliers')
const supIdMap = Object.fromEntries(supRows.map(s => [s.name, s.id]))
// ---- 5.3 产品/原料 ----
const [prodCheck] = await pool.query("SELECT id FROM products WHERE name = '茉莉绿茶' LIMIT 1")
if (prodCheck.length === 0) {
const products = [
// 茶饮原料
{ name: '茉莉绿茶', type: '茶饮原料', quantity: 5000, price: 28.00, unit: 'kg', specification: '500g/袋', supplier: '武汉华源茶叶有限公司' },
{ name: '红茶', type: '茶饮原料', quantity: 3000, price: 32.00, unit: 'kg', specification: '500g/袋', supplier: '武汉华源茶叶有限公司' },
{ name: '乌龙茶', type: '茶饮原料', quantity: 2000, price: 45.00, unit: 'kg', specification: '500g/袋', supplier: '武汉华源茶叶有限公司' },
{ name: '椰浆', type: '茶饮原料', quantity: 8000, price: 12.50, unit: 'L', specification: '1L/盒', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '果糖糖浆', type: '茶饮原料', quantity: 6000, price: 8.00, unit: 'L', specification: '2.5L/桶', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '奶粉', type: '茶饮原料', quantity: 3000, price: 35.00, unit: 'kg', specification: '1kg/袋', supplier: '山东齐鲁冷链物流有限公司' },
{ name: '珍珠粉圆', type: '茶饮原料', quantity: 10000, price: 6.50, unit: 'kg', specification: '1kg/袋', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '红豆罐头', type: '茶饮原料', quantity: 2000, price: 9.80, unit: '罐', specification: '400g/罐', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '芒果果酱', type: '茶饮原料', quantity: 3000, price: 18.00, unit: 'kg', specification: '1kg/瓶', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '草莓果酱', type: '茶饮原料', quantity: 2500, price: 20.00, unit: 'kg', specification: '1kg/瓶', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '柠檬浓缩汁', type: '茶饮原料', quantity: 4000, price: 15.00, unit: 'L', specification: '1L/瓶', supplier: '郑州瑞丰食品原料有限公司' },
{ name: '芋泥粉', type: '茶饮原料', quantity: 1500, price: 22.00, unit: 'kg', specification: '500g/袋', supplier: '郑州瑞丰食品原料有限公司' },
// 包装物料
{ name: '500ml PET杯', type: '包装物料', quantity: 200000, price: 0.15, unit: '个', specification: '500ml 透明', supplier: '河南优品包装科技有限公司' },
{ name: '700ml PP杯', type: '包装物料', quantity: 150000, price: 0.20, unit: '个', specification: '700ml 白色', supplier: '浙江鑫达纸杯制品有限公司' },
{ name: '杯盖', type: '包装物料', quantity: 300000, price: 0.08, unit: '个', specification: '平盖+凸盖 各半', supplier: '河南优品包装科技有限公司' },
{ name: '吸管', type: '包装物料', quantity: 500000, price: 0.03, unit: '根', specification: '粗吸管 12mm', supplier: '河南优品包装科技有限公司' },
// 设备器具
{ name: '制冰机', type: '设备器具', quantity: 50, price: 5800.00, unit: '台', specification: '日产冰量100kg', supplier: '广州冰泉制冰设备有限公司' },
{ name: '封口机', type: '设备器具', quantity: 80, price: 2200.00, unit: '台', specification: '全自动杯装封口', supplier: '广州冰泉制冰设备有限公司' },
]
for (const p of products) {
await pool.query(
'INSERT INTO products (name, type, quantity, price, unit, specification, supplier) VALUES (?, ?, ?, ?, ?, ?, ?)',
[p.name, p.type, p.quantity, p.price, p.unit, p.specification, p.supplier]
)
}
console.log('[init] 已初始化 18 个产品')
}
// ---- 5.4 加盟商 (customers) ----
const [custCheck] = await pool.query("SELECT id FROM customers WHERE name = '赵鑫磊' LIMIT 1")
if (custCheck.length === 0) {
const customers = [
{ name: '赵鑫磊', phone: '13912345001', province: '河南省', city: '郑州市', district: '金水区', address: '花园路与农业路交叉口向南200米', email: 'zhaoxinlei@qq.com', remark: '老加盟商经营3年业绩优秀' },
{ name: '钱小燕', phone: '13912345002', province: '河南省', city: '洛阳市', district: '西工区', address: '中州中路王府井百货对面', email: 'qianxiaoyan@qq.com', remark: '2024年新开店经营情况良好' },
{ name: '孙大伟', phone: '13912345003', province: '山东省', city: '济南市', district: '历下区', address: '泉城路188号', email: 'sundawei@qq.com', remark: '大学城附近,客流量大' },
{ name: '李美玲', phone: '13912345004', province: '山东省', city: '青岛市', district: '市南区', address: '香港中路佳世客商场一楼', email: 'limeiling@qq.com', remark: '商场店,租金较高' },
{ name: '周五六', phone: '13912345005', province: '江苏省', city: '南京市', district: '鼓楼区', address: '新街口中山南路128号', email: 'zhouwuliu@qq.com', remark: '核心商圈,品牌效应好' },
{ name: '吴建华', phone: '13912345006', province: '江苏省', city: '苏州市', district: '姑苏区', address: '观前街太监弄18号', email: 'wujianhua@qq.com', remark: '旅游区店铺,夏季旺季营业额高' },
{ name: '郑雪梅', phone: '13912345007', province: '四川省', city: '成都市', district: '锦江区', address: '春熙路步行街IFS旁', email: 'zhengxuemei@qq.com', remark: '新加盟商,正在筹备开业' },
{ name: '王鹏飞', phone: '13912345008', province: '四川省', city: '成都市', district: '武侯区', address: '科华北路65号川大旁', email: 'wangpengfei@qq.com', remark: '学校周边,学生客群为主' },
{ name: '冯国强', phone: '13912345009', province: '广东省', city: '广州市', district: '天河区', address: '天河路228号正佳广场B1层', email: 'fengguoqiang@qq.com', remark: '商场负一楼,人流密集' },
{ name: '陈小凤', phone: '13912345010', province: '广东省', city: '深圳市', district: '南山区', address: '南海大道与东滨路交汇处', email: 'chenxiaofeng@qq.com', remark: '写字楼区,白领消费为主' },
{ name: '褚明亮', phone: '13912345011', province: '浙江省', city: '杭州市', district: '西湖区', address: '文三路与学院路交叉口', email: 'chumingliang@qq.com', remark: '2023年开业已回本' },
{ name: '卫国庆', phone: '13912345012', province: '湖北省', city: '武汉市', district: '武昌区', address: '中南路中商广场一楼', email: 'weiguoqing@qq.com', remark: '老加盟商,有意向开第二家' },
]
for (const c of customers) {
await pool.query(
`INSERT INTO customers (name, phone, province, city, district, address, email, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[c.name, c.phone, c.province, c.city, c.district, c.address, c.email, c.remark]
)
}
console.log('[init] 已初始化 12 个加盟商')
}
// 查询加盟商 ID 映射
const [custRows] = await pool.query('SELECT id, name FROM customers')
const custIdMap = Object.fromEntries(custRows.map(c => [c.name, c.id]))
// 查询 liming 和 wangli 的 user.id
const [userRows] = await pool.query("SELECT id, username FROM users WHERE username IN ('liming', 'wangli', 'admin')")
const userIdMap = Object.fromEntries(userRows.map(u => [u.username, u.id]))
// 查询 employee_id → user_id 映射(用于合同种子数据:业务员即负责人)
const [userEmpRows] = await pool.query('SELECT id, employee_id FROM users WHERE employee_id IS NOT NULL')
const userByEmpIdMap = Object.fromEntries(userEmpRows.map(u => [u.employee_id, u.id]))
// ---- 5.5 合同 (contracts) ----
const [contractCheck] = await pool.query("SELECT id FROM contracts WHERE contract_no = 'MX-JM-2024-001' LIMIT 1")
if (contractCheck.length === 0) {
// 加盟合同
const franchiseContracts = [
{
contract_name: '蜜雪冰城加盟合同(赵鑫磊-郑州花园路店)',
contract_no: 'MX-JM-2024-001', customer_name: '赵鑫磊', employee_name: '刘洋',
amount: 98000.00, effective_date: '2023-06-01', expiry_date: '2028-05-31',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在郑州市金水区花园路开设蜜雪冰城加盟门店合同期限5年。加盟费8万元保证金1.8万元。',
remark: '经营3年续约意愿强',
},
{
contract_name: '蜜雪冰城加盟合同(钱小燕-洛阳中州路店)',
contract_no: 'MX-JM-2024-002', customer_name: '钱小燕', employee_name: '刘洋',
amount: 88000.00, effective_date: '2024-03-15', expiry_date: '2029-03-14',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在洛阳市西工区中州路开设蜜雪冰城加盟门店合同期限5年。加盟费7万元保证金1.8万元。',
remark: '新加盟商,已完成培训',
},
{
contract_name: '蜜雪冰城加盟合同(孙大伟-济南泉城路店)',
contract_no: 'MX-JM-2024-003', customer_name: '孙大伟', employee_name: '孙婷',
amount: 108000.00, effective_date: '2023-09-01', expiry_date: '2028-08-31',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在济南市历下区泉城路开设蜜雪冰城加盟门店合同期限5年。加盟费9万元保证金1.8万元。',
remark: '大学城门店日均出杯量600+',
},
{
contract_name: '蜜雪冰城加盟合同(李美玲-青岛香港中路店)',
contract_no: 'MX-JM-2024-004', customer_name: '李美玲', employee_name: '孙婷',
amount: 128000.00, effective_date: '2024-01-10', expiry_date: '2029-01-09',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在青岛市市南区香港中路商场开设蜜雪冰城加盟门店合同期限5年。加盟费11万元保证金1.8万元。',
remark: '商场店,租金较高但人流有保障',
},
{
contract_name: '蜜雪冰城加盟合同(周五六-南京新街口店)',
contract_no: 'MX-JM-2024-005', customer_name: '周五六', employee_name: '刘洋',
amount: 148000.00, effective_date: '2023-04-20', expiry_date: '2028-04-19',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在南京市鼓楼区新街口商圈开设蜜雪冰城加盟门店合同期限5年。加盟费12万元保证金1.8万元品牌使用费1万元。',
remark: '核心商圈旗舰店,月营业额稳定',
},
{
contract_name: '蜜雪冰城加盟合同(吴建华-苏州观前街店)',
contract_no: 'MX-JM-2024-006', customer_name: '吴建华', employee_name: '孙婷',
amount: 118000.00, effective_date: '2024-05-01', expiry_date: '2029-04-30',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在苏州市姑苏区观前街开设蜜雪冰城加盟门店合同期限5年。加盟费10万元保证金1.8万元。',
remark: '旅游区店铺,夏季月营业额翻倍',
},
{
contract_name: '蜜雪冰城加盟合同(郑雪梅-成都春熙路店)',
contract_no: 'MX-JM-2024-007', customer_name: '郑雪梅', employee_name: '刘洋',
amount: 138000.00, effective_date: '2025-01-15', expiry_date: '2030-01-14',
status: '草稿', type: 'franchise',
contract_content: '甲方授权乙方在成都市锦江区春熙路商圈开设蜜雪冰城加盟门店合同期限5年。加盟费12万元保证金1.8万元。',
remark: '正在审核中,预计下月签约',
},
{
contract_name: '蜜雪冰城加盟合同(王鹏飞-成都科华路店)',
contract_no: 'MX-JM-2024-008', customer_name: '王鹏飞', employee_name: '孙婷',
amount: 88000.00, effective_date: '2023-08-10', expiry_date: '2028-08-09',
status: '已完成', type: 'franchise',
contract_content: '甲方授权乙方在成都市武侯区科华路川大旁开设蜜雪冰城加盟门店合同期限5年。加盟费7万元保证金1.8万元。',
remark: '合同期满未续约',
},
{
contract_name: '蜜雪冰城加盟合同(冯国强-广州正佳广场店)',
contract_no: 'MX-JM-2024-009', customer_name: '冯国强', employee_name: '刘洋',
amount: 158000.00, effective_date: '2024-02-01', expiry_date: '2029-01-31',
status: '生效', type: 'franchise',
contract_content: '甲方授权乙方在广州市天河区正佳广场B1层开设蜜雪冰城加盟门店合同期限5年。加盟费13万元保证金1.8万元品牌使用费1万元。',
remark: '商场负一楼黄金位置',
},
{
contract_name: '蜜雪冰城加盟合同(陈小凤-深圳南山店)',
contract_no: 'MX-JM-2024-010', customer_name: '陈小凤', employee_name: '孙婷',
amount: 108000.00, effective_date: '2023-11-20', expiry_date: '2028-11-19',
status: '已完成', type: 'franchise',
contract_content: '甲方授权乙方在深圳市南山区南海大道开设蜜雪冰城加盟门店合同期限5年。加盟费9万元保证金1.8万元。',
remark: '合同正常到期完成',
},
]
for (const c of franchiseContracts) {
const empId = empIdMap[c.employee_name]
await pool.query(
`INSERT INTO contracts (contract_name, contract_no, customer_id, employee_id, amount, effective_date, expiry_date, status, type, contract_content, remark, responsible_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[c.contract_name, c.contract_no, custIdMap[c.customer_name], empId,
c.amount, c.effective_date, c.expiry_date, c.status, c.type, c.contract_content, c.remark,
userByEmpIdMap[empId]]
)
}
// 供货合同
const supplyContracts = [
{
contract_name: '2024年度茶叶原料供货合同',
contract_no: 'MX-GH-2024-001', supplier_name: '武汉华源茶叶有限公司', employee_name: '黄磊',
amount: 360000.00, effective_date: '2024-01-01', expiry_date: '2024-12-31',
status: '生效', type: 'supply',
contract_content: '甲方向乙方采购茉莉绿茶、红茶、乌龙茶等茶叶原料年度采购量不低于30吨价格按季度协商调整。',
remark: '年度框架协议',
},
{
contract_name: '2024年度包装材料供货合同',
contract_no: 'MX-GH-2024-002', supplier_name: '河南优品包装科技有限公司', employee_name: '黄磊',
amount: 280000.00, effective_date: '2024-01-01', expiry_date: '2024-12-31',
status: '生效', type: 'supply',
contract_content: '甲方向乙方采购PET杯、PP杯、杯盖、吸管等包装材料按月下单每次不低于10万只。',
remark: '长期合作伙伴',
},
{
contract_name: '2024年度设备采购合同',
contract_no: 'MX-GH-2024-003', supplier_name: '广州冰泉制冰设备有限公司', employee_name: '黄磊',
amount: 520000.00, effective_date: '2024-03-01', expiry_date: '2025-02-28',
status: '生效', type: 'supply',
contract_content: '甲方向乙方采购制冰机、封口机、冷藏柜等门店设备,含安装调试和一年质保服务。',
remark: '新店开业批量采购',
},
{
contract_name: '2023年度食品原料供货合同',
contract_no: 'MX-GH-2023-001', supplier_name: '郑州瑞丰食品原料有限公司', employee_name: '黄磊',
amount: 450000.00, effective_date: '2023-01-01', expiry_date: '2023-12-31',
status: '已完成', type: 'supply',
contract_content: '甲方向乙方采购椰浆、果糖糖浆、珍珠粉圆、果酱等茶饮辅料原料,年度框架协议。',
remark: '已正常履约完毕',
},
{
contract_name: '2023年度冷链配送服务合同',
contract_no: 'MX-GH-2023-002', supplier_name: '山东齐鲁冷链物流有限公司', employee_name: '黄磊',
amount: 180000.00, effective_date: '2023-04-01', expiry_date: '2024-03-31',
status: '已完成', type: 'supply',
contract_content: '甲方委托乙方提供奶粉、椰浆等冷链原料的仓储和配送服务,覆盖华中和华东区域。',
remark: '服务评价良好',
},
]
for (const c of supplyContracts) {
const empId = empIdMap[c.employee_name]
await pool.query(
`INSERT INTO contracts (contract_name, contract_no, supplier_id, employee_id, amount, effective_date, expiry_date, status, type, contract_content, remark, responsible_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[c.contract_name, c.contract_no, supIdMap[c.supplier_name], empId,
c.amount, c.effective_date, c.expiry_date, c.status, c.type, c.contract_content, c.remark,
userByEmpIdMap[empId]]
)
}
console.log('[init] 已初始化 15 份合同10 份加盟 + 5 份供货)')
}
// ---- 5.6 售后单 (after_sales) ----
const [asCheck] = await pool.query('SELECT id FROM after_sales LIMIT 1')
if (asCheck.length === 0) {
const afterSales = [
{
customer_name: '赵鑫磊', employee_name: '郑伟',
feedback: '门店制冰机运行噪音突然变大,制冰速度明显下降,疑似压缩机故障。',
handle_method: '已安排售后工程师上门检修,确认为压缩机老化,已更换配件。',
handle_status: '已完成', service_date: '2024-11-15',
remark: '配件已更换,质保期内免费',
responsible_user: 'wangli',
},
{
customer_name: '孙大伟', employee_name: '郑伟',
feedback: '封口机经常卡杯每天至少出现5次严重影响出杯效率。',
handle_method: '远程指导清洁封口模具,如仍无法解决将安排上门维修。',
handle_status: '处理中', service_date: '2024-12-20',
remark: '已远程指导,待反馈效果',
responsible_user: 'wangli',
},
{
customer_name: '钱小燕', employee_name: '周杰',
feedback: '收到的珍珠粉圆有异味,疑似变质,要求退换货。',
handle_method: null,
handle_status: '待处理', service_date: '2025-01-05',
remark: '已联系供应商核实批次',
responsible_user: 'wangli',
},
{
customer_name: '周五六', employee_name: '周杰',
feedback: '新开店需要进行新品制作培训,包括冬季热饮系列和甜品系列。',
handle_method: '已安排运营专员下周到店进行3天驻店培训。',
handle_status: '处理中', service_date: '2025-01-10',
remark: '培训计划已发送',
responsible_user: 'wangli',
},
{
customer_name: '冯国强', employee_name: '吴敏',
feedback: '果糖机出糖量不稳定,有时偏多有时偏少,导致饮品口感不一致。',
handle_method: '已安排校准果糖机,更换流量计。',
handle_status: '已完成', service_date: '2024-10-28',
remark: '校准后恢复正常',
responsible_user: 'wangli',
},
{
customer_name: '李美玲', employee_name: '吴敏',
feedback: '新店开业前需要门店装修指导和设备布局方案。',
handle_method: '已派运营专员到店指导装修,提供标准门店布局图纸。',
handle_status: '已完成', service_date: '2024-01-05',
remark: '装修已完成,符合标准',
responsible_user: 'wangli',
},
{
customer_name: '吴建华', employee_name: '郑伟',
feedback: '冷藏展示柜温度显示异常实际温度与显示温度相差5度。',
handle_method: null,
handle_status: '待处理', service_date: '2025-01-08',
remark: '已安排就近工程师上门',
responsible_user: 'wangli',
},
{
customer_name: '陈小凤', employee_name: '周杰',
feedback: '希望总部提供暑期营销活动策划支持和宣传物料。',
handle_method: '已提供暑期活动方案和配套宣传海报、立牌设计稿。',
handle_status: '已完成', service_date: '2024-06-15',
remark: '活动期间营业额提升30%',
responsible_user: 'wangli',
},
{
customer_name: '王鹏飞', employee_name: '吴敏',
feedback: '草莓果酱批次口感偏酸,与之前供货品质不一致,学生客户投诉较多。',
handle_method: null,
handle_status: '待处理', service_date: '2025-01-12',
remark: '需要取样送检',
responsible_user: 'wangli',
},
{
customer_name: '郑雪梅', employee_name: '周杰',
feedback: '新店筹备中,需要开业前全套培训,包括设备操作、产品制作、门店管理等。',
handle_method: '已制定5天培训计划安排运营专员驻店指导开业。',
handle_status: '处理中', service_date: '2025-01-15',
remark: '预计2月初完成培训并开业',
responsible_user: 'wangli',
},
]
for (const a of afterSales) {
const empId = empIdMap[a.employee_name]
await pool.query(
`INSERT INTO after_sales (customer_id, employee_id, feedback, handle_method, handle_status, service_date, remark, responsible_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[custIdMap[a.customer_name], empId, a.feedback,
a.handle_method, a.handle_status, a.service_date, a.remark, userByEmpIdMap[empId]]
)
}
console.log('[init] 已初始化 10 条售后单')
}
// ---- 5.7 测试/预览用补充数据 ----
// 补充加盟商(分配给运营部用户,测试跨部门可见性)
const [custCheck2] = await pool.query("SELECT id FROM customers WHERE name = '测试加盟商-OPS' LIMIT 1")
if (custCheck2.length === 0) {
const testCustomers = [
{ name: '测试加盟商-OPS', phone: '13999990001', province: '河南省', city: '郑州市', district: '金水区', address: '测试路1号' },
{ name: '测试加盟商-PROC', phone: '13999990002', province: '山东省', city: '济南市', district: '历下区', address: '测试路2号' },
]
for (const c of testCustomers) {
await pool.query(
'INSERT INTO customers (name, phone, province, city, district, address) VALUES (?, ?, ?, ?, ?, ?)',
[c.name, c.phone, c.province, c.city, c.district, c.address]
)
}
console.log('[init] 已初始化 2 条测试加盟商')
// 补充一个采购合同(归属采购部,用于测试数据范围)
const [custOPS] = await pool.query("SELECT id FROM customers WHERE name = '测试加盟商-OPS' LIMIT 1")
const [empZhao] = await pool.query("SELECT id FROM employees WHERE name = '赵强' LIMIT 1")
if (custOPS.length > 0) {
await pool.query(
`INSERT INTO contracts (contract_name, contract_no, customer_id, employee_id, amount, effective_date, expiry_date, status, type, responsible_user_id, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
['测试采购合同-2025', 'MX-GH-TEST-001', custOPS[0].id, empZhao.length > 0 ? empZhao[0].id : null,
150000, '2025-03-01', '2026-02-28', '生效', 'supply',
empZhao.length > 0 ? (userByEmpIdMap[empZhao[0].id] || uidMap['zhaoqiang']) : null, '测试用采购合同']
)
}
// 补充售后单(归属运营部)
const [custA] = await pool.query("SELECT id FROM customers WHERE name = '赵鑫磊' LIMIT 1")
const [empZheng] = await pool.query("SELECT id FROM employees WHERE name = '郑伟' LIMIT 1")
if (custA.length > 0) {
const empZhengId = empZheng.length > 0 ? empZheng[0].id : null
await pool.query(
`INSERT INTO after_sales (customer_id, employee_id, feedback, handle_method, handle_status, service_date, remark, responsible_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[custA[0].id, empZhengId,
'测试售后-夏季冰块供应不足', '协调增加配送频次', '处理中', '2025-06-01', '旺季保障',
empZhengId ? userByEmpIdMap[empZhengId] : uidMap['wangli']]
)
}
console.log('[init] 已初始化测试用合同和售后单')
}
}
module.exports = { pool, initDB }

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 }

View File

@@ -241,7 +241,7 @@ async function update(req, res) {
let checkSql = 'SELECT id, employee_id FROM contracts WHERE id = ?'
let checkParams = [id]
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)
}
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 checkParams = [id]
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)
}
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: '员工姓名必填' })
}
// 检查是否是添加总经理办公室的员工
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 {
const [result] = await pool.query(
`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 要求同步创建用户账号
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 validRoleLevel = [1, 2, 3, 4].includes(Number(role_level)) ? Number(role_level) : 4
await pool.query(
'INSERT INTO users (username, password, is_active, department_id, employee_id) VALUES (?, ?, 1, ?, ?)',
[username, hash, department_id, result.insertId]
'INSERT INTO users (username, password, is_active, department_id, employee_id, role_level) VALUES (?, ?, 1, ?, ?, ?)',
[username, hash, department_id, result.insertId, validRoleLevel]
)
delete req._createUser // 清理,避免影响后续中间件
}
@@ -255,14 +277,23 @@ async function remove(req, res) {
// GET /api/employees/simple —— 简易员工列表(无数据范围限制,用于下拉选择)
async function simpleList(req, res) {
try {
const { department } = req.query
let sql = 'SELECT id, name, department, position FROM employees WHERE status = 1'
const { department, max_role_level } = req.query
// 关联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 = []
if (department) {
sql += ' AND department = ?'
sql += ' AND e.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)
res.json({ code: 0, message: 'ok', data: rows })
} catch (e) {

View File

@@ -5,7 +5,7 @@ const { pool } = require('../db')
// 安全字段:关联查询 departments 和 employees
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,
d.name AS dept_name, d.description AS dept_desc,
e.name AS real_name
@@ -32,7 +32,7 @@ async function login(req, res) {
try {
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
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
@@ -74,6 +74,7 @@ async function login(req, res) {
department_id: user.department_id,
departmentName: user.dept_name,
departmentDesc: user.dept_desc,
role_level: user.role_level || 4,
permissions,
},
process.env.JWT_SECRET,
@@ -93,6 +94,7 @@ async function login(req, res) {
department_id: user.department_id,
departmentName: user.dept_name,
departmentDesc: user.dept_desc,
role_level: user.role_level || 4,
permissions,
},
},
@@ -253,7 +255,7 @@ async function detail(req, res) {
// POST /api/users —— 创建用户
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) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
@@ -267,10 +269,31 @@ async function create(req, res) {
// 校验 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) {
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 是否存在
@@ -283,9 +306,11 @@ async function create(req, res) {
try {
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(
'INSERT INTO users (username, password, is_active, department_id, employee_id) VALUES (?, ?, ?, ?, ?)',
[username, hash, is_active, department_id || null, employee_id || null]
'INSERT INTO users (username, password, is_active, department_id, employee_id, role_level) VALUES (?, ?, ?, ?, ?, ?)',
[username, hash, is_active, department_id || null, employee_id || null, validRoleLevel]
)
const [rows] = await pool.query(
@@ -305,7 +330,7 @@ async function create(req, res) {
// PUT /api/users/:id —— 更新用户
async function update(req, res) {
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 {
const [existing] = await pool.query('SELECT id, department_id, is_active, employee_id FROM users WHERE id = ?', [id])

View File

@@ -13,7 +13,7 @@ const products = require('./routes/products')
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 { 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/:id', auth, checkPermission('user', 'manage'), users.detail)
app.post('/api/users', auth, checkPermission('user', 'manage'), users.create)
app.put('/api/users/:id', auth, checkPermission('user', 'manage'), protectSelfUpdate, users.update)
app.delete('/api/users/:id', auth, checkPermission('user', 'manage'), protectUserDelete, users.remove)
app.put('/api/users/:id', auth, checkPermission('user', 'manage'), checkHierarchy, protectSelfUpdate, users.update)
app.delete('/api/users/:id', auth, checkPermission('user', 'manage'), checkHierarchy, protectUserDelete, users.remove)
// ============ 加盟商管理 /api/customers ============
app.get('/api/customers/simple', auth, checkPermission('customer', 'read'), customers.simpleList)