Files
backmanager-server/routes/contracts.js
2026-06-25 21:18:13 +08:00

337 lines
11 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')
const { getDataScope } = require('../middleware/permissions')
// 格式化日期为 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
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
`
// 单条查询时用同样的 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 = []
// 数据范围过滤(列表查询使用表别名 c需要加前缀
const scope = getDataScope(req.user, 'contracts')
if (scope.deny) {
return res.status(403).json({ code: 403, message: '无权访问此资源' })
}
if (scope.where) {
// 给 scope.where 中的字段加上表别名 c.
const scopedWhere = scope.where.replace(/\b(responsible_user_id|type)\b/g, 'c.$1')
where += ' AND ' + scopedWhere
params.push(...scope.values)
}
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
${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]
const scope = getDataScope(req.user, 'contracts')
if (scope.deny) {
return res.status(403).json({ code: 403, message: '无权访问此资源' })
}
if (scope.where) {
// detail 查询使用表别名 c需要给 scope.where 加上前缀
const scopedWhere = scope.where.replace(/\b(responsible_user_id|type)\b/g, 'c.$1')
sql += ' AND ' + scopedWhere
params.push(...scope.values)
}
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 || (req.user.roleName === 'procurement_manager' ? 'supply' : 'franchise')
if (req.user.roleName === 'procurement_manager' && contractType !== 'supply') {
return res.status(403).json({ code: 403, message: '采购经理只能创建采购合同' })
}
if (req.user.roleName === 'franchise_manager' && contractType !== 'franchise') {
return res.status(403).json({ code: 403, message: '招商经理只能创建加盟合同' })
}
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: '关联供应商不存在' })
}
}
// 校验业务员(如果填了)
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, 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,
req.body.responsible_user_id || req.user.id]
)
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 FROM contracts WHERE id = ?'
const checkParams = [id]
const scope = getDataScope(req.user, 'contracts')
if (scope.deny) {
return res.status(403).json({ code: 403, message: '无权访问此资源' })
}
if (scope.where) {
checkSql += ' AND ' + scope.where
checkParams.push(...scope.values)
}
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: '关联业务员不存在' })
}
}
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 = ?'
const checkParams = [id]
const scope = getDataScope(req.user, 'contracts')
if (scope.deny) {
return res.status(403).json({ code: 403, message: '无权访问此资源' })
}
if (scope.where) {
checkSql += ' AND ' + scope.where
checkParams.push(...scope.values)
}
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 }