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:
@@ -77,6 +77,23 @@ function daysFromNow(date: Date): number {
|
|||||||
return Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
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> {
|
async function buildOrgContext(orgId: string): Promise<string> {
|
||||||
const [employees, risks] = await Promise.all([
|
const [employees, risks] = await Promise.all([
|
||||||
prisma.employee.findMany({
|
prisma.employee.findMany({
|
||||||
@@ -98,7 +115,7 @@ async function buildOrgContext(orgId: string): Promise<string> {
|
|||||||
if (e.isPregnant) specialStatus.push('孕期/哺乳期')
|
if (e.isPregnant) specialStatus.push('孕期/哺乳期')
|
||||||
if (e.isInMedicalPeriod) specialStatus.push('医疗期')
|
if (e.isInMedicalPeriod) specialStatus.push('医疗期')
|
||||||
if (e.isWorkInjured) 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')
|
}).join('\n')
|
||||||
|
|
||||||
const riskSummary = risks.map((r) => `- ${r.title}(${r.level}):${r.description || '无详细描述'}`).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)}
|
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
|
||||||
员工详情:
|
员工详情:
|
||||||
- 姓名:${emp.name}
|
- 姓名:${emp.name}
|
||||||
|
- 身份证号:${maskIdCard(emp.idCardNumber)}
|
||||||
- 部门:${emp.department}
|
- 部门:${emp.department}
|
||||||
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure})
|
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure})
|
||||||
- 状态:${emp.status}
|
- 状态:${emp.status}
|
||||||
- 特殊状态:${[emp.isPregnant && '孕期/哺乳期', emp.isInMedicalPeriod && '医疗期', emp.isWorkInjured && '工伤'].filter(Boolean).join('、') || '无'}
|
- 特殊状态:${[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 ? '已签署解除协议' : '未签署解除协议'}` : ''}`
|
- 合同:${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) {
|
} 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 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}`
|
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}`
|
||||||
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
|
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
|
||||||
}
|
}
|
||||||
@@ -306,16 +324,17 @@ router.get('/predict-stream', authMiddleware, async (req: AuthRequest, res, next
|
|||||||
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
|
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
|
||||||
员工详情:
|
员工详情:
|
||||||
- 姓名:${emp.name}
|
- 姓名:${emp.name}
|
||||||
|
- 身份证号:${maskIdCard(emp.idCardNumber)}
|
||||||
- 部门:${emp.department}
|
- 部门:${emp.department}
|
||||||
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure})
|
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure})
|
||||||
- 状态:${emp.status}
|
- 状态:${emp.status}
|
||||||
- 特殊状态:${[emp.isPregnant && '孕期/哺乳期', emp.isInMedicalPeriod && '医疗期', emp.isWorkInjured && '工伤'].filter(Boolean).join('、') || '无'}
|
- 特殊状态:${[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 ? '已签署解除协议' : '未签署解除协议'}` : ''}`
|
- 合同:${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) {
|
} 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 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}`
|
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}`
|
||||||
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
|
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,10 +181,12 @@ ${orgContext}
|
|||||||
|
|
||||||
请按以下格式输出:
|
请按以下格式输出:
|
||||||
【未来30天预计风险】
|
【未来30天预计风险】
|
||||||
- [员工姓名/风险描述] → [建议措施]
|
- [员工身份证号(脱敏)/姓名/风险描述] → [建议措施]
|
||||||
|
|
||||||
【优先级建议】
|
【优先级建议】
|
||||||
[先处理什么,再处理什么]`
|
[先处理什么,再处理什么]
|
||||||
|
|
||||||
|
注意:员工唯一标识为身份证号(脱敏),姓名仅作辅助参考,同名员工以身份证号区分。`
|
||||||
|
|
||||||
const response = await client.chat.completions.create({
|
const response = await client.chat.completions.create({
|
||||||
model: 'qwen-plus',
|
model: 'qwen-plus',
|
||||||
@@ -208,8 +210,8 @@ export async function* predictRisksStream(orgContext: string) {
|
|||||||
|
|
||||||
用表格列出每个风险项:
|
用表格列出每个风险项:
|
||||||
|
|
||||||
| 优先级 | 员工/范围 | 风险描述 | 法律依据 | 建议措施 | 截止日期 |
|
| 优先级 | 员工(身份证号/姓名) | 风险描述 | 法律依据 | 建议措施 | 截止日期 |
|
||||||
|--------|-----------|----------|----------|----------|----------|
|
|--------|---------------------|----------|----------|----------|----------|
|
||||||
|
|
||||||
## 优先级建议
|
## 优先级建议
|
||||||
|
|
||||||
@@ -219,6 +221,8 @@ export async function* predictRisksStream(orgContext: string) {
|
|||||||
|
|
||||||
简要总结企业当前用工合规状况和改进方向。
|
简要总结企业当前用工合规状况和改进方向。
|
||||||
|
|
||||||
|
注意:员工唯一标识为身份证号(脱敏),姓名仅作辅助参考,同名员工以身份证号区分。
|
||||||
|
|
||||||
企业数据:
|
企业数据:
|
||||||
${orgContext}`
|
${orgContext}`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user