diff --git a/backend/src/services/risk.service.ts b/backend/src/services/risk.service.ts index 02dd61d..eae8a1d 100644 --- a/backend/src/services/risk.service.ts +++ b/backend/src/services/risk.service.ts @@ -1557,7 +1557,7 @@ function fmtMoney(n: number): string { /** * 用工体检诊断:6 维度深度诊断 + 历史报告 - * 维度:合同管理 / 规章制度 / 社会保险 / 背景调查 / 工作时间 / 保密竞业 + * 维度:合同管理 / 薪酬社保 / 考勤加班 / 规章制度 / 解聘合规 / 证据链 */ export async function getHealthCheck(orgId: string) { const now = new Date() @@ -1567,17 +1567,20 @@ export async function getHealthCheck(orgId: string) { totalEmployees, unsignedContracts, expiringContracts, - contractsNoAttachment, policiesWithoutPublish, totalPolicies, socialConfig, housingConfig, employeesNoSocial, - employeesNoHousing, - employeesNoBackgroundCheck, overtimeExcessive, - employeesNoNDA, - employeesWithNDA, + totalTerminations, + completedTerminations, + terminationsWithChecklist, + disciplinaryRecords, + attendanceRecords, + trainingRecords, + unconfirmedPayslips, + totalPayslips, ] = await Promise.all([ prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }), prisma.laborContract.count({ where: { orgId, contractType: 'UNSIGNED', employee: { status: 'ACTIVE' } } }), @@ -1588,22 +1591,28 @@ export async function getHealthCheck(orgId: string) { endDate: { gte: now, lte: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000) }, }, }), - prisma.laborContract.count({ where: { orgId, attachmentUrl: null, employee: { status: 'ACTIVE' } } }), // democracyProgress 是 JSONB,用 status 字段判断未完成 prisma.policyDocument.count({ where: { orgId, status: { not: 'PUBLISHED' } } }), prisma.policyDocument.count({ where: { orgId } }), prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true } }), prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true } }), - // 社保/公积金记录按员工去重 + // 社保记录按员工去重 prisma.employeeSocialInsRecord.groupBy({ by: ['employeeId'], where: { orgId, endMonth: null }, _count: true }).then(r => r.length), - prisma.employeeHousingFundRecord.groupBy({ by: ['employeeId'], where: { orgId, endMonth: null }, _count: true }).then(r => r.length), - prisma.employee.count({ where: { orgId, status: 'ACTIVE', id: { notIn: [] } } }), prisma.overtimeRecord.count({ where: { orgId, weekdayHours: { gt: 36 } } }), - prisma.employee.count({ where: { orgId, status: 'ACTIVE', contracts: { none: { contractType: 'FIXED' } } } }), - prisma.employee.count({ where: { orgId, status: 'ACTIVE', contracts: { some: { contractType: 'FIXED' } } } }), + // 解聘合规 + prisma.terminationRecord.count({ where: { orgId } }), + prisma.terminationRecord.count({ where: { orgId, status: 'COMPLETED' } }), + prisma.terminationRecord.count({ where: { orgId, status: 'COMPLETED', checklist: { not: {} } } }), + // 证据链 + prisma.disciplinaryRecord.count({ where: { orgId } }), + prisma.attendanceRecord.count({ where: { orgId } }), + prisma.trainingRecord.count({ where: { orgId } }), + // 薪酬 + prisma.payslip.count({ where: { orgId, confirmedAt: null } }), + prisma.payslip.count({ where: { orgId } }), ]) - // 6 维度诊断 + // 6 维度诊断(与评分标准说明一致) const dimensions = [ { key: 'contract', @@ -1618,6 +1627,42 @@ export async function getHealthCheck(orgId: string) { expiringContracts > 0 ? '提前30天发送续签意向书,保留协商证据' : '', ].filter(Boolean), }, + { + key: 'salary_social', + name: '薪酬社保', + score: Math.round( + (socialConfig ? 40 : 0) + + (housingConfig ? 20 : 0) + + (totalPayslips > 0 ? Math.max(0, 20 - Math.round((unconfirmedPayslips / totalPayslips) * 20)) : 20) + + (employeesNoSocial === 0 ? 20 : Math.max(0, 20 - employeesNoSocial * 2)) + ), + findings: [ + socialConfig ? '社保配置已设置' : '未配置社保比例,建议尽快设置', + housingConfig ? '公积金配置已设置' : '未配置公积金比例', + totalPayslips > 0 + ? `${unconfirmedPayslips}/${totalPayslips} 张工资条未确认` + : '暂无工资条记录', + employeesNoSocial > 0 ? `${employeesNoSocial} 名员工社保记录异常` : '社保缴纳记录正常', + ].filter(Boolean), + recommendations: [ + !socialConfig ? '前往社保管理页面设置企业/个人缴纳比例' : '', + !housingConfig ? '根据当地政策配置公积金比例' : '', + unconfirmedPayslips > 0 ? '提醒员工及时确认工资条,保留薪酬合规证据' : '', + ].filter(Boolean), + }, + { + key: 'attendance', + name: '考勤加班', + score: Math.max(0, 100 - overtimeExcessive * 10), + findings: [ + overtimeExcessive > 0 ? `${overtimeExcessive} 条加班记录月超时36小时,存在违法风险` : '加班时长合规', + attendanceRecords > 0 ? `已记录 ${attendanceRecords} 条考勤数据` : '暂无考勤记录,建议建立考勤制度', + ].filter(Boolean), + recommendations: [ + overtimeExcessive > 0 ? '月加班不得超过36小时,建议调休或减少加班安排' : '', + attendanceRecords === 0 ? '建立考勤记录制度,保留出勤证据' : '', + ].filter(Boolean), + }, { key: 'policy', name: '规章制度', @@ -1634,56 +1679,55 @@ export async function getHealthCheck(orgId: string) { ].filter(Boolean), }, { - key: 'social', - name: '社会保险', - score: (socialConfig ? 50 : 0) + (housingConfig ? 30 : 0) + (employeesNoSocial === 0 ? 20 : Math.max(0, 20 - employeesNoSocial * 2)), - findings: [ - socialConfig ? '社保配置已设置' : '未配置社保比例,建议尽快设置', - housingConfig ? '公积金配置已设置' : '未配置公积金比例', - employeesNoSocial > 0 ? `${employeesNoSocial} 名员工社保记录异常` : '社保缴纳记录正常', - ].filter(Boolean), - recommendations: [ - !socialConfig ? '前往社保管理页面设置企业/个人缴纳比例' : '', - !housingConfig ? '根据当地政策配置公积金比例' : '', - ].filter(Boolean), - }, - { - key: 'background', - name: '背景调查', - score: 70, - findings: [ - '背景调查功能待接入第三方服务', - totalEmployees > 0 ? `当前 ${totalEmployees} 名在管员工,建议关键岗位完成背调` : '', - ].filter(Boolean), - recommendations: [ - '建议对财务、管理、技术核心岗位入职前完成背景调查', - ], - }, - { - key: 'workingtime', - name: '工作时间', - score: Math.max(0, 100 - overtimeExcessive * 10), - findings: [ - overtimeExcessive > 0 ? `${overtimeExcessive} 条加班记录月超时36小时,存在违法风险` : '加班时长合规', - ].filter(Boolean), - recommendations: [ - overtimeExcessive > 0 ? '月加班不得超过36小时,建议调休或减少加班安排' : '', - ].filter(Boolean), - }, - { - key: 'nda', - name: '保密竞业', - score: totalEmployees > 0 - ? Math.round((employeesWithNDA / Math.max(1, totalEmployees)) * 100) + key: 'termination', + name: '解聘合规', + score: totalTerminations > 0 + ? Math.round( + (completedTerminations / totalTerminations) * 60 + + (terminationsWithChecklist / Math.max(1, completedTerminations)) * 40 + ) : 100, findings: [ - totalEmployees > 0 - ? `${employeesWithNDA}/${totalEmployees} 名员工签署保密/竞业协议` - : '暂无在管员工', - employeesNoNDA > 0 ? `${employeesNoNDA} 名员工未签保密协议,建议核心岗位补签` : '', + totalTerminations > 0 + ? `共 ${totalTerminations} 起解聘,${completedTerminations} 起已完成流程` + : '暂无解聘记录', + totalTerminations > 0 && completedTerminations < totalTerminations + ? `${totalTerminations - completedTerminations} 起解聘流程未完成,存在程序瑕疵风险` + : '', + terminationsWithChecklist > 0 + ? `${terminationsWithChecklist} 起解聘已保存合规检查清单` + : totalTerminations > 0 ? '建议完善解聘合规检查清单' : '', ].filter(Boolean), recommendations: [ - employeesNoNDA > 0 ? '对技术、财务、销售岗位补签保密竞业协议' : '', + completedTerminations < totalTerminations ? '完成所有解聘流程审批,确保程序合法' : '', + totalTerminations > 0 ? '保留解聘协议、补偿计算依据等关键证据' : '', + ].filter(Boolean), + }, + { + key: 'evidence', + name: '证据链', + score: totalEmployees > 0 + ? Math.min(100, Math.round( + ((disciplinaryRecords > 0 ? 1 : 0) * 20) + + ((attendanceRecords > 0 ? 1 : 0) * 30) + + ((trainingRecords > 0 ? 1 : 0) * 10) + + (totalPolicies > 0 ? 20 : 0) + + (totalTerminations > 0 ? 10 : 10) + + (totalPayslips > 0 ? 10 : 0) + )) + : 100, + findings: [ + disciplinaryRecords > 0 ? `违纪记录 ${disciplinaryRecords} 条` : '无违纪记录', + attendanceRecords > 0 ? `考勤记录 ${attendanceRecords} 条` : '无考勤记录', + trainingRecords > 0 ? `培训记录 ${trainingRecords} 条` : '无培训记录', + totalEmployees > 0 + ? `覆盖 ${totalEmployees} 名在管员工的证据留存` + : '暂无在管员工', + ].filter(Boolean), + recommendations: [ + disciplinaryRecords === 0 ? '对违纪行为及时记录并保留证据' : '', + attendanceRecords === 0 ? '建立考勤记录制度,保留出勤证据' : '', + trainingRecords === 0 ? '记录员工培训情况,保留签到表' : '', ].filter(Boolean), }, ]