import { Router, Response, NextFunction } from 'express' import prisma from '../lib/prisma' import { authMiddleware, AuthRequest } from '../middleware/auth' import { decrypt } from '../lib/crypto' import { z } from 'zod' import OpenAI from 'openai' const router = Router() router.use(authMiddleware) const socialConfigFields = { city: z.string().optional(), pensionOrg: z.number().optional(), pensionEmp: z.number().optional(), medicalOrg: z.number().optional(), medicalEmp: z.number().optional(), unemploymentOrg: z.number().optional(), unemploymentEmp: z.number().optional(), injuryOrg: z.number().optional(), maternityOrg: z.number().optional(), baseMin: z.number().optional(), baseMax: z.number().optional(), medicalBaseMin: z.number().optional(), medicalBaseMax: z.number().optional(), extraInsurances: z.any().optional(), } const housingConfigFields = { city: z.string().optional(), housingOrg: z.number().optional(), housingEmp: z.number().optional(), baseMin: z.number().optional(), baseMax: z.number().optional(), } // 获取当前生效版本(支持按城市筛选) router.get('/config', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const city = req.query.city as string | undefined const where: any = { orgId: req.user!.orgId, isCurrent: true } if (city) where.city = city let config = await prisma.socialInsuranceConfig.findFirst({ where, orderBy: { effectiveFrom: 'desc' }, }) // 未指定城市时,返回任意当前配置 if (!config && !city) { config = await prisma.socialInsuranceConfig.findFirst({ where: { orgId: req.user!.orgId, isCurrent: true }, orderBy: { effectiveFrom: 'desc' }, }) } if (!config) { return res.json({ success: true, data: null }) } res.json({ success: true, data: config }) } catch (err) { next(err) } }) // 获取所有城市列表(从配置中提取) router.get('/config/cities', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const configs = await prisma.socialInsuranceConfig.findMany({ where: { orgId: req.user!.orgId }, select: { city: true }, distinct: ['city'], }) const cities = configs.map(c => c.city).filter(Boolean) if (!cities.includes('北京')) cities.unshift('北京') res.json({ success: true, data: cities }) } catch (err) { next(err) } }) // 获取所有版本列表(支持按城市筛选) router.get('/config/versions', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const city = req.query.city as string | undefined const where: any = { orgId: req.user!.orgId } if (city) where.city = city const versions = await prisma.socialInsuranceConfig.findMany({ where, orderBy: { effectiveFrom: 'desc' }, }) res.json({ success: true, data: versions }) } catch (err) { next(err) } }) // 按月份获取适用版本 router.get('/config/by-month/:month', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { month } = req.params const config = await prisma.socialInsuranceConfig.findFirst({ where: { orgId: req.user!.orgId, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }], }, orderBy: { effectiveFrom: 'desc' }, }) if (!config) { // 回退到当前版本 const current = await prisma.socialInsuranceConfig.findFirst({ where: { orgId: req.user!.orgId, isCurrent: true }, }) return res.json({ success: true, data: current }) } res.json({ success: true, data: config }) } catch (err) { next(err) } }) // 新建版本(年度调基/比例变更) const createVersionSchema = z.object({ ...socialConfigFields, effectiveFrom: z.string().regex(/^\d{4}-\d{2}$/), }) router.post('/config/versions', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const data = createVersionSchema.parse(req.body) const orgId = req.user!.orgId // 检查同一城市同一生效月份是否已有版本 const existing = await prisma.socialInsuranceConfig.findFirst({ where: { orgId, city: data.city, effectiveFrom: data.effectiveFrom }, }) if (existing) { return res.status(400).json({ success: false, message: `${data.effectiveFrom} 已有配置版本` }) } // 将之前当前版本标记为失效 const prevCurrent = await prisma.socialInsuranceConfig.findFirst({ where: { orgId, isCurrent: true }, }) if (prevCurrent) { // 计算上个版本的失效月份 = 新版本生效月份的前一个月 const [year, mon] = data.effectiveFrom.split('-').map(Number) const prevMonth = mon === 1 ? `${year - 1}-12` : `${year}-${String(mon - 1).padStart(2, '0')}` await prisma.socialInsuranceConfig.update({ where: { id: prevCurrent.id }, data: { isCurrent: false, effectiveTo: prevMonth }, }) } // 创建新版本 const version = await prisma.socialInsuranceConfig.create({ data: { orgId, ...data, isCurrent: true, createdBy: req.user!.id, }, }) res.json({ success: true, data: version }) } catch (err) { next(err) } }) // 预览员工基数调整(返回全部在职员工,含当前基数和建议基数) router.get('/config/:id/adjust-preview', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { id } = req.params const orgId = req.user!.orgId const config = await prisma.socialInsuranceConfig.findFirst({ where: { id, orgId }, }) if (!config) return res.status(404).json({ success: false, message: '配置版本不存在' }) if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过基数调整' }) const employees = await prisma.employee.findMany({ where: { orgId, status: 'ACTIVE', city: config.city }, select: { id: true, name: true, department: true, socialInsBase: true, monthlySalary: true }, orderBy: { name: 'asc' }, }) // 计算上年平均工资:查询过去12个月的Payslip的totalPay平均值 const now = new Date() const lastYearStart = `${now.getFullYear() - 1}-01` const lastYearEnd = `${now.getFullYear() - 1}-12` const lastYearPayslips = await prisma.payslip.findMany({ where: { orgId, month: { gte: lastYearStart, lte: lastYearEnd }, }, select: { employeeId: true, totalPay: true }, }) // 按员工汇总上年月均工资 const avgSalaryMap = new Map() const empPayslipMap = new Map() for (const p of lastYearPayslips) { if (!empPayslipMap.has(p.employeeId)) empPayslipMap.set(p.employeeId, []) empPayslipMap.get(p.employeeId)!.push(p.totalPay) } for (const [empId, pays] of empPayslipMap) { const avg = pays.reduce((s, v) => s + v, 0) / pays.length avgSalaryMap.set(empId, avg) } const items = employees.map((emp) => { let monthlyWage = 0 try { monthlyWage = Number(decrypt(emp.monthlySalary)) } catch { monthlyWage = Number(emp.monthlySalary) || 0 } const oldSocialBase = emp.socialInsBase ?? monthlyWage const avgSalary = avgSalaryMap.get(emp.id) ?? monthlyWage const suggestedSocialBase = Math.min(Math.max(avgSalary, config.baseMin), config.baseMax) return { employeeId: emp.id, name: emp.name, department: emp.department, oldBase: oldSocialBase, avgSalary, monthlyWage, suggestedBase: suggestedSocialBase, } }) res.json({ success: true, data: { items, total: items.length, baseMin: config.baseMin, baseMax: config.baseMax } }) } catch (err) { next(err) } }) // 执行员工基数调整(接收用户编辑后的数据) const adjustApplySchema = z.object({ items: z.array(z.object({ employeeId: z.string(), newBase: z.number(), })), }) router.post('/config/:id/adjust-apply', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { id } = req.params const orgId = req.user!.orgId const config = await prisma.socialInsuranceConfig.findFirst({ where: { id, orgId }, }) if (!config) return res.status(404).json({ success: false, message: '配置版本不存在' }) if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过基数调整' }) const { items } = adjustApplySchema.parse(req.body) const adjustMonth = config.effectiveFrom const prevAdjustMonth = (() => { const [y, m] = adjustMonth.split('-').map(Number) const d = new Date(y, m - 2, 1) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` })() let adjusted = 0 for (const item of items) { const socialBase = Math.min(Math.max(item.newBase, config.baseMin), config.baseMax) // 关闭旧社保记录 await prisma.employeeSocialInsRecord.updateMany({ where: { employeeId: item.employeeId, endMonth: null }, data: { endMonth: prevAdjustMonth }, }) // 创建新社保记录 await prisma.employeeSocialInsRecord.create({ data: { orgId, employeeId: item.employeeId, city: config.city, startMonth: adjustMonth, endMonth: null, base: socialBase, changeType: 'ADJUST', createdBy: req.user!.id, }, }) // 同步 Employee 便捷字段 await prisma.employee.update({ where: { id: item.employeeId }, data: { socialInsBase: socialBase, socialInsStartMonth: adjustMonth }, }) adjusted++ } await prisma.socialInsuranceConfig.update({ where: { id }, data: { adjustmentDone: true }, }) res.json({ success: true, data: { adjusted, total: items.length } }) } catch (err) { next(err) } }) // 重置社保基数调整(撤销本次调整,重新来过) router.post('/config/:id/reset-adjustment', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { id } = req.params const orgId = req.user!.orgId const config = await prisma.socialInsuranceConfig.findFirst({ where: { id, orgId }, }) if (!config) return res.status(404).json({ success: false, message: '配置版本不存在' }) if (!config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本尚未执行过基数调整,无需重置' }) // 恢复 adjustmentDone 标志 await prisma.socialInsuranceConfig.update({ where: { id }, data: { adjustmentDone: false }, }) // 删除该版本创建的所有社保记录变更(按城市筛选) await prisma.employeeSocialInsRecord.deleteMany({ where: { orgId, city: config.city, changeType: 'ADJUST', startMonth: config.effectiveFrom, }, }) // 恢复员工社保基数为调整前(找到 adjustment 前的最后一条记录,按城市) const employees = await prisma.employee.findMany({ where: { orgId, status: 'ACTIVE', city: config.city }, select: { id: true }, }) for (const emp of employees) { const prevRecord = await prisma.employeeSocialInsRecord.findFirst({ where: { orgId, employeeId: emp.id, city: config.city, startMonth: { lt: config.effectiveFrom } }, orderBy: { startMonth: 'desc' }, }) await prisma.employee.update({ where: { id: emp.id }, data: { socialInsBase: prevRecord?.base ?? null, socialInsStartMonth: prevRecord?.startMonth ?? null, }, }) } res.json({ success: true, message: '社保基数调整已重置,可以重新调整' }) } catch (err) { next(err) } }) // 社保计算(使用当前版本或指定月份版本) const calcSchema = z.object({ base: z.number().positive(), month: z.string().regex(/^\d{4}-\d{2}$/).optional(), city: z.string().optional(), }) router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { base, month, city } = calcSchema.parse(req.body) const orgId = req.user!.orgId let config const whereBase: any = { orgId } if (city) whereBase.city = city if (month) { config = await prisma.socialInsuranceConfig.findFirst({ where: { ...whereBase, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }], }, orderBy: { effectiveFrom: 'desc' }, }) } if (!config) { config = await prisma.socialInsuranceConfig.findFirst({ where: { ...whereBase, isCurrent: true }, }) } if (!config) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `未找到${city || ''}的社保配置,请先在社保管理中创建` } }) } const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) const medMin = config.medicalBaseMin && config.medicalBaseMin > 0 ? config.medicalBaseMin : config.baseMin const medMax = config.medicalBaseMax && config.medicalBaseMax > 0 ? config.medicalBaseMax : config.baseMax const medicalBase = Math.min(Math.max(base, medMin), medMax) const pensionOrg = actualBase * config.pensionOrg / 100 const pensionEmp = actualBase * config.pensionEmp / 100 const medicalOrg = medicalBase * config.medicalOrg / 100 const medicalEmp = medicalBase * config.medicalEmp / 100 const unemploymentOrg = actualBase * config.unemploymentOrg / 100 const unemploymentEmp = actualBase * config.unemploymentEmp / 100 const injuryOrg = actualBase * config.injuryOrg / 100 const maternityOrg = medicalBase * config.maternityOrg / 100 let totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg let totalEmp = pensionEmp + medicalEmp + unemploymentEmp // 附加险种 const extraItems: any[] = [] if (config.extraInsurances && Array.isArray(config.extraInsurances)) { for (const ins of config.extraInsurances as any[]) { const insBase = ins.baseType === 'medical' ? medicalBase : ins.baseType === 'fixed' ? 1 : actualBase if (ins.baseType === 'fixed' && ins.fixedAmount) { const orgAmt = ins.fixedAmount const empAmt = ins.empFixedAmount || 0 totalOrg += orgAmt totalEmp += empAmt extraItems.push({ name: ins.name, orgRate: 0, empRate: 0, orgAmount: orgAmt, empAmount: empAmt }) } else { const orgAmt = insBase * (ins.orgRate || 0) / 100 const empAmt = insBase * (ins.empRate || 0) / 100 totalOrg += orgAmt totalEmp += empAmt extraItems.push({ name: ins.name, orgRate: ins.orgRate || 0, empRate: ins.empRate || 0, orgAmount: orgAmt, empAmount: empAmt }) } } } const total = totalOrg + totalEmp res.json({ success: true, data: { actualBase, medicalBase, originalBase: base, capped: base > config.baseMax, floored: base < config.baseMin, configVersion: config.effectiveFrom, items: [ { name: '养老保险', orgRate: config.pensionOrg, empRate: config.pensionEmp, orgAmount: pensionOrg, empAmount: pensionEmp }, { name: '医疗保险', orgRate: config.medicalOrg, empRate: config.medicalEmp, orgAmount: medicalOrg, empAmount: medicalEmp }, { name: '失业保险', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: unemploymentOrg, empAmount: unemploymentEmp }, { name: '工伤保险', orgRate: config.injuryOrg, empRate: 0, orgAmount: injuryOrg, empAmount: 0 }, { name: '生育保险', orgRate: config.maternityOrg, empRate: 0, orgAmount: maternityOrg, empAmount: 0 }, ...extraItems, ], totalOrg, totalEmp, total, }, }) } catch (err) { next(err) } }) // ========== 公积金配置 ========== // 获取当前公积金配置(支持按城市筛选) router.get('/housing-config', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const city = req.query.city as string | undefined const where: any = { orgId: req.user!.orgId, isCurrent: true } if (city) where.city = city let config = await prisma.housingFundConfig.findFirst({ where, orderBy: { effectiveFrom: 'desc' }, }) // 未指定城市时,返回任意当前配置 if (!config && !city) { config = await prisma.housingFundConfig.findFirst({ where: { orgId: req.user!.orgId, isCurrent: true }, orderBy: { effectiveFrom: 'desc' }, }) } if (!config) { return res.json({ success: true, data: null }) } res.json({ success: true, data: config }) } catch (err) { next(err) } }) // 公积金配置版本列表(支持按城市筛选) router.get('/housing-config/versions', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const city = req.query.city as string | undefined const where: any = { orgId: req.user!.orgId } if (city) where.city = city const versions = await prisma.housingFundConfig.findMany({ where, orderBy: { effectiveFrom: 'desc' }, }) res.json({ success: true, data: versions }) } catch (err) { next(err) } }) // 新建公积金配置版本 const createHousingVersionSchema = z.object({ ...housingConfigFields, effectiveFrom: z.string().regex(/^\d{4}-\d{2}$/), }) router.post('/housing-config/versions', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const data = createHousingVersionSchema.parse(req.body) const orgId = req.user!.orgId const existing = await prisma.housingFundConfig.findFirst({ where: { orgId, city: data.city, effectiveFrom: data.effectiveFrom }, }) if (existing) { return res.status(400).json({ success: false, message: `${data.effectiveFrom} 已有公积金配置版本` }) } const prevCurrent = await prisma.housingFundConfig.findFirst({ where: { orgId, isCurrent: true }, }) if (prevCurrent) { const [year, mon] = data.effectiveFrom.split('-').map(Number) const prevMonth = mon === 1 ? `${year - 1}-12` : `${year}-${String(mon - 1).padStart(2, '0')}` await prisma.housingFundConfig.update({ where: { id: prevCurrent.id }, data: { isCurrent: false, effectiveTo: prevMonth }, }) } const version = await prisma.housingFundConfig.create({ data: { orgId, ...data, isCurrent: true, createdBy: req.user!.id, }, }) res.json({ success: true, data: version }) } catch (err) { next(err) } }) // 公积金计算 router.post('/housing-calculate', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { base, month, city } = calcSchema.parse(req.body) const orgId = req.user!.orgId let config const whereBase: any = { orgId } if (city) whereBase.city = city if (month) { config = await prisma.housingFundConfig.findFirst({ where: { ...whereBase, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }], }, orderBy: { effectiveFrom: 'desc' }, }) } if (!config) { config = await prisma.housingFundConfig.findFirst({ where: { ...whereBase, isCurrent: true }, }) } if (!config) { return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: `未找到${city || ''}的公积金配置,请先在公积金管理中创建` } }) } const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) const housingOrg = actualBase * config.housingOrg / 100 const housingEmp = actualBase * config.housingEmp / 100 res.json({ success: true, data: { actualBase, originalBase: base, capped: base > config.baseMax, floored: base < config.baseMin, configVersion: config.effectiveFrom, housingOrg, housingEmp, total: housingOrg + housingEmp, }, }) } catch (err) { next(err) } }) // 公积金调基预览 router.get('/housing-config/:id/adjust-preview', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { id } = req.params const orgId = req.user!.orgId const config = await prisma.housingFundConfig.findFirst({ where: { id, orgId }, }) if (!config) return res.status(404).json({ success: false, message: '公积金配置版本不存在' }) if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过公积金基数调整' }) const employees = await prisma.employee.findMany({ where: { orgId, status: 'ACTIVE', city: config.city }, select: { id: true, name: true, department: true, housingFundBase: true, monthlySalary: true }, orderBy: { name: 'asc' }, }) const now = new Date() const lastYearStart = `${now.getFullYear() - 1}-01` const lastYearEnd = `${now.getFullYear() - 1}-12` const lastYearPayslips = await prisma.payslip.findMany({ where: { orgId, month: { gte: lastYearStart, lte: lastYearEnd } }, select: { employeeId: true, totalPay: true }, }) const empPayslipMap = new Map() for (const p of lastYearPayslips) { if (!empPayslipMap.has(p.employeeId)) empPayslipMap.set(p.employeeId, []) empPayslipMap.get(p.employeeId)!.push(p.totalPay) } const items = employees.map((emp) => { let monthlyWage = 0 try { monthlyWage = Number(decrypt(emp.monthlySalary)) } catch { monthlyWage = Number(emp.monthlySalary) || 0 } const oldBase = emp.housingFundBase ?? monthlyWage const payslips = empPayslipMap.get(emp.id) const avgSalary = payslips && payslips.length > 0 ? payslips.reduce((s, v) => s + v, 0) / payslips.length : monthlyWage const suggestedBase = Math.min(Math.max(avgSalary, config.baseMin), config.baseMax) return { employeeId: emp.id, name: emp.name, department: emp.department, oldBase, avgSalary, monthlyWage, suggestedBase, } }) res.json({ success: true, data: { items, total: items.length, baseMin: config.baseMin, baseMax: config.baseMax } }) } catch (err) { next(err) } }) // 执行公积金调基 const adjustHousingSchema = z.object({ items: z.array(z.object({ employeeId: z.string(), newBase: z.number(), })), }) router.post('/housing-config/:id/adjust-apply', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { id } = req.params const orgId = req.user!.orgId const config = await prisma.housingFundConfig.findFirst({ where: { id, orgId }, }) if (!config) return res.status(404).json({ success: false, message: '公积金配置版本不存在' }) if (config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本已执行过公积金基数调整' }) const { items } = adjustHousingSchema.parse(req.body) const adjustMonth = config.effectiveFrom const prevAdjustMonth = (() => { const [y, m] = adjustMonth.split('-').map(Number) const d = new Date(y, m - 2, 1) return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` })() let adjusted = 0 for (const item of items) { const base = Math.min(Math.max(item.newBase, config.baseMin), config.baseMax) // 关闭旧记录 await prisma.employeeHousingFundRecord.updateMany({ where: { employeeId: item.employeeId, endMonth: null }, data: { endMonth: prevAdjustMonth }, }) // 创建新记录 await prisma.employeeHousingFundRecord.create({ data: { orgId, employeeId: item.employeeId, city: config.city, startMonth: adjustMonth, endMonth: null, base, changeType: 'ADJUST', createdBy: req.user!.id, }, }) // 同步 Employee 便捷字段 await prisma.employee.update({ where: { id: item.employeeId }, data: { housingFundBase: base, housingFundStartMonth: adjustMonth }, }) adjusted++ } await prisma.housingFundConfig.update({ where: { id }, data: { adjustmentDone: true }, }) res.json({ success: true, data: { adjusted, total: items.length } }) } catch (err) { next(err) } }) // 重置公积金基数调整(撤销本次调整,重新来过) router.post('/housing-config/:id/reset-adjustment', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { id } = req.params const orgId = req.user!.orgId const config = await prisma.housingFundConfig.findFirst({ where: { id, orgId }, }) if (!config) return res.status(404).json({ success: false, message: '公积金配置版本不存在' }) if (!config.adjustmentDone) return res.status(400).json({ success: false, message: '该版本尚未执行过基数调整,无需重置' }) // 恢复 adjustmentDone 标志 await prisma.housingFundConfig.update({ where: { id }, data: { adjustmentDone: false }, }) // 删除该版本创建的所有公积金记录变更 await prisma.employeeHousingFundRecord.deleteMany({ where: { orgId, changeType: 'ADJUST', startMonth: config.effectiveFrom, }, }) // 恢复员工公积金基数为调整前 const employees = await prisma.employee.findMany({ where: { orgId, status: 'ACTIVE' }, select: { id: true }, }) for (const emp of employees) { const prevRecord = await prisma.employeeHousingFundRecord.findFirst({ where: { orgId, employeeId: emp.id, startMonth: { lt: config.effectiveFrom } }, orderBy: { startMonth: 'desc' }, }) await prisma.employee.update({ where: { id: emp.id }, data: { housingFundBase: prevRecord?.base ?? null, housingFundStartMonth: prevRecord?.startMonth ?? null, }, }) } res.json({ success: true, message: '公积金基数调整已重置,可以重新调整' }) } catch (err) { next(err) } }) // ========== 月度增减员 ========== /** 根据基数和社保配置计算各项企业/个人缴费明细 */ function calcSocialDetail(base: number, config: any) { const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) const medMin = config.medicalBaseMin && config.medicalBaseMin > 0 ? config.medicalBaseMin : config.baseMin const medMax = config.medicalBaseMax && config.medicalBaseMax > 0 ? config.medicalBaseMax : config.baseMax const medicalBase = Math.min(Math.max(base, medMin), medMax) const items = [ { name: '养老', orgRate: config.pensionOrg, empRate: config.pensionEmp, orgAmount: actualBase * config.pensionOrg / 100, empAmount: actualBase * config.pensionEmp / 100 }, { name: '医疗', orgRate: config.medicalOrg, empRate: config.medicalEmp, orgAmount: medicalBase * config.medicalOrg / 100, empAmount: medicalBase * config.medicalEmp / 100 }, { name: '失业', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: actualBase * config.unemploymentOrg / 100, empAmount: actualBase * config.unemploymentEmp / 100 }, { name: '工伤', orgRate: config.injuryOrg, empRate: 0, orgAmount: actualBase * config.injuryOrg / 100, empAmount: 0 }, { name: '生育', orgRate: config.maternityOrg, empRate: 0, orgAmount: medicalBase * config.maternityOrg / 100, empAmount: 0 }, ] // 附加险种 if (config.extraInsurances && Array.isArray(config.extraInsurances)) { for (const ins of config.extraInsurances as any[]) { const insBase = ins.baseType === 'medical' ? medicalBase : ins.baseType === 'fixed' ? 1 : actualBase if (ins.baseType === 'fixed' && ins.fixedAmount) { items.push({ name: ins.name, orgRate: 0, empRate: 0, orgAmount: ins.fixedAmount, empAmount: ins.empFixedAmount || 0 }) } else { items.push({ name: ins.name, orgRate: ins.orgRate || 0, empRate: ins.empRate || 0, orgAmount: insBase * (ins.orgRate || 0) / 100, empAmount: insBase * (ins.empRate || 0) / 100 }) } } } const totalOrg = items.reduce((s, i) => s + i.orgAmount, 0) const totalEmp = items.reduce((s, i) => s + i.empAmount, 0) return { actualBase, medicalBase, items, totalOrg, totalEmp } } /** 根据基数和公积金配置计算企业/个人缴费明细 */ function calcHousingDetail(base: number, config: any) { const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax) const orgAmount = actualBase * config.housingOrg / 100 const empAmount = actualBase * config.housingEmp / 100 return { actualBase, orgAmount, empAmount, total: orgAmount + empAmount } } /** 按月份匹配社保配置版本 */ async function getSocialConfigByMonth(orgId: string, month: string, city?: string) { const where: any = { orgId } if (city) where.city = city let config = await prisma.socialInsuranceConfig.findFirst({ where: { ...where, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }] }, orderBy: { effectiveFrom: 'desc' }, }) if (!config) { config = await prisma.socialInsuranceConfig.findFirst({ where: { ...where, isCurrent: true } }) } return config } /** 按月份匹配公积金配置版本 */ async function getHousingConfigByMonth(orgId: string, month: string, city?: string) { const where: any = { orgId } if (city) where.city = city let config = await prisma.housingFundConfig.findFirst({ where: { ...where, effectiveFrom: { lte: month }, OR: [{ effectiveTo: null }, { effectiveTo: { gte: month } }] }, orderBy: { effectiveFrom: 'desc' }, }) if (!config) { config = await prisma.housingFundConfig.findFirst({ where: { ...where, isCurrent: true } }) } return config } // 社保月度增减员 router.get('/monthly-changes', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const month = (req.query.month as string) || new Date().toISOString().slice(0, 7) const orgId = req.user!.orgId // 增员:startMonth == month const additions = await prisma.employeeSocialInsRecord.findMany({ where: { orgId, startMonth: month }, include: { employee: { select: { name: true, department: true, idCardNumber: true } } }, orderBy: { createdAt: 'asc' }, }) // 减员:endMonth == month 且 changeType 为 TERMINATION 或 CITY_CHANGE const reductions = await prisma.employeeSocialInsRecord.findMany({ where: { orgId, endMonth: month, changeType: { in: ['TERMINATION', 'CITY_CHANGE'] } }, include: { employee: { select: { name: true, department: true, idCardNumber: true } } }, orderBy: { createdAt: 'asc' }, }) // 按城市缓存配置 const configCache = new Map() const getConfigForCity = async (city: string) => { if (!configCache.has(city)) { configCache.set(city, await getSocialConfigByMonth(orgId, month, city)) } return configCache.get(city) } const mapRecord = async (r: any) => { const config = await getConfigForCity(r.city) const detail = config ? calcSocialDetail(r.base, config) : null return { employeeId: r.employeeId, name: r.employee.name, department: r.employee.department, city: r.city, base: r.base, startMonth: r.startMonth, endMonth: r.endMonth, changeType: r.changeType, detail: detail ? { items: detail.items, totalOrg: detail.totalOrg, totalEmp: detail.totalEmp, total: detail.totalOrg + detail.totalEmp, } : null, } } // 按城市分组 const allRecords = [...additions, ...reductions] const cities = [...new Set(allRecords.map((r) => r.city))] const configs: Record = {} for (const c of cities) { const cfg = await getConfigForCity(c) if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax } } res.json({ success: true, data: { month, configs, additions: await Promise.all(additions.map(mapRecord)), reductions: await Promise.all(reductions.map(mapRecord)), }, }) } catch (err) { next(err) } }) // 公积金月度增减员 router.get('/housing/monthly-changes', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const month = (req.query.month as string) || new Date().toISOString().slice(0, 7) const orgId = req.user!.orgId const additions = await prisma.employeeHousingFundRecord.findMany({ where: { orgId, startMonth: month }, include: { employee: { select: { name: true, department: true, idCardNumber: true } } }, orderBy: { createdAt: 'asc' }, }) const reductions = await prisma.employeeHousingFundRecord.findMany({ where: { orgId, endMonth: month, changeType: { in: ['TERMINATION', 'CITY_CHANGE'] } }, include: { employee: { select: { name: true, department: true, idCardNumber: true } } }, orderBy: { createdAt: 'asc' }, }) const configCache = new Map() const getConfigForCity = async (city: string) => { if (!configCache.has(city)) { configCache.set(city, await getHousingConfigByMonth(orgId, month, city)) } return configCache.get(city) } const mapRecord = async (r: any) => { const config = await getConfigForCity(r.city) const detail = config ? calcHousingDetail(r.base, config) : null return { employeeId: r.employeeId, name: r.employee.name, department: r.employee.department, city: r.city, base: r.base, startMonth: r.startMonth, endMonth: r.endMonth, changeType: r.changeType, detail: detail ? { orgAmount: detail.orgAmount, empAmount: detail.empAmount, total: detail.total } : null, } } const allRecords = [...additions, ...reductions] const cities = [...new Set(allRecords.map((r) => r.city))] const configs: Record = {} for (const c of cities) { const cfg = await getConfigForCity(c) if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax, housingOrg: cfg.housingOrg, housingEmp: cfg.housingEmp } } res.json({ success: true, data: { month, configs, additions: await Promise.all(additions.map(mapRecord)), reductions: await Promise.all(reductions.map(mapRecord)), }, }) } catch (err) { next(err) } }) // ========== 在职申报 ========== // 社保在保人员 router.get('/active-declaration', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const month = (req.query.month as string) || new Date().toISOString().slice(0, 7) const orgId = req.user!.orgId const records = await prisma.employeeSocialInsRecord.findMany({ where: { orgId, startMonth: { lt: month }, OR: [{ endMonth: null }, { endMonth: { gt: month } }], }, include: { employee: { select: { name: true, department: true, idCardNumber: true, hireDate: true } } }, orderBy: { createdAt: 'asc' }, }) const configCache = new Map() const getConfigForCity = async (city: string) => { if (!configCache.has(city)) { configCache.set(city, await getSocialConfigByMonth(orgId, month, city)) } return configCache.get(city) } const items = await Promise.all(records.map(async (r) => { const config = await getConfigForCity(r.city) const detail = config ? calcSocialDetail(r.base, config) : null return { employeeId: r.employeeId, name: r.employee.name, department: r.employee.department, city: r.city, base: r.base, startMonth: r.startMonth, endMonth: r.endMonth, changeType: r.changeType, detail: detail ? { items: detail.items, totalOrg: detail.totalOrg, totalEmp: detail.totalEmp, total: detail.totalOrg + detail.totalEmp, } : null, } })) const cities = [...new Set(records.map((r) => r.city))] const configs: Record = {} for (const c of cities) { const cfg = await getConfigForCity(c) if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax } } res.json({ success: true, data: { month, configs, items }, }) } catch (err) { next(err) } }) // 公积金在保人员 router.get('/housing/active-declaration', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const month = (req.query.month as string) || new Date().toISOString().slice(0, 7) const orgId = req.user!.orgId const records = await prisma.employeeHousingFundRecord.findMany({ where: { orgId, startMonth: { lt: month }, OR: [{ endMonth: null }, { endMonth: { gt: month } }], }, include: { employee: { select: { name: true, department: true, idCardNumber: true, hireDate: true } } }, orderBy: { createdAt: 'asc' }, }) const configCache = new Map() const getConfigForCity = async (city: string) => { if (!configCache.has(city)) { configCache.set(city, await getHousingConfigByMonth(orgId, month, city)) } return configCache.get(city) } const items = await Promise.all(records.map(async (r) => { const config = await getConfigForCity(r.city) const detail = config ? calcHousingDetail(r.base, config) : null return { employeeId: r.employeeId, name: r.employee.name, department: r.employee.department, city: r.city, base: r.base, startMonth: r.startMonth, endMonth: r.endMonth, changeType: r.changeType, detail: detail ? { orgAmount: detail.orgAmount, empAmount: detail.empAmount, total: detail.total } : null, } })) const cities = [...new Set(records.map((r) => r.city))] const configs: Record = {} for (const c of cities) { const cfg = await getConfigForCity(c) if (cfg) configs[c] = { city: cfg.city, effectiveFrom: cfg.effectiveFrom, baseMin: cfg.baseMin, baseMax: cfg.baseMax, housingOrg: cfg.housingOrg, housingEmp: cfg.housingEmp } } res.json({ success: true, data: { month, configs, items }, }) } catch (err) { next(err) } }) // ========== 月度办理完成(保存快照) ========== // 列出所有已办理月份(用于办理总览) router.get('/monthly-process/list', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const records = await prisma.socialMonthlyProcess.findMany({ where: { orgId }, orderBy: { month: 'desc' }, select: { id: true, month: true, type: true, status: true, processedAt: true, processedBy: true }, }) res.json({ success: true, data: records }) } catch (err) { next(err) } }) // 查询某月办理状态 router.get('/monthly-process/status', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const month = (req.query.month as string) || new Date().toISOString().slice(0, 7) const orgId = req.user!.orgId const records = await prisma.socialMonthlyProcess.findMany({ where: { orgId, month }, }) res.json({ success: true, data: { month, social: records.find((r) => r.type === 'SOCIAL') || null, housing: records.find((r) => r.type === 'HOUSING') || null, }, }) } catch (err) { next(err) } }) // 办理完成(保存快照) router.post('/monthly-process/complete', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { month, type, snapshot } = req.body as { month: string; type: 'SOCIAL' | 'HOUSING'; snapshot: any } const orgId = req.user!.orgId if (!month || !type || !snapshot) { return res.status(400).json({ success: false, message: '缺少必要参数' }) } const existing = await prisma.socialMonthlyProcess.findUnique({ where: { orgId_month_type: { orgId, month, type } }, }) if (existing) { // 已存在则更新快照 const updated = await prisma.socialMonthlyProcess.update({ where: { id: existing.id }, data: { snapshot, processedBy: req.user!.id, processedAt: new Date() }, }) return res.json({ success: true, data: updated }) } const record = await prisma.socialMonthlyProcess.create({ data: { orgId, month, type, snapshot, processedBy: req.user!.id, createdBy: req.user!.id, }, }) res.json({ success: true, data: record }) } catch (err) { next(err) } }) // ========== 记录修正(直接更新 + 审计日志) ========== // 修正社保记录 router.put('/records/social/:id/correct', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const { city, base, startMonth, endMonth, changeType, remark } = req.body as { city?: string; base?: number; startMonth?: string; endMonth?: string; changeType?: string; remark?: string } const record = await prisma.employeeSocialInsRecord.findFirst({ where: { id: req.params.id, orgId } }) if (!record) return res.status(404).json({ success: false, message: '记录不存在' }) const oldData = { city: record.city, base: record.base, startMonth: record.startMonth, endMonth: record.endMonth, changeType: record.changeType, remark: record.remark } const updateData: any = {} if (city !== undefined) updateData.city = city if (base !== undefined) updateData.base = base if (startMonth !== undefined) updateData.startMonth = startMonth if (endMonth !== undefined) updateData.endMonth = endMonth || null if (changeType !== undefined) updateData.changeType = changeType if (remark !== undefined) updateData.remark = remark const updated = await prisma.employeeSocialInsRecord.update({ where: { id: req.params.id }, data: updateData }) // 同步员工便捷字段(如果修正的是当前在保记录) if (!updated.endMonth) { await prisma.employee.update({ where: { id: record.employeeId }, data: { ...(city !== undefined ? { city } : {}), ...(base !== undefined ? { socialInsBase: base } : {}), ...(startMonth !== undefined ? { socialInsStartMonth: startMonth } : {}), }, }) } // 写审计日志 await prisma.auditLog.create({ data: { orgId, userId: req.user!.id, action: 'CORRECT', entity: 'EmployeeSocialInsRecord', entityId: req.params.id, detail: { old: oldData, new: updateData, reason: req.body.reason || '数据修正' }, }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // 修正公积金记录 router.put('/records/housing/:id/correct', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const { city, base, startMonth, endMonth, changeType, remark } = req.body as { city?: string; base?: number; startMonth?: string; endMonth?: string; changeType?: string; remark?: string } const record = await prisma.employeeHousingFundRecord.findFirst({ where: { id: req.params.id, orgId } }) if (!record) return res.status(404).json({ success: false, message: '记录不存在' }) const oldData = { city: record.city, base: record.base, startMonth: record.startMonth, endMonth: record.endMonth, changeType: record.changeType, remark: record.remark } const updateData: any = {} if (city !== undefined) updateData.city = city if (base !== undefined) updateData.base = base if (startMonth !== undefined) updateData.startMonth = startMonth if (endMonth !== undefined) updateData.endMonth = endMonth || null if (changeType !== undefined) updateData.changeType = changeType if (remark !== undefined) updateData.remark = remark const updated = await prisma.employeeHousingFundRecord.update({ where: { id: req.params.id }, data: updateData }) // 同步员工便捷字段(如果修正的是当前在保记录) if (!updated.endMonth) { await prisma.employee.update({ where: { id: record.employeeId }, data: { ...(city !== undefined ? { city } : {}), ...(base !== undefined ? { housingFundBase: base } : {}), ...(startMonth !== undefined ? { housingFundStartMonth: startMonth } : {}), }, }) } // 写审计日志 await prisma.auditLog.create({ data: { orgId, userId: req.user!.id, action: 'CORRECT', entity: 'EmployeeHousingFundRecord', entityId: req.params.id, detail: { old: oldData, new: updateData, reason: req.body.reason || '数据修正' }, }, }) res.json({ success: true, data: updated }) } catch (err) { next(err) } }) // ========== 专项附加扣除按月录入 ========== // 查询员工某月专项附加扣除 router.get('/special-deduction', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { employeeId, month } = req.query if (!employeeId || !month) return res.status(400).json({ success: false, message: '缺少 employeeId 或 month' }) const record = await prisma.specialDeductionRecord.findUnique({ where: { employeeId_month: { employeeId: employeeId as string, month: month as string } }, }) res.json({ success: true, data: record }) } catch (err) { next(err) } }) // 批量查询员工某月专项附加扣除 router.get('/special-deduction/batch', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { month } = req.query if (!month) return res.status(400).json({ success: false, message: '缺少 month' }) const records = await prisma.specialDeductionRecord.findMany({ where: { orgId: req.user!.orgId, month: month as string }, include: { employee: { select: { name: true, department: true } } }, }) res.json({ success: true, data: records }) } catch (err) { next(err) } }) // 创建/更新专项附加扣除 const upsertDeductionSchema = z.object({ employeeId: z.string().min(1), month: z.string().regex(/^\d{4}-\d{2}$/), amount: z.number().min(0).optional(), children: z.number().min(0).optional(), elderly: z.number().min(0).optional(), housing: z.number().min(0).optional(), education: z.number().min(0).optional(), infant: z.number().min(0).optional(), remark: z.string().optional(), }) router.post('/special-deduction', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const data = upsertDeductionSchema.parse(req.body) const orgId = req.user!.orgId const amount = data.amount ?? (data.children || 0) + (data.elderly || 0) + (data.housing || 0) + (data.education || 0) + (data.infant || 0) const record = await prisma.specialDeductionRecord.upsert({ where: { employeeId_month: { employeeId: data.employeeId, month: data.month } }, create: { orgId, employeeId: data.employeeId, month: data.month, amount, children: data.children || 0, elderly: data.elderly || 0, housing: data.housing || 0, education: data.education || 0, infant: data.infant || 0, remark: data.remark, createdBy: req.user!.id, }, update: { amount, children: data.children || 0, elderly: data.elderly || 0, housing: data.housing || 0, education: data.education || 0, infant: data.infant || 0, remark: data.remark, }, }) // 同步员工便捷字段 await prisma.employee.update({ where: { id: data.employeeId }, data: { specialDeduction: amount } }) res.json({ success: true, data: record }) } catch (err) { next(err) } }) // 批量录入专项附加扣除 router.post('/special-deduction/batch', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { month, items } = req.body as { month: string; items: any[] } if (!month || !items || !Array.isArray(items)) return res.status(400).json({ success: false, message: '缺少 month 或 items' }) const orgId = req.user!.orgId const result = { total: items.length, updated: 0, errors: [] as string[] } for (let i = 0; i < items.length; i++) { const item = items[i] try { const amount = item.amount ?? (item.children || 0) + (item.elderly || 0) + (item.housing || 0) + (item.education || 0) + (item.infant || 0) await prisma.specialDeductionRecord.upsert({ where: { employeeId_month: { employeeId: item.employeeId, month } }, create: { orgId, employeeId: item.employeeId, month, amount, children: item.children || 0, elderly: item.elderly || 0, housing: item.housing || 0, education: item.education || 0, infant: item.infant || 0, remark: item.remark, createdBy: req.user!.id, }, update: { amount, children: item.children || 0, elderly: item.elderly || 0, housing: item.housing || 0, education: item.education || 0, infant: item.infant || 0, remark: item.remark, }, }) await prisma.employee.update({ where: { id: item.employeeId }, data: { specialDeduction: amount } }) result.updated++ } catch (e: any) { result.errors.push(`第${i + 1}行:${e?.message || '录入失败'}`) } } res.json({ success: true, data: result }) } catch (err) { next(err) } }) // ========== AI 社保政策建议 ========== const aiSuggestSchema = z.object({ city: z.string(), effectiveFrom: z.string().regex(/^\d{4}-\d{2}$/), type: z.enum(['social', 'housing']).default('social'), }) router.post('/ai-suggest', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const { city, effectiveFrom, type } = aiSuggestSchema.parse(req.body) const apiKey = process.env.DASHSCOPE_API_KEY || '' if (!apiKey) { return res.status(400).json({ success: false, error: { code: 'NO_API_KEY', message: 'AI服务未配置' } }) } const client = new OpenAI({ apiKey, baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', timeout: 30 * 1000, maxRetries: 1 }) const year = effectiveFrom.split('-')[0] const prompt = type === 'social' ? `请提供${city}在${year}年度的社保缴费政策。请严格按照以下JSON格式返回,不要包含任何其他文字: { "baseMin": 数字, "baseMax": 数字, "medicalBaseMin": 数字, "medicalBaseMax": 数字, "pensionOrg": 数字, "pensionEmp": 数字, "medicalOrg": 数字, "medicalEmp": 数字, "unemploymentOrg": 数字, "unemploymentEmp": 数字, "injuryOrg": 数字, "maternityOrg": 数字, "extraInsurances": [ { "name": "险种名称", "baseType": "fixed或pension或medical", "fixedAmount": 数字, "empFixedAmount": 数字, "orgRate": 数字, "empRate": 数字 } ] } 说明: - baseMin/baseMax: 养老/失业/工伤保险缴费基数上下限 - medicalBaseMin/medicalBaseMax: 医疗/生育保险缴费基数上下限(与养老相同则填相同值) - 所有比例单位为百分比,如养老企业16%则填16 - extraInsurances: 大病医疗、长期护理险等附加险种,fixed类型用fixedAmount/empFixedAmount(元/月),比例类型用orgRate/empRate(百分比) - 如果没有附加险种,返回空数组 - 请基于${year}年度${city}的最新社保政策填写` : `请提供${city}在${year}年度的住房公积金缴存政策。请严格按照以下JSON格式返回,不要包含任何其他文字: { "baseMin": 数字, "baseMax": 数字, "housingOrg": 数字, "housingEmp": 数字 } 说明: - baseMin/baseMax: 公积金缴存基数上下限 - housingOrg/housingEmp: 企业和个人缴存比例(百分比),如12%则填12 - 请基于${year}年度${city}的最新公积金政策填写` const response = await client.chat.completions.create({ model: 'qwen-plus', messages: [ { role: 'system', content: '你是社保政策专家,精通中国各城市的社会保险和住房公积金缴费政策。请只返回JSON格式数据,不要包含markdown代码块标记。' }, { role: 'user', content: prompt }, ], temperature: 0.1, }) const content = response.choices[0]?.message?.content || '' // 提取JSON(兼容markdown代码块) let jsonStr = content.trim() const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/) if (jsonMatch) jsonStr = jsonMatch[1].trim() // 去除可能的非JSON前后文字 const firstBrace = jsonStr.indexOf('{') const lastBrace = jsonStr.lastIndexOf('}') if (firstBrace >= 0 && lastBrace >= 0) jsonStr = jsonStr.substring(firstBrace, lastBrace + 1) const suggested = JSON.parse(jsonStr) res.json({ success: true, data: suggested }) } catch (err: any) { next(err) } }) export default router