# BD 手册：助教工资计算表唯一约束变更（需求 A — 任务 7.4）

## 概述

`dws.dws_assistant_salary_calc` 的唯一约束从 `(site_id, assistant_id, salary_month)` 变更为 `(site_id, assistant_id, salary_month, assistant_level_code)`，以支持同一助教同月按不同档位分段计算工资。

## 变更说明

| Schema | 表 | 变更类型 | 约束名 | 旧定义 | 新定义 |
|--------|---|---------|--------|--------|--------|
| dws | dws_assistant_salary_calc | 改约束 | uk_dws_assistant_salary | `(site_id, assistant_id, salary_month)` | `(site_id, assistant_id, salary_month, assistant_level_code)` |

### 变更原因

上游 `dws_assistant_monthly_summary` 已按 `(site_id, assistant_id, stat_month, assistant_level_code)` 分行（任务 7.2/7.3）。`AssistantSalaryTask` 逐行读取月度汇总并计算工资，同一助教同月可能产生多条工资记录（对应不同档位）。旧约束 `(site_id, assistant_id, salary_month)` 会导致 INSERT 冲突。

### 代码变更

- `assistant_salary_task.py` 的 `get_primary_keys()` 返回值加入 `assistant_level_code`
- `_extract_monthly_summary()`、`transform()`、`_calculate_salary()` 无需修改——已天然按行处理

## 兼容性

- **ETL Connector**：`AssistantSalaryTask` 的 `get_primary_keys()` 已同步更新，`upsert()` 方法使用该键做 ON CONFLICT
- **`_delete_by_month()`**：按 `site_id + salary_month` 整月删除后重新插入，不受约束变更影响
- **后端 API / 小程序**：无直接影响（当前未直接读取 `dws_assistant_salary_calc`）
- **DWS 下游任务**：`AssistantFinanceDailyTask` 读取 salary_calc 做日度成本分摊，需确认按 assistant_id 聚合时能正确处理多档位行（现有逻辑按 assistant_id + stat_date 聚合，不受影响）

## 回滚策略

```sql
-- ⚠️ 回滚前须先清理同一 (site_id, assistant_id, salary_month) 的多行数据，
--    否则恢复旧约束会因重复键失败。

-- 步骤 1：清理多档位数据（保留每组最新一条）
DELETE FROM dws.dws_assistant_salary_calc a
 USING (
     SELECT site_id, assistant_id, salary_month,
            MAX(updated_at) AS keep_updated_at
       FROM dws.dws_assistant_salary_calc
      GROUP BY site_id, assistant_id, salary_month
 ) b
 WHERE a.site_id = b.site_id
   AND a.assistant_id = b.assistant_id
   AND a.salary_month = b.salary_month
   AND a.updated_at <> b.keep_updated_at;

-- 步骤 2：恢复旧约束
BEGIN;
ALTER TABLE dws.dws_assistant_salary_calc
    DROP CONSTRAINT IF EXISTS uk_dws_assistant_salary;
ALTER TABLE dws.dws_assistant_salary_calc
    ADD CONSTRAINT uk_dws_assistant_salary
    UNIQUE (site_id, assistant_id, salary_month);
COMMIT;
```

回滚后需重新执行 `DWS_ASSISTANT_SALARY` 任务，此时月度汇总表也应已回滚为单行模式。

## 验证步骤

```sql
-- 1. 确认新约束存在且列组合正确
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
  FROM pg_constraint
 WHERE conrelid = 'dws.dws_assistant_salary_calc'::regclass
   AND conname = 'uk_dws_assistant_salary';
-- 预期：1 行，constraint_def 包含 (site_id, assistant_id, salary_month, assistant_level_code)

-- 2. 确认约束列数为 4
SELECT COUNT(*) AS col_count
  FROM pg_constraint c
  JOIN LATERAL unnest(c.conkey) AS col_num ON TRUE
 WHERE c.conrelid = 'dws.dws_assistant_salary_calc'::regclass
   AND c.conname = 'uk_dws_assistant_salary';
-- 预期：4

-- 3. 测试同一助教同月不同档位可共存
INSERT INTO dws.dws_assistant_salary_calc
    (site_id, tenant_id, assistant_id, salary_month, assistant_level_code)
VALUES (99999, 99999, 99999, '2099-01-01', 10),
       (99999, 99999, 99999, '2099-01-01', 20);
-- 预期：成功插入 2 行
DELETE FROM dws.dws_assistant_salary_calc
 WHERE site_id = 99999 AND assistant_id = 99999 AND salary_month = '2099-01-01';
```

## 关联文件

- 迁移脚本：`db/etl_feiqiu/migrations/2026-02-22__A_salary_calc_uk_change.sql`
- 主 DDL：`db/etl_feiqiu/schemas/dws.sql`、`db/etl_feiqiu/schemas/schema_dws.sql`
- 代码：`apps/etl/connectors/feiqiu/tasks/dws/assistant_salary_task.py`
- 需求文档：`.kiro/specs/etl-aggregation-fix/requirements.md` — 需求 2.4
- 前置任务：7.1（monthly_summary UK 变更）、7.2/7.3（月度聚合按档位分行）
