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:
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user