# 薪税管理完整处理逻辑和取数逻辑

> 生成时间：2026-08-16
> 涉及模块：薪资发放批次、社保公积金、个税累计预扣、工资条生成与发布
> 核心文件：`backend/src/services/payroll.service.ts`、`backend/src/routes/payroll2.routes.ts`、`backend/src/routes/social.routes.ts`

---

## 一、整体架构

```
┌─────────────────────────────────────────────────────────────┐
│                    薪税管理三大模块                          │
├──────────────┬──────────────────┬───────────────────────────┤
│  社保公积金   │    个税计算       │    工资条生成/发布        │
│  (缴纳/扣除)  │  (累计预扣法)     │    (汇总/发布/定时)       │
└──────────────┴──────────────────┴───────────────────────────┘
         ↓ 取数 ↓                ↓ 取数 ↓
┌──────────────┬──────────────────┬───────────────────────────┐
│ SocialAccount│ Employee 表      │ PayrollBatch (已归档)     │
│ + YearStandard│ SpecialDeduction │ + BatchEntry             │
│ + 旧Config表  │ + Record表       │ → Payslip                │
└──────────────┴──────────────────┴───────────────────────────┘
```

---

## 二、核心计算函数 `calcBatchEntry`

这是所有薪税计算的入口，在三个时机被调用：

1. **创建批次时**：为每个员工初始化计算
2. **编辑条目时**：修改任意金额后实时重算
3. **归档时**：最终锁定前再重算一遍

### 输入参数

```typescript
calcBatchEntry(
  orgId,           // 组织ID
  employeeId,      // 员工ID
  month,           // 批次月份 YYYY-MM
  inputs: {        // HR 可编辑的输入项
    baseSalary,          // 基本工资
    overtimePay,         // 加班费
    allowance,           // 其他津贴
    deduction,           // 扣款
    bonus,               // 奖金
    positionSalary?,     // 岗位工资
    performanceSalary?,  // 绩效工资
    senioritySalary?,    // 工龄工资
    transportAllowance?, // 交通补贴
    mealAllowance?,      // 餐补
    housingAllowance?,   // 住房补贴
    communicationAllowance?, // 通讯补贴
    otherDeduction?,     // 其他扣款
  },
  batchType,       // REGULAR | TERMINATION | BONUS | SEVERANCE
  options?: {      // 可选覆盖
    skipSocial?,        // 跳过社保计算
    overrideSocial?,    // 手动覆盖社保值
    prevDeferred?,      // 上月递延扣款（次月补扣）{ socialEmp, housingEmp, minWage }
  }
)
```

---

## 三、社保公积金取数逻辑

### 取数优先级（三层回退）

```
① 员工 → 根部门(level=0) → socialAccount / housingAccount
   ↓ 找不到
② 公司默认账户 (isDefault=true)
   ↓ 找不到
③ 旧配置表 SocialInsuranceConfig / HousingFundConfig（按城市匹配）
```

### 新逻辑（账户体系）

```
getEmployeeAccounts(orgId, employeeId)
  ├─ 查员工所属部门
  ├─ 向上遍历到 level=0 的根部门
  ├─ 读取根部门的 socialAccountId / housingAccountId
  ├─ 若根部门未关联 → 查公司默认账户 (isDefault=true)
  └─ 返回 { socialAccount, housingAccount }

getStandardByAccountAndMonth(accountId, month)
  ├─ 查 SocialYearStandard: accountId + effectiveFrom ≤ month ≤ effectiveTo
  ├─ 找不到 → 回退到 isCurrent=true 的当前标准
  └─ 返回年度标准（比例 + 基数上下限）
```

### 旧逻辑（兼容回退）

```
SocialInsuranceConfig.findFirst({
  orgId, city: employee.city,
  effectiveFrom ≤ month, effectiveTo ≥ month 或 null
})
```

### 社保基数确定

```typescript
socialBase = employee.socialInsBase || inputs.baseSalary
housingBase = employee.housingFundBase || inputs.baseSalary
```

优先级：员工核定基数 > 基本工资

### 社保金额计算

```typescript
// 当月应缴全额
fullSocialEmp = calcSocialInsurance(socialBase, socialConfig).socialEmp
fullSocialOrg = calcSocialInsurance(socialBase, socialConfig).socialOrg
fullHousingEmp = calcHousingFund(housingBase, housingConfig).housingEmp
fullHousingOrg = calcHousingFund(housingBase, housingConfig).housingOrg

// 已归档批次已扣金额（避免多批次重复扣社保）
deductedSocialEmp = SUM(archivedEntries.socialEmp)
deductedSocialOrg = SUM(archivedEntries.socialOrg)
deductedHousingEmp = SUM(archivedEntries.housingEmp)
deductedHousingOrg = SUM(archivedEntries.housingOrg)

// 本批次应扣 = 应缴全额 - 已扣金额（差额补扣，足额为0）
socialEmp = Math.max(0, fullSocialEmp - deductedSocialEmp)
socialOrg = Math.max(0, fullSocialOrg - deductedSocialOrg)
housingEmp = Math.max(0, fullHousingEmp - deductedHousingEmp)
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)

// 叠加上月递延的社保/公积金（入职当月未扣完的部分，本月补扣）
if (options?.prevDeferred) {
  socialEmp += options.prevDeferred.socialEmp || 0
  housingEmp += options.prevDeferred.housingEmp || 0
}
```

### 关键设计：差额补扣机制

- 同一员工同一月份可能有多个批次（如常规发薪 + 离职结算）
- 第一个批次扣全额，后续批次扣差额（应缴全额 - 已扣）
- 避免重复扣除

### 批次类型对社保的影响

| 批次类型 | 社保公积金 | 说明 |
|---------|-----------|------|
| REGULAR | ✅ 计算 | 常规发薪，差额补扣 |
| TERMINATION | ✅ 计算 | 离职结算，差额补扣 |
| BONUS | ❌ 不扣 | 年终奖单独计税，无社保 |
| SEVERANCE | ❌ 不扣 | 补偿金不走社保个税 |

### 手动覆盖

```typescript
// HR 可在编辑界面手动修改社保值
if (options?.overrideSocial) {
  socialEmp = overrideSocial.socialEmp   // 覆盖个人社保
  socialOrg = overrideSocial.socialOrg   // 覆盖单位社保
  housingEmp = overrideSocial.housingEmp // 覆盖个人公积金
  housingOrg = overrideSocial.housingOrg // 覆盖单位公积金
}
// 同时保存 systemSocialEmp 等系统计算值，用于对比展示
```

---

## 四、应发合计计算

```typescript
totalPay =
  baseSalary              // 基本工资
  + positionSalary        // 岗位工资
  + performanceSalary     // 绩效工资
  + senioritySalary       // 工龄工资
  + overtimePay           // 加班费
  + transportAllowance    // 交通补贴
  + mealAllowance         // 餐补
  + housingAllowance      // 住房补贴
  + communicationAllowance // 通讯补贴
  + allowance             // 其他津贴
  + bonus                 // 奖金
  - deduction             // 扣款
  - otherDeduction        // 其他扣款
```

---

## 五、个税计算逻辑

### 两种计税方式

#### 1. 年终奖单独计税（BONUS 批次）

```typescript
tax = calcBonusTax(inputs.bonus)
// 年终奖 ÷ 12 → 找税率区间 → 年终奖 × 税率 - 速算扣除数
```

#### 2. 累计预扣法（REGULAR / TERMINATION / SEVERANCE）

```typescript
// ── 取数：当年已归档批次的历史数据 ──
archivedEntries = BatchEntry.findMany({
  orgId, employeeId,
  batch: { month: startsWith(year), status: 'ARCHIVED' }
})
// 注意：不依赖 Payslip 是否已生成，直接从已归档批次取数

// ── 累计计算 ──
ytdIncome         = SUM(archivedEntries.totalPay) + totalPay
ytdSocialEmp      = SUM(archivedEntries.socialEmp) + socialEmp
ytdHousingEmp     = SUM(archivedEntries.housingEmp) + housingEmp
ytdTaxDeducted    = SUM(archivedEntries.tax)

// ── 专项附加扣除取数（三层回退）──
deductionRecords = SpecialDeductionRecord.findMany({
  orgId, employeeId,
  month: startsWith(year) AND lte(month)   // 当年至当月
})

if (deductionRecords.length > 0) {
  // ✅ 优先：按月实际填报金额累加
  ytdSpecialDeduction = SUM(deductionRecords.amount)
} else {
  // ⚠️ 回退：员工便捷字段 × 月数（兼容旧数据）
  ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7))
}

// ── 累计应纳税所得额 ──
deductionAmount    = 5000 × 月数                    // 基本减除费用
ytdTaxableIncome   = max(0,
  ytdIncome
  - deductionAmount       // 累计减除费用（5000/月）
  - ytdSocialEmp          // 累计个人社保
  - ytdHousingEmp         // 累计个人公积金
  - ytdSpecialDeduction   // 累计专项附加扣除
)

// ── 当月应扣个税 ──
tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
// = 累计应纳税额 - 已预扣个税
```

### 专项附加扣除数据来源

| 来源 | 表 | 说明 |
|------|---|------|
| **按月记录**（优先） | `SpecialDeductionRecord` | HR/员工按月填报，含子女教育、赡养老人、住房贷款、继续教育、婴幼儿照护 |
| **便捷字段**（回退） | `Employee.specialDeduction` | 员工表上的当前值，× 月数估算累计 |

**按月记录的优势**：员工某月取消专项附加扣除，该月金额为 0，累计值准确反映实际。

### taxBreakdown 返回的明细

```json
{
  "method": "累计预扣法",
  "month": 8,
  "ytdIncome": 80000,           // 累计收入
  "deductionAmount": 40000,      // 累计减除费用 (5000×8)
  "ytdSocialEmp": 6720,          // 累计个人社保
  "ytdHousingEmp": 3840,         // 累计个人公积金
  "ytdSpecialDeduction": 12000,  // 累计专项附加扣除
  "specialDeductionSource": "按月记录",  // 数据来源标识
  "specialDeductionRecords": 8,  // 按月记录条数
  "ytdTaxableIncome": 17440,     // 累计应纳税所得额
  "ytdTaxDeducted": 480,         // 已预扣个税
  "currentMonthTax": 360,        // 当月应扣个税
  "archivedCount": 7             // 已归档批次数
}
```

---

## 六、实发工资与最低工资保护

```typescript
// 初始实发 = 应发 - 个人社保 - 个人公积金 - 个税 - 上月递延最低工资补扣
netPay = totalPay - socialEmp - housingEmp - tax - prevDeferredMinWage

// 最低工资保护（仅 REGULAR / TERMINATION 批次）
if (minWage > 0 && netPay < minWage) {
  // 优先递延社保 → 递延公积金 → 递延最低工资补齐
  // 详见第十五章「最低工资保护与递延扣款机制」
  netPay = minWage
}
```

---

## 七、创建批次时的初始化取数

### 员工来源

| 批次类型 | 员工来源 |
|---------|---------|
| REGULAR / BONUS | `status=ACTIVE` + 本月离职（`status=RESIGNED, updatedAt 在本月`） |
| TERMINATION | 本月 `TerminationRecord` 关联的员工 |
| SEVERANCE | 本月离职记录中已审批（APPROVED/EXECUTING/COMPLETED）且有补偿金的 |

### 数据初始化（5 种模式）

| 模式 | baseSalary 取数 | overtimePay | allowance/deduction |
|------|----------------|-------------|---------------------|
| **copy_last** | 试用期→`probationSalary`；转正→上月工资条`baseSalary`；无→`monthlySalary` | 当月 `OvertimeRecord.totalPay` | 上月工资条复制 |
| **blank_employees** | `employee.monthlySalary`（解密） | 0 | 0 |
| **blank_all** | 0（无员工） | 0 | 0 |
| **copy_batch** | 源批次条目复制 | 源批次复制 | 源批次复制 |
| **custom** | 0（手动填写） | 0 | 0 |

### 试用期判定

```typescript
isInProbation(contract, batchMonthEnd)
// 试用期结束日 = contract.startDate + contract.probationMonths
// 若 batchMonthEnd < 试用期结束日 → 在试用期内
```

统一规则（所有模式适用，SEVERANCE/TERMINATION 除外）：
- 在试用期内且 `probationSalary > 0` → `baseSalary = probationSalary`

---

## 八、编辑条目时的重算

```
HR 修改任意金额字段
  ↓
PUT /batches/:batchId/entries/:employeeId
  ↓
合并新旧 inputs（未修改的保留原值）
  ↓
重新调用 calcBatchEntry() → 重算社保/个税/实发
  ↓
更新 BatchEntry + 重算批次汇总
```

**社保手动覆盖**：编辑社保字段时，通过 `overrideSocial` 传入，覆盖系统计算值，同时保存 `systemSocial*` 用于对比。

---

## 九、归档时的最终重算

```
POST /batches/:batchId/archive
  ↓
① 遍历所有条目，重新调用 calcBatchEntry()
   - 此时已归档批次的累计数据是最新的
   - 社保差额补扣准确
   - 个税累计预扣准确
② 更新批次汇总（totalPay/totalNetPay/各项合计）
③ 标记 status=ARCHIVED, archivedAt=now
④ 批次锁定，不可再编辑
```

**归档时不自动生成工资条**，需单独调用 `POST /payslips/generate`。

---

## 十、工资条生成与发布

### 生成工资条

```
POST /payslips/generate { month }
  ↓
generatePayslipFromBatches(orgId, month)
  ↓
① 查当月所有已归档批次（status=ARCHIVED）
② 按员工汇总所有批次的 BatchEntry（多批次合并）
③ 查当年历史工资条计算累计数据
④ upsert Payslip（employeeId+month 唯一键）
```

### 发布工资条

```
POST /batches/:batchId/publish
  ↓
更新 Payslip.publishStatus = 'PUBLISHED', publishedAt = now
  ↓
员工端可见
```

### 定时发送

```
POST /batches/:batchId/schedule { scheduledAt }
  ↓
更新 Payslip.publishStatus = 'SCHEDULED', scheduledAt = 指定时间
  ↓
（定时任务到点后发布）
```

---

## 十一、取消归档

```
POST /batches/:batchId/unarchive
  ↓
① 只能取消最后一个归档批次
② 批次恢复为 DRAFT
③ 如果还有其他归档批次 → 重新生成工资条（基于剩余批次）
④ 如果没有归档批次了 → 删除该月工资条
```

---

## 十二、完整数据流图

```
                    ┌─────────────────┐
                    │  Employee 表     │
                    │  socialInsBase   │
                    │  housingFundBase │
                    │  monthlySalary   │
                    │  specialDeduction│
                    └────────┬────────┘
                             │
    ┌────────────────────────┼────────────────────┐
    │                        │                    │
    ▼                        ▼                    ▼
┌──────────┐    ┌──────────────────┐    ┌─────────────────┐
│SocialAccount│  │SpecialDeduction  │    │ OvertimeRecord  │
│+YearStandard│  │Record (按月)     │    │ (加班费)        │
│(社保比例)   │  └──────────────────┘    └─────────────────┘
│+minWage    │           │                    │
└──────┬─────┘           │                    │
       │                 │                    │
       ▼                 ▼                    ▼
┌──────────────────────────────────────────────────────┐
│              calcBatchEntry()                         │
│                                                       │
│  社保 = 应缴全额 - 已归档批次已扣（差额补扣）          │
│       + 上月递延社保（次月补扣）                       │
│  应发 = 基本工资 + 各项津贴 + 奖金 - 扣款             │
│  个税 = 累计预扣法（从已归档批次取累计数据）           │
│  实发 = 应发 - 个人社保 - 个人公积金 - 个税            │
│       - 上月递延最低工资补扣                           │
│  最低工资保护：实发 < minWage 时 → 递延扣款补齐        │
│       递延社保 → 递延公积金 → 递延最低工资补齐         │
└──────────────────────┬───────────────────────────────┘
                       │
                       ▼
              ┌────────────────┐
              │  BatchEntry    │
              │  (批次条目)     │
              │  + minWage     │
              │  + minWageApplied│
              │  + deferred*   │
              │  + prevDeferred*│
              └───────┬────────┘
                      │ 归档
                      ▼
              ┌────────────────┐      汇总生成
              │ PayrollBatch   │ ──────────────→ ┌──────────┐
              │ status=ARCHIVED│                 │ Payslip  │
              └────────────────┘                 │ (工资条) │
                                                 └────┬─────┘
                                                      │ 发布
                                                      ▼
                                               员工端可见

次月创建批次时：
  getPrevDeferred() 读取上月 BatchEntry 的 deferred* 字段
  → 传入 calcBatchEntry 的 options.prevDeferred
  → 本月社保/公积金叠加补扣上月递延金额
```

---

## 十三、关键取数表汇总

| 数据项 | 取数表 | 取数条件 | 用途 |
|--------|--------|---------|------|
| 社保比例 | `SocialYearStandard` | accountId + 月份在生效区间 | 计算社保公积金 |
| 社保比例（回退） | `SocialInsuranceConfig` | orgId + city + 月份在生效区间 | 兼容旧数据 |
| 最低工资标准 | `SocialYearStandard.minWage` | accountId + 月份在生效区间 | 最低工资保护 |
| 最低工资标准（回退） | `SocialInsuranceConfig.minWage` | orgId + city + 月份在生效区间 | 兼容旧数据 |
| 社保基数 | `Employee.socialInsBase` | — | 优先于基本工资 |
| 公积金基数 | `Employee.housingFundBase` | — | 优先于基本工资 |
| 已扣社保 | `BatchEntry` | 当月已归档批次 | 差额补扣 |
| 累计收入 | `BatchEntry` | 当年已归档批次 | 个税累计预扣 |
| 累计社保 | `BatchEntry` | 当年已归档批次 | 个税累计预扣 |
| 累计个税 | `BatchEntry` | 当年已归档批次 | 个税累计预扣 |
| 专项附加扣除 | `SpecialDeductionRecord` | 当年至当月 | 个税累计预扣 |
| 专项附加扣除（回退） | `Employee.specialDeduction` | × 月数 | 兼容旧数据 |
| 基本减除费用 | 固定 5000 | × 月数 | 个税累计预扣 |
| 加班费 | `OvertimeRecord` | employeeId + month | 创建批次时自动拉取 |
| 基本工资 | `Payslip`（上月） | employeeId + 上月 | copy_last 模式 |
| 基本工资（回退） | `Employee.monthlySalary` | 解密 | 无上月工资条时 |
| 试用期工资 | `LaborContract.probationSalary` | 最新合同 | 试用期内的 baseSalary |
| 上月递延社保 | `BatchEntry.deferredSocialEmp` | 上月已归档批次 | 次月补扣 |
| 上月递延公积金 | `BatchEntry.deferredHousingEmp` | 上月已归档批次 | 次月补扣 |
| 上月递延最低工资补齐 | `BatchEntry.deferredMinWage` | 上月已归档批次 | 次月补扣 |

---

## 十四、3 步工作流

```
① 编辑薪资（含核对） → ② 归档锁定 → ③ 发布工资条
```

### 步骤 1：编辑薪资（DRAFT 状态）

可操作：
- **点击单元格编辑**：基本工资、加班费、津贴、奖金、扣款、社保个人/单位、公积金个人/单位
- **导入工资表 Excel**：批量填充薪资数据
- **下载导入模板**
- **导入加班费**：从加班记录按月自动填充 `overtimePay`
- **获取提成奖金**：从提成奖金模块按月填充 `bonus`
- **添加/删除人员**：动态调整批次人员
- **查看个税明细**：点击个税金额查看累计预扣计算过程

核对（编辑页面内直接展示，非独立步骤）：
- 批次汇总卡片：应发合计、社保合计、公积金合计、个税合计、实发合计
- 质量门禁自动检查：实发为负、全零记录、最低工资、递延扣款等
- 可导出 **薪资汇总表** / **薪资明细表**（CSV）

质量门禁（草稿状态自动检查）：
- 实发为负的员工 → 红色警告
- 实发低于最低工资（未触发保护）→ 红色警告
- 最低工资保护已触发（递延扣款）→ 黄色警告
- 本月补扣上月递延 → 黄色警告
- 全零记录 → 黄色警告

表格可视化提示：
- **实发列**：最低工资保护触发时显示橙色 + ★标记，悬停显示补齐明细
- **递延扣款列**（新增）：橙色"递延 ¥X"或蓝色"补扣 ¥X"，悬停显示分项明细
- **风险列**：最低工资保护触发时显示橙色警告图标

### 步骤 2：归档锁定（ARCHIVED）

**归档时后端自动执行**：
1. **重算所有条目**：调用 `calcBatchEntry` 重新计算社保公积金和个税
2. **更新批次汇总**：重算 totalPay/totalNetPay/各项合计
3. **标记为 ARCHIVED**：设置 `archivedAt`，不可再编辑

### 步骤 3：发布工资条

- 调用 `POST /batches/:id/publish` → 更新 `Payslip.publishStatus = PUBLISHED`
- 或调用 `POST /batches/:id/schedule` → 设置 `publishStatus = SCHEDULED` + `scheduledAt`
- 员工在员工端查看已发布的工资条

---

## 十五、最低工资保护与递延扣款机制

### 背景

入职当月工资按天折算后，可能不足以扣除社保/公积金个人部分，导致实发为负。
《劳动法》第四十八条规定用人单位支付劳动者的工资不得低于当地最低工资标准。

### 最低工资标准配置

| 配置位置 | 表 | 字段 | 说明 |
|---------|---|------|------|
| 新账户体系 | `SocialYearStandard` | `minWage` | 按账户+年度设置，优先使用 |
| 旧配置表 | `SocialInsuranceConfig` | `minWage` | 兼容回退，按城市匹配 |

前端配置入口：社保配置页面 → 新建版本 → "最低工资标准"输入框（仅社保 Tab）
设为 0 表示不检查最低工资。

### 保护逻辑（calcBatchEntry 中执行）

```
初始实发 = 应发 - 社保个人 - 公积金个人 - 个税 - 上月递延最低工资补扣

if (minWage > 0 且 实发 < minWage) {
  差额 = minWage - 实发

  ① 优先递延社保个人部分
     if (差额 <= 社保个人) {
       递延社保 = 差额
       社保个人 -= 差额
       实发 = minWage
     }

  ② 社保不够，递延全部社保 + 部分公积金
     else if (差额 <= 社保个人 + 公积金个人) {
       递延社保 = 社保个人
       递延公积金 = 差额 - 社保个人
       社保个人 = 0
       公积金个人 -= 递延公积金
       实发 = minWage
     }

  ③ 社保+公积金都不够，差额作为最低工资补齐递延
     else {
       递延社保 = 社保个人
       递延公积金 = 公积金个人
       递延最低工资补齐 = 差额 - 社保个人 - 公积金个人
       社保个人 = 0
       公积金个人 = 0
       实发 = minWage
     }
}
```

### minWageApplied 的含义

`minWageApplied` = 为让实发达到最低工资而减少的扣款总额，包含三部分：

| 组成 | 字段 | 含义 |
|------|------|------|
| 免扣社保 | `deferredSocialEmp` | 本该扣但没扣的社保个人部分 |
| 免扣公积金 | `deferredHousingEmp` | 本该扣但没扣的公积金个人部分 |
| 额外补齐 | `deferredMinWage` | 社保+公积金全免后仍不足的差额 |
| **合计** | `minWageApplied` | = 免扣社保 + 免扣公积金 + 额外补齐 |

**注意**：`minWageApplied` 不是"额外补了这么多现金"，而是"减少了这么多扣款"。

### 递延扣款的次月补扣

```
本月（入职当月）：
  BatchEntry 记录 deferredSocialEmp / deferredHousingEmp / deferredMinWage
  实发 = minWage（保护后）

次月（创建批次时）：
  getPrevDeferred() 读取上月已归档批次的递延金额
  calcBatchEntry 的 options.prevDeferred 传入
  本月社保个人 = 当月应缴社保 + 上月递延社保
  本月实发 = 应发 - (当月社保 + 上月递延社保) - (当月公积金 + 上月递延公积金) - 个税 - 上月递延最低工资补齐
  BatchEntry 记录 prevDeferredSocialEmp / prevDeferredHousingEmp / prevDeferredMinWage
```

### 示例

**张三 8 月 20 日入职，月薪 12000，扣款 20000（测试数据）**

```
应发 = 12000 + 0 + 0 + (-500) - 20000 = -8500
社保个人（原）= 1575
公积金个人（原）= 1800
个税 = 0
初始实发 = -8500 - 1575 - 1800 - 0 = -11875
最低工资 = 2420

保护触发：
  差额 = 2420 - (-11875) = 14295
  递延社保 = 1575（全部免扣）
  递延公积金 = 1800（全部免扣）
  递延最低工资补齐 = 14295 - 1575 - 1800 = 10920
  实发 = 2420

BatchEntry 记录：
  minWage = 2420
  minWageApplied = 14295
  deferredSocialEmp = 1575
  deferredHousingEmp = 1800
  deferredMinWage = 10920
  socialEmp = 0（当月不扣）
  housingEmp = 0（当月不扣）
  netPay = 2420

次月（9 月）：
  getPrevDeferred() 读取 → { socialEmp: 1575, housingEmp: 1800, minWage: 10920 }
  9 月社保个人 = 当月应缴社保 + 1575
  9 月实发 = 应发 - 9月社保(含补扣) - 9月公积金(含补扣) - 个税 - 10920
```

### 前端展示

| 展示位置 | 样式 | 内容 |
|---------|------|------|
| 实发列 | 橙色 + ★ | 触发最低工资保护时，悬停显示补齐明细 |
| 递延扣款列 | 橙色"递延 ¥X" | 当月递延，悬停显示分项（免扣社保/公积金/额外补齐） |
| 递延扣款列 | 蓝色"补扣 ¥X" | 次月补扣上月递延，悬停显示分项 |
| 风险列 | 橙色警告图标 | 最低工资保护触发 |
| 质量门禁 | 黄色提示 | "N 名员工实发已补齐到最低工资标准..." |

### prePayrollCheck 检查项（第 11/12 项）

```
// 11. 实发低于最低工资标准（剔除加班费后比较）
if (minWage > 0 且 可比收入 < minWage 且 实发 < minWage) {
  if (minWageApplied > 0) {
    → WARNING: 最低工资保护已触发（递延扣款）
  } else {
    → FAIL: 实发低于最低工资标准
  }
}

// 12. 上月递延扣款待补扣
if (prevDeferred > 0) {
  → WARNING: 上月递延扣款已补扣
}
```

---

## 十六、状态流转

```
DRAFT（草稿，可编辑）
  ↓ 归档
ARCHIVED（已归档，锁定不可编辑）
  ↓ 取消归档
DRAFT（恢复草稿）
  ↓ 发布工资条
Payslip.publishStatus: PUBLISHED（员工端可见）
```

---

## 十七、批次类型详解

| 类型 | 说明 | 社保公积金 | 个税计算 | 员工来源 |
|------|------|-----------|---------|---------|
| **REGULAR** 常规发薪 | 月度工资 | ✅ 差额补扣 | 累计预扣法 | 在职 + 本月离职 |
| **TERMINATION** 离职结算 | 离职员工当月工资 | ✅ 差额补扣 | 累计预扣法 | 本月离职记录 |
| **BONUS** 年终奖/奖金 | 单独计税 | ❌ 不扣 | 单独计税 | 在职 + 本月离职 |
| **SEVERANCE** 补偿金 | 离职补偿金 | ❌ 不扣 | 累计预扣法（无社保扣除） | 已审批且有补偿金的离职记录 |

---

## 十八、创建批次的 5 种数据初始化模式

| 模式 | 说明 | 员工来源 | 数据来源 |
|------|------|---------|---------|
| **copy_last** 复制上月 | 默认模式 | 在职 + 本月离职 | 上月工资条复制基本工资/津贴/扣款 + 当月加班费 |
| **blank_employees** 本月空白 | 拉入员工 | 在职 + 本月离职 | 所有金额为 0，手动填写 |
| **blank_all** 全空白 | 不拉入员工 | 无 | 后续手动添加人员 |
| **copy_batch** 复制指定批次 | 从源批次 | 源批次的员工 | 复制源批次薪资数据 |
| **custom** 自定义选择 | 按部门/姓名筛选勾选 | 指定员工 | 金额为 0，手动填写 |
