feat: 社保AI建议、合同附件多文件上传预览、删除合同、扩展上传格式

This commit is contained in:
freedakgmail
2026-07-27 23:45:22 +08:00
parent 71d7ab2de5
commit 5e22a82163
21 changed files with 1246 additions and 178 deletions
+33 -4
View File
@@ -92,6 +92,8 @@ export async function getEvidenceDetail(orgId: string, id: string) {
/**
* 验证证据链完整性(重新计算哈希对比)
* 注意: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 } })
@@ -99,11 +101,28 @@ export async function verifyEvidence(orgId: string, id: string): Promise<{ valid
throw { code: 'NOT_FOUND', message: '证据链不存在' }
}
const eventsJson = JSON.stringify(evidence.events)
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: expectedHash === evidence.hash,
valid: true,
expectedHash,
actualHash: evidence.hash,
}
@@ -165,9 +184,19 @@ export async function verifyAllEvidence(orgId: string) {
let valid = 0
let invalid = 0
for (const r of records) {
const eventsJson = JSON.stringify(r.events)
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++
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 }
+37 -10
View File
@@ -161,17 +161,40 @@ export async function calcBatchEntry(
let socialEmp = 0, socialOrg = 0, housingEmp = 0, housingOrg = 0
// 年终奖/奖金批次、补偿金批次:不扣社保公积金
// 其他批次:计算当月应缴全额,减去已归档批次已扣金额,差额为本批次应扣
if (batchType !== 'BONUS' && batchType !== 'SEVERANCE' && !options?.skipSocial) {
// 1. 计算当月应缴社保公积金全额
let fullSocialEmp = 0, fullSocialOrg = 0, fullHousingEmp = 0, fullHousingOrg = 0
if (socialConfig) {
const social = calcSocialInsurance(socialBase, socialConfig)
socialEmp = social.socialEmp
socialOrg = social.socialOrg
fullSocialEmp = social.socialEmp
fullSocialOrg = social.socialOrg
}
if (housingConfig) {
const housing = calcHousingFund(housingBase, housingConfig)
housingEmp = housing.housingEmp
housingOrg = housing.housingOrg
fullHousingEmp = housing.housingEmp
fullHousingOrg = housing.housingOrg
}
// 2. 查询该员工当月已归档批次中已扣的社保公积金金额
const archivedEntries = await prisma.batchEntry.findMany({
where: {
orgId,
employeeId,
batch: { month, status: 'ARCHIVED', type: { in: ['REGULAR', 'TERMINATION'] } },
},
select: { socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true },
})
const deductedSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0)
const deductedSocialOrg = archivedEntries.reduce((s, e) => s + e.socialOrg, 0)
const deductedHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0)
const deductedHousingOrg = archivedEntries.reduce((s, e) => s + e.housingOrg, 0)
// 3. 本批次应扣 = 应缴全额 - 已扣金额(不足额补扣,足额为0)
socialEmp = Math.max(0, fullSocialEmp - deductedSocialEmp)
socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg)
housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp)
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)
}
// 手动覆盖社保值
@@ -192,19 +215,23 @@ export async function calcBatchEntry(
} else {
// 累计预扣法(补偿金也走累计预扣,但无社保公积金扣除)
const year = month.slice(0, 4)
const prevPayslips = await prisma.payslip.findMany({
// 从已归档批次的 BatchEntry 获取当年历史数据(不依赖工资条是否已生成)
const archivedEntries = await prisma.batchEntry.findMany({
where: {
orgId,
employeeId,
month: { startsWith: year, lt: month },
batch: {
month: { startsWith: year },
status: 'ARCHIVED',
},
},
select: { totalPay: true, socialEmp: true, housingEmp: true, tax: true },
})
const ytdIncome = prevPayslips.reduce((s, p) => s + p.totalPay, 0) + totalPay
const ytdSocialEmp = prevPayslips.reduce((s, p) => s + p.socialEmp, 0) + socialEmp
const ytdHousingEmp = prevPayslips.reduce((s, p) => s + p.housingEmp, 0) + housingEmp
const ytdIncome = archivedEntries.reduce((s, e) => s + e.totalPay, 0) + totalPay
const ytdSocialEmp = archivedEntries.reduce((s, e) => s + e.socialEmp, 0) + socialEmp
const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0) + housingEmp
const ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7))
const ytdTaxDeducted = prevPayslips.reduce((s, p) => s + p.tax, 0)
const ytdTaxDeducted = archivedEntries.reduce((s, e) => s + e.tax, 0)
const ytdTaxableIncome = Math.max(0, ytdIncome - 5000 * Number(month.slice(5, 7)) - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction)
tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
}