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 } }