Files
TurboHR/20260816-社保优化.md
T
selfrelease 63b6c9dcc7 feat: 社保公积金账户化重构
新增 SocialAccount(账户)+ SocialYearStandard(年度标准)两层实体,
替代原 SocialInsuranceConfig/HousingFundConfig 按城市管理的方式。

- DB: 新增 SocialAccount、SocialYearStandard 表,Department 加账户关联
- 迁移: 旧 Config 表数据迁移到 Account + YearStandard
- 后端: 新增账户 CRUD + 年度标准 API,薪资计算适配 accountId
- 前端: 设置页新增账户管理 Tab,组织架构提示 level=0 可关联账户
- 前端: SocialInsurance.tsx 城市选择器改为账户选择器
- 兼容: 旧 Config 表保留,薪资计算回退旧表

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-16 13:50:15 +08:00

15 KiB
Raw Blame History

20260816 社保公积金账户化重构

目标:将社保公积金从"按城市直接关联版本"改为"账户 + 年度标准"两层实体。 账户代表用户开设的社保/公积金账户(对应不同子公司/分公司/地区),年度标准是账户下每个年度的缴费比例和基数标准。


一、现状分析

当前数据模型

说明 唯一约束
SocialInsuranceConfig 社保版本(比例+基数+生效月份),按 orgId+city+effectiveFrom @@unique([orgId, city, effectiveFrom])
HousingFundConfig 公积金版本,按 orgId+city+accountType+effectiveFrom @@unique([orgId, city, accountType, effectiveFrom])
EmployeeSocialInsRecord 员工社保参保记录,字段含 city @@index([orgId, city])
EmployeeHousingFundRecord 员工公积金参保记录,字段含 city 无 city 索引
SocialMonthlyProcess 月度办理记录 @@unique([orgId, month, type])

当前问题

  1. "城市"只是字符串:无实体化管理,无法记录账户编号、开户行、缴费主体等
  2. 多子公司/分公司场景缺失:同一城市可能有多个社保账户(不同主体分别开户),当前按 city 唯一无法支持
  3. 版本直接挂在 orgId+city 上:缺少"账户"这一层抽象,无法区分同一城市不同主体的账户
  4. 员工参保记录只有 city:无法精确关联到具体哪个社保账户
  5. 薪资计算中社保配置查询:按 orgId+city 查 SocialInsuranceConfig,无法按账户区分

涉及"关联城市"的代码位置

文件 位置 说明
backend/prisma/schema.prisma SocialInsuranceConfig.city 社保配置按城市区分
backend/prisma/schema.prisma HousingFundConfig.city 公积金配置按城市区分
backend/prisma/schema.prisma EmployeeSocialInsRecord.city 员工社保参保城市
backend/prisma/schema.prisma EmployeeHousingFundRecord.city 员工公积金参保城市
backend/src/routes/social.routes.ts 全文 社保配置 CRUD/版本/城市列表/月度办理 均按 city
backend/src/services/payroll.service.ts 社保计算逻辑 按 city 查 SocialInsuranceConfig
backend/src/routes/payroll2.routes.ts 批次计算 社保配置查询
backend/src/routes/roster.routes.ts socialInsuranceStatus 派生 按 city 判定
backend/src/services/contract.service.ts createEmployee 社保记录创建时关联 city
frontend/src/pages/SocialInsurance.tsx 全文 城市选择器、版本管理、月度办理
frontend/src/pages/roster/modals.tsx 社保公积金区 员工参保城市
frontend/src/pages/Settings.tsx 社保配置 需新增账户管理入口

二、目标数据模型

新增:SocialAccount(社保/公积金账户)

model SocialAccount {
  id            String   @id @default(cuid())
  orgId         String
  org           Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
  type          String   // SOCIAL=社保账户, HOUSING=公积金账户
  name          String   // 账户名称,如"北京总公司社保账户"、"上海分公司社保账户"
  city          String   // 参保城市
  accountNo     String?  // 社保登记号 / 公积金单位账号
  bankName      String?  // 公积金开户行(公积金专用)
  bankAccount   String?  // 公积金银行账号(公积金专用)
  orgName       String?  // 缴费主体名称(子公司/分公司名称)
  orgCode       String?  // 缴费主体统一社会信用代码
  accountType   String?  // 公积金账户类型 BASIC=基本, SUPPLEMENTARY=补充(公积金专用)
  isDefault     Boolean  @default(false) // 是否默认账户(同 type 下仅一个默认)
  status        String   @default("ACTIVE") // ACTIVE=正常, SUSPENDED=停用
  remark        String?
  createdBy     String
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  yearStandards     SocialYearStandard[]
  socialRecords     EmployeeSocialInsRecord[]
  housingRecords    EmployeeHousingFundRecord[]

  @@unique([orgId, type, name])
  @@index([orgId, type, city])
  @@index([orgId, type, isDefault])
}

新增:SocialYearStandard(年度标准,替代原 Config 表)

model SocialYearStandard {
  id              String   @id @default(cuid())
  orgId           String
  org             Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
  accountId       String   // 关联到账户
  account         SocialAccount @relation(fields: [accountId], references: [id], onDelete: Cascade)

  // 社保比例(type=SOCIAL 时使用)
  pensionOrg      Float    @default(16)
  pensionEmp      Float    @default(8)
  medicalOrg      Float    @default(9.8)
  medicalEmp      Float    @default(2)
  unemploymentOrg Float    @default(0.5)
  unemploymentEmp Float    @default(0.5)
  injuryOrg       Float    @default(0.2)
  maternityOrg    Float    @default(0.8)
  baseMin         Float    @default(6326)
  baseMax         Float    @default(33891)
  medicalBaseMin  Float    @default(0)
  medicalBaseMax  Float    @default(0)
  extraInsurances Json?

  // 公积金比例(type=HOUSING 时使用)
  housingOrg      Float    @default(12)
  housingEmp      Float    @default(12)

  effectiveFrom   String   // 生效月份 YYYY-MM
  effectiveTo     String?  // 失效月份 YYYY-MMnull=当前有效)
  isCurrent       Boolean  @default(true)
  adjustmentDone  Boolean  @default(false)
  createdBy       String
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  @@unique([accountId, effectiveFrom])
  @@index([accountId, isCurrent])
}

修改:EmployeeSocialInsRecord

model EmployeeSocialInsRecord {
  // 新增字段
  accountId   String?  // 关联到 SocialAccount(迁移后非空)
  account     SocialAccount? @relation(fields: [accountId], references: [id])

  // 保留 city 字段用于兼容(迁移后从 account.city 派生,后续可废弃)
  city        String   @default("北京")

  // 其余字段不变
}

修改:EmployeeHousingFundRecord

model EmployeeHousingFundRecord {
  // 新增字段
  accountId   String?
  account     SocialAccount? @relation(fields: [accountId], references: [id])

  // 保留 city 字段用于兼容
  city        String   @default("北京")

  // 其余字段不变
}

修改:SocialMonthlyProcess

model SocialMonthlyProcess {
  // 新增字段
  accountId   String?  // 关联到账户(迁移后非空)

  // 保留原字段
  month        String
  type         String
  // ...
}

修改:Organization

model Organization {
  // 新增关联
  socialAccounts   SocialAccount[]
}

三、数据迁移方案

迁移步骤

  1. 创建新表SocialAccount + SocialYearStandard
  2. 给员工记录表加 accountId 字段(可空,迁移期间兼容)
  3. 迁移 SocialInsuranceConfig → SocialAccount + SocialYearStandard
    • (orgId, city) 分组,每组创建一个 SocialAccounttype=SOCIAL
    • 每条 Config 创建一条 SocialYearStandardaccountId 关联到对应账户)
  4. 迁移 HousingFundConfig → SocialAccount + SocialYearStandard
    • (orgId, city, accountType) 分组,每组创建一个 SocialAccounttype=HOUSING
    • 每条 Config 创建一条 SocialYearStandard
  5. 迁移 EmployeeSocialInsRecord.accountId:按 (orgId, city) 匹配到 SocialAccount
  6. 迁移 EmployeeHousingFundRecord.accountId:按 (orgId, city) 匹配到 SocialAccount
  7. 迁移 SocialMonthlyProcess.accountId:按 (orgId, type, snapshot.city) 匹配
  8. 验证数据完整性:确认所有记录都有 accountId
  9. 将 accountId 设为非空(可选,或保持可空兼容)
  10. 保留旧表SocialInsuranceConfig / HousingFundConfig)作为备份,代码切换后再删除

迁移脚本

-- 1. 创建社保账户(从 SocialInsuranceConfig 提取唯一 orgId+city
INSERT INTO "SocialAccount" (id, orgId, type, name, city, "isDefault", status, "createdBy", "createdAt", "updatedAt")
SELECT
  gen_random_uuid(),
  "orgId",
  'SOCIAL',
  city || '社保账户',
  city,
  true,
  'ACTIVE',
  'migration',
  NOW(),
  NOW()
FROM (SELECT DISTINCT "orgId", city FROM "SocialInsuranceConfig") t;

-- 2. 创建公积金账户(从 HousingFundConfig 提取唯一 orgId+city+accountType
INSERT INTO "SocialAccount" (id, orgId, type, name, city, "accountType", "isDefault", status, "createdBy", "createdAt", "updatedAt")
SELECT
  gen_random_uuid(),
  "orgId",
  'HOUSING',
  city || COALESCE("accountType", 'BASIC') || '公积金账户',
  city,
  COALESCE("accountType", 'BASIC'),
  true,
  'ACTIVE',
  'migration',
  NOW(),
  NOW()
FROM (SELECT DISTINCT "orgId", city, "accountType" FROM "HousingFundConfig") t;

-- 3. 迁移社保年度标准
INSERT INTO "SocialYearStandard" (id, orgId, "accountId", "pensionOrg", "pensionEmp", ...)
SELECT
  gen_random_uuid(),
  c."orgId",
  a.id,
  c."pensionOrg", c."pensionEmp", ...
FROM "SocialInsuranceConfig" c
JOIN "SocialAccount" a ON a."orgId" = c."orgId" AND a.city = c.city AND a.type = 'SOCIAL';

-- 4. 迁移公积金年度标准
-- 5. 迁移员工参保记录 accountId
-- 6. 迁移月度办理记录 accountId

四、后端 API 变更

新增:账户管理 API

方法 路径 说明
GET /social/accounts 账户列表(支持 type 筛选)
POST /social/accounts 新建账户
PUT /social/accounts/:id 编辑账户
DELETE /social/accounts/:id 删除账户(无关联记录时可删)
PUT /social/accounts/:id/default 设为默认账户

改造:年度标准 API(原版本 API)

方法 路径 说明 变更
GET /social/config 获取当前标准 改为按 accountId 查询
GET /social/config/versions 版本列表 改为按 accountId 查询
POST /social/config/versions 新建版本 改为按 accountId 创建
GET /social/config/by-month/:month 按月查标准 改为按 accountId+month 查询

废弃:城市相关 API

方法 路径 说明 处理
GET /social/config/cities 城市列表 废弃,改为 /social/accounts 返回账户列表(含 city

改造:月度办理 API

  • 月度办理按账户分别办理,不同账户不同城市的增减员
  • SocialMonthlyProcess 新增 accountId

改造:薪资计算

  • payroll.service.ts 中社保配置查询:从 orgId + city 改为 accountId
  • 员工参保记录查询:从 employeeId + city 改为 employeeId + accountId

五、前端 UI 变更

1. 设置页新增"社保公积金账户管理"

  • 账户列表(按 type 分社保/公积金 Tab)
  • 每个账户卡片:名称、城市、账户编号、缴费主体、关联范围(公司/分公司/子公司)、是否默认
  • 新增/编辑/停用账户弹窗
  • 关联级别:公司(Organization)或根部门(Department level=0,代表分公司/子公司),不向下到普通部门
  • 员工通过所属根部门自动继承账户,未关联根部门的员工使用公司默认账户

2. SocialInsurance.tsx 改造

社保/公积金 Tab

  • 原城市选择器 → 改为账户选择器(下拉,显示"账户名称 - 城市"
  • 选中账户后展示该账户下的年度标准(当前版本 + 历史版本)
  • 年度标准展示比例和基数(与现在版本展示一致)

月度办理 Tab

  • 按账户分别办理
  • 选择账户 → 获取该账户的增减员名单 → 确认办理

员工参保 Tab

  • 员工参保记录展示关联的账户名称
  • 新增参保时选择账户(而非仅选城市)

3. 员工表单(roster/modals.tsx

  • 社保公积金区:原城市选择 → 改为账户选择(下拉,按 type 筛选)
  • 选中账户后自动带出城市(只读展示)

六、影响范围清单

后端

文件 改动范围
prisma/schema.prisma 新增 SocialAccount、SocialYearStandard,修改员工记录表加 accountId
src/routes/social.routes.ts 全面重构:账户 CRUD + 年度标准改为按 accountId
src/services/payroll.service.ts 社保配置查询改为 account → yearStandard 两级
src/routes/payroll2.routes.ts 批次计算中社保配置查询适配
src/routes/payroll.routes.ts 旧版薪资计算适配
src/routes/roster.routes.ts socialInsuranceStatus 派生逻辑适配
src/services/contract.service.ts createEmployee 社保记录创建时关联 accountId
src/routes/import.routes.ts Excel 导入时社保账户关联

前端

文件 改动范围
src/pages/SocialInsurance.tsx 全面重构:账户选择器 + 年度标准 + 月度办理
src/pages/Settings.tsx 新增账户管理入口
src/pages/roster/modals.tsx 社保公积金区改为账户选择
src/lib/api-services.ts 新增 socialAccountApi,改造 socialInsuranceApi

数据库迁移

操作 说明
新增表 SocialAccount、SocialYearStandard
修改表 EmployeeSocialInsRecord 加 accountIdEmployeeHousingFundRecord 加 accountIdSocialMonthlyProcess 加 accountId
数据迁移 SocialInsuranceConfig → SocialAccount + SocialYearStandard
数据迁移 HousingFundConfig → SocialAccount + SocialYearStandard
数据迁移 员工参保记录按 city 匹配 accountId
保留旧表 SocialInsuranceConfig / HousingFundConfig 暂保留,代码切换后删除

七、实施计划

第一步:DB schema + 迁移脚本

  • 新增 SocialAccount、SocialYearStandard 表
  • 员工记录表加 accountId 字段(可空)
  • 编写并执行迁移脚本
  • 验证数据完整性

第二步:后端 API 重构

  • 新增账户 CRUD API
  • 年度标准 API 改为按 accountId
  • 薪资计算适配
  • 员工参保记录适配
  • 保留旧 API 兼容(过渡期)

第三步:前端账户管理

  • 设置页新增账户管理 UI
  • SocialInsurance.tsx 改为账户选择器 + 年度标准

第四步:前端参保流程适配

  • 员工表单社保公积金区改为账户选择
  • 月度办理按账户分别办理
  • 员工参保记录展示账户名称

第五步:清理

  • 删除旧 SocialInsuranceConfig / HousingFundConfig 表
  • 删除旧 API
  • 删除前端城市选择器残留代码

八、风险与回滚

风险 应对
迁移脚本数据丢失 迁移前备份数据库,旧表保留不删
薪资计算引用旧表 过渡期双写,确认新表数据正确后再切换
前端缓存旧城市选择器 强制刷新 + 版本号
员工参保记录无 accountId 迁移脚本按 city 匹配,未匹配的标记待处理

回滚方案:保留旧表和旧 API,前端可切回旧版本,后端旧 API 仍可用。