feat: AI历史记录、企业信息seed、员工状态自动刷新、风险预测数据准确性优化

- 智能问答/风险预测/合同审查/案例匹配 均支持历史记录保存、加载、删除
- 后端 predict/predict-stream 补充离职记录详情和预计算工龄/天数,避免AI计算错误
- Organization 增加 contactName/contactPhone 字段,seed 企业信息
- Settings 页面修复 orgData/usersData 取值路径错误
- Roster/Termination/Contracts mutation 增加 roster cache invalidation
- 员工档案页面 tab 固定、内容区域滚动到窗口底部
- 解聘证据链显示逻辑修复(协商一致离职 vs 员工主动离职)
This commit is contained in:
freedakgmail
2026-07-25 00:33:51 +08:00
parent bb25e1731c
commit 169a0e1117
12 changed files with 828 additions and 70 deletions
+150 -12
View File
@@ -1,6 +1,6 @@
import { Router } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { chat, chatStream, reviewContract, matchCase, predictRisks } from '../services/ai.service'
import { chat, chatStream, reviewContract, matchCase, predictRisks, predictRisksStream } from '../services/ai.service'
import { seedKnowledgeBase, addKnowledge, searchKnowledge, ensureRAGTable } from '../services/rag.service'
import prisma from '../lib/prisma'
import { z } from 'zod'
@@ -48,6 +48,31 @@ async function recordUsage(orgId: string, userId: string, type: 'chat' | 'review
})
}
// 日期计算工具:计算两个日期之间的年月日差,以及相对当前的天数差
function calcDuration(start: Date, end: Date): string {
let years = end.getFullYear() - start.getFullYear()
let months = end.getMonth() - start.getMonth()
let days = end.getDate() - start.getDate()
if (days < 0) {
months--
const prevMonth = new Date(end.getFullYear(), end.getMonth(), 0)
days += prevMonth.getDate()
}
if (months < 0) {
years--
months += 12
}
const parts: string[] = []
if (years > 0) parts.push(`${years}`)
if (months > 0) parts.push(`${months}个月`)
if (days > 0) parts.push(`${days}`)
return parts.length > 0 ? parts.join('') : '不足1天'
}
function daysFromNow(date: Date): number {
return Math.floor((date.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
}
async function buildOrgContext(orgId: string): Promise<string> {
const [employees, risks] = await Promise.all([
prisma.employee.findMany({
@@ -63,14 +88,13 @@ async function buildOrgContext(orgId: string): Promise<string> {
const now = new Date()
const empSummary = employees.map((e) => {
const contract = e.contracts[0]
const daysToExpire = contract?.endDate
? Math.floor((new Date(contract.endDate).getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
: null
const daysToExpire = contract?.endDate ? daysFromNow(new Date(contract.endDate)) : null
const tenure = calcDuration(new Date(e.hireDate), now)
const specialStatus: 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)}${contract ? `合同:${contract.contractType}${contract.endDate ? `到期${contract.endDate.toISOString().slice(0, 10)}(剩余${daysToExpire}天)` : '无固定期限'}` : '未签合同'}${specialStatus.length > 0 ? `,特殊状态:${specialStatus.join('/')}` : ''}`
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('/')}` : ''}`
}).join('\n')
const riskSummary = risks.map((r) => `- ${r.title}${r.level}):${r.description || '无详细描述'}`).join('\n')
@@ -199,27 +223,48 @@ router.get('/predict', authMiddleware, async (req: AuthRequest, res, next) => {
const riskType = req.query.riskType as string
let orgContext = await buildOrgContext(req.user!.orgId)
let scopeHint = ''
if (employeeId) {
const emp = await prisma.employee.findFirst({ where: { id: employeeId, orgId: req.user!.orgId }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } } })
const emp = await prisma.employee.findFirst({ where: { id: employeeId, orgId: req.user!.orgId }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, terminations: { orderBy: { createdAt: 'desc' }, take: 1 } } })
if (emp) {
const contract = emp.contracts[0]
orgContext = `员工详情:
const term = emp.terminations[0]
const statusMap: Record<string, string> = { DRAFT: '草稿', PENDING_APPROVAL: '待审批', APPROVED: '已审批', EXECUTING: '执行中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已撤销' }
const typeMap: Record<string, string> = { RESIGNATION: '员工离职', TERMINATION: '公司解聘' }
const reasonMap: Record<string, string> = { NEGOTIATED: '协商一致', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', RESIGNATION: '员工主动离职' }
const checklist = term?.checklist as any || {}
const handoverItems = (term?.handoverItems as any[]) || []
const handoverDone = handoverItems.length > 0 ? handoverItems.every((h: any) => h.done) : false
const agreementSigned = !!checklist.agreement_signed
const socialInsEnd = term?.socialInsEndMonth ? `,社保截止${term.socialInsEndMonth}` : ''
const housingFundEnd = term?.housingFundEndMonth ? `,公积金截止${term.housingFundEndMonth}` : ''
const tenure = calcDuration(new Date(emp.hireDate), term ? new Date(term.terminationDate) : new Date())
const contractDaysLeft = contract?.endDate ? daysFromNow(new Date(contract.endDate)) : null
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
员工详情:
- 姓名:${emp.name}
- 部门:${emp.department}
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure}
- 状态:${emp.status}
- 特殊状态:${emp.isPregnant ? '孕期/哺乳期 ' : ''}${emp.isInMedicalPeriod ? '医疗期 ' : ''}${emp.isWorkInjured ? '工伤' : '无'}
- 合同:${contract ? `${contract.contractType}${contract.startDate.toISOString().slice(0, 10)}${contract.endDate ? contract.endDate.toISOString().slice(0, 10) : '无固定期限'}` : '未签合同'}\n${orgContext}`
- 合同:${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}】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。`
}
} 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')
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}\n\n${orgContext}`
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}`
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
}
if (riskType && riskType !== 'all') {
orgContext = `请重点关注【${riskType === 'contract' ? '合同' : riskType === 'salary' ? '薪酬' : riskType === 'termination' ? '解聘' : riskType}】类风险。\n\n${orgContext}`
const riskLabel = riskType === 'contract' ? '合同' : riskType === 'salary' ? '薪酬' : riskType === 'termination' ? '解聘' : riskType
scopeHint += `\n请重点关注【${riskLabel}】类风险,其他类型风险可简要提及。`
}
if (scopeHint) {
orgContext = `${scopeHint}\n\n${orgContext}`
}
const result = await predictRisks(orgContext)
@@ -229,12 +274,105 @@ router.get('/predict', authMiddleware, async (req: AuthRequest, res, next) => {
}
})
router.get('/predict-stream', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const department = req.query.department as string
const employeeId = req.query.employeeId as string
const riskType = req.query.riskType as string
let orgContext = await buildOrgContext(req.user!.orgId)
let scopeHint = ''
if (employeeId) {
const emp = await prisma.employee.findFirst({ where: { id: employeeId, orgId: req.user!.orgId }, include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, terminations: { orderBy: { createdAt: 'desc' }, take: 1 } } })
if (emp) {
const contract = emp.contracts[0]
const term = emp.terminations[0]
const statusMap: Record<string, string> = { DRAFT: '草稿', PENDING_APPROVAL: '待审批', APPROVED: '已审批', EXECUTING: '执行中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已撤销' }
const typeMap: Record<string, string> = { RESIGNATION: '员工离职', TERMINATION: '公司解聘' }
const reasonMap: Record<string, string> = { NEGOTIATED: '协商一致', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', RESIGNATION: '员工主动离职' }
const checklist = term?.checklist as any || {}
const handoverItems = (term?.handoverItems as any[]) || []
const handoverDone = handoverItems.length > 0 ? handoverItems.every((h: any) => h.done) : false
const agreementSigned = !!checklist.agreement_signed
const socialInsEnd = term?.socialInsEndMonth ? `,社保截止${term.socialInsEndMonth}` : ''
const housingFundEnd = term?.housingFundEndMonth ? `,公积金截止${term.housingFundEndMonth}` : ''
const tenure = calcDuration(new Date(emp.hireDate), term ? new Date(term.terminationDate) : new Date())
const contractDaysLeft = contract?.endDate ? daysFromNow(new Date(contract.endDate)) : null
orgContext = `当前日期:${new Date().toISOString().slice(0, 10)}
员工详情:
- 姓名:${emp.name}
- 部门:${emp.department}
- 入职日期:${emp.hireDate.toISOString().slice(0, 10)}(已工作${tenure}
- 状态:${emp.status}
- 特殊状态:${emp.isPregnant ? '孕期/哺乳期 ' : ''}${emp.isInMedicalPeriod ? '医疗期 ' : ''}${emp.isWorkInjured ? '工伤' : '无'}
- 合同:${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}】进行风险预测,不要分析其他员工。以上数据中的工龄、天数等均已由系统计算,请直接使用,不要重新计算。`
}
} 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')
orgContext = `部门【${department}】员工列表(${employees.length}人):\n${empSummary}`
scopeHint = `请只针对【${department}】部门的员工进行风险预测,不要分析其他部门的员工。`
}
if (riskType && riskType !== 'all') {
const riskLabel = riskType === 'contract' ? '合同' : riskType === 'salary' ? '薪酬' : riskType === 'termination' ? '解聘' : riskType
scopeHint += `\n请重点关注【${riskLabel}】类风险,其他类型风险可简要提及。`
}
if (scopeHint) {
orgContext = `${scopeHint}\n\n${orgContext}`
}
await checkUsageLimit(req.user!.orgId, 'chat')
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.setHeader('X-Accel-Buffering', 'no')
res.flushHeaders()
let usageRecorded = false
try {
for await (const delta of predictRisksStream(orgContext)) {
res.write(`data: ${JSON.stringify({ delta })}\n\n`)
if (typeof (res as any).flush === 'function') (res as any).flush()
}
res.write('data: [DONE]\n\n')
} catch (streamErr: any) {
res.write(`data: ${JSON.stringify({ error: streamErr.message || 'AI 服务异常' })}\n\n`)
res.write('data: [DONE]\n\n')
} finally {
if (!usageRecorded) {
await recordUsage(req.user!.orgId, req.user!.id, 'chat')
usageRecorded = true
}
}
res.end()
} catch (err) {
if (!res.headersSent) next(err)
else res.end()
}
})
// ========== AI 会话历史 ==========
router.get('/conversations', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const type = req.query.type as string | undefined
const where: any = { orgId: req.user!.orgId, userId: req.user!.id }
if (type) {
const prefixes: Record<string, string> = {
chat: 'chat:',
predict: 'predict:',
review: 'review:',
case: 'case:',
}
const prefix = prefixes[type] || type
where.title = { startsWith: prefix }
}
const conversations = await prisma.aIConversation.findMany({
where: { orgId: req.user!.orgId, userId: req.user!.id },
where,
orderBy: { updatedAt: 'desc' },
take: 50,
select: { id: true, title: true, createdAt: true, updatedAt: true },
+110 -9
View File
@@ -105,7 +105,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
contractType: 'UNSIGNED',
hireDate: e.hireDate,
})
const isResigned = e.terminations.some((t) => t.terminationDate <= today)
const isResigned = e.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today)
const isPreHire = !isResigned && e.hireDate > todayEnd
const dynamicStatus = isResigned ? 'RESIGNED' : (isPreHire ? 'PRE_HIRE' : 'ACTIVE')
return {
@@ -179,7 +179,7 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
const { monthlySalary, bankAccount, idCardNumber, ...rest } = employee
const today = new Date()
today.setHours(0, 0, 0, 0)
const dynamicStatus = employee.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE'
const dynamicStatus = employee.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE'
res.json({
success: true,
data: {
@@ -220,6 +220,11 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
const empDept = employee.department
const hireDate = employee.hireDate.toISOString().slice(0, 10)
// 风险检测
const risks: any[] = []
const today = new Date()
today.setHours(0, 0, 0, 0)
// 1. 劳动关系证据
evidence.push({
category: '劳动关系',
@@ -228,17 +233,86 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
description: `${empName}${hireDate}入职${empDept},建立劳动关系。`,
evidenceType: 'EMPLOYMENT',
})
// 无合同风险检测
if (employee.contracts.length === 0) {
const daysSinceHire = Math.floor((today.getTime() - employee.hireDate.getTime()) / (86400000))
if (daysSinceHire > 30) {
risks.push({
level: daysSinceHire > 365 ? 'DANGER' : 'HIGH',
category: '劳动关系',
title: '未签订书面劳动合同',
description: `入职已${daysSinceHire}天仍未签订书面劳动合同,超过30天未签合同将面临双倍工资赔偿风险(《劳动合同法》第82条)。${daysSinceHire > 365 ? '已满一年未签合同,视为已订立无固定期限劳动合同。' : ''}`,
})
}
}
employee.contracts.forEach((c) => {
const contractTypeText = ({ FIXED: '固定期限', UNFIXED: '无固定期限', LABOR: '劳务协议', INTERNSHIP: '实习协议', UNSIGNED: '未签订' } as Record<string, string>)[c.contractType] || '未签订'
const isSigned = !!c.signDate
evidence.push({
category: '劳动关系',
title: `合同(${({ FIXED: '固定期限', UNFIXED: '无固定期限', LABOR: '劳务协议', INTERNSHIP: '实习协议', UNSIGNED: '未签订' } as Record<string, string>)[c.contractType] || '未签订'}`,
title: `合同(${contractTypeText}`,
date: c.signDate ? c.signDate.toISOString().slice(0, 10) : c.startDate.toISOString().slice(0, 10),
description: `合同期限:${c.startDate.toISOString().slice(0, 10)}${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},试用期${c.probationMonths}个月,试用期工资¥${c.probationSalary}`,
description: `合同期限:${c.startDate.toISOString().slice(0, 10)}${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},试用期${c.probationMonths}个月,试用期工资¥${c.probationSalary}${isSigned ? '' : '⚠ 该合同尚未签订。'}`,
evidenceType: 'CONTRACT',
signed: !!c.signDate,
signed: isSigned,
riskLevel: !isSigned ? 'HIGH' : undefined,
})
// 合同未签字风险
if (!isSigned) {
risks.push({
level: 'HIGH',
category: '劳动关系',
title: '合同未签字',
description: `合同(${contractTypeText})期限${c.startDate.toISOString().slice(0, 10)}${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},尚未签订。未签合同在仲裁中无法证明劳动关系约定内容。`,
})
}
// 合同过期风险
if (c.endDate && c.endDate < today) {
const expiredDays = Math.floor((today.getTime() - c.endDate.getTime()) / 86400000)
risks.push({
level: expiredDays > 30 ? 'HIGH' : 'MEDIUM',
category: '劳动关系',
title: '合同已过期',
description: `合同已于${c.endDate.toISOString().slice(0, 10)}过期,过期${expiredDays}天。过期后继续用工满一个月未续签的,面临双倍工资风险。`,
})
} else if (c.endDate) {
const daysToExpire = Math.floor((c.endDate.getTime() - today.getTime()) / 86400000)
if (daysToExpire <= 30 && daysToExpire >= 0) {
risks.push({
level: 'MEDIUM',
category: '劳动关系',
title: '合同即将到期',
description: `合同将于${c.endDate.toISOString().slice(0, 10)}到期,剩余${daysToExpire}天。请及时办理续签或终止手续。`,
})
}
}
// 试用期工资为0风险
if (c.probationMonths > 0 && c.probationSalary === 0) {
risks.push({
level: 'MEDIUM',
category: '劳动关系',
title: '试用期工资为0',
description: `合同约定试用期${c.probationMonths}个月但试用期工资为0,违反《劳动合同法》第20条(试用期工资不得低于本单位相同岗位最低档工资的80%或劳动合同约定工资的80%)。`,
})
}
})
// 无工资条风险检测(入职超过1个月但无工资条)
const daysSinceHire = Math.floor((today.getTime() - employee.hireDate.getTime()) / 86400000)
if (daysSinceHire > 30 && employee.payslips.length === 0) {
risks.push({
level: 'MEDIUM',
category: '薪酬发放',
title: '无工资条记录',
description: `入职已${daysSinceHire}天但无任何工资条记录,在仲裁中难以证明已按时足额支付工资。建议尽快创建发薪批次并归档。`,
})
}
// 2. 薪酬证据
employee.payslips.forEach((p) => {
evidence.push({
@@ -317,14 +391,39 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
// 7. 解聘证据
employee.terminations.forEach((t) => {
const reasonMap: Record<string, string> = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期' }
const reasonMap: Record<string, string> = { NEGOTIATED: '协商一致', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', RESIGNATION: '员工主动离职' }
const typeLabel = t.type === 'RESIGNATION' && t.reason === 'NEGOTIATED'
? '协商一致离职'
: t.type === 'RESIGNATION' ? '员工主动离职' : '公司解聘'
const statusMap: Record<string, string> = { DRAFT: '草稿', PENDING_APPROVAL: '待审批', APPROVED: '已审批', EXECUTING: '执行中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已撤销' }
evidence.push({
category: '解聘记录',
title: `${t.terminationDate.toISOString().slice(0, 10)} 解聘记录`,
title: `${t.terminationDate.toISOString().slice(0, 10)} ${typeLabel}记录`,
date: t.terminationDate.toISOString().slice(0, 10),
description: `解聘原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}${t.remark || ''}`,
description: `类型:${typeLabel}原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}流程状态:${statusMap[t.status] || t.status}${t.resignationReason ? `离职原因:${t.resignationReason}` : ''}${t.remark || ''}`,
evidenceType: 'TERMINATION',
acknowledged: t.status === 'COMPLETED',
})
// 离职流程未完成风险
if (t.status !== 'COMPLETED') {
risks.push({
level: 'HIGH',
category: '解聘记录',
title: '离职流程未完成',
description: `${typeLabel}流程当前状态为「${statusMap[t.status] || t.status}」,尚未完成闭环。离职日期${t.terminationDate.toISOString().slice(0, 10)},若未签署解除协议/离职确认书、未完成工作交接、未结清工资、未办理社保减员,在仲裁中无法证明离职的合法性与完整性,面临继续履行合同或违法解除赔偿(2N)风险。`,
})
}
// 协商解除但补偿金为0
if (t.reason === 'NEGOTIATED' && t.compensation === 0) {
risks.push({
level: 'MEDIUM',
category: '解聘记录',
title: '协商解除但补偿金为0',
description: `解聘原因为协商解除但经济补偿金为0,若员工事后主张非自愿离职,企业需举证协商一致且员工自愿放弃补偿,否则可能被认定为单方违法解除,面临2N赔偿风险。`,
})
}
})
res.json({
@@ -334,15 +433,17 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
name: empName,
department: empDept,
hireDate,
status: employee.terminations.some((t) => t.terminationDate <= new Date()) ? 'RESIGNED' : 'ACTIVE',
status: employee.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= new Date()) ? 'RESIGNED' : 'ACTIVE',
gender: employee.gender,
phone: employee.phone,
},
evidence,
risks,
summary: {
total: evidence.length,
signed: evidence.filter((e) => e.acknowledged === true).length,
unsigned: evidence.filter((e) => e.acknowledged === false).length,
riskCount: risks.length,
},
},
})
+6 -3
View File
@@ -27,7 +27,7 @@ router.get('/org', async (req: AuthRequest, res, next) => {
try {
const org = await prisma.organization.findUnique({
where: { id: req.user!.orgId },
select: { id: true, name: true, plan: true, maxEmployees: true, createdAt: true },
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollFrequency: true, createdAt: true },
})
res.json({ success: true, data: org })
} catch (err) {
@@ -38,14 +38,17 @@ router.get('/org', async (req: AuthRequest, res, next) => {
// 更新企业信息
router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const { name, payrollFrequency } = req.body as { name?: string; payrollFrequency?: number }
const { name, payrollFrequency, city, contactName, contactPhone } = req.body as { name?: string; payrollFrequency?: number; city?: string; contactName?: string; contactPhone?: string }
const updateData: any = {}
if (name) updateData.name = name
if (payrollFrequency !== undefined) updateData.payrollFrequency = payrollFrequency
if (city !== undefined) updateData.city = city
if (contactName !== undefined) updateData.contactName = contactName
if (contactPhone !== undefined) updateData.contactPhone = contactPhone
const org = await prisma.organization.update({
where: { id: req.user!.orgId },
data: updateData,
select: { id: true, name: true, plan: true, maxEmployees: true, payrollFrequency: true },
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollFrequency: true },
})
res.json({ success: true, data: org })
} catch (err) {
+40
View File
@@ -193,3 +193,43 @@ ${orgContext}
return response.choices[0]?.message?.content || ''
}
export async function* predictRisksStream(orgContext: string) {
const prompt = `基于以下企业用工数据,预测未来30天可能出现的合规风险,并给出优先级建议。
请使用 Markdown 格式输出,包含以下部分:
## 未来30天预计风险
用表格列出每个风险项:
| 优先级 | 员工/范围 | 风险描述 | 法律依据 | 建议措施 | 截止日期 |
|--------|-----------|----------|----------|----------|----------|
## 优先级建议
按紧急程度排序,说明先处理什么、再处理什么。
## 合规建议总结
简要总结企业当前用工合规状况和改进方向。
企业数据:
${orgContext}`
const stream = await client.chat.completions.create({
model: 'qwen-plus',
messages: [
{ role: 'system', content: '你是劳动用工风险预测专家,能基于企业数据分析未来风险趋势。请使用 Markdown 格式输出,善用表格、加粗、列表等格式。' },
{ role: 'user', content: prompt },
],
temperature: 0.5,
max_tokens: 2000,
stream: true,
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content
if (delta) yield delta
}
}