# ODS 任务参数矩阵

> 本文档列举所有 22 个 ODS 任务的关键配置参数，说明每个参数在 ETL 处理流程中的作用，
> 并以通俗语言概述飞球 ETL 的 API → ODS 现状。
>
> 数据来源：`tasks/ods/ods_tasks.py` 中的 `ODS_TASK_SPECS` 声明。
> 最后更新：2026-02-17（ODS 去重与软删除机制标准化改造后；文档与代码对齐校验于 2026-02-17）

---

## 飞球 ETL 的 API → ODS 现状（通俗版）

### 从哪抓

从上游飞球 SaaS 平台的 REST API 抓取，共对接 22 个不同的 API 端点。覆盖台球门店的核心业务：订单结账、支付退款、会员档案与余额、助教服务与废除、商品库存、台桌、团购套餐、台费折扣等。每个端点对应一个 ODS 任务。

### 怎么解析

API 返回的 JSON 响应通过两级路径定位数据：先按 `data_path`（通常是 `"data"`）找到数据容器，再按 `list_key`（如 `"settleList"`、`"tenantMemberInfos"`）取出记录列表。拿到列表后，每条记录被展平为单层字典，然后运行时从数据库的 `information_schema` 读取目标表结构，按列名自动匹配——不需要手写字段映射（`ODS_RECHARGE_SETTLE` 除外，它因为嵌套结构复杂，手动定义了 60+ 个列映射）。原始 JSON 整体存入 `payload` 列，保留完整源数据便于回溯。

### 写到哪

全部写入 PostgreSQL 的 `ods.*` schema（对应实际的 `ods` schema），22 张表一一对应 22 个任务。

### 怎么去重

所有 22 个任务默认开启 `skip_unchanged=True`：基于原始 payload + is_delete 计算 content_hash（SHA-256），若与数据库中该业务 ID 最新版本的 hash 相同则跳过写入，避免无意义的版本膨胀。

冲突处理模式默认是 `update`（全字段对比，有变化才更新），也支持 `backfill`（只回填 NULL 列）和 `nothing`（跳过已存在记录），通过运行时配置 `run.ods_conflict_mode` 控制。

### 怎么处理删除

软删除采用"插入删除版本行"语义——不修改历史版本，而是 INSERT 一条 `is_delete=1` 的新版本行。三种快照模式：

- 全表快照（5 个任务）：API 返回全量数据，本次没返回但数据库里有的记录插入一条 `is_delete=1` 的删除版本行。适用于维度表/档案表，数据量小。
- 窗口快照（8 个任务）：只在指定时间列的窗口范围内做软删除。适用于流水表，数据量大，只能按时间段比对。
- 无快照（9 个任务）：纯增量写入，不做软删除。

下游 DWD 层取数规约：`DISTINCT ON (id) ORDER BY id, fetched_at DESC`，再过滤 `is_delete = 0`。

---

## 参数说明

### OdsTaskSpec 参数（代码层，硬编码在 spec 中）

| 参数 | ETL 处理中的作用 |
|------|-----------------|
| `requires_window` | 控制任务是否需要时间窗口参数来限定抓取范围。为 `True` 时，`execute()` 会解析游标/手动覆盖得到 `(start, end)` 时间段，只抓取该时间段内的数据；为 `False` 时不传时间参数，由 API 自身决定返回范围（通常配合分页抓全量）。注意：此参数跟的是 API 端点的能力，不是数据模型的分类（事实表 vs 维度表）。 |
| `time_fields` | 指定 API 请求中时间窗口参数的键名对。默认 `(startTime, endTime)`，部分任务用 `(rangeStartTime, rangeEndTime)`。必须与上游 API 契约一致。 |
| `include_fetched_at` | 控制是否向目标表写入 `fetched_at` 列（记录抓取时间戳）。仅控制元数据写入，不影响去重行为（去重由 `skip_unchanged` 控制）。 |
| `include_record_index` | 控制是否写入 `record_index` 列（记录在当前批次中的序号）。当表 PK 包含 `record_index` 时，该序号成为冲突检测的一部分。 |
| `skip_unchanged` | 去重开关（默认 `True`）。为 `True` 时，若目标表有 `content_hash` 列且有业务主键，则跳过内容未变的记录。content_hash 基于原始 payload + is_delete 计算 SHA-256。不再隐式依赖 `fetched_at` 列的存在。 |
| `snapshot_mode` | 快照软删除策略枚举。`NONE`（默认）= 不做快照对比；`FULL_TABLE` = 全表快照；`WINDOW` = 窗口快照。替代原 `snapshot_full_table` + `snapshot_window_columns` 两个字段。 |
| `snapshot_time_column` | WINDOW 模式的时间列名。仅当 `snapshot_mode=WINDOW` 时有效且必填，其余模式必须为 `None`。 |

### 运行时配置（通过 `.env` / CLI 控制）

| 配置项 | 默认值 | 作用 |
|--------|--------|------|
| `run.snapshot_missing_delete` | `True` | 全局开关：是否执行软删除标记 |
| `run.snapshot_allow_empty_delete` | `False` | 是否允许空结果集触发软删除 |
| `run.ods_conflict_mode` | `update` | 冲突处理模式：`update`（全字段对比更新）/ `backfill`（仅回填 NULL）/ `nothing`（跳过） |
| `run.window_override.start/end` | — | 手动覆盖时间窗口 |
| `run.window_split.*` | 见 `defaults.py` | 窗口切分策略（天数、补偿小时等） |
| `run.ods_execute_values_page_size` | `200` | 批量写入的 chunk 大小 |

> OdsTaskSpec 参数是代码层硬编码，改动需要改代码；运行时配置走 `.env` / CLI，可以不改代码调整行为。


---

## 任务参数矩阵

| 任务代码 | `requires_window` | `time_fields` | `include_fetched_at` | `include_record_index` | `skip_unchanged` | `snapshot_mode` | `snapshot_time_column` |
|---|:---:|:---:|:---:|:---:|:---:|:---:|---|
| `ODS_ASSISTANT_ACCOUNT` | ✅ | 默认 | ❌ | ✅ | ✅ | `FULL_TABLE` | — |
| `ODS_SETTLEMENT_RECORDS` | ✅ | `(rangeStartTime, rangeEndTime)` | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_TABLE_USE` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `create_time` |
| `ODS_ASSISTANT_LEDGER` | ✅ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `create_time` |
| `ODS_STORE_GOODS_SALES` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `create_time` |
| `ODS_PAYMENT` | ❌ | 默认 | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_REFUND` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `pay_time` |
| `ODS_PLATFORM_COUPON` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `consume_time` |
| `ODS_MEMBER` | ❌ | 默认 | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_MEMBER_CARD` | ❌ | 默认 | ❌ | ✅ | ✅ | `FULL_TABLE` | — |
| `ODS_MEMBER_BALANCE` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `create_time` |
| `ODS_RECHARGE_SETTLE` | ✅ | `(rangeStartTime, rangeEndTime)` | ✅ | ❌ | ✅ | `NONE` | — |
| `ODS_GROUP_PACKAGE` | ❌ | 默认 | ❌ | ✅ | ✅ | `FULL_TABLE` | — |

> `ODS_GROUP_PACKAGE` 额外配置了 `detail_endpoint`，在主流程完成后串行调用 `QueryPackageCouponInfo` 获取每个团购的详情数据，写入 `ods.group_buy_package_details`。
| `ODS_GROUP_BUY_REDEMPTION` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `create_time` |
| `ODS_INVENTORY_STOCK` | ❌ | 默认 | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_INVENTORY_CHANGE` | ✅ | 默认 | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_TABLES` | ❌ | 默认 | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_GOODS_CATEGORY` | ❌ | 默认 | ❌ | ✅ | ✅ | `NONE` | — |
| `ODS_STORE_GOODS` | ❌ | 默认 | ❌ | ✅ | ✅ | `FULL_TABLE` | — |
| `ODS_TABLE_FEE_DISCOUNT` | ❌ | 默认 | ❌ | ✅ | ✅ | `WINDOW` | `create_time` |
| `ODS_TENANT_GOODS` | ❌ | 默认 | ❌ | ✅ | ✅ | `FULL_TABLE` | — |

> - "默认" `time_fields` 表示 `(startTime, endTime)`
> - "—" 表示该参数为 `None` 或不适用
> - `skip_unchanged` 所有任务均为 `True`（默认值），基于 payload + is_delete 计算 content_hash
> - `ODS_RECHARGE_SETTLE` 是唯一手动映射 60+ 列的任务（其余任务按 DB schema 自动匹配）


---

## 去重策略

所有 22 个任务统一使用 `skip_unchanged=True`（默认值）：

- content_hash 基于原始 payload（`json.dumps(sort_keys=True, separators=(',',':'), ensure_ascii=False)`）+ `is_delete` 值计算 SHA-256
- 写入时与数据库中该业务 ID 最新版本的 content_hash 比对，相同则跳过
- 冲突处理模式由运行时配置 `run.ods_conflict_mode` 控制（默认 `update`）

## 软删除策略分类

| 策略 | `snapshot_mode` | 适用任务 | 机制 |
|------|:---:|----------|------|
| 全表快照 | `FULL_TABLE` | `ODS_ASSISTANT_ACCOUNT`、`ODS_MEMBER_CARD`、`ODS_GROUP_PACKAGE`、`ODS_STORE_GOODS`、`ODS_TENANT_GOODS`（5 个） | API 返回全量，不在返回集中的记录插入一条 `is_delete=1` 的删除版本行 |
| 窗口快照 | `WINDOW` | `ODS_TABLE_USE`、`ODS_ASSISTANT_LEDGER`、`ODS_STORE_GOODS_SALES`、`ODS_REFUND`、`ODS_PLATFORM_COUPON`、`ODS_MEMBER_BALANCE`、`ODS_GROUP_BUY_REDEMPTION`、`ODS_TABLE_FEE_DISCOUNT`（8 个） | 仅在 `snapshot_time_column` 指定的时间列窗口范围内做软删除 |
| 无快照 | `NONE` | 其余 9 个 | 纯增量写入，不做软删除 |

软删除语义：INSERT 一条 `is_delete=1` 的新版本行（而非 UPDATE 历史版本），保持 ODS 追加写入的一致性。若最新版本已是 `is_delete=1` 则跳过（幂等）。
