diff --git a/db.js b/db.js index 7a2f64f..7b3cfff 100644 --- a/db.js +++ b/db.js @@ -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 和 employees,after_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') diff --git a/routes/afterSales.js b/routes/afterSales.js new file mode 100644 index 0000000..7998bca --- /dev/null +++ b/routes/afterSales.js @@ -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 } diff --git a/routes/contracts.js b/routes/contracts.js new file mode 100644 index 0000000..f5cce59 --- /dev/null +++ b/routes/contracts.js @@ -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 } diff --git a/routes/customers.js b/routes/customers.js new file mode 100644 index 0000000..4a6a9fa --- /dev/null +++ b/routes/customers.js @@ -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 } diff --git a/routes/employees.js b/routes/employees.js new file mode 100644 index 0000000..1a8c204 --- /dev/null +++ b/routes/employees.js @@ -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 } diff --git a/routes/products.js b/routes/products.js new file mode 100644 index 0000000..601766d --- /dev/null +++ b/routes/products.js @@ -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 } diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..be1803a --- /dev/null +++ b/routes/users.js @@ -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 } diff --git a/server.js b/server.js index e76ae9a..bf72ed4 100644 --- a/server.js +++ b/server.js @@ -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) - // 签 token:payload 里放 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) + }) +} diff --git a/test-all.js b/test-all.js new file mode 100644 index 0000000..7faca2d --- /dev/null +++ b/test-all.js @@ -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) +})