# 设计文档：助教废除（Abolish）全链路清理

## 概述

本设计描述如何安全地从 ETL 全链路中移除"助教废除"独立数据链路。
核心思路：**删除独立的废除链路（API → ODS → DWD），保留服务记录中已有的 `is_trash` 字段作为唯一废除判断源。**

清理范围覆盖：
- ETL 任务定义（ODS 任务 + 注册表）
- DWD 加载映射（FACT_MAPPINGS + TABLE_MAP）
- DWS 聚合逻辑（死代码移除）
- DWD 验证器配置
- 数据库 DDL 和迁移脚本
- 属性测试
- 运维脚本

## 架构

### 清理前数据流

```mermaid
graph LR
    API_Abolish["/AssistantPerformance/GetAbolitionAssistant"] --> ODS_ACR["ods.assistant_cancellation_records"]
    ODS_ACR --> DWD_ATE["dwd.dwd_assistant_trash_event"]
    ODS_ACR --> DWD_ATE_EX["dwd.dwd_assistant_trash_event_ex"]
    DWD_ATE --> |"_extract_trash_records (死代码)"| DWS_DAILY["dws.dws_assistant_daily_detail"]

    API_Service["/AssistantPerformance/GetAssistantServiceRecords"] --> ODS_ASR["ods.assistant_service_records"]
    ODS_ASR --> DWD_SL_EX["dwd.dwd_assistant_service_log_ex"]
    DWD_SL_EX --> |"is_trash 字段 (实际使用)"| DWS_DAILY

    style API_Abolish fill:#f99,stroke:#c00
    style ODS_ACR fill:#f99,stroke:#c00
    style DWD_ATE fill:#f99,stroke:#c00
    style DWD_ATE_EX fill:#f99,stroke:#c00
```

### 清理后数据流

```mermaid
graph LR
    API_Service["/AssistantPerformance/GetAssistantServiceRecords"] --> ODS_ASR["ods.assistant_service_records"]
    ODS_ASR --> DWD_SL_EX["dwd.dwd_assistant_service_log_ex"]
    DWD_SL_EX --> |"is_trash 字段"| DWS_DAILY["dws.dws_assistant_daily_detail"]

    style API_Service fill:#9f9,stroke:#090
    style DWD_SL_EX fill:#9f9,stroke:#090
```

## 组件与接口

### 需要修改的文件清单

| 层级 | 文件 | 操作 | 需求 |
|------|------|------|------|
| ODS 任务 | `tasks/ods/ods_tasks.py` | 删除 `OdsTaskSpec` 条目 + 从默认序列移除 | 1.2, 1.3 |
| 任务注册 | `orchestration/task_registry.py` | 删除 `ODS_ASSISTANT_ABOLISH` 注册 | 1.1 |
| DWD 加载 | `tasks/dwd/dwd_load_task.py` | 删除 FACT_MAPPINGS 和 TABLE_MAP 条目 | 2.1–2.4 |
| DWS 日度 | `tasks/dws/assistant_daily_task.py` | 删除 `_extract_trash_records`、`_build_trash_index`，简化 `_aggregate_by_assistant_date` 签名 | 3.1–3.4 |
| DWD 验证 | `tasks/verification/dwd_verifier.py` | 删除废除表的 ID 和时间字段映射 | 4.1–4.4 |
| DDL | `db/etl_feiqiu/schemas/dwd.sql` | 删除 `dwd_assistant_trash_event` / `_ex` 的 CREATE TABLE + COMMENT | 6.1–6.2 |
| DDL | `db/etl_feiqiu/schemas/ods.sql` | 删除 `assistant_cancellation_records` 的 CREATE TABLE + COMMENT | 6.3 |
| DDL | `db/etl_feiqiu/schemas/schema_dwd_doc.sql` | 删除废除表的 CREATE TABLE + COMMENT | 6.4 |
| DDL | `db/etl_feiqiu/schemas/schema_ODS_doc.sql` | 删除废除表的 CREATE TABLE + COMMENT | 6.5 |
| DDL | `db/etl_feiqiu/schemas/dws.sql` | 更新 `dws_assistant_daily_detail` 注释 | 6.6 |
| DDL | `db/etl_feiqiu/schemas/schema_dws.sql` | 更新 `dws_assistant_daily_detail` 注释 | 6.7 |
| 迁移 | `db/etl_feiqiu/migrations/` | 新建 DROP TABLE 迁移脚本 | 5.1–5.5 |
| 属性测试 | `tests/test_property_1_fact_mappings.py` | 删除 `_REQ3_EXPECTED` 和相关引用 | 7.1–7.3 |
| 运维 | `scripts/ops/dataflow_analyzer.py` | 删除 `ODS_ASSISTANT_ABOLISH` spec 条目 | 8.1 |
| 运维 | `scripts/ops/gen_full_dataflow_doc.py` | 删除 `ODS_ASSISTANT_ABOLISH` spec 条目 | 8.1 |
| 运维 | `scripts/ops/etl_consistency_check.py` | 删除废除相关映射 | 8.1–8.2 |
| 运维 | `scripts/ops/blackbox_test_report.py` | 删除废除相关映射 | 8.1–8.4 |
| 运维 | `scripts/ops/field_audit.py` | 删除废除表审计条目 | 8.3–8.4 |
| 运维 | `scripts/ops/gen_field_review_doc.py` | 删除废除表字段定义 | 8.3–8.4 |
| 运维 | `scripts/ops/gen_api_field_mapping.py` | 从 ODS_TABLES 列表移除 | 8.3 |
| 运维 | `scripts/ops/export_dwd_field_review.py` | 删除废除表条目 | 8.4 |
| 运维 | `scripts/ops/check_ods_latest_indexes.py` | 删除废除表索引检查 | 8.3 |

### 不需要修改的文件（确认安全）

| 文件 | 原因 |
|------|------|
| `dwd_assistant_service_log_ex` 表 DDL | 保留 `is_trash` 等字段（需求 9） |
| `ods.assistant_service_records` 表 DDL | 保留 `is_trash` 等字段（需求 9） |
| `assistant_monthly_task.py` | 仅消费 `dws_assistant_daily_detail` 的 `trashed_seconds`/`trashed_count`，不直接引用废除表 |
| `assistant_salary_task.py` | 仅消费 `dws_assistant_monthly_summary`，不直接引用废除表 |

## 数据模型

### 被删除的表

```sql
-- ODS 层
ods.assistant_cancellation_records  -- 78 条记录

-- DWD 层
dwd.dwd_assistant_trash_event       -- 主表
dwd.dwd_assistant_trash_event_ex    -- 扩展表
```

### 保留的废除相关字段

```sql
-- ods.assistant_service_records 中（保留）
is_trash              INT       -- 废除标记
trash_reason          TEXT      -- 废除原因
trash_applicant_id    BIGINT    -- 废除申请人 ID
trash_applicant_name  TEXT      -- 废除申请人姓名

-- dwd.dwd_assistant_service_log_ex 中（保留）
is_trash              INTEGER   -- 废除标记
trash_applicant_id    BIGINT    -- 废除申请人 ID
trash_applicant_name  VARCHAR(64) -- 废除申请人姓名
trash_reason          VARCHAR(255) -- 废除原因
```

### DWS 层字段（保留，数据来源变更说明）

```sql
-- dws.dws_assistant_daily_detail（保留，注释需更新）
trashed_seconds  INTEGER  -- 数据来源：dwd_assistant_service_log_ex.is_trash + income_seconds
trashed_count    INTEGER  -- 数据来源：dwd_assistant_service_log_ex.is_trash 计数

-- dws.dws_assistant_monthly_summary（保留）
trashed_hours    NUMERIC(10,2)  -- 来自 daily_detail.trashed_seconds 汇总
```

## DWS 代码重构细节

### assistant_daily_task.py 变更

**删除方法：**
- `_extract_trash_records()` — 查询 `dwd.dwd_assistant_trash_event` 的 SQL，已无消费者
- `_build_trash_index()` — 构建废除索引，已不参与判断逻辑

**修改方法：**
- `extract()` — 移除对 `_extract_trash_records` 的调用，移除 `trash_records` 变量
- `transform()` 或调用 `_aggregate_by_assistant_date` 的地方 — 移除 `trash_index` 参数传递
- `_aggregate_by_assistant_date()` — 从签名中移除 `trash_index` 参数；`is_trash` 判断逻辑保持不变

**不变逻辑：**
```python
# 这段逻辑保持不变——通过 is_trash 字段判断废除
is_trashed = bool(record.get('is_trash', 0))
if is_trashed:
    agg['trashed_seconds'] += income_seconds
    agg['trashed_count'] += 1
```


## 正确性属性

*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*

### Property 1：废除聚合逻辑正确性（is_trash 驱动）

*对任意*服务记录集合，其中每条记录包含 `is_trash` 标记和 `income_seconds` 值，聚合后：
- `trashed_seconds` 应等于所有 `is_trash=1` 记录的 `income_seconds` 之和
- `trashed_count` 应等于所有 `is_trash=1` 记录的数量
- `total_service_count` 应等于所有 `is_trash=0` 记录的数量
- `total_seconds` 应等于所有 `is_trash=0` 记录的 `income_seconds` 之和

**Validates: Requirements 3.4, 9.3**

### Property 2：FACT_MAPPINGS 一致性（已有属性测试的回归验证）

*对任意* FACT_MAPPINGS 中的表名，该表名应在 TABLE_MAP 中有对应的 ODS 源表映射，且映射的每个 DWD 列名应为合法的 SQL 标识符。

**Validates: Requirements 2.1–2.4, 7.3**

> 注：此属性已由 `tests/test_property_1_fact_mappings.py` 实现。清理后需确保该测试仍然通过。

## 错误处理

### 迁移脚本安全性

- 所有 `DROP TABLE` 语句使用 `IF EXISTS`，确保幂等执行
- 迁移脚本在单个事务中执行，失败时自动回滚
- 迁移脚本包含注释说明移除原因，便于审计追溯

### 代码删除安全性

- 删除 `_extract_trash_records` 和 `_build_trash_index` 前，确认无其他调用者
- `_aggregate_by_assistant_date` 移除 `trash_index` 参数后，确认所有调用点已同步更新
- 保留 `is_trash` 判断逻辑不变，确保废除统计功能不受影响

### 回滚策略

- DDL 变更通过迁移脚本管理，可通过反向迁移（CREATE TABLE）回滚
- 代码变更通过 Git 版本控制回滚
- ODS 表数据在删除前可选择性备份（数据量小，仅 78 条）

## 测试策略

### 属性测试

- 使用 `hypothesis` 库进行属性测试
- 每个属性测试至少运行 100 次迭代
- 每个测试用注释标注对应的设计属性编号

**Property 1 测试方案：**
- 生成随机服务记录列表（包含随机 `is_trash` 标记和 `income_seconds` 值）
- 调用 `_aggregate_by_assistant_date` 方法
- 验证 `trashed_seconds`/`trashed_count` 与手动计算的期望值一致
- 标签：`Feature: assistant-abolish-cleanup, Property 1: 废除聚合逻辑正确性`

**Property 2 测试方案：**
- 已由 `tests/test_property_1_fact_mappings.py` 覆盖
- 清理后运行 `pytest tests/ -v` 确认无回归
- 标签：`Feature: assistant-abolish-cleanup, Property 2: FACT_MAPPINGS 一致性`

### 单元测试

- 验证 `AssistantDailyTask` 不再有 `_extract_trash_records` 和 `_build_trash_index` 方法
- 验证 `_aggregate_by_assistant_date` 签名不包含 `trash_index` 参数
- 验证 FACT_MAPPINGS 不包含废除表条目
- 验证 TABLE_MAP 不包含废除表映射
- 验证 DwdVerifier 配置不包含废除表

### 集成验证

- 运行现有属性测试套件：`pytest tests/ -v`
- 运行 ETL 单元测试：`cd apps/etl/connectors/feiqiu && pytest tests/unit`
- 确认所有测试通过，无回归
