feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证证据链完整性(重新计算哈希对比)
|
||||
*/
|
||||
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 eventsJson = JSON.stringify(evidence.events)
|
||||
const expectedHash = sha256(eventsJson + orgId + evidence.category + (evidence.refId || ''))
|
||||
|
||||
return {
|
||||
valid: expectedHash === evidence.hash,
|
||||
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 eventsJson = JSON.stringify(r.events)
|
||||
const expectedHash = sha256(eventsJson + orgId + r.category + (r.refId || ''))
|
||||
if (expectedHash === r.hash) valid++
|
||||
else invalid++
|
||||
}
|
||||
return { total: records.length, valid, invalid }
|
||||
}
|
||||
Reference in New Issue
Block a user