添加了更多功能

This commit is contained in:
2026-06-24 22:44:14 +08:00
parent ff7173aa68
commit 56af6ab269
11 changed files with 1416 additions and 359 deletions

44
CLAUDE.md Normal file
View 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
View File

@@ -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
View 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 子句的条件
* - denytrue 表示该角色无权访问此资源
*/
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 }

View File

@@ -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' })

View File

@@ -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' })

View File

@@ -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' })

View File

@@ -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
View 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 }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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: '系统管理员' },
{ username: 'liming', password: '123456', label: '招商经理' },
{ username: 'wangli', password: '123456', label: '运营经理' },
{ 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) { if (r.body?.data?.token) {
token = r.body.data.token tokens[acct.username] = r.body.data.token
console.log(` ↳ 角色: ${r.body.data.userInfo.role}, 姓名: ${r.body.data.userInfo.real_name}`) }
} }
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) {
r = await req('POST', '/api/user/logout') checkValue('admin userInfo.name', adminInfo.name, '系统管理员')
check('POST /api/user/logout', r, 200, '→ JWT 无状态,前端删 token 即可') checkValue('admin userInfo.roleName', adminInfo.roleName, 'admin')
checkValue('admin userInfo.department', adminInfo.department, '信息技术部')
r = await req('PUT', '/api/user/password', { oldPassword: 'wrong', newPassword: 'newpwd999' }) check('admin userInfo.permissions 是数组', { status: Array.isArray(adminInfo.permissions) ? 200 : 500 }, 200)
check('PUT /api/user/password (旧密码错)', r, 400, '→ 旧密码错误') checkValue('admin permissions 数量', adminInfo.permissions?.length, 25)
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123456' })
check('PUT /api/user/password (新旧相同)', r, 400, '→ 新密码不能与旧密码相同')
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123' })
check('PUT /api/user/password (新密码太短)', r, 400, '→ 新密码至少 6 位')
// ────────── 3. 用户管理 CRUD ──────────
console.log('\n━'.repeat(50))
console.log('【3】 用户管理 CRUD管理员')
console.log('━'.repeat(50))
r = await req('GET', '/api/users?page=1&pageSize=10')
check('GET /api/users (列表)', r, 200)
if (r.body?.data) console.log(` ↳ 共 ${r.body.data.total} 个用户, 本页 ${r.body.data.list?.length}`)
const ts = Date.now()
const newUser = `tester_${ts}`
r = await req('POST', '/api/users', { username: newUser, password: 'pass123', real_name: '测试员', role: 'user' })
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' })
check('POST /api/users (重复用户名)', r, 409, '→ 用户名已存在')
r = await req('POST', '/api/users', { username: 'bad', password: '12', role: 'superman' })
check('POST /api/users (非法角色)', r, 400, '→ 角色只能为 admin 或 user')
if (userId) {
r = await req('GET', '/api/users/' + userId)
check('GET /api/users/:id (详情)', r, 200)
r = await req('PUT', '/api/users/' + userId, { real_name: '改名后的测试员', status: 1 })
check('PUT /api/users/:id (更新姓名)', r, 200)
if (r.body?.data) console.log(` ↳ 新姓名: ${r.body.data.real_name}`)
r = await req('PUT', '/api/users/' + userId, { password: 'newpass456' })
check('PUT /api/users/:id (管理员重置密码)', r, 200, '→ 管理员可直接改他人密码')
// 用新用户登录验证改密成功
const r2 = await req('POST', '/api/user/login', { username: newUser, password: 'newpass456' }, false)
check(' └ 新用户用新密码登录', r2, 200)
// 测试非管理员访问
const userToken = r2.body?.data?.token
if (userToken) {
const oldToken = token
token = userToken
r = await req('GET', '/api/users')
check(' └ 普通用户访问用户列表', r, 403, '→ 需要管理员权限')
token = oldToken
} }
r = await req('DELETE', '/api/users/' + userId) // 验证 liming招商经理的权限
check('DELETE /api/users/:id (删除)', r, 200) 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('DELETE', '/api/users/1') // 验证 /api/user/info 返回新字段
check('DELETE /api/users/1 (删自己)', 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('GET', '/api/users', null, false) // ══════════════════════════════════════════
check('GET /api/users (无 token)', r, 401, '→ 未登录') // 【3】 权限隔离测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【3】 权限隔离测试(无权限应返回 403')
console.log('━'.repeat(55))
// ────────── 4. 客户管理 ────────── // 招商经理 → products无权限
console.log('\n━'.repeat(50)) r = await req('GET', '/api/products', null, tokens.liming)
console.log('【4】 客户管理 CRUD') check('liming → GET /api/products → 403', r, 403)
console.log('━'.repeat(50))
r = await req('GET', '/api/customers?page=1&pageSize=10') r = await req('POST', '/api/products', { name: 'x' }, tokens.liming)
check('GET /api/customers (列表)', r, 200) check('liming → POST /api/products → 403', r, 403)
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' }) // 招商经理 → suppliers无权限
check('POST /api/customers (创建VIP客户)', r, 200) r = await req('GET', '/api/suppliers', null, tokens.liming)
let custId = r.body?.data?.id check('liming → GET /api/suppliers → 403', r, 403)
if (custId) console.log(` ↳ 新建客户 ID: ${custId}, 类型: ${r.body.data.customer_type}`)
r = await req('POST', '/api/customers', { name: '' }) // 招商经理 → after-sales无权限
check('POST /api/customers (缺姓名)', r, 400, '→ 客户姓名必填') r = await req('GET', '/api/after-sales', null, tokens.liming)
check('liming → GET /api/after-sales → 403', r, 403)
// 不传 customer_type 应为默认 Normal // 采购经理 → customers无权限
r = await req('POST', '/api/customers', { name: '默认类型测试' }) r = await req('GET', '/api/customers', null, tokens.zhaoqiang)
check('POST /api/customers (不传类型默认Normal)', r, 200) check('zhaoqiang → GET /api/customers → 403', r, 403)
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) { // 采购经理 → after-sales无权限
r = await req('GET', '/api/customers/' + custId) r = await req('GET', '/api/after-sales', null, tokens.zhaoqiang)
check('zhaoqiang → GET /api/after-sales → 403', r, 403)
// 财务人员 → products无权限
r = await req('GET', '/api/products', null, tokens.chenfang)
check('chenfang → GET /api/products → 403', r, 403)
// 财务人员 → suppliers无权限
r = await req('GET', '/api/suppliers', null, tokens.chenfang)
check('chenfang → GET /api/suppliers → 403', r, 403)
// 财务人员 → 创建 customers只有 read 权限,无 create
r = await req('POST', '/api/customers', { name: '财务创建' }, tokens.chenfang)
check('chenfang → POST /api/customers → 403', r, 403)
// 财务人员 → 创建 employees只有 read无 create
r = await req('POST', '/api/employees', { name: '财务创建' }, tokens.chenfang)
check('chenfang → POST /api/employees → 403', r, 403)
// 总经理 → 创建 customers只有 read无 create
r = await req('POST', '/api/customers', { name: '总经理创建' }, tokens.zhangchao)
check('zhangchao → POST /api/customers → 403', r, 403)
// 总经理 → 创建 contracts只有 read无 create
r = await req('POST', '/api/contracts', { contract_name: 'x', customer_id: 1 }, tokens.zhangchao)
check('zhangchao → POST /api/contracts → 403', r, 403)
// 非管理员 → 用户管理
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('PUT', '/api/suppliers/' + supId, { phone: '13800008888', remark: '更新联系方式' }, tokens.zhaoqiang)
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)
}
// ══════════════════════════════════════════
// 【6】 users CRUD 测试(适配新字段)
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【6】 users CRUD管理员操作')
console.log('━'.repeat(55))
r = await req('GET', '/api/users', null, tokens.admin)
check('admin GET /api/users (列表)', r, 200)
if (r.body?.data) console.log(` ↳ 用户总数: ${r.body.data.total}`)
// 创建新用户,关联到已有员工
r = await req('POST', '/api/users', {
username: 'testuser_' + Date.now(),
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/users/' + newUserId, null, tokens.admin)
check('admin GET /api/users/:id (详情)', r, 200)
r = await req('PUT', '/api/users/' + newUserId, { is_active: 0 }, tokens.admin)
check('admin PUT /api/users/:id (禁用)', r, 200)
if (r.body?.data) checkValue('is_active 已禁用', r.body.data.is_active, 0)
r = await req('PUT', '/api/users/' + newUserId, { is_active: 1, password: 'newpass123' }, tokens.admin)
check('admin PUT /api/users/:id (启用+重置密码)', r, 200)
}
// admin 不能删除自己(先获取 admin 的真实 ID
const adminUser = allUsers.body?.data?.list?.find(u => u.username === 'admin')
const adminUserId = adminUser?.id
r = await req('DELETE', '/api/users/' + adminUserId, null, tokens.admin)
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,
effective_date: '2026-06-01', expiry_date: '2027-05-31', status: '生效',
employee_id: empLmId,
}, tokens.admin)
check('admin POST /api/contracts (默认type=franchise)', r, 200)
if (r.body?.data) {
checkValue('合同type默认franchise', r.body.data.type, 'franchise')
check('合同有 customer_name 联查', { status: r.body.data.customer_name ? 200 : 500 }, 200)
created.contracts.push(r.body.data.id)
}
if (conId) { r = await req('POST', '/api/contracts', { customer_id: 99999, contract_name: '无效' }, tokens.admin)
r = await req('GET', '/api/contracts') check('POST /api/contracts (客户不存在)', r, 400)
r = await req('GET', '/api/contracts', null, tokens.admin)
check('GET /api/contracts (列表)', r, 200) check('GET /api/contracts (列表)', r, 200)
r = await req('GET', '/api/contracts/' + conId) // ══════════════════════════════════════════
check('GET /api/contracts/:id (详情)', r, 200) // 【11】 after-sales CRUD 测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【11】 after-sales CRUD')
console.log('━'.repeat(55))
r = await req('PUT', '/api/contracts/' + conId, { status: '完成', remark: '已履约' }) r = await req('POST', '/api/after-sales', {
check('PUT /api/contracts/:id (更新状态)', r, 200) customer_id: custAId, feedback: '冰淇淋机不出料',
employee_id: empWlId, handle_method: '派工程师上门检修',
r = await req('GET', '/api/contracts?status=完成') handle_status: '处理中', service_date: '2026-06-20',
check('GET /api/contracts?status=完成 (状态筛选)', r, 200) }, tokens.wangli)
check('wangli POST /api/after-sales (创建)', r, 200)
r = await req('DELETE', '/api/contracts/' + conId) const asId = r.body?.data?.id
check('DELETE /api/contracts/:id (删除)', r, 200) if (asId) {
created.afterSales.push(asId)
check('售后有 customer_name 联查', { status: r.body.data.customer_name ? 200 : 500 }, 200)
} }
// 清理外键依赖数据 r = await req('POST', '/api/after-sales', { customer_id: 99999, feedback: '测试' }, tokens.wangli)
if (cId) await req('DELETE', '/api/customers/' + cId) check('POST /api/after-sales (客户不存在)', r, 400)
if (eId) await req('DELETE', '/api/employees/' + eId)
// ────────── 8. 售后管理 ────────── r = await req('POST', '/api/after-sales', { customer_id: custAId }, tokens.wangli)
console.log('\n━'.repeat(50)) check('POST /api/after-sales (缺feedback)', r, 400)
console.log('【8】 售后管理 CRUD关联客户+员工)')
console.log('━'.repeat(50))
const c2 = await req('POST', '/api/customers', { name: '售后测试客户' })
const e2 = await req('POST', '/api/employees', { name: '售后处理员' })
const cId2 = c2.body?.data?.id
const eId2 = e2.body?.data?.id
console.log(` ↳ 先创建客户(${cId2}) 和 员工(${eId2})`)
r = await req('POST', '/api/after-sales', { customer_id: cId2, feedback: '设备运行异常,需技术支持', employee_id: eId2, handle_method: '更换故障模块', handle_status: '处理中', service_date: '2026-06-20' })
check('POST /api/after-sales (创建)', r, 200)
let asId = r.body?.data?.id
if (asId) {
console.log(` ↳ 新建售后 ID: ${asId}`)
if (r.body?.data?.customer_name) console.log(` ↳ 联查客户: ${r.body.data.customer_name}, 处理人: ${r.body.data.employee_name}`)
}
r = await req('POST', '/api/after-sales', { customer_id: 99999, feedback: '测试' })
check('POST /api/after-sales (客户不存在)', 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)
} }