# 20260816 优化清单

> 来源：用户测试反馈（编号 4-13，共 10 项）
> 结合 TurboHR 当期代码梳理，按模块归类，标注根因 / 修复方向 / 优先级 / 涉及文件。
> 优先级：P0 紧急（影响业务正确性/合规）｜P1 高（流程阻塞或合规风险）｜P2 中（体验/增强）｜P3 规划（新模块）
>
> **✅ 全部 10 项已于 20260816 完成实现，前后端 tsc 均通过。完成明细见"五、已完成优化"章节（编号 5-15）。**

---

## 一、花名册 · 添加雇员（问题 4-8）

### 问题 4：合同状态未签署时仍显示"正常"
- **现状**：`backend/src/services/contract.service.ts:74` 注释明确"有合同记录（FIXED/UNFIXED/LABOR/INTERNSHIP），即使 signDate 为 null 也按正常合同处理"，`getContractStatus` 在有合同记录时直接返回 `active`/`正常`，未区分"已签署"与"待签署"。
- **修复方向**：
  1. `getContractStatus` 增加"待签署"判定：有合同记录但 `signDate` 为空，或关联的电子签署记录状态为 `PENDING`/未 `COMPLETED` 时，返回 `status: 'pending_sign'`，`statusText: '待签署'`，`riskLevel: 'medium'`。
  2. 仅当 `signDate` 已填且（无电子签署或电子签署已 COMPLETED）时才返回 `active`/`正常`。
  3. 前端花名册列表/详情同步展示"待签署"标签。
- **优先级**：P1 高
- **涉及文件**：`backend/src/services/contract.service.ts`、`frontend/src/pages/Roster.tsx`、`frontend/src/pages/roster/ContractInfo.tsx`

### 问题 5：社保状态未办理时仍显示"正常"
- **现状**：花名册添加雇员表单（`roster/modals.tsx:719`）有 `socialInsBase`/`socialInsStartMonth`/`housingFundBase`/`housingFundStartMonth`，但无"社保状态"字段；花名册列表/详情也未展示社保办理状态，默认视为"正常"。
- **修复方向**：
  1. 新增社保状态字段（派生而非存储）：根据 `socialInsStartMonth` 是否填写、是否已做"办理完成"操作判定。
  2. 状态枚举：`待办`（未填写基数/起始月或未办理完成）、`正常`（已办理且在缴）、`停缴`（已设截止月）。
  3. 后端 `employee.service.ts` 在花名册列表返回 `socialInsStatus` 派生字段。
  4. 前端花名册列表/详情展示社保状态标签。
  5. 提供"办理完成"操作入口（可在社保公积金模块或花名册操作栏）。
- **优先级**：P1 高
- **涉及文件**：`backend/src/services/employee.service.ts`、`backend/src/services/contract.service.ts`（或新增 `social-insurance.service.ts` 派生方法）、`frontend/src/pages/Roster.tsx`、`frontend/src/pages/roster/PayslipSocialInfo.tsx`

### 问题 6：证件号码未验证有效性
- **现状**：`roster/modals.tsx:886` `canSubmit` 仅校验 `idCardNumber.length >= 18`，未做身份证校验位（第 18 位）、出生日期合法性、月份/日期范围等有效性验证；`handleIdCardChange`（755 行）只做性别推导和查重，未做格式校验。
- **修复方向**：
  1. 新增身份证有效性校验：校验位算法（前 17 位加权求和取模映射第 18 位）、出生日期合法、月份 01-12、日期合法。
  2. 校验失败时 `ageWarning` 返回 `BLOCK`，阻断保存。
  3. 后端 `employee.service.ts` 创建/更新员工时同步做校验（双保险）。
  4. 支持非身份证类型（护照/港澳台通行证）时跳过 18 位校验（已部分实现，需确认）。
- **优先级**：P1 高
- **涉及文件**：`frontend/src/pages/roster/modals.tsx`（`handleIdCardChange`、`canSubmit`）、`backend/src/services/employee.service.ts`、`backend/src/routes/employee.routes.ts`

### 问题 7：超龄人员仍可选择劳动合同
- **现状**：`roster/modals.tsx:784` 超龄仅 `WARN` 提示"已达法定退休年龄，建议确认是否按退休处理"，未阻断选择 `FIXED`/`UNFIXED` 劳动合同；超龄人员依法应签劳务协议/实习协议，不能签劳动合同。
- **修复方向**：
  1. 年龄 ≥ 法定退休年龄（男 60、女干部 55、女工人 50）时，合同类型下拉移除 `FIXED`/`UNFIXED`，仅保留 `LABOR`/`INTERNSHIP`/`UNSIGNED`，并提示"超龄人员不可签订劳动合同，请选择劳务协议"。
  2. 强制选 `LABOR` 时联动问题 8 的社保基数置 0 逻辑。
  3. 后端 `employee.service.ts`/`work-process.service.ts` 增加校验：超龄 + 劳动合同类型组合拒绝并返回 400。
  4. `femaleWorkerType` 字段用于判定女职工退休年龄（干部 55/工人 50），需在年龄计算时引用。
- **优先级**：P1 高
- **涉及文件**：`frontend/src/pages/roster/modals.tsx`（合同类型下拉、`ageWarning` 逻辑）、`backend/src/services/employee.service.ts`、`backend/src/services/work-process.service.ts`

### 问题 8：签署劳务协议员工社保基数未置 0 且仍可缴纳社保
- **现状**：`roster/modals.tsx:983` 社保缴费基数为独立输入框，选择 `LABOR` 合同类型时未联动置 0 或禁用；劳务协议人员依法不缴纳社保。
- **修复方向**：
  1. 合同类型选择 `LABOR`/`INTERNSHIP` 时，社保缴费基数、公积金缴费基数自动置 0 且字段禁用，社保开始年月清空。
  2. UI 提示"劳务协议/实习协议人员不缴纳社保公积金"。
  3. 后端 `employee.service.ts` 保存时校验：`contractType === LABOR` 且 `socialInsBase > 0` 时拒绝或强制置 0。
  4. 薪税批次生成时排除劳务协议人员，或其社保公积金项强制为 0。
- **优先级**：P1 高
- **涉及文件**：`frontend/src/pages/roster/modals.tsx`（社保公积金区联动）、`backend/src/services/employee.service.ts`、`backend/src/services/payroll.service.ts`

---

## 二、工作台（问题 9-10）

### 问题 9：待办事项无法点击跳转
- **现状**：`Dashboard.tsx:1148` 已有 `<Link to={todo.actionUrl}>`，但部分待办的 `actionUrl` 为空、指向不存在的路由，或后端 `dashboard.service.ts` 生成待办时未填充 `actionUrl`，导致点击无反应或跳转 404。
- **修复方向**：
  1. 排查后端 `dashboard.service.ts` 各类待办（CONTRACT/TERMINATION/ONBOARDING/RETIREMENT/MONTHLY/SALARY）的 `actionUrl` 生成逻辑，确保每条待办都有有效跳转目标。
  2. `actionUrl` 为空时前端降级为"查看详情"按钮，弹窗展示待办内容，而非死链。
  3. 校验 `actionUrl` 指向的路由真实存在（如 `/roster?employeeId=xxx`、`/termination?draftId=xxx`）。
  4. 跳转后自动定位到对应员工/草稿/合同。
- **优先级**：P1 高
- **涉及文件**：`backend/src/services/dashboard.service.ts`、`frontend/src/pages/Dashboard.tsx`（`TodoIcon`/待办列表渲染）

### 问题 10：风险提醒内容不合理（含反向引导 bug）
- **现状**：`Dashboard.tsx:202` 风险提醒按 `todo.type` 过滤（CONTRACT/TERMINATION/ONBOARDING/RETIREMENT），具体提醒文案、阈值、优先级、跳转目标生成逻辑在后端 `risk.service.ts`。存在两类问题：

  **问题 A · 反向引导（典型 bug）**：`risk.service.ts:309-337` 三期/医疗期/工伤员工的风险提醒，文案是"解聘受限/不得解除劳动合同"，但 `actionUrl` 却指向 `/termination?employee=xxx`（解聘向导页面）。等于提示"该员工不能解聘，点这里去解聘她"——完全反向引导，可能诱导 HR 误操作违法解除。

  ```ts
  // risk.service.ts:309-317（错误示例）
  if (emp.isPregnant) {
    risks.push({
      type: 'TERMINATION',
      title: `${emp.name}处于孕期/哺乳期，解聘受限`,
      description: '三期女职工不得依非过错理由解除劳动合同...',
      actionUrl: `/termination?employee=${encodeURIComponent(emp.name)}`,  // ← 反向引导
    })
  }
  // isInMedicalPeriod（319-328）、isWorkInjured（329-338）同样指向 /termination
  ```

  **问题 B · 提醒规则/阈值/去重不合理**：部分提醒内容重复、阈值不合理或与实际业务无关（如合同到期 30 天提前量偏短，同一合同到期可能生成多条提醒）。

- **修复方向**：
  1. **修正反向引导（问题 A，P0 紧急）**：
     - 三期/医疗期/工伤员工的"解聘受限"提醒，`actionUrl` 改为指向员工详情或特殊状态页（`/roster?employee=xxx` 或 `/special-status?type=PREGNANCY`），让 HR 确认员工状态，而非跳到解聘页面。
     - 这类提醒的本质是"信息提示 + 合规警示"，不是"可操作待办"。应在解聘向导 step 2 合规检查时硬阻断（Termination 已实现法定禁止情形阻断），工作台只做信息展示。
     - 可考虑将这类提醒的 `type` 从 `TERMINATION` 改为 `COMPLIANCE`（合规提示），与可操作的解聘待办区分。
  2. **梳理风险提醒规则清单（问题 B）**：合同到期（30/60/90 天分级）、未签合同（>30 天/>1 年）、试用期将满、退休预警、社保断缴、离职未办结等。
  3. **每条提醒明确**：触发条件、提醒文案模板、优先级、跳转目标、是否可忽略、是"可操作待办"还是"信息提示"。
  4. **去除重复提醒**（如同一合同到期生成多条），合并同类项。
  5. **调整阈值至合理区间**（如合同到期提醒从 30 天提前到 60 天，给 HR 反应时间）。
  6. **前端按优先级排序展示**，高风险置顶；信息提示类提醒不提供"去处理"按钮，仅提供"查看详情"。
- **优先级**：P0 紧急（问题 A 反向引导）/ P2 中（问题 B 规则优化）
- **涉及文件**：`backend/src/services/risk.service.ts`（`detectTerminationRisks` 309-342 行、`detectSpecialStatusRisks`）、`frontend/src/pages/Dashboard.tsx`（风险提醒 Tab 渲染、按 type 区分操作按钮）

---

## 三、薪税管理 · 发放批次（问题 11、13）

### 问题 11：获取工资时自动识别试用期并填充试用期工资
- **现状**：不需要新增"获取试用期工资"按钮。已有的"获取工资"逻辑（创建批次、添加人员时自动填充 `baseSalary`）应自动识别员工是否处于试用期，是则填 `probationSalary`，否则填 `monthlySalary`。

  **根因**：当前 3 处获取工资逻辑用了**错误的试用期判定**，判定的是"入职不到 1 年"而非"当前在试用期内"：

  ```ts
  // payroll2.routes.ts:355-359（创建批次 copy_last 模式）
  // payroll2.routes.ts:610-614（添加人员到批次）
  // payroll.routes.ts:380-388（旧版批量生成工资条）
  if (emp.contracts?.[0]?.probationSalary
      && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) {
    baseSalary = emp.contracts[0].probationSalary  // ← 只要入职不到1年就用试用期工资，转正后仍用
  } else if (emp.monthlySalary) { ... }
  ```

  问题：员工转正后（如试用期 3 个月，入职 6 个月），仍会被判定为试用期，`baseSalary` 错误填为 `probationSalary` 而非转正工资。

  **正确逻辑**（`payroll.service.ts:326-331`、`payroll.service.ts:600-603` 已实现）：
  ```ts
  const probationEnd = new Date(contract.startDate)
  probationEnd.setMonth(probationEnd.getMonth() + contract.probationMonths)
  if (probationEnd > new Date()) { ... }  // 试用期未结束
  ```

- **修复方向**：
  1. 抽取统一的试用期判定工具函数 `isInProbation(contract, referenceDate?)`：`contract.startDate + contract.probationMonths > referenceDate`（默认当前日期，批次场景按批次月份的月末判定）。
  2. 修正 3 处错误逻辑，统一调用 `isInProbation`：
     - `payroll2.routes.ts:355-359`（创建批次 copy_last 模式）
     - `payroll2.routes.ts:610-614`（添加人员到批次）
     - `payroll.routes.ts:380-388`（旧版批量生成工资条，如仍在用）
  3. 判定为试用期 → `baseSalary = contract.probationSalary`；已转正 → `baseSalary = employee.monthlySalary`。
  4. 批次场景按批次月份判定（如批次月份是 2026-08，则判定 2026-08-31 时员工是否仍在试用期），避免跨月转正时填错。
  5. `probationSalary` 为 0 或未填时，即使试用期也回退用 `monthlySalary`（兼容旧数据）。
- **优先级**：P1 高（影响工资发放正确性）
- **涉及文件**：`backend/src/routes/payroll2.routes.ts`（355-359、610-614）、`backend/src/routes/payroll.routes.ts`（380-388）、`backend/src/services/payroll.service.ts`（抽取 `isInProbation` 工具函数，或放 `contract.service.ts`）

### 问题 13：编辑薪资新增"获取提成奖金"功能
- **现状**：`money/BatchTab.tsx` 已有 `BONUS` 批次类型（`isBonus` 分支，617 行），但普通薪资批次编辑薪资时无"获取提成奖金"按钮，无法从提成奖金数据按月拉取填充 `bonus` 字段。
- **修复方向**：
  1. 编辑薪资步骤新增"获取提成奖金"按钮，调用后端接口按批次月份从提成奖金表拉取每位员工的提成奖金。
  2. 拉取的奖金填入 `entry.bonus` 字段，支持正负值（奖金/扣款）。
  3. 后端新增接口 `POST /payroll2/batches/:id/fetch-bonus`，按批次月份查询提成奖金表并返回。
  4. 前端展示获取结果摘要（X 人提成奖金已填充，合计 ¥Y），支持覆盖确认。
  5. 依赖问题 12 的提成奖金数据表先建立。
- **优先级**：P2 中（依赖问题 12）
- **涉及文件**：`frontend/src/pages/money/BatchTab.tsx`（编辑薪资步骤按钮区）、`backend/src/routes/payroll.routes.ts`、`backend/src/services/payroll.service.ts`、`backend/src/services/commission-bonus.service.ts`（新增）

---

## 四、提成奖金 · 新模块（问题 12）

### 问题 12：团队页面下新增提成奖金页面
- **现状**：`SidebarNav.tsx:44` "团队"分组下仅有花名册，无提成奖金入口；`money/BatchTab.tsx` 的 `BONUS` 批次是年终奖发放，非按月提成奖金管理。需新建独立提成奖金模块。
- **修复方向**：
  1. **数据模型**：新增 `CommissionBonus` 表（`orgId`、`employeeId`、`month` YYYY-MM、`amount` 支持正负值、`remark`、`createdAt`、`createdBy`），唯一索引 `(orgId, employeeId, month)`。
  2. **后端**：新增 `commission-bonus.service.ts` + `commission-bonus.routes.ts`：
     - `GET /commission-bonus?month=YYYY-MM` 按月列表
     - `POST /commission-bonus/batch` 按月批量导入（JSON 数组或 Excel）
     - `PUT /commission-bonus/:id` 编辑单条
     - `DELETE /commission-bonus/:id` 删除单条
     - `GET /commission-bonus/template` 下载导入模板
  3. **前端**：新增 `frontend/src/pages/CommissionBonus.tsx` 页面：
     - 月份选择器（默认当月）
     - 列表展示该月所有员工提成奖金（员工/部门/金额/备注）
     - 支持单条编辑、删除
     - "批量导入"按钮（上传 Excel，字段：员工姓名/证件号码/月份/金额/备注）
     - 金额支持正负值（正为奖金，负为扣款）
     - 合计展示（总奖金/总扣款/净额）
  4. **菜单**：`SidebarNav.tsx` "团队"分组新增"提成奖金"入口，路由 `/commission-bonus`。
  5. **权限**：新增权限码 `PERM_COMMISSION_BONUS_VIEW`/`PERM_COMMISSION_BONUS_EDIT`/`PERM_COMMISSION_BONUS_IMPORT`。
- **优先级**：P3 规划（新模块）
- **涉及文件**：`backend/prisma/schema.prisma`（新增 `CommissionBonus` 模型 + migration）、`backend/src/services/commission-bonus.service.ts`（新增）、`backend/src/routes/commission-bonus.routes.ts`（新增）、`frontend/src/pages/CommissionBonus.tsx`（新增）、`frontend/src/components/layout/SidebarNav.tsx`、`frontend/src/App.tsx`（路由注册）、`frontend/src/lib/api-services.ts`（`commissionBonusApi`）

---

## 优先级汇总

| 优先级 | 问题 | 说明 | 状态 |
|--------|------|------|------|
| **P0 紧急** | 10A | 风险提醒反向引导（三期/医疗期/工伤提醒跳转到解聘页面） | ✅ 已完成 |
| **P1 高** | 4 | 合同状态增加待签署判定 | ✅ 已完成 |
| **P1 高** | 5 | 社保状态派生字段（待办/正常/停缴） | ✅ 已完成 |
| **P1 高** | 6 | 证件号码有效性校验（校验位+出生日期） | ✅ 已完成 |
| **P1 高** | 7 | 超龄人员合同类型限制（禁止选劳动合同） | ✅ 已完成 |
| **P1 高** | 8 | 劳务协议社保联动（选 LABOR 社保基数置 0 禁用） | ✅ 已完成 |
| **P1 高** | 9 | 待办跳转 actionUrl 修正 + 空值降级 | ✅ 已完成 |
| **P1 高** | 11 | 试用期工资判定修正（抽取 isInProbation 工具函数） | ✅ 已完成 |
| **P2 中** | 10B | 风险提醒规则优化（到期 30→60 天分级 + 去重排序） | ✅ 已完成 |
| **P2 中** | 13 | 薪资批次获取提成奖金（依赖 12） | ✅ 已完成 |
| **P3 规划** | 12 | 提成奖金新模块（DB migration + 全栈实现） | ✅ 已完成 |

---

## 建议执行顺序

> ✅ 全部 10 项已于 20260816 完成实现，前后端 tsc 均通过。

1. **第一批（P0 + P1，立即）** ✅：
   - 问题 10A（风险提醒反向引导，改 actionUrl）← 最先，改动小风险高
   - 问题 11（试用期工资判定修正）← 纯后端逻辑修正，抽取 `isInProbation` 工具函数
   - 问题 6（证件号码有效性校验）→ 问题 7（超龄人员合同类型限制）→ 问题 8（劳务协议社保联动）→ 问题 4（合同状态待签署）→ 问题 5（社保状态待办）→ 问题 9（待办跳转）
   - 问题 6/7/8 均在 `roster/modals.tsx` 添加雇员表单，可一并修改；问题 4/5 在后端 `contract.service.ts`/`employee.service.ts` 派生字段，前后端协同。
2. **第二批（P2，本迭代）** ✅：
   - 问题 10B（风险提醒规则梳理）→ 问题 13（获取提成奖金，依赖问题 12）
3. **第三批（P3，规划）** ✅：
   - 问题 12（提成奖金新模块，含 DB migration）→ 问题 13（获取提成奖金，依赖 12 完成）

---

## 备注

- 本清单基于当期代码静态分析，部分"根因"为推断，实际修复前需运行复现确认。
- 问题 12 涉及数据库 schema 变更（新增 `CommissionBonus` 表），需走 migration + 回滚方案。
- 问题 4/5 的"待签署""待办"状态为派生字段，不新增存储字段，避免数据冗余；如需记录"办理完成"操作时间，可新增 `socialInsConfirmedAt` 等时间戳字段。
- 问题 7 的超龄判定需引用 `femaleWorkerType`（干部 55/工人 50），需确认该字段在添加雇员表单已正确填写。
- 问题 13 依赖问题 12 的提成奖金数据表，不可先行实现。

---

## 五、已完成优化（20260816 补充）

> 以下为本次会话中已完成并部署的优化项。

### ✅ 已完成 1：员工端密码登录完整支持

- **默认密码**：创建员工时自动设置，密码 = 手机号后6位（无手机号则 `123456`）
- **管理员重置密码**：花名册详情 → "重置密码"按钮，重置为手机号后6位
- **员工修改密码**：员工端导航栏 → "修改密码"，需验证旧密码，新密码至少6位
- **Excel 批量导入**：导入时自动设置默认密码（手机号后6位），与新增员工一致
- **现有员工补初始化**：已通过脚本批量补设 512 名无密码员工（478 人手机号后6位，34 人无手机号用 123456）
- **涉及文件**：
  - `backend/src/services/contract.service.ts`（createEmployee 添加 passwordHash）
  - `backend/src/routes/import.routes.ts`（Excel 导入添加 passwordHash）
  - `backend/src/routes/employee.routes.ts`（新增 `POST /employees/:id/reset-password`）
  - `backend/src/routes/portal.routes.ts`（新增 `POST /portal/change-password`）
  - `frontend/src/pages/roster/BasicInfo.tsx`（重置密码按钮 + 默认密码提示）
  - `frontend/src/pages/portal/PortalNav.tsx`（修改密码弹窗）
  - `frontend/src/lib/api-services.ts`（resetPassword / changePassword 接口）

### ✅ 已完成 2：员工端 portalAxios response interceptor 修复

- **问题**：`portalAxios` 缺少 response interceptor，导致 `unwrap` 取到的是 `{ success, data: {...} }` 而非 `{ token, employee }`，密码登录/验证码登录/扫码自动登录全部失败
- **修复**：给 `portalAxios` 添加与管理端 `api` 实例一致的 response interceptor `(response) => response.data`
- **涉及文件**：`frontend/src/lib/api-services.ts`

### ✅ 已完成 3：同企业内员工身份证和手机号唯一性完整校验

- **createEmployee**：新增手机号查重硬校验（原有身份证查重）
- **updateEmployee**：新增手机号 + 身份证查重（排除自身）
- **Excel 批量导入**：新增身份证 + 手机号查重（重复则跳过并记录错误日志）
- **portal 登录安全**：密码登录改为 `findMany` 遍历校验，避免跨组织手机号重复时登录到错误员工
- **涉及文件**：
  - `backend/src/services/contract.service.ts`（createEmployee / updateEmployee 查重）
  - `backend/src/routes/import.routes.ts`（导入查重）
  - `backend/src/routes/portal.routes.ts`（login / send-code / verify-code 跨组织安全）

### ✅ 已完成 4：电子签署改为「待签合同」，按员工聚合 + 催办功能

- **菜单改名**：「电子签署」→「待签合同」
- **页面重构为两个 Tab**：
  - **待签合同**：按员工聚合，每行一个员工，显示待签数，点击展开查看该员工名下所有待签文件（EsignRecord PENDING/SIGNING + LaborContract signDate 为空）
  - **签署记录**：原有全部签署记录列表（保留状态/场景筛选）
- **催办功能**：点击催办 → 生成 24 小时有效的一次性自动登录链接（指向员工端签署页）→ 弹窗展示二维码 + 可复制链接 → HR 发给员工扫码直接进入签署
- **后端新增接口**：
  - `GET /esign/pending` — 待签合同按员工聚合列表
  - `POST /esign/remind` — 催办生成自动登录链接
- **涉及文件**：
  - `backend/src/routes/esign.routes.ts`（pending + remind 接口）
  - `frontend/src/components/layout/SidebarNav.tsx`（菜单改名）
  - `frontend/src/pages/ESign.tsx`（Tab 切换 + PendingTab 组件 + 催办弹窗）
  - `frontend/src/lib/api-services.ts`（pending / remind 接口调用）

### ✅ 已完成 5：风险提醒反向引导修正（问题 10A）

- **问题**：三期/医疗期/工伤员工的风险提醒文案是"解聘受限"，但 `actionUrl` 却指向 `/termination`（解聘向导），等于提示"不能解聘，点这里去解聘她"——反向引导，可能诱导违法解除。
- **修复**：3 处 `actionUrl` 从 `/termination?employee=xxx` 改为 `/special-status?employee=xxx&type=PREGNANCY|MEDICAL_PERIOD|WORK_INJURY`，让 HR 确认员工状态而非跳去解聘。文案补充"此为合规提示，请勿发起解聘"。
- **涉及文件**：`backend/src/services/risk.service.ts`（`detectTerminationRisks` 309-342 行）

### ✅ 已完成 6：试用期工资判定修正（问题 11）

- **问题**：3 处获取工资逻辑用错误的试用期判定 `startDate > now - 365d`（入职不到1年），转正后仍用 `probationSalary`。正确逻辑应是 `startDate + probationMonths > now`（试用期未结束）。
- **修复**：
  1. 抽取统一工具函数 `isInProbation(contract, referenceDate?)` 到 `contract.service.ts`。
  2. 修正 3 处错误逻辑，统一调用 `isInProbation`，按批次月份月末判定。
  3. `probationSalary` 为 0 时回退用 `monthlySalary`（兼容旧数据）。
- **涉及文件**：
  - `backend/src/services/contract.service.ts`（新增 `isInProbation`）
  - `backend/src/routes/payroll2.routes.ts`（copy_last 模式 355 行、添加人员 614 行）
  - `backend/src/routes/payroll.routes.ts`（旧版批量生成 380 行）

### ✅ 已完成 7：证件号码有效性校验（问题 6）

- **问题**：添加雇员仅校验证件号码长度 ≥18，未做校验位、出生日期合法性验证。
- **修复**：
  1. 前端 `handleIdCardChange` 增加校验位算法（GB 11643-1999）、出生日期合法性（月份/日期范围、是否存在如2月30日、是否晚于今天），校验失败阻断保存。
  2. 后端新增 `validateIdCard`/`getAgeFromIdCard`/`isOverageEmployee` 工具函数，`createEmployee` 增加双保险校验。
- **涉及文件**：
  - `frontend/src/pages/roster/modals.tsx`（`handleIdCardChange`、`canSubmit`）
  - `backend/src/services/contract.service.ts`（新增 3 个工具函数 + `createEmployee` 校验）

### ✅ 已完成 8：超龄人员合同类型限制（问题 7）

- **问题**：超龄人员仅 WARN 提示，仍可选劳动合同（FIXED/UNFIXED），违反合规要求。
- **修复**：
  1. 前端派生 `isOverage`（男≥60、女干部≥55、女工人≥50），合同类型下拉过滤掉 FIXED/UNFIXED，仅保留 LABOR/INTERNSHIP/UNSIGNED。
  2. 草稿恢复时若当前合同类型非法，useEffect 自动切到 UNSIGNED。
  3. 后端 `createEmployee` 增加校验：超龄 + FIXED/UNFIXED 拒绝并返回 400。
- **涉及文件**：
  - `frontend/src/pages/roster/modals.tsx`（`isOverage` 派生、合同类型下拉过滤、useEffect 自动修正）
  - `backend/src/services/contract.service.ts`（`createEmployee` 超龄校验）

### ✅ 已完成 9：劳务协议社保联动（问题 8）

- **问题**：选劳务协议（LABOR）时社保基数未置 0 且仍可缴纳，违反合规要求。
- **修复**：
  1. 前端选 LABOR/INTERNSHIP 时，社保/公积金基数自动置 0、起始月清空、字段禁用，提示"劳务协议/实习协议人员不缴纳社保公积金"。
  2. 后端 `createEmployee` 强制：LABOR/INTERNSHIP 时 `socialInsBase`/`housingFundBase` = 0，起始月清空。
- **涉及文件**：
  - `frontend/src/pages/roster/modals.tsx`（合同类型 onChange 联动、社保区 disabled）
  - `backend/src/services/contract.service.ts`（`createEmployee` 社保强制 0）

### ✅ 已完成 10：合同状态增加待签署判定（问题 4）

- **问题**：有合同记录但 `signDate` 为空时仍显示"正常"，应显示"待签署"。
- **修复**：`getContractStatus` 增加判定：有合同记录但 `signDate` 为空 → `status: 'pending_sign'`，`statusText: '待签署'`，`riskLevel: 'medium'`。电子签署完成后会回写 `signDate`，故 `signDate` 为空即未签署。前端花名册列表 `statusTextMap` 增加 `pending_sign: '待签署'`。
- **涉及文件**：
  - `backend/src/services/contract.service.ts`（`getContractStatus` 74-91 行重写）
  - `frontend/src/pages/Roster.tsx`（`tagStyles`/`statusTextMap` 增加 `pending_sign`）

### ✅ 已完成 11：社保状态派生字段（问题 5）

- **问题**：社保状态无"待办"判定，未办理社保的在职员工显示"—"而非"待办理"。
- **修复**：后端 `socialInsuranceStatus` 派生逻辑：
  - 劳务协议/实习协议 → null（不缴社保，显示"—"）
  - 在职 + 应缴社保 + 无社保记录 → `PENDING`（待办理）
  - 有社保记录 + endMonth 为 null → `ACTIVE`（在保）
  - 有社保记录 + endMonth 不为 null → `SUSPENDED`（停保）
  - 前端已有 PENDING/UNINSURED 的标签映射，无需改前端。
- **涉及文件**：`backend/src/routes/roster.routes.ts`（`socialInsuranceStatus` 派生 243-249 行）

### ✅ 已完成 12：待办跳转 actionUrl 空值降级（问题 9）

- **问题**：待办事项的"立刻办理"链接在 `actionUrl` 为空或 '/' 时仍渲染为 Link，点击跳首页无意义。
- **修复**：Dashboard 待办列表渲染时，`actionUrl` 为空或 '/' 时不渲染 Link 和"立刻办理"按钮，改为纯展示 div；有有效 URL 时才渲染可跳转链接。问题 10A 已修正三期/医疗期/工伤的反向引导 URL，其余 actionUrl（`/roster?employee=xxx`、`/special-status`、`/money`）均指向有效路由，Roster 已处理 `?employee` 参数自动定位。
- **涉及文件**：`frontend/src/pages/Dashboard.tsx`（待办列表渲染 1136-1233 行）

### ✅ 已完成 13：风险提醒规则优化（问题 10B）

- **问题**：合同到期提醒仅 30 天预警，HR 反应时间不足；去重后的待办列表未按优先级排序。
- **修复**：
  1. 合同到期预警从 30 天提前到 60 天，分级：≤30 天 HIGH（需立即处理），31-60 天 MEDIUM（提前预警）。
  2. `dedupedTodos` 按优先级排序（URGENT > HIGH > MEDIUM > LOW），确保最紧急的待办排在最前。
- **涉及文件**：`backend/src/services/risk.service.ts`（`detectContractRisks` 216-248 行、`getDashboardData` dedupedTodos 排序 941-952 行）

### ✅ 已完成 14：提成奖金新模块（问题 12）

- **问题**：无独立提成奖金管理模块，`BONUS` 批次是年终奖发放，非按月提成奖金管理。
- **实现**：
  1. **数据模型**：新增 `CommissionBonus` 表（`orgId`、`employeeId`、`month` YYYY-MM、`amount` 支持正负值、`remark`、`createdBy`），唯一索引 `(orgId, employeeId, month)`。
  2. **后端**：新增 `commission-bonus.service.ts` + `commission-bonus.routes.ts`：
     - `GET /commission-bonus?month=YYYY-MM` 按月列表 + 汇总（总奖金/总扣款/净额）
     - `POST /commission-bonus` 新增单条（upsert，同员工同月自动覆盖）
     - `PUT /commission-bonus/:id` 编辑单条
     - `DELETE /commission-bonus/:id` 删除单条
     - `POST /commission-bonus/import` 批量导入 Excel（字段：员工姓名/证件号码/金额/备注）
     - `GET /commission-bonus/template` 下载导入模板
  3. **前端**：新增 `CommissionBonus.tsx` 页面：
     - 月份选择器（默认当月）
     - 汇总卡片（记录数/总奖金/总扣款/净额）
     - 列表展示（员工/部门/金额/备注），支持搜索、分页
     - 单条新增/编辑/删除
     - 批量导入 Excel + 模板下载
     - 金额支持正负值（正=奖金，负=扣款）
  4. **菜单**：`SidebarNav.tsx` "团队"分组新增"提成奖金"入口，路由 `/commission-bonus`。
  5. **DB 同步**：`prisma db push` 已执行，Prisma Client 已重新生成。
- **涉及文件**：
  - `backend/prisma/schema.prisma`（新增 `CommissionBonus` 模型 + Employee/Organization 反向关联）
  - `backend/src/services/commission-bonus.service.ts`（新增）
  - `backend/src/routes/commission-bonus.routes.ts`（新增）
  - `backend/src/app.ts`（路由注册）
  - `frontend/src/pages/CommissionBonus.tsx`（新增）
  - `frontend/src/components/layout/SidebarNav.tsx`（菜单项）
  - `frontend/src/App.tsx`（路由注册）
  - `frontend/src/lib/api-services.ts`（`commissionBonusApi`）

### ✅ 已完成 15：获取提成奖金（问题 13）

- **问题**：薪资批次编辑薪资时无"获取提成奖金"按钮，无法从提成奖金模块按月拉取填充 `bonus` 字段。
- **实现**：
  1. 后端新增 `POST /payroll2/batches/:batchId/fetch-bonus`：按批次月份从 `CommissionBonus` 表拉取，填充到对应 `entries.bonus`，重算批次汇总。
  2. 前端 `BatchTab.tsx` 编辑薪资步骤新增"获取提成奖金"按钮（在"导入加班费"旁），调用后展示结果摘要（X 人提成奖金已填充，合计 ¥Y）。
  3. 已归档批次禁用按钮。
- **涉及文件**：
  - `backend/src/routes/payroll2.routes.ts`（`fetch-bonus` 接口 552-628 行）
  - `frontend/src/lib/api-services.ts`（`fetchBonusToBatch`）
  - `frontend/src/pages/money/BatchTab.tsx`（`fetchBonusMutation` + 按钮 784-798 行）

