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]) |
当前问题
- "城市"只是字符串:无实体化管理,无法记录账户编号、开户行、缴费主体等
- 多子公司/分公司场景缺失:同一城市可能有多个社保账户(不同主体分别开户),当前按 city 唯一无法支持
- 版本直接挂在 orgId+city 上:缺少"账户"这一层抽象,无法区分同一城市不同主体的账户
- 员工参保记录只有 city:无法精确关联到具体哪个社保账户
- 薪资计算中社保配置查询:按 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(社保/公积金账户)
新增:SocialYearStandard(年度标准,替代原 Config 表)
修改:EmployeeSocialInsRecord
修改:EmployeeHousingFundRecord
修改:SocialMonthlyProcess
修改:Organization
三、数据迁移方案
迁移步骤
- 创建新表:
SocialAccount + SocialYearStandard
- 给员工记录表加 accountId 字段(可空,迁移期间兼容)
- 迁移 SocialInsuranceConfig → SocialAccount + SocialYearStandard:
- 按
(orgId, city) 分组,每组创建一个 SocialAccount(type=SOCIAL)
- 每条 Config 创建一条
SocialYearStandard(accountId 关联到对应账户)
- 迁移 HousingFundConfig → SocialAccount + SocialYearStandard:
- 按
(orgId, city, accountType) 分组,每组创建一个 SocialAccount(type=HOUSING)
- 每条 Config 创建一条
SocialYearStandard
- 迁移 EmployeeSocialInsRecord.accountId:按
(orgId, city) 匹配到 SocialAccount
- 迁移 EmployeeHousingFundRecord.accountId:按
(orgId, city) 匹配到 SocialAccount
- 迁移 SocialMonthlyProcess.accountId:按
(orgId, type, snapshot.city) 匹配
- 验证数据完整性:确认所有记录都有 accountId
- 将 accountId 设为非空(可选,或保持可空兼容)
- 保留旧表(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 加 accountId,EmployeeHousingFundRecord 加 accountId,SocialMonthlyProcess 加 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 仍可用。