init: 项目初始提交 - NeoZQYY Monorepo 完整代码
This commit is contained in:
240
apps/etl/pipelines/feiqiu/docs/etl_tasks/ods_tasks.md
Normal file
240
apps/etl/pipelines/feiqiu/docs/etl_tasks/ods_tasks.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# ODS 层任务详解
|
||||
|
||||
> 本文档说明飞球 ETL 系统中 ODS(操作数据存储)层的所有任务。
|
||||
> ODS 层负责从上游 SaaS API 抽取原始业务数据并落地到 PostgreSQL(`billiards_ods` schema),保留源 payload 便于回溯。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
ODS 层采用**声明式配置**驱动的通用任务模式:由 `BaseOdsTask` + `OdsTaskSpec` 配置驱动,通过 `ODS_TASK_CLASSES` 字典动态注册,共 23 个任务。
|
||||
|
||||
所有 ODS 任务写入 `billiards_ods.*` 表,原始 API 响应以 JSON 格式存入 `payload` 列,元数据列(`fetched_at`、`source_file`、`content_hash` 等)自动填充。
|
||||
|
||||
> **历史说明**:早期版本曾有 14 个独立 ODS 任务(ORDERS、PAYMENTS、MEMBERS 等),写入不存在的 `billiards.*` schema。
|
||||
> 这些任务已于 2026-02-14 废弃删除,全部由下述通用 ODS 任务替代。
|
||||
|
||||
### 任务总览
|
||||
|
||||
| 任务代码 | 动态类名 | API 端点 | 目标 ODS 表 | 说明 |
|
||||
|----------|----------|----------|-------------|------|
|
||||
| `ODS_ASSISTANT_ACCOUNT` | `OdsAssistantAccountsTask` | `/PersonnelManagement/SearchAssistantInfo` | `assistant_accounts_master` | 助教账号档案 |
|
||||
| `ODS_SETTLEMENT_RECORDS` | `OdsOrderSettleTask` | `/Site/GetAllOrderSettleList` | `settlement_records` | 结账记录 |
|
||||
| `ODS_TABLE_USE` | `OdsTableUseTask` | `/Site/GetSiteTableOrderDetails` | `table_fee_transactions` | 台费计费流水 |
|
||||
| `ODS_ASSISTANT_LEDGER` | `OdsAssistantLedgerTask` | `/AssistantPerformance/GetOrderAssistantDetails` | `assistant_service_records` | 助教服务流水 |
|
||||
| `ODS_ASSISTANT_ABOLISH` | `OdsAssistantAbolishTask` | `/AssistantPerformance/GetAbolitionAssistant` | `assistant_cancellation_records` | 助教废除记录 |
|
||||
| `ODS_STORE_GOODS_SALES` | `OdsGoodsLedgerTask` | `/TenantGoods/GetGoodsSalesList` | `store_goods_sales_records` | 门店商品销售流水 |
|
||||
| `ODS_PAYMENT` | `OdsPaymentTask` | `/PayLog/GetPayLogListPage` | `payment_transactions` | 支付流水 |
|
||||
| `ODS_REFUND` | `OdsRefundTask` | `/Order/GetRefundPayLogList` | `refund_transactions` | 退款流水 |
|
||||
| `ODS_PLATFORM_COUPON` | `OdsCouponVerifyTask` | `/Promotion/GetOfflineCouponConsumePageList` | `platform_coupon_redemption_records` | 平台/团购券核销 |
|
||||
| `ODS_MEMBER` | `OdsMemberTask` | `/MemberProfile/GetTenantMemberList` | `member_profiles` | 会员档案 |
|
||||
| `ODS_MEMBER_CARD` | `OdsMemberCardTask` | `/MemberProfile/GetTenantMemberCardList` | `member_stored_value_cards` | 会员储值卡 |
|
||||
| `ODS_MEMBER_BALANCE` | `OdsMemberBalanceTask` | `/MemberProfile/GetMemberCardBalanceChange` | `member_balance_changes` | 会员余额变动 |
|
||||
| `ODS_RECHARGE_SETTLE` | `OdsRechargeSettleTask` | `/Site/GetRechargeSettleList` | `recharge_settlements` | 充值结算 |
|
||||
| `ODS_GROUP_PACKAGE` | `OdsPackageTask` | `/PackageCoupon/QueryPackageCouponList` | `group_buy_packages` | 团购套餐定义 |
|
||||
| `ODS_GROUP_BUY_REDEMPTION` | `OdsGroupBuyRedemptionTask` | `/Site/GetSiteTableUseDetails` | `group_buy_redemption_records` | 团购套餐核销 |
|
||||
| `ODS_INVENTORY_STOCK` | `OdsInventoryStockTask` | `/TenantGoods/GetGoodsStockReport` | `goods_stock_summary` | 库存汇总 |
|
||||
| `ODS_INVENTORY_CHANGE` | `OdsInventoryChangeTask` | `/GoodsStockManage/QueryGoodsOutboundReceipt` | `goods_stock_movements` | 库存变化记录 |
|
||||
| `ODS_TABLES` | `OdsTablesTask` | `/Table/GetSiteTables` | `site_tables_master` | 台桌维表 |
|
||||
| `ODS_GOODS_CATEGORY` | `OdsGoodsCategoryTask` | `/TenantGoodsCategory/QueryPrimarySecondaryCategory` | `stock_goods_category_tree` | 库存商品分类树 |
|
||||
| `ODS_STORE_GOODS` | `OdsStoreGoodsTask` | `/TenantGoods/GetGoodsInventoryList` | `store_goods_master` | 门店商品档案 |
|
||||
| `ODS_TABLE_FEE_DISCOUNT` | `OdsTableDiscountTask` | `/Site/GetTaiFeeAdjustList` | `table_fee_discount_records` | 台费折扣/调账 |
|
||||
| `ODS_TENANT_GOODS` | `OdsTenantGoodsTask` | `/TenantGoods/QueryTenantGoods` | `tenant_goods_master` | 租户商品档案 |
|
||||
| `ODS_SETTLEMENT_TICKET` | `OdsSettlementTicketTask` | `/Order/GetOrderSettleTicketNew` | `settlement_ticket_details` | 结账小票详情 |
|
||||
|
||||
> 所有目标表均位于 `billiards_ods` schema 下。
|
||||
|
||||
---
|
||||
|
||||
## 通用 ODS 任务架构(BaseOdsTask + OdsTaskSpec 模式)
|
||||
|
||||
通用 ODS 任务采用**声明式配置**驱动:开发者只需定义一个 `OdsTaskSpec` 数据类实例,由 `BaseOdsTask` 提供统一的 `execute()` 流程,再通过 `_build_task_class()` 工厂函数动态生成 Python 类,最终在 `ODS_TASK_CLASSES` 字典中注册。
|
||||
|
||||
核心优势:
|
||||
- **零代码新增任务**:只需添加一条 `OdsTaskSpec` 配置即可接入新的 API 端点
|
||||
- **Schema-aware 写入**:运行时从 `information_schema` 读取目标表结构,自动匹配列名和类型,无需手写字段映射
|
||||
- **统一去重与冲突处理**:通过 `content_hash` 和 `ON CONFLICT` 策略保证幂等性
|
||||
|
||||
### OdsTaskSpec 配置结构
|
||||
|
||||
`OdsTaskSpec` 是一个不可变数据类(`@dataclass(frozen=True)`),定义了单个 ODS 任务的全部配置。
|
||||
|
||||
#### 核心字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `code` | `str` | 任务代码,如 `ODS_PAYMENT`,用于注册和调度 |
|
||||
| `class_name` | `str` | 动态生成的 Python 类名,如 `OdsPaymentTask` |
|
||||
| `table_name` | `str` | 目标 ODS 表全限定名,如 `billiards_ods.payment_transactions` |
|
||||
| `endpoint` | `str` | 上游 API 端点路径,如 `/PayLog/GetPayLogListPage` |
|
||||
| `description` | `str` | 任务描述(中文),用于日志和文档 |
|
||||
|
||||
#### 分页与数据提取字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `data_path` | `Tuple[str, ...]` | `("data",)` | API 响应中数据的 JSON 路径,逐层深入 |
|
||||
| `list_key` | `str \| None` | `None` | 数据列表在 `data_path` 下的键名(如 `"settleList"`),为 `None` 时直接取 `data_path` 下的列表 |
|
||||
|
||||
#### 主键与列定义字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `pk_columns` | `Tuple[ColumnSpec, ...]` | `()` | 业务主键列定义,用于冲突检测(通常为 `id`) |
|
||||
| `extra_columns` | `Tuple[ColumnSpec, ...]` | `()` | 额外列定义,用于从嵌套 JSON 中提取特定字段 |
|
||||
| `conflict_columns_override` | `Tuple[str, ...] \| None` | `None` | 覆盖默认冲突列(默认使用表的 PRIMARY KEY) |
|
||||
|
||||
#### 时间窗口字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `requires_window` | `bool` | `True` | 是否需要时间窗口参数(事实表为 `True`,维度快照表为 `False`) |
|
||||
| `time_fields` | `Tuple[str, str] \| None` | `("startTime", "endTime")` | API 请求中时间窗口参数的键名对 |
|
||||
| `include_site_id` | `bool` | `True` | 是否在请求中传 `siteId` 参数 |
|
||||
| `extra_params` | `Dict[str, Any]` | `{}` | 额外的固定请求参数 |
|
||||
|
||||
#### 快照与软删除字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `snapshot_full_table` | `bool` | `False` | 全表快照模式:API 返回全量数据,不在返回集中的记录标记为已删除 |
|
||||
| `snapshot_window_columns` | `Tuple[str, ...] \| None` | `None` | 窗口快照模式:指定用于限定软删除范围的时间列 |
|
||||
|
||||
> **快照模式说明**:当 `snapshot_full_table=True` 或 `snapshot_window_columns` 非空时,任务会在每个分段结束后调用 `_mark_missing_as_deleted()`,将 API 未返回但数据库中存在的记录的 `is_delete` 标记为 `1`。此行为还需配合运行时配置 `run.snapshot_missing_delete=True` 才会生效。
|
||||
|
||||
#### 元数据控制字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `include_source_file` | `bool` | `True` | 是否写入 `source_file` 列 |
|
||||
| `include_source_endpoint` | `bool` | `True` | 是否写入 `source_endpoint` 列 |
|
||||
| `include_fetched_at` | `bool` | `True` | 是否写入 `fetched_at` 列 |
|
||||
| `include_record_index` | `bool` | `False` | 是否写入 `record_index` 列 |
|
||||
| `include_site_column` | `bool` | `True` | 是否写入 `site_id` / `store_id` 列 |
|
||||
|
||||
### ColumnSpec 列映射定义
|
||||
|
||||
`ColumnSpec` 是不可变数据类,定义单个列的映射规则:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `column` | `str` | 目标数据库列名 |
|
||||
| `sources` | `Tuple[str, ...]` | 源 JSON 字段名列表,按优先级回退(支持点号路径) |
|
||||
| `required` | `bool` | 是否为必填字段(缺失时跳过整条记录) |
|
||||
| `default` | `Any` | 默认值(源字段全部为空时使用) |
|
||||
| `transform` | `Callable \| None` | 类型转换函数 |
|
||||
|
||||
代码中提供了三个快捷构造函数:
|
||||
|
||||
| 函数 | 用途 | transform |
|
||||
|------|------|-----------|
|
||||
| `_int_col(name, *sources)` | 整数列 | `TypeParser.parse_int` |
|
||||
| `_decimal_col(name, *sources)` | 金额列(保留 2 位小数) | `TypeParser.parse_decimal(v, 2)` |
|
||||
| `_bool_col(name, *sources)` | 布尔列 | 自定义 `_to_bool` |
|
||||
|
||||
---
|
||||
|
||||
### BaseOdsTask 通用 execute 流程
|
||||
|
||||
所有通用 ODS 任务共享 `BaseOdsTask.execute()` 方法,流程如下:
|
||||
|
||||
```
|
||||
execute(cursor_data)
|
||||
│
|
||||
├── 1. 解析时间窗口(_resolve_window)
|
||||
│ ├── 优先级:用户手动覆盖 > 游标 + MAX(fetched_at) 兜底 > 默认窗口
|
||||
│ └── 若游标推进但表未实际入库,回退到 MAX(fetched_at) 作为起点
|
||||
│
|
||||
├── 2. 窗口分段(build_window_segments)
|
||||
│ └── 按闲忙时段或配置拆分为多个子窗口
|
||||
│
|
||||
├── 3. 准备运行参数
|
||||
│ ├── 读取 store_id、page_size
|
||||
│ ├── 解析快照模式配置
|
||||
│ ├── 获取表主键列(_get_table_pk_columns)
|
||||
│ └── 检查表是否有 is_delete 列
|
||||
│
|
||||
├── 4. 逐段执行(for seg_start, seg_end in segments)
|
||||
│ ├── 4a. 构建 API 请求参数(_build_params)
|
||||
│ ├── 4b. 分页抓取(api.iter_paginated)→ _insert_records_schema_aware 写入
|
||||
│ ├── 4c. 软删除标记(若快照模式启用)
|
||||
│ └── 4d. 提交事务(db.commit)
|
||||
│
|
||||
├── 5. 汇总结果
|
||||
│ └── 返回 {status, counts, window, segments, request_params}
|
||||
│
|
||||
└── 异常处理
|
||||
└── db.rollback + 记录错误日志 + 重新抛出
|
||||
```
|
||||
|
||||
#### Schema-aware 写入(`_insert_records_schema_aware`)
|
||||
|
||||
核心写入方法,运行时动态适配表结构:
|
||||
|
||||
1. **读取表结构**:从 `information_schema.columns` 获取目标表的所有列名、数据类型
|
||||
2. **读取主键**:从 `information_schema.table_constraints` 获取 PRIMARY KEY 列
|
||||
3. **记录合并**:`_merge_record_layers()` 将嵌套 JSON 展平为单层字典
|
||||
4. **is_delete 标准化**:统一为 `0/1`
|
||||
5. **content_hash 计算**:对记录内容计算 SHA-256 哈希
|
||||
6. **content_hash 去重**:与数据库中同一业务主键的最新 `content_hash` 比对,相同则跳过
|
||||
7. **值映射**:逐列匹配,特殊列(`payload`、`source_file`、`fetched_at`、`content_hash`)自动填充
|
||||
8. **冲突处理**:根据 `run.ods_conflict_mode` 配置:
|
||||
|
||||
| 模式 | SQL 行为 | 说明 |
|
||||
|------|----------|------|
|
||||
| `update` | `ON CONFLICT ... DO UPDATE SET ... WHERE IS DISTINCT FROM` | 全字段对比,仅在有变化时更新 |
|
||||
| `backfill` | `ON CONFLICT ... DO UPDATE SET COALESCE(existing, new) WHERE ... IS NULL` | 仅回填 NULL 列 |
|
||||
| `nothing` | `ON CONFLICT ... DO NOTHING` | 跳过已存在记录 |
|
||||
|
||||
9. **批量写入**:使用 `psycopg2.extras.execute_values` 分块写入,通过 `RETURNING (xmax = 0)` 区分插入和更新
|
||||
|
||||
#### 软删除标记(`_mark_missing_as_deleted`)
|
||||
|
||||
当快照模式启用时,任务在每个分段结束后执行软删除:
|
||||
|
||||
- **全表快照**(`snapshot_full_table=True`):将数据库中所有 `is_delete != 1` 且不在本次 API 返回集中的记录标记为 `is_delete=1`
|
||||
- **窗口快照**(`snapshot_window_columns` 非空):仅在指定时间列的窗口范围内执行软删除
|
||||
|
||||
---
|
||||
|
||||
### content_hash 去重机制
|
||||
|
||||
`content_hash` 是通用 ODS 任务的核心去重手段:
|
||||
|
||||
1. **计算**:排除元数据字段后,对剩余字段按 key 排序后 JSON 序列化,计算 SHA-256 哈希
|
||||
2. **比对**:从数据库中按业务主键取最新一条记录的 `content_hash`
|
||||
3. **跳过**:若新记录的 `content_hash` 与数据库中最新记录相同,则跳过写入
|
||||
|
||||
> 仅在目标表包含 `content_hash` 列且有 `fetched_at` 列时生效。
|
||||
|
||||
---
|
||||
|
||||
### 各任务详细配置
|
||||
|
||||
| 任务代码 | 需要窗口 | 快照模式 | 特殊说明 |
|
||||
|----------|----------|----------|----------|
|
||||
| `ODS_ASSISTANT_ACCOUNT` | 是 | 全表快照 | 助教账号档案,全量抓取后标记离职/删除 |
|
||||
| `ODS_SETTLEMENT_RECORDS` | 是 | — | 结账记录,按时间窗口增量抓取 |
|
||||
| `ODS_TABLE_USE` | 否 | 窗口(`create_time`) | 台费计费流水 |
|
||||
| `ODS_ASSISTANT_LEDGER` | 是 | 窗口(`create_time`) | 助教服务流水 |
|
||||
| `ODS_ASSISTANT_ABOLISH` | 是 | — | 助教废除记录 |
|
||||
| `ODS_STORE_GOODS_SALES` | 否 | 窗口(`create_time`) | 门店商品销售流水 |
|
||||
| `ODS_PAYMENT` | 否 | — | 支付流水 |
|
||||
| `ODS_REFUND` | 否 | 窗口(`pay_time`) | 退款流水 |
|
||||
| `ODS_PLATFORM_COUPON` | 否 | 窗口(`consume_time`) | 平台/团购券核销 |
|
||||
| `ODS_MEMBER` | 否 | — | 会员档案 |
|
||||
| `ODS_MEMBER_CARD` | 否 | 全表快照 | 会员储值卡 |
|
||||
| `ODS_MEMBER_BALANCE` | 否 | 窗口(`create_time`) | 会员余额变动 |
|
||||
| `ODS_RECHARGE_SETTLE` | 是 | — | 充值结算 |
|
||||
| `ODS_GROUP_PACKAGE` | 否 | 全表快照 | 团购套餐定义 |
|
||||
| `ODS_GROUP_BUY_REDEMPTION` | 否 | 窗口(`create_time`) | 团购套餐核销 |
|
||||
| `ODS_INVENTORY_STOCK` | 否 | — | 库存汇总 |
|
||||
| `ODS_INVENTORY_CHANGE` | 是 | — | 库存变化记录 |
|
||||
| `ODS_TABLES` | 否 | — | 台桌维表 |
|
||||
| `ODS_GOODS_CATEGORY` | 否 | — | 库存商品分类树 |
|
||||
| `ODS_STORE_GOODS` | 否 | 全表快照 | 门店商品档案 |
|
||||
| `ODS_TABLE_FEE_DISCOUNT` | 否 | 窗口(`create_time`) | 台费折扣/调账 |
|
||||
| `ODS_TENANT_GOODS` | 否 | 全表快照 | 租户商品档案 |
|
||||
| `ODS_SETTLEMENT_TICKET` | 否 | — | 结账小票详情(专用实现,见下文) |
|
||||
|
||||
> **特殊任务**:`ODS_SETTLEMENT_TICKET` 虽然在 `ODS_TASK_SPECS` 中声明,但其 `ODS_TASK_CLASSES` 条目被 `OdsSettlementTicketTask` 专用实现覆盖。该任务不走标准分页抓取流程,而是先从 `payment_transactions` 表或支付 API 收集 `orderSettleId`,再逐个调用小票接口获取详情。
|
||||
Reference in New Issue
Block a user