Files
TurboHR/20260816-社保优化.md
T
selfrelease 54d138cc73 ux: 新建账户按钮移到社保/公积金Tab内部,按类型显示
按钮从页面顶部移到Tab内容区顶部,文案改为"新建社保账户"/"新建公积金账户",
明确表示新建的是当前Tab对应类型的账户。

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

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

17 KiB
Raw Blame History

20260816 社保公积金账户化重构

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

状态:已完成实施2026-08-16) 账户+年度标准模型已上线,前端已重构为账户卡片列表+展开年度标准管理。


一、现状分析

当前数据模型(已重构)

说明 唯一约束
SocialAccount 社保/公积金账户实体(type=SOCIAL/HOUSING @@unique([orgId, type, name])
SocialYearStandard 年度标准(比例+基数+最低工资+生效月份),关联到账户 @@unique([accountId, effectiveFrom])
SocialInsuranceConfig 旧社保版本表(保留兼容,回退用) @@unique([orgId, city, effectiveFrom])
HousingFundConfig 旧公积金版本表(保留兼容)
EmployeeSocialInsRecord 员工社保参保记录,已加 accountId
EmployeeHousingFundRecord 员工公积金参保记录,已加 accountId
SocialMonthlyProcess 月度办理记录 @@unique([orgId, month, type])

已解决的问题

  1. "城市"只是字符串 → 已改为 SocialAccount 实体化管理
  2. 多子公司/分公司场景缺失 → 同一城市可有多个账户,按根部门关联
  3. 版本直接挂在 orgId+city 上 → 改为 SocialYearStandard 关联到 accountId
  4. 员工参保记录只有 city → 已加 accountId 字段
  5. 薪资计算中社保配置查询 → 改为按 accountId 查 SocialYearStandard,旧表回退
  6. 最低工资标准 → SocialYearStandard 新增 minWage 字段,支持最低工资保护和递延扣款

二、目标数据模型

新增: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[]

  // 部门关联(根部门 level=0 代表分公司/子公司)
  deptSocialAccounts   Department[] @relation("SocialAccountDepartments")
  deptHousingAccounts  Department[] @relation("HousingAccountDepartments")

  @@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?

  // 最低工资标准(仅社保,20260816新增)
  minWage         Float    @default(0) // 当地月最低工资标准,0=不检查

  // 公积金比例(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 设为默认账户
PUT /social/accounts/:id/departments 账户关联根部门(批量)
GET /social/accounts/:id/departments 获取账户已关联的根部门

新增:年度标准 API(已实施)

方法 路径 说明
GET /social/accounts/:accountId/standards 账户的年度标准列表
GET /social/accounts/:accountId/current-standard 账户的当前生效标准
GET /social/accounts/:accountId/standard-by-month/:month 按账户+月份获取适用标准
POST /social/accounts/:accountId/standards 新建年度标准(旧版本自动归档)
PUT /social/accounts/:accountId/min-wage 快速更新当前标准的最低工资

改造:薪资计算(已实施)

  • payroll.service.ts 中社保配置查询:从 orgId + city 改为通过员工→根部门→账户→年度标准
  • 员工参保记录查询:从 employeeId + city 改为 employeeId + accountId
  • 新增最低工资保护和递延扣款逻辑(详见 20260816-薪税管理逻辑.md 第十五章)

废弃:城市相关 API

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

改造:月度办理 API

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

改造:薪资计算

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

五、前端 UI 变更(已实施)

1. 社保公积金菜单(SocialInsurance.tsx)— 已重构

社保/公积金 Tab20260816 重构):

  • 原城市选择器 → 改为账户选择器 → 已改为账户卡片列表 + 展开年度标准管理
  • 每个账户卡片:名称、城市、账号、关联部门数、参保记录数
  • 卡片可展开/折叠(ChevronDown/ChevronRight 图标)
  • 展开后显示:
    • 当前年度标准(比例/基数/最低工资)
    • 最低工资快速编辑(仅社保,Input + 保存按钮)
    • 操作按钮:新建年度标准 | 批量调基 | 查看历史
    • 新建年度标准弹窗(原"新建版本"改名,保留 AI 建议、附加险种配置)
    • 版本历史展示
    • 调基预览(保留原有展示和编辑逻辑)
    • 试算工具(使用该账户的城市配置)
  • 顶部"新建账户"按钮(账户新建/编辑/删除/设默认已从设置页面迁移到此)
  • 新增组件:AccountCard.tsx(账户卡片+展开内容)、AccountFormModal.tsx(账户新建/编辑弹窗)

月度办理 Tab:保留不变

员工参保 Tab:保留不变

专项附加扣除 Tab:保留不变

2. 设置页面(Settings.tsx)— 已简化

  • 新增"社保公积金账户管理"Tab → 已去掉该 Tab
  • 账户管理全部移到社保公积金菜单中完成
  • SocialAccountSettings 和 AccountFormModal 组件定义保留在文件中(不再显示)

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 字段(可空)
  • SocialYearStandard 新增 minWage 字段(最低工资标准)
  • Department 表新增 socialAccountId / housingAccountId(根部门关联账户)
  • 编写并执行迁移脚本
  • 验证数据完整性

第二步:后端 API 重构 已完成

  • 新增账户 CRUD API/social/accounts
  • 年度标准 API 改为按 accountId/social/accounts/:accountId/standards
  • 新增按账户获取当前标准 API/social/accounts/:accountId/current-standard
  • 新增快速更新最低工资 API/social/accounts/:accountId/min-wage
  • 薪资计算适配(payroll.service.ts 按 accountId 查标准,旧表回退)
  • 员工参保记录适配
  • 保留旧 API 兼容(过渡期)

第三步:前端账户管理 已完成

  • 设置页新增账户管理 UI → 已移到社保公积金菜单
  • SocialInsurance.tsx 改为账户卡片列表 + 展开年度标准管理
  • 新建版本改名为"新建年度标准"
  • 最低工资快速编辑(当前配置区域可直接编辑)
  • 新增 AccountCard.tsx 组件(账户卡片+展开内容)
  • 新增 AccountFormModal.tsx 组件(账户新建/编辑弹窗)

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

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

第五步:清理 待执行

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

额外完成的功能(20260816

  • 最低工资保护与递延扣款机制(详见 20260816-薪税管理逻辑.md 第十五章)
  • 最低工资保护支持当月累计实发判断(多批次场景)
  • 预入职状态(PRE_ONBOARD),预入职员工不进入薪资批次
  • 薪资批次新增 payMonth(发薪年月)字段,支持提前发薪场景

八、风险与回滚

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

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