feat: 退休提醒功能 - 身份证提取出生日期、渐进式退休年龄计算、AI流式获取政策、用户确认生效
This commit is contained in:
@@ -233,3 +233,122 @@ ${orgContext}`
|
||||
if (delta) yield delta
|
||||
}
|
||||
}
|
||||
|
||||
export interface RetirementPolicyResult {
|
||||
content: string
|
||||
maleRetireAge: number
|
||||
femaleRetireAge: number
|
||||
femaleWorkerAge: number
|
||||
}
|
||||
|
||||
export async function fetchRetirementPolicy(): Promise<RetirementPolicyResult> {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const prompt = `当前日期是${today}。请提供中国最新的法定退休年龄政策。
|
||||
|
||||
已知背景信息(请以此为准):
|
||||
- 2024年9月13日,全国人大常委会通过《关于实施渐进式延迟法定退休年龄的决定》,2025年1月1日起正式实施。
|
||||
- 渐进式延迟退休改革方案(15年过渡期,2025年1月1日至2039年12月31日):
|
||||
- 男性职工:从60岁逐步延迟到63岁,每4个月延迟1个月
|
||||
- 女性干部/管理岗:从55岁逐步延迟到58岁,每4个月延迟1个月
|
||||
- 女性工人/操作岗:从50岁逐步延迟到55岁,每2个月延迟1个月
|
||||
- 改革前基准:男60岁、女干部55岁、女工人50岁(1978年国发〔1978〕104号)
|
||||
|
||||
请根据当前日期${today},计算当前渐进式延迟退休已实施的进度,给出当前实际适用的法定退休年龄(精确到月)。
|
||||
|
||||
要求:
|
||||
1. 说明当前政策状态和已实施的延迟进度
|
||||
2. 给出截至当前日期,各类人员实际适用的法定退休年龄
|
||||
3. 在末尾用以下JSON格式输出结构化数据(用<JSON>和</JSON>标记包裹):
|
||||
<JSON>
|
||||
{
|
||||
"maleRetireAge": <计算后的男性退休年龄,如60.33表示60岁4个月>,
|
||||
"femaleRetireAge": <计算后的女性干部退休年龄>,
|
||||
"femaleWorkerAge": <计算后的女性工人退休年龄>
|
||||
}
|
||||
</JSON>
|
||||
其中:
|
||||
- maleRetireAge: 当前男性法定退休年龄(含渐进延迟,必须填入计算后的值,不要填基准60)
|
||||
- femaleRetireAge: 当前女性干部/管理岗法定退休年龄(必须填入计算后的值,不要填基准55)
|
||||
- femaleWorkerAge: 当前女性工人/操作岗法定退休年龄(必须填入计算后的值,不要填基准50)
|
||||
- 年龄用小数表示,整数部分为岁,小数部分为月/12,如60岁4个月=60.33,50岁8个月=50.67
|
||||
|
||||
请确保JSON中的数值与你在正文中计算出的退休年龄完全一致,不要使用改革前的基准值。`
|
||||
|
||||
const response = await client.chat.completions.create({
|
||||
model: 'qwen-plus',
|
||||
messages: [
|
||||
{ role: 'system', content: '你是劳动政策专家,精通中国退休政策法规。请提供准确的政策信息。' },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 2000,
|
||||
})
|
||||
|
||||
const text = response.choices[0]?.message?.content || ''
|
||||
|
||||
let maleRetireAge = 60
|
||||
let femaleRetireAge = 55
|
||||
let femaleWorkerAge = 50
|
||||
|
||||
const jsonMatch = text.match(/<JSON>([\s\S]*?)<\/JSON>/)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[1].trim())
|
||||
if (parsed.maleRetireAge) maleRetireAge = Number(parsed.maleRetireAge)
|
||||
if (parsed.femaleRetireAge) femaleRetireAge = Number(parsed.femaleRetireAge)
|
||||
if (parsed.femaleWorkerAge) femaleWorkerAge = Number(parsed.femaleWorkerAge)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return { content: text, maleRetireAge, femaleRetireAge, femaleWorkerAge }
|
||||
}
|
||||
|
||||
export async function* fetchRetirementPolicyStream(): AsyncGenerator<string> {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const prompt = `当前日期是${today}。请提供中国最新的法定退休年龄政策。
|
||||
|
||||
已知背景信息(请以此为准):
|
||||
- 2024年9月13日,全国人大常委会通过《关于实施渐进式延迟法定退休年龄的决定》,2025年1月1日起正式实施。
|
||||
- 渐进式延迟退休改革方案(15年过渡期,2025年1月1日至2039年12月31日):
|
||||
- 男性职工:从60岁逐步延迟到63岁,每4个月延迟1个月
|
||||
- 女性干部/管理岗:从55岁逐步延迟到58岁,每4个月延迟1个月
|
||||
- 女性工人/操作岗:从50岁逐步延迟到55岁,每2个月延迟1个月
|
||||
- 改革前基准:男60岁、女干部55岁、女工人50岁(1978年国发〔1978〕104号)
|
||||
|
||||
请根据当前日期${today},计算当前渐进式延迟退休已实施的进度,给出当前实际适用的法定退休年龄(精确到月)。
|
||||
|
||||
要求:
|
||||
1. 说明当前政策状态和已实施的延迟进度
|
||||
2. 给出截至当前日期,各类人员实际适用的法定退休年龄
|
||||
3. 在末尾用以下JSON格式输出结构化数据(用<JSON>和</JSON>标记包裹):
|
||||
<JSON>
|
||||
{
|
||||
"maleRetireAge": <计算后的男性退休年龄,如60.33表示60岁4个月>,
|
||||
"femaleRetireAge": <计算后的女性干部退休年龄>,
|
||||
"femaleWorkerAge": <计算后的女性工人退休年龄>
|
||||
}
|
||||
</JSON>
|
||||
其中:
|
||||
- maleRetireAge: 当前男性法定退休年龄(含渐进延迟,必须填入计算后的值,不要填基准60)
|
||||
- femaleRetireAge: 当前女性干部/管理岗法定退休年龄(必须填入计算后的值,不要填基准55)
|
||||
- femaleWorkerAge: 当前女性工人/操作岗法定退休年龄(必须填入计算后的值,不要填基准50)
|
||||
- 年龄用小数表示,整数部分为岁,小数部分为月/12,如60岁4个月=60.33,50岁8个月=50.67
|
||||
|
||||
请确保JSON中的数值与你在正文中计算出的退休年龄完全一致,不要使用改革前的基准值。`
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: 'qwen-plus',
|
||||
messages: [
|
||||
{ role: 'system', content: '你是劳动政策专家,精通中国退休政策法规。请提供准确的政策信息。' },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 2000,
|
||||
stream: true,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta?.content
|
||||
if (delta) yield delta
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import crypto from 'crypto'
|
||||
import prisma from '../lib/prisma'
|
||||
import { fetchRetirementPolicy } from './ai.service'
|
||||
|
||||
// 从身份证号提取出生日期
|
||||
export function extractBirthDateFromIdCard(idCard: string): Date | null {
|
||||
// 18位身份证:7-14位为出生日期 YYYYMMDD
|
||||
if (idCard.length === 18) {
|
||||
const year = parseInt(idCard.substring(6, 10))
|
||||
const month = parseInt(idCard.substring(10, 12))
|
||||
const day = parseInt(idCard.substring(12, 14))
|
||||
if (year && month && day) {
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
}
|
||||
// 15位老身份证:7-12位为出生日期 YYMMDD
|
||||
if (idCard.length === 15) {
|
||||
const year = parseInt('19' + idCard.substring(6, 8))
|
||||
const month = parseInt(idCard.substring(8, 10))
|
||||
const day = parseInt(idCard.substring(10, 12))
|
||||
if (year && month && day) {
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 从身份证号提取性别(18位:第17位奇数为男,偶数为女;15位:第15位)
|
||||
export function extractGenderFromIdCard(idCard: string): string | null {
|
||||
if (idCard.length === 18) {
|
||||
const genderCode = parseInt(idCard.substring(16, 17))
|
||||
return genderCode % 2 === 1 ? '男' : '女'
|
||||
}
|
||||
if (idCard.length === 15) {
|
||||
const genderCode = parseInt(idCard.substring(14, 15))
|
||||
return genderCode % 2 === 1 ? '男' : '女'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 计算距退休天数(支持小数退休年龄,如 60.33 = 60岁4个月)
|
||||
export function calcRetirementDaysLeft(birthDate: Date, gender: string, maleRetireAge: number, femaleRetireAge: number, femaleWorkerAge: number): number | null {
|
||||
const retireAge = gender === '男' ? maleRetireAge : femaleRetireAge
|
||||
if (!retireAge) return null
|
||||
|
||||
// 分离整数年和月数
|
||||
const years = Math.floor(retireAge)
|
||||
const months = Math.round((retireAge - years) * 12)
|
||||
|
||||
// 计算退休日期:出生日期 + 整数年 + 月数
|
||||
const retireDate = new Date(birthDate)
|
||||
retireDate.setFullYear(retireDate.getFullYear() + years)
|
||||
retireDate.setMonth(retireDate.getMonth() + months)
|
||||
|
||||
// 计算距今天数
|
||||
const now = new Date()
|
||||
const diffMs = retireDate.getTime() - now.getTime()
|
||||
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
// 检查并获取最新退休政策(懒加载:每月最多获取一次,存为 PENDING 待用户确认)
|
||||
export async function checkAndUpdateRetirementPolicy(orgId: string): Promise<void> {
|
||||
const now = new Date()
|
||||
const latestPolicy = await prisma.retirementPolicy.findFirst({
|
||||
where: { orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
// 如果本月已获取过(无论是否确认),则跳过
|
||||
if (latestPolicy) {
|
||||
const updatedThisMonth = latestPolicy.createdAt.getMonth() === now.getMonth() &&
|
||||
latestPolicy.createdAt.getFullYear() === now.getFullYear()
|
||||
if (updatedThisMonth) return
|
||||
}
|
||||
|
||||
// 调用 AI 获取最新政策
|
||||
const result = await fetchRetirementPolicy()
|
||||
|
||||
// 计算内容 hash,判断内容是否变化
|
||||
const contentHash = crypto.createHash('sha256').update(result.content).digest('hex')
|
||||
|
||||
// 如果最新政策的 hash 相同,则不新增版本
|
||||
if (latestPolicy && latestPolicy.contentHash === contentHash) {
|
||||
return
|
||||
}
|
||||
|
||||
// 新建版本(PENDING 状态,待用户确认)
|
||||
const version = latestPolicy ? latestPolicy.version + 1 : 1
|
||||
await prisma.retirementPolicy.create({
|
||||
data: {
|
||||
orgId,
|
||||
version,
|
||||
content: result.content,
|
||||
contentHash,
|
||||
status: 'PENDING',
|
||||
maleRetireAge: result.maleRetireAge,
|
||||
femaleRetireAge: result.femaleRetireAge,
|
||||
femaleWorkerAge: result.femaleWorkerAge,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 用户确认退休政策,正式生效并更新员工退休天数
|
||||
export async function confirmRetirementPolicy(orgId: string, policyId: string): Promise<void> {
|
||||
const policy = await prisma.retirementPolicy.findFirst({
|
||||
where: { id: policyId, orgId },
|
||||
})
|
||||
if (!policy) throw new Error('政策不存在')
|
||||
if (policy.status === 'CONFIRMED') return
|
||||
|
||||
// 将旧的 CONFIRMED 政策标记为 SUPERSEDED
|
||||
await prisma.retirementPolicy.updateMany({
|
||||
where: { orgId, status: 'CONFIRMED', id: { not: policyId } },
|
||||
data: { status: 'SUPERSEDED' },
|
||||
})
|
||||
|
||||
// 确认新政策生效
|
||||
await prisma.retirementPolicy.update({
|
||||
where: { id: policyId },
|
||||
data: { status: 'CONFIRMED', confirmedAt: new Date() },
|
||||
})
|
||||
|
||||
// 更新所有正式合同员工的距退休天数
|
||||
await updateEmployeesRetirementDays(orgId, policy.maleRetireAge, policy.femaleRetireAge, policy.femaleWorkerAge)
|
||||
}
|
||||
|
||||
// 批量更新员工距退休天数
|
||||
export async function updateEmployeesRetirementDays(orgId: string, maleRetireAge: number, femaleRetireAge: number, femaleWorkerAge: number): Promise<void> {
|
||||
// 查询有正式劳动合同的员工
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
||||
},
|
||||
select: { id: true, birthDate: true, gender: true, idCardNumber: true },
|
||||
})
|
||||
|
||||
for (const emp of employees) {
|
||||
if (!emp.birthDate) continue
|
||||
const gender = emp.gender || '男'
|
||||
const daysLeft = calcRetirementDaysLeft(emp.birthDate, gender, maleRetireAge, femaleRetireAge, femaleWorkerAge)
|
||||
if (daysLeft !== null) {
|
||||
await prisma.employee.update({
|
||||
where: { id: emp.id },
|
||||
data: { retirementDaysLeft: daysLeft },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user