feat: 优化退休提醒功能 - 个性化退休年龄计算

- 添加 FemaleWorkerType enum (CADRE/WORKER) 和 Employee.femaleWorkerType 字段
- 新增 calcIndividualRetireAge: 按个人出生日期逐人计算退休年龄
- 重写 calcRetirementDaysLeft: 基于个人出生日期+性别+岗位类型
- updateEmployeesRetirementDays 使用 femaleWorkerType 区分女干部/女工人
- 前端新增/编辑员工表单添加女性岗位类型选择器
- 前端政策展示改为改革规则卡片(基准→目标年龄)
- 前端距退休天数改为退休日期显示(XXXX年XX月XX日)
- 移除AI退休政策计算相关代码和手动刷新路由
- 导入支持女性岗位类型列
This commit is contained in:
freedakgmail
2026-07-25 09:27:45 +08:00
parent b892df1d52
commit 41a2c665ed
9 changed files with 208 additions and 277 deletions
+135 -25
View File
@@ -1,6 +1,5 @@
import crypto from 'crypto'
import prisma from '../lib/prisma'
import { fetchRetirementPolicy } from './ai.service'
// 从身份证号提取出生日期
export function extractBirthDateFromIdCard(idCard: string): Date | null {
@@ -38,21 +37,132 @@ export function extractGenderFromIdCard(idCard: string): string | null {
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
// 渐进式延迟退休固定算法
// 改革起始日:2025-01-01,过渡期15年至2039-12-31
// 男性:60→63岁,每4个月延迟1个月
// 女性干部:55→58岁,每4个月延迟1个月
// 女性工人:50→55岁,每2个月延迟1个月
const REFORM_START = new Date(2025, 0, 1)
const BASE_MALE = 60
const BASE_FEMALE_CADRE = 55
const BASE_FEMALE_WORKER = 50
const TARGET_MALE = 63
const TARGET_FEMALE_CADRE = 58
const TARGET_FEMALE_WORKER = 55
const MALE_DELAY_INTERVAL = 4 // 每4个月延迟1个月
const FEMALE_CADRE_DELAY_INTERVAL = 4
const FEMALE_WORKER_DELAY_INTERVAL = 2 // 每2个月延迟1个月
// 分离整数年和月数
const years = Math.floor(retireAge)
const months = Math.round((retireAge - years) * 12)
// 计算从改革起始日到指定日期经过的完整月数
function calcMonthsSinceReform(date: Date = new Date()): number {
const months = (date.getFullYear() - REFORM_START.getFullYear()) * 12 + (date.getMonth() - REFORM_START.getMonth())
return Math.max(0, months)
}
// 计算退休日期:出生日期 + 整数年 + 月数
const retireDate = new Date(birthDate)
retireDate.setFullYear(retireDate.getFullYear() + years)
retireDate.setMonth(retireDate.getMonth() + months)
// 根据出生日期和性别计算个人基准退休年龄
function getBaseRetireAge(gender: string, femaleWorkerType: string | null): number {
if (gender === '男') return BASE_MALE
if (gender === '女') {
return femaleWorkerType === 'WORKER' ? BASE_FEMALE_WORKER : BASE_FEMALE_CADRE
}
return BASE_MALE
}
// 根据出生日期和性别计算延迟间隔
function getDelayInterval(gender: string, femaleWorkerType: string | null): number {
if (gender === '男') return MALE_DELAY_INTERVAL
if (gender === '女') {
return femaleWorkerType === 'WORKER' ? FEMALE_WORKER_DELAY_INTERVAL : FEMALE_CADRE_DELAY_INTERVAL
}
return MALE_DELAY_INTERVAL
}
// 根据出生日期和性别计算最大延迟月数
function getMaxDelayMonths(gender: string, femaleWorkerType: string | null): number {
if (gender === '男') return (TARGET_MALE - BASE_MALE) * 12
if (gender === '女') {
return femaleWorkerType === 'WORKER'
? (TARGET_FEMALE_WORKER - BASE_FEMALE_WORKER) * 12
: (TARGET_FEMALE_CADRE - BASE_FEMALE_CADRE) * 12
}
return (TARGET_MALE - BASE_MALE) * 12
}
// 计算个人的实际退休年龄(基于出生日期)
// 原理:先算出此人达到基准退休年龄的日期,再算该日期距改革起始日过了多少个月,
// 每N个月延迟1个月,得到个人实际退休年龄
export function calcIndividualRetireAge(birthDate: Date, gender: string, femaleWorkerType: string | null = null): {
retireAge: number
retireDate: Date
delayMonths: number
} {
const baseAge = getBaseRetireAge(gender, femaleWorkerType)
const delayInterval = getDelayInterval(gender, femaleWorkerType)
const maxDelay = getMaxDelayMonths(gender, femaleWorkerType)
// 达到基准退休年龄的日期
const baseRetireDate = new Date(birthDate)
baseRetireDate.setFullYear(baseRetireDate.getFullYear() + baseAge)
// 从改革起始日到达到基准退休年龄日期,经过的月数
const monthsSinceReform = calcMonthsSinceReform(baseRetireDate)
// 延迟月数 = floor(月数 / 间隔),不超过最大延迟
const delayMonths = Math.min(maxDelay, Math.floor(monthsSinceReform / delayInterval))
// 实际退休年龄 = 基准年龄 + 延迟月数/12
const retireAge = baseAge + delayMonths / 12
// 实际退休日期 = 基准退休日期 + 延迟月数
const retireDate = new Date(baseRetireDate)
retireDate.setMonth(retireDate.getMonth() + delayMonths)
return { retireAge, retireDate, delayMonths }
}
// 计算当前改革进度(用于政策展示)
export function calcCurrentRetireAges(date: Date = new Date()): {
maleRetireAge: number
femaleRetireAge: number
femaleWorkerAge: number
description: string
} {
const monthsElapsed = calcMonthsSinceReform(date)
const maleDelayMonths = Math.min(36, Math.floor(monthsElapsed / MALE_DELAY_INTERVAL))
const femaleCadreDelayMonths = Math.min(36, Math.floor(monthsElapsed / FEMALE_CADRE_DELAY_INTERVAL))
const femaleWorkerDelayMonths = Math.min(60, Math.floor(monthsElapsed / FEMALE_WORKER_DELAY_INTERVAL))
const maleRetireAge = BASE_MALE + maleDelayMonths / 12
const femaleRetireAge = BASE_FEMALE_CADRE + femaleCadreDelayMonths / 12
const femaleWorkerAge = BASE_FEMALE_WORKER + femaleWorkerDelayMonths / 12
const description = `渐进式延迟退休改革(2025年1月1日起实施)
【改革规则】
- 男性职工:60岁 → 63岁,每4个月延迟1个月
- 女性干部/管理岗:55岁 → 58岁,每4个月延迟1个月
- 女性工人/操作岗:50岁 → 55岁,每2个月延迟1个月
【当前进度】截至${date.getFullYear()}${date.getMonth() + 1}月,改革已实施${monthsElapsed}个月
- 男性当月退休年龄:${BASE_MALE}${maleDelayMonths > 0 ? `+${maleDelayMonths}个月` : ''}
- 女性干部当月退休年龄:${BASE_FEMALE_CADRE}${femaleCadreDelayMonths > 0 ? `+${femaleCadreDelayMonths}个月` : ''}
- 女性工人当月退休年龄:${BASE_FEMALE_WORKER}${femaleWorkerDelayMonths > 0 ? `+${femaleWorkerDelayMonths}个月` : ''}
【个人退休年龄】根据出生年月逐人计算:
达到基准退休年龄的时间点不同,延迟月数也不同。
例如:1965年1月出生的男性,2025年1月满60岁,延迟0个月,60岁退休;1965年5月出生的男性,2025年5月满60岁,延迟1个月,60岁1个月退休。
改革依据:2024年9月13日全国人大常委会《关于实施渐进式延迟法定退休年龄的决定》
过渡期:2025年1月1日至2039年12月31日(15年)`
return { maleRetireAge, femaleRetireAge, femaleWorkerAge, description }
}
// 计算距退休天数(基于个人出生日期逐人计算)
export function calcRetirementDaysLeft(birthDate: Date, gender: string, femaleWorkerType: string | null = null): number | null {
const { retireDate } = calcIndividualRetireAge(birthDate, gender, femaleWorkerType)
// 计算距今天数
const now = new Date()
const diffMs = retireDate.getTime() - now.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
@@ -73,11 +183,11 @@ export async function checkAndUpdateRetirementPolicy(orgId: string): Promise<voi
if (updatedThisMonth) return
}
// 调用 AI 获取最新政策
const result = await fetchRetirementPolicy()
// 用固定算法计算当前退休年龄
const { maleRetireAge, femaleRetireAge, femaleWorkerAge, description } = calcCurrentRetireAges(now)
// 计算内容 hash,判断内容是否变化
const contentHash = crypto.createHash('sha256').update(result.content).digest('hex')
const contentHash = crypto.createHash('sha256').update(description).digest('hex')
// 如果最新政策的 hash 相同,则不新增版本
if (latestPolicy && latestPolicy.contentHash === contentHash) {
@@ -90,12 +200,12 @@ export async function checkAndUpdateRetirementPolicy(orgId: string): Promise<voi
data: {
orgId,
version,
content: result.content,
content: description,
contentHash,
status: 'PENDING',
maleRetireAge: result.maleRetireAge,
femaleRetireAge: result.femaleRetireAge,
femaleWorkerAge: result.femaleWorkerAge,
maleRetireAge,
femaleRetireAge,
femaleWorkerAge,
},
})
}
@@ -121,24 +231,24 @@ export async function confirmRetirementPolicy(orgId: string, policyId: string):
})
// 更新所有正式合同员工的距退休天数
await updateEmployeesRetirementDays(orgId, policy.maleRetireAge, policy.femaleRetireAge, policy.femaleWorkerAge)
await updateEmployeesRetirementDays(orgId)
}
// 批量更新员工距退休天数
export async function updateEmployeesRetirementDays(orgId: string, maleRetireAge: number, femaleRetireAge: number, femaleWorkerAge: number): Promise<void> {
// 批量更新员工距退休天数(基于个人出生日期 individually 计算)
export async function updateEmployeesRetirementDays(orgId: string): 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 },
select: { id: true, birthDate: true, gender: true, femaleWorkerType: true },
})
for (const emp of employees) {
if (!emp.birthDate) continue
const gender = emp.gender || '男'
const daysLeft = calcRetirementDaysLeft(emp.birthDate, gender, maleRetireAge, femaleRetireAge, femaleWorkerAge)
const daysLeft = calcRetirementDaysLeft(emp.birthDate, gender, emp.femaleWorkerType)
if (daysLeft !== null) {
await prisma.employee.update({
where: { id: emp.id },