feat: 20260805 系统优化 - 身份证复制fallback/薪税日期筛选/社保版本修复/证据链导出/违纪证明/医疗期政策/绩效类型评级/合同作废/帮助更新

This commit is contained in:
freedakgmail
2026-08-05 20:26:16 +08:00
parent a5901d648e
commit c355a7d208
24 changed files with 1185 additions and 257 deletions
+76 -15
View File
@@ -18,6 +18,16 @@ function safeDecrypt(encrypted: string): number {
}
}
function safeDecryptStr(encrypted: string | null): string | null {
if (!encrypted) return null
try {
if (!encrypted.includes(':')) return encrypted
return decrypt(encrypted)
} catch {
return encrypted
}
}
// ========== 花名册聚合 API ==========
// 获取部门列表(去重)
@@ -161,7 +171,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
gender: e.gender,
phone: e.phone,
idCardMasked,
idCardNumber: e.idCardNumber,
idCardNumber: safeDecryptStr(e.idCardNumber),
monthlySalary: safeDecrypt(e.monthlySalary),
isPregnant: e.isPregnant,
isInMedicalPeriod: e.isInMedicalPeriod,
@@ -199,16 +209,11 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
result = result.filter((e) => e.contractStatus === contractStatus)
}
// 身份证号后N位搜索:在内存中过滤(解密完整身份证号后匹配
// 身份证号后N位搜索:在内存中过滤(idCardNumber 已解密为明文
if (isIdCardSearch) {
result = result.filter((e: any) => {
if (!e.idCardNumber) return false
try {
const fullIdCard = decrypt(e.idCardNumber)
return fullIdCard.endsWith(search!)
} catch {
return false
}
return String(e.idCardNumber).endsWith(search!)
})
}
@@ -312,7 +317,7 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
status: dynamicStatus,
monthlySalary: safeDecrypt(monthlySalary),
bankAccount: bankAccount ? safeDecrypt(bankAccount).toString() : null,
idCardNumber: idCardNumber ? safeDecrypt(idCardNumber).toString() : null,
idCardNumber: safeDecryptStr(idCardNumber),
monthlyProcessRecords,
},
})
@@ -736,13 +741,20 @@ router.get('/:id/evidence-chain/export', authMiddleware, async (req: AuthRequest
wsRisk.getRow(1).font = { bold: true }
risks.forEach((r, i) => wsRisk.addRow({ no: i + 1, ...r }))
const encodedName = encodeURIComponent(empName)
const fullFileName = `${empName}_证据链.xlsx`
const encodedName = encodeURIComponent(fullFileName)
const asciiFallback = `evidence_chain_${employee.id.slice(-8)}.xlsx`
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
res.setHeader('Content-Disposition', `attachment; filename="${encodedName}_证据链.xlsx"; filename*=UTF-8''${encodedName}_证据链.xlsx`)
await workbook.xlsx.write(res)
res.end()
} catch (err) {
next(err)
res.setHeader('Content-Disposition', `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodedName}`)
const buffer = await workbook.xlsx.writeBuffer()
res.send(Buffer.from(buffer))
} catch (err: any) {
console.error('证据链导出失败:', err?.message || err)
if (!res.headersSent) {
res.status(500).json({ success: false, error: { code: 'EXPORT_FAILED', message: `导出失败:${err?.message || '服务器错误'}` } })
} else {
res.end()
}
}
})
@@ -919,6 +931,55 @@ router.delete('/:employeeId/disciplinary/:recordId', authMiddleware, async (req:
} catch (err) { next(err) }
})
// 违纪确认证明导出
router.get('/:employeeId/disciplinary/:recordId/certificate', authMiddleware, async (req: AuthRequest, res: Response, next) => {
try {
const record = await prisma.disciplinaryRecord.findFirst({
where: { id: req.params.recordId, orgId: req.user!.orgId },
include: { employee: true },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
}
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const typeMap: Record<string, string> = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' }
const actionMap: Record<string, string> = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' }
const severityMap: Record<string, string> = { WARNING: '警告', SERIOUS: '严重', SEVERE: '重度' }
let idCard = ''
try { if (record.employee.idCardNumber) idCard = decrypt(record.employee.idCardNumber) } catch { idCard = record.employee.idCardNumber || '' }
const content = `违纪确认证明
兹证明 ${record.employee.name}(身份证号:${idCard || '___'})系我单位员工,于 ${record.violationDate.toISOString().slice(0, 10)} 发生以下违纪行为:
违纪类型:${typeMap[record.violationType] || record.violationType}
严重程度:${severityMap[record.severity] || record.severity}
违纪事实:${record.description}
处理结果:${actionMap[record.action] || record.action}${record.actionDetail ? `${record.actionDetail}` : ''}
${record.employeeAck ? `该员工已于 ${record.ackDate ? new Date(record.ackDate).toISOString().slice(0, 10) : '___'} 签字确认上述违纪事实及处理结果。${record.witness ? `见证人:${record.witness}` : ''}` : '该员工尚未签字确认。'}
特此证明。
${org?.name || ''}
${new Date().toLocaleDateString('zh-CN')}`
const blob = Buffer.from('\ufeff' + content, 'utf8')
const certFileName = `${record.employee.name}_违纪确认证明.doc`
const encodedCertName = encodeURIComponent(certFileName)
const asciiCertFallback = `disciplinary_cert_${record.id.slice(-8)}.doc`
res.setHeader('Content-Type', 'application/msword;charset=utf-8')
res.setHeader('Content-Disposition', `attachment; filename="${asciiCertFallback}"; filename*=UTF-8''${encodedCertName}`)
res.send(blob)
} catch (err: any) {
console.error('违纪确认证明导出失败:', err?.message || err)
if (!res.headersSent) {
res.status(500).json({ success: false, error: { code: 'EXPORT_FAILED', message: `导出失败:${err?.message || '服务器错误'}` } })
}
}
})
// ========== 考勤记录 CRUD ==========
router.get('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => {