Files
backmanager-server/test-all.js
2026-06-24 23:12:43 +08:00

691 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test-all.js —— 全量接口测试:权限隔离 + 数据范围隔离 + CRUD
// 用法: 先启动服务 node server.js再运行 node test-all.js
require('dotenv').config()
const http = require('http')
const BASE = 'http://127.0.0.1:3000'
let failCount = 0
let passCount = 0
// 所有用户的 token
const tokens = {}
// 测试中创建的数据 ID用于最后清理
const created = {
customers: [],
employees: [],
products: [],
contracts: [],
afterSales: [],
suppliers: [],
users: [],
}
// ========== 工具函数 ==========
function req(method, path, body, token) {
return new Promise((resolve, reject) => {
const u = new URL(path, BASE)
const opts = {
hostname: u.hostname, port: u.port, path: u.pathname + u.search, method,
headers: { 'Content-Type': 'application/json' },
}
if (token) opts.headers['Authorization'] = 'Bearer ' + token
const r = http.request(opts, (res) => {
let d = ''
res.on('data', (c) => (d += c))
res.on('end', () => {
try { resolve({ status: res.statusCode, body: JSON.parse(d) }) }
catch { resolve({ status: res.statusCode, body: d.substring(0, 500) }) }
})
})
r.on('error', reject)
if (body) r.write(JSON.stringify(body))
r.end()
})
}
function check(label, res, expectStatus) {
if (res.status === expectStatus) {
passCount++
console.log(`${label}`)
} else {
failCount++
console.log(`${label} 预期 HTTP ${expectStatus}, 实际 HTTP ${res.status}`)
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}`)
}
}
// ========== 主流程 ==========
async function main() {
console.log('╔══════════════════════════════════════════════════╗')
console.log('║ 蜜雪冰城企业管理系统 — 全量接口测试 ║')
console.log('╚══════════════════════════════════════════════════╝')
console.log(`服务地址: ${BASE}\n`)
// ══════════════════════════════════════════
// 【1】 多角色登录
// ══════════════════════════════════════════
console.log('━'.repeat(55))
console.log('【1】 多角色登录')
console.log('━'.repeat(55))
const accounts = [
{ 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) {
tokens[acct.username] = r.body.data.token
}
}
// 错误密码
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: '' })
check('POST /api/user/login (空参数)', r, 400)
// ══════════════════════════════════════════
// 【2】 JWT payload 验证
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【2】 JWT payload 验证(新字段)')
console.log('━'.repeat(55))
// 验证 admin 登录返回的 userInfo 结构
const adminLogin = await req('POST', '/api/user/login', { username: 'admin', password: '123456' })
const adminInfo = adminLogin.body?.data?.userInfo
if (adminInfo) {
checkValue('admin userInfo.name', adminInfo.name, '系统管理员')
checkValue('admin userInfo.roleName', adminInfo.roleName, 'admin')
checkValue('admin userInfo.department', adminInfo.department, '信息技术部')
check('admin userInfo.permissions 是数组', { status: Array.isArray(adminInfo.permissions) ? 200 : 500 }, 200)
checkValue('admin permissions 数量', adminInfo.permissions?.length, 25)
}
// 验证 liming招商经理的权限
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)
}
// 验证 /api/user/info 返回新字段
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)
}
// ══════════════════════════════════════════
// 【3】 权限隔离测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【3】 权限隔离测试(无权限应返回 403')
console.log('━'.repeat(55))
// 招商经理 → products无权限
r = await req('GET', '/api/products', null, tokens.liming)
check('liming → GET /api/products → 403', r, 403)
r = await req('POST', '/api/products', { name: 'x' }, tokens.liming)
check('liming → POST /api/products → 403', r, 403)
// 招商经理 → suppliers无权限
r = await req('GET', '/api/suppliers', null, tokens.liming)
check('liming → GET /api/suppliers → 403', r, 403)
// 招商经理 → after-sales无权限
r = await req('GET', '/api/after-sales', null, tokens.liming)
check('liming → GET /api/after-sales → 403', r, 403)
// 采购经理 → customers无权限
r = await req('GET', '/api/customers', null, tokens.zhaoqiang)
check('zhaoqiang → GET /api/customers → 403', r, 403)
// 采购经理 → after-sales无权限
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: '郑州',
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)
r = await req('PUT', '/api/customers/' + custCrudId, { phone: '13900006666', remark: '更新备注' }, tokens.admin)
check('PUT /api/customers/:id (更新)', r, 200)
r = await req('GET', '/api/customers?name=CRUD', null, tokens.admin)
check('GET /api/customers?name=CRUD (搜索)', r, 200)
}
r = await req('GET', '/api/customers/99999', null, tokens.admin)
check('GET /api/customers/99999 (不存在)', r, 404)
// ══════════════════════════════════════════
// 【8】 employees CRUD 测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【8】 employees CRUD')
console.log('━'.repeat(55))
r = await req('POST', '/api/employees', {
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)
if (empCrudId) {
r = await req('GET', '/api/employees/' + empCrudId, null, tokens.admin)
check('GET /api/employees/:id (详情)', r, 200)
r = await req('PUT', '/api/employees/' + empCrudId, { salary: 15000, position: '高级品控专员' }, tokens.admin)
check('PUT /api/employees/:id (更新)', r, 200)
r = await req('GET', '/api/employees?status=1', null, tokens.admin)
check('GET /api/employees?status=1 (在职筛选)', r, 200)
r = await req('GET', '/api/employees?department=品控部', null, tokens.admin)
check('GET /api/employees?department=品控部 (部门筛选)', r, 200)
}
// ══════════════════════════════════════════
// 【9】 products CRUD 测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【9】 products CRUD采购经理操作')
console.log('━'.repeat(55))
r = await req('POST', '/api/products', {
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: '' }, tokens.zhaoqiang)
check('POST /api/products (缺名称)', r, 400)
if (prodId) {
r = await req('GET', '/api/products/' + prodId, null, tokens.zhaoqiang)
check('GET /api/products/:id (详情)', r, 200)
r = await req('PUT', '/api/products/' + prodId, { price: 6.00, quantity: 8000 }, tokens.zhaoqiang)
check('PUT /api/products/:id (更新)', r, 200)
r = await req('GET', '/api/products?name=柠檬', null, tokens.zhaoqiang)
check('GET /api/products?name=柠檬 (搜索)', r, 200)
r = await req('GET', '/api/products?type=原材料', null, tokens.zhaoqiang)
check('GET /api/products?type=原材料 (筛选)', r, 200)
}
// ══════════════════════════════════════════
// 【10】 contracts CRUD 测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【10】 contracts CRUD含 type 字段)')
console.log('━'.repeat(55))
// 采购经理创建合同 → 自动 type='supply'
r = await req('POST', '/api/contracts', {
customer_id: custAId, contract_name: '原材料采购合同', amount: 500000,
effective_date: '2026-06-01', expiry_date: '2027-05-31', status: '生效',
}, tokens.zhaoqiang)
check('zhaoqiang POST /api/contracts (自动type=supply)', r, 200)
if (r.body?.data) {
checkValue('合同type自动设为supply', r.body.data.type, 'supply')
created.contracts.push(r.body.data.id)
}
// 管理员创建合同 → 默认 type='franchise'
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)
}
r = await req('POST', '/api/contracts', { customer_id: 99999, contract_name: '无效' }, tokens.admin)
check('POST /api/contracts (客户不存在)', r, 400)
r = await req('GET', '/api/contracts', null, tokens.admin)
check('GET /api/contracts (列表)', r, 200)
// ══════════════════════════════════════════
// 【11】 after-sales CRUD 测试
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【11】 after-sales CRUD')
console.log('━'.repeat(55))
r = await req('POST', '/api/after-sales', {
customer_id: custAId, feedback: '冰淇淋机不出料',
employee_id: empWlId, handle_method: '派工程师上门检修',
handle_status: '处理中', service_date: '2026-06-20',
}, tokens.wangli)
check('wangli POST /api/after-sales (创建)', r, 200)
const asId = r.body?.data?.id
if (asId) {
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)
check('POST /api/after-sales (客户不存在)', r, 400)
r = await req('POST', '/api/after-sales', { customer_id: custAId }, tokens.wangli)
check('POST /api/after-sales (缺feedback)', r, 400)
if (asId) {
r = await req('GET', '/api/after-sales/' + asId, null, tokens.wangli)
check('GET /api/after-sales/:id (详情)', r, 200)
r = await req('PUT', '/api/after-sales/' + asId, { handle_status: '已完成', handle_method: '更换密封圈' }, tokens.wangli)
check('PUT /api/after-sales/:id (更新)', r, 200)
r = await req('GET', '/api/after-sales?handle_status=已完成', null, tokens.wangli)
check('GET /api/after-sales?handle_status=已完成 (筛选)', r, 200)
}
// ══════════════════════════════════════════
// 【12】 个人信息 & 改密
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【12】 个人信息 & 改密')
console.log('━'.repeat(55))
r = await req('POST', '/api/user/logout', null, tokens.admin)
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}`)
if (failCount > 0) {
console.log('⚠️ 存在失败用例,请检查上方输出')
} else {
console.log('🎉 全部接口通过!')
}
console.log('═'.repeat(55))
process.exit(failCount > 0 ? 1 : 0)
}
main().catch((e) => {
console.error('测试异常:', e.message)
console.error('请确认服务已启动: node server.js')
process.exit(1)
})