feat: AI风险检查以身份证号为员工唯一标识

- buildOrgContext 员工列表加入脱敏身份证号(前6后4,中间*)
- 单员工/部门预测上下文加入脱敏身份证号
- AI prompt 更新:要求用身份证号标识员工,姓名仅辅助参考
- 同名员工以身份证号区分

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-17 11:48:42 +08:00
parent 874f8c49bd
commit d3a9ea041f
2 changed files with 32 additions and 9 deletions
+24 -5
View File
@@ -77,6 +77,23 @@ function daysFromNow(date: Date): number {
return Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
}
/**
* 脱敏身份证号:保留前6位和后4位,中间用 * 代替
* 例:310101199001011234 → 310101********1234
*/
function maskIdCard(encrypted: string | null): string {
if (!encrypted) return '未登记'
try {
const plain = decrypt(encrypted)
if (plain && plain.length >= 10) {
return plain.slice(0, 6) + '********' + plain.slice(-4)
}
return '未登记'
} catch {
return '未登记'
}
}
async function buildOrgContext(orgId: string): Promise<string> {
const [employees, risks] = await Promise.all([
prisma.employee.findMany({
@@ -98,7 +115,7 @@ async function buildOrgContext(orgId: string): Promise<string> {
if (e.isPregnant) specialStatus.push('孕期/哺乳期')
if (e.isInMedicalPeriod) specialStatus.push('医疗期')
if (e.isWorkInjured) specialStatus.push('工伤')
return `- ${e.name}${e.department}),入职${e.hireDate.toISOString().slice(0, 10)}(已工作${tenure}),${contract ? `合同:${contract.contractType}${contract.endDate ? `到期${contract.endDate.toISOString().slice(0, 10)}(剩余${daysToExpire}天)` : '无固定期限'}` : '未签合同'}${specialStatus.length > 0 ? `,特殊状态:${specialStatus.join('/')}` : ''}`
return `- ${e.name}身份证:${maskIdCard(e.idCardNumber)}${e.department}),入职${e.hireDate.toISOString().slice(0, 10)}(已工作${tenure}),${contract ? `合同:${contract.contractType}${contract.endDate ? `到期${contract.endDate.toISOString().slice(0, 10)}(剩余${daysToExpire}天)` : '无固定期限'}` : '未签合同'}${specialStatus.length > 0 ? `,特殊状态:${specialStatus.join('/')}` : ''}`
}).join('\n')
const riskSummary = risks.map((r) => `- ${r.title}${r.level}):${r.description || '无详细描述'}`).join('\n')
@@ -248,16 +265,17 @@ router.get('/predict', authMiddleware, async (req: AuthRequest, res, next) => {
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
员工详情:
- 姓名:${emp.name}
- 身份证号:${maskIdCard(emp.idCardNumber)}
- 部门:${emp.department}
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure}
- 状态:${emp.status}
- 特殊状态:${[emp.isPregnant && '孕期/哺乳期', emp.isInMedicalPeriod && '医疗期', emp.isWorkInjured && '工伤'].filter(Boolean).join('、') || '无'}
- 合同:${contract ? `${contract.contractType}${contract.startDate.toISOString().slice(0, 10)}${contract.endDate ? contract.endDate.toISOString().slice(0, 10) : '无固定期限'}${contractDaysLeft !== null ? `(距到期${contractDaysLeft}天)` : ''}` : '未签合同'}${term ? `\n- 离职/解聘记录:${typeMap[term.type] || term.type},原因:${reasonMap[term.reason] || term.reason},离职日期:${term.terminationDate.toISOString().slice(0, 10)},流程状态:${statusMap[term.status] || term.status},经济补偿金:¥${term.compensation.toFixed(2)}${term.resignationReason ? `,离职原因说明:${term.resignationReason}` : ''}${socialInsEnd}${housingFundEnd}${handoverDone ? '工作交接已完成' : '工作交接未完成'}${agreementSigned ? '已签署解除协议' : '未签署解除协议'}` : ''}`
scopeHint = `请只针对员工【${emp.name}】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。`
scopeHint = `请只针对员工【${emp.name}(身份证:${maskIdCard(emp.idCardNumber)}】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。`
}
} else if (department) {
const employees = await prisma.employee.findMany({ where: { orgId: req.user!.orgId, department, status: 'ACTIVE' }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } })
const empSummary = employees.map(e => `- ${e.name},入职${e.hireDate.toISOString().slice(0, 10)}${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n')
const empSummary = employees.map(e => `- ${e.name}(身份证:${maskIdCard(e.idCardNumber)},入职${e.hireDate.toISOString().slice(0, 10)}${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n')
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}`
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
}
@@ -306,16 +324,17 @@ router.get('/predict-stream', authMiddleware, async (req: AuthRequest, res, next
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
员工详情:
- 姓名:${emp.name}
- 身份证号:${maskIdCard(emp.idCardNumber)}
- 部门:${emp.department}
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure}
- 状态:${emp.status}
- 特殊状态:${[emp.isPregnant && '孕期/哺乳期', emp.isInMedicalPeriod && '医疗期', emp.isWorkInjured && '工伤'].filter(Boolean).join('、') || '无'}
- 合同:${contract ? `${contract.contractType}${contract.startDate.toISOString().slice(0, 10)}${contract.endDate ? contract.endDate.toISOString().slice(0, 10) : '无固定期限'}${contractDaysLeft !== null ? `(距到期${contractDaysLeft}天)` : ''}` : '未签合同'}${term ? `\n- 离职/解聘记录:${typeMap[term.type] || term.type},原因:${reasonMap[term.reason] || term.reason},离职日期:${term.terminationDate.toISOString().slice(0, 10)},流程状态:${statusMap[term.status] || term.status},经济补偿金:¥${term.compensation.toFixed(2)}${term.resignationReason ? `,离职原因说明:${term.resignationReason}` : ''}${socialInsEnd}${housingFundEnd}${handoverDone ? '工作交接已完成' : '工作交接未完成'}${agreementSigned ? '已签署解除协议' : '未签署解除协议'}` : ''}`
scopeHint = `请只针对员工【${emp.name}】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。`
scopeHint = `请只针对员工【${emp.name}(身份证:${maskIdCard(emp.idCardNumber)}】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。`
}
} else if (department) {
const employees = await prisma.employee.findMany({ where: { orgId: req.user!.orgId, department, status: 'ACTIVE' }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } })
const empSummary = employees.map(e => `- ${e.name},入职${e.hireDate.toISOString().slice(0, 10)}${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n')
const empSummary = employees.map(e => `- ${e.name}(身份证:${maskIdCard(e.idCardNumber)},入职${e.hireDate.toISOString().slice(0, 10)}${e.contracts[0] ? e.contracts[0].contractType : '未签合同'}`).join('\n')
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}`
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
}
+8 -4
View File
@@ -181,10 +181,12 @@ ${orgContext}
请按以下格式输出:
【未来30天预计风险】
- [员工姓名/风险描述] → [建议措施]
- [员工身份证号(脱敏)/姓名/风险描述] → [建议措施]
【优先级建议】
[先处理什么,再处理什么]`
[先处理什么,再处理什么]
注意:员工唯一标识为身份证号(脱敏),姓名仅作辅助参考,同名员工以身份证号区分。`
const response = await client.chat.completions.create({
model: 'qwen-plus',
@@ -208,8 +210,8 @@ export async function* predictRisksStream(orgContext: string) {
用表格列出每个风险项:
| 优先级 | 员工/范围 | 风险描述 | 法律依据 | 建议措施 | 截止日期 |
|--------|-----------|----------|----------|----------|----------|
| 优先级 | 员工(身份证号/姓名) | 风险描述 | 法律依据 | 建议措施 | 截止日期 |
|--------|---------------------|----------|----------|----------|----------|
## 优先级建议
@@ -219,6 +221,8 @@ export async function* predictRisksStream(orgContext: string) {
简要总结企业当前用工合规状况和改进方向。
注意:员工唯一标识为身份证号(脱敏),姓名仅作辅助参考,同名员工以身份证号区分。
企业数据:
${orgContext}`