// routes/contracts.js —— 合同管理 CRUD(纯数据库操作,权限由中间件层控制) const { pool } = require('../db') // 格式化日期为 YYYY-MM-DD function formatDate(dateStr) { if (!dateStr) return null if (typeof dateStr === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { return dateStr } try { const date = new Date(dateStr) if (isNaN(date.getTime())) return null const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } catch { return null } } 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, s.name AS supplier_name, e.name AS employee_name, ru.name AS responsible_user_name FROM contracts c LEFT JOIN customers cu ON c.customer_id = cu.id LEFT JOIN suppliers s ON c.supplier_id = s.id LEFT JOIN employees e ON c.employee_id = e.id LEFT JOIN users u ON c.responsible_user_id = u.id LEFT JOIN employees ru ON u.employee_id = ru.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, contract_name, employee_name, id, effective_date_start, effective_date_end } = req.query let where = 'WHERE 1=1' const params = [] // 数据范围过滤(由中间件注入 req.scope,无别名;list/detail 需机械加 c. 前缀) if (req.scope && req.scope.sql) { where += ' ' + req.scope.sql.replace(/\btype\b/g, 'c.type') params.push(...req.scope.params) } if (id) { where += ' AND c.id = ?' params.push(id) } if (status) { where += ' AND c.status = ?' params.push(status) } if (contract_name) { where += ' AND c.contract_name LIKE ?' params.push(`%${contract_name}%`) } 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}%`) } if (effective_date_start) { where += ' AND c.effective_date >= ?' params.push(effective_date_start) } if (effective_date_end) { where += ' AND c.effective_date <= ?' params.push(effective_date_end) } const [[{ total }]] = await pool.query( `SELECT COUNT(*) AS total FROM contracts c LEFT JOIN customers cu ON c.customer_id = cu.id LEFT JOIN suppliers s ON c.supplier_id = s.id LEFT JOIN employees e ON c.employee_id = e.id LEFT JOIN users u ON c.responsible_user_id = u.id LEFT JOIN employees ru ON u.employee_id = ru.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 { let sql = `${DETAIL_SELECT} WHERE c.id = ?` const params = [req.params.id] // 数据范围过滤(由中间件注入 req.scope,无别名;detail 需机械加 c. 前缀) if (req.scope && req.scope.sql) { sql += ' ' + req.scope.sql.replace(/\btype\b/g, 'c.type') params.push(...req.scope.params) } const [rows] = await pool.query(sql, params) 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, supplier_id, contract_name, contract_no, contract_content, amount, effective_date, expiry_date, employee_id, status, remark, } = req.body || {} // 合同类型默认值(类型校验由中间件层控制) const contractType = req.body.type || 'franchise' if (contractType === 'supply' && !supplier_id) { return res.status(400).json({ code: 400, message: '采购合同必须选择供应商' }) } if (contractType !== 'supply' && !customer_id) { return res.status(400).json({ code: 400, message: '加盟合同必须选择客户' }) } if (!contract_name) { return res.status(400).json({ code: 400, message: '合同名称必填' }) } try { // 校验客户是否存在(非采购合同必填) if (customer_id) { 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 (supplier_id) { const [sup] = await pool.query('SELECT id FROM suppliers WHERE id = ?', [supplier_id]) if (sup.length === 0) { return res.status(400).json({ code: 400, message: '关联供应商不存在' }) } } // 校验业务员(如果填了),并通过 employee_id 自动推导负责人 let responsibleUserId = req.user.id 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 [userRows] = await pool.query('SELECT id FROM users WHERE employee_id = ? LIMIT 1', [employee_id]) if (userRows.length > 0) { responsibleUserId = userRows[0].id } } const [result] = await pool.query( `INSERT INTO contracts (customer_id, supplier_id, contract_name, contract_no, contract_content, amount, effective_date, expiry_date, employee_id, status, remark, type, responsible_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [customer_id || null, supplier_id || null, contract_name, contract_no || null, contract_content || null, amount !== undefined ? amount : null, formatDate(effective_date), formatDate(expiry_date), employee_id || null, status || '生效中', remark || null, contractType, responsibleUserId] ) 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', 'supplier_id', 'contract_name', 'contract_no', 'contract_content', 'amount', 'effective_date', 'expiry_date', 'employee_id', 'status', 'remark', 'type', 'responsible_user_id', ] try { // 确认记录存在且在数据范围内(detail/update/delete 不用表别名) let checkSql = 'SELECT id, employee_id FROM contracts WHERE id = ?' let checkParams = [id] if (req.scope && req.scope.sql) { checkSql += ' ' + req.scope.sql checkParams.push(...req.scope.params) } const [existing] = await pool.query(checkSql, checkParams) 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: '关联业务员不存在' }) } } // 业务员即负责人:当 employee_id 变更时自动推导 responsible_user_id if (req.body.employee_id !== undefined) { if (req.body.employee_id) { const [userRows] = await pool.query('SELECT id FROM users WHERE employee_id = ? LIMIT 1', [req.body.employee_id]) if (userRows.length > 0) { req.body.responsible_user_id = userRows[0].id } else { // 该员工无对应系统用户时,设为当前登录用户 req.body.responsible_user_id = req.user.id } } else { // 业务员被清空时,清空负责人 req.body.responsible_user_id = null } } if (req.body.supplier_id) { const [sup] = await pool.query('SELECT id FROM suppliers WHERE id = ?', [req.body.supplier_id]) if (sup.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} = ?`) // 日期字段需要格式化 if (['effective_date', 'expiry_date'].includes(f)) { params.push(formatDate(req.body[f])) } else { 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 { let checkSql = 'SELECT id FROM contracts WHERE id = ?' let checkParams = [id] if (req.scope && req.scope.sql) { checkSql += ' ' + req.scope.sql checkParams.push(...req.scope.params) } const [existing] = await pool.query(checkSql, checkParams) 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 }