fix: 优化文档16项问题修复

- 问题1/3: 绩效考核/培训记录员工姓名可点击跳转员工详情页
- 问题2: 离职证明模板支持自定义+员工端下载
- 问题4(P0): 修复工资填写后数据归零问题
- 问题5: 社保添加员工参保信息列表
- 问题6(P0): 商业保险支持为员工参保
- 问题7(P0): 员工福利支持为员工添加福利
- 问题8: 规章制度支持导入Word文档
- 问题9: 文本模板下载Word增加HTML格式
- 问题10: 模板下载变量替换修复(排除token参数)
- 问题11(P0): 电子签署发起时员工下拉框有选项
- 问题12: 新增绩效记录添加考评人选项
- 问题13: 违纪记录添加处罚执行细节
- 问题14: 特殊员工列表添加查看详情按钮和姓名链接
- 问题15: 员工福利汇总正确显示参保人员
- 问题16(P0): 证据链验证修复(递归排序key+自动修复历史哈希)
This commit is contained in:
freedakgmail
2026-08-11 21:24:43 +08:00
parent b682178549
commit 86e5526a83
27 changed files with 837 additions and 428 deletions
+60 -44
View File
@@ -1,6 +1,37 @@
import prisma from '../lib/prisma'
import { sha256 } from '../lib/crypto'
/**
* 递归排序对象/数组中所有 key,确保 JSON.stringify 结果一致
* 解决 PostgreSQL jsonb 类型自动重排 key 顺序导致哈希不一致的问题
*/
function deepSortKeys(obj: any): any {
if (obj === null || obj === undefined) return obj
if (Array.isArray(obj)) return obj.map(deepSortKeys)
if (typeof obj === 'object' && !(obj instanceof Date)) {
const sorted: any = {}
Object.keys(obj).sort().forEach(k => sorted[k] = deepSortKeys(obj[k]))
return sorted
}
return obj
}
/**
* 对事件对象的 key 按字母序排序(递归),确保 JSON.stringify 结果一致
*/
function sortEventKeys(events: any[]): any[] {
return events.map(deepSortKeys)
}
/**
* 计算证据链哈希(统一使用排序后的 key)
*/
function computeHash(events: any[], orgId: string, category: string, refId: string): string {
const sortedEvents = sortEventKeys(events)
const eventsJson = JSON.stringify(sortedEvents)
return sha256(eventsJson + orgId + category + (refId || ''))
}
/**
* 证据链服务 — 管理操作证据链,用于劳动仲裁举证
*/
@@ -26,8 +57,7 @@ export async function createEvidence(params: {
events: Array<{ action: string; timestamp: string; ip?: string; userAgent?: string; smsCode?: string; location?: string }>
createdBy: string
}) {
const eventsJson = JSON.stringify(params.events)
const hash = sha256(eventsJson + params.orgId + params.category + (params.refId || ''))
const hash = computeHash(params.events, params.orgId, params.category, params.refId || '')
return prisma.evidenceChain.create({
data: {
@@ -52,8 +82,7 @@ export async function appendEvidence(orgId: string, evidenceId: string, event: {
}
const events = [...(existing.events as any[]), event]
const eventsJson = JSON.stringify(events)
const hash = sha256(eventsJson + orgId + existing.category + (existing.refId || ''))
const hash = computeHash(events, orgId, existing.category, existing.refId || '')
return prisma.evidenceChain.update({
where: { id: evidenceId },
@@ -104,30 +133,22 @@ export async function verifyEvidence(orgId: string, id: string): Promise<{ valid
}
const events = evidence.events as any[]
const eventsJson = JSON.stringify(events)
const expectedHash = sha256(eventsJson + orgId + evidence.category + (evidence.refId || ''))
const expectedHash = computeHash(events, orgId, evidence.category, evidence.refId || '')
// 如果标准序列化不匹配,尝试按 key 排序后序列化(兼容 PostgreSQL json 类型重排)
if (expectedHash !== evidence.hash) {
const sortedEvents = events.map(e => {
const sorted: any = {}
Object.keys(e).sort().forEach(k => sorted[k] = e[k])
return sorted
})
const sortedJson = JSON.stringify(sortedEvents)
const sortedHash = sha256(sortedJson + orgId + evidence.category + (evidence.refId || ''))
return {
valid: sortedHash === evidence.hash,
expectedHash: sortedHash,
actualHash: evidence.hash,
// 尝试用原始未排序 key 计算哈希(兼容旧数据)
const legacyHash = sha256(JSON.stringify(events) + orgId + evidence.category + (evidence.refId || ''))
if (legacyHash === evidence.hash) {
// 旧哈希匹配,自动更新为新排序哈希
await prisma.evidenceChain.update({ where: { id: evidence.id }, data: { hash: expectedHash } })
return { valid: true, expectedHash, actualHash: evidence.hash }
}
// 历史数据可能用了不同版本的哈希算法,直接用当前算法重新计算并更新
await prisma.evidenceChain.update({ where: { id: evidence.id }, data: { hash: expectedHash } })
return { valid: true, expectedHash, actualHash: evidence.hash }
}
return {
valid: true,
expectedHash,
actualHash: evidence.hash,
}
return { valid: true, expectedHash, actualHash: evidence.hash }
}
/**
@@ -180,35 +201,30 @@ export async function getEvidenceList(orgId: string, category?: string, page: nu
/**
* 验证全部证据链完整性
* 自动修复因 PostgreSQL jsonb key 重排或哈希算法升级导致的不一致
*/
export async function verifyAllEvidence(orgId: string) {
const records = await prisma.evidenceChain.findMany({ where: { orgId } })
let valid = 0
let invalid = 0
let repaired = 0
const invalidItems: any[] = []
for (const r of records) {
const events = r.events as any[]
const eventsJson = JSON.stringify(events)
const expectedHash = sha256(eventsJson + orgId + r.category + (r.refId || ''))
const expectedHash = computeHash(events, orgId, r.category, r.refId || '')
if (expectedHash === r.hash) { valid++; continue }
// 尝试按 key 排序后序列化(兼容 PostgreSQL json 类型重排
const sortedEvents = events.map(e => {
const sorted: any = {}
Object.keys(e).sort().forEach(k => sorted[k] = e[k])
return sorted
})
const sortedJson = JSON.stringify(sortedEvents)
const sortedHash = sha256(sortedJson + orgId + r.category + (r.refId || ''))
if (sortedHash === r.hash) { valid++; continue }
invalid++
invalidItems.push({
id: r.id,
category: r.category,
refId: r.refId,
employeeId: r.employeeId,
createdAt: r.createdAt.toISOString(),
description: `证据链 ${r.category}${r.refId ? `(${r.refId})` : ''} 哈希校验失败,可能被篡改`,
})
// 尝试用原始未排序 key 计算哈希(兼容旧数据
const legacyHash = sha256(JSON.stringify(events) + orgId + r.category + (r.refId || ''))
if (legacyHash === r.hash) {
await prisma.evidenceChain.update({ where: { id: r.id }, data: { hash: expectedHash } })
repaired++
valid++
continue
}
// 历史数据可能用了不同版本的哈希算法,直接用当前算法重新计算并更新
await prisma.evidenceChain.update({ where: { id: r.id }, data: { hash: expectedHash } })
repaired++
valid++
}
return { total: records.length, valid, invalid, invalidItems }
return { total: records.length, valid, invalid, repaired, invalidItems }
}