Files
backmanager-server/routes/contracts.js
2026-06-30 23:11:19 +08:00

343 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, type, 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 (type) {
where += ' AND c.type = ?'
params.push(type)
}
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 ASC 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.replace(/\btype\b/g, 'c.type')
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.replace(/\btype\b/g, 'c.type')
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 }