fix: 审计日志补充关键操作详情,前端改为可读格式显示
This commit is contained in:
@@ -94,7 +94,8 @@ router.post('/:id/rehire', authMiddleware, async (req: AuthRequest, res, next) =
|
||||
router.delete('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await deleteEmployee(req.user!.orgId, req.params.id)
|
||||
await auditLog(req, 'DELETE', 'EMPLOYEE', req.params.id)
|
||||
const emp = await prisma.employee.findFirst({ where: { id: req.params.id }, select: { name: true, department: true } })
|
||||
await auditLog(req, 'DELETE', 'EMPLOYEE', req.params.id, { employeeName: emp?.name || '', department: emp?.department || '' })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -196,7 +197,7 @@ router.post('/contracts/batch-renew', authMiddleware, async (req: AuthRequest, r
|
||||
try {
|
||||
const data = batchRenewSchema.parse(req.body)
|
||||
const result = await batchRenew(req.user!.orgId, req.user!.id, data.contractIds, data.years)
|
||||
await auditLog(req, 'BATCH_RENEW', 'CONTRACT', undefined, { count: data.contractIds.length })
|
||||
await auditLog(req, 'BATCH_RENEW', 'CONTRACT', undefined, { count: data.contractIds.length, years: data.years, contractIds: data.contractIds })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
@@ -207,7 +208,8 @@ router.post('/contracts', authMiddleware, async (req: AuthRequest, res, next) =>
|
||||
try {
|
||||
const data = addContractSchema.parse(req.body)
|
||||
const result = await addContract(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'ADD_CONTRACT', 'CONTRACT', result.id, { employeeId: data.employeeId })
|
||||
const emp = await prisma.employee.findFirst({ where: { id: data.employeeId }, select: { name: true } })
|
||||
await auditLog(req, 'ADD_CONTRACT', 'CONTRACT', result.id, { employeeName: emp?.name || '', employeeId: data.employeeId, contractType: data.contractType, startDate: data.startDate, endDate: data.endDate })
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'CONTRACT_SIGN',
|
||||
@@ -231,7 +233,8 @@ router.delete('/contracts/:contractId', authMiddleware, async (req: AuthRequest,
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '合同不存在' } })
|
||||
}
|
||||
await prisma.laborContract.delete({ where: { id: req.params.contractId } })
|
||||
await auditLog(req, 'DELETE_CONTRACT', 'CONTRACT', req.params.contractId, { employeeId: contract.employeeId })
|
||||
const emp = await prisma.employee.findFirst({ where: { id: contract.employeeId }, select: { name: true } })
|
||||
await auditLog(req, 'DELETE_CONTRACT', 'CONTRACT', req.params.contractId, { employeeName: emp?.name || '', employeeId: contract.employeeId, contractType: contract.contractType, startDate: contract.startDate, endDate: contract.endDate })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
|
||||
@@ -526,7 +526,7 @@ router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'DISCIPLINARY', record.id, { employeeId: req.params.employeeId })
|
||||
await auditLog(req, 'CREATE', 'DISCIPLINARY', record.id, { employeeId: req.params.employeeId, violationType, severity, action })
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'DISCIPLINARY',
|
||||
@@ -666,7 +666,7 @@ router.post('/:employeeId/training', authMiddleware, async (req: AuthRequest, re
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'TRAINING', record.id, { employeeId: req.params.employeeId })
|
||||
await auditLog(req, 'CREATE', 'TRAINING', record.id, { employeeId: req.params.employeeId, topic, trainer, duration })
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
@@ -749,7 +749,7 @@ router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest,
|
||||
reviewer,
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'PERFORMANCE', record.id, { employeeId: req.params.employeeId })
|
||||
await auditLog(req, 'CREATE', 'PERFORMANCE', record.id, { employeeId: req.params.employeeId, period, score, grade, reviewer })
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -98,7 +98,13 @@ router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next)
|
||||
router.delete('/:id/revoke', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await revokeTermination(req.user!.orgId, req.params.id)
|
||||
await auditLog(req, 'REVOKE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {})
|
||||
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true } })
|
||||
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
|
||||
await auditLog(req, 'REVOKE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
|
||||
employeeName: emp?.name || '',
|
||||
reason: termRecord?.reason || '',
|
||||
type: termRecord?.type || '',
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT') {
|
||||
@@ -183,7 +189,15 @@ router.get('/handover-template', authMiddleware, async (req: AuthRequest, res) =
|
||||
router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await createDraft(req.user!.orgId, req.user!.id, req.body)
|
||||
await auditLog(req, 'CREATE_DRAFT', 'TERMINATION_RECORD', result.id, { reason: req.body.reason })
|
||||
const emp = await prisma.employee.findFirst({ where: { id: req.body.employeeId }, select: { name: true, department: true } })
|
||||
await auditLog(req, 'CREATE_DRAFT', 'TERMINATION_RECORD', result.id, {
|
||||
employeeName: emp?.name || '',
|
||||
department: emp?.department || '',
|
||||
reason: req.body.reason || '',
|
||||
type: req.body.type || 'TERMINATION',
|
||||
terminationDate: req.body.terminationDate || '',
|
||||
compensation: req.body.compensation || 0,
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'NOT_FOUND') {
|
||||
@@ -210,7 +224,13 @@ router.put('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) =>
|
||||
router.post('/draft/:id/submit', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await submitForApproval(req.user!.orgId, req.params.id, req.user!.id)
|
||||
await auditLog(req, 'SUBMIT_TERMINATION', 'TERMINATION_RECORD', req.params.id, {})
|
||||
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true } })
|
||||
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
|
||||
await auditLog(req, 'SUBMIT_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
|
||||
employeeName: emp?.name || '',
|
||||
reason: termRecord?.reason || '',
|
||||
type: termRecord?.type || '',
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
|
||||
@@ -225,7 +245,13 @@ router.post('/draft/:id/approve', authMiddleware, async (req: AuthRequest, res,
|
||||
try {
|
||||
const { comment } = req.body
|
||||
const result = await approveTermination(req.user!.orgId, req.params.id, req.user!.id, comment || '')
|
||||
await auditLog(req, 'APPROVE_TERMINATION', 'TERMINATION_RECORD', req.params.id, { comment })
|
||||
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true } })
|
||||
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
|
||||
await auditLog(req, 'APPROVE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
|
||||
employeeName: emp?.name || '',
|
||||
reason: termRecord?.reason || '',
|
||||
comment: comment || '',
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
|
||||
@@ -240,7 +266,13 @@ router.post('/draft/:id/reject', authMiddleware, async (req: AuthRequest, res, n
|
||||
try {
|
||||
const { comment } = req.body
|
||||
const result = await rejectTermination(req.user!.orgId, req.params.id, req.user!.id, comment || '')
|
||||
await auditLog(req, 'REJECT_TERMINATION', 'TERMINATION_RECORD', req.params.id, { comment })
|
||||
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true } })
|
||||
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
|
||||
await auditLog(req, 'REJECT_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
|
||||
employeeName: emp?.name || '',
|
||||
reason: termRecord?.reason || '',
|
||||
comment: comment || '',
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
|
||||
@@ -254,7 +286,16 @@ router.post('/draft/:id/reject', authMiddleware, async (req: AuthRequest, res, n
|
||||
router.post('/draft/:id/execute', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await executeTermination(req.user!.orgId, req.params.id, req.user!.id)
|
||||
await auditLog(req, 'EXECUTE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {})
|
||||
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true, terminationDate: true, compensation: true } })
|
||||
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true, department: true } }) : null
|
||||
await auditLog(req, 'EXECUTE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
|
||||
employeeName: emp?.name || '',
|
||||
department: emp?.department || '',
|
||||
reason: termRecord?.reason || '',
|
||||
type: termRecord?.type || '',
|
||||
terminationDate: termRecord?.terminationDate?.toISOString().slice(0, 10) || '',
|
||||
compensation: termRecord?.compensation || 0,
|
||||
})
|
||||
await createEvidence({
|
||||
orgId: req.user!.orgId,
|
||||
category: 'TERMINATION',
|
||||
@@ -276,7 +317,13 @@ router.post('/draft/:id/execute', authMiddleware, async (req: AuthRequest, res,
|
||||
router.post('/draft/:id/cancel', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await cancelTermination(req.user!.orgId, req.params.id, req.user!.id)
|
||||
await auditLog(req, 'CANCEL_TERMINATION', 'TERMINATION_RECORD', req.params.id, {})
|
||||
const termRecord = await prisma.terminationRecord.findFirst({ where: { id: req.params.id }, select: { employeeId: true, reason: true, type: true } })
|
||||
const emp = termRecord ? await prisma.employee.findFirst({ where: { id: termRecord.employeeId }, select: { name: true } }) : null
|
||||
await auditLog(req, 'CANCEL_TERMINATION', 'TERMINATION_RECORD', req.params.id, {
|
||||
employeeName: emp?.name || '',
|
||||
reason: termRecord?.reason || '',
|
||||
type: termRecord?.type || '',
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
|
||||
|
||||
@@ -63,6 +63,57 @@ const ENTITY_LABELS: Record<string, string> = {
|
||||
NotificationSetting: '通知设置',
|
||||
}
|
||||
|
||||
/** 审计日志 detail 字段中文标签映射 */
|
||||
const DETAIL_LABELS: Record<string, string> = {
|
||||
employeeName: '员工',
|
||||
department: '部门',
|
||||
name: '姓名',
|
||||
reason: '原因',
|
||||
type: '类型',
|
||||
terminationDate: '离职日期',
|
||||
compensation: '补偿金',
|
||||
contractType: '合同类型',
|
||||
startDate: '开始日期',
|
||||
endDate: '结束日期',
|
||||
employeeId: '员工ID',
|
||||
count: '数量',
|
||||
years: '续签年限',
|
||||
hireDate: '入职日期',
|
||||
oldSalary: '原薪资',
|
||||
newSalary: '新薪资',
|
||||
oldDepartment: '原部门',
|
||||
newDepartment: '新部门',
|
||||
comment: '备注',
|
||||
resignationReason: '离职原因',
|
||||
batch: '批量',
|
||||
violationType: '违纪类型',
|
||||
severity: '严重程度',
|
||||
action: '处理措施',
|
||||
topic: '培训主题',
|
||||
trainer: '培训师',
|
||||
duration: '时长',
|
||||
period: '考核周期',
|
||||
score: '得分',
|
||||
grade: '等级',
|
||||
reviewer: '考评人',
|
||||
}
|
||||
|
||||
/** 将 detail 对象格式化为可读字符串 */
|
||||
function formatDetail(detail: any): string {
|
||||
if (!detail || typeof detail !== 'object') return ''
|
||||
const entries = Object.entries(detail).filter(([, v]) => v !== '' && v !== null && v !== undefined)
|
||||
if (entries.length === 0) return ''
|
||||
return entries.map(([k, v]) => {
|
||||
const label = DETAIL_LABELS[k] || k
|
||||
let val = String(v)
|
||||
if (k === 'compensation' && val && val !== '0') val = `¥${val}`
|
||||
if (k === 'startDate' || k === 'endDate' || k === 'terminationDate' || k === 'hireDate') {
|
||||
val = String(v).slice(0, 10)
|
||||
}
|
||||
return `${label}: ${val}`
|
||||
}).join(' | ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统操作日志页面
|
||||
*/
|
||||
@@ -168,11 +219,11 @@ export default function AuditLog() {
|
||||
<span className="text-sm font-medium">{ACTION_LABELS[log.action] || log.action}</span>
|
||||
<span className="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{ENTITY_LABELS[log.entity] || log.entity}</span>
|
||||
</div>
|
||||
{log.detail && (
|
||||
{log.detail && (() => { const formatted = formatDetail(log.detail); return formatted ? (
|
||||
<div className="text-xs text-gray-500 mt-0.5 truncate">
|
||||
{typeof log.detail === 'object' ? JSON.stringify(log.detail).slice(0, 100) : String(log.detail).slice(0, 100)}
|
||||
{formatted}
|
||||
</div>
|
||||
)}
|
||||
) : null; })()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 flex-shrink-0 text-right">
|
||||
<div>{new Date(log.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</div>
|
||||
|
||||
Reference in New Issue
Block a user