# BD 手册：助教月度汇总表唯一约束变更（需求 A）

## 概述

`dws.dws_assistant_monthly_summary` 的唯一约束从 `(site_id, assistant_id, stat_month)` 变更为 `(site_id, assistant_id, stat_month, assistant_level_code)`，以支持助教月内多档位分段统计。

## 变更说明

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

### 变更原因

助教在同一月内可能因升级/降级而存在多个 `assistant_level_code`。旧约束只允许每个助教每月一行记录，导致：
- `AssistantMonthlyTask` 按档位分组聚合时，INSERT 违反唯一约束
- 临时修复（`MAX()` 聚合）丢失了档位维度信息，无法按档位分段计算工资

新约束允许同一助教同月按不同档位生成多行记录，支持精确的分段业绩统计和工资计算。

## 兼容性

- **ETL Connector**：`AssistantMonthlyTask` 的 `_extract_daily_aggregates()` 将同步修改 GROUP BY 加入 `assistant_level_code`（任务 7.2），INSERT 逻辑适配多行输出
- **AssistantSalaryTask**：需适配多行月度汇总结构，按档位分段计算工资（任务 7.4）
- **后端 API / 小程序**：无直接影响（当前未直接读取 `dws_assistant_monthly_summary`）
- **DWS 下游任务**：`AssistantFinanceTask`、`AssistantCustomerTask` 不依赖此表的唯一约束，无影响

## 回滚策略

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

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

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

回滚后需重新执行 `DWS_ASSISTANT_MONTHLY` 任务以 MAX() 模式重新聚合数据。

## 验证步骤

```sql
-- 1. 确认新约束存在且列组合正确
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
  FROM pg_constraint
 WHERE conrelid = 'dws.dws_assistant_monthly_summary'::regclass
   AND conname = 'uk_dws_assistant_monthly';
-- 预期：1 行，constraint_def 包含 (site_id, assistant_id, stat_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_monthly_summary'::regclass
   AND c.conname = 'uk_dws_assistant_monthly';
-- 预期：4

-- 3. 确认旧的 3 列约束不存在
SELECT conname, array_length(conkey, 1) AS col_count
  FROM pg_constraint
 WHERE conrelid = 'dws.dws_assistant_monthly_summary'::regclass
   AND contype = 'u';
-- 预期：uk_dws_assistant_monthly 的 col_count = 4（非 3）
```

## 关联文件

- 迁移脚本：`db/etl_feiqiu/migrations/2026-02-22__A_monthly_summary_uk_change.sql`
- 主 DDL：`db/etl_feiqiu/schemas/dws.sql`（已合并）
- 工资表约束变更：`BD_Manual_dws_assistant_salary_uk_change.md`
- 需求文档：`.kiro/specs/etl-aggregation-fix/requirements.md` — 需求 2.1, 2.2, 2.3
- 代码变更：
  - `apps/etl/connectors/feiqiu/tasks/dws/assistant_monthly_task.py` — GROUP BY 加入 `assistant_level_code`
  - `apps/etl/connectors/feiqiu/tasks/dws/assistant_salary_task.py` — 按档位分段计算工资
