Compare commits

...

3 Commits

Author SHA1 Message Date
Neo
fafc95e64c 在前后端开发联调前 的提交20260223 2026-02-23 23:02:20 +08:00
Neo
254ccb1e77 feat: TaskSelector v2 全链路展示 + 同步检查 + MCP Server + 服务器 Git 排除
- admin-web: TaskSelector 重构为按域+层全链路展示,新增同步检查功能
- admin-web: TaskConfig 动态加载 Flow/处理模式定义,DWD 表过滤内嵌域面板
- admin-web: App hydrate 完成前显示 loading,避免误跳 /login
- backend: 新增 /tasks/sync-check 对比后端与 ETL 真实注册表
- backend: 新增 /tasks/flows 返回 Flow 和处理模式定义
- apps/mcp-server: 新增 MCP Server 模块(百炼 AI PostgreSQL 只读查询)
- scripts/server: 新增 setup-server-git.py + server-exclude.txt
- docs: 更新 LAUNCH-CHECKLIST 添加 Git 排除配置步骤
- pyproject.toml: workspace members 新增 mcp-server
2026-02-19 10:31:16 +08:00
Neo
4eac07da47 在准备环境前提交次全部更改。 2026-02-19 08:35:13 +08:00
2089 changed files with 16440440 additions and 35158 deletions

108
.env Normal file
View File

@@ -0,0 +1,108 @@
# ==============================================================================
# NeoZQYY Monorepo 根 .env — 公共配置层
# ==============================================================================
# 后端 config.py 从此文件加载公共参数
# 优先级:根 .env < 应用 .env.local < 环境变量 < CLI 参数
# 敏感值禁止提交;本文件已在 .gitignore 中排除
# ------------------------------------------------------------------------------
# 数据库公共连接参数(后端 + ETL 共用同一 PostgreSQL 实例)
# ------------------------------------------------------------------------------
DB_HOST=100.64.0.4
DB_PORT=5432
DB_USER=local-Python
DB_PASSWORD=Neo-local-1991125
# ------------------------------------------------------------------------------
# 数据库名称
# CHANGE 2026-02-15 | 默认指向测试库,避免开发时误操作生产数据
# CHANGE 2026-02-19 | 移除 PG_NAME未被代码引用仅 .env.template 分离式配置预留)
#
# 数据库清单:
# etl_feiqiu — ETL 流程(飞球连接器),正式环境
# test_etl_feiqiu — ETL 流程(飞球连接器),开发/测试环境
# zqyy_app — 小程序业务库,正式环境
# test_zqyy_app — 小程序业务库,开发/测试环境
# ------------------------------------------------------------------------------
APP_DB_NAME=test_zqyy_app
ETL_DB_NAME=test_etl_feiqiu
# ------------------------------------------------------------------------------
# 组合式 DSN各子系统 / 脚本需要完整连接串时使用)
# 格式postgresql://user:password@host:port/dbname
# CHANGE 2026-02-16 | 新增,供 dataflow_analyzer 等跨模块脚本直接读取
# CHANGE 2026-02-19 | 多库 DSNPG_DSNETL 库,向后兼容)+ APP_DB_DSN业务库
# ------------------------------------------------------------------------------
PG_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/test_etl_feiqiu
APP_DB_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/test_zqyy_app
# CHANGE 2026-02-21 | 显式定义测试库 DSN运维脚本/集成测试优先使用
TEST_DB_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/test_etl_feiqiu
TEST_APP_DB_DSN=postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/test_zqyy_app
# ------------------------------------------------------------------------------
# 通用
# ------------------------------------------------------------------------------
TIMEZONE=Asia/Shanghai
LOG_LEVEL=INFO
# ==============================================================================
# 统一输出路径配置export/ 目录)
# ==============================================================================
# CHANGE 2026-02-19 | 统一规划 export 目录结构,所有输出路径集中管理
#
# 目录总览:
# export/
# ├── ETL-Connectors/feiqiu/
# │ ├── JSON/ — API 原始 JSON 导出ODS 抓取落盘)
# │ ├── LOGS/ — ETL 运行日志(每次 run 一个 .log
# │ └── REPORTS/ — ETL 质检/完整性报告JSON 格式)
# ├── SYSTEM/
# │ ├── LOGS/ — 系统级运维日志
# │ ├── REPORTS/
# │ │ ├── dataflow_analysis/ — 数据流结构分析报告Markdown
# │ │ ├── field_audit/ — 字段排查报告
# │ │ └── full_dataflow_doc/ — 全链路数据流文档
# │ └── CACHE/
# │ └── api_samples/ — API 样本缓存gen_full_dataflow_doc 使用)
# └── BACKEND/
# └── LOGS/ — 后端结构化日志(预留)
# ------------------------------------------------------------------------------
# ETL Connector飞球输出路径
# ------------------------------------------------------------------------------
# JSON 导出根目录ODS 抓取落盘,按 TASK_CODE/run_id 自动建子目录)
EXPORT_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/JSON
# ETL 运行日志根目录
LOG_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/LOGS
# 在线抓取 JSON 输出根目录FETCH_ONLY 模式使用)
FETCH_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/JSON
# ETL 质检/完整性报告输出目录
ETL_REPORT_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/REPORTS
# ------------------------------------------------------------------------------
# 系统级输出路径
# ------------------------------------------------------------------------------
# 数据流结构分析报告输出目录gen_dataflow_report.py / analyze_dataflow.py
SYSTEM_ANALYZE_ROOT=C:/NeoZQYY/export/SYSTEM/REPORTS/dataflow_analysis
# 字段排查报告输出目录field_audit.py
FIELD_AUDIT_ROOT=C:/NeoZQYY/export/SYSTEM/REPORTS/field_audit
# 全链路数据流文档输出目录gen_full_dataflow_doc.py
FULL_DATAFLOW_DOC_ROOT=C:/NeoZQYY/export/SYSTEM/REPORTS/full_dataflow_doc
# API 样本缓存目录gen_full_dataflow_doc.py 的 24h 缓存)
API_SAMPLE_CACHE_ROOT=C:/NeoZQYY/export/SYSTEM/CACHE/api_samples
# 系统级运维日志目录
SYSTEM_LOG_ROOT=C:/NeoZQYY/export/SYSTEM/LOGS
# ------------------------------------------------------------------------------
# 后端输出路径(预留)
# ------------------------------------------------------------------------------
# 后端结构化日志目录
BACKEND_LOG_ROOT=C:/NeoZQYY/export/BACKEND/LOGS
# ------------------------------------------------------------------------------
# 阿里云百炼 AI 配置
# CHANGE 2026-02-23 | 从 PRD 文档迁移至 .env禁止在文档中明文存放
# ------------------------------------------------------------------------------
BAILIAN_API_KEY=sk-6def29cab3474cc797e52b82a46a5dba
BAILIAN_TEST_APP_ID=541edb3d5fcd4c18b13cbad81bb5fb9d

View File

@@ -1,30 +1,298 @@
# ============================================================ # ==============================================================================
# NeoZQYY Monorepo 公共环境配置模板 # NeoZQYY Monorepo 公共环境配置模板
# 复制为 .env 后填入实际值 # ==============================================================================
# ============================================================ # 使用方式:复制为 .env 后填入实际值
# 配置优先级DEFAULTS < .env < .env.local < 环境变量 < CLI 参数
#
# 本文件包含所有层级的参数模板:
# [ROOT] — 根 .env公共配置所有子系统共享
# [ETL] — apps/etl/connectors/feiqiu/.envETL 专属配置)
# [BACKEND] — apps/backend/.env.local后端私有覆盖
#
# 语法KEY=VALUE正斜杠路径布尔值用 true/false列表用逗号分隔
# ---------- 数据库(公共) ---------- # ╔════════════════════════════════════════════════════════════════════════════╗
# ║ [ROOT] 根 .env — 公共配置层 ║
# ╚════════════════════════════════════════════════════════════════════════════╝
# ------------------------------------------------------------------------------
# 数据库公共连接参数(后端 + ETL 共用同一 PostgreSQL 实例)
# ------------------------------------------------------------------------------
DB_HOST=localhost DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USER= DB_USER=
DB_PASSWORD= DB_PASSWORD=
# ---------- ETL 数据库etl_feiqiu ---------- # ------------------------------------------------------------------------------
ETL_DB_NAME=etl_feiqiu # 数据库名称
# 数据库清单:
# etl_feiqiu — ETL 流程(飞球连接器),正式环境
# test_etl_feiqiu — ETL 流程(飞球连接器),开发/测试环境
# zqyy_app — 小程序业务库,正式环境
# test_zqyy_app — 小程序业务库,开发/测试环境
# 开发/测试环境使用 test_ 前缀库
# ------------------------------------------------------------------------------
APP_DB_NAME=test_zqyy_app
ETL_DB_NAME=test_etl_feiqiu
# ---------- 业务数据库zqyy_app ---------- # ------------------------------------------------------------------------------
APP_DB_NAME=zqyy_app # 组合式 DSN各子系统 / 脚本需要完整连接串时使用)
# 格式postgresql://user:password@host:port/dbname
# ------------------------------------------------------------------------------
PG_DSN=postgresql://user:password@host:5432/test_etl_feiqiu
APP_DB_DSN=postgresql://user:password@host:5432/test_zqyy_app
# ---------- 时区 ---------- # 测试库 DSN运维脚本、集成测试优先使用
TEST_DB_DSN=postgresql://user:password@host:5432/test_etl_feiqiu
TEST_APP_DB_DSN=postgresql://user:password@host:5432/test_zqyy_app
# ------------------------------------------------------------------------------
# 通用
# ------------------------------------------------------------------------------
TIMEZONE=Asia/Shanghai TIMEZONE=Asia/Shanghai
LOG_LEVEL=INFO
# ---------- 门店标识 ---------- # ==============================================================================
# 统一输出路径配置export/ 目录)
# ==============================================================================
# 目录总览:
# export/
# ├── ETL-Connectors/feiqiu/
# │ ├── JSON/ — API 原始 JSON 导出
# │ ├── LOGS/ — ETL 运行日志
# │ └── REPORTS/ — ETL 质检/完整性报告
# ├── SYSTEM/
# │ ├── LOGS/ — 系统级运维日志
# │ ├── REPORTS/
# │ │ ├── dataflow_analysis/ — 数据流结构分析报告
# │ │ ├── field_audit/ — 字段排查报告
# │ │ └── full_dataflow_doc/ — 全链路数据流文档
# │ └── CACHE/
# │ └── api_samples/ — API 样本缓存
# └── BACKEND/
# └── LOGS/ — 后端结构化日志
# ------------------------------------------------------------------------------
# ETL Connector 输出路径
# ------------------------------------------------------------------------------
EXPORT_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/JSON
LOG_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/LOGS
FETCH_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/JSON
ETL_REPORT_ROOT=C:/NeoZQYY/export/ETL-Connectors/feiqiu/REPORTS
# ------------------------------------------------------------------------------
# 系统级输出路径
# ------------------------------------------------------------------------------
SYSTEM_ANALYZE_ROOT=C:/NeoZQYY/export/SYSTEM/REPORTS/dataflow_analysis
FIELD_AUDIT_ROOT=C:/NeoZQYY/export/SYSTEM/REPORTS/field_audit
FULL_DATAFLOW_DOC_ROOT=C:/NeoZQYY/export/SYSTEM/REPORTS/full_dataflow_doc
API_SAMPLE_CACHE_ROOT=C:/NeoZQYY/export/SYSTEM/CACHE/api_samples
SYSTEM_LOG_ROOT=C:/NeoZQYY/export/SYSTEM/LOGS
# ------------------------------------------------------------------------------
# 后端输出路径
# ------------------------------------------------------------------------------
BACKEND_LOG_ROOT=C:/NeoZQYY/export/BACKEND/LOGS
# ------------------------------------------------------------------------------
# 阿里云百炼 AI 配置
# ------------------------------------------------------------------------------
BAILIAN_API_KEY=
BAILIAN_TEST_APP_ID=
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ [ETL] apps/etl/connectors/feiqiu/.env — ETL 专属配置 ║
# ╚════════════════════════════════════════════════════════════════════════════╝
# 以下参数应放在 apps/etl/connectors/feiqiu/.env 中,而非根 .env
# ------------------------------------------------------------------------------
# 门店配置
# ------------------------------------------------------------------------------
STORE_ID= STORE_ID=
# ---------- 上游 API ---------- # ------------------------------------------------------------------------------
API_BASE_URL= # 数据库配置ETL 专属,覆盖根 .env 的公共参数)
API_TOKEN= # ------------------------------------------------------------------------------
# 完整 DSN优先使用设置后忽略分离式配置
# PG_DSN=postgresql://user:password@host:5432/test_etl_feiqiu
PG_CONNECT_TIMEOUT=10
# ---------- 日志 ---------- # 分离式配置(不使用 DSN 时启用)
LOG_LEVEL=INFO # PG_HOST=localhost
LOG_DIR=logs # PG_PORT=5432
# PG_USER=your_user
# PG_PASSWORD=your_password
# PG_NAME=your_database
# 数据库 Schema
SCHEMA_OLTP=ods
SCHEMA_ETL=meta
# ------------------------------------------------------------------------------
# 数据库会话参数defaults.py → db.session.*
# ------------------------------------------------------------------------------
# DB_SESSION_TIMEZONE=Asia/Shanghai
# DB_STATEMENT_TIMEOUT_MS=30000
# DB_LOCK_TIMEOUT_MS=5000
# DB_IDLE_IN_TX_TIMEOUT_MS=600000
# ------------------------------------------------------------------------------
# API 配置(上游 SaaS API
# ------------------------------------------------------------------------------
API_BASE=
API_TOKEN=
API_TIMEOUT=20
API_PAGE_SIZE=200
API_RETRY_MAX=3
# API_RETRY_BACKOFF=[1, 2, 4]
# API_PARAMS={}
# API_HEADERS_EXTRA={}
# ------------------------------------------------------------------------------
# 管线流程配置
# ------------------------------------------------------------------------------
PIPELINE_FLOW=FULL
# DATA_SOURCE=hybrid
# ------------------------------------------------------------------------------
# 时间窗口配置
# ------------------------------------------------------------------------------
OVERLAP_SECONDS=600
WINDOW_BUSY_MIN=30
WINDOW_IDLE_MIN=180
IDLE_START=04:00
IDLE_END=16:00
WINDOW_SPLIT_UNIT=day
WINDOW_SPLIT_DAYS=10
WINDOW_COMPENSATION_HOURS=2
ALLOW_EMPTY_RESULT_ADVANCE=true
# ------------------------------------------------------------------------------
# 快照配置
# ------------------------------------------------------------------------------
SNAPSHOT_MISSING_DELETE=true
SNAPSHOT_ALLOW_EMPTY_DELETE=false
# ------------------------------------------------------------------------------
# 数据完整性检查配置
# ------------------------------------------------------------------------------
INTEGRITY_MODE=history
INTEGRITY_HISTORY_START=2025-07-01
# INTEGRITY_HISTORY_END=
INTEGRITY_INCLUDE_DIMENSIONS=true
INTEGRITY_AUTO_CHECK=false
INTEGRITY_AUTO_BACKFILL=false
INTEGRITY_COMPARE_CONTENT=true
INTEGRITY_CONTENT_SAMPLE_LIMIT=50
INTEGRITY_BACKFILL_MISMATCH=true
INTEGRITY_RECHECK_AFTER_BACKFILL=true
# INTEGRITY_ODS_TASK_CODES=
# INTEGRITY_FORCE_MONTHLY_SPLIT=true
# ------------------------------------------------------------------------------
# 校验配置
# ------------------------------------------------------------------------------
VERIFY_SKIP_ODS_ON_FETCH=true
VERIFY_ODS_LOCAL_JSON=true
# ------------------------------------------------------------------------------
# IO 配置
# ------------------------------------------------------------------------------
WRITE_PRETTY_JSON=true
# INGEST_SOURCE_DIR=
# MANIFEST_NAME=manifest.json
# INGEST_REPORT_NAME=ingest_report.json
# MAX_FILE_BYTES=52428800
# ------------------------------------------------------------------------------
# 清洗配置defaults.py → clean.*
# ------------------------------------------------------------------------------
# CLEAN_LOG_UNKNOWN_FIELDS=true
# CLEAN_UNKNOWN_FIELDS_LIMIT=50
# CLEAN_HASH_ALGO=sha1
# CLEAN_HASH_SALT=
# CLEAN_STRICT_NUMERIC=true
# CLEAN_ROUND_MONEY_SCALE=2
# ------------------------------------------------------------------------------
# 安全配置defaults.py → security.*
# ------------------------------------------------------------------------------
# SECURITY_REDACT_IN_LOGS=true
# SECURITY_REDACT_KEYS=["token","password","Authorization"]
# SECURITY_ECHO_TOKEN_IN_LOGS=false
# ------------------------------------------------------------------------------
# DWD 层配置
# ------------------------------------------------------------------------------
DWD_FACT_UPSERT=true
# DWD_FACT_UPSERT_BATCH_SIZE=1000
# DWD_FACT_UPSERT_MIN_BATCH_SIZE=100
# DWD_FACT_UPSERT_MAX_RETRIES=2
# DWD_FACT_UPSERT_RETRY_BACKOFF=[1,2,4]
# DWD_FACT_UPSERT_LOCK_TIMEOUT_MS=
# ------------------------------------------------------------------------------
# 任务列表配置
# ------------------------------------------------------------------------------
RUN_TASKS=PRODUCTS,TABLES,MEMBERS,ASSISTANTS,PACKAGES_DEF,ORDERS,PAYMENTS,REFUNDS,COUPON_USAGE,INVENTORY_CHANGE,TOPUPS,TABLE_DISCOUNT,ASSISTANT_ABOLISH,LEDGER
# RUN_DWS_TASKS=
# RUN_INDEX_TASKS=
INDEX_LOOKBACK_DAYS=60
# ------------------------------------------------------------------------------
# DWS 月度/薪资配置
# ------------------------------------------------------------------------------
# DWS_MONTHLY_NEW_HIRE_CAP_EFFECTIVE_FROM=2026-03-01
# DWS_MONTHLY_NEW_HIRE_CAP_DAY=25
# DWS_MONTHLY_NEW_HIRE_MAX_TIER_LEVEL=2
# DWS_SALARY_RUN_DAYS=5
# DWS_SALARY_ALLOW_OUT_OF_CYCLE=false
# DWS_SALARY_ROOM_COURSE_PRICE=138
# DWS_MONTHLY_ALLOW_HISTORY=false
# DWS_MONTHLY_PREV_GRACE_DAYS=5
# DWS_MONTHLY_HISTORY_MONTHS=0
# ------------------------------------------------------------------------------
# ODS 离线回放配置(仅开发/运维使用)
# ------------------------------------------------------------------------------
# ODS_JSON_DOC_DIR=export/test-json-doc
# ODS_INCLUDE_FILES=
# ODS_DROP_SCHEMA_FIRST=true
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ [BACKEND] apps/backend/.env.local — 后端私有覆盖 ║
# ╚════════════════════════════════════════════════════════════════════════════╝
# 以下参数应放在 apps/backend/.env.local 中,而非根 .env
# ------------------------------------------------------------------------------
# ETL 数据库(后端只读访问,用于数据库查看器;省略时复用 DB_HOST/PORT/USER/PASSWORD
# ------------------------------------------------------------------------------
# ETL_DB_HOST=
# ETL_DB_PORT=
# ETL_DB_USER=
# ETL_DB_PASSWORD=
# ------------------------------------------------------------------------------
# JWT 认证
# ------------------------------------------------------------------------------
# JWT_SECRET_KEY=change-me-in-production
# JWT_ALGORITHM=HS256
# JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
# JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
# ------------------------------------------------------------------------------
# 微信小程序配置
# ------------------------------------------------------------------------------
# WX_CALLBACK_TOKEN=
# WX_APP_ID=
# WX_APP_SECRET=
# ------------------------------------------------------------------------------
# CORS逗号分隔
# ------------------------------------------------------------------------------
# CORS_ORIGINS=http://localhost:5173
# ------------------------------------------------------------------------------
# ETL 项目路径(子进程 cwd缺省按 monorepo 相对路径推算)
# ------------------------------------------------------------------------------
# ETL_PROJECT_PATH=C:/NeoZQYY/apps/etl/connectors/feiqiu

66
.gitignore vendored
View File

@@ -1,23 +1,57 @@
# ===== 临时与缓存 ===== # ===== 临时与缓存 =====
tmp/ # tmp/
__pycache__/ __pycache__/
*.pyc *.pyc
*.py[cod]
*$py.class
*.so
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
logs/ pytest-cache-files-*/
# logs/
*.log
*.jsonl
# ===== 运行时产出 =====
#export/
#reports/
# scripts/logs/
# ===== 环境配置(保留模板) ===== # ===== 环境配置(保留模板) =====
.env # .env
.env.local # .env.local
!.env.template # !.env.template
# ===== Node ===== # ===== Node =====
node_modules/ node_modules/
# ===== Python 虚拟环境 ===== # ===== Python 虚拟环境 =====
.venv/ # .venv/
venv/ # venv/
env/ # ENV/
# env/
# ===== Python 构建产物 =====
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
dist/
# ===== 测试覆盖率 =====
.coverage
htmlcov/
# ===== infra 敏感文件 ===== # ===== infra 敏感文件 =====
infra/**/*.key infra/**/*.key
@@ -27,8 +61,16 @@ infra/**/*.secret
# ===== IDE ===== # ===== IDE =====
.idea/ .idea/
.vscode/ .vscode/
*.swp
*.swo
*~
.specstory/
.cursorindexingignore
# ===== 分发/构建产物 ===== # ===== Windows 杂项 =====
dist/ *.lnk
build/ .Deleted/
*.egg-info/
# ===== Kiro 运行时状态 =====
.kiro/.audit_state.json
.kiro/.last_prompt_id.json

View File

@@ -1,4 +1,4 @@
{ {
"at": "2026-02-15T06:00:50.2089232+08:00", "prompt_id": "P20260223-225610",
"prompt_id": "P20260215-060050" "at": "2026-02-23T22:56:10.365734+08:00"
} }

View File

@@ -4,17 +4,26 @@ description: Run post-change audit + docs sync for NeoZQYY Monorepo; write audit
tools: ["read", "write", "shell"] tools: ["read", "write", "shell"]
--- ---
你是专职审计收口/后处理写入子代理。你的执行必须尽量不依赖主对话上下文优先使用本地仓库事实git、文件内容、prompt_log完成审计落盘。 你是专职"审计收口/后处理写入"子代理。你的执行必须尽量不依赖主对话上下文优先使用本地仓库事实git、文件内容、prompt_log完成审计落盘。
## 审计产物路径(统一根目录)
- 变更审计记录:`docs/audit/changes/<YYYY-MM-DD>__<slug>.md`
- 审计一览表:`docs/audit/audit_dashboard.md`(自动生成,勿手动编辑)
- Prompt 日志:`docs/audit/prompt_logs/`
- 一览表刷新命令:`python scripts/audit/gen_audit_dashboard.py`
- 所有审计产物统一写入项目根目录 `docs/audit/`,不要写入子模块(如 `apps/etl/connectors/feiqiu/docs/audit/`)内部
## 输入来源(不要询问主代理) ## 输入来源(不要询问主代理)
- 通过 `git status --porcelain``git diff` 获取本次未提交变更 - 通过 `git status --porcelain``git diff` 获取本次未提交变更
- 通过 `docs/audit/prompt_log.md` `.kiro/.last_prompt_id.json` 获取最新 Prompt-ID 与 prompt 原文(用于溯源) - 通过 `docs/audit/prompt_logs/` 目录下的独立日志文件`.kiro/.last_prompt_id.json` 获取最新 Prompt-ID 与 prompt 原文(用于溯源)
- 通过项目实际文件内容判断是否逻辑改动 - 通过项目实际文件内容判断是否"逻辑改动"
## 何时需要做重型后处理 ## 何时需要做"重型后处理"
满足任一即执行审计收口(否则只输出无逻辑改动/无需审计,并清除待审计标记): 满足任一即执行审计收口(否则只输出"无逻辑改动/无需审计",并清除待审计标记):
- 改动文件命中 ETL 管线高风险路径:`apps/etl/pipelines/feiqiu/` 下的 `api/``cli/``config/``database/``loaders/``models/``orchestration/``scd/``tasks/``utils/``quality/` - 改动文件命中 ETL Connector 高风险路径:`apps/etl/connectors/feiqiu/` 下的 `api/``cli/``config/``database/``loaders/``models/``orchestration/``scd/``tasks/``utils/``quality/`
- 改动文件命中后端 API`apps/backend/app/` - 改动文件命中后端 API`apps/backend/app/`
- 改动文件命中管理后台源码:`apps/admin-web/src/`
- 改动文件命中小程序源码:`apps/miniprogram/miniapp/``apps/miniprogram/miniprogram/`
- 改动文件命中共享包:`packages/shared/` - 改动文件命中共享包:`packages/shared/`
- 改动文件命中数据库定义:`db/` 下的 DDL / migration / seed 文件 - 改动文件命中数据库定义:`db/` 下的 DDL / migration / seed 文件
- 根目录散文件(`pyproject.toml``.env*` 等) - 根目录散文件(`pyproject.toml``.env*` 等)
@@ -29,9 +38,10 @@ tools: ["read", "write", "shell"]
- change-annotation-audit写 docs/audit/changes/... + AI_CHANGELOG + CHANGE 注释) - change-annotation-audit写 docs/audit/changes/... + AI_CHANGELOG + CHANGE 注释)
- bd-manual-db-docs仅当 DB schema 变更) - bd-manual-db-docs仅当 DB schema 变更)
3) 完成后把 `.kiro/.audit_state.json``audit_required` 置为 false或清空 reasons/changed_files/last_reminded_at 3) 完成后把 `.kiro/.audit_state.json``audit_required` 置为 false或清空 reasons/changed_files/last_reminded_at
4) 执行 `python scripts/audit/gen_audit_dashboard.py` 刷新审计一览表
## 输出(强制极短回执) ## 输出(强制极短回执)
你最终只允许输出 3 段信息: 你最终只允许输出 3 段信息:
- done: yes/no - done: yes/no
- files_written: <按行列出相对路径> - files_written: <按行列出相对路径>
- next_step: <若失败给 1~2 条;成功则写 commit when ready> - next_step: <若失败给 1~2 条;成功则写 "commit when ready">

View File

@@ -8,7 +8,7 @@
}, },
"then": { "then": {
"type": "runCommand", "type": "runCommand",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File .kiro/scripts/audit_flagger.ps1" "command": "python .kiro/scripts/audit_flagger.py"
}, },
"workspaceFolderName": "NeoZQYY", "workspaceFolderName": "NeoZQYY",
"shortName": "audit-flagger" "shortName": "audit-flagger"

View File

@@ -8,7 +8,7 @@
}, },
"then": { "then": {
"type": "runCommand", "type": "runCommand",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File .kiro/scripts/audit_reminder.ps1" "command": "python .kiro/scripts/audit_reminder.py"
}, },
"workspaceFolderName": "NeoZQYY", "workspaceFolderName": "NeoZQYY",
"shortName": "audit-reminder" "shortName": "audit-reminder"

View File

@@ -1,15 +0,0 @@
{
"enabled": false,
"name": "change-impact-reviewSteering + README",
"description": "每次 agent 执行结束后,评估本轮代码变更是否需要同步更新 product/tech/structure steering 文档及 README必要时自动更新并输出审计摘要。已禁用改为手动 /audit 子代理流程)",
"version": "1",
"when": {
"type": "agentStop"
},
"then": {
"type": "askAgent",
"prompt": "你必须对本轮执行进行「变更影响审查」。\n\n第一步判断本轮是否引入了「逻辑改动」业务规则、数据处理/ETL 逻辑、API 行为、鉴权/权限、小程序交互逻辑)。如果没有逻辑改动(仅格式化/注释/拼写修正),输出「无逻辑改动」并结束。\n\n第二步如果存在逻辑改动逐一评估以下文档是否需要更新需要则立即更新\n- .kiro/steering/product.md产品定位、业务规则/定义)\n- .kiro/steering/tech.md技术栈/约束、部署/运行时假设)\n- .kiro/steering/structure.md目录结构、关键模块边界\n- README.md运行方式、环境变量、接口、本地部署、集成说明\n- gui/README.md\tGUI 的独立文档,需要说明各子目录用途和常用命令\n- docs/ 文档目录索引,帮助找到正确的子目录\n- scripts/ 脚本较多且分子目录,需要说明各子目录用途和常用命令\n- tasks/ 任务开发约定(如何新增任务、注册流程)\n- database/ Schema 约定、迁移规范\n- tests/ 测试运行方式、FakeDB/FakeAPI 用法\n\n第三步输出审计友好的摘要\n- 变更范围:涉及的模块/接口/数据库对象\n- 变更原因:为什么改\n- 风险评估:回归范围 + 建议运行的测试/验证\n- 文档同步:已更新的文档列表(或明确说明无需更新的理由)\n\n第4步) 变更标注与审计落盘(强制执行):\n创建或更新审计记录文件docs/audit/changes/<YYYY-MM-DD>__<slug>.md内容必须包含\n- 日期/时间Asia/Shanghai\n- 原始用户 Prompt原文或引用 Prompt-ID + 不超过 5 行的摘录)\n- 直接原因AI 分析后“为何必须改” + “修改方案简介”\n- 修改文件清单Files changed list\n- 风险点、回滚要点、验证步骤(至少包含可执行的验证方式)\n对每一个被修改的文件必须在文件内新增或更新 AI_CHANGELOG 记录项,至少包含:\n- 日期\n- PromptPrompt-ID + 摘录)\n- 直接原因(必要性 + 方案简介)\n- 变更摘要(改了什么:模块/函数/接口/字段等)\n- 风险与验证(回归范围 + 验证方法/测试点/SQL/联调步骤)\n对每一处“逻辑变更”的代码块必须在变更附近添加内联 CHANGE 标记注释,至少说明:\n- 变更意图intent\n- 关键假设assumptions\n- 边界条件/资金口径/精度与舍入规则(若相关)\n- 关联 PromptPrompt-ID 或摘录)以及必要的验证提示\n\n硬性规则如果涉及数据库 schema 或表结构变更,必须同步更新 docs/database/ 下对应的表结构文档。"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "change-impact-review"
}

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "Data Flow Structure Analysis",
"description": "手动触发数据流结构分析:先执行 Python 脚本采集 API JSON、DB 表结构、三层字段映射和 BD_manual 业务描述,再由报告生成器输出带锚点链接、业务描述、多示例值、白名单折叠和字段差异报告的 Markdown 文档。",
"version": "4.0.0",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行数据流结构分析,按以下步骤完成。若发现已完成或有历史任务痕迹则清空,重新执行:\n\n第一阶段数据采集\n1. 运行 `python scripts/ops/analyze_dataflow.py` 完成数据采集(如需指定日期范围,加 --date-from / --date-to 参数)\n2. 确认采集结果已落盘,包括:\n - json_trees/(含 samples 多示例值)\n - db_schemas/\n - field_mappings/(三层映射 + 锚点)\n - bd_descriptions/BD_manual 业务描述)\n - collection_manifest.json含 json_field_count、date_from、date_to\n\n第二阶段报告生成\n3. 运行 `python scripts/ops/gen_dataflow_report.py` 生成 Markdown 报告\n4. 报告包含以下增强内容:\n - 报告头含 API 请求日期范围date_from ~ date_to和 JSON 数据总量\n - 总览表含 API JSON 字段数列\n - 1.1 API↔ODS↔DWD 字段对比差异报告(白名单字段折叠汇总,不展开详细表格行)\n - 2.3 覆盖率表含业务描述列\n - API 源字段表含业务描述列 + 多示例值(枚举值解释)\n - ODS 表结构含业务描述列 + 上下游双向映射锚点链接\n - DWD 表结构含业务描述列 + ODS 来源锚点链接\n5. 输出文件路径和关键统计摘要\n\n白名单规则v4\n- ETL 元数据列source_file, source_endpoint, fetched_at, payload, content_hash\n- DWD 维表 SCD2 管理列valid_from, valid_to, is_current, etl_loaded_at, etl_batch_id\n- API siteProfile 嵌套对象字段\n- 白名单字段仍正常参与检查和统计,仅在报告中折叠显示并注明原因\n\n注意当前仅分析飞球feiqiu连接器。未来新增连接器时应自动发现并纳入分析范围。"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "dataflow-analyze"
}

View File

@@ -1,22 +0,0 @@
{
"enabled": false,
"name": "DB Schema 文档执行 (bd_manual)",
"description": "当数据库 schema/migration 相关文件被保存时,检查是否有表结构变更,并自动更新 docs/database/ 下对应的表结构文档。(已禁用:由 /audit 流程统一处理 DB 文档)",
"version": "1",
"when": {
"type": "fileEdited",
"patterns": [
"**/migrations/**/*.*",
"**/*.sql",
"**/*ddl*.*",
"**/*schema*.*",
"**/*.prisma"
]
},
"then": {
"type": "askAgent",
"prompt": "一个数据库相关文件刚被保存。你必须检查是否发生了 schema/表结构变更。\n\n如果发生了表结构变更你必须更新以下目录中的文档\ndocs/database/\n\n最低输出要求必须写入对应 schema 目录 + 表结构文档):\n1) 变更内容:表/字段/类型/可空性/默认值/约束/索引/外键的具体变化\n2) 变更原因:业务背景与动机\n3) 影响范围ETL 管线、后端 API 契约、小程序字段等\n4) 回滚策略:如何回退 + 数据回填注意事项\n5) 验证 SQL至少 3 条查询语句用于验证变更正确性\n6) 溯源留痕日期Asia/ShanghaiYYYY-MM-DDPromptPrompt-ID + ≤5 行摘录或原文Direct cause必要性 + 修改方案简介)\n\n如果没有发生表结构变更例如仅修改注释在变更日志文档中写一条简短说明\"无结构性变更\"(同样要带日期 + Prompt-ID。"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "db-schema-doc-enforcer"
}

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "ETL Data Consistency Check",
"description": "手动触发 ETL 全链路数据一致性黑盒检查:获取最近一次成功的 ETL 任务,对 API→ODS→DWD→DWS/INDEX 逐表逐字段进行实际数据比对,输出详细的数据差异报告。",
"version": "1.0.0",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行 ETL 全链路数据一致性黑盒检查,按以下步骤完成,若发现已完成或有历史任务痕迹则清空,重新执行:\n\n1. 运行 `python scripts/ops/etl_consistency_check.py`\n2. 脚本会自动:\n a. 从 LOG_ROOT 找到最近一次成功的 ETL 日志,解析本次执行的任务列表\n b. 从 FETCH_ROOT 读取本次 ETL 落盘的 API JSON 文件\n c. 连接数据库PG_DSN对本次任务涉及的每张表逐字段比对\n - API JSON vs ODS字段完整性、值采样比对随机 5 条记录的关键字段)\n - ODS vs DWD字段映射正确性、值转换验证采样比对\n - DWD vs DWS/INDEX聚合逻辑验证行数、关键指标抽查\n d. 输出 Markdown 报告到 ETL_REPORT_ROOT\n3. 检查报告输出,汇总关键发现\n\n报告结构\n- 1. ETL 执行概览(任务列表、成功/失败/跳过统计)\n- 2. API↔ODS 数据一致性(逐表逐字段值比对)\n- 3. ODS↔DWD 数据一致性(映射验证 + 值采样)\n- 4. DWD↔DWS 数据一致性(聚合逻辑验证)\n- 5. 异常汇总与建议\n\n注意使用正式库 PG_DSN 连接(只读模式),不修改任何数据。"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "etl-data-consistency"
}

View File

@@ -8,7 +8,7 @@
}, },
"then": { "then": {
"type": "runCommand", "type": "runCommand",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File .kiro/scripts/prompt_audit_log.ps1" "command": "python .kiro/scripts/prompt_audit_log.py"
}, },
"workspaceFolderName": "NeoZQYY", "workspaceFolderName": "NeoZQYY",
"shortName": "prompt-audit-log" "shortName": "prompt-audit-log"

View File

@@ -8,7 +8,7 @@
}, },
"then": { "then": {
"type": "askAgent", "type": "askAgent",
"prompt": "立刻启动名为 audit-writer 的子代理来执行「后处理写入/审计收口」流程。\n\n约束\n- 子代理自行使用 git status/diff 与 .kiro/.last_prompt_id.json 中最新 Prompt-ID 作为溯源;不要依赖主对话上下文。\n- 子代理必须按需调用 skillsteering-readme-maintainer、change-annotation-audit、bd-manual-db-docs仅在满足触发条件时。\n- 子代理结束后,必须把 .kiro/.audit_state.json 中 audit_required 置为 false或清空文件以停止后续提醒。\n- 审计落盘完成后,必须执行 `python scripts/gen_audit_dashboard.py` 刷新审计一览表docs/audit/audit_dashboard.md。\n- 你的最终回复必须是「极短回执」,只包含:\n 1) 是否完成yes/no\n 2) 写了哪些文件(文件列表)\n 3) 如果失败下一步怎么做1~2 条)" "prompt": "立刻启动名为 audit-writer 的子代理来执行「后处理写入/审计收口」流程。\n\n约束\n- 子代理自行使用 git status/diff 与 .kiro/.last_prompt_id.json 中最新 Prompt-ID 作为溯源;不要依赖主对话上下文。\n- 子代理必须按需调用 skillsteering-readme-maintainer、change-annotation-audit、bd-manual-db-docs仅在满足触发条件时。\n- 所有审计产物统一写入项目根目录 docs/audit/(变更记录写 docs/audit/changes/prompt 日志写 docs/audit/prompt_logs/),不要写入子模块内部。\n- 子代理结束后,必须把 .kiro/.audit_state.json 中 audit_required 置为 false或清空文件以停止后续提醒。\n- 审计落盘完成后,必须执行 `python scripts/audit/gen_audit_dashboard.py` 刷新审计一览表docs/audit/audit_dashboard.md。\n- 你的最终回复必须是「极短回执」,只包含:\n 1) 是否完成yes/no\n 2) 写了哪些文件(文件列表)\n 3) 如果失败下一步怎么做1~2 条)"
}, },
"workspaceFolderName": "NeoZQYY", "workspaceFolderName": "NeoZQYY",
"shortName": "audit" "shortName": "audit"

View File

@@ -1,128 +0,0 @@
# audit_flagger.ps1 — 判断 git 工作区是否存在高风险改动
# 兼容 Windows PowerShell 5.1(避免 try{} 内嵌套脚本块的解析器 bug
$ErrorActionPreference = "SilentlyContinue"
function Get-TaipeiNow {
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Taipei Standard Time")
if ($tz) {
return [TimeZoneInfo]::ConvertTime([DateTimeOffset]::Now, $tz)
}
return [DateTimeOffset]::Now
}
function Sha1Hex([string]$s) {
$sha1 = [System.Security.Cryptography.SHA1]::Create()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($s)
$hash = $sha1.ComputeHash($bytes)
return ([BitConverter]::ToString($hash) -replace "-", "").ToLowerInvariant()
}
function Get-ChangedFiles {
$status = git status --porcelain 2>$null
if ($LASTEXITCODE -ne 0) { return @() }
$result = @()
foreach ($line in $status) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$pathPart = $line.Substring([Math]::Min(3, $line.Length - 1)).Trim()
if ($pathPart -match " -> ") { $pathPart = ($pathPart -split " -> ")[-1] }
if ([string]::IsNullOrWhiteSpace($pathPart)) { continue }
$result += $pathPart.Replace("\", "/").Trim()
}
return $result
}
function Test-NoiseFile([string]$f) {
if ($f -match "^docs/audit/") { return $true }
if ($f -match "^\.kiro/\.audit_state\.json$") { return $true }
if ($f -match "^\.kiro/\.last_prompt_id\.json$") { return $true }
if ($f -match "^\.kiro/scripts/") { return $true }
return $false
}
function Write-AuditState([string]$json) {
$null = New-Item -ItemType Directory -Force -Path ".kiro" 2>$null
Set-Content -Path ".kiro/.audit_state.json" -Value $json -Encoding UTF8
}
# --- 主逻辑 ---
# 非 git 仓库直接退出
$null = git rev-parse --is-inside-work-tree 2>$null
if ($LASTEXITCODE -ne 0) { exit 0 }
$allFiles = Get-ChangedFiles
# 过滤噪声
$files = @()
foreach ($f in $allFiles) {
if (-not (Test-NoiseFile $f)) { $files += $f }
}
$files = $files | Sort-Object -Unique
$now = Get-TaipeiNow
if ($files.Count -eq 0) {
$json = '{"audit_required":false,"db_docs_required":false,"reasons":[],"changed_files":[],"change_fingerprint":"","marked_at":"' + $now.ToString("o") + '","last_reminded_at":null}'
Write-AuditState $json
exit 0
}
# 高风险路径("regex|label" 格式,避免 @{} 哈希表在 PS 5.1 的解析问题)
$riskRules = @(
"^apps/etl/pipelines/feiqiu/(api|cli|config|database|loaders|models|orchestration|scd|tasks|utils|quality)/|etl",
"^apps/backend/app/|backend",
"^packages/shared/|shared",
"^db/|db"
)
$reasons = @()
$auditRequired = $false
$dbDocsRequired = $false
foreach ($f in $files) {
foreach ($rule in $riskRules) {
$idx = $rule.LastIndexOf("|")
$pat = $rule.Substring(0, $idx)
$lbl = $rule.Substring($idx + 1)
if ($f -match $pat) {
$auditRequired = $true
$tag = "dir:" + $lbl
if ($reasons -notcontains $tag) { $reasons += $tag }
}
}
if ($f -notmatch "/") {
$auditRequired = $true
if ($reasons -notcontains "root-file") { $reasons += "root-file" }
}
if ($f -match "^db/" -or $f -match "/migrations/" -or $f -match "\.sql$" -or $f -match "\.prisma$") {
$dbDocsRequired = $true
if ($reasons -notcontains "db-schema-change") { $reasons += "db-schema-change" }
}
}
$fp = Sha1Hex(($files -join "`n"))
# 读取已有状态以保留 last_reminded_at
$lastReminded = $null
if (Test-Path ".kiro/.audit_state.json") {
$raw = Get-Content ".kiro/.audit_state.json" -Raw 2>$null
if ($raw) {
$existing = $raw | ConvertFrom-Json 2>$null
if ($existing -and $existing.change_fingerprint -eq $fp) {
$lastReminded = $existing.last_reminded_at
}
}
}
$stateObj = [ordered]@{
audit_required = [bool]$auditRequired
db_docs_required = [bool]$dbDocsRequired
reasons = $reasons
changed_files = $files | Select-Object -First 50
change_fingerprint = $fp
marked_at = $now.ToString("o")
last_reminded_at = $lastReminded
}
Write-AuditState ($stateObj | ConvertTo-Json -Depth 6)
exit 0

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""audit_flagger — 判断 git 工作区是否存在高风险改动,写入 .kiro/.audit_state.json
替代原 PowerShell 版本,避免 Windows PowerShell 5.1 解析器 bug。
"""
import hashlib
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
RISK_RULES = [
(re.compile(r"^apps/etl/connectors/feiqiu/(api|cli|config|database|loaders|models|orchestration|scd|tasks|utils|quality)/"), "etl"),
(re.compile(r"^apps/backend/app/"), "backend"),
(re.compile(r"^apps/admin-web/src/"), "admin-web"),
(re.compile(r"^apps/miniprogram/(miniapp|miniprogram)/"), "miniprogram"),
(re.compile(r"^packages/shared/"), "shared"),
(re.compile(r"^db/"), "db"),
]
NOISE_PATTERNS = [
re.compile(r"^docs/audit/"),
re.compile(r"^\.kiro/"), # .kiro 配置变更不触发业务审计
re.compile(r"^tmp/"),
re.compile(r"^\.hypothesis/"),
]
DB_PATTERNS = [
re.compile(r"^db/"),
re.compile(r"/migrations/"),
re.compile(r"\.sql$"),
re.compile(r"\.prisma$"),
]
STATE_PATH = os.path.join(".kiro", ".audit_state.json")
def now_taipei():
return datetime.now(TZ_TAIPEI).isoformat()
def sha1hex(s: str) -> str:
return hashlib.sha1(s.encode("utf-8")).hexdigest()
def get_changed_files() -> list[str]:
"""从 git status --porcelain 提取变更文件路径"""
try:
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return []
except Exception:
return []
files = []
for line in result.stdout.splitlines():
if len(line) < 4:
continue
path = line[3:].strip()
if " -> " in path:
path = path.split(" -> ")[-1]
path = path.strip().strip('"').replace("\\", "/")
if path:
files.append(path)
return files
def is_noise(f: str) -> bool:
return any(p.search(f) for p in NOISE_PATTERNS)
def write_state(state: dict):
os.makedirs(".kiro", exist_ok=True)
with open(STATE_PATH, "w", encoding="utf-8") as fh:
json.dump(state, fh, indent=2, ensure_ascii=False)
def main():
# 非 git 仓库直接退出
try:
r = subprocess.run(
["git", "rev-parse", "--is-inside-work-tree"],
capture_output=True, text=True, timeout=5
)
if r.returncode != 0:
return
except Exception:
return
all_files = get_changed_files()
files = sorted(set(f for f in all_files if not is_noise(f)))
now = now_taipei()
if not files:
write_state({
"audit_required": False,
"db_docs_required": False,
"reasons": [],
"changed_files": [],
"change_fingerprint": "",
"marked_at": now,
"last_reminded_at": None,
})
return
reasons = []
audit_required = False
db_docs_required = False
for f in files:
for pattern, label in RISK_RULES:
if pattern.search(f):
audit_required = True
tag = f"dir:{label}"
if tag not in reasons:
reasons.append(tag)
# 根目录散文件
if "/" not in f:
audit_required = True
if "root-file" not in reasons:
reasons.append("root-file")
# DB 文档触发
if any(p.search(f) for p in DB_PATTERNS):
db_docs_required = True
if "db-schema-change" not in reasons:
reasons.append("db-schema-change")
fp = sha1hex("\n".join(files))
# 保留已有状态的 last_reminded_at
last_reminded = None
if os.path.isfile(STATE_PATH):
try:
with open(STATE_PATH, "r", encoding="utf-8") as fh:
existing = json.load(fh)
if existing.get("change_fingerprint") == fp:
last_reminded = existing.get("last_reminded_at")
except Exception:
pass
write_state({
"audit_required": audit_required,
"db_docs_required": db_docs_required,
"reasons": reasons,
"changed_files": files[:50],
"change_fingerprint": fp,
"marked_at": now,
"last_reminded_at": last_reminded,
})
if __name__ == "__main__":
try:
main()
except Exception:
# 绝不阻塞 prompt 提交
pass

View File

@@ -1,72 +0,0 @@
$ErrorActionPreference = "Stop"
function Get-TaipeiNow {
try {
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Taipei Standard Time")
return [TimeZoneInfo]::ConvertTime([DateTimeOffset]::Now, $tz)
} catch {
return [DateTimeOffset]::Now
}
}
try {
$statePath = ".kiro/.audit_state.json"
if (-not (Test-Path $statePath)) { exit 0 }
$state = $null
try { $state = Get-Content $statePath -Raw | ConvertFrom-Json } catch { exit 0 }
if (-not $state) { exit 0 }
# If no pending audit, do nothing
if (-not $state.audit_required) { exit 0 }
# If working tree is clean (ignoring logs/state), stop reminding
$null = git rev-parse --is-inside-work-tree 2>$null
if ($LASTEXITCODE -ne 0) { exit 0 }
$status = git status --porcelain
$files = @()
foreach ($line in $status) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$pathPart = $line.Substring([Math]::Min(3, $line.Length-1)).Trim()
if ($pathPart -match " -> ") { $pathPart = ($pathPart -split " -> ")[-1] }
$p = $pathPart.Replace("\", "/").Trim()
if ($p -and $p -notmatch "^docs/audit/" -and $p -notmatch "^\.kiro/") { $files += $p }
}
if (($files | Sort-Object -Unique).Count -eq 0) {
$state.audit_required = $false
$state.reasons = @()
$state.changed_files = @()
$state.last_reminded_at = $null
($state | ConvertTo-Json -Depth 6) | Set-Content -Path $statePath -Encoding UTF8
exit 0
}
$now = Get-TaipeiNow
$minInterval = [TimeSpan]::FromMinutes(15)
$last = $null
if ($state.last_reminded_at) {
try { $last = [DateTimeOffset]::Parse($state.last_reminded_at) } catch { $last = $null }
}
$shouldRemind = $true
if ($last) {
$elapsed = $now - $last
if ($elapsed -lt $minInterval) { $shouldRemind = $false }
}
if (-not $shouldRemind) { exit 0 }
# Update last_reminded_at (persist even if user ignores)
$state.last_reminded_at = $now.ToString("o")
($state | ConvertTo-Json -Depth 6) | Set-Content -Path $statePath -Encoding UTF8
$reasons = @()
if ($state.reasons) { $reasons = @($state.reasons) }
$reasonText = if ($reasons.Count -gt 0) { ($reasons -join ", ") } else { "high-risk paths changed" }
[Console]::Error.WriteLine("[AUDIT REMINDER] Pending audit detected ($reasonText). Run /audit (Manual: Run /audit hook) to sync docs & write audit artifacts. (rate limit: 15min)")
exit 1
} catch {
exit 0
}

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""audit_reminder — Agent 结束时检查是否有待审计改动15 分钟限频提醒。
替代原 PowerShell 版本,避免 Windows PowerShell 5.1 解析器 bug。
"""
import json
import os
import subprocess
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
STATE_PATH = os.path.join(".kiro", ".audit_state.json")
MIN_INTERVAL = timedelta(minutes=15)
def now_taipei():
return datetime.now(TZ_TAIPEI)
def load_state():
if not os.path.isfile(STATE_PATH):
return None
try:
with open(STATE_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def save_state(state):
os.makedirs(".kiro", exist_ok=True)
with open(STATE_PATH, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
def get_real_changes():
"""获取排除噪声后的变更文件"""
try:
r = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, timeout=10)
if r.returncode != 0:
return []
except Exception:
return []
files = []
for line in r.stdout.splitlines():
if len(line) < 4:
continue
path = line[3:].strip().strip('"').replace("\\", "/")
if " -> " in path:
path = path.split(" -> ")[-1]
# 排除审计产物、.kiro 配置、临时文件
if path and not path.startswith("docs/audit/") and not path.startswith(".kiro/") and not path.startswith("tmp/") and not path.startswith(".hypothesis/"):
files.append(path)
return sorted(set(files))
def main():
state = load_state()
if not state:
sys.exit(0)
if not state.get("audit_required"):
sys.exit(0)
# 工作树干净时清除审计状态
real_files = get_real_changes()
if not real_files:
state["audit_required"] = False
state["reasons"] = []
state["changed_files"] = []
state["last_reminded_at"] = None
save_state(state)
sys.exit(0)
now = now_taipei()
# 15 分钟限频
last_str = state.get("last_reminded_at")
if last_str:
try:
last = datetime.fromisoformat(last_str)
if (now - last) < MIN_INTERVAL:
sys.exit(0)
except Exception:
pass
# 更新提醒时间
state["last_reminded_at"] = now.isoformat()
save_state(state)
reasons = state.get("reasons", [])
reason_text = ", ".join(reasons) if reasons else "high-risk paths changed"
sys.stderr.write(
f"[AUDIT REMINDER] Pending audit detected ({reason_text}). "
f"Run /audit (Manual: Run /audit hook) to sync docs & write audit artifacts. "
f"(rate limit: 15min)\n"
)
sys.exit(1)
if __name__ == "__main__":
try:
main()
except Exception:
sys.exit(0)

View File

@@ -1,61 +0,0 @@
$ErrorActionPreference = "Stop"
function Get-TaipeiNow {
try {
$tz = [TimeZoneInfo]::FindSystemTimeZoneById("Taipei Standard Time")
return [TimeZoneInfo]::ConvertTime([DateTimeOffset]::Now, $tz)
} catch {
return [DateTimeOffset]::Now
}
}
try {
$now = Get-TaipeiNow
$promptId = "P{0}" -f $now.ToString("yyyyMMdd-HHmmss")
$promptRaw = $env:USER_PROMPT
if ($null -eq $promptRaw) { $promptRaw = "" }
# 截断过长的 prompt避免意外记录展开的 #context
if ($promptRaw.Length -gt 20000) {
$promptRaw = $promptRaw.Substring(0, 5000) + "`n[TRUNCATED: prompt too long; possible expanded #context]"
}
$summary = ($promptRaw -replace "\s+", " ").Trim()
if ($summary.Length -gt 120) { $summary = $summary.Substring(0, 120) + "" }
if ([string]::IsNullOrWhiteSpace($summary)) { $summary = "(empty prompt)" }
# CHANGE [2026-02-15] intent: 简化为每次直接写独立文件到 prompt_logs/,不再维护 prompt_log.md 中间文件
$logDir = Join-Path "docs" "audit" "prompt_logs"
$null = New-Item -ItemType Directory -Force -Path $logDir 2>$null
$filename = "prompt_log_{0}.md" -f $now.ToString("yyyyMMdd_HHmmss")
$targetLog = Join-Path $logDir $filename
$timestamp = $now.ToString("yyyy-MM-dd HH:mm:ss zzz")
$entry = @"
- [$promptId] $timestamp
- summary: $summary
- prompt:
``````text
$promptRaw
``````
"@
Set-Content -Path $targetLog -Value $entry -Encoding UTF8
# 保存 last prompt id 供下游 /audit 溯源
$stateDir = ".kiro"
$null = New-Item -ItemType Directory -Force -Path $stateDir 2>$null
$lastPrompt = @{
prompt_id = $promptId
at = $now.ToString("o")
} | ConvertTo-Json -Depth 4
Set-Content -Path (Join-Path $stateDir ".last_prompt_id.json") -Value $lastPrompt -Encoding UTF8
exit 0
} catch {
# 不阻塞 prompt 提交
exit 0
}

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""prompt_audit_log — 每次提交 prompt 时生成独立日志文件。
替代原 PowerShell 版本,避免 Windows PowerShell 5.1 解析器 bug。
"""
import json
import os
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
def main():
now = datetime.now(TZ_TAIPEI)
prompt_id = f"P{now.strftime('%Y%m%d-%H%M%S')}"
prompt_raw = os.environ.get("USER_PROMPT", "")
# 截断过长的 prompt避免展开的 #context 占用过多空间)
if len(prompt_raw) > 20000:
prompt_raw = prompt_raw[:5000] + "\n[TRUNCATED: prompt too long; possible expanded #context]"
summary = " ".join(prompt_raw.split()).strip()
if len(summary) > 120:
summary = summary[:120] + ""
if not summary:
summary = "(empty prompt)"
# 写独立日志文件
log_dir = os.path.join("docs", "audit", "prompt_logs")
os.makedirs(log_dir, exist_ok=True)
filename = f"prompt_log_{now.strftime('%Y%m%d_%H%M%S')}.md"
target = os.path.join(log_dir, filename)
timestamp = now.strftime("%Y-%m-%d %H:%M:%S %z")
entry = f"""- [{prompt_id}] {timestamp}
- summary: {summary}
- prompt:
```text
{prompt_raw}
```
"""
with open(target, "w", encoding="utf-8") as f:
f.write(entry)
# 保存 last prompt id 供 /audit 溯源
os.makedirs(".kiro", exist_ok=True)
last_prompt = {"prompt_id": prompt_id, "at": now.isoformat()}
with open(os.path.join(".kiro", ".last_prompt_id.json"), "w", encoding="utf-8") as f:
json.dump(last_prompt, f, indent=2, ensure_ascii=False)
if __name__ == "__main__":
try:
main()
except Exception:
# 不阻塞 prompt 提交
pass

View File

@@ -1,4 +1,80 @@
{ {
"mcpServers": { "mcpServers": {
"git": {
"command": "uvx",
"args": [
"mcp-server-git@2025.12.18",
"--repository",
"C:\\NeoZQYY"
],
"disabled": false,
"autoApprove": [
"all",
"*"
]
},
"postgres": {
"disabled": true
},
"pg-etl": {
"command": "uvx",
"args": [
"postgres-mcp",
"--access-mode=unrestricted"
],
"env": {
"DATABASE_URI": "postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/etl_feiqiu"
},
"disabled": true,
"autoApprove": [
"all",
"*"
]
},
"pg-etl-test": {
"command": "uvx",
"args": [
"postgres-mcp",
"--access-mode=unrestricted"
],
"env": {
"DATABASE_URI": "postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/test_etl_feiqiu"
},
"disabled": false,
"autoApprove": [
"all",
"*"
]
},
"pg-app": {
"command": "uvx",
"args": [
"postgres-mcp",
"--access-mode=unrestricted"
],
"env": {
"DATABASE_URI": "postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/zqyy_app"
},
"disabled": true,
"autoApprove": [
"all",
"*"
]
},
"pg-app-test": {
"command": "uvx",
"args": [
"postgres-mcp",
"--access-mode=unrestricted"
],
"env": {
"DATABASE_URI": "postgresql://local-Python:Neo-local-1991125@100.64.0.4:5432/test_zqyy_app"
},
"disabled": false,
"autoApprove": [
"all",
"*"
]
}
} }
} }

View File

@@ -1,10 +1,10 @@
--- ---
name: steering-readme-maintainer name: steering-readme-maintainer
description: 当发生业务/ETL/API/鉴权/小程序交互等逻辑改动时,用于执行变更影响审查并同步更新 product/tech/structure/README 与审计记录。 description: 当发生业务/ETL/API/鉴权/小程序交互等逻辑改动时,用于执行变更影响审查并同步更新 product/tech/structure/各级 README 与审计记录。
--- ---
# 目的 # 目的
逻辑改动→文档同步→审计留痕流程标准化,减少漏更与口径漂移风险(资金相关场景优先保证可追溯与可复算)。 "逻辑改动→文档同步→审计留痕"流程标准化,减少漏更与口径漂移风险(资金相关场景优先保证可追溯与可复算)。
# 触发条件(何时调用本 Skill # 触发条件(何时调用本 Skill
- 修改了业务规则/计算口径/资金处理(精度、舍入、阈值等) - 修改了业务规则/计算口径/资金处理(精度、舍入、阈值等)
@@ -13,17 +13,30 @@ description: 当发生业务/ETL/API/鉴权/小程序交互等逻辑改动时,
- 修改了小程序关键交互流程(校验、状态机、关键字段) - 修改了小程序关键交互流程(校验、状态机、关键字段)
# 工作流(必须按顺序执行) # 工作流(必须按顺序执行)
## 1) 分类:是否属于逻辑改动 ## 1) 分类:是否属于"逻辑改动"
- 若不是逻辑改动:写明无逻辑改动,并说明为何(例如仅格式化/拼写修正/注释调整)。 - 若不是逻辑改动:写明"无逻辑改动",并说明为何(例如仅格式化/拼写修正/注释调整)。
- 若是逻辑改动:进入下一步。 - 若是逻辑改动:进入下一步。
## 2) Steering 与 README 同步(逐项评估) ## 2) Steering 与 README 同步(逐项评估)
### 2a) Steering 文件
- `.kiro/steering/product.md`:业务定义/口径/资金规则是否变化? - `.kiro/steering/product.md`:业务定义/口径/资金规则是否变化?
- `.kiro/steering/tech.md`:技术栈/运行方式/依赖/部署假设是否变化? - `.kiro/steering/tech.md`:技术栈/运行方式/依赖/部署假设是否变化?
- `.kiro/steering/structure-lite.md摘要/ .kiro/steering/structure.md仅在目录树/边界变化时)`:目录/模块边界/职责是否变化? - `.kiro/steering/structure-lite.md`(摘要)/ `.kiro/steering/structure.md`(仅在目录树/边界变化时):目录/模块边界/职责是否变化?
- `README.md`:运行方式、配置、环境变量、接口契约、联调步骤是否变化?
> 规则:如果“对读者理解系统行为”有帮助,就应更新;不要为了追求“少改文档”而拒绝同步。 ### 2b) 各级 README.md根据变更涉及的模块逐一评估
- `README.md`(根目录):项目总览、快速开始、环境变量、架构概述
- `apps/backend/README.md`:后端 API 路由、配置、运行方式、接口契约
- `apps/etl/connectors/feiqiu/README.md`ETL 任务清单、开发约定、注册流程
- `apps/miniprogram/README.md`:小程序页面结构、构建部署
- `apps/admin-web/README.md`:管理后台功能说明
- `packages/shared/README.md`:共享包模块说明、使用方式
- `db/README.md`Schema 约定、迁移规范、种子数据说明
- `scripts/README.md`:各子目录用途、常用脚本说明
- `tests/README.md`测试运行方式、FakeDB/FakeAPI 用法
- `docs/README.md`:文档目录索引
> 规则:只更新与本次变更相关的 README如果"对读者理解系统行为"有帮助,就应更新;不要为了追求"少改文档"而拒绝同步。若某个 README 尚不存在但变更涉及该模块,应创建。
## 3) 输出审计友好摘要(对话回复/审计记录都需要) ## 3) 输出审计友好摘要(对话回复/审计记录都需要)
- Changed改了哪些模块/接口/表/关键文件 - Changed改了哪些模块/接口/表/关键文件

View File

@@ -0,0 +1,750 @@
# 设计文档Web 管理后台admin-web-console
## 概述
将现有 PySide6 桌面 GUI 替换为 BS 架构的 Web 管理后台。系统分为两部分:
- **后端**:在现有 `apps/backend/` FastAPI 骨架上扩展,新增 ETL 管理相关的 RESTful API 和 WebSocket 端点
- **前端**:在 `apps/admin-web/` 下使用 React + Vite + Ant Design 构建 SPA 应用
核心设计原则:
1. 后端通过子进程调用现有 ETL_CLI不重写 ETL 逻辑
2. 调度任务从本地 JSON 迁移至 PostgreSQL`zqyy_app` 库)
3. 前后端通过 JSON API 通信,实时日志通过 WebSocket 推送
4. 数据库查询限制为只读,防止误操作
5. **多门店隔离**:通过 `site_id` 贯穿全链路Operator 登录后绑定门店,所有 API 请求自动携带 site_id
6. **执行流程Flow分离**:完整保留现有 7 种 Flow 和 4 种处理模式,前端按 Flow 动态展示可选层和任务
## 架构
```mermaid
graph TB
subgraph "前端 (apps/admin-web/)"
FE[React SPA<br/>Vite + Ant Design]
end
subgraph "后端 (apps/backend/)"
API[FastAPI 应用]
AUTH[JWT 认证中间件]
WS[WebSocket 端点<br/>实时日志推送]
EXEC[TaskExecutor<br/>子进程管理]
QUEUE[TaskQueue<br/>队列管理]
SCHED[Scheduler<br/>定时调度]
end
subgraph "数据层"
PG_APP[(zqyy_app<br/>用户/队列/调度/历史)]
PG_ETL[(etl_feiqiu<br/>ODS/DWD/DWS)]
ENV[.env 文件]
end
subgraph "ETL Connector"
CLI[ETL CLI<br/>子进程]
end
FE -->|HTTP/WS| API
API --> AUTH
API --> WS
API --> EXEC
API --> QUEUE
API --> SCHED
EXEC -->|subprocess| CLI
CLI --> PG_ETL
QUEUE --> PG_APP
SCHED --> PG_APP
SCHED -->|到期触发| QUEUE
API -->|只读查询| PG_ETL
API -->|读写| PG_APP
API -->|读写| ENV
```
### 请求流程
1. 前端发起 HTTP 请求 → JWT 中间件验证令牌(提取 site_id→ 路由处理
2. 任务执行API 接收 TaskConfig含 site_id→ TaskQueue 入队 → TaskExecutor 取出执行 → 子进程调用 ETL_CLI`--store-id {site_id}`)→ stdout/stderr 通过 WebSocket 推送
3. 调度触发Scheduler 定时检查到期任务 → 自动入队 → 同上执行流程
### 多门店隔离设计
现有系统通过 `site_id`(即 `store_id`CLI 参数名为 `--store-id`)实现多门店数据隔离:
- ETL 数据库层:所有业务表包含 `site_id` 字段,`app` schema 通过 RLS 按 `current_setting('app.current_site_id')` 自动过滤
- ETL CLI 层:通过 `--store-id` 参数指定门店
Web 管理后台的隔离策略:
1. **用户-门店绑定**`admin_users` 表增加 `site_id` 字段,每个 Operator 绑定一个门店
2. **JWT 令牌携带 site_id**:登录时将 `site_id` 写入 JWT payload后续请求自动提取
3. **API 层自动注入**:所有涉及 ETL 操作的 API从 JWT 中提取 `site_id`,自动注入到 TaskConfig 和数据库查询中
4. **数据库查看器隔离**:查询 ETL 数据库时,设置 `SET LOCAL app.current_site_id = '{site_id}'`,利用 RLS 自动过滤
5. **队列和调度隔离**`task_queue``scheduled_tasks` 表增加 `site_id` 字段,查询时按 site_id 过滤
### 执行流程Flow配置设计
> 术语说明:**Connector**(数据源连接器)指对接的上游 SaaS 平台(如飞球),对应 `apps/etl/pipelines/{connector}/`**Flow**(执行流程)指 ETL 任务的处理链路描述数据从哪一层流到哪一层。CLI 参数 `--pipeline` 实际传递的是 Flow ID。
完整保留现有 7 种 Flow前端根据选择动态展示
| Flow ID | 显示名称 | 包含层 |
|---------|---------|--------|
| `api_ods` | API → ODS | ODS |
| `api_ods_dwd` | API → ODS → DWD | ODS, DWD |
| `api_full` | API → ODS → DWD → DWS汇总 → DWS指数 | ODS, DWD, DWS, INDEX |
| `ods_dwd` | ODS → DWD | DWD |
| `dwd_dws` | DWD → DWS汇总 | DWS |
| `dwd_dws_index` | DWD → DWS汇总 → DWS指数 | DWS, INDEX |
| `dwd_index` | DWD → DWS指数 | INDEX |
4 种处理模式:
- `increment_only`:仅增量处理
- `verify_only`:校验并修复(可选"校验前从 API 获取"
- `increment_verify`:增量 + 校验并修复
- `full_window`:用 API 返回数据的实际时间范围处理全部层,无需校验
时间窗口模式:
- `lookback`:回溯 + 冗余lookback_hours + overlap_seconds
- `custom`自定义时间范围window_start + window_end
- 窗口切分:不切分 / 按天1天/10天/30天
前端交互逻辑:
1. Operator 选择 Flow → 前端根据 Flow 包含的层,动态显示/隐藏 ODS 任务选择、DWD 表选择、DWS 任务选择
2. Operator 选择处理模式 → 前端根据模式显示/隐藏校验相关选项
3. Operator 选择时间窗口模式 → 前端切换回溯配置或自定义日期选择器
## 组件与接口
### 后端 API 路由
```
apps/backend/app/
├── main.py # FastAPI 入口(已有,扩展)
├── config.py # 配置加载(已有)
├── database.py # 数据库连接(已有,扩展)
├── auth/
│ ├── __init__.py
│ ├── jwt.py # JWT 令牌生成/验证
│ └── dependencies.py # FastAPI 依赖注入(当前用户)
├── routers/
│ ├── auth.py # POST /api/auth/login, POST /api/auth/refresh
│ ├── tasks.py # 任务注册表 & 配置 API
│ ├── execution.py # 任务执行 & 队列 API
│ ├── schedules.py # 调度任务 CRUD API
│ ├── env_config.py # 环境配置 API
│ ├── db_viewer.py # 数据库查看器 API
│ └── etl_status.py # ETL 状态 API
├── schemas/
│ ├── auth.py # 认证相关 Pydantic 模型
│ ├── tasks.py # 任务配置 Pydantic 模型
│ ├── execution.py # 执行记录 Pydantic 模型
│ ├── schedules.py # 调度配置 Pydantic 模型
│ └── db_viewer.py # 数据库查看器 Pydantic 模型
├── services/
│ ├── task_executor.py # 子进程管理,执行 ETL_CLI
│ ├── task_queue.py # 任务队列管理
│ ├── scheduler.py # 定时调度器
│ └── cli_builder.py # CLI 命令构建(从 gui/utils/cli_builder.py 迁移)
├── middleware/
│ └── auth.py # JWT 认证中间件
└── ws/
└── logs.py # WebSocket 日志推送端点
```
### 前端结构
```
apps/admin-web/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── index.html
├── src/
│ ├── main.tsx # 入口
│ ├── App.tsx # 根组件Layout + Router
│ ├── api/ # API 客户端
│ │ ├── client.ts # axios 实例JWT 拦截器)
│ │ ├── auth.ts
│ │ ├── tasks.ts
│ │ ├── execution.ts
│ │ ├── schedules.ts
│ │ ├── envConfig.ts
│ │ ├── dbViewer.ts
│ │ └── etlStatus.ts
│ ├── pages/
│ │ ├── Login.tsx
│ │ ├── TaskConfig.tsx # 任务配置
│ │ ├── TaskManager.tsx # 任务管理(队列 + 调度)
│ │ ├── EnvConfig.tsx # 环境配置
│ │ ├── DBViewer.tsx # 数据库查看器
│ │ ├── ETLStatus.tsx # ETL 状态
│ │ └── LogViewer.tsx # 日志查看器
│ ├── components/ # 可复用组件
│ │ ├── TaskSelector.tsx # 任务选择器(按业务域分组)
│ │ ├── DwdTableSelector.tsx
│ │ ├── TimeWindowForm.tsx
│ │ └── LogStream.tsx # WebSocket 日志流组件
│ ├── hooks/
│ │ ├── useAuth.ts
│ │ └── useWebSocket.ts
│ ├── store/ # 状态管理React Context 或 Zustand
│ │ └── authStore.ts
│ └── types/ # TypeScript 类型定义
│ └── index.ts
```
### 核心 API 端点
| 方法 | 路径 | 说明 | 需求 |
|------|------|------|------|
| POST | `/api/auth/login` | 用户登录,返回 JWT | 1 |
| POST | `/api/auth/refresh` | 刷新访问令牌 | 1 |
| GET | `/api/tasks/registry` | 获取任务注册表(按业务域分组) | 2 |
| GET | `/api/tasks/dwd-tables` | 获取 DWD 表定义(按业务域分组) | 2 |
| POST | `/api/tasks/validate` | 验证 TaskConfig | 2, 11 |
| GET | `/api/tasks/flows` | 获取执行流程列表7 种 Flow + 4 种处理模式) | 2 |
| POST | `/api/execution/run` | 提交任务执行 | 3 |
| GET | `/api/execution/queue` | 获取当前队列 | 4 |
| POST | `/api/execution/queue` | 添加任务到队列 | 4 |
| PUT | `/api/execution/queue/reorder` | 调整队列顺序 | 4 |
| DELETE | `/api/execution/queue/{id}` | 从队列删除任务 | 4 |
| POST | `/api/execution/{id}/cancel` | 取消执行中的任务 | 3 |
| GET | `/api/execution/history` | 获取执行历史 | 4 |
| GET | `/api/execution/{id}/logs` | 获取历史任务日志 | 9 |
| GET | `/api/schedules` | 获取调度任务列表 | 5 |
| POST | `/api/schedules` | 创建调度任务 | 5 |
| PUT | `/api/schedules/{id}` | 更新调度任务 | 5 |
| DELETE | `/api/schedules/{id}` | 删除调度任务 | 5 |
| PATCH | `/api/schedules/{id}/toggle` | 启用/禁用调度任务 | 5 |
| GET | `/api/env-config` | 获取环境配置 | 6 |
| PUT | `/api/env-config` | 更新环境配置 | 6 |
| GET | `/api/env-config/export` | 导出配置(去敏感值) | 6 |
| GET | `/api/db/schemas` | 获取 Schema 列表 | 7 |
| GET | `/api/db/schemas/{name}/tables` | 获取表列表和行数 | 7 |
| GET | `/api/db/tables/{schema}/{table}/columns` | 获取表列定义 | 7 |
| POST | `/api/db/query` | 执行只读 SQL 查询 | 7 |
| GET | `/api/etl-status/cursors` | 获取 ETL 游标状态 | 8 |
| GET | `/api/etl-status/recent-runs` | 获取最近执行记录 | 8 |
| WS | `/ws/logs/{execution_id}` | 实时日志 WebSocket | 9 |
### 关键服务组件
#### TaskExecutor任务执行器
```python
class TaskExecutor:
"""管理 ETL_CLI 子进程的生命周期"""
async def execute(self, config: TaskConfig, execution_id: str) -> None:
"""
以子进程方式调用 ETL_CLI。
- 使用 asyncio.create_subprocess_exec 启动子进程
- 逐行读取 stdout/stderr广播到 WebSocket 连接
- 记录退出码和执行时长到数据库
"""
...
async def cancel(self, execution_id: str) -> bool:
"""向子进程发送 SIGTERM等待退出后标记为已取消"""
...
```
#### TaskQueue任务队列
```python
class TaskQueue:
"""基于 PostgreSQL 的任务队列"""
async def enqueue(self, config: TaskConfig) -> str:
"""入队,返回队列任务 ID"""
...
async def dequeue(self) -> Optional[QueuedTask]:
"""取出队首待执行任务"""
...
async def reorder(self, task_id: str, new_position: int) -> None:
"""调整任务在队列中的位置"""
...
async def process_loop(self) -> None:
"""后台循环:队列非空且无运行中任务时,自动取出执行"""
...
```
#### Scheduler调度器
```python
class Scheduler:
"""基于 PostgreSQL 的定时调度器"""
async def check_and_enqueue(self) -> None:
"""检查到期的调度任务,将其 TaskConfig 加入队列"""
...
async def start(self) -> None:
"""启动后台定时检查循环(每 30 秒检查一次)"""
...
```
#### CLIBuilderCLI 命令构建器)
从现有 `gui/utils/cli_builder.py` 迁移,核心逻辑不变:
```python
class CLIBuilder:
"""将 TaskConfig 转换为 ETL_CLI 命令行参数列表"""
def build_command(self, config: TaskConfig, etl_project_path: str) -> list[str]:
"""
构建完整命令:
[python, -m, cli.main, --pipeline, {flow_id}, --tasks, ..., --store-id, {site_id}, ...]
注意CLI 参数名 --pipeline 传递的是 Flow ID
"""
...
```
## 数据模型
### 数据库表zqyy_app
#### admin_users 表
```sql
CREATE TABLE admin_users (
id SERIAL PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(256) NOT NULL,
display_name VARCHAR(128),
site_id BIGINT NOT NULL, -- 绑定的门店 ID
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_admin_users_site ON admin_users(site_id);
```
#### task_queue 表
```sql
CREATE TABLE task_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
site_id BIGINT NOT NULL, -- 门店隔离
config JSONB NOT NULL, -- 序列化的 TaskConfig
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending / running / success / failed / cancelled
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
started_at TIMESTAMPTZ,
finished_at TIMESTAMPTZ,
exit_code INTEGER,
error_message TEXT
);
CREATE INDEX idx_task_queue_status ON task_queue(status);
CREATE INDEX idx_task_queue_site_position ON task_queue(site_id, position) WHERE status = 'pending';
```
#### task_execution_log 表
```sql
CREATE TABLE task_execution_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
queue_id UUID REFERENCES task_queue(id),
site_id BIGINT NOT NULL, -- 门店隔离
task_codes TEXT[] NOT NULL,
status VARCHAR(20) NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ,
exit_code INTEGER,
duration_ms INTEGER,
command TEXT, -- 实际执行的 CLI 命令
output_log TEXT, -- stdout 完整日志
error_log TEXT, -- stderr 日志
summary JSONB, -- 执行摘要
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_execution_log_site_started ON task_execution_log(site_id, started_at DESC);
```
#### scheduled_tasks 表
```sql
CREATE TABLE scheduled_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
site_id BIGINT NOT NULL, -- 门店隔离
name VARCHAR(256) NOT NULL,
task_codes TEXT[] NOT NULL,
task_config JSONB NOT NULL, -- 序列化的 TaskConfig
schedule_config JSONB NOT NULL, -- 序列化的 ScheduleConfig
enabled BOOLEAN DEFAULT TRUE,
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
run_count INTEGER DEFAULT 0,
last_status VARCHAR(20),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_scheduled_tasks_site ON scheduled_tasks(site_id);
CREATE INDEX idx_scheduled_tasks_next_run ON scheduled_tasks(next_run_at)
WHERE enabled = TRUE;
```
### Pydantic 模型(后端 schemas
```python
# schemas/tasks.py
class TaskConfigSchema(BaseModel):
"""任务配置 — 前后端传输格式"""
tasks: list[str]
pipeline: str = "api_ods_dwd" # 执行流程 Flow ID7 种之一),对应 CLI --pipeline 参数
processing_mode: str = "increment_only" # 处理模式3 种之一)
pipeline_flow: str = "FULL" # 传统模式兼容(已弃用,保留向后兼容)
dry_run: bool = False
window_mode: str = "lookback" # lookback / custom
window_start: str | None = None
window_end: str | None = None
window_split: str | None = None # none / day
window_split_days: int | None = None # 1 / 10 / 30
lookback_hours: int = 24
overlap_seconds: int = 600
fetch_before_verify: bool = False
skip_ods_when_fetch_before_verify: bool = False
ods_use_local_json: bool = False
store_id: int | None = None # 门店 ID由后端从 JWT 注入,前端不传)
dwd_only_tables: list[str] | None = None # DWD 表级选择
extra_args: dict[str, Any] = {}
@model_validator(mode="after")
def validate_window(self) -> "TaskConfigSchema":
"""验证时间窗口:结束日期不早于开始日期"""
if self.window_start and self.window_end:
if self.window_end < self.window_start:
raise ValueError("window_end 不能早于 window_start")
return self
class PipelineDefinition(BaseModel):
"""执行流程Flow定义 — 注意:字段名保留 pipeline 以兼容 CLI 参数"""
id: str
name: str
layers: list[str]
class ProcessingModeDefinition(BaseModel):
"""处理模式定义"""
id: str
name: str
description: str
# schemas/schedules.py
class ScheduleConfigSchema(BaseModel):
"""调度配置"""
schedule_type: Literal["once", "interval", "daily", "weekly", "cron"]
interval_value: int = 1
interval_unit: Literal["minutes", "hours", "days"] = "hours"
daily_time: str = "04:00"
weekly_days: list[int] = [1]
weekly_time: str = "04:00"
cron_expression: str = "0 4 * * *"
enabled: bool = True
start_date: str | None = None
end_date: str | None = None
```
### TypeScript 类型(前端)
```typescript
// types/index.ts
interface TaskConfig {
tasks: string[];
pipeline: string; // 执行流程 Flow ID对应 CLI --pipeline
processing_mode: string; // 处理模式
pipeline_flow: string; // 传统模式兼容(已弃用)
dry_run: boolean;
window_mode: string; // lookback / custom
window_start: string | null;
window_end: string | null;
window_split: string | null; // none / day
window_split_days: number | null; // 1 / 10 / 30
lookback_hours: number;
overlap_seconds: number;
fetch_before_verify: boolean;
skip_ods_when_fetch_before_verify: boolean;
ods_use_local_json: boolean;
store_id: number | null; // 由后端注入
dwd_only_tables: string[] | null; // DWD 表级选择
extra_args: Record<string, unknown>;
}
interface PipelineDefinition {
id: string; // Flow ID字段名保留 pipeline 兼容 CLI
name: string;
layers: string[]; // 包含的层ODS / DWD / DWS / INDEX
}
interface ProcessingModeDefinition {
id: string;
name: string;
description: string;
}
interface TaskDefinition {
code: string;
name: string;
description: string;
domain: string;
requires_window: boolean;
is_ods: boolean;
is_dimension: boolean;
default_enabled: boolean;
}
interface ScheduleConfig {
schedule_type: "once" | "interval" | "daily" | "weekly" | "cron";
interval_value: number;
interval_unit: "minutes" | "hours" | "days";
daily_time: string;
weekly_days: number[];
weekly_time: string;
cron_expression: string;
enabled: boolean;
start_date: string | null;
end_date: string | null;
}
interface QueuedTask {
id: string;
site_id: number;
config: TaskConfig;
status: "pending" | "running" | "success" | "failed" | "cancelled";
position: number;
created_at: string;
started_at: string | null;
finished_at: string | null;
exit_code: number | null;
error_message: string | null;
}
interface ExecutionLog {
id: string;
site_id: number;
task_codes: string[];
status: string;
started_at: string;
finished_at: string | null;
exit_code: number | null;
duration_ms: number | null;
command: string;
summary: Record<string, unknown> | null;
}
```
## 正确性属性
*属性Property是指在系统所有有效执行中都应成立的特征或行为——本质上是对系统行为的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: TaskConfig 序列化往返一致性
*For any* 有效的 TaskConfigSchema 对象,将其序列化为 JSON 字符串后再反序列化,应产生与原始对象等价的结果。
**Validates: Requirements 11.1, 11.2, 11.3**
### Property 2: 无效凭据始终被拒绝
*For any* 不存在于 admin_users 表中的用户名/密码组合,登录接口应返回 401 状态码。
**Validates: Requirements 1.2**
### Property 3: 有效 JWT 令牌授权访问
*For any* 由系统签发且未过期的 JWT 令牌,携带该令牌访问受保护端点应返回非 401 状态码。
**Validates: Requirements 1.3**
### Property 4: 任务注册表按业务域正确分组
*For any* Task_Registry 中的任务集合API 返回的分组结果中,每个任务应出现在且仅出现在其所属业务域的分组中。
**Validates: Requirements 2.1**
### Property 5: Flow 层级过滤正确性
*For any* Flow 选择和任务列表,过滤后的结果应只包含与所选 Flow 包含的层兼容的任务,且不遗漏任何兼容任务。
**Validates: Requirements 2.2**
### Property 6: 时间窗口验证
*For any* 两个日期字符串,当 window_end 早于 window_start 时TaskConfigSchema 验证应失败;当 window_end 不早于 window_start 时,验证应通过。
**Validates: Requirements 2.3**
### Property 7: TaskConfig 到 CLI 命令转换完整性
*For any* 有效的 TaskConfigSchemaCLIBuilder 生成的命令参数列表应包含 TaskConfig 中所有非空字段对应的 CLI 参数。
**Validates: Requirements 2.5, 2.6**
### Property 8: 队列 CRUD 不变量
*For any* 任务队列状态,入队一个任务后队列长度增加 1 且新任务状态为 pending删除一个 pending 任务后队列长度减少 1 且该任务不再出现在队列中。
**Validates: Requirements 4.1, 4.4**
### Property 9: 队列出队顺序
*For any* 包含多个 pending 任务的队列dequeue 操作应返回 position 值最小的任务。
**Validates: Requirements 4.2**
### Property 10: 队列重排一致性
*For any* 队列和重排操作(将任务移动到新位置),重排后队列中任务的相对顺序应与请求一致。
**Validates: Requirements 4.3**
### Property 11: 执行历史排序与限制
*For any* 执行历史记录集合API 返回的结果应按 started_at 降序排列,且结果数量不超过请求的 limit 值。
**Validates: Requirements 4.5, 8.2**
### Property 12: 调度任务 CRUD 往返
*For any* 有效的 ScheduleConfigSchema创建调度任务后再查询该任务返回的调度配置应与创建时提交的配置等价。
**Validates: Requirements 5.1, 5.4**
### Property 13: 到期调度任务自动入队
*For any* enabled 为 true 且 next_run_at 早于当前时间的调度任务check_and_enqueue 执行后该任务的 TaskConfig 应出现在执行队列中。
**Validates: Requirements 5.2**
### Property 14: 调度任务启用/禁用状态
*For any* 调度任务,禁用后 next_run_at 应为 NULL重新启用后 next_run_at 应被重新计算为非 NULL 值(对于非一次性调度)。
**Validates: Requirements 5.3**
### Property 15: .env 解析与敏感值掩码
*For any* 包含敏感键PASSWORD、TOKEN、SECRET、DSN的 .env 文件内容API 返回的键值对列表中这些键的值应被掩码替换,不包含原始敏感值。
**Validates: Requirements 6.1, 6.3**
### Property 16: .env 写入往返一致性
*For any* 有效的键值对集合(不含注释和空行),写入 .env 文件后再读取解析,应得到与原始集合等价的键值对。
**Validates: Requirements 6.2**
### Property 17: SQL 写操作拦截
*For any* 包含 INSERT、UPDATE、DELETE、DROP 或 TRUNCATE 关键词(不区分大小写)的 SQL 语句,数据库查看器 API 应拒绝执行并返回错误。
**Validates: Requirements 7.5**
### Property 18: SQL 查询结果行数限制
*For any* SQL 查询执行结果,返回的行数应不超过 1000。
**Validates: Requirements 7.4**
### Property 19: 日志过滤正确性
*For any* 日志行集合和过滤关键词,过滤后的结果应只包含含有该关键词的日志行,且不遗漏任何匹配行。
**Validates: Requirements 9.2**
### Property 20: 门店隔离 — 队列和调度数据不跨站泄露
*For any* 两个不同 site_id 的 Operator一个 Operator 查询队列/调度/执行历史时,结果中不应包含另一个 site_id 的数据。
**Validates: Requirements 1.3(隐含的多门店隔离要求)**
### Property 21: Flow 层级与任务兼容性
*For any* Flow 类型和任务定义,当 Flow 包含的层不包含该任务所属层时,该任务不应出现在可选列表中;当 Flow 包含该任务所属层时,该任务应出现在可选列表中。
**Validates: Requirements 2.2**
## 错误处理
### 认证错误
- 无效凭据:返回 `401 Unauthorized`,响应体包含 `{"detail": "用户名或密码错误"}`
- 令牌过期:返回 `401 Unauthorized`,响应体包含 `{"detail": "令牌已过期"}`
- 令牌无效:返回 `401 Unauthorized`,响应体包含 `{"detail": "无效的令牌"}`
### 任务执行错误
- TaskConfig 验证失败:返回 `422 Unprocessable Entity`,响应体包含字段级错误详情
- ETL_CLI 子进程超时:记录错误日志,任务状态标记为 `failed`error_message 记录超时信息
- ETL_CLI 子进程异常退出:记录 exit_code 和 stderr 输出,任务状态标记为 `failed`
- 取消不存在或已完成的任务:返回 `404 Not Found``409 Conflict`
### 队列错误
- 删除非 pending 状态的任务:返回 `409 Conflict`,提示只能删除待执行任务
- 重排包含非 pending 任务:忽略非 pending 任务,只重排 pending 任务
### 数据库查看器错误
- SQL 写操作拦截:返回 `400 Bad Request`,提示只允许只读查询
- SQL 查询超时30 秒):终止查询,返回 `408 Request Timeout`
- SQL 语法错误:返回 `400 Bad Request`,包含 PostgreSQL 原始错误信息
### 环境配置错误
- .env 文件不存在:返回 `404 Not Found`
- 配置格式错误:返回 `422 Unprocessable Entity`,包含错误行号和描述
- 文件写入权限不足:返回 `500 Internal Server Error`,记录详细错误日志
### 通用错误
- 所有 API 错误响应统一格式:`{"detail": "错误描述", "code": "ERROR_CODE"}`
- 未捕获异常:返回 `500 Internal Server Error`,日志记录完整堆栈
## 测试策略
### 测试框架
- **后端单元测试 & 属性测试**pytest + hypothesis
- 路径:`apps/backend/tests/`
- 运行:`cd apps/backend && pytest tests/ -v`
- **前端单元测试**Vitest + React Testing Library
- 路径:`apps/admin-web/src/__tests__/`
- 运行:`cd apps/admin-web && pnpm test`
### 属性测试Property-Based Testing
使用 hypothesis 库,每个属性测试至少运行 100 次迭代。
每个属性测试必须用注释标注对应的设计文档属性编号:
```python
# Feature: admin-web-console, Property 1: TaskConfig 序列化往返一致性
@given(config=st.builds(TaskConfigSchema, ...))
def test_task_config_round_trip(config):
...
```
属性测试重点覆盖:
- Property 1TaskConfig 序列化往返(核心数据模型)
- Property 6时间窗口验证输入验证
- Property 7TaskConfig 到 CLI 命令转换(关键业务逻辑)
- Property 8-10队列 CRUD 不变量(状态管理)
- Property 15-16.env 解析与写入往返(配置管理)
- Property 17SQL 写操作拦截(安全关键)
- Property 19日志过滤数据过滤逻辑
### 单元测试
单元测试覆盖具体示例和边界条件:
- JWT 令牌生成/验证/过期
- 调度器 next_run 计算(各种调度类型)
- CLI 命令构建的具体场景
- API 端点的请求/响应格式
- 前端组件渲染和交互
### 集成测试
需要数据库环境的测试:
- 任务队列的完整生命周期
- 调度任务的创建/触发/执行
- 数据库查看器的 Schema 浏览和查询执行
- ETL 状态查询

View File

@@ -0,0 +1,149 @@
# 需求文档Web 管理后台admin-web-console
## 简介
将现有 PySide6 桌面 GUI`gui/`)替换为基于浏览器-服务器BS架构的 Web 管理后台。后端 API 在现有 FastAPI 骨架(`apps/backend/`)上扩展,前端部署在 `apps/admin-web/`。功能覆盖现有 GUI 的六大模块任务配置、任务管理队列与调度、环境配置、数据库查看器、ETL 状态监控、日志查看器。
## 术语表
- **Admin_Web**Web 管理后台前端应用,部署在 `apps/admin-web/`
- **Backend_API**FastAPI 后端服务,部署在 `apps/backend/`,为 Admin_Web 提供 RESTful API
- **ETL_CLI**:现有 ETL 命令行工具(`apps/etl/pipelines/feiqiu/cli/main.py`Backend_API 通过子进程调用
- **Connector**:数据源连接器,指对接的上游 SaaS 平台(如飞球),对应目录 `apps/etl/pipelines/{connector_name}/`。后续可扩展更多 Connector台账类、球房类、财务类管理平台
- **Flow**:执行流程,指 ETL 任务的处理链路,描述数据从哪一层流到哪一层(如 `api_ods_dwd` 表示 API → ODS → DWD。对应 CLI 参数 `--pipeline`
- **Task_Registry**:任务注册表,定义所有可用 ETL 任务及其业务域分组
- **Task_Config**:任务执行配置,包含 Flow 类型、时间窗口、任务列表等参数
- **Schedule_Store**:调度任务持久化存储(从本地 JSON 迁移至 PostgreSQL
- **Operator**:管理后台操作员,即门店运营人员
- **Site**:门店,通过 `site_id` 标识,是多门店数据隔离的基本单位
## 需求
### 需求 1用户认证与会话管理
**用户故事:** 作为 Operator我希望通过用户名密码登录管理后台以确保只有授权人员能操作 ETL 系统。
#### 验收标准
1. WHEN Operator 提交有效的用户名和密码, THE Backend_API SHALL 返回一个 JWT 访问令牌和刷新令牌
2. WHEN Operator 提交无效的凭据, THE Backend_API SHALL 返回 401 状态码和错误描述
3. WHILE Operator 持有有效的 JWT 令牌, THE Backend_API SHALL 允许访问受保护的 API 端点
4. WHEN JWT 访问令牌过期, THE Backend_API SHALL 返回 401 状态码Admin_Web SHALL 使用刷新令牌自动获取新的访问令牌
5. WHEN 刷新令牌也过期, THE Admin_Web SHALL 将 Operator 重定向到登录页面
### 需求 2ETL 任务配置
**用户故事:** 作为 Operator我希望在 Web 界面上选择并配置 ETL 任务的执行参数,以便灵活控制数据处理的运行方式。
#### 验收标准
1. WHEN Operator 打开任务配置页面, THE Admin_Web SHALL 从 Backend_API 获取 Task_Registry 中所有可用任务,并按业务域(会员、结算、助教、商品、台桌、团购、库存等)分组展示
2. WHEN Operator 选择执行流程 Flow如 api_ods_dwd、dwd_dws 等), THE Admin_Web SHALL 根据 Flow 包含的层ODS / DWD / DWS / INDEX仅显示与所选层兼容的任务
3. WHEN Operator 配置时间窗口参数(开始日期、结束日期、窗口分割策略), THE Admin_Web SHALL 验证结束日期不早于开始日期
4. WHEN Operator 选择 DWD 装载任务, THE Admin_Web SHALL 展示 DWD 表级选择界面,允许按业务域分组勾选目标表
5. WHEN Operator 切换高级选项dry-run、force-full、skip-quality 等), THE Admin_Web SHALL 将这些选项反映到最终的 Task_Config 中
6. WHEN Operator 提交任务配置, THE Backend_API SHALL 验证 Task_Config 的完整性,并将其转换为有效的 ETL_CLI 命令参数
### 需求 3ETL 任务执行
**用户故事:** 作为 Operator我希望通过 Web 界面触发 ETL 任务执行,并实时查看执行进度和结果。
#### 验收标准
1. WHEN Operator 提交一个有效的 Task_Config, THE Backend_API SHALL 以子进程方式调用 ETL_CLI 执行任务,并返回一个任务执行 ID
2. WHILE ETL_CLI 子进程正在运行, THE Backend_API SHALL 捕获 stdout 和 stderr 输出,并通过 API 端点提供实时日志流
3. WHEN ETL_CLI 子进程执行完毕, THE Backend_API SHALL 记录退出码、执行时长和输出摘要到任务历史
4. IF ETL_CLI 子进程执行超时或异常退出, THEN THE Backend_API SHALL 记录错误信息并将任务状态标记为失败
5. WHEN Operator 请求取消正在执行的任务, THE Backend_API SHALL 向 ETL_CLI 子进程发送终止信号并将任务状态标记为已取消
### 需求 4任务队列管理
**用户故事:** 作为 Operator我希望管理任务执行队列以便批量安排和控制任务的执行顺序。
#### 验收标准
1. WHEN Operator 将一个 Task_Config 添加到队列, THE Backend_API SHALL 创建一个状态为"待执行"的队列任务项并返回其 ID
2. WHEN 队列中存在待执行任务且当前无任务正在运行, THE Backend_API SHALL 按队列顺序自动取出下一个任务并开始执行
3. WHEN Operator 调整队列中任务的顺序, THE Backend_API SHALL 更新任务的执行优先级
4. WHEN Operator 从队列中删除一个待执行任务, THE Backend_API SHALL 将该任务从队列中移除
5. WHEN Operator 查看任务历史, THE Backend_API SHALL 返回按时间倒序排列的历史执行记录,包含任务编码、状态、开始时间、执行时长和退出码
### 需求 5调度任务管理
**用户故事:** 作为 Operator我希望创建和管理定时调度任务以便 ETL Connector 能按计划自动运行。
#### 验收标准
1. WHEN Operator 创建调度任务时, THE Backend_API SHALL 接受调度配置(一次性 / 固定间隔 / 每日 / 每周 / Cron 表达式)并持久化到 Schedule_Store
2. WHEN 调度任务到达预定执行时间, THE Backend_API SHALL 自动将对应的 Task_Config 加入执行队列
3. WHEN Operator 启用或禁用一个调度任务, THE Backend_API SHALL 更新该任务的启用状态并重新计算下次执行时间
4. WHEN Operator 编辑调度任务的配置, THE Backend_API SHALL 验证新配置的有效性并更新 Schedule_Store
5. WHEN Operator 删除一个调度任务, THE Backend_API SHALL 从 Schedule_Store 中移除该任务及其执行历史
6. WHEN Operator 查看调度任务列表, THE Backend_API SHALL 返回所有调度任务及其最近执行状态、下次执行时间和执行次数
### 需求 6环境配置管理
**用户故事:** 作为 Operator我希望通过 Web 界面查看和编辑 ETL 的 .env 配置文件,以便调整运行参数而无需直接操作服务器文件。
#### 验收标准
1. WHEN Operator 打开环境配置页面, THE Backend_API SHALL 读取当前 .env 文件内容并返回键值对列表(敏感值如密码和令牌以掩码形式展示)
2. WHEN Operator 修改配置项并提交, THE Backend_API SHALL 验证配置格式的正确性,写入 .env 文件,并返回更新结果
3. WHEN Operator 导出配置, THE Backend_API SHALL 生成一份去除敏感值的配置文件供下载
4. IF Operator 提交的配置包含格式错误的键值对, THEN THE Backend_API SHALL 返回具体的错误位置和描述,拒绝写入
### 需求 7数据库查看器
**用户故事:** 作为 Operator我希望通过 Web 界面查看数据库表结构和执行查询,以便快速检查 ETL 数据质量。
#### 验收标准
1. WHEN Operator 打开数据库查看器页面, THE Admin_Web SHALL 展示可用的 Schema 列表meta、ods、dwd、core、dws、app
2. WHEN Operator 选择一个 Schema, THE Backend_API SHALL 返回该 Schema 下所有表的名称和行数统计
3. WHEN Operator 选择一张表, THE Backend_API SHALL 返回该表的列定义(列名、数据类型、是否可空、默认值)
4. WHEN Operator 提交一条 SQL 查询, THE Backend_API SHALL 在只读事务中执行该查询,限制返回行数上限为 1000 行,并返回结果集
5. IF Operator 提交的 SQL 包含写操作INSERT / UPDATE / DELETE / DROP / TRUNCATE, THEN THE Backend_API SHALL 拒绝执行并返回错误提示
6. IF SQL 查询执行超过 30 秒, THEN THE Backend_API SHALL 终止查询并返回超时错误
### 需求 8ETL 状态监控
**用户故事:** 作为 Operator我希望在 Web 界面上查看 ETL Connector 的运行状态和数据游标信息,以便及时发现异常。
#### 验收标准
1. WHEN Operator 打开 ETL 状态页面, THE Backend_API SHALL 返回各 ODS 表的最新数据游标(最后抓取时间、记录数)
2. WHEN Operator 查看最近执行记录, THE Backend_API SHALL 返回最近 50 条任务执行记录,包含任务名称、状态、开始时间、执行时长
3. WHEN Operator 刷新状态页面, THE Backend_API SHALL 返回最新的游标和执行状态数据
### 需求 9实时日志查看
**用户故事:** 作为 Operator我希望在 Web 界面上实时查看 ETL 任务的执行日志,以便监控任务进度和排查问题。
#### 验收标准
1. WHILE 一个 ETL 任务正在执行, THE Admin_Web SHALL 通过 WebSocket 或 SSE 连接实时展示该任务的日志输出
2. WHEN Operator 在日志查看器中输入过滤关键词, THE Admin_Web SHALL 仅展示包含该关键词的日志行
3. WHEN 任务执行完毕, THE Admin_Web SHALL 保留完整日志内容供 Operator 回顾
4. WHEN Operator 查看历史任务的日志, THE Backend_API SHALL 返回该任务的完整日志记录
### 需求 10响应式布局与导航
**用户故事:** 作为 Operator我希望管理后台具有清晰的导航结构和响应式布局以便在不同设备上高效操作。
#### 验收标准
1. THE Admin_Web SHALL 提供侧边栏导航包含六个功能模块入口任务配置、任务管理、环境配置、数据库、ETL 状态、日志
2. WHEN Operator 点击导航项, THE Admin_Web SHALL 切换到对应的功能模块页面,且不触发整页刷新
3. THE Admin_Web SHALL 在状态栏区域展示当前数据库连接状态和任务执行状态
4. WHILE 有任务正在执行, THE Admin_Web SHALL 在导航栏或状态栏显示执行中的视觉指示
### 需求 11Task_Config 序列化与反序列化
**用户故事:** 作为 Operator我希望任务配置能在前后端之间正确传输和持久化以确保配置不丢失。
#### 验收标准
1. THE Backend_API SHALL 将 Task_Config 序列化为 JSON 格式进行传输和存储
2. WHEN Backend_API 接收到 JSON 格式的 Task_Config, THE Backend_API SHALL 反序列化为内部 Task_Config 对象并验证所有必填字段
3. FOR ALL 有效的 Task_Config 对象, 序列化后再反序列化 SHALL 产生与原始对象等价的结果(往返一致性)
4. IF 反序列化时遇到缺失或类型错误的字段, THEN THE Backend_API SHALL 返回包含具体字段名和错误原因的验证错误

View File

@@ -0,0 +1,292 @@
# 实现计划Web 管理后台admin-web-console
## 概述
将现有 PySide6 桌面 GUI 替换为 BS 架构的 Web 管理后台。后端在 `apps/backend/` 上扩展 FastAPI API前端在 `apps/admin-web/` 下使用 React + Vite + Ant Design 构建。实现按"后端基础设施 → 核心 API → 前端骨架 → 功能模块逐个对接"的顺序推进。
## 任务
- [x] 1. 后端基础设施搭建
- [x] 1.1 创建数据库迁移脚本,在 `zqyy_app` 库中创建 4 张新表admin_users、task_queue、task_execution_log、scheduled_tasks所有表包含 site_id 字段
- 迁移脚本放在 `db/zqyy_app/migrations/`,日期前缀命名
- 包含索引创建site_id 相关的复合索引)
- 包含种子数据:插入一个默认管理员账号
- _Requirements: 1.1, 4.1, 5.1_
- [x] 1.2 实现 JWT 认证模块(`apps/backend/app/auth/`
- `jwt.py`JWT 令牌生成access_token + refresh_token、验证、解码payload 包含 user_id 和 site_id
- `dependencies.py`FastAPI 依赖注入函数 `get_current_user`,从 JWT 提取用户信息和 site_id
- 新增依赖:`python-jose[cryptography]``passlib[bcrypt]``apps/backend/pyproject.toml`
- _Requirements: 1.1, 1.2, 1.3, 1.4_
- [x] 1.3 实现认证路由(`apps/backend/app/routers/auth.py`
- `POST /api/auth/login`:验证用户名密码,返回 JWT 令牌对
- `POST /api/auth/refresh`:用刷新令牌换取新的访问令牌
- Pydantic schemas`LoginRequest``TokenResponse`
- _Requirements: 1.1, 1.2, 1.4_
- [x] 1.4 编写认证模块属性测试
- **Property 2: 无效凭据始终被拒绝**
- **Property 3: 有效 JWT 令牌授权访问**
- **Validates: Requirements 1.2, 1.3**
- [x] 1.5 扩展 `apps/backend/app/database.py`,新增 ETL 数据库只读连接函数
- `get_etl_readonly_connection(site_id)`:连接 ETL 数据库,设置 `SET LOCAL app.current_site_id`
- 配置项从 .env 读取 ETL 数据库连接参数
- _Requirements: 7.4, 7.5_
- [x] 1.6 在 `apps/backend/app/main.py` 中注册所有路由和中间件,配置 CORS 允许前端开发服务器访问
- _Requirements: 10.2_
- [x] 2. 检查点 — 确保认证模块测试通过
- 确保所有测试通过,如有问题请向用户确认。
- [x] 3. 任务配置与执行 API
- [x] 3.1 迁移 CLIBuilder 到后端(`apps/backend/app/services/cli_builder.py`
-`gui/utils/cli_builder.py` 迁移核心逻辑
- 适配新的 TaskConfigSchema自动注入 `--store-id` 参数
- 支持 7 种 Flow 和 4 种处理模式
- _Requirements: 2.6_
- [x] 3.2 实现任务注册表 API`apps/backend/app/routers/tasks.py`
- `GET /api/tasks/registry`:返回按业务域分组的任务列表
- `GET /api/tasks/dwd-tables`:返回按业务域分组的 DWD 表定义
- `GET /api/tasks/flows`:返回 7 种 Flow 定义和 4 种处理模式定义
- `POST /api/tasks/validate`:验证 TaskConfig 并返回生成的 CLI 命令预览
- Pydantic schemas 在 `apps/backend/app/schemas/tasks.py`
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 11.1, 11.2_
- [x] 3.3 编写 TaskConfig 属性测试
- **Property 1: TaskConfig 序列化往返一致性**
- **Property 6: 时间窗口验证**
- **Property 7: TaskConfig 到 CLI 命令转换完整性**
- **Validates: Requirements 2.3, 2.5, 2.6, 11.1, 11.2, 11.3**
- [x] 3.4 实现 TaskExecutor 服务(`apps/backend/app/services/task_executor.py`
- 使用 `asyncio.create_subprocess_exec` 启动 ETL_CLI 子进程
- 逐行读取 stdout/stderr存储到内存缓冲区并广播到 WebSocket
- 记录退出码、执行时长到 task_execution_log 表
- 支持取消(发送 SIGTERM
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
- [x] 3.5 实现 TaskQueue 服务(`apps/backend/app/services/task_queue.py`
- `enqueue(config, site_id)`:入队,自动分配 position
- `dequeue(site_id)`:取出 position 最小的 pending 任务
- `reorder(task_id, new_position, site_id)`:调整顺序
- `delete(task_id, site_id)`:删除 pending 任务
- `process_loop()`:后台协程,自动取出并执行
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 3.6 实现执行与队列路由(`apps/backend/app/routers/execution.py`
- `POST /api/execution/run`:直接执行任务
- `GET /api/execution/queue`:获取当前队列(按 site_id 过滤)
- `POST /api/execution/queue`:添加到队列
- `PUT /api/execution/queue/reorder`:重排
- `DELETE /api/execution/queue/{id}`:删除
- `POST /api/execution/{id}/cancel`:取消
- `GET /api/execution/history`:执行历史(按 site_id 过滤limit 参数)
- `GET /api/execution/{id}/logs`:获取历史日志
- _Requirements: 3.1, 3.5, 4.1, 4.2, 4.3, 4.4, 4.5_
- [x] 3.7 编写队列属性测试
- **Property 8: 队列 CRUD 不变量**
- **Property 9: 队列出队顺序**
- **Property 10: 队列重排一致性**
- **Property 11: 执行历史排序与限制**
- **Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 8.2**
- [x] 4. 检查点 — 确保任务配置与执行 API 测试通过
- 确保所有测试通过,如有问题请向用户确认。
- [x] 5. 调度与辅助 API
- [x] 5.1 实现 Scheduler 服务(`apps/backend/app/services/scheduler.py`
- `check_and_enqueue()`:查询 enabled=true 且 next_run_at <= now 的调度任务,将其 TaskConfig 入队
- `start()`:启动后台 asyncio 循环,每 30 秒检查一次
- 在 FastAPI lifespan 中启动/停止
- _Requirements: 5.2_
- [x] 5.2 实现调度路由(`apps/backend/app/routers/schedules.py`
- `GET /api/schedules`:列表(按 site_id 过滤)
- `POST /api/schedules`:创建
- `PUT /api/schedules/{id}`:更新
- `DELETE /api/schedules/{id}`:删除
- `PATCH /api/schedules/{id}/toggle`:启用/禁用
- Pydantic schemas 在 `apps/backend/app/schemas/schedules.py`
- _Requirements: 5.1, 5.3, 5.4, 5.5, 5.6_
- [x] 5.3 编写调度属性测试
- **Property 12: 调度任务 CRUD 往返**
- **Property 13: 到期调度任务自动入队**
- **Property 14: 调度任务启用/禁用状态**
- **Validates: Requirements 5.1, 5.2, 5.3, 5.4**
- [x] 5.4 实现环境配置路由(`apps/backend/app/routers/env_config.py`
- `GET /api/env-config`:读取 .env敏感值掩码
- `PUT /api/env-config`:验证并写入 .env
- `GET /api/env-config/export`:导出去敏感值的配置文件
- 敏感键列表PASSWORD、TOKEN、SECRET、DSN
- _Requirements: 6.1, 6.2, 6.3, 6.4_
- [x] 5.5 编写环境配置属性测试
- **Property 15: .env 解析与敏感值掩码**
- **Property 16: .env 写入往返一致性**
- **Validates: Requirements 6.1, 6.2, 6.3**
- [x] 5.6 实现数据库查看器路由(`apps/backend/app/routers/db_viewer.py`
- `GET /api/db/schemas`:返回 Schema 列表
- `GET /api/db/schemas/{name}/tables`:返回表列表和行数
- `GET /api/db/tables/{schema}/{table}/columns`:返回列定义
- `POST /api/db/query`:只读 SQL 执行写操作拦截、1000 行限制、30 秒超时)
- 使用 `get_etl_readonly_connection(site_id)` 确保 RLS 隔离
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_
- [x] 5.7 编写数据库查看器属性测试
- **Property 17: SQL 写操作拦截**
- **Property 18: SQL 查询结果行数限制**
- **Validates: Requirements 7.4, 7.5**
- [x] 5.8 实现 ETL 状态路由(`apps/backend/app/routers/etl_status.py`
- `GET /api/etl-status/cursors`:查询 etl_admin.etl_cursor 表,返回各任务游标
- `GET /api/etl-status/recent-runs`:查询 task_execution_log 表,返回最近 50 条记录
- _Requirements: 8.1, 8.2, 8.3_
- [x] 5.9 实现 WebSocket 日志推送(`apps/backend/app/ws/logs.py`
- `WS /ws/logs/{execution_id}`:实时推送任务执行日志
- TaskExecutor 执行时广播日志行到已连接的 WebSocket 客户端
- _Requirements: 9.1, 9.4_
- [x] 6. 检查点 — 确保所有后端 API 测试通过
- 确保所有测试通过,如有问题请向用户确认。
- [x] 7. 前端项目初始化
- [x] 7.1 在 `apps/admin-web/` 下初始化 React + Vite + TypeScript 项目
- `pnpm create vite . --template react-ts`
- 安装核心依赖:`antd``@ant-design/icons``react-router-dom``axios``zustand`
- 配置 `vite.config.ts`API 代理到后端 `http://localhost:8000`
- 配置中文 localeantd ConfigProvider
- _Requirements: 10.1, 10.2_
- [x] 7.2 实现 API 客户端(`src/api/client.ts`
- 创建 axios 实例baseURL 指向 `/api`
- 请求拦截器:自动附加 JWT Authorization header
- 响应拦截器401 时尝试刷新令牌,刷新失败跳转登录页
- _Requirements: 1.3, 1.4, 1.5_
- [x] 7.3 实现认证状态管理(`src/store/authStore.ts`)和登录页(`src/pages/Login.tsx`
- Zustand store存储 token、user info、site_id
- 登录页Ant Design Form用户名 + 密码
- 登录成功后存储令牌到 localStorage 并跳转首页
- _Requirements: 1.1, 1.2_
- [x] 7.4 实现主布局(`src/App.tsx`)和路由配置
- Ant Design LayoutSider侧边栏导航+ Content + Footer状态栏
- react-router-dom6 个功能页面路由 + 登录页路由
- 路由守卫:未登录重定向到登录页
- 侧边栏导航项任务配置、任务管理、环境配置、数据库、ETL 状态、日志
- _Requirements: 10.1, 10.2, 10.3_
- [x] 8. 前端功能页面 — 任务配置
- [x] 8.1 实现任务配置页面(`src/pages/TaskConfig.tsx`
- Flow 选择器Radio Group7 种 Flow选择后动态显示/隐藏任务区域
- 处理模式选择器3 种模式 + 校验附加选项
- 时间窗口配置回溯模式lookback_hours + overlap_seconds/ 自定义模式DatePicker
- 窗口切分选项:不切分 / 按天1/10/30
- 高级选项折叠面板dry-run、force-full 等 Checkbox
- _Requirements: 2.2, 2.3, 2.5_
- [x] 8.2 实现 TaskSelector 组件(`src/components/TaskSelector.tsx`
-`/api/tasks/registry` 获取任务列表
- 按业务域分组展示Collapse + Checkbox Group
- 根据当前 Flow 包含的层过滤可见任务
- 全选/反选功能
- _Requirements: 2.1, 2.2_
- [x] 8.3 实现 DwdTableSelector 组件(`src/components/DwdTableSelector.tsx`
-`/api/tasks/dwd-tables` 获取 DWD 表定义
- 按业务域分组展示Collapse + Checkbox Group
- 仅在 Flow 包含 DWD 层时显示
- _Requirements: 2.4_
- [x] 8.4 实现任务提交和 CLI 命令预览
- 提交前调用 `/api/tasks/validate` 预览生成的 CLI 命令
- 提交到队列(`/api/execution/queue`)或直接执行(`/api/execution/run`
- 提交成功后跳转到任务管理页面
- _Requirements: 2.6, 3.1, 4.1_
- [x] 8.5 编写 Flow 层级过滤前端单元测试
- **Property 21: Flow 层级与任务兼容性**
- 使用 Vitest 测试过滤逻辑函数
- **Validates: Requirements 2.2**
- [x] 9. 前端功能页面 — 任务管理
- [x] 9.1 实现任务管理页面(`src/pages/TaskManager.tsx`
- Ant Design Tabs队列 + 调度 + 历史
- 队列 TabTable 展示当前队列,支持拖拽排序、删除、取消
- 历史 TabTable 展示执行历史,点击行查看详情和日志
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
- [x] 9.2 实现调度管理 Tab
- 调度任务列表 Table名称、调度描述、启用状态 Switch、下次执行时间、执行次数
- 创建/编辑调度任务 Modal任务选择 + 调度配置(类型、间隔、时间等)
- 删除确认
- _Requirements: 5.1, 5.3, 5.4, 5.5, 5.6_
- [x] 9.3 实现状态栏任务执行指示
- 在 Layout Footer 或 Sider 底部显示当前执行状态
- 轮询 `/api/execution/queue` 检查是否有 running 状态的任务
- 有任务执行时显示 Spin 动画和任务名称
- _Requirements: 10.3, 10.4_
- [x] 10. 前端功能页面 — 辅助模块
- [x] 10.1 实现环境配置页面(`src/pages/EnvConfig.tsx`
- 键值对编辑表格Ant Design Tableeditable cells
- 敏感值显示为 `****`,编辑时可输入新值
- 保存按钮调用 `PUT /api/env-config`
- 导出按钮调用 `GET /api/env-config/export` 下载文件
- _Requirements: 6.1, 6.2, 6.3, 6.4_
- [x] 10.2 实现数据库查看器页面(`src/pages/DBViewer.tsx`
- 左侧 TreeSchema → Table 层级浏览
- 右侧上方SQL 编辑器Ant Design Input.TextArea 或集成 CodeMirror
- 右侧下方:查询结果 Table
- 选择表时自动展示列定义
- 执行查询按钮,结果分页展示
- _Requirements: 7.1, 7.2, 7.3, 7.4_
- [x] 10.3 实现 ETL 状态页面(`src/pages/ETLStatus.tsx`
- 游标状态 Table任务编码、最后抓取时间、记录数
- 最近执行记录 Table任务名称、状态 Tag、开始时间、执行时长
- 刷新按钮
- _Requirements: 8.1, 8.2, 8.3_
- [x] 10.4 实现日志查看器页面(`src/pages/LogViewer.tsx`)和 LogStream 组件
- WebSocket 连接 `/ws/logs/{execution_id}`,实时追加日志行
- 日志过滤输入框:按关键词过滤显示
- 自动滚动到底部,可手动暂停滚动
- 历史日志:从 `/api/execution/{id}/logs` 加载
- _Requirements: 9.1, 9.2, 9.3, 9.4_
- [x] 10.5 编写日志过滤前端单元测试
- **Property 19: 日志过滤正确性**
- 使用 Vitest 测试过滤函数
- **Validates: Requirements 9.2**
- [x] 11. 门店隔离集成验证
- [x] 11.1 编写门店隔离属性测试
- **Property 20: 门店隔离 — 队列和调度数据不跨站泄露**
- 使用 pytest + hypothesis 生成随机 site_id 对,验证数据隔离
- **Validates: Requirements 1.3**
- [x] 11.2 编写任务注册表分组属性测试
- **Property 4: 任务注册表按业务域正确分组**
- **Validates: Requirements 2.1**
- [x] 12. 最终检查点 — 确保所有测试通过 ✅
- 后端 302 passed / 0 failed前端 33 passed / 0 failed全部通过。
## 说明
- 标记 `*` 的子任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯
- 检查点用于阶段性验证,确保增量正确
- 属性测试验证通用正确性属性,单元测试覆盖具体示例和边界条件

View File

@@ -0,0 +1,224 @@
# 设计文档助教废除Abolish全链路清理
## 概述
本设计描述如何安全地从 ETL 全链路中移除"助教废除"独立数据链路。
核心思路:**删除独立的废除链路API → ODS → DWD保留服务记录中已有的 `is_trash` 字段作为唯一废除判断源。**
清理范围覆盖:
- ETL 任务定义ODS 任务 + 注册表)
- DWD 加载映射FACT_MAPPINGS + TABLE_MAP
- DWS 聚合逻辑(死代码移除)
- DWD 验证器配置
- 数据库 DDL 和迁移脚本
- 属性测试
- 运维脚本
## 架构
### 清理前数据流
```mermaid
graph LR
API_Abolish["/AssistantPerformance/GetAbolitionAssistant"] --> ODS_ACR["ods.assistant_cancellation_records"]
ODS_ACR --> DWD_ATE["dwd.dwd_assistant_trash_event"]
ODS_ACR --> DWD_ATE_EX["dwd.dwd_assistant_trash_event_ex"]
DWD_ATE --> |"_extract_trash_records (死代码)"| DWS_DAILY["dws.dws_assistant_daily_detail"]
API_Service["/AssistantPerformance/GetAssistantServiceRecords"] --> ODS_ASR["ods.assistant_service_records"]
ODS_ASR --> DWD_SL_EX["dwd.dwd_assistant_service_log_ex"]
DWD_SL_EX --> |"is_trash 字段 (实际使用)"| DWS_DAILY
style API_Abolish fill:#f99,stroke:#c00
style ODS_ACR fill:#f99,stroke:#c00
style DWD_ATE fill:#f99,stroke:#c00
style DWD_ATE_EX fill:#f99,stroke:#c00
```
### 清理后数据流
```mermaid
graph LR
API_Service["/AssistantPerformance/GetAssistantServiceRecords"] --> ODS_ASR["ods.assistant_service_records"]
ODS_ASR --> DWD_SL_EX["dwd.dwd_assistant_service_log_ex"]
DWD_SL_EX --> |"is_trash 字段"| DWS_DAILY["dws.dws_assistant_daily_detail"]
style API_Service fill:#9f9,stroke:#090
style DWD_SL_EX fill:#9f9,stroke:#090
```
## 组件与接口
### 需要修改的文件清单
| 层级 | 文件 | 操作 | 需求 |
|------|------|------|------|
| ODS 任务 | `tasks/ods/ods_tasks.py` | 删除 `OdsTaskSpec` 条目 + 从默认序列移除 | 1.2, 1.3 |
| 任务注册 | `orchestration/task_registry.py` | 删除 `ODS_ASSISTANT_ABOLISH` 注册 | 1.1 |
| DWD 加载 | `tasks/dwd/dwd_load_task.py` | 删除 FACT_MAPPINGS 和 TABLE_MAP 条目 | 2.12.4 |
| DWS 日度 | `tasks/dws/assistant_daily_task.py` | 删除 `_extract_trash_records``_build_trash_index`,简化 `_aggregate_by_assistant_date` 签名 | 3.13.4 |
| DWD 验证 | `tasks/verification/dwd_verifier.py` | 删除废除表的 ID 和时间字段映射 | 4.14.4 |
| DDL | `db/etl_feiqiu/schemas/dwd.sql` | 删除 `dwd_assistant_trash_event` / `_ex` 的 CREATE TABLE + COMMENT | 6.16.2 |
| DDL | `db/etl_feiqiu/schemas/ods.sql` | 删除 `assistant_cancellation_records` 的 CREATE TABLE + COMMENT | 6.3 |
| DDL | `db/etl_feiqiu/schemas/schema_dwd_doc.sql` | 删除废除表的 CREATE TABLE + COMMENT | 6.4 |
| DDL | `db/etl_feiqiu/schemas/schema_ODS_doc.sql` | 删除废除表的 CREATE TABLE + COMMENT | 6.5 |
| DDL | `db/etl_feiqiu/schemas/dws.sql` | 更新 `dws_assistant_daily_detail` 注释 | 6.6 |
| DDL | `db/etl_feiqiu/schemas/schema_dws.sql` | 更新 `dws_assistant_daily_detail` 注释 | 6.7 |
| 迁移 | `db/etl_feiqiu/migrations/` | 新建 DROP TABLE 迁移脚本 | 5.15.5 |
| 属性测试 | `tests/test_property_1_fact_mappings.py` | 删除 `_REQ3_EXPECTED` 和相关引用 | 7.17.3 |
| 运维 | `scripts/ops/dataflow_analyzer.py` | 删除 `ODS_ASSISTANT_ABOLISH` spec 条目 | 8.1 |
| 运维 | `scripts/ops/gen_full_dataflow_doc.py` | 删除 `ODS_ASSISTANT_ABOLISH` spec 条目 | 8.1 |
| 运维 | `scripts/ops/etl_consistency_check.py` | 删除废除相关映射 | 8.18.2 |
| 运维 | `scripts/ops/blackbox_test_report.py` | 删除废除相关映射 | 8.18.4 |
| 运维 | `scripts/ops/field_audit.py` | 删除废除表审计条目 | 8.38.4 |
| 运维 | `scripts/ops/gen_field_review_doc.py` | 删除废除表字段定义 | 8.38.4 |
| 运维 | `scripts/ops/gen_api_field_mapping.py` | 从 ODS_TABLES 列表移除 | 8.3 |
| 运维 | `scripts/ops/export_dwd_field_review.py` | 删除废除表条目 | 8.4 |
| 运维 | `scripts/ops/check_ods_latest_indexes.py` | 删除废除表索引检查 | 8.3 |
### 不需要修改的文件(确认安全)
| 文件 | 原因 |
|------|------|
| `dwd_assistant_service_log_ex` 表 DDL | 保留 `is_trash` 等字段(需求 9 |
| `ods.assistant_service_records` 表 DDL | 保留 `is_trash` 等字段(需求 9 |
| `assistant_monthly_task.py` | 仅消费 `dws_assistant_daily_detail``trashed_seconds`/`trashed_count`,不直接引用废除表 |
| `assistant_salary_task.py` | 仅消费 `dws_assistant_monthly_summary`,不直接引用废除表 |
## 数据模型
### 被删除的表
```sql
-- ODS 层
ods.assistant_cancellation_records -- 78 条记录
-- DWD 层
dwd.dwd_assistant_trash_event -- 主表
dwd.dwd_assistant_trash_event_ex -- 扩展表
```
### 保留的废除相关字段
```sql
-- ods.assistant_service_records 中(保留)
is_trash INT -- 废除标记
trash_reason TEXT -- 废除原因
trash_applicant_id BIGINT -- 废除申请人 ID
trash_applicant_name TEXT -- 废除申请人姓名
-- dwd.dwd_assistant_service_log_ex 中(保留)
is_trash INTEGER -- 废除标记
trash_applicant_id BIGINT -- 废除申请人 ID
trash_applicant_name VARCHAR(64) -- 废除申请人姓名
trash_reason VARCHAR(255) -- 废除原因
```
### DWS 层字段(保留,数据来源变更说明)
```sql
-- dws.dws_assistant_daily_detail保留注释需更新
trashed_seconds INTEGER -- 数据来源dwd_assistant_service_log_ex.is_trash + income_seconds
trashed_count INTEGER -- 数据来源dwd_assistant_service_log_ex.is_trash 计数
-- dws.dws_assistant_monthly_summary保留
trashed_hours NUMERIC(10,2) -- 来自 daily_detail.trashed_seconds 汇总
```
## DWS 代码重构细节
### assistant_daily_task.py 变更
**删除方法:**
- `_extract_trash_records()` — 查询 `dwd.dwd_assistant_trash_event` 的 SQL已无消费者
- `_build_trash_index()` — 构建废除索引,已不参与判断逻辑
**修改方法:**
- `extract()` — 移除对 `_extract_trash_records` 的调用,移除 `trash_records` 变量
- `transform()` 或调用 `_aggregate_by_assistant_date` 的地方 — 移除 `trash_index` 参数传递
- `_aggregate_by_assistant_date()` — 从签名中移除 `trash_index` 参数;`is_trash` 判断逻辑保持不变
**不变逻辑:**
```python
# 这段逻辑保持不变——通过 is_trash 字段判断废除
is_trashed = bool(record.get('is_trash', 0))
if is_trashed:
agg['trashed_seconds'] += income_seconds
agg['trashed_count'] += 1
```
## 正确性属性
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1废除聚合逻辑正确性is_trash 驱动)
*对任意*服务记录集合,其中每条记录包含 `is_trash` 标记和 `income_seconds` 值,聚合后:
- `trashed_seconds` 应等于所有 `is_trash=1` 记录的 `income_seconds` 之和
- `trashed_count` 应等于所有 `is_trash=1` 记录的数量
- `total_service_count` 应等于所有 `is_trash=0` 记录的数量
- `total_seconds` 应等于所有 `is_trash=0` 记录的 `income_seconds` 之和
**Validates: Requirements 3.4, 9.3**
### Property 2FACT_MAPPINGS 一致性(已有属性测试的回归验证)
*对任意* FACT_MAPPINGS 中的表名,该表名应在 TABLE_MAP 中有对应的 ODS 源表映射,且映射的每个 DWD 列名应为合法的 SQL 标识符。
**Validates: Requirements 2.12.4, 7.3**
> 注:此属性已由 `tests/test_property_1_fact_mappings.py` 实现。清理后需确保该测试仍然通过。
## 错误处理
### 迁移脚本安全性
- 所有 `DROP TABLE` 语句使用 `IF EXISTS`,确保幂等执行
- 迁移脚本在单个事务中执行,失败时自动回滚
- 迁移脚本包含注释说明移除原因,便于审计追溯
### 代码删除安全性
- 删除 `_extract_trash_records``_build_trash_index` 前,确认无其他调用者
- `_aggregate_by_assistant_date` 移除 `trash_index` 参数后,确认所有调用点已同步更新
- 保留 `is_trash` 判断逻辑不变,确保废除统计功能不受影响
### 回滚策略
- DDL 变更通过迁移脚本管理可通过反向迁移CREATE TABLE回滚
- 代码变更通过 Git 版本控制回滚
- ODS 表数据在删除前可选择性备份(数据量小,仅 78 条)
## 测试策略
### 属性测试
- 使用 `hypothesis` 库进行属性测试
- 每个属性测试至少运行 100 次迭代
- 每个测试用注释标注对应的设计属性编号
**Property 1 测试方案:**
- 生成随机服务记录列表(包含随机 `is_trash` 标记和 `income_seconds` 值)
- 调用 `_aggregate_by_assistant_date` 方法
- 验证 `trashed_seconds`/`trashed_count` 与手动计算的期望值一致
- 标签:`Feature: assistant-abolish-cleanup, Property 1: 废除聚合逻辑正确性`
**Property 2 测试方案:**
- 已由 `tests/test_property_1_fact_mappings.py` 覆盖
- 清理后运行 `pytest tests/ -v` 确认无回归
- 标签:`Feature: assistant-abolish-cleanup, Property 2: FACT_MAPPINGS 一致性`
### 单元测试
- 验证 `AssistantDailyTask` 不再有 `_extract_trash_records``_build_trash_index` 方法
- 验证 `_aggregate_by_assistant_date` 签名不包含 `trash_index` 参数
- 验证 FACT_MAPPINGS 不包含废除表条目
- 验证 TABLE_MAP 不包含废除表映射
- 验证 DwdVerifier 配置不包含废除表
### 集成验证
- 运行现有属性测试套件:`pytest tests/ -v`
- 运行 ETL 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit`
- 确认所有测试通过,无回归

View File

@@ -0,0 +1,127 @@
# 需求文档助教废除Abolish全链路清理
## 简介
上游 SaaS 系统提供了一个独立的"助教废除记录"API`/AssistantPerformance/GetAbolitionAssistant`
ETL 系统为此建立了完整的 ODS → DWD → DWS 数据链路。但经排查发现:
1. **废除表 `dwd_assistant_trash_event` 无法与服务记录表 `dwd_assistant_service_log` 做 1:1 关联**——废除表没有 `assistant_service_id` 外键,两个 ID 不同源。
2. **DWS 层已改用 `dwd_assistant_service_log_ex.is_trash` 字段**(来自 `assistant_service_records` API直接判断服务是否被废除不再依赖废除表做跨表匹配。
3. 废除表的 `_extract_trash_records``_build_trash_index` 虽然仍被调用,但 `trash_index` 实际上不再参与废除判断逻辑(仅"备查"),属于死代码。
4. `trashed_seconds` / `trashed_count` 等 DWS 字段的数据来源已从废除表切换为服务记录自身的 `income_seconds`,废除表数据不再被消费。
因此,整条 abolish 独立链路API 抓取 → ODS 表 → DWD 表 → DWS 引用)可以安全移除,
同时保留 `assistant_service_records` 中已有的 `is_trash` / `trash_reason` / `trash_applicant_*` 字段作为废除判断的唯一数据源。
## 术语表
- **ETL_System**:飞球 ETL Connector`apps/etl/connectors/feiqiu/`
- **ODS_Layer**:原始数据层(`ods` schema存储从上游 API 抓取的原始记录
- **DWD_Layer**:明细数据层(`dwd` schema存储清洗后的事实表和维度表
- **DWS_Layer**:汇总数据层(`dws` schema存储按业务粒度聚合的汇总表
- **Abolish_Chain**:助教废除独立链路,包括 `ODS_ASSISTANT_ABOLISH` 任务、`ods.assistant_cancellation_records` 表、`dwd.dwd_assistant_trash_event` / `_ex` 表,以及 DWS 层中引用这些表的代码
- **Service_Trash_Fields**`assistant_service_records` API 中自带的废除标记字段(`is_trash``trash_reason``trash_applicant_id``trash_applicant_name`),已映射到 `dwd_assistant_service_log_ex`
- **FACT_MAPPINGS**`dwd_load_task.py` 中定义的 ODS → DWD 字段映射字典
- **Task_Registry**`orchestration/task_registry.py` 中的任务注册表
## 需求
### 需求 1移除 ODS 层废除任务
**用户故事:** 作为 ETL 维护者,我希望移除不再使用的 ODS 抓取任务,以减少无效 API 调用和维护负担。
#### 验收标准
1. WHEN ETL_System 执行调度时THE Task_Registry SHALL 不包含 `ODS_ASSISTANT_ABOLISH` 任务注册
2. WHEN ETL_System 加载 ODS 任务定义时THE ETL_System SHALL 不包含 `OdsAssistantAbolishTask``OdsTaskSpec` 定义
3. WHEN ETL_System 构建默认执行序列时THE ETL_System SHALL 不包含 `ODS_ASSISTANT_ABOLISH` 任务代码
### 需求 2移除 DWD 层废除表映射
**用户故事:** 作为 ETL 维护者,我希望移除废除表的 FACT_MAPPINGS 和 DWD 加载配置,以消除死代码。
#### 验收标准
1. WHEN DWD_Layer 执行装载时THE FACT_MAPPINGS SHALL 不包含 `dwd.dwd_assistant_trash_event` 的映射条目
2. WHEN DWD_Layer 执行装载时THE FACT_MAPPINGS SHALL 不包含 `dwd.dwd_assistant_trash_event_ex` 的映射条目
3. WHEN DWD_Layer 构建 ODS→DWD 表映射时THE ETL_System SHALL 不包含 `dwd.dwd_assistant_trash_event``ods.assistant_cancellation_records` 的映射关系
4. WHEN DWD_Layer 构建 ODS→DWD 表映射时THE ETL_System SHALL 不包含 `dwd.dwd_assistant_trash_event_ex``ods.assistant_cancellation_records` 的映射关系
### 需求 3清理 DWS 层废除表引用
**用户故事:** 作为 ETL 维护者,我希望移除 DWS 任务中对废除表的查询和索引构建代码,以消除死代码路径。
#### 验收标准
1. WHEN DWS_Layer 执行 `DWS_ASSISTANT_DAILY` 任务时THE AssistantDailyTask SHALL 不调用 `_extract_trash_records` 方法
2. WHEN DWS_Layer 执行 `DWS_ASSISTANT_DAILY` 任务时THE AssistantDailyTask SHALL 不调用 `_build_trash_index` 方法
3. WHEN DWS_Layer 执行 `DWS_ASSISTANT_DAILY` 任务时THE AssistantDailyTask SHALL 不向 `_aggregate_by_assistant_date` 传递 `trash_index` 参数
4. WHEN DWS_Layer 聚合服务记录时THE AssistantDailyTask SHALL 仅通过 `is_trash` 字段(来自 `dwd_assistant_service_log_ex` JOIN判断服务是否被废除
### 需求 4清理 DWD 验证器配置
**用户故事:** 作为 ETL 维护者,我希望移除验证器中对废除表的引用,以避免验证器尝试校验已不存在的表。
#### 验收标准
1. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event` 的 ID 映射配置
2. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event_ex` 的 ID 映射配置
3. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event` 的时间字段映射配置
4. WHEN DWD_Layer 执行数据验证时THE DwdVerifier SHALL 不包含 `dwd_assistant_trash_event_ex` 的时间字段映射配置
### 需求 5创建数据库迁移脚本
**用户故事:** 作为数据库管理员,我希望通过迁移脚本安全地移除废除相关的数据库对象,以保持 schema 整洁。
#### 验收标准
1. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 删除 `dwd.dwd_assistant_trash_event`
2. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 删除 `dwd.dwd_assistant_trash_event_ex`
3. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 删除 `ods.assistant_cancellation_records`
4. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 在 DROP 前使用 `IF EXISTS` 防止重复执行报错
5. WHEN 迁移脚本执行时THE 迁移脚本 SHALL 包含注释说明移除原因
### 需求 6同步更新 DDL 文档
**用户故事:** 作为 ETL 维护者,我希望 DDL schema 文件与实际数据库结构保持一致。
#### 验收标准
1. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/dwd.sql` SHALL 不包含 `dwd_assistant_trash_event` 表的 CREATE TABLE 语句
2. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/dwd.sql` SHALL 不包含 `dwd_assistant_trash_event_ex` 表的 CREATE TABLE 语句
3. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/ods.sql` SHALL 不包含 `assistant_cancellation_records` 表的 CREATE TABLE 语句
4. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/schema_dwd_doc.sql` SHALL 不包含 `dwd_assistant_trash_event` 相关的 CREATE TABLE 和 COMMENT 语句
5. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/schema_ODS_doc.sql` SHALL 不包含 `assistant_cancellation_records` 相关的 CREATE TABLE 和 COMMENT 语句
6. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/dws.sql``dws_assistant_daily_detail` 的注释 SHALL 不再引用 `dwd_assistant_trash_event` 作为数据来源
7. WHEN DDL 文件更新后THE `db/etl_feiqiu/schemas/schema_dws.sql``dws_assistant_daily_detail` 的注释 SHALL 不再引用 `dwd_assistant_trash_event` 作为数据来源
### 需求 7更新属性测试
**用户故事:** 作为开发者,我希望属性测试反映清理后的实际状态,以确保测试的准确性。
#### 验收标准
1. WHEN 属性测试执行时THE `test_property_1_fact_mappings.py` SHALL 不包含 `dwd.dwd_assistant_trash_event` 在 A 类表列表中
2. WHEN 属性测试执行时THE `test_property_1_fact_mappings.py` SHALL 不包含 `assistant_cancellation_records → dwd_assistant_trash_event` 的映射期望(`_REQ3_EXPECTED`
3. WHEN 属性测试执行后THE 所有现有属性测试 SHALL 通过(无回归)
### 需求 8更新运维脚本引用
**用户故事:** 作为运维人员,我希望运维脚本中不再引用已移除的表和任务,以避免脚本执行错误。
#### 验收标准
1. WHEN 运维脚本加载 ODS 任务映射时THE 脚本 SHALL 不包含 `ODS_ASSISTANT_ABOLISH``assistant_cancellation_records` 的映射
2. WHEN 运维脚本加载 DWD 表映射时THE 脚本 SHALL 不包含 `dwd.dwd_assistant_trash_event``ods.assistant_cancellation_records` 的映射
3. WHEN 运维脚本列举 ODS 表时THE 脚本 SHALL 不包含 `assistant_cancellation_records`
4. WHEN 运维脚本列举 DWD 表时THE 脚本 SHALL 不包含 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex`
### 需求 9保留 Service_Trash_Fields 不受影响
**用户故事:** 作为 ETL 维护者,我希望确认清理操作不会影响 `assistant_service_records` 中已有的废除标记字段。
#### 验收标准
1. WHILE 清理操作执行期间THE `dwd_assistant_service_log_ex` 表 SHALL 保留 `is_trash``trash_reason``trash_applicant_id``trash_applicant_name` 字段不变
2. WHILE 清理操作执行期间THE `ods.assistant_service_records` 表 SHALL 保留 `is_trash``trash_reason``trash_applicant_id``trash_applicant_name` 字段不变
3. WHILE 清理操作执行期间THE AssistantDailyTask 中通过 `is_trash` 判断废除的逻辑 SHALL 保持正常工作

View File

@@ -0,0 +1,99 @@
# 实施计划助教废除Abolish全链路清理
## 概述
按 ETL 数据流的逆序DWS → DWD → ODS清理废除链路确保每一步都可验证。先清理代码引用再清理 DDL 和数据库对象,最后更新运维脚本和测试。
## 任务
- [x] 1. 清理 DWS 层死代码
- [x] 1.1 从 `assistant_daily_task.py` 中删除 `_extract_trash_records``_build_trash_index` 方法,从 `extract()` 中移除对 `_extract_trash_records` 的调用和 `trash_records` 变量,从 `_aggregate_by_assistant_date` 签名中移除 `trash_index` 参数,同步更新所有调用点。保留 `is_trash` 判断逻辑不变。
- 更新文件头部的 docstring移除对 `dwd_assistant_trash_event` 的数据来源引用
- _Requirements: 3.1, 3.2, 3.3, 3.4_
- [x] 1.2 编写属性测试验证废除聚合逻辑正确性
- **Property 1: 废除聚合逻辑正确性is_trash 驱动)**
- 生成随机服务记录列表,验证 `trashed_seconds`/`trashed_count``is_trash=1` 记录的手动计算一致
- **Validates: Requirements 3.4, 9.3**
- [x] 2. 清理 DWD 层映射和验证器
- [x] 2.1 从 `dwd_load_task.py``FACT_MAPPINGS` 中删除 `dwd.dwd_assistant_trash_event``dwd.dwd_assistant_trash_event_ex` 条目,从 `TABLE_MAP` 中删除对应的 ODS→DWD 映射
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 2.2 从 `dwd_verifier.py` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 的 ID 映射和时间字段映射配置
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 3. 清理 ODS 层任务定义
- [x] 3.1 从 `ods_tasks.py` 中删除 `ODS_ASSISTANT_ABOLISH``OdsTaskSpec` 定义,从默认执行序列中移除该任务代码
- _Requirements: 1.2, 1.3_
- [x] 3.2 从 `task_registry.py` 中删除 `ODS_ASSISTANT_ABOLISH` 的注册语句(如果存在独立注册)
- _Requirements: 1.1_
- [x] 4. Checkpoint — 确保 ETL 单元测试通过
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
- [x] 5. 更新属性测试
- [x] 5.1 从 `tests/test_property_1_fact_mappings.py` 中删除 `dwd.dwd_assistant_trash_event` 在 A 类表列表中的条目,删除 `_REQ3_EXPECTED` 映射期望及其在参数化测试中的引用
- _Requirements: 7.1, 7.2_
- [x] 6. 创建数据库迁移脚本
- [x] 6.1 在 `db/etl_feiqiu/migrations/` 下创建迁移脚本 `2026-02-22__drop_assistant_abolish_tables.sql`,包含 `DROP TABLE IF EXISTS` 语句删除 `ods.assistant_cancellation_records``dwd.dwd_assistant_trash_event``dwd.dwd_assistant_trash_event_ex`,以及删除相关索引(如 `idx_ods_assistant_cancellation_records_latest`),包含注释说明移除原因
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 7. 更新 DDL schema 文件
- [x] 7.1 从 `db/etl_feiqiu/schemas/dwd.sql` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.1, 6.2_
- [x] 7.2 从 `db/etl_feiqiu/schemas/ods.sql` 中删除 `assistant_cancellation_records` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.3_
- [x] 7.3 从 `db/etl_feiqiu/schemas/schema_dwd_doc.sql` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.4_
- [x] 7.4 从 `db/etl_feiqiu/schemas/schema_ODS_doc.sql` 中删除 `assistant_cancellation_records` 的 CREATE TABLE 和 COMMENT 语句
- _Requirements: 6.5_
- [x] 7.5 更新 `db/etl_feiqiu/schemas/dws.sql``db/etl_feiqiu/schemas/schema_dws.sql``dws_assistant_daily_detail` 的注释,将数据来源从 `dwd_assistant_trash_event` 改为 `dwd_assistant_service_log_ex.is_trash`
- _Requirements: 6.6, 6.7_
- [x] 8. 更新运维脚本
- [x] 8.1 从 `scripts/ops/dataflow_analyzer.py` 中删除 `ODS_ASSISTANT_ABOLISH` spec 条目
- _Requirements: 8.1_
- [x] 8.2 从 `scripts/ops/gen_full_dataflow_doc.py` 中删除 `ODS_ASSISTANT_ABOLISH` spec 条目
- _Requirements: 8.1_
- [x] 8.3 从 `scripts/ops/etl_consistency_check.py` 中删除 `ODS_ASSISTANT_ABOLISH` 映射和 `dwd.dwd_assistant_trash_event` 映射
- _Requirements: 8.1, 8.2_
- [x] 8.4 从 `scripts/ops/blackbox_test_report.py` 中删除 `assistant_cancellation_records` 在 ODS_TABLES 列表中的条目、`ODS_ASSISTANT_ABOLISH` 映射、`dwd.dwd_assistant_trash_event` 映射
- _Requirements: 8.1, 8.2, 8.3, 8.4_
- [x] 8.5 从 `scripts/ops/field_audit.py` 中删除 `assistant_cancellation_records` 审计条目
- _Requirements: 8.3, 8.4_
- [x] 8.6 从 `scripts/ops/gen_field_review_doc.py` 中删除 `assistant_cancellation_records` 相关的字段定义块
- _Requirements: 8.3, 8.4_
- [x] 8.7 从 `scripts/ops/gen_api_field_mapping.py` 中删除 `assistant_cancellation_records` 在 ODS_TABLES 列表中的条目
- _Requirements: 8.3_
- [x] 8.8 从 `scripts/ops/export_dwd_field_review.py` 中删除 `dwd_assistant_trash_event``dwd_assistant_trash_event_ex` 条目
- _Requirements: 8.4_
- [x] 8.9 从 `scripts/ops/check_ods_latest_indexes.py` 中删除 `idx_ods_assistant_cancellation_records_latest` 索引检查
- _Requirements: 8.3_
- [x] 9. 最终 Checkpoint — 确保所有测试通过
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit``cd C:\NeoZQYY && pytest tests/ -v`
- 确认所有测试通过,无回归,如有问题请询问用户。
- _Requirements: 7.3, 9.1, 9.2, 9.3_
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,便于追溯
- Checkpoint 确保增量验证
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 本次清理涉及高风险路径(`tasks/``orchestration/``db/`),完成后需运行 `/audit`

View File

@@ -1,311 +0,0 @@
# 设计文档:数据库文档整理与补全
## 概述
本特性对 `docs/bd_manual/` 目录进行系统性整理和补全,涵盖四个核心工作流:
1. **目录结构规范化** — 统一各层目录布局,新增 `ETL_Admin/` 层和根目录 `README.md` 索引
2. **DDL 对比同步** — 编写 Python 脚本对比四个 schema 的 DDL 文件与数据库实际状态,以数据库为准修正 DDL
3. **ODS 表级文档补全** — 为 `billiards` schema 下所有 ODS 表生成 Markdown 表级文档
4. **API→ODS 字段映射文档** — 建立 API JSON 响应到 ODS 表字段的映射关系文档
本特性以文档和 DDL 维护为主不涉及业务代码逻辑变更。DDL 修正属于 `database/` 高风险路径,完成后需触发 `/audit`
## 架构
```mermaid
graph TD
subgraph 信息来源
DB[(PostgreSQL 数据库)]
DDL[database/schema_*.sql]
API_REF[docs/api-reference/]
PARSERS[models/parsers.py]
COMMENTS[DDL COMMENT ON 注释]
end
subgraph 对比脚本
COMPARE[scripts/compare_ddl_db.py]
end
subgraph 文档产出
README[docs/bd_manual/README.md]
ODS_DOCS[docs/bd_manual/ODS/main/*.md]
ODS_MAP[docs/bd_manual/ODS/mappings/*.md]
ODS_DICT[docs/dictionary/ods_tables_dictionary.md]
ETL_DOCS[docs/bd_manual/ETL_Admin/main/*.md]
CHANGES[docs/bd_manual/*/changes/*.md]
DDL_FIX[database/schema_*.sql 修正]
end
DB -->|information_schema 查询| COMPARE
DDL -->|解析 CREATE TABLE| COMPARE
COMPARE -->|差异报告| CHANGES
COMPARE -->|修正| DDL_FIX
DB -->|表结构 + COMMENT| ODS_DOCS
COMMENTS -->|字段说明| ODS_DOCS
API_REF -->|端点信息| ODS_MAP
PARSERS -->|转换逻辑| ODS_MAP
DB -->|表概览| ODS_DICT
DB -->|表结构| ETL_DOCS
```
## 组件与接口
### 1. DDL 对比脚本 (`scripts/compare_ddl_db.py`)
一个独立的 Python 脚本,用于对比 DDL 文件与数据库实际状态。
**输入**
- DDL 文件路径(`database/schema_*.sql`
- 数据库连接(通过 `PG_DSN` 环境变量或 `--pg-dsn` 参数)
**输出**
- 控制台差异报告(表级、字段级、类型级)
- 可选:`--fix` 模式直接修正 DDL 文件
**对比逻辑**
-`information_schema.columns` 查询数据库实际表结构
- 解析 DDL 文件中的 `CREATE TABLE` 语句提取表名和字段定义
- 逐表逐字段对比:表是否存在、字段是否存在、字段类型是否一致、约束是否一致
- 差异分类:`MISSING_TABLE`DDL 缺表)、`EXTRA_TABLE`DDL 多表)、`MISSING_COLUMN``EXTRA_COLUMN``TYPE_MISMATCH``NULLABLE_MISMATCH`
**接口**
```python
def compare_schema(ddl_path: str, schema_name: str, pg_dsn: str) -> list[SchemaDiff]
```
### 2. ODS 表级文档生成器
手动编写(非自动生成脚本),参考以下信息来源:
- 数据库 `information_schema.columns` 获取字段名、类型、可空性
- DDL 文件中的 `COMMENT ON` 注释获取字段说明、示例值、JSON 字段映射
- 现有 DWD/DWS 表级文档格式作为模板
### 3. API→ODS 映射文档
手动编写,参考以下信息来源:
- `docs/api-reference/endpoints/*.md` — API 端点路径、请求参数、响应字段
- `docs/api-reference/samples/*.json` — JSON 响应样本
- `models/parsers.py``TypeParser` 类中的类型转换方法
- DDL 文件中的 `COMMENT ON` 注释中的 `【JSON字段】` 标注
### 4. 目录结构与索引
**新增目录**
- `docs/bd_manual/ETL_Admin/main/`
- `docs/bd_manual/ETL_Admin/changes/`
- `docs/bd_manual/ODS/mappings/`
**新增文件**
- `docs/bd_manual/README.md` — 根索引,列出目录结构和各层文档清单
## 数据模型
本特性不引入新的数据模型。涉及的现有 schema 如下:
| Schema | DDL 文件 | 用途 | 预估表数 |
|--------|----------|------|----------|
| `billiards_ods` | `database/schema_ODS_doc.sql` | 原始数据存储 | ~22 张 |
| `billiards_dwd` | `database/schema_dwd_doc.sql` | 明细数据层 | ~22 张(含 Ex |
| `billiards_dws` | `database/schema_dws.sql` | 数据服务层 | ~30 张 |
| `etl_admin` | `database/schema_etl_admin.sql` | ETL 管理元数据 | ~5 张 |
### 文档模板格式
**ODS 表级文档模板**(与 DWD/DWS 保持一致):
```markdown
# {表名} {中文说明}
> 生成时间YYYY-MM-DD
## 表信息
| 属性 | 值 |
|------|-----|
| Schema | billiards |
| 表名 | {表名} |
| 主键 | {主键字段} |
| 数据来源 | {API 端点 / JSON 文件} |
| 说明 | {表说明} |
## 字段说明
| 序号 | 字段名 | 类型 | 可空 | 说明 |
|------|--------|------|------|------|
| 1 | ... | ... | ... | ... |
## 使用说明
{SQL 示例}
## 可回溯性
| 项目 | 说明 |
|------|------|
| 可回溯 | ✅ 完全可回溯(保留 payload 原始 JSON |
| 数据来源 | {API 端点路径} |
```
**API→ODS 映射文档模板**
```markdown
# {API端点名} → {ODS表名} 字段映射
> 生成时间YYYY-MM-DD
## 端点信息
| 属性 | 值 |
|------|-----|
| 接口路径 | {路径} |
| 请求方法 | POST |
| ODS 对应表 | {表名} |
| JSON 数据路径 | {如 data.tenantMemberInfos} |
## 字段映射
| JSON 字段 | ODS 列名 | 类型转换 | 说明 |
|-----------|----------|----------|------|
| id | id | int→BIGINT | 主键 |
| ... | ... | ... | ... |
## ETL 补充字段
| ODS 列名 | 生成逻辑 |
|-----------|----------|
| content_hash | 对业务字段计算 SHA256 |
| source_file | 固定值:{文件名}.json |
| source_endpoint | API 端点路径 |
| fetched_at | 入库时间戳 |
| payload | 完整原始 JSON 记录 |
## 类型转换规则
- 时间戳:通过 `TypeParser.parse_timestamp()` 转换,支持字符串和 Unix 毫秒时间戳
- 金额:通过 `TypeParser.parse_decimal(value, scale=2)` 转换ROUND_HALF_UP
- 整数:通过 `TypeParser.parse_int()` 转换
```
## 正确性属性
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: 数据层目录结构一致性
*For any* 数据层目录ODS、DWD、DWS、ETL_Admin该目录下都应包含 `main/``changes/` 两个子目录。
**Validates: Requirements 1.2**
### Property 2: DDL 对比脚本差异检测完整性
*For any* schema 和对应的 DDL 文件,当数据库中存在 DDL 文件未定义的表或字段时,对比脚本应将其报告为 `MISSING_TABLE``MISSING_COLUMN`;当 DDL 文件中存在数据库没有的表或字段时,应报告为 `EXTRA_TABLE``EXTRA_COLUMN`;当字段类型不一致时,应报告为 `TYPE_MISMATCH`
**Validates: Requirements 2.1, 2.2, 2.3, 2.4**
### Property 3: DDL 修正后零差异(不动点)
*For any* schema在以数据库实际状态修正 DDL 文件后,再次运行对比脚本,差异列表应为空。
**Validates: Requirements 2.5**
### Property 4: ODS 表级文档覆盖率
*For any* `billiards` schema 中的 ODS 表,在 `docs/bd_manual/ODS/main/` 目录下都应存在一份对应的 Markdown 文档。
**Validates: Requirements 3.1**
### Property 5: ODS 表级文档格式完整性
*For any* ODS 表级文档,都应包含以下章节:表信息(含 Schema、表名、主键、数据来源、说明、字段说明表格、使用说明含 SQL 示例)、可回溯性信息,以及 ETL 元数据字段content_hash、source_file、source_endpoint、fetched_at、payload的说明。
**Validates: Requirements 3.2, 3.4, 3.5**
### Property 6: ODS 表级文档命名规范
*For any* ODS 表级文档文件,其文件名应匹配 `BD_manual_{表名}.md` 格式。
**Validates: Requirements 3.6**
### Property 7: 映射文档覆盖率
*For any* 有对应 ODS 表的 API 端点,在 `docs/bd_manual/ODS/mappings/` 目录下都应存在一份对应的映射文档。
**Validates: Requirements 4.1**
### Property 8: 映射文档内容完整性
*For any* 映射文档都应包含以下信息API 端点路径、ODS 表名、JSON 数据路径、字段映射表格,以及 ETL 补充字段content_hash、source_file、source_endpoint、fetched_at、payload的生成逻辑。
**Validates: Requirements 4.2, 4.4**
### Property 9: 映射文档命名规范
*For any* 映射文档文件,其文件名应匹配 `mapping_{API端点名}_{ODS表名}.md` 格式。
**Validates: Requirements 4.6**
### Property 10: ODS 数据字典覆盖率
*For any* `billiards` schema 中的 ODS 表ODS 数据字典中都应有对应的条目,包含表名、中文说明、主键、数据来源信息。
**Validates: Requirements 5.2**
## 错误处理
### DDL 对比脚本
| 场景 | 处理方式 |
|------|----------|
| 数据库连接失败 | 输出错误信息并退出,返回非零退出码 |
| DDL 文件不存在 | 输出错误信息并跳过该 schema |
| DDL 文件解析失败 | 输出解析错误位置和原因,尽可能继续解析其余部分 |
| schema 在数据库中不存在 | 输出警告并跳过 |
### 文档生成
| 场景 | 处理方式 |
|------|----------|
| 表无 COMMENT 注释 | 字段说明列填写"(待补充)" |
| API 端点文档缺失 | 映射文档中标注"端点文档待补充",仅基于 DDL COMMENT 生成 |
| 字段类型无法识别 | 保留数据库原始类型字符串 |
## 测试策略
### 单元测试
针对 DDL 对比脚本的核心逻辑编写单元测试(`tests/unit/test_compare_ddl.py`
- 测试 DDL 解析器能正确提取表名、字段名、字段类型、约束
- 测试差异检测逻辑能识别各类差异(缺失表、多余表、字段差异、类型差异)
- 测试边界情况:空 DDL 文件、无表的 schema、COMMENT 中含特殊字符
### 属性测试
使用 `hypothesis`Python 属性测试框架)。
- **Property 2 测试**:生成随机的"DDL 表定义"和"数据库表定义",注入已知差异,验证对比函数能检测到所有差异
- **Feature: bd-manual-docs-consolidation, Property 2: DDL 对比脚本差异检测完整性**
- 最少 100 次迭代
- **Property 3 测试**:生成随机的数据库表定义,用其生成 DDL再运行对比验证差异为零
- **Feature: bd-manual-docs-consolidation, Property 3: DDL 修正后零差异(不动点)**
- 最少 100 次迭代
### 集成验证
文档覆盖率和格式验证通过 Python 脚本实现(`scripts/validate_bd_manual.py`),可在 CI 中运行:
- 验证 Property 1目录结构、Property 4-10文档覆盖率、格式、命名
- 输入:文件系统 + 数据库 `information_schema` 查询
- 输出:通过/失败报告,列出缺失或不合规的文档
### 测试配置
- 属性测试库:`hypothesis`(需添加到开发依赖)
- 单元测试:`pytest tests/unit/test_compare_ddl.py`
- 集成验证:`python scripts/validate_bd_manual.py --pg-dsn "$PG_DSN"`
- 每个属性测试最少 100 次迭代
- 每个测试需注释引用对应的设计属性编号

View File

@@ -1,80 +0,0 @@
# 需求文档
## 简介
整理和补全飞球 ETL 系统的数据库文档体系(`docs/bd_manual/`包括目录结构规范化、DDL 与数据库实际状态的对比同步、ODS 层表级文档补全、以及 API JSON → ODS 字段映射文档的建立。本特性不涉及代码逻辑变更,仅涉及文档和 DDL 文件的维护。
## 术语表
- **BD_Manual**: 数据库手册目录(`docs/bd_manual/`),存放各层表级文档、变更记录等
- **ODS**: 操作数据存储层Operational Data Store`billiards` schema保留 API 原始数据
- **DWD**: 明细数据层Data Warehouse Detail`billiards_dwd` schema清洗后的维度和事实表
- **DWS**: 数据服务层Data Warehouse Service`billiards_dws` schema汇总宽表和配置表
- **DDL**: 数据定义语言文件(`database/schema_*.sql`),定义表结构
- **表级文档**: 以 Markdown 格式编写的单表说明文件,包含表信息、字段说明、使用示例等
- **字段映射文档**: 记录 API JSON 响应字段到 ODS 表字段的对应关系和转换逻辑
- **SCD2**: 缓慢变化维度类型 2用于 DWD 维度表的历史版本管理
- **ETL_Admin**: ETL 管理 schema`etl_admin`),存放调度、游标、运行记录等元数据
## 需求
### 需求 1规范化 BD_Manual 目录结构
**用户故事:** 作为数据开发人员,我希望 BD_Manual 目录结构统一规范,以便快速定位各层各类型的数据库文档。
#### 验收标准
1. THE BD_Manual SHALL 包含以下顶层子目录:`ODS/``DWD/``DWS/``ETL_Admin/`
2. WHEN 某一数据层目录被访问时THE BD_Manual SHALL 在该层目录下提供 `main/`(表级文档)和 `changes/`(变更记录)两个子目录
3. THE DWD 目录 SHALL 额外保留 `Ex/` 子目录用于存放扩展表文档
4. THE BD_Manual SHALL 在根目录提供一个 `README.md` 索引文件,列出目录结构说明和各层文档清单
5. WHEN ETL_Admin schema 存在表时THE BD_Manual SHALL 在 `ETL_Admin/main/` 下为每张表提供表级文档
### 需求 2DDL 文件与数据库实际状态对比同步
**用户故事:** 作为数据开发人员,我希望 DDL 文件与数据库实际表结构保持一致,以便 DDL 文件可作为可信的 schema 参考。
#### 验收标准
1. WHEN 对比 ODS 层 DDL 文件(`database/schema_ODS_doc.sql`)与数据库 `billiards_ods` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项(缺失表、多余表、字段差异、类型差异、约束差异)
2. WHEN 对比 DWD 层 DDL 文件(`database/schema_dwd_doc.sql`)与数据库 `billiards_dwd` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项
3. WHEN 对比 DWS 层 DDL 文件(`database/schema_dws.sql`)与数据库 `billiards_dws` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项
4. WHEN 对比 ETL_Admin 层 DDL 文件(`database/schema_etl_admin.sql`)与数据库 `etl_admin` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项
5. WHEN 发现差异时THE DDL 文件 SHALL 以数据库实际状态为准进行修正
6. WHEN DDL 文件被修正后THE 变更记录 SHALL 在对应层的 `changes/` 目录下生成一份差异说明文档
### 需求 3补全 ODS 层表级文档
**用户故事:** 作为数据开发人员,我希望 ODS 层每张表都有完整的表级文档,以便理解原始数据结构和来源。
#### 验收标准
1. THE ODS 表级文档 SHALL 为 `billiards_ods` schema 中的每张 ODS 表生成一份 Markdown 文档,存放于 `docs/bd_manual/ODS/main/`
2. THE ODS 表级文档 SHALL 遵循与 DWD/DWS 表级文档一致的格式,包含:表信息表格、字段说明表格、使用说明(含 SQL 示例)、可回溯性信息
3. WHEN ODS 表的字段含有 COMMENT 注释时THE 表级文档 SHALL 将 COMMENT 中的说明、示例、JSON 字段映射信息提取并填入字段说明
4. THE ODS 表级文档的表信息 SHALL 包含 Schema、表名、主键、数据来源API 端点或文件)、说明
5. THE ODS 表级文档 SHALL 包含 ETL 元数据字段(`content_hash``source_file``source_endpoint``fetched_at``payload`)的统一说明
6. THE ODS 表级文档的文件命名 SHALL 遵循 `BD_manual_{表名}.md` 格式
### 需求 4建立 API JSON → ODS 字段映射文档
**用户故事:** 作为数据开发人员,我希望有一份清晰的 API 响应字段到 ODS 表字段的映射文档,以便理解数据从 API 到 ODS 的转换逻辑。
#### 验收标准
1. THE 映射文档 SHALL 为每个 API 端点与其对应的 ODS 表建立一份映射文件,存放于 `docs/bd_manual/ODS/mappings/`
2. THE 映射文档 SHALL 包含以下信息API 端点路径、对应 ODS 表名、JSON 响应路径(如 `data.tenantMemberInfos`)、每个字段的 JSON 路径到 ODS 列名的映射
3. WHEN 字段存在类型转换或值处理逻辑时THE 映射文档 SHALL 记录转换规则(如时间格式转换、枚举值映射、金额精度处理)
4. THE 映射文档 SHALL 标注 ETL 补充字段(`content_hash``source_file``source_endpoint``fetched_at``payload`)的生成逻辑
5. THE 映射文档 SHALL 参考 `models/parsers.py` 中的解析逻辑和 `docs/api-reference/` 中的端点文档作为信息来源
6. THE 映射文档的文件命名 SHALL 遵循 `mapping_{API端点名}_{ODS表名}.md` 格式
### 需求 5建立 ODS 数据字典
**用户故事:** 作为数据开发人员,我希望有一份 ODS 层的数据字典汇总文档,以便快速查阅所有 ODS 表的概览信息。
#### 验收标准
1. THE ODS 数据字典 SHALL 创建于 `docs/dictionary/ods_tables_dictionary.md`
2. THE ODS 数据字典 SHALL 列出所有 ODS 表的概览信息,包含:表名、中文说明、主键、记录数、数据来源
3. THE ODS 数据字典 SHALL 遵循与现有 DWD/DWS 数据字典一致的格式

View File

@@ -1,111 +0,0 @@
# 实施计划:数据库文档整理与补全
## 概述
按照"目录结构 → DDL 对比脚本 → DDL 同步 → ODS 文档 → 映射文档 → 数据字典 → 索引"的顺序逐步完成文档体系的整理和补全。DDL 对比脚本先编写并测试,再用它驱动实际的 DDL 同步工作。
## 任务
- [x] 1. 规范化 BD_Manual 目录结构
- 创建 `docs/bd_manual/ETL_Admin/main/``docs/bd_manual/ETL_Admin/changes/` 目录
- 创建 `docs/bd_manual/ODS/mappings/` 目录
- 确认 ODS/DWD/DWS 各层均有 `main/``changes/` 子目录
- _Requirements: 1.1, 1.2, 1.3_
- [ ] 2. 编写 DDL 对比脚本
- [x] 2.1 实现 DDL 解析器和对比核心逻辑 (`scripts/compare_ddl_db.py`)
- 解析 `CREATE TABLE` 语句提取表名、字段名、字段类型、约束、主键
- 查询 `information_schema.columns` 获取数据库实际表结构
- 实现逐表逐字段对比输出差异分类MISSING_TABLE、EXTRA_TABLE、MISSING_COLUMN、EXTRA_COLUMN、TYPE_MISMATCH、NULLABLE_MISMATCH
- 支持 `--pg-dsn``--schema``--ddl-path` 参数
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 2.2 编写 DDL 解析器单元测试 (`tests/unit/test_compare_ddl.py`)
- 测试 DDL 解析器正确提取表名、字段、类型、约束
- 测试差异检测逻辑识别各类差异
- 测试边界情况空文件、COMMENT 含特殊字符
- _Requirements: 2.1_
- [x] 2.3 编写 DDL 对比属性测试
- **Property 2: DDL 对比脚本差异检测完整性**
- **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
- [x] 2.4 编写 DDL 修正不动点属性测试
- **Property 3: DDL 修正后零差异(不动点)**
- **Validates: Requirements 2.5**
- [x] 3. 检查点 — 确认对比脚本可用
- 确保所有测试通过,如有问题请向用户确认。
- [x] 4. 执行 DDL 对比并同步
- [x] 4.1 运行对比脚本对比四个 schemaODS、DWD、DWS、ETL_Admin
- 执行 `scripts/compare_ddl_db.py` 对比每个 schema
- 记录所有差异项
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 4.2 修正 DDL 文件以匹配数据库实际状态
- 以数据库为准修正 `database/schema_ODS_doc.sql``database/schema_dwd_doc.sql``database/schema_dws.sql``database/schema_etl_admin.sql`
- _Requirements: 2.5_
- [x] 4.3 生成 DDL 变更记录
- 在对应层的 `changes/` 目录下生成差异说明文档(日期前缀命名)
- 包含变更说明、兼容性影响、回滚策略、验证 SQL至少 3 条)
- _Requirements: 2.6_
- [x] 5. 检查点 — 确认 DDL 同步完成
- 确保所有测试通过,如有问题请向用户确认。
- [x] 6. 补全 ODS 层表级文档
- [x] 6.1 为每张 ODS 表编写表级文档 (`docs/bd_manual/ODS/main/BD_manual_{表名}.md`)
- 从数据库 `information_schema.columns` 获取字段信息
- 从 DDL `COMMENT ON` 注释提取字段说明、示例值、JSON 字段映射
- 遵循 DWD/DWS 文档格式:表信息、字段说明、使用说明(含 SQL、可回溯性
- 包含 ETL 元数据字段统一说明
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
- [x] 7. 建立 API→ODS 字段映射文档
- [x] 7.1 为每个 API 端点编写映射文档 (`docs/bd_manual/ODS/mappings/mapping_{端点名}_{表名}.md`)
- 参考 `docs/api-reference/endpoints/*.md` 获取端点信息和响应字段
- 参考 DDL `COMMENT ON` 中的 `【JSON字段】` 标注获取映射关系
- 参考 `models/parsers.py``TypeParser` 的转换方法记录类型转换规则
- 包含 ETL 补充字段生成逻辑说明
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_
- [x] 8. 建立 ODS 数据字典和 ETL_Admin 文档
- [x] 8.1 创建 ODS 数据字典 (`docs/dictionary/ods_tables_dictionary.md`)
- 列出所有 ODS 表概览:表名、中文说明、主键、记录数、数据来源
- 遵循现有 DWD/DWS 数据字典格式
- _Requirements: 5.1, 5.2, 5.3_
- [x] 8.2 为 ETL_Admin 表编写表级文档 (`docs/bd_manual/ETL_Admin/main/BD_manual_{表名}.md`)
- 从数据库获取 `etl_admin` schema 表结构
- 遵循统一文档格式
- _Requirements: 1.5_
- [x] 9. 编写 BD_Manual 根目录 README.md 索引
- 创建 `docs/bd_manual/README.md`
- 列出目录结构说明、各层文档清单、文档命名规范
- _Requirements: 1.4_
- [x] 10. 编写文档验证脚本
- [x] 10.1 实现文档覆盖率和格式验证脚本 (`scripts/validate_bd_manual.py`)
- 验证目录结构一致性Property 1
- 验证 ODS 文档覆盖率和命名规范Property 4, 6
- 验证 ODS 文档格式完整性Property 5
- 验证映射文档覆盖率和命名规范Property 7, 9
- 验证映射文档内容完整性Property 8
- 验证数据字典覆盖率Property 10
- 支持 `--pg-dsn` 参数连接数据库获取表清单
- _Requirements: 1.2, 3.1, 3.2, 3.6, 4.1, 4.2, 4.6, 5.2_
- [x] 11. 最终检查点 — 确认所有文档完整
- 运行 `scripts/validate_bd_manual.py` 确认所有验证通过
- 确保所有测试通过,如有问题请向用户确认。
## 备注
- 标记 `*` 的子任务为可选,可跳过以加速 MVP
- 每个任务引用了具体的需求编号以便追溯
- DDL 修正涉及 `database/` 高风险路径,完成后需触发 `/audit`
- 属性测试验证对比脚本的通用正确性,集成验证脚本验证文档体系的完整性
- ODS 表级文档和映射文档为手动编写(非自动生成),需逐表参考数据库和 API 文档

View File

@@ -0,0 +1,447 @@
# 设计文档:数据流字段补全与前后端联调
## 概述
本设计基于 `dataflow_2026-02-19_190440.md` 数据流分析报告,覆盖两大任务:
1. **字段补全**:对 11 张 ODS/DWD 表执行字段映射补全,包括 DDL 更新、ETL loader/task 代码同步、文档精化
2. **DWS 库存汇总**:在 DWS 层新建日/周/月三个粒度的库存汇总表,基于 DWD goods_stock_summary 数据构建
3. **前后端联调**:确保 admin-web 前端与 FastAPI 后端的 ETL 执行流程完整可用,含计时和黑盒测试
核心设计原则:
- **执行依据**:字段补全部分基于排查结论文档 `export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`(由 `FIELD_AUDIT_ROOT` 环境变量配置路径)
- **先确认再新增**:对每个疑似缺失字段,必须先排查是否已存在(可能是命名差异、已映射到其他列、或已在 FACT_MAPPINGS 中以不同名称配置),确认确实缺失后才执行新增
- 所有字段映射变更通过 `DwdLoadTask.FACT_MAPPINGS` 声明式配置,不修改核心合并逻辑
- 新建 DWD 表遵循现有 main/ex 分表模式(核心字段 → main 表,扩展字段 → ex 表)
- DDL 变更通过迁移脚本(`db/etl_feiqiu/migrations/`)执行,同步更新 schema 文件
- 控制无效字段新增:仅在确认字段确实缺失且有业务价值时才新增
## 架构
### 现有 ETL 数据流架构
```mermaid
graph LR
API[上游 SaaS API] -->|JSON| ODS_Loader[GenericODSLoader]
ODS_Loader -->|UPSERT| ODS[(ODS 表)]
ODS -->|SELECT| DWD_Task[DwdLoadTask]
DWD_Task -->|SCD2 合并| DIM[(DWD 维度表)]
DWD_Task -->|增量插入| FACT[(DWD 事实表)]
```
### 字段映射机制
`DwdLoadTask` 使用两层映射策略:
1. **自动映射**ODS 列名与 DWD 列名相同时自动匹配
2. **显式映射**:通过 `FACT_MAPPINGS` 字典声明 `(dwd_col, ods_expr, cast_type)` 三元组
本次变更主要操作 `FACT_MAPPINGS``TABLE_MAP`,以及对应的 DDL。
### 前后端联调架构
```mermaid
graph LR
AdminWeb[Admin Web<br/>React + Ant Design] -->|HTTP/WS| Backend[FastAPI 后端]
Backend -->|subprocess| ETL[ETL CLI]
ETL -->|SQL| DB[(PostgreSQL)]
Backend -->|WebSocket| AdminWeb
```
## 字段排查结论(已完成)
排查工作已完成,详细结论见 `export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`
排查方法包括:查 DWD 表现有列、查 FACT_MAPPINGS、查 ODS 表现有列、查自动映射、查 API JSON 样本、数据库实际数据验证。排查发现 4 个映射错误、21 个待新增字段、2 张需新建 DWD 表、6 个跳过字段。
## 组件与接口
### 任务 1字段补全涉及的组件
| 组件 | 文件路径 | 变更类型 |
|------|---------|---------|
| DWD 加载任务 | `tasks/dwd/dwd_load_task.py` | 修改 `FACT_MAPPINGS``TABLE_MAP` |
| ODS DDL | `db/etl_feiqiu/schemas/ods.sql` | 新增列store_goods_master 嵌套展开) |
| DWD DDL | `db/etl_feiqiu/schemas/dwd.sql` | 新增列、新建表 |
| 迁移脚本 | `db/etl_feiqiu/migrations/` | 新增 ALTER TABLE / CREATE TABLE |
| ODS 加载器 | `loaders/ods/generic.py` | 可能需要扩展 columns 列表 |
| BD_Manual 文档 | `docs/database/` | 更新字段说明 |
### 任务 2前后端联调涉及的组件
| 组件 | 文件路径 | 变更类型 |
|------|---------|---------|
| 执行 API | `apps/backend/app/routers/` | 调试/修复参数传递 |
| 执行页面 | `apps/admin-web/src/pages/TaskManager.tsx` | 调试/修复前端逻辑 |
| 计时模块 | `apps/etl/connectors/feiqiu/utils/` | 新增计时器工具 |
| 黑盒测试 | `apps/etl/connectors/feiqiu/quality/` | 新增数据一致性检查 |
## 数据模型
### 字段补全分类
根据 `field_review_for_user.md` 排查结论,将变更分为四类:
#### 🔴 映射错误修复(高优先级)
| 表 | 问题 | 修正方案 |
|----|------|---------|
| assistant_service_records | DWD `site_assistant_id` 错误映射自 ODS `order_assistant_id` | 修正映射源 + 新增 `order_assistant_id` 列 |
| store_goods_sales_records | DWD `discount_price` 实际映射自 ODS `discount_money`(列名误导) | 重命名 DWD 列 + 新增真正的 `discount_price` |
| store_goods_master | `batch_stock_qty` 映射自 `stock`(错误),`provisional_total_cost` 映射自 `total_purchase_cost`(错误) | 修正 FACT_MAPPINGS 源列 |
#### A 类:新增 DWD 列 + FACT_MAPPINGS
| 表 | 新增字段数 | DWD 目标 |
|----|----------|---------|
| assistant_accounts_master | 4 | dim_assistant_ex |
| assistant_service_records | 2 | dwd_assistant_service_log_ex |
| assistant_cancellation_records | 0仅更新映射 | dwd_assistant_trash_event |
| member_balance_changes | 1 | dwd_member_balance_change_ex |
| site_tables_master | 14 | dim_table_ex |
#### B 类:仅补 FACT_MAPPINGSDWD 列已存在)
| 表 | 说明 |
|----|------|
| recharge_settlements | 5 个字段DWD 列已存在ODS/DWD 两侧数据全为 0业务未启用 |
#### 跳过(无需变更)
| 表 | 原因 |
|----|------|
| tenant_goods_master | `commoditycode``commodity_code` 100% 冗余(花括号包裹格式),跳过 |
| store_goods_mastertime_slot_sale | ODS 列不存在,跳过 |
#### C 类:需新建 DWD 表
| 表 | ODS 字段数 | DWD 新表 | 备注 |
|----|----------|---------|------|
| goods_stock_summary | 14 | dwd_goods_stock_summary | 需先修改 ODS 配置 `requires_window=True` 并重新采集 |
| goods_stock_movements | 19 | dwd_goods_stock_movement | 事实表,按 createtime 增量加载 |
#### C 类:疑似需新建 DWD 表(需排查是否有替代方案)
| 表 | ODS 字段数 | 疑似新建 DWD 表 | 排查重点 |
|----|----------|---------------|---------|
| goods_stock_summary | 14 | dwd_goods_stock_summary | 确认是否有意不建 DWD 表(如数据直接在 ODS 层使用) |
| goods_stock_movements | 19 | dwd_goods_stock_movement | 同上 |
### 已确认的映射关系(排查结论)
以下映射关系已通过数据库实际数据验证确认:
| 字段 | 排查结论 | 所在表 |
|------|---------|-------|
| discount_price (store_goods_sales) | 🔴 DWD `discount_price` 实际映射自 ODS `discount_money`,需重命名 + 新增 | store_goods_sales_records |
| commoditycode (tenant_goods) | ⏭️ 与 `commodity_code` 100% 冗余,跳过 | tenant_goods_master |
| site_assistant_id (assistant_service) | 🔴 DWD 错误映射自 ODS `order_assistant_id`,需修正 | assistant_service_records |
| recharge 电费/券字段 | ✅ DWD 列已存在,仅需补 FACT_MAPPINGS数据全为 0 | recharge_settlements |
| batch_stock_qty (store_goods) | 🔴 错误映射自 `stock`,应映射自 `batch_stock_quantity` | store_goods_master |
| provisional_total_cost (store_goods) | 🔴 错误映射自 `total_purchase_cost`,应映射自 `provisional_total_cost` | store_goods_master |
### 新建 DWD 表设计
#### dwd_goods_stock_summary
```sql
CREATE TABLE dwd.dwd_goods_stock_summary (
site_goods_id bigint NOT NULL,
goods_name text,
goods_unit text,
goods_category_id bigint,
goods_category_second_id bigint,
category_name text,
range_start_stock numeric,
range_end_stock numeric,
range_in numeric,
range_out numeric,
range_sale numeric,
range_sale_money numeric(12,2),
range_inventory numeric,
current_stock numeric,
site_id bigint,
tenant_id bigint,
fetched_at timestamptz,
PRIMARY KEY (site_goods_id)
);
```
#### dwd_goods_stock_movement
```sql
CREATE TABLE dwd.dwd_goods_stock_movement (
site_goods_stock_id bigint NOT NULL,
tenant_id bigint,
site_id bigint,
site_goods_id bigint,
goods_name text,
goods_category_id bigint,
goods_second_category_id bigint,
unit text,
price numeric(12,2),
stock_type integer,
change_num numeric,
start_num numeric,
end_num numeric,
change_num_a numeric,
start_num_a numeric,
end_num_a numeric,
remark text,
operator_name text,
create_time timestamptz,
fetched_at timestamptz,
PRIMARY KEY (site_goods_stock_id)
);
```
### recharge_settlements 映射关系
ODS 列与 DWD 列的对应关系(命名转换):
| ODS 列(驼峰) | DWD 列(蛇形) |
|---------------|--------------|
| plcouponsaleamount | pl_coupon_sale_amount |
| mervousalesamount | mervou_sales_amount |
| electricitymoney | electricity_money |
| realelectricitymoney | real_electricity_money |
| electricityadjustmoney | electricity_adjust_money |
这 5 个字段在 `dwd_recharge_order` 中已有列定义但缺少 FACT_MAPPINGS 条目,需要补充映射。
### store_goods_master 映射修正
根据排查结论,该表存在两个映射错误(非新增字段):
| DWD 列 | 当前错误映射 ODS 列 | 正确 ODS 列 | 验证结果 |
|--------|-------------------|------------|---------|
| `batch_stock_qty` | `stock`(当前库存) | `batch_stock_quantity`(批次库存) | 仅 7.3% 行相等 |
| `provisional_total_cost` | `total_purchase_cost`(实际采购成本) | `provisional_total_cost`(暂估成本) | 93.5% 行相等但 113 行不同 |
`time_slot_sale` ODS 列不存在,跳过。`goodsStockWarningInfo` 嵌套展开不在本次范围内。
### DWS 库存汇总表设计(日/周/月)
基于 `field_review_for_user.md` 第 10 章发现goods_stock_summary API 支持 `startTime`/`endTime` 参数返回时间范围内的库存汇总数据。在 ODS 任务配置修改(`requires_window=True` + `time_fields=("startTime", "endTime")`并重新采集后DWD 层 `dwd_goods_stock_summary` 将拥有带时间范围的真实数据,可在此基础上构建 DWS 层汇总。
#### 三张 DWS 表
| 表名 | 粒度 | 任务代码 | stat_period |
|------|------|---------|-------------|
| `dws.dws_goods_stock_daily_summary` | 日 | `DWS_GOODS_STOCK_DAILY` | `'daily'` |
| `dws.dws_goods_stock_weekly_summary` | 周 | `DWS_GOODS_STOCK_WEEKLY` | `'weekly'` |
| `dws.dws_goods_stock_monthly_summary` | 月 | `DWS_GOODS_STOCK_MONTHLY` | `'monthly'` |
#### DDL 设计(三张表结构相同)
```sql
CREATE TABLE dws.dws_goods_stock_daily_summary (
site_id bigint NOT NULL,
tenant_id bigint,
stat_date date NOT NULL,
site_goods_id bigint NOT NULL,
goods_name text,
goods_unit text,
goods_category_id bigint,
goods_category_second_id bigint,
category_name text,
range_start_stock numeric,
range_end_stock numeric,
range_in numeric,
range_out numeric,
range_sale numeric,
range_sale_money numeric(12,2),
range_inventory numeric,
current_stock numeric,
stat_period text NOT NULL DEFAULT 'daily',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (site_id, stat_date, site_goods_id)
);
```
周度表和月度表结构相同,仅表名和 `stat_period` 默认值不同(`'weekly'` / `'monthly'`)。
#### 任务实现模式
继承 `BaseDwsTask`,实现 `extract` / `transform` / `load` 三阶段:
- **extract**:从 `dwd.dwd_goods_stock_summary` 按时间范围查询数据
- **transform**:按粒度(日/周/月)对 `stat_date` 进行分组聚合,计算各库存指标
- 日度:直接取 DWD 数据(`stat_date` = 采集日期)
- 周度:按 ISO 周分组,`stat_date` = 周一日期
- 月度:按自然月分组,`stat_date` = 月首日期
- **load**:使用 `upsert` 写入目标表,主键冲突时更新
#### 前置依赖
- 需求 7goods_stock_summary 新建 DWD 表)必须先完成
- ODS 任务配置修改(`requires_window=True`)必须先完成并重新采集数据
#### 文件位置
- DDL`db/etl_feiqiu/schemas/dws.sql`
- 迁移脚本:`db/etl_feiqiu/migrations/{date}__create_dws_goods_stock_summary.sql`
- 任务代码:`apps/etl/connectors/feiqiu/tasks/dws/goods_stock_daily_task.py``goods_stock_weekly_task.py``goods_stock_monthly_task.py`
### settlement_ticket_details 彻底移除设计
从项目中完整移除 `settlement_ticket_details`结账小票详情相关的所有代码、DDL、配置、文档和数据。
#### 需要移除的文件/代码位置
| 层级 | 文件路径 | 移除内容 |
|------|---------|---------|
| ETL 任务定义 | `tasks/ods/ods_tasks.py` | `OdsTaskSpec("ODS_SETTLEMENT_TICKET", ...)``OdsSettlementTicketTask` 类、`ENABLED_ODS_CODES` 中的条目、`ODS_TASK_CLASSES` 覆盖 |
| ETL 校验 | `tasks/verification/dwd_verifier.py` | `settlement_ticket_details` 主键映射条目 |
| ETL 校验 | `tasks/verification/ods_verifier.py` | 相关注释和特殊处理逻辑 |
| ETL 手动导入 | `tasks/utility/manual_ingest_task.py` | `settlement_ticket_details` 的表映射和配置 |
| JSON 存储 | `utils/json_store.py` | `/order/getordersettleticketnew` 的路径映射 |
| ODS 间隙检查 | `scripts/check/check_ods_gaps.py` | `_check_settlement_tickets` 函数及调用 |
| 黑盒调试 | `scripts/debug/debug_blackbox.py` | `ODS_SETTLEMENT_TICKET` 跳过逻辑 |
| DDL | `db/etl_feiqiu/schemas/ods.sql``schema_ODS_doc.sql` | `settlement_ticket_details` 建表语句和注释 |
| 种子数据 | `db/etl_feiqiu/seeds/seed_ods_tasks.sql` | `ODS_SETTLEMENT_TICKET` 条目 |
| 索引检查 | `scripts/ops/check_ods_latest_indexes.py` | `idx_ods_settlement_ticket_details_latest` |
| 分析脚本 | `scripts/ops/gen_full_dataflow_doc.py` | ODS spec 条目和特殊跳过逻辑 |
| 分析脚本 | `scripts/ops/gen_field_review_doc.py` | 第 12 章 settlement_ticket_details 配置 |
| 分析脚本 | `scripts/ops/gen_api_field_mapping.py` | 表名列表中的条目 |
| 分析脚本 | `scripts/ops/field_audit.py` | 排查配置和特殊处理 |
| 分析脚本 | `scripts/ops/export_dwd_field_review.py` | 字段列表配置 |
| 分析脚本 | `scripts/ops/dataflow_analyzer.py` | ODS spec 条目和跳过逻辑 |
| 文档 | `docs/database/etl_feiqiu_schema_migration.md` | 索引条目 |
| ETL 文档 | `apps/etl/connectors/feiqiu/docs/etl_tasks/` | 任务表格条目 |
| 单元测试 | `tests/unit/test_ods_tasks.py` | `test_ods_settlement_ticket_by_payment_relate_ids` |
#### 迁移脚本
```sql
-- 移除 settlement_ticket_details 表和索引
DROP INDEX IF EXISTS ods.idx_ods_settlement_ticket_details_latest;
DROP TABLE IF EXISTS ods.settlement_ticket_details;
-- 移除 meta.ods_task_registry 中的任务注册
DELETE FROM meta.ods_task_registry WHERE task_code = 'ODS_SETTLEMENT_TICKET';
```
#### 注意事项
- `export/` 下的报告文件(`field_audit_report.md``dataflow_api_ods_dwd.md` 等)为历史产物,不需要手动清理,下次重新生成时自然不再包含
- `docs/audit/` 下的审计日志为历史记录,保留不动
- `tmp/` 下的临时文件不需要处理
## 正确性属性
*正确性属性是一种在系统所有合法执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1FACT_MAPPINGS 字段映射正确性
*对于任意* ODS 表行和任意已配置的 `FACT_MAPPINGS` 条目 `(dwd_col, ods_expr, cast_type)`,当 DWD 加载任务执行后DWD 目标行中 `dwd_col` 列的值应等于从 ODS 行中按 `ods_expr` 提取并按 `cast_type` 转换后的值。
**Validates: Requirements 1.1, 1.2, 2.1, 3.1, 4.1, 5.1, 6.1, 6.2, 7.2, 8.2, 9.1, 10.3, 11.1**
### Property 2FACT_MAPPINGS 引用完整性
*对于任意* `FACT_MAPPINGS` 中的映射条目,其 DWD 目标列名必须存在于对应 DWD 表的列定义中,其 ODS 源表达式引用的列名必须存在于对应 ODS 表的列定义中(或为合法的 SQL 表达式)。
**Validates: Requirements 6.3**
### Property 3TABLE_MAP 覆盖完整性
*对于任意*`TABLE_MAP` 中注册的 DWD 表,该表的所有非 SCD2 列要么在 `FACT_MAPPINGS` 中有显式映射,要么在对应 ODS 表中存在同名列(自动映射)。
**Validates: Requirements 7.2, 8.2**
### Property 4映射错误修正后数据一致性
*对于任意* 已修正映射的字段assistant_service_records.site_assistant_id、store_goods_sales_records.discount_price、store_goods_master.batch_stock_qty、store_goods_master.provisional_total_cost修正后 DWD 目标列的值应等于从正确的 ODS 源列提取的值,而非修正前的错误源列。
**Validates: Requirements 2.1, 4.1, 10.3**
### Property 5ETL 参数解析与 CLI 命令构建正确性
*对于任意* 合法的 ETL 执行参数组合门店列表、数据源模式、校验模式、时间范围、窗口切分、force-full 标志、任务选择Backend 构建的 CLI 命令字符串应包含所有指定参数,且参数值与输入一致。
**Validates: Requirements 14.1, 14.2**
### Property 6数据一致性检查正确性
*对于任意* ODS 行和对应的 DWD 行,黑盒测试检查器应能正确识别:(a) ODS 中存在但 DWD 中缺失的字段,(b) ODS 与 DWD 之间值不一致的字段。
**Validates: Requirements 16.2, 16.3**
### Property 7计时器记录完整性
*对于任意* ETL 步骤序列,计时器输出应包含每个步骤的名称、开始时间、结束时间和耗时,且耗时等于结束时间减去开始时间。
**Validates: Requirements 15.2**
### Property 8DWS 库存汇总粒度聚合正确性
*对于任意* DWD 库存汇总数据集和任意汇总粒度(日/周/月DWS 汇总任务的 transform 输出应满足:(a) 每条记录的 `stat_period` 与任务粒度一致,(b) 同一 `(site_id, stat_date, site_goods_id)` 组合不重复,(c) 日度汇总的记录数不少于周度和月度汇总的记录数。
**Validates: Requirements 12.2, 12.3, 12.4, 12.5, 12.6**
## 错误处理
### 字段补全错误处理
| 场景 | 处理方式 |
|------|---------|
| DDL 迁移失败 | 回滚事务,记录错误日志,不影响其他表 |
| ODS 列不存在 | 跳过该映射条目,记录 WARNING 日志 |
| 类型转换失败 | 使用 NULLIF + CAST 兜底,转换失败写入 NULL |
| 新建 DWD 表主键冲突 | 使用 ON CONFLICT DO UPDATE 策略 |
### DWS 库存汇总错误处理
| 场景 | 处理方式 |
|------|---------|
| DWD 源表无数据 | 跳过汇总,记录 WARNING 日志 |
| 跨周/跨月边界数据不完整 | 按已有数据汇总,不补零 |
| upsert 主键冲突 | 使用 ON CONFLICT DO UPDATE 更新已有记录 |
| DWD 表尚未创建(前置依赖未完成) | 抛出明确错误,提示需先完成需求 7 |
### 前后端联调错误处理
| 场景 | 处理方式 |
|------|---------|
| 参数校验失败 | 返回 422 状态码,附带详细错误信息 |
| ETL 子进程超时 | 设置超时阈值,超时后终止进程并返回错误 |
| WebSocket 断连 | 前端自动重连,后端缓存最近日志 |
| 黑盒测试发现不一致 | 记录差异明细到报告,不中断流程 |
## 测试策略
### 属性测试
使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
- **Property 1-3**:通过 FakeDB 模拟 ODS/DWD 表结构,生成随机 ODS 行数据,验证 FACT_MAPPINGS 映射逻辑
- **Property 4**:对修正后的映射字段,验证 DWD 值来自正确的 ODS 源列
- **Property 5**:生成随机参数组合,验证 CLI 命令构建
- **Property 6**:生成随机 ODS/DWD 行对,验证一致性检查逻辑
- **Property 7**:生成随机步骤序列,验证计时器输出
- **Property 8**:生成随机 DWD 库存数据,验证日/周/月三个粒度的聚合逻辑正确性
测试标签格式:`Feature: dataflow-field-completion, Property N: {property_text}`
### 单元测试
- DDL 迁移脚本语法正确性SQL 解析)
- 各表 FACT_MAPPINGS 条目的具体映射值验证
- DWS 库存汇总任务的边界值测试(跨周/跨月数据、空数据集)
- 前端参数表单的边界值测试
- 计时器精度测试
### 集成测试
- 端到端 ETL 执行:从 API JSON 到 DWD 落库的完整流程
- 前后端联调:从 Admin Web 触发到 ETL 完成的完整流程
- 黑盒测试:全量数据一致性验证
### 测试工具
- ETL 单元测试使用 `tests/unit/task_test_utils.py` 提供的 FakeDB/FakeAPI
- 属性测试使用 `hypothesis`
- 后端测试使用 `pytest` + FastAPI TestClient

View File

@@ -0,0 +1,228 @@
# 需求文档:数据流字段补全与前后端联调
## 简介
本特性基于 `dataflow_2026-02-19_190440.md` 数据流分析报告,完成三大任务:
1. 补全 11 张 ODS/DWD 表中缺失的字段映射(含 DDL 更新、ETL loader/task 代码同步、文档精化)
2. 在 DWS 层新建库存汇总表,支持日/周/月三个粒度的库存数据汇总
3. 管理后台admin-web前后端联调确保 ETL 全流程可通过 Web 界面正确触发和执行
## 术语表
- **ETL_System**:飞球连接器 ETL 系统(`apps/etl/connectors/feiqiu/`),负责从上游 API 抽取数据并经 ODS→DWD→DWS 三层处理
- **ODS**Operational Data Store原始数据层保留 API 返回的原始字段
- **DWD**Data Warehouse Detail明细数据层经清洗、标准化后的业务字段
- **DDL**Data Definition Language数据库表结构定义位于 `db/etl_feiqiu/schemas/`
- **Loader**ETL 加载器(`loaders/`),负责将 ODS 数据清洗映射到 DWD 表
- **Task**ETL 任务(`tasks/`),编排 loader 的执行逻辑
- **Admin_Web**:管理后台(`apps/admin-web/`React + Vite + Ant Design 前端
- **Backend**FastAPI 后端(`apps/backend/`),提供 ETL 调度和数据查询 API
- **SCD2**:缓慢变化维度类型 2用于维度表历史版本追踪
- **BD_Manual**:业务数据字典文档(`docs/database/`),记录字段含义和映射关系
- **Field_Mapping**:字段映射关系,描述 API JSON → ODS 列 → DWD 列的对应关系
- **DWS**Data Warehouse Summary汇总数据层按业务维度聚合的统计数据
- **BaseDwsTask**DWS 任务基类(`tasks/dws/base_dws_task.py`),提供 extract/transform/load 三阶段框架
## 执行依据
本需求文档的字段补全部分基于以下排查结论文档:
- `export/SYSTEM/REPORTS/field_audit/field_review_for_user.md` — 逐表逐字段的排查结论与操作建议
## 需求
### 需求 1assistant_accounts_master 字段补全
**用户故事:** 作为数据工程师,我希望将助教账号档案表中 4 个未映射的 ODS 字段system_role_id、job_num、cx_unit_price、pd_unit_price补全到 DWD 层,以便下游分析可以使用完整的助教档案数据。
#### 验收标准
1. WHEN ETL_System 执行 assistant_accounts_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `system_role_id` 映射到 DWD 目标表 `dim_assistant_ex` 的对应列
2. WHEN ETL_System 执行 assistant_accounts_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `job_num``cx_unit_price``pd_unit_price` 映射到 DWD 目标表 `dim_assistant_ex` 的对应列
3. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `db/etl_feiqiu/schemas/dwd.sql` 中包含对应的 ALTER TABLE 或 CREATE 语句
4. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档,消除"待补充""待分析"等模糊描述
### 需求 2assistant_service_records 字段补全
**用户故事:** 作为数据工程师,我希望将助教服务流水表中 3 个未映射的 ODS 字段site_assistant_id、operator_id、operator_name补全到 DWD 层,以便追踪服务操作员信息。
#### 验收标准
1. WHEN ETL_System 执行 assistant_service_records 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `site_assistant_id``operator_id``operator_name` 映射到 DWD 目标表 `dwd_assistant_service_log_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 3assistant_cancellation_records 字段补全
**用户故事:** 作为数据工程师,我希望将助教废除记录表中 1 个未映射的 ODS 字段assistanton补全到 DWD 层。
#### 验收标准
1. WHEN ETL_System 执行 assistant_cancellation_records 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `assistanton` 映射到 DWD 目标表 `dwd_assistant_trash_event_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 对 `assistanton` 字段进行语义分析并补充精确说明
### 需求 4store_goods_sales_records 字段补全
**用户故事:** 作为数据工程师,我希望将门店商品销售流水表中 1 个未映射的 ODS 字段discount_price补全到 DWD 层,以便分析折后单价。
#### 验收标准
1. WHEN ETL_System 执行 store_goods_sales_records 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `discount_price` 映射到 DWD 目标表 `dwd_store_goods_sale_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义,类型为 `numeric`(金额精度)
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 5member_balance_changes 字段补全
**用户故事:** 作为数据工程师,我希望将会员余额变动表中 1 个未映射的 ODS 字段relate_id补全到 DWD 层,以便关联充值记录或订单。
#### 验收标准
1. WHEN ETL_System 执行 member_balance_changes 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `relate_id` 映射到 DWD 目标表 `dwd_member_balance_change_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 6recharge_settlements 字段补全与映射建立
**用户故事:** 作为数据工程师,我希望将充值结算表中 5 个 ODS→DWD 未映射字段补全,并为 5 个 DWD 无 ODS 源字段建立正确的映射关系,以便电费和券销售额数据完整流转。
#### 验收标准
1. WHEN ETL_System 执行 recharge_settlements 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `electricityadjustmoney``electricitymoney``mervousalesamount``plcouponsaleamount``realelectricitymoney` 映射到 DWD 目标表 `dwd_recharge_order` 的对应列
2. WHEN DWD 表存在无 ODS 源的列(`pl_coupon_sale_amount``mervou_sales_amount``electricity_money``real_electricity_money``electricity_adjust_money`, THE Loader SHALL 建立从 ODS 对应列到这些 DWD 列的映射关系
3. WHEN 映射建立后, THE ETL_System SHALL 确保 ODS 列名(驼峰式)与 DWD 列名(蛇形式)之间的命名转换正确
4. WHEN 字段映射完成后, THE DDL SHALL 同步更新THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 7goods_stock_summary 新建 DWD 表与字段映射
**用户故事:** 作为数据工程师,我希望为库存汇总表新建 DWD 目标表,并将 14 个 ODS 字段完整映射,以便库存数据可在 DWD 层使用。
#### 验收标准
1. WHEN ETL_System 需要加载 goods_stock_summary 数据到 DWD 层, THE DDL SHALL 在 `dwd.sql` 中创建新的 DWD 目标表(如 `dwd_goods_stock_summary`
2. WHEN DWD 目标表创建后, THE Loader SHALL 将全部 14 个 ODS 列sitegoodsid、goodsname、goodsunit、goodscategoryid、goodscategorysecondid、categoryname、rangestartstock、rangeendstock、rangein、rangeout、rangesale、rangesalemoney、rangeinventory、currentstock映射到 DWD 目标表
3. WHEN 新表创建后, THE ETL_System SHALL 创建对应的 DWD loader 和 task 代码
4. WHEN 新表创建后, THE BD_Manual SHALL 为新表编写完整的字段说明文档
### 需求 8goods_stock_movements 新建 DWD 表与字段映射
**用户故事:** 作为数据工程师,我希望为库存变化记录表新建 DWD 目标表,并将 19 个 ODS 字段完整映射,以便库存变动明细可在 DWD 层使用。
#### 验收标准
1. WHEN ETL_System 需要加载 goods_stock_movements 数据到 DWD 层, THE DDL SHALL 在 `dwd.sql` 中创建新的 DWD 目标表(如 `dwd_goods_stock_movement`
2. WHEN DWD 目标表创建后, THE Loader SHALL 将全部 19 个 ODS 列映射到 DWD 目标表
3. WHEN 新表创建后, THE ETL_System SHALL 创建对应的 DWD loader 和 task 代码
4. WHEN 新表创建后, THE BD_Manual SHALL 为新表编写完整的字段说明文档
### 需求 9site_tables_master 字段补全
**用户故事:** 作为数据工程师,我希望将台桌维表中 14 个未映射的 ODS 字段补全到 DWD 层,以便台桌配置信息完整可用。
#### 验收标准
1. WHEN ETL_System 执行 site_tables_master 的 DWD 加载任务, THE Loader SHALL 将 14 个 ODS 列sitename、appletQrCodeUrl、audit_status、charge_free、create_time、delay_lights_time、is_rest_area、light_status、only_allow_groupon、order_delay_time、self_table、tablestatusname、temporary_light_second、virtual_table映射到 DWD 目标表 `dim_table_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档,消除"待补充""待分析"等模糊描述
### 需求 10store_goods_master 字段补全与嵌套展开
**用户故事:** 作为数据工程师我希望将门店商品档案表中的平层未映射字段、嵌套对象字段、ODS→DWD 未映射字段全部补全,以便商品档案数据完整。
#### 验收标准
1. WHEN ETL_System 执行 store_goods_master 的 ODS 加载任务, THE Loader SHALL 将 API 平层字段 `time_slot_sale` 映射到 ODS 表的对应列
2. WHEN ETL_System 执行 store_goods_master 的 ODS 加载任务, THE Loader SHALL 将嵌套对象 `goodsStockWarningInfo` 的 4 个子字段site_goods_id、sales_day、warning_day_max、warning_day_min展开并映射到 ODS 表的对应列
3. WHEN ETL_System 执行 store_goods_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `batch_stock_quantity``provisional_total_cost` 以及展开后的库存预警字段映射到 DWD 目标表(根据字段用途自动分配到 `dim_store_goods``dim_store_goods_ex`
4. WHEN 新字段被添加, THE DDL SHALL 同步更新 `ods.sql``dwd.sql`
5. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 11tenant_goods_master 字段补全
**用户故事:** 作为数据工程师,我希望将租户商品档案表中 1 个未映射的 ODS 字段commoditycode补全到 DWD 层。
#### 验收标准
1. WHEN ETL_System 执行 tenant_goods_master 的 DWD 加载任务, THE Loader SHALL 将 ODS 列 `commoditycode` 映射到 DWD 目标表 `dim_tenant_goods_ex` 的对应列
2. WHEN 新字段被添加到 DWD 表, THE DDL SHALL 在 `dwd.sql` 中包含对应的列定义
3. WHEN 字段映射完成后, THE BD_Manual SHALL 更新对应的字段说明文档
### 需求 12DWS 库存汇总(日/周/月)
**用户故事:** 作为数据分析师,我希望在 DWS 层拥有日度、周度、月度三个粒度的库存汇总表,以便按不同时间维度分析商品库存变化趋势。
#### 验收标准
1. WHEN 需求 7goods_stock_summary 新建 DWD 表)完成且 ODS 任务配置已修改(`requires_window=True` + `time_fields=("startTime", "endTime")`)并重新采集数据后, THE ETL_System SHALL 具备构建 DWS 库存汇总的数据基础
2. WHEN ETL_System 执行 DWS_GOODS_STOCK_DAILY 任务, THE ETL_System SHALL 从 DWD 层 `dwd_goods_stock_summary` 提取数据,按日粒度汇总并写入 `dws.dws_goods_stock_daily_summary`
3. WHEN ETL_System 执行 DWS_GOODS_STOCK_WEEKLY 任务, THE ETL_System SHALL 从 DWD 层提取数据,按周粒度汇总并写入 `dws.dws_goods_stock_weekly_summary`
4. WHEN ETL_System 执行 DWS_GOODS_STOCK_MONTHLY 任务, THE ETL_System SHALL 从 DWD 层提取数据,按月粒度汇总并写入 `dws.dws_goods_stock_monthly_summary`
5. THE DWS 库存汇总表 SHALL 包含以下字段site_id、tenant_id、stat_date汇总日期、site_goods_id、goods_name、goods_unit、goods_category_id、goods_category_second_id、category_name商品维度、range_start_stock、range_end_stock、range_in、range_out、range_sale、range_sale_money、range_inventory、current_stock库存指标、stat_period汇总粒度标识'daily'/'weekly'/'monthly'
6. THE DWS 库存汇总表 SHALL 以 `(site_id, stat_date, site_goods_id)` 为主键,支持按门店、日期、商品维度的唯一性约束
7. WHEN DWS 库存汇总任务执行时, THE ETL_System SHALL 继承 `BaseDwsTask`,实现 `extract` / `transform` / `load` 三阶段
8. WHEN DWS 库存汇总表创建后, THE DDL SHALL 在 `db/etl_feiqiu/schemas/dws.sql` 中包含建表语句,迁移脚本放在 `db/etl_feiqiu/migrations/`
9. WHEN DWS 库存汇总任务代码创建后, THE ETL_System SHALL 将任务代码放在 `apps/etl/connectors/feiqiu/tasks/dws/` 目录下
### 需求 17彻底移除 settlement_ticket_details
**用户故事:** 作为数据工程师,我希望从项目中彻底移除 settlement_ticket_details结账小票详情相关的所有代码、DDL、配置、文档和数据以便简化系统维护并消除无用的数据流。
#### 验收标准
1. WHEN 移除任务完成后, THE ETL_System SHALL 不再包含 `ODS_SETTLEMENT_TICKET` 任务代码(从 `ods_tasks.py``ENABLED_ODS_CODES``ODS_TASK_CLASSES``OdsSettlementTicketTask` 类中移除)
2. WHEN 移除任务完成后, THE DDL SHALL 不再包含 `ods.settlement_ticket_details` 表定义(从 `ods.sql` / `schema_ODS_doc.sql` 中移除建表语句和注释)
3. WHEN 移除任务完成后, THE ETL_System SHALL 从以下位置移除所有 settlement_ticket_details 引用:
- `tasks/ods/ods_tasks.py`OdsTaskSpec、OdsSettlementTicketTask 类、ENABLED_ODS_CODES
- `tasks/verification/dwd_verifier.py``tasks/verification/ods_verifier.py`
- `tasks/utility/manual_ingest_task.py`
- `utils/json_store.py`
- `scripts/check/check_ods_gaps.py`
- `scripts/debug/debug_blackbox.py`
4. WHEN 移除任务完成后, THE ETL_System SHALL 从 `db/etl_feiqiu/seeds/seed_ods_tasks.sql` 中移除 `ODS_SETTLEMENT_TICKET`
5. WHEN 移除任务完成后, THE BD_Manual SHALL 从 `docs/database/etl_feiqiu_schema_migration.md` 和 ETL 任务文档中移除相关条目
6. WHEN 移除任务完成后, THE ETL_System SHALL 编写迁移脚本 `DROP TABLE IF EXISTS ods.settlement_ticket_details``DROP INDEX IF EXISTS ods.idx_ods_settlement_ticket_details_latest`
7. WHEN 移除任务完成后, THE ETL_System SHALL 从 `scripts/ops/` 下的分析脚本(`gen_full_dataflow_doc.py``gen_field_review_doc.py``gen_api_field_mapping.py``field_audit.py``export_dwd_field_review.py``dataflow_analyzer.py``check_ods_latest_indexes.py`)中移除相关引用
8. WHEN 移除任务完成后, THE ETL_System SHALL 从单元测试 `tests/unit/test_ods_tasks.py` 中移除 `test_ods_settlement_ticket_by_payment_relate_ids` 测试
### 需求 13文档精化
**用户故事:** 作为数据工程师,我希望对所有涉及的 BD_Manual 文档进行精细化更新,消除所有模糊描述,以便团队成员可以准确理解每个字段的含义。
#### 验收标准
1. WHEN 文档精化任务执行时, THE BD_Manual SHALL 逐个文档、逐项排查所有"待补充""待处理""未确定""未定义"等缺失内容
2. WHEN 文档精化任务执行时, THE BD_Manual SHALL 将"金额字段""XX 相关""XXX 类"等粗略说明替换为精确的字段语义描述
3. WHEN 字段说明需要精化时, THE ETL_System SHALL 通过手动字段名称分析、上下文推测、遍历值/枚举值分析、代码取用情况分析来确定字段含义
4. WHEN 文档更新完成后, THE BD_Manual SHALL 确保每个字段说明包含:字段类型、业务含义、取值范围或枚举值、在代码中的使用位置
### 需求 14Admin-Web 前后端联调
**用户故事:** 作为系统管理员,我希望通过管理后台 Web 界面触发 ETL 全流程执行,以便可视化管理数据处理任务。
#### 验收标准
1. WHEN 管理员在 Admin_Web 中配置 ETL 参数全部门店、api_full、仅校验修复且校验前从 API 获取、自定义范围 2025-11-01 至 2026-02-20、窗口切分 10 天、force-full、全选常用功能, THE Backend SHALL 正确接收并解析这些参数
2. WHEN Backend 接收到 ETL 执行请求, THE Backend SHALL 将参数转换为 ETL_System 可识别的命令并触发执行
3. WHEN ETL 任务执行时, THE Admin_Web SHALL 实时展示任务执行状态和进度
4. WHEN 所有选中的任务执行完成后, THE ETL_System SHALL 确保数据处理结果正确(源数据与落库数据/字段一致)
### 需求 15ETL 执行计时机制
**用户故事:** 作为系统管理员,我希望 ETL 执行过程中有详细的计时记录,以便分析各步骤的性能瓶颈。
#### 验收标准
1. WHEN ETL 任务开始执行, THE ETL_System SHALL 启动计时器,记录每个步骤和分步骤的开始时间
2. WHEN 每个步骤完成时, THE ETL_System SHALL 记录该步骤的耗时(精确到毫秒)
3. WHEN 全部任务执行完成后, THE ETL_System SHALL 输出详细颗粒度的计时结果文档,包含每个步骤名称、开始时间、结束时间、耗时
### 需求 16黑盒测试机制
**用户故事:** 作为质量保证工程师,我希望在 ETL 全流程完成后执行黑盒测试,验证数据源与落库数据的一致性。
#### 验收标准
1. WHEN 所有 ETL 步骤顺利完成后, THE ETL_System SHALL 以黑盒测试者角度检查数据源和落库数据/字段情况是否一致
2. WHEN 黑盒测试执行时, THE ETL_System SHALL 对比 API 源数据与 ODS 落库数据的字段完整性
3. WHEN 黑盒测试执行时, THE ETL_System SHALL 对比 ODS 数据与 DWD 落库数据的映射正确性
4. WHEN 黑盒测试完成后, THE ETL_System SHALL 输出黑盒测试报告,包含每张表的检查结果、差异明细、通过/失败状态

View File

@@ -0,0 +1,304 @@
# 实现计划:数据流字段补全与前后端联调
## 概述
按"先排查确认 → 再 DDL 变更 → 再代码映射 → 再移除废弃表 → 再 DWS 汇总 → 再文档精化 → 最后联调"的顺序,分阶段推进。每个表的字段补全遵循"先确认再新增"原则。
执行依据:`export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`(逐表逐字段排查结论)
## 任务
- [x] 1. 字段排查脚本与基础设施
- [x] 1.1 编写字段排查脚本 `scripts/ops/field_audit.py`
- 连接数据库,对每张目标表执行排查流程:查 DWD 现有列、查 FACT_MAPPINGS 现状、查 ODS 列、查自动映射
- 输出排查记录表markdown 格式),标注每个字段的排查结论和建议操作
- 覆盖 11 张表的所有疑似缺失字段
- _Requirements: 1.1-1.4, 2.1-2.3, 3.1-3.3, 4.1-4.3, 5.1-5.3, 6.1-6.4, 7.1-7.4, 8.1-8.4, 9.1-9.3, 10.1-10.5, 11.1-11.3_
- [x] 1.2 执行排查脚本,生成排查报告
- 运行脚本,审查输出结果
- _Requirements: 1.1-1.4_
- [x] 1.3 逐字段调查与推测,确认排查结论(由 Kiro 执行)
- 结论文档:`export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`
- 脚本输出仅为线索,不能直接作为最终结论
- 对脚本标记为"缺失"或"对不齐"的每个字段Kiro 需逐一执行以下调查:
- 查阅 FACT_MAPPINGS 源码,确认是否已以其他名称/表达式映射
- 查阅 DWD DDL确认是否已有同语义列命名差异
- 查阅 ODS loader 代码,确认 ODS 列是否真实写入
- 结合字段命名规律、上下文语义、业务逻辑进行推测
- 必要时查询数据库实际数据SELECT DISTINCT / 采样)辅助判断
- 对每个字段标注最终决策:无需变更 / 仅补映射 / 新增列+映射 / 跳过(附理由)
- 将调查过程和推测依据记录到排查报告中,确保可追溯
- _Requirements: 1.1-1.4, 2.1-2.3, 3.1-3.3, 4.1-4.3, 5.1-5.3, 6.1-6.4, 7.1-7.4, 8.1-8.4, 9.1-9.3, 10.1-10.5, 11.1-11.3_
- [x] 2. Checkpoint - 排查结果确认
- 此项已完成,最终文档为:`export/SYSTEM/REPORTS/field_audit/field_review_for_user.md`
- [x] 3. 🔴 映射错误修复(高优先级)
- 依据:`field_review_for_user.md` 映射错误修复章节
- [x] 3.1 assistant_service_records — site_assistant_id 映射错误修正
- 当前问题DWD `dwd_assistant_service_log.site_assistant_id` 错误映射自 ODS `order_assistant_id`(订单级 ID应映射自 ODS `site_assistant_id`(助教档案 ID
- 修正 FACT_MAPPINGS将 DWD `site_assistant_id` 的 ODS 源从 `order_assistant_id` 改为 `site_assistant_id`
- 新增 DWD 列 `order_assistant_id`bigint`dwd_assistant_service_log`,映射 ODS `order_assistant_id`
- 编写迁移脚本
- 需重新加载历史数据
- _Requirements: 2.1, 2.2_
- [x] 3.2 store_goods_sales_records — discount_price 列名误导修正
- 当前问题DWD `discount_price` 实际映射自 ODS `discount_money`(折扣金额),而非 ODS `discount_price`(折后单价)
- 将 DWD 列名 `discount_price` 重命名为 `discount_money`
- 新增 DWD 列 `discount_price`numeric映射 ODS `discount_price`(折后单价)
- 编写迁移脚本
- _Requirements: 4.1, 4.2_
- [x] 3.3 store_goods_master — batch_stock_qty 和 provisional_total_cost 映射源修正
- `batch_stock_qty` 的 FACT_MAPPINGS 从 `stock`(当前库存)改为 `batch_stock_quantity`(批次库存)
- `provisional_total_cost` 的 FACT_MAPPINGS 从 `total_purchase_cost`(实际采购成本)改为 `provisional_total_cost`(暂估成本)
- 需重新加载历史数据
- _Requirements: 10.3_
- [x] 4. A 类表:新增 DWD 列 + FACT_MAPPINGS
- 依据:`field_review_for_user.md` 各表"待新增/补映射字段"
- [x] 4.1 assistant_accounts_master — 新增 4 个字段到 dim_assistant_ex
- 新增列:`system_role_id`bigint`job_num`text`cx_unit_price`numeric(18,2))、`pd_unit_price`numeric(18,2)
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 1.1, 1.2, 1.3_
- [x] 4.2 assistant_service_records — 新增 2 个字段到 dwd_assistant_service_log_ex
- 新增列:`operator_id`bigint`operator_name`text
- 跳过 `siteprofile`jsonb 嵌套列)
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 2.1, 2.2_
- [x] 4.3 assistant_cancellation_records — 更新 FACT_MAPPINGS
- 更新映射ODS `assistanton` → DWD `dwd_assistant_trash_event.assistant_no`
- 跳过 `siteprofile`jsonb 嵌套列)
- _Requirements: 3.1_
- [x] 4.4 member_balance_changes — 新增 1 个字段到 dwd_member_balance_change_ex
- 新增列:`relate_id`bigint— 关联业务单据 ID
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 5.1, 5.2_
- [x] 4.5 site_tables_master — 新增 14 个字段到 dim_table_ex
- 新增列:`create_time`timestamptz`light_status`integer`tablestatusname`text`sitename`text`appletQrCodeUrl`text`audit_status`integer`charge_free`integer`delay_lights_time`integer`is_rest_area`integer`only_allow_groupon`integer`order_delay_time`integer`self_table`integer`temporary_light_second`integer`virtual_table`integer
- 更新 `dwd.sql`,添加 FACT_MAPPINGS 条目
- 编写迁移脚本
- _Requirements: 9.1, 9.2_
- [x] 4.6 tenant_goods_master — 无需变更(跳过)
- `commoditycode``commodity_code` 100% 冗余(花括号包裹格式),已确认跳过
- _Requirements: 11.1已确认无需操作_
- [x] 4.7 编写 A 类表字段映射属性测试
- **Property 1: FACT_MAPPINGS 字段映射正确性**
- **Validates: Requirements 1.1, 1.2, 2.1, 3.1, 4.1, 5.1, 9.1**
- [x] 5. B 类表:仅补 FACT_MAPPINGS / 修正映射
- 依据:`field_review_for_user.md` B 类表章节
- [x] 5.1 recharge_settlements — 补充 5 个 FACT_MAPPINGS 条目
- 仅补映射DWD 列已存在ODS/DWD 两侧数据全为 0业务未启用
- `plcouponsaleamount → pl_coupon_sale_amount`
- `mervousalesamount → mervou_sales_amount`
- `electricitymoney → electricity_money`
- `realelectricitymoney → real_electricity_money`
- `electricityadjustmoney → electricity_adjust_money`
- 无需 DDL 变更,无需迁移脚本
- _Requirements: 6.1, 6.2, 6.3_
- [x] 5.2 编写 B 类表属性测试
- **Property 2: FACT_MAPPINGS 引用完整性**
- **Validates: Requirements 6.3**
- [x] 5.5. Checkpoint - 映射修复与 A/B 类表完成确认
- 确保所有映射错误已修正、A/B 类表的 FACT_MAPPINGS、DDL、迁移脚本都已更新
- Ask the user if questions arise.
- [x] 6. C 类表:新建 DWD 表与完整映射
- 依据:`field_review_for_user.md` C 类表章节
- [x] 6.1 goods_stock_summary — 修改 ODS 配置 + 新建 DWD 表
- 步骤 1修改 ODS 任务配置 `requires_window=True` + `time_fields=("startTime", "endTime")`
- 步骤 2重新采集历史数据按时间窗口分批
- 步骤 3编写 DDL 创建 `dwd.dwd_goods_stock_summary`14 个字段)
- 步骤 4`TABLE_MAP` 中注册,在 `FACT_MAPPINGS` 中添加映射
- 步骤 5创建 DWD loader 和 task 代码
- 编写迁移脚本
- _Requirements: 7.1, 7.2, 7.3_
- [x] 6.2 goods_stock_movements — 新建 DWD 表
- 编写 DDL 创建 `dwd.dwd_goods_stock_movement`19 个字段,事实表,按 createtime 增量加载)
-`TABLE_MAP` 中注册,在 `FACT_MAPPINGS` 中添加映射(驼峰 → 蛇形命名)
- 创建 DWD loader 和 task 代码
- 编写迁移脚本
- _Requirements: 8.1, 8.2, 8.3_
- [x] 6.3 编写 C 类表属性测试
- **Property 3: TABLE_MAP 覆盖完整性**
- **Validates: Requirements 7.2, 8.2**
- [x] 7. Checkpoint - 全部字段补全完成
- 确保所有 11 张表的字段补全工作完成所有测试通过ask the user if questions arise.
- [x] 7.3. 彻底移除 settlement_ticket_details
- [x] 7.3.1 移除 ETL 核心代码中的 settlement_ticket_details
-`tasks/ods/ods_tasks.py` 中移除:`OdsTaskSpec("ODS_SETTLEMENT_TICKET", ...)``OdsSettlementTicketTask` 类、`ENABLED_ODS_CODES` 中的条目、`ODS_TASK_CLASSES` 覆盖
-`tasks/verification/dwd_verifier.py` 中移除主键映射条目
-`tasks/verification/ods_verifier.py` 中移除相关注释和特殊处理
-`tasks/utility/manual_ingest_task.py` 中移除表映射和配置
-`utils/json_store.py` 中移除 `/order/getordersettleticketnew` 路径映射
- _Requirements: 17.1, 17.3_
- [x] 7.3.2 移除 DDL、种子数据和迁移脚本
-`db/etl_feiqiu/schemas/ods.sql``schema_ODS_doc.sql` 中移除建表语句和注释
-`db/etl_feiqiu/seeds/seed_ods_tasks.sql` 中移除 `ODS_SETTLEMENT_TICKET`
- 编写迁移脚本:`DROP TABLE IF EXISTS ods.settlement_ticket_details``DROP INDEX``DELETE FROM meta.ods_task_registry`
- _Requirements: 17.2, 17.4, 17.6_
- [x] 7.3.3 移除分析脚本和工具中的引用
-`scripts/ops/` 下移除:`gen_full_dataflow_doc.py``gen_field_review_doc.py``gen_api_field_mapping.py``field_audit.py``export_dwd_field_review.py``dataflow_analyzer.py``check_ods_latest_indexes.py` 中的相关引用
-`scripts/check/check_ods_gaps.py` 中移除 `_check_settlement_tickets` 函数及调用
-`scripts/debug/debug_blackbox.py` 中移除跳过逻辑
- _Requirements: 17.7_
- [x] 7.3.4 移除文档和测试中的引用
-`docs/database/etl_feiqiu_schema_migration.md` 中移除索引条目
-`apps/etl/connectors/feiqiu/docs/etl_tasks/` 中移除任务表格条目
-`tests/unit/test_ods_tasks.py` 中移除 `test_ods_settlement_ticket_by_payment_relate_ids`
- _Requirements: 17.5, 17.8_
- [x] 7.5. DWS 库存汇总(日/周/月)
- 前置依赖:任务 6.1goods_stock_summary DWD 表完成、ODS 任务配置修改(`requires_window=True`)完成并重新采集数据
- [x] 7.5.1 编写 DWS 库存汇总 DDL 与迁移脚本
-`db/etl_feiqiu/schemas/dws.sql` 中添加三张表的建表语句:`dws_goods_stock_daily_summary``dws_goods_stock_weekly_summary``dws_goods_stock_monthly_summary`
- 编写迁移脚本 `db/etl_feiqiu/migrations/{date}__create_dws_goods_stock_summary.sql`
- 主键:`(site_id, stat_date, site_goods_id)`
- 字段site_id, tenant_id, stat_date, site_goods_id, goods_name, goods_unit, goods_category_id, goods_category_second_id, category_name, range_start_stock, range_end_stock, range_in, range_out, range_sale, range_sale_money, range_inventory, current_stock, stat_period, created_at, updated_at
- _Requirements: 12.5, 12.6, 12.8_
- [x] 7.5.2 实现 DWS_GOODS_STOCK_DAILY 任务
-`apps/etl/connectors/feiqiu/tasks/dws/` 下创建 `goods_stock_daily_task.py`
- 继承 `BaseDwsTask`,实现 `extract` / `transform` / `load` 三阶段
- extract`dwd.dwd_goods_stock_summary` 按时间范围查询
- transform按日粒度汇总`stat_period='daily'`
- loadupsert 写入 `dws.dws_goods_stock_daily_summary`
- _Requirements: 12.2, 12.7_
- [x] 7.5.3 实现 DWS_GOODS_STOCK_WEEKLY 任务
- 创建 `goods_stock_weekly_task.py`,按 ISO 周分组,`stat_date` = 周一日期,`stat_period='weekly'`
- _Requirements: 12.3, 12.7_
- [x] 7.5.4 实现 DWS_GOODS_STOCK_MONTHLY 任务
- 创建 `goods_stock_monthly_task.py`,按自然月分组,`stat_date` = 月首日期,`stat_period='monthly'`
- _Requirements: 12.4, 12.7_
- [x] 7.5.5 注册 DWS 库存汇总任务到任务调度
- 在任务注册表中添加 `DWS_GOODS_STOCK_DAILY``DWS_GOODS_STOCK_WEEKLY``DWS_GOODS_STOCK_MONTHLY`
- 确保任务依赖关系正确(依赖 DWD goods_stock_summary 加载完成)
- _Requirements: 12.9_
- [x] 7.5.6 编写 DWS 库存汇总属性测试
- **Property 8: DWS 库存汇总粒度聚合正确性**
- **Validates: Requirements 12.2, 12.3, 12.4, 12.5, 12.6**
- [x] 8. 文档精化
- [x] 8.1 精化 A/B/C 类表涉及的 BD_Manual 文档
- 逐个文档、逐项排查所有"待补充""待处理""未确定""未定义"等缺失内容
- 将"金额字段""XX 相关""XXX 类"等粗略说明替换为精确的字段语义描述
- 通过字段名称分析、上下文推测、遍历值/枚举值分析、代码取用情况分析确定字段含义
- 确保每个字段说明包含:字段类型、业务含义、取值范围或枚举值、在代码中的使用位置
- _Requirements: 13.1, 13.2, 13.3, 13.4_
- [x] 8.2 更新 dataflow 分析报告中涉及表的字段说明
- 同步更新 `docs/database/` 下对应的文档
- _Requirements: 1.4, 2.3, 3.3, 4.3, 5.3, 6.4, 7.4, 8.4, 9.3, 10.5, 11.3_
- [x] 9. Checkpoint - 文档精化完成
- 确保所有文档更新完毕,无遗留的"待补充"标记ask the user if questions arise.
- [x] 10. Admin-Web 前后端联调
- [x] 10.1 排查并修复后端 ETL 执行 API
- 检查 `apps/backend/app/routers/` 中 ETL 执行相关路由
- 确保参数解析正确全部门店、api_full、仅校验修复且校验前从 API 获取、自定义范围 2025-11-01 至 2026-02-20、窗口切分 10 天、force-full、全选常用功能
- 确保参数正确转换为 ETL CLI 命令
- _Requirements: 14.1, 14.2_
- [x] 10.2 排查并修复前端 TaskManager 页面
- 检查 `apps/admin-web/src/pages/TaskManager.tsx` 中的参数配置表单
- 确保所有参数选项可正确选择和提交
- 确保任务执行状态实时展示WebSocket 日志流)
- _Requirements: 14.3_
- [x] 10.3 实现 ETL 执行计时器模块
-`apps/etl/connectors/feiqiu/utils/` 中新增计时器工具
- 记录每个步骤和分步骤的开始时间、结束时间、耗时(精确到毫秒)
- 全部任务完成后输出计时结果文档
- _Requirements: 15.1, 15.2, 15.3_
- [x] 10.4 编写计时器属性测试
- **Property 7: 计时器记录完整性**
- **Validates: Requirements 15.2**
- [x] 10.5 编写 ETL 参数解析属性测试
- **Property 5: ETL 参数解析与 CLI 命令构建正确性**
- **Validates: Requirements 14.1, 14.2**
- [x] 11. 黑盒测试机制
- [x] 11.1 实现数据一致性检查器
-`apps/etl/connectors/feiqiu/quality/` 中新增一致性检查模块
- 实现 API 源数据 vs ODS 落库数据的字段完整性对比
- 实现 ODS 数据 vs DWD 落库数据的映射正确性对比
- 输出黑盒测试报告(每张表的检查结果、差异明细、通过/失败状态)
- _Requirements: 16.1, 16.2, 16.3, 16.4_
- [x] 11.2 编写数据一致性检查属性测试
- **Property 6: 数据一致性检查正确性**
- **Validates: Requirements 16.2, 16.3**
- [x] 12. 端到端联调验证
- [x] 12.1 执行完整 ETL 流程并验证数据正确性
-`EtlTimer` 集成到 `orchestration/flow_runner.py``FlowRunner.run()` 方法中
- 增量 ETL 和校验分支均包裹计时步骤(`INCREMENT_ETL``VERIFICATION``FETCH_BEFORE_VERIFY`
- `timer.finish(write_report=True)` 在 Flow 结束时自动输出计时报告到 `ETL_REPORT_ROOT`
- 产出物:`export/ETL-Connectors/feiqiu/REPORTS/etl_timing_*.md`
- _Requirements: 14.4, 15.3_
- [x] 12.2 执行黑盒测试并生成报告
-`ConsistencyChecker` 集成到 `FlowRunner.run()``_run_post_consistency_check()` 方法中
- ETL Flow 完成后自动运行一致性检查API vs ODS + ODS vs DWD输出报告到 `ETL_REPORT_ROOT`
- 独立验证脚本 `scripts/ops/run_post_etl_reports.py` 确认报告生成正常
- 实际执行结果API vs ODS 22/22 通过ODS vs DWD 38/42 通过
- 产出物:`export/ETL-Connectors/feiqiu/REPORTS/consistency_report_*.md`
- _Requirements: 16.1, 16.4_
- [x] 12.3 前后端浏览器联调验证2026-02-20
- 启动后端 `uvicorn app.main:app --reload`localhost:8000+ 前端 `pnpm dev`localhost:5173
- 浏览器登录管理后台,进入"任务配置"页面
- 配置Flow=ods_dwd, 处理模式=仅增量, dry-run=✓, 本地JSON=✓, 回溯24h
- 点击"直接执行"→ 自动跳转"任务管理 > 历史"tab
- 验证结果status=success, duration=22.5s, exit_code=0
- CLI 命令正确构建:`python -m cli.main --flow ods_dwd --processing-mode increment_only --tasks DWD_LOAD_FROM_ODS --lookback-hours 24 --overlap-seconds 600 --dry-run --data-source offline --store-id 2790685415443269`
- 执行日志实时推送到前端 ModalWebSocket /ws/logs/{id})✅
- 计时报告自动生成:`etl_timing_20260220_073610.md`2步骤总耗时20.78s)✅
- 一致性检查报告自动生成:`consistency_report_20260220_073610.md`
- _Requirements: 14.1, 14.2, 14.3, 14.4, 15.3, 16.4_
- [x] 13. Final checkpoint - 全部完成
- 确保所有字段补全、文档精化、前后端联调、黑盒测试均已完成ask the user if questions arise.
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯
- Checkpoint 确保增量验证
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 所有涉及 `loaders/``tasks/``db/` 的变更属于高风险路径,完成后需触发 `/audit`

View File

@@ -0,0 +1,569 @@
# 设计文档
## 概览
`scripts/ops/gen_full_dataflow_doc.py` 进行重构,将其从单体运维脚本升级为"Python 数据采集 + Kiro Agent 语义分析"的双层架构。
核心设计原则:**Python 脚本负责机械性数据准备API 调用、JSON 展开、DB 查询Kiro Agent 负责需要理解代码和业务上下文的语义工作(映射计算、字段作用推断、统计总结编排、报告组装)。**
改进要点:
1. **JSON 层级完整展开**:递归遍历 JSON 树,用 `.` 路径和 `[]` 数组标记展示完整层级,遍历所有记录拼合最全字段集
2. **数据库驱动的表结构**ODS/DWD 表结构从 `information_schema` + `pg_description` 查询,不依赖 DDL 文件
3. **字段作用说明**:由 Kiro Agent 结合 DDL COMMENT、数据样本、ETL 源码和映射关系推断
4. **详细统计总结**:由 Kiro Agent 编排有业务语义的字段统计和上下游映射总结
5. **CLI 任务化**:支持 `--date-from``--date-to``--limit``--tables` 参数,落盘到 `SYSTEM_ANALYZE_ROOT`
6. **Kiro Hook 集成**`userTriggered` 类型 Hook 手动触发,先执行数据采集脚本,再由 Agent 完成语义分析
## 架构
```mermaid
graph TB
subgraph "触发方式"
CLI["CLI 命令行<br/>python scripts/ops/analyze_dataflow.py"]
HOOK["Kiro Hook<br/>userTriggered 手动触发"]
end
subgraph "第一阶段Python 数据采集"
ANALYZER["dataflow_analyzer.py<br/>核心采集模块"]
CLI_ENTRY["analyze_dataflow.py<br/>CLI 入口 (argparse)"]
end
subgraph "数据源"
FEIQIU_API["飞球 SaaS API"]
PG_INFO["PostgreSQL<br/>information_schema.columns"]
PG_COMMENT["PostgreSQL<br/>pg_catalog.pg_description"]
end
subgraph "中间产物 (SYSTEM_ANALYZE_ROOT/)"
JSON_DUMP["api_samples/<br/>各表 JSON 原始数据"]
JSON_TREE["json_trees/<br/>各表展开后的字段结构 (JSON)"]
DB_SCHEMA["db_schemas/<br/>ODS/DWD 表结构 (JSON)"]
end
subgraph "第二阶段Kiro Agent 语义分析"
MAPPING["映射计算<br/>JSON→ODS, ODS→DWD"]
PURPOSE["字段作用推断<br/>COMMENT + 源码 + 样本"]
SUMMARY["统计总结编排"]
REPORT["Markdown 报告组装"]
end
subgraph "参考源码"
ETL_SRC["ETL 源码<br/>loaders/ tasks/ models/ scd/"]
end
subgraph "最终输出"
MD_FILE["Markdown 报告<br/>SYSTEM_ANALYZE_ROOT/dataflow_YYYY-MM-DD_HHMMSS.md"]
end
CLI --> CLI_ENTRY
HOOK --> CLI_ENTRY
CLI_ENTRY --> ANALYZER
ANALYZER --> FEIQIU_API
ANALYZER --> PG_INFO
ANALYZER --> PG_COMMENT
ANALYZER --> JSON_DUMP
ANALYZER --> JSON_TREE
ANALYZER --> DB_SCHEMA
JSON_TREE --> MAPPING
DB_SCHEMA --> MAPPING
DB_SCHEMA --> PURPOSE
JSON_DUMP --> PURPOSE
ETL_SRC --> PURPOSE
ETL_SRC --> MAPPING
MAPPING --> SUMMARY
PURPOSE --> SUMMARY
SUMMARY --> REPORT
REPORT --> MD_FILE
```
## 组件与接口
### 1. 核心采集模块 `scripts/ops/dataflow_analyzer.py`
`gen_full_dataflow_doc.py` 重构提取,专注于机械性数据采集,不做语义推断。
```python
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from collections import OrderedDict
@dataclass
class AnalyzerConfig:
"""采集配置,由 CLI 参数或 Hook 构造"""
date_from: date | None = None
date_to: date | None = None
limit: int = 200 # 每端点最大记录数
tables: list[str] | None = None # 指定表名None=全部
output_dir: Path = Path("docs/reports") # 落盘目录
pg_dsn: str = ""
api_base: str = ""
api_token: str = ""
store_id: str = ""
@dataclass
class FieldInfo:
"""JSON 字段信息(递归展开后)"""
path: str # 完整路径,如 "data.settleList[].amount"
json_type: str # "string" | "integer" | "number" | "boolean" | "object" | "array" | "null"
sample: str # 样本值(截断到 60 字符)
depth: int # 层级深度0 为顶层)
occurrence: int # 在所有记录中出现的次数
total_records: int # 总记录数
@dataclass
class ColumnInfo:
"""数据库列信息"""
name: str
data_type: str
is_nullable: bool
column_default: str | None
comment: str | None # DDL COMMENT 注释
ordinal_position: int
@dataclass
class TableCollectionResult:
"""单张表的采集结果"""
table_name: str
task_code: str
description: str
endpoint: str
record_count: int
json_fields: OrderedDict[str, FieldInfo] # path -> FieldInfo
ods_columns: list[ColumnInfo]
dwd_columns: list[ColumnInfo]
raw_records_path: Path | None # JSON 原始数据文件路径
error: str | None = None
def flatten_json_tree(
records: list[dict],
) -> OrderedDict[str, FieldInfo]:
"""
递归展开 JSON 记录的完整层级结构。
算法:
1. 对每条记录递归遍历所有嵌套层级
2. 用 '.' 分隔符拼接路径,数组用 '[]' 标记
3. 遍历所有记录拼合最全字段集
4. 统计每个字段的出现频率
返回 path -> FieldInfo 的有序字典(按首次出现顺序)。
"""
...
def _recurse_json(
obj: Any,
prefix: str,
depth: int,
field_map: dict[str, FieldInfo],
total_records: int,
):
"""
递归遍历 JSON 值,填充 field_map。
- dict: 遍历每个 key路径追加 ".key"
- list: 路径追加 "[]",遍历每个元素
- 标量: 记录类型、样本值、出现次数
"""
...
def query_table_columns(
conn, schema: str, table: str,
) -> list[ColumnInfo]:
"""
从 information_schema.columns + pg_description 查询表结构。
SQL:
SELECT c.column_name, c.data_type, c.is_nullable,
c.column_default, c.ordinal_position,
pgd.description AS column_comment
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_statio_all_tables st
ON st.schemaname = c.table_schema
AND st.relname = c.table_name
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = st.relid
AND pgd.objsubid = c.ordinal_position
WHERE c.table_schema = %s AND c.table_name = %s
ORDER BY c.ordinal_position;
返回所有列(含版本控制列如 valid_from, valid_to, is_current, fetched_at
"""
...
def collect_all_tables(
config: AnalyzerConfig,
) -> list[TableCollectionResult]:
"""
执行完整数据采集流程:
1. 根据 config.tables 过滤要分析的表
2. 对每张表:调用 API 获取 JSON → flatten_json_tree 展开
3. 对每张表query_table_columns 查询 ODS 和 DWD 表结构
4. 将原始 JSON 和结构化结果落盘到 config.output_dir
5. 返回所有表的采集结果
错误处理:单表失败不中断,记录 error 字段继续处理其余表。
"""
...
def dump_collection_results(
results: list[TableCollectionResult],
output_dir: Path,
) -> dict[str, Path]:
"""
将采集结果序列化为 JSON 文件落盘。
输出结构:
{output_dir}/
api_samples/{table}.json — API 原始记录
json_trees/{table}.json — 展开后的字段结构
db_schemas/ods_{table}.json — ODS 表结构
db_schemas/dwd_{table}.json — DWD 表结构
collection_manifest.json — 采集清单(表名、记录数、时间戳)
返回 {类别: 目录路径} 的字典。
"""
...
```
### 2. CLI 入口 `scripts/ops/analyze_dataflow.py`
```python
"""
数据流结构分析 — CLI 入口
用法:
python scripts/ops/analyze_dataflow.py
python scripts/ops/analyze_dataflow.py --date-from 2025-01-01 --date-to 2025-01-15
python scripts/ops/analyze_dataflow.py --limit 100 --tables settlement_records,payment_transactions
"""
import argparse
from pathlib import Path
def build_parser() -> argparse.ArgumentParser:
"""
构造 CLI 参数解析器。
参数:
--date-from 数据获取起始日期 (YYYY-MM-DD)
--date-to 数据获取截止日期 (YYYY-MM-DD)
--limit 每端点最大记录数 (默认 200)
--tables 要分析的表名列表 (逗号分隔,缺省=全部)
"""
...
def resolve_output_dir() -> Path:
"""
确定输出目录:
1. 优先读取环境变量 SYSTEM_ANALYZE_ROOT
2. 回退到 docs/reports/
3. 在根目录下创建按日期组织的子目录
"""
...
def main():
"""
1. 解析 CLI 参数
2. 加载环境变量(.env 分层叠加)
3. 构造 AnalyzerConfig
4. 调用 collect_all_tables() 执行采集
5. 调用 dump_collection_results() 落盘
6. 输出采集摘要到 stdout
"""
...
```
### 3. Kiro Hook `.kiro/hooks/dataflow-analyze.kiro.hook`
```json
{
"enabled": true,
"name": "数据流结构分析",
"description": "手动触发数据流结构分析:先执行 Python 脚本采集 API JSON 和 DB 表结构,再由 Kiro Agent 基于采集数据执行语义分析(映射计算、字段作用推断、统计总结),生成 Markdown 报告。",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "prompt",
"prompt": "执行数据流结构分析:\n1. 先运行 `python scripts/ops/analyze_dataflow.py` 完成数据采集\n2. 读取采集结果SYSTEM_ANALYZE_ROOT 或 docs/reports/ 下的 JSON 文件)\n3. 读取 ETL 源码loaders/、tasks/、models/、scd/ 等模块)理解数据流转逻辑\n4. 为每个字段推断作用说明(优先使用 DDL COMMENT结合源码和样本\n5. 计算 JSON→ODS 和 ODS→DWD 映射关系\n6. 编排统计总结(字段覆盖率、类型分布、上下游映射)\n7. 组装最终 Markdown 报告并保存"
}
}
```
Hook 执行流程说明:
- `type: "userTriggered"` — 开发者在 Kiro 中手动触发
- `type: "prompt"` — 触发后由 Kiro Agent 执行 prompt 中描述的完整流程
- Agent 先调用 Python 脚本完成数据采集,再基于采集数据执行语义分析
- 当前仅覆盖飞球连接器;架构预留多连接器扩展(通过 `--tables` 参数或连接器发现机制)
### 4. 中间数据格式
Python 脚本采集后落盘的 JSON 文件,供 Kiro Agent 消费。
**`json_trees/{table}.json`** — 展开后的字段结构:
```json
{
"table": "settlement_records",
"total_records": 200,
"fields": [
{
"path": "data.settleList[].amount",
"json_type": "number",
"sample": "128.50",
"depth": 2,
"occurrence": 198,
"total_records": 200
}
]
}
```
**`db_schemas/ods_{table}.json`** — ODS 表结构:
```json
{
"schema": "ods",
"table": "settlement_records",
"columns": [
{
"name": "amount",
"data_type": "numeric",
"is_nullable": true,
"column_default": null,
"comment": "结算金额",
"ordinal_position": 5
}
]
}
```
**`collection_manifest.json`** — 采集清单:
```json
{
"timestamp": "2025-01-15T14:30:22+08:00",
"config": {
"date_from": "2025-01-01",
"date_to": "2025-01-15",
"limit": 200,
"tables": null
},
"tables": [
{
"table": "settlement_records",
"task_code": "ODS_SETTLEMENT_RECORDS",
"record_count": 200,
"ods_column_count": 25,
"dwd_column_count": 18,
"error": null
}
]
}
```
## 数据模型
### FieldInfoJSON 字段信息)
| 字段 | 类型 | 说明 |
|------|------|------|
| path | str | 完整层级路径,如 `data.settleList[].amount` |
| json_type | str | JSON 类型string / integer / number / boolean / object / array / null |
| sample | str | 样本值(截断到 60 字符) |
| depth | int | 层级深度0 为顶层) |
| occurrence | int | 在所有记录中出现的次数 |
| total_records | int | 总记录数 |
### ColumnInfo数据库列信息
| 字段 | 类型 | 说明 |
|------|------|------|
| name | str | 列名 |
| data_type | str | PostgreSQL 数据类型 |
| is_nullable | bool | 是否可空 |
| column_default | str \| None | 默认值 |
| comment | str \| None | DDL COMMENT 注释(来自 pg_description |
| ordinal_position | int | 列序号 |
### TableCollectionResult单表采集结果
| 字段 | 类型 | 说明 |
|------|------|------|
| table_name | str | 表名 |
| task_code | str | ETL 任务编码,如 `ODS_SETTLEMENT_RECORDS` |
| description | str | 中文描述 |
| endpoint | str | API 端点路径 |
| record_count | int | 获取的记录数 |
| json_fields | OrderedDict[str, FieldInfo] | 展开后的 JSON 字段结构 |
| ods_columns | list[ColumnInfo] | ODS 表结构 |
| dwd_columns | list[ColumnInfo] | DWD 表结构 |
| raw_records_path | Path \| None | 原始 JSON 文件路径 |
| error | str \| None | 错误信息None 表示成功) |
### AnalyzerConfig采集配置
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| date_from | date \| None | None | 数据获取起始日期 |
| date_to | date \| None | None | 数据获取截止日期 |
| limit | int | 200 | 每端点最大记录数 |
| tables | list[str] \| None | None | 指定表名列表None=全部 |
| output_dir | Path | docs/reports/ | 落盘目录 |
| pg_dsn | str | "" | PostgreSQL 连接串 |
| api_base | str | "" | API 基础 URL |
| api_token | str | "" | API 认证令牌 |
| store_id | str | "" | 门店 ID |
### 数据库查询 SQL
```sql
-- 查询表结构(含 COMMENT 注释)
SELECT
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
c.ordinal_position,
pgd.description AS column_comment
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_statio_all_tables st
ON st.schemaname = c.table_schema AND st.relname = c.table_name
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
WHERE c.table_schema = %s AND c.table_name = %s
ORDER BY c.ordinal_position;
```
### Kiro Agent 语义分析输出格式
Agent 在消费中间数据后,生成的最终 Markdown 报告遵循以下表格结构:
**API 源字段表格**(每张表一个):
| # | JSON 路径 | 类型 | 层级 | 出现率 | 示例值 | → ODS 列 | 字段作用 | 处理 |
|---|----------|------|------|--------|--------|----------|----------|------|
**ODS 表格**(每张表一个):
| # | 列名 | 数据类型 | 可空 | COMMENT | ← JSON 来源 | → DWD 列 | 字段作用 |
|---|------|----------|------|---------|-------------|----------|----------|
**DWD 表格**(每张表一个):
| # | 列名 | 数据类型 | 可空 | COMMENT | ← ODS 来源 | 字段作用 | 来源类型 |
|---|------|----------|------|---------|-------------|----------|----------|
> "来源类型"列标注:直接映射 / ETL 派生 / SCD2 版本控制 / 元数据
## 正确性属性
*正确性属性是一种在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
以下属性基于需求文档中的验收标准推导,聚焦于 Python 数据采集模块(`dataflow_analyzer.py`)中可自动化测试的核心逻辑。
> 注:需求 3字段作用说明、需求 4统计总结的验收标准由 Kiro Agent 执行语义分析,不属于可自动化测试的代码逻辑,因此不生成正确性属性。需求 6Kiro Hook为平台集成配置同样不生成属性。
### Property 1: JSON 递归展开路径正确性
*对于任意*嵌套 JSON 对象(含 dict、list、标量的任意组合`flatten_json_tree` 的输出应满足:
- 每个叶子节点的路径使用 `.` 分隔层级
- 数组层级使用 `[]` 标记
- 每个 FieldInfo 的 `depth` 等于路径中 `.` 分隔符的数量(即实际嵌套深度)
- 输出中不遗漏任何叶子节点
**Validates: Requirements 1.1, 1.3, 1.4**
### Property 2: 多记录字段合并完整性与出现频率准确性
*对于任意*一组 JSON 记录列表,`flatten_json_tree` 的输出应满足:
- 合并后的字段路径集合是所有单条记录字段路径集合的并集(不遗漏任何记录中出现过的字段)
- 每个字段的 `occurrence` 等于实际包含该字段的记录数
- 每个字段的 `occurrence``total_records`
- `total_records` 等于输入记录列表的长度
**Validates: Requirements 1.2, 1.5**
### Property 3: 输出文件名格式正确性
*对于任意*合法的日期时间值,生成的输出文件名应匹配模式 `dataflow_YYYY-MM-DD_HHMMSS.md`,且文件名中的日期时间与输入时间一致。
**Validates: Requirements 5.6**
## 错误处理
### 数据采集阶段Python 脚本)
| 错误场景 | 处理策略 | 对应需求 |
|----------|----------|----------|
| API 请求失败(超时/HTTP 错误) | 记录错误到 `TableCollectionResult.error`,跳过该表继续处理其余表 | 2.4 |
| 数据库连接失败 | 记录错误信息ODS/DWD 列信息置空,不中断其余表分析 | 2.4 |
| 表不存在information_schema 查询返回空) | 记录警告,该表的 ods_columns/dwd_columns 为空列表 | 2.4 |
| `SYSTEM_ANALYZE_ROOT` 未配置 | 回退到 `docs/reports/` 默认目录 | 5.5 |
| `SYSTEM_ANALYZE_ROOT` 目录不存在 | 自动创建目录(`mkdir -p` | 5.4 |
| JSON 记录为空列表 | json_fields 为空 OrderedDictrecord_count=0正常落盘 | 1.2 |
| API 返回非 JSON 响应 | 捕获 `json.JSONDecodeError`,记录错误,跳过该表 | — |
| CLI 参数格式错误(如日期格式不合法) | argparse 自动报错退出,显示用法帮助 | 5.1 |
### Kiro Agent 语义分析阶段
| 错误场景 | 处理策略 |
|----------|----------|
| 采集数据文件缺失或损坏 | Agent 报告错误,建议重新运行数据采集脚本 |
| ETL 源码结构变更导致映射推断失败 | Agent 在报告中标注"映射待确认",不阻塞其余字段分析 |
| 某张表采集失败error 非空) | Agent 在报告中标注该表采集失败原因,继续处理其余表 |
## 测试策略
### 测试框架
- 单元测试:`pytest`
- 属性测试:`hypothesis`pytest 插件模式)
- 测试位置:`tests/test_dataflow_analyzer.py`Monorepo 级属性测试目录)
### 属性测试
使用 `hypothesis` 生成随机嵌套 JSON 结构,验证 `flatten_json_tree` 的核心正确性属性。
每个属性测试至少运行 100 次迭代。
| 属性 | 测试方法 | 生成器 |
|------|----------|--------|
| Property 1: JSON 递归展开路径正确性 | 生成任意嵌套 JSONdict/list/标量组合),调用 `flatten_json_tree`,验证路径格式、`[]` 标记、depth 一致性 | `hypothesis.strategies` 递归策略生成嵌套 JSON |
| Property 2: 多记录字段合并完整性 | 生成随机记录列表(部分字段随机缺失),调用 `flatten_json_tree`,验证并集完整性和 occurrence 准确性 | `st.lists(st.dictionaries(...))` |
| Property 3: 输出文件名格式 | 生成随机 datetime调用文件名生成函数验证格式匹配 | `st.datetimes()` |
标注格式:
```python
# Feature: dataflow-structure-audit, Property 1: JSON 递归展开路径正确性
# Validates: Requirements 1.1, 1.3, 1.4
@given(...)
def test_flatten_json_tree_path_correctness(...):
...
```
### 单元测试
针对具体示例和边界情况:
| 测试场景 | 验证内容 | 对应需求 |
|----------|----------|----------|
| 空 JSON 记录列表 | `flatten_json_tree([])` 返回空 OrderedDict | 1.2 边界 |
| 单层 JSON无嵌套 | 路径无 `.` 分隔depth=0 | 1.1 |
| 深层嵌套5+ 层) | 路径正确拼接depth 正确 | 1.1, 1.4 |
| 数组内嵌套对象 | `items[].name` 格式正确 | 1.3 |
| `SYSTEM_ANALYZE_ROOT` 环境变量存在/缺失 | 输出目录正确回退 | 5.4, 5.5 |
| CLI 参数解析 | `--date-from``--date-to``--limit``--tables` 正确解析 | 5.1, 5.2, 5.3 |
| 数据库连接失败时的错误隔离 | 单表失败不影响其余表 | 2.4 |
| Hook 配置文件格式 | JSON 结构正确type 为 userTriggered | 6.1 |

View File

@@ -0,0 +1,110 @@
# 需求文档
## 简介
对现有 `scripts/ops/gen_full_dataflow_doc.py` 全链路数据流文档生成器进行大幅改进,使其能够:
1. 以真实 JSON 数据API 获取)和数据库实际表结构(`information_schema` 查询)为唯一数据源,不依赖 DDL 文件
2. 完整展开 JSON 层级结构,遍历所有记录拼合最全字段集
3. 为每个字段增加"字段作用"说明列,由 Kiro Agent 结合 ETL 源码和业务上下文推断
4. 改进统计总结,由 Kiro Agent 编排有业务语义的字段统计和上下游映射总结
5. 将数据采集脚本任务化,支持 CLI 参数(日期范围、条数),落盘到 `SYSTEM_ANALYZE_ROOT` 目录
6. 全流程通过 Kiro Hook 手动触发Python 脚本负责机械性数据准备Kiro Agent 负责语义分析和报告编排
现有脚本输出样本参见 `export/SYSTEM/REPORTS/full_dataflow_doc/dataflow_api_ods_dwd.md`
## 术语表
- **Analyzer**数据采集脚本Python 模块),负责 JSON 采集和数据库表结构查询,输出结构化中间数据供 Kiro Agent 消费
- **Kiro Agent**Kiro 的 AI 能力,负责映射计算、字段作用推断、统计总结编排等需要理解代码和业务上下文的工作
- **JSON_Tree**API 返回 JSON 数据的完整层级结构,用 `.` 分隔路径表示(如 `data.settleList[].amount`
- **Field_Purpose**:字段作用说明,由 Kiro Agent 结合 DDL COMMENT、JSON 数据样本、ETL 源码和 ODS/DWD 映射关系推断得出
- **SYSTEM_ANALYZE_ROOT**:环境变量,定义分析结果落盘的根目录路径
- **ODS_Schema**PostgreSQL 中存储原始数据的 schema`ods``billiards_ods`
- **DWD_Schema**PostgreSQL 中存储明细数据的 schema`dwd`
- **information_schema**PostgreSQL 系统目录,提供表结构、列信息、注释等元数据
## 职责分工
| 职责 | 执行者 | 说明 |
|------|--------|------|
| API 调用获取 JSON 数据 | Python 脚本 | 机械性数据采集,可固化 |
| JSON 结构递归展开 | Python 脚本 | 递归遍历、路径拼接,可固化 |
| 数据库表结构查询 | Python 脚本 | `information_schema` + `pg_description` 查询,可固化 |
| JSON → ODS 映射计算 | Kiro Agent | 需要理解字段语义,纯字符串匹配不可靠 |
| ODS → DWD 映射计算 | Kiro Agent | 需要读 ETL loader/task 源码理解数据流转 |
| 字段作用推断 | Kiro Agent | 需要结合代码上下文、业务语义、DDL COMMENT 综合判断 |
| 统计总结编排 | Kiro Agent | 需要理解业务含义,生成有意义的总结而非纯数字 |
| 报告 Markdown 生成 | Kiro Agent | 基于上述分析结果组装最终文档 |
## 需求
### 需求 1JSON 层级结构完整展开
**用户故事:** 作为数据工程师,我希望看到 API 返回 JSON 的完整层级结构,以便准确理解每个嵌套字段的位置和含义。
#### 验收标准
- 1.1 WHEN Analyzer 处理 API 返回的 JSON 记录时THEN Analyzer SHALL 递归遍历所有嵌套层级,使用 `.` 分隔符表示层级路径(如 `data.settleList[].amount`
- 1.2 WHEN Analyzer 分析多条 JSON 记录时THEN Analyzer SHALL 遍历所有记录并拼合出最全字段结构,确保任何记录中出现过的字段都被包含
- 1.3 WHEN JSON 字段为数组类型时THEN Analyzer SHALL 展开数组内元素的结构,使用 `[]` 标记数组层级(如 `items[].name`
- 1.4 WHEN JSON 字段为嵌套对象时THEN Analyzer SHALL 在 JSON 字段列中明确展示出层级缩进或路径前缀,使层级关系一目了然
- 1.5 IF JSON 记录中某字段在部分记录中缺失THEN Analyzer SHALL 仍将该字段纳入最全字段结构,并标注其出现频率(出现次数/总记录数)
### 需求 2ODS/DWD 表结构从数据库查询
**用户故事:** 作为数据工程师,我希望 ODS 和 DWD 的表结构直接从数据库查询获得,以便反映数据库的真实状态而非 DDL 文件的定义。
#### 验收标准
- 2.1 WHEN Analyzer 获取 ODS 表结构时THEN Analyzer SHALL 通过 `information_schema.columns` 联合 `pg_catalog.pg_description` 查询数据库获取列名、数据类型和列注释
- 2.2 WHEN Analyzer 获取 DWD 表结构时THEN Analyzer SHALL 通过 `information_schema.columns` 联合 `pg_catalog.pg_description` 查询数据库获取列名、数据类型和列注释
- 2.3 WHEN Analyzer 列出表结构时THEN Analyzer SHALL 列出表中所有列,包括业务字段和版本控制字段(如 `valid_from``valid_to``is_current``fetched_at` 等)
- 2.4 IF 数据库连接失败或表不存在THEN Analyzer SHALL 记录错误信息并在文档中标注该表结构获取失败,不中断其余表的分析
### 需求 3字段作用说明列
**用户故事:** 作为数据工程师,我希望每个字段都有作用说明,以便快速理解字段的业务含义和在数据流中的角色。
#### 验收标准
- 3.1 WHEN Kiro Agent 生成 API 源字段表格、ODS 表格或 DWD 表格时THEN Agent SHALL 为每个字段增加"字段作用"列
- 3.2 WHEN Kiro Agent 推断字段作用时THEN Agent SHALL 读取相关 ETL 源码loader、task、model结合 DDL COMMENT、JSON 数据样本和映射关系综合判断
- 3.3 WHEN API 源字段未被 ODS 处理时THEN Agent SHALL 在"处理"列(原"说明"列改名)中明确标注该字段被忽略且未处理
- 3.4 WHEN ODS 或 DWD 字段有 DDL COMMENT 注释时THEN Agent SHALL 优先使用该注释作为字段作用说明
### 需求 4改进统计总结
**用户故事:** 作为数据工程师,我希望每个表格结束后有详细的统计总结,以便快速掌握字段覆盖情况和上下游映射关系。
#### 验收标准
- 4.1 WHEN API 源字段表格结束时THEN Kiro Agent SHALL 生成详细统计总结,包括:总字段数、已映射到 ODS 的字段数、仅存于 payload 的字段数、被忽略的嵌套对象数、各 JSON 类型字段分布
- 4.2 WHEN ODS 表格结束时THEN Kiro Agent SHALL 生成统计总结,包括:总列数、业务列数、元数据列数、有上游 JSON 映射的列数、有下游 DWD 映射的列数、上下游覆盖率
- 4.3 WHEN DWD 表格结束时THEN Kiro Agent SHALL 生成统计总结,包括:总列数、有 ODS 来源的列数、ETL 派生列数、SCD2 版本控制列数、上游 ODS 表名和映射类型(维度/事实)
### 需求 5数据采集脚本 CLI 化与落盘
**用户故事:** 作为数据工程师,我希望通过 CLI 参数控制数据采集范围和输出位置,以便为 Kiro Agent 提供新鲜的结构化数据。
#### 验收标准
- 5.1 WHEN 用户通过 CLI 执行 Analyzer 时THEN Analyzer SHALL 支持 `--date-from``--date-to` 参数定义数据获取的日期范围
- 5.2 WHEN 用户通过 CLI 执行 Analyzer 时THEN Analyzer SHALL 支持 `--limit` 参数定义每个端点获取的最大记录条数(默认 200
- 5.3 WHEN 用户通过 CLI 执行 Analyzer 时THEN Analyzer SHALL 支持 `--tables` 参数指定要分析的表名列表(逗号分隔),缺省时分析全部表
- 5.4 WHEN Analyzer 确定输出目录时THEN Analyzer SHALL 从环境变量 `SYSTEM_ANALYZE_ROOT` 读取落盘根目录,并将采集数据和最终报告保存到该目录下按日期组织的子目录中
- 5.5 IF `SYSTEM_ANALYZE_ROOT` 未配置THEN Analyzer SHALL 回退到默认目录 `docs/reports/`
- 5.6 WHEN Analyzer 生成文档时THEN Analyzer SHALL 使用包含日期和时间戳的文件名(如 `dataflow_2025-01-15_143022.md`),避免覆盖历史文件
### 需求 6Kiro Hook 集成
**用户故事:** 作为开发者,我希望通过 Kiro Hook 手动触发完整的数据流分析流程,一键完成数据采集和语义分析。
#### 验收标准
- 6.1 WHEN Hook 被配置时THEN Hook SHALL 以 `userTriggered` 类型注册,允许开发者在 Kiro 中手动触发
- 6.2 WHEN Hook 被触发时THEN Hook SHALL 先调用 Python 脚本完成数据采集API JSON + DB 表结构),再由 Kiro Agent 基于采集数据执行语义分析(映射计算、字段作用推断、总结编排)
- 6.3 WHEN Kiro Agent 执行语义分析时THEN Agent SHALL 读取 ETL 源码loader、task、model、scd 等模块)理解数据流转逻辑
- 6.4 WHEN 分析完成时THEN Hook SHALL 将最终 Markdown 报告保存到 `SYSTEM_ANALYZE_ROOT` 目录,并输出文件路径和关键统计摘要
- 6.5 WHEN 有 2 个及以上 ETL 连接器完成并启用时THEN Hook SHALL 涵盖所有已启用的连接器,对每个连接器的 API → ODS → DWD 链路分别执行分析
> 当前仅有飞球feiqiu连接器但架构设计需预留多连接器扩展能力。未来新增连接器时Hook 应自动发现并纳入分析范围。

View File

@@ -0,0 +1,90 @@
# 实现计划:数据流结构分析重构
## 概览
`scripts/ops/gen_full_dataflow_doc.py` 重构为"Python 数据采集 + Kiro Agent 语义分析"双层架构。Python 脚本负责 API JSON 采集、JSON 递归展开、DB 表结构查询和结构化落盘Kiro Agent 负责映射计算、字段作用推断、统计总结和报告组装。
## 任务
- [x] 1. 创建核心采集模块 `scripts/ops/dataflow_analyzer.py`
- [x] 1.1 实现数据模型AnalyzerConfig、FieldInfo、ColumnInfo、TableCollectionResult
- 使用 `@dataclass` 定义四个核心数据类
- FieldInfo 包含 path、json_type、sample、depth、occurrence、total_records
- ColumnInfo 包含 name、data_type、is_nullable、column_default、comment、ordinal_position
- _Requirements: 1.1, 1.5, 2.1, 2.2_
- [x] 1.2 实现 `flatten_json_tree()``_recurse_json()` — JSON 递归展开
- 递归遍历 dict路径追加 `.key`、list路径追加 `[]`)、标量(记录类型和样本)
- 遍历所有记录拼合最全字段集,统计每个字段的出现频率
- 返回 OrderedDict[str, FieldInfo],按首次出现顺序排列
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 1.3 编写属性测试JSON 递归展开路径正确性
- **Property 1: JSON 递归展开路径正确性**
- 使用 hypothesis 递归策略生成任意嵌套 JSON
- 验证:`.` 分隔路径、`[]` 数组标记、depth 等于路径深度、不遗漏叶子节点
- **Validates: Requirements 1.1, 1.3, 1.4**
- [x] 1.4 编写属性测试:多记录字段合并完整性
- **Property 2: 多记录字段合并完整性与出现频率准确性**
- 生成随机记录列表(部分字段随机缺失),验证并集完整性和 occurrence 准确性
- **Validates: Requirements 1.2, 1.5**
- [x] 1.5 编写单元测试flatten_json_tree 边界情况
- 空记录列表、单层 JSON、深层嵌套5+ 层)、数组内嵌套对象
- _Requirements: 1.1, 1.2, 1.3, 1.4_
- [x] 2. 实现数据库表结构查询
- [x] 2.1 实现 `query_table_columns()` — 从 information_schema 查询表结构
- 使用 `information_schema.columns` 联合 `pg_catalog.pg_description` 查询
- 返回所有列(含版本控制字段 valid_from、valid_to、is_current、fetched_at
- 错误处理:连接失败或表不存在时返回空列表并记录错误
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 2.2 实现 `collect_all_tables()` — 完整采集流程编排
- 根据 config.tables 过滤表,逐表调用 API + flatten + query_table_columns
- 单表失败不中断,记录 error 字段继续处理其余表
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 2.3 实现 `dump_collection_results()` — 结构化 JSON 落盘
- 输出 api_samples/、json_trees/、db_schemas/ 和 collection_manifest.json
- _Requirements: 5.4, 5.6_
- [x] 3. 检查点 — 确保核心采集逻辑测试通过
- 运行 `pytest tests/test_dataflow_analyzer.py -v`,确保所有测试通过,有问题请询问用户。
- [x] 4. 实现 CLI 入口 `scripts/ops/analyze_dataflow.py`
- [x] 4.1 实现 `build_parser()``resolve_output_dir()`
- argparse 解析 `--date-from``--date-to``--limit``--tables` 参数
- `resolve_output_dir()` 优先读取 `SYSTEM_ANALYZE_ROOT`,回退到 `docs/reports/`
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 4.2 实现 `main()` — 串联采集流程
- 加载环境变量(分层叠加)、构造 AnalyzerConfig、调用 collect_all_tables + dump_collection_results
- 输出文件名使用 `dataflow_YYYY-MM-DD_HHMMSS.md` 格式
- _Requirements: 5.4, 5.6_
- [x] 4.3 编写属性测试:输出文件名格式
- **Property 3: 输出文件名格式正确性**
- 生成随机 datetime验证文件名匹配 `dataflow_YYYY-MM-DD_HHMMSS.md` 模式
- **Validates: Requirements 5.6**
- [x] 4.4 编写单元测试CLI 参数解析和输出目录回退
- 测试各参数组合、默认值、SYSTEM_ANALYZE_ROOT 存在/缺失
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 5. 从现有脚本迁移 API 调用和 ODS_SPECS 配置
- [x] 5.1 将 `gen_full_dataflow_doc.py` 中的 ODS_SPECS、api_post、fetch_records 迁移到 dataflow_analyzer.py
- 保留现有 API 调用逻辑和缓存机制
- 适配新的 AnalyzerConfig 配置结构
- _Requirements: 1.1, 1.2, 5.1, 5.2_
- [x] 6. 创建 Kiro Hook 配置
- [x] 6.1 创建 `.kiro/hooks/dataflow-analyze.kiro.hook`
- type: userTriggeredthen.type: prompt
- prompt 描述完整的数据采集 + 语义分析流程
- _Requirements: 6.1, 6.2, 6.3, 6.4_
- [x] 6.2 编写单元测试Hook 配置文件格式验证
- 验证 JSON 结构正确、type 为 userTriggered
- _Requirements: 6.1_
- [x] 7. 最终检查点 — 确保所有测试通过
- 运行 `pytest tests/test_dataflow_analyzer.py -v`,确保所有测试通过,有问题请询问用户。
## 备注
- 标记 `*` 的子任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 需求 3字段作用推断和需求 4统计总结由 Kiro Agent 在 Hook 触发时执行,不在本实现计划中(非代码任务)

View File

@@ -1,229 +0,0 @@
# 设计文档:文档体系整理与优化
## 概述
本设计针对飞球 ETL 系统的 `docs/` 目录进行结构性优化,包含三个核心工作流:
1. **文档覆盖度补充**:新增 `architecture/``business-rules/``operations/` 三个缺失目录及骨架文档,新增项目级 `CHANGELOG.md`,更新文档总索引
2. **审计一览表生成**:编写 Python 脚本解析 `docs/audit/changes/` 下的审计源记录,生成结构化的 `audit_dashboard.md` 汇总视图
3. **业务规则文档迁移**:将 `index_algorithm_cn.md``docs/database/DWS/` 迁移至 `docs/business-rules/`,原位置保留重定向说明
## 架构
### 文档目录结构(目标状态)
```
docs/
├── README.md ← 文档总索引(更新)
├── CHANGELOG.md ← 新增:项目级变更日志
├── architecture/ ← 新增:架构设计文档
│ ├── README.md ← 目录索引
│ ├── system_overview.md ← 系统整体架构
│ └── data_flow.md ← 数据流向详解
├── business-rules/ ← 新增:业务规则文档
│ ├── README.md ← 目录索引
│ ├── index_algorithm_cn.md ← 迁移自 database/DWS/
│ ├── dws_metrics.md ← DWS 口径定义(骨架)
│ └── scd2_rules.md ← SCD2 处理规则(骨架)
├── operations/ ← 新增:运维文档
│ ├── README.md ← 目录索引
│ ├── environment_setup.md ← 环境搭建指南
│ ├── scheduling.md ← 调度配置说明
│ └── troubleshooting.md ← 故障排查手册
├── audit/
│ ├── audit_dashboard.md ← 新增:审计一览表
│ ├── changes/ ← 审计源记录(不变)
│ └── ...
├── api-reference/ ← 不变
├── database/ ← 不变(移除 index_algorithm_cn.md
├── etl_tasks/ ← 不变
├── reports/ ← 不变
└── requirements/ ← 不变
```
### 审计一览表生成流程
```mermaid
graph LR
A["docs/audit/changes/*.md"] -->|Python 脚本解析| B["结构化数据列表"]
B -->|按时间倒序排列| C["时间线视图"]
B -->|按模块分类| D["模块索引视图"]
C --> E["audit_dashboard.md"]
D --> E
```
## 组件与接口
### 组件 1审计一览表生成脚本
- 路径:`scripts/gen_audit_dashboard.py`
- 职责:扫描 `docs/audit/changes/` 目录,解析每个 Markdown 文件,提取结构化信息,生成 `docs/audit/audit_dashboard.md`
- 入口:`python scripts/gen_audit_dashboard.py`
#### 解析逻辑
脚本需要从审计源记录中提取以下字段:
| 字段 | 提取方式 |
|------|----------|
| 日期 | 从文件名前缀 `YYYY-MM-DD` 提取 |
| 标题/需求摘要 | 从 Markdown 一级标题 `# ...` 提取 |
| slug | 从文件名 `__` 后的部分提取 |
| 修改文件列表 | 从"修改文件清单"或"文件清单"章节的表格/列表中提取 |
| 影响模块 | 根据修改文件路径推断所属模块api/、tasks/、docs/ 等) |
| 变更类型 | 从文件内容中提取bugfix/新增/修改/删除/文档) |
| 风险等级 | 从"风险点"章节推断(高/中/低/极低) |
#### 模块分类规则
根据修改文件路径前缀映射到功能模块:
```python
MODULE_MAP = {
"api/": "API 层",
"tasks/ods": "ODS 层",
"tasks/dwd": "DWD 层",
"tasks/dws": "DWS 层",
"tasks/index": "指数算法",
"loaders/": "数据装载",
"database/": "数据库",
"orchestration/": "调度",
"config/": "配置",
"cli/": "CLI",
"models/": "模型",
"scd/": "SCD2",
"docs/": "文档",
"scripts/": "脚本工具",
"tests/": "测试",
"quality/": "质量校验",
"gui/": "GUI",
"utils/": "工具库",
}
```
### 组件 2静态文档文件
纯 Markdown 文件,无代码逻辑。包括:
- 架构文档(`docs/architecture/`
- 业务规则文档(`docs/business-rules/`
- 运维文档(`docs/operations/`
- 变更日志(`docs/CHANGELOG.md`
- 更新后的文档总索引(`docs/README.md`
### 组件 3文件迁移与重定向
-`docs/database/DWS/index_algorithm_cn.md` 内容迁移至 `docs/business-rules/index_algorithm_cn.md`
- 在原位置 `docs/database/DWS/index_algorithm_cn.md` 替换为重定向说明
## 数据模型
### 审计记录解析结构
```python
@dataclass
class AuditEntry:
"""从单个审计源记录文件解析出的结构化数据"""
date: str # YYYY-MM-DD从文件名提取
slug: str # 文件名中 __ 后的标识符
title: str # Markdown 一级标题
filename: str # 源文件名(不含路径)
changed_files: list[str] # 修改的文件路径列表
modules: set[str] # 影响的功能模块集合
risk_level: str # 风险等级:高/中/低/极低
change_type: str # 变更类型bugfix/功能/文档/重构/清理
```
### 审计一览表输出格式
`audit_dashboard.md` 包含两个视图:
1. **时间线视图**(按日期倒序):
```markdown
| 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 |
|------|----------|----------|----------|------|------|
| 2026-02-15 | docs/database 合并 | 重构 | 文档, 脚本工具 | 极低 | [链接](changes/...) |
```
2. **模块索引视图**(按模块分组):
```markdown
### API 层
| 日期 | 需求摘要 | 变更类型 | 风险 | 详情 |
...
### DWS 层
| 日期 | 需求摘要 | 变更类型 | 风险 | 详情 |
...
```
## 正确性属性
*正确性属性是一种在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是连接人类可读规格说明与机器可验证正确性保证之间的桥梁。*
本特性中,大部分需求涉及静态文档文件的创建和目录组织,属于例子级别的验证(文件是否存在、内容是否包含特定条目)。可提取为通用属性的集中在审计一览表生成脚本的解析、分类和排序逻辑。
### Property 1审计记录解析-渲染完整性
*For any* 格式合规的审计源记录 Markdown 文件(包含一级标题、日期行、修改文件清单章节),解析后生成的表格行应包含:日期、需求摘要、变更类型、影响模块和源文件链接。
**Validates: Requirements 2.1, 2.2**
### Property 2文件路径模块分类正确性
*For any* 文件路径字符串模块分类函数的返回值应属于预定义的模块名称集合API 层、ODS 层、DWD 层、DWS 层、指数算法、数据装载、数据库、调度、配置、CLI、模型、SCD2、文档、脚本工具、测试、质量校验、GUI、工具库、其他
**Validates: Requirements 2.3**
### Property 3审计条目时间倒序排列
*For any* 一组审计条目列表,经过排序函数处理后,输出列表中每个条目的日期应大于等于其后续条目的日期(严格非递增序)。
**Validates: Requirements 2.4**
## 错误处理
### 审计记录解析容错
| 场景 | 处理方式 |
|------|----------|
| 审计文件缺少一级标题 | 使用文件名 slug 作为标题替代 |
| 审计文件缺少"修改文件清单"章节 | `changed_files` 返回空列表,`modules` 标记为 `{"其他"}` |
| 审计文件名不符合 `YYYY-MM-DD__slug.md` 格式 | 跳过该文件,输出警告日志 |
| 审计文件编码非 UTF-8 | 尝试 UTF-8 解码,失败则跳过并警告 |
| `docs/audit/changes/` 目录为空 | 生成空的 dashboard 文件,包含"暂无审计记录"提示 |
### 文件迁移容错
| 场景 | 处理方式 |
|------|----------|
| `index_algorithm_cn.md` 源文件不存在 | 脚本报错退出,提示文件路径 |
| 目标目录 `docs/business-rules/` 已存在同名文件 | 提示用户确认是否覆盖 |
## 测试策略
### 测试框架
- 单元测试:`pytest`
- 属性测试:`hypothesis`Python 属性测试库)
- 测试目录:`tests/unit/test_gen_audit_dashboard.py`
### 属性测试
每个正确性属性对应一个 `hypothesis` 属性测试,最少运行 100 次迭代:
- **Property 1**:生成随机的审计 Markdown 内容(包含标题、日期、文件清单),验证解析+渲染后的表格行包含所有必要字段
- Tag: `Feature: docs-optimization, Property 1: 审计记录解析-渲染完整性`
- **Property 2**:生成随机的文件路径字符串,验证分类结果属于预定义模块集合
- Tag: `Feature: docs-optimization, Property 2: 文件路径模块分类正确性`
- **Property 3**:生成随机的日期列表构造审计条目,验证排序后严格非递增
- Tag: `Feature: docs-optimization, Property 3: 审计条目时间倒序排列`
### 单元测试
- 解析真实审计记录文件的具体例子(使用项目中已有的审计文件作为测试输入)
- 边界情况:空目录、格式异常文件、缺少章节的文件
- 文件存在性检查:验证所有新增目录和文件已创建
- 文档总索引完整性:验证 `docs/README.md` 包含所有一级目录条目
- 重定向文件验证:验证原 `index_algorithm_cn.md` 位置包含重定向说明

View File

@@ -1,56 +0,0 @@
# 需求文档:文档体系整理与优化
## 简介
对飞球 ETL 系统etl-billiards`docs/` 目录进行文档体系整理与优化,涵盖三个核心诉求:文档覆盖度评估与缺失类别补充、审计一览表生成、业务规则文档独立目录建设。目标是让项目文档从宏观架构到微观实现形成完整闭环,同时提供可快速检索的审计变更视图。
## 术语表
- **文档总索引Docs_Index**`docs/README.md`,项目文档的统一入口与导航页
- **审计一览表Audit_Dashboard**:基于 `docs/audit/changes/` 源数据生成的汇总视图文件
- **审计源记录Audit_Record**`docs/audit/changes/` 目录下的单个审计 Markdown 文件
- **业务规则文档目录Business_Rules_Dir**`docs/business-rules/`存放指数算法、DWS 口径定义、SCD2 规则等业务逻辑文档
- **架构设计文档目录Architecture_Dir**`docs/architecture/`,存放系统整体架构、数据流、模块交互等设计文档
- **运维文档目录Operations_Dir**`docs/operations/`,存放环境搭建、调度配置、故障排查等运维指南
- **变更日志Changelog**`docs/CHANGELOG.md`,项目级版本变更历史记录
- **指数算法文档Index_Algorithm_Doc**:当前位于 `docs/database/DWS/index_algorithm_cn.md` 的指数算法说明文件
## 需求
### 需求 1文档覆盖度评估与缺失类别补充
**用户故事:** 作为项目开发者,我希望文档体系涵盖从宏观架构到微观实现的完整说明,以便新成员快速上手、现有成员高效查阅。
#### 验收标准
1. THE Docs_Index SHALL 包含指向所有一级文档目录的链接和简要说明
2. WHEN 新增文档目录时THE Docs_Index SHALL 同步更新对应的目录条目和说明
3. THE Architecture_Dir SHALL 包含系统整体架构文档涵盖数据流向图ODS→DWD→DWS、模块交互关系和技术栈说明
4. THE Business_Rules_Dir SHALL 包含独立的业务规则与算法文档目录,与数据库表结构文档分离
5. THE Operations_Dir SHALL 包含环境搭建指南、调度配置说明和常见故障排查手册
6. THE Changelog SHALL 记录项目级版本变更历史,包含日期、变更摘要和影响范围
### 需求 2审计一览表生成
**用户故事:** 作为项目管理者,我希望基于审计源记录生成一个汇总视图,以便一眼了解项目的修改痕迹并按功能模块检索。
#### 验收标准
1. THE Audit_Dashboard SHALL 从 Audit_Record 文件中提取日期、需求摘要、修改内容和影响范围信息
2. THE Audit_Dashboard SHALL 以表格形式展示每条审计记录的时间日期、用户需求摘要、修改/新增/删除的内容概要和对项目的影响
3. THE Audit_Dashboard SHALL 提供按功能模块分类的索引(如 API 层、ODS 层、DWD 层、DWS 层、文档、基础设施)
4. THE Audit_Dashboard SHALL 提供按时间倒序排列的完整变更列表
5. WHEN 新的 Audit_Record 被添加到 `docs/audit/changes/`THE Audit_Dashboard SHALL 通过手动重新生成的方式保持同步
6. THE Audit_Dashboard SHALL 存放于 `docs/audit/` 目录下,文件名为 `audit_dashboard.md`
### 需求 3业务规则文档独立目录
**用户故事:** 作为项目开发者,我希望业务规则和算法文档有独立的存放目录,以便与数据库表结构文档清晰分离,方便查阅和维护。
#### 验收标准
1. THE Business_Rules_Dir SHALL 作为独立目录存在于 `docs/business-rules/` 路径下
2. WHEN Index_Algorithm_Doc 从 `docs/database/DWS/` 迁移至 Business_Rules_Dir 时THE 原路径 SHALL 保留一个指向新位置的重定向说明
3. THE Business_Rules_Dir SHALL 包含目录级 README 文件,列出所有业务规则文档的索引
4. THE Business_Rules_Dir SHALL 按业务域组织文档如指数算法、DWS 口径定义、SCD2 规则、薪酬计算规则)
5. THE Docs_Index SHALL 包含 Business_Rules_Dir 的条目和说明

View File

@@ -1,100 +0,0 @@
# 实施计划:文档体系整理与优化
## 概述
基于设计文档,将实施分为四个阶段:新增文档目录与骨架文件、业务规则文档迁移、审计一览表生成脚本开发、文档总索引更新。所有任务聚焦于文件创建/修改和 Python 脚本编写。
## 任务
- [x] 1. 新增文档目录与骨架文件
- [x] 1.1 创建 `docs/architecture/` 目录及文档
- 创建 `docs/architecture/README.md`(目录索引)
- 创建 `docs/architecture/system_overview.md`(系统整体架构:数据流向图、模块交互、技术栈)
- 创建 `docs/architecture/data_flow.md`ODS→DWD→DWS 数据流向详解)
- 从根 `README.md``.kiro/steering/` 中提取架构信息填充内容
- _Requirements: 1.3_
- [x] 1.2 创建 `docs/operations/` 目录及文档
- 创建 `docs/operations/README.md`(目录索引)
- 创建 `docs/operations/environment_setup.md`环境搭建指南Python、PostgreSQL、依赖安装
- 创建 `docs/operations/scheduling.md`调度配置说明CLI 参数、定时任务、管道模式)
- 创建 `docs/operations/troubleshooting.md`(故障排查手册:常见错误与解决方案)
- _Requirements: 1.5_
- [x] 1.3 创建 `docs/CHANGELOG.md`
- 基于 `docs/audit/changes/` 中的审计记录,整理项目级版本变更历史
- 包含日期、变更摘要和影响范围
- _Requirements: 1.6_
- [x] 2. 业务规则文档迁移与目录建设
- [x] 2.1 创建 `docs/business-rules/` 目录并迁移指数算法文档
- 创建 `docs/business-rules/README.md`(目录索引,按业务域列出文档)
-`docs/database/DWS/index_algorithm_cn.md` 内容复制到 `docs/business-rules/index_algorithm_cn.md`
- 将原 `docs/database/DWS/index_algorithm_cn.md` 替换为重定向说明
- _Requirements: 3.1, 3.2, 3.3, 3.4_
- [x] 2.2 创建业务规则骨架文档
- 创建 `docs/business-rules/dws_metrics.md`DWS 口径定义骨架)
- 创建 `docs/business-rules/scd2_rules.md`SCD2 处理规则骨架)
- _Requirements: 3.4_
- [x] 3. 检查点 — 确认文档目录结构正确
- 确认所有新增目录和文件已创建,如有问题请提出。
- [x] 4. 审计一览表生成脚本
- [x] 4.1 实现审计记录解析模块
-`scripts/gen_audit_dashboard.py` 中实现 `AuditEntry` 数据类
- 实现 `parse_audit_file(filepath)` 函数:从文件名提取日期/slug从内容提取标题/修改文件/风险等级
- 实现 `classify_module(filepath)` 函数:根据 MODULE_MAP 将文件路径映射到功能模块
- 实现 `scan_audit_dir(dirpath)` 函数:扫描目录并返回 AuditEntry 列表
- _Requirements: 2.1, 2.3_
- [x] 4.2 编写属性测试:审计记录解析-渲染完整性
- **Property 1: 审计记录解析-渲染完整性**
- 使用 hypothesis 生成随机审计 Markdown 内容,验证解析+渲染后表格行包含所有必要字段
- **Validates: Requirements 2.1, 2.2**
- [x] 4.3 编写属性测试:文件路径模块分类正确性
- **Property 2: 文件路径模块分类正确性**
- 使用 hypothesis 生成随机文件路径,验证分类结果属于预定义模块集合
- **Validates: Requirements 2.3**
- [x] 4.4 实现审计一览表渲染模块
- 实现 `render_timeline_table(entries)` 函数:按时间倒序生成 Markdown 表格
- 实现 `render_module_index(entries)` 函数:按模块分组生成 Markdown 章节
- 实现 `render_dashboard(entries)` 函数:组合时间线和模块索引生成完整 dashboard
- _Requirements: 2.2, 2.3, 2.4_
- [x] 4.5 编写属性测试:审计条目时间倒序排列
- **Property 3: 审计条目时间倒序排列**
- 使用 hypothesis 生成随机日期列表,验证排序后严格非递增
- **Validates: Requirements 2.4**
- [x] 4.6 编写单元测试
- 使用真实审计文件作为测试输入验证解析正确性
- 测试边界情况:空目录、格式异常文件、缺少章节的文件
- _Requirements: 2.1, 2.3_
- [x] 4.7 实现主入口并生成 audit_dashboard.md
- 实现 `main()` 函数:扫描 → 解析 → 渲染 → 写入 `docs/audit/audit_dashboard.md`
- 运行脚本生成实际的 audit_dashboard.md 文件
- _Requirements: 2.5, 2.6_
- [x] 5. 更新文档总索引
- [x] 5.1 更新 `docs/README.md`
- 添加 `architecture/``business-rules/``operations/` 三个新目录的条目和说明
- 添加 `CHANGELOG.md` 条目
- 添加 `audit/audit_dashboard.md` 条目
- 移除过时条目(如 `data_exports/``templates/``test-json-doc/` 如果不存在)
- 确保所有一级目录都有对应链接
- _Requirements: 1.1, 1.2, 3.5_
- [x] 6. 最终检查点 — 确认所有文件完整
- 确认所有测试通过,所有文档文件已创建,审计一览表已生成,如有问题请提出。
## 备注
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用了具体的需求编号以便追溯
- 检查点用于增量验证
- 属性测试验证通用正确性属性,单元测试验证具体例子和边界情况

View File

@@ -0,0 +1,245 @@
# 设计文档DWD 第一阶段重构
## 概述
本次重构针对 `DwdLoadTask``tasks/dwd/dwd_load_task.py`),目标是:
1. 统一事实表增量窗口模式,消除水位线与窗口两套并行的范围过滤
2. 删除回补机制(`_insert_missing_by_pk`),简化事实表写入路径
3. 清理已确认的死代码、未使用常量和方法
4. 修复 `_build_column_mapping()` 的参数 bug
所有改动集中在 `dwd_load_task.py` 一个文件(加上删除 `base_dwd_task.py`、更新两个外部引用属于低风险重构。重构后代码路径更清晰为第二阶段架构重构E/T/L → `process_segment` 钩子)做好准备。
## 架构
### 当前架构(重构前)
```mermaid
graph TD
A[DwdLoadTask.load] --> B{事实表?}
B -->|是| C{use_window?}
C -->|是| D["_merge_fact_increment(window_start, window_end)"]
C -->|否| E["_get_fact_watermark() → watermark"]
E --> F["_merge_fact_increment(watermark)"]
D --> G["_insert_missing_by_pk回补"]
F --> G
B -->|否| H{scd_cols_present?}
H -->|是| I[_merge_dim_scd2]
H -->|否| J[_merge_dim_type1_upsert]
```
### 目标架构(重构后)
```mermaid
graph TD
A[DwdLoadTask.load] --> B{事实表?}
B -->|是| D["_merge_fact_increment(window_start, window_end)"]
D --> R[返回计数]
B -->|否| I[_merge_dim_scd2]
```
关键变化:
- 事实表路径:消除 `use_window` 分支判断,始终传 `context.window_start/window_end`;删除水位线获取和回补步骤
- 维度表路径:消除 `scd_cols_present` 条件判断,直接调用 `_merge_dim_scd2`(所有 17 张维度表都有 SCD2 列)
## 组件与接口
### 变更组件清单
| 组件 | 变更类型 | 说明 |
|------|----------|------|
| `DwdLoadTask.load()` | 修改 | 删除 `use_window` 判断,始终传 `context.window_start/window_end` |
| `DwdLoadTask._merge_fact_increment()` | 修改 | 删除 `watermark` 分支和回补调用;`window_start`/`window_end` 改为必填参数 |
| `DwdLoadTask._merge_dim()` | 修改 | 删除 Type1 分支,直接调用 `_merge_dim_scd2` |
| `DwdLoadTask._build_column_mapping()` | 修改 | 将 `ods_table``cur` 加入方法签名 |
| `DwdLoadTask._get_fact_watermark()` | 删除 | 水位线机制不再需要 |
| `DwdLoadTask._insert_missing_by_pk()` | 删除 | 回补机制不再需要 |
| `DwdLoadTask._pick_order_column()` | 删除 | 从未被调用的死代码 |
| `DwdLoadTask._merge_dim_type1_upsert()` | 删除 | Type1 分支永远不触发 |
| `DwdLoadTask._upsert_scd2_row()` | 删除 | 已被批量方法替代 |
| `DwdLoadTask._close_current_dim()` | 删除 | 已被 `_close_current_dim_bulk` 替代 |
| `DwdLoadTask._insert_dim_row()` | 删除 | 已被 `_insert_dim_rows_bulk` 替代 |
| `FACT_ORDER_CANDIDATES` 常量 | 删除 | 配合 `_pick_order_column` 删除 |
| `FACT_MISSING_FILL_TABLES` 常量 | 删除 | 配合回补机制删除 |
| `base_dwd_task.py` | 删除文件 | 死代码,从未被使用 |
| `debug_dwd.py` | 修改 | 内联时间列候选列表,替代对 `FACT_ORDER_CANDIDATES` 的引用 |
| `integrity_checker.py` | 修改 | 内联时间列候选列表,替代对 `FACT_ORDER_CANDIDATES` 的引用 |
### 接口变更详情
#### `_merge_fact_increment()` 签名变更
```python
# 重构前
def _merge_fact_increment(
self, cur, dwd_table, ods_table, dwd_cols, ods_cols,
dwd_types, ods_types,
window_start: datetime | None = None, # 可选
window_end: datetime | None = None, # 可选
) -> Dict[str, int]:
# 重构后
def _merge_fact_increment(
self, cur, dwd_table, ods_table, dwd_cols, ods_cols,
dwd_types, ods_types,
window_start: datetime, # 必填
window_end: datetime, # 必填
) -> Dict[str, int]:
```
#### `_build_column_mapping()` 签名变更
```python
# 重构前bug引用了外部作用域的 ods_table 和 cur
def _build_column_mapping(
self, dwd_table: str, pk_cols: Sequence[str], ods_cols: Sequence[str]
) -> Dict[str, tuple[str, str | None]]:
# 重构后
def _build_column_mapping(
self, cur, dwd_table: str, ods_table: str,
pk_cols: Sequence[str], ods_cols: Sequence[str]
) -> Dict[str, tuple[str, str | None]]:
```
#### `_merge_dim()` 简化
```python
# 重构前
def _merge_dim(self, cur, dwd_table, ods_table, dwd_cols, ods_cols, now):
pk_cols = self._get_primary_keys(cur, dwd_table)
scd_cols_present = any(c.lower() in self.SCD_COLS for c in dwd_cols)
if scd_cols_present:
return self._merge_dim_scd2(...)
return self._merge_dim_type1_upsert(...)
# 重构后
def _merge_dim(self, cur, dwd_table, ods_table, dwd_cols, ods_cols, now):
return self._merge_dim_scd2(cur, dwd_table, ods_table, dwd_cols, ods_cols, now)
```
#### `load()` 事实表分支简化
```python
# 重构前
use_window = bool(
self.config.get("run.window_override.start")
and self.config.get("run.window_override.end")
)
fact_counts = self._merge_fact_increment(
...,
window_start=context.window_start if use_window else None,
window_end=context.window_end if use_window else None,
)
# 重构后
fact_counts = self._merge_fact_increment(
...,
window_start=context.window_start,
window_end=context.window_end,
)
```
### 外部引用处理
`FACT_ORDER_CANDIDATES` 被两个外部模块引用,删除后需要同步处理:
1. **`scripts/debug/debug_dwd.py`**:将候选列表内联为模块级常量 `_TIME_COLUMN_CANDIDATES`
2. **`quality/integrity_checker.py`**:将候选列表内联为模块级常量 `_TIME_COLUMN_CANDIDATES`
两处内联的列表内容与原 `FACT_ORDER_CANDIDATES` 相同:`["pay_time", "create_time", "update_time", "occur_time", "settle_time", "start_use_time", "fetched_at"]`。这些模块的用途是调试和完整性检查,与 DWD 装载的核心逻辑无关,内联不会引入耦合问题。
## 数据模型
本次重构不涉及数据库 schema 变更。所有 DWD 表结构、ODS 表结构、`FACT_MAPPINGS``TABLE_MAP` 保持不变。
变更仅影响运行时行为:
- 事实表增量 SQL 的 WHERE 条件从"水位线或窗口"统一为"窗口"
- 回补 LEFT JOIN SQL 不再执行
- 维度表合并路径从"SCD2 或 Type1"统一为"SCD2"
## 正确性属性
*属性Property是一种在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
基于验收标准的前置分析,以下属性覆盖了本次重构的核心正确性要求。静态代码结构检查(方法/常量/文件是否存在)通过单元测试覆盖,不作为属性列出。
### Property 1: 事实表增量 SQL 始终使用窗口范围条件
*For any* 有效的事实表映射(`TABLE_MAP``dwd_` 前缀的表)和任意的 `window_start`/`window_end` 时间对(`window_start < window_end``_merge_fact_increment()` 生成的主增量 SQL 的 WHERE 子句 SHALL 包含 `fetched_at >= window_start AND fetched_at < window_end` 条件,且不包含单边水位线条件(`fetched_at > watermark`)。
**Validates: Requirements 1.1, 1.2**
### Property 2: 事实表增量不执行回补 SQL
*For any* 有效的事实表映射和任意的窗口参数,`_merge_fact_increment()` 执行的 SQL 语句列表中 SHALL 不包含 LEFT JOIN 回补查询(即不包含 `LEFT JOIN ... WHERE ... IS NULL` 模式的 SQL
**Validates: Requirements 2.3**
### Property 3: 事实表主增量 SQL 结构等价性
*For any* 有效的事实表映射、列集合和窗口参数,重构后 `_merge_fact_increment()` 生成的主增量 `INSERT INTO ... SELECT ... ON CONFLICT` SQL 的结构列列表、SELECT 表达式、ON CONFLICT 子句SHALL 与重构前窗口模式(`use_window=True`)生成的 SQL 结构相同。
**Validates: Requirements 5.2, 5.5**
### Property 4: 维度表始终走 SCD2 路径
*For any* 有效的维度表映射(`TABLE_MAP``dim_` 前缀的表),调用 `_merge_dim()` SHALL 始终委托给 `_merge_dim_scd2()`,不经过任何条件分支判断。
**Validates: Requirements 3.8, 5.1**
## 错误处理
本次重构不引入新的错误处理逻辑,保留现有机制:
| 场景 | 处理方式 | 变更 |
|------|----------|------|
| 单表事实增量 SQL 执行失败(含类型转换错误) | `load()``try/except` 捕获,回滚该表事务,记录错误日志,继续处理下一张表 | 无变更(需求 2.4:异常自然传播到 load() 层面) |
| 维度表 SCD2 合并失败 | 同上 | 无变更 |
| ODS 表缺少 `fetched_at` 列 | `_merge_fact_increment` 记录 error 日志并返回零计数 | 无变更 |
| DWD 表无法获取列信息 | `load()` 记录 warning 并跳过该表 | 无变更 |
删除回补机制后,原本由回补兜底的"部分行丢失"场景不再存在——类型转换失败会导致整条 SQL 报错、整张表事务回滚,这是正确的行为(需求 2.4)。
## 测试策略
### 测试框架
- 单元测试:`pytest`,使用 `FakeDB`/`FakeCursor``tests/unit/task_test_utils.py`
- 属性测试:`hypothesis`,最少 100 次迭代
- 测试位置:
- 单元测试:`apps/etl/pipelines/feiqiu/tests/unit/test_dwd_phase1_refactor.py`
- 属性测试:`tests/test_dwd_phase1_properties.py`monorepo 根目录)
### 双轨测试方法
**单元测试**覆盖:
- 静态代码结构验证(死代码/常量/方法已删除)
- `_build_column_mapping()` bug 修复后的调用正确性
- `_merge_dim()` 直接调用 SCD2 的行为
- 外部模块(`debug_dwd.py``integrity_checker.py`)导入不报错
- 表过滤功能(`dwd.only_tables`)回归
**属性测试**覆盖:
- Property 1: 事实表 SQL 窗口条件(通过 FakeCursor 捕获 SQL验证 WHERE 子句)
- Property 2: 无回补 SQL通过 FakeCursor 捕获所有执行的 SQL验证无 LEFT JOIN
- Property 3: SQL 结构等价性(对比重构前后生成的 SQL
- Property 4: 维度表 SCD2 路径(通过 mock 验证调用链)
### 属性测试配置
- 库:`hypothesis`
- 每个属性最少 100 次迭代
- 每个属性测试标注对应的设计属性编号
- 标注格式:`# Feature: dwd-phase1-refactor, Property N: {property_text}`
### 测试数据生成策略
使用 `hypothesis``@st.composite` 策略生成:
- 随机的 `window_start`/`window_end` 时间对(确保 `start < end`
- 随机的列名集合(确保包含 `fetched_at` 和至少一个主键列)
- 随机的 `FACT_MAPPINGS` 条目(列名 → 源列 + 可选类型转换)
使用 `FakeCursor` 捕获执行的 SQL 语句,而非实际连接数据库。

View File

@@ -0,0 +1,81 @@
# 需求文档DWD 第一阶段重构
## 简介
`DwdLoadTask``tasks/dwd/dwd_load_task.py`)进行低风险重构,统一事实表增量窗口模式、删除回补机制、清理死代码和未使用常量、修复已知 bug。改动集中在单个文件及删除一个死代码文件目标是简化代码路径、消除冗余分支为后续架构重构第二阶段打好基础。
## 术语表
- **DwdLoadTask**DWD 层数据装载任务类,负责从 ODS 表读取数据并合并到 DWD 维度表和事实表
- **ODS**Operational Data Store原始数据存储层
- **DWD**Data Warehouse Detail明细数据层
- **SCD2**Slowly Changing Dimension Type 2缓慢变化维度第二类通过版本号和时间戳追踪历史变更
- **水位线Watermark**:基于 DWD 表中已有数据的最大时间戳来确定增量起点的机制
- **窗口模式Window Mode**:通过 `context.window_start` / `context.window_end` 明确指定时间范围的增量机制
- **回补机制Backfill**`_insert_missing_by_pk()` 方法,在主增量写入后通过 LEFT JOIN 补齐缺失主键记录
- **BaseDwdTask**`base_dwd_task.py` 中定义的死代码基类,从未被任何子类使用
- **FACT_MAPPINGS**:事实表和维度表的列映射字典,定义 ODS 列到 DWD 列的转换规则
- **TaskContext**:运行期上下文数据类,包含 `store_id``window_start``window_end``window_minutes` 等字段
- **overlap_seconds**:自动模式下窗口起点的回退秒数,用于覆盖可能的数据延迟
## 需求
### 需求 1统一事实表增量窗口模式
**用户故事:** 作为 ETL 开发者,我希望事实表增量加载统一使用窗口模式(`window_start` / `window_end`),以消除水位线与窗口两套并行的范围过滤逻辑,降低代码复杂度。
#### 验收标准
1. WHEN `DwdLoadTask.load()` 处理事实表时THE DwdLoadTask SHALL 始终将 `context.window_start``context.window_end` 传递给 `_merge_fact_increment()`,不再根据 `run.window_override` 配置判断是否传递
2. WHEN `_merge_fact_increment()` 构建增量 SQL 时THE DwdLoadTask SHALL 统一使用 `WHERE fetched_at >= %s AND fetched_at < %s` 条件过滤,参数为 `window_start``window_end`
3. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_get_fact_watermark()` 方法
4. WHEN `_merge_fact_increment()` 被调用时THE DwdLoadTask SHALL 不再接受 `watermark` 参数,方法签名中 `window_start``window_end` 为必填参数
5. WHEN 自动模式(无 `window_override`运行时THE DwdLoadTask SHALL 通过 `BaseTask._get_time_window()` 计算的 `context.window_start`(含 `overlap_seconds` 回退)和 `context.window_end` 作为窗口范围
### 需求 2删除回补机制
**用户故事:** 作为 ETL 开发者,我希望删除事实表的回补机制(`_insert_missing_by_pk`),因为统一窗口后回补的 LEFT JOIN 结果集几乎一定为空,且类型转换失败应直接报错而非静默丢失数据。
#### 验收标准
1. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_insert_missing_by_pk()` 方法
2. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `FACT_MISSING_FILL_TABLES` 常量
3. WHEN `_merge_fact_increment()` 执行完主增量 INSERT 后THE DwdLoadTask SHALL 直接返回插入和更新计数,不再调用回补逻辑
4. WHEN 事实表增量 SQL 执行过程中发生类型转换错误时THE DwdLoadTask SHALL 让异常自然传播,触发该表的事务回滚和错误日志记录
### 需求 3清理死代码和未使用常量
**用户故事:** 作为 ETL 开发者,我希望清理所有已确认的死代码和未使用常量,以减少代码体积、消除维护负担和潜在的误导。
#### 验收标准
1. WHEN 重构完成后THE 代码库 SHALL 不再包含 `tasks/dwd/base_dwd_task.py` 文件
2. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_pick_order_column()` 方法
3. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `FACT_ORDER_CANDIDATES` 常量
4. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_upsert_scd2_row()` 方法
5. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_close_current_dim()` 方法
6. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_insert_dim_row()` 方法
7. WHEN 重构完成后THE DwdLoadTask SHALL 不再包含 `_merge_dim_type1_upsert()` 方法
8. WHEN `_merge_dim()` 被调用时THE DwdLoadTask SHALL 直接调用 `_merge_dim_scd2()`,不再检查 `scd_cols_present` 条件分支
9. WHEN 外部模块(`debug_dwd.py``integrity_checker.py`)引用 `FACT_ORDER_CANDIDATES`THE 重构 SHALL 同步更新这些引用,将候选列列表内联到各自模块中或提取为共享常量
### 需求 4修复 `_build_column_mapping()` 参数 bug
**用户故事:** 作为 ETL 开发者,我希望修复 `_build_column_mapping()` 中引用未定义变量 `ods_table``cur` 的 bug以防止未来条件变化时触发 `NameError`
#### 验收标准
1. WHEN `_build_column_mapping()` 被调用时THE DwdLoadTask SHALL 通过方法参数接收 `ods_table``cur`,而非依赖外部作用域的未定义变量
2. WHEN `_build_column_mapping()` 的方法签名变更后THE DwdLoadTask SHALL 同步更新所有调用点,传入正确的 `ods_table``cur` 参数
### 需求 5保持现有功能行为不变
**用户故事:** 作为 ETL 开发者,我希望重构后的代码在功能行为上与重构前保持一致(除了移除水位线和回补),以确保生产环境的数据处理不受影响。
#### 验收标准
1. WHEN 维度表执行 SCD2 合并时THE DwdLoadTask SHALL 保持与重构前相同的关闭旧版本和插入新版本行为
2. WHEN 事实表执行增量插入时THE DwdLoadTask SHALL 保持与重构前相同的 UPSERT 逻辑(含 `ON CONFLICT``IS DISTINCT FROM` 变更检测)
3. WHEN `overlap_seconds` 配置存在时THE DwdLoadTask SHALL 保留自动模式下窗口起点的回退机制
4. WHEN `dwd.only_tables``DWD_ONLY_TABLES` 配置存在时THE DwdLoadTask SHALL 保留表过滤功能
5. FOR ALL 有效的 `FACT_MAPPINGS` 配置和 ODS 源数据,重构后的事实表增量插入 SHALL 产生与重构前窗口模式相同的 DWD 写入结果

View File

@@ -0,0 +1,96 @@
# 实施计划DWD 第一阶段重构
## 概述
按照 confirmed_changes.md 中 2.1-2.4 的顺序,对 `DwdLoadTask` 进行低风险重构。改动集中在 `dwd_load_task.py`,辅以删除 `base_dwd_task.py` 和更新两个外部引用。每个步骤递增构建,确保无悬空代码。
## 任务
- [x] 1. 统一窗口模式,去掉水位线(需求 1
- [x] 1.1 修改 `_merge_fact_increment()` 签名:`window_start``window_end` 改为必填参数(去掉 `| None` 和默认值 `None`);删除方法体内的 watermark 分支(`elif order_col:` 块及 `watermark = None` 赋值),统一使用 `WHERE fetched_at >= %s AND fetched_at < %s`
- 文件:`tasks/dwd/dwd_load_task.py`,方法 `_merge_fact_increment`
- _Requirements: 1.2, 1.4_
- [x] 1.2 修改 `load()` 中事实表分支:删除 `use_window` 变量及条件判断,始终传 `window_start=context.window_start, window_end=context.window_end`
- 文件:`tasks/dwd/dwd_load_task.py`,方法 `load`
- _Requirements: 1.1_
- [x] 1.3 删除 `_get_fact_watermark()` 方法(约 30 行)
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 1.3_
- [x] 1.4 编写属性测试:事实表增量 SQL 始终使用窗口范围条件
- **Property 1: 事实表增量 SQL 始终使用窗口范围条件**
- **Validates: Requirements 1.1, 1.2**
- 使用 `hypothesis` 生成随机 `window_start`/`window_end`,通过 `FakeCursor` 捕获 SQL验证 WHERE 子句包含 `>= %s AND < %s`,不包含单边水位线条件
- 文件:`tests/test_dwd_phase1_properties.py`
- [x] 2. 删除回补机制(需求 2
- [x] 2.1 删除 `_merge_fact_increment()` 末尾的回补调用块:移除对 `_insert_missing_by_pk()` 的调用及 `missing_inserted` 相关代码
- 文件:`tasks/dwd/dwd_load_task.py`,方法 `_merge_fact_increment`
- _Requirements: 2.3_
- [x] 2.2 删除 `_insert_missing_by_pk()` 方法(约 100 行)
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 2.1_
- [x] 2.3 删除 `FACT_MISSING_FILL_TABLES` 常量
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 2.2_
- [x] 2.4 编写属性测试:事实表增量不执行回补 SQL
- **Property 2: 事实表增量不执行回补 SQL**
- **Validates: Requirements 2.3**
- 通过 `FakeCursor` 捕获所有执行的 SQL验证无 `LEFT JOIN ... IS NULL` 模式
- 文件:`tests/test_dwd_phase1_properties.py`
- [x] 3. 检查点 - 确保窗口统一和回补删除后测试通过
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit``cd C:\NeoZQYY && pytest tests/ -v`,确保所有测试通过,如有问题请询问用户。
- [x] 4. 清理死代码和未使用常量(需求 3
- [x] 4.1 删除 `_pick_order_column()` 方法和 `FACT_ORDER_CANDIDATES` 常量
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 3.2, 3.3_
- [x] 4.2 更新外部引用:在 `debug_dwd.py``integrity_checker.py` 中将 `DwdLoadTask.FACT_ORDER_CANDIDATES` 替换为模块级常量 `_TIME_COLUMN_CANDIDATES`(内联相同列表)
- 文件:`scripts/debug/debug_dwd.py``quality/integrity_checker.py`
- _Requirements: 3.9_
- [x] 4.3 删除逐行 SCD2 方法:`_upsert_scd2_row()``_close_current_dim()``_insert_dim_row()`
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 3.4, 3.5, 3.6_
- [x] 4.4 删除 `_merge_dim_type1_upsert()` 方法;简化 `_merge_dim()` 直接调用 `_merge_dim_scd2()`
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 3.7, 3.8_
- [x] 4.5 删除 `base_dwd_task.py` 文件
- 文件:`tasks/dwd/base_dwd_task.py`
- _Requirements: 3.1_
- [x] 4.6 编写属性测试:维度表始终走 SCD2 路径
- **Property 4: 维度表始终走 SCD2 路径**
- **Validates: Requirements 3.8, 5.1**
- 通过 mock `_merge_dim_scd2` 验证 `_merge_dim()` 始终委托给它
- 文件:`tests/test_dwd_phase1_properties.py`
- [x] 5. 修复 `_build_column_mapping()` 参数 bug需求 4
- [x] 5.1 修改 `_build_column_mapping()` 签名:添加 `cur``ods_table` 参数;更新所有调用点(`_merge_dim_type1_upsert` 已删除,剩余调用点为 `_merge_dim_scd2`)传入正确参数
- 文件:`tasks/dwd/dwd_load_task.py`
- _Requirements: 4.1, 4.2_
- [x] 5.2 编写单元测试:验证 `_build_column_mapping()``fetched_at` 缺失时正确使用 `ods_table``cur` 参数记录日志
- 文件:`apps/etl/pipelines/feiqiu/tests/unit/test_dwd_phase1_refactor.py`
- _Requirements: 4.1_
- [x] 6. 编写回归单元测试(需求 5
- [x] 6.1 编写单元测试:验证死代码已清理(`hasattr` 检查所有已删除的方法和常量)
- 文件:`apps/etl/pipelines/feiqiu/tests/unit/test_dwd_phase1_refactor.py`
- _Requirements: 1.3, 2.1, 2.2, 3.1-3.7_
- [x] 6.2 编写单元测试:验证外部模块导入正常(`debug_dwd.py``integrity_checker.py` 无 ImportError
- 文件:`apps/etl/pipelines/feiqiu/tests/unit/test_dwd_phase1_refactor.py`
- _Requirements: 3.9_
- [x] 6.3 编写属性测试:事实表主增量 SQL 结构等价性
- **Property 3: 事实表主增量 SQL 结构等价性**
- **Validates: Requirements 5.2, 5.5**
- 使用 `hypothesis` 生成随机列集合和窗口参数,通过 `FakeCursor` 捕获 SQL验证 INSERT INTO ... ON CONFLICT 结构与预期一致
- 文件:`tests/test_dwd_phase1_properties.py`
- [x] 7. 最终检查点 - 确保所有测试通过
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit``cd C:\NeoZQYY && pytest tests/ -v`,确保所有测试通过,如有问题请询问用户。
## 备注
- 标记 `*` 的子任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯
- 检查点确保增量验证
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 本次重构涉及 `tasks/` 高风险路径,完成后需运行 `/audit`

View File

@@ -0,0 +1,464 @@
# 设计文档
## 概述
本设计覆盖 v8 联调中 4 个"临时止血"修复的深度方案,按优先级排列:
1. **需求 DP0**`DwdLoadTask.load()` 返回值格式规范化
2. **需求 C1P1**:会员生日字段 ETL 链路补齐
3. **需求 BP1**:多门店会员查询支持
4. **需求 AP2**:助教月度聚合按档位分段统计
5. **需求 C2P2**:助教手动补录会员生日
设计原则:
- 每个需求独立可部署,按优先级逐步实施
- DDL 变更通过迁移脚本执行,支持回滚
- 保持现有 ETL 架构BaseTask E/T/L 模板)不变
## 架构
整体架构不变,变更集中在以下层面:
```mermaid
graph TD
subgraph "需求 D: 返回值规范化"
D1[DwdLoadTask.load] -->|errors: int| D2[BaseTask._accumulate_counts]
D2 -->|sum| D3[FlowRunner._safe_int]
end
subgraph "需求 A: 档位分段统计"
A1[dws_assistant_daily_detail] -->|GROUP BY level_code| A2[AssistantMonthlyTask]
A2 -->|多行/档位| A3[dws_assistant_monthly_summary]
A3 --> A4[AssistantSalaryTask 分段计算]
end
subgraph "需求 B: 多门店会员查询"
B1[dwd.事实表] -->|member_id IN| B2[dim_member]
B2 --> B3[DWS 任务]
end
subgraph "需求 C: 生日字段"
C1[ODS payload] -->|birthday 提取| C2[dim_member.birthday]
C3[后端 API] -->|UPSERT| C4[zqyy_app.member_birthday_manual]
C2 --> C5[DWS 任务: COALESCE]
C4 -->|FDW 只读| C5
end
```
## 组件与接口
### 需求 D返回值格式规范化
**变更文件:**
- `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py`
- `apps/etl/connectors/feiqiu/tasks/base_task.py`
**DwdLoadTask.load() 返回值变更:**
```python
# 变更前
return {"tables": summary, "errors": errors}
# errors: list[dict],如 [{"table": "dim_assistant_ex", "error": "..."}]
# 变更后
return {
"tables": summary,
"errors": len(errors), # int — 与其他任务一致
"error_details": errors, # list[dict] — 保留详情供日志使用
}
```
**BaseTask._accumulate_counts() 防御层增强:**
```python
@staticmethod
def _accumulate_counts(total: dict, current: dict) -> dict:
for key, value in (current or {}).items():
if isinstance(value, (int, float)):
total[key] = (total.get(key) or 0) + value
elif isinstance(value, list):
# 防御层list 类型转为 len() 累加
total[key] = (total.get(key) or 0) + len(value)
else:
total.setdefault(key, value)
return total
```
**FlowRunner._safe_int() 保留不变**,作为最终防御层。
### 需求 A助教月度聚合按档位分段统计
**DDL 变更:**
```sql
-- 迁移脚本:删除旧唯一约束,创建新约束
ALTER TABLE dws.dws_assistant_monthly_summary
DROP CONSTRAINT IF EXISTS uk_dws_assistant_monthly;
ALTER TABLE dws.dws_assistant_monthly_summary
ADD CONSTRAINT uk_dws_assistant_monthly
UNIQUE (site_id, assistant_id, stat_month, assistant_level_code);
```
**AssistantMonthlyTask._extract_daily_aggregates() 变更:**
```sql
-- 变更前GROUP BY assistant_id, DATE_TRUNC('month', stat_date)
-- 变更后:加入 assistant_level_code 分组
SELECT
assistant_id,
assistant_level_code,
assistant_level_name,
-- nickname 取时间最后一条
(ARRAY_AGG(assistant_nickname ORDER BY stat_date DESC))[1] AS assistant_nickname,
DATE_TRUNC('month', stat_date)::DATE AS stat_month,
COUNT(DISTINCT stat_date) AS work_days,
SUM(total_service_count) AS total_service_count,
-- ... 其余聚合字段不变
FROM dws.dws_assistant_daily_detail
WHERE site_id = %s AND ({month_where})
GROUP BY assistant_id, assistant_level_code, assistant_level_name,
DATE_TRUNC('month', stat_date)
```
**AssistantSalaryTask 适配:**
- `_extract_monthly_summary()` 返回多行(同一助教不同档位)
- `transform()` 遍历每行分别计算工资,按档位使用对应的 `level_price``tier`
- 最终每个 `(assistant_id, stat_month, assistant_level_code)` 生成一条工资记录
**AssistantFinanceTask._extract_daily_revenue() nickname 修复:**
```sql
-- 变更前MAX(s.nickname) AS assistant_nickname
-- 变更后:
(ARRAY_AGG(s.nickname ORDER BY s.start_use_time DESC))[1] AS assistant_nickname
```
**AssistantCustomerTask._extract_service_pairs() nickname 修复:**
```sql
-- 变更前MAX(assistant_nickname) AS assistant_nickname
-- 变更后:
(ARRAY_AGG(assistant_nickname ORDER BY service_date DESC))[1] AS assistant_nickname
```
### 需求 B多门店会员查询支持
**变更模式:** 所有 `_extract_member_info(site_id)` 方法的 SQL 从:
```sql
WHERE register_site_id = %s AND scd2_is_current = 1
```
改为通过事实表反查:
```sql
WHERE member_id IN (
SELECT DISTINCT tenant_member_id
FROM dwd.{}
WHERE site_id = %s AND tenant_member_id IS NOT NULL AND tenant_member_id != 0
) AND scd2_is_current = 1
```
**受影响的任务和对应事实表:**
| 任务 | 方法 | 事实表 |
|------|------|--------|
| `member_visit_task.py` | `_extract_member_info` | `dwd_settlement_head` |
| `member_consumption_task.py` | `_extract_member_info` | `dwd_settlement_head` |
| `assistant_customer_task.py` | `_extract_member_info` | `dwd_assistant_service_log` |
**`dim_member_card_account` 的处理:**
- `member_consumption_task.py``finance_recharge_task.py` 中对 `dim_member_card_account` 的查询也使用 `register_site_id`
- 同样改为通过事实表的 `tenant_member_id` 反查:
```sql
WHERE tenant_member_id IN (
SELECT DISTINCT tenant_member_id
FROM dwd.{}
WHERE site_id = %s AND tenant_member_id IS NOT NULL AND tenant_member_id != 0
) AND scd2_is_current = 1
```
### 需求 C1会员生日字段 ETL 链路补齐
**DDL 变更:**
```sql
-- dim_member 加列
ALTER TABLE dwd.dim_member ADD COLUMN IF NOT EXISTS birthday DATE;
COMMENT ON COLUMN dwd.dim_member.birthday IS '会员生日来源ODS member_profiles payload 中的 birthday 字段';
```
**ODS → DWD 装载:**
- `DwdLoadTask` 的列映射是自动的(通过 `_get_columns()` 读取 DWD 表列名,与 ODS 列名匹配)
- ODS `member_profiles` 表没有 `birthday` 列,但 `payload` JSONB 中可能包含
- 需要在 `_build_column_mapping()``_fetch_source_rows()` 中增加从 `payload` 提取 `birthday` 的逻辑
- 方案:在 ODS 表也加 `birthday` 列(保持 ODS 与 API 字段对齐ODS 入库时从 JSON 提取
```sql
-- ODS member_profiles 加列
ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS birthday DATE;
```
ODS 入库逻辑(`ods_tasks.py`)已有从 JSON 提取字段的机制,新增 `birthday` 字段映射即可。DwdLoadTask 的自动列匹配会自动将 `ods.member_profiles.birthday` 映射到 `dwd.dim_member.birthday`
**SCD2 处理:**
- `birthday` 作为 `dim_member` 的普通维度列SCD2 变化检测会自动包含
- 当 API 返回的 birthday 值变化时,会触发 SCD2 版本更新
**DWS 任务恢复 birthday 引用:**
- `member_visit_task.py``_extract_member_info()` SQL 中加入 `birthday`
- `member_consumption_task.py` 同理
### 需求 C2助教手动补录会员生日
**DDL`zqyy_app` / `test_zqyy_app` 业务库):**
```sql
CREATE TABLE IF NOT EXISTS member_birthday_manual (
id BIGSERIAL PRIMARY KEY,
member_id BIGINT NOT NULL,
birthday_value DATE NOT NULL,
recorded_by_assistant_id BIGINT,
recorded_by_name VARCHAR(50),
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
source VARCHAR(20) DEFAULT 'assistant',
CONSTRAINT uk_member_birthday_manual
UNIQUE (member_id, recorded_by_assistant_id)
);
COMMENT ON TABLE member_birthday_manual IS '助教手动补录的会员生日信息';
CREATE INDEX idx_mbd_member ON member_birthday_manual (member_id);
```
**FDW 映射ETL 库读取业务库数据):**
当前 FDW 方向是 `zqyy_app``etl_feiqiu`(业务库读 ETL 数据)。需求 C2 需要反向ETL DWS 任务读取业务库的手动补录表。
方案:在 `etl_feiqiu` 库中创建指向 `zqyy_app` 的 FDW 外部表:
```sql
-- 在 etl_feiqiu 中执行
CREATE SERVER IF NOT EXISTS zqyy_app_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', dbname 'zqyy_app', port '5432');
CREATE USER MAPPING IF NOT EXISTS FOR etl_user
SERVER zqyy_app_server
OPTIONS (user 'app_reader', password '***');
CREATE SCHEMA IF NOT EXISTS fdw_app;
CREATE FOREIGN TABLE fdw_app.member_birthday_manual (
id BIGINT,
member_id BIGINT,
birthday_value DATE,
recorded_by_assistant_id BIGINT,
recorded_by_name VARCHAR(50),
recorded_at TIMESTAMPTZ,
source VARCHAR(20)
) SERVER zqyy_app_server
OPTIONS (schema_name 'public', table_name 'member_birthday_manual');
```
**DWS 任务生日读取逻辑:**
```sql
-- 优先手动补录值,其次 API 值
COALESCE(
(SELECT birthday_value
FROM fdw_app.member_birthday_manual
WHERE member_id = m.member_id
ORDER BY recorded_at ASC -- 最早提交优先
LIMIT 1),
m.birthday
) AS member_birthday
```
**后端 API**
```python
# apps/backend/app/routers/member_birthday.py
@router.post("/member-birthday")
async def submit_member_birthday(
member_id: int,
birthday_value: date,
assistant_id: int,
assistant_name: str,
db=Depends(get_db),
):
"""助教提交会员生日UPSERT"""
sql = """
INSERT INTO member_birthday_manual
(member_id, birthday_value, recorded_by_assistant_id, recorded_by_name)
VALUES (%s, %s, %s, %s)
ON CONFLICT (member_id, recorded_by_assistant_id)
DO UPDATE SET
birthday_value = EXCLUDED.birthday_value,
recorded_at = NOW()
"""
db.execute(sql, (member_id, birthday_value, assistant_id, assistant_name))
return {"status": "ok"}
```
## 数据模型
### 变更汇总
| 库 | 表 | 变更类型 | 说明 |
|----|-----|---------|------|
| `etl_feiqiu` | `ods.member_profiles` | 加列 | `birthday DATE` |
| `etl_feiqiu` | `dwd.dim_member` | 加列 | `birthday DATE` |
| `etl_feiqiu` | `dws.dws_assistant_monthly_summary` | 改约束 | UK 加入 `assistant_level_code` |
| `zqyy_app` | `member_birthday_manual` | 新建表 | 手动补录生日 |
| `etl_feiqiu` | `fdw_app.member_birthday_manual` | 新建外部表 | FDW 映射 |
### 迁移脚本清单
按优先级排序,每个迁移脚本独立可执行:
1. `2026-02-22__D_dwd_load_return_format.sql` — 无 DDL纯代码变更
2. `2026-02-22__C1_dim_member_add_birthday.sql` — ODS/DWD 加列
3. `2026-02-22__B_no_ddl_code_only.sql` — 无 DDL纯代码变更
4. `2026-02-22__A_monthly_summary_uk_change.sql` — 唯一约束变更
5. `2026-02-22__C2_member_birthday_manual.sql` — 新建表 + FDW
## 正确性属性
*属性Property是系统在所有合法执行中都应保持为真的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是连接人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: DwdLoadTask 返回值格式一致性
*对于任意* DwdLoadTask.load() 的执行结果,返回字典中 `errors` 键的值应为 `int` 类型,且等于 `error_details` 列表的长度。
**验证: 需求 1.1**
### Property 2: _accumulate_counts 类型安全累加
*对于任意* 包含 `int``float``list` 类型值的计数字典,`_accumulate_counts()` 应将 `int`/`float` 直接累加,将 `list` 转为 `len()` 后累加,且不抛出异常。
**验证: 需求 1.2**
### Property 3: 档位分段聚合正确性
*对于任意* 助教在同一月内存在 N 个不同 `assistant_level_code` 的日度数据,`_extract_daily_aggregates()` 应返回恰好 N 行记录,每行的业绩指标之和应等于该助教该月的总业绩。
**验证: 需求 2.1**
### Property 4: nickname 按时间倒序取值
*对于任意* 助教在聚合周期内有多条不同 nickname 的记录,聚合结果中的 nickname 应等于时间最晚的那条记录的 nickname。此属性适用于 AssistantMonthlyTask、AssistantFinanceTask、AssistantCustomerTask 三个任务。
**验证: 需求 2.3, 2.5, 2.6**
### Property 5: 工资按档位分段计算
*对于任意* 助教在同一月有多个档位的月度汇总记录AssistantSalaryTask 应为每个档位分别计算工资,每个档位使用对应的 `level_price``tier` 配置,且所有档位的工资记录数等于月度汇总的行数。
**验证: 需求 2.4**
### Property 6: 跨店会员可查
*对于任意* 在 A 店注册但在 B 店有消费记录的会员B 店的 DWS 任务通过事实表反查 `dim_member`应能获取到该会员的维度信息nickname、mobile 等)。
**验证: 需求 3.1, 3.2**
### Property 7: birthday ODS→DWD 装载正确性
*对于任意* ODS `member_profiles` 中包含 `birthday` 值的记录DwdLoadTask 装载后 `dwd.dim_member` 中对应记录的 `birthday` 应与 ODS 源值一致。
**验证: 需求 4.2**
### Property 8: birthday SCD2 变化检测
*对于任意* `dim_member` 现有记录,当 ODS 中同一会员的 `birthday` 值发生变化时SCD2 应关闭旧版本并创建新版本,新版本的 `birthday` 等于新值。
**验证: 需求 4.3**
### Property 9: 生日 UPSERT 幂等性
*对于任意* `(member_id, assistant_id)` 组合,连续两次提交不同的 `birthday_value``member_birthday_manual` 表中该组合应只有一条记录,且 `birthday_value` 等于最后一次提交的值。
**验证: 需求 5.2, 5.5**
### Property 10: 手动补录优先于 API 来源
*对于任意* 同时在 `dim_member.birthday``member_birthday_manual` 中有值的会员DWS 任务输出的 `member_birthday` 应等于手动补录表中的值。
**验证: 需求 5.4**
### Property 11: SCD2 更新不影响手动补录表
*对于任意*`member_birthday_manual` 中有记录的会员,执行 DwdLoadTask SCD2 更新 `dim_member.birthday` 后,`member_birthday_manual` 中的记录应保持不变。
**验证: 需求 5.6**
## 错误处理
### 需求 D返回值格式
- `DwdLoadTask.load()` 中单表装载失败时,错误信息追加到 `error_details` 列表,`errors` 计数递增
- `_accumulate_counts()` 遇到未知类型时使用 `setdefault` 保留原值(现有行为不变)
- `_safe_int()` 遇到非 int/list 类型时返回 0现有行为不变
### 需求 A档位分段
- 助教在某月无任何服务记录时,不生成月度汇总行(现有行为不变)
- `assistant_level_code` 为 NULL 时作为独立分组处理NULL 视为一个档位)
- 唯一约束变更后需要清理旧数据DELETE + 重新计算当月数据)
### 需求 B多门店查询
- 事实表中无该门店消费记录时,`_extract_member_info()` 返回空字典(现有行为不变)
- 子查询返回空集时,`WHERE member_id IN (空集)` 等价于 `WHERE FALSE`,不会报错
### 需求 C1生日字段
- ODS 中 `birthday` 为 NULL 或空字符串时DWD 中存为 NULL
- 无效日期格式时DwdLoadTask 的现有类型转换逻辑会将其置为 NULL
### 需求 C2手动补录
- `member_id` 不存在于 `dim_member` 时,仍允许提交(助教可能先于 ETL 发现新客户)
- `birthday_value` 格式校验由后端 API 的 Pydantic schema 处理
- FDW 连接失败时DWS 任务应 catch 异常并降级为仅使用 `dim_member.birthday`
## 测试策略
### 测试框架
- 属性测试:`hypothesis`Python每个属性测试最少 100 次迭代
- 单元测试:`pytest`
- 测试工具:`apps/etl/connectors/feiqiu/tests/unit/task_test_utils.py` 提供 FakeDB/FakeAPI
### 属性测试
每个正确性属性对应一个 hypothesis 属性测试,标注格式:
```python
# Feature: etl-aggregation-fix, Property N: {property_text}
@given(...)
def test_property_N_xxx(data):
...
```
属性测试放置位置:
- 需求 D 相关Property 1-2`apps/etl/connectors/feiqiu/tests/unit/test_return_format_properties.py`
- 需求 A 相关Property 3-5`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- 需求 B 相关Property 6`apps/etl/connectors/feiqiu/tests/unit/test_multi_store_properties.py`
- 需求 C 相关Property 7-11`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
### 单元测试
单元测试覆盖具体示例和边界情况:
- DDL 结构验证(唯一约束、列存在性)
- 空数据 / NULL 值边界
- 迁移脚本的回滚验证
### 测试环境
- 数据库:`test_etl_feiqiu` / `test_zqyy_app`(通过 `TEST_DB_DSN` 环境变量)
- 纯单元测试使用 FakeDB/FakeAPI不涉及真实数据库连接
- ETL 测试 cwd`apps/etl/connectors/feiqiu/`
- 后端测试 cwd`apps/backend/`

View File

@@ -0,0 +1,84 @@
# 需求文档
## 简介
v8 联调修复了 11 个 BUG其中 4 个的当前修复方式是"临时止血",需要更完整的方案。本 Spec 覆盖以下四个需求:
- **需求 A**:助教月度聚合按档位分段统计,替代 `MAX()` 聚合
- **需求 B**:多门店会员查询 `register_site_id` 已知限制标记 + 预留扩展方案
- **需求 C**会员生日字段全链路补齐C1: ETL 链路 / C2: 手动补录)
- **需求 D**`DwdLoadTask.load()` 返回值格式规范化
## 术语表
- **DwdLoadTask**DWD 层装载任务,负责将 ODS 原始数据清洗装载至 DWD 明细层
- **DWS 任务**:数据汇总层任务,从 DWD 层聚合生成业务报表数据
- **SCD2**:缓慢变化维度类型 2通过版本化记录维度属性的历史变化
- **BaseTask**ETL 任务基类,提供 Extract/Transform/Load 模板方法和计数累加逻辑
- **FlowRunner**ETL 流程编排器,按层级顺序执行任务并汇总计数
- **dim_member**DWD 层会员维度表,使用 SCD2 管理历史版本
- **dws_assistant_monthly_summary**DWS 层助教月度汇总表
- **dws_assistant_salary_calc**DWS 层助教工资计算表
- **register_site_id**:会员注册门店 ID当前 `dim_member` 中唯一的门店标识
- **assistant_level_code**:助教档位代码,助教月内可能因升级/降级而变化
- **dim_member_birthday_manual**:手动补录生日表(位于 `zqyy_app` 业务库),存储助教提交的会员生日信息,通过 FDW 只读映射供 ETL DWS 任务读取
- **_safe_int()**`flow_runner.py` 中的类型安全辅助函数,将 `int`/`list`/`None` 统一转为 `int`
- **_accumulate_counts()**`BaseTask` 中的计数累加方法,合并多段执行的统计结果
## 需求
### 需求 1DwdLoadTask 返回值格式规范化(需求 D
**用户故事:** 作为 ETL 开发者,我希望 DwdLoadTask.load() 的返回值格式与其他任务保持一致,以便 FlowRunner 能安全地汇总所有任务的计数。
#### 验收标准
1. WHEN DwdLoadTask.load() 执行完成, THE DwdLoadTask SHALL 返回包含 `errors` 键(值为 `int` 类型,等于错误列表长度)和 `error_details` 键(值为 `list[dict]` 类型,包含错误详情)的字典
2. WHEN BaseTask._accumulate_counts() 遇到值为 `list` 类型的计数项, THE BaseTask SHALL 将该值转换为 `len(list)` 后再累加(防御层)
3. WHEN FlowRunner 汇总所有任务计数, THE FlowRunner SHALL 保留 `_safe_int()` 作为最终防御层,确保 `sum()` 不会因类型不一致而崩溃
### 需求 2助教月度聚合按档位分段统计需求 A
**用户故事:** 作为运营管理者,我希望助教月度汇总按档位分段统计业绩,以便准确反映助教在不同档位期间的表现和工资计算。
#### 验收标准
1. WHEN 助教在同一月内存在多个 assistant_level_code, THE AssistantMonthlyTask SHALL 按 `(assistant_id, stat_month, assistant_level_code)` 分组生成多行记录,分别统计各档位的业绩指标
2. THE dws_assistant_monthly_summary 表 SHALL 使用 `(site_id, assistant_id, stat_month, assistant_level_code)` 作为唯一约束
3. WHEN AssistantMonthlyTask 需要取 nickname 值, THE AssistantMonthlyTask SHALL 按时间倒序取最后一条记录的 nickname而非使用 `MAX()` 聚合
4. WHEN AssistantSalaryTask 计算工资, THE AssistantSalaryTask SHALL 按档位分段计算抽成,适配新的多行月度汇总结构
5. WHEN AssistantFinanceTask 提取日度收入需要 nickname, THE AssistantFinanceTask SHALL 按时间倒序取最后一条记录的 nickname而非使用 `MAX()` 聚合
6. WHEN AssistantCustomerTask 提取服务对需要 nickname, THE AssistantCustomerTask SHALL 按时间倒序取最后一条记录的 nickname而非使用 `MAX()` 聚合
### 需求 3多门店会员查询支持需求 B
**用户故事:** 作为运营管理者,我希望 DWS 任务能正确查询跨店消费的会员信息,以便 B 店能看到在 A 店注册但在 B 店消费的会员维度数据。
#### 验收标准
1. WHEN DWS 任务需要查询会员信息, THE DWS 任务 SHALL 通过事实表中的 `member_id` 反查 `dim_member`,而非使用 `WHERE register_site_id = %s` 预筛选
2. WHEN 会员在 A 店注册并在 B 店消费, THE B 店的 DWS 任务 SHALL 能查询到该会员的昵称、手机号等维度信息
3. WHEN DWS 任务执行会员信息提取, THE DWS 任务 SHALL 使用 `WHERE member_id IN (SELECT DISTINCT member_id FROM dwd.事实表 WHERE site_id = %s)` 模式获取会员维度数据
### 需求 4会员生日字段 ETL 链路补齐(需求 C1
**用户故事:** 作为运营管理者,我希望会员生日信息能从上游 API 完整传递到 DWS 层,以便用于会员分析和销售线索。
#### 验收标准
1. THE dim_member 表 SHALL 包含 `birthday DATE`
2. WHEN DwdLoadTask 执行 ODS → DWD 装载, THE DwdLoadTask SHALL 在列映射中包含 `birthday` 字段,将 ODS 中的生日数据装载到 `dim_member.birthday`
3. WHEN SCD2 更新 dim_member 记录, THE DwdLoadTask SHALL 将 `birthday` 作为变化检测字段之一,正常处理生日值的变化
4. WHEN MemberVisitTask 等 DWS 任务提取会员信息, THE DWS 任务 SHALL 从 `dim_member.birthday` 读取生日字段并写入 DWS 目标表
### 需求 5助教手动补录会员生日需求 C2
**用户故事:** 作为助教,我希望能手动提交客户的生日信息,以便在上游 API 未提供生日数据时补充这一重要销售线索。
#### 验收标准
1. THE `zqyy_app` 业务库(开发/测试环境使用 `test_zqyy_app`SHALL 包含 `member_birthday_manual` 表,结构包含 `member_id``birthday_value``recorded_by_assistant_id``recorded_by_name``recorded_at``source` 字段,唯一约束为 `(member_id, recorded_by_assistant_id)`
2. WHEN 同一助教对同一会员重复提交生日, THE 系统 SHALL 更新该助教的已有记录UPSERT保留所有其他助教的提交记录
3. WHEN DWS 任务需要读取手动补录生日, THE DWS 任务 SHALL 通过 FDW 只读映射从 `zqyy_app.member_birthday_manual` 读取数据
4. WHEN dim_member.birthdayAPI 来源)和 member_birthday_manual手动来源同时存在, THE DWS 任务 SHALL 优先使用手动补录值
5. WHEN 助教通过后端 API 提交生日, THE 后端 SHALL 提供 POST 接口接收 `member_id``birthday_value``assistant_id``assistant_name` 参数,执行 UPSERT 写入 `zqyy_app.member_birthday_manual`
6. WHEN SCD2 更新 dim_member.birthday, THE DwdLoadTask SHALL 正常更新 API 来源的生日值,不影响业务库中手动补录表的数据

View File

@@ -0,0 +1,228 @@
# 实现计划ETL 聚合修复与生日字段补齐
## 概述
按优先级D → C1 → B → A → C2逐步实施每个需求独立可部署。代码变更集中在 ETL Connector 的 tasks 层和后端 APIDDL 变更通过迁移脚本执行。
## 任务
- [x] 1. 需求 DDwdLoadTask 返回值格式规范化
- [x] 1.1 修改 DwdLoadTask.load() 返回值格式
-`apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py``load()` 方法中,将 `return {"tables": summary, "errors": errors}` 改为 `return {"tables": summary, "errors": len(errors), "error_details": errors}`
- _需求: 1.1_
- [x] 1.2 增强 BaseTask._accumulate_counts() 防御层
-`apps/etl/connectors/feiqiu/tasks/base_task.py``_accumulate_counts()` 方法中,增加 `isinstance(value, list)` 分支,将 list 转为 `len()` 后累加
- _需求: 1.2_
- [x] 1.3 编写属性测试DwdLoadTask 返回值格式一致性
- **Property 1: DwdLoadTask 返回值格式一致性**
- **验证: 需求 1.1**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_return_format_properties.py`
- [x] 1.4 编写属性测试_accumulate_counts 类型安全累加
- **Property 2: _accumulate_counts 类型安全累加**
- **验证: 需求 1.2**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_return_format_properties.py`
- [x] 2. 检查点 — 需求 D 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 3. 需求 C1会员生日字段 ETL 链路补齐
- [x] 3.1 编写迁移脚本ODS/DWD 加 birthday 列
- 创建 `db/etl_feiqiu/migrations/2026-02-22__C1_dim_member_add_birthday.sql`
- ODS: `ALTER TABLE ods.member_profiles ADD COLUMN IF NOT EXISTS birthday DATE;`
- DWD: `ALTER TABLE dwd.dim_member ADD COLUMN IF NOT EXISTS birthday DATE;`
- 包含回滚语句和验证 SQL
- _需求: 4.1_
- [-] 3.1a 在测试库执行迁移脚本 C1
-`test_etl_feiqiu` 上执行 `2026-02-22__C1_dim_member_add_birthday.sql`
- 执行验证 SQL 确认列已添加
- _需求: 4.1_
- [x] 3.2 更新 ODS 入库逻辑:提取 birthday 字段
- 在 ODS 入库任务中增加 `birthday` 字段的 JSON 提取映射
- 确认 `ods_tasks.py``member_profiles` 的字段列表包含 `birthday`
- _需求: 4.2_
- [x] 3.3 验证 DwdLoadTask 自动列映射包含 birthday
- DwdLoadTask 通过 `_get_columns()` 自动读取 DWD 表列名,确认 `birthday` 被自动包含在列映射中
- SCD2 变化检测自动包含所有非 SCD2 元数据列,确认 `birthday` 参与变化检测
- _需求: 4.2, 4.3_
- [x] 3.4 恢复 DWS 任务中的 birthday 引用
- 修改 `member_visit_task.py``_extract_member_info()` SQL加入 `birthday` 字段
- 修改 `member_consumption_task.py``_extract_member_info()` SQL加入 `birthday` 字段
- 修改 DWS 任务的 `transform()` 方法,将 `member_birthday` 写入输出记录
- _需求: 4.4_
- [x] 3.5 编写属性测试birthday ODS→DWD 装载正确性
- **Property 7: birthday ODS→DWD 装载正确性**
- **验证: 需求 4.2**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 3.6 编写属性测试birthday SCD2 变化检测
- **Property 8: birthday SCD2 变化检测**
- **验证: 需求 4.3**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 4. 检查点 — 需求 C1 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 5. 需求 B多门店会员查询支持
- [x] 5.1 修改 member_visit_task.py 的 _extract_member_info()
-`WHERE register_site_id = %s` 改为通过 `dwd_settlement_head` 事实表的 `tenant_member_id` 反查
- _需求: 3.1, 3.2_
- [x] 5.2 修改 member_consumption_task.py 的 _extract_member_info()
-`WHERE register_site_id = %s` 改为通过 `dwd_settlement_head` 事实表的 `tenant_member_id` 反查
- 同时修改 `dim_member_card_account` 查询,改为通过事实表反查
- _需求: 3.1, 3.2_
- [x] 5.3 修改 assistant_customer_task.py 的 _extract_member_info()
-`WHERE register_site_id = %s` 改为通过 `dwd_assistant_service_log` 事实表的 `tenant_member_id` 反查
- _需求: 3.1, 3.2_
- [x] 5.4 修改 finance_recharge_task.py 的 dim_member_card_account 查询
-`WHERE register_site_id = %s` 改为通过事实表反查
- _需求: 3.1_
- [x] 5.5 编写属性测试:跨店会员可查
- **Property 6: 跨店会员可查**
- **验证: 需求 3.1, 3.2**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_multi_store_properties.py`
- [x] 6. 检查点 — 需求 B 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 7. 需求 A助教月度聚合按档位分段统计
- [x] 7.1 编写迁移脚本:唯一约束变更
- 创建 `db/etl_feiqiu/migrations/2026-02-22__A_monthly_summary_uk_change.sql`
- DROP 旧约束 `uk_dws_assistant_monthly`ADD 新约束 `(site_id, assistant_id, stat_month, assistant_level_code)`
- 包含回滚语句和验证 SQL
- _需求: 2.2_
- [x] 7.1a 在测试库执行迁移脚本 A
-`test_etl_feiqiu` 上执行 `2026-02-22__A_monthly_summary_uk_change.sql`
- 执行验证 SQL 确认约束已变更(`SELECT conname FROM pg_constraint ...`
- _需求: 2.2_
- [x] 7.2 修改 AssistantMonthlyTask._extract_daily_aggregates()
- GROUP BY 加入 `assistant_level_code, assistant_level_name`
- nickname 改为 `(ARRAY_AGG(assistant_nickname ORDER BY stat_date DESC))[1]`
- _需求: 2.1, 2.3_
- [x] 7.3 修改 AssistantMonthlyTask._process_month() 适配多行
- 确认 `_process_month()` 能正确处理同一助教多个档位的聚合数据
- 每行使用自己的 `assistant_level_code` 进行档位匹配和排名计算
- _需求: 2.1_
- [x] 7.4 修改 AssistantSalaryTask 适配档位分段工资计算
- `_extract_monthly_summary()` 已能返回多行(同一助教不同档位)
- `transform()` 遍历每行分别计算工资,按档位使用对应的 `level_price``tier`
- _需求: 2.4_
- [x] 7.5 修改 AssistantFinanceTask._extract_daily_revenue() nickname 取值
-`MAX(s.nickname)` 改为 `(ARRAY_AGG(s.nickname ORDER BY s.start_use_time DESC))[1]`
- _需求: 2.5_
- [x] 7.6 修改 AssistantCustomerTask._extract_service_pairs() nickname 取值
-`MAX(assistant_nickname)` 改为 `(ARRAY_AGG(assistant_nickname ORDER BY service_date DESC))[1]`
- _需求: 2.6_
- [x] 7.7 编写属性测试:档位分段聚合正确性
- **Property 3: 档位分段聚合正确性**
- **验证: 需求 2.1**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- [x] 7.8 编写属性测试nickname 按时间倒序取值
- **Property 4: nickname 按时间倒序取值**
- **验证: 需求 2.3, 2.5, 2.6**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- [x] 7.9 编写属性测试:工资按档位分段计算
- **Property 5: 工资按档位分段计算**
- **验证: 需求 2.4**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_monthly_aggregation_properties.py`
- [x] 8. 检查点 — 需求 A 完成
- 确保所有测试通过ask the user if questions arise.
- [x] 9. 需求 C2助教手动补录会员生日
- [x] 9.1 编写迁移脚本:创建 member_birthday_manual 表
- 创建 `db/zqyy_app/migrations/2026-02-22__C2_member_birthday_manual.sql`
-`zqyy_app` / `test_zqyy_app` 中创建 `member_birthday_manual`
- 包含回滚语句和验证 SQL
- _需求: 5.1_
- [x] 9.1a 在测试库执行迁移脚本 C2
-`test_zqyy_app` 上执行 `2026-02-22__C2_member_birthday_manual.sql`
- 执行验证 SQL 确认表和约束已创建
- _需求: 5.1_
- [x] 9.2 编写 FDW 映射脚本ETL 库读取业务库
- 创建 `db/fdw/setup_fdw_reverse.sql`etl_feiqiu → zqyy_app 方向)
- 创建 `db/fdw/setup_fdw_reverse_test.sql`test 环境版本)
- 在 ETL 库中创建 `fdw_app.member_birthday_manual` 外部表
- _需求: 5.3_
- [x] 9.2a 在测试库执行 FDW 映射脚本
-`test_etl_feiqiu` 上执行 `setup_fdw_reverse_test.sql`
- 验证 `fdw_app.member_birthday_manual` 外部表可读取
- _需求: 5.3_
- [x] 9.3 修改 DWS 任务:生日读取优先级逻辑
-`member_visit_task.py``member_consumption_task.py``_extract_member_info()` 中,使用 `COALESCE(fdw_app.member_birthday_manual, dim_member.birthday)` 逻辑
- 增加 FDW 连接失败的降级处理
- _需求: 5.4_
- [x] 9.4 实现后端 API生日提交接口
- 创建 `apps/backend/app/routers/member_birthday.py`
- 实现 `POST /member-birthday` 接口,执行 UPSERT
- 创建 Pydantic schema `apps/backend/app/schemas/member_birthday.py`
-`apps/backend/app/main.py` 中注册路由
- _需求: 5.5_
- [x] 9.5 编写属性测试:生日 UPSERT 幂等性
- **Property 9: 生日 UPSERT 幂等性**
- **验证: 需求 5.2, 5.5**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 9.6 编写属性测试:手动补录优先于 API 来源
- **Property 10: 手动补录优先于 API 来源**
- **验证: 需求 5.4**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 9.7 编写属性测试SCD2 更新不影响手动补录表
- **Property 11: SCD2 更新不影响手动补录表**
- **验证: 需求 5.6**
- 文件:`apps/etl/connectors/feiqiu/tests/unit/test_birthday_properties.py`
- [x] 10. 收尾:主 DDL 合并与文档更新
- [x] 10.1 合并迁移变更到主 DDL 文件
-`birthday` 列定义合并到 `db/etl_feiqiu/schemas/ods.sql``member_profiles` 表)
-`birthday` 列定义合并到 `db/etl_feiqiu/schemas/dwd.sql``dim_member` 表)
- 将新唯一约束合并到 `db/etl_feiqiu/schemas/dws.sql``dws_assistant_monthly_summary` 表)
-`member_birthday_manual` 表定义合并到 `db/zqyy_app/schemas/init.sql`
- 将 FDW 反向映射合并到 `db/fdw/setup_fdw.sql``db/fdw/setup_fdw_test.sql`
- [x] 10.2 更新数据库文档
-`docs/database/` 中新建或更新受影响表的文档:
- `dim_member`:新增 `birthday` 列说明
- `dws_assistant_monthly_summary`:唯一约束变更说明
- `member_birthday_manual`:新建表文档(含 FDW 映射说明)
- 遵循现有 `BD_Manual_*.md` 命名规范
- [x] 10.3 最终验证
- 确保所有测试通过
- 确认主 DDL 文件与测试库实际结构一致
- ask the user if questions arise
## 备注
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
- 每个需求独立可部署,检查点确保增量验证
- 迁移脚本需在 `test_etl_feiqiu` / `test_zqyy_app` 上先行验证
- 属性测试使用 hypothesis每个属性最少 100 次迭代
- 单元测试使用 FakeDB/FakeAPI不依赖真实数据库

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,497 @@
# 设计文档ETL DWS/Flow 重构
## 概述
本设计覆盖飞球 ETL 连接器的四阶段重构:
1. **BaseDwsTask 模板方法重构**:在基类中提供默认 extract()/load(),子类通过声明 `DATE_COL` + 实现 `_do_extract()` 即可完成大部分工作;提取公共辅助方法到 `dws_helpers.py`;财务任务共享提取层;合并 MV 刷新 + 数据清理任务MemberIndexBaseTask 模板方法。
2. **--layers CLI 参数**:新增 `--layers ODS,DWD,DWS,INDEX` 自由组合参数,保留 `--pipeline` 作为快捷别名;去掉硬编码回退,统一走 `TaskRegistry.get_tasks_by_layer()`
3. **任务依赖声明**:在 TaskMeta 中增加 `depends_on` 字段,`_resolve_tasks()` 执行拓扑排序。
4. **关键词重命名**`pipeline → flow`类名、变量名、CLI 参数、日志);`pipelines → connectors`(目录路径)。
执行顺序严格按 1→2→3→4→收尾每阶段完成后运行回归测试。
## 架构
### 当前架构
```
BaseTask (tasks/base_task.py)
└── BaseDwsTask (tasks/dws/base_dws_task.py)
├── 15 个 DWS 子类(各自实现 extract/transform/load
└── BaseIndexTask (tasks/dws/index/base_index_task.py)
└── MemberIndexBaseTask (tasks/dws/index/member_index_base.py)
├── WinbackIndexTask
└── NewconvIndexTask
PipelineRunner (orchestration/pipeline_runner.py)
├── PIPELINE_LAYERS: 7 种固定 Flow 定义
└── _resolve_tasks(): 层→任务映射(含硬编码回退)
TaskRegistry (orchestration/task_registry.py)
└── TaskMeta: task_class, requires_db_config, layer, task_type
```
### 目标架构
```
BaseTask (tasks/base_task.py) [不变]
└── BaseDwsTask (tasks/dws/base_dws_task.py) [新增默认 extract/load]
├── DWS 子类(仅声明 DATE_COL + 实现 _do_extract/transform
├── FinanceBaseTask (tasks/dws/finance_base_task.py) [新增]
│ ├── FinanceDailyTask
│ ├── FinanceRechargeTask
│ ├── FinanceIncomeStructureTask
│ └── FinanceDiscountDetailTask
├── DwsMaintenanceTask (tasks/dws/maintenance_task.py) [合并]
└── BaseIndexTask
└── MemberIndexBaseTask [新增模板 execute]
├── WinbackIndexTask仅实现 _calculate_scores/_save_results
└── NewconvIndexTask
FlowRunner (orchestration/flow_runner.py) [重命名]
├── FLOW_LAYERS: 保留快捷别名
└── _resolve_tasks(): 拓扑排序 + 纯 Registry 解析
TaskRegistry (orchestration/task_registry.py)
└── TaskMeta: + depends_on: list[str]
dws_helpers.py (tasks/dws/dws_helpers.py) [新增]
└── mask_mobile(), calc_days_since(), parse_id_list() 等
目录: apps/etl/connectors/feiqiu/ [重命名]
```
### 变更影响范围
```mermaid
graph TD
A[BaseDwsTask 重构] --> B[15 个 DWS 子类简化]
A --> C[dws_helpers.py 提取]
A --> D[FinanceBaseTask 提取]
A --> E[DwsMaintenanceTask 合并]
A --> F[MemberIndexBaseTask 模板]
G[--layers 参数] --> H[CLI main.py]
G --> I[PipelineRunner._resolve_tasks]
G --> J[去掉硬编码回退]
K[任务依赖] --> L[TaskMeta.depends_on]
K --> M[拓扑排序]
N[关键词重命名] --> O[PipelineRunner → FlowRunner]
N --> P[pipelines → connectors 路径]
N --> Q[所有文档更新]
```
## 组件与接口
### 组件 1BaseDwsTask 默认模板方法
```python
class BaseDwsTask(BaseTask):
DATE_COL: str | None = None # 子类声明日期列名
def _do_extract(self, context: TaskContext) -> list[dict]:
"""子类实现:返回从 DWD 提取的原始行列表。"""
raise NotImplementedError
def extract(self, context: TaskContext) -> dict:
"""默认实现:调用 _do_extract 并包装为标准字典。
子类可覆盖以自定义提取逻辑。
"""
rows = self._do_extract(context)
return {
"rows": rows,
"start_date": context.window_start.date(),
"end_date": context.window_end.date(),
"site_id": context.store_id,
}
def load(self, transformed, context: TaskContext) -> dict:
"""默认实现delete-before-insert 幂等写入。
子类可覆盖以自定义加载逻辑。
"""
if not transformed:
return {"counts": {"fetched": 0, "inserted": 0, "updated": 0, "skipped": 0, "errors": 0}}
date_col = self.DATE_COL or "stat_date"
deleted = self.delete_existing_data(context, date_col=date_col)
inserted = self.bulk_insert(transformed)
return {
"counts": {"fetched": len(transformed), "inserted": inserted, "updated": 0, "skipped": 0, "errors": 0},
"extra": {"deleted": deleted},
}
```
**设计决策**
- `_do_extract()` 而非直接修改 `extract()` 签名,是为了保持向后兼容——已覆盖 `extract()` 的子类无需改动。
- `DATE_COL = None` 作为哨兵值,未声明时 load() 回退到 `"stat_date"` 默认值。
- 子类迁移是渐进式的:先在基类添加默认实现,再逐个子类迁移。
### 组件 2dws_helpers.py 公共辅助模块
```python
# tasks/dws/dws_helpers.py
def mask_mobile(phone: str | None) -> str | None:
"""手机号脱敏138****1234"""
def calc_days_since(target_date: date | None, base_date: date | None = None) -> int | None:
"""计算距今天数"""
def parse_id_list(value: Any) -> set[int]:
"""解析逗号分隔的 ID 列表字符串为 int 集合"""
def safe_division(numerator, denominator, default=Decimal("0")) -> Decimal:
"""安全除法,分母为零时返回默认值"""
```
**设计决策**:使用独立模块而非 Mixin因为这些是纯函数不依赖实例状态。
### 组件 3FinanceBaseTask 共享提取层
```python
class FinanceBaseTask(BaseDwsTask):
"""财务任务共享基类,提供公共数据提取方法。"""
def _extract_settlement_summary(self, site_id, start_date, end_date) -> list[dict]:
"""结算汇总提取(共享 SQL"""
def _extract_recharge_summary(self, site_id, start_date, end_date) -> list[dict]:
"""充值汇总提取"""
def _extract_groupbuy_summary(self, site_id, start_date, end_date) -> list[dict]:
"""团购汇总提取"""
def _extract_platform_summary(self, site_id, start_date, end_date) -> list[dict]:
"""平台结算提取"""
```
**设计决策**使用继承FinanceBaseTask而非 Mixin因为财务任务的提取方法需要访问 `self.db``self.config`,且财务任务形成清晰的子类族。
### 组件 4DwsMaintenanceTask 合并任务
```python
class DwsMaintenanceTask(BaseDwsTask):
"""合并 MV 刷新 + 数据清理为单一维护任务。"""
def get_task_code(self) -> str:
return "DWS_MAINTENANCE"
def extract(self, context): ...
def transform(self, extracted, context): ...
def load(self, transformed, context) -> dict:
stats = {"refreshed": 0, "cleaned": 0}
if self._is_mv_enabled():
stats["refreshed"] = self._refresh_all_views()
if self._is_retention_enabled():
stats["cleaned"] = self._cleanup_all_tables(context)
return {"counts": stats}
```
**设计决策**:合并后的任务内部复用原 BaseMvRefreshTask 和 DwsRetentionCleanupTask 的核心逻辑,但作为单一任务注册和调度。
### 组件 5MemberIndexBaseTask 模板方法
```python
class MemberIndexBaseTask(BaseIndexTask):
def execute(self, cursor_data=None) -> dict:
context = self._build_context(cursor_data)
site_id = self._get_site_id(context)
tenant_id = self._get_tenant_id()
params = self._load_params()
activities = self._build_member_activity(site_id, tenant_id, params)
raw_scores = self._calculate_scores(activities, params, site_id, tenant_id)
normalized = self.batch_normalize_to_display(raw_scores, ...)
result = self._save_results(normalized, site_id, tenant_id, context)
return result
def _calculate_scores(self, activities, params, site_id, tenant_id) -> dict:
raise NotImplementedError
def _save_results(self, normalized, site_id, tenant_id, context) -> dict:
raise NotImplementedError
```
### 组件 6TaskMeta 依赖声明与拓扑排序
```python
@dataclass
class TaskMeta:
task_class: type
requires_db_config: bool = True
layer: str | None = None
task_type: str = "etl"
depends_on: list[str] = field(default_factory=list) # 新增
```
拓扑排序算法Kahn's algorithm
```python
def topological_sort(task_codes: list[str], registry: TaskRegistry) -> list[str]:
"""对任务列表执行拓扑排序。
- 仅对当前执行列表内的任务排序
- depends_on 中引用的任务不在列表内时记录警告
- 检测循环依赖并抛出 ValueError
"""
in_degree = {code: 0 for code in task_codes}
graph = {code: [] for code in task_codes}
task_set = set(task_codes)
for code in task_codes:
meta = registry.get_metadata(code)
if meta and meta.depends_on:
for dep in meta.depends_on:
if dep in task_set:
graph[dep].append(code)
in_degree[code] += 1
else:
logger.warning("任务 %s 依赖 %s,但后者不在当前执行列表中", code, dep)
queue = deque(code for code in task_codes if in_degree[code] == 0)
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
if len(result) != len(task_codes):
cycle_tasks = [c for c in task_codes if c not in result]
raise ValueError(f"检测到循环依赖: {cycle_tasks}")
return result
```
### 组件 7--layers CLI 参数
```python
# cli/main.py 新增参数
parser.add_argument(
"--layers",
help="ETL 层自由组合逗号分隔ODS,DWD,DWS,INDEX",
)
# 互斥校验
if args.layers and args.pipeline:
parser.error("--layers 和 --pipeline/--flow 互斥,请只指定其中一个")
# 层解析
VALID_LAYERS = {"ODS", "DWD", "DWS", "INDEX"}
def parse_layers(raw: str) -> list[str]:
layers = [l.strip().upper() for l in raw.split(",")]
invalid = set(layers) - VALID_LAYERS
if invalid:
raise ValueError(f"无效的层名: {invalid}")
return layers
```
### 组件 8FlowRunner 重命名
重命名映射:
| 原名 | 新名 | 文件 |
|------|------|------|
| `PipelineRunner` | `FlowRunner` | `orchestration/flow_runner.py` |
| `PIPELINE_LAYERS` | `FLOW_LAYERS` | 同上 |
| `pipeline_runner.py` | `flow_runner.py` | 文件名 |
| `--pipeline` | `--flow`(保留 `--pipeline` 弃用别名) | `cli/main.py` |
| 日志中 "Pipeline" / "Flow" | 统一为 "Flow" | 全局 |
### 组件 9路径重命名 pipelines → connectors
```
apps/etl/pipelines/feiqiu/ → apps/etl/connectors/feiqiu/
```
影响范围:
- `pyproject.toml` workspace 成员声明
- 所有 `from pipelines.feiqiu...` 导入ETL 内部使用相对导入,影响较小)
- 脚本中的路径引用(`scripts/``run_etl.bat``run_etl.sh`
- 文档中的路径引用
- `.env` 中的路径配置
- CI/CD 配置(如有)
## 数据模型
### TaskMeta 扩展
```python
@dataclass
class TaskMeta:
task_class: type
requires_db_config: bool = True
layer: str | None = None
task_type: str = "etl"
depends_on: list[str] = field(default_factory=list) # 新增:依赖的任务代码列表
```
### 已知任务依赖关系
| 任务 | 依赖 | 说明 |
|------|------|------|
| `DWS_ASSISTANT_FINANCE` | `DWS_ASSISTANT_SALARY` | 财务分析需要工资计算结果 |
| `DWS_ASSISTANT_MONTHLY` | `DWS_ASSISTANT_DAILY` | 月度汇总基于日度明细 |
| `DWS_MAINTENANCE` | 所有其他 DWS 任务 | MV 刷新和清理应在数据写入后执行 |
| `DWS_WINBACK_INDEX` | `DWS_MEMBER_VISIT`, `DWS_MEMBER_CONSUMPTION` | 指数计算依赖会员行为数据 |
| `DWS_NEWCONV_INDEX` | `DWS_MEMBER_VISIT`, `DWS_MEMBER_CONSUMPTION` | 同上 |
| `DWS_RELATION_INDEX` | `DWS_ASSISTANT_DAILY` | 关系指数依赖助教服务记录 |
### DWS 子类 DATE_COL 映射
| 任务类 | DATE_COL | 目标表 |
|--------|----------|--------|
| AssistantDailyTask | `stat_date` | `dws_assistant_daily_detail` |
| AssistantMonthlyTask | `stat_month` | `dws_assistant_monthly_summary` |
| AssistantCustomerTask | `stat_date` | `dws_assistant_customer_stats` |
| AssistantSalaryTask | `salary_month` | `dws_assistant_salary_calc` |
| AssistantFinanceTask | `stat_date` | `dws_assistant_finance_analysis` |
| MemberConsumptionTask | `stat_date` | `dws_member_consumption_summary` |
| MemberVisitTask | `visit_date` | `dws_member_visit_detail` |
| FinanceDailyTask | `stat_date` | `dws_finance_daily_summary` |
| FinanceRechargeTask | `stat_date` | `dws_finance_recharge_summary` |
| FinanceIncomeStructureTask | `stat_date` | `dws_finance_income_structure` |
| FinanceDiscountDetailTask | `stat_date` | `dws_finance_discount_detail` |
## 正确性属性
*正确性属性是系统在所有合法执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1默认 extract() 返回标准结构
*对于任意* 声明了 DATE_COL 且未覆盖 extract() 的 BaseDwsTask 子类,以及任意合法的 TaskContext调用 extract(context) 应返回包含 "rows"、"start_date"、"end_date"、"site_id" 键的字典,且 "rows" 的值等于 _do_extract(context) 的返回值。
**Validates: Requirements 1.1, 1.4**
### Property 2默认 load() 幂等写入与标准统计
*对于任意* 非空的 transformed 列表和任意合法的 TaskContextBaseDwsTask 默认 load() 应返回包含 "counts" 键的字典,其中 "counts" 包含 "fetched"、"inserted"、"updated"、"skipped"、"errors" 五个整数键,且 "fetched" 等于 len(transformed)。对于空的 transformed 列表,所有计数应为 0。
**Validates: Requirements 1.2, 1.5**
### Property 3dws_helpers 函数等价性
*对于任意* 合法输入dws_helpers 模块中的 mask_mobile()、calc_days_since()、parse_id_list() 函数应产生与原子类内联实现完全相同的输出。具体地:
- *对于任意* 11 位数字字符串mask_mobile() 应返回中间 4 位被 `****` 替换的字符串
- *对于任意* 两个 date 对象target_date, base_datecalc_days_since() 应返回 (base_date - target_date).days
- *对于任意* 包含逗号分隔整数的字符串parse_id_list() 应返回对应的 int 集合
**Validates: Requirements 2.3**
### Property 4DwsMaintenanceTask 配置控制
*对于任意* mv_enabled 和 retention_enabled 的布尔组合DwsMaintenanceTask.load() 应:
- 当 mv_enabled=True 时执行物化视图刷新,否则跳过
- 当 retention_enabled=True 时执行数据清理,否则跳过
- 返回的统计字典始终包含 "refreshed" 和 "cleaned" 键
**Validates: Requirements 4.3, 4.4**
### Property 5--layers 解析正确性
*对于任意* {ODS, DWD, DWS, INDEX} 的非空子集以逗号分隔拼接为字符串后parse_layers() 应返回包含且仅包含该子集元素的列表且元素均为大写。对于包含无效层名的字符串parse_layers() 应抛出 ValueError。
**Validates: Requirements 6.1, 6.2**
### Property 6配置优先级——配置值优先于 Registry
*对于任意* 层名和任意非空的配置任务列表_resolve_tasks() 应返回配置中指定的任务列表,而非 TaskRegistry.get_tasks_by_layer() 的结果。当配置为空时,应返回 Registry 的结果。
**Validates: Requirements 7.2**
### Property 7拓扑排序正确性
*对于任意* 有向无环图DAG表示的任务依赖关系topological_sort() 返回的列表中,每个任务的所有依赖(在当前列表内的)都应排在该任务之前。
**Validates: Requirements 8.3**
### Property 8循环依赖检测
*对于任意* 包含至少一个环的有向图表示的任务依赖关系topological_sort() 应抛出 ValueError且错误信息中包含环中涉及的任务代码。
**Validates: Requirements 8.4**
## 错误处理
### BaseDwsTask 模板方法
| 场景 | 处理方式 |
|------|----------|
| 子类未实现 _do_extract() 且未覆盖 extract() | 抛出 NotImplementedError |
| 子类未声明 DATE_COL | load() 回退到 "stat_date" 默认值 |
| _do_extract() 返回 None | extract() 将 rows 设为空列表 |
| bulk_insert() 失败 | 异常向上传播,由 BaseTask.execute() 的 try/except 捕获并 rollback |
### 拓扑排序
| 场景 | 处理方式 |
|------|----------|
| 循环依赖 | 抛出 ValueError包含循环涉及的任务列表 |
| 依赖任务不在执行列表中 | 记录 WARNING 日志,继续执行 |
| 空任务列表 | 返回空列表 |
### CLI 参数
| 场景 | 处理方式 |
|------|----------|
| --layers 和 --pipeline/--flow 同时指定 | argparse 报错退出 |
| --layers 包含无效层名 | 抛出 ValueError提示有效层名 |
| --pipeline 使用已弃用参数 | 输出 DeprecationWarning正常执行 |
### 路径重命名
| 场景 | 处理方式 |
|------|----------|
| 导入路径未更新 | ImportError需在重命名脚本中全量扫描 |
| 配置文件中的旧路径 | 启动时检查并输出警告 |
## 测试策略
### 测试框架
- 单元测试:`pytest`
- 属性测试:`hypothesis`Python 的属性测试库)
- 测试工具:`apps/etl/pipelines/feiqiu/tests/unit/task_test_utils.py` 提供 FakeDB/FakeAPI
### 属性测试配置
- 每个属性测试最少运行 100 次迭代
- 使用 `@settings(max_examples=100)` 配置
- 每个属性测试用注释标注对应的设计属性编号
- 标注格式:`# Feature: etl-dws-flow-refactor, Property N: <属性标题>`
### 双轨测试方法
**属性测试**(验证普遍性质):
- Property 1-2BaseDwsTask 默认模板方法的返回值结构和行为
- Property 3dws_helpers 函数等价性
- Property 4DwsMaintenanceTask 配置控制
- Property 5--layers 解析正确性
- Property 6配置优先级
- Property 7拓扑排序正确性
- Property 8循环依赖检测
**单元测试**(验证具体示例和边界条件):
- DwsMaintenanceTask 执行顺序(先刷新后清理)
- TaskRegistry 注册项替换(旧任务移除、新任务添加)
- --pipeline 快捷别名映射
- --layers 和 --pipeline 互斥报错
- --pipeline 弃用警告
- 空 Registry 返回空列表(边界条件)
- 依赖任务不在执行列表中的警告(边界条件)
- 路径重命名后的导入正确性
### 测试文件组织
| 测试文件 | 内容 |
|----------|------|
| `tests/unit/test_base_dws_template.py` | Property 1-2 + BaseDwsTask 模板方法单元测试 |
| `tests/unit/test_dws_helpers.py` | Property 3 + dws_helpers 函数单元测试 |
| `tests/unit/test_maintenance_task.py` | Property 4 + DwsMaintenanceTask 单元测试 |
| `tests/unit/test_layers_cli.py` | Property 5 + --layers CLI 参数单元测试 |
| `tests/unit/test_resolve_tasks.py` | Property 6 + _resolve_tasks 配置优先级单元测试 |
| `tests/unit/test_topological_sort.py` | Property 7-8 + 拓扑排序单元测试 |
| `tests/unit/test_flow_rename.py` | FlowRunner 重命名相关单元测试 |
| `tests/test_etl_refactor_properties.py` | Monorepo 级属性测试(根目录) |

View File

@@ -0,0 +1,167 @@
# 需求文档ETL DWS/Flow 重构
## 简介
对 NeoZQYY Monorepo 的飞球 ETL 连接器进行大型重构,涵盖四个主要方向:
1. BaseDwsTask 模板方法重构——消除 DWS 子类中的样板代码
2. `--layers` CLI 参数替代固定 pipeline 名称——提升用户体验
3. 任务依赖声明与拓扑排序——消除隐式依赖风险
4. 关键词重命名pipeline → flow、pipelines → connectors——统一术语与路径
执行顺序严格按 1→2→3→4→收尾每一步完成后进行回归测试。
## 术语表
- **BaseDwsTask**DWS 层任务基类,位于 `tasks/dws/base_dws_task.py`,提供 DWD 数据读取、幂等写入、配置缓存等通用能力
- **BaseIndexTask**INDEX 层指数算法基类,继承 BaseDwsTask位于 `tasks/dws/index/base_index_task.py`
- **MemberIndexBaseTask**:会员指数共享基类,继承 BaseIndexTask位于 `tasks/dws/index/member_index_base.py`
- **TaskRegistry**:任务注册表,维护 task_code → TaskMeta 映射,位于 `orchestration/task_registry.py`
- **TaskMeta**:任务元数据数据类,包含 task_class、requires_db_config、layer、task_type 字段
- **PipelineRunner**Flow 编排器,根据 Flow 定义执行多层 ETL 任务,位于 `orchestration/pipeline_runner.py`
- **TaskExecutor**:单任务执行器,管理游标、运行记录和任务生命周期,位于 `orchestration/task_executor.py`
- **Flow**ETL 编排单元,定义一组按层顺序执行的任务集合(原名 pipeline
- **Layer**ETL 数据处理层级,包括 ODS、DWD、DWS、INDEX
- **Connector**ETL 连接器,对接特定上游 SaaS 的数据抽取模块(原名 pipeline 目录)
- **DATE_COL**DWS 子类声明的日期列名,用于 extract 和 delete_existing_data 的时间过滤
- **TaskContext**:运行期上下文数据类,包含 store_id、window_start/end、window_minutes、cursor
- **拓扑排序**:根据任务间依赖关系确定执行顺序的算法,确保被依赖任务先于依赖方执行
- **幂等**:同一操作执行多次与执行一次效果相同,本系统通过 delete-before-insert 实现
## 需求
### 需求 1BaseDwsTask 默认 extract/load 模板方法
**用户故事:** 作为 ETL 开发者,我希望 BaseDwsTask 提供默认的 extract() 和 load() 实现,以便 DWS 子类只需声明 DATE_COL 并实现 _do_extract() 和 transform(),从而减少每个子类 20-30 行样板代码。
#### 验收标准
1. WHEN 一个 DWS 子类声明了 DATE_COL 类属性且未覆盖 extract()THE BaseDwsTask SHALL 使用 DATE_COL 从 DWD 层按时间窗口提取数据并传递给 transform()
2. WHEN 一个 DWS 子类声明了 DATE_COL 类属性且未覆盖 load()THE BaseDwsTask SHALL 执行 delete_existing_data(date_col=DATE_COL) 后调用 bulk_insert(),并返回标准统计字典
3. WHEN 一个 DWS 子类覆盖了 extract() 或 load()THE BaseDwsTask SHALL 使用子类的覆盖实现而非默认实现
4. WHEN 默认 extract() 执行时THE BaseDwsTask SHALL 调用子类实现的 _do_extract(context) 方法获取原始数据
5. THE BaseDwsTask 默认 load() SHALL 返回包含 fetched、inserted、updated、skipped、errors 键的统计字典
### 需求 2DWS 公共辅助方法提取
**用户故事:** 作为 ETL 开发者,我希望将散落在多个 DWS 子类中的重复辅助方法提取到公共位置,以便消除代码重复并统一行为。
#### 验收标准
1. THE dws_helpers 模块 SHALL 提供 _mask_mobile()、_calc_days_since()、_parse_id_list() 等公共辅助函数
2. WHEN 多个 DWS 子类使用相同的辅助逻辑时THE 子类 SHALL 调用 dws_helpers 中的公共实现而非各自维护副本
3. WHEN dws_helpers 中的辅助函数被调用时THE 函数 SHALL 产生与原子类内联实现完全相同的输出结果
### 需求 3财务任务共享提取层
**用户故事:** 作为 ETL 开发者,我希望财务类 DWS 任务FinanceDailyTask、FinanceRechargeTask、FinanceIncomeStructureTask、FinanceDiscountDetailTask共享数据提取逻辑以便减少重复的 SQL 查询和数据获取代码。
#### 验收标准
1. THE FinanceExtractMixin 或 FinanceBaseTask SHALL 提供财务任务共用的数据提取方法(结算汇总、充值汇总、团购汇总等)
2. WHEN 财务类 DWS 子类执行 extract() 时THE 子类 SHALL 通过共享提取层获取公共数据,仅补充各自特有的提取逻辑
3. WHEN 共享提取层返回数据时THE 数据 SHALL 与原各子类独立提取的结果在数值精度和字段结构上完全一致
### 需求 4MV 刷新与数据清理任务合并
**用户故事:** 作为 ETL 运维人员,我希望将 DWS_MV_REFRESH_FINANCE_DAILY、DWS_MV_REFRESH_ASSISTANT_DAILY 和 DWS_RETENTION_CLEANUP 三个任务合并为一个统一的维护任务,以便简化调度配置和减少任务数量。
#### 验收标准
1. THE 合并后的 DWS_MAINTENANCE 任务 SHALL 在单次执行中完成物化视图刷新和历史数据清理
2. WHEN DWS_MAINTENANCE 任务执行时THE 任务 SHALL 先执行物化视图刷新,再执行数据清理
3. WHEN 物化视图刷新或数据清理功能被配置为禁用时THE DWS_MAINTENANCE 任务 SHALL 跳过对应步骤并记录日志
4. WHEN DWS_MAINTENANCE 任务完成时THE 任务 SHALL 返回包含刷新视图数和清理行数的统计信息
5. THE TaskRegistry SHALL 移除原 DWS_MV_REFRESH_FINANCE_DAILY、DWS_MV_REFRESH_ASSISTANT_DAILY、DWS_RETENTION_CLEANUP 三个注册项,替换为 DWS_MAINTENANCE
### 需求 5MemberIndexBaseTask 模板方法
**用户故事:** 作为 ETL 开发者,我希望 MemberIndexBaseTask 提供模板方法 execute()以便子类WinbackIndexTask、NewconvIndexTask只需实现 _calculate_scores() 和 _save_results(),减少重复的编排代码。
#### 验收标准
1. THE MemberIndexBaseTask SHALL 提供 execute() 模板方法,按顺序执行:获取站点信息 → 加载参数 → 构建会员活动数据 → 调用 _calculate_scores() → 归一化 → 调用 _save_results()
2. WHEN 子类实现 _calculate_scores(member_activities, params) 时THE 方法 SHALL 接收会员活动数据和参数字典,返回原始评分字典
3. WHEN 子类实现 _save_results(normalized_scores, context) 时THE 方法 SHALL 接收归一化后的评分和上下文,完成数据持久化
4. WHEN MemberIndexBaseTask 的 execute() 执行完成时THE 方法 SHALL 返回与原子类 execute() 相同结构的结果字典
### 需求 6--layers CLI 参数
**用户故事:** 作为 ETL 运维人员,我希望使用 `--layers ODS,DWD,DWS,INDEX` 的自由组合方式替代固定的 pipeline 名称,以便更灵活地控制 ETL 执行范围。
#### 验收标准
1. WHEN 用户指定 `--layers ODS,DWD`THE CLI SHALL 解析为 ["ODS", "DWD"] 层列表并按顺序执行对应任务
2. WHEN 用户指定 `--layers` 参数时THE CLI SHALL 接受 ODS、DWD、DWS、INDEX 四个层的任意组合
3. THE CLI SHALL 保留 `--pipeline` 参数作为快捷别名(如 `--pipeline api_full` 等价于 `--layers ODS,DWD,DWS,INDEX`
4. WHEN 用户同时指定 `--layers``--pipeline`THE CLI SHALL 报错并提示两者互斥
5. WHEN `--layers` 包含 DWS 或 INDEX 层时THE PipelineRunner SHALL 跳过完整性校验或仅执行轻量级行数校验
### 需求 7统一层→任务解析
**用户故事:** 作为 ETL 开发者,我希望去掉 _resolve_tasks() 中的硬编码回退列表,统一走 TaskRegistry.get_tasks_by_layer() 获取任务,以便新增任务时只需在 TaskRegistry 注册即可自动纳入 Flow。
#### 验收标准
1. THE PipelineRunner._resolve_tasks() SHALL 仅通过 TaskRegistry.get_tasks_by_layer() 获取各层任务列表,移除所有硬编码回退列表
2. WHEN 配置中指定了 run.ods_tasks / run.dws_tasks / run.index_tasks 时THE _resolve_tasks() SHALL 优先使用配置值
3. WHEN TaskRegistry.get_tasks_by_layer() 返回空列表且无配置覆盖时THE _resolve_tasks() SHALL 记录警告日志并返回空列表
### 需求 8任务依赖声明
**用户故事:** 作为 ETL 开发者,我希望在 TaskMeta 中声明任务间的依赖关系,以便系统自动进行拓扑排序,消除隐式依赖导致的执行顺序错误。
#### 验收标准
1. THE TaskMeta 数据类 SHALL 包含 depends_on: list[str] 字段,默认为空列表
2. WHEN 注册任务时指定 depends_on 时THE TaskRegistry SHALL 存储依赖关系
3. WHEN _resolve_tasks() 生成任务列表时THE PipelineRunner SHALL 对任务列表执行拓扑排序,确保被依赖任务排在依赖方之前
4. WHEN 任务依赖关系中存在循环依赖时THE 拓扑排序 SHALL 抛出明确的错误信息,指出循环涉及的任务
5. WHEN 任务 A 声明 depends_on 包含任务 B且任务 B 不在当前执行列表中时THE 拓扑排序 SHALL 记录警告日志但继续执行
### 需求 9关键词重命名 pipeline → flow
**用户故事:** 作为 ETL 开发者,我希望将代码中所有 "pipeline" 相关术语统一为 "flow",以便术语一致性和代码可读性。
#### 验收标准
1. THE PipelineRunner 类 SHALL 重命名为 FlowRunner
2. THE PIPELINE_LAYERS 常量 SHALL 重命名为 FLOW_LAYERS
3. THE CLI 参数 `--pipeline` SHALL 重命名为 `--flow`,同时保留 `--pipeline` 作为已弃用别名
4. WHEN 用户使用已弃用的 `--pipeline` 参数时THE CLI SHALL 输出弃用警告并正常执行
5. THE 代码中所有 pipeline_runner 模块名 SHALL 重命名为 flow_runner
6. THE 所有日志消息、注释和文档中的 "pipeline" 术语 SHALL 替换为 "flow"
### 需求 10路径重命名 pipelines → connectors
**用户故事:** 作为 ETL 开发者,我希望将 `apps/etl/pipelines` 目录重命名为 `apps/etl/connectors`,以便目录名准确反映其"连接器"语义。
#### 验收标准
1. THE 目录 `apps/etl/pipelines/` SHALL 重命名为 `apps/etl/connectors/`
2. WHEN 路径重命名完成后THE 所有 Python 导入路径 SHALL 更新为使用新路径
3. WHEN 路径重命名完成后THE 所有配置文件、脚本和文档中的旧路径引用 SHALL 更新为新路径
4. WHEN 路径重命名完成后THE pyproject.toml 中的 workspace 成员声明 SHALL 更新为新路径
5. WHEN 路径重命名完成后THE 所有测试 SHALL 通过且无导入错误
### 需求 11回归测试与数据验证
**用户故事:** 作为 ETL 运维人员,我希望重构后的系统通过完整的回归测试,以便确保数据处理无错误和偏移。
#### 验收标准
1. WHEN BaseDwsTask 模板方法重构完成后THE 所有现有 DWS 单元测试 SHALL 通过且无失败
2. WHEN --layers 参数实现完成后THE CLI 参数解析测试 SHALL 覆盖所有合法和非法的层组合
3. WHEN 任务依赖声明实现完成后THE 拓扑排序测试 SHALL 覆盖正常依赖、循环依赖和缺失依赖场景
4. WHEN 关键词和路径重命名完成后THE 所有现有测试 SHALL 通过且无导入错误
5. WHEN 整体重构完成后THE 系统 SHALL 通过端到端 dry-run 测试,验证 ODS→DWD→DWS→INDEX 全链路无异常
### 需求 12文档同步更新
**用户故事:** 作为 ETL 开发者,我希望所有相关文档在重构后同步更新,以便文档与代码保持一致。
#### 验收标准
1. WHEN 重构完成后THE `docs/etl-feiqiu-architecture.md` SHALL 反映所有类名、方法名和术语变更
2. WHEN 重构完成后THE `apps/etl/pipelines/feiqiu/docs/` 下的所有文档 SHALL 更新路径引用和术语
3. WHEN 重构完成后THE CLI 帮助文本和示例 SHALL 反映新的 `--layers``--flow` 参数
4. WHEN 重构完成后THE `tasks/README.md` SHALL 更新任务列表和继承关系说明

View File

@@ -0,0 +1,190 @@
# 实施计划ETL DWS/Flow 重构
## 概述
按 4 个阶段顺序实施BaseDwsTask 模板方法重构 → --layers CLI 参数 → 任务依赖声明 → 关键词/路径重命名。每个阶段完成后运行回归测试。
## 任务
- [x] 1. BaseDwsTask 默认模板方法
- [x] 1.1 在 BaseDwsTask 中添加 DATE_COL 类属性和默认 extract()/load() 实现
- 添加 `DATE_COL: str | None = None` 类属性
- 添加 `_do_extract(self, context) -> list[dict]` 抽象方法raise NotImplementedError
- 实现默认 `extract()`:调用 `_do_extract()` 并包装为标准字典
- 实现默认 `load()`delete_existing_data + bulk_insert返回标准统计字典
- _Requirements: 1.1, 1.2, 1.4, 1.5_
- [x] 1.2 编写 BaseDwsTask 默认模板方法的属性测试
- **Property 1: 默认 extract() 返回标准结构**
- **Validates: Requirements 1.1, 1.4**
- **Property 2: 默认 load() 幂等写入与标准统计**
- **Validates: Requirements 1.2, 1.5**
- [x] 1.3 迁移 DWS 子类使用默认模板方法
- 为每个 DWS 子类声明 DATE_COL
- 将各子类的 extract() 逻辑迁移到 _do_extract()
- 移除与默认 load() 行为一致的子类 load() 覆盖
- 保留有自定义逻辑的子类覆盖(如 AssistantSalaryTask 的月度删除)
- 涉及文件assistant_daily_task.py, assistant_monthly_task.py, assistant_customer_task.py, assistant_finance_task.py, member_consumption_task.py, member_visit_task.py, finance_daily_task.py, finance_recharge_task.py, finance_income_task.py, finance_discount_task.py
- _Requirements: 1.1, 1.2, 1.3_
- [x] 1.4 运行现有 DWS 单元测试确认无回归
- `cd apps/etl/pipelines/feiqiu && pytest tests/unit/test_dws_tasks.py -v`
- _Requirements: 11.1_
- [x] 2. 公共辅助方法提取与财务基类
- [x] 2.1 创建 dws_helpers.py 公共辅助模块
- 创建 `tasks/dws/dws_helpers.py`
- 提取 mask_mobile()、calc_days_since()、parse_id_list()、safe_division() 等函数
- 更新各 DWS 子类的导入,替换内联实现为 dws_helpers 调用
- _Requirements: 2.1, 2.2_
- [x] 2.2 编写 dws_helpers 函数等价性属性测试
- **Property 3: dws_helpers 函数等价性**
- **Validates: Requirements 2.3**
- [x] 2.3 创建 FinanceBaseTask 共享提取层
- 创建 `tasks/dws/finance_base_task.py`
- 从 FinanceDailyTask 提取共享方法_extract_settlement_summary, _extract_recharge_summary, _extract_groupbuy_summary, _extract_platform_summary
- 迁移 FinanceDailyTask, FinanceRechargeTask, FinanceIncomeStructureTask, FinanceDiscountDetailTask 继承 FinanceBaseTask
- _Requirements: 3.1, 3.2, 3.3_
- [x] 3. MV 刷新与数据清理合并 + MemberIndexBaseTask 模板
- [x] 3.1 创建 DwsMaintenanceTask 合并任务
- 创建 `tasks/dws/maintenance_task.py`
- 合并 BaseMvRefreshTask 和 DwsRetentionCleanupTask 的核心逻辑
- 在 TaskRegistry 中注册 DWS_MAINTENANCE移除原三个任务注册
- 更新 `tasks/dws/__init__.py` 导出
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
- [x] 3.2 编写 DwsMaintenanceTask 属性测试和单元测试
- **Property 4: DwsMaintenanceTask 配置控制**
- **Validates: Requirements 4.3, 4.4**
- 单元测试:执行顺序(先刷新后清理)、注册项替换
- [x] 3.3 重构 MemberIndexBaseTask 模板方法
- 在 MemberIndexBaseTask 中实现 execute() 模板方法
- 添加 _calculate_scores() 和 _save_results() 抽象方法
- 迁移 WinbackIndexTask 和 NewconvIndexTask 使用新模板
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [x] 4. 检查点 - 阶段 1 回归测试
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit -v` 确保所有测试通过
- 确保所有测试通过,如有问题请询问用户
- [x] 5. --layers CLI 参数与统一层解析
- [x] 5.1 实现 --layers CLI 参数
- 在 cli/main.py 中添加 `--layers` 参数
- 实现 parse_layers() 函数:解析逗号分隔的层名,校验合法性
- 添加 --layers 和 --pipeline 互斥校验
- 更新 main() 函数:当指定 --layers 时,构造层列表传递给 PipelineRunner
- _Requirements: 6.1, 6.2, 6.3, 6.4_
- [x] 5.2 编写 --layers 解析属性测试
- **Property 5: --layers 解析正确性**
- **Validates: Requirements 6.1, 6.2**
- [x] 5.3 统一 _resolve_tasks() 去掉硬编码回退
- 移除 _resolve_tasks() 中所有硬编码回退列表
- 统一走 TaskRegistry.get_tasks_by_layer() 获取任务
- 保留配置优先级run.ods_tasks / run.dws_tasks / run.index_tasks > Registry
- 空 Registry + 无配置时记录警告并返回空列表
- _Requirements: 7.1, 7.2, 7.3_
- [x] 5.4 编写配置优先级属性测试
- **Property 6: 配置优先级——配置值优先于 Registry**
- **Validates: Requirements 7.2**
- [x] 5.5 实现 DWS/INDEX 层轻量级校验
- 当 --layers 包含 DWS 或 INDEX 时,跳过完整性校验或仅执行行数校验
- _Requirements: 6.5_
- [x] 6. 任务依赖声明与拓扑排序
- [x] 6.1 扩展 TaskMeta 添加 depends_on 字段
- 在 TaskMeta 数据类中添加 `depends_on: list[str] = field(default_factory=list)`
- 更新 TaskRegistry.register() 接受 depends_on 参数
- 为已知依赖关系添加声明DWS_ASSISTANT_FINANCE → DWS_ASSISTANT_SALARY 等)
- _Requirements: 8.1, 8.2_
- [x] 6.2 实现拓扑排序函数
- 创建 `orchestration/topological_sort.py`
- 实现 Kahn's algorithm 拓扑排序
- 处理循环依赖检测(抛出 ValueError
- 处理缺失依赖警告(记录日志继续执行)
- 在 PipelineRunner._resolve_tasks() 中集成拓扑排序
- _Requirements: 8.3, 8.4, 8.5_
- [x] 6.3 编写拓扑排序属性测试
- **Property 7: 拓扑排序正确性**
- **Validates: Requirements 8.3**
- **Property 8: 循环依赖检测**
- **Validates: Requirements 8.4**
- [x] 7. 检查点 - 阶段 2+3 回归测试
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit -v` 确保所有测试通过
- 运行 `cd C:\NeoZQYY && pytest tests/ -v` 确保 Monorepo 属性测试通过
- 确保所有测试通过,如有问题请询问用户
- [x] 8. 关键词重命名 pipeline → flow
- [x] 8.1 重命名 PipelineRunner → FlowRunner
-`orchestration/pipeline_runner.py` 重命名为 `orchestration/flow_runner.py`
- 类名 PipelineRunner → FlowRunner
- 常量 PIPELINE_LAYERS → FLOW_LAYERS
- 更新所有导入引用task_executor.py, cli/main.py, scheduler.py 等)
- _Requirements: 9.1, 9.2, 9.5_
- [x] 8.2 更新 CLI 参数 --pipeline → --flow
-`--pipeline` 重命名为 `--flow`
- 保留 `--pipeline` 作为已弃用别名(使用 argparse dest 映射)
- 使用已弃用参数时输出 DeprecationWarning
- 更新 --layers 互斥校验同时检查 --flow 和 --pipeline
- _Requirements: 9.3, 9.4_
- [x] 8.3 更新所有日志消息和注释中的 pipeline 术语
- 全局搜索替换日志中的 "Pipeline" / "pipeline" → "Flow" / "flow"
- 更新代码注释中的术语
- _Requirements: 9.6_
- [x] 9. 路径重命名 pipelines → connectors
- [x] 9.1 重命名目录 apps/etl/pipelines → apps/etl/connectors
- 执行目录重命名
- 更新 pyproject.toml workspace 成员声明
- 更新所有 Python 导入路径
- _Requirements: 10.1, 10.2, 10.4_
- [x] 9.2 更新所有配置文件、脚本和文档中的路径引用
- 更新 .env / .env.template 中的路径
- 更新 run_etl.bat / run_etl.sh 中的路径
- 更新 scripts/ 目录下引用旧路径的脚本
- _Requirements: 10.3_
- [x] 9.3 运行全量测试确认路径重命名无回归
- `cd apps/etl/connectors/feiqiu && pytest tests/unit -v`
- `cd C:\NeoZQYY && pytest tests/ -v`
- _Requirements: 10.5_
- [x] 10. 文档同步更新
- [x] 10.1 更新架构文档和模块文档
- 更新 `docs/etl-feiqiu-architecture.md`:类名、方法名、术语、路径
- 更新 `apps/etl/connectors/feiqiu/docs/` 下所有文档
- 更新 `tasks/README.md`:任务列表和继承关系
- _Requirements: 12.1, 12.2, 12.4_
- [x] 10.2 更新 CLI 帮助文本和示例
- 更新 cli/main.py 中的 epilog 示例
- 更新 argparse 帮助文本反映 --layers 和 --flow 参数
- _Requirements: 12.3_
- [x] 11. 最终检查点 - 全量回归测试
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit -v`
- 运行 `cd C:\NeoZQYY && pytest tests/ -v`
- 确保所有测试通过,如有问题请询问用户
- _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_
## 备注
- 标记 `*` 的子任务为可选,可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯性
- 检查点确保增量验证
- 属性测试验证普遍正确性属性,单元测试验证具体示例和边界条件
- 阶段 4关键词/路径重命名)风险最高,建议在独立 Git 分支上执行

View File

@@ -0,0 +1,126 @@
# 设计文档ETL 全流程前后端联调etl-fullstack-integration
## 概述
本 Spec 是一个运维联调任务,不涉及新功能开发。目标是验证 `admin-web-console` Spec 产出的前后端代码在真实环境下的端到端正确性,同时收集性能数据。
核心流程:
1. 启动后端 + 前端服务
2. 通过 API 登录获取 JWT
3. 提交全流程 ETL 任务api_full, full_window, force-full, 全选常用任务, 自定义窗口 2025-11-01~2026-02-20, 30天切分, 全部门店)
4. 实时监控执行过程,捕获错误/警告
5. 执行完成后生成综合报告
## 架构
```
联调脚本 (scripts/ops/)
├── 1. 启动服务
│ ├── uvicorn app.main:app (后端 :8000)
│ └── pnpm dev (前端 :5173)
├── 2. API 调用链
│ ├── POST /api/auth/login → JWT
│ ├── GET /api/tasks/registry → 任务列表
│ ├── GET /api/tasks/sync-check → 同步检查
│ ├── POST /api/tasks/validate → CLI 预览
│ └── POST /api/execution/run → 触发执行
├── 3. 监控循环
│ ├── GET /api/execution/queue → 状态轮询
│ ├── GET /api/execution/{id}/logs → 日志获取
│ └── 错误/警告检测
└── 4. 报告生成
└── 输出到 SYSTEM_LOG_ROOT
```
## 任务参数
根据用户需求,联调任务的具体参数:
```python
INTEGRATION_TASK_CONFIG = {
"flow": "api_full", # 全流程API → ODS → DWD → DWS → INDEX
"processing_mode": "full_window", # 全窗口处理
"window_mode": "custom", # 自定义时间范围
"window_start": "2025-11-01 00:00",
"window_end": "2026-02-20 00:00",
"window_split": "day", # 按天切分
"window_split_days": 30, # 30天一个切片
"force_full": True, # 强制全量
"dry_run": False,
"tasks": [ # 全选 is_common=True 的任务
# ODS 层22 个)
"ODS_ASSISTANT_ACCOUNT", "ODS_ASSISTANT_LEDGER", "ODS_ASSISTANT_ABOLISH",
"ODS_SETTLEMENT_RECORDS", "ODS_TABLE_USE", "ODS_TABLE_FEE_DISCOUNT",
"ODS_TABLES", "ODS_PAYMENT", "ODS_REFUND", "ODS_PLATFORM_COUPON",
"ODS_MEMBER", "ODS_MEMBER_CARD", "ODS_MEMBER_BALANCE", "ODS_RECHARGE_SETTLE",
"ODS_GROUP_PACKAGE", "ODS_GROUP_BUY_REDEMPTION",
"ODS_INVENTORY_STOCK", "ODS_INVENTORY_CHANGE",
"ODS_GOODS_CATEGORY", "ODS_STORE_GOODS", "ODS_STORE_GOODS_SALES", "ODS_TENANT_GOODS",
# DWD 层1 个常用)
"DWD_LOAD_FROM_ODS",
# DWS 层15 个常用,排除 DWS_MAINTENANCE
"DWS_BUILD_ORDER_SUMMARY", "DWS_ASSISTANT_DAILY", "DWS_ASSISTANT_MONTHLY",
"DWS_ASSISTANT_CUSTOMER", "DWS_ASSISTANT_SALARY", "DWS_ASSISTANT_FINANCE",
"DWS_MEMBER_CONSUMPTION", "DWS_MEMBER_VISIT",
"DWS_FINANCE_DAILY", "DWS_FINANCE_RECHARGE", "DWS_FINANCE_INCOME_STRUCTURE",
"DWS_FINANCE_DISCOUNT_DETAIL",
"DWS_GOODS_STOCK_DAILY", "DWS_GOODS_STOCK_WEEKLY", "DWS_GOODS_STOCK_MONTHLY",
# INDEX 层3 个常用,排除 DWS_ML_MANUAL_IMPORT
"DWS_WINBACK_INDEX", "DWS_NEWCONV_INDEX", "DWS_RELATION_INDEX",
],
# store_id 由后端从 JWT 注入(默认管理员 site_id=1
# 注意:用户要求"全部门店",但当前系统只有 site_id=1后续多门店需逐个执行
}
```
## 监控策略
- 轮询间隔30 秒
- 最长等待30 分钟(无新日志输出时)
- 错误检测:日志行匹配 `ERROR``CRITICAL``Traceback``Exception`
- 警告检测:日志行匹配 `WARNING``WARN`
- 计时解析:从日志中提取时间戳,计算阶段耗时
## 报告格式
报告输出为 Markdown 文件,路径:`{SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md`
```markdown
# ETL 全流程联调报告
## 执行概要
- 任务参数:...
- 开始时间 / 结束时间 / 总时长
- 退出码 / 最终状态
## 性能报告
- 各窗口切片耗时对比表
- Top-5 耗时阶段
- 总体吞吐量估算
## DEBUG 报告(如有)
- 错误摘要
- 警告摘要
- 相关日志片段
- 可能的原因分析
```
## 正确性属性
本 Spec 为运维联调任务,不涉及新功能代码开发,因此不定义形式化的属性测试。验证通过以下方式进行:
- 服务健康检查通过
- 任务提交成功并开始执行
- 执行完成后退出码和日志符合预期
- 报告文件成功生成
## 测试策略
本 Spec 本身就是一次集成测试。不额外编写单元测试或属性测试。验证标准:
- 后端 API 响应正确
- ETL CLI 子进程正常启动和执行
- 日志正确捕获和推送
- 报告文件正确生成到 SYSTEM_LOG_ROOT

View File

@@ -0,0 +1,70 @@
# 需求文档ETL 全流程前后端联调etl-fullstack-integration
## 简介
基于已完成的 `admin-web-console` Spec 产出的前后端代码,进行一次完整的端到端联调验证。通过管理后台 API 提交 ETL 全流程任务api_full覆盖 ODS → DWD → DWS → INDEX 全链路,验证前后端协作、子进程执行、日志推送、错误处理等环节的正确性。同时收集精细计时数据,定位性能瓶颈。
## 术语表
- **联调**:将 admin-web 后端 API 与 feiqiu ETL CLI 串联,通过真实 API 调用触发 ETL 全流程执行
- **全选常用任务**:任务注册表中 `is_common=True` 的所有任务(排除工具类、手动导入、维护类等 `is_common=False` 的任务)
- **精细计时**:在 ETL 执行过程中,通过日志解析或 CLI 输出记录每个阶段ODS/DWD/DWS/INDEX和每个子任务的耗时
## 需求
### 需求 1服务启动与健康检查
**用户故事:** 作为开发者,我希望一键启动后端和前端服务,并确认服务健康可用,以便开始联调。
#### 验收标准
1. WHEN 启动后端服务, THE Backend_API SHALL 在 `http://localhost:8000` 上响应请求
2. WHEN 启动前端服务, THE Admin_Web SHALL 在 `http://localhost:5173` 上可访问
3. WHEN 调用 `POST /api/auth/login`, THE Backend_API SHALL 返回有效的 JWT 令牌
4. WHEN 调用 `GET /api/tasks/registry`, THE Backend_API SHALL 返回非空的任务注册表
5. WHEN 调用 `GET /api/tasks/sync-check`, THE Backend_API SHALL 确认后端任务注册表与 ETL 真实注册表同步
### 需求 2全流程任务提交与执行
**用户故事:** 作为开发者,我希望通过后端 API 提交一个覆盖全链路的 ETL 任务验证任务配置、CLI 构建、子进程执行的完整流程。
#### 验收标准
1. WHEN 提交 TaskConfigapi_full + full_window + 全选常用任务 + 自定义窗口 2025-11-01~2026-02-20 + 30天切分 + force-full, THE Backend_API SHALL 验证配置有效并返回 CLI 命令预览
2. WHEN 提交任务到执行队列, THE Backend_API SHALL 创建队列任务并自动开始执行
3. WHILE ETL 子进程运行中, THE Backend_API SHALL 通过 WebSocket 推送实时日志
4. WHEN ETL 子进程完成, THE Backend_API SHALL 记录退出码、执行时长、完整日志到 task_execution_log
### 需求 3执行监控与错误处理
**用户故事:** 作为开发者,我希望在任务执行过程中实时监控状态,对报错或警告及时发现并进行 DEBUG。
#### 验收标准
1. WHILE 任务执行中, THE 监控脚本 SHALL 每 30 秒轮询执行状态和日志
2. WHEN 日志中出现 ERROR 或 WARNING 级别信息, THE 监控脚本 SHALL 立即记录并标记
3. WHEN 任务执行完成(成功或失败), THE 监控脚本 SHALL 停止轮询并收集最终状态
4. IF 任务执行超过 30 分钟无新日志输出, THEN THE 监控脚本 SHALL 报告超时警告
5. IF 任务执行失败, THEN THE 监控脚本 SHALL 收集完整的 stderr 和错误上下文
### 需求 4性能计时与瓶颈分析
**用户故事:** 作为开发者,我希望获得精细粒度的执行计时数据,以便发现性能瓶颈。
#### 验收标准
1. WHEN 任务执行完成, THE 报告 SHALL 包含总执行时长
2. WHEN 日志中包含阶段性时间戳, THE 报告 SHALL 解析并展示每个窗口切片的耗时
3. THE 报告 SHALL 标注耗时最长的 Top-5 阶段/任务
4. THE 报告 SHALL 包含每个窗口切片30天的独立耗时对比
### 需求 5联调报告输出
**用户故事:** 作为开发者,我希望联调完成后获得一份综合报告,包含执行情况、性能数据和可能的 DEBUG 信息。
#### 验收标准
1. THE 报告 SHALL 包含:执行概要(任务参数、开始/结束时间、总时长、退出码)
2. THE 报告 SHALL 包含性能报告各阶段耗时、窗口切片耗时对比、Top-5 瓶颈)
3. IF 执行过程中出现错误或警告, THEN THE 报告 SHALL 包含 DEBUG 报告(错误摘要、相关日志片段、可能的原因分析)
4. THE 报告 SHALL 输出到 `SYSTEM_LOG_ROOT` 环境变量指定的目录

View File

@@ -0,0 +1,86 @@
# 实现计划ETL 全流程前后端联调etl-fullstack-integration
## 概述
基于 `admin-web-console` 已完成的前后端代码,进行端到端联调验证。通过后端 API 提交 api_full 全流程 ETL 任务(自定义窗口 2025-11-01~2026-02-2030天切分force-full全选常用任务实时监控执行过程收集性能数据最终生成综合报告。
## 任务
- [ ] 1. 服务启动与健康检查
- [ ] 1.1 启动后端服务(`apps/backend/`uvicorn :8000确认 API 可达
- 启动 `uvicorn app.main:app --host 0.0.0.0 --port 8000`cwd 为 `apps/backend/`
- 验证 `GET /api/tasks/flows` 返回 200
- _Requirements: 1.1_
- [ ] 1.2 启动前端服务(`apps/admin-web/`pnpm dev :5173确认页面可访问
- 启动 `pnpm dev`cwd 为 `apps/admin-web/`
- 验证 `http://localhost:5173` 可达
- _Requirements: 1.2_
- [ ] 1.3 API 健康检查:登录获取 JWT验证任务注册表执行 sync-check
- `POST /api/auth/login` 使用默认管理员账号admin / admin123获取 JWT
- `GET /api/tasks/registry` 确认返回非空任务列表
- `GET /api/tasks/sync-check` 确认 `in_sync=true`(后端注册表与 ETL 真实注册表一致)
- 如果 sync-check 不一致,记录差异并向用户报告
- _Requirements: 1.3, 1.4, 1.5_
- [ ] 2. 全流程任务提交
- [ ] 2.1 构建 TaskConfig 并调用 validate 预览 CLI 命令
- 构建 TaskConfigflow=api_full, processing_mode=full_window, window_mode=custom, window_start="2025-11-01 00:00", window_end="2026-02-20 00:00", window_split=day, window_split_days=30, force_full=True, tasks=全选 is_common=True 的任务
- 调用 `POST /api/tasks/validate` 验证配置有效
- 记录生成的 CLI 命令预览,确认参数完整(--flow api_full --processing-mode full_window --window-start ... --window-end ... --window-split day --window-split-days 30 --force-full --store-id 1 --tasks ...
- _Requirements: 2.1_
- [ ] 2.2 提交任务执行(`POST /api/execution/run`),记录 execution_id
- 调用 `POST /api/execution/run` 提交任务
- 记录返回的 execution_id
- 确认任务状态变为 running
- _Requirements: 2.2, 2.4_
- [ ] 3. 执行监控与 DEBUG
- [ ] 3.1 启动监控循环:每 30 秒轮询状态和日志,检测错误/警告,最长等待 30 分钟
- 轮询 `GET /api/execution/queue` 检查任务状态
- 轮询 `GET /api/execution/{id}/logs` 获取增量日志
- 检测日志中的 ERROR / CRITICAL / Traceback / Exception / WARNING
- 记录每次轮询的时间戳和日志增量行数
- 如果连续 30 分钟无新日志输出,报告超时警告
- 任务完成success/failed/cancelled时停止轮询
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
- [ ] 3.2 对执行过程中发现的错误/警告进行 DEBUG 分析
- 收集所有 ERROR 和 WARNING 日志行及其上下文(前后各 5 行)
- 分析错误类型API 超时、数据库连接、数据质量、配置问题等
- 如果任务失败,获取完整 stderr 并分析根因
- 记录 DEBUG 发现到报告中
- _Requirements: 3.2, 3.5_
- [ ] 4. 性能计时与报告生成
- [ ] 4.1 解析执行日志,提取精细计时数据
- 从日志中提取每个窗口切片30天的开始/结束时间
- 计算每个切片的耗时
- 识别 ODS / DWD / DWS / INDEX 各阶段的耗时
- 标注 Top-5 耗时最长的阶段/任务
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [ ] 4.2 生成综合联调报告,输出到 SYSTEM_LOG_ROOT
- 报告包含:执行概要(参数、时间、退出码)
- 报告包含性能报告各切片耗时对比、Top-5 瓶颈)
- 报告包含DEBUG 报告(如有错误/警告)
- 输出路径:`{SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md`
- 路径通过 `SYSTEM_LOG_ROOT` 环境变量获取,缺失时报错
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [ ] 5. 服务清理
- [ ] 5.1 停止后端和前端服务,清理资源
- 停止 uvicorn 进程
- 停止 pnpm dev 进程
- 报告联调完成状态
## 说明
- 本 Spec 为运维联调任务,不涉及新功能代码开发
- 不编写属性测试或单元测试,联调本身即为集成验证
- 全选常用任务 = 任务注册表中 `is_common=True` 的所有任务(共 41 个)
- "全部门店":当前系统仅有 site_id=1默认管理员绑定如需多门店需逐个执行
- 监控允许空闲等待,最长 30 分钟无新日志才报超时
- 报告输出路径遵循 export-paths 规范,通过 SYSTEM_LOG_ROOT 环境变量获取

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,374 @@
# 设计文档ETL 管道全流程调试与 Debug
## 概述
本设计覆盖 `apps/etl/pipelines/feiqiu/` 下 ETL 管道的全流程调试,采用五阶段策略:
1. **分层单元调试**逐层ODS → DWD → DWS → INDEX使用 FakeDB/FakeAPI 和真实数据库验证任务逻辑
2. **全量数据刷新**:执行 2026-01-01 至 2026-02-16 的 `api_full` Flow内嵌性能计时边执行边 Debug发现问题即修复并重试
3. **黑盒数据校验**:从 API 源数据出发,逐层对比各 Schema 各表的记录数、金额汇总、可疑值检测和抽样逐字段比对
4. **架构优化分析**:分析 ETL 整体结构,在保证稳定和正确的基础上提出精简架构的建议,生成架构报告
5. **报告生成**:基于全量刷新阶段采集的计时数据生成性能分析报告,汇总所有调试结果生成 Debug 报告
性能计时从全量刷新阶段开始就嵌入采集(每任务/每层/API 调用耗时),报告阶段仅做分析和输出,不再单独重跑流程采集数据。
调试使用 `.env` 中配置的真实 API 和数据库连接test_etl_feiqiu所有发现的问题和修复记录在结构化 Debug 报告中。性能计时在全量刷新阶段实时采集,报告阶段读取中间数据做分析输出。
## 架构
### 调试流程架构
```mermaid
flowchart TD
A[阶段1: 分层单元调试] --> B[阶段2: 全量数据刷新<br/>内嵌性能计时]
B --> C[阶段3: 黑盒数据校验]
C --> D[阶段4: 架构优化分析]
D --> E[阶段5: 报告生成<br/>含性能分析]
subgraph 阶段1 [分层单元调试]
A1[ODS 层: API抓取 → 表写入] --> A2[DWD 层: ODS → DWD 清洗装载]
A2 --> A3[DWS 层: DWD → DWS 汇总]
A3 --> A4[INDEX 层: 指数算法计算]
A4 --> A5[编排层: PipelineRunner + TaskExecutor]
A5 --> A6[CLI 入口: 参数解析 + 模式切换]
A6 --> A7[配置体系: AppConfig 加载合并]
end
subgraph 阶段2 [全量数据刷新 - 内嵌计时 + 边执行边Debug]
B1[api_full Flow 执行<br/>每任务/每层计时] --> B2{发现Bug?}
B2 -->|是| B3[修复Bug]
B3 --> B4[重试失败的层/任务]
B4 --> B2
B2 -->|否| B5[increment_verify 校验]
B5 --> B6[自动补齐缺失数据]
B6 --> B7[输出计时 JSON 中间文件]
end
subgraph 阶段3 [黑盒数据校验]
C1[API → ODS 记录数对比] --> C2[ODS → DWD 记录数+金额对比]
C2 --> C3[DWD → DWS 聚合一致性]
C3 --> C4[可疑值检测: 边缘值/空值/重复]
C4 --> C5[抽样100条逐字段比对]
end
subgraph 阶段4 [架构优化分析]
D1[代码结构分析] --> D2[冗余识别]
D2 --> D3[精简建议]
end
subgraph 阶段5 [报告生成 - 含性能分析]
E1[读取计时 JSON] --> E2[瓶颈识别 + 优化建议]
E2 --> E3[汇总 Debug 报告]
end
```
### 现有系统架构
```mermaid
flowchart LR
CLI[cli/main.py] --> PR[PipelineRunner]
CLI --> TE[TaskExecutor]
PR --> TE
TE --> TR[TaskRegistry<br/>52个任务]
TE --> CM[CursorManager]
TE --> RT[RunTracker]
TR --> ODS[ODS 任务<br/>BaseOdsTask × 23]
TR --> DWD[DWD 任务<br/>DwdLoadTask]
TR --> DWS[DWS 任务<br/>BaseDwsTask × 15]
TR --> IDX[INDEX 任务 × 4]
TR --> UTL[工具类任务 × 7]
ODS --> API[APIClient<br/>上游 SaaS]
ODS --> DB[(PostgreSQL<br/>ods.*)]
DWD --> DB
DWS --> DB
IDX --> DB
QC[IntegrityChecker] --> API
QC --> DB
```
## 组件与接口
### 调试脚本组件
调试通过一组 Python 脚本实现,放置在 `apps/etl/pipelines/feiqiu/scripts/debug/` 目录下:
| 脚本 | 职责 | 对应需求 |
|------|------|----------|
| `debug_ods.py` | ODS 层逐任务调试:连接真实 API 和 DB执行单个 ODS 任务并验证写入结果 | 需求 1, 9 |
| `debug_dwd.py` | DWD 层调试:执行 DWD_LOAD_FROM_ODS 并验证 TABLE_MAP 中每对映射的记录数 | 需求 2, 9 |
| `debug_dws.py` | DWS 层调试:逐个执行 DWS 任务并验证汇总结果 | 需求 3, 9 |
| `debug_index.py` | INDEX 层调试:执行 4 个指数任务并验证计算结果 | 需求 4, 9 |
| `debug_orchestration.py` | 编排层调试:验证 PipelineRunner 和 TaskExecutor 的流程控制 | 需求 5 |
| `run_full_refresh.py` | 全量刷新:执行 2026-01-01 ~ 2026-02-16 的 api_full内嵌性能计时边执行边 Debug发现问题即修复重试输出计时 JSON | 需求 10, 13 |
| `debug_blackbox.py` | 黑盒校验:从 API 源数据逐层对比各表数据完整性 + 可疑值检测 + 抽样逐字段比对 | 需求 12 |
| `analyze_performance.py` | 性能分析:读取全量刷新采集的计时 JSON统计耗时识别瓶颈生成性能优化报告 | 需求 13 |
| `analyze_architecture.py` | 架构分析:分析代码结构,识别冗余和可精简点,生成架构优化报告 | 需求 14 |
| `generate_report.py` | 报告生成:汇总所有调试结果生成 Markdown 报告 | 需求 11 |
### 关键接口
```python
# 调试脚本的统一入口接口
class DebugResult:
"""单个调试步骤的结果"""
layer: str # "ODS" / "DWD" / "DWS" / "INDEX" / "ORCHESTRATION"
task_code: str # 任务代码
status: str # "PASS" / "FAIL" / "WARN" / "ERROR"
message: str # 结果描述
details: dict # 详细信息(记录数、错误堆栈等)
fix_applied: str | None # 已应用的修复措施
class BlackboxCheckResult:
"""黑盒校验单表结果"""
layer: str # "API_ODS" / "ODS_DWD" / "DWD_DWS"
source_table: str # 源表名
target_table: str # 目标表名
source_count: int # 源记录数
target_count: int # 目标记录数
count_diff: int # 差异数
amount_diffs: list # 金额差异列表
missing_keys: list # 缺失记录主键
mismatch_count: int # 内容不一致数
```
## 数据模型
### Debug 报告结构
```python
@dataclass
class DebugReport:
"""Debug 报告数据模型"""
title: str # 报告标题
generated_at: datetime # 生成时间
environment: dict # 环境信息DB DSN, API base, store_id
# 分层调试结果
ods_results: list[DebugResult] # ODS 层调试结果
dwd_results: list[DebugResult] # DWD 层调试结果
dws_results: list[DebugResult] # DWS 层调试结果
index_results: list[DebugResult] # INDEX 层调试结果
orchestration_results: list[DebugResult] # 编排层调试结果
# 黑盒校验结果
blackbox_results: list[BlackboxCheckResult]
# 全量刷新结果
full_refresh: dict # {status, layers, counts, duration}
verification: dict # {status, total_tables, consistent, backfilled}
# 性能分析
performance_report: dict # {layer_timings, bottlenecks, recommendations}
# 架构优化
architecture_report: dict # {structure_analysis, redundancies, simplification_suggestions}
# 汇总
total_issues: int # 发现的问题总数
fixed_issues: int # 已修复的问题数
remaining_issues: int # 遗留问题数
```
### 黑盒校验数据流
```mermaid
flowchart TD
API[上游 API] -->|iter_paginated| COUNT_API[API 记录数]
API -->|抽样100条| SAMPLE[抽样记录集]
ODS[(ods.* 表)] -->|SELECT COUNT| COUNT_ODS[ODS 记录数]
ODS -->|可疑值检测| SUSPECT[可疑值: 边缘值/空值/重复]
DWD[(dwd.* 表)] -->|SELECT COUNT| COUNT_DWD[DWD 记录数]
DWS[(dws.* 表)] -->|SELECT SUM| SUM_DWS[DWS 汇总值]
COUNT_API --> CMP1{API vs ODS<br/>记录数对比}
COUNT_ODS --> CMP1
COUNT_ODS --> CMP2{ODS vs DWD<br/>记录数+金额对比}
COUNT_DWD --> CMP2
COUNT_DWD --> CMP3{DWD vs DWS<br/>聚合一致性}
SUM_DWS --> CMP3
SAMPLE --> CMP4{抽样逐字段比对<br/>API vs ODS 100条}
ODS --> CMP4
SUSPECT --> CMP5{可疑值分析<br/>追溯上游原因}
CMP1 --> RPT[校验报告]
CMP2 --> RPT
CMP3 --> RPT
CMP4 --> RPT
CMP5 --> RPT
```
## 正确性属性
*属性是在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
基于前置分析,以下属性覆盖了可自动化测试的验收标准。本项目的许多需求(特别是需求 8-12 的黑盒校验和全量刷新)依赖真实数据库和 API 连接,属于集成测试范畴,不适合属性测试。属性测试聚焦于可用 FakeDB/FakeAPI 验证的核心逻辑。
### Property 1: ODS 任务提取记录数一致性
*对任意* ODS 任务和 FakeAPI 提供的任意非空记录列表,任务执行后 FakeDB 接收到的记录数应等于 API 提供的记录数减去被跳过的记录数(缺失主键 + 重复哈希)。
**Validates: Requirements 1.1, 1.2**
### Property 2: ODS 冲突处理策略正确性
*对任意* `ods_conflict_mode` 配置值nothing/backfill/updateBaseOdsTask 生成的 SQL 语句应包含对应的冲突处理子句nothing → `DO NOTHING`backfill → `COALESCE`update → `IS DISTINCT FROM`
**Validates: Requirements 1.3**
### Property 3: ODS 跳过缺失主键记录
*对任意*包含缺失主键字段的记录集合BaseOdsTask 的 skipped 计数应等于缺失主键的记录数,且这些记录不应出现在写入的行中。
**Validates: Requirements 1.4**
### Property 4: ODS content_hash 去重
*对任意*两条内容相同的 ODS 记录(仅 fetched_at 不同),计算出的 content_hash 应相同;当已有记录的 content_hash 与新记录相同时,新记录应被跳过。
**Validates: Requirements 1.5**
### Property 5: ODS 快照删除标记
*对任意*启用 `snapshot_missing_delete` 的 ODS 任务,当 API 返回的记录集是已有记录集的真子集时,差集中的记录应被标记为 `is_delete=1`
**Validates: Requirements 1.7**
### Property 6: DWD FACT_MAPPINGS 列映射完整性
*对任意* FACT_MAPPINGS 中的映射条目 `(dwd_col, ods_expr, cast_type)`,当 `ods_expr` 是简单列名时,该列应存在于对应的 ODS 源表中。
**Validates: Requirements 2.4**
### Property 7: DWD only_tables 过滤
*对任意*非空的 `dwd.only_tables` 配置列表DwdLoadTask 处理的表集合应是配置列表与 TABLE_MAP 键集合的交集。
**Validates: Requirements 2.6**
### Property 8: DWS 分段累加一致性
*对任意*时间窗口和分段配置BaseTask 的 `_accumulate_counts` 方法对各分段计数的累加结果应等于各分段计数的逐键求和。
**Validates: Requirements 3.3**
### Property 9: PipelineRunner Flow 层解析
*对任意*有效的 Flow 名称PIPELINE_LAYERS 的键PipelineRunner 解析出的层列表应与 PIPELINE_LAYERS 中定义的值完全一致。
**Validates: Requirements 5.1, 10.2**
### Property 10: PipelineRunner 无效 Flow 拒绝
*对任意*不在 PIPELINE_LAYERS 键集合中的字符串PipelineRunner.run() 应抛出 ValueError。
**Validates: Requirements 5.2**
### Property 11: TaskExecutor 工具类任务跳过游标
*对任意*被 TaskRegistry 标记为 `requires_db_config=False` 的任务代码TaskExecutor 应通过 `_run_utility_task` 路径执行,不调用 CursorManager 和 RunTracker。
**Validates: Requirements 5.5**
### Property 12: CLI data_source 解析
*对任意* `--data-source` 参数值online/offline/hybrid`--pipeline-flow` 参数值FULL/FETCH_ONLY/INGEST_ONLY`resolve_data_source` 应返回正确的映射值,且 `--data-source` 优先于 `--pipeline-flow`
**Validates: Requirements 6.3, 6.4**
### Property 13: AppConfig 优先级合并
*对任意*嵌套字典 DEFAULTS 和 CLI 覆盖,`_deep_merge` 后 CLI 中的键值应覆盖 DEFAULTS 中的同名键值,未被覆盖的键应保持原值。
**Validates: Requirements 7.1**
### Property 14: AppConfig store_id 验证
*对任意*非整数字符串作为 `app.store_id`AppConfig._normalize 应抛出 SystemExit。
**Validates: Requirements 7.2**
### Property 15: AppConfig DSN 组装
*对任意* host、port、name、user、password 组合db.dsn 为空时AppConfig._normalize 应组装出格式为 `postgresql://{user}:{password}@{host}:{port}/{name}` 的 DSN 字符串。
**Validates: Requirements 7.3**
### Property 16: AppConfig 点号路径 get
*对任意*嵌套字典和有效的点号路径,`config.get(path)` 应返回路径对应的值;对无效路径应返回 default 参数值。
**Validates: Requirements 7.4**
## 错误处理
### 数据库连接错误
- 调试脚本在启动时验证数据库连接,连接失败时记录错误详情并在报告中标注
- 单表处理失败时回滚该表事务继续处理后续表DWD 层已实现此逻辑)
- 全量刷新过程中连接断开时,利用 `DatabaseConnection.ensure_open()` 尝试重连
### API 连接错误
- APIClient 内置重试机制(`retry_max=3`,指数退避)
- API 返回非 0 code 时抛出 ValueError由上层捕获并记录
- Token 过期时在报告中标注,提示用户更新 `.env` 中的 `API_TOKEN`
### 任务执行错误
- ODS 任务:数据库异常时回滚并递增 errors 计数
- DWD 任务:单表失败时回滚该表,继续后续表,最终汇总错误
- DWS/INDEX 任务:继承 BaseTask 的异常处理,回滚后重新抛出
- 工具类任务:异常直接向上传播,不影响游标和运行记录
### 数据质量错误
- 黑盒校验发现的不一致记录在报告中详细列出主键
- 金额差异超过阈值时标记为 WARN 级别
- 校验后自动补齐通过 `run_backfill` 执行,补齐失败的记录单独记录
## 测试策略
### 双轨测试方法
本项目采用单元测试 + 属性测试的双轨方法:
- **属性测试**:使用 `hypothesis` 库验证上述 16 个正确性属性,每个属性至少运行 100 次迭代
- **单元测试**:使用 `pytest` 验证具体示例、边界情况和错误条件
- **集成测试**:使用真实数据库(`TEST_DB_DSN`)验证端到端数据流
### 属性测试配置
- 库:`hypothesis`(已在项目依赖中)
- 最小迭代次数100
- 每个属性测试必须引用设计文档中的属性编号
- 标签格式:`Feature: etl-pipeline-debug, Property {number}: {property_text}`
### 测试文件组织
```
apps/etl/pipelines/feiqiu/tests/unit/
├── test_debug_ods_properties.py # Property 1-5: ODS 层属性测试
├── test_debug_dwd_properties.py # Property 6-8: DWD/DWS 层属性测试
├── test_debug_orchestration_properties.py # Property 9-12: 编排层属性测试
├── test_debug_config_properties.py # Property 13-16: 配置层属性测试
└── (现有测试文件保持不变)
```
### 集成测试
集成测试通过调试脚本实现,连接真实数据库和 API
- `debug_ods.py`:逐个执行 ODS 任务,验证写入结果
- `debug_dwd.py`:执行 DWD 装载,验证映射正确性
- `debug_blackbox.py`:黑盒校验,逐层对比数据完整性
- `run_full_refresh.py`:全量刷新 + 校验
### 现有测试基础设施
项目已有完善的测试工具:
- `FakeDBOperations`:拦截并记录 SQL 操作,提供 commit/rollback 计数
- `FakeAPIClient`:返回预置内存数据,记录调用参数
- `OfflineAPIClient`:从归档 JSON 回放数据
- `RealDBOperationsAdapter`:连接真实 PostgreSQL 的适配器
- `create_test_config()`:构建测试用 AppConfig

View File

@@ -0,0 +1,188 @@
# 需求文档ETL 管道全流程调试与 Debug
## 简介
`apps/etl/pipelines/feiqiu/` 下的 ETL 管道进行全流程调试,覆盖 ODS → DWD → DWS → INDEX 四层数据处理,识别并修复代码缺陷,生成 Debug 报告。调试范围包括 CLI 入口、编排层、任务执行层、数据库操作层和数据质量校验层。
## 术语表
- **ETL_Pipeline**:从上游 SaaS API 抽取数据,经 ODS → DWD → DWS 三层处理的完整数据管道
- **ODS_Layer**原始数据层Operational Data Store负责从 API 抓取数据并落地到 `ods.*`
- **DWD_Layer**明细数据层Data Warehouse Detail负责从 ODS 清洗装载,维度走 SCD2事实按时间增量
- **DWS_Layer**汇总数据层Data Warehouse Summary负责从 DWD 聚合生成业务汇总(助教业绩、财务日报等)
- **INDEX_Layer**指数算法层负责计算自定义业务指数WBI/NCI/RS/ML
- **TaskExecutor**:任务执行器,封装单个 ETL 任务的完整执行生命周期
- **PipelineRunner**Flow 编排器,根据 Flow 定义执行多层 ETL 任务
- **TaskRegistry**:任务注册表,管理 52 个已注册任务的元数据和工厂方法
- **CursorManager**:游标管理器,负责记录和推进每个任务的时间水位
- **FakeDB**:测试用伪数据库操作对象,拦截并记录 SQL 操作
- **FakeAPI**:测试用伪 API 客户端,返回预置内存数据
- **Debug_Report**:调试报告,记录发现的问题、修复措施和验证结果
## 需求
### 需求 1ODS 层任务调试
**用户故事:** 作为开发者,我希望验证 ODS 层所有任务能正确从 API 抓取数据并写入 ODS 表,以确保原始数据完整落地。
#### 验收标准
1. WHEN 使用 FakeAPI 提供样例数据执行 ODS 任务, THE TaskExecutor SHALL 正确调用 API 分页接口并提取记录列表
2. WHEN ODS 任务接收到 API 返回的记录, THE BaseOdsTask SHALL 按照 DB 表结构动态构建 INSERT 语句并写入 ODS 表
3. WHEN ODS 记录的主键已存在于目标表, THE BaseOdsTask SHALL 根据 `ods_conflict_mode` 配置执行对应的冲突处理策略nothing/backfill/update
4. WHEN ODS 记录缺少必需的主键字段值, THE BaseOdsTask SHALL 跳过该记录并递增 skipped 计数
5. WHEN `content_hash` 列存在且新记录的哈希值与已有记录相同, THE BaseOdsTask SHALL 跳过该记录以避免无意义更新
6. IF ODS 任务执行过程中发生数据库异常, THEN THE BaseOdsTask SHALL 回滚当前事务并在 counts 中递增 errors 计数
7. WHEN `snapshot_missing_delete` 配置启用且表包含 `is_delete` 列, THE BaseOdsTask SHALL 将窗口内未出现在 API 返回中的记录标记为已删除
### 需求 2DWD 层装载调试
**用户故事:** 作为开发者,我希望验证 DWD 装载任务能正确从 ODS 清洗数据并写入 DWD 表,以确保维度和事实数据准确。
#### 验收标准
1. WHEN DWD_LOAD_FROM_ODS 任务执行, THE DwdLoadTask SHALL 遍历 TABLE_MAP 中所有 DWD→ODS 映射关系并逐表处理
2. WHEN 处理维度表dim_*, THE DwdLoadTask SHALL 使用 SCD2 合并策略写入,保留历史版本
3. WHEN 处理事实表dwd_*, THE DwdLoadTask SHALL 按时间窗口增量写入,使用 FACT_ORDER_CANDIDATES 中的时间列过滤
4. WHEN ODS 源列名与 DWD 目标列名不同, THE DwdLoadTask SHALL 使用 FACT_MAPPINGS 中定义的列映射和类型转换正确转换
5. WHEN 单张 DWD 表处理失败, THE DwdLoadTask SHALL 回滚该表事务并继续处理后续表,在结果中汇总错误信息
6. WHEN `dwd.only_tables` 配置或 `DWD_ONLY_TABLES` 环境变量指定了表名列表, THE DwdLoadTask SHALL 仅处理指定的表
### 需求 3DWS 层汇总调试
**用户故事:** 作为开发者,我希望验证 DWS 层汇总任务能正确从 DWD 聚合生成业务报表数据,以确保汇总结果准确。
#### 验收标准
1. WHEN DWS 汇总任务执行, THE DWS_Task SHALL 从 DWD 层读取明细数据并按业务规则聚合写入 DWS 表
2. WHEN TaskRegistry 按 "DWS" 层查询任务列表, THE TaskRegistry SHALL 返回所有 15 个已注册的 DWS 层任务代码
3. WHEN DWS 任务的时间窗口跨越多个分段, THE DWS_Task SHALL 按分段逐段处理并累加计数结果
### 需求 4INDEX 层指数算法调试
**用户故事:** 作为开发者,我希望验证 INDEX 层指数算法任务能正确计算业务指数,以确保 WBI/NCI/RS/ML 指数结果准确。
#### 验收标准
1. WHEN INDEX 层任务执行, THE INDEX_Task SHALL 从 DWD/DWS 层读取数据并按指数算法公式计算结果
2. WHEN TaskRegistry 按 "INDEX" 层查询任务列表, THE TaskRegistry SHALL 返回所有 4 个已注册的 INDEX 层任务代码DWS_WINBACK_INDEX, DWS_NEWCONV_INDEX, DWS_ML_MANUAL_IMPORT, DWS_RELATION_INDEX
### 需求 5编排层调试
**用户故事:** 作为开发者,我希望验证 PipelineRunner 和 TaskExecutor 的编排逻辑正确,以确保多层 ETL 任务按正确顺序执行。
#### 验收标准
1. WHEN PipelineRunner 接收到有效的 Flow 名称, THE PipelineRunner SHALL 按 PIPELINE_LAYERS 定义的层顺序解析并执行任务
2. IF PipelineRunner 接收到无效的 Flow 名称, THEN THE PipelineRunner SHALL 抛出 ValueError 并包含描述性错误信息
3. WHEN PipelineRunner 在 `verify_only` 模式下执行, THE PipelineRunner SHALL 跳过增量 ETL 并直接执行校验逻辑
4. WHEN PipelineRunner 在 `increment_verify` 模式下执行, THE PipelineRunner SHALL 先执行增量 ETL 再执行校验逻辑
5. WHEN TaskExecutor 执行工具类任务, THE TaskExecutor SHALL 跳过游标管理和运行记录,直接执行任务
6. WHEN TaskExecutor 执行 ODS 任务且 data_source 包含 fetch 阶段, THE TaskExecutor SHALL 使用 RecordingAPIClient 抓取并落盘后入库
7. WHEN 任务执行成功且返回有效的时间窗口, THE TaskExecutor SHALL 通过 CursorManager 推进游标水位
### 需求 6CLI 入口调试
**用户故事:** 作为开发者,我希望验证 CLI 入口能正确解析参数并启动对应的执行模式,以确保命令行操作可靠。
#### 验收标准
1. WHEN CLI 接收到 `--pipeline` 参数, THE CLI SHALL 进入 Flow 模式并使用 PipelineRunner 执行
2. WHEN CLI 接收到 `--tasks` 参数但无 `--pipeline`, THE CLI SHALL 进入传统模式并使用 TaskExecutor 直接执行任务列表
3. WHEN CLI 接收到 `--data-source` 参数, THE CLI SHALL 使用指定的数据源模式online/offline/hybrid
4. WHEN CLI 接收到已弃用的 `--pipeline-flow` 参数, THE CLI SHALL 映射为对应的 `data_source` 值并发出 DeprecationWarning
5. WHEN CLI 未指定时间窗口参数, THE CLI SHALL 使用 `--lookback-hours`(默认 24 小时)计算回溯窗口
### 需求 7配置体系调试
**用户故事:** 作为开发者,我希望验证配置加载和合并逻辑正确,以确保 DEFAULTS < ENV < CLI 的优先级链可靠。
#### 验收标准
1. WHEN AppConfig.load 被调用, THE AppConfig SHALL 按 DEFAULTS → ENV → CLI 的优先级深度合并配置
2. WHEN 配置中 `app.store_id` 缺失或非整数, THE AppConfig SHALL 抛出 SystemExit 并包含描述性错误信息
3. WHEN 配置中 `db.dsn` 为空, THE AppConfig SHALL 从 host/port/name/user/password 组装 DSN 字符串
4. WHEN 使用点号路径调用 `config.get()`, THE AppConfig SHALL 正确遍历嵌套字典并返回对应值
### 需求 8数据质量校验调试
**用户故事:** 作为开发者,我希望验证数据完整性校验逻辑能正确检测 ODS→DWD 之间的数据差异,以确保数据一致性。
#### 验收标准
1. WHEN 执行 DWD vs ODS 校验, THE IntegrityChecker SHALL 对 TABLE_MAP 中每对 DWD→ODS 表比较记录数和金额列汇总
2. WHEN 校验发现 DWD 与 ODS 记录数不一致, THE IntegrityChecker SHALL 在结果中报告 count diff 值
3. WHEN `compare_content` 配置启用, THE IntegrityChecker SHALL 通过哈希比较检测字段级内容差异并报告 mismatch 数量
### 需求 9真实数据调试
**用户故事:** 作为开发者,我希望使用真实的数据库和 API 连接进行调试,以确保发现的问题贴近生产环境。
#### 验收标准
1. THE ETL_Pipeline SHALL 使用 `apps/etl/pipelines/feiqiu/.env` 中配置的真实 API 地址和数据库 DSN 进行调试
2. WHEN 执行调试任务, THE ETL_Pipeline SHALL 连接真实 PostgreSQL 实例test_etl_feiqiu 数据库)验证数据读写
3. WHEN 执行调试任务, THE ETL_Pipeline SHALL 调用真实上游 SaaS API 验证数据抓取和分页逻辑
4. IF 真实 API 或数据库连接失败, THEN THE ETL_Pipeline SHALL 记录连接错误详情并在 Debug 报告中标注
### 需求 10全量数据更新与校验
**用户故事:** 作为开发者,我希望在调试完成后执行 2026-01-01 至 2026-02-16 的全量数据更新和校验,以确保历史数据完整一致。
#### 验收标准
1. WHEN 调试修复完成后, THE ETL_Pipeline SHALL 使用 `api_full` Flow 执行 2026-01-01 00:00 至 2026-02-16 00:00 的全量数据更新
2. WHEN 全量更新执行, THE PipelineRunner SHALL 按 ODS → DWD → DWS → INDEX 顺序依次处理所有层
3. WHEN 全量更新完成后, THE ETL_Pipeline SHALL 执行 `increment_verify` 模式对同一时间窗口进行数据一致性校验
4. WHEN 校验发现数据不一致, THE IntegrityChecker SHALL 自动执行补齐操作并记录补齐结果
5. THE Debug_Report SHALL 包含全量更新的执行统计(各层记录数、耗时)和校验结果摘要
### 需求 11Debug 报告生成
**用户故事:** 作为开发者,我希望获得一份结构化的 Debug 报告,记录所有发现的问题和修复措施,以便追溯和复查。
#### 验收标准
1. THE Debug_Report SHALL 包含以下章节:概述、发现的问题列表、修复措施、验证结果、全量更新统计、遗留问题
2. WHEN 发现代码缺陷, THE Debug_Report SHALL 记录缺陷位置(文件路径+行号)、缺陷描述、严重程度和修复方案
3. WHEN 修复已验证通过, THE Debug_Report SHALL 记录验证方式(单元测试/属性测试/手动验证)和验证结果
4. THE Debug_Report SHALL 输出为 Markdown 文件,存放在 `apps/etl/pipelines/feiqiu/docs/reports/` 目录下
### 需求 12黑盒数据完整性校验
**用户故事:** 作为检查者,我希望以黑盒视角从 API 源数据出发,逐层对比各 Schema 各表的数据是否完整,以独立验证 ETL 管道的正确性。
#### 验收标准
1. WHEN 执行黑盒校验, THE IntegrityChecker SHALL 从上游 API 重新拉取指定时间窗口的源数据作为基准
2. WHEN 获取到 API 源数据后, THE IntegrityChecker SHALL 将 API 记录数与 ODS 表记录数逐端点对比,报告缺失和多余记录
3. WHEN ODS 层校验完成后, THE IntegrityChecker SHALL 将 ODS 表记录数与 DWD 表记录数逐映射对比,报告数量差异
4. WHEN DWD 层校验完成后, THE IntegrityChecker SHALL 验证 DWS 汇总表的聚合结果与 DWD 明细数据的一致性
5. WHEN 校验发现金额类字段汇总不一致, THE IntegrityChecker SHALL 报告差异金额和涉及的具体记录主键
6. THE IntegrityChecker SHALL 生成逐层校验报告,包含每张表的记录数对比、金额汇总对比和内容哈希差异统计
7. WHEN 校验发现缺失记录, THE IntegrityChecker SHALL 输出缺失记录的主键列表,便于定位补齐
8. WHEN 执行黑盒校验, THE IntegrityChecker SHALL 检测可疑值(边缘值、空值、重复记录等),分析可能的流程问题并追溯原因
9. WHEN 全量更新完成后, THE IntegrityChecker SHALL 从新数据中抽样 100 条记录,逐字段与上游 API 源数据比对,验证字段级一致性
### 需求 13性能分析
**用户故事:** 作为开发者,我希望分析 ETL 整体流程的性能瓶颈,在保证稳定和数据处理结果正确的基础上提高数据处理性能。
#### 验收标准
1. THE Performance_Analyzer SHALL 统计各层ODS/DWD/DWS/INDEX和各任务的执行耗时
2. THE Performance_Analyzer SHALL 识别耗时最长的前 5 个任务作为性能瓶颈
3. THE Performance_Analyzer SHALL 分析数据库查询的执行计划,识别缺失索引和全表扫描
4. THE Performance_Analyzer SHALL 分析 API 调用的响应时间和分页效率
5. THE Performance_Analyzer SHALL 生成性能优化报告,包含瓶颈分析和具体优化建议,存放在 `apps/etl/pipelines/feiqiu/docs/reports/` 目录下
### 需求 14架构优化分析
**用户故事:** 作为开发者,我希望分析 ETL 整体结构,在保证稳定和数据处理结果正确的基础上精简架构。
#### 验收标准
1. THE Architecture_Analyzer SHALL 分析代码结构,识别重复代码和冗余模块
2. THE Architecture_Analyzer SHALL 评估各层之间的耦合度,识别可解耦的组件
3. THE Architecture_Analyzer SHALL 分析任务注册表中 52 个任务的分类合理性
4. THE Architecture_Analyzer SHALL 生成架构优化报告,包含结构分析、冗余识别和精简建议,存放在 `apps/etl/pipelines/feiqiu/docs/reports/` 目录下

View File

@@ -0,0 +1,168 @@
# 实现计划ETL 管道全流程调试与 Debug
## 概述
按五阶段策略实现 ETL 管道全流程调试:分层单元调试 → 全量数据刷新(内嵌计时,边执行边 Debug→ 黑盒数据校验 → 架构优化分析 → Debug 报告生成(含性能分析)。性能计时从全量刷新阶段开始就嵌入采集,报告阶段仅做分析和输出。调试脚本放置在 `apps/etl/pipelines/feiqiu/scripts/debug/` 目录下。
## 任务
- [x] 1. 阶段1分层单元调试 - ODS 层
- [x] 1.1 编写 `scripts/debug/debug_ods.py` 调试脚本
- 连接真实 API 和数据库(从 `.env` 加载配置)
- 逐个执行 23 个 ODS 任务(小窗口,如最近 2 小时)
- 验证每个任务的返回结果status、countsfetched/inserted/updated/skipped/errors
- 检查 ODS 表实际写入行数是否与 counts 一致
- 记录每个任务的执行结果到 DebugResult 列表
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 9.1, 9.2, 9.3_
- [x] 1.2 编写 ODS 层属性测试 `tests/unit/test_debug_ods_properties.py`
- **Property 1: ODS 任务提取记录数一致性**
- **Validates: Requirements 1.1, 1.2**
- **Property 2: ODS 冲突处理策略正确性**
- **Validates: Requirements 1.3**
- **Property 3: ODS 跳过缺失主键记录**
- **Validates: Requirements 1.4**
- **Property 4: ODS content_hash 去重**
- **Validates: Requirements 1.5**
- **Property 5: ODS 快照删除标记**
- **Validates: Requirements 1.7**
- [x] 2. 阶段1分层单元调试 - DWD 层
- [x] 2.1 编写 `scripts/debug/debug_dwd.py` 调试脚本
- 执行 DWD_LOAD_FROM_ODS 任务
- 验证 TABLE_MAP 中每对 DWD→ODS 映射的处理结果
- 检查维度表 SCD2 版本链完整性
- 检查事实表时间窗口增量写入正确性
- 验证 FACT_MAPPINGS 列映射和类型转换
- 记录每张表的处理结果inserted/updated/errors
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 9.1, 9.2_
- [x] 2.2 编写 DWD 层属性测试 `tests/unit/test_debug_dwd_properties.py`
- **Property 6: DWD FACT_MAPPINGS 列映射完整性**
- **Validates: Requirements 2.4**
- **Property 7: DWD only_tables 过滤**
- **Validates: Requirements 2.6**
- **Property 8: DWS 分段累加一致性**
- **Validates: Requirements 3.3**
- [x] 3. 阶段1分层单元调试 - DWS 和 INDEX 层
- [x] 3.1 编写 `scripts/debug/debug_dws.py` 调试脚本
- 逐个执行 15 个 DWS 汇总任务
- 验证每个任务的返回结果和 DWS 表写入情况
- 检查汇总数据与 DWD 明细数据的一致性(抽样验证)
- _Requirements: 3.1, 3.2, 3.3, 9.1, 9.2_
- [x] 3.2 编写 `scripts/debug/debug_index.py` 调试脚本
- 执行 4 个 INDEX 层任务WBI/NCI/RS/ML
- 验证指数计算结果的合理性(非空、范围检查)
- _Requirements: 4.1, 4.2, 9.1, 9.2_
- [x] 4. 阶段1分层单元调试 - 编排层和配置层
- [x] 4.1 编写 `scripts/debug/debug_orchestration.py` 调试脚本
- 验证 PipelineRunner 的 Flow 解析逻辑(所有 7 种 Flow
- 验证 TaskExecutor 的任务分发逻辑ODS/DWD/DWS/工具类)
- 验证 CursorManager 的游标推进逻辑
- 验证 CLI 参数解析(传统模式 vs Flow 模式)
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 4.2 编写编排层属性测试 `tests/unit/test_debug_orchestration_properties.py`
- **Property 9: PipelineRunner Flow 层解析**
- **Validates: Requirements 5.1, 10.2**
- **Property 10: PipelineRunner 无效 Flow 拒绝**
- **Validates: Requirements 5.2**
- **Property 11: TaskExecutor 工具类任务跳过游标**
- **Validates: Requirements 5.5**
- **Property 12: CLI data_source 解析**
- **Validates: Requirements 6.3, 6.4**
- [x] 4.3 编写配置层属性测试 `tests/unit/test_debug_config_properties.py`
- **Property 13: AppConfig 优先级合并**
- **Validates: Requirements 7.1**
- **Property 14: AppConfig store_id 验证**
- **Validates: Requirements 7.2**
- **Property 15: AppConfig DSN 组装**
- **Validates: Requirements 7.3**
- **Property 16: AppConfig 点号路径 get**
- **Validates: Requirements 7.4**
- [x] 5. 检查点 - 阶段1 完成
- 确保所有单元调试脚本可运行,所有属性测试通过,询问用户是否有问题。
- [x] 6. 阶段2全量数据刷新内嵌计时边执行边 Debug
- [x] 6.1 编写 `scripts/debug/run_full_refresh.py` 全量刷新脚本
- 使用 `api_full` Flow 执行 2026-01-01 00:00 至 2026-02-16 00:00 的全量更新
- 按层逐步执行:先 ODS再 DWD再 DWS最后 INDEX
- 内嵌性能计时:记录每个任务的开始/结束时间、每层的总耗时、API 调用响应时间,计时的项目颗粒度尽可能精细。
- 每层执行后检查结果,发现错误时记录并尝试修复
- 支持从失败的层/任务重试(断点续跑)
- 全量更新完成后执行 `increment_verify` 校验
- 校验发现不一致时自动补齐
- 将计时数据和执行统计(记录数、耗时)写入 JSON 中间文件供后续性能分析使用
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 13.1, 13.4, 9.1, 9.2, 9.3_
- [x] 6.2 执行全量刷新并修复发现的 Bug
- 运行 `run_full_refresh.py`
- 对执行过程中发现的每个 Bug定位原因、修复代码、重试验证
- 将所有发现的问题和修复记录到 DebugResult 列表
- _Requirements: 10.1, 10.2, 10.3, 10.4_
- [x] 7. 检查点 - 阶段2 完成
- 确保全量刷新成功完成,所有测试通过,询问用户是否有问题。
- [x] 8. 阶段3黑盒数据校验
- [x] 8.1 编写 `scripts/debug/debug_blackbox.py` 黑盒校验脚本
- API → ODS逐端点从 API 拉取数据,与 ODS 表记录数对比
- ODS → DWD按 TABLE_MAP 逐对比较记录数和金额列汇总
- DWD → DWS验证汇总表聚合结果与明细数据一致性
- 可疑值检测:扫描各表中的边缘值、空值、重复记录,分析可能的流程问题
- 抽样比对:从新数据中随机抽样 100 条记录,逐字段与上游 API 源数据比对
- 输出缺失记录主键列表和差异金额详情
- 生成逐层校验报告
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9_
- [x] 8.2 执行黑盒校验并记录结果
- 运行 `debug_blackbox.py`
- 分析校验结果,对发现的不一致追溯原因
- 将校验结果记录到 BlackboxCheckResult 列表
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9_
- [x] 9. 检查点 - 阶段3 完成
- 确保黑盒校验完成,所有测试通过,询问用户是否有问题。
- [x] 10. 阶段4架构优化分析
- [x] 10.1 编写 `scripts/debug/analyze_architecture.py` 架构分析脚本
- 分析代码结构:模块依赖关系、文件大小、函数复杂度
- 识别重复代码和冗余模块
- 评估各层之间的耦合度
- 分析 52 个任务的分类合理性
- 生成架构优化报告Markdown存放在 `docs/reports/`
- _Requirements: 14.1, 14.2, 14.3, 14.4_
- [x] 11. 阶段5Debug 报告生成(含性能分析)
- [x] 11.1 编写 `scripts/debug/analyze_performance.py` 性能分析脚本
- 读取全量刷新阶段采集的计时 JSON 中间文件
- 统计各层ODS/DWD/DWS/INDEX和各任务的执行耗时
- 识别耗时最长的前 5 个任务作为性能瓶颈
- 分析关键 SQL 查询的执行计划EXPLAIN ANALYZE
- 分析 API 调用的响应时间和分页效率
- 生成性能优化报告Markdown存放在 `docs/reports/`
- _Requirements: 13.1, 13.2, 13.3, 13.4, 13.5_
- [x] 11.2 编写 `scripts/debug/generate_report.py` 报告生成脚本
- 汇总所有阶段的调试结果
- 生成结构化 Markdown 报告,包含:概述、发现的问题列表、修复措施、验证结果、全量更新统计、黑盒校验结果、性能分析摘要、架构优化摘要、遗留问题
- 输出到 `apps/etl/pipelines/feiqiu/docs/reports/debug_report_YYYYMMDD.md`
- _Requirements: 11.1, 11.2, 11.3, 11.4_
- [x] 12. 最终检查点 - 全部完成
- 确保所有测试通过,所有报告已生成,询问用户是否有问题。
## 备注
- 标记 `*` 的任务为可选任务,可跳过以加快 MVP 进度
- 每个任务引用具体需求以确保可追溯性
- 检查点确保增量验证
- 属性测试验证通用正确性属性
- 单元测试验证具体示例和边界情况
- 调试脚本使用真实 API 和数据库连接(`.env` 配置)
- 全量刷新采用迭代式:发现 Bug 即修复并重试

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,354 @@
# 设计文档ETL 员工维度表staff_info
## 概述
为飞球 ETL 连接器新增员工维度表,从 `SearchSystemStaffInfo` API 抓取球房全体员工数据(店长、主管、教练、收银员、助教管理员等),经 ODS 落地后清洗装载至 DWD 层。员工表与现有助教表(`assistant_accounts_master`)是完全独立的实体。
## API 响应结构
```json
{
"data": {
"total": 15,
"staffProfiles": [
{
"id": 3020236636900101,
"cashierPointId": 2790685415443270,
"cashierPointName": "默认",
"job_num": "",
"staff_name": "葛芃",
"mobile": "13811638071",
"auth_code": "",
"avatar": "",
"create_time": "2025-12-24 00:03:37",
"entry_time": "2025-12-23 08:00:00",
"is_delete": 0,
"leave_status": 0,
"resign_time": "2225-12-24 00:03:37",
"site_id": 2790685415443269,
"staff_identity": 2,
"status": 1,
"system_role_id": 4,
"system_user_id": 3020236636293893,
"tenant_id": 2790683160709957,
"tenant_org_id": 2790685415443269,
"job": "店长",
"shop_name": "朗朗桌球",
"account_status": 1,
"is_reserve": 1,
"groupName": "",
"groupId": 0,
"alias_name": "葛芃",
"staff_profile_id": 0,
"site_label": "",
"rank_id": -1,
"ding_talk_synced": 1,
"new_rank_id": 0,
"new_staff_identity": 0,
"salary_grant_enabled": 2,
"rankName": "无职级",
"entry_type": 1,
"userRoles": [],
"entry_sign_status": 0,
"resign_sign_status": 0,
"criticism_status": 1,
"gender": 3
}
]
},
"code": 0
}
```
## 1. ODS 层设计
### 1.1 ODS 任务规格
```python
OdsTaskSpec(
code="ODS_STAFF_INFO",
class_name="OdsStaffInfoTask",
table_name="ods.staff_info_master",
endpoint="/PersonnelManagement/SearchSystemStaffInfo",
data_path=("data",),
list_key="staffProfiles",
pk_columns=(_int_col("id", "id", required=True),),
extra_params={
"workStatusEnum": 0,
"dingTalkSynced": 0,
"staffIdentity": 0,
"rankId": 0,
"criticismStatus": 0,
"signStatus": -1,
},
include_source_endpoint=False,
include_fetched_at=False,
include_record_index=True,
requires_window=False,
time_fields=None,
snapshot_mode=SnapshotMode.FULL_TABLE,
description="员工档案 ODSSearchSystemStaffInfo -> staffProfiles 原始 JSON",
)
```
### 1.2 ODS 表 DDL`ods.staff_info_master`
```sql
CREATE TABLE ods.staff_info_master (
id BIGINT NOT NULL,
tenant_id BIGINT,
site_id BIGINT,
tenant_org_id BIGINT,
system_user_id BIGINT,
staff_name TEXT,
alias_name TEXT,
mobile TEXT,
avatar TEXT,
gender INTEGER,
job TEXT,
job_num TEXT,
staff_identity INTEGER,
status INTEGER,
account_status INTEGER,
system_role_id INTEGER,
rank_id INTEGER,
rank_name TEXT,
new_rank_id INTEGER,
new_staff_identity INTEGER,
leave_status INTEGER,
entry_time TIMESTAMP WITHOUT TIME ZONE,
resign_time TIMESTAMP WITHOUT TIME ZONE,
create_time TIMESTAMP WITHOUT TIME ZONE,
is_delete INTEGER,
is_reserve INTEGER,
shop_name TEXT,
site_label TEXT,
cashier_point_id BIGINT,
cashier_point_name TEXT,
group_id BIGINT,
group_name TEXT,
staff_profile_id BIGINT,
auth_code TEXT,
auth_code_create TIMESTAMP WITHOUT TIME ZONE,
ding_talk_synced INTEGER,
salary_grant_enabled INTEGER,
entry_type INTEGER,
entry_sign_status INTEGER,
resign_sign_status INTEGER,
criticism_status INTEGER,
user_roles JSONB,
-- ETL 元数据
content_hash TEXT NOT NULL,
source_file TEXT,
fetched_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
payload JSONB NOT NULL
);
COMMENT ON TABLE ods.staff_info_master IS '员工档案主数据来源SearchSystemStaffInfo API';
```
### 1.3 ODS 列名映射说明
API 返回的驼峰字段在 ODS 层统一转为蛇形命名(由 BaseOdsTask 自动处理):
- `cashierPointId``cashier_point_id`
- `cashierPointName``cashier_point_name`
- `staffName` / `staff_name``staff_name`API 已是蛇形)
- `systemUserId` / `system_user_id``system_user_id`
- `tenantOrgId` / `tenant_org_id``tenant_org_id`
- `groupName``group_name`注意API 返回驼峰 `groupName`
- `groupId``group_id`API 返回驼峰 `groupId`
- `rankName``rank_name`API 返回驼峰 `rankName`
- `userRoles``user_roles`(数组,存为 JSONB
- `authCodeCreate` / `auth_code_create``auth_code_create`
## 2. DWD 层设计
### 2.1 主表 DDL`dwd.dim_staff`
核心业务字段,高频查询使用。
```sql
CREATE TABLE dwd.dim_staff (
staff_id BIGINT NOT NULL,
staff_name TEXT,
alias_name TEXT,
mobile TEXT,
gender INTEGER,
job TEXT,
tenant_id BIGINT,
site_id BIGINT,
system_role_id INTEGER,
staff_identity INTEGER,
status INTEGER,
leave_status INTEGER,
entry_time TIMESTAMP WITH TIME ZONE,
resign_time TIMESTAMP WITH TIME ZONE,
is_delete INTEGER,
-- SCD2
scd2_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scd2_end_time TIMESTAMP WITH TIME ZONE,
scd2_is_current INTEGER,
scd2_version INTEGER,
PRIMARY KEY (staff_id, scd2_start_time)
);
COMMENT ON TABLE dwd.dim_staff IS '员工档案维度主表SCD2';
```
### 2.2 扩展表 DDL`dwd.dim_staff_ex`
次要/低频变更字段。
```sql
CREATE TABLE dwd.dim_staff_ex (
staff_id BIGINT NOT NULL,
avatar TEXT,
job_num TEXT,
account_status INTEGER,
rank_id INTEGER,
rank_name TEXT,
new_rank_id INTEGER,
new_staff_identity INTEGER,
is_reserve INTEGER,
shop_name TEXT,
site_label TEXT,
tenant_org_id BIGINT,
system_user_id BIGINT,
cashier_point_id BIGINT,
cashier_point_name TEXT,
group_id BIGINT,
group_name TEXT,
staff_profile_id BIGINT,
auth_code TEXT,
auth_code_create TIMESTAMP WITH TIME ZONE,
ding_talk_synced INTEGER,
salary_grant_enabled INTEGER,
entry_type INTEGER,
entry_sign_status INTEGER,
resign_sign_status INTEGER,
criticism_status INTEGER,
create_time TIMESTAMP WITH TIME ZONE,
user_roles JSONB,
-- SCD2
scd2_start_time TIMESTAMP WITH TIME ZONE NOT NULL,
scd2_end_time TIMESTAMP WITH TIME ZONE,
scd2_is_current INTEGER,
scd2_version INTEGER,
PRIMARY KEY (staff_id, scd2_start_time)
);
COMMENT ON TABLE dwd.dim_staff_ex IS '员工档案维度扩展表SCD2';
```
### 2.3 TABLE_MAP 映射
```python
# 在 DwdLoadTask.TABLE_MAP 中新增:
"dwd.dim_staff": "ods.staff_info_master",
"dwd.dim_staff_ex": "ods.staff_info_master",
```
### 2.4 FACT_MAPPINGS 字段映射
```python
# dim_staff 主表映射
"dwd.dim_staff": [
("staff_id", "id", None),
("entry_time", "entry_time", "timestamptz"),
("resign_time", "resign_time", "timestamptz"),
],
# dim_staff_ex 扩展表映射
"dwd.dim_staff_ex": [
("staff_id", "id", None),
("rank_name", "rankname", None),
("cashier_point_id", "cashierpointid", "bigint"),
("cashier_point_name", "cashierpointname", None),
("group_id", "groupid", "bigint"),
("group_name", "groupname", None),
("system_user_id", "systemuserid", "bigint"),
("tenant_org_id", "tenantorgid", "bigint"),
("auth_code_create", "auth_code_create", "timestamptz"),
("create_time", "create_time", "timestamptz"),
("user_roles", "userroles", "jsonb"),
],
```
说明:
- ODS 层的列名由 BaseOdsTask 自动从 API 驼峰转为蛇形(如 `cashierPointId``cashierpointid`,注意 PG 列名全小写无下划线)
- DWD 主表中 `staff_name``alias_name``mobile` 等与 ODS 同名列自动映射,无需显式配置
- `staff_id` 映射自 ODS 的 `id`
## 3. 数据流概览
```
API: SearchSystemStaffInfo
↓ (POST, 分页, extra_params 筛选)
ODS: ods.staff_info_master
↓ (SCD2 合并, FULL_TABLE 快照)
DWD: dwd.dim_staff + dwd.dim_staff_ex
```
## 4. 测试框架
- 测试框架:`pytest` + `hypothesis`
- 单元测试使用 `FakeDB` / `FakeAPI``tests/unit/task_test_utils.py`
## 5. 正确性属性
### P1ODS 任务规格完整性
对于 `ODS_STAFF_INFO` 任务规格,以下属性必须成立:
- `code == "ODS_STAFF_INFO"`
- `table_name == "ods.staff_info_master"`
- `endpoint == "/PersonnelManagement/SearchSystemStaffInfo"`
- `list_key == "staffProfiles"`
- `snapshot_mode == SnapshotMode.FULL_TABLE`
- `requires_window == False`
- `time_fields is None`
- `"staffProfiles"` 存在于 `DEFAULT_LIST_KEYS`
- `"ODS_STAFF_INFO"` 存在于 `ENABLED_ODS_CODES`
验证方式:单元测试直接断言
### P2DWD 映射完整性
对于 DWD 装载配置,以下属性必须成立:
- `TABLE_MAP["dwd.dim_staff"] == "ods.staff_info_master"`
- `TABLE_MAP["dwd.dim_staff_ex"] == "ods.staff_info_master"`
- `FACT_MAPPINGS["dwd.dim_staff"]` 包含 `staff_id``id` 的映射
- `FACT_MAPPINGS["dwd.dim_staff_ex"]` 包含 `staff_id``id` 的映射
验证方式:单元测试直接断言
### P3ODS 列名提取一致性(属性测试)
对于任意 API 返回的员工记录(含驼峰和蛇形混合字段名),经 BaseOdsTask 处理后:
- 所有字段名转为小写蛇形
- `id` 字段不为空且为正整数
- `payload` 字段包含完整原始 JSON
验证方式hypothesis 属性测试,生成随机员工记录验证转换一致性
## 6. 文件变更清单
### 代码变更
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `apps/etl/connectors/feiqiu/api/client.py` | 修改 | `DEFAULT_LIST_KEYS` 添加 `"staffProfiles"` |
| `apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py` | 修改 | 新增 `ODS_STAFF_INFO` 任务规格 + 注册到 `ENABLED_ODS_CODES` |
| `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py` | 修改 | `TABLE_MAP``FACT_MAPPINGS` 新增 dim_staff/dim_staff_ex 映射 |
### DDL / 迁移
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `db/etl_feiqiu/migrations/2026-02-22__add_staff_info_tables.sql` | 新增 | ODS + DWD 建表迁移脚本 |
| `docs/database/ddl/etl_feiqiu__ods.sql` | 修改 | 追加 `ods.staff_info_master` DDL |
| `docs/database/ddl/etl_feiqiu__dwd.sql` | 修改 | 追加 `dwd.dim_staff` + `dwd.dim_staff_ex` DDL |
### 文档
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md` | 新增 | API→ODS 字段映射文档 |
| `apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md` | 新增 | ODS 表 BD 手册 |
| `apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md` | 新增 | DWD 主表 BD 手册 |
| `apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md` | 新增 | DWD 扩展表 BD 手册 |
| `apps/etl/connectors/feiqiu/docs/database/README.md` | 修改 | 增加员工表条目 |
| `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md` | 修改 | 增加 ODS_STAFF_INFO 任务说明 |
| `docs/database/README.md` | 修改 | 增加员工相关表条目 |

View File

@@ -0,0 +1,82 @@
# 需求文档ETL 员工维度表staff_info
## 概述
为飞球 ETL 连接器新增"员工"维度表,从上游 `SearchSystemStaffInfo` API 抓取球房员工数据,经 ODS 落地后清洗装载至 DWD 层,走 SCD2 缓慢变化维度。
## 用户故事
### US-1API 对接与 ODS 落地
作为 ETL 开发者,我需要将 `SearchSystemStaffInfo` API 融入现有 API 请求框架,并将原始数据落地到 ODS 层,以便后续清洗使用。
验收标准:
- 1.1 在 `ODS_TASK_SPECS` 中新增 `ODS_STAFF_INFO` 任务规格endpoint 为 `/PersonnelManagement/SearchSystemStaffInfo`
- 1.2 API 请求体包含必要的筛选参数(`workStatusEnum``staffIdentity` 等),使用现有 `APIClient.iter_paginated` 分页机制
- 1.3 ODS 表 `ods.staff_info_master` 包含 API 返回的所有业务字段 + ETL 元数据列(`content_hash``source_file``fetched_at``payload`
- 1.4 任务配置为 `snapshot_mode=FULL_TABLE`(全量快照,无时间窗口),`requires_window=False`
- 1.5 在 `DEFAULT_LIST_KEYS` 中添加 `staffProfiles`API 响应的列表键名)
- 1.6 在 `ENABLED_ODS_CODES` 中注册 `ODS_STAFF_INFO`
### US-2DWD 维度表设计与 SCD2 装载
作为数据分析师,我需要一张清洗后的员工维度表,以便在 DWS 汇总层关联员工信息。
验收标准:
- 2.1 创建 `dwd.dim_staff` 主表,包含核心业务字段(员工 ID、姓名、手机号、角色、门店、在职状态等+ SCD2 列
- 2.2 创建 `dwd.dim_staff_ex` 扩展表,包含次要/低频变更字段 + SCD2 列
- 2.3 在 `DwdLoadTask.TABLE_MAP` 中注册 `dwd.dim_staff``ods.staff_info_master``dwd.dim_staff_ex``ods.staff_info_master` 的映射
- 2.4 在 `DwdLoadTask.FACT_MAPPINGS` 中配置字段映射ODS 列名 → DWD 列名,含必要的类型转换)
- 2.5 DWD 装载走 SCD2 合并路径,变更检测正常工作
### US-3DDL 创建与归档
作为 DBA我需要 ODS 和 DWD 层的 DDL 被正确创建并归档到项目文档中。
验收标准:
- 3.1 编写 ODS 层 DDL`ods.staff_info_master`),在测试库执行
- 3.2 编写 DWD 层 DDL`dwd.dim_staff``dwd.dim_staff_ex`),在测试库执行
- 3.3 DDL 归档至 `docs/database/ddl/etl_feiqiu__ods.sql``docs/database/ddl/etl_feiqiu__dwd.sql`
- 3.4 编写迁移脚本至 `db/etl_feiqiu/migrations/`(日期前缀)
### US-4文档增补
作为团队成员,我需要所有相关文档同步更新,以便理解新增的数据流。
验收标准:
- 4.1 新增 ODS mapping 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md`
- 4.2 新增 ODS BD_manual 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md`
- 4.3 新增 DWD BD_manual 文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md`
- 4.4 更新 `apps/etl/connectors/feiqiu/docs/database/README.md`,增加员工表条目
- 4.5 更新 `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md`,增加 ODS_STAFF_INFO 任务说明
- 4.6 更新 `docs/database/README.md`,增加员工相关表的条目
- 4.7 新增 DWD BD_manual 扩展表文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md`(如有扩展表)
## API 信息
| 属性 | 值 |
|------|-----|
| URL | `https://pc.ficoo.vip/apiprod/admin/v1/PersonnelManagement/SearchSystemStaffInfo` |
| 方法 | POST |
| 端点路径 | `/PersonnelManagement/SearchSystemStaffInfo` |
| 认证 | Bearer Token标准飞球 API 认证) |
| 分页 | `page` + `limit`(与现有接口一致) |
### 请求体参数
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| workStatusEnum | int | 0 | 在职状态筛选0=全部) |
| dingTalkSynced | int | 0 | 钉钉同步状态0=全部) |
| staffIdentity | int | 0 | 员工身份筛选0=全部) |
| rankId | int | 0 | 职级筛选0=全部) |
| criticismStatus | int | 0 | 批评状态0=全部) |
| signStatus | int | -1 | 签约状态(-1=全部) |
| page | int | 1 | 页码 |
| limit | int | 50 | 每页条数 |
## 技术约束
- 员工表为维度表DWD 层走 SCD2
- ODS 使用 `SnapshotMode.FULL_TABLE`(全量快照软删除)
- 不需要时间窗口(`requires_window=False``time_fields=None`
- 主键为 `id`(员工 ID
- API 响应结构已确认:`data.staffProfiles` 为列表键,`data.total` 为总数
- 员工表与助教表(`assistant_accounts_master`)是完全独立的实体
- DWD 层拆分为主表(`dim_staff`+ 扩展表(`dim_staff_ex`

View File

@@ -0,0 +1,33 @@
# 任务列表ETL 员工维度表staff_info
## 任务
- [x] 1. DDL 创建与数据库执行
- [x] 1.1 编写迁移脚本 `db/etl_feiqiu/migrations/2026-02-22__add_staff_info_tables.sql`,包含 ODS + DWD 三张表的 CREATE TABLE 语句
- [x] 1.2 在测试库test_etl_feiqiu执行迁移脚本创建 `ods.staff_info_master``dwd.dim_staff``dwd.dim_staff_ex`
- [x] 1.3 将 DDL 归档追加至 `docs/database/ddl/etl_feiqiu__ods.sql``docs/database/ddl/etl_feiqiu__dwd.sql`
- [x] 2. ODS 任务注册
- [x] 2.1 在 `apps/etl/connectors/feiqiu/api/client.py``DEFAULT_LIST_KEYS` 中添加 `"staffProfiles"`
- [x] 2.2 在 `apps/etl/connectors/feiqiu/tasks/ods/ods_tasks.py``ODS_TASK_SPECS` 中新增 `ODS_STAFF_INFO` 任务规格
- [x] 2.3 在 `ENABLED_ODS_CODES` 集合中注册 `"ODS_STAFF_INFO"`
- [x] 3. DWD 映射注册
- [x] 3.1 在 `apps/etl/connectors/feiqiu/tasks/dwd/dwd_load_task.py``TABLE_MAP` 中新增 `dwd.dim_staff``dwd.dim_staff_ex` 的映射
- [x] 3.2 在 `FACT_MAPPINGS` 中新增 `dwd.dim_staff``dwd.dim_staff_ex` 的字段映射配置
- [x] 4. 单元测试
- [x] 4.1 编写 ODS 任务规格完整性测试(验证 P1 属性)
- [x] 4.2 编写 DWD 映射完整性测试(验证 P2 属性)
- [x] 5. 属性测试
- [x] 5.1 [PBT] 编写 ODS 列名提取一致性属性测试(验证 P3 属性):对于任意员工记录,字段名转换和 payload 保留正确
- [x] 6. 文档增补
- [x] 6.1 新增 ODS mapping 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/mappings/mapping_SearchSystemStaffInfo_staff_info_master.md`
- [x] 6.2 新增 ODS BD_manual 文档:`apps/etl/connectors/feiqiu/docs/database/ODS/main/BD_manual_staff_info_master.md`
- [x] 6.3 新增 DWD BD_manual 主表文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff.md`
- [x] 6.4 新增 DWD BD_manual 扩展表文档:`apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_dim_staff_ex.md`
- [x] 6.5 更新 `apps/etl/connectors/feiqiu/docs/database/README.md`,增加员工表条目
- [x] 6.6 更新 `apps/etl/connectors/feiqiu/docs/etl_tasks/ods_tasks.md`,增加 ODS_STAFF_INFO 任务说明
- [x] 6.7 更新 `docs/database/README.md`,增加员工相关表条目

View File

@@ -1,285 +0,0 @@
# 设计文档ETL 任务说明文档
## 概述
本设计描述如何为飞球 ETL 系统生成一套完整的任务说明文档,放置于 `docs/etl_tasks/` 目录下。文档以 Markdown 格式编写按数据层ODS / DWD / DWS / INDEX / Utility分文件组织并提供一个总览 README 作为入口。
文档的目标读者是开发者和运维人员,需要覆盖:
- 每个任务的代码标识、Python 类、数据来源与目标
- Extract / Transform / Load 各阶段的处理逻辑
- CLI 参数与管道执行方式
- BaseTask 公共机制
## 架构
文档为纯静态 Markdown 文件,不涉及运行时代码变更。整体结构如下:
```
docs/etl_tasks/
├── README.md # 总览:任务清单 + 跳转链接 + 执行方式
├── ods_tasks.md # ODS 层任务详解
├── dwd_tasks.md # DWD 层任务详解
├── dws_tasks.md # DWS 层任务详解
├── index_tasks.md # INDEX 层任务详解
├── utility_tasks.md # 工具类任务详解
└── base_task_mechanism.md # BaseTask 公共机制与执行参数
```
```mermaid
graph TD
README["README.md<br/>总览入口"] --> ODS["ods_tasks.md"]
README --> DWD["dwd_tasks.md"]
README --> DWS["dws_tasks.md"]
README --> IDX["index_tasks.md"]
README --> UTL["utility_tasks.md"]
README --> BASE["base_task_mechanism.md"]
```
## 组件与接口
本 spec 不涉及代码组件。产出物为 7 个 Markdown 文件,各文件的内容范围如下:
### README.md总览
| 章节 | 内容 |
|------|------|
| 系统简介 | 飞球 ETL 系统概述、数据流向API → ODS → DWD → DWS |
| 任务清单 | 按层分组的表格任务代码、Python 类、简要说明、跳转链接 |
| 管道类型 | 7 种管道api_ods / api_ods_dwd / api_full / ods_dwd / dwd_dws / dwd_dws_index / dwd_index的层组合 |
| 处理模式 | increment_only / verify_only / increment_verify 的区别 |
| 数据源模式 | online / offline / hybrid 的区别 |
| CLI 参数速查 | 所有 CLI 参数的表格(参数名、类型、默认值、说明) |
| 常见命令示例 | 典型使用场景的命令行示例 |
### ods_tasks.mdODS 层)
每个 ODS 任务一个小节,包含:
- 任务代码与 Python 类
- API 端点与请求参数
- 字段解析逻辑transform 阶段)
- 目标 ODS 表与写入策略
- 特殊说明如分页、去重、content_hash 等)
需区分两种 ODS 任务模式:
1. 独立任务类(如 OrdersTask、MembersTask继承 BaseTask有独立的 E/T/L 实现
2. 通用 ODS 任务(由 `ods_tasks.py` 中 OdsTaskSpec + BaseOdsTask 动态生成):通过声明式配置定义端点、列映射等
已注册的 ODS 任务14 个独立 + N 个通用):
| 任务代码 | Python 类 | API 端点 | 目标表 |
|----------|-----------|----------|--------|
| ORDERS | OrdersTask | /Site/GetAllOrderSettleList | billiards_ods.fact_order |
| PAYMENTS | PaymentsTask | 支付相关端点 | billiards_ods.fact_payment |
| MEMBERS | MembersTask | /MemberProfile/GetTenantMemberList | billiards_ods.dim_member |
| PRODUCTS | ProductsTask | 商品相关端点 | billiards_ods 商品表 |
| TABLES | TablesTask | 台桌相关端点 | billiards_ods 台桌表 |
| ASSISTANTS | AssistantsTask | 助教相关端点 | billiards_ods 助教表 |
| PACKAGES_DEF | PackagesDefTask | 套餐相关端点 | billiards_ods 套餐表 |
| REFUNDS | RefundsTask | 退款相关端点 | billiards_ods 退款表 |
| COUPON_USAGE | CouponUsageTask | 优惠券相关端点 | billiards_ods 优惠券表 |
| INVENTORY_CHANGE | InventoryChangeTask | 库存变动端点 | billiards_ods 库存表 |
| TOPUPS | TopupsTask | 充值相关端点 | billiards_ods 充值表 |
| TABLE_DISCOUNT | TableDiscountTask | 台费折扣端点 | billiards_ods 折扣表 |
| ASSISTANT_ABOLISH | AssistantAbolishTask | 助教取消端点 | billiards_ods 取消表 |
| LEDGER | LedgerTask | 台账端点 | billiards_ods 台账表 |
通用 ODS 任务由 `ODS_TASK_CLASSES` 字典动态注册,每个任务通过 `OdsTaskSpec` 声明:
- `endpoint`API 端点路径
- `table`:目标 ODS 表名
- `columns`列定义列表ColumnSpec
- `page_size``data_path``list_key`:分页参数
- `pk_columns`:主键列
- `snapshot_mode`快照模式content_hash 去重)
### dwd_tasks.mdDWD 层)
DWD 层有 5 个已注册任务:
| 任务代码 | Python 类 | 说明 |
|----------|-----------|------|
| DWD_LOAD_FROM_ODS | DwdLoadTask | 核心装载任务:遍历 TABLE_MAP维度走 SCD2事实走增量 |
| TICKET_DWD | TicketDwdTask | 结账小票明细 → fact_order / fact_order_goods / fact_table_usage / fact_assistant_service |
| PAYMENTS_DWD | PaymentsDwdTask | ODS 支付记录 → fact_payment |
| MEMBERS_DWD | MembersDwdTask | ODS 会员记录 → dim_member |
| DWD_QUALITY_CHECK | DwdQualityTask | ODS 与 DWD 行数/金额核对,输出 JSON 报表 |
核心任务 DWD_LOAD_FROM_ODS 的处理逻辑:
- TABLE_MAP 定义了 40+ 对 DWD→ODS 表映射
- 维度表dim_*):检测 SCD2 列是否存在,有则执行 SCD2 合并(关闭旧版+插入新版),无则执行 Type1 Upsert
- 事实表dwd_*、fact_*):按 fetched_at 水位线增量插入,支持 upsert 或 insert-only
- FACT_MAPPINGS 定义了列名映射ODS 驼峰命名 → DWD 下划线命名)
- 每张表独立事务,单表失败不影响后续表
SCD2 处理流程:
1. 从 ODS 取最新快照DISTINCT ON 按业务主键 + fetched_at DESC
2. 与 DWD 当前版本scd2_is_current=1逐列对比
3. 有变更关闭旧版scd2_end_time=now, scd2_is_current=0+ 插入新版version+1
4. 无变更:跳过
### dws_tasks.mdDWS 层)
DWS 层有 15 个已注册任务,按业务域分组:
**助教业绩域:**
| 任务代码 | Python 类 | 目标表 | 粒度 |
|----------|-----------|--------|------|
| DWS_ASSISTANT_DAILY | AssistantDailyTask | dws_assistant_daily_detail | 日期+助教 |
| DWS_ASSISTANT_MONTHLY | AssistantMonthlyTask | dws_assistant_monthly_summary | 月份+助教 |
| DWS_ASSISTANT_CUSTOMER | AssistantCustomerTask | dws_assistant_customer_stats | 日期+助教+会员 |
| DWS_ASSISTANT_SALARY | AssistantSalaryTask | dws_assistant_salary_calc | 月份+助教 |
| DWS_ASSISTANT_FINANCE | AssistantFinanceTask | dws_assistant_finance_analysis | 日期+助教 |
**会员分析域:**
| 任务代码 | Python 类 | 目标表 | 粒度 |
|----------|-----------|--------|------|
| DWS_MEMBER_CONSUMPTION | MemberConsumptionTask | dws_member_consumption_summary | 日期+会员 |
| DWS_MEMBER_VISIT | MemberVisitTask | dws_member_visit_detail | 日期+会员+结账单 |
**财务统计域:**
| 任务代码 | Python 类 | 目标表 | 粒度 |
|----------|-----------|--------|------|
| DWS_FINANCE_DAILY | FinanceDailyTask | dws_finance_daily_summary | 日期 |
| DWS_FINANCE_RECHARGE | FinanceRechargeTask | dws_finance_recharge_summary | 日期 |
| DWS_FINANCE_INCOME_STRUCTURE | FinanceIncomeStructureTask | dws_finance_income_structure | 日期+收入类型 |
| DWS_FINANCE_DISCOUNT_DETAIL | FinanceDiscountDetailTask | dws_finance_discount_detail | 日期+折扣类型 |
**运维任务:**
| 任务代码 | Python 类 | 说明 |
|----------|-----------|------|
| DWS_BUILD_ORDER_SUMMARY | DwsBuildOrderSummaryTask | 构建订单汇总中间表 |
| DWS_RETENTION_CLEANUP | DwsRetentionCleanupTask | 按时间分层清理历史数据 |
| DWS_MV_REFRESH_FINANCE_DAILY | DwsMvRefreshFinanceDailyTask | 刷新财务日报物化视图 |
| DWS_MV_REFRESH_ASSISTANT_DAILY | DwsMvRefreshAssistantDailyTask | 刷新助教日报物化视图 |
所有 DWS 任务继承 BaseDwsTask共享以下机制
- 时间分层范围计算TimeLayer: LAST_2_DAYS / LAST_1_MONTH / LAST_3_MONTHS / LAST_6_MONTHS / ALL
- 配置缓存ConfigCache业绩档位、等级价格、奖金规则、区域分类、技能类型
- delete-before-insert 更新策略(按日期范围先删后插,保证幂等)
- bulk_insert / upsert 写入方法
### index_tasks.mdINDEX 层)
INDEX 层有 4 个已注册任务:
| 任务代码 | Python 类 | 目标表 | 指数类型 |
|----------|-----------|--------|----------|
| DWS_WINBACK_INDEX | WinbackIndexTask | dws_member_winback_index | WBI回流指数 |
| DWS_NEWCONV_INDEX | NewconvIndexTask | dws_member_newconv_index | NCI新客转化指数 |
| DWS_RELATION_INDEX | RelationIndexTask | dws_relation_index | RS关系指数 |
| DWS_ML_MANUAL_IMPORT | MlManualImportTask | dws_ml_manual_ledger | ML手动台账导入 |
所有指数任务继承 BaseIndexTask共享
- 参数从 `billiards_dws.cfg_index_parameters` 表加载
- 百分位历史记录PercentileHistory
- 标准化的指数计算流程
### utility_tasks.md工具类
| 任务代码 | Python 类 | 用途 |
|----------|-----------|------|
| INIT_ODS_SCHEMA | InitOdsSchemaTask | 执行 ODS + etl_admin DDL创建必要目录 |
| INIT_DWD_SCHEMA | InitDwdSchemaTask | 执行 DWD DDL |
| INIT_DWS_SCHEMA | InitDwsSchemaTask | 执行 DWS DDL |
| MANUAL_INGEST | ManualIngestTask | 从本地 JSON 文件手动入库到 ODS |
| ODS_JSON_ARCHIVE | OdsJsonArchiveTask | 归档 ODS JSON 文件 |
| CHECK_CUTOFF | CheckCutoffTask | 检查数据截止时间 |
| SEED_DWS_CONFIG | SeedDwsConfigTask | 初始化 DWS 配置种子数据 |
| DATA_INTEGRITY_CHECK | DataIntegrityTask | 数据完整性校验 |
### base_task_mechanism.md公共机制
覆盖内容:
- BaseTask 模板方法流程execute → build_context → [分段] → extract → transform → load → commit
- TaskContext 字段说明
- 时间窗口计算逻辑(优先级:手动覆盖 > 游标 > 闲忙时段默认值)
- 窗口分段build_window_segments
- TaskRegistry 注册方式与元数据TaskMeta: task_class, requires_db_config, layer, task_type
- PipelineRunner 管道执行流程
- 校验框架Verifier概述
## 数据模型
本 spec 不涉及数据模型变更。文档中引用的数据模型均为现有系统中的表结构,包括:
**ODS 层表**billiards_ods schema
- settlement_records, table_fee_transactions, assistant_service_records, member_profiles, payment_transactions, refund_transactions 等 20+ 表
**DWD 层表**billiards_dwd schema
- 维度表dim_site, dim_table, dim_assistant, dim_member, dim_member_card_account, dim_tenant_goods, dim_store_goods, dim_goods_category, dim_groupbuy_package各含 _ex 扩展表)
- 事实表dwd_settlement_head, dwd_table_fee_log, dwd_table_fee_adjust, dwd_store_goods_sale, dwd_assistant_service_log, dwd_assistant_trash_event, dwd_member_balance_change, dwd_groupbuy_redemption, dwd_platform_coupon_redemption, dwd_recharge_order, dwd_payment, dwd_refund各含 _ex 扩展表)
**DWS 层表**billiards_dws schema
- 助教域dws_assistant_daily_detail, dws_assistant_monthly_summary, dws_assistant_customer_stats, dws_assistant_salary_calc, dws_assistant_finance_analysis
- 会员域dws_member_consumption_summary, dws_member_visit_detail
- 财务域dws_finance_daily_summary, dws_finance_recharge_summary, dws_finance_income_structure, dws_finance_discount_detail
- 指数域dws_member_winback_index, dws_member_newconv_index, dws_relation_index, dws_ml_manual_ledger
- 配置表cfg_index_parameters, cfg_skill_type, cfg_performance_tier, cfg_level_price, cfg_bonus_rule, cfg_area_category
## 正确性属性
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
由于本 spec 的产出物是文档Markdown 文件),而非运行时代码,正确性属性主要关注文档的完整性和一致性——即文档是否覆盖了所有已注册任务。
Property 1: ODS 任务文档覆盖完整性
*对于所有*在 `task_registry.py` 中注册且 layer="ODS" 的任务代码,`ods_tasks.md` 中应包含该任务代码的说明章节,并列出其目标表。
**Validates: Requirements 2.1, 2.4**
Property 2: DWD 任务文档覆盖完整性
*对于所有*在 `task_registry.py` 中注册且 layer="DWD" 的任务代码,`dwd_tasks.md` 中应包含该任务代码的说明章节,并列出其源表和目标表。
**Validates: Requirements 3.1**
Property 3: DWS 任务文档覆盖完整性
*对于所有*在 `task_registry.py` 中注册且 layer="DWS" 的任务代码,`dws_tasks.md` 中应包含该任务代码的说明章节,并标注其更新策略。
**Validates: Requirements 4.1, 4.4**
Property 4: INDEX 任务文档覆盖完整性
*对于所有*在 `task_registry.py` 中注册且 layer="INDEX" 的任务代码,`index_tasks.md` 中应包含该任务代码的说明章节。
**Validates: Requirements 5.1**
Property 5: Utility 任务文档覆盖完整性
*对于所有*在 `task_registry.py` 中注册且 task_type="utility" 的任务代码,`utility_tasks.md` 中应包含该任务代码的说明章节。
**Validates: Requirements 6.1**
Property 6: CLI 参数文档覆盖完整性
*对于所有*在 `cli/main.py``parse_args()` 中定义的 CLI 参数,`README.md``base_task_mechanism.md` 中应包含该参数的说明。
**Validates: Requirements 7.1**
Property 7: 管道类型文档覆盖完整性
*对于所有*在 `PipelineRunner.PIPELINE_LAYERS` 中定义的管道类型,`README.md` 中应包含该管道类型的层组合说明。
**Validates: Requirements 7.2**
## 错误处理
本 spec 为文档生成任务,不涉及运行时错误处理。文档编写过程中需注意:
1. 若源代码中的任务类或注册信息发生变更,文档可能过时——应在 README.md 中注明"最后更新日期"和"基于代码版本"
2. 若某个任务的 API 端点或参数无法从代码中直接读取(如动态配置),应在文档中标注"参见配置文件"
## 测试策略
**单元测试(示例验证):**
- 验证所有 7 个 Markdown 文件存在于 `docs/etl_tasks/` 目录下
- 验证 README.md 包含指向其他 6 个文件的链接
- 验证每个分层文件中包含对应层的所有已注册任务代码
**属性测试(覆盖完整性验证):**
- 使用 pytest 编写脚本,从 `task_registry.py` 动态读取已注册任务列表
- 解析对应的 Markdown 文件,检查每个任务代码是否出现在文档中
-`cli/main.py` 解析 CLI 参数列表,检查文档中是否覆盖
- 属性测试库pytest本项目已使用配合 parametrize 实现参数化验证
- 每个属性测试标注对应的设计属性编号
**测试标注格式:**
```python
# Feature: etl-task-documentation, Property 1: ODS 任务文档覆盖完整性
def test_ods_task_coverage():
...
```
由于本 spec 的产出物是静态文档,属性测试的核心价值在于确保文档与代码的一致性,防止文档遗漏任务。测试应在文档生成后运行一次即可,无需持续集成。

View File

@@ -1,119 +0,0 @@
# 需求文档ETL 任务说明文档
## 简介
为飞球 ETL 系统etl-billiards生成一份完整的任务说明文档覆盖 ODS、DWD、DWS、INDEX 四层所有已注册任务的逻辑、执行方式、参数含义及处理流程。文档面向开发者和运维人员,放置于 `docs/etl_tasks/` 目录下。
## 术语表
- **ETL_System**:飞球 ETL 系统,负责从上游 API 抽取数据并经 ODS → DWD → DWS 三层处理
- **Task_Document**:本次生成的 ETL 任务说明文档
- **ODS**操作数据存储层Operational Data Store保留 API 原始 payload
- **DWD**明细数据层Data Warehouse Detail清洗后的维度表和事实表
- **DWS**数据服务层Data Warehouse Service汇总统计表
- **INDEX**:指数算法层,基于 DWS 数据计算自定义业务指数
- **BaseTask**:所有 ETL 任务的基类,提供 Extract → Transform → Load 模板方法
- **TaskRegistry**:任务注册表,维护任务代码与任务类的映射关系
- **TaskContext**:运行期上下文,包含 store_id、时间窗口等信息
- **Pipeline**:管道,定义多层任务的执行顺序(如 api_ods、api_full、dwd_dws 等)
- **Loader**加载器负责将转换后的数据写入目标表upsert/insert
## 需求
### 需求 1文档结构与组织
**用户故事:** 作为开发者,我希望文档按数据层分章节组织,以便快速定位特定层的任务说明。
#### 验收标准
1. THE Task_Document SHALL 包含一个总览文件(`README.md`),列出所有层及其任务清单,并提供跳转链接
2. THE Task_Document SHALL 按 ODS、DWD、DWS、INDEX、Utility 五个分类分别生成独立的 Markdown 文件
3. THE Task_Document SHALL 放置于 `docs/etl_tasks/` 目录下
4. WHEN 新增或删除任务时THE Task_Document SHALL 通过总览文件的任务清单反映当前已注册任务的完整列表
### 需求 2ODS 层任务说明
**用户故事:** 作为开发者,我希望了解每个 ODS 任务的 API 端点、参数、解析逻辑和目标表,以便排查数据抓取问题。
#### 验收标准
1. THE Task_Document SHALL 为每个 ODS 任务列出任务代码、对应的 Python 类、源 API 端点
2. THE Task_Document SHALL 说明每个 ODS 任务的 extract 阶段调用的 API 参数及其含义
3. THE Task_Document SHALL 说明每个 ODS 任务的 transform 阶段的字段解析和类型转换逻辑
4. THE Task_Document SHALL 说明每个 ODS 任务的 load 阶段的目标表名和写入策略upsert/insert
5. THE Task_Document SHALL 区分"独立 ODS 任务"(如 OrdersTask和"通用 ODS 任务"(由 ODS_TASK_CLASSES 动态生成)两种模式
6. THE Task_Document SHALL 说明通用 ODS 任务的 OdsTaskSpec 配置结构(端点、表名、列映射、分页参数等)
### 需求 3DWD 层任务说明
**用户故事:** 作为开发者,我希望了解 DWD 层任务如何从 ODS 读取数据并清洗装载到维度表和事实表,以便理解数据血缘。
#### 验收标准
1. THE Task_Document SHALL 为每个 DWD 任务列出任务代码、Python 类、源 ODS 表和目标 DWD 表
2. THE Task_Document SHALL 说明 DWD_LOAD_FROM_ODS 任务的 TABLE_MAP 映射关系及维度/事实分流逻辑
3. THE Task_Document SHALL 说明维度表的 SCD2 处理方式(生效区间、变更检测、历史版本管理)
4. THE Task_Document SHALL 说明事实表的增量装载方式(水位线、去重、冲突处理)
5. THE Task_Document SHALL 说明 DWD_QUALITY_CHECK 任务的行数/金额核对逻辑和报表输出格式
6. THE Task_Document SHALL 说明 TICKET_DWD、PAYMENTS_DWD、MEMBERS_DWD 三个独立 DWD 任务各自的处理特点
### 需求 4DWS 层任务说明
**用户故事:** 作为开发者,我希望了解 DWS 层每个汇总任务的业务含义、数据来源和计算规则,以便验证业务报表的正确性。
#### 验收标准
1. THE Task_Document SHALL 为每个 DWS 任务列出任务代码、Python 类、目标表、主键和统计粒度
2. THE Task_Document SHALL 说明每个 DWS 任务的数据来源表DWD 层的哪些表)
3. THE Task_Document SHALL 说明每个 DWS 任务的核心业务计算规则(如工资计算公式、业绩档位、排名逻辑等)
4. THE Task_Document SHALL 说明每个 DWS 任务的更新策略delete-before-insert 或 upsert
5. THE Task_Document SHALL 说明物化视图刷新任务MV_REFRESH的分层刷新机制和配置方式
6. THE Task_Document SHALL 说明数据保留清理任务RETENTION_CLEANUP的时间分层策略和配置参数
### 需求 5INDEX 层任务说明
**用户故事:** 作为开发者,我希望了解指数算法任务的计算逻辑和参数含义,以便调优指数模型。
#### 验收标准
1. THE Task_Document SHALL 为每个 INDEX 任务列出任务代码、Python 类、目标表和指数类型
2. THE Task_Document SHALL 说明每个指数的计算公式或算法概要WBI/NCI/RS/ML
3. THE Task_Document SHALL 说明指数参数的配置来源cfg_index_parameters 表)和参数含义
4. THE Task_Document SHALL 说明 ML_MANUAL_IMPORT 任务的 Excel 导入逻辑和模板格式
### 需求 6工具类任务说明
**用户故事:** 作为运维人员,我希望了解 Schema 初始化、手动入库等工具类任务的用途和使用方式。
#### 验收标准
1. THE Task_Document SHALL 为每个工具类任务列出任务代码、Python 类和用途说明
2. THE Task_Document SHALL 说明 INIT_ODS_SCHEMA、INIT_DWD_SCHEMA、INIT_DWS_SCHEMA 三个初始化任务执行的 DDL 文件和创建的目录
3. THE Task_Document SHALL 说明 MANUAL_INGEST 任务的文件匹配规则、JSON 解析逻辑和入库流程
4. THE Task_Document SHALL 说明 ODS_JSON_ARCHIVE 任务的归档策略
5. THE Task_Document SHALL 说明 CHECK_CUTOFF 和 DATA_INTEGRITY_CHECK 任务的校验逻辑
### 需求 7执行方式与参数说明
**用户故事:** 作为运维人员,我希望了解如何通过 CLI 和管道模式执行任务,以及各参数的含义。
#### 验收标准
1. THE Task_Document SHALL 说明 CLI 入口(`python -m cli.main`)的所有参数及其含义
2. THE Task_Document SHALL 说明管道类型api_ods、api_ods_dwd、api_full、ods_dwd、dwd_dws、dwd_dws_index、dwd_index各自包含的层和执行顺序
3. THE Task_Document SHALL 说明处理模式increment_only、verify_only、increment_verify的区别和适用场景
4. THE Task_Document SHALL 说明时间窗口参数window-start、window-end、window-split、lookback-hours、overlap-seconds的计算逻辑
5. THE Task_Document SHALL 说明数据源模式online、offline、hybrid的区别
6. THE Task_Document SHALL 提供常见使用场景的命令示例
### 需求 8BaseTask 与公共机制说明
**用户故事:** 作为开发者,我希望了解任务基类的模板方法和公共机制,以便开发新任务时遵循统一模式。
#### 验收标准
1. THE Task_Document SHALL 说明 BaseTask 的 Execute → Extract → Transform → Load 模板方法流程
2. THE Task_Document SHALL 说明 TaskContext 的字段含义store_id、window_start、window_end、window_minutes、cursor
3. THE Task_Document SHALL 说明时间窗口的计算逻辑(游标优先、闲忙时段、手动覆盖)
4. THE Task_Document SHALL 说明窗口分段build_window_segments的切分策略
5. THE Task_Document SHALL 说明任务注册表TaskRegistry的注册方式和元数据结构layer、task_type、requires_db_config

View File

@@ -1,125 +0,0 @@
# 实施计划ETL 任务说明文档
## 概述
基于对 `tasks/``loaders/``orchestration/``cli/` 目录下源代码的分析,生成 7 个 Markdown 文档文件,放置于 `docs/etl_tasks/` 目录下。每个任务按照设计文档中定义的结构和内容范围编写。
## 任务
- [x] 1. 创建 `docs/etl_tasks/base_task_mechanism.md`
- 说明 BaseTask 的 execute → extract → transform → load 模板方法流程
- 说明 TaskContext 字段含义store_id、window_start、window_end、window_minutes、cursor
- 说明时间窗口计算逻辑(手动覆盖 > 游标 > 闲忙时段默认值)
- 说明窗口分段build_window_segments切分策略
- 说明 TaskRegistry 注册方式与 TaskMeta 元数据结构layer、task_type、requires_db_config
- 说明 PipelineRunner 管道执行流程与校验框架概述
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_
- [x] 2. 创建 `docs/etl_tasks/ods_tasks.md`
- [x] 2.1 编写独立 ODS 任务说明14 个任务ORDERS、PAYMENTS、MEMBERS、PRODUCTS、TABLES、ASSISTANTS、PACKAGES_DEF、REFUNDS、COUPON_USAGE、INVENTORY_CHANGE、TOPUPS、TABLE_DISCOUNT、ASSISTANT_ABOLISH、LEDGER
- 每个任务列出任务代码、Python 类、API 端点、请求参数、字段解析逻辑、目标表、写入策略
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 2.2 编写通用 ODS 任务说明BaseOdsTask + OdsTaskSpec 模式)
- 说明 OdsTaskSpec 配置结构endpoint、table、columns、pk_columns、snapshot_mode 等)
- 说明 BaseOdsTask 的通用 execute 流程API 调用、schema-aware 插入、content_hash 去重、软删除标记)
- 列出由 ODS_TASK_CLASSES 动态注册的所有任务
- _Requirements: 2.5, 2.6_
- [x] 3. 创建 `docs/etl_tasks/dwd_tasks.md`
- [x] 3.1 编写 DWD_LOAD_FROM_ODS 核心任务说明
- 列出完整的 TABLE_MAP 映射表DWD 表 → ODS 表)
- 说明维度/事实分流逻辑dim_* 走 SCD2 或 Type1 Upsert其余走增量插入
- 说明 SCD2 处理流程(最新快照选取、变更检测、版本关闭与新建)
- 说明事实表增量装载fetched_at 水位线、upsert/insert-only、FACT_MAPPINGS 列映射)
- _Requirements: 3.2, 3.3, 3.4_
- [x] 3.2 编写独立 DWD 任务说明TICKET_DWD、PAYMENTS_DWD、MEMBERS_DWD
- 每个任务列出:源 ODS 表、目标 DWD 表、处理特点
- _Requirements: 3.6_
- [x] 3.3 编写 DWD_QUALITY_CHECK 任务说明
- 说明行数/金额核对逻辑、金额列自动扫描规则、JSON 报表输出格式
- _Requirements: 3.5_
- [x] 4. 创建 `docs/etl_tasks/dws_tasks.md`
- [x] 4.1 编写 BaseDwsTask 公共机制说明
- 说明时间分层TimeLayer、配置缓存ConfigCache、delete-before-insert 策略、bulk_insert/upsert 方法
- _Requirements: 4.1_
- [x] 4.2 编写助教业绩域任务说明5 个任务)
- DWS_ASSISTANT_DAILY日度服务明细聚合数据来源、聚合维度、输出字段
- DWS_ASSISTANT_MONTHLY月度汇总业绩档位计算、排名逻辑、新人封顶规则
- DWS_ASSISTANT_CUSTOMER助教-客户关系统计
- DWS_ASSISTANT_SALARY工资计算公式基础工资+提成+奖金+扣款)
- DWS_ASSISTANT_FINANCE助教收支分析收入 vs 日均成本、毛利率)
- _Requirements: 4.2, 4.3, 4.4_
- [x] 4.3 编写会员分析域任务说明2 个任务)
- DWS_MEMBER_CONSUMPTION会员消费汇总、客户分层
- DWS_MEMBER_VISIT会员到店明细、服务时长、折扣计算
- _Requirements: 4.2, 4.3, 4.4_
- [x] 4.4 编写财务统计域任务说明4 个任务)
- DWS_FINANCE_DAILY财务日报结算汇总、团购、充值、赠卡消费、费用、平台
- DWS_FINANCE_RECHARGE充值统计首充/续充、现金/赠送、卡余额)
- DWS_FINANCE_INCOME_STRUCTURE收入结构分析按类型、按区域
- DWS_FINANCE_DISCOUNT_DETAIL折扣明细统计
- _Requirements: 4.2, 4.3, 4.4_
- [x] 4.5 编写运维任务说明4 个任务)
- DWS_BUILD_ORDER_SUMMARY订单汇总中间表构建
- DWS_RETENTION_CLEANUP时间分层清理策略、配置参数enabled、layer、tables、table_layers
- DWS_MV_REFRESH_FINANCE_DAILY / DWS_MV_REFRESH_ASSISTANT_DAILY物化视图分层刷新机制、L1-L4 层级、配置方式
- _Requirements: 4.5, 4.6_
- [x] 5. 创建 `docs/etl_tasks/index_tasks.md`
- 编写 BaseIndexTask 公共机制(参数加载、百分位历史)
- 编写 4 个指数任务说明WBI回流指数、NCI新客转化指数、RS关系指数、ML手动台账导入
- 说明 cfg_index_parameters 配置表结构和参数含义
- 说明 ML_MANUAL_IMPORT 的 Excel 模板格式和导入逻辑
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [x] 6. 创建 `docs/etl_tasks/utility_tasks.md`
- 编写 8 个工具类任务说明INIT_ODS_SCHEMA、INIT_DWD_SCHEMA、INIT_DWS_SCHEMA、MANUAL_INGEST、ODS_JSON_ARCHIVE、CHECK_CUTOFF、SEED_DWS_CONFIG、DATA_INTEGRITY_CHECK
- 每个任务列出:用途、执行的 DDL/操作、配置参数
- 重点说明 MANUAL_INGEST 的文件匹配规则和入库流程
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 7. 创建 `docs/etl_tasks/README.md`(总览)
- 编写系统简介和数据流向图API → ODS → DWD → DWS
- 编写按层分组的任务清单表格任务代码、Python 类、简要说明、跳转链接)
- 编写管道类型说明7 种管道的层组合)
- 编写处理模式说明increment_only / verify_only / increment_verify
- 编写数据源模式说明online / offline / hybrid
- 编写 CLI 参数速查表(所有参数的表格)
- 编写常见命令示例
- _Requirements: 1.1, 1.2, 1.3, 7.1, 7.2, 7.3, 7.5, 7.6_
- [x] 8. 检查点 - 验证文档完整性
- 确认 7 个文件全部存在于 `docs/etl_tasks/` 目录下
- 确认 README.md 中的任务清单覆盖所有已注册任务
- 确认各分层文件中的任务代码与 task_registry.py 一致
- Ensure all files are valid Markdown, ask the user if questions arise.
- [x] 9. 编写文档覆盖完整性验证脚本
- [x] 9.1 编写 ODS 任务覆盖验证
- **Property 1: ODS 任务文档覆盖完整性**
- **Validates: Requirements 2.1, 2.4**
- [x] 9.2 编写 DWD 任务覆盖验证
- **Property 2: DWD 任务文档覆盖完整性**
- **Validates: Requirements 3.1**
- [x] 9.3 编写 DWS 任务覆盖验证
- **Property 3: DWS 任务文档覆盖完整性**
- **Validates: Requirements 4.1, 4.4**
- [x] 9.4 编写 INDEX 和 Utility 任务覆盖验证
- **Property 4: INDEX 任务文档覆盖完整性**
- **Property 5: Utility 任务文档覆盖完整性**
- **Validates: Requirements 5.1, 6.1**
- [x] 9.5 编写 CLI 参数和管道类型覆盖验证
- **Property 6: CLI 参数文档覆盖完整性**
- **Property 7: 管道类型文档覆盖完整性**
- **Validates: Requirements 7.1, 7.2**
- [x] 10. 最终检查点
- Ensure all tests pass, ask the user if questions arise.
## 说明
- 任务标记 `*` 的为可选项,可跳过以加快 MVP 进度
- 每个任务引用了具体的需求编号以便追溯
- 检查点确保增量验证
- 文档编写顺序先写公共机制task 1再按层写各任务task 2-6最后写总览task 7

View File

@@ -1,553 +0,0 @@
# 设计文档Monorepo 迁移
## 概述
本设计将现有单一 ETL 仓库(`FQ-ETL`)迁移为 Monorepo 单体仓库(`NeoZQYY`),采用一次性搬迁策略。核心设计原则:
1. **最小破坏性**ETL 整体平移,保持内部结构不变,仅调整外部引用
2. **分层隔离**:通过 uv workspace 实现 Python 包依赖隔离,通过 `.env` 分层实现配置隔离
3. **数据库重组**:从现有 4 个 schemabilliards_ods/billiards_dwd/billiards_dws/etl_admin重组为 6 层 schemameta/ods/dwd/core/dws/app
4. **渐进式扩展**:第一阶段只建必要骨架,未来扩展点记录在 Roadmap 中
## 架构
### 整体架构
```mermaid
graph TB
subgraph "NeoZQYY Monorepo"
subgraph "apps/"
ETL["apps/etl/pipelines/feiqiu/"]
Backend["apps/backend/ (FastAPI)"]
Mini["apps/miniprogram/ (Donut+TDesign)"]
Admin["apps/admin-web/ (未来)"]
end
subgraph "packages/"
Shared["packages/shared/"]
end
subgraph "gui/"
GUI["gui/ (PySide6过渡期)"]
end
subgraph "db/"
ETLDB["db/etl_feiqiu/"]
AppDB["db/zqyy_app/"]
FDW["db/fdw/"]
end
end
ETL --> Shared
Backend --> Shared
GUI --> Shared
Backend --> AppDB
ETL --> ETLDB
AppDB -.->|postgres_fdw 只读| ETLDB
```
### 数据流架构
```mermaid
graph LR
API["上游 SaaS API"] --> ODS["ods (原始数据)"]
ODS --> DWD["dwd (main+EX 明细)"]
DWD --> Core["core (统一最小字段集)"]
DWD --> DWS["dws (汇总/工资)"]
Core --> DWS
DWS --> App["app (视图+RLS)"]
App -.->|FDW 只读映射| ZqyyApp["zqyy_app DB"]
ZqyyApp --> FastAPI["FastAPI 后端"]
FastAPI --> MiniApp["微信小程序"]
subgraph "etl_feiqiu DB"
Meta["meta (调度/游标)"]
ODS
DWD
Core
DWS
App
end
```
## 组件与接口
### 1. 目录结构生成器Scaffold
负责创建 Monorepo 完整目录结构和基础配置文件。
**输入**:目标路径 `C:\NeoZQYY\`
**输出**:完整目录树 + README.md + 配置文件
**关键行为**
- 创建所有一级和二级目录
- 为每个一级目录生成 README.md作用 + 结构 + Roadmap
- 生成 `.gitignore``.kiroignore``.env.template`
- 初始化 Git 仓库
### 2. uv Workspace 配置
**根 `pyproject.toml`**
```toml
[project]
name = "neozqyy"
version = "0.1.0"
requires-python = ">=3.10"
[tool.uv.workspace]
members = [
"apps/etl/pipelines/feiqiu",
"apps/backend",
"packages/shared",
"gui",
]
```
**子项目 `pyproject.toml` 模式**(以 ETL 为例):
```toml
[project]
name = "etl-feiqiu"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"psycopg2-binary>=2.9.0",
"requests>=2.28.0",
"python-dateutil>=2.8.0",
"tzdata>=2023.0",
"python-dotenv",
"openpyxl>=3.1.0",
"neozqyy-shared",
]
[tool.uv.sources]
neozqyy-shared = { workspace = true }
```
### 3. 配置隔离机制
**分层加载顺序**
```
根 .env公共配置→ 应用 .env.local私有覆盖→ 环境变量 → CLI 参数
```
**实现方式**
- 现有 `AppConfig``DEFAULTS < ENV < CLI` 模式保持不变
- 新增:在 `load_env_overrides()` 中先加载根 `.env`,再加载应用级 `.env.local`
- 冲突策略:应用级优先(后加载覆盖先加载)
- 缺失检测:在 `_validate()` 中检查必需项,报告缺失项名称
### 4. ETL 平移策略
**平移范围**
| 源路径 | 目标路径 | 说明 |
|--------|----------|------|
| `api/` | `apps/etl/pipelines/feiqiu/api/` | API 客户端 |
| `cli/` | `apps/etl/pipelines/feiqiu/cli/` | CLI 入口 |
| `config/` | `apps/etl/pipelines/feiqiu/config/` | 配置 |
| `loaders/` | `apps/etl/pipelines/feiqiu/loaders/` | 加载器 |
| `models/` | `apps/etl/pipelines/feiqiu/models/` | 模型 |
| `orchestration/` | `apps/etl/pipelines/feiqiu/orchestration/` | 调度 |
| `scd/` | `apps/etl/pipelines/feiqiu/scd/` | SCD2 |
| `tasks/` | `apps/etl/pipelines/feiqiu/tasks/` | 任务 |
| `utils/` | `apps/etl/pipelines/feiqiu/utils/` | 工具 |
| `quality/` | `apps/etl/pipelines/feiqiu/quality/` | 质量检查 |
| `tests/` | `apps/etl/pipelines/feiqiu/tests/` | 测试 |
| `database/*.sql` | `db/etl_feiqiu/schemas/` | DDL |
| `database/migrations/` | `db/etl_feiqiu/migrations/` | 迁移脚本 |
| `database/seed_*.sql` | `db/etl_feiqiu/seeds/` | 种子数据 |
| `gui/` | `gui/` | GUI顶层 |
**import 路径策略**
- ETL 内部使用相对 import`from .config.settings import AppConfig`)或保持现有绝对 import
- `pyproject.toml` 中设置 `pythonpath`,使 `apps/etl/pipelines/feiqiu/` 为 Python 路径根
- `pytest.ini` 同步更新 `pythonpath = .`
- 目标ETL 内部代码零修改或最小修改
### 5. 小程序平移策略
**平移范围**
| 源路径 | 目标路径 |
|--------|----------|
| `C:\ZQYY\XCX\`(除 Prototype | `apps/miniprogram/` |
| `C:\ZQYY\XCX\Prototype\` | `docs/h5_ui/` |
小程序为独立前端项目Donut + TDesign不涉及 Python 依赖管理,直接复制即可。
### 6. 数据库 Schema 重组etl_feiqiu
**现有 → 新 schema 映射**
| 现有 Schema | 新 Schema | 说明 |
|-------------|-----------|------|
| `etl_admin` | `meta` | 调度、游标、运行记录 |
| `billiards_ods` | `ods` | ODS 原始数据,结构不变 |
| `billiards_dwd` | `dwd` | DWD 明细,保留 main+EX 拆分 |
| (新增) | `core` | 统一维度/事实最小字段集 |
| `billiards_dws` | `dws` | DWS 汇总,结构不变 |
| (新增) | `app` | 面向外部的视图/函数 + RLS |
**core schema 设计原则**
- 仅包含跨系统共享的最小字段集(如会员 ID、姓名、手机号、状态
- 维度表从 DWD 维度表提取核心字段
- 事实表从 DWD 事实表提取核心度量
- 第一版保持精简,后续按需扩展
**app schema 设计原则**
- 以视图VIEW封装 DWS/Core 层数据
- 所有视图启用 RLS`site_id` 过滤
- 提供函数接口供 FDW 映射使用
- 不存储实际数据,仅做访问层
**RLS 实现方案**
```sql
-- 创建应用角色
CREATE ROLE app_reader;
-- 在 app schema 的视图上启用 RLS
ALTER TABLE app.v_member_summary ENABLE ROW LEVEL SECURITY;
-- 创建策略:根据会话变量 app.current_site_id 过滤
CREATE POLICY site_isolation ON app.v_member_summary
FOR SELECT TO app_reader
USING (site_id = current_setting('app.current_site_id')::bigint);
```
### 7. 业务数据库设计zqyy_app
**核心表**
- `users`:用户账户(微信 OpenID、手机号、角色
- `roles` / `permissions`RBAC 权限模型
- `user_roles`:用户-角色关联
- `tasks`:任务管理(审批流)
- `approvals`:审批记录
**FDW 映射**
```sql
-- 在 zqyy_app 中创建外部服务器
CREATE SERVER etl_feiqiu_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', dbname 'etl_feiqiu', port '5432');
-- 创建用户映射
CREATE USER MAPPING FOR app_user
SERVER etl_feiqiu_server
OPTIONS (user 'app_reader', password '***');
-- 导入 app schema 的外部表
IMPORT FOREIGN SCHEMA app
FROM SERVER etl_feiqiu_server
INTO fdw_etl;
```
**约束**FDW 映射为只读,`zqyy_app` 不存储 ETL 数据副本。
### 8. FastAPI 后端骨架
**项目结构**
```
apps/backend/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 入口
│ ├── config.py # 配置加载
│ ├── database.py # 数据库连接
│ ├── routers/ # 路由模块
│ │ └── __init__.py
│ ├── middleware/ # 中间件
│ │ └── __init__.py
│ └── schemas/ # Pydantic 模型
│ └── __init__.py
├── tests/
│ └── __init__.py
├── pyproject.toml
└── README.md
```
**关键配置**
- 连接 `zqyy_app` 数据库(通过 FDW 访问 ETL 数据)
- OpenAPI 文档自动生成FastAPI 内置)
- 依赖 `packages/shared` 获取通用工具
### 9. 共享包packages/shared
**模块划分**
```
packages/shared/
├── src/
│ └── neozqyy_shared/
│ ├── __init__.py
│ ├── enums.py # 字段枚举定义
│ ├── money.py # 金额精度工具CNY, numeric(2)
│ └── datetime_utils.py # 时间处理工具
├── tests/
│ └── __init__.py
├── pyproject.toml
└── README.md
```
**提取来源**
- `enums.py`:从 ETL 的 `models/` 中提取通用枚举
- `money.py`:金额四舍五入、格式化(`Decimal` + `ROUND_HALF_UP`scale=2
- `datetime_utils.py`:时区转换、日期范围计算(从 `utils/` 提取)
### 10. .kiro 迁移
**迁移内容**
- 复制 `.kiro/steering/` 到 Monorepo
- 更新 `product.md`:从单一 ETL 视角扩展为 Monorepo 全局视角
- 更新 `tech.md`:新增 FastAPI、uv workspace、Donut+TDesign 等技术栈
- 更新 `structure-lite.md`:反映 Monorepo 目录结构和模块边界
- 更新路径引用:所有 steering 文件中的路径适配新结构
## 数据模型
### etl_feiqiu 数据库(六层 Schema
```mermaid
erDiagram
META {
bigint run_id PK
text task_code
timestamptz started_at
timestamptz ended_at
text status
jsonb result_summary
}
META ||--o{ ODS : "调度触发"
ODS {
bigint id PK
text content_hash PK
jsonb payload
text source_endpoint
timestamptz fetched_at
}
ODS ||--o{ DWD : "清洗装载"
DWD {
bigint id PK
timestamptz scd2_start_time
timestamptz scd2_end_time
int scd2_is_current
int scd2_version
}
DWD ||--o{ CORE : "提取最小字段集"
CORE {
bigint id PK
text name
bigint site_id
}
DWD ||--o{ DWS : "汇总聚合"
CORE ||--o{ DWS : "汇总聚合"
DWS {
bigint id PK
date stat_date
numeric amount
bigint site_id
}
DWS ||--o{ APP : "视图封装"
APP {
text view_name
text rls_policy
}
```
### zqyy_app 数据库
```mermaid
erDiagram
USERS {
bigint id PK
text wx_openid UK
text mobile
text nickname
int status
timestamptz created_at
}
ROLES {
int id PK
text name UK
text description
}
USER_ROLES {
bigint user_id FK
int role_id FK
}
PERMISSIONS {
int id PK
text resource
text action
}
ROLE_PERMISSIONS {
int role_id FK
int permission_id FK
}
USERS ||--o{ USER_ROLES : "拥有"
ROLES ||--o{ USER_ROLES : "分配给"
ROLES ||--o{ ROLE_PERMISSIONS : "包含"
PERMISSIONS ||--o{ ROLE_PERMISSIONS : "授予"
FDW_ETL_VIEWS {
text foreign_table_name
text source_schema
text mapping_type
}
```
### 配置分层模型
```
优先级(低 → 高):
┌─────────────────────────────┐
│ 根 .env公共配置模板 │ DB_HOST, DB_PORT, TIMEZONE
├─────────────────────────────┤
│ 应用 .env.local私有覆盖 │ DB_NAME, DB_PASSWORD, API_TOKEN
├─────────────────────────────┤
│ 环境变量 │ 运行时覆盖
├─────────────────────────────┤
│ CLI 参数 │ 最高优先级
└─────────────────────────────┘
```
## 正确性属性
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1: README.md 结构完整性
*对于任意* Monorepo 一级目录,其 README.md 文件应存在且包含"作用说明"、"结构描述"和"Roadmap"三个段落。
**Validates: Requirements 1.5**
### Property 2: Python 子项目配置完整性
*对于任意* uv workspace 声明的 Python 子项目成员,该子项目目录下应存在独立的 `pyproject.toml` 文件,且文件中包含 `[project]` 段落。
**Validates: Requirements 3.2**
### Property 3: 配置优先级 - .env.local 覆盖
*对于任意*配置项名称和两个不同的值,当根 `.env` 和应用 `.env.local` 都定义了该配置项时,配置加载器返回的值应等于 `.env.local` 中的值。
**Validates: Requirements 4.3**
### Property 4: 必需配置缺失检测
*对于任意*必需配置项,当所有配置层级(.env、.env.local、环境变量、CLI均未提供该项时配置加载器应抛出错误且错误信息中包含该缺失配置项的名称。
**Validates: Requirements 4.4**
### Property 5: 文件迁移完整性
*对于任意*源-目标目录映射关系ETL 业务代码、database 文件、tests 目录),源目录中的每个文件在目标目录的对应位置都应存在且内容一致。
**Validates: Requirements 5.1, 5.2, 5.3**
### Property 6: Schema 表定义迁移完整性
*对于任意*现有数据库 schemabilliards_ods、billiards_dws中的表新 schemaods、dws的 DDL 文件中应包含该表的 CREATE TABLE 定义。
**Validates: Requirements 7.3, 7.6**
### Property 7: Core schema 最小字段集
*对于任意* core schema 中的表,其字段数量应严格少于对应 dwd schema 中同名(或对应)表的字段数量。
**Validates: Requirements 7.5**
### Property 8: 测试数据库结构一致性
*对于任意*生产数据库etl_feiqiu、zqyy_app中的 schema 和表定义对应的测试数据库test_etl_feiqiu、test_zqyy_app中应存在相同的 schema 和表结构。
**Validates: Requirements 9.1, 9.2**
### Property 9: Steering 文件路径更新
*对于任意* `.kiro/steering/` 目录下的文件,文件内容中不应包含旧仓库路径引用(如 `FQ-ETL``C:\ZQYY\FQ-ETL`)。
**Validates: Requirements 10.2**
### Property 10: 业务表 site_id 字段存在性
*对于任意* app schema 中的业务视图和 dws/core schema 中的业务表,其定义中应包含 `site_id` 字段。
**Validates: Requirements 13.1**
### Property 11: RLS 按 site_id 隔离
*对于任意* app schema 中启用了 RLS 的视图,当会话变量 `app.current_site_id` 设置为某个门店 ID 时,查询结果应仅包含该 `site_id` 的数据行。
**Validates: Requirements 13.2**
## 错误处理
### 配置错误
- **缺失必需配置**:启动时立即报错,列出所有缺失项名称,不启动服务
- **配置值格式错误**:报告具体的配置项路径和期望格式
- **.env 文件不存在**:使用默认值继续,不报错(.env.template 仅为模板)
### 迁移错误
- **源文件不存在**:记录警告日志,继续迁移其他文件,最终汇总报告缺失文件列表
- **目标目录已存在**:提示用户确认是否覆盖,默认不覆盖
- **import 路径修复失败**:记录错误日志,标记需要手动修复的文件
### 数据库错误
- **Schema 创建失败**:回滚当前 schema 的所有 DDL报告失败原因
- **FDW 连接失败**:记录错误日志,不影响本地表的正常使用
- **RLS 策略创建失败**:回滚策略创建,报告受影响的表
### 测试数据库错误
- **结构不一致**:提供 diff 工具比较生产与测试库结构差异
- **数据迁移失败**:回滚到迁移前状态,报告失败的表和原因
## 测试策略
### 测试框架
- **单元测试**`pytest`Python 子项目)
- **属性测试**`hypothesis`Python 属性测试库)
- 每个属性测试配置最少 100 次迭代
### 单元测试覆盖
1. **Scaffold 测试**:验证目录创建、文件生成的具体示例
2. **配置加载器测试**:验证分层加载、冲突处理、缺失检测的边界情况
3. **迁移脚本测试**:验证文件复制、路径映射的具体场景
4. **DDL 语法测试**:验证生成的 SQL 语法正确性
### 属性测试覆盖
每个属性测试必须引用设计文档中的属性编号:
- **Feature: monorepo-migration, Property 1: README.md 结构完整性** — 验证所有一级目录 README 包含必需段落
- **Feature: monorepo-migration, Property 2: Python 子项目配置完整性** — 验证所有 workspace 成员有 pyproject.toml
- **Feature: monorepo-migration, Property 3: 配置优先级** — 生成随机配置项,验证 .env.local 覆盖行为
- **Feature: monorepo-migration, Property 4: 必需配置缺失检测** — 生成随机必需项组合,验证缺失报错
- **Feature: monorepo-migration, Property 5: 文件迁移完整性** — 验证源-目标文件映射的完整性
- **Feature: monorepo-migration, Property 6: Schema 表定义迁移完整性** — 验证现有表在新 DDL 中存在
- **Feature: monorepo-migration, Property 7: Core schema 最小字段集** — 验证 core 表字段数少于 dwd
- **Feature: monorepo-migration, Property 8: 测试数据库结构一致性** — 验证测试库与生产库结构相同
- **Feature: monorepo-migration, Property 9: Steering 文件路径更新** — 验证无旧路径残留
- **Feature: monorepo-migration, Property 10: 业务表 site_id 存在性** — 验证业务表包含 site_id
- **Feature: monorepo-migration, Property 11: RLS 隔离** — 验证 RLS 按 site_id 过滤(集成测试)
### 集成测试
- **ETL 运行验证**:在新目录结构下运行 `pytest tests/unit`,确保所有现有测试通过
- **数据库 Schema 验证**:在测试数据库上执行 DDL验证 schema 创建成功
- **FDW 连接验证**:验证 zqyy_app 通过 FDW 可读取 etl_feiqiu 的 app schema 数据
- **uv workspace 验证**:运行 `uv sync`,验证所有子项目依赖正确解析

View File

@@ -1,186 +0,0 @@
# 需求文档Monorepo 迁移
## 简介
将现有台球厅运营助手项目从单一 ETL 仓库(`FQ-ETL`)扩展为 Monorepo 单体仓库(`NeoZQYY`),整合 ETL 管线、微信小程序后端、小程序前端、管理后台等多个子项目。迁移采用一次性搬迁策略,不保留 Git 历史,所有架构决策已在前期讨论中确认。
## 术语表
- **Monorepo**:单体仓库,多个子项目共存于同一 Git 仓库中
- **ETL_Pipeline**:数据抽取-转换-加载管线,负责从上游 SaaS API 抓取数据并逐层处理
- **ODS**操作数据存储层Operational Data Store保留源 payload
- **DWD**明细数据层Data Warehouse Detail清洗后的明细数据
- **DWS**数据服务层Data Warehouse Service汇总与聚合数据
- **Core**:统一维度/事实最小字段集层,位于 DWD 与 DWS 之间
- **App_Schema**:应用层 schema提供视图/函数 + RLS 供外部访问
- **Meta_Schema**:元数据层 schema存储 ETL 调度、游标、运行记录
- **FDW**PostgreSQL 外部数据包装器Foreign Data Wrapper用于跨库只读映射
- **uv_Workspace**Python 包管理工具 uv 的 workspace 模式,管理多包依赖
- **RLS**行级安全策略Row Level Security用于多门店数据隔离
- **SCD2**:缓慢变化维度类型 2Slowly Changing Dimension Type 2维度历史追踪
- **etl_feiqiu**:飞球平台 ETL 数据库实例名
- **zqyy_app**:业务应用数据库实例名(用户/权限/任务/审批)
- **site_id**:门店标识字段,用于多门店数据隔离
- **Scaffold**项目骨架包含目录结构、配置文件、README 等基础设施
## 需求
### 需求 1Monorepo 骨架搭建
**用户故事:** 作为开发者,我希望在 `C:\NeoZQYY\` 创建完整的 Monorepo 目录结构和基础配置,以便所有子项目有统一的组织方式和开发规范。
#### 验收标准
1. WHEN Scaffold 初始化执行时THE Scaffold SHALL 在 `C:\NeoZQYY\` 下创建以下一级目录:`apps/``gui/``packages/``db/``docs/``infra/``scripts/``samples/``tests/``tmp/``.kiro/`
2. WHEN Scaffold 初始化执行时THE Scaffold SHALL 在 `apps/` 下创建子目录:`etl/`(含 `pipelines/feiqiu/`)、`backend/``miniprogram/``admin-web/`
3. WHEN Scaffold 初始化执行时THE Scaffold SHALL 在 `db/` 下创建子目录:`etl_feiqiu/`(含 `schemas/``migrations/``seeds/`)、`zqyy_app/`(含 `schemas/``migrations/``seeds/`)、`fdw/`
4. WHEN Scaffold 初始化执行时THE Scaffold SHALL 在 `docs/` 下创建子目录:`prd/``contracts/`(含 `openapi/``schemas/``data_dictionary/`)、`permission_matrix/``architecture/``database/``h5_ui/``ops/``audit/``roadmap/`
5. THE Scaffold SHALL 为每个一级目录生成 `README.md` 文件,包含该目录的作用说明、内部结构描述和 Roadmap 段落
6. WHEN 某个功能"暂不实施但未来必须做"时THE Scaffold SHALL 将该内容记录在对应目录 `README.md` 的 Roadmap 段落中
### 需求 2Git 仓库与版本控制配置
**用户故事:** 作为开发者,我希望新 Monorepo 有正确的 Git 配置,以便代码版本管理规范且安全。
#### 验收标准
1. WHEN Git 仓库初始化时THE Scaffold SHALL 创建新的 Git 仓库,不迁移旧仓库历史
2. THE Scaffold SHALL 生成 `.gitignore` 文件,排除 `tmp/``__pycache__/``.env`(非模板)、`*.pyc``.hypothesis/``.pytest_cache/``logs/``node_modules/`、虚拟环境目录等
3. THE Scaffold SHALL 生成 `.kiroignore` 文件,排除不需要 Kiro 索引的目录
### 需求 3Python 包管理与 uv Workspace 配置
**用户故事:** 作为开发者,我希望使用 `pyproject.toml` + `uv` workspace 管理多包依赖,以便各子项目的依赖隔离且可统一管理。
#### 验收标准
1. THE Scaffold SHALL 在 Monorepo 根目录生成 `pyproject.toml`,配置 uv workspace 并声明所有 Python 子项目成员
2. THE Scaffold SHALL 为每个 Python 子项目(`apps/etl/pipelines/feiqiu/``apps/backend/``packages/shared/``gui/`)生成独立的 `pyproject.toml`
3. WHEN 子项目声明对 `packages/shared` 的依赖时THE uv_Workspace SHALL 通过 workspace 路径引用解析该依赖
### 需求 4环境配置隔离
**用户故事:** 作为开发者,我希望公共配置和各应用私有配置分层管理,以便敏感信息不泄露且配置不冲突。
#### 验收标准
1. THE Scaffold SHALL 在 Monorepo 根目录生成 `.env.template` 文件,包含公共配置项(数据库主机、端口等非敏感信息)的模板
2. WHEN 各应用需要私有配置时THE Scaffold SHALL 支持在应用目录下放置 `.env.local` 文件覆盖公共配置
3. IF 公共 `.env` 与应用 `.env.local` 存在同名配置项且值冲突THEN THE 配置加载器 SHALL 以应用级 `.env.local` 的值为准
4. IF 必需的配置项在所有层级均缺失THEN THE 配置加载器 SHALL 在启动时报告明确的错误信息,指出缺失的配置项名称
### 需求 5ETL 项目平移
**用户故事:** 作为开发者,我希望将现有 ETL 项目整体平移到 Monorepo 中,以便 ETL 功能在新仓库中正常运行。
#### 验收标准
1. WHEN ETL 平移执行时THE 迁移脚本 SHALL 将 `C:\ZQYY\FQ-ETL` 的业务代码(`api/``cli/``config/``loaders/``models/``orchestration/``scd/``tasks/``utils/``quality/`)复制到 `apps/etl/pipelines/feiqiu/`
2. WHEN ETL 平移执行时THE 迁移脚本 SHALL 将 `database/` 目录的 DDL、seed、migration 文件迁移到 `db/etl_feiqiu/` 对应子目录
3. WHEN ETL 平移执行时THE 迁移脚本 SHALL 将 `tests/` 目录复制到 `apps/etl/pipelines/feiqiu/tests/`
4. WHEN ETL 平移完成后THE ETL_Pipeline SHALL 通过 `pytest tests/unit` 验证所有单元测试通过
5. IF ETL 内部存在需要调整的 import 路径THEN THE 迁移脚本 SHALL 更新这些路径以适配新目录结构
6. WHEN ETL 平移执行时THE 迁移脚本 SHALL 将现有 `gui/` 目录迁移到 Monorepo 顶层 `gui/`
### 需求 6小程序前端平移
**用户故事:** 作为开发者,我希望将微信小程序项目迁移到 Monorepo 中,以便前端代码与后端统一管理。
#### 验收标准
1. WHEN 小程序平移执行时THE 迁移脚本 SHALL 将 `C:\ZQYY\XCX` 的项目文件复制到 `apps/miniprogram/`
2. WHEN 小程序平移执行时THE 迁移脚本 SHALL 将 `C:\ZQYY\XCX\Prototype` 目录复制到 `docs/h5_ui/`
3. WHEN 小程序平移完成后THE 小程序项目 SHALL 保持原有的 Donut + TDesign 技术栈配置不变
### 需求 7数据库 Schema 重组etl_feiqiu
**用户故事:** 作为数据工程师,我希望将 ETL 数据库重组为六层 schema 架构,以便数据分层清晰、职责明确。
#### 验收标准
1. THE 数据库迁移 SHALL 为 `etl_feiqiu` 数据库创建六个 schema`meta``ods``dwd``core``dws``app`
2. WHEN `meta` schema 创建时THE DDL SHALL 包含 ETL 调度、游标、运行记录相关表(从现有 `etl_admin` schema 迁移)
3. WHEN `ods` schema 创建时THE DDL SHALL 包含现有 `billiards_ods` 的所有表定义
4. WHEN `dwd` schema 创建时THE DDL SHALL 保留现有 main + EX 拆分模式(因字段量大)
5. WHEN `core` schema 创建时THE DDL SHALL 仅包含统一维度表和事实表的最小字段集
6. WHEN `dws` schema 创建时THE DDL SHALL 包含现有 `billiards_dws` 的汇总表定义(助教业绩、财务日报、工资计算等)
7. WHEN `app` schema 创建时THE DDL SHALL 创建面向外部访问的视图和函数,并配置 RLS 策略以 `site_id` 隔离多门店数据
8. THE 数据库迁移 SHALL 将所有 DDL 文件存放在 `db/etl_feiqiu/schemas/` 目录下,每个 schema 一个独立文件
### 需求 8业务数据库设计zqyy_app
**用户故事:** 作为后端开发者,我希望有独立的业务数据库存储用户、权限、任务、审批等应用数据,以便业务逻辑与 ETL 数据解耦。
#### 验收标准
1. THE 数据库迁移 SHALL 为 `zqyy_app` 数据库创建用户管理、权限控制、任务管理、审批流程相关的表结构
2. THE 数据库迁移 SHALL 将 `zqyy_app` 的 DDL 文件存放在 `db/zqyy_app/schemas/` 目录下
3. WHEN `zqyy_app` 需要访问 ETL 数据时THE FDW 配置 SHALL 通过 `postgres_fdw``etl_feiqiu``app` schema 映射为 `zqyy_app` 中的外部表
4. THE FDW 配置 SHALL 以只读方式映射,`zqyy_app` 不存储 ETL 数据的副本
5. THE FDW 配置文件 SHALL 存放在 `db/fdw/` 目录下
### 需求 9测试数据库镜像
**用户故事:** 作为开发者,我希望有与生产结构完全一致的测试数据库,以便在不影响生产数据的情况下进行开发和测试。
#### 验收标准
1. THE 数据库迁移 SHALL 创建 `test_etl_feiqiu` 数据库,其 schema 结构与 `etl_feiqiu` 完全一致
2. THE 数据库迁移 SHALL 创建 `test_zqyy_app` 数据库,其 schema 结构与 `zqyy_app` 完全一致
3. WHEN 测试数据库创建完成后THE 迁移脚本 SHALL 提供从现有 `LLZQ-test` 数据库迁移测试数据到新结构的脚本
4. WHEN 生产数据库 schema 发生变更时THE 测试数据库 SHALL 同步应用相同的迁移脚本以保持结构一致
### 需求 10.kiro 配置迁移与 Steering 更新
**用户故事:** 作为开发者,我希望 Kiro IDE 的配置和 steering 文件适配 Monorepo 结构,以便 AI 辅助开发在新仓库中正常工作。
#### 验收标准
1. WHEN .kiro 迁移执行时THE 迁移脚本 SHALL 将现有 `.kiro/steering/` 文件复制到 Monorepo 的 `.kiro/steering/`
2. WHEN .kiro 迁移完成后THE Steering 文件 SHALL 更新所有路径引用以反映 Monorepo 目录结构
3. WHEN .kiro 迁移完成后THE Steering 文件 SHALL 更新 `product.md``tech.md``structure.md` 为 Monorepo 视角的内容
### 需求 11FastAPI 后端骨架
**用户故事:** 作为后端开发者,我希望有 FastAPI 项目骨架,以便快速开始小程序后端 API 的开发。
#### 验收标准
1. WHEN 后端骨架创建时THE Scaffold SHALL 在 `apps/backend/` 下生成 FastAPI 项目结构,包含入口文件、路由目录、中间件目录、配置文件
2. THE 后端骨架 SHALL 配置 OpenAPI 文档自动生成
3. THE 后端骨架 SHALL 配置数据库连接模块,支持连接 `zqyy_app` 数据库
4. THE 后端骨架 SHALL 包含独立的 `pyproject.toml`,声明 FastAPI 及相关依赖
### 需求 12共享包基础结构
**用户故事:** 作为开发者,我希望有统一的共享包存放跨项目复用的工具代码,以便避免代码重复。
#### 验收标准
1. WHEN 共享包创建时THE Scaffold SHALL 在 `packages/shared/` 下生成 Python 包结构,包含 `__init__.py``pyproject.toml`
2. THE 共享包 SHALL 提取并包含以下通用工具模块字段枚举定义、金额精度处理工具CNYnumeric(2))、时间处理工具
3. WHEN ETL 或后端项目引用共享包时THE uv_Workspace SHALL 通过 workspace 路径依赖解析 `packages/shared`
### 需求 13多门店数据隔离
**用户故事:** 作为系统架构师,我希望在同一数据库内通过 RLS 实现多门店数据隔离,以便未来扩展到多门店场景。
#### 验收标准
1. THE 数据库设计 SHALL 在所有业务表中包含 `site_id` 字段标识门店归属
2. WHEN RLS 策略启用时THE 数据库 SHALL 根据当前会话的 `site_id` 参数自动过滤查询结果,仅返回该门店的数据
3. WHEN 每个门店运行 ETL 时THE ETL_Pipeline SHALL 作为独立进程执行,使用该门店的 `site_id` 标识数据
### 需求 14基础设施配置管理
**用户故事:** 作为运维人员,我希望基础设施配置纳入版本控制,以便环境配置可追溯、可复现。
### 需求15避免影响kiro性能完成Monorepo后根据文件和目录结构。编辑.kiroignore
验收标准:完善
#### 验收标准
1. THE Scaffold SHALL 在 `infra/` 下创建 `jump_proxy/``tailscale/``firewall/` 子目录
2. THE infra 目录 SHALL 纳入 Git 版本控制
3. IF infra 目录中包含敏感配置文件THEN THE `.gitignore` SHALL 排除这些敏感文件,同时保留非敏感的配置模板

View File

@@ -1,215 +0,0 @@
# 实施计划Monorepo 迁移
## 概述
将现有 ETL 仓库迁移为 Monorepo 单体仓库,分 7 个阶段执行。每个阶段包含具体的代码/文件操作任务,按依赖顺序排列。
## 任务
- [x] 1. Monorepo 骨架搭建
- [x] 1.1 在 `C:\NeoZQYY\` 创建完整目录结构
- 创建所有一级目录:`apps/``gui/``packages/``db/``docs/``infra/``scripts/``samples/``tests/``tmp/``.kiro/`
- 创建 `apps/` 子目录:`etl/pipelines/feiqiu/``backend/``miniprogram/``admin-web/`
- 创建 `db/` 子目录:`etl_feiqiu/schemas/``etl_feiqiu/migrations/``etl_feiqiu/seeds/``zqyy_app/schemas/``zqyy_app/migrations/``zqyy_app/seeds/``fdw/`
- 创建 `docs/` 子目录:`prd/``contracts/openapi/``contracts/schemas/``contracts/data_dictionary/``permission_matrix/``architecture/``database/``h5_ui/``ops/``audit/``roadmap/`
- 创建 `infra/` 子目录:`jump_proxy/``tailscale/``firewall/`
- _Requirements: 1.1, 1.2, 1.3, 1.4, 14.1_
- [x] 1.2 生成所有一级目录的 README.md
- 每个 README 包含作用说明、内部结构描述、Roadmap 段落
- 一级目录列表:`apps/``gui/``packages/``db/``docs/``infra/``scripts/``samples/``tests/`
- `apps/etl/README.md` 的 Roadmap 记录未来 sdk/connectors 拆分计划
- `packages/README.md` 的 Roadmap 记录 etl_sdk、authz、data_contracts 候选
- `db/README.md` 的 Roadmap 记录 FDW 演进计划
- _Requirements: 1.5, 1.6_
- [x] 1.3 编写 README 结构完整性属性测试
- **Property 1: README.md 结构完整性**
- **Validates: Requirements 1.5**
- [x] 1.4 初始化 Git 仓库并生成版本控制配置
-`C:\NeoZQYY\` 执行 `git init`
- 生成 `.gitignore`:排除 `tmp/``__pycache__/``.env``*.pyc``.hypothesis/``.pytest_cache/``logs/``node_modules/`、虚拟环境目录、`infra/` 下的敏感文件
- 生成 `.kiroignore`
- _Requirements: 2.1, 2.2, 2.3, 14.2, 14.3_
- [x] 1.5 配置 pyproject.toml 和 uv workspace
- 生成根 `pyproject.toml`,声明 workspace 成员:`apps/etl/pipelines/feiqiu``apps/backend``packages/shared``gui`
- 为每个 Python 子项目生成独立 `pyproject.toml`
- _Requirements: 3.1, 3.2_
- [x] 1.6 编写 Python 子项目配置完整性属性测试
- **Property 2: Python 子项目配置完整性**
- **Validates: Requirements 3.2**
- [x] 1.7 生成环境配置模板
- 生成根 `.env.template`包含公共配置项模板DB_HOST、DB_PORT、TIMEZONE 等)
- _Requirements: 4.1_
- [x] 2. 检查点 - 骨架验证
- 确保所有目录和文件已创建ask the user if questions arise.
- [x] 3. ETL 项目平移
- [x] 3.1 复制 ETL 业务代码到 Monorepo
-`C:\ZQYY\FQ-ETL``api/``cli/``config/``loaders/``models/``orchestration/``scd/``tasks/``utils/``quality/` 复制到 `apps/etl/pipelines/feiqiu/`
-`tests/` 复制到 `apps/etl/pipelines/feiqiu/tests/`
-`requirements.txt``pytest.ini``run_etl.bat``run_etl.sh` 复制到 `apps/etl/pipelines/feiqiu/`
- _Requirements: 5.1, 5.3_
- [x] 3.2 迁移数据库文件到 db/etl_feiqiu/
-`database/schema_*.sql` 复制到 `db/etl_feiqiu/schemas/`
-`database/migrations/` 复制到 `db/etl_feiqiu/migrations/`
-`database/seed_*.sql` 复制到 `db/etl_feiqiu/seeds/`
-`database/connection.py``database/operations.py``database/base.py` 保留在 ETL 内部(`apps/etl/pipelines/feiqiu/database/`
- _Requirements: 5.2_
- [x] 3.3 迁移 GUI 到顶层
-`C:\ZQYY\FQ-ETL\gui/` 复制到 `C:\NeoZQYY\gui/`
- 生成 `gui/pyproject.toml`,声明 PySide6 依赖
- _Requirements: 5.6_
- [x] 3.4 调整 ETL 的 pyproject.toml 和 pytest.ini
- 更新 `apps/etl/pipelines/feiqiu/pyproject.toml`,从 `requirements.txt` 提取依赖
- 更新 `apps/etl/pipelines/feiqiu/pytest.ini`,设置 `pythonpath = .`
- _Requirements: 5.4, 5.5_
- [x] 3.5 编写文件迁移完整性属性测试
- **Property 5: 文件迁移完整性**
- **Validates: Requirements 5.1, 5.2, 5.3**
- [x] 4. 检查点 - ETL 平移验证
-`apps/etl/pipelines/feiqiu/` 下运行 `pytest tests/unit`,确保所有单元测试通过
- Ensure all tests pass, ask the user if questions arise.
- [x] 5. 小程序前端平移
- [x] 5.1 复制小程序项目到 Monorepo
-`C:\ZQYY\XCX\`(除 Prototype 目录)复制到 `apps/miniprogram/`
-`C:\ZQYY\XCX\Prototype\` 复制到 `docs/h5_ui/`
- 生成 `apps/miniprogram/README.md`
- _Requirements: 6.1, 6.2, 6.3_
- [x] 6. 数据库 Schema 重组
- [x] 6.1 编写 etl_feiqiu 六层 Schema DDL
- 创建 `db/etl_feiqiu/schemas/meta.sql`:从现有 `etl_admin` schema 迁移调度、游标、运行记录表
- 创建 `db/etl_feiqiu/schemas/ods.sql`:从现有 `billiards_ods` 迁移所有表定义schema 名改为 `ods`
- 创建 `db/etl_feiqiu/schemas/dwd.sql`:从现有 `billiards_dwd` 迁移,保留 main+EX 拆分
- 创建 `db/etl_feiqiu/schemas/core.sql`:设计统一维度/事实最小字段集表
- 创建 `db/etl_feiqiu/schemas/dws.sql`:从现有 `billiards_dws` 迁移汇总表
- 创建 `db/etl_feiqiu/schemas/app.sql`:创建面向外部的视图 + RLS 策略(以 `site_id` 隔离)
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8_
- [x] 6.2 编写 Schema 表定义迁移完整性属性测试
- **Property 6: Schema 表定义迁移完整性**
- **Validates: Requirements 7.3, 7.6**
- [x] 6.3 编写 Core schema 最小字段集属性测试
- **Property 7: Core schema 最小字段集**
- **Validates: Requirements 7.5**
- [x] 6.4 编写 zqyy_app 数据库 Schema DDL
- 创建 `db/zqyy_app/schemas/init.sql`:用户表、角色表、权限表、用户角色关联表、任务表、审批表
- 所有业务表包含 `site_id` 字段
- _Requirements: 8.1, 8.2, 13.1_
- [x] 6.5 编写 FDW 映射配置
- 创建 `db/fdw/setup_fdw.sql`CREATE SERVER、CREATE USER MAPPING只读角色、IMPORT FOREIGN SCHEMA
- _Requirements: 8.3, 8.4, 8.5_
- [x] 6.6 编写业务表 site_id 存在性属性测试
- **Property 10: 业务表 site_id 字段存在性**
- **Validates: Requirements 13.1**
- [x] 6.7 编写测试数据库创建脚本
- 创建 `db/etl_feiqiu/scripts/create_test_db.sql`:创建 `test_etl_feiqiu`,复用生产 DDL
- 创建 `db/zqyy_app/scripts/create_test_db.sql`:创建 `test_zqyy_app`,复用生产 DDL
- 创建 `db/scripts/migrate_test_data.sql`:从 `LLZQ-test` 迁移测试数据的脚本
- _Requirements: 9.1, 9.2, 9.3_
- [x] 6.8 编写测试数据库结构一致性属性测试
- **Property 8: 测试数据库结构一致性**
- **Validates: Requirements 9.1, 9.2**
- [x] 7. 检查点 - 数据库 Schema 验证
- 确保所有 DDL 文件语法正确ask the user if questions arise.
- [ ] 8. .kiro 迁移与 Steering 更新
- [-] 8.1 复制 .kiro/steering/ 到 Monorepo
-`C:\ZQYY\FQ-ETL\.kiro\steering\` 所有文件复制到 `C:\NeoZQYY\.kiro\steering\`
-`C:\ZQYY\FQ-ETL\.kiro\specs\` 复制到 `C:\NeoZQYY\.kiro\specs\`(包含本 spec
- _Requirements: 10.1_
- [~] 8.2 更新 Steering 文件为 Monorepo 视角
- 更新 `product.md`:从单一 ETL 扩展为 Monorepo 全局视角ETL + 后端 + 小程序 + GUI
- 更新 `tech.md`:新增 FastAPI、uv workspace、Donut+TDesign 技术栈
- 更新 `structure-lite.md`:反映 Monorepo 目录结构和模块边界
- 更新所有 steering 文件中的路径引用,移除旧仓库路径(`FQ-ETL``C:\ZQYY\FQ-ETL`
- _Requirements: 10.2, 10.3_
- [~] 8.3 编写 Steering 文件路径更新属性测试
- **Property 9: Steering 文件路径更新**
- **Validates: Requirements 10.2**
- [ ] 9. FastAPI 后端骨架
- [~] 9.1 创建 FastAPI 项目结构
- 创建 `apps/backend/app/__init__.py``main.py``config.py``database.py`
- 创建 `apps/backend/app/routers/__init__.py`
- 创建 `apps/backend/app/middleware/__init__.py`
- 创建 `apps/backend/app/schemas/__init__.py`
- 创建 `apps/backend/tests/__init__.py`
- `main.py` 中配置 FastAPI 实例,启用 OpenAPI 文档自动生成
- `database.py` 中配置 `zqyy_app` 数据库连接
- _Requirements: 11.1, 11.2, 11.3_
- [~] 9.2 生成 apps/backend/pyproject.toml
- 声明 FastAPI、uvicorn、psycopg2-binary、neozqyy-shared 等依赖
- 配置 uv workspace 源引用 `neozqyy-shared`
- _Requirements: 11.4_
- [~] 9.3 生成 apps/backend/README.md
- 包含作用说明、项目结构、启动方式、Roadmap
- _Requirements: 1.5_
- [ ] 10. 共享包搭建
- [~] 10.1 创建 packages/shared 包结构
- 创建 `packages/shared/src/neozqyy_shared/__init__.py`
- 创建 `packages/shared/src/neozqyy_shared/enums.py`:字段枚举定义(从 ETL models/ 提取通用枚举)
- 创建 `packages/shared/src/neozqyy_shared/money.py`金额精度工具Decimal + ROUND_HALF_UPscale=2
- 创建 `packages/shared/src/neozqyy_shared/datetime_utils.py`:时区转换、日期范围计算
- 创建 `packages/shared/tests/__init__.py`
- _Requirements: 12.1, 12.2_
- [~] 10.2 生成 packages/shared/pyproject.toml
- 声明包名 `neozqyy-shared`最小依赖python-dateutil、tzdata
- _Requirements: 12.3_
- [~] 10.3 编写配置优先级属性测试
- **Property 3: 配置优先级 - .env.local 覆盖**
- **Validates: Requirements 4.3**
- [~] 10.4 编写必需配置缺失检测属性测试
- **Property 4: 必需配置缺失检测**
- **Validates: Requirements 4.4**
- [ ] 11. 检查点 - 全局验证
- 验证 uv workspace 依赖解析:在根目录运行 `uv sync`
- 验证 ETL 单元测试:在 `apps/etl/pipelines/feiqiu/` 下运行 `pytest tests/unit`
- Ensure all tests pass, ask the user if questions arise.
- [ ] 12. RLS 与多门店隔离验证
- [~] 12.1 编写 RLS 按 site_id 隔离属性测试
- **Property 11: RLS 按 site_id 隔离**
- **Validates: Requirements 13.2**
- 需要集成测试环境test_etl_feiqiu 数据库)
- [ ] 13. 最终检查点
- 确保所有文件已创建、所有 README 已编写、所有 DDL 语法正确
- Ensure all tests pass, ask the user if questions arise.
## 备注
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用具体需求编号,确保可追溯
- 检查点确保增量验证,避免问题累积
- 属性测试验证通用正确性,单元测试验证具体边界情况
- 文件复制操作需要用户在终端手动执行涉及跨目录操作Kiro 负责生成目标文件内容

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,449 @@
# 设计文档ODS 去重与软删除机制标准化
## 概述
本设计对 ODS 层的 `OdsTaskSpec` 配置、content_hash 去重策略、软删除语义进行标准化改造。核心原则ODS 是追加写入的版本化存储,每次内容变更(包括删除)都是一个新版本行。
改造分四个阶段:
1. **配置精简**(方案 1删除无效/冗余字段,引入 SnapshotMode 枚举
2. **去重优化**(方案 2默认开启 skip_unchangedhash 改用 payload + is_delete
3. **索引支持**(方案 3为"取最新版本"查询添加复合索引
4. **软删除语义**(方案 4从 UPDATE 改为 INSERT 删除版本行
## 改造前后对比
### 配置层对比
| 维度 | 改造前 | 改造后 |
|------|--------|--------|
| 去重开关 | `enable_content_hash_dedup=False`22/23 任务关闭) | `skip_unchanged=True`(默认开启) |
| 快照策略 | `snapshot_full_table` + `snapshot_window_columns` 两个字段组合 | `SnapshotMode` 枚举NONE/FULL_TABLE/WINDOW+ `snapshot_time_column` |
| 冲突列 | `conflict_columns_override`(运行时不生效,仅声明性标注) | 删除PK 唯一来源为 DDL |
| 冗余字段 | `include_site_column`/`include_page_no`/`include_page_size`(全部 False | 删除,硬编码移除 |
### content_hash 计算对比
| 维度 | 改造前 | 改造后 |
|------|--------|--------|
| 输入 | 展平后的 merged_rec排除 7 个元数据字段 | 原始 payload JSON + is_delete |
| 排除逻辑 | `_sanitize_record_for_hash` 递归排除 source_file/source_endpoint/fetched_at/record_index/content_hash/payload/data | 无需排除——payload 天然不含元数据字段 |
| is_delete 参与 | 不参与is_delete 变化不改变 hash | 参与is_delete 变化产生新 hash → 新版本行) |
| 默认行为 | 22/23 任务不算 hash每次抓取都插入新行 | 所有任务默认算 hash内容不变则跳过 |
### 软删除对比
| 维度 | 改造前 | 改造后 |
|------|--------|--------|
| 操作方式 | `UPDATE ... SET is_delete=1`(修改所有历史版本) | INSERT 一条 is_delete=1 的新版本行 |
| 历史版本影响 | 所有历史版本的 is_delete 被改为 1 | 历史版本完全不变 |
| 幂等性 | 重复执行无副作用UPDATE 幂等) | 重复执行无副作用(最新版本已是 is_delete=1 则跳过) |
| 下游取数 | `WHERE is_delete = 0`(但历史版本也被改了) | `DISTINCT ON (id) ORDER BY fetched_at DESC` + `WHERE is_delete = 0` |
### 新版本数据处理流程
#### 正常写入流程(每次 ETL 运行)
```
1. API 抓取 → 获得一批记录
2. 对每条记录:
a. _normalize_is_delete_flag标准化 is_delete 字段API 可能返回 isDelete/is_deleted 等变体)
b. 取原始 record 作为 payload
c. _compute_content_hash(payload, is_delete) → 计算 hash
d. 若 skip_unchanged=True
- 查询该业务 ID 在数据库中的最新 content_hash
- 若 hash 相同 → 跳过(内容未变,无需新版本)
- 若 hash 不同或无历史记录 → 继续插入
e. INSERT INTO ods.xxx (..., content_hash, payload, is_delete, fetched_at)
ON CONFLICT (id, content_hash) DO UPDATE ...
```
#### 软删除流程(快照对比,路径 B
```
前提:任务配置了 snapshot_mode != NONE且 run.snapshot_missing_delete=True
1. 收集本次抓取到的所有业务 ID → fetched_keys
2. 查询快照范围内数据库中已有的业务 IDis_delete != 1
- FULL_TABLE 模式:全表范围
- WINDOW 模式WHERE {snapshot_time_column} >= window_start AND < window_end
3. 差集 = 数据库中的 ID - fetched_keys → 缺失 ID
4. 对每个缺失 ID
a. SELECT DISTINCT ON (id) * FROM ods.xxx WHERE id = ? ORDER BY fetched_at DESC
→ 读取最新版本行
b. 若最新版本已是 is_delete=1 → 跳过(幂等)
c. 否则:
- 复制最新版本行的所有字段
- 设 is_delete = 1
- _compute_content_hash(原payload, is_delete=1) → 新 hash
- INSERT 新版本行hash 不同,不会与现有行冲突)
5. 历史版本行完全不变
```
#### 下游取数规约
```sql
-- DWD 层从 ODS 取最新有效版本的标准查询
SELECT DISTINCT ON (id) *
FROM ods.{table_name}
WHERE is_delete IS DISTINCT FROM 1 -- 排除已删除
ORDER BY id, fetched_at DESC; -- 利用 (id, fetched_at DESC) 索引
-- 若需要包含删除状态(如审计场景)
SELECT DISTINCT ON (id) *
FROM ods.{table_name}
ORDER BY id, fetched_at DESC;
-- 然后在应用层判断 is_delete 字段
```
## 架构
改造集中在 ODS 写入管线的三个核心环节:
```mermaid
flowchart TD
A[上游 API / JSON 回放] --> B[BaseOdsTask.execute]
B --> C{记录处理}
C --> D[_normalize_is_delete_flag<br/>标准化 is_delete 字段]
D --> E[_compute_content_hash<br/>基于 payload + is_delete 算 hash]
E --> F{skip_unchanged?}
F -->|hash 相同| G[跳过]
F -->|hash 不同或新记录| H[INSERT 新版本行]
B --> I{快照对比}
I -->|snapshot_mode != NONE| J[_mark_missing_as_deleted]
J --> K[读取缺失 ID 的最新版本]
K --> L[构造 is_delete=1 的新版本]
L --> M{最新版本已是 is_delete=1?}
M -->|是| N[跳过]
M -->|否| O[INSERT 删除版本行]
```
**影响范围:**
- `apps/etl/pipelines/feiqiu/tasks/ods/ods_tasks.py` — 主要改动文件
- `db/etl_feiqiu/migrations/` — 新增索引迁移脚本
- `db/etl_feiqiu/schemas/ods.sql` — DDL 注释更新(索引)
- 7 个文档文件 — 同步更新
## 组件与接口
### 1. SnapshotMode 枚举
```python
from enum import Enum
class SnapshotMode(Enum):
"""ODS 快照软删除策略。"""
NONE = "none" # 不做快照对比,不触发软删除
FULL_TABLE = "full_table" # 全表快照:对比全表所有记录
WINDOW = "window" # 窗口快照:仅对比时间窗口内的记录
```
定义在 `ods_tasks.py` 顶部,与 OdsTaskSpec 同文件。
### 2. OdsTaskSpec改造后
```python
@dataclass(frozen=False)
class OdsTaskSpec:
code: str
class_name: str
table_name: str
endpoint: str
data_path: Tuple[str, ...] = ("data",)
list_key: str | None = None
pk_columns: Tuple[ColumnSpec, ...] = ()
extra_columns: Tuple[ColumnSpec, ...] = ()
# --- 保留字段(语义不变)---
include_source_file: bool = True
include_source_endpoint: bool = True
include_record_index: bool = False
include_fetched_at: bool = True
requires_window: bool = True
time_fields: Tuple[str, str] | None = ("startTime", "endTime")
include_site_id: bool = True
description: str = ""
extra_params: Dict[str, Any] = field(default_factory=dict)
# --- 改造字段 ---
skip_unchanged: bool = True # 原 enable_content_hash_dedup默认翻转
snapshot_mode: SnapshotMode = SnapshotMode.NONE # 替代 snapshot_full_table + snapshot_window_columns
snapshot_time_column: str | None = None # WINDOW 模式的时间列
def __post_init__(self) -> None:
if self.snapshot_mode == SnapshotMode.WINDOW and not self.snapshot_time_column:
raise ValueError(
f"任务 {self.code}: snapshot_mode=WINDOW 时必须指定 snapshot_time_column"
)
if self.snapshot_mode != SnapshotMode.WINDOW and self.snapshot_time_column is not None:
raise ValueError(
f"任务 {self.code}: snapshot_mode={self.snapshot_mode.value} 时不应指定 snapshot_time_column"
)
```
**删除的字段:**
- `conflict_columns_override` — 运行时不生效
- `include_site_column` — 全部 False
- `include_page_no` — 全部 False
- `include_page_size` — 全部 False
- `snapshot_full_table` — 被 SnapshotMode 替代
- `snapshot_window_columns` — 被 SnapshotMode + snapshot_time_column 替代
- `enable_content_hash_dedup` — 被 skip_unchanged 替代
### 3. 23 个任务的 SnapshotMode 映射
当前配置到新配置的映射规则:
| 原配置 | 新配置 |
|--------|--------|
| `snapshot_full_table=True` | `snapshot_mode=SnapshotMode.FULL_TABLE` |
| `snapshot_window_columns=("col",)` | `snapshot_mode=SnapshotMode.WINDOW, snapshot_time_column="col"` |
| 两者都未设置 | `snapshot_mode=SnapshotMode.NONE`(默认值) |
具体任务映射:
| 任务 | 原配置 | 新 snapshot_mode | snapshot_time_column |
|------|--------|-----------------|---------------------|
| ODS_ASSISTANT_ACCOUNT | snapshot_full_table=True | FULL_TABLE | None |
| ODS_MEMBER_CARD | snapshot_full_table=True | FULL_TABLE | None |
| ODS_GROUP_PACKAGE | snapshot_full_table=True | FULL_TABLE | None |
| ODS_STORE_GOODS | snapshot_full_table=True | FULL_TABLE | None |
| ODS_TENANT_GOODS | snapshot_full_table=True | FULL_TABLE | None |
| ODS_TABLE_USE | snapshot_window_columns=("create_time",) | WINDOW | "create_time" |
| ODS_ASSISTANT_LEDGER | snapshot_window_columns=("create_time",) | WINDOW | "create_time" |
| ODS_STORE_GOODS_SALES | snapshot_window_columns=("create_time",) | WINDOW | "create_time" |
| ODS_REFUND | snapshot_window_columns=("pay_time",) | WINDOW | "pay_time" |
| ODS_PLATFORM_COUPON | snapshot_window_columns=("consume_time",) | WINDOW | "consume_time" |
| ODS_MEMBER_BALANCE | snapshot_window_columns=("create_time",) | WINDOW | "create_time" |
| ODS_GROUP_BUY_REDEMPTION | snapshot_window_columns=("create_time",) | WINDOW | "create_time" |
| ODS_TABLE_FEE_DISCOUNT | snapshot_window_columns=("create_time",) | WINDOW | "create_time" |
| 其余 10 个任务 | 无快照配置 | NONE | None |
### 4. _compute_content_hash改造后
```python
@classmethod
def _compute_content_hash(cls, record: dict, payload: Any, is_delete: int) -> str:
"""基于原始 payload 和 is_delete 计算 content_hash。
payload: 原始 API 返回的 JSON 对象(未展平)
is_delete: 0 或 1
"""
payload_str = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=cls._hash_default,
)
raw = f"{payload_str}|{is_delete}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
```
**关键变更:**
- 输入从"展平后的 merged_rec"改为"原始 payload + is_delete"
- 删除 `_sanitize_record_for_hash` 方法(不再需要字段排除逻辑)
- 删除 `include_fetched_at` 参数payload 天然不含 fetched_at
- 分隔符 `|` 确保 payload 和 is_delete 不会产生歧义
**一次性代价:** 切换后首次运行,所有记录的 hash 都会变化(因为算法不同),会插入一批新版本行。这是预期行为,后续运行恢复正常去重。
### 5. _mark_missing_as_deleted改造后
```python
def _mark_missing_as_deleted(self, *, table, business_pk_cols,
snapshot_mode, snapshot_time_column,
window_start, window_end,
key_values, allow_empty) -> int:
"""快照对比软删除INSERT 删除版本行,而非 UPDATE 历史版本。"""
# 1. 查询快照范围内、is_delete != 1 的业务 ID
# 2. 排除本次抓取到的 key_values得到缺失 ID 集合
# 3. 对每个缺失 ID
# a. 读取最新版本行DISTINCT ON ... ORDER BY fetched_at DESC
# b. 若最新版本已是 is_delete=1跳过
# c. 否则:复制该行,设 is_delete=1重算 content_hashINSERT
# 4. 返回插入的删除版本行数
```
**接口变更:**
- `window_columns` 参数改为 `snapshot_mode` + `snapshot_time_column`
- `full_table` 参数删除(由 snapshot_mode 表达)
- 内部从 UPDATE 改为 SELECT + INSERT
### 6. _insert_records_schema_aware 的适配
- `compare_latest` 判断条件中 `self.SPEC.enable_content_hash_dedup` 改为 `self.SPEC.skip_unchanged`
- `_compute_content_hash` 调用签名变更:传入原始 record作为 payload和 is_delete 值
- 删除对 `include_site_column``include_page_no``include_page_size` 的引用
### 7. BaseOdsTask.execute 的适配
- `snapshot_full_table` / `snapshot_window_columns` 的读取改为 `spec.snapshot_mode` / `spec.snapshot_time_column`
- `_mark_missing_as_deleted` 调用参数适配
- 删除对已移除字段的引用
## 数据模型
### ODS 表结构(不变)
所有 23 个 ODS 表的 DDL 结构不变PK 仍为 `(业务id, content_hash)`
### 新增索引(迁移脚本)
每张含 `fetched_at` 列的 ODS 表新增复合索引:
```sql
-- 迁移脚本db/etl_feiqiu/migrations/YYYY-MM-DD__add_ods_latest_version_indexes.sql
-- 为 DISTINCT ON (id) ORDER BY id, fetched_at DESC 查询模式提供索引支持
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_ods_member_profiles_latest
ON ods.member_profiles (id, fetched_at DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_ods_member_balance_changes_latest
ON ods.member_balance_changes (id, fetched_at DESC);
-- ... 对每张含 fetched_at 的 ODS 表重复此模式
-- 索引命名规范idx_ods_{table_name}_latest
-- 业务主键列名因表而异(大多数是 id少数是 recharge_order_id、sitegoodsstockid 等)
```
**注意:**
- `include_fetched_at=False` 的任务(如 ODS_ASSISTANT_ACCOUNT其表中 fetched_at 列有 DEFAULT now(),实际仍有值,也需要索引。但需确认 DDL 中是否所有表都有 fetched_at 列。
- 索引定义需同步写入 `db/etl_feiqiu/schemas/ods.sql`DDL 源文件),确保新环境初始化时自动创建索引。
- 迁移脚本 `db/etl_feiqiu/migrations/YYYY-MM-DD__add_ods_latest_version_indexes.sql` 用于已有环境的增量部署。
### 下游查询规约
DWD 层从 ODS 取数的标准模式:
```sql
SELECT DISTINCT ON (id) *
FROM ods.{table_name}
WHERE is_delete = 0 -- 或 is_delete IS DISTINCT FROM 1
ORDER BY id, fetched_at DESC;
```
此查询利用新增的 `(id, fetched_at DESC)` 索引,避免全表扫描。
## 正确性属性
*正确性属性是系统在所有合法执行路径上都应保持的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1: SnapshotMode 与 snapshot_time_column 一致性
*For any* OdsTaskSpec 实例,当 snapshot_mode 为 WINDOW 时 snapshot_time_column 必须为非空字符串,当 snapshot_mode 为 FULL_TABLE 或 NONE 时 snapshot_time_column 必须为 None违反此约束应抛出 ValueError。
**Validates: Requirements 2.3, 2.4, 2.5, 2.6**
### Property 2: content_hash 确定性
*For any* 原始 payload合法 JSON 对象)和 is_delete 值0 或 1对相同的 (payload, is_delete) 输入调用 `_compute_content_hash` 应始终产生相同的 SHA-256 哈希值。
**Validates: Requirements 5.1, 5.4**
### Property 3: content_hash 区分性
*For any* 两组不同的 (payload, is_delete) 输入payload 不同或 is_delete 不同),`_compute_content_hash` 应产生不同的哈希值。
**Validates: Requirements 5.5**
### Property 4: skip_unchanged 跳过内容未变的记录
*For any* ODS 任务skip_unchanged=True当一条记录的 content_hash 与数据库中该业务 ID 最新版本的 content_hash 相同时,该记录应被计入 skipped 而非 inserted。
**Validates: Requirements 4.3, 8.5**
### Property 5: 记录数闭合不变量
*For any* 非空记录列表被写入 ODS 时,`fetched == inserted + updated + skipped` 恒成立。
**Validates: Requirements 8.3**
### Property 6: 软删除构造正确性
*For any* 快照对比中发现的缺失业务 ID`_mark_missing_as_deleted` 应读取该 ID 的最新版本行,构造一条 is_delete=1 的新版本行,其 content_hash 基于原始 payload + is_delete=1 重算,并通过 INSERT而非 UPDATE写入。
**Validates: Requirements 7.1, 7.2, 7.4**
### Property 7: 软删除幂等性
*For any* 业务 ID若其最新版本已经是 is_delete=1再次执行 `_mark_missing_as_deleted` 不应插入新的删除版本行。
**Validates: Requirements 7.3, 8.7**
### Property 8: 软删除不修改历史版本
*For any* 软删除操作执行后,数据库中该业务 ID 的所有历史版本行(执行前已存在的行)的内容应保持不变——不应有 UPDATE 语句作用于 ODS 表。
**Validates: Requirements 7.4, 8.6**
## 错误处理
### OdsTaskSpec 校验错误
- `SnapshotMode.WINDOW` + `snapshot_time_column=None``__post_init__` 抛出 `ValueError`
- `SnapshotMode.FULL_TABLE/NONE` + `snapshot_time_column` 不为 None → `__post_init__` 抛出 `ValueError`
- 这些错误在任务注册时(模块加载时)即触发,属于 fail-fast 设计
### hash 算法切换的一次性代价
- 首次运行后所有记录的 content_hash 都会变化,导致全量插入新版本行
- 这是预期行为,不是错误
- 日志中应记录 "hash 算法已变更,本次运行将插入全量新版本" 的提示信息
- 后续运行恢复正常去重
### 软删除的边界情况
- 缺失 ID 在数据库中无任何记录(从未抓取过)→ 跳过,不插入删除版本
- 缺失 ID 的最新版本已是 is_delete=1 → 跳过(幂等性)
- 快照范围内无任何记录且 allow_empty=False → 返回 0不执行任何操作
### 迁移脚本错误
- `CREATE INDEX CONCURRENTLY` 不能在事务块内执行 → 迁移脚本需单独执行
- 索引创建失败不影响数据写入,仅影响查询性能 → 可重试
## 测试策略
### 属性测试hypothesis
使用 `pytest` + `hypothesis` 库,每个属性测试至少运行 100 次迭代。
**测试文件:** `apps/etl/pipelines/feiqiu/tests/unit/test_ods_dedup_properties.py`
| 属性 | 测试方法 | 生成策略 |
|------|---------|---------|
| Property 1 | 生成随机 SnapshotMode + snapshot_time_column 组合,验证校验逻辑 | `st.sampled_from(SnapshotMode)` × `st.one_of(st.none(), st.text(min_size=1))` |
| Property 2 | 生成随机 JSON payload + is_delete验证两次调用结果相同 | `st.dictionaries(st.text(), st.text())` × `st.sampled_from([0, 1])` |
| Property 3 | 生成两组不同的 (payload, is_delete),验证 hash 不同 | 同上,加 `assume(pair1 != pair2)` |
| Property 4 | 用 PkAwareFakeDB 预设最新 hash验证相同记录被跳过 | `_ods_record_with_id` 策略 |
| Property 5 | 生成随机记录列表,验证 fetched == inserted + updated + skipped | `st.lists(_ods_record_with_id)` |
| Property 6 | 用 FakeDB 模拟缺失 ID 场景,验证 INSERT 而非 UPDATE | `st.lists(st.integers())` |
| Property 7 | 预设最新版本 is_delete=1验证不产生新行 | 同上 |
| Property 8 | 执行软删除后检查 FakeDB 中无 UPDATE 语句 | 同上 |
每个测试用注释标注:`# Feature: ods-dedup-standardize, Property N: {title}`
### 单元测试
**测试文件:** 适配现有 `test_ods_tasks.py``test_debug_ods_properties.py`
- 适配 OdsTaskSpec 构造函数变更(删除旧字段,使用新字段)
- 适配 `_compute_content_hash` 签名变更
- 适配 `_mark_missing_as_deleted` 参数变更
- 验证 SnapshotMode 枚举的边界情况edge cases from prework 2.5, 2.6
### 现有测试适配
现有测试中需要适配的关键点:
- `test_debug_ods_properties.py` 中的 Property 4content_hash 确定性)需要适配新的 `_compute_content_hash` 签名
- `test_debug_ods_properties.py` 中的 Property 5快照删除标记需要适配新的 INSERT 语义(检查 INSERT 而非 UPDATE
- `test_ods_tasks.py` 中的所有任务测试需要确保在新的 OdsTaskSpec 下仍能正常运行
### 测试执行命令
```bash
# ETL 单元测试(包含属性测试)
cd apps/etl/pipelines/feiqiu && pytest tests/unit -v
# 仅运行本次改造的属性测试
cd apps/etl/pipelines/feiqiu && pytest tests/unit/test_ods_dedup_properties.py -v
```

View File

@@ -0,0 +1,143 @@
# 需求文档ODS 去重与软删除机制标准化
## 简介
NeoZQYY ETL 系统的 23 个 ODS 任务在去重和软删除机制上存在配置误导、无意义版本膨胀、软删除语义不清等问题。本需求旨在精简 `OdsTaskSpec` 配置、标准化 content_hash 去重策略、优化软删除语义,使 ODS 层真正实现"忠实记录上游数据版本变更"的职责。
## 术语表
- **OdsTaskSpec**`ods_tasks.py` 中定义 ODS 任务配置的 dataclass包含端点、主键、去重开关等字段
- **BaseOdsTask**ODS 任务执行基类,包含写入、去重、软删除等核心逻辑
- **content_hash**:基于记录内容计算的 SHA-256 哈希值,与业务 ID 组成复合主键
- **skip_unchanged**:重命名后的去重开关(原 `enable_content_hash_dedup`),为 True 时跳过内容未变的记录
- **SnapshotMode**新增枚举统一表达软删除快照策略NONE / FULL_TABLE / WINDOW
- **业务主键**ODS 表复合主键中除 content_hash 外的列(通常为 `id`
- **软删除**:将上游已不存在的记录标记为 `is_delete=1`
- **payload**ODS 表中存储的原始 API 响应 JSON
## 需求
### 需求 1删除运行时无效的 conflict_columns_override 字段
**用户故事:** 作为 ETL 开发者,我希望移除不生效的配置字段,以避免对 ODS 写入行为产生误解。
#### 验收标准
1. WHEN `OdsTaskSpec` dataclass 被定义时THE OdsTaskSpec SHALL 不包含 `conflict_columns_override` 字段
2. WHEN `OdsTaskSpec.__post_init__` 执行校验时THE OdsTaskSpec SHALL 不包含任何引用 `conflict_columns_override` 的校验逻辑
3. WHEN 23 个 `ODS_TASK_SPECS` 声明被更新后THE ODS_TASK_SPECS SHALL 不包含任何 `conflict_columns_override` 参数
### 需求 2用 SnapshotMode 枚举替代软删除组合字段
**用户故事:** 作为 ETL 开发者,我希望用单一枚举字段表达快照策略,以消除 `snapshot_full_table``snapshot_window_columns` 的组合歧义。
#### 验收标准
1. THE SnapshotMode 枚举 SHALL 定义三个值NONE、FULL_TABLE、WINDOW
2. WHEN `OdsTaskSpec` dataclass 被定义时THE OdsTaskSpec SHALL 使用 `snapshot_mode: SnapshotMode` 替代 `snapshot_full_table: bool``snapshot_window_columns`
3. WHEN `snapshot_mode` 为 WINDOW 时THE OdsTaskSpec SHALL 要求 `snapshot_time_column` 为非空字符串
4. WHEN `snapshot_mode` 为 FULL_TABLE 或 NONE 时THE OdsTaskSpec SHALL 要求 `snapshot_time_column` 为 None
5. IF `snapshot_mode` 为 WINDOW 且 `snapshot_time_column` 为 NoneTHEN THE OdsTaskSpec SHALL 在 `__post_init__` 中抛出 ValueError
6. IF `snapshot_mode` 为 FULL_TABLE 且 `snapshot_time_column` 不为 NoneTHEN THE OdsTaskSpec SHALL 在 `__post_init__` 中抛出 ValueError
7. WHEN 23 个 `ODS_TASK_SPECS` 声明被迁移后THE ODS_TASK_SPECS SHALL 使用 `snapshot_mode``snapshot_time_column` 替代原有的 `snapshot_full_table``snapshot_window_columns`
### 需求 3删除全局恒定的冗余布尔字段
**用户故事:** 作为 ETL 开发者,我希望移除所有任务中值恒定的配置字段,以减少 OdsTaskSpec 的认知负担。
#### 验收标准
1. WHEN `OdsTaskSpec` dataclass 被定义时THE OdsTaskSpec SHALL 不包含 `include_site_column``include_page_no``include_page_size` 字段
2. WHEN `BaseOdsTask` 执行逻辑中引用上述三个字段时THE BaseOdsTask SHALL 将对应逻辑硬编码为 False 或直接移除
3. WHEN 23 个 `ODS_TASK_SPECS` 声明被更新后THE ODS_TASK_SPECS SHALL 不包含 `include_site_column``include_page_no``include_page_size` 参数
### 需求 4重命名去重开关并默认开启
**用户故事:** 作为 ETL 开发者,我希望去重开关名称更直观且默认开启,以减少无意义的版本膨胀。
#### 验收标准
1. WHEN `OdsTaskSpec` dataclass 被定义时THE OdsTaskSpec SHALL 使用 `skip_unchanged: bool = True` 替代 `enable_content_hash_dedup: bool = False`
2. WHEN `BaseOdsTask._insert_records_schema_aware` 执行去重判断时THE BaseOdsTask SHALL 引用 `self.SPEC.skip_unchanged` 而非 `self.SPEC.enable_content_hash_dedup`
3. WHEN `skip_unchanged` 为 True 且目标表有 content_hash 列和业务主键时THE BaseOdsTask SHALL 跳过 content_hash 与数据库中最新版本相同的记录
### 需求 5改用 payload + is_delete 计算 content_hash
**用户故事:** 作为 ETL 开发者,我希望 content_hash 基于原始 payload 和 is_delete 计算,以获得最干净的语义且不受展平逻辑影响。
#### 验收标准
1. WHEN `_compute_content_hash` 计算哈希时THE BaseOdsTask SHALL 仅基于记录的 payload原始 JSON和 is_delete 字段计算 SHA-256 哈希
2. WHEN `_compute_content_hash` 计算哈希时THE BaseOdsTask SHALL 对 payload 进行 `json.dumps(sort_keys=True, separators=(',',':'), ensure_ascii=False)` 序列化后拼接 is_delete 值再计算哈希
3. WHEN `_sanitize_record_for_hash` 被调用时THE BaseOdsTask SHALL 移除该方法,因为新的 hash 计算不再需要字段排除逻辑
4. THE _compute_content_hash SHALL 对相同的 payload 和 is_delete 组合始终产生相同的哈希值
5. THE _compute_content_hash SHALL 对不同的 payload 或不同的 is_delete 值产生不同的哈希值
### 需求 6为 ODS 表添加"取最新版本"索引
**用户故事:** 作为 ETL 开发者,我希望每张 ODS 表有 `(业务id, fetched_at DESC)` 复合索引,以高效支持 `DISTINCT ON` 取最新版本的查询模式。
#### 验收标准
1. WHEN DDL 迁移脚本执行后THE 数据库 SHALL 为每张含 fetched_at 列的 ODS 表创建 `(业务主键, fetched_at DESC)` 复合索引
2. WHEN 索引创建时THE 迁移脚本 SHALL 使用 `CREATE INDEX IF NOT EXISTS` 以保证幂等性
3. WHEN 索引创建时THE 迁移脚本 SHALL 使用 `CONCURRENTLY` 选项以避免锁表
### 需求 7软删除改为"插入删除版本"
**用户故事:** 作为 ETL 开发者,我希望软删除操作插入一条 `is_delete=1` 的新版本行,而非 UPDATE 所有历史版本,以保持 ODS 追加写入的语义一致性。
**背景:** 软删除有两个触发路径:
- **路径 AAPI 返回)**:上游 API 的 JSON 响应中自带 `is_delete`/`isDelete` 等字段,由 `_normalize_is_delete_flag` 标准化后随记录正常写入。此路径在需求 5is_delete 参与 hash生效后自动产生新版本行无需额外改造。
- **路径 B快照空缺**:通过 `_mark_missing_as_deleted` 在快照范围内FULL_TABLE 模式对比全表WINDOW 模式仅对比时间窗口内的记录)对比本次抓取的业务 ID 集合与数据库已有记录,发现缺失的 ID 需标记为删除。仅当任务配置了 `snapshot_mode` 为 FULL_TABLE 或 WINDOW 时才触发。此路径是本需求的改造重点。
#### 验收标准
1. WHEN `_mark_missing_as_deleted` 检测到某业务 ID 在上游已不存在时(路径 BTHE BaseOdsTask SHALL 读取该业务 ID 的最新版本行内容
2. WHEN 构造删除版本行时THE BaseOdsTask SHALL 将 is_delete 设为 1保留其余字段不变并基于 payload + is_delete=1 重算 content_hash
3. WHEN 删除版本行的 content_hash 与该业务 ID 最新版本的 content_hash 相同时THE BaseOdsTask SHALL 跳过插入(该记录已经是删除状态)
4. WHEN 删除版本行被插入后THE BaseOdsTask SHALL 不修改该业务 ID 的任何历史版本行
5. WHEN 上游 API 返回的记录中 is_delete=1 时(路径 ATHE BaseOdsTask SHALL 通过正常写入流程插入新版本行is_delete 参与 hash 计算hash 变化即为新版本)
6. WHEN 下游查询 ODS 数据时THE 查询规约 SHALL 先按业务 ID 取 `fetched_at DESC` 最新版本,再过滤 `is_delete = 0`
### 需求 8回归验证策略
**用户故事:** 作为 ETL 开发者,我希望有完善的回归测试覆盖本次改造的所有核心逻辑变更,以确保 23 个 ODS 任务在改造后行为正确。
**挑战:** 本次改造涉及 OdsTaskSpec 字段重构、hash 算法变更、软删除语义变更,影响所有 23 个任务的写入和删除路径。需要分层验证:
- 单元级OdsTaskSpec 校验逻辑、SnapshotMode 枚举约束、hash 计算纯函数
- 行为级skip_unchanged 去重、软删除插入版本、记录数闭合
- 属性级:用 hypothesis 对核心不变量进行随机化验证
#### 验收标准
1. WHEN OdsTaskSpec 使用 SnapshotMode.WINDOW 且 snapshot_time_column 为 None 时THE 单元测试 SHALL 验证 __post_init__ 抛出 ValueError
2. WHEN OdsTaskSpec 使用 SnapshotMode.FULL_TABLE 且 snapshot_time_column 不为 None 时THE 单元测试 SHALL 验证 __post_init__ 抛出 ValueError
3. WHEN 任意非空记录列表被写入 ODS 时THE 属性测试 SHALL 验证 fetched == inserted + updated + skipped记录数闭合不变量
4. WHEN 同一条记录的 payload 和 is_delete 不变时THE 属性测试 SHALL 验证 _compute_content_hash 产生相同的哈希值
5. WHEN skip_unchanged=True 且记录内容未变时THE 属性测试 SHALL 验证该记录被跳过而非重复插入
6. WHEN 快照对比发现缺失 ID 时THE 属性测试 SHALL 验证生成的是 INSERT 语句(而非 UPDATE且历史版本行不被修改
7. WHEN 缺失 ID 的最新版本已经是 is_delete=1 时THE 属性测试 SHALL 验证不会重复插入删除版本
8. WHEN 现有的 test_ods_tasks.py 和 test_debug_ods_properties.py 中的测试用例被适配到新接口后THE 测试套件 SHALL 全部通过
### 需求 9同步更新所有相关文档
**用户故事:** 作为 ETL 开发者,我希望所有涉及 ODS 去重、软删除、OdsTaskSpec 配置的文档与代码变更保持同步,以确保文档准确反映当前实现。
**涉及文档清单:**
- `apps/etl/pipelines/feiqiu/docs/etl_tasks/ods_task_params_matrix.md` — 任务参数矩阵
- `apps/etl/pipelines/feiqiu/docs/etl_tasks/ods_tasks.md` — ODS 任务说明
- `apps/etl/pipelines/feiqiu/docs/etl_tasks/base_task_mechanism.md` — 基础任务机制
- `apps/etl/pipelines/feiqiu/docs/architecture/ods_taskspec_refactor_proposal.md` — OdsTaskSpec 重构提案
- `apps/etl/pipelines/feiqiu/docs/database/ODS/` — ODS 数据库文档目录
- `apps/etl/pipelines/feiqiu/docs/database/overview/ods_tables_dictionary.md` — ODS 表字典
- `docs/database/etl_feiqiu_schema_migration.md` — 项目级 schema 迁移文档
#### 验收标准
1. WHEN OdsTaskSpec 字段发生变更后THE ods_task_params_matrix.md SHALL 反映新的字段名称和默认值skip_unchanged、snapshot_mode、snapshot_time_column并移除已删除字段的列
2. WHEN OdsTaskSpec 字段发生变更后THE ods_task_params_matrix.md SHALL 包含所有 23 个任务的完整参数矩阵
3. WHEN 去重和软删除机制发生变更后THE ods_tasks.md 和 base_task_mechanism.md SHALL 更新对应的机制说明
4. WHEN DDL 迁移脚本添加索引后THE ODS 数据库文档和 ods_tables_dictionary.md SHALL 记录新增索引
5. WHEN DDL 迁移脚本添加索引后THE docs/database/etl_feiqiu_schema_migration.md SHALL 记录本次迁移变更
6. WHEN 所有文档更新完成后THE 文档 SHALL 逐个文件检查并确保内容与代码实现一致

View File

@@ -0,0 +1,141 @@
# 实现计划ODS 去重与软删除机制标准化
## 概述
按方案 1→2→3→4 的顺序递进实现,每个方案完成后有检查点。核心改动集中在 `ods_tasks.py`,辅以 DDL 迁移和文档同步。
## 任务
- [x] 1. 方案 1清理 OdsTaskSpec 无效/冗余配置
- [x] 1.1 添加 SnapshotMode 枚举,重构 OdsTaskSpec dataclass
-`ods_tasks.py` 顶部定义 `SnapshotMode` 枚举NONE / FULL_TABLE / WINDOW
- 从 OdsTaskSpec 中删除 `conflict_columns_override``include_site_column``include_page_no``include_page_size``snapshot_full_table``snapshot_window_columns``enable_content_hash_dedup`
- 添加 `skip_unchanged: bool = True``snapshot_mode: SnapshotMode = SnapshotMode.NONE``snapshot_time_column: str | None = None`
- 重写 `__post_init__` 校验逻辑WINDOW 必须有 snapshot_time_columnFULL_TABLE/NONE 不能有
- _Requirements: 1.1, 1.2, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 3.1, 4.1_
- [x] 1.2 迁移 23 个 ODS_TASK_SPECS 声明到新字段
- 按设计文档中的映射表,将每个任务的 snapshot_full_table/snapshot_window_columns 转换为 snapshot_mode/snapshot_time_column
- 删除所有任务中的 conflict_columns_override、include_site_column、include_page_no、include_page_size
- 将 ODS_RECHARGE_SETTLE 的 enable_content_hash_dedup=True 改为 skip_unchanged=True其余任务使用默认值 True
- _Requirements: 1.3, 2.7, 3.3_
- [x] 1.3 适配 BaseOdsTask.execute 和相关方法中对旧字段的引用
- `execute` 方法中 `snapshot_full_table` / `snapshot_window_columns` 改为读取 `spec.snapshot_mode` / `spec.snapshot_time_column`
- `_mark_missing_as_deleted` 参数签名适配(暂保持 UPDATE 语义,方案 4 再改)
- 删除 BaseOdsTask 中对 `include_site_column``include_page_no``include_page_size` 的引用
- `_insert_records_schema_aware``enable_content_hash_dedup` 改为 `skip_unchanged`
- _Requirements: 1.2, 3.2, 4.2_
- [x] 1.4 编写 SnapshotMode 校验属性测试
- **Property 1: SnapshotMode 与 snapshot_time_column 一致性**
- **Validates: Requirements 2.3, 2.4, 2.5, 2.6**
- [x] 2. 检查点 - 方案 1 完成
- 确保所有测试通过ask the user if questions arise.
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit -v`
- 验证现有 test_ods_tasks.py 和 test_debug_ods_properties.py 适配后通过
- [x] 3. 方案 2默认开启 skip_unchanged + hash 算法改为 payload + is_delete
- [x] 3.1 重写 _compute_content_hash删除 _sanitize_record_for_hash
- 新签名:`_compute_content_hash(cls, record: dict, payload: Any, is_delete: int) -> str`
- 基于 `json.dumps(payload, sort_keys=True, separators=(',',':'), ensure_ascii=False)` + `|` + `is_delete` 计算 SHA-256
- 删除 `_sanitize_record_for_hash` 方法
- 删除 `include_fetched_at` 参数
- _Requirements: 5.1, 5.2, 5.3_
- [x] 3.2 适配 _insert_records_schema_aware 中的 hash 计算调用
-`_compute_content_hash(merged_rec, include_fetched_at=False)` 改为 `_compute_content_hash(merged_rec, payload=rec, is_delete=merged_rec.get("is_delete", 0))`
- 其中 `rec` 是原始 API 返回的记录(未展平),`merged_rec` 中的 is_delete 已被 `_normalize_is_delete_flag` 标准化
- _Requirements: 5.1, 5.2_
- [x] 3.3 编写 content_hash 确定性和区分性属性测试
- **Property 2: content_hash 确定性**
- **Validates: Requirements 5.1, 5.4**
- **Property 3: content_hash 区分性**
- **Validates: Requirements 5.5**
- [x] 3.4 编写 skip_unchanged 和记录数闭合属性测试
- **Property 4: skip_unchanged 跳过内容未变的记录**
- **Validates: Requirements 4.3, 8.5**
- **Property 5: 记录数闭合不变量**
- **Validates: Requirements 8.3**
- [x] 4. 检查点 - 方案 2 完成
- 确保所有测试通过ask the user if questions arise.
- 适配 test_debug_ods_properties.py 中 Property 4content_hash 确定性)到新签名
- [x] 5. 方案 3DDL 迁移 - 添加"取最新版本"索引
- [x] 5.1 创建迁移脚本并更新 DDL 源文件
- 创建 `db/etl_feiqiu/migrations/YYYY-MM-DD__add_ods_latest_version_indexes.sql`
- 为每张含 fetched_at 列的 ODS 表创建 `(业务主键, fetched_at DESC)` 复合索引
- 使用 `CREATE INDEX CONCURRENTLY IF NOT EXISTS`
- 索引命名:`idx_ods_{table_name}_latest`
- 同步更新 `db/etl_feiqiu/schemas/ods.sql` 中的索引定义
- _Requirements: 6.1, 6.2, 6.3_
- [x] 6. 方案 4软删除改为"插入删除版本"
- [x] 6.1 重写 _mark_missing_as_deleted 方法
- 接口变更:`window_columns`/`full_table` 参数改为 `snapshot_mode`/`snapshot_time_column`
- 查询快照范围内 is_delete != 1 的业务 ID排除本次抓取到的 key_values
- 对每个缺失 ID读取最新版本行DISTINCT ON + ORDER BY fetched_at DESC
- 若最新版本已是 is_delete=1 → 跳过
- 否则:复制该行,设 is_delete=1重算 content_hashINSERT 新行
- _Requirements: 7.1, 7.2, 7.3, 7.4_
- [x] 6.2 适配 BaseOdsTask.execute 中的 _mark_missing_as_deleted 调用
- 传入 snapshot_mode 和 snapshot_time_column 替代旧参数
- 更新 deleted 计数逻辑(从 UPDATE rowcount 改为 INSERT count
- _Requirements: 7.1_
- [x] 6.3 编写软删除属性测试
- **Property 6: 软删除构造正确性**
- **Validates: Requirements 7.1, 7.2, 7.4**
- **Property 7: 软删除幂等性**
- **Validates: Requirements 7.3, 8.7**
- **Property 8: 软删除不修改历史版本**
- **Validates: Requirements 7.4, 8.6**
- [x] 7. 检查点 - 方案 4 完成
- 确保所有测试通过ask the user if questions arise.
- 适配 test_debug_ods_properties.py 中 Property 5快照删除标记到新的 INSERT 语义
- 运行完整测试套件:`cd apps/etl/pipelines/feiqiu && pytest tests/unit -v`
- [x] 8. 文档同步
- [x] 8.1 更新 ods_task_params_matrix.md
- 反映新字段skip_unchanged、snapshot_mode、snapshot_time_column
- 移除已删除字段列
- 确保 23 个任务的完整参数矩阵
- _Requirements: 9.1, 9.2_
- [x] 8.2 更新 ods_tasks.md 和 base_task_mechanism.md
- 更新去重机制说明skip_unchanged 默认开启、hash 基于 payload + is_delete
- 更新软删除机制说明INSERT 删除版本行、双路径覆盖)
- _Requirements: 9.3_
- [x] 8.3 更新 ODS 数据库文档和 ods_tables_dictionary.md
- 记录新增的 `(业务主键, fetched_at DESC)` 索引
- 更新下游取数规约说明
- _Requirements: 9.4_
- [x] 8.4 更新 docs/database/etl_feiqiu_schema_migration.md
- 记录本次迁移变更(索引添加)
- _Requirements: 9.5_
- [x] 8.5 更新 ods_taskspec_refactor_proposal.md
- 标记本次改造已完成的方案1-4
- 记录方案 5冷数据归档为中长期待办
- _Requirements: 9.6_
- [x] 9. 最终检查点
- 确保所有测试通过ask the user if questions arise.
- 运行 `cd apps/etl/pipelines/feiqiu && pytest tests/unit -v`
- 运行 `cd C:\NeoZQYY && pytest tests/ -v`monorepo 属性测试)
## 备注
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯
- 检查点确保增量验证
- 属性测试验证通用正确性属性,单元测试验证具体示例和边界情况
- 本次改造涉及高风险路径tasks/),完成后需触发 `/audit`

View File

@@ -1,424 +0,0 @@
# 设计文档:仓库治理只读审计
## 概述
本设计描述三个 Python 审计脚本的实现方案,用于对 etl-billiards 仓库进行只读分析并生成三份 Markdown 报告。脚本仅读取文件系统和源代码,不连接数据库、不修改任何现有文件,仅在 `docs/audit/repo/` 目录下输出报告。
审计脚本采用模块化设计:一个共享的仓库扫描器负责遍历文件系统,三个独立的分析器分别生成文件清单、流程树和文档对齐报告。
## 架构
```mermaid
graph TD
A[scripts/audit/run_audit.py<br/>审计主入口] --> B[scripts/audit/scanner.py<br/>仓库扫描器]
A --> C[scripts/audit/inventory_analyzer.py<br/>文件清单分析器]
A --> D[scripts/audit/flow_analyzer.py<br/>流程树分析器]
A --> E[scripts/audit/doc_alignment_analyzer.py<br/>文档对齐分析器]
B --> F[文件系统<br/>只读遍历]
C --> G[docs/audit/repo/file_inventory.md]
D --> H[docs/audit/repo/flow_tree.md]
E --> I[docs/audit/repo/doc_alignment.md]
C --> B
D --> B
E --> B
```
### 执行流程
1. `run_audit.py` 作为主入口,初始化扫描器并依次调用三个分析器
2. `scanner.py` 递归遍历仓库,构建文件元信息列表(路径、大小、类型)
3. 各分析器接收扫描结果,执行各自的分析逻辑,输出 Markdown 报告
4. 所有报告写入 `docs/audit/repo/` 目录
## 组件与接口
### 1. 仓库扫描器 (`scripts/audit/scanner.py`)
负责递归遍历仓库文件系统,返回结构化的文件元信息。
```python
@dataclass
class FileEntry:
"""单个文件/目录的元信息"""
rel_path: str # 相对于仓库根目录的路径
is_dir: bool # 是否为目录
size_bytes: int # 文件大小(目录为 0
extension: str # 文件扩展名(小写,含点号)
is_empty_dir: bool # 是否为空目录
EXCLUDED_PATTERNS: list[str] = [
".git", "__pycache__", ".pytest_cache",
"*.pyc", ".kiro",
]
def scan_repo(root: Path, exclude: list[str] = EXCLUDED_PATTERNS) -> list[FileEntry]:
"""递归扫描仓库,返回所有文件和目录的元信息列表"""
...
```
### 2. 文件清单分析器 (`scripts/audit/inventory_analyzer.py`)
对扫描结果进行用途分类和处置标签分配。
```python
# 用途分类枚举
class Category(str, Enum):
CORE_CODE = "核心代码"
CONFIG = "配置"
DATABASE_DEF = "数据库定义"
TEST = "测试"
DOCS = "文档"
SCRIPTS = "脚本工具"
GUI = "GUI"
BUILD_DEPLOY = "构建与部署"
LOG_OUTPUT = "日志与输出"
TEMP_DEBUG = "临时与调试"
OTHER = "其他"
# 处置标签枚举
class Disposition(str, Enum):
KEEP = "保留"
CANDIDATE_DELETE = "候选删除"
CANDIDATE_ARCHIVE = "候选归档"
NEEDS_REVIEW = "待确认"
@dataclass
class InventoryItem:
"""清单条目"""
rel_path: str
category: Category
disposition: Disposition
description: str
def classify(entry: FileEntry) -> InventoryItem:
"""根据路径、扩展名等规则对单个文件/目录进行分类和标签分配"""
...
def build_inventory(entries: list[FileEntry]) -> list[InventoryItem]:
"""批量分类所有文件条目"""
...
def render_inventory_report(items: list[InventoryItem], repo_root: str) -> str:
"""生成 Markdown 格式的文件清单报告"""
...
```
**分类规则(按优先级从高到低)**
| 路径模式 | 用途分类 | 默认处置 |
|---------|---------|---------|
| `tmp/` 下所有文件 | 临时与调试 | 候选删除/候选归档 |
| `logs/``export/` 下的运行时产出 | 日志与输出 | 候选归档 |
| `*.lnk``*.rar` 文件 | 其他 | 候选删除 |
| 空目录(如 `Deleded & backup/` | 其他 | 候选删除 |
| `tasks/``loaders/``scd/``orchestration/``quality/``models/``utils/``api/` | 核心代码 | 保留 |
| `config/` | 配置 | 保留 |
| `database/*.sql``database/migrations/` | 数据库定义 | 保留 |
| `database/*.py` | 核心代码 | 保留 |
| `tests/` | 测试 | 保留 |
| `docs/` | 文档 | 保留 |
| `scripts/` 下的 `.py` 文件 | 脚本工具 | 保留/待确认 |
| `gui/` | GUI | 保留 |
| `setup.py``build_exe.py``*.bat``*.sh``*.ps1` | 构建与部署 | 保留 |
| 根目录散落文件(`Prompt用.md``Untitled``fix_symbols.py` 等) | 其他 | 待确认 |
### 3. 流程树分析器 (`scripts/audit/flow_analyzer.py`)
通过静态分析 Python 源码的 `import` 语句和类继承关系,构建从入口到末端模块的调用树。
```python
@dataclass
class FlowNode:
"""流程树节点"""
name: str # 节点名称(模块名/类名/函数名)
source_file: str # 所在源文件路径
node_type: str # 类型entry/module/class/function
children: list["FlowNode"]
def parse_imports(filepath: Path) -> list[str]:
"""使用 ast 模块解析 Python 文件的 import 语句,返回被导入的本地模块列表"""
...
def build_flow_tree(repo_root: Path, entry_file: str) -> FlowNode:
"""从指定入口文件出发,递归追踪 import 链,构建流程树"""
...
def find_orphan_modules(repo_root: Path, all_entries: list[FileEntry], reachable: set[str]) -> list[str]:
"""找出未被任何入口直接或间接引用的 Python 模块"""
...
def render_flow_report(trees: list[FlowNode], orphans: list[str], repo_root: str) -> str:
"""生成 Markdown 格式的流程树报告(含 Mermaid 图和缩进文本)"""
...
```
**入口点识别**
- CLI 入口:`cli/main.py``main()` 函数
- GUI 入口:`gui/main.py``main()` 函数
- 批处理入口:`run_etl.bat``run_gui.bat``run_ods.bat` → 解析其中的 `python` 命令
- 运维脚本:`scripts/*.py` → 各自的 `if __name__ == "__main__"`
**静态分析策略**
- 使用 Python `ast` 模块解析源文件,提取 `import``from ... import` 语句
- 仅追踪项目内部模块(排除标准库和第三方包)
- 通过 `orchestration/task_registry.py` 的注册语句识别所有任务类及其源文件
- 通过类继承关系(`BaseTask``BaseLoader``BaseDwsTask` 等)识别任务和加载器层级
### 4. 文档对齐分析器 (`scripts/audit/doc_alignment_analyzer.py`)
检查文档与代码之间的映射关系、过期点、冲突点和缺失点。
```python
@dataclass
class DocMapping:
"""文档与代码的映射关系"""
doc_path: str # 文档文件路径
doc_topic: str # 文档主题
related_code: list[str] # 关联的代码文件/模块
status: str # 状态aligned/stale/conflict/orphan
@dataclass
class AlignmentIssue:
"""对齐问题"""
doc_path: str
issue_type: str # stale/conflict/missing
description: str
related_code: str
def scan_docs(repo_root: Path) -> list[str]:
"""扫描所有文档文件路径"""
...
def extract_code_references(doc_path: Path) -> list[str]:
"""从文档中提取代码引用(文件路径、类名、函数名、表名等)"""
...
def check_reference_validity(ref: str, repo_root: Path) -> bool:
"""检查文档中的代码引用是否仍然有效"""
...
def find_undocumented_modules(repo_root: Path, documented: set[str]) -> list[str]:
"""找出缺少文档的核心代码模块"""
...
def check_ddl_vs_dictionary(repo_root: Path) -> list[AlignmentIssue]:
"""比对 DDL 文件与数据字典文档的覆盖度"""
...
def check_api_samples_vs_parsers(repo_root: Path) -> list[AlignmentIssue]:
"""比对 API 响应样本与 ODS 表结构/解析器的一致性"""
...
def render_alignment_report(mappings: list[DocMapping], issues: list[AlignmentIssue], repo_root: str) -> str:
"""生成 Markdown 格式的文档对齐报告"""
...
```
**文档来源识别**
- `docs/` 目录下的 `.md``.txt``.csv` 文件
- 根目录的 `README.md`
- `开发笔记/` 目录
- 各模块内的 `README.md``gui/README.md``fetch-test/README.md`
- `.kiro/steering/` 下的引导文件
- `docs/test-json-doc/` 下的 API 响应样本及分析文档
**对齐检查策略**
- 过期点检测:文档中引用的文件路径、类名、函数名在代码中已不存在
- 冲突点检测DDL 中的表/字段定义与数据字典文档不一致API 样本字段与解析器不匹配
- 缺失点检测:核心代码模块(`tasks/``loaders/``orchestration/` 等)缺少对应文档
### 5. 审计主入口 (`scripts/audit/run_audit.py`)
```python
def run_audit(repo_root: Path | None = None) -> None:
"""执行完整审计流程,生成三份报告到 docs/audit/"""
...
if __name__ == "__main__":
run_audit()
```
## 数据模型
### FileEntry文件元信息
| 字段 | 类型 | 说明 |
|------|------|------|
| `rel_path` | `str` | 相对路径 |
| `is_dir` | `bool` | 是否为目录 |
| `size_bytes` | `int` | 文件大小 |
| `extension` | `str` | 扩展名 |
| `is_empty_dir` | `bool` | 是否为空目录 |
### InventoryItem清单条目
| 字段 | 类型 | 说明 |
|------|------|------|
| `rel_path` | `str` | 相对路径 |
| `category` | `Category` | 用途分类 |
| `disposition` | `Disposition` | 处置标签 |
| `description` | `str` | 简要说明 |
### FlowNode流程树节点
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | `str` | 节点名称 |
| `source_file` | `str` | 源文件路径 |
| `node_type` | `str` | 节点类型 |
| `children` | `list[FlowNode]` | 子节点列表 |
### DocMapping / AlignmentIssue文档对齐
| 字段 | 类型 | 说明 |
|------|------|------|
| `doc_path` | `str` | 文档路径 |
| `doc_topic` / `issue_type` | `str` | 主题/问题类型 |
| `related_code` | `list[str]` / `str` | 关联代码 |
| `status` / `description` | `str` | 状态/描述 |
## 正确性属性
*属性Property是指在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是连接人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: classify 完整性
*对于任意* `FileEntry``classify` 函数返回的 `InventoryItem``category` 字段应属于 `Category` 枚举,`disposition` 字段应属于 `Disposition` 枚举,且 `description` 字段为非空字符串。
**Validates: Requirements 1.2, 1.3**
### Property 2: 清单渲染完整性
*对于任意* `InventoryItem` 列表,`render_inventory_report` 生成的 Markdown 文本中,每个条目对应的行应包含该条目的 `rel_path``category.value``disposition.value``description` 四个字段。
**Validates: Requirements 1.4**
### Property 3: 空目录标记为候选删除
*对于任意* `is_empty_dir=True``FileEntry``classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_DELETE`
**Validates: Requirements 1.5**
### Property 4: .lnk/.rar 文件标记为候选删除
*对于任意* 扩展名为 `.lnk``.rar``FileEntry``classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_DELETE`
**Validates: Requirements 1.6**
### Property 5: tmp/ 下文件处置范围
*对于任意* `rel_path``tmp/` 开头的 `FileEntry``classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_DELETE``Disposition.CANDIDATE_ARCHIVE` 之一。
**Validates: Requirements 1.7**
### Property 6: 运行时产出目录标记为候选归档
*对于任意* `rel_path``logs/``export/` 开头且非 `__init__.py``FileEntry``classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_ARCHIVE`
**Validates: Requirements 1.8**
### Property 7: 扫描器排除规则
*对于任意* 文件树,`scan_repo` 返回的 `FileEntry` 列表中不应包含 `rel_path` 匹配排除模式(`.git``__pycache__``.pytest_cache`)的条目。
**Validates: Requirements 1.1**
### Property 8: 清单按分类分组
*对于任意* `InventoryItem` 列表,`render_inventory_report` 生成的 Markdown 中,同一 `Category` 的条目应连续出现(即按分类分组排列)。
**Validates: Requirements 1.10**
### Property 9: 流程树节点 source_file 有效性
*对于任意* `FlowNode` 树中的节点,`source_file` 字段应为非空字符串,且对应的文件在仓库中实际存在。
**Validates: Requirements 2.7**
### Property 10: 孤立模块检测正确性
*对于任意* 文件集合和可达模块集合,`find_orphan_modules` 返回的孤立模块列表中的每个模块都不应出现在可达集合中,且可达集合中的每个模块都不应出现在孤立列表中。
**Validates: Requirements 2.8**
### Property 11: 过期引用检测
*对于任意* 文档中提取的代码引用,若该引用指向的文件路径在仓库中不存在,则 `check_reference_validity` 应返回 `False`
**Validates: Requirements 3.3**
### Property 12: 缺失文档检测
*对于任意* 核心代码模块集合和已文档化模块集合,`find_undocumented_modules` 返回的缺失列表应恰好等于核心模块集合与已文档化集合的差集。
**Validates: Requirements 3.5**
### Property 13: 统计摘要一致性
*对于任意* 报告的统计摘要,各分类/标签的计数之和应等于对应条目列表的总长度。
**Validates: Requirements 4.5, 4.6, 4.7**
### Property 14: 报告头部元信息
*对于任意* 报告输出,头部应包含一个符合 ISO 格式的时间戳字符串和仓库根目录路径字符串。
**Validates: Requirements 4.2**
### Property 15: 写操作仅限 docs/audit/
*对于任意* 审计执行过程,所有文件写操作的目标路径应以 `docs/audit/repo/` 为前缀。
**Validates: Requirements 5.2**
### Property 16: 文档对齐报告分区完整性
*对于任意* `render_alignment_report` 的输出Markdown 文本应包含"映射关系"、"过期点"、"冲突点"、"缺失点"四个分区标题。
**Validates: Requirements 3.8**
## 错误处理
| 场景 | 处理方式 |
|------|---------|
| 文件读取权限不足 | 记录警告到报告的"错误"分区,跳过该文件,继续处理 |
| Python 源文件语法错误(`ast.parse` 失败) | 记录警告,将该文件标记为"待确认",不中断流程树构建 |
| 文档中的代码引用格式无法解析 | 跳过该引用,不产生误报 |
| DDL 文件 SQL 语法不规范 | 使用正则提取 `CREATE TABLE` 和列定义,容忍非标准语法 |
| `docs/audit/repo/` 目录创建失败 | 抛出异常并终止,因为无法输出报告 |
| 编码问题(非 UTF-8 文件) | 尝试 `utf-8``gbk``latin-1` 回退读取,记录编码警告 |
## 测试策略
### 测试框架
- 单元测试与属性测试均使用 `pytest`
- 属性测试库:`hypothesis`Python 生态最成熟的属性测试框架)
- 测试文件位于 `tests/unit/test_audit_*.py`
### 单元测试
针对具体示例和边界情况:
- 扫描器对实际仓库子集的遍历结果
- classify 对已知文件路径的分类正确性(如 `tmp/hebing.py` → 临时与调试/候选删除)
- 入口点识别对实际仓库的结果
- DDL 与数据字典的比对结果
- 文件读取失败时的容错行为
- `docs/audit/repo/` 目录不存在时的自动创建
### 属性测试
每个正确性属性对应一个属性测试,使用 `hypothesis` 生成随机输入:
- 每个属性测试至少运行 100 次迭代
- 每个测试用注释标注对应的设计属性编号
- 标注格式:**Feature: repo-audit, Property {N}: {属性标题}**
**生成器策略**
- `FileEntry` 生成器:随机路径(含各种扩展名、目录层级)、随机大小、随机 is_dir/is_empty_dir
- `InventoryItem` 生成器:随机 Category/Disposition 组合、随机描述文本
- `FlowNode` 生成器:随机树结构(限制深度和宽度)
- 文件树生成器:构造临时目录结构用于扫描器测试

View File

@@ -1,90 +0,0 @@
# 需求文档:仓库治理只读审计
## 简介
对飞球 ETL 系统 (etl-billiards) 仓库进行全面的只读审计分析,产出三份结构化报告:文件/目录清单(含处置建议)、项目流程树(从入口到末端逻辑)、文档对齐报告(文档与代码的映射关系)。本阶段不修改任何文件,所有处置决策留待用户逐一确认后再执行。
## 术语表
- **审计脚本 (Audit_Script)**:执行只读分析并生成报告的 Python 脚本集合
- **文件清单 (File_Inventory)**:按用途归类的仓库文件与目录列表,每项附带处置标签
- **处置标签 (Disposition_Tag)**:对文件/目录的处置建议,取值为:保留、候选删除、候选归档、待确认
- **流程树 (Flow_Tree)**:从程序入口出发,沿调用链展开到各子模块/子逻辑的树状结构
- **文档对齐报告 (Doc_Alignment_Report)**:文档与代码之间映射关系的分析报告,包含过期点、冲突点、缺失点
- **入口 (Entry_Point)**:程序的顶层启动点,如 `cli/main.py``gui/main.py``scripts/*.py`
- **ODS/DWD/DWS**:数据仓库三层架构——操作数据存储层/明细数据层/数据服务层
- **SCD2**:缓慢变化维度类型 2维度表的历史版本管理策略
## 需求
### 需求 1文件与目录清单生成
**用户故事:** 作为项目维护者,我希望获得一份按用途归类的仓库文件与目录清单,以便了解每个文件的角色并决定其去留。
#### 验收标准
1. WHEN 审计脚本扫描仓库根目录时THE Audit_Script SHALL 递归遍历所有文件和目录(排除 `.git/``__pycache__/``.pytest_cache/` 等运行时缓存目录)
2. WHEN 审计脚本处理每个文件或目录时THE Audit_Script SHALL 将其归入以下用途分类之一核心代码、配置、数据库定义、测试、文档、脚本工具、GUI、构建与部署、日志与输出、临时与调试、其他
3. WHEN 审计脚本完成归类后THE Audit_Script SHALL 为每个条目分配一个处置标签(保留/候选删除/候选归档/待确认)
4. WHEN 审计脚本生成清单时THE File_Inventory SHALL 包含以下字段:相对路径、用途分类、处置标签、简要说明
5. WHEN 审计脚本遇到空目录(如 `database/Deleded & backup/``scripts/Deleded & backup/`THE Audit_Script SHALL 将其标记为"候选删除"
6. WHEN 审计脚本遇到 `.lnk` 快捷方式文件或 `.rar` 压缩包时THE Audit_Script SHALL 将其标记为"候选删除"
7. WHEN 审计脚本遇到 `tmp/` 目录下的文件时THE Audit_Script SHALL 逐一评估并标记为"候选删除"或"候选归档"
8. WHEN 审计脚本遇到 `logs/``export/` 目录下的运行时产出文件时THE Audit_Script SHALL 将其标记为"候选归档"
9. IF 审计脚本无法确定某文件的用途分类THEN THE Audit_Script SHALL 将其标记为"待确认"并在说明中注明原因
10. WHEN 审计脚本完成清单生成后THE File_Inventory SHALL 以 Markdown 表格格式输出,按用途分类分组排列
### 需求 2项目流程树生成
**用户故事:** 作为项目维护者,我希望获得一份从入口到各子模块的调用流程树,以便理解系统的执行路径和模块依赖关系。
#### 验收标准
1. WHEN 审计脚本分析项目入口时THE Audit_Script SHALL 识别以下入口点:`cli/main.py`CLI 主入口)、`gui/main.py`GUI 主入口)、`scripts/*.py`(运维脚本)、批处理文件(`run_etl.bat``run_gui.bat``run_ods.bat` 等)
2. WHEN 审计脚本从 CLI 入口展开时THE Flow_Tree SHALL 追踪以下调用链CLI 参数解析 → 配置加载 → 调度器初始化 → 任务注册表查询 → 任务执行Extract → Transform → Load→ 加载器调用 → 数据库操作
3. WHEN 审计脚本从 GUI 入口展开时THE Flow_Tree SHALL 追踪以下调用链GUI 主窗口初始化 → 各面板/组件加载 → 后台工作线程 → CLI 命令构建 → 任务执行
4. WHEN 审计脚本分析任务模块时THE Flow_Tree SHALL 区分以下任务类型ODS 抓取任务、DWD 加载任务、DWS 汇总任务、校验任务、Schema 初始化任务
5. WHEN 审计脚本分析加载器模块时THE Flow_Tree SHALL 区分以下加载器类型ODS 通用加载器、维度加载器SCD2、事实表加载器
6. WHEN 审计脚本生成流程树时THE Flow_Tree SHALL 以缩进文本或 Mermaid 图的形式输出,层级深度至少达到函数/方法级别
7. WHEN 审计脚本分析模块依赖时THE Flow_Tree SHALL 标注每个节点所在的源文件路径
8. IF 审计脚本发现存在孤立模块未被任何入口直接或间接引用的代码文件THEN THE Flow_Tree SHALL 在报告末尾单独列出这些孤立模块
### 需求 3文档对齐报告生成
**用户故事:** 作为项目维护者,我希望了解现有文档与代码之间的对齐状况,以便识别过期、冲突和缺失的文档。
#### 验收标准
1. WHEN 审计脚本扫描文档目录时THE Audit_Script SHALL 识别以下文档来源:`docs/` 目录、`README.md``开发笔记/`、各模块内的 `README.md`(如 `gui/README.md``fetch-test/README.md`)、`.kiro/steering/` 下的引导文件
2. WHEN 审计脚本分析每份文档时THE Doc_Alignment_Report SHALL 建立文档与代码模块之间的映射关系
3. WHEN 审计脚本检测到文档引用了已不存在的代码实体函数、类、文件路径THE Doc_Alignment_Report SHALL 将该引用标记为"过期点"
4. WHEN 审计脚本检测到文档描述与代码实际行为不一致时THE Doc_Alignment_Report SHALL 将该处标记为"冲突点"
5. WHEN 审计脚本检测到核心代码模块缺少对应文档时THE Doc_Alignment_Report SHALL 将该模块标记为"缺失点"
6. WHEN 审计脚本分析 DDL 文件(`database/schema_*.sql`THE Doc_Alignment_Report SHALL 检查数据字典文档(`docs/dwd_main_tables_dictionary.md``docs/dws_tables_dictionary.md`)是否覆盖了所有表和字段
7. WHEN 审计脚本分析 `docs/test-json-doc/` 下的 API 响应样本时THE Doc_Alignment_Report SHALL 检查样本字段是否与 ODS 表结构和解析器(`models/parsers.py`)一致
8. WHEN 审计脚本完成分析后THE Doc_Alignment_Report SHALL 以 Markdown 格式输出,包含以下分区:映射关系表、过期点列表、冲突点列表、缺失点列表
### 需求 4报告输出与格式规范
**用户故事:** 作为项目维护者,我希望审计报告以统一、可读的格式输出,以便后续逐项决策和执行。
#### 验收标准
1. THE Audit_Script SHALL 将三份报告输出到 `docs/audit/repo/` 目录下,文件名分别为 `file_inventory.md``flow_tree.md``doc_alignment.md`
2. THE Audit_Script SHALL 在每份报告的头部包含生成时间戳和仓库根目录路径
3. WHEN 报告引用代码标识符类名、函数名、变量名、文件路径THE Audit_Script SHALL 保留英文原文,使用行内代码格式(反引号)
4. WHEN 报告包含说明性文字时THE Audit_Script SHALL 使用简体中文
5. THE Audit_Script SHALL 在文件清单报告末尾附加统计摘要:各用途分类的文件数量、各处置标签的文件数量
6. THE Audit_Script SHALL 在流程树报告末尾附加统计摘要:入口点数量、任务数量、加载器数量、孤立模块数量
7. THE Audit_Script SHALL 在文档对齐报告末尾附加统计摘要:过期点数量、冲突点数量、缺失点数量
### 需求 5只读安全保障
**用户故事:** 作为项目维护者,我希望审计过程不会修改仓库中的任何文件,以确保分析阶段的安全性。
#### 验收标准
1. THE Audit_Script SHALL 仅执行文件系统的读取操作(读取文件内容、列出目录、获取文件元信息)
2. THE Audit_Script SHALL 仅在 `docs/audit/repo/` 目录下创建新文件,该目录为报告专用输出目录
3. IF 审计脚本在执行过程中遇到权限错误或文件读取失败THEN THE Audit_Script SHALL 在报告中记录该错误并继续处理其余文件
4. THE Audit_Script SHALL 在运行前检查 `docs/audit/repo/` 目录是否存在,若不存在则创建该目录

View File

@@ -1,118 +0,0 @@
# 实施计划:仓库治理只读审计
## 概述
将设计文档中的审计脚本拆分为增量式编码任务。每个任务构建在前一个任务之上,最终产出可运行的审计工具集。所有脚本位于 `scripts/audit/` 目录,报告输出到 `docs/audit/repo/`
## 任务
- [x] 1. 搭建审计脚本骨架和数据模型
- [x] 1.1 创建 `scripts/audit/__init__.py` 和数据模型定义
- 定义 `FileEntry` dataclass`rel_path`, `is_dir`, `size_bytes`, `extension`, `is_empty_dir`
- 定义 `Category``Disposition` 枚举
- 定义 `InventoryItem` dataclass
- 定义 `FlowNode` dataclass
- 定义 `DocMapping``AlignmentIssue` dataclass
- _Requirements: 1.2, 1.3, 1.4, 2.7, 3.2, 3.3_
- [x] 1.2 编写 classify 完整性属性测试
- **Property 1: classify 完整性**
- **Validates: Requirements 1.2, 1.3**
- [x] 2. 实现仓库扫描器
- [x] 2.1 创建 `scripts/audit/scanner.py`
- 实现 `EXCLUDED_PATTERNS` 常量和排除匹配逻辑
- 实现 `scan_repo(root, exclude)` 函数:递归遍历文件系统,返回 `list[FileEntry]`
- 处理空目录检测(`is_empty_dir`
- 处理文件读取权限错误(跳过并记录)
- _Requirements: 1.1, 5.1, 5.3_
- [x] 2.2 编写扫描器排除规则属性测试
- **Property 7: 扫描器排除规则**
- **Validates: Requirements 1.1**
- [x] 3. 实现文件清单分析器
- [x] 3.1 创建 `scripts/audit/inventory_analyzer.py`
- 实现 `classify(entry: FileEntry) -> InventoryItem` 函数,包含完整分类规则表
- 实现 `build_inventory(entries) -> list[InventoryItem]` 批量分类函数
- 实现 `render_inventory_report(items, repo_root) -> str` Markdown 渲染函数
- 包含统计摘要生成(各分类/标签计数)
- 注意:需求 1.8 仅覆盖 `logs/``export/` 目录(不含 `reports/`
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 4.2, 4.5_
- [x] 3.2 编写 classify 分类规则属性测试
- **Property 3: 空目录标记为候选删除**
- **Property 4: .lnk/.rar 文件标记为候选删除**
- **Property 5: tmp/ 下文件处置范围**
- **Property 6: 运行时产出目录标记为候选归档**(仅 `logs/``export/`
- **Validates: Requirements 1.5, 1.6, 1.7, 1.8**
- [x] 3.3 编写清单渲染属性测试
- **Property 2: 清单渲染完整性**
- **Property 8: 清单按分类分组**
- **Validates: Requirements 1.4, 1.10**
- [x] 4. 检查点 - 确保文件清单模块测试通过
- 确保所有测试通过,如有疑问请向用户确认。
- [x] 5. 实现流程树分析器
- [x] 5.1 创建 `scripts/audit/flow_analyzer.py`
- 实现 `parse_imports(filepath)` 函数:使用 `ast` 模块解析 Python 文件的 import 语句
- 实现 `build_flow_tree(repo_root, entry_file)` 函数:从入口递归追踪 import 链
- 实现 `find_orphan_modules(repo_root, all_entries, reachable)` 函数
- 实现 `render_flow_report(trees, orphans, repo_root)` 函数:生成 Mermaid 图和缩进文本
- 包含入口点识别逻辑CLI、GUI、批处理、运维脚本
- 包含任务类型和加载器类型区分逻辑
- 包含统计摘要生成
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 4.6_
- [x] 5.2 编写流程树属性测试
- **Property 9: 流程树节点 source_file 有效性**
- **Property 10: 孤立模块检测正确性**
- **Validates: Requirements 2.7, 2.8**
- [x] 6. 实现文档对齐分析器
- [x] 6.1 创建 `scripts/audit/doc_alignment_analyzer.py`
- 实现 `scan_docs(repo_root)` 函数:扫描所有文档来源
- 实现 `extract_code_references(doc_path)` 函数:从文档提取代码引用
- 实现 `check_reference_validity(ref, repo_root)` 函数
- 实现 `find_undocumented_modules(repo_root, documented)` 函数
- 实现 `check_ddl_vs_dictionary(repo_root)` 函数DDL 与数据字典比对
- 实现 `check_api_samples_vs_parsers(repo_root)` 函数API 样本与解析器比对
- 实现 `render_alignment_report(mappings, issues, repo_root)` 函数
- 包含统计摘要生成
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 4.7_
- [x] 6.2 编写文档对齐属性测试
- **Property 11: 过期引用检测**
- **Property 12: 缺失文档检测**
- **Property 16: 文档对齐报告分区完整性**
- **Validates: Requirements 3.3, 3.5, 3.8**
- [x] 7. 检查点 - 确保流程树和文档对齐模块测试通过
- 确保所有测试通过,如有疑问请向用户确认。
- [x] 8. 实现审计主入口和报告输出
- [x] 8.1 创建 `scripts/audit/run_audit.py`
- 实现 `run_audit(repo_root)` 主函数:依次调用扫描器和三个分析器
- 实现 `docs/audit/repo/` 目录检查与创建逻辑
- 实现报告头部元信息(时间戳、仓库路径)注入
- 实现三份报告的文件写入
- 添加 `if __name__ == "__main__"` 入口
- _Requirements: 4.1, 4.2, 4.3, 4.4, 5.2, 5.4_
- [x] 8.2 编写报告输出属性测试
- **Property 13: 统计摘要一致性**
- **Property 14: 报告头部元信息**
- **Property 15: 写操作仅限 docs/audit/**
- **Validates: Requirements 4.2, 4.5, 4.6, 4.7, 5.2**
- [x] 9. 最终检查点 - 确保所有测试通过
- 确保所有测试通过,如有疑问请向用户确认。
## 备注
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
- 每个任务引用了具体的需求编号,便于追溯
- 属性测试使用 `hypothesis` 库,每个测试至少 100 次迭代
- 单元测试验证具体示例和边界情况,属性测试验证通用正确性

View File

@@ -1,462 +0,0 @@
# 设计文档ETL 调度器重构
## 概述
本次重构将 `ETLScheduler`(约 900 行,职责混乱的"上帝类")拆分为三层清晰的架构:
1. **CLI 层**`cli/main.py`):参数解析、配置加载、资源创建与释放
2. **PipelineRunner**`orchestration/pipeline_runner.py`):管道定义、层→任务映射、校验编排
3. **TaskExecutor**`orchestration/task_executor.py`):单任务执行、游标管理、运行记录
核心设计原则:**单个任务是最小执行单元,管道模式只是"调度拼接"**。每层通过依赖注入接收协作对象,不自行创建资源,便于独立测试。
## 架构
### 分层架构图
```mermaid
graph TD
CLI["CLI 层<br/>cli/main.py<br/>参数解析 · 配置加载 · 资源管理"]
PR["PipelineRunner<br/>orchestration/pipeline_runner.py<br/>管道定义 · 层→任务映射 · 校验编排"]
TE["TaskExecutor<br/>orchestration/task_executor.py<br/>单任务执行 · 游标管理 · 运行记录"]
TR["TaskRegistry<br/>orchestration/task_registry.py<br/>任务注册 · 元数据查询"]
CM["CursorManager"]
RT["RunTracker"]
DB["DatabaseConnection"]
API["APIClient"]
CLI -->|"创建并注入"| PR
CLI -->|"创建并注入"| TE
CLI -->|"管理生命周期"| DB
CLI -->|"管理生命周期"| API
PR -->|"委托执行"| TE
PR -->|"查询任务"| TR
TE -->|"查询元数据"| TR
TE -->|"管理游标"| CM
TE -->|"记录运行"| RT
TE -->|"使用"| DB
TE -->|"使用"| API
```
### 调用流程
**传统模式**`--tasks`
```
CLI → TaskExecutor.run_tasks([task_codes]) → TaskExecutor._run_single_task() × N
```
**管道模式**`--pipeline`
```
CLI → PipelineRunner.run(pipeline, processing_mode, ...)
→ PipelineRunner._resolve_tasks(layers)
→ TaskExecutor.run_tasks([resolved_tasks])
→ [可选] PipelineRunner._run_verification(layers, ...)
```
## 组件与接口
### TaskExecutor
负责单任务执行的完整生命周期。从原 `ETLScheduler` 中提取 `_run_single_task``_execute_fetch``_execute_ingest``_execute_ods_record_and_load``_run_utility_task` 等方法。
```python
class TaskExecutor:
def __init__(
self,
config: AppConfig,
db_ops: DatabaseOperations,
api_client: APIClient,
cursor_mgr: CursorManager,
run_tracker: RunTracker,
task_registry: TaskRegistry,
logger: logging.Logger,
):
...
def run_tasks(
self,
task_codes: list[str],
data_source: str = "hybrid", # online / offline / hybrid
) -> list[dict[str, Any]]:
"""批量执行任务列表,返回每个任务的结果。"""
...
def run_single_task(
self,
task_code: str,
run_uuid: str,
store_id: int,
data_source: str = "hybrid",
) -> dict[str, Any]:
"""执行单个任务的完整生命周期。"""
...
```
关键变化:
- `data_source` 作为显式参数传入,不再读取 `self.pipeline_flow` 全局状态
- 工具类任务判断通过 `TaskRegistry.get_metadata(task_code)` 查询,不再硬编码
- 不自行创建 `DatabaseConnection``APIClient`
### PipelineRunner
负责管道编排。从原 `ETLScheduler` 中提取 `run_pipeline_with_verification``_run_layer_verification``_get_tasks_for_layers` 等方法。
```python
class PipelineRunner:
# 管道定义(从 scheduler.py 模块级常量迁移至此)
PIPELINE_LAYERS: dict[str, list[str]] = {
"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"],
}
def __init__(
self,
config: AppConfig,
task_executor: TaskExecutor,
task_registry: TaskRegistry,
db_conn: DatabaseConnection,
api_client: APIClient,
logger: logging.Logger,
):
...
def run(
self,
pipeline: str,
processing_mode: str = "increment_only",
data_source: str = "hybrid",
window_start: datetime | None = None,
window_end: datetime | None = None,
window_split: str | None = None,
task_codes: list[str] | None = None,
fetch_before_verify: bool = False,
verify_tables: list[str] | None = None,
) -> dict[str, Any]:
"""执行管道,返回汇总结果。"""
...
def _resolve_tasks(self, layers: list[str]) -> list[str]:
"""根据层列表解析任务代码,优先查询 TaskRegistry 元数据。"""
...
def _run_verification(self, layers, window_start, window_end, ...):
"""执行后置校验(从原 _run_layer_verification 迁移)。"""
...
```
### TaskRegistry增强
在现有注册功能基础上增加元数据支持。
```python
@dataclass
class TaskMeta:
"""任务元数据"""
task_class: type
requires_db_config: bool = True
layer: str | None = None # "ODS" / "DWD" / "DWS" / "INDEX" / None
task_type: str = "etl" # "etl" / "utility" / "verification"
class TaskRegistry:
def __init__(self):
self._tasks: dict[str, TaskMeta] = {}
def register(
self,
task_code: str,
task_class: type,
requires_db_config: bool = True,
layer: str | None = None,
task_type: str = "etl",
):
"""注册任务类及其元数据。"""
self._tasks[task_code.upper()] = TaskMeta(
task_class=task_class,
requires_db_config=requires_db_config,
layer=layer,
task_type=task_type,
)
def create_task(self, task_code, config, db_connection, api_client, logger):
"""创建任务实例(保持原有接口不变)。"""
...
def get_metadata(self, task_code: str) -> TaskMeta | None:
"""查询任务元数据。"""
...
def get_tasks_by_layer(self, layer: str) -> list[str]:
"""获取指定层的所有任务代码。"""
...
def is_utility_task(self, task_code: str) -> bool:
"""判断是否为工具类任务(不需要游标/运行记录)。"""
meta = self.get_metadata(task_code)
return meta is not None and not meta.requires_db_config
def get_all_task_codes(self) -> list[str]:
"""获取所有已注册的任务代码(保持原有接口)。"""
...
```
### CLI 层重构
```python
# cli/main.py 核心流程伪代码
def main():
args = parse_args()
config = AppConfig.load(build_cli_overrides(args))
# 资源创建
db_conn = DatabaseConnection(...)
api_client = APIClient(...)
try:
# 组装依赖
db_ops = DatabaseOperations(db_conn)
cursor_mgr = CursorManager(db_conn)
run_tracker = RunTracker(db_conn)
registry = default_registry
executor = TaskExecutor(config, db_ops, api_client, cursor_mgr, run_tracker, registry, logger)
if args.pipeline:
runner = PipelineRunner(config, executor, registry, db_conn, api_client, logger)
runner.run(
pipeline=args.pipeline,
processing_mode=args.processing_mode,
data_source=resolve_data_source(args),
...
)
else:
task_codes = config.get("run.tasks")
data_source = resolve_data_source(args)
executor.run_tasks(task_codes, data_source=data_source)
finally:
db_conn.close()
```
### 参数映射
| 旧参数 | 旧值 | 新参数 | 新值 | 说明 |
|--------|------|--------|------|------|
| `--pipeline-flow` | `FULL` | `--data-source` | `hybrid` | 在线抓取 + 本地入库 |
| `--pipeline-flow` | `FETCH_ONLY` | `--data-source` | `online` | 仅在线抓取落盘 |
| `--pipeline-flow` | `INGEST_ONLY` | `--data-source` | `offline` | 仅本地清洗入库 |
### 静态方法归位
| 方法 | 原位置 | 新位置 | 理由 |
|------|--------|--------|------|
| `_map_run_status` | `ETLScheduler` | `RunTracker` | 状态映射是运行记录的职责 |
| `_filter_verify_tables` | `ETLScheduler` | `tasks/verification/` 模块 | 校验表过滤是校验模块的职责 |
## 数据模型
### TaskMeta新增
```python
@dataclass
class TaskMeta:
task_class: type # 任务类引用
requires_db_config: bool = True # 是否需要数据库任务配置(游标/运行记录)
layer: str | None = None # 所属层:"ODS"/"DWD"/"DWS"/"INDEX"/None
task_type: str = "etl" # 任务类型:"etl"/"utility"/"verification"
```
### DataSource 枚举
```python
class DataSource(str, Enum):
ONLINE = "online" # 仅在线抓取(原 FETCH_ONLY
OFFLINE = "offline" # 仅本地入库(原 INGEST_ONLY
HYBRID = "hybrid" # 抓取 + 入库(原 FULL
```
### 配置键映射
| 旧键 | 新键 | 默认值 |
|------|------|--------|
| `app.timezone` | `app.timezone` | `Asia/Shanghai`(原 `Asia/Shanghai` |
| `pipeline.flow` | `run.data_source` | `hybrid` |
| `pipeline.fetch_root` | `io.fetch_root` | `export/JSON` |
| `pipeline.ingest_source_dir` | `io.ingest_source_dir` | `""` |
### 任务执行结果(不变)
```python
# 单任务结果
{
"task_code": str,
"status": str, # "SUCCESS" / "FAIL" / "SKIP"
"counts": {
"fetched": int,
"inserted": int,
"updated": int,
"skipped": int,
"errors": int,
},
"window": {"start": datetime, "end": datetime, "minutes": int} | None,
"dump_dir": str | None,
}
# 管道结果
{
"status": str,
"pipeline": str,
"layers": list[str],
"results": list[dict], # 各任务结果
"verification_summary": dict | None, # 校验汇总
}
```
## 正确性属性
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1data_source 参数决定执行路径
*对于任意* 任务代码和任意 `data_source`online/offline/hybridTaskExecutor 执行该任务时,抓取阶段执行当且仅当 `data_source``online``hybrid`,入库阶段执行当且仅当 `data_source``offline``hybrid`
**验证:需求 1.2**
### Property 2成功任务推进游标
*对于任意* 非工具类任务,当任务执行成功且返回包含有效 `window`(含 `start``end`的结果时CursorManager.advance 应被调用且参数与返回的窗口一致。
**验证:需求 1.3**
### Property 3失败任务标记 FAIL 并重新抛出
*对于任意* 非工具类任务当任务执行过程中抛出异常时RunTracker 应被更新为 FAIL 状态,且该异常应被重新抛出给调用方。
**验证:需求 1.4**
### Property 4工具类任务由元数据决定
*对于任意* 任务代码TaskExecutor 是否跳过游标管理和运行记录,取决于 TaskRegistry 中该任务的 `requires_db_config` 元数据。当 `requires_db_config=False` 时跳过,否则执行完整生命周期。
**验证:需求 1.6, 4.2**
### Property 5管道名称→层列表映射
*对于任意* 有效的管道名称PipelineRunner 解析出的层列表应与 `PIPELINE_LAYERS` 字典中的定义完全一致。
**验证:需求 2.1**
### Property 6processing_mode 控制执行流程
*对于任意* processing_mode 值,增量 ETL 执行当且仅当模式包含 `increment`(即 `increment_only``increment_verify`),校验流程执行当且仅当模式包含 `verify`(即 `verify_only``increment_verify`)。
**验证:需求 2.3, 2.4**
### Property 7管道结果汇总完整性
*对于任意* 一组任务执行结果PipelineRunner 返回的汇总字典应包含 `status``pipeline``layers``results` 字段,且 `results` 列表长度等于实际执行的任务数。
**验证:需求 2.6**
### Property 8TaskRegistry 元数据 round-trip
*对于任意* 任务代码、任务类和元数据组合requires_db_config、layer、task_type注册后通过 `get_metadata` 查询应返回相同的元数据值。
**验证:需求 4.1**
### Property 9TaskRegistry 向后兼容默认值
*对于任意* 使用旧接口(仅 task_code 和 task_class注册的任务查询元数据应返回 `requires_db_config=True``layer=None``task_type="etl"`
**验证:需求 4.4**
### Property 10按层查询任务
*对于任意* 注册了 `layer` 元数据的任务集合,`get_tasks_by_layer(layer)` 返回的任务代码集合应等于所有 `layer` 匹配的已注册任务代码集合。
**验证:需求 4.3**
### Property 11pipeline_flow → data_source 映射一致性
*对于任意*`pipeline_flow`FULL/FETCH_ONLY/INGEST_ONLY映射到 `data_source` 的结果应与预定义映射表一致FULL→hybrid、FETCH_ONLY→online、INGEST_ONLY→offline。同样配置键 `pipeline.flow` 应自动映射到 `run.data_source`
**验证:需求 8.1, 8.2, 8.3, 5.2, 8.4**
## 错误处理
### TaskExecutor 错误处理
- 任务执行异常:更新 RunTracker 状态为 FAIL含 error_message然后重新抛出异常
- 游标推进失败:记录错误日志,不影响任务结果(任务本身已成功)
- 任务配置不存在:返回 `{"status": "SKIP"}` 结果,不抛异常
### PipelineRunner 错误处理
- 单个任务失败:记录错误,继续执行后续任务(与当前行为一致)
- 校验框架未安装:返回 `{"status": "SKIPPED"}` 并记录警告
- 无效管道名称:抛出 `ValueError`
### CLI 错误处理
- 配置加载失败:`SystemExit` 并输出错误信息
- 资源创建失败:`SystemExit` 并输出错误信息
- 执行过程异常:记录错误日志,`finally` 块确保资源释放,返回非零退出码
### 弃用警告
- 使用 Python `warnings.warn(DeprecationWarning)` 发出弃用警告
- 同时在日志中记录映射详情,便于运维排查
## 测试策略
### 单元测试
使用 `pytest` + 现有的 `FakeDB`/`FakeAPI` 测试工具(`tests/unit/task_test_utils.py`)。
**TaskExecutor 测试**
- 注入 mock 依赖FakeDB、FakeAPI、mock CursorManager、mock RunTracker
- 验证成功/失败/跳过三种路径
- 验证工具类任务不触发游标/运行记录
- 验证 data_source 参数正确控制抓取/入库阶段
**PipelineRunner 测试**
- 注入 mock TaskExecutor
- 验证不同 processing_mode 下的执行流程
- 验证管道→层→任务的解析链
**TaskRegistry 测试**
- 验证元数据注册和查询
- 验证向后兼容(无元数据注册)
- 验证按层查询
**配置兼容性测试**
- 验证旧键→新键映射
- 验证优先级规则
- 验证默认值变更
### 属性测试
使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
每个属性测试必须用注释标注对应的设计属性编号:
```python
# Feature: scheduler-refactor, Property 8: TaskRegistry 元数据 round-trip
```
**属性测试覆盖**
- Property 1: data_source 参数决定执行路径
- Property 2: 成功任务推进游标
- Property 3: 失败任务标记 FAIL 并重新抛出
- Property 4: 工具类任务由元数据决定
- Property 5: 管道名称→层列表映射
- Property 6: processing_mode 控制执行流程
- Property 7: 管道结果汇总完整性
- Property 8: TaskRegistry 元数据 round-trip
- Property 9: TaskRegistry 向后兼容默认值
- Property 10: 按层查询任务
- Property 11: pipeline_flow → data_source 映射一致性

View File

@@ -1,123 +0,0 @@
# 需求文档ETL 调度器重构
## 简介
当前 `orchestration/scheduler.py`(约 900 行)中的 `ETLScheduler` 类承担了过多职责单任务执行、管道编排、资源管理。CLI 参数命名混乱(`--pipeline` vs `--pipeline-flow` vs `--processing-mode`全局状态耦合严重配置键语义重叠。本次重构将调度器拆分为三层架构CLI → PipelineRunner → TaskExecutor重新设计参数命名消除全局状态依赖使每层可独立测试。
## 术语表
- **TaskExecutor**:任务执行器,负责单个 ETL 任务的执行、游标管理和运行记录
- **PipelineRunner**:管道运行器,负责管道定义、层→任务映射、校验编排
- **TaskRegistry**:任务注册表,管理所有已注册的任务类及其元数据
- **DataSource**:数据源模式,取代原 `pipeline.flow`,表示数据来自在线 API`online`)、本地 JSON`offline`)或混合模式(`hybrid`
- **ProcessingMode**:处理模式,控制 ETL 执行策略(仅增量 / 仅校验 / 增量+校验)
- **Pipeline**:管道,定义一组按层顺序执行的 ETL 任务集合(如 `api_full` = ODS → DWD → DWS → INDEX
- **CursorManager**:游标管理器,管理任务的时间水位(上次处理到哪里)
- **RunTracker**:运行记录器,在 `etl_admin` Schema 中记录每次任务执行的状态和统计
## 需求
### 需求 1架构分层 — TaskExecutor执行层
**用户故事:** 作为开发者,我希望单任务执行逻辑独立封装在 TaskExecutor 中,以便可以脱离管道上下文独立测试和复用。
#### 验收标准
1. THE TaskExecutor SHALL 封装单个任务的完整执行生命周期:创建运行记录、执行任务、更新游标、记录结果
2. WHEN TaskExecutor 执行一个任务时THE TaskExecutor SHALL 接收显式的 `data_source` 参数,而非读取全局状态
3. WHEN 任务执行成功且返回有效时间窗口时THE TaskExecutor SHALL 推进该任务的游标水位
4. WHEN 任务执行过程中发生异常时THE TaskExecutor SHALL 将运行记录状态更新为 FAIL 并重新抛出异常
5. THE TaskExecutor SHALL 通过构造函数接收 `db_ops``api_client``cursor_manager``run_tracker``task_registry` 等依赖,而非自行创建
6. WHEN 执行工具类任务(如 INIT_ODS_SCHEMATHE TaskExecutor SHALL 跳过游标管理和运行记录,直接执行任务
### 需求 2架构分层 — PipelineRunner编排层
**用户故事:** 作为开发者,我希望管道编排逻辑独立封装在 PipelineRunner 中,以便管道定义和校验流程可以独立演进。
#### 验收标准
1. THE PipelineRunner SHALL 根据管道名称解析出需要执行的层列表(如 `api_full``["ODS", "DWD", "DWS", "INDEX"]`
2. WHEN PipelineRunner 执行管道时THE PipelineRunner SHALL 委托 TaskExecutor 逐个执行任务,而非直接操作数据库或 API
3. WHEN 处理模式为 `verify_only`THE PipelineRunner SHALL 跳过增量 ETL仅执行校验流程
4. WHEN 处理模式为 `increment_verify`THE PipelineRunner SHALL 先执行增量 ETL再执行校验流程
5. THE PipelineRunner SHALL 根据层列表自动选择对应的任务代码,支持配置覆盖
6. WHEN 管道执行完成时THE PipelineRunner SHALL 汇总所有任务的执行结果并返回统一的结果字典
### 需求 3架构分层 — CLI 层重构
**用户故事:** 作为运维人员,我希望 CLI 参数命名清晰、语义无歧义,以便快速理解和正确使用各种执行模式。
#### 验收标准
1. THE CLI SHALL 将 `--pipeline-flow`FULL/FETCH_ONLY/INGEST_ONLY重命名为 `--data-source`online/offline/hybrid并保留旧名称作为别名
2. THE CLI SHALL 保留 `--pipeline` 参数用于管道模式,保留 `--tasks` 参数用于传统模式
3. WHEN 用户同时指定 `--pipeline``--tasks`THE CLI SHALL 将 `--tasks` 作为管道内的任务过滤器
4. THE CLI SHALL 保留 `--processing-mode`increment_only/verify_only/increment_verify参数不变
5. WHEN 用户使用旧参数名 `--pipeline-flow`THE CLI SHALL 发出弃用警告并将值映射到新的 `--data-source` 参数
6. THE CLI SHALL 仅负责参数解析和配置加载,将执行逻辑委托给 PipelineRunner 或 TaskExecutor
### 需求 4任务分类元数据化
**用户故事:** 作为开发者,我希望任务的分类信息(是否需要数据库配置、所属层等)由任务注册表管理,而非硬编码在调度器中。
#### 验收标准
1. THE TaskRegistry SHALL 支持在注册任务时附带元数据(`requires_db_config``layer``task_type`
2. WHEN TaskExecutor 需要判断任务是否为工具类任务时THE TaskExecutor SHALL 查询 TaskRegistry 的元数据,而非检查硬编码集合
3. WHEN PipelineRunner 需要根据层获取任务列表时THE PipelineRunner SHALL 查询 TaskRegistry 的 `layer` 元数据
4. THE TaskRegistry SHALL 保持向后兼容,无元数据的任务默认为 `requires_db_config=True``layer=None`
### 需求 5配置键重构
**用户故事:** 作为运维人员,我希望配置键命名合理、语义清晰,以便正确配置 ETL 系统的运行参数。
#### 验收标准
1. THE AppConfig SHALL 将 `app.timezone` 默认值从 `Asia/Shanghai` 改为 `Asia/Shanghai`
2. THE AppConfig SHALL 将 `pipeline.flow` 配置键重命名为 `run.data_source`,并保留旧键作为兼容别名
3. WHEN 配置中同时存在旧键 `pipeline.flow` 和新键 `run.data_source`THE AppConfig SHALL 优先使用新键的值
4. THE AppConfig SHALL 将 `pipeline.fetch_root``pipeline.ingest_source_dir` 移至 `io` 命名空间下(`io.fetch_root``io.ingest_source_dir`
### 需求 6资源管理与生命周期
**用户故事:** 作为开发者,我希望数据库连接和 API 客户端的创建与关闭由 CLI 层统一管理,以便确保资源正确释放。
#### 验收标准
1. THE CLI SHALL 在 `finally` 块中关闭数据库连接和 API 客户端,确保异常情况下资源也能释放
2. THE TaskExecutor SHALL 通过依赖注入接收已创建的数据库连接和 API 客户端,而非自行创建
3. THE PipelineRunner SHALL 通过依赖注入接收已创建的数据库连接和 API 客户端,而非自行创建
4. WHEN CLI 创建资源时THE CLI SHALL 使用 Python 上下文管理器(`with` 语句)或 `try/finally` 模式管理生命周期
### 需求 7静态方法归位
**用户故事:** 作为开发者,我希望与调度器无关的静态工具方法移至合适的模块,以便保持类的职责单一。
#### 验收标准
1. THE `_map_run_status` 方法 SHALL 从 ETLScheduler 移至 RunTracker 或独立的工具模块
2. THE `_filter_verify_tables` 方法 SHALL 从 ETLScheduler 移至校验相关模块
3. WHEN 静态方法被移动后THE 原调用方 SHALL 更新导入路径以引用新位置
### 需求 8向后兼容与过渡
**用户故事:** 作为运维人员,我希望重构后的系统在过渡期内兼容旧的 CLI 参数和配置键,以便平滑迁移。
#### 验收标准
1. WHEN 用户使用旧参数 `--pipeline-flow FULL`THE CLI SHALL 将其等价映射为 `--data-source hybrid` 并发出弃用警告
2. WHEN 用户使用旧参数 `--pipeline-flow FETCH_ONLY`THE CLI SHALL 将其等价映射为 `--data-source online` 并发出弃用警告
3. WHEN 用户使用旧参数 `--pipeline-flow INGEST_ONLY`THE CLI SHALL 将其等价映射为 `--data-source offline` 并发出弃用警告
4. WHEN 配置文件中使用旧键 `pipeline.flow`THE AppConfig SHALL 自动映射到新键 `run.data_source`
5. THE 系统 SHALL 在日志中记录所有弃用映射,便于运维人员逐步迁移
### 需求 9可测试性
**用户故事:** 作为开发者,我希望重构后的每一层都可以独立进行单元测试,以便快速验证逻辑正确性。
#### 验收标准
1. THE TaskExecutor SHALL 支持通过注入 mock 依赖FakeDB、FakeAPI进行单元测试无需真实数据库
2. THE PipelineRunner SHALL 支持通过注入 mock TaskExecutor 进行单元测试,无需执行真实任务
3. THE TaskRegistry SHALL 支持在测试中创建独立实例,不依赖全局 `default_registry`
4. WHEN 运行单元测试时THE 测试 SHALL 验证各层之间的交互契约(调用参数、返回值格式)

View File

@@ -1,147 +0,0 @@
# 实现计划ETL 调度器重构
## 概述
`ETLScheduler`~900 行)拆分为 TaskExecutor执行层、PipelineRunner编排层、增强版 TaskRegistry元数据重构 CLI 参数和配置键,保持向后兼容。采用自底向上的实现顺序:先基础组件,再上层编排,最后 CLI 集成。
## 任务
- [x] 1. 增强 TaskRegistry支持元数据注册与查询
- [x] 1.1 扩展 TaskRegistry 类,添加 TaskMeta 数据类和元数据相关方法
-`orchestration/task_registry.py` 中添加 `TaskMeta` dataclass`task_class``requires_db_config``layer``task_type`
- 修改 `register()` 方法签名,增加可选的 `requires_db_config``layer``task_type` 参数
- 添加 `get_metadata()``get_tasks_by_layer()``is_utility_task()` 方法
- 保持 `create_task()``get_all_task_codes()` 接口不变
- _需求: 4.1, 4.4_
- [x] 1.2 更新所有任务注册调用,添加元数据
- 将原 `NO_DB_CONFIG_TASKS` 硬编码集合中的任务标记为 `requires_db_config=False`
- 为 ODS 任务添加 `layer="ODS"`DWD 任务添加 `layer="DWD"`DWS 任务添加 `layer="DWS"`INDEX 任务添加 `layer="INDEX"`
- 工具类任务标记 `task_type="utility"`,校验类任务标记 `task_type="verification"`
- _需求: 4.1, 4.2, 4.3_
- [x] 1.3 编写 TaskRegistry 属性测试
- **Property 8: TaskRegistry 元数据 round-trip**
- **验证: 需求 4.1**
- [x] 1.4 编写 TaskRegistry 向后兼容和按层查询属性测试
- **Property 9: TaskRegistry 向后兼容默认值**
- **Property 10: 按层查询任务**
- **验证: 需求 4.4, 4.3**
- [x] 2. 配置键重构与向后兼容
- [x] 2.1 修改 `config/defaults.py` 默认值
-`app.timezone` 默认值从 `Asia/Shanghai` 改为 `Asia/Shanghai`
-`db.session.timezone` 默认值从 `Asia/Shanghai` 改为 `Asia/Shanghai`
- 添加 `run.data_source` 键(默认 `hybrid`
-`pipeline.fetch_root``pipeline.ingest_source_dir` 复制到 `io.fetch_root``io.ingest_source_dir`(保留旧键兼容)
- _需求: 5.1, 5.2, 5.4_
- [x] 2.2 在 `config/settings.py``_normalize()` 中添加兼容映射逻辑
- 旧键 `pipeline.flow` → 新键 `run.data_source`值映射FULL→hybrid, FETCH_ONLY→online, INGEST_ONLY→offline
- 旧键 `pipeline.fetch_root``io.fetch_root``pipeline.ingest_source_dir``io.ingest_source_dir`
- 新键优先:当新旧键同时存在时,使用新键的值
- 记录弃用警告日志
- _需求: 5.2, 5.3, 5.4, 8.4, 8.5_
- [x] 2.3 编写配置映射属性测试
- **Property 11: pipeline_flow → data_source 映射一致性**
- **验证: 需求 8.1, 8.2, 8.3, 5.2, 8.4**
- [x] 3. 静态方法归位
- [x] 3.1 将 `_map_run_status` 移至 RunTracker
-`orchestration/run_tracker.py` 中添加 `map_run_status()` 静态方法(从 `ETLScheduler._map_run_status` 复制)
- _需求: 7.1_
- [x] 3.2 将 `_filter_verify_tables` 移至校验模块
-`tasks/verification/` 下合适的模块中添加 `filter_verify_tables()` 函数
- _需求: 7.2_
- [x] 4. 检查点 — 确保所有测试通过
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
- [x] 5. 实现 TaskExecutor执行层
- [x] 5.1 创建 `orchestration/task_executor.py`
- 实现 `TaskExecutor` 类,构造函数接收 `config``db_ops``api_client``cursor_mgr``run_tracker``task_registry``logger`
-`ETLScheduler` 迁移以下方法:`run_tasks``_run_single_task``_execute_fetch``_execute_ingest``_execute_ods_record_and_load``_run_utility_task``_build_fetch_dir``_resolve_ingest_source``_counts_from_fetch``_load_task_config``_maybe_run_integrity_check``_attach_run_file_logger`
-`data_source` 改为方法参数(替代原 `self.pipeline_flow` 全局状态)
- 使用 `self.task_registry.is_utility_task()` 替代硬编码的 `NO_DB_CONFIG_TASKS`
- 使用 `RunTracker.map_run_status()` 替代 `self._map_run_status()`
- 添加 `DataSource` 枚举类(`online`/`offline`/`hybrid`
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
- [x] 5.2 编写 TaskExecutor 属性测试
- **Property 1: data_source 参数决定执行路径**
- **Property 2: 成功任务推进游标**
- **Property 3: 失败任务标记 FAIL 并重新抛出**
- **Property 4: 工具类任务由元数据决定**
- **验证: 需求 1.2, 1.3, 1.4, 1.6, 4.2**
- [x] 6. 实现 PipelineRunner编排层
- [x] 6.1 创建 `orchestration/pipeline_runner.py`
- 实现 `PipelineRunner` 类,构造函数接收 `config``task_executor``task_registry``db_conn``api_client``logger`
-`PIPELINE_LAYERS` 常量从 `scheduler.py` 迁移至此
-`ETLScheduler` 迁移以下方法:`run_pipeline_with_verification`(重命名为 `run`)、`_run_layer_verification`(重命名为 `_run_verification`)、`_get_tasks_for_layers`(重命名为 `_resolve_tasks`
- 使用 `filter_verify_tables()`(已移至校验模块)替代原内联静态方法
- 使用 `task_registry.get_tasks_by_layer()` 作为默认任务解析,配置覆盖优先
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
- [x] 6.2 编写 PipelineRunner 属性测试
- **Property 5: 管道名称→层列表映射**
- **Property 6: processing_mode 控制执行流程**
- **Property 7: 管道结果汇总完整性**
- **验证: 需求 2.1, 2.3, 2.4, 2.6**
- [x] 7. 检查点 — 确保所有测试通过
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
- [x] 8. 重构 CLI 层
- [x] 8.1 重构 `cli/main.py` 参数解析
- 添加 `--data-source` 参数choices: online/offline/hybrid默认 hybrid
- 保留 `--pipeline-flow` 作为弃用别名,使用时发出 `DeprecationWarning` 并映射到 `--data-source`
- 更新 `build_cli_overrides()``--data-source` 写入 `run.data_source` 配置键
- _需求: 3.1, 3.5, 8.1, 8.2, 8.3_
- [x] 8.2 重构 `cli/main.py``main()` 函数
-`try/finally` 块中管理 `DatabaseConnection``APIClient` 的生命周期
-`try` 块内组装 `TaskExecutor``PipelineRunner`(依赖注入)
- 管道模式委托 `PipelineRunner.run()`,传统模式委托 `TaskExecutor.run_tasks()`
- 添加 `resolve_data_source(args)` 辅助函数处理新旧参数映射
- _需求: 3.2, 3.3, 3.4, 3.6, 6.1, 6.4_
- [x] 8.3 编写 CLI 参数解析单元测试
- 测试 `--data-source` 新参数正确解析
- 测试 `--pipeline-flow` 旧参数弃用映射
- 测试 `--pipeline` + `--tasks` 同时使用时的行为
- _需求: 3.1, 3.3, 3.5_
- [x] 9. 清理旧代码与集成
- [x] 9.1 重构 `orchestration/scheduler.py` 为薄包装层
-`ETLScheduler` 改为薄包装,内部委托 `TaskExecutor``PipelineRunner`
- 保留 `ETLScheduler` 类名和 `run_tasks()``run_pipeline_with_verification()``close()` 公共接口,标记为弃用
- 确保 GUI 层(`gui/workers/`)等现有调用方无需立即修改
- _需求: 8.1, 8.4_
- [x] 9.2 更新 GUI 工作线程中的调度器引用
- 检查 `gui/workers/` 中对 `ETLScheduler` 的使用
- 如有直接引用内部方法,更新为使用新的公共接口
- _需求: 7.3_
- [x] 9.3 编写集成测试验证端到端流程
- 使用 FakeDB/FakeAPI 验证 CLI → PipelineRunner → TaskExecutor 完整调用链
- 验证传统模式和管道模式均正常工作
- _需求: 9.4_
- [x] 10. 最终检查点 — 确保所有测试通过
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
## 备注
- 标记 `*` 的子任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯性
- 检查点确保增量验证,避免问题累积
- 属性测试使用 `hypothesis` 库,验证通用正确性属性
- 单元测试验证具体示例和边界条件
- `ETLScheduler` 保留为薄包装层,确保 GUI 等现有调用方平滑过渡

View File

@@ -0,0 +1 @@
{"generationMode": "requirements-first"}

View File

@@ -0,0 +1,395 @@
# 设计文档SPI 消费力指数Spending Power Index
## 概述
SPI 是 NeoZQYY 指数体系的第 7 个指数(继 WBI/NCI/RS/OS/MS/ML 之后),粒度为 `(site_id, member_id)`,用于衡量会员在门店内的综合消费力层级。
SPI 采用"主分 + 子分"结构:
- Level消费水平基于消费金额和客单价的 log1p 压缩加权
- Speed消费速度基于绝对速度、相对速度、EWMA 速度的加权
- Stability消费稳定性基于近 90 天周覆盖率
SPI 不继承 `MemberIndexBaseTask`(该基类为 WBI/NCI 共享的会员分群逻辑SPI 不需要 NEW/OLD/STOP 分群),而是直接继承 `BaseIndexTask`,自行实现数据提取和评分逻辑。
### 设计决策
1. **继承 BaseIndexTask 而非 MemberIndexBaseTask**SPI 不需要会员分群NEW/OLD/STOP所有有消费记录的会员均参与计算。MemberIndexBaseTask 的 `_build_member_activity` 提取的特征intervals、t_v/t_r/t_a 等)与 SPI 需求不匹配,复用反而增加耦合。
2. **独立数据提取**SPI 需要按周聚合、日消费序列等 MemberIndexBaseTask 不提供的特征,因此自行编写 SQL 提取逻辑。
3. **金额压缩基数自动校准**:首次执行时从门店数据计算中位数作为基数,后续可通过 cfg_index_parameters 手动覆盖。
4. **子分独立映射**Level/Speed/Stability 各自独立做 batch_normalize_to_display使用不同的 index_type 后缀SPI_LEVEL/SPI_SPEED/SPI_STABILITY隔离分位历史。
## 架构
```mermaid
graph TD
subgraph 数据来源
A[dwd.dwd_settlement_head<br/>消费订单]
B[dwd.dwd_recharge_order<br/>充值订单]
end
subgraph SpendingPowerIndexTask
C[extract_spending_features<br/>提取基础特征]
D[calculate_level<br/>Level 子分]
E[calculate_speed<br/>Speed 子分]
F[calculate_stability<br/>Stability 子分]
G[calculate_spi_raw<br/>SPI 总分合成]
H[batch_normalize_to_display<br/>展示分映射]
end
subgraph 配置
I[cfg_index_parameters<br/>index_type='SPI']
end
subgraph 输出
J[dws.dws_member_spending_power_index]
K[dws.dws_index_percentile_history]
end
A --> C
B --> C
I --> C
I --> D
I --> E
I --> F
I --> G
C --> D
C --> E
C --> F
D --> G
E --> G
F --> G
G --> H
H --> J
H --> K
```
### 继承体系
```
BaseTask
└── BaseDwsTask
└── BaseIndexTask
├── MemberIndexBaseTask ← WBI / NCI不使用
├── RelationIndexTask ← RS/OS/MS/ML
├── MlManualImportTask ← ML 台账导入
└── SpendingPowerIndexTask ← SPI新增
```
## 组件与接口
### SpendingPowerIndexTask
继承 `BaseIndexTask`,实现以下接口:
```python
class SpendingPowerIndexTask(BaseIndexTask):
INDEX_TYPE = "SPI"
DEFAULT_PARAMS = {
# 窗口参数
'spend_window_short_days': 30,
'spend_window_long_days': 90,
'ewma_alpha_daily_spend': 0.3,
# 金额压缩基数(初始默认值,可被自动校准或配置表覆盖)
'amount_base_spend_30': 500.0,
'amount_base_spend_90': 1500.0,
'amount_base_ticket_90': 200.0,
'amount_base_recharge_90': 1000.0,
'amount_base_speed_abs': 100.0,
'amount_base_ewma_90': 50.0,
# Level 子分权重
'w_level_spend_30': 0.30,
'w_level_spend_90': 0.35,
'w_level_ticket_90': 0.20,
'w_level_recharge_90': 0.15,
# Speed 子分权重
'w_speed_abs': 0.50,
'w_speed_rel': 0.30,
'w_speed_ewma': 0.20,
# 总分权重
'weight_level': 0.60,
'weight_speed': 0.30,
'weight_stability': 0.10,
# 稳定性参数
'stability_window_days': 90,
'use_stability': 1,
# 映射与平滑
'percentile_lower': 5,
'percentile_upper': 95,
'compression_mode': 1, # log1p
'use_smoothing': 1,
'ewma_alpha': 0.2,
# 速度计算
'speed_epsilon': 1e-6,
}
# --- 必须实现的抽象方法 ---
def get_task_code(self) -> str: ...
def get_target_table(self) -> str: ...
def get_primary_keys(self) -> List[str]: ...
def get_index_type(self) -> str: ...
# --- 核心执行流程 ---
def execute(self, context=None) -> Dict[str, Any]: ...
# --- 数据提取 ---
def _extract_spending_features(self, site_id, params) -> Dict[int, SPIMemberFeatures]: ...
def _extract_recharge_features(self, site_id, params) -> Dict[int, RechargeFeatures]: ...
def _calibrate_amount_bases(self, features, params) -> Dict[str, float]: ...
# --- 子分计算(纯函数,可独立测试) ---
@staticmethod
def compute_level(features, params) -> float: ...
@staticmethod
def compute_speed(features, params) -> float: ...
@staticmethod
def compute_stability(features, params) -> float: ...
@staticmethod
def compute_spi_raw(level, speed, stability, params) -> float: ...
# --- 持久化 ---
def _save_spi_data(self, data_list, site_id) -> int: ...
```
### 关键设计:子分计算为静态方法
`compute_level``compute_speed``compute_stability``compute_spi_raw` 设计为 `@staticmethod`,不依赖数据库或任务实例状态。这使得属性测试可以直接调用这些纯函数,无需 mock 数据库连接。
### SPIMemberFeatures 数据类
```python
@dataclass
class SPIMemberFeatures:
"""SPI 计算所需的会员级特征"""
member_id: int
site_id: int
# 基础特征
spend_30: float = 0.0 # 近30天消费总额
spend_90: float = 0.0 # 近90天消费总额
recharge_90: float = 0.0 # 近90天充值总额
orders_30: int = 0 # 近30天消费笔数
orders_90: int = 0 # 近90天消费笔数
visit_days_30: int = 0 # 近30天消费日数按天去重
visit_days_90: int = 0 # 近90天消费日数按天去重
avg_ticket_90: float = 0.0 # 90天客单价
active_weeks_90: int = 0 # 近90天有消费的自然周数
daily_spend_ewma_90: float = 0.0 # 日消费 EWMA
# 子分
score_level_raw: float = 0.0
score_speed_raw: float = 0.0
score_stability_raw: float = 0.0
# 展示分(归一化后填充)
score_level_display: float = 0.0
score_speed_display: float = 0.0
score_stability_display: float = 0.0
# 总分
raw_score: float = 0.0
display_score: float = 0.0
```
## 数据模型
### dws.dws_member_spending_power_index 表结构
```sql
CREATE TABLE dws.dws_member_spending_power_index (
spi_id BIGSERIAL PRIMARY KEY,
site_id INTEGER NOT NULL,
member_id BIGINT NOT NULL,
-- 基础特征
spend_30 NUMERIC(14,2) DEFAULT 0,
spend_90 NUMERIC(14,2) DEFAULT 0,
recharge_90 NUMERIC(14,2) DEFAULT 0,
orders_30 INTEGER DEFAULT 0,
orders_90 INTEGER DEFAULT 0,
visit_days_30 INTEGER DEFAULT 0,
visit_days_90 INTEGER DEFAULT 0,
avg_ticket_90 NUMERIC(14,2) DEFAULT 0,
active_weeks_90 INTEGER DEFAULT 0,
daily_spend_ewma_90 NUMERIC(14,2) DEFAULT 0,
-- 子分Raw
score_level_raw NUMERIC(10,4) DEFAULT 0,
score_speed_raw NUMERIC(10,4) DEFAULT 0,
score_stability_raw NUMERIC(10,4) DEFAULT 0,
-- 子分Display 0-10
score_level_display NUMERIC(5,2) DEFAULT 0,
score_speed_display NUMERIC(5,2) DEFAULT 0,
score_stability_display NUMERIC(5,2) DEFAULT 0,
-- 总分
raw_score NUMERIC(10,4) DEFAULT 0,
display_score NUMERIC(5,2) DEFAULT 0,
-- 元数据
calc_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 唯一约束(业务主键)
CREATE UNIQUE INDEX idx_spi_site_member
ON dws.dws_member_spending_power_index (site_id, member_id);
-- 查询索引
CREATE INDEX idx_spi_display_score
ON dws.dws_member_spending_power_index (site_id, display_score DESC);
```
### cfg_index_parameters 新增种子数据
`db/etl_feiqiu/seeds/seed_index_parameters.sql` 中追加 `index_type='SPI'` 的参数行,格式与现有 WBI/NCI 参数一致。
### 执行流程
```mermaid
sequenceDiagram
participant Scheduler as 调度器
participant Task as SpendingPowerIndexTask
participant DB as PostgreSQL
participant Base as BaseIndexTask
Scheduler->>Task: execute(context)
Task->>DB: 获取 site_id
Task->>Base: load_index_parameters('SPI')
Base->>DB: SELECT FROM cfg_index_parameters
Base-->>Task: params dict
Task->>DB: 提取消费订单近90天
Task->>DB: 提取充值订单近90天
Task->>Task: 聚合会员级特征
Task->>Task: 校准金额压缩基数(如需)
loop 每个会员
Task->>Task: compute_level(features, params)
Task->>Task: compute_speed(features, params)
Task->>Task: compute_stability(features, params)
Task->>Task: compute_spi_raw(L, S, P, params)
end
Task->>Base: batch_normalize_to_display(SPI raw scores)
Task->>Base: batch_normalize_to_display(Level raw scores)
Task->>Base: batch_normalize_to_display(Speed raw scores)
Task->>Base: batch_normalize_to_display(Stability raw scores)
Task->>DB: DELETE FROM dws_member_spending_power_index WHERE site_id = %s
Task->>DB: INSERT INTO dws_member_spending_power_index (batch)
Task->>Base: save_percentile_history('SPI')
Task-->>Scheduler: {status, member_count, records_inserted}
```
## 正确性属性
*正确性属性Correctness Property是系统在所有合法执行路径上都应成立的行为特征——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
以下属性基于需求文档中的验收标准推导,每个属性都是可通过 hypothesis 属性测试验证的全称量化命题。子分计算函数(`compute_level``compute_speed``compute_stability``compute_spi_raw`)设计为纯静态方法,不依赖数据库,可直接用于属性测试。
### Property 1: SPI 总分非负性
*For any* 非负的 Level、Speed、Stability 子分和非负的权重参数,`compute_spi_raw(L, S, P, params)` 的返回值应为非负。
推导:`SPI_raw = w_L × L + w_S × S + w_P × P`,当所有子分 ≥ 0 且所有权重 ≥ 0 时,加权和必然 ≥ 0。
**Validates: Requirements 6.1, 10.1**
### Property 2: Level 子分关于消费金额单调非递减
*For any* 非负的特征值和参数,若仅增加 `spend_30``spend_90`(其他特征不变),`compute_level` 的返回值不应减少。
推导:`L` 中每一项形如 `w × ln(1 + x/M)``ln(1 + x/M)` 关于 `x` 单调递增(`x ≥ 0, M > 0`),权重 `w ≥ 0`,因此增加任一消费金额项只会使 `L` 增加或不变。
**Validates: Requirements 3.1, 10.2**
### Property 3: Speed 子分关于 spend_30 单调非递减
*For any* 非负的特征值和参数,若仅增加 `spend_30`(其他特征不变),`compute_speed` 的返回值不应减少。
推导:
- `V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`:关于 spend_30 单调递增
- `V_rel = ln((spend_30/30 + ε) / (spend_90/90 + ε))`spend_30 增加使分子增大,`max(0, V_rel)` 不减
- `V_ewma`:不依赖 spend_30不变
- 三项加权和中前两项不减,第三项不变,总和不减
**Validates: Requirements 4.1, 4.4, 10.3**
### Property 4: Stability 子分取值范围 [0, 1]
*For any* `active_weeks_90` 在 [0, 13] 范围内,`compute_stability` 的返回值应在 [0, 1] 范围内。
推导:`P = active_weeks_90 / 13`,当 `active_weeks_90 ∈ {0, 1, ..., 13}` 时,`P ∈ [0, 1]`
**Validates: Requirements 5.2, 5.4, 10.4**
### Property 5: Display Score 取值范围 [0, 10]
*For any* 非空的 raw_score 列表(所有值非负),经 `batch_normalize_to_display` 映射后,所有 display_score 应在 [0.00, 10.00] 范围内。
推导:`batch_normalize_to_display` 内部先 Winsorize 到 [P5, P95],再 MinMax 映射到 [0, 10],最后 `max(0, min(10, score))` 截断。
**Validates: Requirements 6.6, 10.5**
## 错误处理
| 场景 | 处理方式 |
|------|----------|
| 门店无消费/充值数据 | 返回 `{'status': 'skipped', 'reason': 'no_data'}`,不写入任何记录 |
| cfg_index_parameters 中缺少 SPI 参数 | 使用 `DEFAULT_PARAMS` 字典中的默认值,日志 WARNING |
| 金额压缩基数为 0 或负数 | 使用 DEFAULT_PARAMS 中的默认基数,日志 WARNING |
| orders_90 = 0 导致除零 | `avg_ticket_90 = spend_90 / max(orders_90, 1)`,分母至少为 1 |
| visit_days_30 = 0 导致除零 | `V_abs` 公式中 `max(visit_days_30, 1)`,分母至少为 1 |
| v_30 和 v_90 均为 0 导致 V_rel 除零 | 使用 `ε`speed_epsilon默认 1e-6防除零 |
| 所有会员 raw_score 相同 | `batch_normalize_to_display``max - min < ε` 时返回 5.0 |
| 数据库写入失败 | 事务回滚,抛出异常由调度器处理 |
| EWMA 分位历史不存在(首次执行) | 不平滑,直接使用当前分位点 |
## 测试策略
### 属性测试hypothesis
属性测试位于 `tests/` 目录Monorepo 级),使用 `hypothesis` 库。
每个属性测试对应设计文档中的一个 Property最少运行 100 次迭代。
测试文件:`tests/test_spi_properties.py`
```python
# Feature: spi-spending-power-index, Property 1: SPI 总分非负性
@given(
level=st.floats(min_value=0, max_value=100),
speed=st.floats(min_value=0, max_value=100),
stability=st.floats(min_value=0, max_value=1),
)
@settings(max_examples=200)
def test_spi_raw_non_negative(level, speed, stability):
params = SpendingPowerIndexTask.DEFAULT_PARAMS
result = SpendingPowerIndexTask.compute_spi_raw(level, speed, stability, params)
assert result >= 0
```
属性测试库:`hypothesis`(已在项目依赖中)
### 单元测试
单元测试位于 `apps/etl/connectors/feiqiu/tests/unit/`,使用 FakeDB/FakeAPI 工具。
重点覆盖:
- 边界情况:全零输入、单一极大值输入
- 配置回退:参数缺失时使用默认值
- 任务注册:验证 task_registry 中 SPI 任务的注册信息
- use_stability=0 时稳定性子分不参与计算
### 测试配置
- 属性测试:`cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
- 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
- 每个属性测试标注 `@settings(max_examples=200)`
- 每个属性测试注释引用设计文档 Property 编号

View File

@@ -0,0 +1,156 @@
# 需求文档SPI 消费力指数Spending Power Index
## 简介
SPISpending Power Index消费力指数是 NeoZQYY 指数体系的新增客户级指数用于评估会员在门店场景内的综合消费力层级。SPI 基于消费水平Level、消费速度Speed、消费稳定性Stability三个子分加权合成与现有 NCI/WBI/RS/OS/MS/ML 指数协同使用,为运营人员提供客户分层和资源分配依据。
## 术语表
- **SPI_Task**`SpendingPowerIndexTask`,负责计算 SPI 指数的 ETL 任务
- **BaseIndexTask**指数算法任务基类提供展示分映射Winsorize → 压缩 → MinMax 0-10 → EWMA 平滑)
- **cfg_index_parameters**`dws.cfg_index_parameters` 表,按 `index_type` + `param_name` 存储指数算法参数
- **dws_member_spending_power_index**SPI 指数结果表,存储会员级消费力评分
- **Raw_Score**:原始评分,由算法直接计算得出的未归一化分数
- **Display_Score**展示分Raw_Score 经 P5/P95 Winsorize → 可选压缩 → MinMax 映射到 [0, 10] 的归一化分数
- **Level_Sub_Score**:消费水平子分,衡量客户消费金额层级与客单水平
- **Speed_Sub_Score**:消费速度子分,衡量近期消费推进速度与节奏变化
- **Stability_Sub_Score**:消费稳定性子分,衡量消费行为的时间覆盖稳定性
- **Winsorize**:分位截断,将值限制在 [P5, P95] 范围内以消除极端值影响
- **EWMA**指数加权移动平均Exponential Weighted Moving Average用于平滑分位点避免批次间波动
- **log1p**`ln(1 + x)` 压缩变换,用于处理长尾分布
- **settle_type**结算类型1=台桌结账3=商城订单5=充值订单
- **task_registry**`orchestration/task_registry.py`ETL 任务注册表
- **delete-before-insert**:按门店全量刷新策略,先删除该门店所有旧记录再插入新记录
## 需求
### 需求 1SPI 结果表创建
**用户故事:** 作为 ETL 开发者,我需要创建 SPI 指数结果表,以便存储会员级消费力评分及中间特征。
#### 验收标准
1. THE SPI_Task SHALL 将结果写入 `dws.dws_member_spending_power_index` 表,主键为 `(site_id, member_id)`
2. THE dws_member_spending_power_index 表 SHALL 包含基础特征字段:`spend_30``spend_90``recharge_90``orders_30``orders_90``visit_days_30``visit_days_90``avg_ticket_90``active_weeks_90``daily_spend_ewma_90`
3. THE dws_member_spending_power_index 表 SHALL 包含子分字段:`score_level_raw``score_level_display``score_speed_raw``score_speed_display``score_stability_raw``score_stability_display`
4. THE dws_member_spending_power_index 表 SHALL 包含总分字段:`raw_score`SPI 原始分)和 `display_score`SPI 展示分numeric(5,2)
5. THE dws_member_spending_power_index 表 SHALL 包含元数据字段:`calc_time``created_at``updated_at`
6. THE 开发者 SHALL 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>_create_dws_member_spending_power_index.sql`,在测试库 test_etl_feiqiu 中执行建表
7. WHEN DDL 在测试库执行成功后THE 开发者 SHALL 运行 `python scripts/ops/gen_consolidated_ddl.py` 从测试库导出最新 DDL自动合并到 `docs/database/ddl/etl_feiqiu__dws.sql`
### 需求 2SPI 基础特征提取
**用户故事:** 作为 ETL 开发者,我需要从 DWD 层提取会员消费和充值数据,以便计算 SPI 所需的基础特征。
#### 验收标准
1. THE SPI_Task SHALL 从 `dwd.dwd_settlement_head` 提取近 90 天消费订单settle_type IN (1, 3)),聚合为客户级特征
2. THE SPI_Task SHALL 从 `dwd.dwd_recharge_order` 提取近 90 天充值订单settle_type = 5聚合为客户级充值特征
3. THE SPI_Task SHALL 计算以下基础特征:`spend_30`近30天消费总额`spend_90`近90天消费总额`recharge_90`近90天充值总额`orders_30`近30天消费笔数`orders_90`近90天消费笔数`visit_days_30`近30天消费日数按天去重`visit_days_90`近90天消费日数按天去重
4. THE SPI_Task SHALL 计算 `avg_ticket_90 = spend_90 / max(orders_90, 1)`
5. THE SPI_Task SHALL 计算 `active_weeks_90`近90天有消费的自然周数最多13周
6. THE SPI_Task SHALL 对近90天日消费序列计算 EWMA 得到 `daily_spend_ewma_90`,平滑系数从 cfg_index_parameters 读取
### 需求 3Level 子分计算
**用户故事:** 作为 ETL 开发者我需要实现消费水平Level子分算法以便衡量客户的消费金额层级。
#### 验收标准
1. THE SPI_Task SHALL 按以下公式计算 Level 子分:`L = w_s30 × ln(1 + spend_30/M30) + w_s90 × ln(1 + spend_90/M90) + w_ticket × ln(1 + avg_ticket_90/T0) + w_r90 × ln(1 + recharge_90/R90)`
2. THE SPI_Task SHALL 从 cfg_index_parameters 读取 Level 子分的权重参数(`w_level_spend_30``w_level_spend_90``w_level_ticket_90``w_level_recharge_90`)和金额压缩基数(`amount_base_spend_30``amount_base_spend_90``amount_base_ticket_90``amount_base_recharge_90`
3. WHEN 所有消费和充值金额均为 0 时THE SPI_Task SHALL 将 Level 子分 Raw 设为 0.0
### 需求 4Speed 子分计算
**用户故事:** 作为 ETL 开发者我需要实现消费速度Speed子分算法以便衡量客户近期消费推进速度。
#### 验收标准
1. THE SPI_Task SHALL 计算绝对速度:`V_abs = ln(1 + spend_30 / (max(visit_days_30, 1) × V0))`
2. THE SPI_Task SHALL 计算相对速度:`V_rel = ln((v_30 + ε) / (v_90 + ε))`,其中 `v_30 = spend_30 / 30``v_90 = spend_90 / 90``ε` 为防除零小量
3. THE SPI_Task SHALL 计算 EWMA 速度:`V_ewma = ln(1 + daily_spend_ewma_90 / E0)`
4. THE SPI_Task SHALL 按以下公式合成 Speed 子分:`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
5. THE SPI_Task SHALL 仅对加速(`V_rel > 0`)加分,不对减速直接扣分(通过 `max(0, V_rel)` 实现)
### 需求 5Stability 子分计算
**用户故事:** 作为 ETL 开发者我需要实现消费稳定性Stability子分算法以便识别稳定高消费与偶发冲高。
#### 验收标准
1. THE SPI_Task SHALL 使用近 90 天数据计算稳定性,窗口固定为 90 天
2. THE SPI_Task SHALL 按周覆盖率计算稳定性:`P = active_weeks_90 / 13`近90天共约13个自然周
3. WHEN cfg_index_parameters 中 `use_stability = 0`THE SPI_Task SHALL 将 Stability 子分权重视为 0跳过稳定性计算
4. THE Stability_Sub_Score SHALL 的取值范围为 [0, 1]
### 需求 6SPI 总分合成与展示分映射
**用户故事:** 作为 ETL 开发者,我需要将三个子分加权合成 SPI 总分并映射为展示分,以便业务人员直观理解客户消费力层级。
#### 验收标准
1. THE SPI_Task SHALL 按以下公式计算 SPI 总分:`SPI_raw = w_L × L + w_S × S + w_P × P`,默认权重 `w_L=0.60``w_S=0.30``w_P=0.10`
2. THE SPI_Task SHALL 复用 BaseIndexTask 的 `batch_normalize_to_display` 方法将 Raw_Score 映射为 Display_Score0-10 分)
3. THE SPI_Task SHALL 对 Level、Speed、Stability 三个子分分别独立映射为展示分0-10 分)
4. THE SPI_Task SHALL 支持通过 cfg_index_parameters 配置压缩模式(`compression_mode`0=无压缩1=log1p2=asinh
5. THE SPI_Task SHALL 支持通过 cfg_index_parameters 配置 EWMA 分位平滑(`use_smoothing``ewma_alpha`
6. THE Display_Score SHALL 保留 2 位小数,取值范围为 [0.00, 10.00]
### 需求 7SPI 配置参数管理
**用户故事:** 作为 ETL 开发者,我需要在 cfg_index_parameters 中注册 SPI 的全部默认参数,以便算法参数可配置、可追溯。
#### 验收标准
1. THE 种子数据脚本 SHALL 在 cfg_index_parameters 中插入 `index_type='SPI'` 的全部参数,包括窗口参数、金额压缩基数、子分权重、总分权重、映射与平滑参数
2. THE SPI_Task SHALL 通过 BaseIndexTask 的 `load_index_parameters(index_type='SPI')` 加载参数
3. IF cfg_index_parameters 中缺少某个 SPI 参数THEN THE SPI_Task SHALL 使用 DEFAULT_PARAMS 字典中定义的默认值
4. THE 种子数据脚本 SHALL 追加到 `db/etl_feiqiu/seeds/seed_index_parameters.sql`
### 需求 8金额压缩基数校准
**用户故事:** 作为 ETL 开发者,我需要提供金额压缩基数的校准机制,以便各门店的 SPI 评分能适配不同的消费水平分布。
#### 验收标准
1. THE SPI_Task SHALL 在首次执行或参数缺失时,支持从门店历史数据自动计算金额压缩基数的建议值
2. THE SPI_Task SHALL 以近 90 天消费数据的中位数作为各金额压缩基数的默认校准值:`amount_base_spend_30` 取近30天消费中位数、`amount_base_spend_90` 取近90天消费中位数、`amount_base_ticket_90` 取90天客单中位数、`amount_base_recharge_90` 取90天充值中位数、`amount_base_speed_abs` 取每消费日平均消费中位数、`amount_base_ewma_90` 取日消费 EWMA 中位数
3. IF cfg_index_parameters 中已存在对应的金额压缩基数参数THEN THE SPI_Task SHALL 优先使用配置表中的值而非自动校准值
4. THE SPI_Task SHALL 在日志中输出实际使用的金额压缩基数值,便于运营人员审查和手动调优
5. THE 种子数据脚本 SHALL 为金额压缩基数提供合理的初始默认值(基于典型台球门店消费水平)
### 需求 9SPI 任务注册与执行
**用户故事:** 作为 ETL 开发者,我需要将 SPI 任务注册到 task_registry 并实现完整的执行流程,以便通过调度器触发计算。
#### 验收标准
1. THE SPI_Task SHALL 以任务代码 `DWS_SPENDING_POWER_INDEX` 注册到 task_registry`layer="INDEX"``requires_db_config=False`
2. THE SPI_Task SHALL 声明依赖 `depends_on=["DWS_MEMBER_CONSUMPTION"]`
3. THE SPI_Task SHALL 采用 delete-before-insert 策略:先按 `site_id` 删除旧记录,再批量插入新记录
4. WHEN 门店无任何消费或充值数据时THE SPI_Task SHALL 返回 `{'status': 'skipped', 'reason': 'no_data'}` 并跳过计算
5. THE SPI_Task SHALL 在执行完成后保存分位点历史到 `dws_index_percentile_history`index_type='SPI'
### 需求 10SPI 算法正确性测试
**用户故事:** 作为 ETL 开发者我需要通过属性测试hypothesis验证 SPI 算法的正确性,以便确保计算逻辑符合 PRD 定义。
#### 验收标准
1. THE 属性测试 SHALL 验证:对于任意非负消费/充值金额SPI_raw 为非负值
2. THE 属性测试 SHALL 验证:在其他条件不变时,增加 spend_30 或 spend_90 不会导致 Level 子分下降(单调性)
3. THE 属性测试 SHALL 验证:在其他条件不变时,增加 spend_30 不会导致 Speed 子分下降(单调性)
4. THE 属性测试 SHALL 验证Stability 子分取值范围为 [0, 1]
5. THE 属性测试 SHALL 验证Display_Score 取值范围为 [0.00, 10.00]
6. THE 属性测试 SHALL 验证SPI 总分权重 `w_L + w_S + w_P` 之和为 1.0(权重归一化)
### 需求 11文档更新
**用户故事:** 作为 ETL 开发者,我需要更新相关文档,以便团队成员了解 SPI 的表结构、算法逻辑和使用方式。
#### 验收标准
1. THE 开发者 SHALL 编写数据库手册文档 `docs/database/BD_Manual_dws_member_spending_power_index.md`,包含表结构、字段说明、索引、验证 SQL
2. THE 开发者 SHALL 更新 ETL 任务文档 `apps/etl/connectors/feiqiu/docs/etl_tasks/index_tasks.md`,新增 SPI 任务章节
3. THE 文档 SHALL 包含 SPI 算法公式、参数清单、数据来源、计算流程说明

View File

@@ -0,0 +1,131 @@
# 实现计划SPI 消费力指数Spending Power Index
## 概述
基于设计文档,将 SPI 指数建设拆分为 DDL 建表 → 核心算法实现 → 任务注册与执行流程 → 配置种子数据 → 属性测试 → 文档更新 六个阶段,每个阶段增量构建并可验证。
## 任务
- [x] 1. 创建 DDL 与数据库表
- [x] 1.1 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>_create_dws_member_spending_power_index.sql`
- 创建 `dws.dws_member_spending_power_index` 表(含序列、唯一索引、查询索引)
- 字段定义参照设计文档数据模型章节
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 1.2 在测试库 test_etl_feiqiu 执行迁移脚本建表
- 通过 TEST_DB_DSN 连接测试库执行 SQL
- _Requirements: 1.6_
- [x] 1.3 运行 `gen_consolidated_ddl.py` 从测试库导出 DDL 合并到主 DDL
- 执行 `python scripts/ops/gen_consolidated_ddl.py`,验证 `docs/database/ddl/etl_feiqiu__dws.sql` 已包含新表
- _Requirements: 1.7_
- [x] 2. 实现 SPI 核心算法(纯函数)
- [x] 2.1 创建 `SPIMemberFeatures` 数据类和 `SpendingPowerIndexTask` 骨架
- 新建 `apps/etl/connectors/feiqiu/tasks/dws/index/spending_power_index_task.py`
- 定义 `SPIMemberFeatures` dataclass
- 定义 `SpendingPowerIndexTask` 类继承 `BaseIndexTask`,包含 `INDEX_TYPE``DEFAULT_PARAMS`、抽象方法实现
- _Requirements: 7.2, 7.3, 9.1_
- [x] 2.2 实现 `compute_level` 静态方法
- Level 子分公式:`L = w_s30 × ln(1 + spend_30/M30) + w_s90 × ln(1 + spend_90/M90) + w_ticket × ln(1 + avg_ticket_90/T0) + w_r90 × ln(1 + recharge_90/R90)`
- 全零输入返回 0.0
- _Requirements: 3.1, 3.2, 3.3_
- [x] 2.3 实现 `compute_speed` 静态方法
- 绝对速度 V_abs、相对速度 V_rel仅加速加分、EWMA 速度 V_ewma
- Speed 子分公式:`S = w_abs × V_abs + w_rel × max(0, V_rel) + w_ewma × V_ewma`
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
- [x] 2.4 实现 `compute_stability` 静态方法
- 周覆盖率:`P = active_weeks_90 / 13`
- 支持 `use_stability=0` 时返回 0.0
- _Requirements: 5.1, 5.2, 5.3, 5.4_
- [x] 2.5 实现 `compute_spi_raw` 静态方法
- 总分公式:`SPI_raw = w_L × L + w_S × S + w_P × P`
- _Requirements: 6.1_
- [x] 2.6 编写属性测试SPI 总分非负性
- **Property 1: SPI 总分非负性**
- **Validates: Requirements 6.1, 10.1**
- [x] 2.7 编写属性测试Level 子分单调性
- **Property 2: Level 子分关于消费金额单调非递减**
- **Validates: Requirements 3.1, 10.2**
- [x] 2.8 编写属性测试Speed 子分单调性
- **Property 3: Speed 子分关于 spend_30 单调非递减**
- **Validates: Requirements 4.1, 4.4, 10.3**
- [x] 2.9 编写属性测试Stability 子分范围
- **Property 4: Stability 子分取值范围 [0, 1]**
- **Validates: Requirements 5.2, 5.4, 10.4**
- [x] 2.10 编写属性测试Display Score 范围
- **Property 5: Display Score 取值范围 [0, 10]**
- **Validates: Requirements 6.6, 10.5**
- [x] 3. 检查点 - 确保核心算法测试通过
- 运行 `cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
- 确保所有属性测试通过,如有问题请询问用户。
- [x] 4. 实现数据提取与执行流程
- [x] 4.1 实现 `_extract_spending_features` 方法
-`dwd.dwd_settlement_head` 提取近 90 天消费订单,聚合为会员级特征
- 计算 spend_30/90、orders_30/90、visit_days_30/90、avg_ticket_90、active_weeks_90
- _Requirements: 2.1, 2.3, 2.4, 2.5_
- [x] 4.2 实现 `_extract_recharge_features` 方法
-`dwd.dwd_recharge_order` 提取近 90 天充值订单
- _Requirements: 2.2_
- [x] 4.3 实现 `_compute_daily_spend_ewma` 方法
- 对近 90 天日消费序列计算 EWMA
- _Requirements: 2.6_
- [x] 4.4 实现 `_calibrate_amount_bases` 方法
- 从门店数据计算中位数作为金额压缩基数校准值
- 配置表优先级高于自动校准值
- 日志输出实际使用的基数值
- _Requirements: 8.1, 8.2, 8.3, 8.4_
- [x] 4.5 实现 `execute` 方法(完整执行流程)
- 获取 site_id → 加载参数 → 提取特征 → 校准基数 → 计算子分 → 合成总分 → 归一化展示分 → 持久化
- delete-before-insert 策略
- 无数据时返回 skipped
- 保存分位点历史
- _Requirements: 6.2, 6.3, 6.4, 6.5, 6.6, 9.3, 9.4, 9.5_
- [x] 4.6 实现 `_save_spi_data` 方法
- 批量 INSERT 到 dws_member_spending_power_index
- _Requirements: 1.1_
- [x] 5. 任务注册与模块导出
- [x] 5.1 在 `tasks/dws/index/__init__.py` 中导出 `SpendingPowerIndexTask`
- 添加 import 和 __all__ 条目
- _Requirements: 9.1_
- [x] 5.2 在 `tasks/dws/__init__.py` 中导出 `SpendingPowerIndexTask`
- 添加 import 和 __all__ 条目
- _Requirements: 9.1_
- [x] 5.3 在 `orchestration/task_registry.py` 中注册 SPI 任务
- `default_registry.register("DWS_SPENDING_POWER_INDEX", SpendingPowerIndexTask, requires_db_config=False, layer="INDEX", depends_on=["DWS_MEMBER_CONSUMPTION"])`
- _Requirements: 9.1, 9.2_
- [-] 6. 配置种子数据
- [x] 6.1 在 `db/etl_feiqiu/seeds/seed_index_parameters.sql` 中追加 SPI 参数
- 插入 `index_type='SPI'` 的全部参数行(窗口、基数、权重、映射、稳定性)
- 金额压缩基数使用合理初始默认值
- _Requirements: 7.1, 7.4, 8.5_
- [~] 6.2 在测试库执行种子数据脚本
- 通过 TEST_DB_DSN 连接测试库执行 INSERT
- _Requirements: 7.1_
- [~] 7. 检查点 - 确保单元测试和集成验证通过
- 运行 `cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
- 确保所有测试通过,如有问题请询问用户。
- [ ] 8. 文档更新
- [~] 8.1 编写数据库手册 `docs/database/BD_Manual_dws_member_spending_power_index.md`
- 包含表结构、字段说明、索引、验证 SQL至少 3 条)、兼容性说明、回滚策略
- _Requirements: 11.1_
- [~] 8.2 更新 ETL 任务文档 `apps/etl/connectors/feiqiu/docs/etl_tasks/index_tasks.md`
- 新增 DWS_SPENDING_POWER_INDEX 章节,包含算法公式、参数清单、数据来源、计算流程
- 更新概述表格和继承体系图
- _Requirements: 11.2, 11.3_
- [~] 9. 最终检查点 - 确保所有测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_spi_properties.py -v`
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_spi_task.py -v`
- 确保所有测试通过,如有问题请询问用户。
## 备注
- 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯
- 检查点确保增量验证
- 属性测试验证全称正确性属性,单元测试验证具体示例和边界情况

View File

@@ -0,0 +1,48 @@
---
inclusion: fileMatch
fileMatchPattern: "**/.env*,**/scripts/**,**/export/**,**/EXPORT-PATHS*"
name: export-paths-full
description: 输出路径完整规范(目录结构、环境变量映射、检查清单)。读到 .env / scripts / export 文件时自动加载。
---
# 输出路径完整规范
## 目录结构与环境变量
```
export/
├── ETL-Connectors/feiqiu/
│ ├── JSON/ — EXPORT_ROOT / FETCH_ROOT
│ ├── LOGS/ — LOG_ROOT
│ └── REPORTS/ — ETL_REPORT_ROOT
├── SYSTEM/
│ ├── LOGS/ — SYSTEM_LOG_ROOT
│ ├── REPORTS/
│ │ ├── dataflow_analysis/ — SYSTEM_ANALYZE_ROOT
│ │ ├── field_audit/ — FIELD_AUDIT_ROOT
│ │ └── full_dataflow_doc/ — FULL_DATAFLOW_DOC_ROOT
│ └── CACHE/
│ └── api_samples/ — API_SAMPLE_CACHE_ROOT
└── BACKEND/
└── LOGS/ — BACKEND_LOG_ROOT
```
## 路径读取方式详细
- `scripts/ops/` 脚本:通过 `_env_paths.get_output_path("变量名")` 读取(内部自动 `load_dotenv`
- ETL 核心模块:通过 `env_parser.py``AppConfig``io.*` 配置节读取
- ETL 独立脚本:通过 `os.environ.get("ETL_REPORT_ROOT")` 读取,缺失时抛错
- 后端:通过 `os.environ.get("BACKEND_LOG_ROOT")` 读取
## 新增输出场景的检查清单
当任何操作需要写入文件时,按以下顺序确认:
1. 该输出是否已有对应的环境变量?→ 直接使用
2. 是否属于现有目录分类ETL/SYSTEM/BACKEND→ 使用对应父目录变量 + 子路径
3. 都不匹配?→ 在 `export/` 下新建合理子目录,新增环境变量,更新 `.env` / `.env.template` / `EXPORT-PATHS.md`
## 共享工具
- `scripts/ops/_env_paths.py`:提供 `get_output_path(env_var)` 函数,自动 `load_dotenv` + 读取 + 建目录 + 缺失报错
## 参考文档
- 完整目录说明:`docs/deployment/EXPORT-PATHS.md`
- 环境变量定义:根 `.env` 的"统一输出路径配置"节

View File

@@ -0,0 +1,16 @@
---
inclusion: always
---
# 输出路径规范(强制)
## 核心原则
所有文件输出必须写入 `export/` 统一目录结构,通过 `.env` 环境变量控制路径。禁止在 `export/` 体系外自行创建输出目录。
## 编码规则
1. 路径必须从 `.env` 读取,禁止硬编码任何目录结构(含绝对路径和相对路径)
2. 环境变量缺失时必须报错(`KeyError` / `RuntimeError`),禁止静默回退
3. 路径读取方式:`scripts/ops/``_env_paths.get_output_path()`ETL 核心用 `AppConfig.io.*`;独立脚本用 `os.environ.get()` + 显式报错
4. 新增输出类型:先在根 `.env` + `.env.template` 新增变量,再在代码中引用,同步更新 `docs/deployment/EXPORT-PATHS.md`
> 完整目录结构、环境变量映射表、新增场景检查清单见 `export-paths-full.md`fileMatch读到 `.env*` / `scripts/` / `export/` 文件时自动加载,也可 `#export-paths-full` 手动加载)。

View File

@@ -0,0 +1,18 @@
---
inclusion: fileMatch
fileMatchPattern: "**/tasks/**,**/models/**,**/loaders/**,**/scd/**,**/quality/**,**/business-rules/**"
name: product-full
description: 产品详细说明ETL 功能、指数算法、在线/离线模式)。读到 ETL 任务/模型/业务规则文件时自动加载。
---
# 产品详细说明
## ETL 功能
- 从上游 SaaS API 抽取运营数据(订单、支付、会员、助教、库存等)
- 原始数据落地 ODS保留源 payload 便于回溯
- 清洗装载至 DWD维度走 SCD2事实按时间增量
- 汇总至 DWS助教业绩、财务日报、会员分析、工资计算、自定义指数算法WBI/NCI/RS/OS/MS/ML/SPI
- 支持在线API 抓取和离线JSON 回放)两种模式
## 主要入口
详见 `tech.md` 常用命令节。

View File

@@ -1,22 +1,22 @@
---
inclusion: always
---
# 产品概述 # 产品概述
飞球 ETL 系统 (etl-billiards) — 面向台球门店业务的数据仓库 ETL 管线 NeoZQYY Monorepo — 面向台球门店业务的全栈数据平台
## 功能 ## 子系统
- 从上游 SaaS API 抽取运营数据(订单、支付、会员、助教、库存等) - ETL Connector`apps/etl/connectors/feiqiu/`):上游 SaaS API → ODS → DWD → DWS 三层处理
- 原始数据落地 **ODS**(操作数据存储层),保留源 payload 便于回溯 - FastAPI 后端(`apps/backend/`):业务 API 服务
- 清洗装载至 **DWD**(明细数据层),维度走 SCD2事实按时间增量 - 微信小程序(`apps/miniprogram/`C 端用户界面
- 汇总至 **DWS**数据服务层助教业绩、财务日报、会员分析、工资计算、自定义指数算法WBI/NCI/RS/OS/MS/ML - 管理后台(`apps/admin-web/`任务管理、调度配置、数据查看、ETL 监控
- 提供 **PySide6 桌面 GUI**,支持任务管理、调度配置 - MCP Server`apps/mcp-server/`AI 工具集成服务
- 支持在线API 抓取和离线JSON 回放)两种模式 - 共享包(`packages/shared/`):枚举、金额精度、时间工具
## 业务上下文 ## 业务上下文
- 单租户:一家台球门店(由 `STORE_ID` 标识) - 多门店隔离:`site_id` + RLS
- 核心实体:会员(客户)、助教(教练)、台桌、订单、支付、退款、团购套餐、库存 - 核心实体:会员、助教、台桌、订单、支付、退款、团购套餐、库存
- 领域语言中文为主代码注释、文档、UI 文案均为中文 - 领域语言中文;货币 CNY金额 numeric(2)
- 货币人民币CNY金额以 numeric(2) 存储
## 主要入口 > ETL 功能细节、指数算法等见 `product-full.md`fileMatch读到 ETL 任务/模型/业务规则文件时自动加载,也可 `#product-full` 手动加载)。
- CLI`python -m cli.main`(主入口)
- GUI`python -m gui.main`
- 批处理脚本:`run_etl.bat``run_gui.bat`(根目录)、`scripts/run_ods.bat`

View File

@@ -4,30 +4,31 @@ inclusion: always
# 项目结构Lite # 项目结构Lite
目标:在不注入大段目录树的前提下,让 Agent 快速理解“模块边界 + 高风险区” > 详细目录树、架构模式、文件归属规则展开说明见 `structure.md`(读到 pyproject.toml 或 agent 定义时自动加载,也可 `#structure-full` 手动加载)
## 关键模块边界(高风险路径 = 变更默认需要审计) ## 顶层目录
- `cli/`:命令行入口与参数/运行模式(影响一键增量、调度参数等) - `apps/etl/connectors/feiqiu/` — 飞球 Connector
- `config/`默认值、环境变量解析、AppConfig、调度任务配置影响运行时假设 - `apps/backend/` — FastAPI 后端
- `api/`:外部接口客户端与端点路由(影响抓取/契约/回放) - `apps/miniprogram/` — 微信小程序
- `database/`连接、DDL/schema、seed、migrations影响数据结构与回滚 - `apps/admin-web/` — 管理后台React + Vite + Ant Design
- `tasks/`ETL 任务ODS/DWD/DWS/指数/校验),业务规则主要落在这里 - `apps/mcp-server/` — MCP ServerAI 工具集成)
- `loaders/`upsert 与维度/事实装载(影响落库与冲突处理) - `packages/shared/` — 跨项目共享包
- `scd/`SCD2 处理(影响维度历史与生效区间 - `db/` — DDL / 迁移 / 种子(`etl_feiqiu/``zqyy_app/``fdw/`
- `orchestration/`:调度/注册/游标/运行记录(影响增量水位与可重复性 - `docs/` — 项目级文档 + `audit/`(统一审计落地点
- `models/`:解析与验证器(影响字段校验与转换) - `tests/` — Monorepo 级属性测试
- `utils/`日志、JSON 存储、窗口切分等通用工具(影响全局行为 - `scripts/` — 项目级运维脚本(`ops/``audit/``migrate/``server/`
- 根目录散文件:`.env*``pyproject.toml``requirements*``Makefile``README.md` 等(影响运行/依赖/发布)
## 架构要点(摘要 ## 高风险路径(变更需审计
- 任务模式:每个 ETL 任务继承 `BaseTask`Extract → Transform → Load并在 `orchestration/task_registry.py` 注册 - `apps/etl/connectors/feiqiu/` 下:`api/``cli/``config/``database/``loaders/``models/``orchestration/``scd/``tasks/``utils/``quality/`
- 加载器模式:每张目标表一个 Loader维度/事实分目录;核心是 `upsert()` 与冲突处理策略 - `apps/backend/app/``apps/admin-web/src/``apps/miniprogram/miniprogram/`
- 配置分层DEFAULTS → `.env` → CLI 覆盖;通过 `AppConfig.get("dotted.path")` 访问 - `packages/shared/``db/`、根目录散文件(`.env*``pyproject.toml`
- 管线流程:`FULL` / `FETCH_ONLY` / `INGEST_ONLY` 由 CLI 或环境变量控制
- 调度器:负责游标(水位)与运行记录(增量正确性关键)
## 编码/命名约定(摘要 ## 文件归属规则(强制
- 文件编码UTF-8 - 模块专属的 docs/tests/scripts → 放模块内部
- SQL纯 SQL非 ORM迁移脚本放 `database/migrations/`,推荐“日期前缀”命名 - 项目级/跨模块的 docs/tests/scripts → 放根目录
- 任务:大写蛇形命名(例如 `DWD_LOAD_FROM_ODS` - 审计产物统一写 `docs/audit/`,禁止写入子模块内部
- 日志:统一经由 `utils/logging_utils.py` - 一览表刷新:`python scripts/audit/gen_audit_dashboard.py`
## 编码/命名约定
- UTF-8、纯 SQL非 ORM、迁移脚本 `db/etl_feiqiu/migrations/`(日期前缀)
- 任务大写蛇形(`DWD_LOAD_FROM_ODS`)、日志经 `utils/logging_utils.py`

View File

@@ -1,124 +1,112 @@
--- ---
inclusion: auto inclusion: fileMatch
fileMatchPattern: "pyproject.toml,**/pyproject.toml,.kiro/steering/structure-lite.md,.kiro/agents/**"
name: structure-full name: structure-full
description: Full directory tree + architecture patterns. Load only for large refactors, module moves, or changes spanning multiple subsystems. description: 完整目录树 + 架构模式 + 文件归属规则展开。读到项目配置或 steering/agent 定义时自动加载。
--- ---
# 项目结构 # NeoZQYY Monorepo 完整结构
``` ```
NeoZQYY/ # Monorepo 工作区根目录C:\NeoZQYY NeoZQYY/
├── cli/ # CLI 入口main.py ├── apps/
├── config/ # 配置默认值、环境变量解析、AppConfig、调度任务配置 │ ├── etl/connectors/feiqiu/ # 飞球 Connector数据源连接器
└── scheduled_tasks.json │ ├── api/ # API 客户端HTTP、本地 JSON 回放、录制)
├── api/ # API 客户端HTTP、本地 JSON 回放、录制) │ │ ├── cli/ # CLI 入口
└── endpoint_routing.py # 端点路由映射 │ ├── config/ # 配置默认值、环境变量、AppConfig、调度任务
├── database/ # 数据库连接操作、DDL Schema、种子脚本、迁移 │ │ ├── database/ # 数据库连接操作Python 模块)
│ ├── migrations/ # 迁移脚本(纯 SQL日期前缀命名 │ ├── tasks/ # ETL 任务ods/dwd/dws/index/utility/verification
│ ├── schema_*.sql # DDL 定义 │ ├── loaders/ # 数据加载器ods/dimensions/facts
└── seed_*.sql # 种子数据 │ ├── scd/ # SCD2 处理器
├── tasks/ # ETL 任务实现(按数据层分目录) │ │ ├── orchestration/ # 调度器、任务注册、游标、运行记录
│ ├── base_task.py # BaseTask 基类,提供 Extract/Transform/Load 模板 │ ├── quality/ # 数据质量检查
│ ├── ods/ # ODS 层抓取任务16 个业务实体 + ods_tasks 工厂) │ ├── models/ # 解析器与验证器
│ ├── dwd/ # DWD 层装载任务base_dwd_task、维度/事实装载、质量检查 │ ├── utils/ # 工具函数日志、JSON 存储、窗口切分
│ ├── dws/ # DWS 汇总与指数任务 │ ├── docs/ # ETL 专属文档api-reference、business-rules、etl_tasks 等)
│ │ ── index/ # 指数计算任务(亲密度、新客转化、召回、关系、赢回 │ │ ── tests/ # ETL 测试unit/integration
│ ├── utility/ # 工具类任务Schema 初始化、手动入库、完整性检查、DWS 构建等 │ ├── scripts/ # ETL 专属脚本check/repair/rebuild/export/audit
│ └── verification/ # ETL 后置校验任务ODS/DWD/DWS/指数校验器) │ └── pyproject.toml
├── loaders/ # 数据加载器ODS、维度、事实 │ ├── backend/ # FastAPI 后端
│ ├── base_loader.py # BaseLoader 基类,定义 upsert 接口 │ ├── app/ # main.py, config.py, database.py, routers/, middleware/, schemas/
│ ├── ods/ # 通用 ODS 加载器 │ ├── tests/ # 后端测试
├── dimensions/ # SCD2 维度加载器(会员、助教、商品、台桌、套餐) │ └── pyproject.toml
── facts/ # 事实表加载器(订单、支付、退款、小票、充值等) ── miniprogram/ # 微信小程序
├── scd/ # SCD2缓慢变化维度处理器 ├── miniprogram/ # 小程序源码
├── orchestration/ # 调度器、任务注册表、游标管理、运行记录 │ │ └── doc/ # 小程序文档
│ ├── pipeline_runner.py # 管线运行器 │ ├── admin-web/ # 管理后台
│ ├── task_executor.py # 任务执行器 │ ├── src/ # 前端源码api/components/pages/store/types
├── task_registry.py # 任务注册表 │ └── src/__tests__/ # 前端测试
── scheduler.py # ETL 调度器 ── mcp-server/ # MCP ServerAI 工具集成)
├── cursor_manager.py # 游标(水位)管理 ├── server.py
└── run_tracker.py # 运行记录追踪 └── pyproject.toml
├── quality/ # 数据质量检查器(余额一致性、完整性 ├── packages/shared/ # 跨项目共享包enums, money, datetime_utils
│ └── integrity_service.py # 完整性检查服务 ├── db/
├── models/ # 解析器与验证器 │ ├── etl_feiqiu/
├── utils/ # 工具函数日志、JSON 存储、报告、窗口切分 ├── schemas/ # 六层 Schema DDLmeta/ods/dwd/core/dws/app
├── gui/ # PySide6 桌面 GUI ├── migrations/ # 迁移脚本(日期前缀)
│ ├── main_window.py │ ├── seeds/ # 种子数据
├── widgets/ # UI 面板与组件 │ └── scripts/ # 测试数据库脚本
│ ├── workers/ # 后台工作线程 │ ├── zqyy_app/schemas/ # 业务数据库 DDL
── models/ # GUI 数据模型(任务、调度) ── fdw/ # FDW 跨库映射
│ ├── utils/ # GUI 专用工具设置、CLI 构建器) ├── docs/ # 项目级文档
── resources/ # 样式表 ── audit/ # 统一审计落地点
├── scripts/ # 运维/工具脚本 │ │ ├── changes/ # 变更审计记录
│ ├── run_update.py # 一键增量更新入口ODS → DWD → DWS │ ├── prompt_logs/ # Prompt 日志
├── run_ods.bat # ODS 批处理入口 │ └── audit_dashboard.md # 审计一览表(自动生成)
│ ├── audit/ # 仓库审计脚本(扫描器、分析器、报告生成) │ ├── database/ # 全局数据库文档
│ ├── check/ # 数据检查脚本完整性、ODS 缺口、DWD 服务、内容哈希等) │ ├── architecture/ # 架构设计
│ ├── db_admin/ # 数据库管理脚本Excel 导入 │ ├── deployment/ # 部署文档EXPORT-PATHS.md、LAUNCH-CHECKLIST.md
│ ├── export/ # 数据导出脚本(指数、团购、亲密度、会员明细等) │ ├── prd/ # 产品需求
│ ├── rebuild/ # 数据重建脚本(全量 ODS→DWD 重建) │ ├── contracts/ # 数据契约
│ └── repair/ # 数据修复脚本回填、去重、hash 修复、维度修复、索引调优) │ └── ...
├── tests/ # 测试套件 ├── tests/ # Monorepo 级属性测试hypothesis
│ ├── unit/ # 单元测试FakeDB/FakeAPI无需真实数据库 ├── scripts/ # 项目级运维脚本
── integration/ # 集成测试(需要 TEST_DB_DSN 或真实数据库 ── audit/ # 审计工具gen_audit_dashboard.py
├── docs/ # 文档 ├── ops/ # 日常运维init_databases、clone_to_test_db 等)
│ ├── CHANGELOG.md # 项目级版本变更历史 │ ├── migrate/ # 一次性迁移脚本
── audit/ # 审计产物 ── server/ # 服务器部署脚本
├── changes/ # AI 逐次变更审计记录 ├── pyproject.toml # uv workspace 根配置4 成员)
│ │ ├── repo/ # 仓库审计报告(自动生成) ├── .env.template
│ │ ├── prompt_logs/ # Prompt 日志(每次 prompt 一个独立文件,按时间戳命名) └── README.md
│ │ └── audit_dashboard.md # 审计一览表(/audit 自动刷新)
│ ├── architecture/ # 架构设计文档(系统概览、数据流向)
│ ├── business-rules/ # 业务规则文档指数算法、DWS 口径、SCD2 规则)
│ ├── operations/ # 运维文档(环境搭建、调度配置、故障排查)
│ ├── database/ # 数据库文档统一目录ODS/DWD/DWS/ETL_Admin 表手册 + 概览索引)
│ │ ├── overview/ # 层级概览 / 速查索引
│ │ ├── ODS/ # ODS 层表手册main/mappings/changes
│ │ ├── DWD/ # DWD 层表手册main + Ex 扩展)
│ │ ├── DWS/ # DWS 层表手册
│ │ └── ETL_Admin/ # ETL 管理层表手册
│ ├── etl_tasks/ # ETL 任务文档
│ ├── requirements/ # 需求文档(功能需求、口径补充、指数 PRD
│ ├── reports/ # 分析报告
│ ├── api-reference/ # API 参考文档(标准化)
│ │ ├── api_registry.json # API 注册表25 个端点定义)
│ │ ├── summary/ # 每个 API 一个精简版 .md25 个)
│ │ ├── endpoints/ # 每个 API 一个详细版 .md 文档24 个)
│ │ └── samples/ # 最新响应样本JSON
├── reports/ # 质检输出JSON已 gitignore
├── export/ # JSON 落盘与日志(已 gitignore
├── logs/ # 运行日志(已 gitignore
└── .Deleted/ # 已归档/废弃文件(隐藏目录,已 gitignore
``` ```
## 架构模式 ## 架构模式
- 任务模式:继承 `BaseTask`Extract → Transform → Load`orchestration/task_registry.py` 注册
- 加载器模式:每张目标表一个 Loader`upsert()` + 冲突处理
- 配置分层DEFAULTS → `.env` → CLI 覆盖
- Flow通过 `--pipeline` 参数指定(如 `api_full`
- 多门店隔离:`site_id` + RLS`app` schema 视图层)
- 跨库访问:`zqyy_app` 通过 FDW 只读映射 `etl_feiqiu.app`
- **任务模式**:每个 ETL 任务继承 `BaseTask`Extract → Transform → Load 模板方法),在 `orchestration/task_registry.py` 中注册。 ## 文件归属规则(展开说明)
- **加载器模式**:每张目标表对应一个加载器,继承 `BaseLoader` 并实现 `upsert()` 方法。维度加载器在 `loaders/dimensions/`,事实加载器在 `loaders/facts/`
- **配置分层**`DEFAULTS` 字典 → `.env` 覆盖 → CLI 参数覆盖。通过 `AppConfig.get("dotted.path")` 访问。
- **管线流程**`FULL`(抓取 + 入库)、`FETCH_ONLY`(仅抓取)、`INGEST_ONLY`(仅入库)。由 `--pipeline-flow` CLI 参数或 `PIPELINE_FLOW` 环境变量控制。
- **调度器**`ETLScheduler` 编排任务执行,管理游标(水位),在 `etl_admin` Schema 中记录运行状态。
- **API 抽象**`APIClient`HTTP`LocalJsonClient`(离线回放)、`RecordingAPIClient`(抓取 + 落盘)共享相同接口,任务代码无需关心数据来源。
## 编码约定 ### 模块内部(各 APP / Connector 自治)
- 文件编码UTF-8文件头加 `# -*- coding: utf-8 -*-` 每个子模块的 `docs/``tests/``scripts/` 属于模块专属,只放该模块自身的内容。
- 日志格式:通过 `utils/logging_utils.py` 统一 禁止将项目级内容放入模块内部目录,也禁止将模块专属内容放到根目录。
- 任务代码:大写蛇形命名(如 `DWD_LOAD_FROM_ODS``DWS_ASSISTANT_DAILY`
- SQL 文件:纯 SQL不使用 ORM通过 `psycopg2` 执行
- 数据库操作:批量 upsert + 冲突处理,显式 commit/rollback
- 中文注释和文档字符串是正常且预期的
<!-- ### 项目级(根目录统管)
AI_CHANGELOG: - `docs/` — 跨模块文档架构设计、PRD、权限矩阵、数据契约、运维手册、路线图
- 日期: 2026-02-13 - `docs/audit/` — 统一审计落地点所有模块的变更记录、Prompt 日志、审计一览表)
- Prompt: P20260213-171500 — "继续"Task 3 API 文档全面重构续接 - `docs/database/` — 全局数据库文档(跨模块共享的 DB 视角
- 直接原因: 新增 docs/api-reference/ 目录替代旧 test-json-doc需在项目结构文档中反映 - `tests/` — Monorepo 级属性测试(守护项目结构/约定/跨模块一致性)
- 变更摘要: docs/ 树中新增 api-reference/(含 api_registry.json、endpoints/、samples/test-json-doc 标记为 [已废弃] - `scripts/` — 项目级运维脚本(数据库初始化、迁移、审计工具等)
- 风险与验证: 纯文档结构描述变更,无运行时影响;验证方式:对比实际目录 `ls docs/api-reference/` 确认一致
- 日期: 2026-02-14 ### 审计产物路径(硬约束)
- Prompt: P20260214-130000 — 25 个 API 文档归档至 summary/ + 字段分组修正 - 变更审计记录:`docs/audit/changes/<YYYY-MM-DD>__<slug>.md`
- 直接原因: 新增 summary/ 子目录存放精简版 API 文档,需在项目结构中反映 - 审计一览表:`docs/audit/audit_dashboard.md`(自动生成,勿手动编辑)
- 变更摘要: api-reference/ 树中新增 summary/25 个精简版 .mdendpoints/ 说明从"25 个"更正为"24 个" - Prompt 日志:`docs/audit/prompt_logs/`
- 风险与验证: 纯文档结构描述变更,无运行时影响 - 一览表生成脚本:`scripts/audit/gen_audit_dashboard.py`
--> - 禁止将审计产物写入子模块内部
### 速查表
| 判断标准 | 放置位置 |
|----------|----------|
| 只有本模块开发者需要看的文档 | 模块内 `docs/` |
| 跨模块对照或全局视角的文档 | 根 `docs/` |
| 只验证本模块逻辑的测试 | 模块内 `tests/` |
| 守护 monorepo 结构/约定的测试 | 根 `tests/` |
| 只操作本模块数据的脚本 | 模块内 `scripts/` |
| 运维/全局工具脚本 | 根 `scripts/` |
| 审计记录(任何模块的变更) | 根 `docs/audit/` |
| 数据库文档(全局 schema 视角) | 根 `docs/database/` |

View File

@@ -0,0 +1,27 @@
---
inclusion: fileMatch
fileMatchPattern: "**/pyproject.toml,**/config/**,**/migrations/**,**/.env*,**/seeds/**"
name: tech-full
description: 技术栈详细信息依赖清单、DDL 基线、测试工具、种子数据)。读到配置/迁移/依赖文件时自动加载。
---
# 技术栈详细信息
## 核心依赖
- ETL`psycopg2-binary``requests``python-dateutil``tzdata``python-dotenv``openpyxl`
- 后端:`fastapi``uvicorn[standard]``psycopg2-binary``python-dotenv`
- 管理后台:`React``Vite``Ant Design`(独立 pnpm 管理)
- 共享包:`neozqyy-shared`workspace 内部引用)
- 测试:`pytest``hypothesis`
## 数据库详细
- 业务库 `zqyy_app`(用户/RBAC/任务/审批),通过 FDW 只读映射 ETL 数据
- DDL 基线:`docs/database/ddl/`(从测试库自动导出,按 schema 分文件),重新生成:`python scripts/ops/gen_consolidated_ddl.py`
- 旧 DDL / 迁移脚本已归档至 `db/_archived/ddl_baseline_2026-02-22/`
- 种子数据:`db/etl_feiqiu/seeds/``db/zqyy_app/seeds/`
## 测试
- ETL 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit`
- ETL 集成测试:`TEST_DB_DSN="..." pytest tests/integration`
- Monorepo 属性测试:`pytest tests/ -v`根目录hypothesis
- 测试工具:`apps/etl/connectors/feiqiu/tests/unit/task_test_utils.py` 提供 FakeDB/FakeAPI

View File

@@ -1,60 +1,33 @@
---
inclusion: always
---
# 技术栈与构建 # 技术栈与构建
## 语言与运行时 ## 语言与运行时
- Python 3.10+(测试缓存中观察到 3.13 - Python 3.10+uv workspace`pyproject.toml` 声明 4 个成员etl/connectors/feiqiu、backend、mcp-server、shared
- 未提交虚拟环境;用户自行管理 - 管理后台React + Vite + Ant Design`apps/admin-web/`,独立 pnpm
## 核心依赖requirements.txt
- `psycopg2-binary>=2.9.0` — PostgreSQL 驱动
- `requests>=2.28.0` — 上游 API 的 HTTP 客户端
- `python-dateutil>=2.8.0` / `tzdata>=2023.0` — 日期解析与时区处理
- `python-dotenv``.env` 文件加载
- `openpyxl>=3.1.0` — Excel 导入导出DWS 数据)
- `PySide6>=6.5.0` — Qt 桌面 GUI 框架
- `flask>=2.3` — 可选 Web API
- `pyinstaller>=6.0.0` — 可选,仅打包 EXE 时需要
## 数据库 ## 数据库
- PostgreSQL(连接远程实例) - PostgreSQL 远程实例,四库:`etl_feiqiu` / `test_etl_feiqiu`ETL`zqyy_app` / `test_zqyy_app`(业务
- Schema`billiards_ods`ODS 原始数据)、`billiards_dwd`(明细数据)、`billiards_dws`(汇总数据)、`etl_admin`(调度/运行记录) - ETL 六层 Schemameta / ods / dwd / core / dws / app
- DDL 文件位于 `database/schema_*.sql`,种子脚本位于 `database/seed_*.sql` - DSN`PG_DSN`ETL`APP_DB_DSN`(业务),根 `.env` 定义
- 迁移脚本位于 `database/migrations/`(纯 SQL日期前缀命名
## 测试
- 框架:`pytest`(未固定在 requirements 中,需单独安装)
- 配置:`pytest.ini` 设置 `pythonpath = .`
- 结构:`tests/unit/`(基于 mock无需数据库`tests/integration/`(需要 `TEST_DB_DSN`
- 测试工具:`tests/unit/task_test_utils.py` 提供 FakeDB/FakeAPI 辅助类
## 常用命令 ## 常用命令
```bash ```bash
# 安装依赖 uv sync # 安装依赖
pip install -r requirements.txt cd apps/etl/connectors/feiqiu && python -m cli.main --dry-run --tasks DWD_LOAD_FROM_ODS
cd apps/backend && uvicorn app.main:app --reload
# 在线全流程 ETL抓取 + 入库) cd apps/etl/connectors/feiqiu && pytest tests/unit # ETL 单元测试
python -m cli.main --pg-dsn "$PG_DSN" --store-id "$STORE_ID" --api-token "$API_TOKEN" cd C:\NeoZQYY && pytest tests/ -v # 属性测试
# 运行指定任务
python -m cli.main --tasks INIT_ODS_SCHEMA,MANUAL_INGEST --data-source offline
# 试运行(不写库)
python -m cli.main --dry-run --tasks DWD_LOAD_FROM_ODS
# 单元测试
pytest tests/unit
# 集成测试(需要数据库)
TEST_DB_DSN="postgresql://..." pytest tests/integration
# 启动 GUI
python -m gui.main
``` ```
## 配置体系 ## 配置体系
- 分层叠加:`config/defaults.py` < `.env` / 环境变量 < CLI 参数 - 分层叠加:`.env` < 应用 `.env.local` < 环境变量 < CLI 参数
- 配置类:`config.settings.AppConfig`,支持点号路径访问(`config.get("db.dsn")` - ETL 配置类:`apps/etl/connectors/feiqiu/config/settings.py``AppConfig`
- 敏感值DSN、API Token放在 `.env` 中,禁止提交
## 打包 ## 脚本执行规范
- 已移除 EXE 打包支持(`build_exe.py``setup.py` 已归档至 `.Deleted/` - 复杂操作优先写 Python 脚本再执行,避免 PowerShell 复杂逻辑
- 直接通过 `python -m cli.main``python -m gui.main` 运行 - 一次性运维脚本放 `scripts/ops/`,模块专属脚本放模块内 `scripts/`
> 核心依赖清单、DDL 基线、种子数据等详细信息见 `tech-full.md`fileMatch读到 pyproject.toml / 配置 / 迁移文件时自动加载,也可 `#tech-full` 手动加载)。

View File

@@ -0,0 +1,25 @@
---
inclusion: always
---
# 测试与验证环境规范(强制)
## 核心原则
AI 执行测试、验证、调试、一次性脚本时,必须使用与正式运行一致的参数环境。禁止因"只是测试"而省略配置、跳过 `.env` 加载、或使用不完整的参数集。
## 具体要求
1. **环境变量必须完整加载**:测试脚本必须通过 `load_dotenv` 或等效方式加载根 `.env`(及模块 `.env`),不得假设"测试不需要路径配置"
2. **禁止空值回退到意外默认**:如果某个必需参数(如 `FETCH_ROOT``EXPORT_ROOT``PG_DSN`)未加载到,应立即报错终止,而非静默使用空字符串或其他配置项的值
3. **cwd 必须与正式运行一致**ETL CLI 测试的 `cwd` 应为 `apps/etl/connectors/feiqiu/`;后端测试的 `cwd` 应为 `apps/backend/`
4. **不得为测试单独构造简化配置**:除非用户明确要求隔离测试环境,否则一律使用 `AppConfig.load()` 正常流程加载配置
5. **数据库连接使用测试库**:测试涉及数据库时,优先使用 `test_etl_feiqiu` / `test_zqyy_app`(通过 `TEST_DB_DSN` 环境变量),而非正式库
## 例外情况
以下场景允许偏离:
- 用户明确指定使用特定参数或简化环境
- 纯单元测试使用 FakeDB/FakeAPI`tests/unit/task_test_utils.py`),不涉及真实路径或连接
- `--dry-run` 模式下的 CLI 验证(但路径配置仍需完整)
## 背景
此规则源于实际事故:测试时 `FETCH_ROOT` 未正确加载,`or` 链回退到空字符串,导致时区值 `Asia/Shanghai` 被误用为文件路径,在项目目录下创建了垃圾目录 `Asia/Shanghai/ODS_JSON_ARCHIVE/`

View File

@@ -4,5 +4,6 @@
"path": "." "path": "."
} }
], ],
"settings": {} "settings": {
}
} }

View File

@@ -1,16 +1,15 @@
# NeoZQYY Monorepo # NeoZQYY Monorepo
台球门店运营助手一体化平台,整合 ETL 数据管线、微信小程序后端、小程序前端管理后台与桌面 GUI 台球门店运营助手一体化平台,整合 ETL 数据 Connector、微信小程序后端、小程序前端管理后台。
## 项目结构 ## 项目结构
| 目录 | 说明 | | 目录 | 说明 |
|------|------| |------|------|
| apps/etl/pipelines/feiqiu/ | ETL 数据管线(飞球平台 | | apps/etl/connectors/feiqiu/ | 飞球 Connector数据源连接器 |
| apps/backend/ | FastAPI 后端(小程序 API | | apps/backend/ | FastAPI 后端(小程序 API |
| apps/miniprogram/ | 微信小程序Donut + TDesign | | apps/miniprogram/ | 微信小程序Donut + TDesign |
| apps/admin-web/ | 管理后台(规划中 | | apps/admin-web/ | 管理后台(React + Vite + Ant Design |
| gui/ | PySide6 桌面 GUI过渡期 |
| packages/shared/ | 跨项目共享包(枚举、金额精度、时间工具) | | packages/shared/ | 跨项目共享包(枚举、金额精度、时间工具) |
| db/ | 数据库 DDL、迁移、种子脚本 | | db/ | 数据库 DDL、迁移、种子脚本 |
| docs/ | 文档PRD、契约、权限矩阵、架构等 | | docs/ | 文档PRD、契约、权限矩阵、架构等 |
@@ -26,7 +25,7 @@
uv sync uv sync
# 运行 ETL # 运行 ETL
cd apps/etl/pipelines/feiqiu cd apps/etl/connectors/feiqiu
python -m cli.main --pg-dsn "$PG_DSN" --store-id "$STORE_ID" --api-token "$API_TOKEN" python -m cli.main --pg-dsn "$PG_DSN" --store-id "$STORE_ID" --api-token "$API_TOKEN"
# 启动后端 API # 启动后端 API
@@ -34,7 +33,7 @@ cd apps/backend
uvicorn app.main:app --reload uvicorn app.main:app --reload
# 运行 ETL 单元测试 # 运行 ETL 单元测试
cd apps/etl/pipelines/feiqiu cd apps/etl/connectors/feiqiu
pytest tests/unit pytest tests/unit
``` ```
@@ -49,5 +48,5 @@ pytest tests/unit
- Python 3.10+, uv workspace - Python 3.10+, uv workspace
- PostgreSQL六层 Schemameta/ods/dwd/core/dws/app - PostgreSQL六层 Schemameta/ods/dwd/core/dws/app
- FastAPI + uvicorn - FastAPI + uvicorn
- PySide6桌面 GUI - React + Vite + Ant Design管理后台
- Donut + TDesign微信小程序 - Donut + TDesign微信小程序

View File

@@ -1,17 +1,18 @@
# apps/ # apps/
## 作用说明 ## 作用说明
应用项目顶层目录,存放所有可独立部署/运行的子项目。当前包含 ETL 数据管线、FastAPI 后端、微信小程序前端,以及预留的管理后台。 应用项目顶层目录,存放所有可独立部署/运行的子项目。当前包含 ETL Connector、FastAPI 后端、微信小程序前端,以及预留的管理后台。
## 内部结构 ## 内部结构
- `etl/pipelines/feiqiu/` — 飞球平台 ETL 管线(抽取→清洗→汇总全流程) - `etl/pipelines/feiqiu/` — 飞球 Connector数据源连接器抽取→清洗→汇总全流程)
- `backend/` — FastAPI 后端(小程序 API、权限、审批 - `backend/` — FastAPI 后端(小程序 API、权限、审批
- `miniprogram/` — 微信小程序前端Donut + TDesign - `miniprogram/` — 微信小程序前端Donut + TDesign
- `admin-web/` — 管理后台(预留,暂未实施) - `admin-web/` — 管理后台(预留,暂未实施)
- `mcp-server/` — MCP Server为百炼 AI 应用提供 PostgreSQL 只读查询)
## Roadmap
## Roadmap
- 新增更多数据源管线时,在 `etl/pipelines/` 下按平台名创建子目录
- `admin-web/` 待产品需求确认后启动 - 新增更多 Connector 时,在 `etl/pipelines/` 下按平台名创建子目录
- `admin-web/` 待产品需求确认后启动

View File

@@ -0,0 +1,8 @@
{
"hash": "75f75ae2",
"configHash": "3c6579c7",
"lockfileHash": "4e1d8c76",
"browserHash": "dc64490c",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

Some files were not shown because too many files have changed in this diff Show More