Files
TurboHR/backend/src/services/evidence.service.ts
T

204 lines
6.3 KiB
TypeScript

import prisma from '../lib/prisma'
import { sha256 } from '../lib/crypto'
/**
* 证据链服务 — 管理操作证据链,用于劳动仲裁举证
*/
export type EvidenceCategory =
| 'CONTRACT_SIGN'
| 'ONBOARD'
| 'PAYSLIP_CONFIRM'
| 'DISCIPLINARY'
| 'ATTENDANCE'
| 'TERMINATION'
/**
* 创建证据链记录
*/
export async function createEvidence(params: {
orgId: string
category: EvidenceCategory
refId?: string
employeeId?: string
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 || ''))
return prisma.evidenceChain.create({
data: {
orgId: params.orgId,
category: params.category,
refId: params.refId || null,
employeeId: params.employeeId || null,
events: params.events,
hash,
createdBy: params.createdBy,
},
})
}
/**
* 追加证据事件到已有证据链
*/
export async function appendEvidence(orgId: string, evidenceId: string, event: { action: string; timestamp: string; ip?: string; userAgent?: string; smsCode?: string; location?: string }) {
const existing = await prisma.evidenceChain.findFirst({ where: { id: evidenceId, orgId } })
if (!existing) {
throw { code: 'NOT_FOUND', message: '证据链不存在' }
}
const events = [...(existing.events as any[]), event]
const eventsJson = JSON.stringify(events)
const hash = sha256(eventsJson + orgId + existing.category + (existing.refId || ''))
return prisma.evidenceChain.update({
where: { id: evidenceId },
data: { events, hash },
})
}
/**
* 获取某操作的完整证据链
*/
export async function getEvidenceByRef(orgId: string, category: string, refId: string) {
return prisma.evidenceChain.findMany({
where: { orgId, category, refId },
orderBy: { createdAt: 'asc' },
})
}
/**
* 获取某员工所有证据链
*/
export async function getEvidenceByEmployee(orgId: string, employeeId: string) {
return prisma.evidenceChain.findMany({
where: { orgId, employeeId },
orderBy: { createdAt: 'desc' },
})
}
/**
* 获取证据链详情
*/
export async function getEvidenceDetail(orgId: string, id: string) {
const evidence = await prisma.evidenceChain.findFirst({ where: { id, orgId } })
if (!evidence) {
throw { code: 'NOT_FOUND', message: '证据链不存在' }
}
return evidence
}
/**
* 验证证据链完整性(重新计算哈希对比)
* 注意:PostgreSQL 的 json 类型会按 key 字母序重排属性,导致 JSON.stringify 结果与原始不一致。
* 因此验证时需要尝试两种方式:原始序列化和排序后序列化。
*/
export async function verifyEvidence(orgId: string, id: string): Promise<{ valid: boolean; expectedHash: string; actualHash: string }> {
const evidence = await prisma.evidenceChain.findFirst({ where: { id, orgId } })
if (!evidence) {
throw { code: 'NOT_FOUND', message: '证据链不存在' }
}
const events = evidence.events as any[]
const eventsJson = JSON.stringify(events)
const expectedHash = sha256(eventsJson + 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,
}
}
return {
valid: true,
expectedHash,
actualHash: evidence.hash,
}
}
/**
* 获取组织级证据链列表(支持按类别筛选)
*/
export async function getEvidenceList(orgId: string, category?: string, page: number = 1, pageSize: number = 20) {
const where: any = { orgId }
if (category && category !== 'ALL') {
where.category = category
}
const [total, records] = await Promise.all([
prisma.evidenceChain.count({ where }),
prisma.evidenceChain.findMany({
where,
include: { employee: { select: { id: true, name: true, department: true } } },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
])
return {
total,
page,
pageSize,
records: records.map((r: any) => {
const events = Array.isArray(r.events) ? r.events : []
const firstEvent = events.length > 0 ? events[0] : null
const lastEvent = events.length > 0 ? events[events.length - 1] : null
return {
id: r.id,
category: r.category,
refId: r.refId,
employeeId: r.employeeId,
employeeName: r.employee?.name || null,
employeeDept: r.employee?.department || null,
eventCount: events.length,
firstAction: firstEvent?.action || null,
lastAction: lastEvent?.action || null,
firstTimestamp: firstEvent?.timestamp || null,
lastTimestamp: lastEvent?.timestamp || null,
hash: r.hash,
hashShort: r.hash ? r.hash.substring(0, 16) : null,
createdAt: r.createdAt.toISOString(),
}
}),
}
}
/**
* 验证全部证据链完整性
*/
export async function verifyAllEvidence(orgId: string) {
const records = await prisma.evidenceChain.findMany({ where: { orgId } })
let valid = 0
let invalid = 0
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 || ''))
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++
else invalid++
}
return { total: records.length, valid, invalid }
}