添加了更多功能
This commit is contained in:
44
CLAUDE.md
Normal file
44
CLAUDE.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Enterprise information management system backend (企业信息管理系统后端服务). Node.js REST API built on Express 5 with raw MySQL queries (no ORM). Provides CRUD APIs for users, customers (franchisees), employees, contracts, after-sales services, and products/inventory.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `npm run dev` — start dev server with auto-restart (`node --watch server.js`)
|
||||||
|
- `npm start` — start production server (`node server.js`)
|
||||||
|
- `node test-all.js` — run integration tests (server must be running on port 3000 first; uses Node's built-in `http` module, no test framework)
|
||||||
|
|
||||||
|
No build step, no linter, no formatter configured.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Flat structure** — no controllers/services/layers. Entry point `server.js` directly imports route handlers and registers them on the Express app.
|
||||||
|
|
||||||
|
- `server.js` — Express app setup, JWT `auth` middleware, `requireAdmin` middleware, all route registration, server startup
|
||||||
|
- `db.js` — MySQL connection pool (`mysql2/promise`), auto-initialization on startup (creates database, tables, seeds default admin `admin`/`123456`)
|
||||||
|
- `routes/*.js` — each exports `{ list, detail, create, update, remove }` functions for a single resource
|
||||||
|
|
||||||
|
**Auth flow**: JWT in `Authorization: Bearer <token>` header. Two middleware layers in `server.js`: `auth` (verifies token, attaches `req.user`) and `requireAdmin` (checks `req.user.role === 'admin'`).
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
MySQL 5.7+ / MariaDB 10.2+. Raw SQL with `pool.query()` and `?` parameterized placeholders. No foreign key constraints — referential integrity is enforced in application code by checking entity existence before inserts/updates.
|
||||||
|
|
||||||
|
Tables: `users`, `customers`, `employees`, `products`, `contracts`, `after_sales`
|
||||||
|
|
||||||
|
Relationships: contracts and after_sales reference customers and employees by ID, with `LEFT JOIN` in list/detail queries to include related names.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **API response format**: `{ "code": 0, "message": "ok", "data": { ... } }` (errors use non-zero `code` and descriptive `message`)
|
||||||
|
- **List endpoints** return `{ list, total, page, pageSize, totalPages }` with pagination (`page`, `pageSize` params, max 100)
|
||||||
|
- **Route modules** use dynamic `WHERE 1=1` clause building for search/filter, and dynamic `SET` clause building for partial updates
|
||||||
|
- **Environment config**: dotenv via `.env` file (see `.env.example` for required variables: `PORT`, `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`, `JWT_SECRET`, `JWT_EXPIRES_IN`)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
express, mysql2, jsonwebtoken, bcryptjs, dotenv
|
||||||
239
db.js
239
db.js
@@ -39,9 +39,10 @@ async function initDB() {
|
|||||||
id INT NOT NULL AUTO_INCREMENT COMMENT '用户ID (主键)',
|
id INT NOT NULL AUTO_INCREMENT COMMENT '用户ID (主键)',
|
||||||
username VARCHAR(50) NOT NULL COMMENT '用户名 (登录账号)',
|
username VARCHAR(50) NOT NULL COMMENT '用户名 (登录账号)',
|
||||||
password VARCHAR(255) NOT NULL COMMENT '登录密码 (应存储加密后的值)',
|
password VARCHAR(255) NOT NULL COMMENT '登录密码 (应存储加密后的值)',
|
||||||
real_name VARCHAR(50) DEFAULT NULL COMMENT '真实姓名',
|
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
|
||||||
role VARCHAR(20) NOT NULL DEFAULT 'user' COMMENT '角色: admin-管理员, user-普通用户',
|
role_id INT DEFAULT NULL COMMENT '角色ID (关联roles表)',
|
||||||
status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
|
employee_id INT DEFAULT NULL COMMENT '员工ID (关联employees表)',
|
||||||
|
department VARCHAR(100) DEFAULT NULL COMMENT '所属部门 (从employees同步的冗余字段)',
|
||||||
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),
|
||||||
@@ -152,20 +153,228 @@ async function initDB() {
|
|||||||
`)
|
`)
|
||||||
console.log('[init] 数据表初始化完成')
|
console.log('[init] 数据表初始化完成')
|
||||||
|
|
||||||
// 2.3 插 admin(仅当不存在时)
|
// ============ 2.3 RBAC 权限相关表 ============
|
||||||
const [rows] = await pool.query(
|
await pool.query(`
|
||||||
'SELECT id FROM users WHERE username = ?',
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
['admin']
|
id INT NOT NULL AUTO_INCREMENT COMMENT '角色ID (主键)',
|
||||||
)
|
name VARCHAR(50) NOT NULL COMMENT '角色标识 (英文)',
|
||||||
if (rows.length === 0) {
|
description VARCHAR(200) DEFAULT NULL COMMENT '角色描述 (中文)',
|
||||||
const hash = await bcrypt.hash('123456', 10)
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_roles_name (name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS permissions (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT COMMENT '权限ID (主键)',
|
||||||
|
name VARCHAR(100) NOT NULL COMMENT '权限标识 (resource:action)',
|
||||||
|
description VARCHAR(200) DEFAULT NULL COMMENT '权限描述 (中文)',
|
||||||
|
resource VARCHAR(50) NOT NULL COMMENT '资源名称',
|
||||||
|
action VARCHAR(50) NOT NULL COMMENT '操作类型: read/create/update/delete',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_permissions_name (name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||||
|
role_id INT NOT NULL COMMENT '角色ID',
|
||||||
|
permission_id INT NOT NULL COMMENT '权限ID',
|
||||||
|
PRIMARY KEY (role_id, permission_id),
|
||||||
|
KEY idx_rp_permission_id (permission_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-权限关联表'
|
||||||
|
`)
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS suppliers (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT COMMENT '供应商ID (主键)',
|
||||||
|
name VARCHAR(200) NOT NULL COMMENT '供应商名称',
|
||||||
|
contact VARCHAR(100) DEFAULT NULL COMMENT '联系人',
|
||||||
|
phone VARCHAR(50) DEFAULT NULL COMMENT '联系电话',
|
||||||
|
address VARCHAR(500) DEFAULT NULL COMMENT '地址',
|
||||||
|
type VARCHAR(50) DEFAULT NULL COMMENT '类型: 原材料/包装/设备',
|
||||||
|
content TEXT DEFAULT NULL COMMENT '备注说明',
|
||||||
|
status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态: 0-停用, 1-正常',
|
||||||
|
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),
|
||||||
|
KEY idx_suppliers_name (name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商信息表'
|
||||||
|
`)
|
||||||
|
|
||||||
|
// ============ 2.4 旧表迁移(兼容已有数据库) ============
|
||||||
|
// users 表:删除 real_name、role,新增 is_active、role_id、employee_id、department
|
||||||
|
try { await pool.query(`ALTER TABLE users DROP COLUMN real_name`) } catch {}
|
||||||
|
try { await pool.query(`ALTER TABLE users DROP COLUMN role`) } catch {}
|
||||||
|
try { await pool.query(`ALTER TABLE users CHANGE COLUMN status is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用'`) } catch {}
|
||||||
|
try { await pool.query(`ALTER TABLE users ADD COLUMN role_id INT DEFAULT NULL COMMENT '角色ID' AFTER is_active`) } catch {}
|
||||||
|
try { await pool.query(`ALTER TABLE users ADD COLUMN employee_id INT DEFAULT NULL COMMENT '员工ID' AFTER role_id`) } catch {}
|
||||||
|
try { await pool.query(`ALTER TABLE users ADD COLUMN department VARCHAR(100) DEFAULT NULL COMMENT '所属部门' AFTER employee_id`) } catch {}
|
||||||
|
|
||||||
|
// customers 表:新增 responsible_user_id
|
||||||
|
try { await pool.query(`ALTER TABLE customers ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {}
|
||||||
|
|
||||||
|
// contracts 表:新增 type
|
||||||
|
try { await pool.query(`ALTER TABLE contracts ADD COLUMN type VARCHAR(20) DEFAULT 'franchise' COMMENT '合同类型: franchise-加盟合同, supply-采购合同'`) } catch {}
|
||||||
|
// contracts 表:新增 responsible_user_id(用于数据范围隔离)
|
||||||
|
try { await pool.query(`ALTER TABLE contracts ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {}
|
||||||
|
|
||||||
|
// after_sales 表:新增 responsible_user_id(用于数据范围隔离)
|
||||||
|
try { await pool.query(`ALTER TABLE after_sales ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {}
|
||||||
|
|
||||||
|
// ============ 2.5 种子数据 ============
|
||||||
|
await seedRolesAndPermissions()
|
||||||
|
await seedDefaultUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 3. 种子数据:角色与权限 ============
|
||||||
|
async function seedRolesAndPermissions() {
|
||||||
|
const [roles] = await pool.query('SELECT id FROM roles LIMIT 1')
|
||||||
|
if (roles.length > 0) return // 已初始化过,跳过
|
||||||
|
|
||||||
|
const roleList = [
|
||||||
|
{ name: 'admin', description: '系统管理员' },
|
||||||
|
{ name: 'general_manager', description: '总经理' },
|
||||||
|
{ name: 'franchise_manager', description: '招商经理' },
|
||||||
|
{ name: 'operations_manager', description: '运营经理' },
|
||||||
|
{ name: 'procurement_manager', description: '采购经理' },
|
||||||
|
{ name: 'finance', description: '财务人员' },
|
||||||
|
]
|
||||||
|
for (const r of roleList) {
|
||||||
|
await pool.query('INSERT INTO roles (name, description) VALUES (?, ?)', [r.name, r.description])
|
||||||
|
}
|
||||||
|
console.log('[init] 已初始化 6 个角色')
|
||||||
|
|
||||||
|
const permissionList = [
|
||||||
|
{ name: 'customer:read', description: '查看加盟商', resource: 'customer', action: 'read' },
|
||||||
|
{ name: 'customer:create', description: '新增加盟商', resource: 'customer', action: 'create' },
|
||||||
|
{ name: 'customer:update', description: '修改加盟商', resource: 'customer', action: 'update' },
|
||||||
|
{ name: 'customer:delete', description: '删除加盟商', resource: 'customer', action: 'delete' },
|
||||||
|
{ name: 'contract:read', description: '查看合同', resource: 'contract', action: 'read' },
|
||||||
|
{ name: 'contract:create', description: '新增合同', resource: 'contract', action: 'create' },
|
||||||
|
{ name: 'contract:update', description: '修改合同', resource: 'contract', action: 'update' },
|
||||||
|
{ name: 'contract:delete', description: '删除合同', resource: 'contract', action: 'delete' },
|
||||||
|
{ name: 'after_sale:read', description: '查看售后', resource: 'after_sale', action: 'read' },
|
||||||
|
{ name: 'after_sale:create',description: '新增售后', resource: 'after_sale', action: 'create' },
|
||||||
|
{ name: 'after_sale:update',description: '修改售后', resource: 'after_sale', action: 'update' },
|
||||||
|
{ name: 'after_sale:delete',description: '删除售后', resource: 'after_sale', action: 'delete' },
|
||||||
|
{ name: 'product:read', description: '查看产品原料', resource: 'product', action: 'read' },
|
||||||
|
{ name: 'product:create', description: '新增产品原料', resource: 'product', action: 'create' },
|
||||||
|
{ name: 'product:update', description: '修改产品原料', resource: 'product', action: 'update' },
|
||||||
|
{ name: 'product:delete', description: '删除产品原料', resource: 'product', action: 'delete' },
|
||||||
|
{ name: 'supplier:read', description: '查看供应商', resource: 'supplier', action: 'read' },
|
||||||
|
{ name: 'supplier:create', description: '新增供应商', resource: 'supplier', action: 'create' },
|
||||||
|
{ name: 'supplier:update', description: '修改供应商', resource: 'supplier', action: 'update' },
|
||||||
|
{ name: 'supplier:delete', description: '删除供应商', resource: 'supplier', action: 'delete' },
|
||||||
|
{ name: 'employee:read', description: '查看员工', resource: 'employee', action: 'read' },
|
||||||
|
{ name: 'employee:create', description: '新增员工', resource: 'employee', action: 'create' },
|
||||||
|
{ name: 'employee:update', description: '修改员工', resource: 'employee', action: 'update' },
|
||||||
|
{ name: 'employee:delete', description: '删除员工', resource: 'employee', action: 'delete' },
|
||||||
|
{ name: 'user:manage', description: '管理用户账号', resource: 'user', action: 'manage' },
|
||||||
|
]
|
||||||
|
for (const p of permissionList) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'INSERT INTO users (username, password, real_name, role) VALUES (?, ?, ?, ?)',
|
'INSERT INTO permissions (name, description, resource, action) VALUES (?, ?, ?, ?)',
|
||||||
['admin', hash, '超级管理员', 'admin']
|
[p.name, p.description, p.resource, p.action]
|
||||||
)
|
)
|
||||||
console.log('[init] 已创建默认账号 admin / 123456')
|
}
|
||||||
} else {
|
console.log('[init] 已初始化 25 个权限')
|
||||||
console.log('[init] admin 已存在,跳过')
|
|
||||||
|
// 角色-权限映射
|
||||||
|
const allPerms = permissionList.map(p => p.name)
|
||||||
|
const rolePermMap = {
|
||||||
|
admin: allPerms, // 管理员拥有所有权限
|
||||||
|
general_manager: [
|
||||||
|
'customer:read', 'contract:read', 'after_sale:read',
|
||||||
|
'product:read', 'supplier:read', 'employee:read',
|
||||||
|
],
|
||||||
|
franchise_manager: [
|
||||||
|
'customer:read', 'customer:create', 'customer:update', 'customer:delete',
|
||||||
|
'contract:read', 'contract:create', 'contract:update', 'contract:delete',
|
||||||
|
'employee:read',
|
||||||
|
],
|
||||||
|
operations_manager: [
|
||||||
|
'customer:read', 'customer:update',
|
||||||
|
'contract:read',
|
||||||
|
'after_sale:read', 'after_sale:create', 'after_sale:update', 'after_sale:delete',
|
||||||
|
'employee:read',
|
||||||
|
],
|
||||||
|
procurement_manager: [
|
||||||
|
'contract:read', 'contract:create', 'contract:update', 'contract:delete',
|
||||||
|
'product:read', 'product:create', 'product:update', 'product:delete',
|
||||||
|
'supplier:read', 'supplier:create', 'supplier:update', 'supplier:delete',
|
||||||
|
'employee:read',
|
||||||
|
],
|
||||||
|
finance: [
|
||||||
|
'customer:read', 'contract:read', 'after_sale:read', 'employee:read',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const [roleRows] = await pool.query('SELECT id, name FROM roles')
|
||||||
|
const [permRows] = await pool.query('SELECT id, name FROM permissions')
|
||||||
|
const roleIdMap = Object.fromEntries(roleRows.map(r => [r.name, r.id]))
|
||||||
|
const permIdMap = Object.fromEntries(permRows.map(p => [p.name, p.id]))
|
||||||
|
|
||||||
|
for (const [roleName, permNames] of Object.entries(rolePermMap)) {
|
||||||
|
for (const permName of permNames) {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)',
|
||||||
|
[roleIdMap[roleName], permIdMap[permName]]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('[init] 已初始化角色-权限映射')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 4. 种子数据:默认用户 ============
|
||||||
|
async function seedDefaultUsers() {
|
||||||
|
const hash = await bcrypt.hash('123456', 10)
|
||||||
|
|
||||||
|
const [roleRows] = await pool.query('SELECT id, name FROM roles')
|
||||||
|
const roleIdMap = Object.fromEntries(roleRows.map(r => [r.name, r.id]))
|
||||||
|
|
||||||
|
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' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const seedUsers = [
|
||||||
|
{ username: 'zhangchao', roleName: 'general_manager' },
|
||||||
|
{ username: 'liming', roleName: 'franchise_manager' },
|
||||||
|
{ username: 'wangli', roleName: 'operations_manager' },
|
||||||
|
{ username: 'zhaoqiang', roleName: 'procurement_manager' },
|
||||||
|
{ username: 'chenfang', roleName: 'finance' },
|
||||||
|
{ username: 'admin', roleName: 'admin' },
|
||||||
|
]
|
||||||
|
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
const empId = empResult.insertId
|
||||||
|
|
||||||
|
// 再插入 users,关联 employee 和 role
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO users (username, password, is_active, role_id, employee_id, department)
|
||||||
|
VALUES (?, ?, 1, ?, ?, ?)`,
|
||||||
|
[usr.username, hash, roleIdMap[usr.roleName], empId, emp.department]
|
||||||
|
)
|
||||||
|
console.log(`[init] 已创建用户 ${usr.username} (${emp.name} - ${emp.department})`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
73
middleware/permissions.js
Normal file
73
middleware/permissions.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// middleware/permissions.js —— RBAC 权限检查 + 数据范围过滤
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查当前用户是否拥有指定权限
|
||||||
|
* @param {string} resource - 资源名,如 'customer'
|
||||||
|
* @param {string} action - 操作名,如 'read'
|
||||||
|
*/
|
||||||
|
function checkPermission(resource, action) {
|
||||||
|
return (req, res, next) => {
|
||||||
|
const permName = `${resource}:${action}`
|
||||||
|
if (!req.user || !req.user.permissions || !req.user.permissions.includes(permName)) {
|
||||||
|
return res.status(403).json({ code: 403, message: '权限不足' })
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前用户角色,返回该资源的数据范围过滤条件
|
||||||
|
* @param {object} user - req.user(含 roleName, id, department)
|
||||||
|
* @param {string} resource - 资源名:customers / contracts / after_sales / products / suppliers / employees / users
|
||||||
|
* @returns {{ where?: string, values?: any[], deny?: boolean }}
|
||||||
|
* - where + values:追加到 SQL WHERE 子句的条件
|
||||||
|
* - deny:true 表示该角色无权访问此资源
|
||||||
|
*/
|
||||||
|
function getDataScope(user, resource) {
|
||||||
|
const { roleName, id: userId, department } = user
|
||||||
|
|
||||||
|
// admin 和 general_manager(总经理):全量数据,无限制
|
||||||
|
if (roleName === 'admin' || roleName === 'general_manager') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (resource) {
|
||||||
|
case 'customers':
|
||||||
|
if (roleName === 'finance') return {}
|
||||||
|
return { where: 'responsible_user_id = ?', values: [userId] }
|
||||||
|
|
||||||
|
case 'contracts':
|
||||||
|
if (roleName === 'finance') return {}
|
||||||
|
if (roleName === 'procurement_manager') {
|
||||||
|
return { where: 'c.type = ?', values: ['supply'] }
|
||||||
|
}
|
||||||
|
if (roleName === 'franchise_manager' || roleName === 'operations_manager') {
|
||||||
|
return { where: 'c.responsible_user_id = ? AND c.type = ?', values: [userId, 'franchise'] }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
case 'after_sales':
|
||||||
|
if (roleName === 'finance') return {}
|
||||||
|
if (roleName === 'operations_manager') {
|
||||||
|
return { where: 'responsible_user_id = ?', values: [userId], tableAlias: 'a' }
|
||||||
|
}
|
||||||
|
// franchise_manager 和 procurement_manager 无权限(checkPermission 已拦截)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
case 'employees':
|
||||||
|
if (roleName === 'finance') return {}
|
||||||
|
// 所有非管理员/总经理角色:只能看自己部门的员工
|
||||||
|
return { where: 'department = ?', values: [department] }
|
||||||
|
|
||||||
|
case 'products':
|
||||||
|
case 'suppliers':
|
||||||
|
case 'users':
|
||||||
|
// 无数据范围过滤,仅靠 checkPermission 控制访问
|
||||||
|
return {}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { checkPermission, getDataScope }
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// routes/afterSales.js —— 售后管理 CRUD
|
// routes/afterSales.js —— 售后管理 CRUD
|
||||||
const { pool } = require('../db')
|
const { pool } = require('../db')
|
||||||
|
const { getDataScope } = require('../middleware/permissions')
|
||||||
|
|
||||||
function pagination(query) {
|
function pagination(query) {
|
||||||
const page = Math.max(Number(query.page) || 1, 1)
|
const page = Math.max(Number(query.page) || 1, 1)
|
||||||
@@ -27,6 +28,18 @@ async function list(req, res) {
|
|||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
|
// 数据范围过滤
|
||||||
|
const scope = getDataScope(req.user, 'after_sales')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
// list 查询有 JOIN,需要表别名前缀避免列名歧义
|
||||||
|
const scopeCol = scope.tableAlias ? scope.where.replace('responsible_user_id', `${scope.tableAlias}.responsible_user_id`) : scope.where
|
||||||
|
where += ' AND ' + scopeCol
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
if (handle_status) {
|
if (handle_status) {
|
||||||
where += ' AND a.handle_status = ?'
|
where += ' AND a.handle_status = ?'
|
||||||
params.push(handle_status)
|
params.push(handle_status)
|
||||||
@@ -69,10 +82,20 @@ async function list(req, res) {
|
|||||||
// GET /api/after-sales/:id —— 详情
|
// GET /api/after-sales/:id —— 详情
|
||||||
async function detail(req, res) {
|
async function detail(req, res) {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
let sql = `${LIST_SELECT} WHERE a.id = ?`
|
||||||
`${LIST_SELECT} WHERE a.id = ?`,
|
const params = [req.params.id]
|
||||||
[req.params.id]
|
|
||||||
)
|
const scope = getDataScope(req.user, 'after_sales')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
const scopeCol = scope.tableAlias ? scope.where.replace('responsible_user_id', `${scope.tableAlias}.responsible_user_id`) : scope.where
|
||||||
|
sql += ' AND ' + scopeCol
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await pool.query(sql, params)
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '售后记录不存在' })
|
return res.status(404).json({ code: 404, message: '售后记录不存在' })
|
||||||
}
|
}
|
||||||
@@ -87,7 +110,7 @@ async function detail(req, res) {
|
|||||||
async function create(req, res) {
|
async function create(req, res) {
|
||||||
const {
|
const {
|
||||||
customer_id, feedback, employee_id, handle_method,
|
customer_id, feedback, employee_id, handle_method,
|
||||||
handle_status, service_date, remark,
|
handle_status, service_date, remark, responsible_user_id,
|
||||||
} = req.body || {}
|
} = req.body || {}
|
||||||
|
|
||||||
if (!customer_id) {
|
if (!customer_id) {
|
||||||
@@ -111,16 +134,20 @@ async function create(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没指定负责人,默认设为当前用户
|
||||||
|
const ownerId = responsible_user_id || req.user.id
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO after_sales
|
`INSERT INTO after_sales
|
||||||
(customer_id, feedback, employee_id, handle_method, handle_status, service_date, remark)
|
(customer_id, feedback, employee_id, handle_method, handle_status, service_date, remark, responsible_user_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[customer_id, feedback,
|
[customer_id, feedback,
|
||||||
employee_id || null,
|
employee_id || null,
|
||||||
handle_method || null,
|
handle_method || null,
|
||||||
handle_status || '待处理',
|
handle_status || '待处理',
|
||||||
service_date || null,
|
service_date || null,
|
||||||
remark || null]
|
remark || null,
|
||||||
|
ownerId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [rows] = await pool.query(`${LIST_SELECT} WHERE a.id = ?`, [result.insertId])
|
const [rows] = await pool.query(`${LIST_SELECT} WHERE a.id = ?`, [result.insertId])
|
||||||
@@ -136,13 +163,24 @@ async function update(req, res) {
|
|||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const fields = [
|
const fields = [
|
||||||
'customer_id', 'feedback', 'employee_id', 'handle_method',
|
'customer_id', 'feedback', 'employee_id', 'handle_method',
|
||||||
'handle_status', 'service_date', 'remark',
|
'handle_status', 'service_date', 'remark', 'responsible_user_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM after_sales WHERE id = ?', [id])
|
// 确认记录存在且在数据范围内
|
||||||
|
let checkSql = 'SELECT id FROM after_sales WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'after_sales')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '售后记录不存在' })
|
return res.status(404).json({ code: 404, message: '售后记录不存在或无权操作' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.body.customer_id) {
|
if (req.body.customer_id) {
|
||||||
@@ -185,9 +223,19 @@ async function update(req, res) {
|
|||||||
async function remove(req, res) {
|
async function remove(req, res) {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM after_sales WHERE id = ?', [id])
|
let checkSql = 'SELECT id FROM after_sales WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'after_sales')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '售后记录不存在' })
|
return res.status(404).json({ code: 404, message: '售后记录不存在或无权操作' })
|
||||||
}
|
}
|
||||||
await pool.query('DELETE FROM after_sales WHERE id = ?', [id])
|
await pool.query('DELETE FROM after_sales WHERE id = ?', [id])
|
||||||
res.json({ code: 0, message: 'ok' })
|
res.json({ code: 0, message: 'ok' })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// routes/contracts.js —— 合同管理 CRUD
|
// routes/contracts.js —— 合同管理 CRUD
|
||||||
const { pool } = require('../db')
|
const { pool } = require('../db')
|
||||||
|
const { getDataScope } = require('../middleware/permissions')
|
||||||
|
|
||||||
function pagination(query) {
|
function pagination(query) {
|
||||||
const page = Math.max(Number(query.page) || 1, 1)
|
const page = Math.max(Number(query.page) || 1, 1)
|
||||||
@@ -31,6 +32,16 @@ async function list(req, res) {
|
|||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
|
// 数据范围过滤
|
||||||
|
const scope = getDataScope(req.user, 'contracts')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
where += ' AND ' + scope.where
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
where += ' AND c.status = ?'
|
where += ' AND c.status = ?'
|
||||||
params.push(status)
|
params.push(status)
|
||||||
@@ -81,10 +92,19 @@ async function list(req, res) {
|
|||||||
// GET /api/contracts/:id —— 详情
|
// GET /api/contracts/:id —— 详情
|
||||||
async function detail(req, res) {
|
async function detail(req, res) {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
let sql = `${DETAIL_SELECT} WHERE c.id = ?`
|
||||||
`${DETAIL_SELECT} WHERE c.id = ?`,
|
const params = [req.params.id]
|
||||||
[req.params.id]
|
|
||||||
)
|
const scope = getDataScope(req.user, 'contracts')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
sql += ' AND ' + scope.where
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await pool.query(sql, params)
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '合同不存在' })
|
return res.status(404).json({ code: 404, message: '合同不存在' })
|
||||||
}
|
}
|
||||||
@@ -125,8 +145,8 @@ async function create(req, res) {
|
|||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO contracts
|
`INSERT INTO contracts
|
||||||
(customer_id, contract_name, contract_no, contract_content, amount, effective_date, expiry_date, employee_id, status, remark)
|
(customer_id, contract_name, contract_no, contract_content, amount, effective_date, expiry_date, employee_id, status, remark, type, responsible_user_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[customer_id, contract_name,
|
[customer_id, contract_name,
|
||||||
contract_no || null,
|
contract_no || null,
|
||||||
contract_content || null,
|
contract_content || null,
|
||||||
@@ -135,7 +155,9 @@ async function create(req, res) {
|
|||||||
expiry_date || null,
|
expiry_date || null,
|
||||||
employee_id || null,
|
employee_id || null,
|
||||||
status || '生效',
|
status || '生效',
|
||||||
remark || null]
|
remark || null,
|
||||||
|
req.body.type || (req.user.roleName === 'procurement_manager' ? 'supply' : 'franchise'),
|
||||||
|
req.body.responsible_user_id || req.user.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [rows] = await pool.query(`${DETAIL_SELECT} WHERE c.id = ?`, [result.insertId])
|
const [rows] = await pool.query(`${DETAIL_SELECT} WHERE c.id = ?`, [result.insertId])
|
||||||
@@ -151,13 +173,24 @@ async function update(req, res) {
|
|||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const fields = [
|
const fields = [
|
||||||
'customer_id', 'contract_name', 'contract_no', 'contract_content',
|
'customer_id', 'contract_name', 'contract_no', 'contract_content',
|
||||||
'amount', 'effective_date', 'expiry_date', 'employee_id', 'status', 'remark',
|
'amount', 'effective_date', 'expiry_date', 'employee_id', 'status', 'remark', 'type', 'responsible_user_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM contracts WHERE id = ?', [id])
|
// 确认记录存在且在数据范围内
|
||||||
|
let checkSql = 'SELECT id FROM contracts WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'contracts')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '合同不存在' })
|
return res.status(404).json({ code: 404, message: '合同不存在或无权操作' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果更新了 customer_id 或 employee_id,需要校验
|
// 如果更新了 customer_id 或 employee_id,需要校验
|
||||||
@@ -201,9 +234,19 @@ async function update(req, res) {
|
|||||||
async function remove(req, res) {
|
async function remove(req, res) {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM contracts WHERE id = ?', [id])
|
let checkSql = 'SELECT id FROM contracts WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'contracts')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '合同不存在' })
|
return res.status(404).json({ code: 404, message: '合同不存在或无权操作' })
|
||||||
}
|
}
|
||||||
await pool.query('DELETE FROM contracts WHERE id = ?', [id])
|
await pool.query('DELETE FROM contracts WHERE id = ?', [id])
|
||||||
res.json({ code: 0, message: 'ok' })
|
res.json({ code: 0, message: 'ok' })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// routes/customers.js —— 客户管理 CRUD
|
// routes/customers.js —— 客户管理 CRUD
|
||||||
const { pool } = require('../db')
|
const { pool } = require('../db')
|
||||||
|
const { getDataScope } = require('../middleware/permissions')
|
||||||
|
|
||||||
// 提取分页参数
|
// 提取分页参数
|
||||||
function pagination(query) {
|
function pagination(query) {
|
||||||
@@ -18,6 +19,16 @@ async function list(req, res) {
|
|||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
|
// 数据范围过滤
|
||||||
|
const scope = getDataScope(req.user, 'customers')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
where += ' AND ' + scope.where
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
where += ' AND name LIKE ?'
|
where += ' AND name LIKE ?'
|
||||||
params.push(`%${name}%`)
|
params.push(`%${name}%`)
|
||||||
@@ -71,7 +82,19 @@ async function list(req, res) {
|
|||||||
// GET /api/customers/:id —— 详情
|
// GET /api/customers/:id —— 详情
|
||||||
async function detail(req, res) {
|
async function detail(req, res) {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM customers WHERE id = ?', [req.params.id])
|
let sql = 'SELECT * FROM customers WHERE id = ?'
|
||||||
|
const params = [req.params.id]
|
||||||
|
|
||||||
|
const scope = getDataScope(req.user, 'customers')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
sql += ' AND ' + scope.where
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await pool.query(sql, params)
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '客户不存在' })
|
return res.status(404).json({ code: 404, message: '客户不存在' })
|
||||||
}
|
}
|
||||||
@@ -86,7 +109,7 @@ async function detail(req, res) {
|
|||||||
async function create(req, res) {
|
async function create(req, res) {
|
||||||
const {
|
const {
|
||||||
name, phone, province, city, district,
|
name, phone, province, city, district,
|
||||||
address, customer_type, email, remark,
|
address, customer_type, email, remark, responsible_user_id,
|
||||||
} = req.body || {}
|
} = req.body || {}
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -94,11 +117,14 @@ async function create(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 如果没指定负责人,默认设为当前用户
|
||||||
|
const ownerId = responsible_user_id || req.user.id
|
||||||
|
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`INSERT INTO customers (name, phone, province, city, district, address, customer_type, email, remark)
|
`INSERT INTO customers (name, phone, province, city, district, address, customer_type, email, remark, responsible_user_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[name, phone || null, province || null, city || null, district || null,
|
[name, phone || null, province || null, city || null, district || null,
|
||||||
address || null, customer_type || 'Normal', email || null, remark || null]
|
address || null, customer_type || 'Normal', email || null, remark || null, ownerId]
|
||||||
)
|
)
|
||||||
const [rows] = await pool.query('SELECT * FROM customers WHERE id = ?', [result.insertId])
|
const [rows] = await pool.query('SELECT * FROM customers WHERE id = ?', [result.insertId])
|
||||||
res.json({ code: 0, message: 'ok', data: rows[0] })
|
res.json({ code: 0, message: 'ok', data: rows[0] })
|
||||||
@@ -113,14 +139,24 @@ async function update(req, res) {
|
|||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const fields = [
|
const fields = [
|
||||||
'name', 'phone', 'province', 'city', 'district',
|
'name', 'phone', 'province', 'city', 'district',
|
||||||
'address', 'customer_type', 'email', 'remark',
|
'address', 'customer_type', 'email', 'remark', 'responsible_user_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先确认记录存在
|
// 确认记录存在且在数据范围内
|
||||||
const [existing] = await pool.query('SELECT id FROM customers WHERE id = ?', [id])
|
let checkSql = 'SELECT id FROM customers WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'customers')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '客户不存在' })
|
return res.status(404).json({ code: 404, message: '客户不存在或无权操作' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态构建 SET 子句(只更新传入的字段)
|
// 动态构建 SET 子句(只更新传入的字段)
|
||||||
@@ -151,9 +187,19 @@ async function update(req, res) {
|
|||||||
async function remove(req, res) {
|
async function remove(req, res) {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM customers WHERE id = ?', [id])
|
let checkSql = 'SELECT id FROM customers WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'customers')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '客户不存在' })
|
return res.status(404).json({ code: 404, message: '客户不存在或无权操作' })
|
||||||
}
|
}
|
||||||
await pool.query('DELETE FROM customers WHERE id = ?', [id])
|
await pool.query('DELETE FROM customers WHERE id = ?', [id])
|
||||||
res.json({ code: 0, message: 'ok' })
|
res.json({ code: 0, message: 'ok' })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// routes/employees.js —— 员工管理 CRUD
|
// routes/employees.js —— 员工管理 CRUD
|
||||||
const { pool } = require('../db')
|
const { pool } = require('../db')
|
||||||
|
const { getDataScope } = require('../middleware/permissions')
|
||||||
|
|
||||||
function pagination(query) {
|
function pagination(query) {
|
||||||
const page = Math.max(Number(query.page) || 1, 1)
|
const page = Math.max(Number(query.page) || 1, 1)
|
||||||
@@ -17,6 +18,16 @@ async function list(req, res) {
|
|||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
|
// 数据范围过滤(部门隔离)
|
||||||
|
const scope = getDataScope(req.user, 'employees')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
where += ' AND ' + scope.where
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
where += ' AND name LIKE ?'
|
where += ' AND name LIKE ?'
|
||||||
params.push(`%${name}%`)
|
params.push(`%${name}%`)
|
||||||
@@ -60,7 +71,19 @@ async function list(req, res) {
|
|||||||
// GET /api/employees/:id —— 详情
|
// GET /api/employees/:id —— 详情
|
||||||
async function detail(req, res) {
|
async function detail(req, res) {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query('SELECT * FROM employees WHERE id = ?', [req.params.id])
|
let sql = 'SELECT * FROM employees WHERE id = ?'
|
||||||
|
const params = [req.params.id]
|
||||||
|
|
||||||
|
const scope = getDataScope(req.user, 'employees')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
sql += ' AND ' + scope.where
|
||||||
|
params.push(...scope.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rows] = await pool.query(sql, params)
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '员工不存在' })
|
return res.status(404).json({ code: 404, message: '员工不存在' })
|
||||||
}
|
}
|
||||||
@@ -116,9 +139,20 @@ async function update(req, res) {
|
|||||||
]
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM employees WHERE id = ?', [id])
|
// 确认记录存在且在数据范围内
|
||||||
|
let checkSql = 'SELECT id FROM employees WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'employees')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '员工不存在' })
|
return res.status(404).json({ code: 404, message: '员工不存在或无权操作' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const sets = []
|
const sets = []
|
||||||
@@ -148,9 +182,19 @@ async function update(req, res) {
|
|||||||
async function remove(req, res) {
|
async function remove(req, res) {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id FROM employees WHERE id = ?', [id])
|
let checkSql = 'SELECT id FROM employees WHERE id = ?'
|
||||||
|
const checkParams = [id]
|
||||||
|
const scope = getDataScope(req.user, 'employees')
|
||||||
|
if (scope.deny) {
|
||||||
|
return res.status(403).json({ code: 403, message: '无权访问此资源' })
|
||||||
|
}
|
||||||
|
if (scope.where) {
|
||||||
|
checkSql += ' AND ' + scope.where
|
||||||
|
checkParams.push(...scope.values)
|
||||||
|
}
|
||||||
|
const [existing] = await pool.query(checkSql, checkParams)
|
||||||
if (existing.length === 0) {
|
if (existing.length === 0) {
|
||||||
return res.status(404).json({ code: 404, message: '员工不存在' })
|
return res.status(404).json({ code: 404, message: '员工不存在或无权操作' })
|
||||||
}
|
}
|
||||||
await pool.query('DELETE FROM employees WHERE id = ?', [id])
|
await pool.query('DELETE FROM employees WHERE id = ?', [id])
|
||||||
res.json({ code: 0, message: 'ok' })
|
res.json({ code: 0, message: 'ok' })
|
||||||
|
|||||||
147
routes/suppliers.js
Normal file
147
routes/suppliers.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// routes/suppliers.js —— 供应商管理 CRUD
|
||||||
|
const { pool } = require('../db')
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/suppliers —— 列表
|
||||||
|
async function list(req, res) {
|
||||||
|
try {
|
||||||
|
const { page, pageSize, offset } = pagination(req.query)
|
||||||
|
const { name, type, status } = req.query
|
||||||
|
|
||||||
|
let where = 'WHERE 1=1'
|
||||||
|
const params = []
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
where += ' AND name LIKE ?'
|
||||||
|
params.push(`%${name}%`)
|
||||||
|
}
|
||||||
|
if (type) {
|
||||||
|
where += ' AND type LIKE ?'
|
||||||
|
params.push(`%${type}%`)
|
||||||
|
}
|
||||||
|
if (status !== undefined && status !== '') {
|
||||||
|
where += ' AND status = ?'
|
||||||
|
params.push(Number(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
const [[{ total }]] = await pool.query(
|
||||||
|
`SELECT COUNT(*) AS total FROM suppliers ${where}`,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT * FROM suppliers ${where} ORDER BY id DESC 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('[suppliers list] error:', e)
|
||||||
|
res.status(500).json({ code: 500, message: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/suppliers/:id —— 详情
|
||||||
|
async function detail(req, res) {
|
||||||
|
try {
|
||||||
|
const [rows] = await pool.query('SELECT * FROM suppliers WHERE 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('[suppliers detail] error:', e)
|
||||||
|
res.status(500).json({ code: 500, message: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/suppliers —— 新增
|
||||||
|
async function create(req, res) {
|
||||||
|
const { name, contact, phone, address, type, content, status } = req.body || {}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ code: 400, message: '供应商名称必填' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result] = await pool.query(
|
||||||
|
`INSERT INTO suppliers (name, contact, phone, address, type, content, status)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
[name, contact || null, phone || null, address || null,
|
||||||
|
type || null, content || null, status !== undefined ? status : 1]
|
||||||
|
)
|
||||||
|
const [rows] = await pool.query('SELECT * FROM suppliers WHERE id = ?', [result.insertId])
|
||||||
|
res.json({ code: 0, message: 'ok', data: rows[0] })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[suppliers create] error:', e)
|
||||||
|
res.status(500).json({ code: 500, message: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/suppliers/:id —— 更新
|
||||||
|
async function update(req, res) {
|
||||||
|
const { id } = req.params
|
||||||
|
const fields = ['name', 'contact', 'phone', 'address', 'type', 'content', 'status']
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [existing] = await pool.query('SELECT id FROM suppliers WHERE id = ?', [id])
|
||||||
|
if (existing.length === 0) {
|
||||||
|
return res.status(404).json({ code: 404, message: '供应商不存在' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const sets = []
|
||||||
|
const params = []
|
||||||
|
for (const f of fields) {
|
||||||
|
if (req.body[f] !== undefined) {
|
||||||
|
sets.push(`${f} = ?`)
|
||||||
|
params.push(req.body[f])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sets.length === 0) {
|
||||||
|
return res.status(400).json({ code: 400, message: '没有需要更新的字段' })
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(id)
|
||||||
|
await pool.query(`UPDATE suppliers SET ${sets.join(', ')} WHERE id = ?`, params)
|
||||||
|
|
||||||
|
const [rows] = await pool.query('SELECT * FROM suppliers WHERE id = ?', [id])
|
||||||
|
res.json({ code: 0, message: 'ok', data: rows[0] })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[suppliers update] error:', e)
|
||||||
|
res.status(500).json({ code: 500, message: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/suppliers/:id —— 删除
|
||||||
|
async function remove(req, res) {
|
||||||
|
const { id } = req.params
|
||||||
|
try {
|
||||||
|
const [existing] = await pool.query('SELECT id FROM suppliers WHERE id = ?', [id])
|
||||||
|
if (existing.length === 0) {
|
||||||
|
return res.status(404).json({ code: 404, message: '供应商不存在' })
|
||||||
|
}
|
||||||
|
await pool.query('DELETE FROM suppliers WHERE id = ?', [id])
|
||||||
|
res.json({ code: 0, message: 'ok' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[suppliers delete] error:', e)
|
||||||
|
res.status(500).json({ code: 500, message: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { list, detail, create, update, remove }
|
||||||
147
routes/users.js
147
routes/users.js
@@ -3,8 +3,16 @@ const jwt = require('jsonwebtoken')
|
|||||||
const bcrypt = require('bcryptjs')
|
const bcrypt = require('bcryptjs')
|
||||||
const { pool } = require('../db')
|
const { pool } = require('../db')
|
||||||
|
|
||||||
// 安全字段:不返回密码哈希
|
// 安全字段:关联查询 roles 和 employees
|
||||||
const SAFE_FIELDS = 'id, username, real_name, role, status, created_at, updated_at'
|
const USER_LIST_SQL = `
|
||||||
|
SELECT u.id, u.username, u.is_active, u.role_id, u.employee_id, u.department,
|
||||||
|
u.created_at, u.updated_at,
|
||||||
|
r.name AS role_name, r.description AS role_description,
|
||||||
|
e.name AS real_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN roles r ON u.role_id = r.id
|
||||||
|
LEFT JOIN employees e ON u.employee_id = e.id
|
||||||
|
`
|
||||||
|
|
||||||
function pagination(query) {
|
function pagination(query) {
|
||||||
const page = Math.max(Number(query.page) || 1, 1)
|
const page = Math.max(Number(query.page) || 1, 1)
|
||||||
@@ -24,18 +32,22 @@ async function login(req, res) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
'SELECT id, username, password, real_name, role, status FROM users WHERE username = ?',
|
`SELECT u.id, u.username, u.password, u.is_active, u.role_id, u.department, u.employee_id,
|
||||||
|
r.name AS role_name, e.name AS real_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN roles r ON u.role_id = r.id
|
||||||
|
LEFT JOIN employees e ON u.employee_id = e.id
|
||||||
|
WHERE u.username = ?`,
|
||||||
[username]
|
[username]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 用户不存在或密码错,统一提示避免枚举
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return res.status(400).json({ code: 400, message: '账号或密码错误' })
|
return res.status(400).json({ code: 400, message: '账号或密码错误' })
|
||||||
}
|
}
|
||||||
const user = rows[0]
|
const user = rows[0]
|
||||||
|
|
||||||
// 检查账号是否被禁用
|
// 检查账号是否被禁用
|
||||||
if (user.status === 0) {
|
if (user.is_active === 0) {
|
||||||
return res.status(403).json({ code: 403, message: '账号已被禁用,请联系管理员' })
|
return res.status(403).json({ code: 403, message: '账号已被禁用,请联系管理员' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +56,25 @@ async function login(req, res) {
|
|||||||
return res.status(400).json({ code: 400, message: '账号或密码错误' })
|
return res.status(400).json({ code: 400, message: '账号或密码错误' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询该角色的所有权限标识
|
||||||
|
const [perms] = await pool.query(
|
||||||
|
`SELECT p.name FROM permissions p
|
||||||
|
JOIN role_permissions rp ON p.id = rp.permission_id
|
||||||
|
WHERE rp.role_id = ?`,
|
||||||
|
[user.role_id]
|
||||||
|
)
|
||||||
|
const permissions = perms.map(p => p.name)
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ id: user.id, username: user.username, role: user.role },
|
{
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
name: user.real_name || user.username,
|
||||||
|
role_id: user.role_id,
|
||||||
|
roleName: user.role_name,
|
||||||
|
department: user.department,
|
||||||
|
permissions,
|
||||||
|
},
|
||||||
process.env.JWT_SECRET,
|
process.env.JWT_SECRET,
|
||||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '2h' }
|
{ expiresIn: process.env.JWT_EXPIRES_IN || '2h' }
|
||||||
)
|
)
|
||||||
@@ -58,8 +87,11 @@ async function login(req, res) {
|
|||||||
userInfo: {
|
userInfo: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
real_name: user.real_name,
|
name: user.real_name || user.username,
|
||||||
role: user.role,
|
role_id: user.role_id,
|
||||||
|
roleName: user.role_name,
|
||||||
|
department: user.department,
|
||||||
|
permissions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -73,7 +105,7 @@ async function login(req, res) {
|
|||||||
async function info(req, res) {
|
async function info(req, res) {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
|
`${USER_LIST_SQL} WHERE u.id = ?`,
|
||||||
[req.user.id]
|
[req.user.id]
|
||||||
)
|
)
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
@@ -126,35 +158,35 @@ async function changePassword(req, res) {
|
|||||||
|
|
||||||
// ========== 用户管理 CRUD(管理员) ==========
|
// ========== 用户管理 CRUD(管理员) ==========
|
||||||
|
|
||||||
// GET /api/users —— 用户列表(管理员)
|
// GET /api/users —— 用户列表
|
||||||
async function list(req, res) {
|
async function list(req, res) {
|
||||||
try {
|
try {
|
||||||
const { page, pageSize, offset } = pagination(req.query)
|
const { page, pageSize, offset } = pagination(req.query)
|
||||||
const { username, role, status } = req.query
|
const { username, role_id, is_active } = req.query
|
||||||
|
|
||||||
let where = 'WHERE 1=1'
|
let where = 'WHERE 1=1'
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
where += ' AND username LIKE ?'
|
where += ' AND u.username LIKE ?'
|
||||||
params.push(`%${username}%`)
|
params.push(`%${username}%`)
|
||||||
}
|
}
|
||||||
if (role) {
|
if (role_id) {
|
||||||
where += ' AND role = ?'
|
where += ' AND u.role_id = ?'
|
||||||
params.push(role)
|
params.push(Number(role_id))
|
||||||
}
|
}
|
||||||
if (status !== undefined && status !== '') {
|
if (is_active !== undefined && is_active !== '') {
|
||||||
where += ' AND status = ?'
|
where += ' AND u.is_active = ?'
|
||||||
params.push(Number(status))
|
params.push(Number(is_active))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [[{ total }]] = await pool.query(
|
const [[{ total }]] = await pool.query(
|
||||||
`SELECT COUNT(*) AS total FROM users ${where}`,
|
`SELECT COUNT(*) AS total FROM users u ${where}`,
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
|
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT ${SAFE_FIELDS} FROM users ${where} ORDER BY id DESC LIMIT ? OFFSET ?`,
|
`${USER_LIST_SQL} ${where} ORDER BY u.id DESC LIMIT ? OFFSET ?`,
|
||||||
[...params, pageSize, offset]
|
[...params, pageSize, offset]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -175,11 +207,11 @@ async function list(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/users/:id —— 用户详情(管理员)
|
// GET /api/users/:id —— 用户详情
|
||||||
async function detail(req, res) {
|
async function detail(req, res) {
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
|
`${USER_LIST_SQL} WHERE u.id = ?`,
|
||||||
[req.params.id]
|
[req.params.id]
|
||||||
)
|
)
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
@@ -192,9 +224,9 @@ async function detail(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/users —— 创建用户(管理员)
|
// POST /api/users —— 创建用户
|
||||||
async function create(req, res) {
|
async function create(req, res) {
|
||||||
const { username, password, real_name = null, role = 'user', status = 1 } = req.body || {}
|
const { username, password, role_id, employee_id, department, is_active = 1 } = req.body || {}
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
|
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
|
||||||
@@ -205,19 +237,36 @@ async function create(req, res) {
|
|||||||
if (typeof password !== 'string' || password.length < 6) {
|
if (typeof password !== 'string' || password.length < 6) {
|
||||||
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
|
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
|
||||||
}
|
}
|
||||||
if (!['admin', 'user'].includes(role)) {
|
|
||||||
return res.status(400).json({ code: 400, message: '角色只能为 admin 或 user' })
|
// 校验 role_id 是否存在
|
||||||
|
if (role_id) {
|
||||||
|
const [role] = await pool.query('SELECT id FROM roles WHERE id = ?', [role_id])
|
||||||
|
if (role.length === 0) {
|
||||||
|
return res.status(400).json({ code: 400, message: '指定的角色不存在' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 employee_id 是否存在
|
||||||
|
if (employee_id) {
|
||||||
|
const [emp] = await pool.query('SELECT id, department FROM employees WHERE id = ?', [employee_id])
|
||||||
|
if (emp.length === 0) {
|
||||||
|
return res.status(400).json({ code: 400, message: '指定的员工不存在' })
|
||||||
|
}
|
||||||
|
// 如果没传 department,自动从员工表同步
|
||||||
|
if (!department) {
|
||||||
|
req.body.department = emp[0].department
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hash = await bcrypt.hash(password, 10)
|
const hash = await bcrypt.hash(password, 10)
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
'INSERT INTO users (username, password, real_name, role, status) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO users (username, password, is_active, role_id, employee_id, department) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
[username, hash, real_name, role, status]
|
[username, hash, is_active, role_id || null, employee_id || null, req.body.department || department || null]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
|
`${USER_LIST_SQL} WHERE u.id = ?`,
|
||||||
[result.insertId]
|
[result.insertId]
|
||||||
)
|
)
|
||||||
res.json({ code: 0, message: 'ok', data: rows[0] })
|
res.json({ code: 0, message: 'ok', data: rows[0] })
|
||||||
@@ -230,38 +279,52 @@ 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', 'real_name', 'role', 'status']
|
const fields = ['username', 'role_id', 'employee_id', 'department', 'is_active']
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [existing] = await pool.query('SELECT id, role FROM users WHERE id = ?', [id])
|
const [existing] = await pool.query('SELECT id, role_id, is_active 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: '用户不存在' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUser = existing[0]
|
|
||||||
|
|
||||||
// 不允许修改自己的角色或禁用自己
|
// 不允许修改自己的角色或禁用自己
|
||||||
if (Number(id) === req.user.id) {
|
if (Number(id) === req.user.id) {
|
||||||
if (req.body.role !== undefined && req.body.role !== req.user.role) {
|
if (req.body.role_id !== undefined && req.body.role_id !== req.user.role_id) {
|
||||||
return res.status(400).json({ code: 400, message: '不能修改自己的角色' })
|
return res.status(400).json({ code: 400, message: '不能修改自己的角色' })
|
||||||
}
|
}
|
||||||
if (req.body.status === 0) {
|
if (req.body.is_active === 0) {
|
||||||
return res.status(400).json({ code: 400, message: '不能禁用自己' })
|
return res.status(400).json({ code: 400, message: '不能禁用自己' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验 role_id
|
||||||
|
if (req.body.role_id) {
|
||||||
|
const [role] = await pool.query('SELECT id FROM roles WHERE id = ?', [req.body.role_id])
|
||||||
|
if (role.length === 0) {
|
||||||
|
return res.status(400).json({ code: 400, message: '指定的角色不存在' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验 employee_id
|
||||||
|
if (req.body.employee_id) {
|
||||||
|
const [emp] = await pool.query('SELECT id, department FROM employees WHERE id = ?', [req.body.employee_id])
|
||||||
|
if (emp.length === 0) {
|
||||||
|
return res.status(400).json({ code: 400, message: '指定的员工不存在' })
|
||||||
|
}
|
||||||
|
// 自动同步 department
|
||||||
|
if (req.body.department === undefined) {
|
||||||
|
req.body.department = emp[0].department
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sets = []
|
const sets = []
|
||||||
const params = []
|
const params = []
|
||||||
|
|
||||||
for (const f of fields) {
|
for (const f of fields) {
|
||||||
if (req.body[f] !== undefined) {
|
if (req.body[f] !== undefined) {
|
||||||
// role 字段只允许 admin / user
|
|
||||||
if (f === 'role' && !['admin', 'user'].includes(req.body[f])) {
|
|
||||||
return res.status(400).json({ code: 400, message: '角色只能为 admin 或 user' })
|
|
||||||
}
|
|
||||||
sets.push(`${f} = ?`)
|
sets.push(`${f} = ?`)
|
||||||
params.push(req.body[f])
|
params.push(req.body[f])
|
||||||
}
|
}
|
||||||
@@ -285,7 +348,7 @@ async function update(req, res) {
|
|||||||
await pool.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, params)
|
await pool.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, params)
|
||||||
|
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
|
`${USER_LIST_SQL} WHERE u.id = ?`,
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
res.json({ code: 0, message: 'ok', data: rows[0] })
|
res.json({ code: 0, message: 'ok', data: rows[0] })
|
||||||
@@ -298,7 +361,7 @@ async function update(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /api/users/:id —— 删除用户(管理员)
|
// DELETE /api/users/:id —— 删除用户
|
||||||
async function remove(req, res) {
|
async function remove(req, res) {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
try {
|
||||||
|
|||||||
81
server.js
81
server.js
@@ -10,6 +10,8 @@ const employees = require('./routes/employees')
|
|||||||
const contracts = require('./routes/contracts')
|
const contracts = require('./routes/contracts')
|
||||||
const afterSales = require('./routes/afterSales')
|
const afterSales = require('./routes/afterSales')
|
||||||
const products = require('./routes/products')
|
const products = require('./routes/products')
|
||||||
|
const suppliers = require('./routes/suppliers')
|
||||||
|
const { checkPermission } = require('./middleware/permissions')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(express.json()) // 解析 application/json 请求体
|
app.use(express.json()) // 解析 application/json 请求体
|
||||||
@@ -30,61 +32,60 @@ function auth(req, res, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 管理员校验中间件 ============
|
|
||||||
function requireAdmin(req, res, next) {
|
|
||||||
if (!req.user || req.user.role !== 'admin') {
|
|
||||||
return res.status(403).json({ code: 403, message: '需要管理员权限' })
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ 登录/登出/个人信息 ============
|
// ============ 登录/登出/个人信息 ============
|
||||||
app.post('/api/user/login', users.login)
|
app.post('/api/user/login', users.login)
|
||||||
app.get('/api/user/info', auth, users.info)
|
app.get('/api/user/info', auth, users.info)
|
||||||
app.post('/api/user/logout', auth, users.logout)
|
app.post('/api/user/logout', auth, users.logout)
|
||||||
app.put('/api/user/password', auth, users.changePassword)
|
app.put('/api/user/password', auth, users.changePassword)
|
||||||
|
|
||||||
// ============ 用户管理 CRUD(管理员)/api/users ============
|
// ============ 用户管理 CRUD /api/users(管理员专属) ============
|
||||||
app.get('/api/users', auth, requireAdmin, users.list)
|
app.get('/api/users', auth, checkPermission('user', 'manage'), users.list)
|
||||||
app.get('/api/users/:id', auth, requireAdmin, users.detail)
|
app.get('/api/users/:id', auth, checkPermission('user', 'manage'), users.detail)
|
||||||
app.post('/api/users', auth, requireAdmin, users.create)
|
app.post('/api/users', auth, checkPermission('user', 'manage'), users.create)
|
||||||
app.put('/api/users/:id', auth, requireAdmin, users.update)
|
app.put('/api/users/:id', auth, checkPermission('user', 'manage'), users.update)
|
||||||
app.delete('/api/users/:id', auth, requireAdmin, users.remove)
|
app.delete('/api/users/:id', auth, checkPermission('user', 'manage'), users.remove)
|
||||||
|
|
||||||
// ============ 客户管理 /api/customers ============
|
// ============ 加盟商管理 /api/customers ============
|
||||||
app.get('/api/customers', auth, customers.list)
|
app.get('/api/customers', auth, checkPermission('customer', 'read'), customers.list)
|
||||||
app.get('/api/customers/:id', auth, customers.detail)
|
app.get('/api/customers/:id', auth, checkPermission('customer', 'read'), customers.detail)
|
||||||
app.post('/api/customers', auth, customers.create)
|
app.post('/api/customers', auth, checkPermission('customer', 'create'), customers.create)
|
||||||
app.put('/api/customers/:id', auth, customers.update)
|
app.put('/api/customers/:id', auth, checkPermission('customer', 'update'), customers.update)
|
||||||
app.delete('/api/customers/:id', auth, customers.remove)
|
app.delete('/api/customers/:id', auth, checkPermission('customer', 'delete'), customers.remove)
|
||||||
|
|
||||||
// ============ 员工管理 /api/employees ============
|
// ============ 员工管理 /api/employees ============
|
||||||
app.get('/api/employees', auth, employees.list)
|
app.get('/api/employees', auth, checkPermission('employee', 'read'), employees.list)
|
||||||
app.get('/api/employees/:id', auth, employees.detail)
|
app.get('/api/employees/:id', auth, checkPermission('employee', 'read'), employees.detail)
|
||||||
app.post('/api/employees', auth, employees.create)
|
app.post('/api/employees', auth, checkPermission('employee', 'create'), employees.create)
|
||||||
app.put('/api/employees/:id', auth, employees.update)
|
app.put('/api/employees/:id', auth, checkPermission('employee', 'update'), employees.update)
|
||||||
app.delete('/api/employees/:id', auth, employees.remove)
|
app.delete('/api/employees/:id', auth, checkPermission('employee', 'delete'), employees.remove)
|
||||||
|
|
||||||
// ============ 合同管理 /api/contracts ============
|
// ============ 合同管理 /api/contracts ============
|
||||||
app.get('/api/contracts', auth, contracts.list)
|
app.get('/api/contracts', auth, checkPermission('contract', 'read'), contracts.list)
|
||||||
app.get('/api/contracts/:id', auth, contracts.detail)
|
app.get('/api/contracts/:id', auth, checkPermission('contract', 'read'), contracts.detail)
|
||||||
app.post('/api/contracts', auth, contracts.create)
|
app.post('/api/contracts', auth, checkPermission('contract', 'create'), contracts.create)
|
||||||
app.put('/api/contracts/:id', auth, contracts.update)
|
app.put('/api/contracts/:id', auth, checkPermission('contract', 'update'), contracts.update)
|
||||||
app.delete('/api/contracts/:id', auth, contracts.remove)
|
app.delete('/api/contracts/:id', auth, checkPermission('contract', 'delete'), contracts.remove)
|
||||||
|
|
||||||
// ============ 售后管理 /api/after-sales ============
|
// ============ 售后管理 /api/after-sales ============
|
||||||
app.get('/api/after-sales', auth, afterSales.list)
|
app.get('/api/after-sales', auth, checkPermission('after_sale', 'read'), afterSales.list)
|
||||||
app.get('/api/after-sales/:id', auth, afterSales.detail)
|
app.get('/api/after-sales/:id', auth, checkPermission('after_sale', 'read'), afterSales.detail)
|
||||||
app.post('/api/after-sales', auth, afterSales.create)
|
app.post('/api/after-sales', auth, checkPermission('after_sale', 'create'), afterSales.create)
|
||||||
app.put('/api/after-sales/:id', auth, afterSales.update)
|
app.put('/api/after-sales/:id', auth, checkPermission('after_sale', 'update'), afterSales.update)
|
||||||
app.delete('/api/after-sales/:id', auth, afterSales.remove)
|
app.delete('/api/after-sales/:id', auth, checkPermission('after_sale', 'delete'), afterSales.remove)
|
||||||
|
|
||||||
// ============ 产品管理 /api/products ============
|
// ============ 产品管理 /api/products ============
|
||||||
app.get('/api/products', auth, products.list)
|
app.get('/api/products', auth, checkPermission('product', 'read'), products.list)
|
||||||
app.get('/api/products/:id', auth, products.detail)
|
app.get('/api/products/:id', auth, checkPermission('product', 'read'), products.detail)
|
||||||
app.post('/api/products', auth, products.create)
|
app.post('/api/products', auth, checkPermission('product', 'create'), products.create)
|
||||||
app.put('/api/products/:id', auth, products.update)
|
app.put('/api/products/:id', auth, checkPermission('product', 'update'), products.update)
|
||||||
app.delete('/api/products/:id', auth, products.remove)
|
app.delete('/api/products/:id', auth, checkPermission('product', 'delete'), products.remove)
|
||||||
|
|
||||||
|
// ============ 供应商管理 /api/suppliers ============
|
||||||
|
app.get('/api/suppliers', auth, checkPermission('supplier', 'read'), suppliers.list)
|
||||||
|
app.get('/api/suppliers/:id', auth, checkPermission('supplier', 'read'), suppliers.detail)
|
||||||
|
app.post('/api/suppliers', auth, checkPermission('supplier', 'create'), suppliers.create)
|
||||||
|
app.put('/api/suppliers/:id', auth, checkPermission('supplier', 'update'), suppliers.update)
|
||||||
|
app.delete('/api/suppliers/:id', auth, checkPermission('supplier', 'delete'), suppliers.remove)
|
||||||
|
|
||||||
// ============ 启动 ============
|
// ============ 启动 ============
|
||||||
const PORT = Number(process.env.PORT) || 3000
|
const PORT = Number(process.env.PORT) || 3000
|
||||||
|
|||||||
781
test-all.js
781
test-all.js
@@ -1,29 +1,42 @@
|
|||||||
// test-all.js —— 手动运行,逐接口验证全部 CRUD
|
// test-all.js —— 全量接口测试:权限隔离 + 数据范围隔离 + CRUD
|
||||||
// 用法: node test-all.js
|
// 用法: 先启动服务 node server.js,再运行 node test-all.js
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
|
|
||||||
const BASE = 'http://127.0.0.1:3000'
|
const BASE = 'http://127.0.0.1:3000'
|
||||||
let token = ''
|
|
||||||
let failCount = 0
|
let failCount = 0
|
||||||
let passCount = 0
|
let passCount = 0
|
||||||
|
|
||||||
|
// 所有用户的 token
|
||||||
|
const tokens = {}
|
||||||
|
|
||||||
|
// 测试中创建的数据 ID,用于最后清理
|
||||||
|
const created = {
|
||||||
|
customers: [],
|
||||||
|
employees: [],
|
||||||
|
products: [],
|
||||||
|
contracts: [],
|
||||||
|
afterSales: [],
|
||||||
|
suppliers: [],
|
||||||
|
users: [],
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 工具函数 ==========
|
// ========== 工具函数 ==========
|
||||||
function req(method, path, body, useToken = true) {
|
function req(method, path, body, token) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const u = new URL(path, BASE)
|
const u = new URL(path, BASE)
|
||||||
const opts = {
|
const opts = {
|
||||||
hostname: u.hostname, port: u.port, path: u.pathname + u.search, method,
|
hostname: u.hostname, port: u.port, path: u.pathname + u.search, method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
}
|
}
|
||||||
if (useToken && token) opts.headers['Authorization'] = 'Bearer ' + token
|
if (token) opts.headers['Authorization'] = 'Bearer ' + token
|
||||||
|
|
||||||
const r = http.request(opts, (res) => {
|
const r = http.request(opts, (res) => {
|
||||||
let d = ''
|
let d = ''
|
||||||
res.on('data', (c) => (d += c))
|
res.on('data', (c) => (d += c))
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
try { resolve({ status: res.statusCode, body: JSON.parse(d) }) }
|
try { resolve({ status: res.statusCode, body: JSON.parse(d) }) }
|
||||||
catch { resolve({ status: res.statusCode, body: d.substring(0, 300) }) }
|
catch { resolve({ status: res.statusCode, body: d.substring(0, 500) }) }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.on('error', reject)
|
r.on('error', reject)
|
||||||
@@ -32,318 +45,644 @@ function req(method, path, body, useToken = true) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function check(label, res, expectStatus, expectInfo) {
|
function check(label, res, expectStatus) {
|
||||||
const ok = res.status === expectStatus
|
if (res.status === expectStatus) {
|
||||||
if (ok) {
|
|
||||||
passCount++
|
passCount++
|
||||||
console.log(` ✅ ${label}`)
|
console.log(` ✅ ${label}`)
|
||||||
} else {
|
} else {
|
||||||
failCount++
|
failCount++
|
||||||
console.log(` ❌ ${label} 预期 HTTP ${expectStatus}, 实际 HTTP ${res.status}`)
|
console.log(` ❌ ${label} 预期 HTTP ${expectStatus}, 实际 HTTP ${res.status}`)
|
||||||
console.log(` 响应: ${JSON.stringify(res.body).substring(0, 200)}`)
|
console.log(` 响应: ${JSON.stringify(res.body).substring(0, 300)}`)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValue(label, actual, expected) {
|
||||||
|
if (actual === expected) {
|
||||||
|
passCount++
|
||||||
|
console.log(` ✅ ${label} (${actual})`)
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
console.log(` ❌ ${label} 预期 ${expected}, 实际 ${actual}`)
|
||||||
}
|
}
|
||||||
if (expectInfo) console.log(` ↳ ${expectInfo}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 主流程 ==========
|
// ========== 主流程 ==========
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('╔══════════════════════════════════════════╗')
|
console.log('╔══════════════════════════════════════════════════╗')
|
||||||
console.log('║ 企业管理系统 — 接口全量测试 ║')
|
console.log('║ 蜜雪冰城企业管理系统 — 全量接口测试 ║')
|
||||||
console.log('╚══════════════════════════════════════════╝')
|
console.log('╚══════════════════════════════════════════════════╝')
|
||||||
console.log(`服务地址: ${BASE}\n`)
|
console.log(`服务地址: ${BASE}\n`)
|
||||||
|
|
||||||
// ────────── 1. 登录 ──────────
|
// ══════════════════════════════════════════
|
||||||
console.log('━'.repeat(50))
|
// 【1】 多角色登录
|
||||||
console.log('【1】 用户登录模块')
|
// ══════════════════════════════════════════
|
||||||
console.log('━'.repeat(50))
|
console.log('━'.repeat(55))
|
||||||
|
console.log('【1】 多角色登录')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
let r = await req('POST', '/api/user/login', { username: 'admin', password: '123456' }, false)
|
const accounts = [
|
||||||
check('POST /api/user/login (正确密码)', r, 200)
|
{ username: 'admin', password: '123456', label: '系统管理员' },
|
||||||
if (r.body?.data?.token) {
|
{ username: 'liming', password: '123456', label: '招商经理' },
|
||||||
token = r.body.data.token
|
{ username: 'wangli', password: '123456', label: '运营经理' },
|
||||||
console.log(` ↳ 角色: ${r.body.data.userInfo.role}, 姓名: ${r.body.data.userInfo.real_name}`)
|
{ username: 'zhaoqiang', password: '123456', label: '采购经理' },
|
||||||
|
{ username: 'chenfang', password: '123456', label: '财务人员' },
|
||||||
|
{ username: 'zhangchao', password: '123456', label: '总经理' },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const acct of accounts) {
|
||||||
|
const r = await req('POST', '/api/user/login', { username: acct.username, password: acct.password })
|
||||||
|
check(`POST /api/user/login (${acct.label} ${acct.username})`, r, 200)
|
||||||
|
if (r.body?.data?.token) {
|
||||||
|
tokens[acct.username] = r.body.data.token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r = await req('POST', '/api/user/login', { username: 'admin', password: 'wrong' }, false)
|
// 错误密码
|
||||||
check('POST /api/user/login (错误密码)', r, 400, '→ 账号或密码错误')
|
let r = await req('POST', '/api/user/login', { username: 'admin', password: 'wrong' })
|
||||||
|
check('POST /api/user/login (错误密码)', r, 400)
|
||||||
|
|
||||||
r = await req('POST', '/api/user/login', { username: '', password: '' }, false)
|
r = await req('POST', '/api/user/login', { username: '', password: '' })
|
||||||
check('POST /api/user/login (空参数)', r, 400, '→ 用户名和密码必填')
|
check('POST /api/user/login (空参数)', r, 400)
|
||||||
|
|
||||||
// ────────── 2. 个人信息 ──────────
|
// ══════════════════════════════════════════
|
||||||
console.log('\n━'.repeat(50))
|
// 【2】 JWT payload 验证
|
||||||
console.log('【2】 个人信息 & 改密')
|
// ══════════════════════════════════════════
|
||||||
console.log('━'.repeat(50))
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【2】 JWT payload 验证(新字段)')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
r = await req('GET', '/api/user/info')
|
// 验证 admin 登录返回的 userInfo 结构
|
||||||
check('GET /api/user/info', r, 200)
|
const adminLogin = await req('POST', '/api/user/login', { username: 'admin', password: '123456' })
|
||||||
if (r.body?.data) console.log(` ↳ 用户名: ${r.body.data.username}, 角色: ${r.body.data.role}`)
|
const adminInfo = adminLogin.body?.data?.userInfo
|
||||||
|
if (adminInfo) {
|
||||||
|
checkValue('admin userInfo.name', adminInfo.name, '系统管理员')
|
||||||
|
checkValue('admin userInfo.roleName', adminInfo.roleName, 'admin')
|
||||||
|
checkValue('admin userInfo.department', adminInfo.department, '信息技术部')
|
||||||
|
check('admin userInfo.permissions 是数组', { status: Array.isArray(adminInfo.permissions) ? 200 : 500 }, 200)
|
||||||
|
checkValue('admin permissions 数量', adminInfo.permissions?.length, 25)
|
||||||
|
}
|
||||||
|
|
||||||
r = await req('POST', '/api/user/logout')
|
// 验证 liming(招商经理)的权限
|
||||||
check('POST /api/user/logout', r, 200, '→ JWT 无状态,前端删 token 即可')
|
const limingLogin = await req('POST', '/api/user/login', { username: 'liming', password: '123456' })
|
||||||
|
const limingInfo = limingLogin.body?.data?.userInfo
|
||||||
|
if (limingInfo) {
|
||||||
|
checkValue('liming userInfo.name', limingInfo.name, '李明')
|
||||||
|
checkValue('liming userInfo.roleName', limingInfo.roleName, 'franchise_manager')
|
||||||
|
checkValue('liming userInfo.department', limingInfo.department, '招商部')
|
||||||
|
check('liming 有 customer:read', { status: limingInfo.permissions?.includes('customer:read') ? 200 : 500 }, 200)
|
||||||
|
check('liming 无 product:read', { status: !limingInfo.permissions?.includes('product:read') ? 200 : 500 }, 200)
|
||||||
|
check('liming 无 supplier:read', { status: !limingInfo.permissions?.includes('supplier:read') ? 200 : 500 }, 200)
|
||||||
|
}
|
||||||
|
|
||||||
r = await req('PUT', '/api/user/password', { oldPassword: 'wrong', newPassword: 'newpwd999' })
|
// 验证 /api/user/info 返回新字段
|
||||||
check('PUT /api/user/password (旧密码错)', r, 400, '→ 旧密码错误')
|
r = await req('GET', '/api/user/info', null, tokens.admin)
|
||||||
|
check('GET /api/user/info (admin)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
check('info 包含 role_name 字段', { status: r.body.data.role_name ? 200 : 500 }, 200)
|
||||||
|
check('info 包含 department 字段', { status: r.body.data.department ? 200 : 500 }, 200)
|
||||||
|
}
|
||||||
|
|
||||||
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123456' })
|
// ══════════════════════════════════════════
|
||||||
check('PUT /api/user/password (新旧相同)', r, 400, '→ 新密码不能与旧密码相同')
|
// 【3】 权限隔离测试
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【3】 权限隔离测试(无权限应返回 403)')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123' })
|
// 招商经理 → products(无权限)
|
||||||
check('PUT /api/user/password (新密码太短)', r, 400, '→ 新密码至少 6 位')
|
r = await req('GET', '/api/products', null, tokens.liming)
|
||||||
|
check('liming → GET /api/products → 403', r, 403)
|
||||||
|
|
||||||
// ────────── 3. 用户管理 CRUD ──────────
|
r = await req('POST', '/api/products', { name: 'x' }, tokens.liming)
|
||||||
console.log('\n━'.repeat(50))
|
check('liming → POST /api/products → 403', r, 403)
|
||||||
console.log('【3】 用户管理 CRUD(管理员)')
|
|
||||||
console.log('━'.repeat(50))
|
|
||||||
|
|
||||||
r = await req('GET', '/api/users?page=1&pageSize=10')
|
// 招商经理 → suppliers(无权限)
|
||||||
check('GET /api/users (列表)', r, 200)
|
r = await req('GET', '/api/suppliers', null, tokens.liming)
|
||||||
if (r.body?.data) console.log(` ↳ 共 ${r.body.data.total} 个用户, 本页 ${r.body.data.list?.length} 条`)
|
check('liming → GET /api/suppliers → 403', r, 403)
|
||||||
|
|
||||||
const ts = Date.now()
|
// 招商经理 → after-sales(无权限)
|
||||||
const newUser = `tester_${ts}`
|
r = await req('GET', '/api/after-sales', null, tokens.liming)
|
||||||
r = await req('POST', '/api/users', { username: newUser, password: 'pass123', real_name: '测试员', role: 'user' })
|
check('liming → GET /api/after-sales → 403', r, 403)
|
||||||
check('POST /api/users (创建)', r, 200)
|
|
||||||
let userId = r.body?.data?.id
|
|
||||||
if (userId) console.log(` ↳ 新建用户 ID: ${userId}, 用户名: ${newUser}`)
|
|
||||||
|
|
||||||
r = await req('POST', '/api/users', { username: 'admin', password: '123456' })
|
// 采购经理 → customers(无权限)
|
||||||
check('POST /api/users (重复用户名)', r, 409, '→ 用户名已存在')
|
r = await req('GET', '/api/customers', null, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang → GET /api/customers → 403', r, 403)
|
||||||
|
|
||||||
r = await req('POST', '/api/users', { username: 'bad', password: '12', role: 'superman' })
|
// 采购经理 → after-sales(无权限)
|
||||||
check('POST /api/users (非法角色)', r, 400, '→ 角色只能为 admin 或 user')
|
r = await req('GET', '/api/after-sales', null, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang → GET /api/after-sales → 403', r, 403)
|
||||||
|
|
||||||
if (userId) {
|
// 财务人员 → products(无权限)
|
||||||
r = await req('GET', '/api/users/' + userId)
|
r = await req('GET', '/api/products', null, tokens.chenfang)
|
||||||
check('GET /api/users/:id (详情)', r, 200)
|
check('chenfang → GET /api/products → 403', r, 403)
|
||||||
|
|
||||||
r = await req('PUT', '/api/users/' + userId, { real_name: '改名后的测试员', status: 1 })
|
// 财务人员 → suppliers(无权限)
|
||||||
check('PUT /api/users/:id (更新姓名)', r, 200)
|
r = await req('GET', '/api/suppliers', null, tokens.chenfang)
|
||||||
if (r.body?.data) console.log(` ↳ 新姓名: ${r.body.data.real_name}`)
|
check('chenfang → GET /api/suppliers → 403', r, 403)
|
||||||
|
|
||||||
r = await req('PUT', '/api/users/' + userId, { password: 'newpass456' })
|
// 财务人员 → 创建 customers(只有 read 权限,无 create)
|
||||||
check('PUT /api/users/:id (管理员重置密码)', r, 200, '→ 管理员可直接改他人密码')
|
r = await req('POST', '/api/customers', { name: '财务创建' }, tokens.chenfang)
|
||||||
|
check('chenfang → POST /api/customers → 403', r, 403)
|
||||||
|
|
||||||
// 用新用户登录验证改密成功
|
// 财务人员 → 创建 employees(只有 read,无 create)
|
||||||
const r2 = await req('POST', '/api/user/login', { username: newUser, password: 'newpass456' }, false)
|
r = await req('POST', '/api/employees', { name: '财务创建' }, tokens.chenfang)
|
||||||
check(' └ 新用户用新密码登录', r2, 200)
|
check('chenfang → POST /api/employees → 403', r, 403)
|
||||||
|
|
||||||
// 测试非管理员访问
|
// 总经理 → 创建 customers(只有 read,无 create)
|
||||||
const userToken = r2.body?.data?.token
|
r = await req('POST', '/api/customers', { name: '总经理创建' }, tokens.zhangchao)
|
||||||
if (userToken) {
|
check('zhangchao → POST /api/customers → 403', r, 403)
|
||||||
const oldToken = token
|
|
||||||
token = userToken
|
// 总经理 → 创建 contracts(只有 read,无 create)
|
||||||
r = await req('GET', '/api/users')
|
r = await req('POST', '/api/contracts', { contract_name: 'x', customer_id: 1 }, tokens.zhangchao)
|
||||||
check(' └ 普通用户访问用户列表', r, 403, '→ 需要管理员权限')
|
check('zhangchao → POST /api/contracts → 403', r, 403)
|
||||||
token = oldToken
|
|
||||||
|
// 非管理员 → 用户管理
|
||||||
|
r = await req('GET', '/api/users', null, tokens.liming)
|
||||||
|
check('liming → GET /api/users → 403', r, 403)
|
||||||
|
|
||||||
|
// 无 token
|
||||||
|
r = await req('GET', '/api/customers', null, null)
|
||||||
|
check('无 token → GET /api/customers → 401', r, 401)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
// 【4】 数据范围隔离测试
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【4】 数据范围隔离测试')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
|
// 4.1 加盟商范围隔离:admin 创建 2 个加盟商,分别分配给 liming 和 wangli
|
||||||
|
// 获取 liming 和 wangli 的 user ID
|
||||||
|
const allUsers = await req('GET', '/api/users', null, tokens.admin)
|
||||||
|
const limingUserId = allUsers.body?.data?.list?.find(u => u.username === 'liming')?.id
|
||||||
|
const wangliUserId = allUsers.body?.data?.list?.find(u => u.username === 'wangli')?.id
|
||||||
|
|
||||||
|
r = await req('POST', '/api/customers', { name: '加盟商A-归属liming', phone: '13811111111', responsible_user_id: limingUserId }, tokens.admin)
|
||||||
|
check('admin 创建加盟商A (归属liming)', r, 200)
|
||||||
|
const custAId = r.body?.data?.id
|
||||||
|
if (custAId) created.customers.push(custAId)
|
||||||
|
|
||||||
|
r = await req('POST', '/api/customers', { name: '加盟商B-归属wangli', phone: '13822222222', responsible_user_id: wangliUserId }, tokens.admin)
|
||||||
|
check('admin 创建加盟商B (归属wangli)', r, 200)
|
||||||
|
const custBId = r.body?.data?.id
|
||||||
|
if (custBId) created.customers.push(custBId)
|
||||||
|
|
||||||
|
// liming 只能看到自己负责的
|
||||||
|
r = await req('GET', '/api/customers', null, tokens.liming)
|
||||||
|
check('liming GET /api/customers (应只看到自己的)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
const limingCusts = r.body.data.list || []
|
||||||
|
const allBelongToLiming = limingCusts.every(c => c.responsible_user_id === limingUserId)
|
||||||
|
checkValue('liming 只看到自己的加盟商', allBelongToLiming ? 'YES' : 'NO', 'YES')
|
||||||
|
checkValue('liming 加盟商数量 >= 1', limingCusts.length >= 1 ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// wangli 只能看到自己负责的
|
||||||
|
r = await req('GET', '/api/customers', null, tokens.wangli)
|
||||||
|
check('wangli GET /api/customers (应只看到自己的)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
const wangliCusts = r.body.data.list || []
|
||||||
|
const allBelongToWangli = wangliCusts.every(c => c.responsible_user_id === wangliUserId)
|
||||||
|
checkValue('wangli 只看到自己的加盟商', allBelongToWangli ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// admin 能看到全部
|
||||||
|
r = await req('GET', '/api/customers', null, tokens.admin)
|
||||||
|
check('admin GET /api/customers (应看到全部)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
checkValue('admin 加盟商总数 >= 2', r.body.data.total >= 2 ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// liming 不能看 wangli 的加盟商详情
|
||||||
|
if (custBId) {
|
||||||
|
r = await req('GET', '/api/customers/' + custBId, null, tokens.liming)
|
||||||
|
check('liming GET wangli的加盟商详情 → 404', r, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// liming 不能修改 wangli 的加盟商
|
||||||
|
if (custBId) {
|
||||||
|
r = await req('PUT', '/api/customers/' + custBId, { name: '越权修改' }, tokens.liming)
|
||||||
|
check('liming PUT wangli的加盟商 → 404', r, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.2 员工部门隔离:admin 创建 2 个不同部门的员工
|
||||||
|
r = await req('POST', '/api/employees', { name: '招商部测试员工', department: '招商部', position: '专员', status: 1 }, tokens.admin)
|
||||||
|
check('admin 创建招商部员工', r, 200)
|
||||||
|
const empLmId = r.body?.data?.id
|
||||||
|
if (empLmId) created.employees.push(empLmId)
|
||||||
|
|
||||||
|
r = await req('POST', '/api/employees', { name: '运营部测试员工', department: '运营部', position: '专员', status: 1 }, tokens.admin)
|
||||||
|
check('admin 创建运营部员工', r, 200)
|
||||||
|
const empWlId = r.body?.data?.id
|
||||||
|
if (empWlId) created.employees.push(empWlId)
|
||||||
|
|
||||||
|
// liming 只能看到招商部员工
|
||||||
|
r = await req('GET', '/api/employees', null, tokens.liming)
|
||||||
|
check('liming GET /api/employees (部门隔离)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
const limingEmps = r.body.data.list || []
|
||||||
|
const allInDept = limingEmps.every(e => e.department === '招商部')
|
||||||
|
checkValue('liming 只看到招商部员工', allInDept ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// wangli 只能看到运营部员工
|
||||||
|
r = await req('GET', '/api/employees', null, tokens.wangli)
|
||||||
|
check('wangli GET /api/employees (部门隔离)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
const wangliEmps = r.body.data.list || []
|
||||||
|
const allInDept = wangliEmps.every(e => e.department === '运营部')
|
||||||
|
checkValue('wangli 只看到运营部员工', allInDept ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// admin 看到全部
|
||||||
|
r = await req('GET', '/api/employees', null, tokens.admin)
|
||||||
|
check('admin GET /api/employees (应看到全部)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
checkValue('admin 员工总数 >= 2', r.body.data.total >= 2 ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.3 合同范围隔离
|
||||||
|
// admin 创建加盟合同(归属 liming)和采购合同
|
||||||
|
r = await req('POST', '/api/contracts', {
|
||||||
|
customer_id: custAId, contract_name: '加盟合同A', amount: 100000,
|
||||||
|
effective_date: '2026-01-01', expiry_date: '2027-01-01', status: '生效',
|
||||||
|
type: 'franchise',
|
||||||
|
}, tokens.admin)
|
||||||
|
check('admin 创建加盟合同 (归属liming的客户)', r, 200)
|
||||||
|
const franchiseConId = r.body?.data?.id
|
||||||
|
if (franchiseConId) created.contracts.push(franchiseConId)
|
||||||
|
|
||||||
|
r = await req('POST', '/api/contracts', {
|
||||||
|
customer_id: custAId, contract_name: '采购合同X', amount: 200000,
|
||||||
|
effective_date: '2026-01-01', expiry_date: '2027-01-01', status: '生效',
|
||||||
|
type: 'supply',
|
||||||
|
}, tokens.admin)
|
||||||
|
check('admin 创建采购合同', r, 200)
|
||||||
|
const supplyConId = r.body?.data?.id
|
||||||
|
if (supplyConId) created.contracts.push(supplyConId)
|
||||||
|
|
||||||
|
// 采购经理只看 supply 合同
|
||||||
|
r = await req('GET', '/api/contracts', null, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang GET /api/contracts (只看supply)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
const zhaoContracts = r.body.data.list || []
|
||||||
|
const allSupply = zhaoContracts.every(c => c.type === 'supply')
|
||||||
|
checkValue('zhaoqiang 只看到supply合同', allSupply ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.4 after_sales 范围隔离:admin 创建售后记录
|
||||||
|
r = await req('POST', '/api/after-sales', {
|
||||||
|
customer_id: custAId, feedback: '设备故障,归属wangli处理',
|
||||||
|
responsible_user_id: wangliUserId, handle_status: '待处理',
|
||||||
|
}, tokens.admin)
|
||||||
|
check('admin 创建售后记录 (归属wangli)', r, 200)
|
||||||
|
const asScopeId = r.body?.data?.id
|
||||||
|
if (asScopeId) created.afterSales.push(asScopeId)
|
||||||
|
|
||||||
|
r = await req('POST', '/api/after-sales', {
|
||||||
|
customer_id: custBId, feedback: '原料质量问题,归属当前用户处理',
|
||||||
|
handle_status: '待处理',
|
||||||
|
}, tokens.wangli)
|
||||||
|
check('wangli 创建售后记录 (默认归属自己)', r, 200)
|
||||||
|
const asScopeId2 = r.body?.data?.id
|
||||||
|
if (asScopeId2) created.afterSales.push(asScopeId2)
|
||||||
|
|
||||||
|
// wangli 应该能看到 2 条售后(都是自己负责的)
|
||||||
|
r = await req('GET', '/api/after-sales', null, tokens.wangli)
|
||||||
|
check('wangli GET /api/after-sales (应看到自己负责的)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
const wangliAS = r.body.data.list || []
|
||||||
|
const allBelong = wangliAS.every(a => a.responsible_user_id === wangliUserId)
|
||||||
|
checkValue('wangli 只看到自己的售后', allBelong ? 'YES' : 'NO', 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
// liming 没有 after_sale:read 权限,直接 403
|
||||||
|
r = await req('GET', '/api/after-sales', null, tokens.liming)
|
||||||
|
check('liming GET /api/after-sales → 403 (无权限)', r, 403)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
// 【5】 suppliers CRUD 测试
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【5】 suppliers CRUD(采购经理操作)')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
|
r = await req('POST', '/api/suppliers', {
|
||||||
|
name: '柠檬供应商-测试', contact: '张三', phone: '13800009999',
|
||||||
|
address: '四川安岳', type: '原材料', content: '优质柠檬供应商',
|
||||||
|
}, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang POST /api/suppliers (创建)', r, 200)
|
||||||
|
const supId = r.body?.data?.id
|
||||||
|
if (supId) created.suppliers.push(supId)
|
||||||
|
|
||||||
|
r = await req('POST', '/api/suppliers', { name: '' }, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang POST /api/suppliers (缺名称)', r, 400)
|
||||||
|
|
||||||
|
if (supId) {
|
||||||
|
r = await req('GET', '/api/suppliers/' + supId, null, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang GET /api/suppliers/:id (详情)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
|
checkValue('供应商名称', r.body.data.name, '柠檬供应商-测试')
|
||||||
}
|
}
|
||||||
|
|
||||||
r = await req('DELETE', '/api/users/' + userId)
|
r = await req('PUT', '/api/suppliers/' + supId, { phone: '13800008888', remark: '更新联系方式' }, tokens.zhaoqiang)
|
||||||
check('DELETE /api/users/:id (删除)', r, 200)
|
check('zhaoqiang PUT /api/suppliers/:id (更新)', r, 200)
|
||||||
|
|
||||||
|
r = await req('GET', '/api/suppliers', null, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang GET /api/suppliers (列表)', r, 200)
|
||||||
|
if (r.body?.data) console.log(` ↳ 供应商总数: ${r.body.data.total}`)
|
||||||
|
|
||||||
|
r = await req('GET', '/api/suppliers?name=柠檬', null, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang GET /api/suppliers?name=柠檬 (搜索)', r, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
r = await req('DELETE', '/api/users/1')
|
// ══════════════════════════════════════════
|
||||||
check('DELETE /api/users/1 (删自己)', r, 400, '→ 不能删除自己')
|
// 【6】 users CRUD 测试(适配新字段)
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【6】 users CRUD(管理员操作)')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
r = await req('GET', '/api/users', null, false)
|
r = await req('GET', '/api/users', null, tokens.admin)
|
||||||
check('GET /api/users (无 token)', r, 401, '→ 未登录')
|
check('admin GET /api/users (列表)', r, 200)
|
||||||
|
if (r.body?.data) console.log(` ↳ 用户总数: ${r.body.data.total}`)
|
||||||
|
|
||||||
// ────────── 4. 客户管理 ──────────
|
// 创建新用户,关联到已有员工
|
||||||
console.log('\n━'.repeat(50))
|
r = await req('POST', '/api/users', {
|
||||||
console.log('【4】 客户管理 CRUD')
|
username: 'testuser_' + Date.now(),
|
||||||
console.log('━'.repeat(50))
|
password: 'pass123456',
|
||||||
|
role_id: 3, // franchise_manager
|
||||||
|
employee_id: empLmId,
|
||||||
|
department: '招商部',
|
||||||
|
}, tokens.admin)
|
||||||
|
check('admin POST /api/users (创建带role_id+employee_id)', r, 200)
|
||||||
|
const newUserId = r.body?.data?.id
|
||||||
|
if (newUserId) {
|
||||||
|
created.users.push(newUserId)
|
||||||
|
if (r.body?.data) {
|
||||||
|
check('新用户有 role_name', { status: r.body.data.role_name ? 200 : 500 }, 200)
|
||||||
|
check('新用户有 real_name (从employees)', { status: r.body.data.real_name ? 200 : 500 }, 200)
|
||||||
|
console.log(` ↳ 新用户: ${r.body.data.username}, 角色: ${r.body.data.role_name}, 姓名: ${r.body.data.real_name}`)
|
||||||
|
}
|
||||||
|
|
||||||
r = await req('GET', '/api/customers?page=1&pageSize=10')
|
r = await req('GET', '/api/users/' + newUserId, null, tokens.admin)
|
||||||
check('GET /api/customers (列表)', r, 200)
|
check('admin GET /api/users/:id (详情)', r, 200)
|
||||||
if (r.body?.data) console.log(` ↳ 共 ${r.body.data.total} 条`)
|
|
||||||
|
|
||||||
r = await req('POST', '/api/customers', { name: '测试加盟商', phone: '13800001111', province: '广东', city: '东莞', district: '松山湖', address: '科技路88号', customer_type: 'VIP', email: 'test@test.com' })
|
r = await req('PUT', '/api/users/' + newUserId, { is_active: 0 }, tokens.admin)
|
||||||
check('POST /api/customers (创建VIP客户)', r, 200)
|
check('admin PUT /api/users/:id (禁用)', r, 200)
|
||||||
let custId = r.body?.data?.id
|
if (r.body?.data) checkValue('is_active 已禁用', r.body.data.is_active, 0)
|
||||||
if (custId) console.log(` ↳ 新建客户 ID: ${custId}, 类型: ${r.body.data.customer_type}`)
|
|
||||||
|
|
||||||
r = await req('POST', '/api/customers', { name: '' })
|
r = await req('PUT', '/api/users/' + newUserId, { is_active: 1, password: 'newpass123' }, tokens.admin)
|
||||||
check('POST /api/customers (缺姓名)', r, 400, '→ 客户姓名必填')
|
check('admin PUT /api/users/:id (启用+重置密码)', r, 200)
|
||||||
|
}
|
||||||
|
|
||||||
// 不传 customer_type 应为默认 Normal
|
// admin 不能删除自己(先获取 admin 的真实 ID)
|
||||||
r = await req('POST', '/api/customers', { name: '默认类型测试' })
|
const adminUser = allUsers.body?.data?.list?.find(u => u.username === 'admin')
|
||||||
check('POST /api/customers (不传类型默认Normal)', r, 200)
|
const adminUserId = adminUser?.id
|
||||||
if (r.body?.data) console.log(` ↳ 默认类型: ${r.body.data.customer_type}`)
|
|
||||||
if (r.body?.data?.id) await req('DELETE', '/api/customers/' + r.body.data.id)
|
|
||||||
|
|
||||||
if (custId) {
|
r = await req('DELETE', '/api/users/' + adminUserId, null, tokens.admin)
|
||||||
r = await req('GET', '/api/customers/' + custId)
|
check('admin DELETE /api/users/' + adminUserId + ' (删自己) → 400', r, 400)
|
||||||
|
|
||||||
|
// admin 不能禁用自己
|
||||||
|
r = await req('PUT', '/api/users/' + adminUserId, { is_active: 0 }, tokens.admin)
|
||||||
|
check('admin PUT /api/users/' + adminUserId + ' (禁用自己) → 400', r, 400)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
// 【7】 customers CRUD 测试
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【7】 customers CRUD')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
|
r = await req('POST', '/api/customers', {
|
||||||
|
name: 'CRUD测试加盟商', phone: '13800007777', province: '河南', city: '郑州',
|
||||||
|
customer_type: 'VIP', email: 'test@mixue.com',
|
||||||
|
}, tokens.admin)
|
||||||
|
check('admin POST /api/customers (创建)', r, 200)
|
||||||
|
const custCrudId = r.body?.data?.id
|
||||||
|
if (custCrudId) {
|
||||||
|
created.customers.push(custCrudId)
|
||||||
|
checkValue('默认 responsible_user_id = admin', r.body.data.responsible_user_id, adminUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
r = await req('POST', '/api/customers', { name: '' }, tokens.admin)
|
||||||
|
check('POST /api/customers (缺名称)', r, 400)
|
||||||
|
|
||||||
|
if (custCrudId) {
|
||||||
|
r = await req('GET', '/api/customers/' + custCrudId, null, tokens.admin)
|
||||||
check('GET /api/customers/:id (详情)', r, 200)
|
check('GET /api/customers/:id (详情)', r, 200)
|
||||||
|
|
||||||
r = await req('PUT', '/api/customers/' + custId, { phone: '13900002222', customer_type: 'Normal', remark: '更新备注' })
|
r = await req('PUT', '/api/customers/' + custCrudId, { phone: '13900006666', remark: '更新备注' }, tokens.admin)
|
||||||
check('PUT /api/customers/:id (更新类型+电话)', r, 200)
|
check('PUT /api/customers/:id (更新)', r, 200)
|
||||||
if (r.body?.data) console.log(` ↳ 更新后类型: ${r.body.data.customer_type}, 电话: ${r.body.data.phone}`)
|
|
||||||
|
|
||||||
r = await req('GET', '/api/customers?name=测试')
|
r = await req('GET', '/api/customers?name=CRUD', null, tokens.admin)
|
||||||
check('GET /api/customers?name=测试 (按姓名搜索)', r, 200)
|
check('GET /api/customers?name=CRUD (搜索)', r, 200)
|
||||||
|
|
||||||
r = await req('GET', '/api/customers?customer_type=Normal')
|
r = await req('GET', '/api/customers?customer_type=VIP', null, tokens.admin)
|
||||||
check('GET /api/customers?customer_type=Normal (按类型筛选)', r, 200)
|
check('GET /api/customers?customer_type=VIP (筛选)', r, 200)
|
||||||
if (r.body?.data) console.log(` ↳ Normal 类型共 ${r.body.data.total} 条`)
|
|
||||||
|
|
||||||
r = await req('DELETE', '/api/customers/' + custId)
|
|
||||||
check('DELETE /api/customers/:id (删除)', r, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r = await req('GET', '/api/customers/99999')
|
r = await req('GET', '/api/customers/99999', null, tokens.admin)
|
||||||
check('GET /api/customers/99999 (不存在)', r, 404, '→ 客户不存在')
|
check('GET /api/customers/99999 (不存在)', r, 404)
|
||||||
|
|
||||||
// ────────── 5. 员工管理 ──────────
|
// ══════════════════════════════════════════
|
||||||
console.log('\n━'.repeat(50))
|
// 【8】 employees CRUD 测试
|
||||||
console.log('【5】 员工管理 CRUD')
|
// ══════════════════════════════════════════
|
||||||
console.log('━'.repeat(50))
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【8】 employees CRUD')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
r = await req('GET', '/api/employees?page=1&pageSize=10')
|
r = await req('POST', '/api/employees', {
|
||||||
check('GET /api/employees (列表)', r, 200)
|
name: 'CRUD测试员工', gender: '男', age: 28, education: '本科',
|
||||||
|
department: '品控部', position: '品控专员', salary: 12000,
|
||||||
|
phone: '13700005555', email: 'emp@mixue.com', status: 1,
|
||||||
|
}, tokens.admin)
|
||||||
|
check('admin POST /api/employees (创建)', r, 200)
|
||||||
|
const empCrudId = r.body?.data?.id
|
||||||
|
if (empCrudId) created.employees.push(empCrudId)
|
||||||
|
|
||||||
r = await req('POST', '/api/employees', { name: '测试员工', gender: '男', age: 30, education: '本科', department: '研发部', position: '工程师', salary: 15000, phone: '13700000001', email: 'emp@test.com' })
|
if (empCrudId) {
|
||||||
check('POST /api/employees (创建)', r, 200)
|
r = await req('GET', '/api/employees/' + empCrudId, null, tokens.admin)
|
||||||
let empId = r.body?.data?.id
|
|
||||||
if (empId) console.log(` ↳ 新建员工 ID: ${empId}, 部门: ${r.body.data.department}`)
|
|
||||||
|
|
||||||
if (empId) {
|
|
||||||
r = await req('GET', '/api/employees/' + empId)
|
|
||||||
check('GET /api/employees/:id (详情)', r, 200)
|
check('GET /api/employees/:id (详情)', r, 200)
|
||||||
|
|
||||||
r = await req('PUT', '/api/employees/' + empId, { salary: 18000, position: '高级工程师' })
|
r = await req('PUT', '/api/employees/' + empCrudId, { salary: 15000, position: '高级品控专员' }, tokens.admin)
|
||||||
check('PUT /api/employees/:id (更新)', r, 200)
|
check('PUT /api/employees/:id (更新)', r, 200)
|
||||||
|
|
||||||
r = await req('GET', '/api/employees?status=1')
|
r = await req('GET', '/api/employees?status=1', null, tokens.admin)
|
||||||
check('GET /api/employees?status=1 (在职筛选)', r, 200)
|
check('GET /api/employees?status=1 (在职筛选)', r, 200)
|
||||||
|
|
||||||
r = await req('DELETE', '/api/employees/' + empId)
|
r = await req('GET', '/api/employees?department=品控部', null, tokens.admin)
|
||||||
check('DELETE /api/employees/:id (删除)', r, 200)
|
check('GET /api/employees?department=品控部 (部门筛选)', r, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────── 6. 产品管理 ──────────
|
// ══════════════════════════════════════════
|
||||||
console.log('\n━'.repeat(50))
|
// 【9】 products CRUD 测试
|
||||||
console.log('【6】 产品管理 CRUD')
|
// ══════════════════════════════════════════
|
||||||
console.log('━'.repeat(50))
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【9】 products CRUD(采购经理操作)')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
r = await req('GET', '/api/products?page=1&pageSize=10')
|
r = await req('POST', '/api/products', {
|
||||||
check('GET /api/products (列表)', r, 200)
|
name: '安岳柠檬-测试', type: '原材料', quantity: 10000,
|
||||||
|
price: 5.50, unit: 'kg', specification: '特级', supplier: '安岳柠檬基地',
|
||||||
|
}, tokens.zhaoqiang)
|
||||||
|
check('zhaoqiang POST /api/products (创建)', r, 200)
|
||||||
|
const prodId = r.body?.data?.id
|
||||||
|
if (prodId) created.products.push(prodId)
|
||||||
|
|
||||||
r = await req('POST', '/api/products', { name: '测试产品-X1', type: '电子产品', quantity: 500, price: 299.99, unit: '台', specification: '型号 X1-2026', supplier: '供应商A' })
|
r = await req('POST', '/api/products', { name: '' }, tokens.zhaoqiang)
|
||||||
check('POST /api/products (创建)', r, 200)
|
check('POST /api/products (缺名称)', r, 400)
|
||||||
let prodId = r.body?.data?.id
|
|
||||||
if (prodId) console.log(` ↳ 新建产品 ID: ${prodId}, 库存: ${r.body.data.quantity}`)
|
|
||||||
|
|
||||||
if (prodId) {
|
if (prodId) {
|
||||||
r = await req('GET', '/api/products/' + prodId)
|
r = await req('GET', '/api/products/' + prodId, null, tokens.zhaoqiang)
|
||||||
check('GET /api/products/:id (详情)', r, 200)
|
check('GET /api/products/:id (详情)', r, 200)
|
||||||
|
|
||||||
r = await req('PUT', '/api/products/' + prodId, { price: 259.99, quantity: 480 })
|
r = await req('PUT', '/api/products/' + prodId, { price: 6.00, quantity: 8000 }, tokens.zhaoqiang)
|
||||||
check('PUT /api/products/:id (更新价格库存)', r, 200)
|
check('PUT /api/products/:id (更新)', r, 200)
|
||||||
|
|
||||||
r = await req('GET', '/api/products?name=测试')
|
r = await req('GET', '/api/products?name=柠檬', null, tokens.zhaoqiang)
|
||||||
check('GET /api/products?name=测试 (搜索)', r, 200)
|
check('GET /api/products?name=柠檬 (搜索)', r, 200)
|
||||||
|
|
||||||
r = await req('DELETE', '/api/products/' + prodId)
|
r = await req('GET', '/api/products?type=原材料', null, tokens.zhaoqiang)
|
||||||
check('DELETE /api/products/:id (删除)', r, 200)
|
check('GET /api/products?type=原材料 (筛选)', r, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────── 7. 合同管理 ──────────
|
// ══════════════════════════════════════════
|
||||||
console.log('\n━'.repeat(50))
|
// 【10】 contracts CRUD 测试
|
||||||
console.log('【7】 合同管理 CRUD(关联客户+员工)')
|
// ══════════════════════════════════════════
|
||||||
console.log('━'.repeat(50))
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【10】 contracts CRUD(含 type 字段)')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
// 先创建客户和员工做外键
|
// 采购经理创建合同 → 自动 type='supply'
|
||||||
const cRes = await req('POST', '/api/customers', { name: '合同测试客户' })
|
r = await req('POST', '/api/contracts', {
|
||||||
const eRes = await req('POST', '/api/employees', { name: '合同业务员' })
|
customer_id: custAId, contract_name: '原材料采购合同', amount: 500000,
|
||||||
const cId = cRes.body?.data?.id
|
effective_date: '2026-06-01', expiry_date: '2027-05-31', status: '生效',
|
||||||
const eId = eRes.body?.data?.id
|
}, tokens.zhaoqiang)
|
||||||
console.log(` ↳ 先创建客户(${cId}) 和 员工(${eId}) 供合同关联`)
|
check('zhaoqiang POST /api/contracts (自动type=supply)', r, 200)
|
||||||
|
if (r.body?.data) {
|
||||||
r = await req('POST', '/api/contracts', { customer_id: cId, contract_name: '年度供货协议', contract_no: 'HT-2026-088', amount: 500000, effective_date: '2026-06-01', expiry_date: '2027-05-31', employee_id: eId, status: '生效' })
|
checkValue('合同type自动设为supply', r.body.data.type, 'supply')
|
||||||
check('POST /api/contracts (创建)', r, 200)
|
created.contracts.push(r.body.data.id)
|
||||||
let conId = r.body?.data?.id
|
|
||||||
if (conId) {
|
|
||||||
console.log(` ↳ 新建合同 ID: ${conId}`)
|
|
||||||
if (r.body?.data?.customer_name) console.log(` ↳ 联查客户: ${r.body.data.customer_name}, 业务员: ${r.body.data.employee_name}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r = await req('POST', '/api/contracts', { customer_id: 99999, contract_name: '无效合同' })
|
// 管理员创建合同 → 默认 type='franchise'
|
||||||
check('POST /api/contracts (客户不存在)', r, 400, '→ 关联客户不存在')
|
r = await req('POST', '/api/contracts', {
|
||||||
|
customer_id: custAId, contract_name: '加盟协议-测试', amount: 300000,
|
||||||
if (conId) {
|
effective_date: '2026-06-01', expiry_date: '2027-05-31', status: '生效',
|
||||||
r = await req('GET', '/api/contracts')
|
employee_id: empLmId,
|
||||||
check('GET /api/contracts (列表)', r, 200)
|
}, tokens.admin)
|
||||||
|
check('admin POST /api/contracts (默认type=franchise)', r, 200)
|
||||||
r = await req('GET', '/api/contracts/' + conId)
|
if (r.body?.data) {
|
||||||
check('GET /api/contracts/:id (详情)', r, 200)
|
checkValue('合同type默认franchise', r.body.data.type, 'franchise')
|
||||||
|
check('合同有 customer_name 联查', { status: r.body.data.customer_name ? 200 : 500 }, 200)
|
||||||
r = await req('PUT', '/api/contracts/' + conId, { status: '完成', remark: '已履约' })
|
created.contracts.push(r.body.data.id)
|
||||||
check('PUT /api/contracts/:id (更新状态)', r, 200)
|
|
||||||
|
|
||||||
r = await req('GET', '/api/contracts?status=完成')
|
|
||||||
check('GET /api/contracts?status=完成 (状态筛选)', r, 200)
|
|
||||||
|
|
||||||
r = await req('DELETE', '/api/contracts/' + conId)
|
|
||||||
check('DELETE /api/contracts/:id (删除)', r, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理外键依赖数据
|
r = await req('POST', '/api/contracts', { customer_id: 99999, contract_name: '无效' }, tokens.admin)
|
||||||
if (cId) await req('DELETE', '/api/customers/' + cId)
|
check('POST /api/contracts (客户不存在)', r, 400)
|
||||||
if (eId) await req('DELETE', '/api/employees/' + eId)
|
|
||||||
|
|
||||||
// ────────── 8. 售后管理 ──────────
|
r = await req('GET', '/api/contracts', null, tokens.admin)
|
||||||
console.log('\n━'.repeat(50))
|
check('GET /api/contracts (列表)', r, 200)
|
||||||
console.log('【8】 售后管理 CRUD(关联客户+员工)')
|
|
||||||
console.log('━'.repeat(50))
|
|
||||||
|
|
||||||
const c2 = await req('POST', '/api/customers', { name: '售后测试客户' })
|
// ══════════════════════════════════════════
|
||||||
const e2 = await req('POST', '/api/employees', { name: '售后处理员' })
|
// 【11】 after-sales CRUD 测试
|
||||||
const cId2 = c2.body?.data?.id
|
// ══════════════════════════════════════════
|
||||||
const eId2 = e2.body?.data?.id
|
console.log('\n' + '━'.repeat(55))
|
||||||
console.log(` ↳ 先创建客户(${cId2}) 和 员工(${eId2})`)
|
console.log('【11】 after-sales CRUD')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
r = await req('POST', '/api/after-sales', { customer_id: cId2, feedback: '设备运行异常,需技术支持', employee_id: eId2, handle_method: '更换故障模块', handle_status: '处理中', service_date: '2026-06-20' })
|
r = await req('POST', '/api/after-sales', {
|
||||||
check('POST /api/after-sales (创建)', r, 200)
|
customer_id: custAId, feedback: '冰淇淋机不出料',
|
||||||
let asId = r.body?.data?.id
|
employee_id: empWlId, handle_method: '派工程师上门检修',
|
||||||
|
handle_status: '处理中', service_date: '2026-06-20',
|
||||||
|
}, tokens.wangli)
|
||||||
|
check('wangli POST /api/after-sales (创建)', r, 200)
|
||||||
|
const asId = r.body?.data?.id
|
||||||
if (asId) {
|
if (asId) {
|
||||||
console.log(` ↳ 新建售后 ID: ${asId}`)
|
created.afterSales.push(asId)
|
||||||
if (r.body?.data?.customer_name) console.log(` ↳ 联查客户: ${r.body.data.customer_name}, 处理人: ${r.body.data.employee_name}`)
|
check('售后有 customer_name 联查', { status: r.body.data.customer_name ? 200 : 500 }, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
r = await req('POST', '/api/after-sales', { customer_id: 99999, feedback: '测试' })
|
r = await req('POST', '/api/after-sales', { customer_id: 99999, feedback: '测试' }, tokens.wangli)
|
||||||
check('POST /api/after-sales (客户不存在)', r, 400, '→ 关联客户不存在')
|
check('POST /api/after-sales (客户不存在)', r, 400)
|
||||||
|
|
||||||
|
r = await req('POST', '/api/after-sales', { customer_id: custAId }, tokens.wangli)
|
||||||
|
check('POST /api/after-sales (缺feedback)', r, 400)
|
||||||
|
|
||||||
if (asId) {
|
if (asId) {
|
||||||
r = await req('GET', '/api/after-sales')
|
r = await req('GET', '/api/after-sales/' + asId, null, tokens.wangli)
|
||||||
check('GET /api/after-sales (列表)', r, 200)
|
|
||||||
|
|
||||||
r = await req('GET', '/api/after-sales/' + asId)
|
|
||||||
check('GET /api/after-sales/:id (详情)', r, 200)
|
check('GET /api/after-sales/:id (详情)', r, 200)
|
||||||
|
|
||||||
r = await req('PUT', '/api/after-sales/' + asId, { handle_status: '已完成', handle_method: '远程升级固件解决' })
|
r = await req('PUT', '/api/after-sales/' + asId, { handle_status: '已完成', handle_method: '更换密封圈' }, tokens.wangli)
|
||||||
check('PUT /api/after-sales/:id (更新)', r, 200)
|
check('PUT /api/after-sales/:id (更新)', r, 200)
|
||||||
|
|
||||||
r = await req('GET', '/api/after-sales?handle_status=已完成')
|
r = await req('GET', '/api/after-sales?handle_status=已完成', null, tokens.wangli)
|
||||||
check('GET /api/after-sales?handle_status=已完成 (状态筛选)', r, 200)
|
check('GET /api/after-sales?handle_status=已完成 (筛选)', r, 200)
|
||||||
|
|
||||||
r = await req('DELETE', '/api/after-sales/' + asId)
|
|
||||||
check('DELETE /api/after-sales/:id (删除)', r, 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cId2) await req('DELETE', '/api/customers/' + cId2)
|
// ══════════════════════════════════════════
|
||||||
if (eId2) await req('DELETE', '/api/employees/' + eId2)
|
// 【12】 个人信息 & 改密
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【12】 个人信息 & 改密')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
// ────────── 收尾 ──────────
|
r = await req('POST', '/api/user/logout', null, tokens.admin)
|
||||||
console.log('\n' + '═'.repeat(50))
|
check('POST /api/user/logout', r, 200)
|
||||||
|
|
||||||
|
r = await req('PUT', '/api/user/password', { oldPassword: 'wrong', newPassword: 'newpwd999' }, tokens.admin)
|
||||||
|
check('PUT /api/user/password (旧密码错)', r, 400)
|
||||||
|
|
||||||
|
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123456' }, tokens.admin)
|
||||||
|
check('PUT /api/user/password (新旧相同)', r, 400)
|
||||||
|
|
||||||
|
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123' }, tokens.admin)
|
||||||
|
check('PUT /api/user/password (新密码太短)', r, 400)
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
// 【13】 数据清理
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '━'.repeat(55))
|
||||||
|
console.log('【13】 测试数据清理')
|
||||||
|
console.log('━'.repeat(55))
|
||||||
|
|
||||||
|
// 按依赖顺序删除
|
||||||
|
for (const id of created.afterSales) {
|
||||||
|
await req('DELETE', '/api/after-sales/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
for (const id of created.contracts) {
|
||||||
|
await req('DELETE', '/api/contracts/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
for (const id of created.customers) {
|
||||||
|
await req('DELETE', '/api/customers/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
for (const id of created.employees) {
|
||||||
|
await req('DELETE', '/api/employees/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
for (const id of created.products) {
|
||||||
|
await req('DELETE', '/api/products/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
for (const id of created.suppliers) {
|
||||||
|
await req('DELETE', '/api/suppliers/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
for (const id of created.users) {
|
||||||
|
await req('DELETE', '/api/users/' + id, null, tokens.admin)
|
||||||
|
}
|
||||||
|
console.log(' 🧹 清理完成')
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
// 收尾
|
||||||
|
// ══════════════════════════════════════════
|
||||||
|
console.log('\n' + '═'.repeat(55))
|
||||||
console.log(`测试完成: 通过 ${passCount} / 失败 ${failCount}`)
|
console.log(`测试完成: 通过 ${passCount} / 失败 ${failCount}`)
|
||||||
if (failCount > 0) {
|
if (failCount > 0) {
|
||||||
console.log('⚠️ 存在失败用例,请检查上方输出')
|
console.log('⚠️ 存在失败用例,请检查上方输出')
|
||||||
} else {
|
} else {
|
||||||
console.log('🎉 全部接口通过!')
|
console.log('🎉 全部接口通过!')
|
||||||
}
|
}
|
||||||
console.log('═'.repeat(50))
|
console.log('═'.repeat(55))
|
||||||
|
|
||||||
// 优雅退出
|
|
||||||
process.exit(failCount > 0 ? 1 : 0)
|
process.exit(failCount > 0 ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user