init: 项目初始提交 - NeoZQYY Monorepo 完整代码
This commit is contained in:
346
apps/etl/pipelines/feiqiu/docs/etl_tasks/README.md
Normal file
346
apps/etl/pipelines/feiqiu/docs/etl_tasks/README.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# 飞球 ETL 任务说明文档
|
||||
|
||||
> 本文档是飞球 ETL 系统(etl-billiards)任务说明的总览入口。
|
||||
> 系统从上游 SaaS API 抽取台球门店运营数据,经 ODS → DWD → DWS 三层处理后,
|
||||
> 输出助教业绩、财务日报、会员分析、工资计算及自定义指数等业务报表。
|
||||
|
||||
## 目录
|
||||
|
||||
- [数据流向](#数据流向)
|
||||
- [文档索引](#文档索引)
|
||||
- [任务清单](#任务清单)
|
||||
- [ODS 层(操作数据存储)](#ods-层操作数据存储)
|
||||
- [DWD 层(明细数据)](#dwd-层明细数据)
|
||||
- [DWS 层(数据服务)](#dws-层数据服务)
|
||||
- [INDEX 层(指数算法)](#index-层指数算法)
|
||||
- [工具类 / 校验类](#工具类--校验类)
|
||||
- [管道类型](#管道类型)
|
||||
- [处理模式](#处理模式)
|
||||
- [数据源模式](#数据源模式)
|
||||
- [CLI 参数速查表](#cli-参数速查表)
|
||||
- [常见命令示例](#常见命令示例)
|
||||
|
||||
---
|
||||
|
||||
## 数据流向
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
API["上游 SaaS API"] -->|抓取| ODS["ODS<br/>操作数据存储层"]
|
||||
JSON["本地 JSON 文件"] -->|手动入库| ODS
|
||||
ODS -->|清洗 / SCD2 / 增量| DWD["DWD<br/>明细数据层"]
|
||||
DWD -->|聚合 / 计算| DWS["DWS<br/>数据服务层"]
|
||||
DWD -->|指数算法| INDEX["INDEX<br/>指数算法层"]
|
||||
DWS --> REPORT["业务报表<br/>助教业绩 · 财务日报<br/>会员分析 · 工资计算"]
|
||||
INDEX --> REPORT
|
||||
```
|
||||
|
||||
**层级说明:**
|
||||
|
||||
| 层 | Schema | 职责 |
|
||||
|---|---|---|
|
||||
| ODS | `billiards_ods` | 保留 API 原始 payload,便于回溯 |
|
||||
| DWD | `billiards_dwd` | 清洗后的维度表(dim_*,SCD2)和事实表(fact_* / dwd_*,增量) |
|
||||
| DWS | `billiards_dws` | 按业务维度聚合的汇总统计表 |
|
||||
| INDEX | `billiards_dws` | 基于 DWD/DWS 数据计算的自定义业务指数 |
|
||||
|
||||
---
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [BaseTask 公共机制](base_task_mechanism.md) | 任务基类模板方法、TaskContext、时间窗口、注册表、管道执行 |
|
||||
| [ODS 层任务](ods_tasks.md) | 23 个通用 ODS 任务的架构、配置结构、API 端点、目标表 |
|
||||
| [DWD 层任务](dwd_tasks.md) | DWD_LOAD_FROM_ODS 核心装载、SCD2 处理、质量校验 |
|
||||
| [DWS 层任务](dws_tasks.md) | 助教业绩、会员分析、财务统计、运维任务共 15 个 DWS 任务 |
|
||||
| [INDEX 层任务](index_tasks.md) | WBI/NCI/RS 指数算法 + ML 手动台账导入 |
|
||||
| [工具类任务](utility_tasks.md) | Schema 初始化、手动入库、归档、截止检查、完整性校验 |
|
||||
|
||||
---
|
||||
|
||||
## 任务清单
|
||||
|
||||
### ODS 层(操作数据存储)
|
||||
|
||||
#### 通用 ODS 任务(OdsTaskSpec 动态注册)
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 简要说明 | 详情 |
|
||||
|----------|-----------|--------|----------|------|
|
||||
| `ODS_ASSISTANT_ACCOUNT` | `OdsAssistantAccountsTask` | `billiards_ods.assistant_accounts_master` | 助教账号档案 | [查看](ods_tasks.md) |
|
||||
| `ODS_ASSISTANT_LEDGER` | `OdsAssistantLedgerTask` | `billiards_ods.assistant_service_records` | 助教服务流水 | [查看](ods_tasks.md) |
|
||||
| `ODS_ASSISTANT_ABOLISH` | `OdsAssistantAbolishTask` | `billiards_ods.assistant_cancellation_records` | 助教废除记录 | [查看](ods_tasks.md) |
|
||||
| `ODS_INVENTORY_CHANGE` | `OdsInventoryChangeTask` | `billiards_ods.goods_stock_movements` | 库存变化记录 | [查看](ods_tasks.md) |
|
||||
| `ODS_INVENTORY_STOCK` | `OdsInventoryStockTask` | `billiards_ods.goods_stock_summary` | 库存汇总 | [查看](ods_tasks.md) |
|
||||
| `ODS_GROUP_PACKAGE` | `OdsPackageTask` | `billiards_ods.group_buy_packages` | 团购套餐定义 | [查看](ods_tasks.md) |
|
||||
| `ODS_GROUP_BUY_REDEMPTION` | `OdsGroupBuyRedemptionTask` | `billiards_ods.group_buy_redemption_records` | 团购套餐核销 | [查看](ods_tasks.md) |
|
||||
| `ODS_MEMBER` | `OdsMemberTask` | `billiards_ods.member_profiles` | 会员档案 | [查看](ods_tasks.md) |
|
||||
| `ODS_MEMBER_BALANCE` | `OdsMemberBalanceTask` | `billiards_ods.member_balance_changes` | 会员余额变动 | [查看](ods_tasks.md) |
|
||||
| `ODS_MEMBER_CARD` | `OdsMemberCardTask` | `billiards_ods.member_stored_value_cards` | 会员储值卡 | [查看](ods_tasks.md) |
|
||||
| `ODS_PAYMENT` | `OdsPaymentTask` | `billiards_ods.payment_transactions` | 支付流水 | [查看](ods_tasks.md) |
|
||||
| `ODS_REFUND` | `OdsRefundTask` | `billiards_ods.refund_transactions` | 退款流水 | [查看](ods_tasks.md) |
|
||||
| `ODS_PLATFORM_COUPON` | `OdsCouponVerifyTask` | `billiards_ods.platform_coupon_redemption_records` | 平台/团购券核销 | [查看](ods_tasks.md) |
|
||||
| `ODS_RECHARGE_SETTLE` | `OdsRechargeSettleTask` | `billiards_ods.recharge_settlements` | 充值结算 | [查看](ods_tasks.md) |
|
||||
| `ODS_TABLE_USE` | `OdsTableUseTask` | `billiards_ods.table_fee_transactions` | 台费计费流水 | [查看](ods_tasks.md) |
|
||||
| `ODS_TABLES` | `OdsTablesTask` | `billiards_ods.site_tables_master` | 台桌维表 | [查看](ods_tasks.md) |
|
||||
| `ODS_GOODS_CATEGORY` | `OdsGoodsCategoryTask` | `billiards_ods.stock_goods_category_tree` | 库存商品分类 | [查看](ods_tasks.md) |
|
||||
| `ODS_STORE_GOODS` | `OdsStoreGoodsTask` | `billiards_ods.store_goods_master` | 门店商品档案 | [查看](ods_tasks.md) |
|
||||
| `ODS_TABLE_FEE_DISCOUNT` | `OdsTableDiscountTask` | `billiards_ods.table_fee_discount_records` | 台费折扣/调账 | [查看](ods_tasks.md) |
|
||||
| `ODS_STORE_GOODS_SALES` | `OdsGoodsLedgerTask` | `billiards_ods.store_goods_sales_records` | 门店商品销售流水 | [查看](ods_tasks.md) |
|
||||
| `ODS_TENANT_GOODS` | `OdsTenantGoodsTask` | `billiards_ods.tenant_goods_master` | 租户商品档案 | [查看](ods_tasks.md) |
|
||||
| `ODS_SETTLEMENT_TICKET` | `OdsSettlementTicketTask` | `billiards_ods.settlement_ticket_details` | 结账小票详情 | [查看](ods_tasks.md) |
|
||||
| `ODS_SETTLEMENT_RECORDS` | `OdsOrderSettleTask` | `billiards_ods.settlement_records` | 结账记录 | [查看](ods_tasks.md) |
|
||||
|
||||
### DWD 层(明细数据)
|
||||
|
||||
| 任务代码 | Python 类 | 简要说明 | 详情 |
|
||||
|----------|-----------|----------|------|
|
||||
| `DWD_LOAD_FROM_ODS` | `DwdLoadTask` | 核心装载:遍历 TABLE_MAP,维度走 SCD2,事实走增量 | [查看](dwd_tasks.md) |
|
||||
| `DWD_QUALITY_CHECK` | `DwdQualityTask` | ODS 与 DWD 行数/金额核对,输出 JSON 报表 | [查看](dwd_tasks.md) |
|
||||
|
||||
### DWS 层(数据服务)
|
||||
|
||||
#### 助教业绩域
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 粒度 | 详情 |
|
||||
|----------|-----------|--------|------|------|
|
||||
| `DWS_ASSISTANT_DAILY` | `AssistantDailyTask` | `dws_assistant_daily_detail` | 日期+助教 | [查看](dws_tasks.md) |
|
||||
| `DWS_ASSISTANT_MONTHLY` | `AssistantMonthlyTask` | `dws_assistant_monthly_summary` | 月份+助教 | [查看](dws_tasks.md) |
|
||||
| `DWS_ASSISTANT_CUSTOMER` | `AssistantCustomerTask` | `dws_assistant_customer_stats` | 日期+助教+会员 | [查看](dws_tasks.md) |
|
||||
| `DWS_ASSISTANT_SALARY` | `AssistantSalaryTask` | `dws_assistant_salary_calc` | 月份+助教 | [查看](dws_tasks.md) |
|
||||
| `DWS_ASSISTANT_FINANCE` | `AssistantFinanceTask` | `dws_assistant_finance_analysis` | 日期+助教 | [查看](dws_tasks.md) |
|
||||
|
||||
#### 会员分析域
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 粒度 | 详情 |
|
||||
|----------|-----------|--------|------|------|
|
||||
| `DWS_MEMBER_CONSUMPTION` | `MemberConsumptionTask` | `dws_member_consumption_summary` | 日期+会员 | [查看](dws_tasks.md) |
|
||||
| `DWS_MEMBER_VISIT` | `MemberVisitTask` | `dws_member_visit_detail` | 日期+会员+结账单 | [查看](dws_tasks.md) |
|
||||
|
||||
#### 财务统计域
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 粒度 | 详情 |
|
||||
|----------|-----------|--------|------|------|
|
||||
| `DWS_FINANCE_DAILY` | `FinanceDailyTask` | `dws_finance_daily_summary` | 日期 | [查看](dws_tasks.md) |
|
||||
| `DWS_FINANCE_RECHARGE` | `FinanceRechargeTask` | `dws_finance_recharge_summary` | 日期 | [查看](dws_tasks.md) |
|
||||
| `DWS_FINANCE_INCOME_STRUCTURE` | `FinanceIncomeStructureTask` | `dws_finance_income_structure` | 日期+收入类型 | [查看](dws_tasks.md) |
|
||||
| `DWS_FINANCE_DISCOUNT_DETAIL` | `FinanceDiscountDetailTask` | `dws_finance_discount_detail` | 日期+折扣类型 | [查看](dws_tasks.md) |
|
||||
|
||||
#### 运维任务
|
||||
|
||||
| 任务代码 | Python 类 | 简要说明 | 详情 |
|
||||
|----------|-----------|----------|------|
|
||||
| `DWS_BUILD_ORDER_SUMMARY` | `DwsBuildOrderSummaryTask` | 构建订单汇总中间表 | [查看](dws_tasks.md) |
|
||||
| `DWS_RETENTION_CLEANUP` | `DwsRetentionCleanupTask` | 按时间分层清理历史数据 | [查看](dws_tasks.md) |
|
||||
| `DWS_MV_REFRESH_FINANCE_DAILY` | `DwsMvRefreshFinanceDailyTask` | 刷新财务日报物化视图 | [查看](dws_tasks.md) |
|
||||
| `DWS_MV_REFRESH_ASSISTANT_DAILY` | `DwsMvRefreshAssistantDailyTask` | 刷新助教日报物化视图 | [查看](dws_tasks.md) |
|
||||
|
||||
### INDEX 层(指数算法)
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 指数类型 | 详情 |
|
||||
|----------|-----------|--------|----------|------|
|
||||
| `DWS_WINBACK_INDEX` | `WinbackIndexTask` | `dws_member_winback_index` | WBI(回流指数) | [查看](index_tasks.md) |
|
||||
| `DWS_NEWCONV_INDEX` | `NewconvIndexTask` | `dws_member_newconv_index` | NCI(新客转化指数) | [查看](index_tasks.md) |
|
||||
| `DWS_RELATION_INDEX` | `RelationIndexTask` | `dws_relation_index` | RS(关系指数) | [查看](index_tasks.md) |
|
||||
| `DWS_ML_MANUAL_IMPORT` | `MlManualImportTask` | `dws_ml_manual_ledger` | ML(手动台账导入) | [查看](index_tasks.md) |
|
||||
|
||||
### 工具类 / 校验类
|
||||
|
||||
| 任务代码 | Python 类 | 类型 | 简要说明 | 详情 |
|
||||
|----------|-----------|------|----------|------|
|
||||
| `INIT_ODS_SCHEMA` | `InitOdsSchemaTask` | utility | 执行 ODS + etl_admin DDL,创建必要目录 | [查看](utility_tasks.md) |
|
||||
| `INIT_DWD_SCHEMA` | `InitDwdSchemaTask` | utility | 执行 DWD DDL | [查看](utility_tasks.md) |
|
||||
| `INIT_DWS_SCHEMA` | `InitDwsSchemaTask` | utility | 执行 DWS DDL | [查看](utility_tasks.md) |
|
||||
| `MANUAL_INGEST` | `ManualIngestTask` | utility | 从本地 JSON 文件手动入库到 ODS | [查看](utility_tasks.md) |
|
||||
| `ODS_JSON_ARCHIVE` | `OdsJsonArchiveTask` | utility | 归档 ODS JSON 文件 | [查看](utility_tasks.md) |
|
||||
| `CHECK_CUTOFF` | `CheckCutoffTask` | utility | 检查数据截止时间 | [查看](utility_tasks.md) |
|
||||
| `SEED_DWS_CONFIG` | `SeedDwsConfigTask` | utility | 初始化 DWS 配置种子数据 | [查看](utility_tasks.md) |
|
||||
| `DATA_INTEGRITY_CHECK` | `DataIntegrityTask` | verification | 数据完整性校验 | [查看](utility_tasks.md) |
|
||||
|
||||
---
|
||||
|
||||
## 管道类型
|
||||
|
||||
管道(Pipeline)定义了多层任务的执行顺序。通过 `--pipeline` 参数指定,系统自动解析对应层并按顺序执行该层的所有已注册任务。
|
||||
|
||||
| 管道类型 | 包含层 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `api_ods` | ODS | 仅从 API 抓取数据到 ODS |
|
||||
| `api_ods_dwd` | ODS → DWD | 抓取数据并清洗装载到 DWD |
|
||||
| `api_full` | ODS → DWD → DWS → INDEX | 全流程:抓取 → 清洗 → 汇总 → 指数 |
|
||||
| `ods_dwd` | DWD | 仅执行 ODS → DWD 清洗装载(不抓取) |
|
||||
| `dwd_dws` | DWS | 仅执行 DWD → DWS 汇总计算 |
|
||||
| `dwd_dws_index` | DWS → INDEX | 汇总计算 + 指数算法 |
|
||||
| `dwd_index` | INDEX | 仅执行指数算法 |
|
||||
|
||||
> 管道定义位于 `orchestration/pipeline_runner.py` 的 `PipelineRunner.PIPELINE_LAYERS`。
|
||||
|
||||
---
|
||||
|
||||
## 处理模式
|
||||
|
||||
通过 `--processing-mode` 参数指定,控制管道的执行行为。
|
||||
|
||||
| 模式 | 说明 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `increment_only` | 仅增量处理(默认) | 日常定时调度,只处理新增/变更数据 |
|
||||
| `verify_only` | 仅校验并修复,跳过增量 ETL | 数据质量巡检、手动修复不一致 |
|
||||
| `increment_verify` | 先增量处理,再校验并修复 | 需要确保数据一致性的关键批次 |
|
||||
|
||||
**补充参数:**
|
||||
|
||||
- `--fetch-before-verify`:仅在 `verify_only` 模式下有效,校验前先从 API 获取最新数据
|
||||
- `--verify-tables`:指定仅校验的表名(逗号分隔),用于单表验证
|
||||
|
||||
---
|
||||
|
||||
## 数据源模式
|
||||
|
||||
通过 `--data-source` 参数指定,控制 ODS 层的数据来源。
|
||||
|
||||
| 模式 | 说明 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `online` | 仅在线抓取(从 API 获取数据) | 正常运行,网络可用 |
|
||||
| `offline` | 仅本地入库(从 JSON 文件读取) | 离线环境、JSON 回放 |
|
||||
| `hybrid` | 抓取 + 入库(默认) | 同时从 API 抓取并处理本地文件 |
|
||||
|
||||
> 旧参数 `--pipeline-flow`(`FULL` / `FETCH_ONLY` / `INGEST_ONLY`)已弃用,请使用 `--data-source`。
|
||||
|
||||
---
|
||||
|
||||
## CLI 参数速查表
|
||||
|
||||
入口命令:`python -m cli.main`
|
||||
|
||||
### 基本参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--store-id` | int | — | 门店 ID |
|
||||
| `--tasks` | str | — | 任务列表,逗号分隔(传统模式) |
|
||||
| `--dry-run` | flag | `false` | 试运行,不提交数据库事务 |
|
||||
|
||||
### 管道与模式参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--pipeline` | choice | — | 管道类型(见[管道类型](#管道类型)) |
|
||||
| `--processing-mode` | choice | `increment_only` | 处理模式(见[处理模式](#处理模式)) |
|
||||
| `--data-source` | choice | `hybrid` | 数据源模式(见[数据源模式](#数据源模式)) |
|
||||
| `--fetch-before-verify` | flag | `false` | 校验前先从 API 获取数据(仅 `verify_only`) |
|
||||
| `--verify-tables` | str | — | 仅校验指定表(逗号分隔) |
|
||||
|
||||
### 时间窗口参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--window-start` | datetime | — | 固定时间窗口开始(优先级高于游标) |
|
||||
| `--window-end` | datetime | — | 固定时间窗口结束 |
|
||||
| `--force-window-override` | flag | `false` | 强制使用 window_start/window_end,不走 MAX(fetched_at) 兜底 |
|
||||
| `--window-split` | choice | `none` | 时间窗口切分:`none` / `day` / `week` / `month` |
|
||||
| `--window-split-unit` | str | 配置值 | 窗口切分单位(`day`/`week`/`month`/`none`) |
|
||||
| `--window-split-days` | int | 配置值 | 按天切分的天数(`1`/`10`/`30`) |
|
||||
| `--window-compensation-hours` | int | 配置值 | 窗口前后补偿小时数 |
|
||||
| `--lookback-hours` | int | `24` | 回溯小时数 |
|
||||
| `--overlap-seconds` | int | `3600` | 冗余秒数(默认 1 小时) |
|
||||
|
||||
### 数据库参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--pg-dsn` | str | — | PostgreSQL DSN 连接串 |
|
||||
| `--pg-host` | str | — | PostgreSQL 主机 |
|
||||
| `--pg-port` | int | — | PostgreSQL 端口 |
|
||||
| `--pg-name` | str | — | PostgreSQL 数据库名 |
|
||||
| `--pg-user` | str | — | PostgreSQL 用户名 |
|
||||
| `--pg-password` | str | — | PostgreSQL 密码 |
|
||||
|
||||
### API 参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--api-base` | str | — | API 基础 URL |
|
||||
| `--api-token` | str | — | API 令牌(Bearer Token) |
|
||||
| `--api-timeout` | int | — | API 超时(秒) |
|
||||
| `--api-page-size` | int | — | 分页大小 |
|
||||
| `--api-retry-max` | int | — | API 重试最大次数 |
|
||||
|
||||
### 目录与运行参数
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `--export-root` | str | — | 导出根目录 |
|
||||
| `--log-root` | str | — | 日志根目录 |
|
||||
| `--fetch-root` | str | — | 抓取 JSON 输出根目录 |
|
||||
| `--ingest-source` | str | — | 本地清洗入库源目录 |
|
||||
| `--write-pretty-json` | flag | `false` | 抓取 JSON 美化输出 |
|
||||
| `--idle-start` | str | — | 闲时窗口开始(HH:MM) |
|
||||
| `--idle-end` | str | — | 闲时窗口结束(HH:MM) |
|
||||
| `--allow-empty-advance` | flag | `false` | 允许空结果推进窗口 |
|
||||
|
||||
### 已弃用参数
|
||||
|
||||
| 参数 | 替代方案 | 说明 |
|
||||
|------|----------|------|
|
||||
| `--pipeline-flow` | `--data-source` | `FULL` → `hybrid`,`FETCH_ONLY` → `online`,`INGEST_ONLY` → `offline` |
|
||||
|
||||
---
|
||||
|
||||
## 常见命令示例
|
||||
|
||||
```bash
|
||||
# 全流程 ETL(API 抓取 → ODS → DWD → DWS → INDEX)
|
||||
python -m cli.main --pipeline api_full --pg-dsn "$PG_DSN" --store-id 1 --api-token "$TOKEN"
|
||||
|
||||
# 仅抓取 ODS 数据
|
||||
python -m cli.main --pipeline api_ods --store-id 1
|
||||
|
||||
# ODS → DWD 清洗装载(不抓取 API)
|
||||
python -m cli.main --pipeline ods_dwd
|
||||
|
||||
# 仅执行 DWS 汇总
|
||||
python -m cli.main --pipeline dwd_dws
|
||||
|
||||
# 仅执行指数算法
|
||||
python -m cli.main --pipeline dwd_index
|
||||
|
||||
# 指定时间窗口
|
||||
python -m cli.main --pipeline api_ods --window-start "2026-02-01" --window-end "2026-02-02"
|
||||
|
||||
# 按天切分时间窗口
|
||||
python -m cli.main --pipeline api_ods --window-start "2026-01-01" --window-end "2026-02-01" --window-split day
|
||||
|
||||
# 传统模式:指定任务列表
|
||||
python -m cli.main --tasks ODS_PAYMENT,ODS_MEMBER,ODS_SETTLEMENT_RECORDS --store-id 1
|
||||
|
||||
# 校验并修复(跳过增量)
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only
|
||||
|
||||
# 校验前先从 API 获取数据
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only --fetch-before-verify
|
||||
|
||||
# 增量 + 校验
|
||||
python -m cli.main --pipeline api_full --processing-mode increment_verify
|
||||
|
||||
# 仅校验指定表
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only --verify-tables "dim_member,fact_payment"
|
||||
|
||||
# 试运行(不提交)
|
||||
python -m cli.main --dry-run --tasks DWD_LOAD_FROM_ODS
|
||||
|
||||
# Schema 初始化
|
||||
python -m cli.main --tasks INIT_ODS_SCHEMA,INIT_DWD_SCHEMA,INIT_DWS_SCHEMA
|
||||
|
||||
# 手动入库(离线模式)
|
||||
python -m cli.main --tasks MANUAL_INGEST --data-source offline --ingest-source ./data/json
|
||||
|
||||
# DWS 配置种子数据初始化
|
||||
python -m cli.main --tasks SEED_DWS_CONFIG
|
||||
|
||||
# 数据完整性校验
|
||||
python -m cli.main --tasks DATA_INTEGRITY_CHECK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> 最后更新日期:2026-02-14
|
||||
435
apps/etl/pipelines/feiqiu/docs/etl_tasks/base_task_mechanism.md
Normal file
435
apps/etl/pipelines/feiqiu/docs/etl_tasks/base_task_mechanism.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# BaseTask 公共机制与执行参数
|
||||
|
||||
> 本文档说明飞球 ETL 系统中所有任务共享的基类机制、运行期上下文、时间窗口计算、任务注册以及管道执行流程。
|
||||
> 面向开发者,便于在开发新任务时遵循统一模式。
|
||||
|
||||
---
|
||||
|
||||
## 1. BaseTask 模板方法流程
|
||||
|
||||
`BaseTask`(位于 `tasks/base_task.py`)采用经典的**模板方法模式**,定义了 ETL 任务的标准执行骨架。所有具体任务(ODS / DWD / DWS / INDEX)均继承此基类。
|
||||
|
||||
### 1.1 核心方法签名
|
||||
|
||||
```python
|
||||
class BaseTask:
|
||||
def __init__(self, config, db_connection, api_client, logger): ...
|
||||
def get_task_code(self) -> str: ... # 子类必须实现
|
||||
def extract(self, context: TaskContext): ... # 子类必须实现
|
||||
def transform(self, extracted, context: TaskContext): ... # 默认直接返回 extracted
|
||||
def load(self, transformed, context: TaskContext) -> dict: ... # 子类必须实现
|
||||
def execute(self, cursor_data: dict | None = None) -> dict: ... # 主入口
|
||||
```
|
||||
|
||||
### 1.2 execute() 执行流程
|
||||
|
||||
`execute()` 是任务的统一入口,由调度器(TaskExecutor)调用。完整流程如下:
|
||||
|
||||
```
|
||||
execute(cursor_data)
|
||||
│
|
||||
├─ 1. _build_context(cursor_data) → 构建 TaskContext(含时间窗口计算)
|
||||
│
|
||||
├─ 2. build_window_segments(...) → 按配置切分时间窗口为多段
|
||||
│ 若无切分配置 → 退化为单段 [(window_start, window_end)]
|
||||
│
|
||||
├─ 3. 遍历每个窗口段 (window_start, window_end):
|
||||
│ │
|
||||
│ ├─ _build_context_for_window(...) → 为当前段构建独立 TaskContext
|
||||
│ │
|
||||
│ ├─ extract(context) → 从数据源提取数据
|
||||
│ │
|
||||
│ ├─ transform(extracted, context) → 清洗/转换数据
|
||||
│ │
|
||||
│ ├─ load(transformed, context) → 写入目标表,返回统计 counts
|
||||
│ │
|
||||
│ ├─ db.commit() → 提交事务
|
||||
│ │
|
||||
│ └─ _accumulate_counts(...) → 累加各段统计
|
||||
│
|
||||
└─ 4. 构建并返回结果字典
|
||||
{status, counts, window: {start, end, minutes}, segments: [...]}
|
||||
```
|
||||
|
||||
**关键行为:**
|
||||
|
||||
- 每个窗口段独立执行 E/T/L 三步,段内失败会 `db.rollback()` 并抛出异常
|
||||
- 多段执行时,日志会输出进度信息(已处理天数 / 总天数)
|
||||
- `transform()` 默认实现为直接返回 `extracted`,子类可选择性覆盖
|
||||
- 统计累加逻辑:数值类型(int/float)求和,其他类型取首次出现的值
|
||||
|
||||
---
|
||||
|
||||
## 2. TaskContext 字段含义
|
||||
|
||||
`TaskContext` 是一个不可变数据类(`frozen=True`),在 E/T/L 各阶段间传递运行期信息。
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class TaskContext:
|
||||
store_id: int # 门店 ID
|
||||
window_start: datetime # 时间窗口起始(含时区)
|
||||
window_end: datetime # 时间窗口结束(含时区)
|
||||
window_minutes: int # 窗口时长(分钟)
|
||||
cursor: dict | None = None # 游标数据(来自上次运行记录)
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `store_id` | `int` | 当前门店标识,从 `config.get("app.store_id")` 读取 |
|
||||
| `window_start` | `datetime` | 本次抽取的时间窗口起点,带时区信息 |
|
||||
| `window_end` | `datetime` | 本次抽取的时间窗口终点,带时区信息 |
|
||||
| `window_minutes` | `int` | `window_end - window_start` 的分钟数(最小为 1) |
|
||||
| `cursor` | `dict \| None` | 调度器传入的游标字典,通常包含 `last_end`(上次窗口终点)等字段;首次运行时为 `None` |
|
||||
|
||||
---
|
||||
|
||||
## 3. 时间窗口计算逻辑
|
||||
|
||||
时间窗口由 `_get_time_window(cursor_data)` 方法计算,决定了本次任务抽取的数据范围。优先级从高到低:
|
||||
|
||||
### 3.1 优先级一:手动覆盖(Manual Override)
|
||||
|
||||
当配置中同时存在 `run.window_override.start` 和 `run.window_override.end` 时,直接使用用户指定的窗口。
|
||||
|
||||
- 对应 CLI 参数:`--window-start` / `--window-end`
|
||||
- 支持字符串自动解析(通过 `dateutil.parser.parse`)
|
||||
- 无时区信息时自动附加 `app.timezone`(默认 `Asia/Shanghai`)
|
||||
- 校验:两者必须同时提供,且 `end > start`
|
||||
|
||||
```
|
||||
window_start = run.window_override.start
|
||||
window_end = run.window_override.end
|
||||
window_minutes = (end - start) 的分钟数
|
||||
```
|
||||
|
||||
### 3.2 优先级二:游标续跑(Cursor Resume)
|
||||
|
||||
当存在游标数据且包含 `last_end` 字段时,从上次结束位置回退一段重叠时间开始:
|
||||
|
||||
```
|
||||
window_start = cursor["last_end"] - overlap_seconds
|
||||
window_end = now
|
||||
```
|
||||
|
||||
- `overlap_seconds` 来自 `config.get("run.overlap_seconds", 600)`,默认 600 秒(10 分钟)
|
||||
- 重叠设计目的:防止因时钟偏差或事务延迟导致数据遗漏
|
||||
|
||||
### 3.3 优先级三:闲忙时段默认值(Idle/Busy Window)
|
||||
|
||||
当既无手动覆盖也无游标时,根据当前时间是否处于闲时窗口,选择不同的默认窗口长度:
|
||||
|
||||
```
|
||||
闲时(idle):window_minutes = run.window_minutes.default_idle (默认 180 分钟)
|
||||
忙时(busy):window_minutes = run.window_minutes.default_busy (默认 30 分钟)
|
||||
|
||||
window_start = now - window_minutes
|
||||
window_end = now
|
||||
```
|
||||
|
||||
闲时判断逻辑:比较当前时间的 `HH:MM` 字符串是否在 `[idle_start, idle_end]` 区间内。
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `run.idle_window.start` | `"04:00"` | 闲时开始 |
|
||||
| `run.idle_window.end` | `"16:00"` | 闲时结束 |
|
||||
| `run.window_minutes.default_idle` | `180` | 闲时默认窗口(分钟) |
|
||||
| `run.window_minutes.default_busy` | `30` | 忙时默认窗口(分钟) |
|
||||
| `run.overlap_seconds` | `600` | 游标重叠秒数 |
|
||||
|
||||
### 3.4 流程图
|
||||
|
||||
```
|
||||
_get_time_window(cursor_data)
|
||||
│
|
||||
├─ run.window_override.start/end 都存在?
|
||||
│ ├─ 是 → 解析并返回 (override_start, override_end)
|
||||
│ └─ 否 ↓
|
||||
│
|
||||
├─ 判断闲忙时段 → 确定 window_minutes
|
||||
│
|
||||
├─ cursor_data 存在且含 last_end?
|
||||
│ ├─ 是 → window_start = last_end - overlap_seconds
|
||||
│ └─ 否 → window_start = now - window_minutes
|
||||
│
|
||||
└─ window_end = now → 返回 (start, end, minutes)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 窗口分段(build_window_segments)
|
||||
|
||||
当时间窗口跨度较大时(如回溯一个月的数据),系统支持将窗口切分为多个小段逐段执行,避免单次请求数据量过大。
|
||||
|
||||
### 4.1 切分入口
|
||||
|
||||
`build_window_segments()` 位于 `utils/windowing.py`,由 `BaseTask.execute()` 调用:
|
||||
|
||||
```python
|
||||
def build_window_segments(
|
||||
cfg, # 配置对象
|
||||
start: datetime, # 窗口起点
|
||||
end: datetime, # 窗口终点
|
||||
*,
|
||||
tz: ZoneInfo | None, # 时区
|
||||
override_only: bool, # 是否仅在手动覆盖时才切分
|
||||
) -> List[Tuple[datetime, datetime]]:
|
||||
```
|
||||
|
||||
### 4.2 切分策略
|
||||
|
||||
**`override_only=True`(默认行为):**
|
||||
- 仅当 `run.window_override.start/end` 都存在时,才按配置切分
|
||||
- 否则强制 `split_unit="none"`,返回单段(即不切分)
|
||||
- 设计意图:自动游标模式下窗口通常较短,无需切分;手动指定大范围时才需要
|
||||
|
||||
**切分配置项:**
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `run.window_split.unit` | `"month"` | 切分单位:`none` / `day` / `week` / `month` |
|
||||
| `run.window_split.days` | `1` | 按天切分时的步长(1 / 10 / 30) |
|
||||
| `run.window_split.compensation_hours` | `0` | 窗口前后补偿小时数 |
|
||||
|
||||
### 4.3 底层切分函数 split_window()
|
||||
|
||||
```python
|
||||
def split_window(start, end, *, tz, split_unit, compensation_hours, split_days=None):
|
||||
```
|
||||
|
||||
**补偿机制:** 若 `compensation_hours > 0`,会在切分前将窗口向两端各扩展指定小时数,用于覆盖跨段边界的数据。
|
||||
|
||||
**各切分模式:**
|
||||
|
||||
| split_unit | 行为 |
|
||||
|------------|------|
|
||||
| `none` / `off` / `false` / `""` | 不切分,返回 `[(start, end)]` |
|
||||
| `day` / `daily` | 按 `split_days` 天为步长切分(默认 1 天一段) |
|
||||
| `week` / `weekly` | 按 7 天为步长切分 |
|
||||
| `month` / `monthly` | 按自然月边界切分(每段从当前位置到下月 1 日 00:00) |
|
||||
|
||||
**示例:** 窗口 `2026-01-15 ~ 2026-03-10`,`split_unit=month`:
|
||||
|
||||
```
|
||||
段 1: 2026-01-15 ~ 2026-02-01
|
||||
段 2: 2026-02-01 ~ 2026-03-01
|
||||
段 3: 2026-03-01 ~ 2026-03-10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. TaskRegistry 注册方式与 TaskMeta 元数据
|
||||
|
||||
### 5.1 TaskMeta 数据结构
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TaskMeta:
|
||||
task_class: type # 任务类(BaseTask 的子类)
|
||||
requires_db_config: bool = True # 是否需要数据库配置(游标/运行记录)
|
||||
layer: str | None = None # 所属层:"ODS" / "DWD" / "DWS" / "INDEX" / None
|
||||
task_type: str = "etl" # 任务类型:"etl" / "utility" / "verification"
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `task_class` | 任务的 Python 类引用,用于 `create_task()` 时实例化 |
|
||||
| `requires_db_config` | `True` 表示需要游标管理和运行记录;`False` 表示工具类/校验类任务,不走游标 |
|
||||
| `layer` | 标识任务所属数据层,用于 `get_tasks_by_layer()` 按层查询 |
|
||||
| `task_type` | 区分 ETL 任务、工具类任务和校验类任务 |
|
||||
|
||||
### 5.2 注册方式
|
||||
|
||||
`TaskRegistry` 提供 `register()` 方法,所有任务在 `orchestration/task_registry.py` 模块加载时完成注册:
|
||||
|
||||
```python
|
||||
class TaskRegistry:
|
||||
def register(self, task_code, task_class, requires_db_config=True, layer=None, task_type="etl"):
|
||||
self._tasks[task_code.upper()] = TaskMeta(...)
|
||||
```
|
||||
|
||||
**注册示例:**
|
||||
|
||||
```python
|
||||
# ODS 层 ETL 任务(默认 requires_db_config=True, task_type="etl")
|
||||
default_registry.register("ORDERS", OrdersTask, layer="ODS")
|
||||
|
||||
# 工具类任务(不需要游标)
|
||||
default_registry.register("INIT_ODS_SCHEMA", InitOdsSchemaTask, requires_db_config=False, task_type="utility")
|
||||
|
||||
# 校验类任务
|
||||
default_registry.register("DATA_INTEGRITY_CHECK", DataIntegrityTask, requires_db_config=False, task_type="verification")
|
||||
|
||||
# 通用 ODS 任务(由 ODS_TASK_CLASSES 字典动态注册)
|
||||
for code, task_cls in ODS_TASK_CLASSES.items():
|
||||
default_registry.register(code, task_cls, layer="ODS")
|
||||
```
|
||||
|
||||
### 5.3 TaskRegistry 核心方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `register(task_code, task_class, ...)` | 注册任务类及元数据 |
|
||||
| `create_task(task_code, config, db, api, logger)` | 根据任务代码创建实例 |
|
||||
| `get_metadata(task_code) -> TaskMeta` | 查询任务元数据 |
|
||||
| `get_tasks_by_layer(layer) -> list[str]` | 获取指定层的所有任务代码 |
|
||||
| `is_utility_task(task_code) -> bool` | 判断是否为工具类任务(`requires_db_config=False`) |
|
||||
| `get_all_task_codes() -> list[str]` | 获取所有已注册任务代码 |
|
||||
|
||||
### 5.4 当前注册任务统计
|
||||
|
||||
| 层 | 数量 | 说明 |
|
||||
|----|------|------|
|
||||
| ODS | 14 + N | 14 个独立任务 + N 个通用 ODS 任务(由 `ODS_TASK_CLASSES` 动态生成) |
|
||||
| DWD | 5 | 含核心装载任务 `DWD_LOAD_FROM_ODS` 和质量检查 |
|
||||
| DWS | 15 | 助教业绩、会员分析、财务统计、运维任务 |
|
||||
| INDEX | 4 | 回流指数、新客转化指数、关系指数、手动台账导入 |
|
||||
| 工具类 | 7 | Schema 初始化、手动入库、归档、校验等 |
|
||||
| 校验类 | 1 | 数据完整性校验 |
|
||||
|
||||
---
|
||||
|
||||
## 6. PipelineRunner 管道执行流程
|
||||
|
||||
`PipelineRunner`(位于 `orchestration/pipeline_runner.py`)负责编排多层 ETL 任务的执行顺序,并可选地运行后置校验。
|
||||
|
||||
### 6.1 管道定义
|
||||
|
||||
系统预定义了 7 种管道,每种管道包含一组数据层:
|
||||
|
||||
```python
|
||||
PIPELINE_LAYERS = {
|
||||
"api_ods": ["ODS"],
|
||||
"api_ods_dwd": ["ODS", "DWD"],
|
||||
"api_full": ["ODS", "DWD", "DWS", "INDEX"],
|
||||
"ods_dwd": ["DWD"],
|
||||
"dwd_dws": ["DWS"],
|
||||
"dwd_dws_index": ["DWS", "INDEX"],
|
||||
"dwd_index": ["INDEX"],
|
||||
}
|
||||
```
|
||||
|
||||
| 管道 | 包含层 | 典型场景 |
|
||||
|------|--------|----------|
|
||||
| `api_ods` | ODS | 仅从 API 抓取数据到 ODS |
|
||||
| `api_ods_dwd` | ODS → DWD | 抓取并清洗到 DWD |
|
||||
| `api_full` | ODS → DWD → DWS → INDEX | 全流程 ETL |
|
||||
| `ods_dwd` | DWD | 仅执行 ODS→DWD 清洗(假设 ODS 已有数据) |
|
||||
| `dwd_dws` | DWS | 仅执行 DWD→DWS 汇总 |
|
||||
| `dwd_dws_index` | DWS → INDEX | 汇总 + 指数计算 |
|
||||
| `dwd_index` | INDEX | 仅执行指数计算 |
|
||||
|
||||
### 6.2 处理模式
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| `increment_only` | 仅执行增量 ETL,不做校验(默认) |
|
||||
| `verify_only` | 跳过增量 ETL,仅执行后置校验并自动修复 |
|
||||
| `increment_verify` | 先执行增量 ETL,再执行后置校验并修复 |
|
||||
|
||||
### 6.3 run() 执行流程
|
||||
|
||||
```
|
||||
PipelineRunner.run(pipeline, processing_mode, ...)
|
||||
│
|
||||
├─ 校验管道名称合法性
|
||||
│
|
||||
├─ 设置默认时间窗口(未指定时:过去 24 小时)
|
||||
│
|
||||
├─ 根据 processing_mode 分支:
|
||||
│
|
||||
│ ┌─ verify_only ─────────────────────────────────┐
|
||||
│ │ ├─ fetch_before_verify? │
|
||||
│ │ │ ├─ 是 → 先执行 ODS 任务获取 API 数据 │
|
||||
│ │ │ └─ 否 → 跳过 │
|
||||
│ │ └─ _run_verification(...) │
|
||||
│ └────────────────────────────────────────────────┘
|
||||
│
|
||||
│ ┌─ increment_only / increment_verify ───────────┐
|
||||
│ │ ├─ _resolve_tasks(layers) → 解析任务列表 │
|
||||
│ │ ├─ task_executor.run_tasks(tasks) │
|
||||
│ │ └─ increment_verify? │
|
||||
│ │ └─ 是 → _run_verification(...) │
|
||||
│ └────────────────────────────────────────────────┘
|
||||
│
|
||||
└─ 汇总计数 → 返回结果字典
|
||||
```
|
||||
|
||||
### 6.4 任务解析(_resolve_tasks)
|
||||
|
||||
`_resolve_tasks(layers)` 根据层列表解析出具体的任务代码:
|
||||
|
||||
| 层 | 解析逻辑 |
|
||||
|----|----------|
|
||||
| ODS | 优先使用 `config.run.ods_tasks`,否则从 TaskRegistry 按层查询 |
|
||||
| DWD | 固定返回 `["DWD_LOAD_FROM_ODS"]` |
|
||||
| DWS | 优先使用 `config.run.dws_tasks`,否则从 TaskRegistry 按层查询 |
|
||||
| INDEX | 优先使用 `config.run.index_tasks`,否则从 TaskRegistry 按层查询 |
|
||||
|
||||
### 6.5 数据源模式
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| `online` | 仅从上游 API 在线抓取 |
|
||||
| `offline` | 仅从本地 JSON 文件入库 |
|
||||
| `hybrid` | 先抓取再入库(默认) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 校验框架概述
|
||||
|
||||
校验框架(`tasks/verification/`)提供各层数据的批量后置校验和自动补齐功能。
|
||||
|
||||
### 7.1 架构
|
||||
|
||||
```
|
||||
BaseVerifier(基类)
|
||||
├─ OdsVerifier — 主键 + content_hash 对比,批量 UPSERT
|
||||
├─ DwdVerifier — 维度 SCD2 / 事实主键对比,批量 UPSERT
|
||||
├─ DwsVerifier — 聚合对比,批量重算 UPSERT
|
||||
└─ IndexVerifier — 实体覆盖对比,批量重算 UPSERT
|
||||
```
|
||||
|
||||
### 7.2 核心接口
|
||||
|
||||
- `get_verifier_for_layer(layer, db_connection, logger, **kwargs)` — 工厂函数,根据层名返回对应校验器实例
|
||||
- `verifier.verify_and_backfill(window_start, window_end, auto_backfill, split_unit, tables)` — 执行校验并自动补齐
|
||||
|
||||
### 7.3 校验结果模型
|
||||
|
||||
| 类 | 说明 |
|
||||
|----|------|
|
||||
| `VerificationResult` | 单表校验结果 |
|
||||
| `VerificationSummary` | 层级汇总(total_tables / consistent_tables / total_backfilled / error_tables) |
|
||||
| `VerificationStatus` | 校验状态枚举 |
|
||||
| `WindowSegment` | 校验时间段 |
|
||||
|
||||
### 7.4 在管道中的触发方式
|
||||
|
||||
校验由 `PipelineRunner._run_verification()` 触发,支持:
|
||||
- 按层逐一校验
|
||||
- 按表名过滤(`verify_tables` 参数)
|
||||
- 时间窗口切分(`window_split` 参数)
|
||||
- ODS 层支持 API 数据对比或本地 JSON 对比两种模式
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关配置速查
|
||||
|
||||
| 配置路径 | 默认值 | 说明 |
|
||||
|----------|--------|------|
|
||||
| `app.store_id` | — | 门店 ID |
|
||||
| `app.timezone` | `"Asia/Shanghai"` | 系统时区 |
|
||||
| `run.window_override.start` | — | 手动窗口起点 |
|
||||
| `run.window_override.end` | — | 手动窗口终点 |
|
||||
| `run.idle_window.start` | `"04:00"` | 闲时开始 |
|
||||
| `run.idle_window.end` | `"16:00"` | 闲时结束 |
|
||||
| `run.window_minutes.default_idle` | `180` | 闲时默认窗口(分钟) |
|
||||
| `run.window_minutes.default_busy` | `30` | 忙时默认窗口(分钟) |
|
||||
| `run.overlap_seconds` | `600` | 游标重叠秒数 |
|
||||
| `run.window_split.unit` | `"month"` | 切分单位 |
|
||||
| `run.window_split.days` | `1` | 按天切分步长 |
|
||||
| `run.window_split.compensation_hours` | `0` | 窗口补偿小时数 |
|
||||
| `run.ods_tasks` | `[]` | ODS 层任务列表覆盖 |
|
||||
| `run.dws_tasks` | `[]` | DWS 层任务列表覆盖 |
|
||||
| `run.index_tasks` | `[]` | INDEX 层任务列表覆盖 |
|
||||
554
apps/etl/pipelines/feiqiu/docs/etl_tasks/dwd_tasks.md
Normal file
554
apps/etl/pipelines/feiqiu/docs/etl_tasks/dwd_tasks.md
Normal file
@@ -0,0 +1,554 @@
|
||||
# DWD 层任务详解
|
||||
|
||||
> 本文档说明飞球 ETL 系统中 DWD(明细数据层)的所有任务。
|
||||
> DWD 层负责从 ODS 读取原始数据,经清洗、类型转换和列映射后写入维度表(dim_*)和事实表(dwd_* / fact_*),
|
||||
> 维度表采用 SCD2 或 Type1 Upsert 策略,事实表按时间增量装载。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
DWD 层共有 2 个已注册任务:
|
||||
|
||||
| 任务代码 | Python 类 | 注册参数 | 说明 |
|
||||
|----------|-----------|----------|------|
|
||||
| `DWD_LOAD_FROM_ODS` | `DwdLoadTask` | `layer="DWD"` | 核心装载任务:遍历 TABLE_MAP,维度走 SCD2/Type1,事实走增量 |
|
||||
| `DWD_QUALITY_CHECK` | `DwdQualityTask` | `layer="DWD"`, `task_type="verification"` | ODS 与 DWD 行数/金额核对 |
|
||||
|
||||
> 注册位置:`orchestration/task_registry.py`
|
||||
>
|
||||
> **历史说明**:早期版本曾有 3 个独立 DWD 任务(TICKET_DWD、PAYMENTS_DWD、MEMBERS_DWD),
|
||||
> 通过专用 Loader 写入不存在的 `billiards.*` schema。这些任务已于 2026-02-14 废弃删除,
|
||||
> 其功能由 `DWD_LOAD_FROM_ODS` 的 TABLE_MAP 映射完全覆盖。
|
||||
|
||||
---
|
||||
|
||||
## DWD_LOAD_FROM_ODS — 核心装载任务
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWD_LOAD_FROM_ODS` |
|
||||
| Python 类 | `tasks.dwd.dwd_load_task.DwdLoadTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 数据来源 | `billiards_ods.*`(ODS 层各表) |
|
||||
| 数据目标 | `billiards_dwd.*`(维度表 + 事实表,共 40+ 对映射) |
|
||||
| 事务模式 | 每张表一次独立事务,单表失败回滚后继续后续表 |
|
||||
| 配置项 | `dwd.only_tables`(可选,限定只处理指定表)、`dwd.fact_upsert`(默认 `True`) |
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract(context) → 返回 {"now": datetime.now()}
|
||||
↓
|
||||
load(extracted, context) → 遍历 TABLE_MAP
|
||||
↓
|
||||
对每张 DWD 表:
|
||||
├─ 获取 DWD 列信息 + ODS 列信息
|
||||
├─ 判断表名前缀:
|
||||
│ ├─ dim_* → _merge_dim()(维度合并)
|
||||
│ └─ 其他 → _merge_fact_increment()(事实增量)
|
||||
├─ commit(成功)或 rollback(失败)
|
||||
└─ 记录 summary / errors
|
||||
```
|
||||
|
||||
### TABLE_MAP 映射表
|
||||
|
||||
`TABLE_MAP` 定义了 DWD 表到 ODS 表的完整映射关系。每对映射中,DWD 表名为 key,ODS 表名为 value。
|
||||
主表与扩展表(`_ex`)共享同一 ODS 源表,通过 `FACT_MAPPINGS` 中的列映射区分写入哪些字段。
|
||||
|
||||
#### 维度表映射
|
||||
|
||||
| DWD 表 | ODS 源表 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `billiards_dwd.dim_site` | `billiards_ods.table_fee_transactions` | 门店维度(从台费流水的 siteprofile 快照提取) |
|
||||
| `billiards_dwd.dim_site_ex` | `billiards_ods.table_fee_transactions` | 门店扩展(灯控、WiFi、客服等) |
|
||||
| `billiards_dwd.dim_table` | `billiards_ods.site_tables_master` | 台桌维度 |
|
||||
| `billiards_dwd.dim_table_ex` | `billiards_ods.site_tables_master` | 台桌扩展(台布使用时间等) |
|
||||
| `billiards_dwd.dim_assistant` | `billiards_ods.assistant_accounts_master` | 助教维度 |
|
||||
| `billiards_dwd.dim_assistant_ex` | `billiards_ods.assistant_accounts_master` | 助教扩展(简介、分组、灯控设备等) |
|
||||
| `billiards_dwd.dim_member` | `billiards_ods.member_profiles` | 会员维度 |
|
||||
| `billiards_dwd.dim_member_ex` | `billiards_ods.member_profiles` | 会员扩展(注册来源、组织等) |
|
||||
| `billiards_dwd.dim_member_card_account` | `billiards_ods.member_stored_value_cards` | 会员储值卡维度 |
|
||||
| `billiards_dwd.dim_member_card_account_ex` | `billiards_ods.member_stored_value_cards` | 储值卡扩展(电费抵扣、冻结余额等) |
|
||||
| `billiards_dwd.dim_tenant_goods` | `billiards_ods.tenant_goods_master` | 租户商品维度 |
|
||||
| `billiards_dwd.dim_tenant_goods_ex` | `billiards_ods.tenant_goods_master` | 租户商品扩展(条码、备注等) |
|
||||
| `billiards_dwd.dim_store_goods` | `billiards_ods.store_goods_master` | 门店商品维度 |
|
||||
| `billiards_dwd.dim_store_goods_ex` | `billiards_ods.store_goods_master` | 门店商品扩展(库存、安全库存等) |
|
||||
| `billiards_dwd.dim_goods_category` | `billiards_ods.stock_goods_category_tree` | 商品分类维度(含子类展开) |
|
||||
| `billiards_dwd.dim_groupbuy_package` | `billiards_ods.group_buy_packages` | 团购套餐维度 |
|
||||
| `billiards_dwd.dim_groupbuy_package_ex` | `billiards_ods.group_buy_packages` | 团购套餐扩展 |
|
||||
|
||||
|
||||
#### 事实表映射
|
||||
|
||||
| DWD 表 | ODS 源表 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `billiards_dwd.dwd_settlement_head` | `billiards_ods.settlement_records` | 结算头(订单结算主记录) |
|
||||
| `billiards_dwd.dwd_settlement_head_ex` | `billiards_ods.settlement_records` | 结算头扩展(支付方式、撤单、促销等) |
|
||||
| `billiards_dwd.dwd_table_fee_log` | `billiards_ods.table_fee_transactions` | 台费流水 |
|
||||
| `billiards_dwd.dwd_table_fee_log_ex` | `billiards_ods.table_fee_transactions` | 台费流水扩展(销售员、消费类型等) |
|
||||
| `billiards_dwd.dwd_table_fee_adjust` | `billiards_ods.table_fee_discount_records` | 台费调整/折扣 |
|
||||
| `billiards_dwd.dwd_table_fee_adjust_ex` | `billiards_ods.table_fee_discount_records` | 台费调整扩展 |
|
||||
| `billiards_dwd.dwd_store_goods_sale` | `billiards_ods.store_goods_sales_records` | 商品销售记录 |
|
||||
| `billiards_dwd.dwd_store_goods_sale_ex` | `billiards_ods.store_goods_sales_records` | 商品销售扩展 |
|
||||
| `billiards_dwd.dwd_assistant_service_log` | `billiards_ods.assistant_service_records` | 助教服务记录 |
|
||||
| `billiards_dwd.dwd_assistant_service_log_ex` | `billiards_ods.assistant_service_records` | 助教服务扩展 |
|
||||
| `billiards_dwd.dwd_assistant_trash_event` | `billiards_ods.assistant_cancellation_records` | 助教取消/废单事件 |
|
||||
| `billiards_dwd.dwd_assistant_trash_event_ex` | `billiards_ods.assistant_cancellation_records` | 助教取消扩展 |
|
||||
| `billiards_dwd.dwd_member_balance_change` | `billiards_ods.member_balance_changes` | 会员余额变动 |
|
||||
| `billiards_dwd.dwd_member_balance_change_ex` | `billiards_ods.member_balance_changes` | 会员余额变动扩展 |
|
||||
| `billiards_dwd.dwd_groupbuy_redemption` | `billiards_ods.group_buy_redemption_records` | 团购核销记录 |
|
||||
| `billiards_dwd.dwd_groupbuy_redemption_ex` | `billiards_ods.group_buy_redemption_records` | 团购核销扩展 |
|
||||
| `billiards_dwd.dwd_platform_coupon_redemption` | `billiards_ods.platform_coupon_redemption_records` | 平台优惠券核销 |
|
||||
| `billiards_dwd.dwd_platform_coupon_redemption_ex` | `billiards_ods.platform_coupon_redemption_records` | 平台优惠券核销扩展 |
|
||||
| `billiards_dwd.dwd_recharge_order` | `billiards_ods.recharge_settlements` | 充值订单 |
|
||||
| `billiards_dwd.dwd_recharge_order_ex` | `billiards_ods.recharge_settlements` | 充值订单扩展 |
|
||||
| `billiards_dwd.dwd_payment` | `billiards_ods.payment_transactions` | 支付记录 |
|
||||
| `billiards_dwd.dwd_refund` | `billiards_ods.refund_transactions` | 退款记录 |
|
||||
| `billiards_dwd.dwd_refund_ex` | `billiards_ods.refund_transactions` | 退款扩展 |
|
||||
|
||||
> 共计 **17 对维度映射**(含 `_ex`)+ **23 对事实映射**(含 `_ex`)= **40 对**映射。
|
||||
|
||||
---
|
||||
|
||||
### 维度/事实分流逻辑
|
||||
|
||||
`load()` 方法遍历 `TABLE_MAP` 时,根据 DWD 表名前缀自动分流:
|
||||
|
||||
```python
|
||||
if self._table_base(dwd_table).startswith("dim_"):
|
||||
# 维度表 → _merge_dim()
|
||||
else:
|
||||
# 事实表 → _merge_fact_increment()
|
||||
```
|
||||
|
||||
`_merge_dim()` 内部进一步判断维度合并策略:
|
||||
|
||||
| 条件 | 策略 | 方法 |
|
||||
|------|------|------|
|
||||
| DWD 表列中包含 SCD2 列(`scd2_start_time` / `scd2_end_time` / `scd2_is_current` / `scd2_version`) | **SCD2 合并**:关闭旧版 + 插入新版 | `_merge_dim_scd2()` |
|
||||
| DWD 表列中不包含 SCD2 列 | **Type1 Upsert**:主键冲突则更新 | `_merge_dim_type1_upsert()` |
|
||||
|
||||
> SCD2 列集合定义:`SCD_COLS = {"scd2_start_time", "scd2_end_time", "scd2_is_current", "scd2_version"}`
|
||||
|
||||
---
|
||||
|
||||
### SCD2 处理流程
|
||||
|
||||
当维度表包含 SCD2 列时,执行 `_merge_dim_scd2()` 方法,完整流程如下:
|
||||
|
||||
#### 1. 最新快照选取
|
||||
|
||||
从 ODS 源表中按业务主键取最新快照,使用 `DISTINCT ON` + `fetched_at DESC` 去重:
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT ON (业务主键表达式)
|
||||
<列映射表达式>
|
||||
FROM billiards_ods.<ods_table>
|
||||
WHERE "fetched_at" IS NOT NULL
|
||||
ORDER BY 业务主键表达式, "fetched_at" DESC NULLS LAST
|
||||
```
|
||||
|
||||
- **业务主键**:从 DWD 表主键中剔除 SCD2 列后得到(`_strip_scd2_keys()`)
|
||||
- **列映射**:优先使用 `FACT_MAPPINGS` 中的显式映射(支持 JSON 路径提取如 `siteprofile->>'shop_name'`、类型转换如 `::numeric`),其次按同名列直接映射
|
||||
- **特殊处理**:`dim_goods_category` 表会额外读取 `categoryboxes` 列并展开子分类行(`_expand_goods_category_rows()`)
|
||||
|
||||
#### 2. 变更检测
|
||||
|
||||
将 ODS 最新快照与 DWD 当前版本(`scd2_is_current=1`)逐列对比:
|
||||
|
||||
```python
|
||||
# 预加载 DWD 当前版本(避免逐行 SELECT)
|
||||
SELECT * FROM billiards_dwd.<dwd_table> WHERE COALESCE(scd2_is_current, 1) = 1
|
||||
|
||||
# 逐行对比(跳过 SCD2 列本身)
|
||||
for col in dwd_cols:
|
||||
if col in SCD_COLS: continue
|
||||
if not _values_equal(current[col], incoming[col]):
|
||||
return True # 有变更
|
||||
```
|
||||
|
||||
`_values_equal()` 在对比前会做类型归一化:
|
||||
- **空值归一化**:空字符串 `""` 和 `None` 视为等价
|
||||
- **日期时间归一化**:朴素时间(naive)与时区感知时间(aware)统一比较
|
||||
- **布尔值归一化**:`"true"/"false"/"1"/"0"/"yes"/"no"` 等字符串与布尔值统一比较
|
||||
- **数值归一化**:字符串形式的数字(如 `"3.14"`)与 `Decimal` / `float` 统一比较
|
||||
|
||||
#### 3. 版本关闭与新建
|
||||
|
||||
对于检测到变更的记录,分两步批量操作:
|
||||
|
||||
**步骤 A — 批量关闭旧版本**(`_close_current_dim_bulk()`):
|
||||
|
||||
```sql
|
||||
-- 单主键优化:使用 ANY 数组
|
||||
UPDATE billiards_dwd.<table>
|
||||
SET scd2_end_time = <now>, scd2_is_current = 0
|
||||
WHERE COALESCE(scd2_is_current, 1) = 1 AND "<pk>" = ANY(<ids>)
|
||||
|
||||
-- 复合主键:逐条 execute_batch
|
||||
UPDATE billiards_dwd.<table>
|
||||
SET scd2_end_time = <now>, scd2_is_current = 0
|
||||
WHERE COALESCE(scd2_is_current, 1) = 1 AND "<pk1>" = %s AND "<pk2>" = %s
|
||||
```
|
||||
|
||||
**步骤 B — 批量插入新版本**(`_insert_dim_rows_bulk()`):
|
||||
|
||||
```sql
|
||||
INSERT INTO billiards_dwd.<table> (<所有列>) VALUES %s
|
||||
```
|
||||
|
||||
新版本行的 SCD2 列填充规则:
|
||||
| SCD2 列 | 值 |
|
||||
|---------|-----|
|
||||
| `scd2_start_time` | 当前时间(`now`) |
|
||||
| `scd2_end_time` | `9999-12-31 00:00:00`(表示"当前有效") |
|
||||
| `scd2_is_current` | `1` |
|
||||
| `scd2_version` | 旧版本号 + 1(新记录为 1) |
|
||||
|
||||
#### 4. 返回统计
|
||||
|
||||
```python
|
||||
{
|
||||
"processed": <ODS 去重后总行数>,
|
||||
"inserted": <新增记录数(首次出现的业务主键)>,
|
||||
"updated": <变更记录数(关闭旧版+插入新版)>,
|
||||
"skipped": <无变更跳过数>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Type1 Upsert 处理流程
|
||||
|
||||
当维度表不包含 SCD2 列时,执行 `_merge_dim_type1_upsert()` 方法:
|
||||
|
||||
1. 从 ODS 取最新快照(同 SCD2 的 `DISTINCT ON` 逻辑)
|
||||
2. 按主键去重,跳过主键为 `NULL` 的行
|
||||
3. 使用 PostgreSQL `INSERT ... ON CONFLICT DO UPDATE` 一次性写入:
|
||||
|
||||
```sql
|
||||
INSERT INTO billiards_dwd.<table> (<列>) VALUES %s
|
||||
ON CONFLICT (<主键>) DO UPDATE SET <非主键列> = EXCLUDED.<列>
|
||||
WHERE <任一非主键列 IS DISTINCT FROM EXCLUDED 值> -- 仅在有实际变更时才更新
|
||||
RETURNING (xmax = 0) AS inserted -- 区分新增 vs 更新
|
||||
```
|
||||
|
||||
> Type1 Upsert 不保留历史版本,直接覆盖旧值。SCD2 列(如果存在于列定义中)会被填充默认值(`scd2_start_time=now`, `scd2_version=1`)但不参与冲突判断。
|
||||
|
||||
---
|
||||
|
||||
### 事实表增量装载
|
||||
|
||||
非 `dim_*` 前缀的表走 `_merge_fact_increment()` 方法,核心逻辑如下:
|
||||
|
||||
#### 1. 水位线(Watermark)机制
|
||||
|
||||
事实表统一使用 `fetched_at` 列作为增量过滤依据,水位线获取优先级:
|
||||
|
||||
| 优先级 | 条件 | 行为 |
|
||||
|--------|------|------|
|
||||
| 1 | 配置了手动时间窗口(`run.window_override.start/end`) | `WHERE fetched_at >= <start> AND fetched_at < <end>` |
|
||||
| 2 | 无手动窗口 | 自动计算水位线 `_get_fact_watermark()`,`WHERE fetched_at > <watermark>` |
|
||||
|
||||
`_get_fact_watermark()` 的计算逻辑:
|
||||
- 若 DWD 表包含 `fetched_at` 列 → `SELECT MAX(fetched_at) FROM <dwd_table>`
|
||||
- 若 DWD 表不含 `fetched_at` 但有主键 → 通过 ODS JOIN DWD 取 `MAX(ods.fetched_at)`
|
||||
- 兜底 → `"1970-01-01"`(全量装载)
|
||||
|
||||
#### 2. 列映射与类型转换
|
||||
|
||||
列映射来源(按优先级):
|
||||
1. `FACT_MAPPINGS` 中的显式映射:`(dwd_列名, ods_源表达式, 可选类型转换)`
|
||||
2. DWD 与 ODS 同名列直接映射
|
||||
3. 主键兜底:若 DWD 主键在 ODS 中不存在但 ODS 有 `id` 列,自动映射 `pk → id`
|
||||
|
||||
类型转换(`_build_fact_select_exprs()`):
|
||||
- 当 DWD 列为数值类型(`integer`/`numeric`/`bigint` 等)而 ODS 列为文本类型时,自动添加 `CAST(NULLIF(CAST("<col>" AS text), '') AS <type>)`
|
||||
|
||||
#### 3. 快照去重
|
||||
|
||||
若 ODS 表包含 `content_hash` 列(`snapshot_mode=True`),则使用 `DISTINCT ON` 按主键 + `fetched_at DESC` 取最新快照,避免重复记录。
|
||||
|
||||
#### 4. 写入策略
|
||||
|
||||
根据配置 `dwd.fact_upsert`(默认 `True`)和 `snapshot_mode` 决定冲突处理:
|
||||
|
||||
| 条件 | SQL 策略 |
|
||||
|------|----------|
|
||||
| `snapshot_mode=True` 或 `fact_upsert=True` | `ON CONFLICT (<pk>) DO UPDATE SET ... WHERE <任一列 IS DISTINCT FROM>` |
|
||||
| `fact_upsert=False` 且无 `content_hash` | `ON CONFLICT (<pk>) DO NOTHING` |
|
||||
|
||||
写入后通过 `RETURNING (xmax = 0) AS inserted` 区分新增(`xmax=0`)和更新(`xmax≠0`)。
|
||||
|
||||
#### 5. 缺失主键回补
|
||||
|
||||
对于可能出现"回补旧记录"的事实表(如 `dwd_assistant_service_log`,定义在 `FACT_MISSING_FILL_TABLES` 中),
|
||||
在主增量写入完成后,额外执行 `_insert_missing_by_pk()`:
|
||||
|
||||
```sql
|
||||
INSERT INTO <dwd_table> (<列>)
|
||||
SELECT <列> FROM <ods_table> o
|
||||
LEFT JOIN <dwd_table> d ON d.<pk> = o.<pk>
|
||||
WHERE d.<pk> IS NULL
|
||||
AND o.fetched_at > <watermark> -- 同样受水位线约束
|
||||
ON CONFLICT (<pk>) DO NOTHING
|
||||
```
|
||||
|
||||
> 此步骤确保因时序乱序导致的遗漏记录能被补齐。
|
||||
|
||||
#### 6. FACT_MAPPINGS 列映射详解
|
||||
|
||||
`FACT_MAPPINGS` 是一个字典,key 为 DWD 表全名,value 为三元组列表 `(dwd_列名, ods_源表达式, 类型转换)`。
|
||||
|
||||
映射类型示例:
|
||||
|
||||
| 映射类型 | 示例 | 说明 |
|
||||
|----------|------|------|
|
||||
| 简单重命名 | `("table_id", "id", None)` | ODS `id` → DWD `table_id` |
|
||||
| JSON 路径提取 | `("shop_name", "siteprofile->>'shop_name'", None)` | 从 JSONB 字段提取 |
|
||||
| 类型转换 | `("longitude", "siteprofile->>'longitude'", "numeric")` | 提取后转 `numeric` |
|
||||
| 布尔转换 | `("is_first_limit", "is_first_limit", "boolean")` | 转布尔类型 |
|
||||
| 日期转换 | `("pay_date", "pay_time", "date")` | 时间戳截断为日期 |
|
||||
| SQL 表达式 | `("category_level", "CASE WHEN pid = 0 THEN 1 ELSE 2 END", None)` | 计算列 |
|
||||
|
||||
> 维度表和事实表共用 `FACT_MAPPINGS`(名称虽含 "FACT" 但实际覆盖所有表)。
|
||||
|
||||
---
|
||||
|
||||
### 配置与环境变量
|
||||
|
||||
| 配置项 | 来源 | 说明 |
|
||||
|--------|------|------|
|
||||
| `dwd.only_tables` | AppConfig / 环境变量 `DWD_ONLY_TABLES` | 逗号分隔的表名列表,限定只处理指定表(支持全名或短名) |
|
||||
| `dwd.fact_upsert` | AppConfig | 事实表是否使用 upsert(默认 `True`),设为 `False` 则用 `DO NOTHING` |
|
||||
| `run.window_override.start` / `end` | AppConfig | 手动指定时间窗口,覆盖自动水位线 |
|
||||
|
||||
---
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 每张表独立事务:成功则 `commit`,失败则 `rollback` 并记录错误后继续下一张表
|
||||
- 主键缺失的行会被跳过并记录警告日志
|
||||
- ODS 表缺少 `fetched_at` 列时跳过该表并记录错误
|
||||
- `fetched_at` 为 `NULL` 的 ODS 行会被过滤(`WHERE fetched_at IS NOT NULL`)
|
||||
- 最终返回 `{"tables": [<各表统计>], "errors": [<失败表及原因>]}`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DWD_QUALITY_CHECK — 数据质量检查
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWD_QUALITY_CHECK` |
|
||||
| Python 类 | `tasks.dwd.dwd_quality_task.DwdQualityTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 任务类型 | `verification`(校验类任务) |
|
||||
| 数据来源 | `DwdLoadTask.TABLE_MAP` 中定义的所有 ODS / DWD 表对(40 对) |
|
||||
| 输出文件 | `reports/dwd_quality_report.json` |
|
||||
|
||||
### 用途
|
||||
|
||||
在 `DWD_LOAD_FROM_ODS` 装载完成后,对 ODS 与 DWD 两端进行**行数**和**金额**的交叉核对,
|
||||
自动生成 JSON 格式的质检报表。用于发现装载过程中可能出现的数据丢失或金额偏差。
|
||||
|
||||
---
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract(context) → 返回 {"now": datetime.now()}
|
||||
↓
|
||||
load(extracted, context)
|
||||
↓
|
||||
遍历 DwdLoadTask.TABLE_MAP 中的每对 (dwd_table, ods_table):
|
||||
├─ _compare_counts() → 行数核对
|
||||
├─ _compare_amounts() → 金额核对
|
||||
└─ 结果追加到 report["tables"]
|
||||
↓
|
||||
写入 reports/dwd_quality_report.json
|
||||
```
|
||||
|
||||
> 注意:本任务不执行 `transform()` 阶段,直接在 `load()` 中完成查询与报表输出。
|
||||
|
||||
---
|
||||
|
||||
### 行数核对逻辑
|
||||
|
||||
`_compare_counts(cur, dwd_table, ods_table)` 分别对 DWD 表和 ODS 表执行 `COUNT(1)`,
|
||||
返回两端行数及差值:
|
||||
|
||||
```sql
|
||||
-- DWD 端
|
||||
SELECT COUNT(1) AS cnt FROM "billiards_dwd"."<dwd_table>"
|
||||
|
||||
-- ODS 端
|
||||
SELECT COUNT(1) AS cnt FROM "billiards_ods"."<ods_table>"
|
||||
```
|
||||
|
||||
返回结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"dwd": 12345,
|
||||
"ods": 12350,
|
||||
"diff": -5
|
||||
}
|
||||
```
|
||||
|
||||
- `diff = dwd - ods`:正值表示 DWD 多于 ODS(可能因 SCD2 产生多版本),负值表示 DWD 少于 ODS(可能有数据丢失)
|
||||
- 维度表因 SCD2 历史版本的存在,DWD 行数通常 ≥ ODS 行数,`diff > 0` 属于正常现象
|
||||
- 事实表理论上 DWD 行数应 ≤ ODS 行数(去重后),`diff > 0` 需要关注
|
||||
|
||||
**表名解析**:`_split_table_name()` 将全限定名(如 `billiards_dwd.dim_member`)拆分为 `(schema, table)`。
|
||||
若表名不含 `.`,则使用默认 schema(DWD 端默认 `billiards_dwd`,ODS 端默认 `billiards_ods`)。
|
||||
|
||||
---
|
||||
|
||||
### 金额列自动扫描规则
|
||||
|
||||
`_compare_amounts(cur, dwd_table, ods_table)` 自动识别两端表中的金额相关列,
|
||||
对公共列逐列汇总对比。
|
||||
|
||||
#### 扫描机制
|
||||
|
||||
通过 `_get_numeric_amount_columns(cur, schema, table)` 从 `information_schema.columns` 查询:
|
||||
|
||||
```sql
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = %s
|
||||
AND table_name = %s
|
||||
AND data_type IN ('numeric', 'double precision', 'integer', 'bigint', 'smallint', 'real', 'decimal')
|
||||
```
|
||||
|
||||
在返回的数值型列中,进一步按**列名关键词**过滤,只保留列名中包含以下任一关键词的列:
|
||||
|
||||
| 关键词 | 匹配示例 |
|
||||
|--------|----------|
|
||||
| `amount` | `pay_amount`, `discount_amount`, `actual_amount` |
|
||||
| `money` | `consume_money`, `member_money` |
|
||||
| `fee` | `table_fee`, `service_fee`, `fee_amount` |
|
||||
| `balance` | `card_balance`, `gift_balance` |
|
||||
|
||||
> 关键词定义:`AMOUNT_KEYWORDS = ("amount", "money", "fee", "balance")`
|
||||
> 匹配规则:列名(小写)包含关键词即命中,如 `pay_amount` 包含 `amount`。
|
||||
|
||||
#### 公共列取交集
|
||||
|
||||
分别获取 DWD 端和 ODS 端的金额列后,取**交集**(`set(dwd_cols) & set(ods_cols)`)并排序,
|
||||
只对两端都存在的同名金额列进行汇总对比。
|
||||
|
||||
#### 汇总对比
|
||||
|
||||
对每个公共金额列执行 `SUM()`:
|
||||
|
||||
```sql
|
||||
-- DWD 端
|
||||
SELECT COALESCE(SUM("<col>"), 0) AS val FROM "billiards_dwd"."<dwd_table>"
|
||||
|
||||
-- ODS 端
|
||||
SELECT COALESCE(SUM("<col>"), 0) AS val FROM "billiards_ods"."<ods_table>"
|
||||
```
|
||||
|
||||
返回结构(每个金额列一条记录):
|
||||
|
||||
```json
|
||||
{
|
||||
"column": "pay_amount",
|
||||
"dwd_sum": 98765.50,
|
||||
"ods_sum": 98770.00,
|
||||
"diff": -4.50
|
||||
}
|
||||
```
|
||||
|
||||
- `diff = dwd_sum - ods_sum`:非零值表示两端金额不一致,需排查原因
|
||||
- `COALESCE(..., 0)` 确保 `NULL` 值不影响汇总结果
|
||||
- 若某对表没有公共金额列,`amounts` 数组为空
|
||||
|
||||
---
|
||||
|
||||
### JSON 报表输出格式
|
||||
|
||||
报表写入路径:`reports/dwd_quality_report.json`(目录不存在时自动创建)。
|
||||
|
||||
#### 完整结构
|
||||
|
||||
```json
|
||||
{
|
||||
"generated_at": "2025-01-15T14:30:00.123456",
|
||||
"tables": [
|
||||
{
|
||||
"dwd_table": "billiards_dwd.dim_member",
|
||||
"ods_table": "billiards_ods.member_profiles",
|
||||
"count": {
|
||||
"dwd": 1200,
|
||||
"ods": 1000,
|
||||
"diff": 200
|
||||
},
|
||||
"amounts": [
|
||||
{
|
||||
"column": "balance",
|
||||
"dwd_sum": 50000.00,
|
||||
"ods_sum": 50000.00,
|
||||
"diff": 0.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dwd_table": "billiards_dwd.dwd_payment",
|
||||
"ods_table": "billiards_ods.payment_transactions",
|
||||
"count": {
|
||||
"dwd": 5000,
|
||||
"ods": 5000,
|
||||
"diff": 0
|
||||
},
|
||||
"amounts": [
|
||||
{
|
||||
"column": "pay_amount",
|
||||
"dwd_sum": 123456.78,
|
||||
"ods_sum": 123456.78,
|
||||
"diff": 0.0
|
||||
},
|
||||
{
|
||||
"column": "fee_amount",
|
||||
"dwd_sum": 1234.56,
|
||||
"ods_sum": 1234.56,
|
||||
"diff": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"note": "行数/金额核对,金额字段基于列名包含 amount/money/fee/balance 的数值列自动扫描。"
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `generated_at` | `string` | 报表生成时间(ISO 8601 格式) |
|
||||
| `tables` | `array` | 每对 DWD↔ODS 表的核对结果 |
|
||||
| `tables[].dwd_table` | `string` | DWD 表全限定名 |
|
||||
| `tables[].ods_table` | `string` | ODS 表全限定名 |
|
||||
| `tables[].count` | `object` | 行数核对结果 |
|
||||
| `tables[].count.dwd` | `integer` | DWD 端行数 |
|
||||
| `tables[].count.ods` | `integer` | ODS 端行数 |
|
||||
| `tables[].count.diff` | `integer` | 行数差值(`dwd - ods`) |
|
||||
| `tables[].amounts` | `array` | 金额列核对结果(可能为空数组) |
|
||||
| `tables[].amounts[].column` | `string` | 金额列名 |
|
||||
| `tables[].amounts[].dwd_sum` | `float` | DWD 端汇总值 |
|
||||
| `tables[].amounts[].ods_sum` | `float` | ODS 端汇总值 |
|
||||
| `tables[].amounts[].diff` | `float` | 金额差值(`dwd_sum - ods_sum`) |
|
||||
| `note` | `string` | 报表说明文字 |
|
||||
|
||||
---
|
||||
|
||||
### 注意事项
|
||||
|
||||
- **全量扫描**:本任务对 TABLE_MAP 中所有 40 对表执行全表 `COUNT` 和 `SUM`,在数据量较大时可能耗时较长
|
||||
- **不区分增量**:行数和金额对比基于全表统计,不受时间窗口限制
|
||||
- **SCD2 影响**:维度表因 SCD2 历史版本的存在,DWD 行数通常大于 ODS 行数,这是预期行为
|
||||
- **列名匹配大小写**:金额列扫描时将列名统一转为小写后匹配关键词
|
||||
- **报表覆盖**:每次运行会覆盖上一次的报表文件(`reports/dwd_quality_report.json`)
|
||||
1648
apps/etl/pipelines/feiqiu/docs/etl_tasks/dws_tasks.md
Normal file
1648
apps/etl/pipelines/feiqiu/docs/etl_tasks/dws_tasks.md
Normal file
File diff suppressed because it is too large
Load Diff
731
apps/etl/pipelines/feiqiu/docs/etl_tasks/index_tasks.md
Normal file
731
apps/etl/pipelines/feiqiu/docs/etl_tasks/index_tasks.md
Normal file
@@ -0,0 +1,731 @@
|
||||
# INDEX 层任务详解
|
||||
|
||||
> 本文档说明飞球 ETL 系统中 INDEX(指数算法层)的所有任务。
|
||||
> INDEX 层基于 DWD/DWS 层数据,通过自定义算法计算业务指数,
|
||||
> 服务于会员运营(回流挽回、新客转化)和助教管理(关系归属、付费关联)等场景。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
INDEX 层共有 4 个已注册任务:
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 指数类型 | 更新策略 |
|
||||
|----------|-----------|--------|----------|----------|
|
||||
| `DWS_WINBACK_INDEX` | `WinbackIndexTask` | `dws_member_winback_index` | WBI(回流指数) | delete-before-insert(按门店全量刷新) |
|
||||
| `DWS_NEWCONV_INDEX` | `NewconvIndexTask` | `dws_member_newconv_index` | NCI(新客转化指数) | delete-before-insert(按门店全量刷新) |
|
||||
| `DWS_RELATION_INDEX` | `RelationIndexTask` | `dws_member_assistant_relation_index` | RS/OS/MS/ML(关系指数) | delete-before-insert(按门店全量刷新) |
|
||||
| `DWS_ML_MANUAL_IMPORT` | `MlManualImportTask` | `dws_ml_manual_order_source` / `dws_ml_manual_order_alloc` | ML(手动台账导入) | 按 scope 先删后写 |
|
||||
|
||||
> 注册位置:`orchestration/task_registry.py`,所有 INDEX 任务的 `requires_db_config=False`、`layer="INDEX"`。
|
||||
|
||||
---
|
||||
|
||||
## BaseIndexTask 公共机制
|
||||
|
||||
`BaseIndexTask`(位于 `tasks/dws/index/base_index_task.py`)继承自 `BaseDwsTask`,为所有指数任务提供统一的算法基础设施。
|
||||
|
||||
### 继承层次
|
||||
|
||||
```
|
||||
BaseTask
|
||||
└── BaseDwsTask
|
||||
└── BaseIndexTask
|
||||
├── MemberIndexBaseTask ← WBI / NCI 共享的会员特征提取
|
||||
│ ├── WinbackIndexTask
|
||||
│ └── NewconvIndexTask
|
||||
├── RelationIndexTask ← RS/OS/MS/ML 四合一
|
||||
└── MlManualImportTask ← ML 人工台账导入
|
||||
```
|
||||
|
||||
### 子类必须实现的抽象方法
|
||||
|
||||
```python
|
||||
def get_index_type(self) -> str:
|
||||
"""返回指数类型标识,如 'WBI'、'NCI'、'RS'"""
|
||||
```
|
||||
|
||||
### 核心能力
|
||||
|
||||
#### 1. 半衰期时间衰减函数
|
||||
|
||||
所有指数共享的时间权重模型,核心思想是"越近越重要":
|
||||
|
||||
```
|
||||
decay(d; h) = exp(-ln(2) × d / h)
|
||||
```
|
||||
|
||||
| 参数 | 含义 | 示例 |
|
||||
|------|------|------|
|
||||
| `d` | 事件距今天数(≥0) | 7 天 |
|
||||
| `h` | 半衰期(>0),单位:天 | 7 天 |
|
||||
| 返回值 | 衰减权重,范围 (0, 1] | 0.5 |
|
||||
|
||||
当 `d = h` 时权重恰好衰减到 0.5;`d = 0` 时权重为 1.0。
|
||||
|
||||
#### 2. 分位数计算与截断(Winsorize)
|
||||
|
||||
用于消除极端值对归一化的影响:
|
||||
|
||||
1. 计算 P5 和 P95 分位点
|
||||
2. 将所有 Raw Score 截断到 [P5, P95] 范围内
|
||||
|
||||
```python
|
||||
calculate_percentiles(scores, lower=5, upper=95) → (P5, P95)
|
||||
winsorize(value, lower, upper) → clipped_value
|
||||
```
|
||||
|
||||
#### 3. 0-10 归一化映射
|
||||
|
||||
将 Raw Score 映射到 0-10 分的 Display Score,便于业务理解和排序:
|
||||
|
||||
```
|
||||
映射流程:Raw Score → [可选压缩] → Winsorize(P5, P95) → MinMax(0, 10)
|
||||
```
|
||||
|
||||
压缩模式(由 `compression_mode` 参数控制):
|
||||
|
||||
| compression_mode | 方式 | 公式 | 适用场景 |
|
||||
|------------------|------|------|----------|
|
||||
| 0 | 无压缩 | `y = x` | 分布较均匀时 |
|
||||
| 1 | log1p | `y = ln(1 + x)` | 右偏分布(默认) |
|
||||
| 2 | asinh | `y = asinh(x)` | 含负值或极端右偏 |
|
||||
|
||||
当所有分数几乎相同(`max - min < ε`)时,返回中间值 5.0。
|
||||
|
||||
#### 4. 算法参数加载
|
||||
|
||||
从 `billiards_dws.cfg_index_parameters` 表按 `index_type` 加载参数:
|
||||
|
||||
- 按 `effective_from` 降序取最新生效的参数值
|
||||
- 支持按 `index_type` 隔离的内存缓存(TTL = 300 秒)
|
||||
- 子类可通过 `get_param(name, default)` 获取单个参数
|
||||
|
||||
```python
|
||||
load_index_parameters(index_type=None) → Dict[str, float]
|
||||
get_param(name, default=0.0, index_type=None) → float
|
||||
```
|
||||
|
||||
#### 5. 分位点历史管理(EWMA 平滑)
|
||||
|
||||
为避免分位点在不同批次间剧烈波动,支持 EWMA(指数加权移动平均)平滑:
|
||||
|
||||
```
|
||||
Q_t = (1 - α) × Q_{t-1} + α × Q_now
|
||||
```
|
||||
|
||||
| 参数 | 含义 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `α`(ewma_alpha) | 平滑系数,越大越跟随当前值 | 0.2 |
|
||||
| `Q_{t-1}` | 上一次平滑后的分位点 | 从 `dws_index_percentile_history` 表读取 |
|
||||
| `Q_now` | 当前批次计算的分位点 | 实时计算 |
|
||||
|
||||
首次计算时无历史记录,直接使用当前分位点(不平滑)。每次计算后将原始分位点和平滑分位点保存到 `dws_index_percentile_history` 表。
|
||||
|
||||
#### 6. 统计工具方法
|
||||
|
||||
| 方法 | 功能 |
|
||||
|------|------|
|
||||
| `calculate_median(values)` | 中位数 |
|
||||
| `calculate_mad(values)` | MAD(中位绝对偏差),比标准差更稳健 |
|
||||
| `safe_log(value)` | 安全对数(value ≤ 0 时返回默认值) |
|
||||
| `safe_ln1p(value)` | 安全的 `ln(1+x)` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## MemberIndexBaseTask 会员指数共享基类
|
||||
|
||||
`MemberIndexBaseTask`(位于 `tasks/dws/index/member_index_base.py`)继承自 `BaseIndexTask`,为 WBI 和 NCI 提供共享的会员活动特征提取逻辑。
|
||||
|
||||
### 会员活动特征(MemberActivityData)
|
||||
|
||||
从 DWD 层提取并计算的会员特征数据结构:
|
||||
|
||||
| 字段 | 类型 | 含义 |
|
||||
|------|------|------|
|
||||
| `member_id` | int | 会员 ID |
|
||||
| `site_id` / `tenant_id` | int | 门店 / 租户 ID |
|
||||
| `member_create_time` | datetime | 会员建档时间 |
|
||||
| `first_visit_time` | datetime | 首次到店时间 |
|
||||
| `last_visit_time` | datetime | 最近到店时间 |
|
||||
| `last_recharge_time` | datetime | 最近充值时间 |
|
||||
| `t_v` | float | 距最近到店天数(截断到 recency 窗口) |
|
||||
| `t_r` | float | 距最近充值天数(截断到 recency 窗口) |
|
||||
| `t_a` | float | `min(t_v, t_r)`,综合活跃度 |
|
||||
| `visits_14d` / `visits_60d` / `visits_total` | int | 近 14 天 / 60 天 / 总到店次数 |
|
||||
| `spend_30d` / `spend_180d` | float | 近 30 天 / 180 天消费金额 |
|
||||
| `sv_balance` | float | 储值卡余额 |
|
||||
| `recharge_60d_amt` | float | 近 60 天充值金额 |
|
||||
| `intervals` | List[float] | 到店间隔天数序列 |
|
||||
| `interval_ages_days` | List[int] | 每个间隔对应的"年龄"(距今天数) |
|
||||
| `recharge_unconsumed` | int | 充值后是否未回访(1=是) |
|
||||
|
||||
### 数据来源
|
||||
|
||||
| 数据 | 来源表 | 提取方式 |
|
||||
|------|--------|----------|
|
||||
| 到店记录 | `billiards_dwd.dwd_settlement_head` | 按天去重,仅计入正常结账(settle_type=1)和激励课结账(settle_type=3 且关联 BONUS 技能) |
|
||||
| 充值记录 | `billiards_dwd.dwd_recharge_order` | settle_type=5,近 recency_days 天 |
|
||||
| 会员建档时间 | `billiards_dwd.dim_member` | scd2_is_current=1 |
|
||||
| 首次到店时间 | `billiards_dwd.dwd_settlement_head` | 全量 MIN(pay_time) |
|
||||
| 储值卡余额 | `billiards_dwd.dim_member_card_account` | 按 card_type_id 筛选现金卡 |
|
||||
|
||||
> 会员 ID 规范化:优先使用 `member_id`,若为 0 则通过 `dim_member_card_account` 关联取 `tenant_member_id`。
|
||||
|
||||
### 会员分群(classify_segment)
|
||||
|
||||
WBI 和 NCI 共享的三分群逻辑,决定会员进入哪个指数的计算范围:
|
||||
|
||||
| 分群 | 条件 | 进入指数 |
|
||||
|------|------|----------|
|
||||
| **STOP** | `t_a ≥ recency_days`(默认 60 天无活动) | 不参与评分(除 STOP_HIGH_BALANCE 例外) |
|
||||
| **NEW** | 满足以下任一:到店 ≤ 2 次、首访 ≤ 30 天、近期充值未回访 | NCI |
|
||||
| **OLD** | 不满足 STOP 和 NEW 条件 | WBI |
|
||||
|
||||
STOP_HIGH_BALANCE 例外:当 `enable_stop_high_balance_exception=1` 且储值余额 ≥ `high_balance_threshold`(默认 1000 元)时,STOP 会员仍参与 WBI 评分。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DWS_WINBACK_INDEX — 老客挽回指数(WBI)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWS_WINBACK_INDEX` |
|
||||
| Python 类 | `WinbackIndexTask`(`tasks/dws/index/winback_index_task.py`) |
|
||||
| 继承链 | `BaseTask → BaseDwsTask → BaseIndexTask → MemberIndexBaseTask → WinbackIndexTask` |
|
||||
| 目标表 | `billiards_dws.dws_member_winback_index` |
|
||||
| 主键 | `site_id, member_id` |
|
||||
| 指数类型 | `WBI` |
|
||||
| 更新策略 | 按门店全量刷新(先 DELETE WHERE site_id = %s,再 INSERT) |
|
||||
|
||||
### 业务含义
|
||||
|
||||
WBI 衡量老客的"挽回紧急度"——分数越高,表示该会员越需要运营人员主动触达。适用于已有多次到店记录但近期活跃度下降的老客群体。
|
||||
|
||||
### 计算范围
|
||||
|
||||
仅对 `segment = "OLD"` 或 `status = "STOP_HIGH_BALANCE"` 的会员计算。
|
||||
|
||||
### 算法概要
|
||||
|
||||
WBI Raw Score 由 4 个分项加权求和,再乘以近期抑制系数:
|
||||
|
||||
```
|
||||
WBI_raw = suppression × (w_over × Overdue + w_drop × Drop + w_re × Recharge + w_value × Value)
|
||||
```
|
||||
|
||||
#### 分项 1:超期紧急性(Overdue)
|
||||
|
||||
基于会员个人历史到店间隔的加权经验 CDF,衡量当前缺席天数的异常程度:
|
||||
|
||||
1. 收集会员历史到店间隔序列 `{interval_i, age_i}`
|
||||
2. 计算加权 CDF:`P(interval ≤ t_v)`,权重按间隔年龄半衰期衰减
|
||||
3. 对小样本混合等权分布与加权分布(`λ = min(1, N / blend_min_samples)`)
|
||||
4. `Overdue = P^α`(α 默认 2.0,放大高概率区间的紧急性)
|
||||
|
||||
同时计算理想到店间隔(加权中位数),用于推算 `ideal_next_visit_date`。
|
||||
|
||||
#### 分项 2:降频分(Drop)
|
||||
|
||||
检测近期到店频率是否低于历史均值:
|
||||
|
||||
```
|
||||
expected_14d = visits_60d × 14 / 60
|
||||
Drop = clip((expected_14d - visits_14d) / (expected_14d + 1), 0, 1)
|
||||
```
|
||||
|
||||
#### 分项 3:充值未回访压力(Recharge)
|
||||
|
||||
若会员充值后未回访(`recharge_unconsumed = 1`),按充值距今天数衰减:
|
||||
|
||||
```
|
||||
Recharge = decay(t_r, h_recharge) # h_recharge 默认 7 天
|
||||
```
|
||||
|
||||
#### 分项 4:价值分(Value)
|
||||
|
||||
综合消费金额和储值余额的对数压缩:
|
||||
|
||||
```
|
||||
Value = w_spend × ln(1 + spend_180d / M0) + w_bal × ln(1 + sv_balance / B0)
|
||||
```
|
||||
|
||||
| 参数 | 默认值 | 含义 |
|
||||
|------|--------|------|
|
||||
| `M0` (amount_base_M0) | 300 | 消费金额压缩基准 |
|
||||
| `B0` (balance_base_B0) | 500 | 余额压缩基准 |
|
||||
|
||||
#### 近期抑制(Suppression)
|
||||
|
||||
防止刚到店的会员获得高分,使用 Sigmoid 门控:
|
||||
|
||||
```
|
||||
suppression = σ((t_v - gate_days) / slope_days)
|
||||
```
|
||||
|
||||
- 当 `t_v < hard_floor_days`(默认 14 天)时,`suppression = 0`(完全抑制)
|
||||
- 当 `t_v` 远大于 `gate_days` 时,`suppression → 1`(不抑制)
|
||||
|
||||
### 默认权重
|
||||
|
||||
| 参数 | 默认值 | 含义 |
|
||||
|------|--------|------|
|
||||
| `w_over` | 2.0 | 超期紧急性权重 |
|
||||
| `w_drop` | 1.0 | 降频权重 |
|
||||
| `w_re` | 0.4 | 充值压力权重 |
|
||||
| `w_value` | 1.2 | 价值权重 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DWS_NEWCONV_INDEX — 新客转化指数(NCI)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWS_NEWCONV_INDEX` |
|
||||
| Python 类 | `NewconvIndexTask`(`tasks/dws/index/newconv_index_task.py`) |
|
||||
| 继承链 | `BaseTask → BaseDwsTask → BaseIndexTask → MemberIndexBaseTask → NewconvIndexTask` |
|
||||
| 目标表 | `billiards_dws.dws_member_newconv_index` |
|
||||
| 主键 | `site_id, member_id` |
|
||||
| 指数类型 | `NCI` |
|
||||
| 更新策略 | 按门店全量刷新(先 DELETE WHERE site_id = %s,再 INSERT) |
|
||||
|
||||
### 业务含义
|
||||
|
||||
NCI 衡量新客的"转化紧迫度"——分数越高,表示该新客越需要及时跟进以促成二访/三访。适用于首次到店不久或到店次数较少的新客群体。
|
||||
|
||||
### 计算范围
|
||||
|
||||
仅对 `segment = "NEW"` 的会员计算。
|
||||
|
||||
### 算法概要
|
||||
|
||||
NCI 由两部分组成:欢迎建联分(Welcome)和转化召回分(Convert),合并为总分:
|
||||
|
||||
```
|
||||
NCI_raw = raw_score_welcome + raw_score_convert
|
||||
```
|
||||
|
||||
#### 欢迎建联分(Welcome)
|
||||
|
||||
针对首访后 3 天内的会员,鼓励立即触达:
|
||||
|
||||
```
|
||||
welcome_new = clip(1 - t_v / welcome_window_days, 0, 1) # 仅 visits_total ≤ 1 时生效
|
||||
raw_score_welcome = w_welcome × welcome_new
|
||||
```
|
||||
|
||||
#### 转化召回分(Convert)
|
||||
|
||||
由 4 个分项加权组成,并受活跃度抑制:
|
||||
|
||||
```
|
||||
raw_score_convert = active_multiplier × (
|
||||
w_need × (Need × Salvage) + w_re × Recharge × touch_multiplier + w_value × Value × touch_multiplier
|
||||
)
|
||||
```
|
||||
|
||||
##### 分项 1:紧迫度(Need)
|
||||
|
||||
衡量距二访目标窗口的紧迫程度:
|
||||
|
||||
```
|
||||
Need = clip((t_v - no_touch_days) / (2 × t2_target_days - no_touch_days), 0, 1)
|
||||
```
|
||||
|
||||
- `no_touch_days`(默认 3 天):免打扰窗口,首访后短期内不催促
|
||||
- `t2_target_days`(默认 7 天):二访目标天数
|
||||
|
||||
##### 分项 2:挽救系数(Salvage)
|
||||
|
||||
30-60 天线性衰减,超过 60 天视为流失:
|
||||
|
||||
```
|
||||
Salvage = clip((salvage_end - t_a) / (salvage_end - salvage_start), 0, 1)
|
||||
```
|
||||
|
||||
##### 分项 3:充值未回访压力(Recharge)
|
||||
|
||||
与 WBI 相同:`Recharge = decay(t_r, h_recharge)`
|
||||
|
||||
##### 分项 4:价值分(Value)
|
||||
|
||||
与 WBI 相同:`Value = w_spend × ln(1 + spend_180d / M0) + w_bal × ln(1 + sv_balance / B0)`
|
||||
|
||||
#### 活跃新客抑制
|
||||
|
||||
近期高频到店的新客不需要催促,降低其排名权重:
|
||||
|
||||
```
|
||||
若 visits_14d ≥ active_new_visit_threshold_14d 且 t_v ≤ active_new_recency_days:
|
||||
active_multiplier = active_new_penalty (默认 0.2)
|
||||
否则:
|
||||
active_multiplier = 1.0
|
||||
```
|
||||
|
||||
#### 免打扰窗口乘数
|
||||
|
||||
价值分和充值分在进入免打扰窗口后才逐步生效:
|
||||
|
||||
```
|
||||
touch_multiplier = clip(t_v / no_touch_days, 0, 1)
|
||||
```
|
||||
|
||||
### Display Score 归一化
|
||||
|
||||
NCI 产出 3 个 Display Score:
|
||||
- `display_score`:总分归一化(使用 EWMA 平滑)
|
||||
- `display_score_welcome`:欢迎分归一化(不平滑)
|
||||
- `display_score_convert`:转化分归一化(不平滑)
|
||||
|
||||
### 默认权重
|
||||
|
||||
| 参数 | 默认值 | 含义 |
|
||||
|------|--------|------|
|
||||
| `w_welcome` | 1.0 | 欢迎建联权重 |
|
||||
| `w_need` | 1.6 | 紧迫度权重 |
|
||||
| `w_re` | 0.8 | 充值压力权重 |
|
||||
| `w_value` | 1.0 | 价值权重 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DWS_RELATION_INDEX — 关系指数(RS/OS/MS/ML)
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWS_RELATION_INDEX` |
|
||||
| Python 类 | `RelationIndexTask`(`tasks/dws/index/relation_index_task.py`) |
|
||||
| 继承链 | `BaseTask → BaseDwsTask → BaseIndexTask → RelationIndexTask` |
|
||||
| 目标表 | `billiards_dws.dws_member_assistant_relation_index` |
|
||||
| 主键 | `site_id, member_id, assistant_id` |
|
||||
| 指数类型 | RS / OS / MS / ML(单任务产出四个子指数) |
|
||||
| 更新策略 | 按门店全量刷新(先 DELETE WHERE site_id = %s,再 INSERT) |
|
||||
|
||||
### 业务含义
|
||||
|
||||
关系指数以"会员-助教"关系对为粒度,一次执行产出 4 个子指数:
|
||||
|
||||
| 子指数 | 全称 | 含义 |
|
||||
|--------|------|------|
|
||||
| **RS** | Relation Strength | 关系强度——衡量会员与助教之间的服务关系紧密程度 |
|
||||
| **OS** | Ownership Share | 归属份额——确定会员的"主责助教"归属关系 |
|
||||
| **MS** | Momentum Score | 升温动量——衡量关系是在升温还是降温 |
|
||||
| **ML** | Money Link | 付费关联——基于人工台账的付费归因强度 |
|
||||
|
||||
### 数据来源
|
||||
|
||||
| 数据 | 来源表 | 说明 |
|
||||
|------|--------|------|
|
||||
| 服务记录 | `billiards_dwd.dwd_assistant_service_log` | RS/MS 的核心数据源 |
|
||||
| 助教维度 | `billiards_dwd.dim_assistant` | 通过 user_id 关联获取 assistant_id |
|
||||
| 人工台账 | `billiards_dws.dws_ml_manual_order_alloc` | ML 的唯一数据源 |
|
||||
|
||||
### 会话合并
|
||||
|
||||
服务记录按 `(member_id, assistant_id)` 分组后,相邻服务间隔 ≤ `session_merge_hours`(默认 4 小时)的记录合并为一个会话(ServiceSession)。合并后保留:
|
||||
- 会话起止时间、总时长
|
||||
- 课程权重(激励课 `course_weight = incentive_weight`,默认 1.5;普通课 = 1.0)
|
||||
- 是否包含激励课标记
|
||||
|
||||
### 子指数 1:RS(关系强度)
|
||||
|
||||
```
|
||||
RS_raw = (w_f × F + w_d × D) × Gate(R)
|
||||
```
|
||||
|
||||
| 分项 | 公式 | 含义 |
|
||||
|------|------|------|
|
||||
| F(频次) | `Σ course_weight × decay(days_ago, halflife_session)` | 加权会话频次 |
|
||||
| D(时长) | `Σ √(duration_min / 60) × course_weight × decay(days_ago, halflife_session)` | 加权服务时长 |
|
||||
| R(近期性) | `decay(days_since_last_session, halflife_last)` | 最近一次服务的时间衰减 |
|
||||
| Gate | `R^gate_alpha` | 近期性门控,无近期服务则整体压低 |
|
||||
|
||||
默认参数:`halflife_session=14`, `halflife_last=10`, `w_f=1.0`, `w_d=0.7`, `gate_alpha=0.6`
|
||||
|
||||
### 子指数 2:OS(归属份额)
|
||||
|
||||
OS 不是独立计算的 Raw Score,而是基于 RS_raw 的份额分配:
|
||||
|
||||
1. 筛选 `rs_raw ≥ min_rs_raw_for_ownership`(默认 0.05)的关系对
|
||||
2. 计算份额:`os_share = rs_raw / Σ rs_raw`(同一会员下所有合格助教)
|
||||
3. 若 `Σ rs_raw < min_total_rs_raw`(默认 0.10),标记为 `UNASSIGNED`
|
||||
|
||||
归属标签判定规则:
|
||||
|
||||
| 标签 | 条件 |
|
||||
|------|------|
|
||||
| `MAIN` | 第一名份额 ≥ `ownership_main_threshold`(0.60)且与第二名差距 ≥ `ownership_gap_threshold`(0.15) |
|
||||
| `COMANAGE` | 份额 ≥ `ownership_comanage_threshold`(0.35)但不满足 MAIN 条件 |
|
||||
| `POOL` | 其余合格关系对 |
|
||||
| `UNASSIGNED` | 总 RS 不足,无法形成稳定归属 |
|
||||
|
||||
### 子指数 3:MS(升温动量)
|
||||
|
||||
衡量关系是在升温(MS > 0)还是降温(MS ≈ 0):
|
||||
|
||||
```
|
||||
f_short = Σ course_weight × decay(days_ago, halflife_short) # 短期半衰期 7 天
|
||||
f_long = Σ course_weight × decay(days_ago, halflife_long) # 长期半衰期 30 天
|
||||
MS_raw = max(0, ln(f_short + ε) / (f_long + ε))
|
||||
```
|
||||
|
||||
短期频次高于长期频次时 MS 为正,表示关系在升温。
|
||||
|
||||
### 子指数 4:ML(付费关联)
|
||||
|
||||
以人工台账窄表(`dws_ml_manual_order_alloc`)为唯一数据源:
|
||||
|
||||
```
|
||||
ML_raw = Σ ln(1 + allocated_amount / amount_base) × decay(days_ago, halflife_recharge)
|
||||
```
|
||||
|
||||
| 参数 | 默认值 | 含义 |
|
||||
|------|--------|------|
|
||||
| `amount_base` | 500 | 金额压缩基准 |
|
||||
| `halflife_recharge` | 21 天 | 充值半衰期 |
|
||||
|
||||
若某 `(member_id, assistant_id)` 对仅在台账中出现而无服务记录,会自动创建关系对。
|
||||
|
||||
### Display Score 归一化
|
||||
|
||||
RS、MS、ML 各自独立归一化到 0-10 分,分位历史按 `index_type` 隔离(分别记录 RS/MS/ML 的分位点)。OS 不做归一化,直接输出份额和标签。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DWS_ML_MANUAL_IMPORT — ML 人工台账导入
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DWS_ML_MANUAL_IMPORT` |
|
||||
| Python 类 | `MlManualImportTask`(`tasks/dws/index/ml_manual_import_task.py`) |
|
||||
| 继承链 | `BaseTask → BaseDwsTask → BaseIndexTask → MlManualImportTask` |
|
||||
| 目标表 | `billiards_dws.dws_ml_manual_order_source`(宽表)+ `billiards_dws.dws_ml_manual_order_alloc`(窄表) |
|
||||
| 主键 | 宽表:`site_id, external_id, import_scope_key, row_no`;窄表:`site_id, external_id, assistant_id` |
|
||||
| 指数类型 | `ML` |
|
||||
| 更新策略 | 按 scope 先删后写(DAY 或 P30 批次覆盖) |
|
||||
|
||||
### 业务含义
|
||||
|
||||
ML 人工台账导入是一个工具型任务,用于将运营人员手工整理的订单-助教归因数据导入系统。导入后的数据作为 `DWS_RELATION_INDEX` 任务中 ML 子指数的唯一数据源。
|
||||
|
||||
该任务不依赖时间窗口,由调度器以工具任务方式直接触发。
|
||||
|
||||
### 文件路径解析
|
||||
|
||||
按以下优先级查找台账文件:
|
||||
|
||||
1. 配置项 `run.ml_manual_ledger_file`
|
||||
2. 配置项 `run.ml_manual_file`
|
||||
3. 环境变量 `ML_MANUAL_LEDGER_FILE`
|
||||
|
||||
### Excel 模板格式
|
||||
|
||||
台账文件为 `.xlsx` 格式,第一行为表头,第二行起为数据。模板列定义:
|
||||
|
||||
| 列名 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `site_id` | int | 否(默认取配置) | 门店 ID |
|
||||
| `biz_date` | date | 是 | 业务日期 |
|
||||
| `external_id` | string | 是 | 外部订单 ID(唯一标识) |
|
||||
| `member_id` | int | 否 | 会员 ID |
|
||||
| `pay_time` | datetime | 否(默认取 biz_date) | 支付时间 |
|
||||
| `order_amount` | decimal | 否 | 订单金额 |
|
||||
| `currency` | string | 否(默认 CNY) | 币种 |
|
||||
| `assistant_id_1` ~ `assistant_id_5` | int | 否 | 助教 ID(最多 5 个) |
|
||||
| `assistant_name_1` ~ `assistant_name_5` | string | 否 | 助教姓名 |
|
||||
| `remark` | string | 否 | 备注 |
|
||||
|
||||
### 导入逻辑
|
||||
|
||||
#### 1. 读取与规范化
|
||||
|
||||
- 使用 `openpyxl` 读取 Excel,跳过空行
|
||||
- 每行规范化:类型转换、缺省值填充、助教列表提取
|
||||
- `external_id` 为必填,缺失则抛出 `ValueError`
|
||||
|
||||
#### 2. 助教分摊
|
||||
|
||||
同一订单支持最多 5 个助教归因,默认均分:
|
||||
|
||||
```
|
||||
share_ratio = 1 / N
|
||||
allocated_amount = order_amount × share_ratio
|
||||
```
|
||||
|
||||
#### 3. 覆盖策略(ImportScope)
|
||||
|
||||
根据 `biz_date` 与当前日期的距离,采用不同的覆盖粒度:
|
||||
|
||||
| 条件 | scope_type | 覆盖范围 | 说明 |
|
||||
|------|------------|----------|------|
|
||||
| `today - biz_date ≤ 30 天` | `DAY` | 单日 | 按 `site_id + biz_date` 日覆盖 |
|
||||
| `today - biz_date > 30 天` | `P30` | 30 天批次 | 以固定纪元(2026-01-01)为锚点,按 30 天分桶 |
|
||||
|
||||
P30 分桶算法:
|
||||
```
|
||||
bucket_index = (biz_date - EPOCH_ANCHOR).days // 30
|
||||
bucket_start = EPOCH_ANCHOR + bucket_index × 30 天
|
||||
bucket_end = bucket_start + 29 天
|
||||
```
|
||||
|
||||
#### 4. 写入流程
|
||||
|
||||
1. 按 scope 删除旧数据(宽表 + 窄表)
|
||||
2. 插入宽表(`dws_ml_manual_order_source`)
|
||||
3. Upsert 窄表(`dws_ml_manual_order_alloc`),冲突键为 `(site_id, external_id, assistant_id)`
|
||||
4. 提交事务
|
||||
|
||||
#### 5. 导入批次号
|
||||
|
||||
格式:`MLM_<YYYYMMDDHHmmss>_<uuid8>`,如 `MLM_20260215143022_a1b2c3d4`
|
||||
|
||||
导入用户按优先级取:环境变量 `ETL_OPERATOR` → `USERNAME` → `USER` → `"system"`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## cfg_index_parameters 配置表
|
||||
|
||||
所有指数任务的算法参数统一存储在 `billiards_dws.cfg_index_parameters` 表中,支持按时间生效和历史追溯。
|
||||
|
||||
### 表结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `param_id` | SERIAL PK | 自增主键 |
|
||||
| `index_type` | VARCHAR(50) NOT NULL | 指数类型:`RS` / `OS` / `MS` / `ML` / `NCI` / `WBI` |
|
||||
| `param_name` | VARCHAR(100) NOT NULL | 参数名称 |
|
||||
| `param_value` | NUMERIC(14,6) NOT NULL | 参数值 |
|
||||
| `description` | TEXT | 参数说明 |
|
||||
| `effective_from` | DATE NOT NULL | 生效起始日期(默认当天) |
|
||||
| `effective_to` | DATE | 生效截止日期(NULL = 永久有效) |
|
||||
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||
| `updated_at` | TIMESTAMPTZ | 更新时间 |
|
||||
|
||||
唯一约束:`(index_type, param_name, effective_from)`
|
||||
|
||||
索引:`idx_cfg_index_params_type`(index_type)、`idx_cfg_index_params_effective`(effective_from, effective_to)
|
||||
|
||||
### 参数加载逻辑
|
||||
|
||||
```sql
|
||||
SELECT param_name, param_value
|
||||
FROM billiards_dws.cfg_index_parameters
|
||||
WHERE index_type = %s
|
||||
AND effective_from <= CURRENT_DATE
|
||||
AND (effective_to IS NULL OR effective_to >= CURRENT_DATE)
|
||||
ORDER BY effective_from DESC
|
||||
```
|
||||
|
||||
同一 `param_name` 若有多条生效记录,取 `effective_from` 最新的一条(代码中通过 `seen` 集合去重)。
|
||||
|
||||
### 参数调优方式
|
||||
|
||||
新增一条 `effective_from` 为新日期的记录即可覆盖旧参数,旧记录自动失效(无需删除)。如需回滚,将新记录的 `effective_to` 设为过去日期即可。
|
||||
|
||||
### WBI 参数清单
|
||||
|
||||
| 参数名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `lookback_days_recency` | 60 | 近期活跃判定窗口(天) |
|
||||
| `visit_lookback_days` | 180 | 到店记录回溯窗口(天) |
|
||||
| `percentile_lower` / `percentile_upper` | 5 / 95 | 归一化分位点 |
|
||||
| `compression_mode` | 0 | 压缩模式(0=无/1=log1p/2=asinh) |
|
||||
| `use_smoothing` | 1 | 是否启用 EWMA 分位平滑 |
|
||||
| `ewma_alpha` | 0.2 | EWMA 平滑系数 |
|
||||
| `new_visit_threshold` | 2 | 新客到店次数阈值 |
|
||||
| `new_days_threshold` | 30 | 新客首访天数阈值 |
|
||||
| `recharge_recent_days` | 14 | 近期充值窗口(天) |
|
||||
| `new_recharge_max_visits` | 10 | 充值新客最大到店次数 |
|
||||
| `overdue_alpha` | 2.0 | 超期 CDF 幂指数 |
|
||||
| `overdue_weight_halflife_days` | 30 | 超期加权 CDF 间隔半衰期(天) |
|
||||
| `overdue_weight_blend_min_samples` | 8 | 加权 CDF 最小样本数 |
|
||||
| `h_recharge` | 7 | 充值衰减半衰期(天) |
|
||||
| `amount_base_M0` | 300 | 消费金额压缩基准 |
|
||||
| `balance_base_B0` | 500 | 余额压缩基准 |
|
||||
| `value_w_spend` / `value_w_bal` | 1.0 / 1.0 | 价值分中消费/余额权重 |
|
||||
| `w_over` / `w_drop` / `w_re` / `w_value` | 2.0 / 1.0 / 0.4 / 1.2 | 四分项权重 |
|
||||
| `recency_hard_floor_days` | 14 | 近期硬抑制天数 |
|
||||
| `recency_gate_days` | 14 | Sigmoid 门控中心(天) |
|
||||
| `recency_gate_slope_days` | 3 | Sigmoid 门控斜率(天) |
|
||||
| `enable_stop_high_balance_exception` | 0 | 是否启用 STOP 高余额例外 |
|
||||
| `high_balance_threshold` | 1000 | 高余额阈值(元) |
|
||||
|
||||
### NCI 参数清单
|
||||
|
||||
| 参数名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `lookback_days_recency` | 60 | 近期活跃判定窗口(天) |
|
||||
| `visit_lookback_days` | 180 | 到店记录回溯窗口(天) |
|
||||
| `percentile_lower` / `percentile_upper` | 5 / 95 | 归一化分位点 |
|
||||
| `compression_mode` | 0 | 压缩模式 |
|
||||
| `use_smoothing` | 1 | 是否启用 EWMA 分位平滑 |
|
||||
| `ewma_alpha` | 0.2 | EWMA 平滑系数 |
|
||||
| `no_touch_days_new` | 3 | 免打扰窗口(天) |
|
||||
| `t2_target_days` | 7 | 二访目标天数 |
|
||||
| `salvage_start` / `salvage_end` | 30 / 60 | 挽救系数衰减区间(天) |
|
||||
| `welcome_window_days` | 3 | 欢迎建联窗口(天) |
|
||||
| `active_new_visit_threshold_14d` | 2 | 活跃新客 14 天到店阈值 |
|
||||
| `active_new_recency_days` | 7 | 活跃新客近期天数 |
|
||||
| `active_new_penalty` | 0.2 | 活跃新客抑制系数 |
|
||||
| `h_recharge` | 7 | 充值衰减半衰期(天) |
|
||||
| `amount_base_M0` / `balance_base_B0` | 300 / 500 | 价值分压缩基准 |
|
||||
| `value_w_spend` / `value_w_bal` | 1.0 / 0.8 | 价值分权重 |
|
||||
| `w_welcome` / `w_need` / `w_re` / `w_value` | 1.0 / 1.6 / 0.8 / 1.0 | 分项权重 |
|
||||
|
||||
### RS 参数清单
|
||||
|
||||
| 参数名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `lookback_days` | 60 | 服务行为回溯窗口(天) |
|
||||
| `session_merge_hours` | 4 | 会话合并阈值(小时) |
|
||||
| `incentive_weight` | 1.5 | 激励课权重 |
|
||||
| `halflife_session` | 14 | 会话半衰期(天) |
|
||||
| `halflife_last` | 10 | 最近服务半衰期(天) |
|
||||
| `weight_f` / `weight_d` | 1.0 / 0.7 | 频次/时长权重 |
|
||||
| `gate_alpha` | 0.6 | 近期性门控指数 |
|
||||
| `percentile_lower` / `percentile_upper` | 5 / 95 | 归一化分位点 |
|
||||
| `compression_mode` | 1 | 压缩模式(默认 log1p) |
|
||||
| `use_smoothing` / `ewma_alpha` | 1 / 0.2 | EWMA 平滑 |
|
||||
|
||||
### OS 参数清单
|
||||
|
||||
| 参数名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `min_rs_raw_for_ownership` | 0.05 | 参与归属计算的最小 RS_raw |
|
||||
| `min_total_rs_raw` | 0.10 | 形成稳定归属的最小 sum_rs |
|
||||
| `ownership_main_threshold` | 0.60 | 主责份额阈值 |
|
||||
| `ownership_comanage_threshold` | 0.35 | 共管份额阈值 |
|
||||
| `ownership_gap_threshold` | 0.15 | 主责与次席差距阈值 |
|
||||
| `eps` | 0.000001 | 数值稳定项 |
|
||||
|
||||
### MS 参数清单
|
||||
|
||||
| 参数名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `lookback_days` | 60 | 服务行为回溯窗口(天) |
|
||||
| `session_merge_hours` | 4 | 会话合并阈值(小时) |
|
||||
| `incentive_weight` | 1.5 | 激励课权重 |
|
||||
| `halflife_short` / `halflife_long` | 7 / 30 | 短期/长期半衰期(天) |
|
||||
| `eps` | 0.000001 | 数值稳定项 |
|
||||
| `percentile_lower` / `percentile_upper` | 5 / 95 | 归一化分位点 |
|
||||
| `compression_mode` | 1 | 压缩模式(默认 log1p) |
|
||||
| `use_smoothing` / `ewma_alpha` | 1 / 0.2 | EWMA 平滑 |
|
||||
|
||||
### ML 参数清单
|
||||
|
||||
| 参数名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `lookback_days` | 60 | 充值行为回溯窗口(天) |
|
||||
| `amount_base` | 500 | 金额压缩基准 |
|
||||
| `halflife_recharge` | 21 | 充值半衰期(天) |
|
||||
| `percentile_lower` / `percentile_upper` | 5 / 95 | 归一化分位点 |
|
||||
| `compression_mode` | 1 | 压缩模式(默认 log1p) |
|
||||
| `use_smoothing` / `ewma_alpha` | 1 / 0.2 | EWMA 平滑 |
|
||||
|
||||
> 种子数据脚本:`database/seed_index_parameters.sql`
|
||||
> DDL 定义:`database/schema_dws.sql`(第 21 节)
|
||||
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`,再逐个调用小票接口获取详情。
|
||||
591
apps/etl/pipelines/feiqiu/docs/etl_tasks/utility_tasks.md
Normal file
591
apps/etl/pipelines/feiqiu/docs/etl_tasks/utility_tasks.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# 工具类任务详解
|
||||
|
||||
> 本文档说明飞球 ETL 系统中所有工具类(Utility)和校验类(Verification)任务。
|
||||
> 这些任务不属于 ODS/DWD/DWS/INDEX 四层业务管线,而是为系统初始化、
|
||||
> 数据灌入、归档、截止时间检查和完整性校验等运维场景服务。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
工具类任务共 8 个(含 1 个校验类任务),注册于 `orchestration/task_registry.py`:
|
||||
|
||||
| 任务代码 | Python 类 | 用途 | task_type | requires_db_config |
|
||||
|----------|-----------|------|-----------|-------------------|
|
||||
| `INIT_ODS_SCHEMA` | `InitOdsSchemaTask` | 执行 ODS + etl_admin DDL,创建必要目录 | utility | `False` |
|
||||
| `INIT_DWD_SCHEMA` | `InitDwdSchemaTask` | 执行 DWD DDL | utility | `False` |
|
||||
| `INIT_DWS_SCHEMA` | `InitDwsSchemaTask` | 执行 DWS DDL | utility | `False` |
|
||||
| `MANUAL_INGEST` | `ManualIngestTask` | 从本地 JSON 文件手动入库到 ODS | utility | `False` |
|
||||
| `ODS_JSON_ARCHIVE` | `OdsJsonArchiveTask` | 在线抓取 ODS 接口数据并落盘 JSON | utility | `False` |
|
||||
| `CHECK_CUTOFF` | `CheckCutoffTask` | 检查各任务/表的数据截止时间 | utility | `False` |
|
||||
| `SEED_DWS_CONFIG` | `SeedDwsConfigTask` | 初始化 DWS 配置种子数据 | utility | `True`(默认) |
|
||||
| `DATA_INTEGRITY_CHECK` | `DataIntegrityTask` | API → ODS → DWD 数据完整性校验 | verification | `False` |
|
||||
|
||||
> 典型执行顺序(首次部署):`INIT_ODS_SCHEMA` → `INIT_DWD_SCHEMA` → `INIT_DWS_SCHEMA` → `SEED_DWS_CONFIG`
|
||||
|
||||
---
|
||||
|
||||
## 1. INIT_ODS_SCHEMA — ODS + etl_admin Schema 初始化
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `INIT_ODS_SCHEMA` |
|
||||
| Python 类 | `tasks.utility.init_schema_task.InitOdsSchemaTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 创建 ODS 层和 etl_admin 调度元数据的数据库结构,并准备运行时目录 |
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract()
|
||||
├── 读取 DDL 文件路径(schema_ODS_doc.sql、schema_etl_admin.sql)
|
||||
├── 收集需创建的目录列表
|
||||
└── 返回 SQL 文本 + 目录列表
|
||||
|
||||
load()
|
||||
├── 创建必要目录(log_root、export_root、fetch_root、ingest_dir)
|
||||
├── 执行 etl_admin DDL(schema_etl_admin.sql)
|
||||
└── 执行 ODS DDL(schema_ODS_doc.sql,清洗后)
|
||||
```
|
||||
|
||||
### 执行的 DDL 文件
|
||||
|
||||
| DDL 文件 | 创建的 Schema | 主要内容 |
|
||||
|----------|--------------|----------|
|
||||
| `database/schema_etl_admin.sql` | `etl_admin` | `etl_task`(任务注册表)、`etl_cursor`(游标表)、`etl_run`(运行记录表) |
|
||||
| `database/schema_ODS_doc.sql` | `billiards_ods` | 20+ 张 ODS 原始表(member_profiles、settlement_records、payment_transactions 等) |
|
||||
|
||||
### ODS DDL 清洗逻辑
|
||||
|
||||
ODS DDL 文件可能包含头部说明文本和 `COMMENT ON` 语句(CamelCase 未加引号会导致执行失败),因此 `load()` 阶段会做轻量清洗:
|
||||
|
||||
1. 定位第一个 `DROP SCHEMA` 语句,丢弃之前的非 SQL 文本
|
||||
2. 逐行过滤掉以 `COMMENT ON` 开头的行
|
||||
|
||||
### 创建的目录
|
||||
|
||||
| 配置路径 | 说明 |
|
||||
|----------|------|
|
||||
| `io.log_root` | 日志输出根目录 |
|
||||
| `io.export_root` | 数据导出根目录 |
|
||||
| `pipeline.fetch_root` | API 抓取数据落盘目录 |
|
||||
| `pipeline.ingest_source_dir` | 手动入库数据源目录(默认同 fetch_root) |
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `schema.ods_file` | `database/schema_ODS_doc.sql` | ODS DDL 文件路径 |
|
||||
| `schema.etl_admin_file` | `database/schema_etl_admin.sql` | etl_admin DDL 文件路径 |
|
||||
| `io.log_root` | — | 日志目录 |
|
||||
| `io.export_root` | — | 导出目录 |
|
||||
| `pipeline.fetch_root` | — | 抓取数据目录 |
|
||||
| `pipeline.ingest_source_dir` | 同 fetch_root | 入库数据源目录 |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
python -m cli.main --tasks INIT_ODS_SCHEMA --pg-dsn "$PG_DSN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. INIT_DWD_SCHEMA — DWD Schema 初始化
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `INIT_DWD_SCHEMA` |
|
||||
| Python 类 | `tasks.utility.init_dwd_schema_task.InitDwdSchemaTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 创建 DWD 明细数据层的数据库结构 |
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract()
|
||||
├── 读取 DDL 文件路径(schema_dwd_doc.sql)
|
||||
└── 读取 drop_first 配置
|
||||
|
||||
load()
|
||||
├── [可选] DROP SCHEMA billiards_dwd CASCADE
|
||||
└── 执行 DWD DDL(schema_dwd_doc.sql)
|
||||
```
|
||||
|
||||
### 执行的 DDL 文件
|
||||
|
||||
| DDL 文件 | 创建的 Schema | 主要内容 |
|
||||
|----------|--------------|----------|
|
||||
| `database/schema_dwd_doc.sql` | `billiards_dwd` | 维度表(dim_*,含 SCD2 约束)、事实表(dwd_*、fact_*)、扩展表(*_ex) |
|
||||
|
||||
DWD DDL 的特殊处理:
|
||||
- 自动为含 `scd2_start_time` 列的表设置 SCD2 默认值(`scd2_start_time=now()`、`scd2_end_time='9999-12-31'`、`scd2_is_current=1`、`scd2_version=1`)
|
||||
- 自动创建 SCD2 排他约束(`EXCLUDE USING gist`,防止同一业务主键的生效区间重叠)
|
||||
- 自动创建当前版本唯一索引(`WHERE scd2_is_current = 1`)
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `schema.dwd_file` | `database/schema_dwd_doc.sql` | DWD DDL 文件路径 |
|
||||
| `dwd.drop_schema_first` | `False` | 是否先 DROP 再重建(危险操作,会丢失所有 DWD 数据) |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
# 常规初始化
|
||||
python -m cli.main --tasks INIT_DWD_SCHEMA --pg-dsn "$PG_DSN"
|
||||
|
||||
# 重建(先删后建,慎用)
|
||||
python -m cli.main --tasks INIT_DWD_SCHEMA --pg-dsn "$PG_DSN" --extra dwd.drop_schema_first=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. INIT_DWS_SCHEMA — DWS Schema 初始化
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `INIT_DWS_SCHEMA` |
|
||||
| Python 类 | `tasks.utility.init_dws_schema_task.InitDwsSchemaTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 创建 DWS 数据服务层的数据库结构 |
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract()
|
||||
├── 读取 DDL 文件路径(schema_dws.sql)
|
||||
└── 读取 drop_first 配置
|
||||
|
||||
load()
|
||||
├── [可选] DROP SCHEMA billiards_dws CASCADE
|
||||
└── 执行 DWS DDL(schema_dws.sql)
|
||||
```
|
||||
|
||||
### 执行的 DDL 文件
|
||||
|
||||
| DDL 文件 | 创建的 Schema | 主要内容 |
|
||||
|----------|--------------|----------|
|
||||
| `database/schema_dws.sql` | `billiards_dws` | 配置表(5 张 cfg_*)、助教域(5 张)、会员域(2 张)、财务域(7 张)、订单汇总(1 张) |
|
||||
|
||||
DWS Schema 包含的配置表:
|
||||
|
||||
| 配置表 | 说明 |
|
||||
|--------|------|
|
||||
| `cfg_performance_tier` | 绩效档位配置(阈值、抽成比例、假期天数) |
|
||||
| `cfg_assistant_level_price` | 助教等级定价 |
|
||||
| `cfg_bonus_rules` | 奖金规则配置 |
|
||||
| `cfg_area_category` | 台区分类映射 |
|
||||
| `cfg_skill_type` | 技能课程类型映射 |
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `schema.dws_file` | `database/schema_dws.sql` | DWS DDL 文件路径 |
|
||||
| `dws.drop_schema_first` | `False` | 是否先 DROP 再重建(危险操作) |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
python -m cli.main --tasks INIT_DWS_SCHEMA --pg-dsn "$PG_DSN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. SEED_DWS_CONFIG — DWS 配置种子数据初始化
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `SEED_DWS_CONFIG` |
|
||||
| Python 类 | `tasks.utility.seed_dws_config_task.SeedDwsConfigTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 向 DWS 配置表插入初始数据(绩效档位、等级定价、奖金规则等) |
|
||||
|
||||
### 前置条件
|
||||
|
||||
- `billiards_dws` schema 已创建(需先执行 `INIT_DWS_SCHEMA`)
|
||||
- 配置表(`cfg_*`)已存在
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract()
|
||||
└── 读取 seed_dws_config.sql 文件内容
|
||||
|
||||
load()
|
||||
└── 执行 SQL(TRUNCATE + INSERT 配置数据)
|
||||
```
|
||||
|
||||
### 执行的种子文件
|
||||
|
||||
| 文件 | 目标表 | 说明 |
|
||||
|------|--------|------|
|
||||
| `database/seed_dws_config.sql` | `cfg_performance_tier` | 绩效档位(含历史口径:旧方案至 2026-02-28,新方案 2026-03-01 起) |
|
||||
| | `cfg_assistant_level_price` | 助教等级定价 |
|
||||
| | `cfg_bonus_rules` | 奖金规则 |
|
||||
| | `cfg_area_category` | 台区分类映射 |
|
||||
| | `cfg_skill_type` | 技能课程类型映射 |
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `schema.seed_dws_file` | `database/seed_dws_config.sql` | 种子数据 SQL 文件路径 |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
# 通常与 INIT_DWS_SCHEMA 一起执行
|
||||
python -m cli.main --tasks INIT_DWS_SCHEMA,SEED_DWS_CONFIG --pg-dsn "$PG_DSN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. MANUAL_INGEST — 手动 JSON 入库
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `MANUAL_INGEST` |
|
||||
| Python 类 | `tasks.utility.manual_ingest_task.ManualIngestTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 从本地 JSON 文件批量灌入 ODS 表,用于离线回放或示例数据导入 |
|
||||
|
||||
### 执行流程概览
|
||||
|
||||
```
|
||||
execute()
|
||||
├── 确定数据目录(manual.data_dir / pipeline.ingest_source_dir / tests/testdata_json)
|
||||
├── 遍历目录下所有 .json 文件(按文件名排序)
|
||||
│ ├── [可选] 按 include_files 过滤
|
||||
│ ├── 读取并解析 JSON
|
||||
│ ├── 提取记录列表(兼容多层 data/list 包装)
|
||||
│ ├── 按文件名关键字匹配目标 ODS 表
|
||||
│ └── 批量入库(INSERT ON CONFLICT)
|
||||
└── 返回统计计数(fetched/inserted/updated/skipped/errors)
|
||||
```
|
||||
|
||||
### 文件匹配规则
|
||||
|
||||
`MANUAL_INGEST` 通过 `FILE_MAPPING` 将文件名关键字映射到目标 ODS 表。匹配逻辑:**文件名中包含关键字即匹配**(大小写敏感)。
|
||||
|
||||
| 文件名关键字 | 目标 ODS 表 |
|
||||
|-------------|------------|
|
||||
| `member_profiles` | `billiards_ods.member_profiles` |
|
||||
| `member_balance_changes` | `billiards_ods.member_balance_changes` |
|
||||
| `member_stored_value_cards` | `billiards_ods.member_stored_value_cards` |
|
||||
| `recharge_settlements` | `billiards_ods.recharge_settlements` |
|
||||
| `settlement_records` | `billiards_ods.settlement_records` |
|
||||
| `assistant_cancellation_records` | `billiards_ods.assistant_cancellation_records` |
|
||||
| `assistant_accounts_master` | `billiards_ods.assistant_accounts_master` |
|
||||
| `assistant_service_records` | `billiards_ods.assistant_service_records` |
|
||||
| `site_tables_master` | `billiards_ods.site_tables_master` |
|
||||
| `table_fee_discount_records` | `billiards_ods.table_fee_discount_records` |
|
||||
| `table_fee_transactions` | `billiards_ods.table_fee_transactions` |
|
||||
| `goods_stock_movements` | `billiards_ods.goods_stock_movements` |
|
||||
| `stock_goods_category_tree` | `billiards_ods.stock_goods_category_tree` |
|
||||
| `goods_stock_summary` | `billiards_ods.goods_stock_summary` |
|
||||
| `payment_transactions` | `billiards_ods.payment_transactions` |
|
||||
| `refund_transactions` | `billiards_ods.refund_transactions` |
|
||||
| `platform_coupon_redemption_records` | `billiards_ods.platform_coupon_redemption_records` |
|
||||
| `group_buy_redemption_records` | `billiards_ods.group_buy_redemption_records` |
|
||||
| `group_buy_packages` | `billiards_ods.group_buy_packages` |
|
||||
| `settlement_ticket_details` | `billiards_ods.settlement_ticket_details` |
|
||||
| `store_goods_master` | `billiards_ods.store_goods_master` |
|
||||
| `tenant_goods_master` | `billiards_ods.tenant_goods_master` |
|
||||
| `store_goods_sales_records` | `billiards_ods.store_goods_sales_records` |
|
||||
|
||||
### JSON 解析逻辑
|
||||
|
||||
`_extract_records()` 方法兼容多种 JSON 包装格式:
|
||||
|
||||
1. **顶层数组**:`[{...}, {...}]` → 直接作为记录列表
|
||||
2. **data 包装**:`{"data": [...]}` 或 `{"code": 0, "data": [...]}` → 展开 `data` 字段
|
||||
3. **嵌套 list**:`{"data": {"someKey": [{...}]}}` → 自动查找第一个 list 类型的值
|
||||
4. **settleList 特殊处理**:充值/结算记录的 `data.settleList` 结构会被展开,内层 `settleList` 提取为独立记录,并保留外层 `siteProfile` 供字段补充
|
||||
|
||||
### 入库流程
|
||||
|
||||
对每张目标表,入库过程如下:
|
||||
|
||||
1. **查询表结构**:通过 `information_schema.columns` 获取目标表的列名、数据类型
|
||||
2. **构建 SQL**:生成 `INSERT INTO ... VALUES %s ON CONFLICT ...` 语句
|
||||
- 有 `content_hash` 列:`ON CONFLICT (pk, content_hash) DO NOTHING`(内容去重)
|
||||
- 无 `content_hash` 列:`ON CONFLICT (pk) DO UPDATE SET ...`(upsert 覆盖)
|
||||
3. **值映射**:逐列匹配 JSON 字段(忽略大小写),特殊列处理:
|
||||
- `payload`:存储原始 JSON 记录
|
||||
- `source_file`:填入文件名
|
||||
- `fetched_at`:取记录中的值或当前时间
|
||||
- `content_hash`:基于记录内容计算 SHA-256(排除 `fetched_at`、`payload` 等 ETL 元数据字段)
|
||||
- JSON 类型列:自动包装为 `psycopg2.extras.Json`
|
||||
- 整数/浮点/时间戳列:自动类型转换
|
||||
4. **批量执行**:使用 `psycopg2.extras.execute_values` 分批提交(默认 chunk_size=50,最大 500)
|
||||
5. **降级处理**:批量执行失败时,降级为逐行 + `SAVEPOINT` 模式,跳过异常行继续处理
|
||||
6. **事务粒度**:每个文件一次 `commit`,避免长事务
|
||||
|
||||
### 特殊处理
|
||||
|
||||
- **充值/结算记录**(`recharge_settlements`、`settlement_records`):自动从 `siteProfile` 补齐 `tenantid`、`siteid`、`sitename`
|
||||
- **空值规范化**:空字符串 `""`、空 JSON `"{}"` / `"[]"` 统一转为 `None`
|
||||
- **主键校验**:主键值为 `None` 或空字符串的记录直接跳过
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `manual.data_dir` | — | JSON 数据文件目录(优先级最高) |
|
||||
| `pipeline.ingest_source_dir` | — | 入库数据源目录(次优先) |
|
||||
| — | `tests/testdata_json` | 兜底默认目录 |
|
||||
| `manual.include_files` | `[]`(全部) | 限定处理的文件名列表(不含扩展名,小写匹配) |
|
||||
| `manual.execute_values_page_size` | `50` | 批量插入每批行数(1-500) |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
# 从默认目录灌入所有 JSON
|
||||
python -m cli.main --tasks MANUAL_INGEST --pg-dsn "$PG_DSN"
|
||||
|
||||
# 指定数据目录
|
||||
python -m cli.main --tasks MANUAL_INGEST --pg-dsn "$PG_DSN" \
|
||||
--extra manual.data_dir=/path/to/json_files
|
||||
|
||||
# 只灌入指定文件
|
||||
python -m cli.main --tasks MANUAL_INGEST --pg-dsn "$PG_DSN" \
|
||||
--extra manual.include_files=member_profiles,settlement_records
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. ODS_JSON_ARCHIVE — ODS 接口数据归档
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `ODS_JSON_ARCHIVE` |
|
||||
| Python 类 | `tasks.ods.ods_json_archive_task.OdsJsonArchiveTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 在线抓取所有 ODS 相关 API 接口数据,落盘为简化 JSON 文件,供后续离线回放/入库 |
|
||||
|
||||
> 注意:虽然注册为 `task_type="utility"`,但该任务的源文件位于 `tasks/ods/` 目录下,因为它本质上是 ODS 数据的抓取归档。
|
||||
|
||||
### 归档策略
|
||||
|
||||
- **输出格式**:每页一个 JSON 文件,格式为 `{"code": 0, "data": [...records...]}`,与 `MANUAL_INGEST` 的解析逻辑兼容
|
||||
- **文件命名**:`{endpoint_stem}__p{page_no:04d}.json`(如 `GetAllOrderSettleList__p0001.json`)
|
||||
- **小票文件**:按 `orderSettleId` 分文件写入(`GetOrderSettleTicketNew__{orderSettleId}.json`)
|
||||
- **清单文件**:抓取完成后生成 `manifest.json`,记录窗口、端点、记录数等元信息
|
||||
|
||||
### 抓取的 API 端点
|
||||
|
||||
任务内置 22 个端点配置(`ENDPOINTS`),按窗口参数风格分类:
|
||||
|
||||
| 窗口风格 | 参数 | 端点示例 |
|
||||
|----------|------|----------|
|
||||
| `site` | `siteId` | `/MemberProfile/GetTenantMemberList`、`/Table/GetSiteTables` 等 |
|
||||
| `start_end` | `siteId` + `startTime` / `endTime` | `/MemberProfile/GetMemberCardBalanceChange`、`/TenantGoods/GetGoodsSalesList` 等 |
|
||||
| `range` | `siteId` + `rangeStartTime` / `rangeEndTime` | `/Site/GetAllOrderSettleList`、`/Site/GetRechargeSettleList` |
|
||||
| `pay` | `siteId` + `StartPayTime` / `EndPayTime` | `/PayLog/GetPayLogListPage` |
|
||||
|
||||
此外,还有一个特殊端点 `/Order/GetOrderSettleTicketNew`(小票详情),按支付日志中提取的 `orderSettleId` 逐单抓取。
|
||||
|
||||
### 执行流程
|
||||
|
||||
```
|
||||
extract()
|
||||
├── 验证 API 客户端类型(必须为 APIClient,即在线模式)
|
||||
├── 确定输出目录(api.output_dir / pipeline.fetch_root)
|
||||
├── 遍历 ENDPOINTS,逐端点分页抓取
|
||||
│ ├── 构建请求参数(按 window_style 选择参数格式)
|
||||
│ ├── 调用 iter_paginated() 分页获取
|
||||
│ ├── 每页落盘为独立 JSON 文件
|
||||
│ └── 从支付日志中收集 orderSettleId(用于小票抓取)
|
||||
├── 按 orderSettleId 逐单抓取小票详情
|
||||
└── 生成 manifest.json 清单文件
|
||||
```
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `pipeline.fetch_root` | — | JSON 文件输出目录 |
|
||||
| `api.page_size` | `200` | API 分页大小 |
|
||||
| `io.write_pretty_json` | `False` | 是否格式化输出 JSON |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
# 在线抓取并归档
|
||||
python -m cli.main --tasks ODS_JSON_ARCHIVE --pg-dsn "$PG_DSN" \
|
||||
--store-id "$STORE_ID" --api-token "$API_TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. CHECK_CUTOFF — 数据截止时间检查
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `CHECK_CUTOFF` |
|
||||
| Python 类 | `tasks.utility.check_cutoff_task.CheckCutoffTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 用途 | 报告各任务的游标截止时间和各层数据表的最新时间戳,用于运维监控 |
|
||||
|
||||
### 执行流程
|
||||
|
||||
该任务不走标准的 extract → transform → load 流程,而是直接在 `execute()` 中完成所有逻辑:
|
||||
|
||||
```
|
||||
execute()
|
||||
├── 1. 查询 etl_admin 游标截止时间
|
||||
│ ├── 关联 etl_task + etl_cursor 表
|
||||
│ ├── 筛选当前门店已启用的任务
|
||||
│ ├── [可选] 按 task_codes 过滤
|
||||
│ └── 计算总体截止时间(排除 INIT_* 任务的最小 last_end)
|
||||
├── 2. 探测 ODS 表抓取时间
|
||||
│ ├── 遍历 DwdLoadTask.TABLE_MAP 中的 ODS 表
|
||||
│ ├── 查询每张表的 MAX(fetched_at) 和 COUNT(*)
|
||||
│ └── 计算 ODS 截止时间(最小 max_fetched_at)
|
||||
└── 3. 探测 DWD/DWS 关键时间列
|
||||
├── DWD: max(pay_time) from dwd_settlement_head / dwd_payment / dwd_refund
|
||||
└── DWS: max(order_date) / max(updated_at) from dws_order_summary
|
||||
```
|
||||
|
||||
### 校验逻辑
|
||||
|
||||
- **游标截止时间**:从 `etl_admin.etl_cursor.last_end` 获取每个任务的最后成功窗口结束时间,排除 `INIT_*` 任务后取最小值作为总体截止时间
|
||||
- **ODS 抓取时间**:查询每张 ODS 表的 `MAX(fetched_at)`,取最小值作为 ODS 层截止时间
|
||||
- **DWD/DWS 业务时间**:探测关键业务时间列(`pay_time`、`order_date`、`updated_at`),反映数据实际覆盖范围
|
||||
|
||||
### 输出
|
||||
|
||||
任务通过日志输出检查结果,同时在返回值的 `report` 字段中包含结构化数据:
|
||||
|
||||
```python
|
||||
{
|
||||
"rows": [...], # 每个任务的游标信息
|
||||
"overall_cutoff": datetime, # 总体截止时间
|
||||
"ods_fetched_at": {...}, # 每张 ODS 表的 max_fetched_at
|
||||
"dw_max_times": {...}, # DWD/DWS 关键时间列
|
||||
}
|
||||
```
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `app.store_id` | — | 门店 ID(必填) |
|
||||
| `run.cutoff_task_codes` | `None`(全部) | 逗号分隔的任务代码列表,限定检查范围 |
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
# 检查所有任务的截止时间
|
||||
python -m cli.main --tasks CHECK_CUTOFF --pg-dsn "$PG_DSN" --store-id "$STORE_ID"
|
||||
|
||||
# 只检查指定任务
|
||||
python -m cli.main --tasks CHECK_CUTOFF --pg-dsn "$PG_DSN" --store-id "$STORE_ID" \
|
||||
--extra run.cutoff_task_codes=ORDERS,PAYMENTS,MEMBERS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. DATA_INTEGRITY_CHECK — 数据完整性校验
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 任务代码 | `DATA_INTEGRITY_CHECK` |
|
||||
| Python 类 | `tasks.utility.data_integrity_task.DataIntegrityTask` |
|
||||
| 继承 | `BaseTask` |
|
||||
| 注册 task_type | `verification`(非 utility,但在本文档中一并说明) |
|
||||
| 用途 | 检查 API → ODS → DWD 全链路数据完整性,支持自动回填缺失数据 |
|
||||
|
||||
### 两种运行模式
|
||||
|
||||
#### 1. 历史模式(`history`,默认)
|
||||
|
||||
从指定起始日期到结束日期,按月分段检查全量历史数据的完整性。
|
||||
|
||||
```
|
||||
execute() [mode=history]
|
||||
├── 解析 history_start / history_end 时间范围
|
||||
├── 调用 run_history_flow()
|
||||
│ ├── 按月分段执行完整性检查
|
||||
│ ├── 对比 API 记录数 vs ODS 记录数
|
||||
│ ├── [可选] 对比内容一致性(content_hash)
|
||||
│ └── [可选] 自动回填缺失数据
|
||||
└── 生成 JSON 报表
|
||||
```
|
||||
|
||||
#### 2. 窗口模式(`window`)
|
||||
|
||||
检查指定时间窗口内的数据完整性,当提供 CLI 窗口覆盖参数时自动切换到此模式。
|
||||
|
||||
```
|
||||
execute() [mode=window]
|
||||
├── 获取时间窗口(支持 CLI 覆盖)
|
||||
├── 构建窗口分段(build_window_segments)
|
||||
├── 调用 run_window_flow()
|
||||
│ ├── 逐段执行完整性检查
|
||||
│ ├── 汇总缺失/不一致/错误计数
|
||||
│ └── [可选] 自动回填 + 复查
|
||||
└── 生成 JSON 报表
|
||||
```
|
||||
|
||||
### 校验逻辑
|
||||
|
||||
核心校验由 `quality/integrity_service.py` 和 `quality/integrity_checker.py` 实现:
|
||||
|
||||
1. **记录数对比**:API 返回的记录数 vs ODS 表中的记录数
|
||||
2. **内容一致性**(可选):抽样对比 API 记录与 ODS 记录的 `content_hash`
|
||||
3. **缺失检测**:识别 API 中存在但 ODS 中缺失的记录
|
||||
4. **不一致检测**:识别 API 与 ODS 中内容不匹配的记录
|
||||
|
||||
### 自动回填
|
||||
|
||||
当 `auto_backfill=True` 时,检测到缺失或不一致数据后会自动触发回填:
|
||||
|
||||
1. 调用 `scripts/repair/backfill_missing_data.run_backfill()` 重新抓取缺失数据
|
||||
2. 回填完成后可选复查(`recheck_after_backfill`),验证回填效果
|
||||
|
||||
### 配置参数
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `integrity.mode` | `history` | 运行模式:`history`(历史全量)/ `window`(时间窗口) |
|
||||
| `integrity.history_start` | `2025-07-01` | 历史模式起始日期 |
|
||||
| `integrity.history_end` | —(当前时间) | 历史模式结束日期 |
|
||||
| `integrity.include_dimensions` | `False` | 是否包含维度表检查 |
|
||||
| `integrity.ods_task_codes` | —(全部) | 限定检查的 ODS 任务代码 |
|
||||
| `integrity.auto_backfill` | `False` | 是否自动回填缺失数据 |
|
||||
| `integrity.compare_content` | `True` | 是否对比内容一致性 |
|
||||
| `integrity.content_sample_limit` | — | 内容对比抽样上限 |
|
||||
| `integrity.backfill_mismatch` | `True` | 是否回填不一致数据(仅 auto_backfill 时生效) |
|
||||
| `integrity.recheck_after_backfill` | `True` | 回填后是否复查 |
|
||||
| `integrity.force_monthly_split` | `True` | 是否强制按月分段 |
|
||||
| `run.window_override.start` | — | CLI 窗口覆盖起始时间(触发 window 模式) |
|
||||
| `run.window_override.end` | — | CLI 窗口覆盖结束时间 |
|
||||
|
||||
### 输出报表
|
||||
|
||||
检查结果以 JSON 格式写入 `reports/` 目录:
|
||||
|
||||
- 历史模式:`reports/data_integrity_history_{timestamp}.json`
|
||||
- 窗口模式:`reports/data_integrity_window_{timestamp}.json`
|
||||
|
||||
### CLI 示例
|
||||
|
||||
```bash
|
||||
# 历史全量检查(默认从 2025-07-01 至今)
|
||||
python -m cli.main --tasks DATA_INTEGRITY_CHECK --pg-dsn "$PG_DSN" \
|
||||
--store-id "$STORE_ID" --api-token "$API_TOKEN"
|
||||
|
||||
# 指定时间范围
|
||||
python -m cli.main --tasks DATA_INTEGRITY_CHECK --pg-dsn "$PG_DSN" \
|
||||
--store-id "$STORE_ID" --api-token "$API_TOKEN" \
|
||||
--extra integrity.history_start=2026-01-01 --extra integrity.history_end=2026-02-01
|
||||
|
||||
# 窗口模式 + 自动回填
|
||||
python -m cli.main --tasks DATA_INTEGRITY_CHECK --pg-dsn "$PG_DSN" \
|
||||
--store-id "$STORE_ID" --api-token "$API_TOKEN" \
|
||||
--window-start "2026-02-01" --window-end "2026-02-15" \
|
||||
--extra integrity.auto_backfill=true
|
||||
```
|
||||
Reference in New Issue
Block a user