# 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-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[] } ``` --- ## 三、数据迁移方案 ### 迁移步骤 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 加 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 仍可用。