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:
selfrelease
2026-07-26 20:32:38 +08:00
parent 9cb0d1f63b
commit d79e3baa34
71 changed files with 18561 additions and 3230 deletions
+174
View File
@@ -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 }
}