init: 项目初始提交 - NeoZQYY Monorepo 完整代码

This commit is contained in:
Neo
2026-02-15 14:58:14 +08:00
commit ded6dfb9d8
769 changed files with 182616 additions and 0 deletions

View 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`,再逐个调用小票接口获取详情。