From fe427ef13f7266bf71d85c219e16b3c644a0edd4 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Sun, 16 Aug 2026 14:08:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=96=B0=E5=A2=9E=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E5=8E=BB=E6=8E=89=E5=8F=82=E4=BF=9D=E5=9F=8E=E5=B8=82?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=EF=BC=8C=E5=9F=8E=E5=B8=82=E4=BB=8E?= =?UTF-8?q?=E7=A4=BE=E4=BF=9D=E8=B4=A6=E6=88=B7=E7=BB=A7=E6=89=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端:参保城市改为只读显示,从选中的社保账户自动继承 - 前端:去掉参保城市下拉选择器和 cities 查询 - 后端:clampSocialInsBase/clampHousingFundBase 改为优先按 accountId 查年度标准裁剪基数,回退旧配置表 - 后端:createEmployee 传入 socialAccountId/housingAccountId 给 clamp 函数 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- 20260816-账户关联部门.md | 119 +++++++++++++++++++++++ backend/src/services/contract.service.ts | 26 ++++- frontend/src/pages/roster/modals.tsx | 31 +----- 3 files changed, 143 insertions(+), 33 deletions(-) create mode 100644 20260816-账户关联部门.md diff --git a/20260816-账户关联部门.md b/20260816-账户关联部门.md new file mode 100644 index 0000000..5c86027 --- /dev/null +++ b/20260816-账户关联部门.md @@ -0,0 +1,119 @@ +# 社保公积金账户关联根部门 + 员工新增自动带出账户 + +> 生成时间:2026-08-16 +> 涉及模块:设置(账户管理)、花名册(新增员工)、组织架构(根部门关联) +> 核心文件:`backend/src/routes/social.routes.ts`、`backend/src/routes/department.routes.ts`、`backend/src/services/contract.service.ts`、`frontend/src/pages/Settings.tsx`、`frontend/src/pages/roster/modals.tsx`、`frontend/src/lib/api-services.ts` + +--- + +## 一、需求背景 + +1. 社保公积金账户创建后,需要关联组织架构的根节点(公司/分公司/子公司,即 level=0 的部门)。 +2. 员工新增时,选定部门后自动带出该部门所属根节点关联的社保公积金账户,可手工调整,确定后按账户当前生效的年度标准执行。 + +--- + +## 二、完成内容 + +### 1. 账户关联组织架构根节点 + +#### 前端:设置页 → 社保公积金账户 Tab + +- 新建/编辑账户弹窗新增"关联根部门"区域 +- 列出所有 level=0 根部门(公司/分公司/子公司),可多选勾选 +- 保存账户时同步关联到选中的根部门 +- 编辑时自动加载已关联的部门并回显勾选状态 + +#### 后端新增 API + +| 方法 | 路径 | 说明 | +|---|---|---| +| `PUT` | `/social/accounts/:id/departments` | 批量设置账户关联的根部门(先清除旧关联,再批量设置新关联,仅限 level=0) | +| `GET` | `/social/accounts/:id/departments` | 查询账户已关联的根部门列表 | +| `GET` | `/social/department-account/:departmentId` | 按部门带出适用账户 + 当前生效年度标准 | + +#### 部门 API + +- `PUT /departments/:id` 的 schema 新增 `socialAccountId`、`housingAccountId` 可选字段 +- 更新时写入 `department.socialAccountId` / `department.housingAccountId` + +#### 账户关联逻辑 + +- 账户类型为 `SOCIAL` 时,写入 `department.socialAccountId` +- 账户类型为 `HOUSING` 时,写入 `department.housingAccountId` +- 设置新关联前,先清除该账户的所有旧关联(`updateMany` 置 null) +- 仅允许关联 `level=0` 的根部门 + +--- + +### 2. 员工新增时自动带出账户 + +#### 前端:花名册 → 新增员工 + +- 表单 state 新增 `departmentId` 字段,部门选择改为按 `id` 取值 +- 选定部门后,自动调用 `GET /social/department-account/:departmentId` 查询适用账户 +- 社保账户、公积金账户下拉框自动选中继承的账户 +- 账户下方显示当前生效标准(基数范围、比例) +- 可手动调整账户选择(下拉切换到其他账户) +- 缴费基数默认与月工资一致,可手动修改 +- 提交时将 `socialAccountId` / `housingAccountId` 写入参保记录 + +#### 后端:按部门带出账户逻辑 + +`GET /social/department-account/:departmentId` 处理流程: + +1. 从当前部门向上查找,直到 `level=0` 的根部门 +2. 读取根部门的 `socialAccount` / `housingAccount` +3. 若根部门未关联账户,回退到公司默认账户(`isDefault=true`) +4. 查询账户当前生效的 `SocialYearStandard`(`isCurrent=true`) +5. 返回 `{ socialAccount, housingAccount, socialStandard, housingStandard }` + +#### 后端:参保记录写入 accountId + +`contract.service.ts` 中 `createEmployee`: + +- `EmployeeSocialInsRecord.create` 写入 `accountId: data.socialAccountId || null` +- `EmployeeHousingFundRecord.create` 写入 `accountId: data.housingAccountId || null` + +--- + +## 三、数据流 + +``` +设置页:创建账户 → 勾选根部门 → 保存 + ↓ +department.socialAccountId / housingAccountId + ↓ +新增员工:选择部门 → 向上找根部门 → 读取关联账户 + ↓ +自动带出账户 + 当前标准 → 可手动调整 + ↓ +提交 → EmployeeSocialInsRecord.accountId / EmployeeHousingFundRecord.accountId + ↓ +薪资计算:通过员工账户查年度标准 → 计算社保公积金 +``` + +--- + +## 四、使用流程 + +1. 在 **设置 → 社保公积金账户** 中创建账户,勾选关联的根部门 +2. 新增员工时选择部门 → 自动带出对应账户 → 确认基数 → 保存 +3. 薪资计算时按员工账户的年度标准执行 + +--- + +## 五、兼容性 + +- 旧 `SocialInsuranceConfig` / `HousingFundConfig` 表保留 +- 薪资计算优先用新账户年度标准,查不到回退旧表 +- 未关联账户的根部门,员工新增时回退到公司默认账户 +- RehireModal(重新入职)保持原有逻辑,不自动带出账户(员工已有参保记录) + +--- + +## 六、部署记录 + +- 提交:`014c94e` feat: 账户关联根部门 + 员工新增自动带出账户 +- 部署:2026-08-16 已部署到 https://on.hr8ai.top/ +- 数据库:schema 同步完成(Department 表 socialAccountId/housingAccountId 字段已存在) diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts index 085d48d..c4c29cb 100644 --- a/backend/src/services/contract.service.ts +++ b/backend/src/services/contract.service.ts @@ -14,7 +14,16 @@ function dateToMonth(date: Date): string { return `${y}-${m}` } -async function clampSocialInsBase(orgId: string, base: number, city?: string): Promise { +async function clampSocialInsBase(orgId: string, base: number, city?: string, accountId?: string): Promise { + // 优先按账户查年度标准 + if (accountId) { + const standard = await prisma.socialYearStandard.findFirst({ + where: { accountId, isCurrent: true }, + orderBy: { effectiveFrom: 'desc' }, + }) + if (standard) return Math.min(Math.max(base, standard.baseMin), standard.baseMax) + } + // 回退到旧配置表 const config = await prisma.socialInsuranceConfig.findFirst({ where: { orgId, ...(city ? { city } : {}) }, orderBy: { effectiveFrom: 'desc' }, @@ -23,7 +32,16 @@ async function clampSocialInsBase(orgId: string, base: number, city?: string): P return base } -async function clampHousingFundBase(orgId: string, base: number, city?: string): Promise { +async function clampHousingFundBase(orgId: string, base: number, city?: string, accountId?: string): Promise { + // 优先按账户查年度标准 + if (accountId) { + const standard = await prisma.socialYearStandard.findFirst({ + where: { accountId, isCurrent: true }, + orderBy: { effectiveFrom: 'desc' }, + }) + if (standard) return Math.min(Math.max(base, standard.baseMin), standard.baseMax) + } + // 回退到旧配置表 const config = await prisma.housingFundConfig.findFirst({ where: { orgId, ...(city ? { city } : {}) }, orderBy: { effectiveFrom: 'desc' }, @@ -350,8 +368,8 @@ export async function createEmployee(orgId: string, userId: string, data: any) { const isNoSocialContract = data.contract && ['LABOR', 'INTERNSHIP'].includes(data.contract.contractType) const rawSocialInsBase = isNoSocialContract ? 0 : (data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum) const rawHousingFundBase = isNoSocialContract ? 0 : (data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum) - const socialInsBase = isNoSocialContract ? 0 : await clampSocialInsBase(orgId, rawSocialInsBase, city) - const housingFundBase = isNoSocialContract ? 0 : await clampHousingFundBase(orgId, rawHousingFundBase, city) + const socialInsBase = isNoSocialContract ? 0 : await clampSocialInsBase(orgId, rawSocialInsBase, city, data.socialAccountId) + const housingFundBase = isNoSocialContract ? 0 : await clampHousingFundBase(orgId, rawHousingFundBase, city, data.housingAccountId) const socialInsStartMonth = isNoSocialContract ? '' : (data.socialInsStartMonth || hireMonth) const housingFundStartMonth = isNoSocialContract ? '' : (data.housingFundStartMonth || hireMonth) diff --git a/frontend/src/pages/roster/modals.tsx b/frontend/src/pages/roster/modals.tsx index b4f4be2..1d26681 100644 --- a/frontend/src/pages/roster/modals.tsx +++ b/frontend/src/pages/roster/modals.tsx @@ -670,13 +670,6 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: { error: any }) { const todayStr = new Date().toISOString().slice(0, 10) - const { data: cities = ['北京'] } = useQuery({ - queryKey: ['social-config-cities'], - queryFn: async () => { - const res = await socialInsuranceApi.cities() as any - return res?.length ? res : ['北京'] - }, - }) const { data: contractTypes = [] } = useQuery>({ queryKey: ['contract-types'], queryFn: async () => { @@ -951,6 +944,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: { idCardNumber: form.idCardNumber || undefined, phone: form.phone || undefined, education: form.education || undefined, + city: autoAccounts?.socialAccount?.city || form.city || '北京', socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined, socialInsStartMonth: form.socialInsStartMonth || undefined, housingFundBase: form.housingFundBase ? parseFloat(form.housingFundBase) : undefined, @@ -1040,28 +1034,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: { )}
-
+
{autoAccounts?.socialAccount?.city || form.city || '—'}
从社保账户自动继承
{/* 社保公积金 */}