Phase 1 紧急修复(8项): - 社保城市选择改为可输入 - 社保上下限拆分(三险/医保独立基数) - 公积金试算结果展示修复 - 花名册合同保存修复(日期ISO格式) - 薪酬批次创建失败修复(城市过滤+错误处理) - 证据链查看修复 - 个税计算修复(blank_employees读取基本工资) - 加班费倍率读取配置 Phase 2 功能完善(3项): - 批量导入per-row异常捕获+导入按钮 - 单人发薪UI入口优化 - 解除协议模板补充(员工提出离职版) Phase 3 后期规划(4项): - 工资表导入功能(POST /import/payroll + 前端入口) - 大病险/长护险附加险种(extraInsurances JSON + 计算适配) - 专项附加扣除按月录入(SpecialDeductionRecord模型 + 前端Tab) - 预置河北省社保政策(seed数据)
30 KiB
TurboHR 20260727 优化需求梳理
来源:HR 用户测试反馈
日期:2026-07-27
测试地区:河北省(重点)
梳理方式:逐条对照源码分析根因,标注涉及文件和行号
一、需求分类总览
| # | 模块 | 优先级 | 类型 | 简述 | 根因已定位 |
|---|---|---|---|---|---|
| 1 | 社保政策 | 🔴 高 | Bug+功能 | 仅北京/上海,无法添加其他城市,河北无法使用 | ✅ |
| 2 | 社保政策 | 🔴 高 | 功能 | 河北五险不同上下限,三险与医保需分开 | ✅ |
| 3 | 社保政策 | 🟡 中 | 功能 | 大病险、长护险各地市收费不同,需单独添加险种 | ✅ |
| 4 | 公积金 | 🔴 高 | Bug | 公积金模块无法测算数据,显示报错 | ✅ |
| 5 | 专项附加扣除 | 🟡 中 | 功能 | 无法关联自然人数据,需手动录入,是否可按月累计 | ✅ |
| 6 | 花名册-合同 | 🔴 高 | Bug | 花名册中添加劳动合同信息无法正常保存 | ✅ |
| 7 | 花名册-导入 | 🔴 高 | Bug+UI | 批量导入仅员工基本信息成功,合同/加班/考勤均失败 | ✅ |
| 8 | 花名册-UI | 🟡 中 | UI | 缺少明显的批量导入按钮入口 | ✅ |
| 9 | 薪酬管理 | 🔴 高 | Bug | 创建批次总提示创建失败 | ✅ |
| 10 | 薪酬管理 | 🔴 高 | 功能 | 无法单独添加一个人发薪,必须全员生成再删除 | ✅ |
| 11 | 薪酬管理 | 🟡 中 | 功能 | 缺少工资表导入功能 | ✅ |
| 12 | 个税计算 | 🔴 高 | Bug | 导入工资后显示无个税,未匹配自动算税 | ✅ |
| 13 | 证据链 | 🔴 高 | Bug | 证据链无法查看(手动录入的员工也无法查看) | ✅ |
| 14 | 加班费 | 🟡 中 | 功能 | 加班费倍率硬编码(1.5/2.0/3.0),需支持公司自定义标准 | ✅ |
| 15 | 考勤对接 | 🟢 低 | 功能 | 考勤制度是否可关联企微等外部系统 | — |
| 16 | 电子签 | 🟢 低 | 功能 | 合同签署是否可关联电子签平台 | — |
| 17 | 文本模板 | 🟡 中 | 功能 | 缺少「个人提出离职」的解除协议模板 | ✅ |
二、详细分析(含源码根因)
2.1 社保政策 — 多城市支持(#1, #2, #3)
根因分析:
-
城市选择为下拉固定列表,无法手动输入
SocialInsurance.tsx:340-347— 城市选择是<select>下拉,选项来自GET /social/config/cities返回的已有城市列表- 如果数据库中只有北京/上海,用户无法选择其他城市
- 但新建版本表单
SocialInsurance.tsx:570中城市字段是<Input>文本框,可以手动输入 - 真正问题:城市选择下拉限制了查看范围,用户在新建版本时可以输入「河北」,但切换城市查看时下拉没有「河北」选项
-
社保上下限单一,无法区分三险与医保
schema.prisma:389-390—SocialInsuranceConfig仅有baseMin/baseMax两个字段payroll.service.ts:37-42—calcSocialInsurance()使用单一actualBase计算所有险种- 河北省政策:养老/失业/工伤保险基数上下限 ≠ 医疗/生育保险基数上下限
-
无大病险/长护险字段
schema.prisma:376-401—SocialInsuranceConfig无大病险、长护险相关字段payroll.service.ts:39-40— 计算仅包含养老/医疗/失业/工伤/生育五险
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| Schema 扩展 | prisma/schema.prisma:376-401 |
新增 medicalBaseMin Float @default(0) medicalBaseMax Float @default(0) extraInsurances String? // JSON |
| 社保计算适配 | backend/src/services/payroll.service.ts:37-42 |
三险用 baseMin/baseMax,医保用 medicalBaseMin/medicalBaseMax(为 0 时 fallback 到 baseMin/baseMax) |
| 前端城市选择 | frontend/src/pages/SocialInsurance.tsx:340-347 |
<select> 改为 <input list> + <datalist>,支持手动输入城市名 |
| 新建版本表单 | frontend/src/pages/SocialInsurance.tsx:568-590 |
新增医保上下限输入框 + 附加险种配置区 |
| 预置河北数据 | seed 脚本或手动 | 添加河北省社保配置(养老/失业/工伤基数 3920 |
河北省 2024 社保参考数据:
养老/失业/工伤:基数下限 3920,上限 19602
医疗/生育: 基数下限 5360,上限 26796
养老 企业16% 个人8%
失业 企业0.7% 个人0.3%
工伤 企业0.2~1.9%(按行业)
医疗 企业8% 个人2%
生育 企业1%(已并入医疗,河北单独列)
2.2 公积金测算报错(#4)
根因分析:
-
social.routes.ts:557-606—/housing-calculate接口 -
核心问题在第 580-583 行:当查不到公积金配置时,自动创建一条默认配置:
config = await prisma.housingFundConfig.create({ data: { orgId, effectiveFrom: ..., city: city || '北京', createdBy: ... }, })创建的配置使用 schema 默认值(
housingOrg: 12, housingEmp: 12, baseMin: 6326, baseMax: 33891),但city参数可能为undefined -
前端调用链:
SocialInsurance.tsx:611—calcHousingMutate()调用POST /social/housing-calculate,传参{ base, month, city } -
可能原因:
city未正确传递(undefined),创建的配置城市为「北京」而非用户期望的城市- 前端
housingResult的items字段不存在(公积金返回的是housingOrg/housingEmp而非items数组),但前端试算结果展示复用了社保的r.items.map()逻辑,导致undefined.map()报错
-
前端结果展示 Bug:
SocialInsurance.tsx:625-677— 试算结果展示区同时用于社保和公积金,使用r.items.map()渲染表格。但公积金计算接口返回{ housingOrg, housingEmp, total },没有items数组,导致r.items为undefined→.map()抛出 TypeError
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 前端结果展示 | frontend/src/pages/SocialInsurance.tsx:625-677 |
公积金试算结果单独渲染(显示企业/个人比例和金额),不复用社保的 items 表格 |
| 后端兜底优化 | backend/src/routes/social.routes.ts:580-583 |
不自动创建默认配置,改为返回提示「该城市暂无公积金配置,请先创建」 |
2.3 专项附加扣除(#5)
根因分析:
payroll.service.ts:179— 个税累计预扣计算:const ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7))Employee.specialDeduction是单一 Float 字段,表示每月专项附加扣除金额- 个税计算时直接乘以月份序号作为累计扣除额
- 问题:不支持按月不同金额(如某月子女教育扣除变更),且需手动在员工档案中录入
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 新建 Prisma 模型 | prisma/schema.prisma |
model SpecialDeductionRecord { id, orgId, employeeId, month, amount, type(子女教育/住房贷款/赡养老人/...), createdBy, createdAt } |
| 个税计算适配 | backend/src/services/payroll.service.ts:179 |
改为查询 SpecialDeductionRecord 按月累加,fallback 到 employee.specialDeduction * 月份 |
| 前端录入入口 | frontend/src/pages/roster/BasicInfo.tsx |
在社保/公积金基数旁增加「专项附加扣除」按月录入区 |
2.4 花名册合同保存失败(#6)
根因分析:
- 前端调用:
ContractInfo.tsx:19—api.post('/employees/contracts', { ...data, employeeId }) - 后端路由:
employee.routes.ts:206-209—router.post('/contracts', ...)→addContractSchema.parse(req.body)→addContract() - Schema 校验:
contract.schema.ts:54-64:signDate: z.string().datetime().nullable(), // 必须是 ISO datetime 字符串 startDate: z.string().datetime(), // 必须是 ISO datetime 字符串 endDate: z.string().datetime().nullable(), - 前端提交:
ContractInfo.tsx表单中日期用<Input type="date">,值为YYYY-MM-DD格式(如2026-07-27),不是 ISO datetime 格式(2026-07-27T00:00:00.000Z) - 根因:Zod 校验
z.string().datetime()要求 RFC 3339 格式,YYYY-MM-DD不通过校验 →ZodError→ 返回 400 → 前端显示「保存失败」
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| Schema 日期校验放宽 | backend/src/schemas/contract.schema.ts:56-58 |
改为 z.string().nullable() + 在 addContract() 中用 new Date() 解析 |
| 或前端提交时转换 | frontend/src/pages/roster/ContractInfo.tsx |
提交前将日期转为 ISO 格式:new Date(form.signDate).toISOString() |
推荐方案:前端转换(改动最小,且 Contracts.tsx 新建员工时已用 new Date(form.signDate).toISOString() 转换,ContractInfo.tsx 遗漏了同样的转换)
2.5 批量导入问题(#7, #8)
根因分析:
import.routes.ts:214-390— 多 Sheet 导入逻辑- Sheet 名称精确匹配:代码中硬编码 Sheet 名称为中文(如「员工信息」「劳动合同」「加班记录」「考勤记录」),如果用户修改了 Sheet 名或模板格式不一致,则无法匹配
- 合同匹配逻辑:先按
idCardHash匹配,再按name匹配。如果员工信息 Sheet 和合同 Sheet 中的身份证号或姓名不一致(空格、别称),则匹配失败 - 错误信息未充分展示:后端返回
errors数组,但前端可能只显示了「成功 N 条」的汇总,未展示详细错误
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 花名册增加导入按钮 | frontend/src/pages/Roster.tsx 或 Contracts.tsx |
在列表页顶部增加「批量导入」按钮,点击后弹出导入向导 |
| 导入模板下载 | frontend |
调用 GET /import/template 下载标准模板 |
| 错误详情展示 | frontend |
导入结果弹窗中展示 errors[] 数组的每条错误(行号+原因) |
| Sheet 名称容错 | backend/src/routes/import.routes.ts |
Sheet 名称匹配改为包含关键词即可(如包含「合同」即视为劳动合同 Sheet) |
2.6 薪酬管理创建失败(#9)
根因分析:
payroll2.routes.ts:220-403— 批次创建逻辑- 关键链路:
createBatchSchema.parse(req.body)→ 查询员工 → 循环calcBatchEntry()→ 创建BatchEntry calcBatchEntry()可能抛异常:payroll.service.ts:109-127— 查询socialInsuranceConfig和housingFundConfig时不带city过滤:如果组织有多个城市的配置,可能取到错误城市的配置;如果无配置,prisma.socialInsuranceConfig.findFirst({ where: { orgId, effectiveFrom: { lte: month }, OR: [...] }, orderBy: { effectiveFrom: 'desc' }, })socialConfig为null,社保为 0(不报错)- 更可能的根因:
payroll.service.ts:329—Number(decrypt(emp.monthlySalary))如果monthlySalary加密格式异常,decrypt抛出错误,虽然有catch回退到Number(emp.monthlySalary),但如果monthlySalary本身是加密后的非数字字符串,Number()返回NaN,后续计算NaN传播可能导致 Prisma 写入失败 - 另一个可能:
payroll2.routes.ts:346—calcBatchEntry()内部prisma.payslip.findMany()查询历史工资条,如果数据量大可能超时
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| calcBatchEntry 增加城市过滤 | backend/src/services/payroll.service.ts:111-118 |
查询社保配置时加入 city: employee.city 过滤 |
| 错误处理增强 | backend/src/routes/payroll2.routes.ts:346 |
calcBatchEntry() 调用加 try-catch,单条失败跳过并记录,不阻塞整批 |
| 前端错误展示 | frontend/src/pages/Money.tsx:111-120 |
onError 时展示后端返回的具体错误信息 |
2.7 单人发薪(#10)
根因分析:
- 后端已有接口:
payroll2.routes.ts:489-547—POST /batches/:batchId/employees支持向批次添加员工 - 前端已有调用:
Money.tsx:735-739—addMutation调用api.post('/payroll2/batches/${batchId}/employees', { employeeIds }) - 结论:功能已存在,用户可能未找到入口。需检查前端 UI 是否暴露了「添加员工」按钮
修复方案:
- 检查
Money.tsx批次详情页中是否有「添加员工」按钮 - 如果按钮存在但隐藏,调整 UI 使其更明显
- 如果按钮不存在,在批次详情页增加「添加员工」操作
2.8 工资表导入(#11)
现状:无工资表导入功能
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 后端导入接口 | backend/src/routes/import.routes.ts |
新增 POST /import/payroll 解析 Excel 工资表(员工姓名/身份证 + 基本工资/津贴/奖金/扣款) |
| 前端导入入口 | frontend/src/pages/Money.tsx |
批次详情页增加「导入工资表」按钮 |
2.9 个税计算问题(#12)
根因分析:
payroll.service.ts:160-183— 累计预扣法个税计算逻辑完整- 个税为 0 的正常情况:
totalPay < 5000→ytdTaxableIncome ≤ 0→ 个税 = 0totalPay - 社保 - 公积金 - 5000*月份 - 专项附加扣除 ≤ 0→ 个税 = 0
- 个税为 0 的异常情况:
- 社保配置缺失 →
socialEmp = 0(不会导致个税为 0,反而个税应更高) specialDeduction为 0 → 减除费用仅 5000/月,如果工资 > 5000 应有个税- 真正问题:如果
baseSalary = 0(使用blank_all或blank_employees模式创建批次),totalPay = 0→ 个税 = 0
- 社保配置缺失 →
- 用户反馈场景:用户说「导入工资后显示无个税」,说明工资数据已导入但个税仍为 0
- 可能原因:导入工资数据后未触发
calcBatchEntry()重新计算 - 或前端显示的个税字段映射有误
- 可能原因:导入工资数据后未触发
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 编辑条目时重算 | backend/src/routes/payroll2.routes.ts |
PUT /batches/:batchId/entries/:employeeId 已调用 calcBatchEntry(),确认前端编辑后是否触发重算 |
| 前端字段映射 | frontend/src/pages/Money.tsx |
确认 entry.tax 字段正确显示 |
| 导入后自动重算 | backend/src/routes/import.routes.ts |
如果新增工资表导入,导入后自动调用 calcBatchEntry() |
2.10 证据链无法查看(#13)
根因分析:
- 后端 API 完整:
roster.routes.ts:240-494—GET /:id/evidence-chain返回{ employee, evidence[], risks[], summary } - 前端组件完整:
EvidenceChain.tsx:14-136— 使用useQuery调用api.get('/roster/${employeeId}/evidence-chain'),渲染证据列表和风险提醒 - 前端引用正确:
EmployeeProfile.tsx:96—{tab === 'evidence' && <EvidenceChain employeeId={employeeId} />} - 可能根因:
- Tab 未显示:
EmployeeProfile.tsx的 Tab 列表中是否有「证据链」Tab?需检查TAB_GROUPS定义 - API 路由前缀:前端
api.get('/roster/${employeeId}/evidence-chain')→ 实际请求路径需确认是否匹配后端路由挂载前缀 - 新员工无数据:手动录入的新员工如果没有合同/工资条/考勤等关联数据,
evidence[]数组可能为空,前端显示「无数据」
- Tab 未显示:
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 检查 Tab 定义 | frontend/src/pages/roster/shared.ts |
确认 TAB_GROUPS 中包含证据链 Tab |
| 空数据提示优化 | frontend/src/pages/roster/EvidenceChain.tsx:24 |
`if (!data |
| API 路径验证 | frontend/src/lib/api.ts |
确认 baseURL + /roster/:id/evidence-chain 是否匹配后端挂载路径 |
2.11 加班费自定义标准(#14)
根因分析:
- 后端已有 OvertimeConfig 模型和 API:
schema.prisma:444-454—OvertimeConfig模型,含weekdayRate/weekendRate/holidayRate/monthlyDays/dailyHourspayroll.routes.ts:334-370—GET /overtime/config和POST /overtime/config接口
- 但加班费计算仍用硬编码:
payroll.routes.ts:45-47和107-109:未读取const weekdayPay = hourlyWage * 1.5 * data.weekdayHours const weekendPay = hourlyWage * 2.0 * data.weekendHours const holidayPay = hourlyWage * 3.0 * data.holidayHoursOvertimeConfig中的倍率,直接硬编码 1.5/2.0/3.0
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 加班费计算读取配置 | backend/src/routes/payroll.routes.ts:44-47 |
先查 OvertimeConfig,用配置中的倍率替代硬编码 |
| 同上 | backend/src/routes/payroll.routes.ts:107-109 |
PUT 接口同样修复 |
| 前端增加配置入口 | frontend/src/pages/Money.tsx 或 Settings.tsx |
增加加班费倍率配置 UI |
2.12 考勤企微对接(#15)
现状:无企微对接
需求:评估对接企业微信考勤数据的可行性
方案:后期规划,需企微 API 文档调研。企微提供考勤数据接口 checkin/getcheckindata,可定时拉取同步到系统
2.13 电子签对接(#16)
现状:合同记录有 signMethod 字段(PAPER/ELECTRONIC),ContractInfo.tsx 表单已支持选择签署方式并填写电子合同编号/链接
需求:评估对接电子签平台(如法大大/上上签)的可行性
方案:后期规划,需第三方平台 API 调研。当前可先完善手动录入电子合同信息的流程
2.14 文本模板补充(#17)
根因分析:
template.service.ts:15-249— 硬编码模板数组documentTemplates- 已有模板:固定期限劳动合同、无固定期限劳动合同、协商解除劳动合同协议书(
tpl_termination_agreement)、员工手册公示通知、规章制度讨论通知、违纪处分通知书、试用期转正通知书、合同到期不续签通知书 - 缺少:员工主动提出离职的解除协议书模板
修复方案:
| 改动 | 文件 | 具体内容 |
|---|---|---|
| 新增模板 | backend/src/services/template.service.ts |
在 documentTemplates 数组中新增 tpl_voluntary_termination_agreement(员工提出离职版解除协议) |
模板内容要点:
- 乙方主动提出离职,甲方同意
- 无经济补偿金(员工主动辞职,法定无需支付)
- 工作交接条款
- 社保公积金截止月份
- 竞业限制延续条款(如有)
- 变量:
companyName, employeeName, idCard, resignDate, lastWorkDay, socialInsEndMonth, housingFundEndMonth, resignReason
三、实施优先级建议
Phase 1 — 紧急修复(阻塞客户使用)
| 序号 | 任务 | 根因 | 预估工作量 |
|---|---|---|---|
| 1 | 社保城市选择改为可输入 | 城市下拉限制了已有城市 | 0.5 天 |
| 2 | 社保上下限拆分(三险/医保) | Schema 单一 baseMin/baseMax | 1 天 |
| 3 | 预置河北省社保政策 | 无河北数据 | 0.5 天 |
| 4 | 公积金试算结果展示修复 | 公积金返回无 items 数组,复用社保表格渲染报错 | 0.5 天 |
| 5 | 花名册合同保存修复 | 前端提交 YYYY-MM-DD 未转 ISO datetime | 0.5 天 |
| 6 | 薪酬批次创建失败修复 | calcBatchEntry 缺城市过滤 + 错误处理不足 | 0.5 天 |
| 7 | 证据链查看修复 | 需确认 Tab 定义 + 空数据提示 | 0.5 天 |
| 8 | 个税计算排查 | 确认导入后是否触发重算 | 0.5 天 |
Phase 2 — 功能完善
| 序号 | 任务 | 预估工作量 |
|---|---|---|
| 9 | 批量导入 Bug 修复 + 导入按钮 | 0.5 天 |
| 10 | 单人发薪 UI 入口优化 | 0.5 天 |
| 11 | 工资表导入功能 | 1 天 |
| 12 | 大病险/长护险单独险种模块 | 1 天 |
| 13 | 专项附加扣除按月录入 | 0.5 天 |
| 14 | 加班费倍率读取配置(后端已有模型,改计算逻辑) | 0.5 天 |
| 15 | 解除协议模板补充(员工提出离职版) | 0.5 天 |
Phase 3 — 后期规划
| 序号 | 任务 | 说明 |
|---|---|---|
| 16 | 企微考勤对接 | 需 API 调研 |
| 17 | 电子签平台对接 | 需第三方平台选型 |
四、技术要点
4.1 社保上下限拆分方案
// schema.prisma — SocialInsuranceConfig 新增字段
model SocialInsuranceConfig {
// ... 现有字段 ...
baseMin Float @default(6326) // 三险基数下限(养老/失业/工伤)
baseMax Float @default(33891) // 三险基数上限
medicalBaseMin Float @default(0) // 医保基数下限(0 = fallback 到 baseMin)
medicalBaseMax Float @default(0) // 医保基数上限(0 = fallback 到 baseMax)
extraInsurances String? // JSON: [{ name, type: 'fixed'|'rate', orgRate, empRate, orgAmount, empAmount }]
}
// payroll.service.ts — calcSocialInsurance 适配
export function calcSocialInsurance(base: number, config: any) {
const pensionBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
const medicalBase = Math.min(
Math.max(base, config.medicalBaseMin || config.baseMin),
config.medicalBaseMax || config.baseMax
)
// 三险用 pensionBase,医保用 medicalBase
const socialEmp = pensionBase * (config.pensionEmp + config.unemploymentEmp) / 100
+ medicalBase * (config.medicalEmp) / 100
const socialOrg = pensionBase * (config.pensionOrg + config.unemploymentOrg + config.injuryOrg) / 100
+ medicalBase * (config.medicalOrg + config.maternityOrg) / 100
return { actualBase: pensionBase, socialEmp, socialOrg }
}
4.2 合同日期格式修复
// ContractInfo.tsx — 提交前转换日期格式
const handleSubmit = () => {
const data = {
...formData,
signDate: form.signDate ? new Date(form.signDate).toISOString() : null,
startDate: new Date(form.startDate).toISOString(),
endDate: form.endDate ? new Date(form.endDate).toISOString() : null,
}
addContractMutation.mutate(data)
}
4.3 公积金试算结果展示修复
// SocialInsurance.tsx — 公积金试算结果单独渲染
{isHousing && r ? (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>缴费基数</span><span className="font-medium">¥{fmt(r.actualBase)}</span>
</div>
<div className="flex justify-between text-sm">
<span>企业缴纳 ({r.housingOrg ? '' : ''})</span>
<span className="text-danger">¥{fmt(r.housingOrg)}</span>
</div>
<div className="flex justify-between text-sm">
<span>个人缴纳</span>
<span className="text-warning">¥{fmt(r.housingEmp)}</span>
</div>
<div className="flex justify-between text-sm border-t pt-2 font-medium">
<span>合计</span><span className="text-primary">¥{fmt(r.total)}</span>
</div>
</div>
) : !isHousing && r ? (
// 社保试算结果保持原有 items 表格渲染
...
) : ...}
4.4 加班费倍率读取配置
// payroll.routes.ts — 保存加班费记录时读取 OvertimeConfig
router.post('/overtime', async (req, res, next) => {
const data = overtimeSchema.parse(req.body)
let config = await prisma.overtimeConfig.findUnique({ where: { orgId: req.user!.orgId } })
if (!config) config = { weekdayRate: 1.5, weekendRate: 2.0, holidayRate: 3.0, monthlyDays: 21.75, dailyHours: 8 } as any
const hourlyWage = data.monthlyWage / config.monthlyDays / config.dailyHours
const weekdayPay = hourlyWage * config.weekdayRate * data.weekdayHours
const weekendPay = hourlyWage * config.weekendRate * data.weekendHours
const holidayPay = hourlyWage * config.holidayRate * data.holidayHours
// ...
})
4.5 个税计算链路
创建/编辑批次条目 → calcBatchEntry()
→ 查社保配置(orgId + month,需加 city 过滤)
→ 查公积金配置(同上)
→ 累计预扣法:
ytdIncome = 历史工资条 totalPay 之和 + 本月 totalPay
ytdDeductions = 5000 * 月份 + ytdSocialEmp + ytdHousingEmp + ytdSpecialDeduction
ytdTaxableIncome = max(0, ytdIncome - ytdDeductions)
tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
→ 返回 { tax, netPay, socialEmp, socialOrg, housingEmp, housingOrg }
个税为 0 的条件:ytdTaxableIncome ≤ 0,即累计收入 ≤ 累计减除费用(5000×月份数 + 累计社保 + 累计公积金 + 累计专项附加扣除)
4.6 证据链 API 链路
前端 EvidenceChain.tsx
→ useQuery(['evidence-chain', employeeId])
→ api.get('/roster/${employeeId}/evidence-chain')
→ 后端 roster.routes.ts:240
→ prisma.employee.findFirst({ include: { contracts, payslips, overtimeRecords, ... } })
→ 组装 evidence[](劳动关系/薪酬发放/考勤记录/违纪处理/培训签收/绩效考核/解聘记录)
→ 风险检测 risks[](未签合同/合同到期/工资异常/无考勤记录等)
→ 返回 { employee, evidence, risks, summary: { total, signed, unsigned, riskCount } }
五、实施进度追踪(2026-07-27 更新)
Phase 1 — 紧急修复(全部完成 ✅)
| 序号 | 任务 | 状态 | 修改文件 | 实施内容 |
|---|---|---|---|---|
| 1 | 社保城市选择改为可输入 | ✅ 已完成 | SocialInsurance.tsx |
城市下拉改为 <input list> + <datalist>,支持手动输入 |
| 2 | 社保上下限拆分(三险/医保) | ✅ 已完成 | schema.prisma social.routes.ts payroll.service.ts SocialInsurance.tsx |
Schema 新增 medicalBaseMin/medicalBaseMax;calcSocialInsurance() 和 calcSocialDetail() 医保使用独立基数(为 0 时 fallback);试算接口适配;前端新建版本表单增加医保上下限输入;配置展示区显示医保上下限 |
| 3 | 公积金试算结果展示修复 | ✅ 已完成 | SocialInsurance.tsx |
公积金试算结果单独渲染(企业/个人缴纳金额),不再复用社保 items.map() |
| 4 | 花名册合同保存修复 | ✅ 已完成 | ContractInfo.tsx |
提交前将日期转为 ISO 格式 new Date(form.signDate).toISOString() |
| 5 | 薪酬批次创建失败修复 | ✅ 已完成 | payroll.service.ts payroll2.routes.ts |
calcBatchEntry 查社保/公积金配置加 city 过滤;单员工计算 try-catch 不阻塞整批;返回 failedEmployees 详情 |
| 6 | 证据链查看修复 | ✅ 已完成 | shared.ts EmployeeProfile.tsx EvidenceChain.tsx |
添加 evidence Tab 到 TAB_GROUPS;EmployeeProfile 渲染 EvidenceChain 组件;空数据友好提示 |
| 7 | 个税计算排查修复 | ✅ 已完成 | payroll2.routes.ts |
blank_employees 模式下从员工记录获取基本工资,避免 baseSalary=0 导致个税为 0 |
| 8 | 加班费倍率读取配置 | ✅ 已完成 | payroll.routes.ts |
从 OvertimeConfig 读取倍率,fallback 到 1.5/2.0/3.0 |
Phase 2 — 功能完善(全部完成 ✅)
| 序号 | 任务 | 状态 | 修改文件 | 实施内容 |
|---|---|---|---|---|
| 9 | 批量导入 Bug 修复 + 导入按钮 | ✅ 已完成 | import.routes.ts Roster.tsx |
加班/违纪/考勤导入添加 per-row try-catch;Roster 页面添加「批量导入」按钮入口 |
| 10 | 单人发薪 UI 入口优化 | ✅ 已完成 | Roster.tsx |
操作列添加「发薪」按钮(Wallet 图标),跳转薪税管理页面 |
| 11 | 解除协议模板补充 | ✅ 已完成 | template.service.ts |
新增 tpl_termination_agreement_employee(员工主动提出离职版),含离职原因/无补偿金/竞业限制等条款 |
Phase 3 — 后期规划(部分完成)
| 序号 | 任务 | 状态 | 修改文件 | 实施内容 |
|---|---|---|---|---|
| 12 | 工资表导入功能 | ✅ 已完成 | import.routes.ts Money.tsx |
新增 POST /import/payroll 接口,解析 Excel 工资表批量更新批次条目(基本工资/加班费/津贴/扣款/奖金),自动重算税费;前端批次详情页添加「导入工资表」按钮 + 模板下载 + 结果展示 |
| 13 | 大病险/长护险单独险种模块 | ✅ 已完成 | schema.prisma social.routes.ts payroll.service.ts SocialInsurance.tsx |
Schema 新增 extraInsurances JSON 字段;calcSocialInsurance() 和 calcSocialDetail() 支持附加险种计算(按养老基数/医保基数/固定金额三种方式);试算接口返回附加险种明细;前端新建版本表单增加附加险种动态配置区(添加/删除险种行) |
| 14 | 专项附加扣除按月录入 | ✅ 已完成 | schema.prisma social.routes.ts SocialInsurance.tsx |
新建 SpecialDeductionRecord 模型(子女教育/赡养老人/住房/继续教育/婴幼儿照护五项分项);后端 CRUD + 批量录入 API;前端新增「专项附加扣除」Tab,支持按月查看/编辑/新增,自动计算合计并同步员工便捷字段 |
| 15 | 预置河北省社保政策 | ✅ 已完成 | seed.ts |
添加河北省石家庄市社保配置(养老16/8、医疗8/2、失业0.7/0.3、工伤0.3、生育0.5,基数3920~19602)+ 大病医疗(固定5元) + 长期护理险(0.1%);公积金配置(12%/12%) |
待实施
| 序号 | 任务 | 说明 |
|---|---|---|
| 16 | 企微考勤对接 | 需 API 调研 |
| 17 | 电子签平台对接 | 需第三方平台选型 |
技术备注
- Prisma migration:
medicalBaseMin/medicalBaseMax、extraInsurances、SpecialDeductionRecord已通过prisma db push同步到数据库 - 向后兼容:医保独立上下限为 0 时自动 fallback 到
baseMin/baseMax;extraInsurances为 null 时不影响现有计算 - 附加险种计算方式:
baseType: 'pension'按养老基数 × 比例,baseType: 'medical'按医保基数 × 比例,baseType: 'fixed'按固定金额 - 专项附加扣除:录入后自动同步
employee.specialDeduction便捷字段,个税计算时直接使用 - 解除协议模板:原
tpl_termination_agreement描述更新为「用人单位提出」,新增tpl_termination_agreement_employee为「员工主动提出离职」版