feat: 北京解聘合规增强 — 政策法规库+地区过滤+工会回执证据链

1. 政策法规库(RAG知识库):
   - 新增6条北京地区单方解除劳动合同工作指引种子数据
   - 涵盖通知工会程序、函件内容要求、回执要求、监督提示函、仲裁审查等
   - 企业用户通过AI问答可检索到北京地区工会通知规定

2. 地区差异化合规检查:
   - getChecklistForReason 增加 orgCity 参数
   - 工会通知检查项仅北京地区显示(FAULT/NONFAULT/LAYOFF)
   - 前端解聘方式说明中北京工会提示仅北京地区动态显示
   - 非北京地区不显示工会通知检查项,避免误导

3. 工会回执上传+证据链留存:
   - 后端新增3个接口:上传回执文件、保存回执信息、获取回执信息
   - 回执信息保存到草稿 checklistOverrides
   - 自动追加到证据链(appendEvidence),作为劳动仲裁举证材料
   - 前端合规检查步骤增加工会回执上传区域
   - 确认提交步骤展示回执文件链接
   - 新增 uploads 静态文件服务

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-15 16:01:02 +08:00
parent e8c6d27979
commit 7e720d9bfc
6 changed files with 332 additions and 34 deletions
+153 -2
View File
@@ -4,7 +4,10 @@ import { auditLog } from '../middleware/auditLog'
import { terminationChecklistSchema, resignationSchema, batchTerminatePreviewSchema, batchTerminateSchema, createTerminationDraftSchema, updateTerminationDraftSchema } from '../schemas/termination.schema'
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems, validateTerminationStep } from '../services/termination.service'
import prisma from '../lib/prisma'
import { createEvidence } from '../services/evidence.service'
import { createEvidence, appendEvidence } from '../services/evidence.service'
import multer from 'multer'
import path from 'path'
import fs from 'fs'
const router = Router()
@@ -39,7 +42,11 @@ router.get('/checklist/:reason', authMiddleware, async (req: AuthRequest, res, n
}
}
const checklist = getChecklistForReason(req.params.reason, employee)
// 获取组织所在城市,用于地区差异化合规检查(如北京通知工会程序)
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId }, select: { city: true } })
const orgCity = org?.city || undefined
const checklist = getChecklistForReason(req.params.reason, employee, orgCity)
res.json({ success: true, data: checklist })
} catch (err) {
next(err)
@@ -360,4 +367,148 @@ router.delete('/draft/:id', authMiddleware, async (req: AuthRequest, res, next)
}
})
// ========== 工会回执上传(北京地区单方解除证据链) ==========
// 工会回执文件上传目录
const unionReceiptDir = path.join(process.cwd(), 'uploads', 'union-receipt')
if (!fs.existsSync(unionReceiptDir)) fs.mkdirSync(unionReceiptDir, { recursive: true })
const unionReceiptUpload = multer({
storage: multer.diskStorage({
destination: unionReceiptDir,
filename: (_req, file, cb) => {
const ext = path.extname(file.originalname)
cb(null, `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`)
},
}),
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
const allowed = ['.jpg', '.jpeg', '.png', '.pdf', '.bmp']
const ext = path.extname(file.originalname).toLowerCase()
if (allowed.includes(ext)) cb(null, true)
else cb(new Error('仅支持 JPG/PNG/PDF/BMP 格式'))
},
})
/**
* 上传工会回执文件
* 北京地区单方解除劳动合同,工会收到通知后出具的书面回执扫描件
*/
router.post('/draft/:id/union-receipt/upload', authMiddleware, unionReceiptUpload.single('file'), async (req: AuthRequest, res, next) => {
try {
if (!req.file) {
return res.status(400).json({ success: false, error: { code: 'NO_FILE', message: '请选择文件' } })
}
const record = await prisma.terminationRecord.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!record) {
fs.unlinkSync(req.file.path)
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '解聘记录不存在' } })
}
const fileUrl = `/uploads/union-receipt/${req.file.filename}`
res.json({ success: true, data: { fileName: req.file.originalname, fileUrl, fileSize: req.file.size } })
} catch (err) {
next(err)
}
})
/**
* 保存工会回执信息(文件URL + 回执编号 + 工会意见)到草稿,并追加到证据链
*/
router.post('/draft/:id/union-receipt', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const record = await prisma.terminationRecord.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '解聘记录不存在' } })
}
const { receiptNo, unionName, receiptDate, fileUrl, fileName, unionOpinion } = req.body
// 将工会回执信息保存到草稿的 checklistOverrides 中
const checklistOverrides: any = (record.checklistOverrides as any) || {}
checklistOverrides['union_receipt'] = {
checked: true,
overrideReason: '已收到工会书面回执',
receiptNo,
unionName,
receiptDate,
fileUrl,
fileName,
unionOpinion,
}
// 同步标记 notify_union 已完成
if (!checklistOverrides['notify_union']) {
checklistOverrides['notify_union'] = {
checked: true,
overrideReason: '已通知工会并收到回执',
}
}
await prisma.terminationRecord.update({
where: { id: record.id },
data: { checklistOverrides },
})
// 追加到证据链
await appendEvidence(
req.user!.orgId,
// 查找该解聘记录对应的证据链
(await prisma.evidenceChain.findFirst({
where: { orgId: req.user!.orgId, category: 'TERMINATION', refId: record.id },
}))?.id || '',
{
action: '工会书面回执已收到',
timestamp: new Date().toISOString(),
ip: req.ip,
userAgent: req.headers['user-agent'] as string,
location: `回执编号:${receiptNo || '无'},工会:${unionName || '未填写'},回执日期:${receiptDate || '未填写'}`,
}
).catch(() => {
// 证据链可能不存在(草稿阶段未创建),创建新的证据链
return createEvidence({
orgId: req.user!.orgId,
category: 'TERMINATION',
refId: record.id,
employeeId: record.employeeId,
events: [{
action: '工会书面回执已收到',
timestamp: new Date().toISOString(),
ip: req.ip,
userAgent: req.headers['user-agent'] as string,
location: `回执编号:${receiptNo || '无'},工会:${unionName || '未填写'},回执日期:${receiptDate || '未填写'}`,
}],
createdBy: req.user!.id,
})
})
await auditLog(req, 'UNION_RECEIPT', 'TERMINATION_RECORD', record.id, { receiptNo, unionName, fileUrl })
res.json({ success: true, data: { receiptNo, unionName, receiptDate, fileUrl, fileName, unionOpinion } })
} catch (err) {
next(err)
}
})
/**
* 获取工会回执信息
*/
router.get('/draft/:id/union-receipt', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const record = await prisma.terminationRecord.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '解聘记录不存在' } })
}
const checklistOverrides: any = (record.checklistOverrides as any) || {}
const unionReceipt = checklistOverrides['union_receipt'] || null
res.json({ success: true, data: unionReceipt })
} catch (err) {
next(err)
}
})
export default router