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

423 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)` 分组,每组创建一个 `SocialAccount`type=SOCIAL
- 每条 Config 创建一条 `SocialYearStandard`accountId 关联到对应账户)
4. **迁移 HousingFundConfig → SocialAccount + SocialYearStandard**
-`(orgId, city, accountType)` 分组,每组创建一个 `SocialAccount`type=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)作为备份,代码切换后再删除
### 迁移脚本
```sql
-- 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)— 已重构
**社保/公积金 Tab**20260816 重构):
- ~~原城市选择器 → 改为账户选择器~~ → 已改为**账户卡片列表 + 展开年度标准管理**
- 每个账户卡片:名称、城市、账号、关联部门数、参保记录数
- 卡片可展开/折叠(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 仍可用。