This commit is contained in:
2026-06-22 20:48:29 +08:00
parent 48af2df4e2
commit e00eb2c1f8
9 changed files with 1765 additions and 142 deletions

126
db.js
View File

@@ -33,19 +33,125 @@ async function initDB() {
)
await conn.end()
// 2.2 建表
// 2.2 建表(如果不存在)
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
nickname VARCHAR(50) DEFAULT '',
avatar VARCHAR(255) DEFAULT '',
role VARCHAR(20) DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
id INT NOT NULL AUTO_INCREMENT COMMENT '用户ID (主键)',
username VARCHAR(50) NOT NULL COMMENT '用户名 (登录账号)',
password VARCHAR(255) NOT NULL COMMENT '登录密码 (应存储加密后的值)',
real_name VARCHAR(50) DEFAULT NULL COMMENT '真实姓名',
role VARCHAR(20) NOT NULL DEFAULT 'user' COMMENT '角色: admin-管理员, user-普通用户',
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),
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS customers (
id INT NOT NULL AUTO_INCREMENT COMMENT '加盟商ID (主键)',
name VARCHAR(100) NOT NULL COMMENT '加盟商姓名',
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
province VARCHAR(50) DEFAULT NULL COMMENT '省',
city VARCHAR(50) DEFAULT NULL COMMENT '市',
district VARCHAR(50) DEFAULT NULL COMMENT '区',
address VARCHAR(200) DEFAULT NULL COMMENT '详细地址',
customer_type VARCHAR(20) DEFAULT 'Normal' COMMENT '客户类型: VIP-地区总代理, Normal-普通代理',
email VARCHAR(100) DEFAULT NULL COMMENT '电子邮箱',
remark TEXT DEFAULT NULL COMMENT '备注信息',
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_customers_name (name),
KEY idx_customers_phone (phone)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='加盟商信息表'
`)
// 确保 customer_type 列存在(兼容旧表)
try { await pool.query(`ALTER TABLE customers ADD COLUMN customer_type VARCHAR(20) DEFAULT 'Normal' COMMENT '客户类型: VIP-地区总代理, Normal-普通代理' AFTER address`) } catch {}
await pool.query(`
CREATE TABLE IF NOT EXISTS employees (
id INT NOT NULL AUTO_INCREMENT COMMENT '员工ID (主键)',
name VARCHAR(100) NOT NULL COMMENT '员工姓名',
gender VARCHAR(4) DEFAULT NULL COMMENT '性别: 男/女',
age INT DEFAULT NULL COMMENT '年龄',
education VARCHAR(50) DEFAULT NULL COMMENT '学历: 高中/专科/本科/硕士/博士',
department VARCHAR(100) DEFAULT NULL COMMENT '所属部门',
entry_date DATE DEFAULT NULL COMMENT '入职时间',
position VARCHAR(100) DEFAULT NULL COMMENT '职务/岗位',
salary DECIMAL(10,2) DEFAULT NULL COMMENT '工资金额',
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
email VARCHAR(100) DEFAULT NULL COMMENT '电子邮箱',
status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '在职状态: 0-离职, 1-在职',
remark TEXT DEFAULT NULL COMMENT '备注',
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_employees_name (name),
KEY idx_employees_department (department)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS products (
id INT NOT NULL AUTO_INCREMENT COMMENT '产品ID (主键)',
name VARCHAR(200) NOT NULL COMMENT '产品名称',
type VARCHAR(100) DEFAULT NULL COMMENT '产品类型/分类',
quantity INT NOT NULL DEFAULT 0 COMMENT '产品库存数量',
price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '产品单价',
unit VARCHAR(20) DEFAULT '件' COMMENT '计量单位',
specification VARCHAR(200) DEFAULT NULL COMMENT '产品规格/型号',
supplier VARCHAR(200) DEFAULT NULL COMMENT '供应商',
remark TEXT DEFAULT NULL COMMENT '备注',
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_products_name (name),
KEY idx_products_type (type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品信息表'
`)
// contracts 依赖 customers 和 employeesafter_sales 同样依赖
await pool.query(`
CREATE TABLE IF NOT EXISTS contracts (
id INT NOT NULL AUTO_INCREMENT COMMENT '合同ID (主键)',
customer_id INT NOT NULL COMMENT '客户ID (关联客户表)',
contract_name VARCHAR(200) NOT NULL COMMENT '合同名称',
contract_no VARCHAR(100) DEFAULT NULL COMMENT '合同编号',
contract_content TEXT DEFAULT NULL COMMENT '合同内容/条款',
amount DECIMAL(12,2) DEFAULT NULL COMMENT '合同金额',
effective_date DATE DEFAULT NULL COMMENT '合同生效日期',
expiry_date DATE DEFAULT NULL COMMENT '合同有效期 (截止日期)',
employee_id INT DEFAULT NULL COMMENT '业务员ID (关联员工表)',
status VARCHAR(20) NOT NULL DEFAULT '生效' COMMENT '合同状态: 草稿/生效/完成/作废',
remark TEXT DEFAULT NULL COMMENT '备注',
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_contracts_customer_id (customer_id),
KEY idx_contracts_employee_id (employee_id),
KEY idx_contracts_effective_date (effective_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同信息表'
`)
await pool.query(`
CREATE TABLE IF NOT EXISTS after_sales (
id INT NOT NULL AUTO_INCREMENT COMMENT '售后记录ID (主键)',
customer_id INT NOT NULL COMMENT '客户ID (关联客户表)',
feedback TEXT NOT NULL COMMENT '客户反馈意见/售后内容',
employee_id INT DEFAULT NULL COMMENT '处理业务员ID (关联员工表)',
handle_method TEXT DEFAULT NULL COMMENT '处理方式/解决方案',
handle_status VARCHAR(20) NOT NULL DEFAULT '待处理' COMMENT '处理状态: 待处理/处理中/已完成',
service_date DATE DEFAULT NULL COMMENT '售后日期',
remark TEXT DEFAULT NULL COMMENT '备注',
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_after_sales_customer_id (customer_id),
KEY idx_after_sales_employee_id (employee_id),
KEY idx_after_sales_status (handle_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后信息表'
`)
console.log('[init] 数据表初始化完成')
// 2.3 插 admin仅当不存在时
const [rows] = await pool.query(
'SELECT id FROM users WHERE username = ?',
@@ -54,7 +160,7 @@ async function initDB() {
if (rows.length === 0) {
const hash = await bcrypt.hash('123456', 10)
await pool.query(
'INSERT INTO users (username, password, nickname, role) VALUES (?, ?, ?, ?)',
'INSERT INTO users (username, password, real_name, role) VALUES (?, ?, ?, ?)',
['admin', hash, '超级管理员', 'admin']
)
console.log('[init] 已创建默认账号 admin / 123456')

200
routes/afterSales.js Normal file
View File

@@ -0,0 +1,200 @@
// routes/afterSales.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 }
}
const LIST_SELECT = `
SELECT
a.*,
cu.name AS customer_name,
e.name AS employee_name
FROM after_sales a
LEFT JOIN customers cu ON a.customer_id = cu.id
LEFT JOIN employees e ON a.employee_id = e.id
`
// GET /api/after-sales —— 列表
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { handle_status, customer_name } = req.query
let where = 'WHERE 1=1'
const params = []
if (handle_status) {
where += ' AND a.handle_status = ?'
params.push(handle_status)
}
if (customer_name) {
where += ' AND cu.name LIKE ?'
params.push(`%${customer_name}%`)
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM after_sales a
LEFT JOIN customers cu ON a.customer_id = cu.id
LEFT JOIN employees e ON a.employee_id = e.id
${where}`,
params
)
const [rows] = await pool.query(
`${LIST_SELECT} ${where} ORDER BY a.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('[after-sales list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/after-sales/:id —— 详情
async function detail(req, res) {
try {
const [rows] = await pool.query(
`${LIST_SELECT} WHERE a.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('[after-sales detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/after-sales —— 新增
async function create(req, res) {
const {
customer_id, feedback, employee_id, handle_method,
handle_status, service_date, remark,
} = req.body || {}
if (!customer_id) {
return res.status(400).json({ code: 400, message: '客户ID必填' })
}
if (!feedback) {
return res.status(400).json({ code: 400, message: '售后反馈内容必填' })
}
try {
// 校验客户存在
const [cust] = await pool.query('SELECT id FROM customers WHERE id = ?', [customer_id])
if (cust.length === 0) {
return res.status(400).json({ code: 400, message: '关联客户不存在' })
}
// 校验业务员
if (employee_id) {
const [emp] = await pool.query('SELECT id FROM employees WHERE id = ?', [employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, message: '关联业务员不存在' })
}
}
const [result] = await pool.query(
`INSERT INTO after_sales
(customer_id, feedback, employee_id, handle_method, handle_status, service_date, remark)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[customer_id, feedback,
employee_id || null,
handle_method || null,
handle_status || '待处理',
service_date || null,
remark || null]
)
const [rows] = await pool.query(`${LIST_SELECT} WHERE a.id = ?`, [result.insertId])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[after-sales create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/after-sales/:id —— 更新
async function update(req, res) {
const { id } = req.params
const fields = [
'customer_id', 'feedback', 'employee_id', 'handle_method',
'handle_status', 'service_date', 'remark',
]
try {
const [existing] = await pool.query('SELECT id FROM after_sales WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '售后记录不存在' })
}
if (req.body.customer_id) {
const [cust] = await pool.query('SELECT id FROM customers WHERE id = ?', [req.body.customer_id])
if (cust.length === 0) {
return res.status(400).json({ code: 400, message: '关联客户不存在' })
}
}
if (req.body.employee_id) {
const [emp] = await pool.query('SELECT id FROM employees WHERE id = ?', [req.body.employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, 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 after_sales SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query(`${LIST_SELECT} WHERE a.id = ?`, [id])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[after-sales update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/after-sales/:id —— 删除
async function remove(req, res) {
const { id } = req.params
try {
const [existing] = await pool.query('SELECT id FROM after_sales WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '售后记录不存在' })
}
await pool.query('DELETE FROM after_sales WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[after-sales delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { list, detail, create, update, remove }

220
routes/contracts.js Normal file
View File

@@ -0,0 +1,220 @@
// routes/contracts.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 }
}
// 列表查询时 JOIN 客户名和员工名
const LIST_SELECT = `
SELECT
c.*,
cu.name AS customer_name,
e.name AS employee_name
FROM contracts c
LEFT JOIN customers cu ON c.customer_id = cu.id
LEFT JOIN employees e ON c.employee_id = e.id
`
// 单条查询时用同样的 JOIN
const DETAIL_SELECT = LIST_SELECT
// GET /api/contracts —— 列表
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { status, customer_name, contract_no, employee_name } = req.query
let where = 'WHERE 1=1'
const params = []
if (status) {
where += ' AND c.status = ?'
params.push(status)
}
if (customer_name) {
where += ' AND cu.name LIKE ?'
params.push(`%${customer_name}%`)
}
if (contract_no) {
where += ' AND c.contract_no LIKE ?'
params.push(`%${contract_no}%`)
}
if (employee_name) {
where += ' AND e.name LIKE ?'
params.push(`%${employee_name}%`)
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM contracts c
LEFT JOIN customers cu ON c.customer_id = cu.id
LEFT JOIN employees e ON c.employee_id = e.id
${where}`,
params
)
const [rows] = await pool.query(
`${LIST_SELECT} ${where} ORDER BY c.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('[contracts list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/contracts/:id —— 详情
async function detail(req, res) {
try {
const [rows] = await pool.query(
`${DETAIL_SELECT} WHERE c.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('[contracts detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/contracts —— 新增
async function create(req, res) {
const {
customer_id, contract_name, contract_no, contract_content,
amount, effective_date, expiry_date, employee_id, status, remark,
} = req.body || {}
if (!customer_id) {
return res.status(400).json({ code: 400, message: '客户ID必填' })
}
if (!contract_name) {
return res.status(400).json({ code: 400, message: '合同名称必填' })
}
try {
// 校验客户是否存在
const [cust] = await pool.query('SELECT id FROM customers WHERE id = ?', [customer_id])
if (cust.length === 0) {
return res.status(400).json({ code: 400, message: '关联客户不存在' })
}
// 校验业务员(如果填了)
if (employee_id) {
const [emp] = await pool.query('SELECT id FROM employees WHERE id = ?', [employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, message: '关联业务员不存在' })
}
}
const [result] = await pool.query(
`INSERT INTO contracts
(customer_id, contract_name, contract_no, contract_content, amount, effective_date, expiry_date, employee_id, status, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[customer_id, contract_name,
contract_no || null,
contract_content || null,
amount !== undefined ? amount : null,
effective_date || null,
expiry_date || null,
employee_id || null,
status || '生效',
remark || null]
)
const [rows] = await pool.query(`${DETAIL_SELECT} WHERE c.id = ?`, [result.insertId])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[contracts create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/contracts/:id —— 更新
async function update(req, res) {
const { id } = req.params
const fields = [
'customer_id', 'contract_name', 'contract_no', 'contract_content',
'amount', 'effective_date', 'expiry_date', 'employee_id', 'status', 'remark',
]
try {
const [existing] = await pool.query('SELECT id FROM contracts WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '合同不存在' })
}
// 如果更新了 customer_id 或 employee_id需要校验
if (req.body.customer_id) {
const [cust] = await pool.query('SELECT id FROM customers WHERE id = ?', [req.body.customer_id])
if (cust.length === 0) {
return res.status(400).json({ code: 400, message: '关联客户不存在' })
}
}
if (req.body.employee_id) {
const [emp] = await pool.query('SELECT id FROM employees WHERE id = ?', [req.body.employee_id])
if (emp.length === 0) {
return res.status(400).json({ code: 400, 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 contracts SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query(`${DETAIL_SELECT} WHERE c.id = ?`, [id])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[contracts update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/contracts/:id —— 删除
async function remove(req, res) {
const { id } = req.params
try {
const [existing] = await pool.query('SELECT id FROM contracts WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '合同不存在' })
}
await pool.query('DELETE FROM contracts WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[contracts delete] error:', e)
// 如果因为外键约束customer_id 被 after_sales 引用)导致删除失败
if (e.code === 'ER_ROW_IS_REFERENCED_2') {
return res.status(400).json({ code: 400, message: '该合同关联了售后记录,无法删除' })
}
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { list, detail, create, update, remove }

166
routes/customers.js Normal file
View File

@@ -0,0 +1,166 @@
// routes/customers.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/customers —— 列表(支持搜索、分页)
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, phone, province, city, customer_type } = req.query
let where = 'WHERE 1=1'
const params = []
if (name) {
where += ' AND name LIKE ?'
params.push(`%${name}%`)
}
if (phone) {
where += ' AND phone LIKE ?'
params.push(`%${phone}%`)
}
if (province) {
where += ' AND province = ?'
params.push(province)
}
if (city) {
where += ' AND city = ?'
params.push(city)
}
if (customer_type) {
where += ' AND customer_type = ?'
params.push(customer_type)
}
// 查总数
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM customers ${where}`,
params
)
// 查分页数据
const [rows] = await pool.query(
`SELECT * FROM customers ${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('[customers list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/customers/:id —— 详情
async function detail(req, res) {
try {
const [rows] = await pool.query('SELECT * FROM customers 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('[customers detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/customers —— 新增
async function create(req, res) {
const {
name, phone, province, city, district,
address, customer_type, email, remark,
} = req.body || {}
if (!name) {
return res.status(400).json({ code: 400, message: '客户姓名必填' })
}
try {
const [result] = await pool.query(
`INSERT INTO customers (name, phone, province, city, district, address, customer_type, email, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[name, phone || null, province || null, city || null, district || null,
address || null, customer_type || 'Normal', email || null, remark || null]
)
const [rows] = await pool.query('SELECT * FROM customers WHERE id = ?', [result.insertId])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[customers create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/customers/:id —— 更新
async function update(req, res) {
const { id } = req.params
const fields = [
'name', 'phone', 'province', 'city', 'district',
'address', 'customer_type', 'email', 'remark',
]
try {
// 先确认记录存在
const [existing] = await pool.query('SELECT id FROM customers WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '客户不存在' })
}
// 动态构建 SET 子句(只更新传入的字段)
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 customers SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query('SELECT * FROM customers WHERE id = ?', [id])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[customers update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/customers/:id —— 删除
async function remove(req, res) {
const { id } = req.params
try {
const [existing] = await pool.query('SELECT id FROM customers WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '客户不存在' })
}
await pool.query('DELETE FROM customers WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[customers delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { list, detail, create, update, remove }

163
routes/employees.js Normal file
View File

@@ -0,0 +1,163 @@
// routes/employees.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/employees —— 列表
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, department, status } = req.query
let where = 'WHERE 1=1'
const params = []
if (name) {
where += ' AND name LIKE ?'
params.push(`%${name}%`)
}
if (department) {
where += ' AND department LIKE ?'
params.push(`%${department}%`)
}
if (status !== undefined && status !== '') {
where += ' AND status = ?'
params.push(Number(status))
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM employees ${where}`,
params
)
const [rows] = await pool.query(
`SELECT * FROM employees ${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('[employees list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/employees/:id —— 详情
async function detail(req, res) {
try {
const [rows] = await pool.query('SELECT * FROM employees 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('[employees detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/employees —— 新增
async function create(req, res) {
const {
name, gender, age, education, department,
entry_date, position, salary, phone, email, status, remark,
} = req.body || {}
if (!name) {
return res.status(400).json({ code: 400, message: '员工姓名必填' })
}
try {
const [result] = await pool.query(
`INSERT INTO employees (name, gender, age, education, department, entry_date, position, salary, phone, email, status, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[name,
gender || null,
age !== undefined ? age : null,
education || null,
department || null,
entry_date || null,
position || null,
salary !== undefined ? salary : null,
phone || null,
email || null,
status !== undefined ? status : 1,
remark || null]
)
const [rows] = await pool.query('SELECT * FROM employees WHERE id = ?', [result.insertId])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[employees create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/employees/:id —— 更新
async function update(req, res) {
const { id } = req.params
const fields = [
'name', 'gender', 'age', 'education', 'department',
'entry_date', 'position', 'salary', 'phone', 'email', 'status', 'remark',
]
try {
const [existing] = await pool.query('SELECT id FROM employees 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 employees SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query('SELECT * FROM employees WHERE id = ?', [id])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[employees update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/employees/:id —— 删除
async function remove(req, res) {
const { id } = req.params
try {
const [existing] = await pool.query('SELECT id FROM employees WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '员工不存在' })
}
await pool.query('DELETE FROM employees WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[employees delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { list, detail, create, update, remove }

157
routes/products.js Normal file
View File

@@ -0,0 +1,157 @@
// routes/products.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/products —— 列表
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, type, supplier } = 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 (supplier) {
where += ' AND supplier LIKE ?'
params.push(`%${supplier}%`)
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM products ${where}`,
params
)
const [rows] = await pool.query(
`SELECT * FROM products ${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('[products list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/products/:id —— 详情
async function detail(req, res) {
try {
const [rows] = await pool.query('SELECT * FROM products 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('[products detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/products —— 新增
async function create(req, res) {
const {
name, type, quantity, price, unit, specification, supplier, remark,
} = req.body || {}
if (!name) {
return res.status(400).json({ code: 400, message: '产品名称必填' })
}
try {
const [result] = await pool.query(
`INSERT INTO products (name, type, quantity, price, unit, specification, supplier, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[name,
type || null,
quantity !== undefined ? quantity : 0,
price !== undefined ? price : 0.00,
unit || '件',
specification || null,
supplier || null,
remark || null]
)
const [rows] = await pool.query('SELECT * FROM products WHERE id = ?', [result.insertId])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[products create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/products/:id —— 更新
async function update(req, res) {
const { id } = req.params
const fields = [
'name', 'type', 'quantity', 'price', 'unit', 'specification', 'supplier', 'remark',
]
try {
const [existing] = await pool.query('SELECT id FROM products 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 products SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query('SELECT * FROM products WHERE id = ?', [id])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
console.error('[products update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/products/:id —— 删除
async function remove(req, res) {
const { id } = req.params
try {
const [existing] = await pool.query('SELECT id FROM products WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '产品不存在' })
}
await pool.query('DELETE FROM products WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[products delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { list, detail, create, update, remove }

322
routes/users.js Normal file
View File

@@ -0,0 +1,322 @@
// routes/users.js —— 用户登录 / 信息 / CRUD
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const { pool } = require('../db')
// 安全字段:不返回密码哈希
const SAFE_FIELDS = 'id, username, real_name, role, status, created_at, updated_at'
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 }
}
// ========== 登录 / 登出 / 个人信息 ==========
// POST /api/user/login —— 登录(无需 token
async function login(req, res) {
const { username, password } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
try {
const [rows] = await pool.query(
'SELECT id, username, password, real_name, role, status FROM users WHERE username = ?',
[username]
)
// 用户不存在或密码错,统一提示避免枚举
if (rows.length === 0) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
const user = rows[0]
// 检查账号是否被禁用
if (user.status === 0) {
return res.status(403).json({ code: 403, message: '账号已被禁用,请联系管理员' })
}
const ok = await bcrypt.compare(password, user.password)
if (!ok) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '2h' }
)
res.status(200).json({
code: 200,
message: 'ok',
data: {
token,
userInfo: {
id: user.id,
username: user.username,
real_name: user.real_name,
role: user.role,
},
},
})
} catch (e) {
console.error('[login] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/user/info —— 获取当前登录用户信息(需要 token
async function info(req, res) {
try {
const [rows] = await pool.query(
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
[req.user.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('[info] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/user/logout —— 登出(需要 token仅做应答
async function logout(req, res) {
res.json({ code: 0, message: 'ok' })
}
// PUT /api/user/password —— 当前用户修改自己的密码(需要 token
async function changePassword(req, res) {
const { oldPassword, newPassword } = req.body || {}
if (!oldPassword || !newPassword) {
return res.status(400).json({ code: 400, message: '旧密码和新密码必填' })
}
if (typeof newPassword !== 'string' || newPassword.length < 6) {
return res.status(400).json({ code: 400, message: '新密码至少 6 位' })
}
if (oldPassword === newPassword) {
return res.status(400).json({ code: 400, message: '新密码不能与旧密码相同' })
}
try {
const [rows] = await pool.query('SELECT password FROM users WHERE id = ?', [req.user.id])
if (rows.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
const ok = await bcrypt.compare(oldPassword, rows[0].password)
if (!ok) {
return res.status(400).json({ code: 400, message: '旧密码错误' })
}
const hash = await bcrypt.hash(newPassword, 10)
await pool.query('UPDATE users SET password = ? WHERE id = ?', [hash, req.user.id])
res.json({ code: 0, message: '密码修改成功' })
} catch (e) {
console.error('[changePassword] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// ========== 用户管理 CRUD管理员 ==========
// GET /api/users —— 用户列表(管理员)
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { username, role, status } = req.query
let where = 'WHERE 1=1'
const params = []
if (username) {
where += ' AND username LIKE ?'
params.push(`%${username}%`)
}
if (role) {
where += ' AND role = ?'
params.push(role)
}
if (status !== undefined && status !== '') {
where += ' AND status = ?'
params.push(Number(status))
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM users ${where}`,
params
)
const [rows] = await pool.query(
`SELECT ${SAFE_FIELDS} FROM users ${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('[users list] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// GET /api/users/:id —— 用户详情(管理员)
async function detail(req, res) {
try {
const [rows] = await pool.query(
`SELECT ${SAFE_FIELDS} FROM users 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('[users detail] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// POST /api/users —— 创建用户(管理员)
async function create(req, res) {
const { username, password, real_name = null, role = 'user', status = 1 } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
if (typeof username !== 'string' || username.length < 3 || username.length > 50) {
return res.status(400).json({ code: 400, message: '用户名长度需在 3-50 之间' })
}
if (typeof password !== 'string' || password.length < 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' })
}
try {
const hash = await bcrypt.hash(password, 10)
const [result] = await pool.query(
'INSERT INTO users (username, password, real_name, role, status) VALUES (?, ?, ?, ?, ?)',
[username, hash, real_name, role, status]
)
const [rows] = await pool.query(
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
[result.insertId]
)
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[users create] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// PUT /api/users/:id —— 更新用户(管理员)
async function update(req, res) {
const { id } = req.params
const fields = ['username', 'real_name', 'role', 'status']
try {
const [existing] = await pool.query('SELECT id, role FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
const targetUser = existing[0]
// 不允许修改自己的角色或禁用自己
if (Number(id) === req.user.id) {
if (req.body.role !== undefined && req.body.role !== req.user.role) {
return res.status(400).json({ code: 400, message: '不能修改自己的角色' })
}
if (req.body.status === 0) {
return res.status(400).json({ code: 400, message: '不能禁用自己' })
}
}
const sets = []
const params = []
for (const f of fields) {
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} = ?`)
params.push(req.body[f])
}
}
// 密码单独处理
if (req.body.password !== undefined) {
if (typeof req.body.password !== 'string' || req.body.password.length < 6) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
}
const hash = await bcrypt.hash(req.body.password, 10)
sets.push('password = ?')
params.push(hash)
}
if (sets.length === 0) {
return res.status(400).json({ code: 400, message: '没有需要更新的字段' })
}
params.push(id)
await pool.query(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, params)
const [rows] = await pool.query(
`SELECT ${SAFE_FIELDS} FROM users WHERE id = ?`,
[id]
)
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[users update] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
// DELETE /api/users/:id —— 删除用户(管理员)
async function remove(req, res) {
const { id } = req.params
try {
if (Number(id) === req.user.id) {
return res.status(400).json({ code: 400, message: '不能删除自己' })
}
const [existing] = await pool.query('SELECT id FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
await pool.query('DELETE FROM users WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[users delete] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
}
module.exports = { login, info, logout, changePassword, list, detail, create, update, remove }

199
server.js
View File

@@ -1,15 +1,20 @@
// server.js —— 启动服务 + 鉴权中间件 + 3 个接口
// server.js —— 启动服务 + 鉴权中间件 + 全部 CRUD 路由
require('dotenv').config()
const express = require('express')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const { pool, initDB } = require('./db')
// 导入各模块路由
const users = require('./routes/users')
const customers = require('./routes/customers')
const employees = require('./routes/employees')
const contracts = require('./routes/contracts')
const afterSales = require('./routes/afterSales')
const products = require('./routes/products')
const app = express()
app.use(express.json()) // 解析 application/json 请求体
// ============ 鉴权中间件 ============
// 验证请求头里的 Bearer token把解析出来的用户挂到 req.user
function auth(req, res, next) {
const header = req.headers.authorization || ''
const token = header.replace(/^Bearer\s+/i, '')
@@ -17,6 +22,7 @@ function auth(req, res, next) {
return res.status(401).json({ code: 401, message: '未登录' })
}
try {
const jwt = require('jsonwebtoken')
req.user = jwt.verify(token, process.env.JWT_SECRET)
next()
} catch (e) {
@@ -25,7 +31,6 @@ function auth(req, res, next) {
}
// ============ 管理员校验中间件 ============
// 必须排在 auth 之后使用(依赖 req.user
function requireAdmin(req, res, next) {
if (!req.user || req.user.role !== 'admin') {
return res.status(403).json({ code: 403, message: '需要管理员权限' })
@@ -33,141 +38,71 @@ function requireAdmin(req, res, next) {
next()
}
// ============ POST /api/login ============
app.post('/api/user/login', async (req, res) => {
const { username, password } = req.body || {}
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
// ============ 登录/登出/个人信息 ============
app.post('/api/user/login', users.login)
app.get('/api/user/info', auth, users.info)
app.post('/api/user/logout', auth, users.logout)
app.put('/api/user/password', auth, users.changePassword)
try {
const [rows] = await pool.query(
'SELECT id, username, password, nickname, avatar, role FROM users WHERE username = ?',
[username]
)
// ============ 用户管理 CRUD管理员/api/users ============
app.get('/api/users', auth, requireAdmin, users.list)
app.get('/api/users/:id', auth, requireAdmin, users.detail)
app.post('/api/users', auth, requireAdmin, users.create)
app.put('/api/users/:id', auth, requireAdmin, users.update)
app.delete('/api/users/:id', auth, requireAdmin, users.remove)
// 用户不存在 或 密码错 —— 都用同一条提示,避免被枚举账号
if (rows.length === 0) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
const user = rows[0]
const ok = await bcrypt.compare(password, user.password)
if (!ok) {
return res.status(400).json({ code: 400, message: '账号或密码错误' })
}
// ============ 客户管理 /api/customers ============
app.get('/api/customers', auth, customers.list)
app.get('/api/customers/:id', auth, customers.detail)
app.post('/api/customers', auth, customers.create)
app.put('/api/customers/:id', auth, customers.update)
app.delete('/api/customers/:id', auth, customers.remove)
// 签 tokenpayload 里放 id / username / role
const token = jwt.sign(
{ id: user.id,
username: user.username,
role: user.role },
// ============ 员工管理 /api/employees ============
app.get('/api/employees', auth, employees.list)
app.get('/api/employees/:id', auth, employees.detail)
app.post('/api/employees', auth, employees.create)
app.put('/api/employees/:id', auth, employees.update)
app.delete('/api/employees/:id', auth, employees.remove)
process.env.JWT_SECRET,
// ============ 合同管理 /api/contracts ============
app.get('/api/contracts', auth, contracts.list)
app.get('/api/contracts/:id', auth, contracts.detail)
app.post('/api/contracts', auth, contracts.create)
app.put('/api/contracts/:id', auth, contracts.update)
app.delete('/api/contracts/:id', auth, contracts.remove)
{ expiresIn: process.env.JWT_EXPIRES_IN || '2h' }
)
// ============ 售后管理 /api/after-sales ============
app.get('/api/after-sales', auth, afterSales.list)
app.get('/api/after-sales/:id', auth, afterSales.detail)
app.post('/api/after-sales', auth, afterSales.create)
app.put('/api/after-sales/:id', auth, afterSales.update)
app.delete('/api/after-sales/:id', auth, afterSales.remove)
res.status(200).json({
code: 200,
message: 'ok',
data: {
token,
userInfo: {
id: user.id,
username: user.username,
nickname: user.nickname,
avatar: user.avatar,
role: user.role,
},
},
})
} catch (e) {
console.error('[login] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
})
// ============ GET /api/info ============
// 需要登录后访问,前端刷新页面时调用,用来拿最新的用户信息
app.get('/api/user/info', auth, async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, username, nickname, avatar, role FROM users WHERE id = ?',
[req.user.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('[info] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
})
// ============ POST /api/logout ============
// JWT 是无状态的:后端没法"作废"token主流做法是前端清掉 localStorage 里的 token
// 这里只是给前端一个明确的应答,告诉你"我收到了"
app.post('/api/user/logout', auth, (req, res) => {
res.json({ code: 0, message: 'ok' })
})
// ============ POST /api/user/create ============
// 仅管理员可调用:在 users 表里新增一行
app.post('/api/user/create', auth, requireAdmin, async (req, res) => {
//role不填默认user
const { username, password, nickname = '', role = 'user' } = req.body || {}
// 1) 入参校验
if (!username || !password) {
return res.status(400).json({ code: 400, message: '用户名和密码必填' })
}
if (typeof username !== 'string' || username.length < 3 || username.length > 50) {
return res.status(400).json({ code: 400, message: '用户名长度需在 3-50 之间' })
}
if (typeof password !== 'string' || password.length < 6) {
return res.status(400).json({ code: 400, message: '密码至少 6 位' })
}
try {
// 2) 密码哈希10 轮 salt与 db.js initDB 的 admin 同强度)
const hash = await bcrypt.hash(password, 10)
// 3) 写入数据库role 不强制 'user',允许前端传来的 role
const [result] = await pool.query(
'INSERT INTO users (username, password, nickname, role) VALUES (?, ?, ?, ?)',
[username, hash, nickname, role]
)
// 4) 把新行的关键信息回给前端(不包含 password 哈希)
res.json({
code: 0,
message: 'ok',
data: { id: result.insertId, username, nickname, role: role },
})
} catch (e) {
// users.username 是 UNIQUE重复时 MySQL 抛 ER_DUP_ENTRY (errno 1062)
if (e.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ code: 409, message: '用户名已存在' })
}
console.error('[create user] error:', e)
res.status(500).json({ code: 500, message: e.message })
}
})
// ============ 产品管理 /api/products ============
app.get('/api/products', auth, products.list)
app.get('/api/products/:id', auth, products.detail)
app.post('/api/products', auth, products.create)
app.put('/api/products/:id', auth, products.update)
app.delete('/api/products/:id', auth, products.remove)
// ============ 启动 ============
const PORT = Number(process.env.PORT) || 3000
initDB()
.then(() => {
app.listen(PORT, () => {
console.log(`[server] 已启动 → http://127.0.0.1:${PORT}`)
console.log(`[test] curl -X POST http://127.0.0.1:${PORT}/api/login \\`)
console.log(` -H "Content-Type: application/json" \\`)
console.log(` -d '{"username":"admin","password":"123456"}'`)
module.exports = app
if (require.main === module) {
initDB()
.then(() => {
app.listen(PORT, () => {
console.log(`[server] 已启动 → http://127.0.0.1:${PORT}`)
console.log(`[test] curl -X POST http://127.0.0.1:${PORT}/api/user/login \\`)
console.log(` -H "Content-Type: application/json" \\`)
console.log(` -d '{"username":"admin","password":"123456"}'`)
})
})
})
.catch((err) => {
console.error('[init] 数据库初始化失败:', err)
process.exit(1)
})
.catch((err) => {
console.error('[init] 数据库初始化失败:', err)
process.exit(1)
})
}

354
test-all.js Normal file
View File

@@ -0,0 +1,354 @@
// test-all.js —— 手动运行,逐接口验证全部 CRUD
// 用法: node test-all.js
require('dotenv').config()
const http = require('http')
const BASE = 'http://127.0.0.1:3000'
let token = ''
let failCount = 0
let passCount = 0
// ========== 工具函数 ==========
function req(method, path, body, useToken = true) {
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 (useToken && 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, 300) }) }
})
})
r.on('error', reject)
if (body) r.write(JSON.stringify(body))
r.end()
})
}
function check(label, res, expectStatus, expectInfo) {
const ok = res.status === expectStatus
if (ok) {
passCount++
console.log(`${label}`)
} else {
failCount++
console.log(`${label} 预期 HTTP ${expectStatus}, 实际 HTTP ${res.status}`)
console.log(` 响应: ${JSON.stringify(res.body).substring(0, 200)}`)
}
if (expectInfo) console.log(`${expectInfo}`)
}
// ========== 主流程 ==========
async function main() {
console.log('╔══════════════════════════════════════════╗')
console.log('║ 企业管理系统 — 接口全量测试 ║')
console.log('╚══════════════════════════════════════════╝')
console.log(`服务地址: ${BASE}\n`)
// ────────── 1. 登录 ──────────
console.log('━'.repeat(50))
console.log('【1】 用户登录模块')
console.log('━'.repeat(50))
let r = await req('POST', '/api/user/login', { username: 'admin', password: '123456' }, false)
check('POST /api/user/login (正确密码)', r, 200)
if (r.body?.data?.token) {
token = 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, '→ 账号或密码错误')
r = await req('POST', '/api/user/login', { username: '', password: '' }, false)
check('POST /api/user/login (空参数)', r, 400, '→ 用户名和密码必填')
// ────────── 2. 个人信息 ──────────
console.log('\n━'.repeat(50))
console.log('【2】 个人信息 & 改密')
console.log('━'.repeat(50))
r = await req('GET', '/api/user/info')
check('GET /api/user/info', r, 200)
if (r.body?.data) console.log(` ↳ 用户名: ${r.body.data.username}, 角色: ${r.body.data.role}`)
r = await req('POST', '/api/user/logout')
check('POST /api/user/logout', r, 200, '→ JWT 无状态,前端删 token 即可')
r = await req('PUT', '/api/user/password', { oldPassword: 'wrong', newPassword: 'newpwd999' })
check('PUT /api/user/password (旧密码错)', r, 400, '→ 旧密码错误')
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)
check('DELETE /api/users/:id (删除)', r, 200)
}
r = await req('DELETE', '/api/users/1')
check('DELETE /api/users/1 (删自己)', r, 400, '→ 不能删除自己')
r = await req('GET', '/api/users', null, false)
check('GET /api/users (无 token)', r, 401, '→ 未登录')
// ────────── 4. 客户管理 ──────────
console.log('\n━'.repeat(50))
console.log('【4】 客户管理 CRUD')
console.log('━'.repeat(50))
r = await req('GET', '/api/customers?page=1&pageSize=10')
check('GET /api/customers (列表)', r, 200)
if (r.body?.data) console.log(` ↳ 共 ${r.body.data.total}`)
r = await req('POST', '/api/customers', { name: '测试加盟商', phone: '13800001111', province: '广东', city: '东莞', district: '松山湖', address: '科技路88号', customer_type: 'VIP', email: 'test@test.com' })
check('POST /api/customers (创建VIP客户)', r, 200)
let custId = r.body?.data?.id
if (custId) console.log(` ↳ 新建客户 ID: ${custId}, 类型: ${r.body.data.customer_type}`)
r = await req('POST', '/api/customers', { name: '' })
check('POST /api/customers (缺姓名)', r, 400, '→ 客户姓名必填')
// 不传 customer_type 应为默认 Normal
r = await req('POST', '/api/customers', { name: '默认类型测试' })
check('POST /api/customers (不传类型默认Normal)', r, 200)
if (r.body?.data) console.log(` ↳ 默认类型: ${r.body.data.customer_type}`)
if (r.body?.data?.id) await req('DELETE', '/api/customers/' + r.body.data.id)
if (custId) {
r = await req('GET', '/api/customers/' + custId)
check('GET /api/customers/:id (详情)', r, 200)
r = await req('PUT', '/api/customers/' + custId, { phone: '13900002222', customer_type: 'Normal', remark: '更新备注' })
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=测试')
check('GET /api/customers?name=测试 (按姓名搜索)', r, 200)
r = await req('GET', '/api/customers?customer_type=Normal')
check('GET /api/customers?customer_type=Normal (按类型筛选)', 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')
check('GET /api/customers/99999 (不存在)', r, 404, '→ 客户不存在')
// ────────── 5. 员工管理 ──────────
console.log('\n━'.repeat(50))
console.log('【5】 员工管理 CRUD')
console.log('━'.repeat(50))
r = await req('GET', '/api/employees?page=1&pageSize=10')
check('GET /api/employees (列表)', r, 200)
r = await req('POST', '/api/employees', { name: '测试员工', gender: '男', age: 30, education: '本科', department: '研发部', position: '工程师', salary: 15000, phone: '13700000001', email: 'emp@test.com' })
check('POST /api/employees (创建)', r, 200)
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)
r = await req('PUT', '/api/employees/' + empId, { salary: 18000, position: '高级工程师' })
check('PUT /api/employees/:id (更新)', r, 200)
r = await req('GET', '/api/employees?status=1')
check('GET /api/employees?status=1 (在职筛选)', r, 200)
r = await req('DELETE', '/api/employees/' + empId)
check('DELETE /api/employees/:id (删除)', r, 200)
}
// ────────── 6. 产品管理 ──────────
console.log('\n━'.repeat(50))
console.log('【6】 产品管理 CRUD')
console.log('━'.repeat(50))
r = await req('GET', '/api/products?page=1&pageSize=10')
check('GET /api/products (列表)', r, 200)
r = await req('POST', '/api/products', { name: '测试产品-X1', type: '电子产品', quantity: 500, price: 299.99, unit: '台', specification: '型号 X1-2026', supplier: '供应商A' })
check('POST /api/products (创建)', r, 200)
let prodId = r.body?.data?.id
if (prodId) console.log(` ↳ 新建产品 ID: ${prodId}, 库存: ${r.body.data.quantity}`)
if (prodId) {
r = await req('GET', '/api/products/' + prodId)
check('GET /api/products/:id (详情)', r, 200)
r = await req('PUT', '/api/products/' + prodId, { price: 259.99, quantity: 480 })
check('PUT /api/products/:id (更新价格库存)', r, 200)
r = await req('GET', '/api/products?name=测试')
check('GET /api/products?name=测试 (搜索)', r, 200)
r = await req('DELETE', '/api/products/' + prodId)
check('DELETE /api/products/:id (删除)', r, 200)
}
// ────────── 7. 合同管理 ──────────
console.log('\n━'.repeat(50))
console.log('【7】 合同管理 CRUD关联客户+员工)')
console.log('━'.repeat(50))
// 先创建客户和员工做外键
const cRes = await req('POST', '/api/customers', { name: '合同测试客户' })
const eRes = await req('POST', '/api/employees', { name: '合同业务员' })
const cId = cRes.body?.data?.id
const eId = eRes.body?.data?.id
console.log(` ↳ 先创建客户(${cId}) 和 员工(${eId}) 供合同关联`)
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: '生效' })
check('POST /api/contracts (创建)', r, 200)
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: '无效合同' })
check('POST /api/contracts (客户不存在)', r, 400, '→ 关联客户不存在')
if (conId) {
r = await req('GET', '/api/contracts')
check('GET /api/contracts (列表)', r, 200)
r = await req('GET', '/api/contracts/' + conId)
check('GET /api/contracts/:id (详情)', r, 200)
r = await req('PUT', '/api/contracts/' + conId, { status: '完成', remark: '已履约' })
check('PUT /api/contracts/:id (更新状态)', r, 200)
r = await req('GET', '/api/contracts?status=完成')
check('GET /api/contracts?status=完成 (状态筛选)', r, 200)
r = await req('DELETE', '/api/contracts/' + conId)
check('DELETE /api/contracts/:id (删除)', r, 200)
}
// 清理外键依赖数据
if (cId) await req('DELETE', '/api/customers/' + cId)
if (eId) await req('DELETE', '/api/employees/' + eId)
// ────────── 8. 售后管理 ──────────
console.log('\n━'.repeat(50))
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) {
r = await req('GET', '/api/after-sales')
check('GET /api/after-sales (列表)', r, 200)
r = await req('GET', '/api/after-sales/' + asId)
check('GET /api/after-sales/:id (详情)', r, 200)
r = await req('PUT', '/api/after-sales/' + asId, { handle_status: '已完成', handle_method: '远程升级固件解决' })
check('PUT /api/after-sales/:id (更新)', r, 200)
r = await req('GET', '/api/after-sales?handle_status=已完成')
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)
// ────────── 收尾 ──────────
console.log('\n' + '═'.repeat(50))
console.log(`测试完成: 通过 ${passCount} / 失败 ${failCount}`)
if (failCount > 0) {
console.log('⚠️ 存在失败用例,请检查上方输出')
} else {
console.log('🎉 全部接口通过!')
}
console.log('═'.repeat(50))
// 优雅退出
process.exit(failCount > 0 ? 1 : 0)
}
main().catch((e) => {
console.error('测试异常:', e.message)
console.error('请确认服务已启动: node server.js')
process.exit(1)
})