diff --git a/backend/src/routes/ai.routes.ts b/backend/src/routes/ai.routes.ts index 924773f..d0b5762 100644 --- a/backend/src/routes/ai.routes.ts +++ b/backend/src/routes/ai.routes.ts @@ -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 { const [employees, risks] = await Promise.all([ prisma.employee.findMany({ @@ -98,7 +115,7 @@ async function buildOrgContext(orgId: string): Promise { 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}】部门的员工进行风险预测,不要分析其他部门的员工。` } diff --git a/backend/src/services/ai.service.ts b/backend/src/services/ai.service.ts index 2049db8..0957c17 100644 --- a/backend/src/services/ai.service.ts +++ b/backend/src/services/ai.service.ts @@ -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}`