# BD 手册：DWD 层 BC 哨兵日期修复

## 概述

上游飞球 API 对"未设置"的日期字段返回哨兵值 `0001-01-01T00:00:00`。该值在 ODS 层以 `timestamp without time zone` 存储，转入 DWD 层 `timestamp with time zone` 列时，PostgreSQL 在 `Asia/Shanghai` 时区下将其转换为 `0001-12-31 23:59:43 BC`（公元前日期）。psycopg2 无法解析 BC 日期，导致 `ValueError: year -1 is out of range`。

## 受影响对象

| Schema | 表 | 列 | ODS 源列类型 | DWD 列类型 | 受影响行数 |
|--------|---|---|---|---|---|
| dwd | dim_assistant_ex | birth_date | timestamp | timestamptz | ~1,107 |
| dwd | dim_member_card_account_ex | disable_start_time | timestamp | timestamptz | ~18,172 |
| dwd | dim_member_card_account_ex | disable_end_time | timestamp | timestamptz | ~18,172 |
| dwd | dwd_assistant_service_log_ex | composite_grade_time | timestamp | timestamptz | ~5,297 |
| dwd | dwd_recharge_order_ex | revoke_time | timestamp | timestamptz | ~485 |
| dwd | dwd_settlement_head_ex | revoke_time | timestamp | timestamptz | ~26,435 |

## 修复方案

### 存量数据

迁移脚本 `2026-02-22__fix_bc_sentinel_dates_to_null.sql`：将所有 `EXTRACT(year) < 1` 的 BC 日期置为 NULL。

### 增量防御（代码层）

在 `dwd_load_task.py` 的 SQL 表达式构造中加入哨兵值过滤（阈值 `0002-01-01`）：

1. `_cast_expr`：对 `timestamptz` CAST 包裹 `CASE WHEN (base)::timestamp >= '0002-01-01'::timestamp THEN ... ELSE NULL END`
2. `_build_fact_select_exprs`：事实表 `timestamp → timestamptz` 同类型列加过滤
3. `_merge_dim_scd2`：维度表 ODS→DWD SELECT 和 DWD 现有数据读取均加过滤

### 设计决策

- 阈值选 `0002-01-01` 而非 `0001-01-02`：留出安全余量，公元 1 年的日期在业务上不可能出现
- BC 日期 → NULL 而非保留原值：哨兵值本身无业务含义（"未设置"），NULL 语义更准确
- `(base)::timestamp` 显式 CAST：因为 base 可能是 JSONB `->>'key'` 提取的 text 类型，不能直接与 timestamp 比较

## 兼容性

- ETL Connector：v10 验证 19/19 任务全部成功，0 错误
- 后端 API / 小程序：无影响
- DWS 层：无影响（不引用这些时间列）

## 回滚

存量数据回滚不可行（BC 日期是错误数据）。代码回滚需还原 `_cast_expr`、`_build_fact_select_exprs`、`_merge_dim_scd2` 中的哨兵过滤逻辑。

## 验证

见 `docs/database/etl_feiqiu_schema_migration.md` 迁移 11 的验证 SQL。

## 关联文件

- 迁移脚本：`db/etl_feiqiu/migrations/2026-02-22__fix_bc_sentinel_dates_to_null.sql`
- 代码修改：`apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py`
- 存量修复脚本（已废弃）：`scripts/ops/fix_bc_dates.py`
