瞪眼法测试通过

This commit is contained in:
Kyaru
2026-06-25 21:18:13 +08:00
parent b9266dfc1f
commit 67c9330659
11 changed files with 919 additions and 49 deletions

View File

@@ -2,6 +2,24 @@
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)
@@ -23,7 +41,7 @@ const LIST_SELECT = `
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { handle_status, customer_name } = req.query
const { handle_status, customer_name, id, customer_id, feedback } = req.query
let where = 'WHERE 1=1'
const params = []
@@ -35,11 +53,23 @@ async function list(req, res) {
}
if (scope.where) {
// list 查询有 JOIN需要表别名前缀避免列名歧义
const scopeCol = scope.tableAlias ? scope.where.replace('responsible_user_id', `${scope.tableAlias}.responsible_user_id`) : scope.where
const scopeCol = scope.tableAlias ? scope.where.replace(/\bresponsible_user_id\b/g, `${scope.tableAlias}.responsible_user_id`) : scope.where
where += ' AND ' + scopeCol
params.push(...scope.values)
}
if (id) {
where += ' AND a.id = ?'
params.push(id)
}
if (customer_id) {
where += ' AND a.customer_id = ?'
params.push(customer_id)
}
if (feedback) {
where += ' AND a.feedback LIKE ?'
params.push(`%${feedback}%`)
}
if (handle_status) {
where += ' AND a.handle_status = ?'
params.push(handle_status)
@@ -145,7 +175,7 @@ async function create(req, res) {
employee_id || null,
handle_method || null,
handle_status || '待处理',
service_date || null,
formatDate(service_date),
remark || null,
ownerId]
)
@@ -201,7 +231,12 @@ async function update(req, res) {
for (const f of fields) {
if (req.body[f] !== undefined) {
sets.push(`${f} = ?`)
params.push(req.body[f])
// 日期字段需要格式化
if (f === 'service_date') {
params.push(formatDate(req.body[f]))
} else {
params.push(req.body[f])
}
}
}
if (sets.length === 0) {

View File

@@ -2,6 +2,24 @@
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)
@@ -9,14 +27,16 @@ function pagination(query) {
return { page, pageSize, offset }
}
// 列表查询时 JOIN 客户名和员工名
// 列表查询时 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
`
@@ -27,25 +47,35 @@ const DETAIL_SELECT = LIST_SELECT
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { status, customer_name, contract_no, employee_name } = 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) {
where += ' AND ' + 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}%`)
@@ -58,10 +88,19 @@ async function list(req, res) {
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
@@ -100,7 +139,9 @@ async function detail(req, res) {
return res.status(403).json({ code: 403, message: '无权访问此资源' })
}
if (scope.where) {
sql += ' AND ' + 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)
}
@@ -118,22 +159,43 @@ async function detail(req, res) {
// POST /api/contracts —— 新增
async function create(req, res) {
const {
customer_id, contract_name, contract_no, contract_content,
customer_id, supplier_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必填' })
// 根据角色限制合同类型
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 {
// 校验客户是否存在
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 (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) {
@@ -145,18 +207,18 @@ async function create(req, res) {
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, type, responsible_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[customer_id, contract_name,
(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,
effective_date || null,
expiry_date || null,
formatDate(effective_date),
formatDate(expiry_date),
employee_id || null,
status || '生效',
status || '生效',
remark || null,
req.body.type || (req.user.roleName === 'procurement_manager' ? 'supply' : 'franchise'),
contractType,
req.body.responsible_user_id || req.user.id]
)
@@ -172,12 +234,12 @@ async function create(req, res) {
async function update(req, res) {
const { id } = req.params
const fields = [
'customer_id', 'contract_name', 'contract_no', 'contract_content',
'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')
@@ -206,13 +268,24 @@ async function update(req, res) {
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} = ?`)
params.push(req.body[f])
// 日期字段需要格式化
if (['effective_date', 'expiry_date'].includes(f)) {
params.push(formatDate(req.body[f]))
} else {
params.push(req.body[f])
}
}
}
if (sets.length === 0) {

View File

@@ -14,7 +14,7 @@ function pagination(query) {
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, phone, province, city } = req.query
const { name, phone, province, city, id } = req.query
let where = 'WHERE 1=1'
const params = []
@@ -29,32 +29,40 @@ async function list(req, res) {
params.push(...scope.values)
}
if (id) {
where += ' AND c.id = ?'
params.push(id)
}
if (name) {
where += ' AND name LIKE ?'
where += ' AND c.name LIKE ?'
params.push(`%${name}%`)
}
if (phone) {
where += ' AND phone LIKE ?'
where += ' AND c.phone LIKE ?'
params.push(`%${phone}%`)
}
if (province) {
where += ' AND province = ?'
where += ' AND c.province = ?'
params.push(province)
}
if (city) {
where += ' AND city = ?'
where += ' AND c.city = ?'
params.push(city)
}
// 查总数
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM customers ${where}`,
`SELECT COUNT(*) AS total FROM customers c ${where}`,
params
)
// 查分页数据
// 查分页数据JOIN users+employees 获取负责人姓名)
const [rows] = await pool.query(
`SELECT * FROM customers ${where} ORDER BY id DESC LIMIT ? OFFSET ?`,
`SELECT c.*, e.name AS responsible_user_name
FROM customers c
LEFT JOIN users u ON c.responsible_user_id = u.id
LEFT JOIN employees e ON u.employee_id = e.id
${where} ORDER BY c.id DESC LIMIT ? OFFSET ?`,
[...params, pageSize, offset]
)
@@ -201,6 +209,9 @@ async function remove(req, res) {
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[customers delete] error:', e)
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 })
}
}

View File

@@ -2,6 +2,24 @@
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)
@@ -13,7 +31,7 @@ function pagination(query) {
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, department, status } = req.query
const { id, name, status, department } = req.query
let where = 'WHERE 1=1'
const params = []
@@ -28,18 +46,22 @@ async function list(req, res) {
params.push(...scope.values)
}
if (id) {
where += ' AND id = ?'
params.push(Number(id))
}
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))
}
if (department) {
where += ' AND department LIKE ?'
params.push(`%${department}%`)
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM employees ${where}`,
@@ -114,7 +136,7 @@ async function create(req, res) {
age !== undefined ? age : null,
education || null,
department || null,
entry_date || null,
formatDate(entry_date),
position || null,
salary !== undefined ? salary : null,
phone || null,
@@ -160,7 +182,12 @@ async function update(req, res) {
for (const f of fields) {
if (req.body[f] !== undefined) {
sets.push(`${f} = ?`)
params.push(req.body[f])
// 日期字段需要格式化
if (f === 'entry_date') {
params.push(formatDate(req.body[f]))
} else {
params.push(req.body[f])
}
}
}
if (sets.length === 0) {
@@ -170,6 +197,11 @@ async function update(req, res) {
params.push(id)
await pool.query(`UPDATE employees SET ${sets.join(', ')} WHERE id = ?`, params)
// 同步 department 到关联的 users 表
if (req.body.department !== undefined) {
await pool.query('UPDATE users SET department = ? WHERE employee_id = ?', [req.body.department, id])
}
const [rows] = await pool.query('SELECT * FROM employees WHERE id = ?', [id])
res.json({ code: 0, message: 'ok', data: rows[0] })
} catch (e) {
@@ -196,10 +228,20 @@ async function remove(req, res) {
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '员工不存在或无权操作' })
}
// 禁止删除自己关联的员工
const [selfCheck] = await pool.query('SELECT id FROM users WHERE employee_id = ? AND id = ?', [id, req.user.id])
if (selfCheck.length > 0) {
return res.status(400).json({ code: 400, message: '不能删除自己的员工账号' })
}
// 同步删除关联的用户账号
await pool.query('DELETE FROM users WHERE employee_id = ?', [id])
await pool.query('DELETE FROM employees WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[employees delete] error:', e)
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 })
}
}

View File

@@ -12,11 +12,15 @@ function pagination(query) {
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, type, supplier } = req.query
const { name, type, supplier, id } = req.query
let where = 'WHERE 1=1'
const params = []
if (id) {
where += ' AND id = ?'
params.push(id)
}
if (name) {
where += ' AND name LIKE ?'
params.push(`%${name}%`)
@@ -150,6 +154,9 @@ async function remove(req, res) {
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[products delete] error:', e)
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 })
}
}

View File

@@ -12,11 +12,15 @@ function pagination(query) {
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { name, type, status } = req.query
const { id, name, type, status } = req.query
let where = 'WHERE 1=1'
const params = []
if (id) {
where += ' AND id = ?'
params.push(Number(id))
}
if (name) {
where += ' AND name LIKE ?'
params.push(`%${name}%`)
@@ -140,6 +144,9 @@ async function remove(req, res) {
res.json({ code: 0, message: 'ok' })
} catch (e) {
console.error('[suppliers delete] error:', e)
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 })
}
}

View File

@@ -118,6 +118,21 @@ async function info(req, res) {
}
}
// GET /api/user/list —— 简易用户列表(用于下拉选择负责人,仅需登录)
async function simpleList(req, res) {
try {
const [rows] = await pool.query(
`SELECT u.id, u.username, e.name AS real_name
FROM users u LEFT JOIN employees e ON u.employee_id = e.id
WHERE u.is_active = 1 ORDER BY u.id`
)
res.json({ code: 0, message: 'ok', data: rows })
} catch (e) {
console.error('[user simpleList] 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' })
@@ -162,7 +177,7 @@ async function changePassword(req, res) {
async function list(req, res) {
try {
const { page, pageSize, offset } = pagination(req.query)
const { username, role_id, is_active } = req.query
const { username, real_name, id, role_id, is_active } = req.query
let where = 'WHERE 1=1'
const params = []
@@ -171,6 +186,14 @@ async function list(req, res) {
where += ' AND u.username LIKE ?'
params.push(`%${username}%`)
}
if (real_name) {
where += ' AND e.name LIKE ?'
params.push(`%${real_name}%`)
}
if (id) {
where += ' AND u.id = ?'
params.push(Number(id))
}
if (role_id) {
where += ' AND u.role_id = ?'
params.push(Number(role_id))
@@ -181,7 +204,9 @@ async function list(req, res) {
}
const [[{ total }]] = await pool.query(
`SELECT COUNT(*) AS total FROM users u ${where}`,
`SELECT COUNT(*) AS total FROM users u
LEFT JOIN employees e ON u.employee_id = e.id
${where}`,
params
)
@@ -369,11 +394,20 @@ async function remove(req, res) {
return res.status(400).json({ code: 400, message: '不能删除自己' })
}
const [existing] = await pool.query('SELECT id FROM users WHERE id = ?', [id])
const [existing] = await pool.query('SELECT id, role_id FROM users WHERE id = ?', [id])
if (existing.length === 0) {
return res.status(404).json({ code: 404, message: '用户不存在' })
}
// 禁止删除最后一个管理员
const [adminRole] = await pool.query('SELECT id FROM roles WHERE name = ?', ['admin'])
if (adminRole.length > 0 && existing[0].role_id === adminRole[0].id) {
const [[{ cnt }]] = await pool.query('SELECT COUNT(*) AS cnt FROM users WHERE role_id = ?', [adminRole[0].id])
if (cnt <= 1) {
return res.status(400).json({ code: 400, message: '不能删除最后一个管理员' })
}
}
await pool.query('DELETE FROM users WHERE id = ?', [id])
res.json({ code: 0, message: 'ok' })
} catch (e) {
@@ -382,4 +416,4 @@ async function remove(req, res) {
}
}
module.exports = { login, info, logout, changePassword, list, detail, create, update, remove }
module.exports = { login, info, simpleList, logout, changePassword, list, detail, create, update, remove }