54d138cc73
按钮从页面顶部移到Tab内容区顶部,文案改为"新建社保账户"/"新建公积金账户", 明确表示新建的是当前Tab对应类型的账户。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
17 KiB
17 KiB
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]) |
已解决的问题
- ✅ "城市"只是字符串 → 已改为 SocialAccount 实体化管理
- ✅ 多子公司/分公司场景缺失 → 同一城市可有多个账户,按根部门关联
- ✅ 版本直接挂在 orgId+city 上 → 改为 SocialYearStandard 关联到 accountId
- ✅ 员工参保记录只有 city → 已加 accountId 字段
- ✅ 薪资计算中社保配置查询 → 改为按 accountId 查 SocialYearStandard,旧表回退
- ✅ 最低工资标准 → 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-MM(null=当前有效)
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[]
}
三、数据迁移方案
迁移步骤
- 创建新表:
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 |
设为默认账户 |
| 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 加 accountId,EmployeeHousingFundRecord 加 accountId,SocialMonthlyProcess 加 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 仍可用。