# 飞球 ETL 系统（ODS → DWD）

面向门店业务的 ETL：从上游 API 或离线 JSON 采集订单、支付、会员、库存等数据，先落地 **ODS**，再清洗装载 **DWD**（含 SCD2 维度、事实增量），并输出质量校验报表。

## 快速运行（离线示例 JSON）
> 以下命令默认在 `etl_billiards/` 目录执行（项目会从 `etl_billiards/.env` 读取配置；也可直接设置环境变量）。

1) 环境：Python 3.10+、PostgreSQL。
2) 配置：编辑 `etl_billiards/.env`（或设环境变量），至少包含：
   ```env
   STORE_ID=123
   PG_DSN=postgresql://<user>:<password>@<host>:<port>/<db>
   # 示例：使用仓库内置的最小样例（仅 1 个 JSON）
   INGEST_SOURCE_DIR=../tmp/single_ingest
   ```
3) 安装依赖：
   ```bash
   cd etl_billiards
   pip install -r requirements.txt
   ```
4) 回放入库（ODS）→ 装载 DWD → 质检（可用 `--ingest-source` 覆盖 `INGEST_SOURCE_DIR`）：
   ```bash
   python -m cli.main --pipeline-flow INGEST_ONLY --tasks INIT_ODS_SCHEMA,INIT_DWD_SCHEMA
   python -m cli.main --pipeline-flow INGEST_ONLY --tasks MANUAL_INGEST --ingest-source "../tmp/single_ingest"
   python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWD_LOAD_FROM_ODS
   python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWD_QUALITY_CHECK
   # 报表：reports/dwd_quality_report.json
   ```

> 可按需单独运行：  
> - 仅建表：`python -m cli.main --tasks INIT_ODS_SCHEMA`  
> - 仅 ODS 灌入：`python -m cli.main --tasks MANUAL_INGEST`  
> - 仅 DWD 装载：`python -m cli.main --tasks INIT_DWD_SCHEMA,DWD_LOAD_FROM_ODS`

> Windows：可用 `etl_billiards/run_ods.bat` 一键执行 ODS 建表 + 灌入示例 JSON（`INIT_ODS_SCHEMA` + `MANUAL_INGEST`）。

## 正式环境（在线抓取 → 更新 ODS → 更新 DWD）

**核心入口 CLI（推荐在 `etl_billiards/` 目录执行）**
- `python -m cli.main`

### 必备配置（建议通过环境变量或 `.env`）
- 数据库：`PG_DSN`、`STORE_ID`
- 在线抓取：`API_TOKEN`（可选 `API_BASE`、`API_TIMEOUT`、`API_PAGE_SIZE`、`API_RETRY_MAX`）
- 输出目录（可选）：`EXPORT_ROOT`、`LOG_ROOT`、`FETCH_ROOT`/`JSON_FETCH_ROOT`

**安全提示**：建议将数据库/Token 等凭证保存在 `.env` 或受控秘钥管理中，生产环境使用最小权限账号。

### 推荐定时方式 A（两段定时，更清晰）
1) **更新 ODS（在线抓取 + 入库，FULL）**
   ```bash
   python -m cli.main \
     --pipeline-flow FULL \
     --tasks PRODUCTS,TABLES,MEMBERS,ASSISTANTS,PACKAGES_DEF,ORDERS,PAYMENTS,REFUNDS,COUPON_USAGE,INVENTORY_CHANGE,TOPUPS,TABLE_DISCOUNT,ASSISTANT_ABOLISH,LEDGER \
     --pg-dsn "$PG_DSN" --store-id "$STORE_ID" \
     --api-token "$API_TOKEN"
   ```
2) **ODS → DWD（将新增/变更同步到 DWD）**
   ```bash
   python -m cli.main \
     --pipeline-flow INGEST_ONLY \
     --tasks DWD_LOAD_FROM_ODS \
     --pg-dsn "$PG_DSN" --store-id "$STORE_ID"
   ```

### 推荐定时方式 B（一条命令串起来）
同一条命令先跑在线抓取/入库任务，再跑 DWD 装载任务：
```bash
python -m cli.main \
  --pipeline-flow FULL \
  --tasks PRODUCTS,TABLES,MEMBERS,ASSISTANTS,PACKAGES_DEF,ORDERS,PAYMENTS,REFUNDS,COUPON_USAGE,INVENTORY_CHANGE,TOPUPS,TABLE_DISCOUNT,ASSISTANT_ABOLISH,LEDGER,DWD_LOAD_FROM_ODS \
  --pg-dsn "$PG_DSN" --store-id "$STORE_ID" \
  --api-token "$API_TOKEN"
```

### `pipeline-flow` 说明
- `FULL`：在线抓取落盘 + 本地清洗入库（ODS 任务会走抓取；`DWD_LOAD_FROM_ODS` 仅走入库阶段）
- `FETCH_ONLY`：仅在线抓取落盘，不入库
- `INGEST_ONLY`：仅从本地 JSON 回放入库（适合离线回放/补跑）

## 目录结构与关键文件
- 仓库根目录：`etl_billiards/` 主代码；`app/` 示例 runner；`开发笔记/` 项目笔记；`tmp/` 草稿/调试归档；`requirements.txt`（仓库根）依赖；`run_etl.sh/.bat` 启动脚本。  
  - 注意：根目录的 `run_etl.sh/.bat` 运行时要求当前目录为 `etl_billiards/`（因为入口是 `python -m cli.main`）。
- `etl_billiards/`（主代码目录）
  - `.env`：本地配置文件（可选，用环境变量也可）
  - `cli/`：CLI 入口（`cli/main.py`）
  - `config/`：`defaults.py` 默认值；`env_parser.py` 解析 `.env`/环境变量；`settings.py` AppConfig 加载/校验
  - `api/`：`client.py` HTTP 请求、重试、分页与落盘
  - `database/`：`connection.py` 连接封装；`operations.py` 批量 upsert；DDL：`schema_ODS_doc.sql`、`schema_dwd_doc.sql`
  - `tasks/`：业务任务（ODS 抓取/回放、DWD 装载、质检等）
  - `loaders/`：ODS/DWD/SCD2 Loader 实现
  - `scd/`：`scd2_handler.py`（维度 SCD2 历史）
  - `quality/`：质量检查器（行数/金额对照）
  - `orchestration/`：`scheduler.py` 调度；`task_registry.py` 注册；`cursor_manager.py` 水位管理；`run_tracker.py` 运行记录
  - `scripts/`：重建/测试/探活工具
  - `docs/`：映射/样例/质检说明文档
  - `fetch-test/`：接口联调/规则验证的一次性脚本与报告（不影响主流程）
  - `reports/`：质检输出（如 `dwd_quality_report.json`）
  - `tests/`：单元/集成测试

## 项目文件索引（维护/AI 快速定位）
> 说明：用于维护/AI 快速定位文件路径与用途；默认不列出 `.git/`、`__pycache__/`、`.pytest_cache/`、`*.pyc` 等自动生成内容。

### /
- `.gitignore`：Git 忽略规则。
- `.gitkeep`：占位文件（用于保留空目录）。
- `README.md`：项目总览与使用说明（本文档）。
- `requirements.txt`：仓库根依赖清单（不含版本约束，建议优先用 `etl_billiards/requirements.txt`）。
- `run_etl.bat`：Windows 启动脚本（需先 `cd etl_billiards`；入口为 `python -m cli.main`）。
- `run_etl.sh`：Linux/macOS 启动脚本（需先 `cd etl_billiards`；会加载当前目录 `.env`）。

### app/

### etl_billiards/
- `etl_billiards/.env`：本地运行环境变量（含敏感信息，勿提交/勿外传）。
- `etl_billiards/__init__.py`：Python 包标记文件。
- `etl_billiards/ods_row_report.json`：ODS 行数对照报告（source json vs ODS 表）。
- `etl_billiards/requirements.txt`：ETL 运行依赖（带最低版本约束）。
- `etl_billiards/run_ods.bat`：Windows 一键脚本：重建 ODS 并灌入示例 JSON。
- `etl_billiards/setup.py`：打包/安装脚本（当前项目主要按“`cd etl_billiards; python -m cli.main`”方式运行）。

### etl_billiards/api/
- `etl_billiards/api/__init__.py`：Python 包标记文件。
- `etl_billiards/api/client.py`：API客户端：统一封装 POST/重试/分页与列表提取逻辑。
- `etl_billiards/api/endpoint_routing.py`：“近期记录 / 历史记录(Former)”接口路由规则。
- `etl_billiards/api/local_json_client.py`：本地 JSON 客户端，模拟 APIClient 的分页接口，从落盘的 JSON 回放数据。
- `etl_billiards/api/recording_client.py`：包装 APIClient，将分页响应落盘便于后续本地清洗。

### etl_billiards/cli/
- `etl_billiards/cli/__init__.py`：Python 包标记文件。
- `etl_billiards/cli/main.py`：CLI主入口

### etl_billiards/config/
- `etl_billiards/config/__init__.py`：Python 包标记文件。
- `etl_billiards/config/defaults.py`：配置默认值定义
- `etl_billiards/config/env_parser.py`：环境变量解析
- `etl_billiards/config/settings.py`：配置管理主类

### etl_billiards/database/
- `etl_billiards/database/__init__.py`：Python 包标记文件。
- `etl_billiards/database/base.py`：数据库操作（批量、RETURNING支持）
- `etl_billiards/database/connection.py`：Database connection manager with capped connect_timeout.
- `etl_billiards/database/operations.py`：数据库批量操作
- `etl_billiards/database/schema_dwd_doc.sql`：DWD Schema DDL（含字段/注释/口径说明）。
- `etl_billiards/database/schema_etl_admin.sql`：etl_admin 元数据 Schema DDL（任务/水位/运行记录等）。
- `etl_billiards/database/schema_ODS_doc.sql`：ODS Schema DDL（含字段/注释/口径说明）。
- `etl_billiards/database/seed_ods_tasks.sql`：SQL 种子脚本：初始化/注册 ODS 任务。
- `etl_billiards/database/seed_scheduler_tasks.sql`：SQL 种子脚本：初始化调度任务配置。

### etl_billiards/database/Deleded & backup/
- （本目录无直接文件）

### etl_billiards/docs/
- `etl_billiards/docs/dwd_main_tables_dictionary.md`：DWD 主表（非 Ex）表格说明书
- `etl_billiards/docs/在线抓取，更新ODS ，然后将更新的ODS内容，对应到DWD的更新。.md`：在线抓取，更新ODS ，然后将更新的ODS内容，对应到DWD的更新。

### etl_billiards/fetch-test/
- `etl_billiards/fetch-test/compare_recent_former_endpoints.py`：对比“近期记录”与“历史记录(Former)”接口：
- `etl_billiards/fetch-test/README.md`：fetch-test
- `etl_billiards/fetch-test/recent_vs_former_report.json`：报告/比对输出（JSON）。
- `etl_billiards/fetch-test/recent_vs_former_report.md`：近期记录 vs 历史记录(Former) 接口对比报告

### etl_billiards/loaders/
- `etl_billiards/loaders/__init__.py`：Python 包标记文件。
- `etl_billiards/loaders/base_loader.py`：数据加载器基类

### etl_billiards/loaders/dimensions/
- `etl_billiards/loaders/dimensions/__init__.py`：Python 包标记文件。
- `etl_billiards/loaders/dimensions/assistant.py`：助教维度加载器
- `etl_billiards/loaders/dimensions/member.py`：会员维度表加载器
- `etl_billiards/loaders/dimensions/package.py`：团购/套餐定义加载器
- `etl_billiards/loaders/dimensions/product.py`：商品维度 + 价格SCD2 加载器
- `etl_billiards/loaders/dimensions/table.py`：台桌维度加载器

### etl_billiards/loaders/facts/
- `etl_billiards/loaders/facts/__init__.py`：Python 包标记文件。
- `etl_billiards/loaders/facts/assistant_abolish.py`：助教作废事实表
- `etl_billiards/loaders/facts/assistant_ledger.py`：助教流水事实表
- `etl_billiards/loaders/facts/coupon_usage.py`：券核销事实表
- `etl_billiards/loaders/facts/inventory_change.py`：库存变动事实表
- `etl_billiards/loaders/facts/order.py`：订单事实表加载器
- `etl_billiards/loaders/facts/payment.py`：支付事实表加载器
- `etl_billiards/loaders/facts/refund.py`：退款事实表加载器
- `etl_billiards/loaders/facts/table_discount.py`：台费打折事实表
- `etl_billiards/loaders/facts/ticket.py`：小票详情加载器
- `etl_billiards/loaders/facts/topup.py`：充值记录事实表

### etl_billiards/loaders/ods/
- `etl_billiards/loaders/ods/__init__.py`：Python 包标记文件。
- `etl_billiards/loaders/ods/generic.py`：Generic ODS loader that keeps raw payload + primary keys.

### etl_billiards/models/
- `etl_billiards/models/__init__.py`：Python 包标记文件。
- `etl_billiards/models/parsers.py`：数据类型解析器
- `etl_billiards/models/validators.py`：数据验证器

### etl_billiards/orchestration/
- `etl_billiards/orchestration/__init__.py`：Python 包标记文件。
- `etl_billiards/orchestration/cursor_manager.py`：游标管理器
- `etl_billiards/orchestration/run_tracker.py`：运行记录追踪器
- `etl_billiards/orchestration/scheduler.py`：ETL 调度：支持在线抓取、离线清洗入库、全流程三种模式。
- `etl_billiards/orchestration/task_registry.py`：任务注册表

### etl_billiards/quality/
- `etl_billiards/quality/__init__.py`：Python 包标记文件。
- `etl_billiards/quality/balance_checker.py`：余额一致性检查器
- `etl_billiards/quality/base_checker.py`：数据质量检查器基类

### etl_billiards/reports/
- `etl_billiards/reports/dwd_quality_report.json`：DWD 质量核对输出（行数/金额对照）。

### etl_billiards/scd/
- `etl_billiards/scd/__init__.py`：Python 包标记文件。
- `etl_billiards/scd/scd2_handler.py`：SCD2 (Slowly Changing Dimension Type 2) 处理逻辑

### etl_billiards/scripts/
- `etl_billiards/scripts/bootstrap_schema.py`：Apply the PRD-aligned warehouse schema (ODS/DWD/DWS) to PostgreSQL.
- `etl_billiards/scripts/build_dwd_from_ods.py`：Populate PRD DWD tables from ODS payload snapshots.
- `etl_billiards/scripts/build_dws_order_summary.py`：Recompute billiards_dws.dws_order_summary from DWD fact tables.
- `etl_billiards/scripts/check_ods_json_vs_table.py`：ODS JSON 字段核对脚本：对照当前数据库中的 ODS 表字段，检查示例 JSON（默认目录 export/test-json-doc）
- `etl_billiards/scripts/check_ods_gaps.py`：ODS 缺失校验脚本：API 主键 vs ODS 主键逐条比对，输出缺失明细样例。
- `etl_billiards/scripts/reload_ods_windowed.py`：ODS 窗口化补跑脚本：按时间切片重跑 ODS 任务，并可配置窗口粒度与延时。
- `etl_billiards/scripts/rebuild_db_and_run_ods_to_dwd.py`：一键重建 ETL 相关 Schema，并执行 ODS → DWD。
- `etl_billiards/scripts/rebuild_ods_from_json.py`：从本地 JSON 示例目录重建 billiards_ods.* 表，并导入样例数据。
- `etl_billiards/scripts/run_tests.py`：灵活的测试执行脚本，可像搭积木一样组合不同参数或预置命令（模式/数据库/归档路径等），
- `etl_billiards/scripts/test_db_connection.py`：Quick utility for validating PostgreSQL connectivity (ASCII-only output).
- `etl_billiards/scripts/test_presets.py`：测试命令仓库：集中维护 run_tests.py 的常用组合，支持一键执行。

### etl_billiards/scripts/Deleded & backup/
- （本目录无直接文件）

### etl_billiards/tasks/
- `etl_billiards/tasks/__init__.py`：Python 包标记文件。
- `etl_billiards/tasks/assistant_abolish_task.py`：助教作废任务
- `etl_billiards/tasks/assistants_task.py`：助教账号任务
- `etl_billiards/tasks/base_dwd_task.py`：DWD任务基类
- `etl_billiards/tasks/base_task.py`：ETL任务基类（引入 Extract/Transform/Load 模板方法）
- `etl_billiards/tasks/coupon_usage_task.py`：平台券核销任务
- `etl_billiards/tasks/dwd_load_task.py`：DWD 装载任务：从 ODS 增量写入 DWD（维度 SCD2，事实按时间增量）。
- `etl_billiards/tasks/dwd_quality_task.py`：DWD 质量核对任务：按 dwd_quality_check.md 输出行数/金额对照报表。
- `etl_billiards/tasks/init_dwd_schema_task.py`：初始化 DWD Schema：执行 schema_dwd_doc.sql，可选先 DROP SCHEMA。
- `etl_billiards/tasks/init_schema_task.py`：任务：初始化运行环境，执行 ODS 与 etl_admin 的 DDL，并准备日志/导出目录。
- `etl_billiards/tasks/inventory_change_task.py`：库存变更任务
- `etl_billiards/tasks/ledger_task.py`：助教流水任务
- `etl_billiards/tasks/manual_ingest_task.py`：手工示例数据灌入：按 schema_ODS_doc.sql 的表结构写入 ODS。
- `etl_billiards/tasks/members_dwd_task.py`：DWD Task：Process Member Records from ODS to Dimension Table.
- `etl_billiards/tasks/members_task.py`：会员ETL任务
- `etl_billiards/tasks/ods_json_archive_task.py`：在线抓取 ODS 相关接口并落盘为 JSON（用于后续离线回放/入库）。
- `etl_billiards/tasks/ods_tasks.py`：ODS ingestion tasks.
- `etl_billiards/tasks/orders_task.py`：订单ETL任务
- `etl_billiards/tasks/packages_task.py`：团购/套餐定义任务
- `etl_billiards/tasks/payments_dwd_task.py`：DWD Task：Process Payment Records from ODS to Fact Table.
- `etl_billiards/tasks/payments_task.py`：支付记录ETL任务
- `etl_billiards/tasks/products_task.py`：商品档案（PRODUCTS）ETL任务
- `etl_billiards/tasks/refunds_task.py`：退款记录任务
- `etl_billiards/tasks/table_discount_task.py`：台费折扣任务
- `etl_billiards/tasks/tables_task.py`：台桌档案任务
- `etl_billiards/tasks/ticket_dwd_task.py`：DWD Task：Process Ticket Details from ODS to DWD fact tables.
- `etl_billiards/tasks/topups_task.py`：充值记录任务

### etl_billiards/tasks/dwd/
- （本目录无直接文件）

### etl_billiards/tests/
- `etl_billiards/tests/__init__.py`：Python 包标记文件。

### etl_billiards/tests/integration/
- `etl_billiards/tests/integration/__init__.py`：Python 包标记文件。
- `etl_billiards/tests/integration/test_database.py`：数据库集成测试

### etl_billiards/tests/unit/
- `etl_billiards/tests/unit/__init__.py`：Python 包标记文件。
- `etl_billiards/tests/unit/task_test_utils.py`：ETL 任务测试的共用辅助模块，涵盖在线/离线模式所需的伪造数据、客户端与配置等工具函数。
- `etl_billiards/tests/unit/test_config.py`：配置管理测试
- `etl_billiards/tests/unit/test_endpoint_routing.py`：Unit tests for recent/former endpoint routing.
- `etl_billiards/tests/unit/test_etl_tasks_offline.py`：离线模式任务测试，通过回放归档 JSON 来验证 T+L 链路可用。
- `etl_billiards/tests/unit/test_etl_tasks_online.py`：在线模式下的端到端任务测试，验证所有任务在模拟 API 下能顺利执行。
- `etl_billiards/tests/unit/test_etl_tasks_stages.py`：验证 14 个任务的 E/T/L 分阶段调用（FakeDB/FakeAPI，不访问真实接口或数据库）。
- `etl_billiards/tests/unit/test_ods_tasks.py`：Unit tests for the new ODS ingestion tasks.
- `etl_billiards/tests/unit/test_parsers.py`：解析器测试
- `etl_billiards/tests/unit/test_reporting.py`：汇总与报告工具的单测。

### etl_billiards/utils/
- `etl_billiards/utils/__init__.py`：Python 包标记文件。
- `etl_billiards/utils/helpers.py`：通用工具函数
- `etl_billiards/utils/json_store.py`：JSON 归档/读取的通用工具。
- `etl_billiards/utils/reporting.py`：简单的任务结果汇总与格式化工具。

### tmp/
- `tmp/20251121-task.txt`：历史任务/计划记录（可能存在编码问题）。
- `tmp/doc_extracted.txt`：从 DWD 文档抽取的正文（大文本）。
- `tmp/doc_lines.txt`：DWD 文档按行抽取/对照（文本）。
- `tmp/dwd_tables.json`：DWD 表清单（JSON）。
- `tmp/dwd_tables_full.json`：DWD 表清单（完整版 JSON）。
- `tmp/hebing.py`：临时脚本：按“同名 key”合并目录内 md+json 输出 merged_output.txt。
- `tmp/README_FULL.md`：历史/草稿：README 详细版（已合并进根 README）。
- `tmp/rebuild_run_20251214-042115.log`：运行日志/调试输出（临时文件）。
- `tmp/rewrite_schema_dwd_doc_comments.py`：临时脚本：批量重写 DWD DDL 注释（归档/草稿）。
- `tmp/rewrite_schema_ods_doc_comments.py`：临时脚本：批量重写 ODS DDL 注释（归档/草稿）。
- `tmp/schema_dwd.sql`：DWD schema 草稿/导出（归档）。
- `tmp/schema_dwd_doc.sql`：DWD schema doc 版本（归档）。
- `tmp/schema_ODS_doc copy.sql`：ODS schema doc 备份（归档）。
- `tmp/schema_ODS_doc.sql`：ODS schema doc 版本（归档）。
- `tmp/temp_chinese.txt`：编码/文本对照测试。
- `tmp/tmp_debug_sql.py`：临时脚本：调试 SQL/映射（归档）。
- `tmp/tmp_drop_dwd.py`：临时脚本：DROP SCHEMA billiards_dwd（危险，勿在生产执行）。
- `tmp/tmp_dwd_tasks.py`：临时脚本：调试 DWD 相关任务（归档）。
- `tmp/tmp_problems.py`：临时脚本：问题排查记录/复现（归档）。
- `tmp/tmp_run_sql.py`：临时脚本：拼接/执行一条 INSERT...SELECT 验证映射（需 PG_DSN）。
- `tmp/非球接口API.md`：上游接口笔记/汇总（草稿/归档）。

### tmp/a/
- （本目录无直接文件）

### tmp/b/
- （本目录无直接文件）

### tmp/etl_billiards_misc/
- `tmp/etl_billiards_misc/0.py`：Simple PostgreSQL connectivity smoke-checker.
- `tmp/etl_billiards_misc/feiqiu-ETL.code-workspace`：VS Code workspace 文件（归档）。
- `tmp/etl_billiards_misc/草稿.txt`：草稿/说明（归档）。

### tmp/etl_billiards_misc/backups/
- `tmp/etl_billiards_misc/backups/manual_ingest_task.py`：历史版本备份（归档）。
- `tmp/etl_billiards_misc/backups/manual_ingest_task.py.bak_20251209`：历史版本备份（归档）。
- `tmp/etl_billiards_misc/backups/schema_ODS_doc.sql`：历史版本备份（归档）。
- `tmp/etl_billiards_misc/backups/schema_ODS_doc.sql.bak_20251209`：历史版本备份（归档）。

### tmp/etl_billiards_misc/tmp & Delete/
- `tmp/etl_billiards_misc/tmp & Delete/.env.example`：旧示例配置（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/dwd_schema_columns.txt`：DWD 字段提取/对照文本（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/DWD层设计建议.docx`：DWD 设计建议文档（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/DWD层设计草稿.md`：DWD 设计草稿（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/schema_dwd_doc.sql.bak`：schema 备份（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/schema_ODS_doc.sql.bak`：schema 备份（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/schema_ODS_doc.sql.rewrite2.bak`：schema 重写过程备份（归档）。
- `tmp/etl_billiards_misc/tmp & Delete/schema_v2.sql`：schema v2 草稿（归档）。

### tmp/recharge_only/
- `tmp/recharge_only/recharge_settlements.json`：离线样例 JSON（仅充值结算）。

### tmp/single_ingest/
- `tmp/single_ingest/goods_stock_movements.json`：离线最小样例 JSON（单文件）。

### 开发笔记/
- `开发笔记/记录.md`：开发/迁移过程的备忘与待办（归档）。

## 架构与流程
执行链路（控制流）：
1) CLI（`cli/main.py`）解析参数 → 生成 AppConfig → 初始化日志/DB/API；  
2) 调度层（`orchestration/scheduler.py`）按 `task_registry.py` 实例化任务，设置 run_uuid、cursor（水位）、上下文；  
3) 任务执行模板：获取时间窗口/水位（`cursor_manager.py`）→ Extract（API 分页/重试或离线读 JSON）→ Transform（解析/校验）→ Load（Loader 批量 upsert/SCD2/增量写入，底层 `database/operations.py`）→（可选）质量检查 → 更新水位与运行记录（`run_tracker.py`），提交/回滚事务。

数据流与依赖：
- 配置：`config/defaults.py` + `.env`/环境变量 + CLI 参数叠加
- 在线：`api/client.py` 支撑分页/重试；可落盘 JSON（`pipeline.fetch_root`）
- 离线：`manual_ingest_task.py` 从 `INGEST_SOURCE_DIR` 回放入库
- DWD：`dwd_load_task.py` 依据 `TABLE_MAP/FACT_MAPPINGS` 映射装载，维度走 SCD2，事实走增量
- 质检：`dwd_quality_task.py` 输出 `reports/dwd_quality_report.json`

## ODS → DWD 策略与建模要点
1) ODS 留底：保留源主键、payload、时间/来源信息，便于回溯。  
2) DWD 清洗：维度走 SCD2，事实按时间/水位增量；字段类型、单位、枚举标准化，同时保留溯源字段。  
3) 颗粒一致：一张 DWD 表只承载一种业务事件/颗粒，避免混颗粒。  
4) 业务键统一：site_id、member_id、table_id、order_settle_id、order_trade_no 等统一命名。  
5) 不过度汇总：DWD 只做明细/轻度清洗，聚合留到 DWS/报表。  
6) 去嵌套：数组展开为子表/子行，重复 profile 提炼为维度。  
7) 长期演进：优先加列/加表，减少对已有表结构的破坏。

## 常用 CLI
```bash
cd etl_billiards

# 运行 defaults.py 中的默认任务列表（在线 FULL 流程）
python -m cli.main --pg-dsn "$PG_DSN" --store-id "$STORE_ID" --api-token "$API_TOKEN"

# 运行指定任务
python -m cli.main --tasks INIT_ODS_SCHEMA,MANUAL_INGEST --pipeline-flow INGEST_ONLY --ingest-source "../tmp/single_ingest"

# 覆盖 DSN / API / 输出目录
python -m cli.main --pg-dsn "postgresql://user:pwd@host:5432/db" --store-id 123 --api-token "..." --fetch-root "./json_fetch"

# 试运行（不写库）
python -m cli.main --dry-run --tasks DWD_LOAD_FROM_ODS
```

## 窗口切分与补偿
用于 ETL 任务、ODS 缺失校验、数据一致性检查等“带时间窗口”的执行场景。逻辑如下：
- 仅当传入窗口参数（如 CLI `--window-start/--window-end` 或脚本 `--start/--end`）时启用切分。
- 先对整体窗口前后补偿 N 小时，再按月切分（`month` 为最大单位）。不需要切分时设为 `none`。
- 分段窗口将依次执行并汇总结果。

配置项（默认值见 `config/defaults.py`）：
- `run.window_split.unit`：`month` / `none`（默认 `month`）
- `run.window_split.compensation_hours`：整数小时（默认 2）

环境变量：
- `WINDOW_SPLIT_UNIT`
- `WINDOW_COMPENSATION_HOURS`

CLI 参数（覆盖配置）：
- `python -m cli.main`：`--window-split-unit`，`--window-compensation-hours`
- `scripts/check_ods_gaps.py`：`--window-split-unit`，`--window-compensation-hours`
- `scripts/check_data_integrity.py`：`--window-split-unit`，`--window-compensation-hours`
- `scripts/reload_ods_windowed.py`：`--window-split-unit`，`--window-compensation-hours`

示例（`unit=month`，`compensation_hours=2`）：
- 传入窗口：`2025/11/10 10:00` - `2026/1/19 10:15`
- 实际处理窗口切分：
  - `2025/11/10 08:00` - `2025/12/01 00:00`
  - `2025/12/01 00:00` - `2026/01/01 00:00`
  - `2026/01/01 00:00` - `2026/01/19 12:15`

## 测试
说明：仓库未固定 pytest 版本（运行测试需自行安装 `pytest`）。

```bash
cd etl_billiards
pip install pytest

# 单元测试（模拟 API + FakeDB）
pytest tests/unit

# 集成测试（需要设置 TEST_DB_DSN）
TEST_DB_DSN="postgresql://user:pwd@host:5432/db" pytest tests/integration/test_database.py

# 便捷测试执行器（可选）
python scripts/run_tests.py --suite online -k ORDERS
python scripts/test_db_connection.py --dsn "postgresql://user:pwd@host:5432/db" --query "SELECT 1"
```

## 开发与扩展
- 新任务：在 `tasks/` 继承 BaseTask，实现 `get_task_code/execute`，并在 `orchestration/task_registry.py` 注册。  
- 新 Loader/Checker：参考 `loaders/`、`quality/`，复用批量 upsert/质检接口。  
- 新配置项：在 `config/defaults.py` 增加默认值，并在 `config/env_parser.py` 增加环境变量映射（如需要）。

## ODS 任务上线指引
- 元数据/任务注册脚本：
  - `etl_billiards/database/seed_ods_tasks.sql`
  - `etl_billiards/database/seed_scheduler_tasks.sql`
- 确认 `etl_admin.etl_task` 中已启用所需任务（不同环境需替换 store_id / schema）。  
- 离线回放/重建 ODS（开发/运维）：  
  ```bash
  cd etl_billiards
  python scripts/rebuild_ods_from_json.py --dsn "$PG_DSN" --json-dir "export/test-json-doc"
  ```

## ODS 表概览（数据路径）
| ODS 表名                           | 接口 Path                                         | 数据列表路径                  |
| ---------------------------------- | ------------------------------------------------- | ----------------------------- |
| assistant_accounts_master          | /PersonnelManagement/SearchAssistantInfo          | data.assistantInfos           |
| assistant_service_records          | /AssistantPerformance/GetOrderAssistantDetails    | data.orderAssistantDetails    |
| assistant_cancellation_records     | /AssistantPerformance/GetAbolitionAssistant       | data.abolitionAssistants      |
| goods_stock_movements              | /GoodsStockManage/QueryGoodsOutboundReceipt       | data.queryDeliveryRecordsList |
| goods_stock_summary                | /TenantGoods/GetGoodsStockReport                  | data                          |
| group_buy_packages                 | /PackageCoupon/QueryPackageCouponList             | data.packageCouponList        |
| group_buy_redemption_records       | /Site/GetSiteTableUseDetails                      | data.siteTableUseDetailsList  |
| member_profiles                    | /MemberProfile/GetTenantMemberList                | data.tenantMemberInfos        |
| member_balance_changes             | /MemberProfile/GetMemberCardBalanceChange         | data.tenantMemberCardLogs     |
| member_stored_value_cards          | /MemberProfile/GetTenantMemberCardList            | data.tenantMemberCards        |
| payment_transactions               | /PayLog/GetPayLogListPage                         | data                          |
| platform_coupon_redemption_records | /Promotion/GetOfflineCouponConsumePageList        | data                          |
| recharge_settlements               | /Site/GetRechargeSettleList                       | data.settleList               |
| refund_transactions                | /Order/GetRefundPayLogList                        | data                          |
| settlement_records                 | /Site/GetAllOrderSettleList                       | data.settleList               |
| settlement_ticket_details          | /Order/GetOrderSettleTicketNew                    | 完整 JSON                     |
| site_tables_master                 | /Table/GetSiteTables                              | data.siteTables               |
| stock_goods_category_tree          | /TenantGoodsCategory/QueryPrimarySecondaryCategory| data.goodsCategoryList        |
| store_goods_master                 | /TenantGoods/GetGoodsInventoryList                | data.orderGoodsList           |
| store_goods_sales_records          | /TenantGoods/GetGoodsSalesList                    | data.orderGoodsLedgers        |
| table_fee_discount_records         | /Site/GetTaiFeeAdjustList                         | data.taiFeeAdjustInfos        |
| table_fee_transactions             | /Site/GetSiteTableOrderDetails                    | data.siteTableUseDetailsList  |
| tenant_goods_master                | /TenantGoods/QueryTenantGoods                     | data.tenantGoodsList          |

> 完整字段级映射见 `etl_billiards/docs/` 与 ODS/DWD DDL。

## 当前状态（2025-12-09）
- 示例 JSON 已全量灌入，DWD 行数与 ODS 对齐。  
- 分类维度已展平大类+子类：`dim_goods_category` 26 行（category_level/leaf 已赋值）。  
- 部分空字段源数据即为空，如需补值请先确认上游。

## 可精简/归档
- `tmp/`、`tmp/etl_billiards_misc/` 中草稿、旧备份、调试脚本仅供参考，不影响运行。  
- 根级保留必要文件（README、requirements、run_etl.*），其余临时文件按需归档至 `tmp/`。

## 一键更新（推荐）

日常需要把数据从 ODS 更新到最新，并同步刷新 DWD/DWS 时，直接运行一键脚本：

```bash
cd etl_billiards
python run_update.py
```

常用参数：
- `--overlap-seconds 3600`：冗余抓取窗口（默认 3600 秒）
- `--dws-rebuild-days 1`：DWS 回算冗余天数（默认 1 天）
- `--dws-start YYYY-MM-DD --dws-end YYYY-MM-DD`：手工指定 DWS 回算日期范围
- `--skip-ods`：跳过 ODS 在线抓取（仅跑 DWD/DWS）
- `--ods-tasks ODS_PAYMENT,ODS_TABLE_USE,...`：只跑指定 ODS 任务
- `--check-ods-gaps`：在 ODS 更新完成后执行缺失校验（API 主键 vs ODS 主键）
- `--check-ods-overlap-hours 24`：缺失校验时，从 ODS 最新截止时间回溯的小时数（默认 24）
- `--check-ods-window-days 1`：缺失校验 API 窗口粒度（默认 1 天）
- `--check-ods-page-size 200`：缺失校验 API 每页大小（默认 200）
- `--check-ods-timeout-sec 1800`：缺失校验步骤超时秒数（默认 1800）
- `--check-ods-task-codes ODS_PAYMENT,ODS_TABLE_USE,...`：仅校验指定 ODS 任务

### ODS 缺失校验（API vs ODS）

说明：
- 校验口径为 ODS 表 `MAX(fetched_at)` 的最小值，视为“最新一致截止时间”。  
- `--from-cutoff` 会从该截止时间回溯 N 小时（默认 24 小时）到当前，便于日常增量校验。  

全量校验（从 2025-07 至今）：
```bash
cd etl_billiards
python scripts/check_ods_gaps.py --start 2025-07-01
```

更新时校验（从 ODS 最新截止时间回溯 24h）：
```bash
cd etl_billiards
python run_update.py --check-ods-gaps
```

## FAQ
- 字段空值：若映射已存在且源列非空仍为空，再检查上游 JSON；维度 SCD2 按全量合并。  
- DSN/路径：确认 `PG_DSN`、`STORE_ID`、`INGEST_SOURCE_DIR` 与本地一致。  
- 新增任务：在 `tasks/` 实现并注册到 `task_registry.py`，必要时同步更新 DDL 与映射。  
- 权限/运行：检查网络、账号权限；脚本需执行权限（如 `chmod +x run_etl.sh`）。

---

## Cutoff（截止时间）检查

当你需要“上次数据截止到什么时候”“现在应该从哪里开始补跑”时，使用任务 `CHECK_CUTOFF`：

```bash
cd etl_billiards
python -m cli.main --pipeline-flow INGEST_ONLY --tasks CHECK_CUTOFF
```

它会输出：
- `etl_admin.etl_cursor`：每个任务的 `last_start/last_end/last_run_id`（调度游标）
- ODS：对 `DWD_LOAD_FROM_ODS` 依赖的各个 `billiards_ods.*` 表做 `MAX(fetched_at)`（真实已入库 ODS 的截止）
- DWD/DWS：输出若干关键表的最大业务时间/最大更新时刻，便于快速核对

> 如果 `etl_cursor.last_end` 很新，但 ODS 的 `MAX(fetched_at)` 很旧，通常表示在线抓取没跑通（最常见是 `API_TOKEN` 过期导致 401）。

## 冗余抓取方案（推荐）

为避免边界时间丢数（上游延迟写入、接口分页抖动、窗口切换等），建议在 cutoff 基础上向前追加 **1 小时** 冗余量：

- 配置：将 `OVERLAP_SECONDS` 设为 3600（默认 120 秒）

```env
# etl_billiards/.env
OVERLAP_SECONDS=3600
```

冗余方案的关键点是“重抓不重落”，依靠各层的去重/幂等机制只落新数据：
- **ODS 层**：主键/冲突列 UPSERT（重复抓取只会 upsert，不会重复插入）
- **DWD 层**：事实表增量插入 + 主键冲突不重复落（重复范围会被跳过），维度表按 SCD2 合并
- **DWS 层**：对指定日期窗口先 delete 再 upsert（窗口内重算幂等）

> 如果你希望“冗余窗口内的数据发生变更也要覆盖更新”，需要把对应层的冲突策略从 `DO NOTHING` 调整为 `DO UPDATE`（当前实现以“只落新数据”为主）。

## DWS（汇总层）入库

本项目已包含 `billiards_dws` 汇总层（当前提供 `dws_order_summary`）：

1) 初始化 DWS 表结构：

```bash
python -m cli.main --pipeline-flow INGEST_ONLY --tasks INIT_DWS_SCHEMA
```

2) 生成/刷新汇总表（按窗口重算，建议配合 `--window-start/--window-end`）：

```bash
python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_BUILD_ORDER_SUMMARY \
  --window-start "2025-10-01 00:00:00" \
  --window-end "2025-12-26 23:59:59"
```

3) 推荐串联（ODS -> DWD -> DWS）：

```bash
# 先跑在线 ODS 抓取（需要有效 API_TOKEN；如果出现 401 请更新 token）
python -m cli.main --pipeline-flow FULL --tasks ODS_MEMBER,ODS_PAYMENT,ODS_REFUND,ODS_SETTLEMENT_RECORDS

# 再把 ODS 增量同步到 DWD
python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWD_LOAD_FROM_ODS

# 最后重算 DWS
python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_BUILD_ORDER_SUMMARY
```


## 日志 (UTF-8)
- 默认日志目录：`etl_billiards/logs/`
- 每次运行都会生成一个带有时间戳的 `.log` 文件，以便于使用外部工具查看。
常用选项：

- `--log-file` 自定义日志路径（覆盖默认值）。
- `--log-dir` 自定义日志目录。
- `--log-level` 日志级别（`INFO`/`DEBUG`）。
- `--no-log-console` 禁用控制台日志记录（仅写入文件）。

示例（按月切分 + 前后补偿 2h）：
```bash
cd etl_billiards
python scripts/check_ods_gaps.py --start 2025-07-01 --end 2025-09-30 --window-split-unit month --window-compensation-hours 2 --task-codes ODS_PAYMENT --sleep-per-window-seconds 0.5
python scripts/reload_ods_windowed.py --tasks ODS_PAYMENT,ODS_TABLE_USE --start 2025-07-01 --end 2025-09-30 --window-split-unit month --window-compensation-hours 2 --sleep-seconds 1
python run_update.py --check-ods-gaps --check-ods-window-days 1 --check-ods-sleep-per-window-seconds 0.5
```
