Files
backmanager-server/test-all.js

728 lines
33 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}`)
const preview = JSON.stringify(res.body).substring(0, 300)
if (preview) console.log(` 响应: ${preview}`)
}
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('║ 蜜雪冰城企业管理系统 — 全量接口测试 V2 ║')
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: 'zhangchao', password: '123456', label: '总经理办公室' },
{ username: 'liming', password: '123456', label: '招商部' },
{ username: 'wangli', password: '123456', label: '运营部' },
{ username: 'zhaoqiang', password: '123456', label: '采购部' },
{ username: 'chenfang', 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 (错误密码 → 400)', r, 400)
r = await req('POST', '/api/user/login', { username: '', password: '' })
check('POST /api/user/login (空参数 → 400)', r, 400)
// ══════════════════════════════════════════
// 【2】 JWT payload 验证(新字段名)
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【2】 JWT payload 验证departmentName / departmentDesc / permissions')
console.log('━'.repeat(55))
const adminLogin = await req('POST', '/api/user/login', { username: 'admin', password: '123456' })
const adminInfo = adminLogin.body?.data?.userInfo
if (adminInfo) {
checkValue('admin name', adminInfo.name, '系统管理员')
checkValue('admin departmentName', adminInfo.departmentName, 'admin')
checkValue('admin departmentDesc', adminInfo.departmentDesc, '信息技术部')
check('admin permissions 是数组', { status: Array.isArray(adminInfo.permissions) ? 200 : 500 }, 200)
checkValue('admin permissions 数量', adminInfo.permissions?.length, 25)
}
const limingLogin = await req('POST', '/api/user/login', { username: 'liming', password: '123456' })
const limingInfo = limingLogin.body?.data?.userInfo
if (limingInfo) {
checkValue('liming name', limingInfo.name, '李明')
checkValue('liming departmentName', limingInfo.departmentName, 'franchise_manager')
checkValue('liming departmentDesc', limingInfo.departmentDesc, '招商部')
check('liming 有 customer:read', { status: limingInfo.permissions?.includes('customer:read') ? 200 : 500 }, 200)
check('liming 有 customer:create', { status: limingInfo.permissions?.includes('customer:create') ? 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)
check('liming 无 after_sale:read', { status: !limingInfo.permissions?.includes('after_sale:read') ? 200 : 500 }, 200)
}
const zhaoqiangLogin = await req('POST', '/api/user/login', { username: 'zhaoqiang', password: '123456' })
const zhaoqiangInfo = zhaoqiangLogin.body?.data?.userInfo
if (zhaoqiangInfo) {
checkValue('zhaoqiang name', zhaoqiangInfo.name, '赵强')
check('zhaoqiang 有 customer:read新增权限', { status: zhaoqiangInfo.permissions?.includes('customer:read') ? 200 : 500 }, 200)
check('zhaoqiang 有 supplier:create', { status: zhaoqiangInfo.permissions?.includes('supplier:create') ? 200 : 500 }, 200)
}
// GET /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 有 dept_name', { status: r.body.data.dept_name ? 200 : 500 }, 200)
check('info 有 dept_desc', { status: r.body.data.dept_desc ? 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)
// 运营部 → products无权限
r = await req('GET', '/api/products', null, tokens.wangli)
check('wangli → GET /api/products → 403', r, 403)
// 运营部 → suppliers无权限
r = await req('GET', '/api/suppliers', null, tokens.wangli)
check('wangli → GET /api/suppliers → 403', r, 403)
// 运营部 → contracts无权限
r = await req('GET', '/api/contracts', null, tokens.wangli)
check('wangli → GET /api/contracts → 403 (运营部无合同权限)', r, 403)
// 采购部 → after-sales无权限
r = await req('GET', '/api/after-sales', null, tokens.zhaoqiang)
check('zhaoqiang → GET /api/after-sales → 403', r, 403)
// 财务部 → POST customers只有 read
r = await req('POST', '/api/customers', { name: '财务创建' }, tokens.chenfang)
check('chenfang → POST /api/customers → 403 (仅可读)', r, 403)
// 财务部 → POST employees只有 read
r = await req('POST', '/api/employees', { name: '财务创建' }, tokens.chenfang)
check('chenfang → POST /api/employees → 403 (仅可读)', r, 403)
// 总经理 → POST customers只有 read
r = await req('POST', '/api/customers', { name: '总经理创建' }, tokens.zhangchao)
check('zhangchao → POST /api/customers → 403 (仅可读)', r, 403)
// 总经理 → POST contracts只有 read
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))
r = await req('GET', '/api/user/list', null, tokens.admin)
check('GET /api/user/list (admin)', r, 200)
if (r.body?.data) {
checkValue('user/list 数量 >= 6', r.body.data.length >= 6 ? 'YES' : 'NO', 'YES')
}
// 放宽为 auth 后,非管理员也能调
r = await req('GET', '/api/user/list', null, tokens.liming)
check('GET /api/user/list (liming auth即可)', r, 200)
r = await req('GET', '/api/employees/simple', null, tokens.liming)
check('GET /api/employees/simple (liming)', r, 200)
r = await req('GET', '/api/customers/simple', null, tokens.liming)
check('GET /api/customers/simple (liming)', r, 200)
if (r.body?.data) {
check('customers/simple 有 id+name', { status: r.body.data[0]?.id && r.body.data[0]?.name ? 200 : 500 }, 200)
}
r = await req('GET', '/api/suppliers/simple', null, tokens.zhaoqiang)
check('GET /api/suppliers/simple (zhaoqiang)', r, 200)
// 无 supplier:read 的看不到供应商简易列表
r = await req('GET', '/api/suppliers/simple', null, tokens.liming)
check('GET /api/suppliers/simple (liming 无权限 → 403)', r, 403)
// ══════════════════════════════════════════
// 【5】 数据范围隔离测试(过渡期规则)
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【5】 数据范围隔离测试')
console.log('━'.repeat(55))
// 5.1 加盟商:招商部看全量(过渡期)
r = await req('GET', '/api/customers', null, tokens.admin)
check('admin GET /api/customers (全量)', r, 200)
const adminCustCount = r.body?.data?.total || 0
r = await req('GET', '/api/customers', null, tokens.liming)
check('liming GET /api/customers (全量-过渡期)', r, 200)
if (r.body?.data) {
checkValue('liming 看到加盟商数 = admin', r.body.data.total >= adminCustCount ? 'YES' : 'NO', 'YES')
}
r = await req('GET', '/api/customers', null, tokens.wangli)
check('wangli GET /api/customers (全量-过渡期)', r, 200)
if (r.body?.data) {
checkValue('wangli 看到加盟商数 = admin', r.body.data.total >= adminCustCount ? 'YES' : 'NO', 'YES')
}
// 采购部现在也能看加盟商
r = await req('GET', '/api/customers', null, tokens.zhaoqiang)
check('zhaoqiang GET /api/customers (采购部可看)', r, 200)
// 5.2 合同类型隔离
// 全量看
r = await req('GET', '/api/contracts', null, tokens.admin)
check('admin GET /api/contracts (全量)', r, 200)
// 招商部只看 franchise
r = await req('GET', '/api/contracts', null, tokens.liming)
check('liming GET /api/contracts (只看加盟合同)', r, 200)
if (r.body?.data) {
const allFranchise = (r.body.data.list || []).every(c => c.type === 'franchise')
checkValue('liming 合同全是 franchise', allFranchise ? 'YES' : 'NO', 'YES')
check('liming 有加盟合同', { status: r.body.data.total > 0 ? 200 : 500 }, 200)
}
// 采购部只看 supply
r = await req('GET', '/api/contracts', null, tokens.zhaoqiang)
check('zhaoqiang GET /api/contracts (只看采购合同)', r, 200)
if (r.body?.data) {
const allSupply = (r.body.data.list || []).every(c => c.type === 'supply')
checkValue('zhaoqiang 合同全是 supply', allSupply ? 'YES' : 'NO', 'YES')
}
// 运营部无合同权限(走不到 dataScope
r = await req('GET', '/api/contracts', null, tokens.wangli)
check('wangli → GET /api/contracts → 403', r, 403)
// 5.3 售后:运营部看全量(过渡期)
r = await req('GET', '/api/after-sales', null, tokens.wangli)
check('wangli GET /api/after-sales (全量-过渡期)', r, 200)
if (r.body?.data) {
check('wangli 看到售后数据', { status: r.body.data.total > 0 ? 200 : 500 }, 200)
}
r = await req('GET', '/api/after-sales', null, tokens.chenfang)
check('chenfang GET /api/after-sales (财务全量)', r, 200)
// 5.4 员工部门隔离
r = await req('GET', '/api/employees', null, tokens.liming)
check('liming GET /api/employees (部门隔离)', r, 200)
if (r.body?.data) {
const allInDept = (r.body.data.list || []).every(e => e.department === '招商部')
checkValue('liming 只看招商部员工', allInDept ? 'YES' : 'NO', 'YES')
}
r = await req('GET', '/api/employees', null, tokens.wangli)
check('wangli GET /api/employees (部门隔离)', r, 200)
if (r.body?.data) {
const allInDept = (r.body.data.list || []).every(e => e.department === '运营部')
checkValue('wangli 只看运营部员工', allInDept ? 'YES' : 'NO', 'YES')
}
r = await req('GET', '/api/employees', null, tokens.admin)
check('admin GET /api/employees (全量)', r, 200)
// ══════════════════════════════════════════
// 【6】 合同类型校验中间件
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【6】 validateContractType 中间件测试')
console.log('━'.repeat(55))
// 获取一个加盟商 ID 用于测试
const custList = await req('GET', '/api/customers?pageSize=1', null, tokens.admin)
const testCustId = custList.body?.data?.list?.[0]?.id
// 招商部不能创建采购合同
r = await req('POST', '/api/contracts', {
customer_id: testCustId, contract_name: '越权采购合同',
type: 'supply', effective_date: '2026-01-01', expiry_date: '2027-01-01',
}, tokens.liming)
check('liming → POST supply合同 → 403', r, 403)
// 采购部不能创建加盟合同
r = await req('POST', '/api/contracts', {
customer_id: testCustId, contract_name: '越权加盟合同',
type: 'franchise', effective_date: '2026-01-01', expiry_date: '2027-01-01',
}, tokens.zhaoqiang)
check('zhaoqiang → POST franchise合同 → 403', r, 403)
// admin 可以创建任意类型
r = await req('POST', '/api/contracts', {
customer_id: testCustId, contract_name: 'admin测试合同',
amount: 100000, effective_date: '2026-01-01', expiry_date: '2027-01-01',
type: 'franchise', status: '生效',
}, tokens.admin)
check('admin → POST franchise合同 → 200', r, 200)
if (r.body?.data?.id) created.contracts.push(r.body.data.id)
// ══════════════════════════════════════════
// 【7】 suppliers CRUD
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【7】 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 (缺名 → 400)', r, 400)
if (supId) {
r = await req('GET', '/api/suppliers/' + supId, null, tokens.zhaoqiang)
check('GET /api/suppliers/:id (详情)', r, 200)
if (r.body?.data) checkValue('供应商名称', r.body.data.name, '柠檬供应商-测试')
r = await req('PUT', '/api/suppliers/' + supId, { phone: '13800008888' }, tokens.zhaoqiang)
check('PUT /api/suppliers/:id (更新)', r, 200)
r = await req('GET', '/api/suppliers?name=柠檬', null, tokens.zhaoqiang)
check('GET /api/suppliers?name=柠檬 (搜索)', r, 200)
}
// ══════════════════════════════════════════
// 【8】 users CRUD新字段 department_id
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【8】 users CRUD管理员操作新字段 department_id')
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}`)
// 验证新字段
const sample = r.body.data.list[0]
check('用户有 dept_name', { status: sample?.dept_name ? 200 : 500 }, 200)
check('用户有 dept_desc', { status: sample?.dept_desc ? 200 : 500 }, 200)
check('用户有 real_name (employees联查)', { status: sample?.real_name ? 200 : 500 }, 200)
}
// 管理员查自己 ID
const allUsers = await req('GET', '/api/users', null, tokens.admin)
const adminUser = allUsers.body?.data?.list?.find(u => u.username === 'admin')
const adminUserId = adminUser?.id
// 创建新用户(用新字段 department_id
r = await req('POST', '/api/users', {
username: 'testuser_' + Date.now(),
password: 'pass123456',
department_id: 3, // franchise_manager 的 ID
}, tokens.admin)
check('admin POST /api/users (创建,新字段)', r, 200)
const newUserId = r.body?.data?.id
if (newUserId) {
created.users.push(newUserId)
if (r.body?.data) {
check('新用户有 dept_name', { status: r.body.data.dept_name ? 200 : 500 }, 200)
check('新用户有 dept_desc', { status: r.body.data.dept_desc ? 200 : 500 }, 200)
}
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)
r = await req('PUT', '/api/users/' + newUserId, { is_active: 1, password: 'newpass123' }, tokens.admin)
check('admin PUT /api/users/:id (启用+改密)', r, 200)
}
// admin 不能删除自己
r = await req('DELETE', '/api/users/' + adminUserId, null, tokens.admin)
check('admin DELETE 自己 → 400', r, 400)
// admin 不能禁用自己
r = await req('PUT', '/api/users/' + adminUserId, { is_active: 0 }, tokens.admin)
check('admin PUT 禁用自己 → 400', r, 400)
// admin 不能改自己部门
r = await req('PUT', '/api/users/' + adminUserId, { department_id: 3 }, tokens.admin)
check('admin PUT 改自己部门 → 400', r, 400)
// 按部门筛选用户
r = await req('GET', '/api/users?department_id=1', null, tokens.admin)
check('GET /api/users?department_id=1 (筛选admin部门)', r, 200)
// ══════════════════════════════════════════
// 【9】 allowUserCreation创建员工同时建用户
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【9】 allowUserCreation 中间件测试')
console.log('━'.repeat(55))
// 管理员创建员工同时建账号
r = await req('POST', '/api/employees', {
name: '测试员工-带账号', gender: '男', department: '招商部',
position: '专员', status: 1,
username: 'testemp_' + Date.now(),
password: 'pass123456',
department_id: 3, // franchise_manager
}, tokens.admin)
check('admin POST /api/employees (同步建用户)', r, 200)
if (r.body?.data?.id) created.employees.push(r.body.data.id)
// 非管理员创建员工不能带用户字段
r = await req('POST', '/api/employees', {
name: '测试员工-越权', department: '招商部',
username: 'hackuser', password: 'pass123', department_id: 3,
}, tokens.liming)
check('liming POST /api/employees 带 user 字段 → 403', r, 403)
// 管理员不带用户字段 → 只创建员工
r = await req('POST', '/api/employees', {
name: '测试员工-纯员工', department: '招商部', position: '专员', status: 1,
}, tokens.admin)
check('admin POST /api/employees (不建用户)', r, 200)
if (r.body?.data?.id) created.employees.push(r.body.data.id)
// 管理员建用户但缺字段
r = await req('POST', '/api/employees', {
name: '测试员工-缺字段', department: '招商部',
username: 'testuser2', // 缺 password 和 department_id
}, tokens.admin)
check('admin POST /api/employees (建用户缺字段 → 400)', r, 400)
// ══════════════════════════════════════════
// 【10】 customers CRUD
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【10】 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)
check('客户创建成功', { status: r.body.data.id ? 200 : 500 }, 200)
}
r = await req('POST', '/api/customers', { name: '' }, tokens.admin)
check('POST /api/customers (缺名 → 400)', r, 400)
if (custCrudId) {
r = await req('GET', '/api/customers/' + custCrudId, null, tokens.admin)
check('GET /api/customers/:id (详情)', r, 200)
if (r.body?.data) checkValue('加盟商名', r.body.data.name, 'CRUD测试加盟商')
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 (不存在 → 404)', r, 404)
// ══════════════════════════════════════════
// 【11】 employees CRUD
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【11】 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?department=品控部', null, tokens.admin)
check('GET /api/employees?department=品控部 (筛选)', r, 200)
}
// ══════════════════════════════════════════
// 【12】 products CRUD
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【12】 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 (缺名 → 400)', 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)
}
// ══════════════════════════════════════════
// 【13】 contracts CRUD
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【13】 contracts CRUD')
console.log('━'.repeat(55))
// 招商部正确创建加盟合同
r = await req('POST', '/api/contracts', {
customer_id: custCrudId, contract_name: '加盟协议-测试', amount: 300000,
effective_date: '2026-06-01', expiry_date: '2027-05-31', status: '生效',
}, tokens.liming)
check('liming POST /api/contracts (加盟合同)', 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)
}
// 采购部正确创建采购合同(需要 supplier_id
r = await req('POST', '/api/contracts', {
customer_id: custCrudId, contract_name: '采购合同-测试', amount: 200000,
effective_date: '2026-06-01', expiry_date: '2027-05-31',
type: 'supply', status: '生效', supplier_id: 1,
}, tokens.zhaoqiang)
check('zhaoqiang POST /api/contracts (采购合同)', r, 200)
if (r.body?.data) {
checkValue('合同type=supply', r.body.data.type, 'supply')
created.contracts.push(r.body.data.id)
}
r = await req('POST', '/api/contracts', { customer_id: 99999, contract_name: '无效' }, tokens.admin)
check('POST /api/contracts (客户不存在 → 400)', r, 400)
// ══════════════════════════════════════════
// 【14】 after-sales CRUD
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【14】 after-sales CRUD')
console.log('━'.repeat(55))
r = await req('POST', '/api/after-sales', {
customer_id: custCrudId, feedback: '冰淇淋机不出料',
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 (客户不存在 → 400)', r, 400)
r = await req('POST', '/api/after-sales', { customer_id: custCrudId }, tokens.wangli)
check('POST /api/after-sales (缺feedback → 400)', 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)
}
// ══════════════════════════════════════════
// 【15】 个人信息 & 改密
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【15】 个人信息 & 改密')
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 (旧密码错 → 400)', r, 400)
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123456' }, tokens.admin)
check('PUT /api/user/password (新旧相同 → 400)', r, 400)
r = await req('PUT', '/api/user/password', { oldPassword: '123456', newPassword: '123' }, tokens.admin)
check('PUT /api/user/password (新密码太短 → 400)', r, 400)
// ══════════════════════════════════════════
// 【16】 数据清理
// ══════════════════════════════════════════
console.log('\n' + '━'.repeat(55))
console.log('【16】 测试数据清理')
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)
})