fix: 身份证号必填+去重+报告按姓名兜底

- 后端 createEmployeeSchema 加 idCardNumber 必填18位校验
- 后端 updateEmployeeSchema 加 idCardNumber 可选编辑
- updateEmployee 支持编辑身份证号,自动生成 hash/生日/性别
- Excel 导入时身份证号必填,为空跳过
- 年度价值报告去重 key 改为 idCardHash > name > employeeId 兜底
This commit is contained in:
selfrelease
2026-07-31 09:24:44 +08:00
parent 57587dd5ed
commit f5a03f82bc
4 changed files with 15 additions and 2 deletions
+1
View File
@@ -260,6 +260,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
if (salary === '0') { result.skipped++; result.errors.push(`员工第${i + 2}行:月工资为空`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '月工资为空' }); continue }
let idCard = val(getField(r, '身份证号'))
if (!idCard) { result.skipped++; result.errors.push(`员工第${i + 2}行:身份证号为空,跳过`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: '身份证号为空' }); continue }
if (idCard) {
const idCheck = validateIdCard(idCard)
if (!idCheck.valid) { result.skipped++; result.errors.push(`员工第${i + 2}行:${idCheck.error}`); result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'skipped', message: idCheck.error }); continue }
+2
View File
@@ -5,6 +5,7 @@ export const createEmployeeSchema = z.object({
department: z.string().min(1, '部门不能为空').max(50, '部门最多50个字'),
hireDate: z.string().datetime(),
monthlySalary: z.string().min(1, '月薪不能为空'),
idCardNumber: z.string().min(18, '身份证号不能为空且必须18位').max(18, '身份证号必须18位'),
gender: z.enum(['男', '女']).optional(),
femaleWorkerType: z.enum(['CADRE', 'WORKER']).optional(),
phone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
@@ -30,6 +31,7 @@ export const updateEmployeeSchema = z.object({
department: z.string().min(1).max(50).optional(),
hireDate: z.string().datetime().optional(),
monthlySalary: z.string().min(1).optional(),
idCardNumber: z.string().min(18).max(18).optional(),
gender: z.enum(['男', '女']).optional(),
femaleWorkerType: z.enum(['CADRE', 'WORKER']).optional(),
phone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
+10
View File
@@ -1,6 +1,7 @@
import prisma from '../lib/prisma'
import { encrypt, decrypt, sha256 } from '../lib/crypto'
import { runRiskDetection } from './risk.service'
import { extractBirthDateFromIdCard, extractGenderFromIdCard } from './retirement.service'
function daysBetween(a: Date, b: Date): number {
return Math.floor((a.getTime() - b.getTime()) / (1000 * 60 * 60 * 24))
@@ -525,6 +526,15 @@ export async function updateEmployee(orgId: string, id: string, data: any) {
if (data.gender !== undefined) updateData.gender = data.gender
if (data.femaleWorkerType !== undefined) updateData.femaleWorkerType = data.femaleWorkerType
if (data.phone !== undefined) updateData.phone = data.phone
if (data.idCardNumber !== undefined) {
updateData.idCardNumber = encrypt(data.idCardNumber)
updateData.idCardHash = sha256(data.idCardNumber)
updateData.birthDate = extractBirthDateFromIdCard(data.idCardNumber)
if (!updateData.gender) {
const gender = extractGenderFromIdCard(data.idCardNumber)
if (gender) updateData.gender = gender
}
}
if (data.bankName !== undefined) updateData.bankName = data.bankName
if (data.bankAccount !== undefined) updateData.bankAccount = encrypt(data.bankAccount)
if (data.emergencyContact !== undefined) updateData.emergencyContact = data.emergencyContact
+2 -2
View File
@@ -1674,8 +1674,8 @@ export async function getAnnualValueReport(orgId: string, year: number) {
}
for (const r of employeeRiskDetails) {
// 按身份证号分组,无身份证号时回退到 employeeId
const personKey = r.employee?.idCardHash || r.employeeId || '_unknown'
// 去重优先级:身份证号 > 姓名回退到姓名,避免同一人多条 Employee 记录被重复计算
const personKey = r.employee?.idCardHash || r.employee?.name || r.employeeId || '_unknown'
if (!personBreakdown[personKey]) {
personBreakdown[personKey] = {
personKey,