Compare commits

...

9 Commits

Author SHA1 Message Date
Neo
075caf067f 小程序迁移 静态页面完成!!! 2026-03-18 05:14:35 +08:00
Neo
72bb11b34f 1 2026-03-15 10:15:02 +08:00
Neo
2dd217522c 微信小程序页面迁移校验之前 P5任务处理之前 2026-03-09 01:24:20 +08:00
Neo
6e20987d2f 微信小程序页面迁移校验之前 P5任务处理之前 2026-03-09 01:19:21 +08:00
Neo
263bf96035 微信小程序页面迁移校验之前 P5任务处理之前 2026-03-09 01:18:47 +08:00
Neo
b25308c3f4 feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系
## P1 数据库基础
- zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu
- etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表
- 清理 assistant_abolish 残留数据

## P2 ETL/DWS 扩展
- 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution)
- 新增 assistant_order_contribution_task 任务及 RLS 视图
- member_consumption 增加充值字段、assistant_daily 增加处罚字段
- 更新 ODS/DWD/DWS 任务文档及业务规则文档
- 更新 consistency_checker、flow_runner、task_registry 等核心模块

## P3 小程序鉴权系统
- 新增 xcx_auth 路由/schema(微信登录 + JWT)
- 新增 wechat/role/matching/application 服务层
- zqyy_app 鉴权表迁移 + 角色权限种子数据
- auth/dependencies.py 支持小程序 JWT 鉴权

## 文档与审计
- 新增 DOCUMENTATION-MAP 文档导航
- 新增 7 份 BD_Manual 数据库变更文档
- 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth)
- 新增全栈集成审计记录、部署检查清单更新
- 新增 BACKLOG 路线图、FDW→Core 迁移计划

## Kiro 工程化
- 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务)
- 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan)
- 新增 6 个 Hook(合规检查/会话日志/提交审计等)
- 新增 doc-map steering 文件

## 运维与测试
- 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告
- 新增属性测试:test_dws_contribution / test_auth_system
- 清理过期 export 报告文件
- 更新 .gitignore 排除规则
2026-02-26 08:03:53 +08:00
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
2552 changed files with 333968 additions and 38909 deletions

149
.env Normal file
View File

@@ -0,0 +1,149 @@
# ==============================================================================
# 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
# ------------------------------------------------------------------------------
# 营业日切点(统计日/周/月分割小时,默认 8 即 08:00
# ------------------------------------------------------------------------------
BUSINESS_DAY_START_HOUR=8
# ==============================================================================
# 统一输出路径配置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_MODEL=qwen-plus
BAILIAN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
BAILIAN_TEST_APP_ID=541edb3d5fcd4c18b13cbad81bb5fb9d
# CHANGE 2026-03-05 | 8 个百炼 AI 应用 ID从百炼平台获取2026-03-05 更新)
BAILIAN_APP_ID_1_CHAT=979dabe6f22a43989632b8c662cac97c
BAILIAN_APP_ID_2_FINANCE=1dcdb5f39c3040b6af8ef79215b9b051
BAILIAN_APP_ID_3_CLUE=708bf45439cd48c7ab9a514d03482890
BAILIAN_APP_ID_4_ANALYSIS=ea7b1c374f574b9a925a2fb5789a9b90
BAILIAN_APP_ID_5_TACTICS=46f54e6053df4bb0b83be29366025cf6
BAILIAN_APP_ID_6_NOTE=025bb344146b4e4e8be30c444adab3b4
BAILIAN_APP_ID_7_CUSTOMER=df35e06991b24d49971c03c6428a9c87
BAILIAN_APP_ID_8_CONSOLIDATE=407dfb89283b4196934eec5fefe3ebc2
# ------------------------------------------------------------------------------
# 微信小程序
# CHANGE 2026-02-27 | 开发模式启用 mock 登录,跳过微信 code2Session
# 生产环境需设置真实 WX_APPID / WX_SECRET 并关闭 WX_DEV_MODE
# ------------------------------------------------------------------------------
WX_APPID=wx7c07793d82732921
WX_SECRET=wx7c07793d82732921
WX_DEV_MODE=true
# ------------------------------------------------------------------------------
# 管道限流配置RateLimiter 请求间隔)
# CHANGE 2026-03-06 | 从默认 5-20s 降至 0.1-2sODS_PAYMENT 46请求从582s→~48s
# ------------------------------------------------------------------------------
PIPELINE_RATE_MIN=0.1
PIPELINE_RATE_MAX=2.0
# ------------------------------------------------------------------------------
# 后端运维面板路径配置
# CHANGE 2026-03-06 | 显式锁定,避免 __file__ 推算在不同部署环境指向错误路径
# ------------------------------------------------------------------------------
OPS_SERVER_BASE=C:/NeoZQYY
ETL_PROJECT_PATH=C:/NeoZQYY/apps/etl/connectors/feiqiu
ETL_PYTHON_EXECUTABLE=C:/NeoZQYY/.venv/Scripts/python.exe

View File

@@ -1,30 +1,343 @@
# ============================================================ # ==============================================================================
# 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
# ---------- 门店标识 ---------- # ------------------------------------------------------------------------------
# 营业日切点(统计日/周/月分割小时,默认 8 即 08:00
# 日统计 = 当日 08:00 ~ 次日 08:00
# 月统计 = 当月1日 08:00 ~ 次月1日 08:00
# 周统计 = 周一 08:00 ~ 次周一 08:00
# ------------------------------------------------------------------------------
BUSINESS_DAY_START_HOUR=8
# ==============================================================================
# 统一输出路径配置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_MODEL=qwen-plus
BAILIAN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
BAILIAN_TEST_APP_ID=
# 8 个百炼 AI 应用 ID从百炼平台获取
# 应用 1通用对话 | 应用 2财务洞察 | 应用 3客户数据维客线索分析
# 应用 4关系分析/任务建议 | 应用 5话术参考 | 应用 6备注分析
# 应用 7客户分析 | 应用 8维客线索整理
BAILIAN_APP_ID_1_CHAT=
BAILIAN_APP_ID_2_FINANCE=
BAILIAN_APP_ID_3_CLUE=
BAILIAN_APP_ID_4_ANALYSIS=
BAILIAN_APP_ID_5_TACTICS=
BAILIAN_APP_ID_6_NOTE=
BAILIAN_APP_ID_7_CUSTOMER=
BAILIAN_APP_ID_8_CONSOLIDATE=
# ------------------------------------------------------------------------------
# 管道限流配置RateLimiter 请求间隔,秒)
# 默认 0.1-2.0s,防止上游风控同时避免过度等待
# ------------------------------------------------------------------------------
PIPELINE_RATE_MIN=0.1
PIPELINE_RATE_MAX=2.0
# ╔════════════════════════════════════════════════════════════════════════════╗
# ║ [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,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_APPID / WX_SECRET注意无下划线分隔
# WX_DEV_MODE=true 时启用 mock 登录端点,跳过微信 code2Session
# ------------------------------------------------------------------------------
# WX_APPID=
# WX_SECRET=
# WX_DEV_MODE=false
# ------------------------------------------------------------------------------
# CORS逗号分隔
# ------------------------------------------------------------------------------
# CORS_ORIGINS=http://localhost:5173
# ------------------------------------------------------------------------------
# ETL 项目路径(子进程 cwd
# CHANGE 2026-03-06 | 必须显式设置,禁止依赖 __file__ 推算
# ------------------------------------------------------------------------------
ETL_PROJECT_PATH=C:/NeoZQYY/apps/etl/connectors/feiqiu
# ------------------------------------------------------------------------------
# ETL 子进程 Python 可执行路径
# CHANGE 2026-03-06 | 必须显式设置,避免 PATH 歧义
# ------------------------------------------------------------------------------
ETL_PYTHON_EXECUTABLE=C:/NeoZQYY/.venv/Scripts/python.exe
# ------------------------------------------------------------------------------
# 运维面板服务器根目录
# CHANGE 2026-03-06 | 必须显式设置,消除 __file__ 推算风险
# ------------------------------------------------------------------------------
OPS_SERVER_BASE=C:/NeoZQYY

82
.gitignore vendored
View File

@@ -1,23 +1,64 @@
# ===== 临时与缓存 ===== # ===== 临时与缓存 =====
tmp/ # tmp/
__pycache__/ __pycache__/
*.pyc *.pyc
*.py[cod]
*$py.class
*.so
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
pytest-cache-files-*/
logs/ logs/
*.log
*.jsonl
# ===== 审计文件 =====
docs/audit/
# ===== 运行时产出 =====
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/
# ===== 测试 =====
tests/
# ===== infra 敏感文件 ===== # ===== infra 敏感文件 =====
infra/**/*.key infra/**/*.key
@@ -27,8 +68,27 @@ 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
.kiro/.git_snapshot.json
.kiro/.file_baseline.json
.kiro/.compliance_state.json
.kiro/.audit_context.json
# ===== 运维脚本运行时状态 =====
scripts/ops/.monitor_token
# ===== Kiro Powers含敏感 DSN =====
powers/

View File

@@ -1,4 +0,0 @@
{
"at": "2026-02-15T06:00:50.2089232+08:00",
"prompt_id": "P20260215-060050"
}

View File

@@ -4,34 +4,183 @@ description: Run post-change audit + docs sync for NeoZQYY Monorepo; write audit
tools: ["read", "write", "shell"] tools: ["read", "write", "shell"]
--- ---
你是专职审计收口/后处理写入子代理。你的执行必须尽量不依赖主对话上下文优先使用本地仓库事实git、文件内容、prompt_log完成审计落盘。 你是专职"审计收口/后处理写入"子代理。
## 输入来源(不要询问主代理) ## 核心原则:从预构建上下文工作,禁止全盘扫描
- 通过 `git status --porcelain``git diff` 获取本次未提交变更
- 通过 `docs/audit/prompt_log.md``.kiro/.last_prompt_id.json` 获取最新 Prompt-ID 与 prompt 原文(用于溯源)
- 通过项目实际文件内容判断是否“逻辑改动”
## 何时需要做“重型后处理” 你的唯一输入是 `.kiro/state/.audit_context.json`(由 `build_audit_context.py` 预构建)。
满足任一即执行审计收口(否则只输出“无逻辑改动/无需审计”,并清除待审计标记): 该文件已包含所有你需要的信息
- 改动文件命中 ETL 管线高风险路径:`apps/etl/pipelines/feiqiu/` 下的 `api/``cli/``config/``database/``loaders/``models/``orchestration/``scd/``tasks/``utils/``quality/`
- 改动文件命中后端 API`apps/backend/app/`
- 改动文件命中共享包:`packages/shared/`
- 改动文件命中数据库定义:`db/` 下的 DDL / migration / seed 文件
- 根目录散文件(`pyproject.toml``.env*` 等)
- 发生 DB schema / migration / *.sql / *.prisma 变更
- 明确属于业务口径/资金精度舍入/API 契约/鉴权权限/调度游标 等逻辑改变
## 执行策略(尽量少写、但必须完整) | 字段 | 来源 | 内容 |
1) 判断是否逻辑改动 |------|------|------|
2) 若是逻辑改动: | `changed_files` | audit-flagger | 全部变更文件列表 |
- 按需调用 skill | `high_risk_files` | audit-flagger | 高风险文件子集 |
- steering-readme-maintainer同步 product/tech/structure-lite/README | `reasons` | audit-flagger | 风险分类标签 |
- change-annotation-audit写 docs/audit/changes/... + AI_CHANGELOG + CHANGE 注释) | `high_risk_diff` | git diff | 高风险文件的 diff已截断 |
- bd-manual-db-docs仅当 DB schema 变更) | `diff_stat` | git diff --stat | 变更统计摘要 |
3) 完成后把 `.kiro/.audit_state.json``audit_required` 置为 false或清空 reasons/changed_files/last_reminded_at | `compliance.code_without_docs` | compliance-prescan | 缺少文档同步的代码文件及其应更新的文档 |
| `compliance.new_migration_sql` | compliance-prescan | 新增迁移 SQL 列表 |
| `compliance.has_bd_manual` | compliance-prescan | 是否已有 BD_Manual 文档 |
| `compliance.has_ddl_baseline` | compliance-prescan | 是否已更新 DDL 基线 |
| `compliance.api_changed` | compliance-prescan | 是否有接口相关文件变更 |
| `compliance.openapi_spec_stale` | compliance-prescan | OpenAPI spec 是否需要重新导出 |
| `session_diff` | agent-on-stop (file baseline) | 本次对话期间的精确变更:`added`/`modified`/`deleted` |
| `prompt_id` / `latest_prompt_log` | prompt-audit-log | Prompt-ID 与原文(溯源用) |
**禁止操作**
- ❌ 运行 `git status --porcelain`(已有 `changed_files`
- ❌ 运行 `git diff` 全量(已有 `high_risk_diff` + `diff_stat`
- ❌ 遍历目录寻找变更文件(已有分类好的列表)
- ❌ 运行 `change_compliance_prescan.py`(已有 `compliance` 数据)
**允许操作**
- ✅ 读取具体文件内容(如需更新某个 README 时读取其当前内容)
- ✅ 对单个文件运行 `git diff HEAD -- <file>`(仅当 context 中 diff 被截断时)
- ✅ 连接测试库验证迁移执行状态(仅当 `new_migration_sql` 非空时)
## 审计产物路径(统一根目录)
- 变更审计记录:`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/`,不要写入子模块内部
## 何时需要做"重型后处理"
根据 `audit_context.json` 中的 `audit_required``reasons` 判断:
- `audit_required: true` → 执行完整审计流程
- `audit_required: false` → 输出"无需审计",清除标记,退出
## 执行策略(从 context 驱动,不做冗余扫描)
### 步骤 1读取上下文
读取 `.kiro/state/.audit_context.json`,提取关键字段。
### 步骤 1b读取 Session 索引
读取 `docs/audit/session_logs/_session_index.json`,按 `startTime` 找到与 `audit_context.json``prompt_at` 最接近的 entry`is_sub` 的主对话)。提取:
- `description`:作为审计记录的「操作摘要」(比从 diff 推断更准确、更完整)
- `summary.files_modified` / `summary.files_created`:交叉验证 `session_diff`
- executionId 前 8 位:作为 `session_id` 写入审计记录,建立双向链接
- `summary.sub_agents`:记录本次对话调用了哪些子代理
- `summary.errors`:标注执行中的异常
若索引不存在或无匹配 entry跳过此步骤不影响后续流程。
### 步骤 2审计落盘按需调用 skill
根据 `reasons` 判断需要哪些 skill
-`dir:backend` / `dir:etl` / `dir:shared` 等 → 调用 `steering-readme-maintainer`
- 含任意高风险标签 → 调用 `change-annotation-audit`(写 docs/audit/changes/ + AI_CHANGELOG + CHANGE 注释)
-`db-schema-change` → 调用 `bd-manual-db-docs`,并执行 DB 文档全量对账(见步骤 2b
所有审计记录中涉及日期时间的字段,必须精确到秒(格式:`YYYY-MM-DD HH:MM:SS`,时区 Asia/Shanghai。包括但不限于审计记录头部的"日期"、AI_CHANGELOG 条目的时间戳、CHANGE 标记注释中的日期。
`session_diff` 中有 `added``deleted` 文件,在审计记录中增加「本次对话文件变更」段落,分别列出新增和删除的文件。
若步骤 1b 成功获取了 Session 信息,在审计记录头部元数据中增加:
- `session_id`executionId 前 8 位(如 `f29acdea`
- `操作摘要`Session 索引中的 `description`LLM 生成的操作摘要)
- `session_path`Session 日志文件的相对路径(`output_dir` 字段值)
审计记录头部模板:
```markdown
# 变更审计记录:<标题>
| 字段 | 值 |
|------|-----|
| 日期 | YYYY-MM-DD HH:MM:SS |
| Prompt-ID | <从 audit_context> |
| Session-ID | <executionId 前 8 位> |
| Session 路径 | <output_dir 相对路径> |
## 操作摘要
<Session 索引中的 description或从 diff 推断的摘要>
```
### 步骤 2bDB 文档全量对账(当 reasons 含 db-schema-change 时)
`reasons``db-schema-change` 时,除了调用 `bd-manual-db-docs` skill 处理本次变更外,还必须执行全量对账:
1. 连接测试库(使用 pg power 的 `pg-etl-test` / `pg-app-test`),查询 `information_schema.tables``information_schema.columns` 获取所有表和字段的实际结构
2. 扫描 `docs/database/` 下现有文档,逐表对比:
- 文档中缺失的表 → 新建表结构文档
- 文档中字段与实际不一致类型、nullable、默认值等→ 更新文档
- 文档中存在但数据库已删除的表 → 在文档中标注已废弃
3. 输出对账摘要到审计记录中,列出:新增文档数、更新文档数、废弃标注数
4. 所有文档输出到 `docs/database/`,遵循现有目录结构和模板格式
注意全量对账使用测试库TEST_DB_DSN禁止连接正式库。
### 步骤 3文档校对补齐
遍历 `compliance.code_without_docs`,对每个缺失项:
- 读取对应代码文件当前内容(不需要 diff直接读文件
- 更新对应文档:
| 代码路径前缀 | 应同步更新的文档 |
|---|---|
| `apps/backend/app/routers/` | `apps/backend/docs/API-REFERENCE.md` + `docs/contracts/openapi/backend-api.json` |
| `apps/backend/app/services/` | `apps/backend/docs/API-REFERENCE.md` + `apps/backend/README.md` |
| `apps/backend/app/auth/` | `apps/backend/docs/API-REFERENCE.md` + `apps/backend/README.md` + `docs/contracts/openapi/backend-api.json` |
| `apps/backend/app/schemas/` | `docs/contracts/openapi/backend-api.json` |
| `apps/etl/connectors/feiqiu/tasks/` | `apps/etl/connectors/feiqiu/docs/etl_tasks/` |
| `apps/etl/connectors/feiqiu/loaders/` | `apps/etl/connectors/feiqiu/docs/etl_tasks/` |
| `apps/etl/connectors/feiqiu/scd/` | `apps/etl/connectors/feiqiu/docs/business-rules/scd2_rules.md` |
| `apps/etl/connectors/feiqiu/orchestration/` | `apps/etl/connectors/feiqiu/docs/architecture/` |
| `apps/admin-web/src/` | `apps/admin-web/README.md` |
| `apps/miniprogram/` | `apps/miniprogram/README.md` |
| `packages/shared/` | `packages/shared/README.md` |
| `db/*/migrations/*.sql` | `docs/database/BD_Manual_*.md` + `apps/etl/connectors/feiqiu/docs/database/` + `docs/database/ddl/` |
### 步骤 4DDL/迁移检查
-`compliance.new_migration_sql` 非空:
- 连接测试库验证迁移是否已执行
- 在审计记录中标注执行状态
-`compliance.new_migration_sql` 非空且 `compliance.has_ddl_baseline` 为 false
- 在审计记录中标注 ⚠️ DDL 基线待合并
### 步骤 4bOpenAPI Spec 同步检查
-`compliance.api_changed` 为 true 且 `compliance.openapi_spec_stale` 为 true
- 在审计记录中标注 ⚠️ 接口代码已变更但 OpenAPI spec 未同步
- 运行 `python scripts/ops/_export_openapi.py` 重新导出 spec需后端可导入
- 若导出失败(后端未启动等),在审计记录中标注待手动导出
- 导出成功后提醒用户重连 OpenAPI Power 的 MCP server 以加载新 spec
-`compliance.api_changed` 为 true 且 `compliance.openapi_spec_stale` 为 false
- spec 已同步更新,无需额外操作
### 步骤 5改动注解Change Annotations
对本次审计涉及的所有变更文件,在审计记录(`docs/audit/changes/<YYYY-MM-DD>__<slug>.md`)中生成逐文件的改动注解段落。
注解内容包括:
- 文件路径
- 变更类型(新增 / 修改 / 删除)
- 原始原因:为什么要做这个改动(从 `latest_prompt_log` 和 diff 上下文推断用户意图)
- 思路分析:改动的技术思路和设计决策(从 diff 内容和代码结构推断)
- 修改结果:改动后的效果和影响范围
格式模板(写入审计记录的 `## 改动注解` 段落):
```markdown
## 改动注解
### `<文件路径>`
- 变更类型:新增 / 修改 / 删除
- 原始原因:<从 prompt log 和 diff 推断的改动动机>
- 思路分析:<技术思路、设计决策、为什么选择这种实现方式>
- 修改结果:<改动后的效果、影响范围、与其他模块的关联>
```
执行规则:
- 只对 `high_risk_files``session_diff.added` 中的文件写详细注解
- 对非高风险的 `session_diff.modified` 文件写简要一行注解即可
-`session_diff.deleted` 文件只记录删除原因
- 注解内容从 `high_risk_diff``latest_prompt_log`、文件内容综合推断,不要编造
- 若某文件的 diff 被截断,可对该单个文件运行 `git diff HEAD -- <file>` 获取完整 diff
- 注解语言使用简体中文
### 步骤 6收尾
-`.kiro/state/.audit_state.json``audit_required` 置为 false清空 `reasons`/`changed_files`/`last_reminded_at`
- 执行 `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

@@ -0,0 +1,16 @@
{
"enabled": true,
"name": "Agent On Stop (Merged)",
"description": "合并 hook对话结束时检测变更含非 Kiro 外部变更)、记录 session log、合规预扫描、构建审计上下文、审计提醒。无变更时跳过。纯 Shell零 Token。",
"version": "1",
"when": {
"type": "agentStop"
},
"then": {
"type": "runCommand",
"command": "python C:/NeoZQYY/.kiro/scripts/agent_on_stop.py",
"timeout": 360
},
"workspaceFolderName": "NeoZQYY",
"shortName": "agent-on-stop"
}

View File

@@ -1,15 +0,0 @@
{
"enabled": true,
"name": "Audit Flagger (Prompt Submit)",
"description": "每次提交 prompt 时,基于 git status 判断是否存在高风险改动;若需要审计则写入 .kiro/.audit_state.json无 stdout。",
"version": "1",
"when": {
"type": "promptSubmit"
},
"then": {
"type": "runCommand",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File .kiro/scripts/audit_flagger.ps1"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "audit-flagger"
}

View File

@@ -1,15 +0,0 @@
{
"enabled": true,
"name": "Audit Reminder (Agent Stop, 15min)",
"description": "若检测到高风险改动且未审计,则在 agentStop 以 stderr+非0 形式提醒15 分钟限频;不写 stdout。",
"version": "1",
"when": {
"type": "agentStop"
},
"then": {
"type": "runCommand",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File .kiro/scripts/audit_reminder.ps1"
},
"workspaceFolderName": "NeoZQYY",
"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,16 @@
{
"enabled": true,
"name": "CWD Guard for Shell",
"description": "在 AI 执行 shell 命令前,检查是否在运行 Python 脚本。如果是,提醒 AI 确认 cwd 是否正确(仓库根 C:\\NeoZQYY避免相对路径解析到错误位置。",
"version": "1",
"when": {
"type": "preToolUse",
"toolTypes": [
"shell"
]
},
"then": {
"type": "askAgent",
"prompt": "如果即将执行的命令包含 `python` 且涉及 scripts/ops/、.kiro/scripts/、apps/etl/connectors/feiqiu/scripts/ 下的脚本请确认1) cwd 参数是否设置为仓库根目录 C:\\NeoZQYY2) 脚本是否已有 ensure_repo_root() 校验。如果 cwd 不对且脚本无校验,请修正 cwd 后再执行。对于非 Python 命令或不涉及上述目录的命令,直接放行。"
}
}

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "每日经营数据报告",
"description": "手动触发后执行 daily_revenue_report.py统计 3月1日至当天的每日经营数据实收、充值、团购结算、到店人次、新会员、充值人数等输出到 docs/reports/daily-revenue-latest.md",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行 python C:\\NeoZQYY\\scripts\\ops\\daily_revenue_report.py"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "daily-revenue-report"
}

View File

@@ -1,15 +0,0 @@
{
"enabled": true,
"name": "Manual: DB 文档全量同步",
"description": "按需触发:对比 Postgres 实际 schema 与 docs/database/ 下的文档,自动补全或更新缺失/过时的表结构说明,并输出变更摘要。",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行一次按需的数据库文档全量同步。\n\n步骤\n1) 检查当前 Postgres schema使用环境中可用的工具/命令,例如 pg_dump --schema-only 或查询 information_schema。\n2) 与 docs/database 下现有文档进行对比。\n3) 更新缺失或过时的 schema/表结构文档。\n4) 输出对账摘要:哪些文档被修改了、修改原因。输出路径遵循.env路径定义。\n\n注意如果需要执行 shell 命令,请通过 agent 的 shell 工具调用。"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "db-docs-sync"
}

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 FULL TEST",
"description": "一键执行 ETL 全流程前后端联调:启动服务 → Playwright 浏览器提交任务 → 实时监控 → 性能报告 → 黑盒一致性测试 → 服务清理。详细步骤参考 .kiro/specs/[ETL]-fullstack-integration/tasks.md",
"version": "1.1.0",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行 ETL 全栈联调运维任务。先读取 `.kiro/specs/[ETL]-fullstack-integration/tasks.md` 获取完整步骤细节,然后严格按以下 6 大步骤依次执行。全程使用 Playwright 浏览器模拟真实用户操作,不直接调用 API。\n\n## 步骤 1服务启动与健康检查\n- 用 controlPwshProcess 启动后端uvicorn app.main:app --host 0.0.0.0 --port 8000cwd=apps/backend/\n- 用 controlPwshProcess 启动前端pnpm devcwd=apps/admin-web/\n- 等待服务就绪,验证 http://localhost:8000/docs 和 http://localhost:5173 可访问\n- Playwright 打开 http://localhost:5173登录用户名 admin密码 admin123\n- 验证登录成功后跳转到任务配置页,侧边栏菜单正常渲染\n\n## 步骤 2浏览器操作 - 任务配置与提交\n- 在任务配置页(/)依次操作:\n - Flow 选择 api_fullAPI → ODS → DWD → DWS → INDEX\n - 处理模式选择 full_window\n - 时间窗口模式设为【自定义】,开始 2025-7-01结束为当前时间\n - 窗口切分【按天】,切分天数 30\n - 勾选 force_full强制全量\n - 任务选择区域全选 is_common=True 的常用任务(共 41 个)\n- 确认 CLI 命令预览区显示完整参数\n- 点击【直接执行】按钮SendOutlined 图标),触发 POST /api/execution/run\n- 确认提交成功提示,记录 execution_id\n\n## 步骤 3执行监控与 DEBUG\n- 导航到【任务管理】页面(/task-manager\n- 在【队列】Tab 确认任务状态为 running\n- 点击 running 任务行,打开 WebSocket 实时日志流抽屉\n- 按需以 30秒~20分钟 弹性间隔检查页面状态\n- 检测日志中的 ERROR / CRITICAL / Traceback / Exception / WARNING 关键字\n- 连续 20 分钟无新日志输出则报超时警告\n- 任务完成success/failed/cancelled时停止监控\n- 收集所有 ERROR 和 WARNING 日志行及上下文,分析错误类型\n- 如果任务失败切换到【历史】Tab 查看完整执行详情\n\n## 步骤 4性能计时与报告生成\n- 在【历史】Tab 点击已完成任务查看执行详情\n- 通过 GET /api/execution/{id}/logs 获取完整日志\n- 从日志提取每个窗口切片30天的开始/结束时间,计算耗时\n- 识别 ODS / DWD / DWS / INDEX 各阶段耗时,标注 Top-5 瓶颈\n- 生成综合联调报告到 {SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md\n- 报告包含执行概要、性能报告各切片耗时对比、Top-5、DEBUG 报告\n\n## 步骤 5黑盒数据一致性测试\n- 运行全链路检查器uv run python scripts/ops/etl_consistency_check.pycwd=C:\\\\NeoZQYY\n - 脚本自动从 LOG_ROOT 找最近 ETL 日志,从 FETCH_ROOT 读 API JSON\n - 连接数据库PG_DSN逐表逐字段比对API vs ODS、ODS vs DWD、DWD vs DWS\n - 白名单ETL_META_COLS、SCD2_COLS 排除API 空字符串 vs DB None 视为等价\n - 报告输出到 ETL_REPORT_ROOT\n- 检查 FlowRunner 内置一致性报告ETL_REPORT_ROOT 下已自动生成)\n- 对比两份报告结论是否一致\n- 将黑盒测试结果摘要追加到步骤 4 的综合报告中(通过/失败统计、白名单差异、失败表清单)\n\n## 步骤 6服务清理\n- 关闭 Playwright 浏览器实例\n- 停止 uvicorn 后端进程controlPwshProcess stop\n- 停止 pnpm dev 前端进程controlPwshProcess stop\n- 报告联调完成状态\n\n## 环境与规范要求\n- 环境变量从根 .env 加载load_dotenv缺失必须报错禁止静默回退\n- 数据库使用测试库PG_DSN 指向 test_etl_feiqiu\n- 报告路径遵循 export-paths 规范,从环境变量读取\n- 需要的环境变量PG_DSN、FETCH_ROOT、LOG_ROOT、ETL_REPORT_ROOT、SYSTEM_LOG_ROOT"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "etl-fullstack-integration"
}

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "ETL Unified Analysis",
"description": "手动触发 ETL 统一分析:合并数据流结构分析和数据一致性检查为一个流程。支持 --mode structure|consistency|full默认 full支持 --source api|etl-log默认 api 主动采集最近 60 天)。",
"version": "1.0.0",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行 ETL 统一分析,按以下步骤完成。若发现已完成或有历史任务痕迹则清空,重新执行:\n\n运行 `python scripts/ops/etl_unified_analysis.py`\n\n默认行为full 模式):\n1. 第一阶段:数据流结构分析\n - 运行 analyze_dataflow.py 采集 API JSON、DB 表结构、三层字段映射、BD_manual 业务描述(默认最近 60 天)\n - 运行 gen_dataflow_report.py 生成结构分析报告\n2. 第二阶段ETL 数据一致性检查\n - 运行 etl_consistency_check.py 对 API→ODS→DWD→DWS 逐表逐字段比对\n - 每张表展示数据截止日期create_time/createtime/fetched_at 的 MAX 值)\n3. 第三阶段:报告合并\n - 将两份报告合并为一份统一报告,输出到 ETL_REPORT_ROOT\n\n可选参数\n- `--mode structure` 仅执行结构分析\n- `--mode consistency` 仅执行一致性检查\n- `--source etl-log` 切换为读 ETL 落盘 JSON而非主动调 API\n- `--date-from YYYY-MM-DD` 指定起始日期\n- `--date-to YYYY-MM-DD` 指定截止日期\n- `--limit N` 每端点最大记录数\n- `--tables t1,t2` 指定分析的表\n\n白名单规则继承 v5\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\n注意\n- 当前仅分析飞球feiqiu连接器\n- 数据库使用测试库TEST_DB_DSN只读模式"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "etl-unified-analysis"
}

View File

@@ -0,0 +1,14 @@
{
"enabled": true,
"name": "字段消失扫描",
"description": "手动触发 DWD 表字段消失扫描检测字段值从某天起突然全部为空的异常≥3天且≥20条连续空记录。输出终端报告 + CSV。",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "runCommand",
"command": "python scripts/ops/field_disappearance_scan.py",
"timeout": 300
}
}

View File

@@ -0,0 +1,13 @@
{
"enabled": true,
"name": "H5 原型截图",
"description": "手动触发:启动 HTTP 服务器 → 运行 screenshot_h5_pages.py 批量截取 docs/h5_ui/pages/ 下所有 H5 原型页面iPhone 15 Pro Max, 430×932, DPR:3输出到 docs/h5_ui/screenshots/。完成后关闭服务器。",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "执行 H5 原型页面批量截图流程:\n1. 启动 HTTP 服务器:`python -m http.server 8765 --directory docs/h5_ui/pages`(用 controlPwshProcess 后台启动cwd 为 C:\\NeoZQYY\n2. 等待 2 秒确认服务器就绪\n3. 运行截图脚本:`python C:\\NeoZQYY\\scripts\\ops\\screenshot_h5_pages.py`cwd 为 C:\\NeoZQYYtimeout 180s\n4. 检查输出:列出 docs/h5_ui/screenshots/*.png 的文件名和大小,确认数量和关键交互态截图大小合理\n5. 停止 HTTP 服务器controlPwshProcess stop\n6. 简要汇报结果:总截图数、像素尺寸验证(应为 1290×N、异常文件如有"
}
}

View File

@@ -0,0 +1,16 @@
{
"enabled": false,
"name": "Pre-Change Research Guard",
"description": "在写操作执行前检查:是否已完成逻辑改动前置调研(审计历史、文档阅读、上下文摘要)。若未完成则阻止写入,先完成调研流程。",
"version": "1",
"when": {
"type": "preToolUse",
"toolTypes": [
"write"
]
},
"then": {
"type": "askAgent",
"prompt": "你即将执行写操作。请确认:\n\n1. 本次写操作是否涉及逻辑改动ETL/业务规则/API/数据模型/前端交互)?\n2. 如果涉及逻辑改动,你是否已通过 context-gatherer 子代理完成前置调研,并向用户输出了上下文摘要且获得确认?\n\n若属于例外情况纯格式/注释/文档纯文字/配置文件/.kiro 目录/用户明确跳过/新建不涉及已有逻辑),可直接继续。\n若未完成前置调研必须先停止写操作使用 context-gatherer 子代理完成调研流程后再继续。"
}
}

View File

@@ -1,15 +0,0 @@
{
"enabled": true,
"name": "Prompt Audit Log (Shell)",
"description": "每次提交 prompt 时,用本地 Shell 在 docs/audit/prompt_logs/ 生成独立日志文件(按时间戳命名);不触发 LLM避免上下文膨胀。",
"version": "3",
"when": {
"type": "promptSubmit"
},
"then": {
"type": "runCommand",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File .kiro/scripts/prompt_audit_log.ps1"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "prompt-audit-log"
}

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "Prompt On Submit (Merged)",
"description": "合并 hook每次提交 prompt 时执行风险标记 + prompt 日志记录 + git 快照。纯 Shell零 Token。",
"version": "1",
"when": {
"type": "promptSubmit"
},
"then": {
"type": "runCommand",
"command": "python C:/NeoZQYY/.kiro/scripts/prompt_on_submit.py"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "prompt-on-submit"
}

View File

@@ -1,14 +1,14 @@
{ {
"enabled": true, "enabled": true,
"name": "Manual: Run /audit (via audit-writer subagent)", "name": "Manual: Run /audit (via audit-writer subagent)",
"description": "按需触发:启动 audit-writer 子代理执行变更影响审查+文档同步+审计落盘,完成后自动刷新审计一览表,并仅回传极短回执。", "description": "按需触发:读取 agent-on-stop 预构建的审计上下文 + Session 索引,启动 audit-writer 子代理执行审计落盘+文档校对+DB文档全量对账+Session关联。上下文过期时自动重建。",
"version": "2", "version": "11",
"when": { "when": {
"type": "userTriggered" "type": "userTriggered"
}, },
"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 审计流程:\n\n**第零步:获取当前时间**:运行 `python -c \"from datetime import datetime, timezone, timedelta; print(datetime.now(timezone(timedelta(hours=8))).isoformat())\"` 获取当前北京时间,记为 `now`。后续所有「超过 30 分钟」的判断以此 `now` 为基准。\n\n**前置检查**:读取 `.kiro/state/.audit_context.json`,检查 `built_at` 时间戳。若文件不存在或 `built_at` 距 `now` 超过 30 分钟,先运行 `python .kiro/scripts/agent_on_stop.py --force-rebuild` 重建上下文,再重新读取。\n\n**Session 索引读取**:读取 `docs/audit/session_logs/_session_index.json`,找到与本次对话时间最接近的 entry按 `startTime` 匹配),提取其 `description`LLM 操作摘要)和 `summary`(结构化摘要)。这些信息将用于:\n- 作为审计记录头部的「操作摘要」来源(比从 diff 推断更准确)\n- 交叉验证 audit_context.json 中的 session_difffiles_modified/created\n- 记录本次审计关联的 session executionId建立双向链接\n\n**主流程**:启动名为 audit-writer 的子代理,传入以下指令:\n\n> 读取 `.kiro/state/.audit_context.json` 作为主输入,同时参考 Session 索引中匹配的 entry。不要自行运行 git status/diff/扫描文件。audit_context.json 已包含:变更文件列表、高风险文件 diff、合规检查清单文档缺失/迁移状态/DDL 基线/接口变更/OpenAPI spec 状态、本次对话精确变更session_diff: added/modified/deleted、Prompt-ID 溯源。按 audit-writer.md 中定义的执行策略完成审计落盘+文档校对补齐。\n\n约束\n- 子代理禁止重复运行 git status --porcelain 或 git diff 全量扫描,所有信息已在 .audit_context.json 中预备好。\n- 子代理需要读取具体文件内容时(如更新文档),可以直接读取对应文件,但不要做全仓库遍历。\n- 子代理必须按需调用 skillsteering-readme-maintainer、change-annotation-audit、bd-manual-db-docs仅在满足触发条件时。\n- 子代理必须根据 compliance.code_without_docs 自动补齐缺失的文档同步。\n- 当 reasons 含 db-schema-change 时,子代理必须执行 DB 文档全量对账连接测试库TEST_DB_DSN查询 information_schema与 docs/database/ 下现有文档全量对比,补全或更新所有缺失/过时的表结构说明(不仅限于本次变更涉及的表),输出对账摘要。\n- 子代理应参考 session_diff 中的 added/modified/deleted 列表,精确定位本次对话的变更范围。\n- **Session 关联**在审计记录docs/audit/changes/*.md头部增加 `session_id` 字段executionId 前 8 位),并将 Session 索引中的 description 作为「操作摘要」写入审计记录。这建立了审计记录 ↔ Session 日志的双向链接。\n- 子代理必须为所有变更文件生成改动注解(步骤 5写入审计记录的「改动注解」段落包含变更类型、原始原因、思路分析、修改结果。高风险文件写详细注解普通修改写简要一行删除文件只记录原因。\n- 若 compliance.api_changed=true 且 compliance.openapi_spec_stale=true运行 `python scripts/ops/_export_openapi.py` 重新导出 OpenAPI spec导出失败则在审计记录标注待手动导出导出成功则提醒用户重连 OpenAPI Power MCP server。\n- 所有审计产物统一写入 docs/audit/,不写入子模块内部。\n- 完成后把 .kiro/state/.audit_state.json 中 audit_required 置为 false。\n- 执行 `python scripts/audit/gen_audit_dashboard.py` 刷新审计一览表。\n- **文档地图更新**:审计完成后,自动更新 `docs/DOCUMENTATION-MAP.md`\n - 检查本次审计涉及的文档变更(从审计记录中识别)\n - 扫描 `docs/` 目录和各模块内部文档的变化(新增、修改、删除)\n - 特别关注数据库文档(`docs/database/`)是否有新增的 BD_Manual 文件\n - 根据发现的文档变更,更新文档地图中的相应条目\n - 确保文档地图的结构完整,所有重要文档都有记录\n- 最终回复必须是极短回执done/files_written/next_step。"
}, },
"workspaceFolderName": "NeoZQYY", "workspaceFolderName": "NeoZQYY",
"shortName": "audit" "shortName": "audit"

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "Session description maker",
"description": "手动触发:为缺少 description 的 session log 调用百炼千问 API 生成摘要写入双索引。askAgent 模式可看到实时输出。",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "请在后台运行以下命令并展示实时输出python -B C:/NeoZQYY/scripts/ops/batch_generate_summaries.py"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "session-summary"
}

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""cwd 校验工具 — .kiro/scripts/ 下所有脚本共享。
用法:
from _ensure_root import ensure_repo_root
ensure_repo_root()
委托给 neozqyy_shared.repo_root共享包未安装时 fallback。
"""
from __future__ import annotations
import os
import warnings
from pathlib import Path
def ensure_repo_root() -> Path:
"""校验 cwd 是否为仓库根目录,不是则自动切换。"""
try:
from neozqyy_shared.repo_root import ensure_repo_root as _shared
return _shared()
except ImportError:
pass
# fallback
cwd = Path.cwd()
if (cwd / "pyproject.toml").is_file() and (cwd / ".kiro").is_dir():
return cwd
root = Path(__file__).resolve().parents[2]
if (root / "pyproject.toml").is_file() and (root / ".kiro").is_dir():
os.chdir(root)
warnings.warn(
f"cwd 不是仓库根目录,已自动切换: {cwd}{root}",
stacklevel=2,
)
return root
raise RuntimeError(
f"无法定位仓库根目录。当前 cwd={cwd},推断 root={root}"
f"请在仓库根目录下运行脚本。"
)

View File

@@ -0,0 +1,650 @@
#!/usr/bin/env python3
"""agent_on_stop — agentStop 合并 hook 脚本v3含 LLM 摘要生成)。
合并原 audit_reminder + change_compliance_prescan + build_audit_context + session_extract
1. 全量会话记录提取 → docs/audit/session_logs/(无论是否有代码变更)
2. 为刚提取的 session 调用百炼 API 生成 description → 写入双索引
3. 扫描工作区 → 与 promptSubmit 基线对比 → 精确检测本次对话变更
4. 若无任何文件变更 → 跳过审查,静默退出
5. 合规预扫描 → .kiro/state/.compliance_state.json
6. 构建审计上下文 → .kiro/state/.audit_context.json
7. 审计提醒15 分钟限频)→ stderr
变更检测基于文件 mtime+size 基线对比,不依赖 git commit 历史。
所有功能块用 try/except 隔离,单个失败不影响其他。
"""
import hashlib
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
# 同目录导入文件基线模块 + cwd 校验
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from file_baseline import scan_workspace, load_baseline, diff_baselines, total_changes
from _ensure_root import ensure_repo_root
TZ_TAIPEI = timezone(timedelta(hours=8))
MIN_INTERVAL = timedelta(minutes=15)
# 路径常量
STATE_PATH = os.path.join(".kiro", "state", ".audit_state.json")
COMPLIANCE_PATH = os.path.join(".kiro", "state", ".compliance_state.json")
CONTEXT_PATH = os.path.join(".kiro", "state", ".audit_context.json")
PROMPT_ID_PATH = os.path.join(".kiro", "state", ".last_prompt_id.json")
# 噪声路径(用于过滤变更列表中的非业务文件)
NOISE_PATTERNS = [
re.compile(r"^docs/audit/"),
re.compile(r"^\.kiro/"),
re.compile(r"^\.hypothesis/"),
re.compile(r"^tmp/"),
re.compile(r"\.png$"),
re.compile(r"\.jpg$"),
]
# 高风险路径
HIGH_RISK_PATTERNS = [
re.compile(r"^apps/etl/connectors/feiqiu/(api|cli|config|database|loaders|models|orchestration|scd|tasks|utils|quality)/"),
re.compile(r"^apps/backend/app/"),
re.compile(r"^apps/admin-web/src/"),
re.compile(r"^apps/miniprogram/"),
re.compile(r"^packages/shared/"),
re.compile(r"^db/"),
]
# 文档映射(合规检查用)
DOC_MAP = {
"apps/backend/app/routers/": ["apps/backend/docs/API-REFERENCE.md", "docs/contracts/openapi/backend-api.json"],
"apps/backend/app/services/": ["apps/backend/docs/API-REFERENCE.md", "apps/backend/README.md"],
"apps/backend/app/auth/": ["apps/backend/docs/API-REFERENCE.md", "apps/backend/README.md", "docs/contracts/openapi/backend-api.json"],
"apps/backend/app/schemas/": ["docs/contracts/openapi/backend-api.json"],
"apps/backend/app/main.py": ["docs/contracts/openapi/backend-api.json"],
"apps/etl/connectors/feiqiu/tasks/": ["apps/etl/connectors/feiqiu/docs/etl_tasks/"],
"apps/etl/connectors/feiqiu/loaders/": ["apps/etl/connectors/feiqiu/docs/etl_tasks/"],
"apps/etl/connectors/feiqiu/scd/": ["apps/etl/connectors/feiqiu/docs/business-rules/scd2_rules.md"],
"apps/etl/connectors/feiqiu/orchestration/": ["apps/etl/connectors/feiqiu/docs/architecture/"],
"apps/admin-web/src/": ["apps/admin-web/README.md"],
"apps/miniprogram/": ["apps/miniprogram/README.md"],
"packages/shared/": ["packages/shared/README.md"],
}
# 接口变更检测模式routers / auth / schemas / main.py
API_CHANGE_PATTERNS = [
re.compile(r"^apps/backend/app/routers/"),
re.compile(r"^apps/backend/app/auth/"),
re.compile(r"^apps/backend/app/schemas/"),
re.compile(r"^apps/backend/app/main\.py$"),
]
MIGRATION_PATTERNS = [
re.compile(r"^db/etl_feiqiu/migrations/.*\.sql$"),
re.compile(r"^db/zqyy_app/migrations/.*\.sql$"),
re.compile(r"^db/fdw/.*\.sql$"),
]
BD_MANUAL_PATTERN = re.compile(r"^docs/database/BD_Manual_.*\.md$")
DDL_BASELINE_DIR = "docs/database/ddl/"
AUDIT_CHANGES_DIR = "docs/audit/changes/"
def now_taipei():
return datetime.now(TZ_TAIPEI)
def sha1hex(s: str) -> str:
return hashlib.sha1(s.encode("utf-8")).hexdigest()
def is_noise(f: str) -> bool:
return any(p.search(f) for p in NOISE_PATTERNS)
def safe_read_json(path):
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def write_json(path, data):
os.makedirs(os.path.dirname(path) or os.path.join(".kiro", "state"), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def git_diff_stat():
try:
r = subprocess.run(
["git", "diff", "--stat", "HEAD"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=15
)
return r.stdout.strip() if r.returncode == 0 else ""
except Exception:
return ""
def git_diff_files(files, max_total=30000, max_per_file=15000):
"""获取文件的实际 diff 内容。对已跟踪文件用 git diff HEAD对新文件直接读取内容。"""
if not files:
return ""
all_diff = []
total_len = 0
for f in files:
if total_len >= max_total:
all_diff.append(f"\n[TRUNCATED: diff exceeds {max_total // 1000}KB]")
break
try:
# 先尝试 git diff HEAD
r = subprocess.run(
["git", "diff", "HEAD", "--", f],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10
)
chunk = ""
if r.returncode == 0 and r.stdout.strip():
chunk = r.stdout.strip()
elif os.path.isfile(f):
# untracked 新文件:直接读取内容作为 diff
try:
with open(f, "r", encoding="utf-8", errors="replace") as fh:
file_content = fh.read(max_per_file + 100)
chunk = f"--- /dev/null\n+++ b/{f}\n@@ -0,0 +1 @@\n" + file_content
except Exception:
continue
if chunk:
if len(chunk) > max_per_file:
chunk = chunk[:max_per_file] + f"\n[TRUNCATED: {f} diff too long]"
all_diff.append(chunk)
total_len += len(chunk)
except Exception:
continue
return "\n".join(all_diff)
def get_latest_prompt_log():
log_dir = os.path.join("docs", "audit", "prompt_logs")
if not os.path.isdir(log_dir):
return ""
try:
files = sorted(
[f for f in os.listdir(log_dir) if f.startswith("prompt_log_")],
reverse=True
)
if not files:
return ""
with open(os.path.join(log_dir, files[0]), "r", encoding="utf-8") as f:
content = f.read()
return content[:3000] + "\n[TRUNCATED]" if len(content) > 3000 else content
except Exception:
return ""
# ── 步骤 1基于文件基线检测变更 ──
def detect_changes_via_baseline():
"""扫描当前工作区,与 promptSubmit 基线对比,返回精确的变更列表。
返回 (all_changed_files, external_files, diff_result, no_change)
- all_changed_files: 本次对话期间所有变更文件added + modified
- external_files: 暂时等于 all_changed_files后续可通过 Kiro 写入日志细化)
- diff_result: 完整的 diff 结果 {added, modified, deleted}
- no_change: 是否无任何变更
"""
before = load_baseline()
after = scan_workspace(".")
if not before:
# 没有基线(首次运行或基线丢失),无法对比,回退到全部文件
return [], [], {"added": [], "modified": [], "deleted": []}, True
diff = diff_baselines(before, after)
count = total_changes(diff)
if count == 0:
return [], [], diff, True
# 所有变更文件 = added + modifieddeleted 的文件已不存在,不参与风险判定)
all_changed = sorted(set(diff["added"] + diff["modified"]))
# 过滤噪声
real_files = [f for f in all_changed if not is_noise(f)]
if not real_files:
return [], [], diff, True
# 外部变更:目前所有基线检测到的变更都记录,
# 因为 Kiro 的写入也会改变 mtime所以这里的"外部"含义是
# "本次对话期间发生的所有变更",包括 Kiro 和非 Kiro 的。
# 精确区分需要 Kiro 运行时提供写入文件列表,目前不可用。
external_files = [] # 不再误报外部变更
return real_files, external_files, diff, False
# ── 步骤 3合规预扫描 ──
def do_compliance_prescan(all_files):
result = {
"new_migration_sql": [],
"new_or_modified_sql": [],
"code_without_docs": [],
"new_files": [],
"has_bd_manual": False,
"has_audit_record": False,
"has_ddl_baseline": False,
"api_changed": False,
"openapi_spec_stale": False,
}
code_files = []
doc_files = set()
for f in all_files:
if is_noise(f):
continue
for mp in MIGRATION_PATTERNS:
if mp.search(f):
result["new_migration_sql"].append(f)
break
if f.endswith(".sql"):
result["new_or_modified_sql"].append(f)
if BD_MANUAL_PATTERN.search(f):
result["has_bd_manual"] = True
if f.startswith(AUDIT_CHANGES_DIR):
result["has_audit_record"] = True
if f.startswith(DDL_BASELINE_DIR):
result["has_ddl_baseline"] = True
if f.endswith(".md") or "/docs/" in f:
doc_files.add(f)
if f.endswith((".py", ".ts", ".tsx", ".js", ".jsx")):
code_files.append(f)
# 检测接口相关文件变更
for ap in API_CHANGE_PATTERNS:
if ap.search(f):
result["api_changed"] = True
break
# 接口变更但 openapi spec 未同步更新 → 标记过期
if result["api_changed"] and "docs/contracts/openapi/backend-api.json" not in all_files:
result["openapi_spec_stale"] = True
for cf in code_files:
expected_docs = []
for prefix, docs in DOC_MAP.items():
if cf.startswith(prefix):
expected_docs.extend(docs)
if expected_docs:
has_doc = False
for ed in expected_docs:
if ed in doc_files:
has_doc = True
break
if ed.endswith("/") and any(d.startswith(ed) for d in doc_files):
has_doc = True
break
if not has_doc:
result["code_without_docs"].append({
"file": cf,
"expected_docs": expected_docs,
})
needs_check = bool(
result["new_migration_sql"]
or result["code_without_docs"]
or result["openapi_spec_stale"]
)
now = now_taipei()
write_json(COMPLIANCE_PATH, {
"needs_check": needs_check,
"scanned_at": now.isoformat(),
**result,
})
return result
# ── 步骤 4构建审计上下文 ──
def do_build_audit_context(all_files, diff_result, compliance):
now = now_taipei()
audit_state = safe_read_json(STATE_PATH)
prompt_info = safe_read_json(PROMPT_ID_PATH)
# 使用 audit_state 中的 changed_files来自 git status 的风险文件)
# 与本次对话的 baseline diff 合并
git_changed = audit_state.get("changed_files", [])
session_changed = all_files # 本次对话期间变更的文件
# 合并两个来源,去重
all_changed = sorted(set(git_changed + session_changed))
high_risk_files = [
f for f in all_changed
if any(p.search(f) for p in HIGH_RISK_PATTERNS)
]
diff_stat = git_diff_stat()
high_risk_diff = git_diff_files(high_risk_files)
prompt_log = get_latest_prompt_log()
context = {
"built_at": now.isoformat(),
"prompt_id": prompt_info.get("prompt_id", "unknown"),
"prompt_at": prompt_info.get("at", ""),
"audit_required": audit_state.get("audit_required", False),
"db_docs_required": audit_state.get("db_docs_required", False),
"reasons": audit_state.get("reasons", []),
"changed_files": all_changed[:100],
"high_risk_files": high_risk_files,
"session_diff": {
"added": diff_result.get("added", [])[:50],
"modified": diff_result.get("modified", [])[:50],
"deleted": diff_result.get("deleted", [])[:50],
},
"compliance": {
"code_without_docs": compliance.get("code_without_docs", []),
"new_migration_sql": compliance.get("new_migration_sql", []),
"has_bd_manual": compliance.get("has_bd_manual", False),
"has_audit_record": compliance.get("has_audit_record", False),
"has_ddl_baseline": compliance.get("has_ddl_baseline", False),
"api_changed": compliance.get("api_changed", False),
"openapi_spec_stale": compliance.get("openapi_spec_stale", False),
},
"diff_stat": diff_stat,
"high_risk_diff": high_risk_diff,
"latest_prompt_log": prompt_log,
}
write_json(CONTEXT_PATH, context)
# ── 步骤 5审计提醒15 分钟限频) ──
def do_audit_reminder(real_files):
state = safe_read_json(STATE_PATH)
if not state.get("audit_required"):
return
# 无变更时不提醒
if not real_files:
return
now = now_taipei()
last_str = state.get("last_reminded_at")
if last_str:
try:
last = datetime.fromisoformat(last_str)
if (now - last) < MIN_INTERVAL:
return
except Exception:
pass
state["last_reminded_at"] = now.isoformat()
write_json(STATE_PATH, state)
reasons = state.get("reasons", [])
reason_text = ", ".join(reasons) if reasons else "high-risk paths changed"
# 仅信息性提醒exit(0) 避免 agent 将其视为错误并自行执行审计
# 审计留痕统一由用户手动触发 /audit 完成
sys.stderr.write(
f"[AUDIT REMINDER] Pending audit ({reason_text}), "
f"{len(real_files)} files changed this session. "
f"Run /audit to sync. (15min rate limit)\n"
)
sys.exit(0)
# ── 步骤 6全量会话记录提取 ──
def do_full_session_extract():
"""从 Kiro globalStorage 提取当前 execution 的全量对话记录。
调用 scripts/ops/extract_kiro_session.py 的核心逻辑。
仅提取最新一条未索引的 execution避免重复。
"""
# 动态导入提取器(避免启动时 import 开销)
scripts_ops = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "scripts", "ops")
scripts_ops = os.path.normpath(scripts_ops)
if scripts_ops not in sys.path:
sys.path.insert(0, scripts_ops)
try:
from extract_kiro_session import extract_latest
except ImportError:
return # 提取器不存在则静默跳过
# globalStorage 路径:从环境变量或默认位置
global_storage = os.environ.get(
"KIRO_GLOBAL_STORAGE",
os.path.join(os.environ.get("APPDATA", ""), "Kiro", "User", "globalStorage")
)
workspace_path = os.getcwd()
extract_latest(global_storage, workspace_path)
def _extract_summary_content(md_content: str) -> str:
"""从 session log markdown 中提取适合生成摘要的内容。
策略:如果"用户输入"包含 CONTEXT TRANSFER跨轮续接
则替换为简短标注,避免历史背景干扰本轮摘要生成。
"""
import re
# 检测用户输入中是否包含 context transfer
ct_pattern = re.compile(r"## 2\. 用户输入\s*\n```\s*\n.*?CONTEXT TRANSFER", re.DOTALL)
if ct_pattern.search(md_content):
# 替换"用户输入"section 为简短标注
# 匹配从 "## 2. 用户输入" 到下一个 "## 3." 之间的内容
md_content = re.sub(
r"(## 2\. 用户输入)\s*\n```[\s\S]*?```\s*\n(?=## 3\.)",
r"\1\n\n[本轮为 Context Transfer 续接,用户输入为历史多轮摘要,已省略。请基于执行摘要和对话记录中的实际工具调用判断本轮工作。]\n\n",
md_content,
)
return md_content
# ── 步骤 7为最新 session 生成 LLM 摘要 ──
_SUMMARY_SYSTEM_PROMPT = """你是一个专业的技术对话分析师。你的任务是为 AI 编程助手的一轮执行execution生成简洁的中文摘要。
背景一个对话chatSession包含多轮执行execution。每轮执行 = 用户发一条消息 → AI 完成响应。你收到的是单轮执行的完整记录。
摘要规则:
1. 只描述本轮执行实际完成的工作,不要描述历史背景
2. 列出完成的功能点/任务(一轮可能完成多个)
3. 包含关键技术细节文件路径、模块名、数据库表、API 端点等
4. bug 修复要说明原因和方案
5. 不写过程性描述("用户说..."),只写结果
6. 内容太短或无实质内容的,写"无实质内容"
7. 不限字数,信息完整优先,避免截断失真
重要:
- "执行摘要"(📋)是最可靠的信息源,优先基于它判断本轮做了什么
- 如果"用户输入"包含 CONTEXT TRANSFER那是之前多轮的历史摘要不是本轮工作
- 对话记录中的实际工具调用和文件变更才是本轮的真实操作
请直接输出摘要,不要添加任何前缀或解释。"""
def do_generate_description():
"""为缺少 description 的主对话 entry 调用百炼 API 生成摘要,写入双索引。"""
from dotenv import load_dotenv
load_dotenv()
api_key = os.environ.get("BAILIAN_API_KEY", "")
if not api_key:
return
model = os.environ.get("BAILIAN_MODEL", "qwen-plus")
base_url = os.environ.get("BAILIAN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1")
scripts_ops = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "scripts", "ops")
scripts_ops = os.path.normpath(scripts_ops)
if scripts_ops not in sys.path:
sys.path.insert(0, scripts_ops)
try:
from extract_kiro_session import load_index, save_index, load_full_index, save_full_index
except ImportError:
return
index = load_index()
entries = index.get("entries", {})
if not entries:
return
# 收集所有缺少 description 的主对话 entry
targets = []
for eid, ent in entries.items():
if ent.get("is_sub"):
continue
if not ent.get("description"):
targets.append((eid, ent))
if not targets:
return
# agent_on_stop 场景下限制处理数量,避免超时
# 批量处理积压用独立脚本 batch_generate_summaries.py
MAX_PER_RUN = 10
if len(targets) > MAX_PER_RUN:
# 优先处理最新的(按 startTime 降序)
targets.sort(key=lambda t: t[1].get("startTime", ""), reverse=True)
targets = targets[:MAX_PER_RUN]
try:
from openai import OpenAI
client = OpenAI(api_key=api_key, base_url=base_url)
except Exception:
return
full_index = load_full_index()
full_entries = full_index.get("entries", {})
generated = 0
for target_eid, target_entry in targets:
out_dir = target_entry.get("output_dir", "")
if not out_dir or not os.path.isdir(out_dir):
continue
# 找到该 entry 对应的 main_*.md 文件
main_files = sorted(
f for f in os.listdir(out_dir)
if f.startswith("main_") and f.endswith(".md")
and target_eid[:8] in f # 按 executionId 短码匹配
)
if not main_files:
# 回退:取目录下所有 main 文件
main_files = sorted(
f for f in os.listdir(out_dir)
if f.startswith("main_") and f.endswith(".md")
)
if not main_files:
continue
content_parts = []
for mf in main_files:
try:
with open(os.path.join(out_dir, mf), "r", encoding="utf-8") as fh:
content_parts.append(fh.read())
except Exception:
continue
if not content_parts:
continue
content = "\n\n---\n\n".join(content_parts)
content = _extract_summary_content(content)
if len(content) > 60000:
content = content[:60000] + "\n\n[TRUNCATED]"
try:
resp = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": _SUMMARY_SYSTEM_PROMPT},
{"role": "user", "content": f"请为以下单轮执行记录生成摘要:\n\n{content}"},
],
max_tokens=4096,
)
description = resp.choices[0].message.content.strip()
except Exception:
continue # 单条失败不影响其他
if not description:
continue
# 写入双索引(内存中)
entries[target_eid]["description"] = description
if target_eid in full_entries:
full_entries[target_eid]["description"] = description
generated += 1
# 批量保存
if generated > 0:
save_index(index)
save_full_index(full_index)
def main():
ensure_repo_root()
now = now_taipei()
force_rebuild = "--force-rebuild" in sys.argv
# 全量会话记录提取(无论是否有文件变更,每次对话都要记录)
try:
do_full_session_extract()
except Exception:
pass
# 步骤 1基于文件基线检测变更
real_files, external_files, diff_result, no_change = detect_changes_via_baseline()
# 无任何文件变更 → 跳过所有审查(除非 --force-rebuild
if no_change and not force_rebuild:
return
# --force-rebuild 且无变更时,仍需基于 git status 重建 context
if no_change and force_rebuild:
try:
compliance = do_compliance_prescan(real_files or [])
except Exception:
compliance = {}
try:
do_build_audit_context(real_files or [], diff_result, compliance)
except Exception:
pass
return
# 步骤 2合规预扫描基于本次对话变更的文件
compliance = {}
try:
compliance = do_compliance_prescan(real_files)
except Exception:
pass
# 步骤 4构建审计上下文
try:
do_build_audit_context(real_files, diff_result, compliance)
except Exception:
pass
# 步骤 7审计提醒信息性exit(0),不触发 agent 自行审计)
try:
do_audit_reminder(real_files)
except SystemExit:
pass # exit(0) 信息性退出,不需要 re-raise
except Exception:
pass
if __name__ == "__main__":
try:
main()
except SystemExit as e:
sys.exit(e.code)
except Exception:
pass

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/state/.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", "state", ".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(os.path.join(".kiro", "state"), 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", "state", ".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(os.path.join(".kiro", "state"), 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

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""build_audit_context — 合并所有前置 hook 产出,生成统一审计上下文快照。
读取:
- .kiro/state/.audit_state.jsonaudit-flagger 产出:风险判定、变更文件列表)
- .kiro/state/.compliance_state.jsonchange-compliance 产出:文档缺失、迁移状态)
- .kiro/state/.last_prompt_id.jsonprompt-audit-log 产出Prompt ID 溯源)
- git diff --stat HEAD变更统计摘要
- git diff HEAD仅高风险文件的 diff截断到合理长度
输出:.kiro/state/.audit_context.jsonaudit-writer 子代理的唯一输入)
"""
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
CONTEXT_PATH = os.path.join(".kiro", "state", ".audit_context.json")
# 高风险路径(只对这些文件取 diff避免 diff 过大)
HIGH_RISK_PATTERNS = [
re.compile(r"^apps/etl/connectors/feiqiu/(api|cli|config|database|loaders|models|orchestration|scd|tasks|utils|quality)/"),
re.compile(r"^apps/backend/app/"),
re.compile(r"^apps/admin-web/src/"),
re.compile(r"^apps/miniprogram/"),
re.compile(r"^packages/shared/"),
re.compile(r"^db/"),
]
def safe_read_json(path):
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def git_diff_stat():
try:
r = subprocess.run(
["git", "diff", "--stat", "HEAD"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=15
)
return r.stdout.strip() if r.returncode == 0 else ""
except Exception:
return ""
def git_diff_files(files, max_total=30000):
"""获取指定文件的 git diff截断到 max_total 字符"""
if not files:
return ""
# 分批取 diff避免命令行过长
all_diff = []
total_len = 0
for f in files:
if total_len >= max_total:
all_diff.append(f"\n[TRUNCATED: diff exceeds {max_total // 1000}KB limit]")
break
try:
r = subprocess.run(
["git", "diff", "HEAD", "--", f],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10
)
if r.returncode == 0 and r.stdout.strip():
chunk = r.stdout.strip()
# 单文件 diff 截断
if len(chunk) > 5000:
chunk = chunk[:5000] + f"\n[TRUNCATED: {f} diff too long]"
all_diff.append(chunk)
total_len += len(chunk)
except Exception:
continue
return "\n".join(all_diff)
def get_latest_prompt_log():
"""获取最新的 prompt log 文件内容(用于溯源)"""
log_dir = os.path.join("docs", "audit", "prompt_logs")
if not os.path.isdir(log_dir):
return ""
try:
files = sorted(
[f for f in os.listdir(log_dir) if f.startswith("prompt_log_")],
reverse=True
)
if not files:
return ""
latest = os.path.join(log_dir, files[0])
with open(latest, "r", encoding="utf-8") as f:
content = f.read()
# 截断过长内容
if len(content) > 3000:
content = content[:3000] + "\n[TRUNCATED]"
return content
except Exception:
return ""
def main():
now = datetime.now(TZ_TAIPEI)
# 读取前置 hook 产出
audit_state = safe_read_json(os.path.join(".kiro", "state", ".audit_state.json"))
compliance = safe_read_json(os.path.join(".kiro", "state", ".compliance_state.json"))
prompt_id_info = safe_read_json(os.path.join(".kiro", "state", ".last_prompt_id.json"))
# 从 audit_state 提取高风险文件
changed_files = audit_state.get("changed_files", [])
high_risk_files = [
f for f in changed_files
if any(p.search(f) for p in HIGH_RISK_PATTERNS)
]
# 获取 diff仅高风险文件
diff_stat = git_diff_stat()
high_risk_diff = git_diff_files(high_risk_files)
# 获取最新 prompt log
prompt_log = get_latest_prompt_log()
# 构建统一上下文
context = {
"built_at": now.isoformat(),
"prompt_id": prompt_id_info.get("prompt_id", "unknown"),
"prompt_at": prompt_id_info.get("at", ""),
# 来自 audit-flagger
"audit_required": audit_state.get("audit_required", False),
"db_docs_required": audit_state.get("db_docs_required", False),
"reasons": audit_state.get("reasons", []),
"changed_files": changed_files,
"high_risk_files": high_risk_files,
# 来自 change-compliance-prescan
"compliance": {
"code_without_docs": compliance.get("code_without_docs", []),
"new_migration_sql": compliance.get("new_migration_sql", []),
"has_bd_manual": compliance.get("has_bd_manual", False),
"has_audit_record": compliance.get("has_audit_record", False),
"has_ddl_baseline": compliance.get("has_ddl_baseline", False),
},
# git 摘要
"diff_stat": diff_stat,
"high_risk_diff": high_risk_diff,
# prompt 溯源
"latest_prompt_log": prompt_log,
}
os.makedirs(os.path.join(".kiro", "state"), exist_ok=True)
with open(CONTEXT_PATH, "w", encoding="utf-8") as f:
json.dump(context, f, indent=2, ensure_ascii=False)
# 输出摘要到 stdout
print(f"audit_context built: {len(changed_files)} files, "
f"{len(high_risk_files)} high-risk, "
f"{len(compliance.get('code_without_docs', []))} docs missing")
if __name__ == "__main__":
try:
main()
except Exception as e:
sys.stderr.write(f"build_audit_context failed: {e}\n")
sys.exit(1)

View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""change_compliance_prescan — 预扫描变更文件,输出需要合规审查的项目。
在 agentStop 时由 askAgent hook 调用,为 LLM 提供精简的审查清单,
避免 LLM 自行扫描文件浪费 Token。
输出到 stdout供 askAgent 读取):
- 若无需审查:输出 "NO_CHECK_NEEDED"
- 若需审查:输出结构化 JSON 清单
"""
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
TZ_TAIPEI = timezone(timedelta(hours=8))
STATE_PATH = os.path.join(".kiro", "state", ".audit_state.json")
# doc-map 中定义的文档对应关系
DOC_MAP = {
# 代码路径前缀 → 应同步更新的文档
"apps/backend/app/routers/": ["apps/backend/docs/API-REFERENCE.md"],
"apps/backend/app/services/": ["apps/backend/docs/API-REFERENCE.md", "apps/backend/README.md"],
"apps/backend/app/auth/": ["apps/backend/docs/API-REFERENCE.md", "apps/backend/README.md"],
"apps/etl/connectors/feiqiu/tasks/": ["apps/etl/connectors/feiqiu/docs/etl_tasks/"],
"apps/etl/connectors/feiqiu/loaders/": ["apps/etl/connectors/feiqiu/docs/etl_tasks/"],
"apps/etl/connectors/feiqiu/scd/": ["apps/etl/connectors/feiqiu/docs/business-rules/scd2_rules.md"],
"apps/etl/connectors/feiqiu/orchestration/": ["apps/etl/connectors/feiqiu/docs/architecture/"],
"apps/admin-web/src/": ["apps/admin-web/README.md"],
"apps/miniprogram/": ["apps/miniprogram/README.md"],
"packages/shared/": ["packages/shared/README.md"],
}
# DDL 基线文件doc-map 中定义)
DDL_BASELINE_DIR = "docs/database/ddl/"
# 迁移脚本路径
MIGRATION_PATTERNS = [
re.compile(r"^db/etl_feiqiu/migrations/.*\.sql$"),
re.compile(r"^db/zqyy_app/migrations/.*\.sql$"),
re.compile(r"^db/fdw/.*\.sql$"),
]
# DB 文档路径
BD_MANUAL_PATTERN = re.compile(r"^docs/database/BD_Manual_.*\.md$")
# 审计记录路径
AUDIT_CHANGES_DIR = "docs/audit/changes/"
# 噪声路径(不参与合规检查)
NOISE = [
re.compile(r"^docs/audit/"),
re.compile(r"^\.kiro/"),
re.compile(r"^\.hypothesis/"),
re.compile(r"^tmp/"),
re.compile(r"\.png$"),
re.compile(r"\.jpg$"),
]
def safe_read_json(path):
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def get_changed_files():
"""从 audit_state 或 git status 获取变更文件"""
state = safe_read_json(STATE_PATH)
files = state.get("changed_files", [])
if files:
return files
# 回退到 git status
try:
r = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True, timeout=10
)
if r.returncode != 0:
return []
result = []
for line in r.stdout.splitlines():
if len(line) < 4:
continue
path = line[3:].strip().strip('"').replace("\\", "/")
if " -> " in path:
path = path.split(" -> ")[-1]
if path:
result.append(path)
return sorted(set(result))
except Exception:
return []
def is_noise(f):
return any(p.search(f) for p in NOISE)
def classify_files(files):
"""将变更文件分类,输出审查清单"""
result = {
"new_migration_sql": [], # 新增的迁移 SQL
"new_or_modified_sql": [], # 所有 SQL 变更
"code_without_docs": [], # 有代码改动但缺少对应文档改动
"new_files": [], # 新增文件(需检查目录规范)
"has_bd_manual": False, # 是否有 BD_Manual 文档变更
"has_audit_record": False, # 是否有审计记录变更
"has_ddl_baseline": False, # 是否有 DDL 基线变更
}
code_files = []
doc_files = set()
for f in files:
if is_noise(f):
continue
# 迁移 SQL
for mp in MIGRATION_PATTERNS:
if mp.search(f):
result["new_migration_sql"].append(f)
break
# SQL 文件
if f.endswith(".sql"):
result["new_or_modified_sql"].append(f)
# BD_Manual
if BD_MANUAL_PATTERN.search(f):
result["has_bd_manual"] = True
# 审计记录
if f.startswith(AUDIT_CHANGES_DIR):
result["has_audit_record"] = True
# DDL 基线
if f.startswith(DDL_BASELINE_DIR):
result["has_ddl_baseline"] = True
# 文档文件
if f.endswith(".md") or "/docs/" in f:
doc_files.add(f)
# 代码文件(非文档、非配置)
if f.endswith((".py", ".ts", ".tsx", ".js", ".jsx")):
code_files.append(f)
# 检查代码文件是否有对应文档变更
for cf in code_files:
expected_docs = []
for prefix, docs in DOC_MAP.items():
if cf.startswith(prefix):
expected_docs.extend(docs)
if expected_docs:
# 检查是否有任一对应文档在变更列表中
has_doc = False
for ed in expected_docs:
if ed in doc_files:
has_doc = True
break
# 目录级匹配
if ed.endswith("/"):
if any(d.startswith(ed) for d in doc_files):
has_doc = True
break
if not has_doc:
result["code_without_docs"].append({
"file": cf,
"expected_docs": expected_docs,
})
return result
COMPLIANCE_STATE_PATH = os.path.join(".kiro", "state", ".compliance_state.json")
def save_compliance_state(result, needs_check):
"""持久化合规检查结果,供 audit-writer 子代理读取"""
os.makedirs(os.path.join(".kiro", "state"), exist_ok=True)
now = datetime.now(TZ_TAIPEI)
state = {
"needs_check": needs_check,
"scanned_at": now.isoformat(),
**result,
}
with open(COMPLIANCE_STATE_PATH, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
def main():
files = get_changed_files()
if not files:
save_compliance_state({"new_migration_sql": [], "new_or_modified_sql": [],
"code_without_docs": [], "new_files": [],
"has_bd_manual": False, "has_audit_record": False,
"has_ddl_baseline": False}, False)
print("NO_CHECK_NEEDED")
return
# 过滤噪声
real_files = [f for f in files if not is_noise(f)]
if not real_files:
save_compliance_state({"new_migration_sql": [], "new_or_modified_sql": [],
"code_without_docs": [], "new_files": [],
"has_bd_manual": False, "has_audit_record": False,
"has_ddl_baseline": False}, False)
print("NO_CHECK_NEEDED")
return
result = classify_files(files)
# 判断是否需要审查
needs_check = (
result["new_migration_sql"]
or result["code_without_docs"]
or (result["new_migration_sql"] and not result["has_ddl_baseline"])
)
# 始终持久化结果
save_compliance_state(result, needs_check)
if not needs_check:
print("NO_CHECK_NEEDED")
return
# 输出精简 JSON 供 LLM 审查
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
try:
main()
except Exception as e:
# 出错时不阻塞,输出无需检查
print("NO_CHECK_NEEDED")

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""file_baseline — 基于文件 mtime+size 的独立基线快照系统。
不依赖 git commit 历史,通过扫描工作区文件的 (mtime, size) 指纹,
在 promptSubmit 和 agentStop 之间精确检测"本次对话期间"的文件变更。
用法:
from file_baseline import scan_workspace, diff_baselines, save_baseline, load_baseline
"""
import json
import os
import re
from typing import TypedDict
BASELINE_PATH = os.path.join(".kiro", "state", ".file_baseline.json")
# 扫描时排除的目录(与 .gitignore 对齐 + 额外排除)
EXCLUDE_DIRS = {
".git", ".venv", "venv", "ENV", "env",
"node_modules", "__pycache__", ".hypothesis", ".pytest_cache",
".idea", ".vscode", ".specstory",
"build", "dist", "eggs", ".eggs",
"export", "reports", "tmp",
"htmlcov", ".coverage",
# Kiro 运行时状态不参与业务变更检测
".kiro",
}
# 扫描时排除的文件后缀
EXCLUDE_SUFFIXES = {
".pyc", ".pyo", ".pyd", ".so", ".egg", ".whl",
".log", ".jsonl", ".lnk",
".swp", ".swo",
}
# 扫描时排除的文件名模式
EXCLUDE_NAMES = {
".DS_Store", "Thumbs.db", "desktop.ini",
}
# 业务目录白名单(只扫描这些顶层目录 + 根目录散文件)
# 这样可以避免扫描 .vite/deps 等深层缓存目录
SCAN_ROOTS = [
"apps",
"packages",
"db",
"docs",
"scripts",
"tests",
]
class FileEntry(TypedDict):
mtime: float
size: int
class DiffResult(TypedDict):
added: list[str]
modified: list[str]
deleted: list[str]
def _should_exclude_dir(dirname: str) -> bool:
"""判断目录是否应排除"""
return dirname in EXCLUDE_DIRS or dirname.startswith(".")
def _should_exclude_file(filename: str) -> bool:
"""判断文件是否应排除"""
if filename in EXCLUDE_NAMES:
return True
_, ext = os.path.splitext(filename)
if ext.lower() in EXCLUDE_SUFFIXES:
return True
return False
def scan_workspace(root: str = ".") -> dict[str, FileEntry]:
"""扫描工作区,返回 {相对路径: {mtime, size}} 字典。
只扫描 SCAN_ROOTS 中的目录 + 根目录下的散文件,
跳过 EXCLUDE_DIRS / EXCLUDE_SUFFIXES / EXCLUDE_NAMES。
"""
result: dict[str, FileEntry] = {}
# 1. 根目录散文件pyproject.toml, .env 等)
try:
for entry in os.scandir(root):
if entry.is_file(follow_symlinks=False):
if _should_exclude_file(entry.name):
continue
try:
st = entry.stat(follow_symlinks=False)
rel = entry.name.replace("\\", "/")
result[rel] = {"mtime": st.st_mtime, "size": st.st_size}
except OSError:
continue
except OSError:
pass
# 2. 业务目录递归扫描
for scan_root in SCAN_ROOTS:
top = os.path.join(root, scan_root)
if not os.path.isdir(top):
continue
for dirpath, dirnames, filenames in os.walk(top):
# 原地修改 dirnames 以跳过排除目录
dirnames[:] = [
d for d in dirnames
if not _should_exclude_dir(d)
]
for fname in filenames:
if _should_exclude_file(fname):
continue
full = os.path.join(dirpath, fname)
try:
st = os.stat(full)
rel = os.path.relpath(full, root).replace("\\", "/")
result[rel] = {"mtime": st.st_mtime, "size": st.st_size}
except OSError:
continue
return result
def diff_baselines(
before: dict[str, FileEntry],
after: dict[str, FileEntry],
) -> DiffResult:
"""对比两次快照,返回 added/modified/deleted 列表。"""
before_keys = set(before.keys())
after_keys = set(after.keys())
added = sorted(after_keys - before_keys)
deleted = sorted(before_keys - after_keys)
modified = []
for path in sorted(before_keys & after_keys):
b = before[path]
a = after[path]
# mtime 或 size 任一变化即视为修改
if b["mtime"] != a["mtime"] or b["size"] != a["size"]:
modified.append(path)
return {"added": added, "modified": modified, "deleted": deleted}
def save_baseline(data: dict[str, FileEntry], path: str = BASELINE_PATH):
"""保存基线快照到 JSON 文件。"""
os.makedirs(os.path.dirname(path) or ".kiro", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
def load_baseline(path: str = BASELINE_PATH) -> dict[str, FileEntry]:
"""加载基线快照,文件不存在返回空字典。"""
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def total_changes(diff: DiffResult) -> int:
"""变更文件总数"""
return len(diff["added"]) + len(diff["modified"]) + len(diff["deleted"])

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(os.path.join(".kiro", "state"), exist_ok=True)
last_prompt = {"prompt_id": prompt_id, "at": now.isoformat()}
with open(os.path.join(".kiro", "state", ".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

@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""prompt_on_submit — promptSubmit 合并 hook 脚本v2文件基线模式
合并原 audit_flagger + prompt_audit_log 的功能:
1. 扫描工作区文件 → 保存基线快照 → .kiro/state/.file_baseline.json
2. 基于基线文件列表做风险判定 → .kiro/state/.audit_state.json
3. 记录 prompt 日志 → docs/audit/prompt_logs/
变更检测不再依赖 git status解决不常 commit 导致的误判问题)。
风险判定仍基于 git status因为需要知道哪些文件相对于 commit 有变化)。
所有功能块用 try/except 隔离,单个失败不影响其他。
"""
import hashlib
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
# 同目录导入文件基线模块 + cwd 校验
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from file_baseline import scan_workspace, save_baseline
from _ensure_root import ensure_repo_root
TZ_TAIPEI = timezone(timedelta(hours=8))
# ── 风险规则(来自 audit_flagger ──
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/"),
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", "state", ".audit_state.json")
PROMPT_ID_PATH = os.path.join(".kiro", "state", ".last_prompt_id.json")
def now_taipei():
return datetime.now(TZ_TAIPEI)
def sha1hex(s: str) -> str:
return hashlib.sha1(s.encode("utf-8")).hexdigest()
def get_git_changed_files() -> list[str]:
"""通过 git status 获取变更文件(仅用于风险判定,不用于变更检测)"""
try:
r = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True, encoding="utf-8", errors="replace", 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()
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 safe_read_json(path):
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def write_json(path, data):
os.makedirs(os.path.dirname(path) or os.path.join(".kiro", "state"), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# ── 功能块 1风险标记基于 git status判定哪些文件需要审计 ──
def do_audit_flag(git_files, now):
files = sorted(set(f for f in git_files if not is_noise(f)))
if not files:
write_json(STATE_PATH, {
"audit_required": False,
"db_docs_required": False,
"reasons": [],
"changed_files": [],
"change_fingerprint": "",
"marked_at": now.isoformat(),
"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")
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
existing = safe_read_json(STATE_PATH)
if existing.get("change_fingerprint") == fp:
last_reminded = existing.get("last_reminded_at")
write_json(STATE_PATH, {
"audit_required": audit_required,
"db_docs_required": db_docs_required,
"reasons": reasons,
"changed_files": files[:50],
"change_fingerprint": fp,
"marked_at": now.isoformat(),
"last_reminded_at": last_reminded,
})
# ── 功能块 2Prompt 日志 ──
def do_prompt_log(now):
prompt_id = f"P{now.strftime('%Y%m%d-%H%M%S')}"
prompt_raw = os.environ.get("USER_PROMPT", "")
if len(prompt_raw) > 20000:
prompt_raw = prompt_raw[:5000] + "\n[TRUNCATED: prompt too long]"
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"
entry = f"""- [{prompt_id}] {now.strftime('%Y-%m-%d %H:%M:%S %z')}
- summary: {summary}
- prompt:
```text
{prompt_raw}
```
"""
with open(os.path.join(log_dir, filename), "w", encoding="utf-8") as f:
f.write(entry)
write_json(PROMPT_ID_PATH, {"prompt_id": prompt_id, "at": now.isoformat()})
# ── 功能块 3文件基线快照替代 git snapshot ──
def do_file_baseline():
"""扫描工作区文件 mtime+size保存为基线快照。
agentStop 时再扫一次对比,即可精确检测本次对话期间的变更。
"""
baseline = scan_workspace(".")
save_baseline(baseline)
def main():
ensure_repo_root()
now = now_taipei()
# 功能块 3文件基线快照最先执行记录对话开始时的文件状态
try:
do_file_baseline()
except Exception:
pass
# 功能块 1风险标记仍用 git status因为需要知道未提交的变更
try:
git_files = get_git_changed_files()
do_audit_flag(git_files, now)
except Exception:
pass
# 功能块 2Prompt 日志
try:
do_prompt_log(now)
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception:
pass

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""session_log — agentStop 时记录本次对话的完整日志。
收集来源:
- 环境变量 AGENT_OUTPUTKiro 注入的 agent 输出)
- 环境变量 USER_PROMPT最近一次用户输入
- .kiro/state/.last_prompt_id.jsonPrompt ID 溯源)
- .kiro/state/.audit_state.json变更文件列表
- git diff --stat变更统计
输出docs/audit/session_logs/session_<timestamp>.md
"""
import json
import os
import subprocess
import sys
from datetime import datetime, timezone, timedelta
# cwd 校验
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _ensure_root import ensure_repo_root
TZ_TAIPEI = timezone(timedelta(hours=8))
LOG_DIR = os.path.join("docs", "audit", "session_logs")
STATE_PATH = os.path.join(".kiro", "state", ".audit_state.json")
PROMPT_ID_PATH = os.path.join(".kiro", "state", ".last_prompt_id.json")
def now_taipei():
return datetime.now(TZ_TAIPEI)
def safe_read_json(path):
if not os.path.isfile(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def git_diff_stat():
try:
r = subprocess.run(
["git", "diff", "--stat", "HEAD"],
capture_output=True, text=True, timeout=10
)
return r.stdout.strip() if r.returncode == 0 else "(git diff failed)"
except Exception:
return "(git not available)"
def git_status_short():
try:
r = subprocess.run(
["git", "status", "--short"],
capture_output=True, text=True, timeout=10
)
return r.stdout.strip() if r.returncode == 0 else ""
except Exception:
return ""
def main():
ensure_repo_root()
now = now_taipei()
ts = now.strftime("%Y%m%d_%H%M%S")
timestamp_display = now.strftime("%Y-%m-%d %H:%M:%S %z")
# 收集数据
agent_output = os.environ.get("AGENT_OUTPUT", "")
user_prompt = os.environ.get("USER_PROMPT", "")
prompt_info = safe_read_json(PROMPT_ID_PATH)
audit_state = safe_read_json(STATE_PATH)
prompt_id = prompt_info.get("prompt_id", "unknown")
# 截断超长内容,避免日志文件过大
max_len = 50000
if len(agent_output) > max_len:
agent_output = agent_output[:max_len] + "\n\n[TRUNCATED: output exceeds 50KB]"
if len(user_prompt) > 10000:
user_prompt = user_prompt[:10000] + "\n\n[TRUNCATED: prompt exceeds 10KB]"
diff_stat = git_diff_stat()
status_short = git_status_short()
changed_files = audit_state.get("changed_files", [])
os.makedirs(LOG_DIR, exist_ok=True)
filename = f"session_{ts}.md"
filepath = os.path.join(LOG_DIR, filename)
content = f"""# Session Log — {timestamp_display}
- Prompt-ID: `{prompt_id}`
- Audit Required: `{audit_state.get('audit_required', 'N/A')}`
- Reasons: {', '.join(audit_state.get('reasons', [])) or 'none'}
## User Input
```text
{user_prompt or '(not captured)'}
```
## Agent Output
```text
{agent_output or '(not captured)'}
```
## Changed Files ({len(changed_files)})
```
{chr(10).join(changed_files[:80]) if changed_files else '(none)'}
```
## Git Diff Stat
```
{diff_stat}
```
## Git Status
```
{status_short or '(clean)'}
```
"""
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
if __name__ == "__main__":
try:
main()
except Exception:
pass

View File

@@ -1,4 +1,90 @@
{ {
"mcpServers": { "mcpServers": {
"weixin-devtools-mcp": {
"command": "npx",
"args": ["-y", "weixin-devtools-mcp", "--tools-profile=full", "--ws-endpoint=ws://127.0.0.1:9420"],
"env": {
"WECHAT_DEVTOOLS_CLI": "C:\\dev\\WechatDevtools\\cli.bat",
"WECHAT_DEVTOOLS_PROJECT": "C:\\NeoZQYY\\apps\\miniprogram"
},
"disabled": true,
"autoApprove": ["*"]
},
"git": {
"command": "uvx",
"args": [
"mcp-server-git@2025.12.18",
"--repository",
"C:\\NeoZQYY"
],
"disabled": true,
"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": true,
"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": true,
"autoApprove": [
"all",
"*"
]
}
} }
} }

View File

@@ -1,6 +1,6 @@
# Schema 变更日志Schema Change Log # Schema 变更日志Schema Change Log
- 日期Asia/ShanghaiYYYY-MM-DD - 日期Asia/ShanghaiYYYY-MM-DD HH:MM:SS精确到秒
- Prompt-ID - Prompt-ID
- 原始原因Prompt 摘录/原文): - 原始原因Prompt 摘录/原文):
- 直接原因(必要性 + 方案简介): - 直接原因(必要性 + 方案简介):

View File

@@ -19,4 +19,4 @@
- 例如:状态机枚举范围、唯一性、跨字段一致性约束(如有) - 例如:状态机枚举范围、唯一性、跨字段一致性约束(如有)
## 变更历史Change History ## 变更历史Change History
- YYYY-MM-DD | Prompt-ID | 直接原因 | 变更摘要 - YYYY-MM-DD HH:MM:SS | Prompt-ID | 直接原因 | 变更摘要

View File

@@ -1,6 +1,6 @@
# 变更审计记录Change Audit Record # 变更审计记录Change Audit Record
- 日期/时间Asia/Shanghai - 日期/时间Asia/Shanghai,精确到秒,格式 YYYY-MM-DD HH:MM:SS
- Prompt-ID - Prompt-ID
- 原始原因Prompt 原文或 ≤5 行摘录): - 原始原因Prompt 原文或 ≤5 行摘录):
- 直接原因(必要性 + 修改方案简介): - 直接原因(必要性 + 修改方案简介):

View File

@@ -1,20 +1,22 @@
# 文件内 AI_CHANGELOG 与 CHANGE 标记模板 # 文件内 AI_CHANGELOG 与 CHANGE 标记模板
## 通用 AI_CHANGELOG建议放在文件头部或“变更记录”小节 > 所有时间戳精确到秒,格式:`YYYY-MM-DD HH:MM:SS`,时区 Asia/Shanghai。
- 2026-02-13 | Prompt: P20260213-101530摘录...| Direct cause... | Summary... | Verify...
## 通用 AI_CHANGELOG建议放在文件头部或"变更记录"小节)
- 2026-02-13 10:15:30 | Prompt: P20260213-101530摘录...| Direct cause... | Summary... | Verify...
--- ---
## Markdown / 文档(放在文档末尾或变更记录小节) ## Markdown / 文档(放在文档末尾或"变更记录"小节)
### AI_CHANGELOG ### AI_CHANGELOG
- YYYY-MM-DD | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify... - YYYY-MM-DD HH:MM:SS | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify...
--- ---
## JS/TS块注释 ## JS/TS块注释
/* /*
AI_CHANGELOG AI_CHANGELOG
- YYYY-MM-DD | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify... - YYYY-MM-DD HH:MM:SS | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify...
*/ */
// [CHANGE P...] intent: ... // [CHANGE P...] intent: ...
@@ -27,7 +29,7 @@ AI_CHANGELOG
## Pythondocstring/块注释) ## Pythondocstring/块注释)
""" """
AI_CHANGELOG AI_CHANGELOG
- YYYY-MM-DD | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify... - YYYY-MM-DD HH:MM:SS | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify...
""" """
# [CHANGE P...] intent: ... # [CHANGE P...] intent: ...
@@ -40,7 +42,7 @@ AI_CHANGELOG
## SQL块注释 + 行注释) ## SQL块注释 + 行注释)
/* /*
AI_CHANGELOG AI_CHANGELOG
- YYYY-MM-DD | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify... - YYYY-MM-DD HH:MM:SS | Prompt: P...(摘录:...| Direct cause... | Summary... | Verify...
*/ */
-- [CHANGE P...] intent: ... -- [CHANGE P...] intent: ...
-- assumptions: ... -- assumptions: ...

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,431 @@
# 设计文档小程序数据库基础设施层miniapp-db-foundation
## 概述
本设计实现 P1 基础设施层的三大核心能力:
1. **业务库 Schema 划分**:在 `test_zqyy_app` 中创建 `auth`(认证)和 `biz`(业务)两个 Schema配合权限管理
2. **ETL 库 RLS 视图层**:在 `test_etl_feiqiu.app` Schema 中为 35 张 DWD/DWS 表创建行级安全视图,通过 `site_id` 隔离多门店数据
3. **FDW 跨库映射**:通过 `postgres_fdw` 将 ETL 库的 RLS 视图映射为业务库的只读外部表
**环境变量驱动**:所有数据库名称通过 `.env` 环境变量引用,不硬编码。迁移脚本中使用占位符,验证脚本从 `PG_DSN` / `APP_DB_DSN` 解析连接信息。
| 环境变量 | 用途 | 示例值 |
|---------|------|--------|
| `PG_DSN` | ETL 库连接字符串 | `postgresql://user:pass@host:5432/test_etl_feiqiu` |
| `APP_DB_DSN` | 业务库连接字符串 | `postgresql://user:pass@host:5432/test_zqyy_app` |
整体数据流向:
```
ETL 库PG_DSN 业务库APP_DB_DSN
┌─────────────────────┐ ┌─────────────────────┐
│ dwd.dim_member │ │ auth (用户认证) │
│ dwd.dim_assistant │ │ biz (业务数据) │
│ dws.dws_* │ │ public (系统管理) │
│ dws.cfg_* │ │ │
│ │ │ │ fdw_etl │
│ ▼ │ postgres_fdw │ ├ v_dim_member │
│ app.v_dim_member │ ◄──────────────► │ ├ v_dim_assistant │
│ app.v_dws_* │ IMPORT SCHEMA │ └ v_dws_* │
│ (RLS: site_id 过滤) │ │ (外部表,只读) │
└─────────────────────┘ └─────────────────────┘
```
## 架构
### 分层架构
```mermaid
graph TB
subgraph "业务库APP_DB_DSN"
AUTH["auth Schema<br/>用户认证、权限、映射"]
BIZ["biz Schema<br/>业务数据"]
PUBLIC["public Schema<br/>系统管理表(保留)"]
FDW["fdw_etl Schema<br/>FDW 外部表(只读)"]
end
subgraph "ETL 库PG_DSN"
APP["app Schema<br/>RLS 视图层"]
DWD["dwd Schema<br/>明细层11 张表)"]
DWS["dws Schema<br/>汇总层24 张表)"]
end
FDW -->|"postgres_fdw<br/>IMPORT FOREIGN SCHEMA"| APP
APP -->|"WHERE site_id = current_setting(...)"| DWD
APP -->|"WHERE site_id = current_setting(...)"| DWS
```
### 执行顺序
迁移脚本必须按以下顺序执行:
1. **ETL 库**(通过 `PG_DSN` 连接):创建 `app` Schema → 创建 `app_reader` 角色 → 创建 RLS 视图 → 授权
2. **业务库**(通过 `APP_DB_DSN` 连接):创建 `auth`/`biz` Schema → 安装 `postgres_fdw` → 创建外部服务器 → 用户映射 → 导入外部表 → 授权
## 组件与接口
### 组件 1Schema 管理(业务库)
**职责**:在业务库(`APP_DB_DSN`)中创建 `auth``biz` Schema配置权限。
**SQL 接口**
```sql
-- 创建 Schema
CREATE SCHEMA IF NOT EXISTS auth;
CREATE SCHEMA IF NOT EXISTS biz;
-- 授权 app_user
GRANT USAGE ON SCHEMA auth TO app_user;
GRANT USAGE ON SCHEMA biz TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA biz TO app_user;
-- 未来新表自动授权
ALTER DEFAULT PRIVILEGES IN SCHEMA auth
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA biz
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
```
### 组件 2RLS 视图层ETL 库)
**职责**:在 ETL 库(`PG_DSN`)的 `app` Schema 中为每张源表创建带 `site_id` 过滤的视图。
**视图命名规则**`app.v_<源表名>`,例如 `app.v_dim_member``app.v_dws_member_consumption_summary`
**视图模板**
```sql
CREATE OR REPLACE VIEW app.v_<> AS
SELECT * FROM <schema>.<>
WHERE site_id = current_setting('app.current_site_id')::bigint;
```
**DWD 层视图清单11 张)**
| 视图名 | 源表 |
|--------|------|
| `app.v_dim_member` | `dwd.dim_member` |
| `app.v_dim_assistant` | `dwd.dim_assistant` |
| `app.v_dim_member_card_account` | `dwd.dim_member_card_account` |
| `app.v_dim_table` | `dwd.dim_table` |
| `app.v_dwd_settlement_head` | `dwd.dwd_settlement_head` |
| `app.v_dwd_table_fee_log` | `dwd.dwd_table_fee_log` |
| `app.v_dwd_assistant_service_log` | `dwd.dwd_assistant_service_log` |
| `app.v_dwd_recharge_order` | `dwd.dwd_recharge_order` |
| `app.v_dwd_store_goods_sale` | `dwd.dwd_store_goods_sale` |
| `app.v_dim_staff` | `dwd.dim_staff` |
| `app.v_dim_staff_ex` | `dwd.dim_staff_ex` |
**DWS 层视图清单24 张)**
| 视图名 | 源表 |
|--------|------|
| `app.v_dws_member_consumption_summary` | `dws.dws_member_consumption_summary` |
| `app.v_dws_member_visit_detail` | `dws.dws_member_visit_detail` |
| `app.v_dws_member_winback_index` | `dws.dws_member_winback_index` |
| `app.v_dws_member_newconv_index` | `dws.dws_member_newconv_index` |
| `app.v_dws_member_recall_index` | `dws.dws_member_recall_index` |
| `app.v_dws_member_assistant_relation_index` | `dws.dws_member_assistant_relation_index` |
| `app.v_dws_member_assistant_intimacy` | `dws.dws_member_assistant_intimacy` |
| `app.v_dws_assistant_daily_detail` | `dws.dws_assistant_daily_detail` |
| `app.v_dws_assistant_monthly_summary` | `dws.dws_assistant_monthly_summary` |
| `app.v_dws_assistant_salary_calc` | `dws.dws_assistant_salary_calc` |
| `app.v_dws_assistant_customer_stats` | `dws.dws_assistant_customer_stats` |
| `app.v_dws_assistant_finance_analysis` | `dws.dws_assistant_finance_analysis` |
| `app.v_dws_finance_daily_summary` | `dws.dws_finance_daily_summary` |
| `app.v_dws_finance_income_structure` | `dws.dws_finance_income_structure` |
| `app.v_dws_finance_recharge_summary` | `dws.dws_finance_recharge_summary` |
| `app.v_dws_finance_discount_detail` | `dws.dws_finance_discount_detail` |
| `app.v_dws_finance_expense_summary` | `dws.dws_finance_expense_summary` |
| `app.v_dws_platform_settlement` | `dws.dws_platform_settlement` |
| `app.v_dws_assistant_recharge_commission` | `dws.dws_assistant_recharge_commission` |
| `app.v_cfg_performance_tier` | `dws.cfg_performance_tier` |
| `app.v_cfg_assistant_level_price` | `dws.cfg_assistant_level_price` |
| `app.v_cfg_bonus_rules` | `dws.cfg_bonus_rules` |
| `app.v_cfg_index_parameters` | `dws.cfg_index_parameters` |
| `app.v_dws_order_summary` | `dws.dws_order_summary` |
**P2 预留(注释标记,暂不创建)**
- `dws.dws_member_spending_power_index` → 待 P2 完成后补充
- `dws.dws_assistant_order_contribution` → 待 P2 完成后补充
**`cfg_*` 表特殊处理**:配置表(`cfg_performance_tier``cfg_assistant_level_price``cfg_bonus_rules``cfg_index_parameters`)可能不含 `site_id` 列。对于不含 `site_id` 的配置表,视图直接 `SELECT *` 不加过滤条件。
**权限配置**
```sql
-- 创建只读角色(如不存在)
DO $$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_reader') THEN
CREATE ROLE app_reader LOGIN;
END IF;
END $$;
GRANT USAGE ON SCHEMA app TO app_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT ON TABLES TO app_reader;
```
### 组件 3FDW 跨库映射(业务库)
**职责**:通过 `postgres_fdw` 将 ETL 库 `app` Schema 的视图映射为业务库 `fdw_etl` Schema 的外部表。
**实现方式**:使用 `IMPORT FOREIGN SCHEMA` 批量导入,而非逐表定义外部表。这与现有 `db/fdw/setup_fdw_test.sql` 的模式一致。
**环境感知**:迁移脚本中的 `host``dbname``port``password` 等连接参数使用占位符 `'***'`,部署时根据环境替换。项目已有 `db/fdw/setup_fdw_test.sql`(测试环境)和 `db/fdw/setup_fdw.sql`(生产环境)的分环境模式,本次迁移脚本遵循相同模式——提供测试环境和生产环境两个版本。
```sql
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- 创建外部服务器
-- host / dbname / port 按环境替换,从 PG_DSN 解析 ETL 库连接信息
CREATE SERVER IF NOT EXISTS etl_feiqiu_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '***', dbname '***', port '***');
-- 用户映射(密码按环境替换)
CREATE USER MAPPING IF NOT EXISTS FOR app_user
SERVER etl_feiqiu_server
OPTIONS (user 'app_reader', password '***');
-- 创建目标 Schema
CREATE SCHEMA IF NOT EXISTS fdw_etl;
-- 批量导入
IMPORT FOREIGN SCHEMA app
FROM SERVER etl_feiqiu_server
INTO fdw_etl;
-- 授权
GRANT USAGE ON SCHEMA fdw_etl TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA fdw_etl TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_etl GRANT SELECT ON TABLES TO app_user;
```
**设计决策**
1. 使用 `IMPORT FOREIGN SCHEMA` 而非逐表 `CREATE FOREIGN TABLE`——自动匹配列定义,避免手动维护列类型不一致的风险
2. 新增 RLS 视图后只需重新执行 `IMPORT` 即可同步
3. 与现有 `db/fdw/setup_fdw_test.sql` 保持一致
4. 服务器名使用通用名 `etl_feiqiu_server`(不含环境前缀),通过连接参数区分环境
### 组件 4验证脚本
**职责**:自动化检查所有数据库对象是否正确创建。
**文件位置**`scripts/ops/validate_p1_db_foundation.py`
**接口**
```python
def validate_p1_db_foundation() -> dict:
"""
返回验证结果字典:
{
"schemas": {"auth": bool, "biz": bool, "app": bool, "fdw_etl": bool},
"rls_views": {"app.v_dim_member": bool, ...},
"fdw_tables": {"fdw_etl.v_dim_member": bool, ...},
"rls_filtering": bool,
"permissions": {"app_user": bool, "app_reader": bool},
"errors": [str, ...]
}
"""
```
**环境变量依赖**(强制从 `.env` 加载,缺失时 `RuntimeError` 终止):
- `PG_DSN`ETL 库连接字符串(从中解析 host、port、dbname
- `APP_DB_DSN`:业务库连接字符串(从中解析 host、port、dbname
- 脚本通过 `load_dotenv()` 加载根 `.env`,禁止硬编码任何数据库名称或连接参数
## 数据模型
### Schema 拓扑
```mermaid
erDiagram
test_zqyy_app_auth {
string schema_name "auth"
string purpose "用户认证、权限、映射"
}
test_zqyy_app_biz {
string schema_name "biz"
string purpose "业务数据任务、备注、AI、Excel"
}
test_zqyy_app_fdw_etl {
string schema_name "fdw_etl"
string purpose "FDW 外部表(只读)"
}
test_zqyy_app_public {
string schema_name "public"
string purpose "系统管理表(保留)"
}
test_etl_feiqiu_app {
string schema_name "app"
string purpose "RLS 视图层"
}
test_etl_feiqiu_dwd {
string schema_name "dwd"
string purpose "明细层11 张表)"
}
test_etl_feiqiu_dws {
string schema_name "dws"
string purpose "汇总层24 张表)"
}
test_etl_feiqiu_app ||--o{ test_etl_feiqiu_dwd : "视图引用"
test_etl_feiqiu_app ||--o{ test_etl_feiqiu_dws : "视图引用"
test_zqyy_app_fdw_etl ||--|| test_etl_feiqiu_app : "postgres_fdw 映射"
```
### RLS 视图数据流
对于含 `site_id` 的表:
```
源表数据(全量)→ RLS 视图site_id 过滤)→ FDW 外部表(只读访问)
```
对于不含 `site_id` 的配置表(`cfg_*`
```
源表数据(全量)→ 直通视图(无过滤)→ FDW 外部表(只读访问)
```
### 迁移脚本清单
| 序号 | 目标库 | 文件名 | 内容 |
|------|--------|--------|------|
| 1 | ETL 库(`PG_DSN` | `YYYY-MM-DD__p1_create_app_schema_rls_views.sql` | 创建 app Schema + 全部 RLS 视图 + app_reader 权限 |
| 2 | 业务库(`APP_DB_DSN` | `YYYY-MM-DD__p1_create_auth_biz_schemas.sql` | 创建 auth/biz Schema + app_user 权限 |
| 3 | 业务库(`APP_DB_DSN` | `YYYY-MM-DD__p1_setup_fdw_etl.sql` | FDW 扩展 + 外部服务器 + 用户映射 + 导入外部表 |
## 正确性属性Correctness Properties
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1默认权限自动授予
*For any*`auth``biz` Schema 中新创建的表,`app_user` 角色都应自动获得 SELECT、INSERT、UPDATE、DELETE 权限,无需额外手动授权。
**Validates: Requirements 1.5**
### Property 2public Schema 不变量
*For any* 迁移脚本执行前后,`test_zqyy_app.public` Schema 中的表集合应保持不变——迁移不应删除、重命名或修改 `public` 中的现有表。
**Validates: Requirements 1.6**
### Property 3RLS 视图定义包含 site_id 过滤
*For any* `test_etl_feiqiu.app` Schema 中的 RLS 视图(含 `site_id` 列的源表对应的视图),其视图定义 SQL 中都应包含 `current_setting('app.current_site_id')` 过滤条件。
**Validates: Requirements 2.4**
### Property 4RLS 过滤正确性
*For any*`site_id` 列的 RLS 视图和任意有效的 `site_id` 值,设置 `app.current_site_id` 后查询该视图,返回结果中所有行的 `site_id` 都应等于设置的值。
**Validates: Requirements 2.5**
### Property 5未设置 site_id 时 RLS 视图拒绝访问
*For any*`site_id` 过滤的 RLS 视图,在未设置 `app.current_site_id` 的会话中执行查询,应抛出错误而非返回数据。
**Validates: Requirements 2.6**
### Property 6FDW 外部表完整性与数据一致性
*For any* `test_etl_feiqiu.app` Schema 中的视图,`test_zqyy_app.fdw_etl` Schema 中都应存在对应的可查询外部表,且在相同 `site_id` 条件下,外部表返回的数据与 RLS 视图返回的数据一致。
**Validates: Requirements 3.4, 3.5, 3.6**
### Property 7迁移脚本结构合规性
*For any* `db/etl_feiqiu/migrations/``db/zqyy_app/migrations/` 中本次新增的迁移脚本文件,文件名应匹配 `YYYY-MM-DD__*.sql` 模式,且文件内容中应包含回滚语句(以注释形式)。
**Validates: Requirements 4.3, 4.4**
### Property 8迁移脚本幂等性
*For any* 本次新增的迁移脚本,连续执行两次的结果应与执行一次相同——第二次执行不应产生错误。
**Validates: Requirements 4.5**
### Property 9环境变量缺失时验证脚本报错
*For any* 必需环境变量(`PG_DSN``APP_DB_DSN`)的缺失组合,验证脚本应立即抛出错误终止,而非静默使用默认值或空字符串。
**Validates: Requirements 5.8**
## 错误处理
### 迁移脚本错误处理
| 场景 | 处理方式 |
|------|---------|
| Schema 已存在 | `CREATE SCHEMA IF NOT EXISTS` 幂等跳过 |
| 视图已存在 | `CREATE OR REPLACE VIEW` 覆盖更新 |
| 角色不存在 | `DO $$ ... IF NOT EXISTS ... END $$` 条件创建 |
| 源表不存在P2 待建表) | 以注释形式预留,不创建视图 |
| FDW 服务器已存在 | `CREATE SERVER IF NOT EXISTS` 幂等跳过 |
| 用户映射已存在 | `CREATE USER MAPPING IF NOT EXISTS` 幂等跳过 |
| `IMPORT FOREIGN SCHEMA` 表已存在 | 先 `DROP SCHEMA fdw_etl CASCADE` 再重新导入(脚本中提供选项) |
### 验证脚本错误处理
| 场景 | 处理方式 |
|------|---------|
| 环境变量缺失 | `RuntimeError` 立即终止,输出缺失变量名 |
| 数据库连接失败 | 捕获 `psycopg2.OperationalError`,输出连接参数(脱敏)和错误信息 |
| Schema/视图/外部表不存在 | 记录为失败项,继续检查其余项目 |
| RLS 过滤验证无数据 | 标记为 SKIP无法验证不标记为失败 |
| 权限查询失败 | 记录具体错误,继续检查 |
### `current_setting` 未设置时的行为
PostgreSQL 中 `current_setting('app.current_site_id')` 在未设置时会抛出 `ERROR: unrecognized configuration parameter "app.current_site_id"`。这是期望行为(需求 2.6),确保不会意外返回全量数据。
如果需要更友好的错误信息,可以使用 `current_setting('app.current_site_id', true)` 返回 NULL然后在视图中用 `CASE` 处理。但当前设计选择让 PostgreSQL 原生报错,因为:
1. 更安全——不可能绕过
2. 后端代码必须显式设置 `SET app.current_site_id = ...`,这是一个强制约束
## 测试策略
### 属性测试Property-Based Testing
使用 Python `hypothesis` 框架,测试目录:`tests/`Monorepo 级属性测试目录)。
每个属性测试至少运行 100 次迭代。每个测试用注释标注对应的设计属性编号。
标注格式:`# Feature: miniapp-db-foundation, Property N: <属性标题>`
**属性测试清单**
| 属性 | 测试方法 | 生成器 |
|------|---------|--------|
| P1 默认权限 | 生成随机表名,在 auth/biz 中创建表,验证 app_user 权限 | `hypothesis.strategies.text` 生成合法 SQL 标识符 |
| P3 视图定义过滤 | 遍历所有 app schema 视图,检查定义 SQL | 无需生成器,遍历所有视图 |
| P4 RLS 过滤正确性 | 生成随机 site_id设置后查询视图验证结果 | `hypothesis.strategies.integers` 生成 site_id |
| P5 未设置 site_id 报错 | 遍历所有 RLS 视图,在新会话中查询 | 无需生成器,遍历所有视图 |
| P7 脚本结构合规 | 遍历所有新增迁移脚本,验证命名和内容 | 无需生成器,遍历文件 |
| P8 幂等性 | 对每个迁移脚本执行两次,验证无错误 | 无需生成器 |
| P9 环境变量缺失 | 生成环境变量缺失组合,验证报错 | `hypothesis.strategies.sampled_from` 生成缺失组合 |
**注意**P2public schema 不变量)和 P6FDW 数据一致性)需要真实数据库环境,作为集成测试在验证脚本中实现,而非 hypothesis 属性测试。
### 单元测试
单元测试聚焦于验证脚本的逻辑正确性:
- 验证脚本在 Schema 缺失时正确报告失败
- 验证脚本在权限不足时正确报告
- 验证脚本的输出格式正确JSON 结构)
- 环境变量缺失时的错误消息包含变量名
### 集成测试
集成测试通过验证脚本 `scripts/ops/validate_p1_db_foundation.py` 实现,覆盖:
- 全部 Schema 存在性检查
- 全部 RLS 视图存在性和过滤正确性
- 全部 FDW 外部表存在性和可查询性
- 权限配置完整性
- FDW 数据与 RLS 视图数据一致性

View File

@@ -0,0 +1,88 @@
# 需求文档小程序数据库基础设施层miniapp-db-foundation
## 简介
P1 基础设施层是整个小程序系统的第一个 SPEC无前置依赖是所有后续 SPEC 的硬依赖。本 SPEC 负责在业务库 `test_zqyy_app` 中建立清晰的 Schema 划分(`auth` + `biz`),在 ETL 库 `test_etl_feiqiu` 中为数据依赖矩阵列出的所有 DWD/DWS 表创建 RLS 视图(按 `site_id` 隔离),并通过 `postgres_fdw` 将 RLS 视图映射为业务库的外部表,使后端无需直连 ETL 库即可读取汇总/维度数据。
## 术语表
- **Schema_Manager**:负责在 PostgreSQL 数据库中创建和管理 Schema、权限配置的迁移脚本系统
- **RLS_View_Layer**:在 `test_etl_feiqiu.app` Schema 中创建的一组视图,通过 `current_setting('app.current_site_id')``site_id` 过滤数据,实现行级安全隔离
- **FDW_Bridge**:通过 `postgres_fdw` 扩展在 `test_zqyy_app.fdw_etl` Schema 中创建的外部表集合,只读映射 ETL 库 `app` Schema 的 RLS 视图
- **Migration_Script**:存放在 `db/zqyy_app/migrations/``db/etl_feiqiu/migrations/` 中的纯 SQL 迁移脚本,以日期前缀命名
- **Validation_Script**:用于验证数据库对象是否正确创建、权限是否配置正确、数据是否可查询的 Python 脚本
- **site_id**:门店标识符,类型为 `BIGINT`,用于多门店数据隔离
- **app_reader**ETL 库侧的只读角色,供 FDW 用户映射使用
- **app_user**:业务库侧的应用连接角色,通过 FDW 读取 ETL 数据
## 需求
### 需求 1业务库 Schema 划分与权限配置
**用户故事:** 作为后端开发者,我需要 `test_zqyy_app` 中有清晰的 Schema 划分(`auth` + `biz`),以便按功能组织业务表。
#### 验收标准
1. WHEN Migration_Script 执行完成, THE Schema_Manager SHALL 在 `test_zqyy_app` 中创建 `auth` Schema
2. WHEN Migration_Script 执行完成, THE Schema_Manager SHALL 在 `test_zqyy_app` 中创建 `biz` Schema
3. WHEN `auth` Schema 创建完成, THE Schema_Manager SHALL 授予 `app_user` 角色对 `auth` Schema 的 USAGE 权限和对其中所有表的 SELECT、INSERT、UPDATE、DELETE 权限
4. WHEN `biz` Schema 创建完成, THE Schema_Manager SHALL 授予 `app_user` 角色对 `biz` Schema 的 USAGE 权限和对其中所有表的 SELECT、INSERT、UPDATE、DELETE 权限
5. WHEN 新表在 `auth``biz` Schema 中创建, THE Schema_Manager SHALL 通过 ALTER DEFAULT PRIVILEGES 自动授予 `app_user` 角色相应权限
6. THE Migration_Script SHALL 保留 `public` Schema 中现有的系统管理表(`admin_users``roles``permissions` 等)不受影响
### 需求 2ETL 库 RLS 视图层创建
**用户故事:** 作为系统管理员,我需要 RLS 视图按 `site_id` 隔离数据,以便多门店数据安全。
#### 验收标准
1. WHEN Migration_Script 执行完成, THE Schema_Manager SHALL 在 `test_etl_feiqiu` 中创建 `app` Schema如不存在
2. WHEN `app` Schema 创建完成, THE RLS_View_Layer SHALL 为数据依赖矩阵中列出的每张 DWD 表创建对应的 RLS 视图(共 11 张:`dim_member``dim_assistant``dim_member_card_account``dim_table``dwd_settlement_head``dwd_table_fee_log``dwd_assistant_service_log``dwd_recharge_order``dwd_store_goods_sale``dim_staff``dim_staff_ex`
3. WHEN `app` Schema 创建完成, THE RLS_View_Layer SHALL 为数据依赖矩阵中列出的每张 DWS 表创建对应的 RLS 视图(共 24 张,包含 `dws_*``cfg_*` 表)
4. THE RLS_View_Layer 中每个视图 SHALL 包含 `WHERE site_id = current_setting('app.current_site_id')::bigint` 过滤条件
5. WHEN 设置 `app.current_site_id` 为某门店 ID 后查询 RLS 视图, THE RLS_View_Layer SHALL 仅返回该门店的数据
6. WHEN 未设置 `app.current_site_id` 时查询 RLS 视图, THE RLS_View_Layer SHALL 抛出错误而非返回全部数据
7. THE Schema_Manager SHALL 授予 `app_reader` 角色对 `app` Schema 的 USAGE 权限和对其中所有视图的 SELECT 权限
8. THE RLS_View_Layer SHALL 为 P2 待建表(`dws_member_spending_power_index``dws_assistant_order_contribution`)在迁移脚本中以注释形式预留位置
### 需求 3FDW 外部表映射
**用户故事:** 作为后端开发者,我需要通过 FDW 从 `test_zqyy_app` 读取 ETL 库的 DWS/DWD 数据,以便小程序页面展示 ETL 计算结果。
#### 验收标准
1. WHEN Migration_Script 执行完成, THE FDW_Bridge SHALL 在 `test_zqyy_app` 中安装 `postgres_fdw` 扩展
2. WHEN `postgres_fdw` 扩展安装完成, THE FDW_Bridge SHALL 创建指向 `test_etl_feiqiu` 的外部服务器 `test_etl_feiqiu_server`
3. WHEN 外部服务器创建完成, THE FDW_Bridge SHALL 创建 `app_user``app_reader` 的用户映射
4. WHEN 用户映射创建完成, THE FDW_Bridge SHALL 在 `fdw_etl` Schema 中通过 `IMPORT FOREIGN SCHEMA app` 导入所有外部表
5. WHEN 外部表导入完成, THE FDW_Bridge SHALL 对 `fdw_etl` Schema 中的每张外部表执行 `SELECT` 查询验证可读性
6. WHEN 外部表查询成功, THE FDW_Bridge 返回的数据 SHALL 与 ETL 库 `app` Schema 中对应 RLS 视图的数据一致
7. THE FDW_Bridge SHALL 授予 `app_user` 角色对 `fdw_etl` Schema 的 USAGE 权限和对其中所有外部表的 SELECT 权限
### 需求 4迁移脚本管理
**用户故事:** 作为后端开发者,我需要所有数据库变更都有对应的迁移脚本,以便变更可追溯、可重放、可回滚。
#### 验收标准
1. THE Migration_Script SHALL 将 ETL 库变更RLS 视图创建、`app` Schema 权限)存放在 `db/etl_feiqiu/migrations/` 目录中
2. THE Migration_Script SHALL 将业务库变更Schema 创建、FDW 配置)存放在 `db/zqyy_app/migrations/` 目录中
3. THE Migration_Script SHALL 使用日期前缀命名(格式:`YYYY-MM-DD__<描述>.sql`
4. THE Migration_Script SHALL 在每个脚本中包含回滚语句(以注释形式)
5. THE Migration_Script SHALL 使用 `IF NOT EXISTS` / `OR REPLACE` 等幂等语法,确保重复执行不会报错
6. THE Migration_Script SHALL 使用 UTF-8 编码,纯 SQL非 ORM
### 需求 5端到端验证
**用户故事:** 作为后端开发者,我需要一个自动化验证脚本,确认所有数据库对象正确创建且数据可访问。
#### 验收标准
1. WHEN Validation_Script 执行时, THE Validation_Script SHALL 检查 `test_zqyy_app``auth``biz` Schema 是否存在
2. WHEN Validation_Script 执行时, THE Validation_Script SHALL 检查 `test_etl_feiqiu.app` Schema 中所有 RLS 视图是否存在
3. WHEN Validation_Script 执行时, THE Validation_Script SHALL 检查 `test_zqyy_app.fdw_etl` 中所有外部表是否存在
4. WHEN Validation_Script 执行时, THE Validation_Script SHALL 对每张外部表执行 `SELECT count(*)` 验证可查询性
5. WHEN Validation_Script 执行时, THE Validation_Script SHALL 设置 `app.current_site_id` 后验证 RLS 视图正确过滤数据
6. WHEN Validation_Script 执行时, THE Validation_Script SHALL 验证 `app_user``app_reader` 角色的权限配置正确
7. WHEN 验证发现异常, THE Validation_Script SHALL 输出具体的失败项和错误信息
8. THE Validation_Script SHALL 从 `.env` 加载数据库连接参数(`PG_DSN``APP_DB_DSN`),缺失时立即报错终止

View File

@@ -0,0 +1,108 @@
# 实现计划小程序数据库基础设施层miniapp-db-foundation
## 概述
按照"ETL 库先行 → 业务库跟进 → FDW 桥接 → 验证收尾"的顺序,将设计拆分为可增量执行的编码任务。每个迁移脚本使用幂等语法,所有连接参数通过环境变量驱动。
## 任务
- [x] 1. ETL 库:创建 app Schema 与 RLS 视图
- [x] 1.1 编写迁移脚本 `db/etl_feiqiu/migrations/YYYY-MM-DD__p1_create_app_schema_rls_views.sql`
- 创建 `app` Schema`CREATE SCHEMA IF NOT EXISTS app`
- 创建 `app_reader` 角色(条件创建,`DO $$ ... IF NOT EXISTS ... END $$`
- 为 11 张 DWD 表创建 RLS 视图(`CREATE OR REPLACE VIEW app.v_<表名> AS SELECT * FROM dwd.<表名> WHERE site_id = current_setting('app.current_site_id')::bigint`
- 为 24 张 DWS 表创建 RLS 视图(同上模式;`cfg_*` 配置表若无 `site_id` 列则直接 `SELECT *` 不加过滤)
- 在脚本末尾以注释形式预留 P2 待建表位置(`dws_member_spending_power_index``dws_assistant_order_contribution`
- 授予 `app_reader``app` Schema 的 USAGE + SELECT 权限 + ALTER DEFAULT PRIVILEGES
- 包含回滚语句(注释形式)
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.7, 2.8, 4.1, 4.3, 4.4, 4.5, 4.6_
- [x] 1.2 编写属性测试RLS 视图定义包含 site_id 过滤
- **Property 3: RLS 视图定义包含 site_id 过滤**
- 遍历 `app` Schema 所有视图,查询 `pg_views.definition`,验证含 `site_id` 列的源表对应视图包含 `current_setting` 过滤条件
- **Validates: Requirements 2.4**
- [x] 1.3 编写属性测试:未设置 site_id 时 RLS 视图拒绝访问
- **Property 5: 未设置 site_id 时 RLS 视图拒绝访问**
- 遍历所有含 `site_id` 过滤的 RLS 视图,在新会话(未设置 `app.current_site_id`)中执行 `SELECT`,验证抛出错误
- **Validates: Requirements 2.6**
- [x] 2. 业务库:创建 auth/biz Schema 与权限
- [x] 2.1 编写迁移脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p1_create_auth_biz_schemas.sql`
- 创建 `auth` Schema`CREATE SCHEMA IF NOT EXISTS auth`
- 创建 `biz` Schema`CREATE SCHEMA IF NOT EXISTS biz`
- 授予 `app_user``auth`/`biz` 的 USAGE + CRUD 权限
- 设置 ALTER DEFAULT PRIVILEGES 自动授权未来新表
- 包含回滚语句(注释形式)
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 4.2, 4.3, 4.4, 4.5, 4.6_
- [x] 2.2 编写属性测试:默认权限自动授予
- **Property 1: 默认权限自动授予**
- 使用 hypothesis 生成随机合法表名,在 `auth`/`biz` 中创建临时表,验证 `app_user` 自动获得 SELECT/INSERT/UPDATE/DELETE 权限,测试后清理
- **Validates: Requirements 1.5**
- [x] 3. 业务库:配置 FDW 跨库映射
- [x] 3.1 编写迁移脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p1_setup_fdw_etl.sql`
- 安装 `postgres_fdw` 扩展
- 创建外部服务器 `etl_feiqiu_server`host/dbname/port 使用占位符 `'***'`,按环境替换)
- 创建 `app_user``app_reader` 用户映射
- 创建 `fdw_etl` Schema
- 执行 `IMPORT FOREIGN SCHEMA app FROM SERVER etl_feiqiu_server INTO fdw_etl`
- 授予 `app_user``fdw_etl` 的 USAGE + SELECT 权限 + ALTER DEFAULT PRIVILEGES
- 包含回滚语句(注释形式)
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.7, 4.2, 4.3, 4.4, 4.5, 4.6_
- [x] 3.2 编写属性测试FDW 外部表完整性与数据一致性
- **Property 6: FDW 外部表完整性与数据一致性**
- 遍历 ETL 库 `app` Schema 所有视图,验证 `fdw_etl` 中存在对应外部表且可查询;在相同 `site_id` 下对比数据一致性
- **Validates: Requirements 3.4, 3.5, 3.6**
- [x] 4. Checkpoint — 迁移脚本验证
- 确保三个迁移脚本均可在测试环境中成功执行按顺序ETL 库 → 业务库 Schema → 业务库 FDW
- 验证重复执行不报错(幂等性)
- 如有问题请告知用户
- [x] 5. 编写端到端验证脚本
- [x] 5.1 创建 `scripts/ops/validate_p1_db_foundation.py`
- 通过 `load_dotenv()` 加载根 `.env`,读取 `PG_DSN``APP_DB_DSN`,缺失时 `RuntimeError` 终止
- 检查业务库中 `auth``biz` Schema 存在性
- 检查 ETL 库中 `app` Schema 及所有 RLS 视图存在性(对照设计文档中的 35 张视图清单)
- 检查业务库中 `fdw_etl` Schema 及所有外部表存在性
- 对每张外部表执行 `SELECT count(*)` 验证可查询性
- 设置 `app.current_site_id` 后验证 RLS 视图过滤正确性
- 验证 `app_user``app_reader` 角色权限配置
- 输出结构化验证结果(通过/失败/跳过),失败项附带错误信息
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8_
- [x] 5.2 编写属性测试RLS 过滤正确性
- **Property 4: RLS 过滤正确性**
- 使用 hypothesis 生成随机 `site_id`,设置 `app.current_site_id` 后查询含 `site_id` 的 RLS 视图,验证返回结果中所有行的 `site_id` 等于设置值
- **Validates: Requirements 2.5**
- [x] 5.3 编写属性测试:环境变量缺失报错
- **Property 9: 环境变量缺失时验证脚本报错**
- 使用 hypothesis 生成 `PG_DSN`/`APP_DB_DSN` 的缺失组合,验证脚本抛出 `RuntimeError`
- **Validates: Requirements 5.8**
- [x] 5.4 编写属性测试:迁移脚本结构合规性
- **Property 7: 迁移脚本结构合规性**
- 遍历本次新增的迁移脚本文件,验证文件名匹配 `YYYY-MM-DD__*.sql` 模式,且内容包含回滚语句注释
- **Validates: Requirements 4.3, 4.4**
- [x] 5.5 编写属性测试:迁移脚本幂等性
- **Property 8: 迁移脚本幂等性**
- 对每个迁移脚本连续执行两次,验证第二次执行无错误
- **Validates: Requirements 4.5**
- [x] 6. Final checkpoint — 全量验证
- 运行验证脚本 `python scripts/ops/validate_p1_db_foundation.py`,确认所有检查项通过
- 运行属性测试 `cd C:\NeoZQYY && pytest tests/ -v -k p1`,确认所有属性测试通过
- 如有问题请告知用户
## 说明
- 标记 `*` 的子任务为可选,可跳过以加速 MVP
- 每个任务引用具体的需求编号,确保可追溯
- Checkpoint 确保增量验证
- 属性测试使用 Python hypothesis 框架,测试文件放在根目录 `tests/`
- 迁移脚本中的数据库连接参数host/dbname/port/password均使用占位符按环境替换

View File

@@ -0,0 +1,562 @@
# 设计文档ETL DWS 层扩展 — 小程序数据支撑
## 概述
本设计覆盖三个独立但相关的 DWS 层扩展模块:
1. **助教订单流水四项统计**:新建 `AssistantOrderContributionTask`,计算每名助教每日的订单总流水、订单净流水、时效贡献流水、时效净贡献。算法核心在于"时效贡献流水"的台费分摊和酒水食品均分逻辑。
2. **会员消费汇总扩展**:修改现有 `MemberConsumptionTask`,新增 30/60/90 天充值窗口统计和次均消费字段。
3. **定档折算惩罚**:修改现有 `AssistantDailyTask`,新增时间重叠检测和惩罚计算逻辑。
三个模块共享同一套 RLS 视图 + FDW 映射基础设施。
### 设计决策
1. **助教订单流水独立建表**:四项统计粒度为 `(site_id, assistant_id, stat_date)`,与现有 `dws_assistant_daily_detail` 粒度相同但语义不同daily_detail 聚焦服务时长/金额contribution 聚焦订单级流水分摊),独立建表避免字段膨胀。`stat_date` 为营业日(以 `BUSINESS_DAY_START_HOUR` 08:00 为日切点)。
2. **时效贡献流水计算为纯函数**:核心分摊算法(`compute_time_weighted_revenue`)设计为静态方法,输入为结构化的订单数据,输出为每名助教的贡献值。不依赖数据库,便于属性测试。
3. **惩罚检测在 transform 阶段完成**:定档折算惩罚的时间重叠检测和计算在 `AssistantDailyTask.transform` 中完成,不新建独立任务,因为惩罚字段与日度明细同粒度。
4. **充值统计复用现有 extract 模式**:在 `MemberConsumptionTask` 中新增一个 `_extract_recharge_stats` 方法,与现有的 `_extract_consumption_stats` 并行提取,在 transform 阶段合并。
## 架构
```mermaid
graph TD
subgraph 数据来源DWD
SH[dwd_settlement_head<br/>结算主表]
TF[dwd_table_fee_log<br/>台费明细]
ASL[dwd_assistant_service_log<br/>助教服务记录]
RO[dwd_recharge_order<br/>充值订单]
end
subgraph 新建任务
AOC[AssistantOrderContributionTask<br/>助教订单流水统计]
end
subgraph 修改任务
MCT[MemberConsumptionTask<br/>+充值窗口 +次均消费]
ADT[AssistantDailyTask<br/>+惩罚检测 +惩罚计算]
end
subgraph 输出DWS
T1[dws_assistant_order_contribution<br/>新建]
T2[dws_member_consumption_summary<br/>扩展字段]
T3[dws_assistant_daily_detail<br/>扩展字段]
end
subgraph 基础设施
RLS[app schema RLS 视图]
FDW[fdw_etl 外部表映射]
end
SH --> AOC
TF --> AOC
ASL --> AOC
AOC --> T1
RO --> MCT
SH --> MCT
MCT --> T2
ASL --> ADT
TF --> ADT
ADT --> T3
T1 --> RLS
T2 --> RLS
T3 --> RLS
RLS --> FDW
```
### 任务依赖关系
```
DWD_LOAD_FROM_ODS
├── DWS_ASSISTANT_DAILY (扩展:+惩罚检测计算)
├── DWS_MEMBER_CONSUMPTION (扩展:+充值窗口+次均消费)
└── DWS_ASSISTANT_ORDER_CONTRIBUTION (新建:四项统计)
```
`DWS_ASSISTANT_ORDER_CONTRIBUTION` 依赖 `DWD_LOAD_FROM_ODS`(需要最新的结算、台费、服务记录数据)。
## 组件与接口
### AssistantOrderContributionTask新建
继承 `BaseDwsTask`,实现四项统计计算:
```python
class AssistantOrderContributionTask(BaseDwsTask):
DATE_COL = "stat_date"
def get_task_code(self) -> str:
return "DWS_ASSISTANT_ORDER_CONTRIBUTION"
def get_target_table(self) -> str:
return "dws_assistant_order_contribution"
def get_primary_keys(self) -> List[str]:
return ["site_id", "assistant_id", "stat_date"]
# --- ETL 主流程 ---
def extract(self, context: TaskContext) -> Dict[str, Any]: ...
def transform(self, extracted, context) -> List[Dict[str, Any]]: ...
# load() 使用 BaseDwsTask 默认实现
# --- 数据提取 ---
def _extract_order_data(self, site_id, start_date, end_date) -> List[Dict]: ...
# --- 核心计算(纯函数,可独立测试) ---
@staticmethod
def compute_order_gross_revenue(order: OrderData) -> Decimal:
"""订单总流水 = 台费 + 酒水食品 + 所有助教服务费"""
...
@staticmethod
def compute_order_net_revenue(order: OrderData) -> Decimal:
"""订单净流水 = 订单总流水 - 所有助教服务分成"""
...
@staticmethod
def compute_time_weighted_revenue(
order: OrderData, assistant_id: int
) -> Decimal:
"""时效贡献流水 = 台费按时长分摊 + 个人服务费 + 酒水食品按时长比例"""
...
@staticmethod
def compute_time_weighted_net_revenue(
time_weighted_revenue: Decimal, assistant_commission: Decimal
) -> Decimal:
"""时效净贡献 = 时效贡献流水 - 个人服务分成"""
...
```
### 核心数据结构
```python
@dataclass
class TableUsage:
"""台桌使用信息"""
table_id: int
table_area: str # 区域名称A/B/C/S/TV/M1-M7 等)
usage_seconds: int # 台桌使用时长(秒)
table_fee: Decimal # 台费/房费
@dataclass
class AssistantService:
"""助教服务记录"""
assistant_id: int
table_id: int
service_seconds: int # 服务时长(秒)
ledger_amount: Decimal # 服务流水(助教收费)
commission: Decimal # 助教分成
skill_id: int
course_type: str # BASE / BONUS / ROOM
@dataclass
class OrderData:
"""订单聚合数据(一个结算单的完整信息)"""
order_settle_id: int
site_id: int
total_table_fee: Decimal # 台费总额
total_goods_amount: Decimal # 酒水食品总额
tables: List[TableUsage] # 台桌列表
assistants: List[AssistantService] # 助教服务列表
```
### 四项统计算法详解
#### 1. 订单总流水order_gross_revenue
```
order_gross_revenue = total_table_fee + total_goods_amount + SUM(所有助教的 ledger_amount)
```
每个参与助教获得相同的 order_gross_revenue 值。
#### 2. 订单净流水order_net_revenue
```
order_net_revenue = order_gross_revenue - SUM(所有助教的 commission)
```
每个参与助教获得相同的 order_net_revenue 值。
#### 3. 时效贡献流水time_weighted_revenue— 核心算法
这是最复杂的计算,按以下步骤进行:
**步骤 1确定每张台桌的有效计费时长**
```
对于每张台桌 t
助教总服务时长 = SUM(该台桌所有助教的 service_seconds)
有效计费时长 = MAX(助教总服务时长, 台桌使用时长)
每小时单价 = table_fee / (有效计费时长 / 3600)
```
**步骤 2按助教在台桌的服务时长分摊台费**
```
对于每个助教 a 在台桌 t
台费分摊 = 每小时单价 × (助教在该台桌的服务时长 / 3600)
特殊情况:当助教总服务时长 < 台桌使用时长时
按比例缩放:台费分摊 = (table_fee × 台桌使用时长对应比例) / 该台桌助教人数中按时长占比分配
即:台费分摊 = (table_fee / 台桌使用时长 × MIN(助教总服务时长, 台桌使用时长))
× (助教个人时长 / 助教总服务时长)
```
更精确的公式(统一处理两种情况):
```
对于台桌 t
billable_seconds = MAX(SUM(助教服务时长), 台桌使用时长)
对于助教 a
台费分摊_a = table_fee_t × (service_seconds_a / billable_seconds)
```
> 注意:当 `SUM(助教服务时长) > 台桌使用时长` 时,`billable_seconds = SUM(助教服务时长)`
> 此时各助教按自己的时长占比分摊台费,总和 = table_fee。
> 当 `SUM(助教服务时长) < 台桌使用时长` 时,`billable_seconds = 台桌使用时长`
> 此时各助教分摊的台费总和 < table_fee未被助教覆盖的时段不分配给任何人
**步骤 3助教个人服务费直接计入**
```
个人服务费 = 助教的 ledger_amount
```
**步骤 4酒水食品按助教总时长比例均分**
```
助教总时长 = SUM(所有助教在所有台桌的 service_seconds)
对于助教 a
酒水食品分摊 = total_goods_amount × (助教 a 的总服务时长 / 助教总时长)
```
**合成:**
```
time_weighted_revenue_a = SUM(各台桌台费分摊_a) + 个人服务费_a + 酒水食品分摊_a
```
#### 4. 时效净贡献time_weighted_net_revenue
```
time_weighted_net_revenue_a = time_weighted_revenue_a - commission_a
```
#### 5. 超休/打赏课特殊处理
当助教为超休/打赏课类型(`course_type = BONUS`)时,该助教不参与订单级分摊:
```
order_gross_revenue = ledger_amount个人服务流水
order_net_revenue = ledger_amount - commission
time_weighted_revenue = ledger_amount
time_weighted_net_revenue = ledger_amount - commission
```
### MemberConsumptionTask 扩展
在现有任务中新增:
```python
# extract 阶段新增
def _extract_recharge_stats(self, site_id: int, stat_date: date) -> Dict[int, Dict]:
"""从 dwd_recharge_order 提取 30/60/90 天充值统计"""
...
# transform 阶段新增字段
record['recharge_count_30d'] = recharge.get('count_30d', 0)
record['recharge_count_60d'] = recharge.get('count_60d', 0)
record['recharge_count_90d'] = recharge.get('count_90d', 0)
record['recharge_amount_30d'] = recharge.get('amount_30d', Decimal('0'))
record['recharge_amount_60d'] = recharge.get('amount_60d', Decimal('0'))
record['recharge_amount_90d'] = recharge.get('amount_90d', Decimal('0'))
record['avg_ticket_amount'] = (
record['total_consume_amount'] / max(record['total_visit_count'], 1)
)
```
### AssistantDailyTask 扩展 — 惩罚检测
在现有任务的 transform 阶段新增惩罚检测逻辑:
```python
# 惩罚检测核心逻辑
@staticmethod
def detect_overlap_violations(
service_records: List[Dict],
penalty_areas: Set[str]
) -> Dict[Tuple[int, date], List[Dict]]:
"""
检测同一台桌同一时间段超过 2 名助教挂台的违规。
penalty_areas: 指定区域集合,如 {'A','B','C','S','TV','M1','M2',...,'M7'}
返回: {(assistant_id, stat_date): [violation_info, ...]}
"""
...
@staticmethod
def compute_penalty_minutes(
actual_minutes: Decimal,
per_hour_contribution: Decimal,
threshold: Decimal = Decimal('24')
) -> Decimal:
"""
计算惩罚分钟数。
per_hour_contribution >= threshold: 返回 0
per_hour_contribution < threshold:
返回 actual_minutes × (1 - per_hour_contribution / threshold)
"""
...
```
**惩罚区域定义:**
- 大厅A、B、C、S、TV
- 麻将房M1、M2、M3、M4、M5、M6、M7
**时间重叠检测算法:**
1.`(台桌ID, 服务日期)` 分组所有服务记录
2. 对每组内的服务记录,检查时间段是否有重叠(任意两个助教的 `[start_time, end_time]` 有交集)
3. 若同一时间段内助教数 > 2标记为违规
4. 对违规记录计算 `per_hour_contribution = 台费每小时单价 / 该时段助教人数`
5. 根据 `per_hour_contribution` 与 24 元阈值比较,计算 `penalty_minutes`
## 数据模型
### dws.dws_assistant_order_contribution新建
```sql
CREATE TABLE dws.dws_assistant_order_contribution (
contribution_id BIGSERIAL PRIMARY KEY,
site_id INTEGER NOT NULL,
tenant_id INTEGER NOT NULL,
assistant_id BIGINT NOT NULL,
assistant_nickname VARCHAR(100),
stat_date DATE NOT NULL,
-- 四项统计
order_gross_revenue NUMERIC(14,2) DEFAULT 0,
order_net_revenue NUMERIC(14,2) DEFAULT 0,
time_weighted_revenue NUMERIC(14,2) DEFAULT 0,
time_weighted_net_revenue NUMERIC(14,2) DEFAULT 0,
-- 辅助字段
order_count INTEGER DEFAULT 0,
total_service_seconds INTEGER DEFAULT 0,
-- 元数据
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_aoc_site_assistant_date
ON dws.dws_assistant_order_contribution (site_id, assistant_id, stat_date);
CREATE INDEX idx_aoc_stat_date
ON dws.dws_assistant_order_contribution (site_id, stat_date);
```
### dws_member_consumption_summary 扩展字段
```sql
ALTER TABLE dws.dws_member_consumption_summary
ADD COLUMN IF NOT EXISTS recharge_count_30d INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS recharge_count_60d INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS recharge_count_90d INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS recharge_amount_30d NUMERIC(14,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS recharge_amount_60d NUMERIC(14,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS recharge_amount_90d NUMERIC(14,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS avg_ticket_amount NUMERIC(14,2) DEFAULT 0;
```
### dws_assistant_daily_detail 扩展字段
```sql
ALTER TABLE dws.dws_assistant_daily_detail
ADD COLUMN IF NOT EXISTS penalty_minutes NUMERIC(10,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS penalty_reason TEXT,
ADD COLUMN IF NOT EXISTS is_exempt BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS per_hour_contribution NUMERIC(14,2);
```
### RLS 视图app schema
```sql
-- 新建:助教订单流水统计
CREATE OR REPLACE VIEW app.v_dws_assistant_order_contribution AS
SELECT * FROM dws.dws_assistant_order_contribution
WHERE site_id = current_setting('app.current_site_id')::bigint;
-- 已有视图无需修改dws_member_consumption_summary 和 dws_assistant_daily_detail
-- 的 RLS 视图使用 SELECT *,新增字段自动包含
```
### FDW 映射fdw_etl schema
`test_zqyy_app.fdw_etl` 中新建外部表:
```sql
CREATE FOREIGN TABLE fdw_etl.dws_assistant_order_contribution (
contribution_id BIGINT,
site_id INTEGER,
tenant_id INTEGER,
assistant_id BIGINT,
assistant_nickname VARCHAR(100),
stat_date DATE,
order_gross_revenue NUMERIC(14,2),
order_net_revenue NUMERIC(14,2),
time_weighted_revenue NUMERIC(14,2),
time_weighted_net_revenue NUMERIC(14,2),
order_count INTEGER,
total_service_seconds INTEGER,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE
) SERVER etl_server
OPTIONS (schema_name 'app', table_name 'v_dws_assistant_order_contribution');
```
对于扩展字段的表(`dws_member_consumption_summary``dws_assistant_daily_detail`),需要 `DROP` 并重建 FDW 外部表定义以包含新字段。
## 正确性属性
*正确性属性Correctness Property是系统在所有合法执行路径上都应成立的行为特征——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
以下属性基于需求文档中的验收标准推导。四项统计的核心计算函数(`compute_order_gross_revenue``compute_time_weighted_revenue` 等)和惩罚计算函数(`compute_penalty_minutes`)设计为纯静态方法,不依赖数据库,可直接用于属性测试。
### Property 1: 订单级统计不变量 — gross/net 各助教相等
*For any* 订单数据(包含任意数量的台桌、助教服务和酒水食品),所有参与该订单的助教应获得相同的 `order_gross_revenue` 值,且获得相同的 `order_net_revenue` 值。
推导:`order_gross_revenue``order_net_revenue` 是订单级聚合值,不按助教个人拆分,因此所有参与助教共享同一个值。
**Validates: Requirements 2.2, 2.3, 10.1, 10.2**
### Property 2: 时效贡献流水之和约束
*For any* 订单数据,所有参与助教的 `time_weighted_revenue` 之和应满足:
- 当所有台桌的助教总服务时长 ≥ 台桌使用时长时,之和 = `order_gross_revenue`
- 当存在台桌的助教总服务时长 < 台桌使用时长时,之和 ≤ `order_gross_revenue`
且在所有情况下,之和 ≥ 0。
推导:台费按时长比例分摊,当助教完全覆盖台桌时长时分摊总和等于台费;酒水食品按时长比例均分总和等于酒水总额;助教服务费直接计入。因此总和 = 台费分摊总和 + 酒水分摊总和 + 服务费总和 ≤ order_gross_revenue。
**Validates: Requirements 2.4, 10.3**
### Property 3: 时效净贡献减法关系
*For any* 助教和订单数据,该助教的 `time_weighted_net_revenue` 应等于 `time_weighted_revenue - commission`(该助教个人的服务分成)。
推导:这是定义性等式,直接从需求 2.5 得出。
**Validates: Requirements 2.5, 10.4**
### Property 4: 惩罚分钟数分段公式
*For any* 非负的 `actual_minutes` 和非负的 `per_hour_contribution`
-`per_hour_contribution >= 24` 时,`penalty_minutes = 0`
-`per_hour_contribution < 24` 时,`penalty_minutes = actual_minutes × (1 - per_hour_contribution / 24)`
且在所有情况下,`0 ≤ penalty_minutes ≤ actual_minutes`
推导:直接从需求 6.3/6.4 的分段公式得出。上界 `actual_minutes``per_hour_contribution = 0` 时取到。
**Validates: Requirements 6.3, 6.4, 10.5, 10.6**
### Property 5: 次均消费公式
*For any* 非负的 `total_consume_amount` 和非负整数 `total_visit_count``avg_ticket_amount` 应等于 `total_consume_amount / MAX(total_visit_count, 1)`
推导:直接从需求 3.4 得出。`MAX(total_visit_count, 1)` 防止除零。
**Validates: Requirements 3.4, 10.7**
### Property 6: 重叠检测正确性
*For any* 一组助教服务记录,若在指定区域的同一台桌上存在 3 名或以上助教的服务时间段有重叠,则 `detect_overlap_violations` 应返回非空的违规列表。
推导:需求 6.1 要求检测"同一台桌同一时间段超过 2 名助教挂台"。我们可以生成随机的服务记录(包含时间段重叠和不重叠的情况),验证检测函数的正确性。
**Validates: Requirements 6.1**
## 错误处理
| 场景 | 处理方式 |
|------|----------|
| 订单无助教服务记录 | 跳过该订单,不生成统计记录 |
| 台桌使用时长为 0 | 台费分摊设为 0避免除零 |
| 助教总服务时长为 0 | 酒水食品分摊设为 0避免除零 |
| 会员无充值记录 | 充值次数/金额设为 0 |
| 会员无消费记录 | avg_ticket_amount 设为 0 |
| 助教当日无违规 | penalty_minutes = 0penalty_reason = NULL |
| 服务记录缺少时间段信息 | 跳过该记录的重叠检测,日志 WARNING |
| per_hour_contribution 为负数 | 视为 0 处理(防御性编程) |
| FDW 映射创建失败 | 事务回滚,报错终止 |
| 数据库写入失败 | 事务回滚,抛出异常由调度器处理 |
> **注意:所有数据库操作均在测试库(`test_etl_feiqiu` / `test_zqyy_app`)中进行。**
## 测试策略
### 属性测试hypothesis
属性测试位于 `tests/` 目录Monorepo 级),使用 `hypothesis` 库。
每个属性测试对应设计文档中的一个 Property最少运行 100 次迭代。
测试文件:`tests/test_dws_contribution_properties.py`
```python
# Feature: 02-etl-dws-miniapp-extensions, Property 1: 订单级统计不变量
@given(order_data=order_data_strategy())
@settings(max_examples=200)
def test_gross_net_equal_across_assistants(order_data):
"""所有参与助教的 order_gross_revenue 和 order_net_revenue 应分别相等"""
gross = AssistantOrderContributionTask.compute_order_gross_revenue(order_data)
net = AssistantOrderContributionTask.compute_order_net_revenue(order_data)
# 每个助教获得相同的 gross 和 net
for assistant in order_data.assistants:
assert assistant_gross == gross
assert assistant_net == net
```
```python
# Feature: 02-etl-dws-miniapp-extensions, Property 4: 惩罚分钟数分段公式
@given(
actual_minutes=st.decimals(min_value=0, max_value=600, places=2),
per_hour_contribution=st.decimals(min_value=0, max_value=200, places=2),
)
@settings(max_examples=200)
def test_penalty_minutes_formula(actual_minutes, per_hour_contribution):
"""惩罚分钟数应符合分段公式且在 [0, actual_minutes] 范围内"""
result = AssistantDailyTask.compute_penalty_minutes(
actual_minutes, per_hour_contribution
)
if per_hour_contribution >= 24:
assert result == 0
else:
expected = actual_minutes * (1 - per_hour_contribution / 24)
assert abs(result - expected) < Decimal('0.01')
assert 0 <= result <= actual_minutes
```
属性测试库:`hypothesis`(已在项目依赖中)
### 单元测试
单元测试位于 `apps/etl/connectors/feiqiu/tests/unit/`,使用 FakeDB/FakeAPI 工具。
重点覆盖:
- PRD 示例数据验算:使用 PRD 中的具体订单示例3 名助教、2 张台桌、酒水 600 元)验证四项统计的精确数值
- 超休/打赏课边界:验证超休助教的四项统计等于个人流水
- 零值边界:无台费、无酒水、无助教服务的极端情况
- 惩罚计算边界per_hour_contribution 恰好等于 24 元的临界值
- 充值窗口:验证 30/60/90 天窗口的正确切分
- 豁免逻辑is_exempt = TRUE 时跳过惩罚
### 测试配置
- 属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
- 单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_assistant_order_contribution.py -v`
- 每个属性测试标注 `@settings(max_examples=200)`
- 每个属性测试注释引用设计文档 Property 编号

View File

@@ -0,0 +1,157 @@
# 需求文档ETL DWS 层扩展 — 小程序数据支撑
## 简介
本 Spec 覆盖 P2 任务中 T4T11 的 ETL DWS 层扩展,为小程序提供三类核心数据支撑:
1. 助教订单流水四项统计(`dws_assistant_order_contribution`
2. 会员消费汇总扩展(充值窗口 + 次均消费)
3. 定档折算惩罚检测与计算
同时包含新表的 RLS 视图创建、FDW 映射同步,以及影子跑数验证。
> SPI 消费力指数T1T3已在独立 Spec `.kiro/specs/spi-spending-power-index/` 中完成,本文档不再重复。
## 术语表
- **AssistantOrderContributionTask**:助教订单流水统计 ETL 任务,粒度 `(site_id, assistant_id, stat_date)`
- **MemberConsumptionTask**:会员消费汇总 ETL 任务,粒度 `(site_id, member_id, stat_date)`
- **AssistantDailyTask**:助教日度业绩明细 ETL 任务,粒度 `(site_id, assistant_id, stat_date)`
- **dws_assistant_order_contribution**:助教订单流水四项统计结果表
- **dws_member_consumption_summary**:会员消费汇总表(已有,需扩展字段)
- **dws_assistant_daily_detail**:助教日度业绩明细表(已有,需扩展字段)
- **order_gross_revenue**:订单总流水 — 助教参与订单的全部流水(台费 + 酒水食品 + 助教服务费)
- **order_net_revenue**:订单净流水 — 订单总流水 - 该订单所有助教的服务分成总额
- **time_weighted_revenue**:时效贡献流水 — 按助教个人服务时长折算的订单金额贡献
- **time_weighted_net_revenue**:时效净贡献 — 时效贡献流水 - 该助教个人的服务分成
- **penalty_minutes**:定档折算惩罚分钟数 — 因违规被扣减的定档业绩时长
- **per_hour_contribution**:单人每小时贡献流水 — 台费/房费每小时实收单价 ÷ 本次基础课助教人数
- **RLS 视图**:行级安全视图,位于 ETL 库 `app` schema`site_id` 隔离数据
- **FDW 映射**:外部数据包装器映射,将 ETL 库表映射到业务库 `fdw_etl` schema
- **settle_type**结算类型1=台桌结账3=商城订单5=充值订单
- **BaseDwsTask**DWS 层任务基类,提供 delete-before-insert 幂等机制
- **delete-before-insert**:幂等更新策略,先按条件删除旧记录再批量插入新记录
## 需求
### 需求 1助教订单流水统计表创建T4
**用户故事:** 作为 ETL 开发者,我需要创建助教订单流水四项统计表,以便存储每名助教每日的订单流水贡献数据。
#### 验收标准
1. THE 开发者 SHALL 创建 `dws.dws_assistant_order_contribution` 表,主键为 `(site_id, assistant_id, stat_date)`
2. THE dws_assistant_order_contribution 表 SHALL 包含四项统计字段:`order_gross_revenue`(订单总流水)、`order_net_revenue`(订单净流水)、`time_weighted_revenue`(时效贡献流水)、`time_weighted_net_revenue`(时效净贡献),精度为 `NUMERIC(14,2)`
3. THE dws_assistant_order_contribution 表 SHALL 包含辅助字段:`order_count`(参与订单数)、`total_service_seconds`(总服务时长秒数)、`assistant_nickname`(助教昵称)
4. THE dws_assistant_order_contribution 表 SHALL 包含元数据字段:`tenant_id``created_at``updated_at`
5. THE 开发者 SHALL 编写迁移脚本 `db/etl_feiqiu/migrations/<日期>__create_dws_assistant_order_contribution.sql`,在测试库 `test_etl_feiqiu` 中执行建表
6. WHEN DDL 在测试库执行成功后THE 开发者 SHALL 运行 `gen_consolidated_ddl.py` 导出最新 DDL
### 需求 2助教订单流水四项统计计算T5
**用户故事:** 作为产品经理,我需要助教订单流水四项统计(订单总流水/订单净流水/时效贡献流水/时效净贡献),以便评估助教个人能力。
#### 验收标准
1. THE AssistantOrderContributionTask SHALL 从 `dwd.dwd_settlement_head``dwd.dwd_table_fee_log``dwd.dwd_assistant_service_log` 提取订单、台费和助教服务数据
2. WHEN 计算 order_gross_revenue 时THE AssistantOrderContributionTask SHALL 将助教参与订单的全部流水(台费 + 酒水食品 + 所有助教服务费)累加,每个参与助教获得相同的订单总流水值
3. WHEN 计算 order_net_revenue 时THE AssistantOrderContributionTask SHALL 从订单总流水中减去该订单所有助教的服务分成总额,每个参与助教获得相同的订单净流水值
4. WHEN 计算 time_weighted_revenue 时THE AssistantOrderContributionTask SHALL 按以下步骤折算个人贡献:
- 确定每张台桌的有效计费时长:取 MAX(该台桌所有助教服务时长之和, 台桌使用时长)
- 按助教在该台桌的服务时长占比分摊台费
- 助教个人服务费直接计入
- 酒水食品按助教个人服务总时长占所有助教服务总时长的比例均分
5. WHEN 计算 time_weighted_net_revenue 时THE AssistantOrderContributionTask SHALL 从该助教的时效贡献流水中减去该助教个人的服务分成
6. WHEN 助教为超休/打赏课类型时THE AssistantOrderContributionTask SHALL 将四项统计均设为该助教个人的服务流水和分成(不参与订单级分摊)
7. THE AssistantOrderContributionTask SHALL 以任务代码 `DWS_ASSISTANT_ORDER_CONTRIBUTION` 注册到 task_registry
8. THE AssistantOrderContributionTask SHALL 采用 delete-before-insert 策略按日期窗口幂等更新
### 需求 3会员消费汇总扩展T6
**用户故事:** 作为产品经理,我需要客户 30/60/90 天充值次数和金额、次均消费,以便在客户看板中展示。
#### 验收标准
1. THE 开发者 SHALL 在 `dws.dws_member_consumption_summary` 表中新增以下字段:`recharge_count_30d``recharge_count_60d``recharge_count_90d`充值次数INTEGER`recharge_amount_30d``recharge_amount_60d``recharge_amount_90d`充值金额NUMERIC(14,2))、`avg_ticket_amount`次均消费额度NUMERIC(14,2)
2. THE 开发者 SHALL 编写 ALTER TABLE 迁移脚本在测试库 `test_etl_feiqiu` 中执行字段扩展
3. THE 充值数据 SHALL 从 `dwd.dwd_recharge_order` 提取,按 `member_id` 和时间窗口聚合
4. THE avg_ticket_amount SHALL 按公式 `total_consume_amount / MAX(total_visit_count, 1)` 计算
### 需求 4会员消费汇总任务修改T7
**用户故事:** 作为 ETL 开发者,我需要修改 MemberConsumptionTask 以填充新增的充值窗口和次均消费字段。
#### 验收标准
1. THE MemberConsumptionTask SHALL 在 extract 阶段新增充值订单提取逻辑,从 `dwd.dwd_recharge_order` 按 30/60/90 天窗口聚合充值次数和金额
2. THE MemberConsumptionTask SHALL 在 transform 阶段将充值统计和次均消费填充到输出记录中
3. WHEN 会员无充值记录时THE MemberConsumptionTask SHALL 将充值次数设为 0、充值金额设为 0.00
4. WHEN 会员无消费记录时THE MemberConsumptionTask SHALL 将 avg_ticket_amount 设为 0.00
### 需求 5助教日度明细表扩展 — 定档折算惩罚字段T8
**用户故事:** 作为 ETL 开发者,我需要在助教日度明细表中新增定档折算惩罚相关字段,以便存储惩罚检测和计算结果。
#### 验收标准
1. THE 开发者 SHALL 在 `dws.dws_assistant_daily_detail` 表中新增以下字段:`penalty_minutes` (NUMERIC(10,2))、`penalty_reason` (TEXT)、`is_exempt` (BOOLEAN DEFAULT FALSE)、`per_hour_contribution` (NUMERIC(14,2))
2. THE 开发者 SHALL 编写 ALTER TABLE 迁移脚本在测试库 `test_etl_feiqiu` 中执行字段扩展
3. WHEN 助教当日无惩罚时THE penalty_minutes SHALL 为 0penalty_reason SHALL 为 NULL
### 需求 6定档折算惩罚检测与计算逻辑T9
**用户故事:** 作为产品经理,我需要定档折算惩罚数据,以便在绩效页面展示折算详情,防止助教利用低价订单冲档位。
#### 验收标准
1. THE AssistantDailyTask SHALL 检测规则 2 违规:在指定区域(大厅 A/B/C/S/TV 和麻将房 M1M7同一台桌同一时间段超过 2 名助教挂台(课程时间段有重叠即算)
2. WHEN 检测到违规时THE AssistantDailyTask SHALL 计算单人每小时贡献流水:台费/房费每小时实收单价 ÷ 本次基础课助教人数
3. WHEN per_hour_contribution >= 24 元时THE AssistantDailyTask SHALL 按满额计入定档业绩时长penalty_minutes = 0
4. WHEN per_hour_contribution < 24 元时THE AssistantDailyTask SHALL 按比例折算:`penalty_minutes = 实际服务分钟数 × (1 - per_hour_contribution / 24)`
5. WHEN 订单标记为 is_exempt = TRUE 时THE AssistantDailyTask SHALL 跳过惩罚计算penalty_minutes 设为 0
6. THE 定档折算惩罚 SHALL 仅影响定档业绩时长统计,不影响实际工资时长
7. THE AssistantDailyTask SHALL 每日自动计算惩罚,计算频率与现有日度任务一致
### 需求 7RLS 视图创建T10
**用户故事:** 作为 ETL 开发者,我需要为新表创建 RLS 视图,以便通过 FDW 安全地向业务库暴露数据。
#### 验收标准
1. THE 开发者 SHALL 在 ETL 库 `app` schema 中为 `dws_assistant_order_contribution` 创建 RLS 视图,按 `site_id` 过滤
2. THE 开发者 SHALL 更新已有 RLS 视图以包含 `dws_member_consumption_summary``dws_assistant_daily_detail` 的新增字段
3. THE RLS 视图 SHALL 使用 `current_setting('app.current_site_id')::INTEGER` 进行行级过滤
### 需求 8FDW 映射同步T10
**用户故事:** 作为后端开发者,我需要在业务库中通过 FDW 访问新建和扩展的 ETL 表,以便小程序后端读取数据。
#### 验收标准
1. THE 开发者 SHALL 在 `test_zqyy_app.fdw_etl` schema 中创建 `dws_assistant_order_contribution` 的外部表映射
2. THE 开发者 SHALL 更新 `dws_member_consumption_summary``dws_assistant_daily_detail` 的 FDW 映射以包含新增字段
3. THE FDW 映射 SHALL 通过 `app` schema 的 RLS 视图访问数据,而非直接访问 `dws` schema
### 需求 9影子跑数验证T11
**用户故事:** 作为 ETL 开发者,我需要通过影子跑数验证新增统计的正确性,以便确保数据质量。
#### 验收标准
1. THE 开发者 SHALL 编写验证脚本,对照 PRD 示例数据验算四项统计的计算结果
2. THE 验证脚本 SHALL 检查 `dws_assistant_order_contribution` 中四项统计数值的一致性order_gross_revenue 各助教相等、order_net_revenue 各助教相等、time_weighted_revenue 各助教之和加上误差容忍度等于订单总流水
3. THE 验证脚本 SHALL 检查 `dws_member_consumption_summary` 新增字段有值且充值金额与 `dwd_recharge_order` 源数据一致
4. THE 验证脚本 SHALL 检查定档折算惩罚字段在符合惩罚条件的记录上正确填充
### 需求 10算法正确性测试
**用户故事:** 作为 ETL 开发者我需要通过属性测试hypothesis验证四项统计和惩罚计算的正确性。
#### 验收标准
1. THE 属性测试 SHALL 验证:对于任意订单,所有参与助教的 order_gross_revenue 值相等
2. THE 属性测试 SHALL 验证:对于任意订单,所有参与助教的 order_net_revenue 值相等
3. THE 属性测试 SHALL 验证:对于任意订单,所有参与助教的 time_weighted_revenue 之和应在订单总流水的合理误差范围内±0.01 元)
4. THE 属性测试 SHALL 验证对于任意助教time_weighted_net_revenue = time_weighted_revenue - 该助教个人服务分成
5. THE 属性测试 SHALL 验证:对于任意 per_hour_contribution >= 24 的记录penalty_minutes 为 0
6. THE 属性测试 SHALL 验证:对于任意 per_hour_contribution < 24 且 per_hour_contribution >= 0 的记录penalty_minutes = 实际分钟数 × (1 - per_hour_contribution / 24)
7. THE 属性测试 SHALL 验证对于任意会员avg_ticket_amount = total_consume_amount / MAX(total_visit_count, 1)

View File

@@ -0,0 +1,152 @@
# 实现计划ETL DWS 层扩展 — 小程序数据支撑
## 概述
基于设计文档将实现拆分为DDL 建表/扩展 → 助教订单流水统计任务 → 会员消费汇总扩展 → 定档折算惩罚 → RLS 视图 + FDW 映射 → 影子跑数验证 六个阶段。所有数据库操作在测试库(`test_etl_feiqiu` / `test_zqyy_app`)中进行。
## 任务
- [x] 1. DDL 建表与字段扩展
- [x] 1.1 编写迁移脚本创建 `dws.dws_assistant_order_contribution`
- 新建 `db/etl_feiqiu/migrations/<日期>__create_dws_assistant_order_contribution.sql`
- 包含表定义、唯一索引 `idx_aoc_site_assistant_date`、查询索引 `idx_aoc_stat_date`
- 字段参照设计文档数据模型章节
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 1.2 编写迁移脚本扩展 `dws_member_consumption_summary` 字段
- 新建 `db/etl_feiqiu/migrations/<日期>__alter_member_consumption_add_recharge_fields.sql`
- ALTER TABLE 添加 `recharge_count_30d/60d/90d``recharge_amount_30d/60d/90d``avg_ticket_amount`
- _Requirements: 3.1, 3.2_
- [x] 1.3 编写迁移脚本扩展 `dws_assistant_daily_detail` 字段
- 新建 `db/etl_feiqiu/migrations/<日期>__alter_assistant_daily_add_penalty_fields.sql`
- ALTER TABLE 添加 `penalty_minutes``penalty_reason``is_exempt``per_hour_contribution`
- _Requirements: 5.1, 5.2_
- [x] 1.4 在测试库 `test_etl_feiqiu` 执行全部迁移脚本
- 通过 `PG_DSN`(指向测试库)连接执行 SQL
- _Requirements: 1.5, 3.2, 5.2_
- [x] 1.5 运行 `gen_consolidated_ddl.py` 导出最新 DDL
- 执行 `python scripts/ops/gen_consolidated_ddl.py`
- 验证 `docs/database/ddl/etl_feiqiu__dws.sql` 已包含新表和扩展字段
- _Requirements: 1.6_
- [x] 2. 实现助教订单流水统计任务
- [x] 2.1 创建数据结构和 `AssistantOrderContributionTask` 骨架
- 新建 `apps/etl/connectors/feiqiu/tasks/dws/assistant_order_contribution_task.py`
- 定义 `TableUsage``AssistantService``OrderData` dataclass
- 定义 `AssistantOrderContributionTask` 类继承 `BaseDwsTask`
- 实现 `get_task_code``get_target_table``get_primary_keys`
- _Requirements: 1.1, 2.7_
- [x] 2.2 实现四项统计核心计算(纯函数)
- 实现 `compute_order_gross_revenue` 静态方法
- 实现 `compute_order_net_revenue` 静态方法
- 实现 `compute_time_weighted_revenue` 静态方法(含台费分摊、酒水均分逻辑)
- 实现 `compute_time_weighted_net_revenue` 静态方法
- 处理超休/打赏课特殊情况
- _Requirements: 2.2, 2.3, 2.4, 2.5, 2.6_
- [x] 2.3 编写属性测试:订单级统计不变量
- **Property 1: 订单级统计不变量 — gross/net 各助教相等**
- **Validates: Requirements 2.2, 2.3, 10.1, 10.2**
- [x] 2.4 编写属性测试:时效贡献流水之和约束
- **Property 2: 时效贡献流水之和 ≤ order_gross_revenue**
- **Validates: Requirements 2.4, 10.3**
- [x] 2.5 编写属性测试:时效净贡献减法关系
- **Property 3: time_weighted_net_revenue = time_weighted_revenue - commission**
- **Validates: Requirements 2.5, 10.4**
- [x] 2.6 实现 `extract` 方法
-`dwd_settlement_head``dwd_table_fee_log``dwd_assistant_service_log` 提取数据
-`order_settle_id` 聚合为 `OrderData` 结构
- _Requirements: 2.1_
- [x] 2.7 实现 `transform` 方法
- 遍历订单,调用四项统计计算函数
-`(assistant_id, stat_date)` 聚合日度统计
- _Requirements: 2.2, 2.3, 2.4, 2.5, 2.6_
- [x] 2.8 注册任务到 task_registry 并导出模块
-`tasks/dws/__init__.py` 中导出 `AssistantOrderContributionTask`
-`orchestration/task_registry.py` 中注册 `DWS_ASSISTANT_ORDER_CONTRIBUTION``layer="DWS"``depends_on=["DWD_LOAD_FROM_ODS"]`
- _Requirements: 2.7, 2.8_
- [x] 3. 检查点 — 确保助教订单流水统计测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
- 确保所有属性测试通过,如有问题请询问用户。
- [x] 4. 扩展会员消费汇总任务
- [x] 4.1 在 `MemberConsumptionTask` 中新增充值统计提取
- 新增 `_extract_recharge_stats` 方法,从 `dwd.dwd_recharge_order` 按 30/60/90 天窗口聚合
-`extract` 方法中调用并返回充值统计数据
- _Requirements: 4.1, 3.3_
- [x] 4.2 在 `MemberConsumptionTask.transform` 中填充新字段
- 合并充值统计到输出记录
- 计算 `avg_ticket_amount = total_consume_amount / MAX(total_visit_count, 1)`
- 处理无充值/无消费的边界情况
- _Requirements: 4.2, 4.3, 4.4, 3.4_
- [x] 4.3 编写属性测试:次均消费公式
- **Property 5: avg_ticket_amount = total_consume_amount / MAX(total_visit_count, 1)**
- **Validates: Requirements 3.4, 10.7**
- [x] 5. 实现定档折算惩罚检测与计算
- [x] 5.1 实现时间重叠检测逻辑
-`AssistantDailyTask` 中新增 `detect_overlap_violations` 静态方法
- 定义惩罚区域集合(大厅 A/B/C/S/TV + 麻将房 M1M7
-`(table_id, service_date)` 分组,检测时间段重叠且助教数 > 2
- _Requirements: 6.1_
- [x] 5.2 实现惩罚分钟数计算
-`AssistantDailyTask` 中新增 `compute_penalty_minutes` 静态方法
- 计算 `per_hour_contribution = 台费每小时单价 / 助教人数`
- 按分段公式计算 `penalty_minutes`
- 处理 `is_exempt = TRUE` 豁免逻辑
- _Requirements: 6.2, 6.3, 6.4, 6.5_
- [x] 5.3 集成惩罚逻辑到 `AssistantDailyTask.transform`
- 在现有聚合逻辑后调用重叠检测和惩罚计算
-`penalty_minutes``penalty_reason``is_exempt``per_hour_contribution` 填充到输出记录
- _Requirements: 6.6, 6.7_
- [x] 5.4 编写属性测试:惩罚分钟数分段公式
- **Property 4: 惩罚分钟数符合分段公式且在 [0, actual_minutes] 范围内**
- **Validates: Requirements 6.3, 6.4, 10.5, 10.6**
- [x] 5.5 编写属性测试:重叠检测正确性
- **Property 6: 3+ 助教时间重叠时应检测到违规**
- **Validates: Requirements 6.1**
- [x] 6. 检查点 — 确保惩罚计算和消费汇总测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/ -k "contribution or penalty or consumption" -v`
- 确保所有测试通过,如有问题请询问用户。
- [x] 7. RLS 视图与 FDW 映射
- [x] 7.1 创建 `dws_assistant_order_contribution` 的 RLS 视图
- 在测试库 `test_etl_feiqiu``app` schema 中创建 `v_dws_assistant_order_contribution` 视图
- 使用 `current_setting('app.current_site_id')::bigint` 过滤
- 授予 `app_reader` 角色 SELECT 权限
- _Requirements: 7.1, 7.3_
- [x] 7.2 验证已有 RLS 视图自动包含新增字段
- 确认 `app.v_dws_member_consumption_summary``app.v_dws_assistant_daily_detail` 使用 `SELECT *`,新增字段自动包含
- _Requirements: 7.2_
- [x] 7.3 创建/更新 FDW 外部表映射
- 在测试库 `test_zqyy_app``fdw_etl` schema 中创建 `dws_assistant_order_contribution` 外部表
- 重建 `dws_member_consumption_summary``dws_assistant_daily_detail` 的 FDW 外部表以包含新字段
- FDW 映射通过 `app` schema RLS 视图访问
- _Requirements: 8.1, 8.2, 8.3_
- [x] 8. 影子跑数验证
- [x] 8.1 编写验证脚本
- 新建 `apps/etl/connectors/feiqiu/scripts/verify_dws_extensions.py`
- 验证四项统计:对照 PRD 示例数据验算,检查 gross/net 各助教相等
- 验证充值窗口:检查新增字段有值,充值金额与源数据一致
- 验证惩罚字段:检查符合条件的记录正确填充
- _Requirements: 9.1, 9.2, 9.3, 9.4_
- [x] 8.2 编写数据库手册文档
- 新建 `docs/database/BD_Manual_dws_assistant_order_contribution.md`
- 包含表结构、字段说明、索引、验证 SQL至少 3 条)、兼容性说明、回滚策略
- 更新 `docs/database/``dws_member_consumption_summary``dws_assistant_daily_detail` 的文档
- _Requirements: 1.1_
- [x] 9. 最终检查点 — 确保所有测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_dws_contribution_properties.py -v`
- 运行单元测试:`cd apps/etl/connectors/feiqiu && pytest tests/unit/ -k "contribution or penalty or consumption" -v`
- 确保所有测试通过,如有问题请询问用户。
## 备注
- 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP
- 每个任务引用具体需求编号以确保可追溯
- 所有数据库操作在测试库(`test_etl_feiqiu` / `test_zqyy_app`)中进行
- 检查点确保增量验证
- 属性测试验证全称正确性属性,单元测试验证具体示例和边界情况

View File

@@ -0,0 +1,859 @@
# 设计文档小程序用户认证系统miniapp-auth-system
## 概述
本设计在 P1miniapp-db-foundation已建立的 `auth` Schema 基础上,实现完整的小程序用户认证链路:
1. **微信登录**:小程序端发送 `code` → 后端调用微信 `code2Session` → 获取 `openid` → 创建/查找用户 → 签发 JWT
2. **用户申请**新用户填写球房ID + 手机号 + 申请身份 → 系统自动匹配助教/员工 → 管理员审核
3. **权限控制**:基于 `user_site_roles` + `role_permissions` 的 RBAC 模型,权限中间件拦截无权请求
4. **多店铺支持**:一个用户可关联多个 `site_id`,切换店铺时重新签发 JWT
**环境变量依赖**
| 环境变量 | 用途 | 来源 |
|---------|------|------|
| `APP_DB_DSN` / `DB_HOST` 等 | 业务库连接 | 根 `.env` |
| `PG_DSN` / `ETL_DB_HOST` 等 | ETL 库连接FDW 匹配) | 根 `.env` |
| `JWT_SECRET_KEY` | JWT 签名密钥 | `.env.local` |
| `WX_APPID` | 微信小程序 AppID | `.env.local` |
| `WX_SECRET` | 微信小程序 AppSecret | `.env.local` |
**整体认证流程**
```
小程序端 FastAPI 后端 微信服务器
│ │ │
│── wx.login() ──► │ │
│ 获取 code │ │
│ │ │
│── POST /api/xcx/login ──► │ │
│ {code} │── GET code2Session ──────────► │
│ │◄── {openid, session_key} ──── │
│ │ │
│ │── 查找/创建 auth.users ──► │
│ │── 签发 JWT ──► │
│◄── {jwt, status} ─────── │ │
│ │ │
│ [status=pending] │ │
│── POST /api/xcx/apply ──► │ │
│ {site_code, phone, ...} │── 创建 user_applications ──► │
│◄── {application_id} ───── │ │
```
## 架构
### 分层架构
```mermaid
graph TB
subgraph "小程序端"
MP["微信小程序<br/>wx.login() / wx.request()"]
end
subgraph "FastAPI 后端apps/backend/"
subgraph "路由层"
XCX_AUTH["routers/xcx_auth.py<br/>微信登录 + 申请"]
XCX_USER["routers/xcx_user.py<br/>用户状态 + 店铺切换"]
ADMIN_APP["routers/admin_applications.py<br/>管理端审核"]
end
subgraph "中间件层"
PERM_MW["middleware/permission.py<br/>权限中间件"]
end
subgraph "服务层"
WX_SVC["services/wechat.py<br/>code2Session 调用"]
APP_SVC["services/application.py<br/>申请 CRUD + 审核"]
MATCH_SVC["services/matching.py<br/>人员匹配"]
ROLE_SVC["services/role.py<br/>角色权限查询"]
end
subgraph "认证层(已有 + 扩展)"
JWT["auth/jwt.py<br/>JWT 签发/验证(扩展)"]
DEPS["auth/dependencies.py<br/>依赖注入(扩展)"]
end
DB["database.py<br/>数据库连接"]
end
subgraph "数据库"
AUTH_SCHEMA["auth Schema<br/>users / applications / roles / ..."]
FDW_ETL["fdw_etl Schema<br/>v_dim_assistant / v_dim_staff"]
end
subgraph "外部服务"
WX_API["微信 API<br/>code2Session"]
end
MP --> XCX_AUTH
MP --> XCX_USER
XCX_AUTH --> PERM_MW
XCX_USER --> PERM_MW
ADMIN_APP --> PERM_MW
PERM_MW --> JWT
PERM_MW --> DEPS
XCX_AUTH --> WX_SVC
XCX_AUTH --> APP_SVC
ADMIN_APP --> APP_SVC
ADMIN_APP --> MATCH_SVC
XCX_USER --> ROLE_SVC
WX_SVC --> WX_API
APP_SVC --> DB
MATCH_SVC --> DB
ROLE_SVC --> DB
DB --> AUTH_SCHEMA
DB --> FDW_ETL
```
### 请求处理流程
```mermaid
sequenceDiagram
participant MP as 小程序
participant MW as Permission Middleware
participant R as Router
participant S as Service
participant DB as PostgreSQL
MP->>R: POST /api/xcx/login {code}
R->>S: wechat.code2session(code)
S-->>R: openid
R->>DB: SELECT FROM auth.users WHERE wx_openid = ?
alt 用户不存在
R->>DB: INSERT INTO auth.users
end
R->>R: jwt.create_token_pair(user_id, site_id)
R-->>MP: {access_token, refresh_token, status}
Note over MP,DB: 后续请求(需认证)
MP->>MW: GET /api/xcx/... (Bearer token)
MW->>MW: decode_access_token(token)
MW->>DB: SELECT permissions FROM auth.user_site_roles JOIN ...
alt 权限不足
MW-->>MP: 403 Forbidden
else 权限通过
MW->>R: 放行
R->>S: 业务逻辑
S->>DB: 数据操作
R-->>MP: 200 OK
end
```
## 组件与接口
### 组件 1微信认证服务services/wechat.py
**职责**:封装微信 `code2Session` API 调用。
```python
import httpx
from app.config import get
WX_APPID: str = get("WX_APPID", "")
WX_SECRET: str = get("WX_SECRET", "")
CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session"
async def code2session(code: str) -> dict:
"""
调用微信 code2Session 接口。
返回:
{"openid": str, "session_key": str, "unionid": str | None}
异常:
WeChatAuthError: 微信接口返回错误码时抛出
"""
...
class WeChatAuthError(Exception):
"""微信认证错误,包含 errcode 和 errmsg。"""
def __init__(self, errcode: int, errmsg: str): ...
```
**设计决策**
- 使用 `httpx.AsyncClient` 异步调用微信 API与 FastAPI 异步模型一致
- `WX_APPID` / `WX_SECRET` 从环境变量读取,缺失时在调用时报错(而非启动时,因为非所有端点都需要微信认证)
### 组件 2申请服务services/application.py
**职责**:处理用户申请的创建、查询、审核。
```python
async def create_application(
user_id: int,
site_code: str,
applied_role_text: str,
phone: str,
employee_number: str | None = None,
nickname: str | None = None,
) -> dict:
"""
创建用户申请。
1. 查找 site_code → site_id 映射
2. 检查是否有 pending 申请(有则 409
3. 插入 user_applications 记录
4. 更新 users.nickname如提供
"""
...
async def approve_application(
application_id: int,
reviewer_id: int,
role_id: int,
binding: dict | None = None, # {"assistant_id": ..., "staff_id": ..., "binding_type": ...}
review_note: str | None = None,
) -> dict:
"""
批准申请。
1. 检查申请状态为 pending否则 409
2. 更新 user_applications.status = 'approved'
3. 创建 user_site_roles 记录
4. 创建 user_assistant_binding 记录(如有 binding
5. 更新 users.status = 'approved'(如果是首次通过)
6. 记录 reviewer_id 和 reviewed_at
"""
...
async def reject_application(
application_id: int,
reviewer_id: int,
review_note: str,
) -> dict:
"""
拒绝申请。
1. 检查申请状态为 pending否则 409
2. 更新 user_applications.status = 'rejected'
3. 记录 reviewer_id、review_note、reviewed_at
"""
...
async def get_user_applications(user_id: int) -> list[dict]:
"""查询用户的所有申请记录。"""
...
```
### 组件 3人员匹配服务services/matching.py
**职责**:根据申请信息在 FDW 外部表中查找候选匹配。
```python
async def find_candidates(
site_id: int,
phone: str,
employee_number: str | None = None,
) -> list[dict]:
"""
在助教表和员工表中查找匹配候选。
查询逻辑:
1. fdw_etl.v_dim_assistant: WHERE site_id = ? AND mobile = ?
2. fdw_etl.v_dim_staff + v_dim_staff_ex: WHERE site_id = ? AND (mobile = ? OR job_num = ?)
3. 合并结果,每条包含 source_type / name / mobile / job_num
注意:查询 FDW 外部表前需设置 app.current_site_idRLS 隔离)。
但 fdw_etl 中的外部表映射的是 app schema 的 RLS 视图,
所以需要在 ETL 库连接上设置 site_id。
实际上,我们直接在业务库通过 fdw_etl 查询,
FDW 会透传 session 变量到远端。
返回:
[{"source_type": "assistant"|"staff", "id": int, "name": str, "mobile": str, "job_num": str | None}]
"""
...
```
**设计决策**
- FDW 查询需要在业务库连接上设置 `app.current_site_id`,因为 FDW 外部表映射的是 ETL 库 `app` Schema 的 RLS 视图
- 匹配查询使用业务库连接(`get_connection()`),通过 `SET LOCAL app.current_site_id` 设置隔离
- 如果 `site_code` 无法映射到 `site_id`,直接返回空列表
### 组件 4角色权限服务services/role.py
**职责**:查询用户在指定店铺下的角色和权限。
```python
async def get_user_permissions(user_id: int, site_id: int) -> list[str]:
"""
获取用户在指定 site_id 下的权限 code 列表。
SQL: SELECT DISTINCT p.code
FROM auth.user_site_roles usr
JOIN auth.role_permissions rp ON usr.role_id = rp.role_id
JOIN auth.permissions p ON rp.permission_id = p.id
WHERE usr.user_id = ? AND usr.site_id = ?
"""
...
async def get_user_sites(user_id: int) -> list[dict]:
"""
获取用户关联的所有店铺及对应角色。
返回: [{"site_id": int, "site_name": str, "roles": [{"code": str, "name": str}]}]
"""
...
async def check_user_has_site_role(user_id: int, site_id: int) -> bool:
"""检查用户在指定 site_id 下是否有任何角色绑定。"""
...
```
### 组件 5权限中间件middleware/permission.py
**职责**:基于 JWT 中的 `user_id` + `site_id` 检查用户权限。
```python
from functools import wraps
from fastapi import Depends, HTTPException, status
from app.auth.dependencies import get_current_user, CurrentUser
def require_permission(*permission_codes: str):
"""
权限装饰器/依赖,用于路由端点。
用法:
@router.get("/finance")
async def get_finance(
user: CurrentUser = Depends(require_permission("view_board_finance"))
):
...
逻辑:
1. 从 JWT 提取 user_id + site_id
2. 查询 auth.users.status非 approved 则 403
3. 查询 user_site_roles + role_permissions 获取权限列表
4. 检查是否包含所需权限,不包含则 403
"""
...
def require_approved():
"""
仅检查用户状态为 approved 的依赖(不检查具体权限)。
用于通用的已认证端点。
"""
...
```
**设计决策**
- 使用 FastAPI 依赖注入模式而非全局中间件,更灵活且可按端点配置
- `pending` 用户只能访问申请提交和状态查询端点,其他端点需要 `approved` 状态
- 权限检查结果可考虑短期缓存(当前版本不缓存,每次查库)
### 组件 6JWT 服务扩展auth/jwt.py 扩展)
**职责**:扩展现有 JWT 服务,支持微信登录场景。
**扩展内容**
```python
# 新增创建受限令牌pending 用户)
def create_limited_token_pair(user_id: int) -> dict[str, str]:
"""
为 pending 用户签发受限令牌。
payload 不含 site_id 和 roles仅包含 user_id + type + limited=True。
"""
...
# 扩展create_access_token payload 增加 roles 字段
def create_access_token(user_id: int, site_id: int, roles: list[str] | None = None) -> str:
"""
生成 access_token。
payload: sub=user_id, site_id, roles, type=access, exp
"""
...
```
**设计决策**
- 保持向后兼容:现有 `create_access_token(user_id, site_id)` 调用不受影响(`roles` 默认 `None`
- `pending` 用户的受限令牌通过 `limited=True` 标记区分,权限中间件据此拦截
### 组件 7路由端点
#### 7.1 小程序认证路由routers/xcx_auth.py
| 方法 | 路径 | 说明 | 认证要求 |
|------|------|------|---------|
| POST | `/api/xcx/login` | 微信登录 | 无(公开) |
| POST | `/api/xcx/apply` | 提交申请 | JWT含 pending |
| GET | `/api/xcx/me` | 查询自身状态 | JWT含 pending |
| GET | `/api/xcx/me/sites` | 查询关联店铺 | JWTapproved |
| POST | `/api/xcx/switch-site` | 切换店铺 | JWTapproved |
| POST | `/api/xcx/refresh` | 刷新令牌 | refresh_token |
#### 7.2 管理端审核路由routers/admin_applications.py
| 方法 | 路径 | 说明 | 认证要求 |
|------|------|------|---------|
| GET | `/api/admin/applications` | 查询申请列表 | JWT + site_admin/tenant_admin |
| GET | `/api/admin/applications/{id}` | 查询申请详情 + 候选匹配 | JWT + site_admin/tenant_admin |
| POST | `/api/admin/applications/{id}/approve` | 批准申请 | JWT + site_admin/tenant_admin |
| POST | `/api/admin/applications/{id}/reject` | 拒绝申请 | JWT + site_admin/tenant_admin |
### 组件 8Pydantic 模型schemas/xcx_auth.py
```python
from pydantic import BaseModel, Field
import re
class WxLoginRequest(BaseModel):
code: str = Field(..., min_length=1, description="微信临时登录凭证")
class WxLoginResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str = "bearer"
user_status: str # pending / approved / rejected / disabled
user_id: int
class ApplicationRequest(BaseModel):
site_code: str = Field(..., pattern=r"^[A-Za-z]{2}\d{3}$", description="球房ID")
applied_role_text: str = Field(..., min_length=1, max_length=100)
phone: str = Field(..., pattern=r"^\d{11}$", description="手机号")
employee_number: str | None = Field(None, max_length=50)
nickname: str | None = Field(None, max_length=50)
class ApplicationResponse(BaseModel):
id: int
site_code: str
applied_role_text: str
status: str
review_note: str | None = None
created_at: str
reviewed_at: str | None = None
class UserStatusResponse(BaseModel):
user_id: int
status: str
nickname: str | None
applications: list[ApplicationResponse]
class SiteInfo(BaseModel):
site_id: int
site_name: str
roles: list[dict]
class SwitchSiteRequest(BaseModel):
site_id: int
class MatchCandidate(BaseModel):
source_type: str # assistant / staff
id: int
name: str
mobile: str | None
job_num: str | None
class ApproveRequest(BaseModel):
role_id: int
binding: dict | None = None # {"assistant_id": ..., "staff_id": ..., "binding_type": ...}
review_note: str | None = None
class RejectRequest(BaseModel):
review_note: str = Field(..., min_length=1)
```
## 数据模型
### ER 图
```mermaid
erDiagram
users {
serial id PK
varchar wx_openid UK
varchar wx_union_id
varchar wx_avatar_url
varchar nickname
varchar phone
varchar status "pending/approved/rejected/disabled"
timestamptz created_at
timestamptz updated_at
}
user_applications {
serial id PK
int user_id FK
varchar site_code
int site_id "可空,映射后填入"
varchar applied_role_text
varchar employee_number "可选"
varchar phone
varchar status "pending/approved/rejected"
int reviewer_id
text review_note
timestamptz created_at
timestamptz reviewed_at
}
site_code_mapping {
serial id PK
varchar site_code UK "2字母+3数字"
bigint site_id UK
varchar site_name
int tenant_id
timestamptz created_at
}
roles {
serial id PK
varchar code UK
varchar name
text description
timestamptz created_at
}
permissions {
serial id PK
varchar code UK
varchar name
text description
timestamptz created_at
}
role_permissions {
int role_id FK
int permission_id FK
}
user_site_roles {
serial id PK
int user_id FK
bigint site_id
int role_id FK
timestamptz created_at
}
user_assistant_binding {
serial id PK
int user_id FK
bigint site_id
bigint assistant_id "可空"
bigint staff_id "可空"
varchar binding_type "assistant/staff/manager"
timestamptz created_at
}
users ||--o{ user_applications : "提交申请"
users ||--o{ user_site_roles : "店铺角色"
users ||--o{ user_assistant_binding : "人员绑定"
roles ||--o{ user_site_roles : "角色分配"
roles ||--o{ role_permissions : "角色权限"
permissions ||--o{ role_permissions : "权限定义"
site_code_mapping ||--o{ user_applications : "球房映射"
```
### 表 DDL 概要
所有表在 `auth` Schema 下,迁移脚本位于 `db/zqyy_app/migrations/`
**关键约束**
- `users.wx_openid` UNIQUE — 一个微信用户对应一条记录
- `site_code_mapping.site_code` UNIQUE — 球房ID 唯一
- `site_code_mapping.site_id` UNIQUE — site_id 唯一映射
- `user_site_roles (user_id, site_id, role_id)` UNIQUE — 防止重复分配
- `role_permissions (role_id, permission_id)` 联合主键
**索引**
- `users`: `ix_users_wx_openid` (wx_openid)
- `user_applications`: `ix_user_applications_user_id` (user_id), `ix_user_applications_status` (status)
- `user_site_roles`: `ix_user_site_roles_user_site` (user_id, site_id)
- `site_code_mapping`: `ix_site_code_mapping_site_code` (site_code)
### 迁移脚本清单
| 序号 | 文件名 | 内容 |
|------|--------|------|
| 1 | `YYYY-MM-DD__p3_create_auth_tables.sql` | 创建 users / user_applications / site_code_mapping / roles / permissions / role_permissions / user_site_roles / user_assistant_binding |
| 2 | `YYYY-MM-DD__p3_seed_roles_permissions.sql` | 种子数据:权限列表 + 默认角色 + 角色权限映射 |
## 正确性属性Correctness Properties
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1迁移脚本幂等性
*For any* 本次新增的迁移脚本DDL + 种子数据),连续执行两次的结果应与执行一次相同——第二次执行不应产生错误,且数据库状态不变。
**Validates: Requirements 1.9, 2.4, 11.5**
### Property 2登录创建/查找用户正确性
*For any* 有效的微信 `openid`,调用登录逻辑后:若该 `openid` 已存在于 `auth.users` 中,应返回已有用户的 `user_id`若不存在应创建新用户status=`pending`)并返回新 `user_id`。无论哪种情况,返回的 JWT 中 `sub` 应等于该 `user_id`
**Validates: Requirements 3.2, 3.3**
### Property 3disabled 用户登录拒绝
*For any* `auth.users` 中 status 为 `disabled` 的用户,通过其 `openid` 登录时应返回 403 错误,不签发 JWT。
**Validates: Requirements 3.5**
### Property 4申请创建正确性
*For any* 有效的申请数据(合法 `site_code` 格式、11 位手机号、非空 `applied_role_text`),提交申请后 `auth.user_applications` 中应新增一条 status=`pending` 的记录。若 `site_code``site_code_mapping` 中有映射,记录的 `site_id` 应等于映射值;若无映射,`site_id` 为 NULL 但申请仍成功。若提供了 `nickname``auth.users` 中该用户的 `nickname` 应更新。
**Validates: Requirements 4.1, 4.2, 4.3, 4.4**
### Property 5手机号格式验证
*For any* 非 11 位纯数字的字符串作为 `phone` 提交申请,系统应返回 422 错误,`auth.user_applications` 中不应新增记录。
**Validates: Requirements 4.5**
### Property 6重复申请拒绝
*For any* 已有一条 status=`pending` 申请的用户,再次提交申请时应返回 409 错误,`auth.user_applications` 中不应新增记录。
**Validates: Requirements 4.6**
### Property 7人员匹配合并正确性
*For any* 有效的 `site_id``phone` 组合,匹配服务返回的候选列表应满足:(1) 每条候选的 `source_type``assistant``staff`(2) 助教来源的候选来自 `v_dim_assistant` 表中 `site_id``mobile` 匹配的记录;(3) 员工来源的候选来自 `v_dim_staff` 表中 `site_id``mobile`(或 `job_num`)匹配的记录;(4) 列表是两个来源结果的并集,无遗漏。
**Validates: Requirements 5.1, 5.2, 5.3, 5.4**
### Property 8审核操作正确性
*For any* status=`pending` 的申请:(1) 批准操作后,申请 status 变为 `approved``auth.user_site_roles` 中新增角色记录,`auth.users.status` 变为 `approved``reviewer_id``reviewed_at` 非空;(2) 若提供了 binding 信息,`auth.user_assistant_binding` 中新增绑定记录;(3) 拒绝操作后,申请 status 变为 `rejected``review_note` 非空,`reviewer_id``reviewed_at` 非空。
**Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5**
### Property 9非 pending 申请审核拒绝
*For any* status 不是 `pending` 的申请(`approved` / `rejected`),对其执行批准或拒绝操作应返回 409 错误,申请状态不变。
**Validates: Requirements 6.6**
### Property 10用户状态查询完整性
*For any* 用户,查询自身状态应返回:(1) 用户的 `status` 字段;(2) 该用户所有申请记录的完整列表。若用户 status 为 `approved`,还应返回已关联的店铺列表和对应角色。
**Validates: Requirements 7.1, 7.2**
### Property 11多店铺角色独立分配
*For any* 用户和多个不同的 `site_id`,系统应允许为该用户在每个 `site_id` 下独立分配不同的角色,且 `auth.user_site_roles` 中的记录互不干扰。
**Validates: Requirements 8.1**
### Property 12店铺切换令牌正确性
*For any* 拥有多店铺绑定的 approved 用户,切换到目标 `site_id` 后签发的新 JWT 中 `site_id` 应等于目标值,`roles` 应等于该用户在目标 `site_id` 下的角色列表。若用户在目标 `site_id` 下无角色绑定,切换应失败。
**Validates: Requirements 8.2, 10.4**
### Property 13权限中间件拦截正确性
*For any* 用户、`site_id` 和所需权限 code 的组合:(1) 若用户 status 非 `approved`,返回 403(2) 若用户在该 `site_id` 下的权限列表不包含所需权限,返回 403(3) 若用户在该 `site_id` 下拥有所需权限且 status 为 `approved`,放行。
**Validates: Requirements 8.3, 9.1, 9.2, 9.3**
### Property 14JWT payload 结构与状态一致性
*For any* 通过登录签发的 JWT(1) 解码后应包含 `sub`user_id`type``exp` 字段;(2) 若用户 status 为 `approved`payload 应包含 `site_id``roles`(3) 若用户 status 为 `pending`payload 应包含 `limited=True`,不含 `site_id``roles`
**Validates: Requirements 10.1, 10.2, 10.3**
### Property 15JWT 过期/无效令牌拒绝
*For any* 过期的 JWT 或被篡改的 JWT 字符串,权限中间件应返回 401 错误,不放行请求。
**Validates: Requirements 9.4**
## 错误处理
### API 错误码规范
| HTTP 状态码 | 场景 | 响应体 |
|------------|------|--------|
| 401 | JWT 无效/过期、微信 code2Session 失败 | `{"detail": "具体错误描述"}` |
| 403 | 用户 disabled、权限不足、用户未 approved | `{"detail": "具体错误描述"}` |
| 404 | 申请不存在 | `{"detail": "申请不存在"}` |
| 409 | 重复提交 pending 申请、审核非 pending 申请 | `{"detail": "具体冲突描述"}` |
| 422 | 请求体校验失败手机号格式、site_code 格式等) | Pydantic 标准错误格式 |
| 500 | 数据库连接失败、微信 API 超时 | `{"detail": "服务器内部错误"}` |
### 微信 API 错误处理
| 微信 errcode | 含义 | 处理方式 |
|-------------|------|---------|
| 0 | 成功 | 正常流程 |
| 40029 | code 无效 | 返回 401提示"登录凭证无效,请重新登录" |
| 45011 | 频率限制 | 返回 429提示"请求过于频繁" |
| 40226 | 高风险用户 | 返回 403提示"账号存在风险" |
| 其他 | 未知错误 | 返回 401记录日志提示"微信登录失败" |
### 数据库错误处理
| 场景 | 处理方式 |
|------|---------|
| 连接失败 | 捕获 `psycopg2.OperationalError`,返回 500 |
| 唯一约束冲突wx_openid | 并发创建时捕获 `UniqueViolation`,改为查询已有记录 |
| 外键约束失败 | 返回 422提示具体的关联数据不存在 |
| FDW 查询失败 | 捕获异常,匹配服务返回空列表,记录日志 |
### 环境变量缺失处理
| 变量 | 缺失时行为 |
|------|-----------|
| `WX_APPID` / `WX_SECRET` | 微信登录端点返回 500日志记录"微信配置缺失" |
| `JWT_SECRET_KEY` | 应用启动时警告空字符串不安全JWT 签发/验证使用空密钥(仅开发环境) |
| `DB_HOST` 等数据库参数 | 数据库连接失败,返回 500 |
## 测试策略
### DDL 测试库落库与文档同步
DDL 变更必须经过以下流程:
1. **测试库执行**:在 `test_zqyy_app` 中执行迁移脚本,验证无错误
2. **幂等性验证**:连续执行两次,第二次无错误
3. **数据库手册更新**:创建/更新 `docs/database/BD_Manual_auth_tables.md`,格式参照现有 `BD_Manual_auth_biz_schemas.md`
4. **DDL 基线刷新**:运行 `python scripts/ops/gen_consolidated_ddl.py` 重新生成 `docs/database/ddl/zqyy_app__auth.sql`
### 小程序认证前端页面
#### 页面清单
| 页面 | 路径 | 说明 | H5 原型 |
|------|------|------|---------|
| login | `pages/login/login` | 微信登录页(自动调用 wx.login | `docs/h5_ui/pages/login.html` |
| apply | `pages/apply/apply` | 申请表单页球房ID + 手机号 + 身份 + 编号 + 昵称) | `docs/h5_ui/pages/apply.html` |
| reviewing | `pages/reviewing/reviewing` | 审核等待页(显示状态 + 申请摘要) | `docs/h5_ui/pages/reviewing.html` |
| no-permission | `pages/no-permission/no-permission` | 无权限/已禁用页 | `docs/h5_ui/pages/no-permission.html` |
#### 认证路由流程
```
app.ts onLaunch()
├── wx.login() → 获取 code
├── POST /api/xcx/login {code}
│ │
│ ├── 返回 user_status = "approved"
│ │ └── 跳转主页task-list 或 home
│ │
│ ├── 返回 user_status = "pending"
│ │ ├── 查询 /api/xcx/me → 有 pending 申请
│ │ │ └── 跳转 reviewing 页面
│ │ └── 查询 /api/xcx/me → 无 pending 申请
│ │ └── 跳转 apply 页面
│ │
│ ├── 返回 user_status = "rejected"
│ │ └── 跳转 reviewing 页面(显示拒绝原因 + 重新申请按钮)
│ │
│ └── 返回 403disabled
│ └── 跳转 no-permission 页面
└── 登录失败(网络错误等)
└── 显示错误提示,提供重试按钮
```
#### app.ts 全局状态管理
```typescript
// globalData 扩展
interface IAppOption {
globalData: {
userInfo?: {
userId: number;
status: string; // pending / approved / rejected / disabled
nickname?: string;
};
token?: string;
refreshToken?: string;
currentSiteId?: number;
sites?: Array<{ siteId: number; siteName: string; roles: string[] }>;
};
}
```
#### 请求封装utils/request.ts
```typescript
/**
* 统一请求封装:
* 1. 自动附加 Authorization: Bearer <token>
* 2. 401 时自动尝试 refresh_token 刷新
* 3. 刷新失败时跳转 login 页面
*/
function request(options: RequestOptions): Promise<any> { ... }
```
### 开发模式联调
#### Mock 登录端点
后端在 `WX_DEV_MODE=true` 时注册 `POST /api/xcx/dev-login`
```python
@router.post("/api/xcx/dev-login")
async def dev_login(openid: str, status: str = "approved"):
"""
开发模式 mock 登录。
直接根据 openid 查找/创建用户,跳过微信 code2Session。
可通过 status 参数模拟不同用户状态。
仅在 WX_DEV_MODE=true 时可用。
"""
...
```
#### 微信开发者工具联调步骤
联调指南文档位于 `apps/miniprogram/doc/auth-integration-guide.md`,包含:
1. 微信开发者工具项目导入配置appid、不校验合法域名
2. 后端启动命令(`cd apps/backend && uvicorn app.main:app --reload`
3. 小程序请求域名配置(开发环境指向 `http://localhost:8000`
4. 测试流程:登录 → 申请 → 管理端审核 → 重新登录验证
5. Mock 模式使用说明
### 属性测试Property-Based Testing
使用 Python `hypothesis` 框架,测试目录:`tests/`Monorepo 级属性测试目录)。
每个属性测试至少运行 100 次迭代。每个测试用注释标注对应的设计属性编号。
标注格式:`# Feature: miniapp-auth-system, Property N: <属性标题>`
**属性测试清单**
| 属性 | 测试文件 | 测试方法 | 生成器 |
|------|---------|---------|--------|
| P2 登录创建/查找用户 | `tests/test_auth_system_properties.py` | 生成随机 openid模拟登录验证用户创建/查找逻辑 | `hypothesis.strategies.text` 生成 openid |
| P4 申请创建正确性 | `tests/test_auth_system_properties.py` | 生成随机合法申请数据,验证申请记录创建 | 自定义 strategy 生成 site_code2字母+3数字、phone11位数字 |
| P5 手机号格式验证 | `tests/test_auth_system_properties.py` | 生成随机非法手机号,验证 422 拒绝 | `hypothesis.strategies.text` 过滤非 11 位数字 |
| P6 重复申请拒绝 | `tests/test_auth_system_properties.py` | 生成随机用户+申请,提交两次,验证第二次 409 | 复用申请数据生成器 |
| P7 人员匹配合并 | `tests/test_auth_system_properties.py` | 生成随机助教/员工数据,验证匹配结果合并 | 自定义 strategy 生成匹配数据 |
| P8 审核操作正确性 | `tests/test_auth_system_properties.py` | 生成随机 pending 申请,执行批准/拒绝,验证状态流转 | 自定义 strategy 生成审核数据 |
| P9 非 pending 审核拒绝 | `tests/test_auth_system_properties.py` | 生成随机非 pending 申请,验证 409 | `hypothesis.strategies.sampled_from(["approved", "rejected"])` |
| P11 多店铺角色独立 | `tests/test_auth_system_properties.py` | 生成随机用户+多个 site_id验证角色独立分配 | `hypothesis.strategies.lists` 生成 site_id 列表 |
| P12 店铺切换令牌 | `tests/test_auth_system_properties.py` | 生成多店铺用户,切换店铺,验证 JWT 内容 | 复用多店铺生成器 |
| P13 权限中间件拦截 | `tests/test_auth_system_properties.py` | 生成随机用户+权限组合,验证中间件判断 | 自定义 strategy 生成权限矩阵 |
| P14 JWT payload 结构 | `tests/test_auth_system_properties.py` | 生成随机用户(不同 status签发 JWT验证 payload | `hypothesis.strategies.sampled_from(["pending", "approved"])` |
| P15 JWT 过期/无效拒绝 | `tests/test_auth_system_properties.py` | 生成随机过期/篡改 JWT验证 401 | 自定义 strategy 生成无效 JWT |
**注意**P1迁移幂等性、P3disabled 登录拒绝、P10状态查询完整性作为集成测试在后端测试目录实现因为它们需要真实数据库环境或涉及具体的数据库状态验证。
### 单元测试
单元测试位于 `apps/backend/tests/`,聚焦于:
- `test_xcx_auth_router.py`微信登录路由测试mock 微信 API
- `test_application_service.py`:申请服务的边界情况
- `test_matching_service.py`匹配逻辑的边界情况空结果、FDW 异常)
- `test_permission_middleware.py`:权限中间件的各种组合
- `test_jwt_extended.py`:扩展 JWT 的 limited token 逻辑
### 集成测试
集成测试通过验证脚本实现,覆盖:
- 迁移脚本幂等性验证(执行两次无错误)
- 种子数据完整性验证(权限和角色数量正确)
- 完整认证流程:登录 → 申请 → 审核 → 权限验证

View File

@@ -0,0 +1,196 @@
# 需求文档小程序用户认证系统miniapp-auth-system
## 简介
本 SPEC 实现小程序用户认证系统,涵盖微信登录、用户申请审核、人员匹配、多店铺权限管理等完整认证链路。系统基于 P1miniapp-db-foundation已建立的 `auth` Schema 和 FDW 映射,在 `test_zqyy_app.auth` 中创建用户、申请、角色、绑定等业务表,并在 FastAPI 后端实现对应的 API 端点和权限中间件。
## 术语表
- **Auth_System**:小程序用户认证系统,负责微信登录、用户管理、申请审核、权限控制的完整后端服务
- **WeChat_Auth_Service**:微信认证服务模块,负责调用微信 `code2Session` 接口换取 `openid``session_key`
- **Application_Service**:用户申请服务模块,负责处理用户提交的入驻申请、状态流转和审核操作
- **Matching_Service**人员匹配服务模块负责根据球房ID和手机号/编号在助教表和员工表中查找候选匹配
- **Permission_Middleware**:权限中间件,基于用户的 `site_id` + `role` 拦截无权请求
- **JWT_Service**JWT 令牌服务,负责签发和刷新 access_token / refresh_token已有实现本 SPEC 扩展)
- **site_code**球房ID格式为 2 字母 + 3 数字(如 `AB123`),与 `site_id` 一一映射
- **site_id**:门店标识符,类型为 `BIGINT`,用于多门店数据隔离
- **user_status**:用户状态枚举,取值为 `pending`(审核中)/ `approved`(已通过)/ `rejected`(已拒绝)/ `disabled`(已禁用)
- **binding_type**:绑定类型枚举,取值为 `assistant`(助教)/ `staff`(员工)/ `manager`(管理员)
- **FDW**`postgres_fdw` 外部数据包装器,通过 `fdw_etl` Schema 读取 ETL 库数据
- **Migration_Script**:存放在 `db/zqyy_app/migrations/` 中的纯 SQL 迁移脚本,以日期前缀命名
- **BD_Manual**:数据库手册文档,存放在 `docs/database/` 中,记录表结构变更、兼容性影响、回滚策略和验证 SQL
- **DDL_Baseline**DDL 基线文件,存放在 `docs/database/ddl/` 中,由 `gen_consolidated_ddl.py` 自动生成
- **Miniprogram_Auth_Pages**:小程序认证相关前端页面,包括登录页、申请表单页、审核等待页、无权限页
- **Dev_Login**:开发模式下的 mock 登录端点,绕过微信 code2Session 调用,用于联调测试
## 需求
### 需求 1认证数据表创建
**用户故事:** 作为后端开发者,我需要在 `auth` Schema 中创建用户认证相关的数据表,以便支撑完整的认证和权限管理功能。
#### 验收标准
1. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `users` 表,包含 `id`SERIAL PK`wx_openid`UNIQUE`wx_union_id``wx_avatar_url``nickname``phone``status`(默认 `pending`)、`created_at``updated_at` 字段
2. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `user_applications` 表,包含 `id`SERIAL PK`user_id`FK → users`site_code``applied_role_text``employee_number`(可选)、`phone``status`(默认 `pending`)、`reviewer_id``review_note``created_at``reviewed_at` 字段
3. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `site_code_mapping` 表,包含 `id`SERIAL PK`site_code`UNIQUE格式 2 字母 + 3 数字)、`site_id`BIGINT UNIQUE`site_name``tenant_id``created_at` 字段
4. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `user_site_roles` 表,包含 `id`SERIAL PK`user_id`FK → users`site_id`BIGINT`role_id`FK → roles`created_at` 字段,并对 `(user_id, site_id, role_id)` 建立唯一约束
5. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `user_assistant_binding` 表,包含 `id`SERIAL PK`user_id`FK → users`site_id`BIGINT`assistant_id`BIGINT可选`staff_id`BIGINT可选`binding_type``created_at` 字段
6. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `roles` 表,包含 `id`SERIAL PK`code`UNIQUE`name``description``created_at` 字段
7. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `permissions` 表,包含 `id`SERIAL PK`code`UNIQUE`name``description``created_at` 字段
8. WHEN Migration_Script 执行完成, THE Auth_System SHALL 在 `auth` Schema 中创建 `role_permissions` 表,包含 `role_id`FK → roles`permission_id`FK → permissions字段并以 `(role_id, permission_id)` 为联合主键
9. THE Migration_Script SHALL 使用 `IF NOT EXISTS` / `OR REPLACE` 等幂等语法,确保重复执行不会报错
10. THE Migration_Script SHALL 在脚本中包含回滚语句(以注释形式)
### 需求 2种子数据预置
**用户故事:** 作为系统管理员,我需要系统预置固定的权限列表和默认角色,以便审核时可直接分配。
#### 验收标准
1. WHEN 种子数据脚本执行完成, THE Auth_System SHALL 在 `auth.permissions` 表中插入 5 条固定权限记录:`view_tasks``view_board``view_board_finance``view_board_customer``view_board_coach`
2. WHEN 种子数据脚本执行完成, THE Auth_System SHALL 在 `auth.roles` 表中插入默认角色(至少包含 `coach`(助教)、`staff`(员工)、`site_admin`(店铺管理员)、`tenant_admin`(租户管理员))
3. WHEN 种子数据脚本执行完成, THE Auth_System SHALL 在 `auth.role_permissions` 表中为每个默认角色分配对应的权限组合
4. THE 种子数据脚本 SHALL 使用 `ON CONFLICT DO NOTHING` 语法,确保重复执行不会产生重复数据
### 需求 3微信登录
**用户故事:** 作为球房工作人员,我需要通过微信登录小程序,以便快速进入系统。
#### 验收标准
1. WHEN 小程序端发送微信临时登录凭证(`code`, THE WeChat_Auth_Service SHALL 调用微信 `code2Session` 接口换取 `openid``session_key`
2. WHEN `code2Session` 返回有效 `openid` 且该 `openid` 已存在于 `auth.users` 表中, THE Auth_System SHALL 返回该用户的 JWT 令牌对access_token + refresh_token和用户状态信息
3. WHEN `code2Session` 返回有效 `openid` 且该 `openid` 不存在于 `auth.users` 表中, THE Auth_System SHALL 创建新用户记录status 为 `pending`),返回 JWT 令牌对和 `pending` 状态标识
4. IF `code2Session` 接口调用失败或返回错误码, THEN THE WeChat_Auth_Service SHALL 返回 HTTP 401 错误,包含具体的错误描述
5. WHEN 用户状态为 `disabled`, THE Auth_System SHALL 返回 HTTP 403 错误,拒绝登录
### 需求 4用户申请提交
**用户故事:** 作为球房工作人员我需要在首次登录后填写申请表单球房ID、申请身份、手机号、编号、昵称以便管理员审核我的身份。
#### 验收标准
1. WHEN 用户提交申请表单(包含 `site_code``applied_role_text``phone`,可选 `employee_number`, THE Application_Service SHALL 在 `auth.user_applications` 表中创建一条 status 为 `pending` 的申请记录
2. WHEN 用户提交的 `site_code``auth.site_code_mapping` 中存在映射, THE Application_Service SHALL 将申请记录关联到对应的 `site_id`
3. WHEN 用户提交的 `site_code``auth.site_code_mapping` 中不存在映射, THE Application_Service SHALL 仍然接受申请,申请记录中保留 `site_code` 文本,管理端显示"未找到关联信息"
4. WHEN 用户提交申请时提供了 `nickname`, THE Auth_System SHALL 更新 `auth.users` 表中该用户的 `nickname` 字段
5. IF 用户提交的 `phone` 为空或格式无效(非 11 位数字), THEN THE Application_Service SHALL 返回 HTTP 422 错误,包含具体的校验失败信息
6. WHEN 用户已有一条 `pending` 状态的申请, THE Application_Service SHALL 拒绝重复提交,返回 HTTP 409 错误
### 需求 5人员匹配
**用户故事:** 作为系统我需要根据球房ID和手机号自动建议用户与助教/员工的对应关系,以便管理员快速审核。
#### 验收标准
1. WHEN 管理员查看某条申请详情时, THE Matching_Service SHALL 根据申请中的 `site_id``phone``fdw_etl.v_dim_assistant` 中按 `site_id` + `mobile` 匹配助教记录
2. WHEN 管理员查看某条申请详情时, THE Matching_Service SHALL 根据申请中的 `site_id``phone``fdw_etl.v_dim_staff``fdw_etl.v_dim_staff_ex` 中按 `site_id` + `mobile` 匹配员工记录
3. WHEN 申请中包含 `employee_number`, THE Matching_Service SHALL 额外按 `job_num` 字段匹配员工记录
4. THE Matching_Service SHALL 将助教匹配结果和员工匹配结果合并为统一的候选列表返回,每条候选包含来源类型(`assistant` / `staff`)、姓名、手机号、编号
5. WHEN 助教表和员工表均无匹配结果, THE Matching_Service SHALL 返回空候选列表,管理端显示"未找到关联信息"
6. WHEN 申请的 `site_code` 无法映射到 `site_id`, THE Matching_Service SHALL 跳过匹配,返回空候选列表
### 需求 6申请审核
**用户故事:** 作为租户管理员,我需要审核用户申请,将用户关联到对应的助教/员工,并分配身份权限。
#### 验收标准
1. WHEN 管理员批准申请并选择了候选匹配对象, THE Application_Service SHALL 将申请状态更新为 `approved`,在 `auth.user_assistant_binding` 中创建绑定记录,在 `auth.user_site_roles` 中分配角色
2. WHEN 管理员批准申请但无候选匹配(手动审核), THE Application_Service SHALL 将申请状态更新为 `approved`,仅在 `auth.user_site_roles` 中分配角色,不创建绑定记录
3. WHEN 管理员拒绝申请, THE Application_Service SHALL 将申请状态更新为 `rejected`,记录 `review_note`(拒绝原因)
4. WHEN 申请审核通过后, THE Auth_System SHALL 将 `auth.users` 表中该用户的 `status` 更新为 `approved`
5. WHEN 审核操作完成, THE Application_Service SHALL 记录 `reviewer_id``reviewed_at` 时间戳
6. IF 审核目标申请的状态不是 `pending`, THEN THE Application_Service SHALL 返回 HTTP 409 错误,拒绝重复审核
### 需求 7用户状态查询
**用户故事:** 作为用户,我需要看到自己的申请状态(审核中/通过/拒绝),以便了解审核进度。
#### 验收标准
1. WHEN 用户查询自身状态, THE Auth_System SHALL 返回用户的 `status`、所有申请记录列表(含每条申请的 `site_code``applied_role_text``status``review_note`
2. WHEN 用户状态为 `approved`, THE Auth_System SHALL 同时返回用户已关联的店铺列表和对应角色
### 需求 8多店铺支持与店铺切换
**用户故事:** 作为用户,我可以同时属于多个店铺(连锁场景),切换店铺后数据正确隔离。
#### 验收标准
1. THE Auth_System SHALL 允许一个用户通过多次申请关联到多个不同的 `site_id`,每个 `site_id` 独立分配角色
2. WHEN 用户切换当前店铺, THE JWT_Service SHALL 签发包含新 `site_id` 的 JWT 令牌对
3. WHEN 用户携带某 `site_id` 的 JWT 访问 API, THE Permission_Middleware SHALL 仅允许访问该 `site_id` 下用户拥有权限的资源
### 需求 9权限中间件
**用户故事:** 作为系统,我需要权限中间件正确拦截无权请求,确保数据安全。
#### 验收标准
1. WHEN 用户携带有效 JWT 访问受保护端点, THE Permission_Middleware SHALL 从 JWT 中提取 `user_id``site_id`,查询 `auth.user_site_roles``auth.role_permissions` 获取用户在该店铺下的权限列表
2. WHEN 用户的权限列表不包含端点所需的权限 code, THE Permission_Middleware SHALL 返回 HTTP 403 错误
3. WHEN 用户的 `status` 不是 `approved`, THE Permission_Middleware SHALL 返回 HTTP 403 错误,拒绝访问受保护端点
4. WHEN JWT 令牌过期或无效, THE Permission_Middleware SHALL 返回 HTTP 401 错误
### 需求 10JWT 令牌扩展
**用户故事:** 作为后端开发者,我需要扩展现有 JWT 服务以支持微信登录场景和多店铺切换。
#### 验收标准
1. THE JWT_Service SHALL 在 JWT payload 中包含 `user_id`sub`site_id``roles`(角色 code 列表)、`type`access/refresh`exp` 字段
2. WHEN 用户通过微信登录且状态为 `approved`, THE JWT_Service SHALL 使用用户默认店铺(第一个关联的 site_id签发令牌
3. WHEN 用户通过微信登录且状态为 `pending`, THE JWT_Service SHALL 签发不含 `site_id``roles` 的受限令牌,仅允许访问申请提交和状态查询端点
4. WHEN 用户请求切换店铺, THE JWT_Service SHALL 验证用户在目标 `site_id` 下有角色绑定后签发新令牌
### 需求 11迁移脚本管理
**用户故事:** 作为后端开发者,我需要所有数据库变更都有对应的迁移脚本,以便变更可追溯、可重放。
#### 验收标准
1. THE Migration_Script SHALL 将所有认证相关表的 DDL 存放在 `db/zqyy_app/migrations/` 目录中
2. THE Migration_Script SHALL 使用日期前缀命名(格式:`YYYY-MM-DD__<描述>.sql`
3. THE Migration_Script SHALL 使用 UTF-8 编码,纯 SQL非 ORM
4. THE Migration_Script SHALL 在每个脚本中包含回滚语句(以注释形式)
5. THE Migration_Script SHALL 使用幂等语法(`IF NOT EXISTS``ON CONFLICT DO NOTHING`),确保重复执行不会报错
### 需求 12DDL 测试库落库与文档同步
**用户故事:** 作为后端开发者,我需要所有 DDL 变更在测试库(`test_zqyy_app`)中实际执行验证,并同步更新数据库手册和 DDL 基线,确保文档与实际 Schema 一致。
#### 验收标准
1. WHEN 迁移脚本编写完成, THE Auth_System SHALL 在 `test_zqyy_app` 测试库中执行迁移脚本,验证无错误
2. WHEN 迁移脚本执行成功, THE Auth_System SHALL 创建或更新 `docs/database/BD_Manual_auth_tables.md` 数据库手册,包含变更说明、兼容性影响、回滚策略、验证 SQL至少 3 条)
3. WHEN 迁移脚本执行成功, THE Auth_System SHALL 运行 `python scripts/ops/gen_consolidated_ddl.py` 重新生成 DDL 基线文件 `docs/database/ddl/zqyy_app__auth.sql`
4. WHEN 种子数据脚本执行成功, THE Auth_System SHALL 在数据库手册中记录种子数据内容(角色、权限、角色-权限映射)
### 需求 13小程序认证前端页面
**用户故事:** 作为球房工作人员,我需要在小程序中看到登录页、申请表单页、审核状态页,以便完成从微信登录到正式使用的完整流程。
#### 验收标准
1. WHEN 用户首次打开小程序, THE Auth_System SHALL 展示登录页面,调用 `wx.login()` 获取 code 并发送到后端 `/api/xcx/login`
2. WHEN 后端返回 `user_status=pending` 且用户无 pending 申请, THE Auth_System SHALL 跳转到申请表单页面包含球房ID`site_code`)、申请身份、手机号、编号(选填)、昵称输入框
3. WHEN 用户提交申请表单, THE Auth_System SHALL 调用 `/api/xcx/apply` 提交申请,成功后跳转到审核等待页面
4. WHEN 用户状态为 `pending` 且已有 pending 申请, THE Auth_System SHALL 展示审核等待页面,显示"审核中"状态和申请信息摘要
5. WHEN 用户状态为 `rejected`, THE Auth_System SHALL 在审核等待页面显示拒绝原因,并提供"重新申请"按钮
6. WHEN 用户状态为 `approved`, THE Auth_System SHALL 跳转到小程序主页(任务列表)
7. WHEN 用户状态为 `disabled`, THE Auth_System SHALL 展示无权限页面,提示账号已被禁用
8. THE Auth_System SHALL 在小程序 `app.ts``onLaunch` 中实现自动登录逻辑,根据用户状态路由到对应页面
9. WHEN 用户拥有多个店铺, THE Auth_System SHALL 在主页提供店铺切换入口
### 需求 14前后端联调验证
**用户故事:** 作为开发者,我需要在微信开发者工具中验证完整的认证流程(登录→申请→审核→进入主页),确保前后端接口对接正确。
#### 验收标准
1. THE Auth_System SHALL 提供联调验证脚本或文档,说明如何在微信开发者工具中测试完整认证流程
2. THE Auth_System SHALL 在后端提供开发模式下的 mock 登录端点(`POST /api/xcx/dev-login`),接受任意 openid 直接返回 JWT绕过微信 code2Session 调用
3. WHEN 开发模式启用时, THE Auth_System SHALL 允许通过环境变量 `WX_DEV_MODE=true` 切换到 mock 模式
4. THE Auth_System SHALL 在 `apps/miniprogram/doc/` 中提供联调指南文档,包含微信开发者工具配置、后端启动步骤、测试账号说明

View File

@@ -0,0 +1,288 @@
# 实现计划小程序用户认证系统miniapp-auth-system
## 概述
基于已批准的需求和设计文档,将小程序用户认证系统拆分为增量式编码任务。每个任务构建在前一个任务之上,最终完成完整的认证链路。后端使用 Python + FastAPI数据库使用 PostgreSQL 纯 SQL属性测试使用 hypothesis。
## 任务
- [x] 1. 创建认证数据表和种子数据
- [x] 1.1 创建迁移脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p3_create_auth_tables.sql`
-`auth` Schema 下创建 `users``user_applications``site_code_mapping``roles``permissions``role_permissions``user_site_roles``user_assistant_binding` 共 8 张表
- 包含所有字段定义、约束、索引、外键
- 使用 `IF NOT EXISTS` 幂等语法
- 包含回滚语句(注释形式)
- _Requirements: 1.1-1.10_
- [x] 1.2 创建种子数据脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p3_seed_roles_permissions.sql`
- 插入 5 条固定权限:`view_tasks``view_board``view_board_finance``view_board_customer``view_board_coach`
- 插入默认角色:`coach``staff``site_admin``tenant_admin`
- 插入角色-权限映射
- 使用 `ON CONFLICT DO NOTHING` 幂等语法
- _Requirements: 2.1-2.4_
- [x] 1.3 在测试库执行迁移脚本并验证
-`test_zqyy_app` 中执行建表脚本和种子数据脚本
- 验证幂等性:连续执行两次无错误
- 验证表结构、约束、索引正确
- 验证种子数据完整5 权限、4 角色、角色-权限映射)
- _Requirements: 12.1_
- [x] 1.4 更新数据库手册和 DDL 基线
- 创建 `docs/database/BD_Manual_auth_tables.md`,包含变更说明、兼容性影响、回滚策略、验证 SQL至少 3 条)
- 运行 `python scripts/ops/gen_consolidated_ddl.py` 刷新 DDL 基线
- 在数据库手册中记录种子数据内容
- _Requirements: 12.2, 12.3, 12.4_
- [x] 1.5 编写迁移脚本幂等性属性测试
- **Property 1: 迁移脚本幂等性**
- **Validates: Requirements 1.9, 2.4, 11.5**
- [x] 2. 扩展 JWT 服务和认证依赖
- [x] 2.1 扩展 `apps/backend/app/auth/jwt.py`
- 新增 `create_limited_token_pair(user_id)` 函数pending 用户受限令牌)
- 扩展 `create_access_token` 支持 `roles` 参数
- 保持向后兼容
- _Requirements: 10.1, 10.2, 10.3_
- [x] 2.2 扩展 `apps/backend/app/auth/dependencies.py`
- 扩展 `CurrentUser` 数据类,增加 `roles``status``limited` 字段
- 新增 `get_current_user_or_limited` 依赖(允许 pending 用户)
- _Requirements: 10.3, 9.1_
- [x] 2.3 编写 JWT payload 结构属性测试
- **Property 14: JWT payload 结构与状态一致性**
- **Validates: Requirements 10.1, 10.2, 10.3**
- [x] 2.4 编写 JWT 过期/无效拒绝属性测试
- **Property 15: JWT 过期/无效令牌拒绝**
- **Validates: Requirements 9.4**
- [x] 3. 检查点 - 确保所有测试通过
- 确保所有测试通过,如有问题请向用户确认。
- [x] 4. 实现微信认证服务
- [x] 4.1 创建 `apps/backend/app/services/wechat.py`
- 实现 `code2session(code)` 异步函数
- 使用 `httpx.AsyncClient` 调用微信 API
- 从环境变量读取 `WX_APPID` / `WX_SECRET`
- 定义 `WeChatAuthError` 异常类
- _Requirements: 3.1, 3.4_
- [x] 4.2 创建 Pydantic 模型 `apps/backend/app/schemas/xcx_auth.py`
- 定义 `WxLoginRequest``WxLoginResponse``ApplicationRequest``ApplicationResponse``UserStatusResponse``SiteInfo``SwitchSiteRequest``MatchCandidate``ApproveRequest``RejectRequest`
- `site_code` 使用正则校验 `^[A-Za-z]{2}\d{3}$`
- `phone` 使用正则校验 `^\d{11}$`
- _Requirements: 4.5_
- [x] 4.3 创建小程序认证路由 `apps/backend/app/routers/xcx_auth.py`
- 实现 `POST /api/xcx/login`:微信登录(查找/创建用户 + 签发 JWT
- 实现 `POST /api/xcx/apply`:提交申请
- 实现 `GET /api/xcx/me`:查询自身状态
- 实现 `GET /api/xcx/me/sites`:查询关联店铺
- 实现 `POST /api/xcx/switch-site`:切换店铺
- 实现 `POST /api/xcx/refresh`:刷新令牌
-`apps/backend/app/main.py` 中注册路由
- _Requirements: 3.2, 3.3, 3.5, 4.1-4.6, 7.1, 7.2, 8.2_
- [x] 4.4 编写登录创建/查找用户属性测试
- **Property 2: 登录创建/查找用户正确性**
- **Validates: Requirements 3.2, 3.3**
- [x] 4.5 编写申请创建正确性属性测试
- **Property 4: 申请创建正确性**
- **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
- [x] 4.6 编写手机号格式验证属性测试
- **Property 5: 手机号格式验证**
- **Validates: Requirements 4.5**
- [x] 4.7 编写重复申请拒绝属性测试
- **Property 6: 重复申请拒绝**
- **Validates: Requirements 4.6**
- [x] 5. 实现申请服务和人员匹配
- [x] 5.1 创建申请服务 `apps/backend/app/services/application.py`
- 实现 `create_application()`:创建申请 + site_code 映射查找
- 实现 `approve_application()`:批准 + 创建绑定/角色
- 实现 `reject_application()`:拒绝 + 记录原因
- 实现 `get_user_applications()`:查询用户申请列表
- _Requirements: 4.1-4.4, 6.1-6.6_
- [x] 5.2 创建人员匹配服务 `apps/backend/app/services/matching.py`
- 实现 `find_candidates(site_id, phone, employee_number)`
- 通过 FDW 查询 `fdw_etl.v_dim_assistant``fdw_etl.v_dim_staff` / `v_dim_staff_ex`
- 设置 `app.current_site_id` 进行 RLS 隔离
- 合并助教和员工匹配结果
- _Requirements: 5.1-5.6_
- [x] 5.3 创建角色权限服务 `apps/backend/app/services/role.py`
- 实现 `get_user_permissions(user_id, site_id)`
- 实现 `get_user_sites(user_id)`
- 实现 `check_user_has_site_role(user_id, site_id)`
- _Requirements: 8.1, 9.1_
- [x] 5.4 编写人员匹配合并属性测试
- **Property 7: 人员匹配合并正确性**
- **Validates: Requirements 5.1, 5.2, 5.3, 5.4**
- [x] 5.5 编写审核操作正确性属性测试
- **Property 8: 审核操作正确性**
- **Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5**
- [x] 5.6 编写非 pending 审核拒绝属性测试
- **Property 9: 非 pending 申请审核拒绝**
- **Validates: Requirements 6.6**
- [x] 6. 检查点 - 确保所有测试通过
- 确保所有测试通过,如有问题请向用户确认。
- [x] 7. 实现权限中间件和管理端路由
- [x] 7.1 创建权限中间件 `apps/backend/app/middleware/permission.py`
- 实现 `require_permission(*permission_codes)` 依赖
- 实现 `require_approved()` 依赖
- 检查用户 status + 权限列表
- _Requirements: 9.1-9.4_
- [x] 7.2 创建管理端审核路由 `apps/backend/app/routers/admin_applications.py`
- 实现 `GET /api/admin/applications`:查询申请列表
- 实现 `GET /api/admin/applications/{id}`:查询申请详情 + 候选匹配
- 实现 `POST /api/admin/applications/{id}/approve`:批准申请
- 实现 `POST /api/admin/applications/{id}/reject`:拒绝申请
-`apps/backend/app/main.py` 中注册路由
- _Requirements: 6.1-6.6, 5.1-5.6_
- [x] 7.3 编写权限中间件拦截属性测试
- **Property 13: 权限中间件拦截正确性**
- **Validates: Requirements 8.3, 9.1, 9.2, 9.3**
- [x] 7.4 编写多店铺角色独立分配属性测试
- **Property 11: 多店铺角色独立分配**
- **Validates: Requirements 8.1**
- [x] 7.5 编写店铺切换令牌属性测试
- **Property 12: 店铺切换令牌正确性**
- **Validates: Requirements 8.2, 10.4**
- [x] 8. 集成与端到端验证
- [x] 8.1 更新 `apps/backend/app/config.py` 新增微信配置项
- 新增 `WX_APPID``WX_SECRET``WX_DEV_MODE` 配置读取
- _Requirements: 3.1, 14.3_
- [x] 8.2 更新 `apps/backend/app/main.py` 注册所有新路由
- 确保 `xcx_auth``admin_applications` 路由已注册
- 验证无路由冲突
- _Requirements: 全部_
- [x] 8.3 实现开发模式 mock 登录端点
-`routers/xcx_auth.py` 中新增 `POST /api/xcx/dev-login`
- 仅在 `WX_DEV_MODE=true` 时注册
- 接受 `openid` 和可选 `status` 参数,直接查找/创建用户并返回 JWT
- _Requirements: 14.2, 14.3_
- [x] 8.4 编写用户状态查询完整性属性测试
- **Property 10: 用户状态查询完整性**
- **Validates: Requirements 7.1, 7.2**
- [x] 8.5 编写 disabled 用户登录拒绝属性测试
- **Property 3: disabled 用户登录拒绝**
- **Validates: Requirements 3.5**
- [x] 9. 小程序认证前端页面
- [x] 9.1 实现请求封装工具 `apps/miniprogram/miniprogram/utils/request.ts`
- 统一请求封装:自动附加 Authorization header
- 401 时自动尝试 refresh_token 刷新
- 刷新失败时跳转 login 页面
- 后端 base URL 从配置读取(开发环境 `http://localhost:8000`
- _Requirements: 13.8_
- [x] 9.2 实现登录页 `apps/miniprogram/miniprogram/pages/login/`
- 调用 `wx.login()` 获取 code
- 发送 code 到 `POST /api/xcx/login`
- 根据返回的 `user_status` 路由到对应页面
- 存储 token 到 globalData 和 Storage
- 参考 H5 原型 `docs/h5_ui/pages/login.html`
- _Requirements: 13.1, 13.6, 13.7, 13.8_
- [x] 9.3 实现申请表单页 `apps/miniprogram/miniprogram/pages/apply/`
- 表单字段球房IDsite_code、申请身份、手机号、编号选填、昵称
- 前端校验site_code 格式2字母+3数字、手机号11位数字
- 提交到 `POST /api/xcx/apply`
- 成功后跳转 reviewing 页面
- 参考 H5 原型 `docs/h5_ui/pages/apply.html`
- _Requirements: 13.2, 13.3_
- [x] 9.4 实现审核等待页 `apps/miniprogram/miniprogram/pages/reviewing/`
- 显示当前申请状态(审核中/已拒绝)
- 显示申请信息摘要球房ID、申请身份、手机号
- 拒绝时显示拒绝原因 + "重新申请"按钮
- 支持下拉刷新查询最新状态
- 参考 H5 原型 `docs/h5_ui/pages/reviewing.html`
- _Requirements: 13.4, 13.5_
- [x] 9.5 实现无权限页 `apps/miniprogram/miniprogram/pages/no-permission/`
- 显示账号已禁用提示
- 参考 H5 原型 `docs/h5_ui/pages/no-permission.html`
- _Requirements: 13.7_
- [x] 9.6 更新 `app.ts``app.json`
-`app.json` 中注册新页面login、apply、reviewing、no-permission
-`app.ts``onLaunch` 中实现自动登录逻辑
- 根据用户状态路由到对应页面
- 扩展 globalData 类型定义token、userInfo、currentSiteId、sites
- _Requirements: 13.8_
- [x] 10. 前后端联调验证
- [x] 10.1 编写联调指南文档 `apps/miniprogram/doc/auth-integration-guide.md`
- 微信开发者工具项目导入配置说明
- 后端启动步骤(含 `WX_DEV_MODE=true` 配置)
- 测试流程mock 登录 → 申请 → 管理端审核 → 重新登录验证
- 常见问题排查
- _Requirements: 14.1, 14.4_
- [x] 10.2 在微信开发者工具中执行联调验证
- 验证登录流程wx.login → 后端 → JWT 返回
- 验证申请流程:表单提交 → 后端创建申请 → 审核等待页展示
- 验证状态路由pending/approved/rejected/disabled 各状态正确跳转
- 验证 token 刷新access_token 过期后自动刷新
- _Requirements: 14.1_
- [x] 11. 属性测试全量运行100 次迭代)— ✅ 15/15 全部通过
- 前面各任务中的属性测试仅用 5 次迭代快速验证逻辑正确性
- 本任务集中对所有属性测试执行 100 次迭代,确保健壮性
- 运行脚本:`scripts/ops/_run_auth_pbt_full.py`
- 结果报告:`export/reports/auth_pbt_full_20260227_034401.md`
- 总耗时 375s15 个属性测试全部通过100 次迭代/每个)
- [x] 11.1 P1 迁移脚本幂等性 — ✅ 25.0s
- [x] 11.2 P2 登录创建/查找用户 — ✅ 49.8s
- [x] 11.3 P3 disabled 用户登录拒绝 — ✅ 37.9s
- [x] 11.4 P4 申请创建正确性 — ✅ 21.3s
- [x] 11.5 P5 手机号格式验证 — ✅ 2.5s
- [x] 11.6 P6 重复申请拒绝 — ✅ 24.7s
- [x] 11.7 P7 人员匹配合并正确性 — ✅ 14.9s
- [x] 11.8 P8 审核操作正确性 — ✅ 22.7s
- [x] 11.9 P9 非 pending 审核拒绝 — ✅ 18.8s
- [x] 11.10 P10 用户状态查询完整性 — ✅ 28.6s
- [x] 11.11 P11 多店铺角色独立分配 — ✅ 46.6s
- [x] 11.12 P12 店铺切换令牌正确性 — ✅ 45.3s
- [x] 11.13 P13 权限中间件拦截正确性 — ✅ 11.9s
- [x] 11.14 P14 JWT payload 结构一致性 — ✅ 4.6s
- [x] 11.15 P15 JWT 过期/无效拒绝 — ✅ 3.2s
- [x] 12. 最终检查点
- 任务 1-12 全部完成
- 15 个属性测试在 100 次迭代下全部通过(报告见 `export/reports/auth_pbt_full_20260227_034401.md`
- 小程序 4 个认证页面login/apply/reviewing/no-permission已创建
- app.ts / app.json 已更新为认证感知版本
- 联调指南文档已编写
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯
- 检查点确保增量验证
- **属性测试策略**:开发阶段各任务中属性测试用 5 次迭代快速验证;任务 11 集中用 100 次迭代全量运行,逐个报告进度
- 单元测试验证具体例子和边界情况
- 所有数据库操作在测试库 `test_zqyy_app` 进行
- 迁移脚本放在 `db/zqyy_app/migrations/` 目录
- 属性测试放在 `tests/` 目录Monorepo 级)

View File

@@ -0,0 +1 @@
{"specId": "27029642-a405-4932-8c22-5bc54fad5173", "workflowType": "requirements-first", "specType": "feature"}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
# 需求文档小程序核心业务模块miniapp-core-business
## 简介
本 SPEC 实现小程序的核心业务逻辑涵盖助教任务系统生成、分配、状态流转、完成检测、备注系统CRUD、星星评分、类型区分、以及后台触发器/轮询调度框架。系统基于 P1miniapp-db-foundation的数据库基础设施、P2etl-dws-miniapp-extensions的 DWS 指数数据、P3miniapp-auth-system的用户认证体系`test_zqyy_app.biz` Schema 中创建任务、备注、触发器等业务表,并在 FastAPI 后端实现对应的 API 端点和后台调度逻辑。
## 术语表
- **Task_Generator**:任务生成器,每日 4:00 后运行,基于 WBI/NCI/RS 指数为每个助教分配 4 种类型任务的后台服务
- **Task_Manager**:任务管理服务,负责任务 CRUD、置顶、放弃、状态流转的后端模块
- **Task_Expiry_Checker**:任务有效期轮询器,每小时检查 `expires_at` 并将过期任务标记为无效
- **Recall_Completion_Detector**召回完成检测器ETL 数据更新后检查助教是否为匹配客户提供了服务
- **Note_Reclassifier**:备注回溯重分类器,召回完成时回溯检查是否有普通备注需重分类为回访备注
- **Note_Service**:备注服务模块,负责备注 CRUD、星星评分存储与读取
- **Trigger_Scheduler**:触发器调度框架,支持 cron/interval/event 三种触发方式的统一调度引擎
- **coach_tasks**:助教任务表,位于 `biz` Schema存储任务分配、状态、有效期等信息
- **coach_task_history**:任务变更历史表,记录任务关闭/新建的追溯链
- **notes**:统一备注表,位于 `biz` Schema通过 `type` 字段区分普通备注/回访备注/放弃原因
- **trigger_jobs**:触发器配置表,位于 `biz` Schema存储轮询/事件触发器的配置与执行状态
- **task_type**:任务类型枚举,取值为 `high_priority_recall`(高优先召回)/ `priority_recall`(优先召回)/ `follow_up_visit`(客户回访)/ `relationship_building`(关系构建)
- **task_status**:任务状态枚举,取值为 `active`(有效)/ `inactive`(无效)/ `completed`(已完成)/ `abandoned`(已放弃)
- **note_type**:备注类型枚举,取值为 `normal`(普通备注)/ `follow_up`(回访备注)/ `abandon_reason`(放弃原因)
- **priority_score**:优先级分数,取 `max(WBI, NCI)` 的快照值,用于任务排序
- **expires_at**:有效期时间戳,默认 NULL无限期填充后表示任务将在该时间点过期
- **FDW**`postgres_fdw` 外部数据包装器,通过 `fdw_etl` Schema 读取 ETL 库指数数据
- **Migration_Script**:存放在 `db/zqyy_app/migrations/` 中的纯 SQL 迁移脚本,以日期前缀命名
- **site_id**:门店标识符,类型为 `BIGINT`,用于多门店数据隔离
- **member_retention_clue**:维客线索表,位于 `public` Schema存储助教为客户记录的维护线索大类 + 摘要 + 详情),独立于 ETL 数据。当前已有基础表结构和 CRUD API`/api/retention-clue`),若不足以支撑本 SPEC 的任务系统需求,可对其 DDL、Pydantic 模型及路由进行扩展或修改
## 需求
### 需求 1业务数据表创建
**用户故事:** 作为后端开发者,我需要在 `biz` Schema 中创建任务、备注、触发器等业务表,以便支撑核心业务功能。
#### 验收标准
1. WHEN Migration_Script 执行完成, THE Task_Manager SHALL 在 `biz` Schema 中创建 `coach_tasks` 表,包含 `id`BIGSERIAL PK`site_id`BIGINT NOT NULL`assistant_id`BIGINT NOT NULL`member_id`BIGINT NOT NULL`task_type`VARCHAR NOT NULL`status`VARCHAR NOT NULL DEFAULT 'active')、`priority_score`NUMERIC(5,2))、`expires_at`TIMESTAMPTZ可空`is_pinned`BOOLEAN DEFAULT FALSE`abandon_reason`TEXT可空`completed_at`TIMESTAMPTZ可空`completed_task_type`VARCHAR可空`parent_task_id`BIGINT可空FK → coach_tasks`created_at`TIMESTAMPTZ DEFAULT NOW())、`updated_at`TIMESTAMPTZ DEFAULT NOW())字段
2. WHEN Migration_Script 执行完成, THE Task_Manager SHALL 在 `biz` Schema 中创建 `coach_task_history` 表,包含 `id`BIGSERIAL PK`task_id`BIGINT FK → coach_tasks`action`VARCHAR NOT NULL`old_status`VARCHAR`new_status`VARCHAR`old_task_type`VARCHAR`new_task_type`VARCHAR`detail`JSONB`created_at`TIMESTAMPTZ DEFAULT NOW())字段
3. WHEN Migration_Script 执行完成, THE Note_Service SHALL 在 `biz` Schema 中创建 `notes` 表,包含 `id`BIGSERIAL PK`site_id`BIGINT NOT NULL`user_id`INTEGER NOT NULL`target_type`VARCHAR NOT NULL`target_id`BIGINT NOT NULL`type`VARCHAR NOT NULL DEFAULT 'normal')、`content`TEXT NOT NULL`rating_service_willingness`SMALLINT可空CHECK 1-5`rating_revisit_likelihood`SMALLINT可空CHECK 1-5`task_id`BIGINT可空FK → coach_tasks`ai_score`SMALLINT可空`ai_analysis`TEXT可空`created_at`TIMESTAMPTZ DEFAULT NOW())、`updated_at`TIMESTAMPTZ DEFAULT NOW())字段
4. WHEN Migration_Script 执行完成, THE Trigger_Scheduler SHALL 在 `biz` Schema 中创建 `trigger_jobs` 表,包含 `id`SERIAL PK`job_type`VARCHAR NOT NULL`job_name`VARCHAR NOT NULL UNIQUE`trigger_condition`VARCHAR NOT NULL`trigger_config`JSONB NOT NULL`last_run_at`TIMESTAMPTZ可空`next_run_at`TIMESTAMPTZ可空`status`VARCHAR NOT NULL DEFAULT 'enabled')、`created_at`TIMESTAMPTZ DEFAULT NOW())字段
5. THE Migration_Script SHALL 对 `coach_tasks` 表创建唯一索引 `idx_coach_tasks_site_assistant_member_type``(site_id, assistant_id, member_id, task_type)` 上,仅对 `status = 'active'` 的记录生效(部分唯一索引)
6. THE Migration_Script SHALL 对 `coach_tasks` 表创建索引 `idx_coach_tasks_assistant_status``(site_id, assistant_id, status)` 上,用于助教任务列表查询
7. THE Migration_Script SHALL 对 `notes` 表创建索引 `idx_notes_target``(site_id, target_type, target_id)` 上,用于按目标查询备注
8. THE Migration_Script SHALL 使用 `IF NOT EXISTS` 幂等语法,确保重复执行不会报错
9. THE Migration_Script SHALL 在脚本中包含回滚语句(以注释形式)
### 需求 2触发器种子数据预置
**用户故事:** 作为系统管理员,我需要系统预置核心触发器配置,以便后台调度任务自动运行。
#### 验收标准
1. WHEN 种子数据脚本执行完成, THE Trigger_Scheduler SHALL 在 `biz.trigger_jobs` 表中插入 `task_generator` 记录trigger_condition='cron'trigger_config 包含 cron 表达式 '0 4 * * *'
2. WHEN 种子数据脚本执行完成, THE Trigger_Scheduler SHALL 在 `biz.trigger_jobs` 表中插入 `task_expiry_check` 记录trigger_condition='interval'trigger_config 包含间隔秒数 3600
3. WHEN 种子数据脚本执行完成, THE Trigger_Scheduler SHALL 在 `biz.trigger_jobs` 表中插入 `recall_completion_check` 记录trigger_condition='event'trigger_config 包含事件名 'etl_data_updated'
4. WHEN 种子数据脚本执行完成, THE Trigger_Scheduler SHALL 在 `biz.trigger_jobs` 表中插入 `note_reclassify_backfill` 记录trigger_condition='event'trigger_config 包含事件名 'recall_completed'
5. THE 种子数据脚本 SHALL 使用 `ON CONFLICT (job_name) DO NOTHING` 语法,确保重复执行不会产生重复数据
### 需求 3任务生成器
**用户故事:** 作为助教,我每天打开小程序能看到系统为我分配的任务列表,按优先级排序。
#### 验收标准
1. WHEN Task_Generator 运行时, THE Task_Generator SHALL 通过 FDW 读取 `fdw_etl` 中的 `dws_member_winback_index`WBI`dws_member_newconv_index`NCI指数数据计算 `priority_score = max(WBI, NCI)`
2. WHEN `priority_score > 7`, THE Task_Generator SHALL 为该客户-助教对生成 `high_priority_recall`(高优先召回)类型任务
3. WHEN `priority_score > 5``priority_score <= 7`, THE Task_Generator SHALL 为该客户-助教对生成 `priority_recall`(优先召回)类型任务
4. WHEN 助教完成某客户的召回任务后该客户无回访备注, THE Task_Generator SHALL 为该客户-助教对生成 `follow_up_visit`(客户回访)类型任务
5. WHEN 客户-助教对的 RS 指数 < 6通过 FDW 读取 `dws_member_assistant_relation_index`, THE Task_Generator SHALL 为该客户-助教对生成 `relationship_building`(关系构建)类型任务
6. WHEN Task_Generator 生成任务时发现已存在相同 `(site_id, assistant_id, member_id, task_type)``status = 'active'` 的任务, THE Task_Generator SHALL 跳过该任务不做任何操作
7. WHEN Task_Generator 生成任务时发现已存在相同 `(site_id, assistant_id, member_id)``task_type` 不同且 `status = 'active'` 的任务, THE Task_Generator SHALL 将旧任务状态设为 `inactive`,创建新任务,并在 `coach_task_history` 中记录变更
8. THE Task_Generator SHALL 按优先级从高到低的顺序处理任务类型:`high_priority_recall`0> `priority_recall`0> `follow_up_visit`1> `relationship_building`2高优先级任务覆盖低优先级任务
9. THE Task_Generator SHALL 通过 `auth.user_assistant_binding` 确定助教与小程序用户的映射关系,仅为已绑定的助教生成任务
10. THE Task_Generator SHALL 在 `trigger_jobs` 中更新 `last_run_at``next_run_at` 时间戳
### 需求 448 小时回访滞留机制
**用户故事:** 作为系统,回访任务至少保留 48 小时,到期后自动失效。
#### 验收标准
1. WHEN Task_Generator 生成 `follow_up_visit` 类型任务时, THE Task_Generator SHALL 将 `expires_at` 设为 NULL无限期有效`status` 设为 `active`
2. WHEN Task_Generator 检测到某 `follow_up_visit` 任务的触发条件不再满足(指数变化), THE Task_Generator SHALL 将该任务的 `expires_at` 填充为 `created_at + 48 小时``status` 保持 `active`
3. WHEN Task_Expiry_Checker 轮询检查时发现某任务的 `expires_at` 不为 NULL 且当前时间超过 `expires_at`, THE Task_Expiry_Checker SHALL 将该任务 `status` 设为 `inactive`
4. WHEN 新的 `follow_up_visit` 任务生成时发现同一 `(site_id, assistant_id, member_id)` 已存在一个有 `expires_at``follow_up_visit` 任务, THE Task_Generator SHALL 将旧任务标记为 `inactive`,创建新的 `active` 任务(`expires_at` 为 NULL
5. THE Task_Expiry_Checker SHALL 每小时运行一次,由 `trigger_jobs` 中的 `task_expiry_check` 配置驱动
### 需求 5任务类型变更与状态流转
**用户故事:** 作为系统,当客户指数变化导致任务类型变更时,系统正确关闭旧任务并创建新任务。
#### 验收标准
1. WHEN 任务类型从 `priority_recall` 变更为 `high_priority_recall`, THE Task_Generator SHALL 将旧 `priority_recall` 任务标记为 `inactive``expires_at` 保持 NULL创建新的 `high_priority_recall` 任务
2. WHEN 任务类型从 `follow_up_visit` 变更为 `high_priority_recall``priority_recall`, THE Task_Generator SHALL 将旧 `follow_up_visit` 任务标记为 `active` 并填充 `expires_at = created_at + 48 小时`,创建新的召回任务
3. WHEN 任务类型从召回类型变回 `follow_up_visit`, THE Task_Generator SHALL 检查是否存在有 `expires_at` 的旧 `follow_up_visit` 任务,若存在则将旧任务标记为 `inactive`,创建新的 `follow_up_visit` 任务
4. THE Task_Manager SHALL 在每次状态变更时在 `coach_task_history` 中记录 `action``old_status``new_status``old_task_type``new_task_type`
### 需求 6召回完成检测
**用户故事:** 作为助教,我完成召回任务后(客户到店被服务),系统自动标记任务完成。
#### 验收标准
1. WHEN ETL 数据更新后, THE Recall_Completion_Detector SHALL 通过 FDW 读取 `fdw_etl.dwd_assistant_service_log` 中的新增服务记录
2. WHEN 发现某助教为某客户提供了服务, THE Recall_Completion_Detector SHALL 查找该 `(site_id, assistant_id, member_id)` 下所有 `status = 'active'` 的任务
3. WHEN 匹配到活跃任务, THE Recall_Completion_Detector SHALL 将任务 `status` 设为 `completed`,记录 `completed_at` 为服务时间,记录 `completed_task_type` 为完成时的任务类型
4. WHEN 召回完成后, THE Recall_Completion_Detector SHALL 触发 `note_reclassify_backfill` 事件,通知 Note_Reclassifier 执行备注回溯
5. THE Recall_Completion_Detector SHALL 由 `trigger_jobs` 中的 `recall_completion_check` 配置驱动,在 ETL 数据更新事件后触发
### 需求 7备注回溯重分类
**用户故事:** 作为系统,当 ETL 数据延迟导致召回完成晚于备注提交时,需要回溯重分类备注。
#### 验收标准
1. WHEN 召回完成事件触发后, THE Note_Reclassifier SHALL 查找该 `(site_id, assistant_id, member_id)` 在召回服务结束时间之后提交的第一条 `type = 'normal'` 的备注
2. WHEN 找到符合条件的普通备注, THE Note_Reclassifier SHALL 将该备注的 `type``normal` 更新为 `follow_up`
3. WHEN 备注重分类完成后, THE Note_Reclassifier SHALL 触发 AI 应用 6 对该备注进行含金量评分(评分逻辑由 P5 AI 集成层实现,本 SPEC 仅定义触发接口)
4. WHEN AI 应用 6 返回评分 >= 6, THE Note_Reclassifier SHALL 生成一条 `follow_up_visit` 任务并标记为 `completed`(回溯完成)
5. WHEN AI 应用 6 返回评分 < 6, THE Note_Reclassifier SHALL 生成一条 `follow_up_visit` 任务,`status``active`(回访未完成,需助教重新备注)
### 需求 8任务 CRUD API
**用户故事:** 作为助教,我可以查看任务列表、置顶/放弃任务、取消置顶/取消放弃。
#### 验收标准
1. WHEN 助教请求任务列表, THE Task_Manager SHALL 返回该助教在当前 `site_id` 下所有 `status = 'active'` 的任务,按 `is_pinned DESC, priority_score DESC, created_at ASC` 排序
2. WHEN 助教请求任务列表, THE Task_Manager SHALL 在每条任务中包含客户基本信息(通过 FDW 读取 `dim_member`、RS 指数(通过 FDW 读取 `dws_member_assistant_relation_index`)、爱心 icon 档位(💖>8.5 / 🧡>7 / 💛>5 / 💙<5
3. WHEN 助教置顶某任务, THE Task_Manager SHALL 将该任务的 `is_pinned` 设为 TRUE并在 `coach_task_history` 中记录
4. WHEN 助教放弃某任务, THE Task_Manager SHALL 将该任务 `status` 设为 `abandoned`,记录 `abandon_reason`(必填),并在 `coach_task_history` 中记录
5. WHEN 助教取消置顶某任务, THE Task_Manager SHALL 将该任务的 `is_pinned` 设为 FALSE
6. WHEN 助教取消放弃某任务, THE Task_Manager SHALL 将该任务 `status` 恢复为 `active`,清空 `abandon_reason`
7. IF 助教放弃任务时未提供 `abandon_reason`, THEN THE Task_Manager SHALL 返回 HTTP 422 错误
8. THE Task_Manager SHALL 通过 Permission_Middleware 验证用户身份,仅允许操作自己的任务
### 需求 9备注 CRUD API
**用户故事:** 作为助教,我给客户添加备注后,系统正确存储备注内容和星星评分。
#### 验收标准
1. WHEN 助教创建备注时, THE Note_Service SHALL 在 `biz.notes` 表中创建记录,包含 `site_id``user_id``target_type`'member')、`target_id`member_id`type``content`、可选的 `rating_service_willingness`1-5、可选的 `rating_revisit_likelihood`1-5、可选的 `task_id`
2. WHEN 备注关联的任务类型为 `follow_up_visit`, THE Note_Service SHALL 将备注 `type` 自动设为 `follow_up`
3. WHEN 备注关联的任务类型不是 `follow_up_visit`, THE Note_Service SHALL 将备注 `type` 设为 `normal`
4. WHEN 备注创建成功且 `type = 'follow_up'`, THE Note_Service SHALL 触发 AI 应用 6 备注分析接口(由 P5 实现),传入备注内容和客户信息
5. WHEN AI 应用 6 返回评分 >= 6 且备注关联的 `follow_up_visit` 任务 `status = 'active'`, THE Note_Service SHALL 将该任务标记为 `completed`
6. WHEN 助教查询某客户的备注列表, THE Note_Service SHALL 返回该客户在当前 `site_id` 下的所有备注,按 `created_at DESC` 排序,包含星星评分和 AI 评分
7. WHEN 助教删除备注, THE Note_Service SHALL 执行软删除或硬删除(根据业务需要),删除前需二次确认(前端实现)
8. IF 星星评分值不在 1-5 范围内, THEN THE Note_Service SHALL 返回 HTTP 422 错误
9. THE Note_Service 的星星评分 SHALL 不参与回访完成判定(完成判定仅看 AI 应用 6 评分 >= 6不参与 AI 应用 6 分析,仅作辅助数据存储
### 需求 10触发器调度框架
**用户故事:** 作为系统,我需要一个统一的触发器调度框架,支持定时、间隔、事件驱动三种触发方式。
#### 验收标准
1. THE Trigger_Scheduler SHALL 支持 `cron` 类型触发器,按 cron 表达式计算下次运行时间
2. THE Trigger_Scheduler SHALL 支持 `interval` 类型触发器,按固定间隔秒数计算下次运行时间
3. THE Trigger_Scheduler SHALL 支持 `event` 类型触发器,在指定事件发生时立即执行
4. WHEN 触发器执行完成, THE Trigger_Scheduler SHALL 更新 `trigger_jobs` 表中的 `last_run_at``next_run_at`
5. WHEN 触发器 `status = 'disabled'`, THE Trigger_Scheduler SHALL 跳过该触发器不执行
6. THE Trigger_Scheduler SHALL 提供 `fire_event(event_name, payload)` 方法,用于触发事件驱动型任务
7. IF 触发器执行过程中发生错误, THEN THE Trigger_Scheduler SHALL 记录错误日志但不中断其他触发器的执行
### 需求 11迁移脚本管理
**用户故事:** 作为后端开发者,我需要所有数据库变更都有对应的迁移脚本,以便变更可追溯、可重放。
#### 验收标准
1. THE Migration_Script SHALL 将所有业务表的 DDL 存放在 `db/zqyy_app/migrations/` 目录中
2. THE Migration_Script SHALL 使用日期前缀命名(格式:`YYYY-MM-DD__<描述>.sql`
3. THE Migration_Script SHALL 使用 UTF-8 编码,纯 SQL非 ORM
4. THE Migration_Script SHALL 在每个脚本中包含回滚语句(以注释形式)
5. THE Migration_Script SHALL 使用幂等语法(`IF NOT EXISTS``ON CONFLICT DO NOTHING`),确保重复执行不会报错
### 需求 12DDL 测试库落库与文档同步
**用户故事:** 作为后端开发者,我需要所有 DDL 变更在测试库中实际执行验证,并同步更新数据库手册和 DDL 基线。
#### 验收标准
1. WHEN 迁移脚本编写完成, THE Task_Manager SHALL 在 `test_zqyy_app` 测试库中执行迁移脚本,验证无错误
2. WHEN 迁移脚本执行成功, THE Task_Manager SHALL 创建或更新 `docs/database/BD_Manual_biz_tables.md` 数据库手册,包含变更说明、兼容性影响、回滚策略、验证 SQL至少 3 条)
3. WHEN 迁移脚本执行成功, THE Task_Manager SHALL 运行 `python scripts/ops/gen_consolidated_ddl.py` 重新生成 DDL 基线文件
4. WHEN 种子数据脚本执行成功, THE Task_Manager SHALL 在数据库手册中记录种子数据内容(触发器配置)
### 需求 13小程序前端页面原型还原强制
**用户故事:** 作为产品经理,我需要小程序前端页面严格忠于 `docs/h5_ui/pages/` 中的 H5 原型图结构和视觉细节,确保最终实现与设计稿高度一致。
#### 原型图索引
| 原型文件 | 对应小程序页面 | 说明 |
|---------|--------------|------|
| `docs/h5_ui/pages/task-list.html` | `pages/task-list/task-list` | 任务列表页(首页),含业绩进度卡片、置顶/一般/已放弃三区域 |
| `docs/h5_ui/pages/task-detail.html` | `pages/task-detail/task-detail` | 任务详情页 - 高优先召回theme-red Banner |
| `docs/h5_ui/pages/task-detail-priority.html` | `pages/task-detail/task-detail` | 任务详情页 - 优先召回theme-orange Banner |
| `docs/h5_ui/pages/task-detail-relationship.html` | `pages/task-detail/task-detail` | 任务详情页 - 关系构建theme-pink Banner |
| `docs/h5_ui/pages/task-detail-callback.html` | `pages/task-detail/task-detail` | 任务详情页 - 客户回访theme-teal Banner |
| `docs/h5_ui/pages/notes.html` | `pages/notes/notes` | 备注记录页 |
| `docs/h5_ui/pages/customer-detail.html` | `pages/customer-detail/customer-detail` | 客户详情页 |
#### 验收标准
##### 13.A 结构还原(强制)
1. WHEN 实现任务列表页时, THE 小程序页面 SHALL 严格还原原型图中的以下结构层次:顶部用户信息区(头像 + 姓名 + 角色标签 + 门店名)→ 业绩进度卡片5 段档位进度条 + 课时数据含红戳 + 奖金激励 + 预计收入)→ 任务列表区(📌 置顶区 / 一般任务区 / 已放弃区三个分区,每个分区有标签 + 计数)
2. WHEN 实现任务卡片时, THE 每张任务卡片 SHALL 包含原型图中的全部元素:左侧 4px 彩色边框(高优先=红、优先=橙、关系构建=粉、客户回访=青)、任务类型标签(渐变色圆角矩形)、客户姓名、爱心 icon💖/🧡/💛/💙)、备注指示器(📝)、描述行(最近到店 + 余额、AI 建议行(含 AI 机器人 icon、右侧箭头
3. WHEN 实现任务详情页时, THE 页面 SHALL 严格还原原型图中的以下模块顺序:通栏 Banner导航栏 + 客户信息 + 放弃按钮)→ 维客线索卡片(客户基础/消费习惯/玩法偏好/重要反馈,每条含大类标签 + 摘要 + 详情 + 来源标注)→ 与我的关系卡片(爱心档位标签 + 进度条 + RS 分数 + 描述 + 近期服务记录列表)→ 任务建议卡片(建议执行 + 话术参考含复制按钮)→ 我给 TA 的备注卡片(备注列表含星星评分 + 删除按钮)→ 底部操作栏(问问助手 + 备注两个按钮)
4. WHEN 实现备注弹窗时, THE 弹窗 SHALL 包含原型图中的全部元素:标题行(添加备注 + 展开评价按钮)、可折叠的星星评分区(再次服务意愿 1-5 星 + 再来店可能性 1-5 星,各含文字提示)、文本输入区、保存按钮
5. WHEN 实现长按上下文菜单时, THE 菜单 SHALL 还原原型图中的交互:遮罩层 + 圆角菜单面板(置顶/取消置顶、备注、放弃/取消放弃等选项)
6. WHEN 实现备注记录页时, THE 页面 SHALL 还原原型图中的列表结构:每条备注含内容文本 + 底部标签(助教/客户类型标签 + 时间戳)
##### 13.B 视觉还原(强制)
1. THE 小程序页面 SHALL 使用与原型图一致的 TDesign 色彩体系primary=#0052d9、success=#00a870、warning=#ed7b2f、error=#e34d59,灰阶色板 gray-1(#f3f3f3) 至 gray-13(#242424)
2. THE 任务详情页 Banner SHALL 根据任务类型使用不同主题色:高优先召回=theme-red、优先召回=theme-orange、关系构建=theme-pink、客户回访=theme-teal与原型图中的渐变背景一致
3. THE 维客线索大类标签 SHALL 使用原型图中的配色方案:客户基础=primary/10 底色 + primary 文字、消费习惯=success/10 底色 + success 文字、玩法偏好=purple-500/10 底色 + purple-600 文字、重要反馈=error/10 底色 + error 文字
4. THE 星星评分组件 SHALL 还原原型图中的视觉效果:填充星/空心星 SVG、支持半星显示用于展示 AI 评分映射)
5. THE 业绩进度卡片 SHALL 还原原型图中的 5 段档位进度条按比例宽度0-100 占 45.45%、100-130/130-160/160-190/190-220 各占 13.64%)、红戳动画(盖戳效果)、奖金金额突出样式
##### 13.C WXML/WXSS 技术规范(强制)
1. THE 小程序页面 SHALL 使用 WXML 语法而非 HTML 语法:`<view>` 替代 `<div>``<text>` 替代 `<span>`/`<p>``<image>` 替代 `<img>``<navigator>` 替代 `<a>`,禁止使用 HTML 标签
2. THE 小程序样式 SHALL 使用 WXSS 语法:使用 `rpx` 单位替代 `px`750rpx = 屏幕宽度)、使用 `@import` 导入公共样式、禁止使用 `rem`/`em`/`vw`/`vh` 等 CSS 单位
3. THE 小程序页面 SHALL 使用 `wx:for` 替代 JavaScript 循环渲染、`wx:if`/`wx:elif`/`wx:else` 替代条件渲染、`bind:tap` 替代 `onclick``data-*` + `e.currentTarget.dataset` 替代 DOM 操作
4. THE 小程序页面 SHALL 禁止使用以下 Web 特性:`document.*``window.*``localStorage`(用 `wx.setStorageSync`)、`fetch`/`XMLHttpRequest`(用 `wx.request`、CSS `position: fixed``bottom: 0` 底部栏(用小程序安全区域适配)
5. THE 小程序样式 SHALL 仅使用小程序支持的 CSS 选择器:`.class``#id``element``element, element``::after``::before`,禁止使用 `>`(子选择器)、`+`(相邻兄弟)、`~`(通用兄弟)、`[attr]`(属性选择器)等不支持的选择器
6. THE 小程序页面 SHALL 使用 `<block>` 标签作为无渲染包裹容器(替代 HTML 的 `<template>` 或 React 的 `<Fragment>``<block>` 不会生成真实 DOM 节点
##### 13.D TDesign 组件使用规范(强制)
1. THE 小程序页面 SHALL 优先使用 TDesign 组件库中的组件,组件引入路径格式为 `tdesign-miniprogram/{组件名}/{组件名}`,在页面 `.json``usingComponents` 中注册
2. THE 以下 UI 元素 SHALL 使用对应的 TDesign 组件:导航栏→`t-navbar`、底部标签栏→`t-tab-bar`、对话框→`t-dialog`、轻提示→`t-toast`、弹出层→`t-popup`、空状态→`t-empty`、加载→`t-loading`、骨架屏→`t-skeleton`、标签→`t-tag`、搜索框→`t-search`
3. THE TDesign 组件样式覆盖 SHALL 使用以下 4 种方式之一:`style`/`custom-style` 属性、解除样式隔离(`addGlobalClass`)、外部样式类(`t-class`、CSS 变量(`--td-*`),禁止直接修改 `node_modules` 中的组件源码
4. THE 小程序 `app.json` SHALL 移除 `"style": "v2"` 配置项,避免 TDesign 组件样式错乱
5. WHEN 原型图中的 UI 元素无法用 TDesign 组件直接实现时(如自定义进度条、红戳动画、话术气泡等), THE 开发者 SHALL 使用原生 WXML + WXSS 自定义实现,但视觉效果必须与原型图一致
##### 13.E 原型图参考流程(强制)
1. WHEN 开始实现任何小程序页面前, THE 开发者 SHALL 首先阅读对应的 `docs/h5_ui/pages/*.html` 原型文件,提取页面结构、组件层次、样式细节、交互行为
2. WHEN 原型图中使用 Tailwind CSS 类名时, THE 开发者 SHALL 将其转换为等效的 WXSS 样式(如 `px-4``padding: 0 32rpx``rounded-xl``border-radius: 24rpx``text-sm``font-size: 28rpx`
3. WHEN 原型图中使用 `<iframe>` 嵌套页面时, THE 开发者 SHALL 理解这是原型展示方式,实际小程序中使用 `wx.navigateTo` 页面跳转
4. WHEN 原型图中使用 `onclick`/`history.back()` 等 Web API 时, THE 开发者 SHALL 转换为小程序等效 API`bind:tap` + `wx.navigateBack()`
5. THE 开发者 SHALL 在实现前加载 `wechat-miniprogram` Power 的相关 steering 文件(`view-layer.md``tdesign.md``builtin-components.md`),确保使用正确的小程序语法和 TDesign 组件规范
6. THE 开发者 SHALL 在实现前阅读项目内的 H5 转小程序避坑指南 `apps/miniprogram/doc/h5-to-miniprogram-pitfalls.md`该文档基于本项目已转换页面的实际踩坑经验整理涵盖标签映射、rpx 换算、事件系统、TDesign 覆盖方式、高频踩坑清单及新页面开发 Checklist所有条目具有强制参考效力
### 需求 14任务系统属性测试
**用户故事:** 作为后端开发者,我需要通过属性测试验证任务系统核心逻辑的正确性。
#### 验收标准
1. THE 属性测试 SHALL 验证:对于任意 `(site_id, assistant_id, member_id, task_type)` 组合,`status = 'active'` 的任务最多只有一条(唯一性不变量)
2. THE 属性测试 SHALL 验证:对于任意任务类型变更操作,旧任务被标记为 `inactive` 且新任务被创建为 `active`(状态机正确性)
3. THE 属性测试 SHALL 验证:对于任意 `follow_up_visit` 任务,当 `expires_at` 不为 NULL 且当前时间超过 `expires_at` 时,轮询后 `status` 变为 `inactive`(有效期机制)
4. THE 属性测试 SHALL 验证:对于任意任务放弃操作,`abandon_reason` 不为空字符串(放弃原因必填)
5. THE 属性测试 SHALL 验证:对于任意备注创建操作,`rating_service_willingness``rating_revisit_likelihood` 的值在 NULL 或 1-5 范围内(评分范围约束)
6. THE 属性测试 SHALL 验证:对于任意召回完成事件,`completed_task_type` 记录了完成时的任务类型快照(完成类型快照不变量)
7. THE 属性测试 SHALL 验证:对于任意备注回溯操作,重分类后的备注 `type``normal` 变为 `follow_up`(回溯分类正确性)
---
## 附录:原型还原强制规则摘要
> 以下规则适用于本 SPEC 及所有后续小程序页面开发 SPEC具有全局约束力。
1. **原型图是唯一视觉真相**`docs/h5_ui/pages/*.html` 中的结构、层次、元素、配色、间距、交互行为是小程序页面实现的唯一参考标准。任何偏离原型图的实现都需要明确的产品确认。
2. **WXML ≠ HTML**:严禁在小程序中使用 HTML 标签div/span/p/a/img 等必须使用小程序原生标签view/text/image/navigator 等)。
3. **WXSS ≠ CSS**:使用 rpx 单位、仅支持有限选择器、无 DOM/BOM API、样式隔离机制不同。Tailwind CSS 类名必须手动转换为 WXSS。
4. **TDesign 优先**:凡 TDesign 组件库能覆盖的 UI 元素,必须使用 TDesign 组件;自定义实现仅限 TDesign 无法覆盖的场景。
5. **Power 文档优先**:实现前必须加载 `wechat-miniprogram` Power 的相关 steering 文件,确保语法和组件用法正确。
6. **项目踩坑指南必读**:实现前必须阅读 `apps/miniprogram/doc/h5-to-miniprogram-pitfalls.md`,该文档是基于本项目实际转换经验的避坑手册,涵盖 WXML/WXSS 差异、事件系统、TDesign 用法、rpx 换算规则及新页面开发 Checklist。

View File

@@ -0,0 +1,239 @@
# 实现计划小程序核心业务模块miniapp-core-business
## 概述
基于已批准的需求和设计文档,将小程序核心业务模块拆分为增量式编码任务。按照"DDL 建表 → 触发器调度框架 → 任务生成器 → 任务管理 → 有效期轮询 → 召回检测 → 备注系统 → 路由集成"的顺序实现。后端使用 Python + FastAPI数据库使用 PostgreSQL 纯 SQL属性测试使用 hypothesis。所有数据库操作在测试库 `test_zqyy_app` 中进行。
## 任务
- [x] 1. 创建业务数据表和种子数据
- [x] 1.1 创建迁移脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p4_create_biz_tables.sql`
-`biz` Schema 下创建 `coach_tasks``coach_task_history``notes``trigger_jobs` 共 4 张表
- 包含所有字段定义、约束、CHECK 约束(评分 1-5、外键coach_task_history → coach_tasks、notes → coach_tasks、coach_tasks → coach_tasks 自引用)
- 创建部分唯一索引 `idx_coach_tasks_site_assistant_member_type`(仅 status='active'
- 创建查询索引 `idx_coach_tasks_assistant_status``idx_notes_target`
- 使用 `IF NOT EXISTS` 幂等语法
- 包含回滚语句(注释形式)
- _Requirements: 1.1-1.9_
- [x] 1.2 创建种子数据脚本 `db/zqyy_app/migrations/YYYY-MM-DD__p4_seed_trigger_jobs.sql`
- 插入 4 条触发器配置:`task_generator`cron, 0 4 * * *)、`task_expiry_check`interval, 3600s`recall_completion_check`event, etl_data_updated`note_reclassify_backfill`event, recall_completed
- 使用 `ON CONFLICT (job_name) DO NOTHING` 幂等语法
- _Requirements: 2.1-2.5_
- [x] 1.3 在测试库执行迁移脚本并验证
-`test_zqyy_app` 中执行建表脚本和种子数据脚本
- 验证幂等性:连续执行两次无错误
- 验证表结构、约束、索引正确
- 验证种子数据完整4 条触发器配置)
- _Requirements: 11.1-11.5, 12.1_
- [x] 1.4 更新数据库手册和 DDL 基线
- 创建 `docs/database/BD_Manual_biz_tables.md`,包含变更说明、兼容性影响、回滚策略、验证 SQL至少 3 条)
- 运行 `python scripts/ops/gen_consolidated_ddl.py` 刷新 DDL 基线
- 在数据库手册中记录种子数据内容(触发器配置)
- _Requirements: 12.2, 12.3, 12.4_
- [x] 1.5 编写迁移脚本幂等性属性测试
- **Property 13: 迁移脚本幂等性**
- 对 DDL 脚本和种子数据脚本连续执行两次,验证第二次执行无错误且数据库状态不变
- **Validates: Requirements 1.8, 2.5, 11.4, 11.5**
- [x] 2. 检查点 - 确保 DDL 和种子数据正确
- 确保迁移脚本在测试库中执行成功,幂等性验证通过,如有问题请向用户确认。
- [x] 3. 实现 Pydantic 模型和纯函数核心逻辑
- [x] 3.1 创建 Pydantic 模型 `apps/backend/app/schemas/xcx_tasks.py`
- 定义 `TaskListItem`(含 member_name、member_phone、rs_score、heart_icon
- 定义 `AbandonRequest`reason 必填min_length=1
- _Requirements: 8.1, 8.2, 8.4, 8.7_
- [x] 3.2 创建 Pydantic 模型 `apps/backend/app/schemas/xcx_notes.py`
- 定义 `NoteCreateRequest`(含 target_type、target_id、content、task_id、rating_service_willingness、rating_revisit_likelihood评分 ge=1 le=5
- 定义 `NoteOut`(含 type、content、评分、ai_score、ai_analysis
- _Requirements: 9.1, 9.8_
- [x] 3.3 创建任务生成器核心纯函数 `apps/backend/app/services/task_generator.py`
- 定义 `TaskPriority` 枚举、`TASK_TYPE_PRIORITY` 映射
- 定义 `IndexData` 数据类
- 实现 `determine_task_type(index_data)` 纯函数:根据 WBI/NCI/RS 指数确定任务类型
- 实现 `should_replace_task(existing_type, new_type)` 纯函数:判断是否应替换现有任务
- 实现 `compute_heart_icon(rs_score)` 纯函数:根据 RS 指数计算爱心 icon 档位
- _Requirements: 3.1-3.5, 3.8, 8.2_
- [x] 3.4 编写任务类型确定正确性属性测试
- **Property 1: 任务类型确定正确性**
- 生成随机 WBI/NCI/RS 值Decimal, 0-10, 2 位小数),验证 `determine_task_type()` 返回值符合优先级规则
- **Validates: Requirements 3.1, 3.2, 3.3, 3.5**
- [x] 3.5 编写星星评分范围约束属性测试
- **Property 9: 星星评分范围约束**
- 生成随机整数(-100 到 100验证 Pydantic 模型对 1-5 范围外的值拒绝ValidationError
- **Validates: Requirements 9.8, 14.5**
- [x] 3.6 编写爱心 icon 档位计算属性测试
- **Property 11: 爱心 icon 档位计算**
- 生成随机 RS 值Decimal, 0-10, 1 位小数),验证 `compute_heart_icon()` 返回正确 icon
- **Validates: Requirements 8.2**
- [x] 4. 实现触发器调度框架
- [x] 4.1 创建 `apps/backend/app/services/trigger_scheduler.py`
- 实现 `_JOB_REGISTRY` 注册表和 `register_job(job_type, handler)` 函数
- 实现 `fire_event(event_name, payload)` 方法:查找 event 类型触发器并执行
- 实现 `check_scheduled_jobs()` 方法:检查 cron/interval 到期 job 并执行
- 实现 `_calculate_next_run(trigger_condition, trigger_config)` 方法:计算下次运行时间
- 每个 job 独立事务,失败不影响其他触发器
- _Requirements: 10.1-10.7_
- [x] 4.2 编写触发器 next_run_at 计算属性测试
- **Property 12: 触发器 next_run_at 计算**
- 生成随机 cron/interval 配置和当前时间,验证 cron 类型 next_run_at > 当前时间interval 类型 next_run_at = 当前时间 + interval_seconds
- **Validates: Requirements 10.1, 10.2**
- [x] 5. 实现任务生成器完整流程
- [x] 5.1 实现 `TaskGenerator.run()` 主流程
- 通过 `auth.user_assistant_binding` 获取所有已绑定助教
- 对每个助教,通过 FDW 读取 WBI/NCI/RS 指数(`SET LOCAL app.current_site_id`
- 调用 `determine_task_type()` 确定任务类型
- 检查已存在的 active 任务:相同 task_type → 跳过;不同 task_type → 关闭旧任务 + 创建新任务 + 记录 history
- 处理 `follow_up_visit` 的 48 小时滞留机制expires_at 填充)
- 更新 `trigger_jobs` 时间戳
- _Requirements: 3.1-3.10, 4.1-4.5, 5.1-5.4_
- [x] 5.2 编写活跃任务唯一性不变量属性测试
- **Property 2: 活跃任务唯一性不变量**
- 生成随机 (site_id, assistant_id, member_id, task_type) 组合,模拟插入操作,验证 active 任务最多一条
- **Validates: Requirements 1.5, 3.6, 14.1**
- [x] 5.3 编写任务类型变更状态机属性测试
- **Property 3: 任务类型变更状态机**
- 生成随机现有任务 + 新任务类型,执行变更,验证旧任务 inactive + 新任务 active + history 记录
- **Validates: Requirements 3.7, 5.1, 5.4, 14.2**
- [x] 5.4 编写 48 小时滞留机制属性测试
- **Property 4: 48 小时滞留机制**
- 生成随机 follow_up_visit 任务 + 时间偏移,验证 expires_at 填充和过期逻辑
- **Validates: Requirements 4.1, 4.2, 4.3, 4.4, 14.3**
- [x] 6. 检查点 - 确保任务生成器测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_core_business_properties.py -v -k "property_1 or property_2 or property_3 or property_4"`
- 确保所有属性测试通过,如有问题请向用户确认。
- [x] 7. 实现任务管理服务
- [x] 7.1 创建 `apps/backend/app/services/task_manager.py`
- 实现 `get_task_list(user_id, site_id)` 异步方法:查询活跃任务 + FDW 读取客户信息和 RS 指数 + 爱心 icon 计算 + 排序
- 实现 `pin_task(task_id, user_id, site_id)` 异步方法:验证归属 + 设置 is_pinned=TRUE + 记录 history
- 实现 `unpin_task(task_id, user_id, site_id)` 异步方法:验证归属 + 设置 is_pinned=FALSE
- 实现 `abandon_task(task_id, user_id, site_id, reason)` 异步方法:验证 reason 非空 + 设置 abandoned + 记录 history
- 实现 `cancel_abandon(task_id, user_id, site_id)` 异步方法:恢复 active + 清空 abandon_reason + 记录 history
- 实现 `_record_history()` 内部方法
- _Requirements: 8.1-8.8_
- [x] 7.2 编写放弃与取消放弃往返属性测试
- **Property 5: 放弃与取消放弃往返**
- 生成随机 active 任务 + 非空放弃原因,执行放弃→取消放弃,验证状态恢复;空原因应返回 422
- **Validates: Requirements 8.4, 8.6, 8.7, 14.4**
- [x] 7.3 编写任务列表排序正确性属性测试
- **Property 10: 任务列表排序正确性**
- 生成随机任务列表(不同 is_pinned/priority_score/created_at验证排序为 is_pinned DESC, priority_score DESC, created_at ASC
- **Validates: Requirements 8.1**
- [x] 7.4 编写状态变更历史完整性属性测试
- **Property 15: 状态变更历史完整性**
- 生成随机状态变更操作序列(置顶/放弃/取消放弃),验证 history 记录数量和内容正确
- **Validates: Requirements 5.4, 8.3**
- [x] 8. 实现有效期轮询器
- [x] 8.1 创建 `apps/backend/app/services/task_expiry.py`
- 实现 `run()` 方法:查询 expires_at 不为 NULL 且已过期的 active 任务,标记为 inactive记录 history
- _Requirements: 4.3, 4.5_
- [x] 9. 实现召回完成检测器
- [x] 9.1 创建 `apps/backend/app/services/recall_detector.py`
- 实现 `run(payload)` 方法:通过 FDW 读取新增服务记录,匹配 active 任务标记 completed记录 completed_at 和 completed_task_type 快照,触发 `recall_completed` 事件
- _Requirements: 6.1-6.5_
- [x] 9.2 编写召回完成检测与类型快照属性测试
- **Property 6: 召回完成检测与类型快照**
- 生成随机 active 任务 + 服务记录,执行完成检测,验证 completed_task_type 记录了完成时的 task_type 快照
- **Validates: Requirements 6.2, 6.3, 14.6**
- [x] 10. 检查点 - 确保任务管理和召回检测测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_core_business_properties.py -v -k "property_5 or property_6 or property_10 or property_15"`
- 确保所有属性测试通过,如有问题请向用户确认。
- [-] 11. 实现备注系统
- [x] 11.1 创建备注服务 `apps/backend/app/services/note_service.py`
- 实现 `create_note()` 异步方法:验证评分范围 + 确定 note type关联 follow_up_visit 任务 → follow_up否则 normal+ INSERT + 触发 AI 应用 6 接口(占位)+ 若 ai_score >= 6 标记任务 completed
- 实现 `get_notes()` 异步方法:按 created_at DESC 排序,包含评分和 AI 评分
- 实现 `delete_note()` 异步方法:验证归属后硬删除
- _Requirements: 9.1-9.9_
- [x] 11.2 创建备注回溯重分类器 `apps/backend/app/services/note_reclassifier.py`
- 实现 `run(payload)` 方法:查找 service_time 之后的第一条 normal 备注 → 更新为 follow_up → 触发 AI 应用 6 接口(占位)→ 根据 ai_score 生成 follow_up_visit 任务
- 实现 `ai_analyze_note(note_id)` 占位函数(返回 NoneP5 实现后替换)
- _Requirements: 7.1-7.5_
- [x] 11.3 编写备注回溯重分类属性测试
- **Property 7: 备注回溯重分类**
- 生成随机备注列表 + service_time执行回溯验证符合条件的 normal 备注 type 变为 follow_up
- **Validates: Requirements 7.1, 7.2, 14.7**
- [x] 11.4 编写备注类型自动设置属性测试
- **Property 8: 备注类型自动设置**
- 生成随机 task_type + 备注创建,验证关联 follow_up_visit → type=follow_up其他 → type=normal
- **Validates: Requirements 9.2, 9.3**
- [x] 11.5 编写 AI 评分驱动的任务完成判定属性测试
- **Property 14: AI 评分驱动的任务完成判定**
- 生成随机 ai_score + 任务状态,验证 ai_score >= 6 且 active → completedai_score < 6 → 保持 active
- **Validates: Requirements 7.4, 7.5, 9.5**
- [x] 12. 实现 API 路由层
- [x] 12.1 创建小程序任务路由 `apps/backend/app/routers/xcx_tasks.py`
- 实现 `GET /api/xcx/tasks`获取任务列表require_approved
- 实现 `POST /api/xcx/tasks/{id}/pin`:置顶任务
- 实现 `POST /api/xcx/tasks/{id}/unpin`:取消置顶
- 实现 `POST /api/xcx/tasks/{id}/abandon`放弃任务AbandonRequest 校验)
- 实现 `POST /api/xcx/tasks/{id}/cancel-abandon`:取消放弃
- _Requirements: 8.1-8.8_
- [x] 12.2 创建小程序备注路由 `apps/backend/app/routers/xcx_notes.py`
- 实现 `POST /api/xcx/notes`创建备注NoteCreateRequest 校验)
- 实现 `GET /api/xcx/notes`查询备注列表query: target_type, target_id
- 实现 `DELETE /api/xcx/notes/{id}`:删除备注
- _Requirements: 9.1-9.9_
- [x] 12.3 在 `apps/backend/app/main.py` 中注册新路由
- 注册 `xcx_tasks``xcx_notes` 路由
- 验证无路由冲突
- _Requirements: 全部_
- [x] 12.4 注册触发器 job handler
- 在应用启动时调用 `register_job()` 注册 `task_generator``task_expiry_check``recall_completion_check``note_reclassify_backfill` 四个 handler
- _Requirements: 10.1-10.6_
- [x] 13. 检查点 - 确保所有测试通过
- 运行属性测试:`cd C:\NeoZQYY && pytest tests/test_core_business_properties.py -v`
- 26/26 全部通过16.81s
- [x] 14. 最终检查点 - 全量验证
- 运行全部属性测试26/26 通过16.81s
- 验证迁移脚本幂等性Property 133 个测试)通过
- 验证种子数据完整性4 条触发器配置全部存在
- 验证表结构coach_tasks / coach_task_history / notes / trigger_jobs 全部存在
- 验证部分唯一索引idx_coach_tasks_site_assistant_member_type 存在
## 备注
- 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯
- 检查点确保增量验证
- 属性测试验证通用正确性属性hypothesis最少 200 次迭代)
- 所有数据库操作在测试库 `test_zqyy_app` 进行
- 迁移脚本放在 `db/zqyy_app/migrations/` 目录
- 属性测试放在 `tests/test_core_business_properties.py`Monorepo 级)
- AI 应用 6 接口为占位实现(返回 None由 P5 AI 集成层替换
- 维客线索功能由独立模块 `routers/member_retention_clue.py` 处理,不在本 SPEC 范围内
- FDW 查询需在事务中 `SET LOCAL app.current_site_id` 设置 RLS 隔离

View File

@@ -0,0 +1 @@
{"specId": "cf5c24d6-ec72-4c49-8650-264ef414e10e", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,878 @@
# 设计文档P5 AI 集成层miniapp-ai-integration
## 概述
本设计文档描述 P5-A 阶段 AI 集成层的技术架构与实现方案。系统在现有 FastAPI 后端(`apps/backend/`)中新增 AI 模块,通过阿里云百炼 API通义千问为 8 个 AI 应用提供统一的调用能力。
核心交付物:
- 3 张新表(`biz.ai_conversations``biz.ai_messages``biz.ai_cache`
- 百炼 API 统一封装层(流式 + 非流式)
- 应用 1 SSE 流式对话端点
- 应用 2 财务洞察Prompt 完整)+ 应用 8 维客线索整理Prompt 完整)
- 应用 3/4/5/6/7 触发机制与调用骨架
- 事件调度与调用链编排
- AI 缓存读写 API
设计原则:
- **统一封装**:所有 AI 调用经 `BailianClient` 统一出口,便于重试、计量、日志
- **事件驱动**:复用现有 `trigger_scheduler.fire_event()` 机制,扩展支持串行调用链
- **骨架优先**P5-A 只实现管道和框架Prompt 细化留给 P5-B 阶段
- **site_id 隔离**:所有表和查询强制 site_id 过滤
## 架构
### 系统架构图
```mermaid
graph TB
subgraph 微信小程序
MP_CHAT[对话页面]
MP_PAGES[其他页面<br/>财务看板/任务详情/客户详情]
end
subgraph FastAPI 后端
subgraph AI 模块 - apps/backend/app/ai/
SSE[SSE 端点<br/>/api/ai/chat/stream]
CACHE_API[缓存 API<br/>/api/ai/cache]
HISTORY_API[历史对话 API<br/>/api/ai/conversations]
DISPATCHER[AI Event Dispatcher<br/>调用链编排]
BAILIAN[BailianClient<br/>百炼 API 封装]
end
subgraph 现有服务
NOTE_SVC[note_service<br/>备注服务]
TRIGGER[trigger_scheduler<br/>触发器调度]
TASK_GEN[task_generator<br/>任务生成]
end
end
subgraph 外部服务
BAILIAN_API[阿里云百炼 API<br/>通义千问]
end
subgraph PostgreSQL - zqyy_app
AI_CONV[biz.ai_conversations]
AI_MSG[biz.ai_messages]
AI_CACHE_T[biz.ai_cache]
CLUE_T[member_retention_clue]
end
MP_CHAT -->|SSE| SSE
MP_PAGES -->|REST| CACHE_API
MP_CHAT -->|REST| HISTORY_API
SSE --> BAILIAN
DISPATCHER --> BAILIAN
BAILIAN -->|HTTP/SSE| BAILIAN_API
NOTE_SVC -->|备注提交事件| DISPATCHER
TRIGGER -->|消费/任务分配事件| DISPATCHER
DISPATCHER -->|写入| AI_CONV
DISPATCHER -->|写入| AI_MSG
DISPATCHER -->|写入| AI_CACHE_T
DISPATCHER -->|全量替换 AI 线索| CLUE_T
SSE -->|写入| AI_CONV
SSE -->|写入| AI_MSG
```
### 事件调用链
```mermaid
sequenceDiagram
participant E as 业务事件
participant D as AI Dispatcher
participant A3 as App3 线索
participant A8 as App8 整理
participant A7 as App7 客户分析
participant A4 as App4 关系分析
participant A5 as App5 话术
Note over E,A5: 消费事件链(无助教)
E->>D: consumption_event(member_id, site_id)
D->>A3: 调用(串行)
A3-->>D: 线索结果 → ai_cache
D->>A8: 调用(串行)
A8-->>D: 整合线索 → ai_cache + member_retention_clue
D->>A7: 调用(串行)
A7-->>D: 客户分析 → ai_cache
Note over E,A5: 消费事件链(有助教)
E->>D: consumption_event(member_id, assistant_id, site_id)
D->>A3: 调用
A3-->>D: 线索结果
D->>A8: 调用
A8-->>D: 整合线索
D->>A7: 调用
D->>A4: 调用A8 完成后)
A4-->>D: 关系分析
D->>A5: 调用A4 完成后)
A5-->>D: 话术参考
Note over E,A5: 备注事件链
E->>D: note_event(member_id, note_id, site_id)
D->>+A6: 调用
Note right of A6: App6 备注分析
A6-->>-D: 线索 + 评分
D->>A8: 调用
A8-->>D: 整合线索
Note over E,A5: 任务分配事件链
E->>D: task_assign_event(assistant_id, member_id, site_id)
D->>A4: 调用(读已有 A8 缓存)
A4-->>D: 关系分析
D->>A5: 调用
A5-->>D: 话术参考
```
### 模块目录结构
```
apps/backend/app/ai/
├── __init__.py
├── bailian_client.py # 百炼 API 统一封装
├── dispatcher.py # AI 事件调度与调用链编排
├── cache_service.py # AI 缓存读写服务
├── conversation_service.py # 对话记录持久化服务
├── apps/
│ ├── __init__.py
│ ├── app1_chat.py # 应用 1 通用对话
│ ├── app2_finance.py # 应用 2 财务洞察
│ ├── app3_clue.py # 应用 3 客户数据维客线索
│ ├── app4_analysis.py # 应用 4 关系分析
│ ├── app5_tactics.py # 应用 5 话术参考
│ ├── app6_note.py # 应用 6 备注分析
│ ├── app7_customer.py # 应用 7 客户分析
│ └── app8_consolidation.py # 应用 8 维客线索整理
├── prompts/
│ ├── __init__.py
│ ├── app2_finance_prompt.py # 应用 2 完整 Prompt
│ └── app8_consolidation_prompt.py # 应用 8 完整 Prompt
└── schemas.py # Pydantic 模型
apps/backend/app/routers/
├── xcx_ai_chat.py # SSE 对话路由
└── xcx_ai_cache.py # 缓存查询路由
```
## 组件与接口
### 1. BailianClient百炼 API 统一封装)
文件:`apps/backend/app/ai/bailian_client.py`
技术方案(基于百炼官方文档):
- **流式调用**:使用 OpenAI 兼容接口(百炼支持 OpenAI SDK 协议),`stream=True` 返回 SSE 事件流
- **非流式调用**`stream=False`,返回完整 JSON 响应
- **JSON 输出模式**:通过 System Prompt 约束 + `response_format={"type": "json_object"}` 参数(百炼兼容 OpenAI 的 JSON mode
- **重试策略**:指数退避,最多 3 次,基础间隔 1s → 2s → 4s
- **SDK 选择**:使用 `openai` Python SDK百炼兼容 OpenAI 协议),`base_url` 指向百炼端点
```python
class BailianClient:
"""百炼 API 统一封装层。"""
def __init__(self, api_key: str, base_url: str, model: str):
"""
Args:
api_key: 百炼 API Key从 BAILIAN_API_KEY 环境变量读取)
base_url: 百炼 API 端点(从 BAILIAN_BASE_URL 环境变量读取)
model: 模型标识(如 qwen-plus
"""
async def chat_stream(
self,
messages: list[dict],
*,
temperature: float = 0.7,
max_tokens: int = 2000,
) -> AsyncGenerator[str, None]:
"""流式调用,逐 chunk 返回文本。用于应用 1 SSE。"""
async def chat_json(
self,
messages: list[dict],
*,
temperature: float = 0.3,
max_tokens: int = 4000,
) -> tuple[dict, int]:
"""非流式调用,返回解析后的 JSON dict 和 tokens_used。
用于应用 2-8。
Raises:
BailianJsonParseError: JSON 解析失败时抛出
BailianApiError: API 调用失败(重试耗尽后)
"""
def _inject_current_time(self, messages: list[dict]) -> list[dict]:
"""在首条消息的 content JSON 中注入 current_time 字段。"""
async def _call_with_retry(self, **kwargs) -> Any:
"""带指数退避的重试封装。"""
```
环境变量(新增到 `.env` / `.env.template`
```
BAILIAN_API_KEY=sk-xxx
BAILIAN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
BAILIAN_MODEL=qwen-plus
```
### 2. AI Event Dispatcher事件调度器
文件:`apps/backend/app/ai/dispatcher.py`
调度器负责根据业务事件编排 AI 应用调用链。与现有 `trigger_scheduler` 的关系:
- `trigger_scheduler.fire_event()` 触发业务事件 → 调用 `ai_dispatcher` 对应的 handler
- `ai_dispatcher` 内部管理串行调用链的执行顺序
```python
class AIDispatcher:
"""AI 应用调用链编排器。"""
async def handle_consumption_event(
self,
member_id: int,
site_id: int,
settle_id: int,
assistant_id: int | None = None,
) -> None:
"""消费事件链App3 → App8 → App7+ App4 → App5 如有助教)。"""
async def handle_note_event(
self,
member_id: int,
site_id: int,
note_id: int,
note_content: str,
noted_by_name: str,
) -> None:
"""备注事件链App6 → App8。"""
async def handle_task_assign_event(
self,
assistant_id: int,
member_id: int,
site_id: int,
task_type: str,
) -> None:
"""任务分配事件链App4 → App5读已有 App8 缓存)。"""
async def _run_chain(
self,
chain: list[Callable],
context: dict,
) -> None:
"""串行执行调用链,某步失败记录日志后继续。"""
```
容错策略:
- 调用链中某个应用失败 → 记录错误日志 + 写入 `ai_conversations`(标记失败)
- 后续应用使用已有缓存继续执行,不阻塞整条链
- 整条链在后台异步执行,不阻塞业务请求
### 3. AI Cache Service缓存读写服务
文件:`apps/backend/app/ai/cache_service.py`
```python
class AICacheService:
"""AI 缓存读写服务。"""
def get_latest(
self,
cache_type: str,
site_id: int,
target_id: str,
) -> dict | None:
"""查询最新缓存记录。"""
def get_history(
self,
cache_type: str,
site_id: int,
target_id: str,
limit: int = 2,
) -> list[dict]:
"""查询历史缓存记录(按 created_at DESC用于 Prompt reference。"""
def write_cache(
self,
cache_type: str,
site_id: int,
target_id: str,
result_json: dict,
triggered_by: str | None = None,
score: int | None = None,
expires_at: datetime | None = None,
) -> int:
"""写入缓存记录,返回 id。写入后异步清理超限记录。"""
def _cleanup_excess(
self,
cache_type: str,
site_id: int,
target_id: str,
max_count: int = 500,
) -> int:
"""清理超限记录,保留最近 max_count 条,返回删除数量。"""
```
### 4. Conversation Service对话记录持久化
文件:`apps/backend/app/ai/conversation_service.py`
```python
class ConversationService:
"""AI 对话记录持久化服务。"""
def create_conversation(
self,
user_id: int | str,
nickname: str,
app_id: str,
site_id: int,
source_page: str | None = None,
source_context: dict | None = None,
) -> int:
"""创建对话记录,返回 conversation_id。
系统自动调用时 user_id 为 'system'"""
def add_message(
self,
conversation_id: int,
role: str,
content: str,
tokens_used: int | None = None,
) -> int:
"""添加消息记录,返回 message_id。"""
def get_conversations(
self,
user_id: int,
site_id: int,
page: int = 1,
page_size: int = 20,
) -> list[dict]:
"""查询用户历史对话列表,按时间倒序,懒加载。"""
def get_messages(
self,
conversation_id: int,
) -> list[dict]:
"""查询对话的所有消息。"""
```
### 5. Clue Writer维客线索写入器
文件:集成在 `apps/backend/app/ai/apps/app8_consolidation.py`
```python
class ClueWriter:
"""维客线索全量替换写入器。"""
def replace_ai_clues(
self,
member_id: int,
site_id: int,
clues: list[dict],
) -> int:
"""全量替换该客户的 AI 来源线索。
1. DELETE FROM member_retention_clue
WHERE member_id = %s AND site_id = %s
AND source IN ('ai_consumption', 'ai_note')
2. INSERT 新线索(人工线索 source='manual' 不受影响)
字段映射:
- category → category
- emoji + summary → summary"📅 偏好周末下午时段消费"
- detail → detail
- providers → recorded_by_name
- source: 纯 App3 → ai_consumption纯 App6 → ai_note混合 → ai_consumption
- recorded_by_assistant_id: NULL系统触发
返回写入的线索数量。
"""
```
### 6. API 端点
#### 6.1 SSE 对话端点
路由文件:`apps/backend/app/routers/xcx_ai_chat.py`
```
POST /api/ai/chat/stream
Content-Type: application/json
Accept: text/event-stream
Request Body:
{
"message": "string",
"source_page": "string",
"page_context": {},
"screen_content": "string" // 页面可见内容文本化
}
SSE Events:
data: {"type": "chunk", "content": "..."}
data: {"type": "done", "conversation_id": 123, "tokens_used": 456}
data: {"type": "error", "message": "..."}
认证JWT Token从 xcx_auth 获取)
隔离:从 JWT 中提取 user_id、site_id、nickname、role
```
#### 6.2 历史对话 API
```
GET /api/ai/conversations?page=1&page_size=20
→ [{ id, app_id, source_page, created_at, first_message_preview }]
GET /api/ai/conversations/{conversation_id}/messages
→ [{ id, role, content, tokens_used, created_at }]
```
#### 6.3 缓存查询 API
路由文件:`apps/backend/app/routers/xcx_ai_cache.py`
```
GET /api/ai/cache/{cache_type}?target_id=xxx
→ { id, cache_type, target_id, result_json, score, created_at }
认证JWT Token
隔离site_id 从 JWT 提取,强制过滤
```
### 7. 各应用骨架接口
每个应用实现统一的调用接口:
```python
# 应用基类模式(非继承,约定接口)
async def run(
context: dict, # 包含 member_id, site_id, 及应用特定参数
bailian: BailianClient,
cache_svc: AICacheService,
conv_svc: ConversationService,
) -> dict:
"""
执行 AI 应用调用。
1. 构建 Promptbuild_prompt
2. 调用百炼 APIbailian.chat_json
3. 写入 ai_conversations + ai_messages
4. 写入 ai_cache
5. 返回结果 dict
"""
```
骨架应用App3/4/5/6/7`build_prompt` 函数留接口:
```python
def build_prompt(context: dict) -> list[dict]:
"""构建 Prompt 消息列表。
P5-A 阶段:返回占位 Prompt标注待细化字段。
P5-B 阶段:由对应页面 spec 补充完整 Prompt。
"""
# TODO: P5-B 细化
return [
{"role": "system", "content": json.dumps({
"task": "...",
"current_time": "", # BailianClient 自动注入
# 以下字段待细化
"data": {},
"reference": {},
})},
]
```
## 数据模型
### DDLbiz.ai_conversations
```sql
CREATE TABLE IF NOT EXISTS biz.ai_conversations (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(50) NOT NULL, -- 用户 ID 或 'system'
nickname VARCHAR(100) NOT NULL DEFAULT '',
app_id VARCHAR(30) NOT NULL, -- app1_chat / app2_finance / ... / app8_consolidation
site_id BIGINT NOT NULL,
source_page VARCHAR(100), -- 来源页面标识
source_context JSONB, -- 页面上下文 JSON
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.ai_conversations IS 'AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条';
COMMENT ON COLUMN biz.ai_conversations.app_id IS '应用标识app1_chat / app2_finance / app3_clue / app4_analysis / app5_tactics / app6_note / app7_customer / app8_consolidation';
COMMENT ON COLUMN biz.ai_conversations.user_id IS '用户 ID系统自动调用时为 system';
CREATE INDEX IF NOT EXISTS idx_ai_conv_user_site ON biz.ai_conversations (user_id, site_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ai_conv_app_site ON biz.ai_conversations (app_id, site_id, created_at DESC);
```
### DDLbiz.ai_messages
```sql
CREATE TABLE IF NOT EXISTS biz.ai_messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES biz.ai_conversations(id) ON DELETE CASCADE,
role VARCHAR(10) NOT NULL
CONSTRAINT chk_ai_msg_role CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
tokens_used INTEGER, -- 本条消息消耗的 token 数
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.ai_messages IS 'AI 消息记录:对话中的每条消息(输入/输出/系统)';
CREATE INDEX IF NOT EXISTS idx_ai_msg_conv ON biz.ai_messages (conversation_id, created_at);
```
### DDLbiz.ai_cache
```sql
CREATE TABLE IF NOT EXISTS biz.ai_cache (
id BIGSERIAL PRIMARY KEY,
cache_type VARCHAR(30) NOT NULL
CONSTRAINT chk_ai_cache_type CHECK (
cache_type IN (
'app2_finance', 'app3_clue', 'app4_analysis',
'app5_tactics', 'app6_note_analysis',
'app7_customer_analysis', 'app8_clue_consolidated'
)
),
site_id BIGINT NOT NULL,
target_id VARCHAR(100) NOT NULL, -- 含义因 cache_type 而异
result_json JSONB NOT NULL,
score INTEGER, -- 应用 6 专用评分
triggered_by VARCHAR(100), -- 触发来源标识
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ -- 可选过期时间
);
COMMENT ON TABLE biz.ai_cache IS 'AI 应用缓存:各应用的结构化输出结果';
COMMENT ON COLUMN biz.ai_cache.target_id IS '目标 IDApp2=时间维度编码 / App3,6,7,8=member_id / App4,5={assistant_id}_{member_id}';
COMMENT ON COLUMN biz.ai_cache.score IS '评分:仅应用 6 使用1-10 分)';
CREATE INDEX IF NOT EXISTS idx_ai_cache_lookup ON biz.ai_cache (cache_type, site_id, target_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ai_cache_cleanup ON biz.ai_cache (cache_type, site_id, target_id, created_at);
```
### target_id 约定
| cache_type | target_id 格式 | 示例 |
|---|---|---|
| app2_finance | 时间维度编码 | `this_month`, `last_week` |
| app3_clue | member_id | `12345` |
| app4_analysis | `{assistant_id}_{member_id}` | `100_12345` |
| app5_tactics | `{assistant_id}_{member_id}` | `100_12345` |
| app6_note_analysis | member_id | `12345` |
| app7_customer_analysis | member_id | `12345` |
| app8_clue_consolidated | member_id | `12345` |
### 应用 2 时间维度编码
| 编码 | 含义 | 计算规则 |
|---|---|---|
| `this_month` | 本月 | 当前营业日所在月 |
| `last_month` | 上月 | 当前月 - 1 |
| `this_week` | 本周 | 当前营业日所在周(周一起) |
| `last_week` | 上周 | 当前周 - 1 |
| `last_3_months` | 前 3 月(不含本月) | 当前月 - 3 ~ 当前月 - 1 |
| `this_quarter` | 本季 | 当前营业日所在季度 |
| `last_quarter` | 上季 | 当前季度 - 1 |
| `last_6_months` | 近 6 月(不含本月) | 当前月 - 6 ~ 当前月 - 1 |
营业日分界点:每日 08:00`BUSINESS_DAY_START_HOUR` 环境变量)。
### 应用输出 JSON Schema
#### 应用 3/6 线索格式(写入 ai_cache.result_json
```json
{
"clues": [
{
"category": "消费习惯",
"summary": "偏好周末下午时段消费",
"detail": "近 3 个月 8 次消费中 6 次在周六/日 14:00-18:00",
"emoji": "📅"
}
]
}
```
应用 6 额外包含 `score` 字段1-10写入 `ai_cache.score`
#### 应用 8 整合线索格式
```json
{
"clues": [
{
"category": "消费习惯",
"summary": "偏好周末下午时段消费",
"detail": "近 3 个月 8 次消费中 6 次在周六/日 14:00-18:00",
"emoji": "📅",
"providers": "系统,张三"
}
]
}
```
#### 应用 4 关系分析格式
```json
{
"task_description": "...",
"action_suggestions": ["建议1", "建议2"],
"one_line_summary": "..."
}
```
#### 应用 5 话术参考格式
```json
{
"tactics": [
{ "scenario": "...", "script": "..." }
]
}
```
#### 应用 7 客户分析格式
```json
{
"strategies": [
{ "title": "...", "content": "..." }
],
"summary": "..."
}
```
#### 应用 2 财务洞察格式
```json
{
"insights": [
{ "seq": 1, "title": "...", "body": "..." }
]
}
```
### 与现有表的关系
- `member_retention_clue`:应用 8 的 `ClueWriter` 全量替换 `source IN ('ai_consumption', 'ai_note')` 的记录,`source='manual'` 的人工线索不受影响
- `biz.notes`:应用 6 触发点,`note_service.create_note()` 中的 `ai_analyze_note()` 占位函数将被替换为真实调用
- `biz.trigger_jobs`:新增 AI 相关的事件触发器配置(`consumption_settled``note_created``task_assigned`
- `biz.coach_tasks`:应用 4 触发条件之一(任务分配事件)
## 正确性属性
*正确性属性是系统在所有合法执行路径上都应保持为真的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1: BailianClient 双模式调用一致性
*For any* 合法的消息列表,`chat_stream` 应返回非空的 chunk 序列(拼接后为完整文本),`chat_json` 应返回可解析的 JSON dict 和正整数 tokens_used。两种模式对相同输入都应成功返回mock API 正常响应时)。
**Validates: Requirements 2.1, 2.3, 2.4**
### Property 2: 指数退避重试策略
*For any* 失败次数 n1 ≤ n ≤ max_retriesBailianClient 应在第 n 次失败后等待 base_interval × 2^(n-1) 秒后重试;当失败次数超过 max_retries 时,应抛出 BailianApiError。
**Validates: Requirements 2.2**
### Property 3: JSON 解析失败错误处理
*For any* 非法 JSON 字符串作为 API 响应,`chat_json` 应抛出 BailianJsonParseError 而非静默返回空值或崩溃。
**Validates: Requirements 2.5**
### Property 4: current_time 注入不变量
*For any* 消息列表,经 `_inject_current_time` 处理后,首条消息的 content解析为 JSON应包含 `current_time` 字段,且值为 ISO 格式的时间字符串(精确到秒),其余消息不受影响。
**Validates: Requirements 2.6**
### Property 5: AI 调用记录持久化 round-trip
*For any* AI 应用调用app1-app8调用完成后(a) `ai_conversations` 应包含一条匹配 app_id、site_id 的记录;(b) `ai_messages` 应包含至少一条 role='system' 或 role='user' 的输入消息和一条 role='assistant' 的输出消息;(c) 输出消息的 tokens_used 应为正整数。
**Validates: Requirements 3.2, 3.4, 3.5, 13.1, 13.2, 13.3**
### Property 6: 历史对话列表排序与分页
*For any* 用户和 site_id查询历史对话列表返回的记录应按 created_at 严格降序排列,且每页数量不超过 page_size默认 20
**Validates: Requirements 3.7**
### Property 7: 缓存写入 round-trip
*For any* AI 应用app2-app8的调用结果写入 `ai_cache` 后,按 (cache_type, site_id, target_id) 查询最新记录应返回与写入内容一致的 result_json。
**Validates: Requirements 4.7, 5.6, 6.6, 7.5, 8.6, 9.5, 10.10**
### Property 8: AI 应用输出 JSON 结构验证
*For any* AI 应用调用结果的 result_json
- App2: 应包含 `insights` 数组,每项含 `seq`(正整数)、`title`(非空字符串)、`body`(非空字符串)
- App3: 应包含 `clues` 数组,每条含 `category`(∈ {客户基础, 消费习惯, 玩法偏好})、`summary``detail``emoji`
- App4: 应包含 `task_description``action_suggestions`(数组)、`one_line_summary`
- App5: 应包含 `tactics` 数组
- App6: 应包含 `score`1-10 整数)和 `clues` 数组,每条 category ∈ 6 个枚举值
- App7: 应包含 `strategies` 数组(每项含 `title``content`)和 `summary`
- App8: 应包含 `clues` 数组,每条含 `category`(∈ 6 个枚举值)、`summary``detail``emoji``providers`
**Validates: Requirements 4.4, 5.2, 5.3, 5.4, 6.3, 7.3, 8.2, 8.3, 8.4, 9.2, 10.4, 10.5**
### Property 9: Prompt reference 历史注入
*For any* 应用 3/4/5/6/7/8 的 Prompt 构建reference 字段应包含相关应用的缓存结果(如有),且历史记录附带 `generated_at` 时间戳。当缓存不存在时reference 应为空对象。
**Validates: Requirements 5.8, 6.4, 6.5, 7.2, 7.4, 8.8, 9.7**
### Property 10: 事件调用链顺序正确性
*For any* 业务事件:
- 消费事件(无助教):调用顺序严格为 App3 → App8 → App7
- 消费事件(有助教):调用顺序严格为 App3 → App8 → {App7, App4 → App5}App7 和 App4 均在 App8 之后)
- 备注事件:调用顺序严格为 App6 → App8
- 任务分配事件:调用顺序严格为 App4 → App5
**Validates: Requirements 11.1, 11.2, 11.3, 11.4, 11.5, 11.6**
### Property 11: 调用链容错不变量
*For any* 调用链执行过程中某个应用调用失败,后续应用应继续执行(使用已有缓存),整条链不应因单点失败而中断。失败的应用应有错误日志记录。
**Validates: Requirements 11.7**
### Property 12: ClueWriter 全量替换不变量
*For any* member_id 和 site_id执行 `ClueWriter.replace_ai_clues(member_id, site_id, new_clues)` 后:
- (a) 该客户的 AI 来源线索source IN ('ai_consumption', 'ai_note'))应恰好等于 new_clues 的数量
- (b) 人工线索source='manual')的数量应与替换前完全一致
- (c) 写入的记录中 recorded_by_assistant_id 应为 NULL
- (d) summary 字段应为 emoji + 空格 + 原始 summary 的拼接格式
**Validates: Requirements 10.7, 10.8, 10.9**
### Property 13: 缓存查询 site_id 隔离
*For any* 两个不同的 site_idA 和 B写入 site_id=A 的缓存记录后,以 site_id=B 查询应返回空结果(即使 cache_type 和 target_id 相同)。
**Validates: Requirements 12.1, 12.5**
### Property 14: 缓存保留上限
*For any* (cache_type, site_id, target_id) 组合,无论写入多少条记录,清理后该组合的记录总数应 ≤ 500。
**Validates: Requirements 12.3**
## 错误处理
### 百炼 API 层
| 错误场景 | 处理策略 |
|---|---|
| API 超时 | 指数退避重试(最多 3 次),超时阈值 30s |
| API 返回 HTTP 4xx | 不重试,立即抛出 BailianApiError |
| API 返回 HTTP 5xx | 指数退避重试 |
| 响应非法 JSON | 抛出 BailianJsonParseError记录原始响应到日志 |
| API Key 无效 | 不重试,抛出 BailianAuthError记录告警日志 |
| 流式连接中断 | 已接收的 chunk 拼接为部分回复,标记 incomplete |
### 事件调度层
| 错误场景 | 处理策略 |
|---|---|
| 调用链中某应用失败 | 记录错误日志 + 写入失败 conversation 记录,后续应用使用已有缓存继续 |
| 数据库连接失败 | 整条链中止,记录错误日志 |
| 缓存查询失败 | 传空 reference 继续执行,不阻塞 |
### SSE 端点层
| 错误场景 | 处理策略 |
|---|---|
| 用户未认证 | 返回 HTTP 401 |
| 消息为空 | 返回 HTTP 422 |
| 流式过程中百炼 API 失败 | 发送 `{"type": "error", "message": "..."}` SSE 事件 |
| 客户端断开连接 | 取消百炼 API 调用,清理资源 |
### 缓存服务层
| 错误场景 | 处理策略 |
|---|---|
| 查询无结果 | 返回 null/None不抛异常 |
| 写入失败 | 抛出异常,由调用方处理 |
| 清理超限失败 | 记录警告日志,不影响写入操作 |
### ClueWriter 层
| 错误场景 | 处理策略 |
|---|---|
| 全量替换事务失败 | 回滚整个事务,保留原有线索不变 |
| 线索数据不符合 CHECK 约束 | 回滚事务记录错误日志category 枚举不匹配) |
## 测试策略
### 属性测试Property-Based Testing
- **测试库**hypothesisPython
- **最小迭代次数**:每个属性测试 100 次
- **测试文件位置**`tests/test_p5_ai_integration_properties.py`Monorepo 级)+ `apps/backend/tests/test_ai_*.py`(模块级)
- **标签格式**`# Feature: 05-miniapp-ai-integration, Property {N}: {property_text}`
每个正确性属性对应一个属性测试:
| Property | 测试策略 | 生成器 |
|---|---|---|
| P1: 双模式调用 | Mock 百炼 API验证两种模式返回格式 | 随机消息列表 |
| P2: 重试策略 | Mock 可控失败次数的 API | 随机失败次数 (0-5) |
| P3: JSON 解析失败 | Mock 返回非法 JSON | 随机非 JSON 字符串 |
| P4: current_time 注入 | 纯函数测试 | 随机消息列表 |
| P5: 记录持久化 | Mock 百炼 + 真实 DBtest_zqyy_app | 随机 app_id、消息内容 |
| P6: 历史列表排序 | 真实 DB | 随机对话记录(随机时间戳) |
| P7: 缓存 round-trip | 真实 DB | 随机 cache_type、target_id、result_json |
| P8: 输出 JSON 结构 | JSON Schema 验证 | 随机 AI 响应(符合各应用 schema |
| P9: reference 历史注入 | Mock 缓存数据 | 随机缓存记录(含/不含历史) |
| P10: 调用链顺序 | Mock 所有应用,记录调用序列 | 随机事件类型和参数 |
| P11: 调用链容错 | Mock 随机应用失败 | 随机失败位置 |
| P12: ClueWriter 替换 | 真实 DB | 随机线索列表 + 预置人工线索 |
| P13: site_id 隔离 | 真实 DB | 随机 site_id 对 |
| P14: 缓存上限 | 真实 DB | 批量写入(>500 条) |
### 单元测试
单元测试聚焦于具体示例和边界条件,与属性测试互补:
| 测试范围 | 测试内容 |
|---|---|
| 表结构验证 | 验证 3 张表的列、类型、约束、索引(需求 1.1-1.5 |
| App2 时间维度 | 验证 8 个时间维度编码的计算逻辑(营业日分界点 08:00 |
| App2 字段映射 | 验证 Prompt 使用 items_sum 口径而非 consume_money |
| SSE 协议 | 验证 Content-Type: text/event-stream 和事件格式 |
| ClueWriter 字段映射 | 验证 emoji+summary 拼接、source 判断逻辑 |
| 缓存 CHECK 约束 | 验证非法 cache_type 被拒绝 |
| App6 评分范围 | 验证 score 字段存储在 ai_cache.score |
### 集成测试
| 测试范围 | 测试内容 |
|---|---|
| 完整消费事件链 | Mock 百炼 API验证 App3→App8→App7 全链路 |
| 备注事件链 | Mock 百炼 API验证 App6→App8 全链路 |
| note_service 集成 | 验证 `ai_analyze_note` 占位函数被替换后的调用流程 |
| SSE 端到端 | 使用 httpx 的 SSE 客户端验证流式响应 |

View File

@@ -0,0 +1,257 @@
# 需求文档P5 AI 集成层miniapp-ai-integration
## 简介
本文档定义小程序 AI 集成层的需求规格,覆盖 P5-A 阶段(管道 + 骨架)。系统为台球门店助教和管理者提供 8 个 AI 应用,包括通用对话、财务洞察、客户数据分析、关系分析、话术参考、备注分析、客户分析和维客线索整理。技术栈为 FastAPI 后端 + 微信小程序前端 + PostgreSQLzqyy_app 业务库),通过阿里云百炼 API通义千问提供 AI 能力。
P5-A 阶段交付"管道":建表、百炼封装、缓存 API、SSE 框架,以及 Prompt 已完全确定的应用(应用 2、应用 8。应用 3/4/5/6/7 只实现触发机制和调用骨架Prompt 拼接函数留接口)。
> P5-B 阶段Prompt 细化)不在本 spec 范围,将分散到 P6task-detail和 P9customer-detail的对应任务中完成。
## 术语表
- **AI_Integration_System**P5 AI 集成层系统整体,包含后端 API、百炼封装、事件调度、缓存管理等
- **Bailian_Client**:百炼 API 统一封装层,负责与阿里云通义千问 API 的通信(流式/非流式)
- **SSE_Endpoint**Server-Sent Events 流式返回端点,用于应用 1 通用对话的逐字推送
- **AI_Cache_Service**AI 缓存读写服务,管理 `biz.ai_cache` 表的 CRUD 和保留策略
- **Event_Dispatcher**:事件调度器,负责根据业务事件(消费、备注、任务分配)触发对应 AI 应用调用链
- **Clue_Writer**:维客线索写入器,负责将应用 8 整合后的线索全量替换写入 `member_retention_clue`
- **App1_Chat**:应用 1 通用对话,用户主动发起的流式对话
- **App2_Finance**:应用 2 财务洞察,每日自动生成 8 个时间维度的财务分析
- **App3_Clue**:应用 3 客户数据维客线索分析,客户新增消费时自动触发
- **App4_Analysis**:应用 4 关系分析/任务建议,助教参与新结算或任务分配时触发
- **App5_Tactics**:应用 5 话术参考,联动应用 4 自动触发
- **App6_Note**:应用 6 备注分析,备注提交时自动触发
- **App7_Customer**:应用 7 客户分析,消费事件链中应用 8 完成后触发
- **App8_Consolidation**:应用 8 维客线索整理,应用 3 或应用 6 产出后触发
- **items_sum**:校准后的消费金额口径,= table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money禁止使用 consume_money
- **ai_conversations**AI 对话表,记录每次对话的元信息
- **ai_messages**AI 消息表,记录对话中的每条消息
- **ai_cache**AI 缓存表,存储各应用的结构化输出结果
- **member_retention_clue**:维客线索表,存储整合后的客户维护线索
- **营业日分界点**:每日 08:00用于时间维度计算的日切点
## 需求
### 需求 1数据库表结构
**用户故事:** 作为系统,我需要持久化存储所有 AI 对话记录和缓存结果,以便支撑 8 个 AI 应用的数据读写需求。
#### 验收标准
1. THE AI_Integration_System SHALL 在 `biz` schema 下创建 `ai_conversations`包含字段id、user_id、nickname、app_id、site_id、source_page、source_contextJSON、created_at
2. THE AI_Integration_System SHALL 在 `biz` schema 下创建 `ai_messages`包含字段id、conversation_id外键关联 ai_conversations、roleuser/assistant/system、content、tokens_used、created_at
3. THE AI_Integration_System SHALL 在 `biz` schema 下创建 `ai_cache`包含字段id、cache_type枚举app2_finance / app3_clue / app4_analysis / app5_tactics / app6_note_analysis / app7_customer_analysis / app8_clue_consolidated、site_id、target_id、result_json、score应用 6 专用、triggered_bytrigger_job_id、created_at、expires_at
4. THE AI_Integration_System SHALL 对 ai_cache 表的 target_id 按应用约定存储:应用 2 存时间维度编码、应用 3/6/7/8 存 member_id、应用 4/5 存 `{assistant_id}_{member_id}` 格式
5. THE AI_Integration_System SHALL 对所有三张表启用 site_id 字段以支持多门店隔离
### 需求 2百炼 API 统一封装层
**用户故事:** 作为开发者,我需要一个统一的百炼 API 封装层,以便所有 AI 应用通过一致的接口调用阿里云通义千问,降低重复代码和维护成本。
#### 验收标准
1. THE Bailian_Client SHALL 支持流式调用模式(用于应用 1 SSE 推送)和非流式调用模式(用于应用 2-8 结构化输出)
2. THE Bailian_Client SHALL 在 API 调用失败时执行自动重试(含指数退避策略)
3. THE Bailian_Client SHALL 记录每次 API 调用的 tokens_used 统计信息
4. THE Bailian_Client SHALL 支持 JSON 输出模式,确保应用 2-8 返回的内容可解析为结构化 JSON
5. IF 百炼 API 返回非预期格式或解析失败THEN THE Bailian_Client SHALL 记录错误日志并返回明确的错误信息
6. THE Bailian_Client SHALL 在每次调用的首条 Prompt JSON 中统一注入 `current_time` 字段(精确到秒)
### 需求 3应用 1 通用对话SSE 流式)
**用户故事:** 作为助教,我可以在任意页面点击 AI 按钮,跳转到对话页面与 AI 交流AI 了解当前页面上下文。
#### 验收标准
1. THE SSE_Endpoint SHALL 以 Server-Sent Events 协议向前端推送 AI 回复,实现逐字展示效果
2. WHEN 用户从任意页面进入 chat 页面时THE App1_Chat SHALL 始终新建一条 ai_conversations 记录(不复用已有对话)
3. THE App1_Chat SHALL 在首条消息中注入页面上下文,包含 source_page来源页面标识、page_context页面上下文摘要、screen_content屏幕可见内容文本化描述
4. WHEN 用户发送消息时THE App1_Chat SHALL 立即将用户消息写入 ai_messagesrole=user
5. WHEN 流式返回完成后THE App1_Chat SHALL 将完整的 assistant 回复写入 ai_messagesrole=assistant包含 tokens_used
6. THE App1_Chat SHALL 通过 `biz_params.user_prompt_params` 传入 User_ID、Role助教/管理者、Nickname 实现信息隔离
7. THE App1_Chat SHALL 提供历史对话列表接口,按时间倒序展示,每页 20 条懒加载
8. THE App1_Chat SHALL 搭建上下文注入框架页面文本化工具留接口P5-B 阶段各页面逐步实现)
### 需求 4应用 2 财务洞察
**用户故事:** 作为管理者,我在财务看板能看到 AI 生成的财务洞察分析,覆盖多个时间维度。
#### 验收标准
1. THE App2_Finance SHALL 由 ETL 调度器在每日 08:00营业日分界点后的首次任务执行时触发
2. THE App2_Finance SHALL 在 DWS 日更数据更新完成后,依次对 8 个时间维度发起独立调用(共 8 次百炼 API 调用)
3. THE App2_Finance SHALL 覆盖 8 个时间维度本月this_month、上月last_month、本周this_week、上周last_week、前 3 月不含本月last_3_months、本季this_quarter、上季last_quarter、近 6 月不含本月last_6_months
4. THE App2_Finance SHALL 返回结构化 JSON格式为序号 + 标题 + 正文的数组
5. THE App2_Finance SHALL 在 Prompt 中包含当期和上期的收入结构table_fee、assistant_pd、assistant_cx、goods、recharge、储值资产、费用汇总、平台结算数据
6. THE App2_Finance SHALL 使用已校准的收入结构字段映射table_fee = table_charge_money、assistant_pd = assistant_pd_money、assistant_cx = assistant_cx_money、goods = goods_money、recharge = 充值 pay_amountsettle_type=5
7. THE App2_Finance SHALL 将每次调用结果写入 ai_cachecache_type=app2_financetarget_id=时间维度编码)
8. IF ETL 调度器中尚无应用 2 的调度逻辑THEN THE AI_Integration_System SHALL 在 P5-A 阶段补充该调度任务
### 需求 5应用 3 客户数据维客线索分析(骨架)
**用户故事:** 作为系统,客户新增消费时自动通过 AI 分析客户数据,提取维客线索。
#### 验收标准
1. WHEN 客户新增消费结账单出现THE Event_Dispatcher SHALL 触发 App3_Clue 调用
2. THE App3_Clue SHALL 返回 JSON 格式的线索数组,每条线索包含 category分类标签、summary摘要、detail详情、emoji
3. THE App3_Clue SHALL 将分类标签限定为 3 个枚举值:客户基础、消费习惯、玩法偏好
4. THE App3_Clue SHALL 将线索提供者统一标记为"系统"
5. THE App3_Clue SHALL 使用 items_sum 作为消费金额口径(= table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money禁止使用 consume_money
6. THE App3_Clue SHALL 将结果写入 ai_cachecache_type=app3_cluetarget_id=member_id
7. THE App3_Clue SHALL 实现触发机制和调用框架Prompt 拼接函数留接口consumption_records 等字段待 P9-T1 细化)
8. THE App3_Clue SHALL 在 Prompt 的 reference 中包含应用 6 的线索结果(如有)和最近 2 套应用 8 的历史信息(附 generated_at 时间)
### 需求 6应用 4 关系分析/任务建议(骨架)
**用户故事:** 作为系统,助教参与新结算或被分配召回任务时,自动生成关系分析和任务建议。
#### 验收标准
1. WHEN 助教参与新结算时THE Event_Dispatcher SHALL 在消费事件链中等待应用 3 → 应用 8 完成后触发 App4_Analysis
2. WHEN 优先召回任务分配或高优先召回任务分配时THE Event_Dispatcher SHALL 直接触发 App4_Analysis读取应用 8 已有缓存)
3. THE App4_Analysis SHALL 返回 JSON 格式,包含任务描述、行动建议数组、一句话总结
4. THE App4_Analysis SHALL 在 Prompt 的 reference 中包含应用 8 当前最新维客线索和最近 2 套历史信息(附 generated_at 时间)
5. IF 应用 8 缓存不存在如新客户首次结算THEN THE App4_Analysis SHALL 在 reference 中传空对象Prompt 中标注"暂无历史线索"
6. THE App4_Analysis SHALL 将结果写入 ai_cachecache_type=app4_analysistarget_id=`{assistant_id}_{member_id}`
7. THE App4_Analysis SHALL 实现触发机制和调用框架Prompt 拼接函数留接口service_history、assistant_info 等字段待 P6-T4 细化)
### 需求 7应用 5 话术参考(骨架)
**用户故事:** 作为系统,应用 4 生成任务建议后,自动联动生成沟通话术参考。
#### 验收标准
1. WHEN App4_Analysis 调用完成后THE Event_Dispatcher SHALL 自动触发 App5_Tactics
2. THE App5_Tactics SHALL 接收应用 4 的完整返回结果作为 Prompt 中的 task_suggestion 字段
3. THE App5_Tactics SHALL 返回 JSON 格式的话术内容数组
4. THE App5_Tactics SHALL 在 Prompt 的 reference 中包含最近 2 套应用 8 的历史信息(附 generated_at 时间)
5. THE App5_Tactics SHALL 将结果写入 ai_cachecache_type=app5_tacticstarget_id=`{assistant_id}_{member_id}`
6. THE App5_Tactics SHALL 实现联动框架Prompt 拼接函数留接口service_history、assistant_info 等字段随应用 4 同步在 P6-T4 细化)
### 需求 8应用 6 备注分析(骨架)
**用户故事:** 作为系统,助教提交备注后,自动通过 AI 分析备注内容,提取维客线索并评分。
#### 验收标准
1. WHEN 备注提交时THE Event_Dispatcher SHALL 触发 App6_Note 调用
2. THE App6_Note SHALL 返回 JSON 格式,包含 score评分 1-10和 clues线索数组每条含 category、summary、detail、emoji
3. THE App6_Note SHALL 将分类标签限定为 6 个枚举值:客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈
4. THE App6_Note SHALL 将线索提供者标记为当前备注提供人
5. THE App6_Note SHALL 使用 6 分为标准分的评分规则:重复信息/低价值/时效性低酌情扣分,高价值信息酌情加分
6. THE App6_Note SHALL 将结果写入 ai_cachecache_type=app6_note_analysistarget_id=member_idscore 字段存储评分
7. THE App6_Note SHALL 实现触发机制和调用框架Prompt 拼接函数留接口consumption_data 等字段待 P9-T1 细化)
8. THE App6_Note SHALL 在 Prompt 的 reference 中包含应用 3 的线索结果(如有)和最近 2 套应用 8 的历史信息(附 generated_at 时间)
### 需求 9应用 7 客户分析(骨架)
**用户故事:** 作为系统,客户结账单出现后自动生成客户全量分析与运营建议。
#### 验收标准
1. WHEN 消费事件链中 App8_Consolidation 完成后THE Event_Dispatcher SHALL 串行触发 App7_Customer确保读到本次消费触发的最新线索
2. THE App7_Customer SHALL 返回 JSON 格式,包含 strategies 数组(每条含 title 和 content和 summary一句话总结
3. THE App7_Customer SHALL 使用 items_sum 作为消费金额口径,禁止使用 consume_money
4. THE App7_Customer SHALL 对主观信息来自备注标注【来源XXX请甄别信息真实性】
5. THE App7_Customer SHALL 将结果写入 ai_cachecache_type=app7_customer_analysistarget_id=member_id
6. THE App7_Customer SHALL 实现触发机制和调用框架Prompt 拼接函数留接口objective_data 等字段待 P9-T1 细化)
7. THE App7_Customer SHALL 在 Prompt 的 reference 中包含最新 + 最近 2 套应用 8 的历史信息(附 generated_at 时间)
### 需求 10应用 8 维客线索整理
**用户故事:** 作为系统,应用 3 或应用 6 产出新线索后,自动整合去重生成统一维客线索,并写入 member_retention_clue 表。
#### 验收标准
1. WHEN App3_Clue 产出新线索后THE Event_Dispatcher SHALL 立即触发 App8_Consolidation
2. WHEN App6_Note 产出新线索后THE Event_Dispatcher SHALL 立即触发 App8_Consolidation
3. THE App8_Consolidation SHALL 接收应用 3 和应用 6 的全部线索内容作为输入(附 generated_at 时间)
4. THE App8_Consolidation SHALL 返回 JSON 格式的整合后线索数组,每条含 category、summary、detail、emoji、providers
5. THE App8_Consolidation SHALL 将分类标签限定为 6 个枚举值:客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈(与 member_retention_clue 表 CHECK 约束一致)
6. THE App8_Consolidation SHALL 合并相似线索(多提供者以逗号分隔),其余线索原文返回,遵循最小改动原则
7. THE App8_Consolidation SHALL 将整合后的线索全量替换该客户在 member_retention_clue 中的所有 AI 来源线索source IN ('ai_consumption', 'ai_note')人工线索source='manual')不受影响
8. THE Clue_Writer SHALL 按以下字段映射写入 member_retention_cluecategory → category、emoji + summary → summaryemoji 拼接在前,如"📅 偏好周末下午时段消费"、detail → detail、providers → recorded_by_name、source 根据线索来源判断(纯应用 3 → ai_consumption纯应用 6 → ai_note混合来源 → ai_consumption
9. THE Clue_Writer SHALL 对系统触发的线索将 recorded_by_assistant_id 填 NULL
10. THE App8_Consolidation SHALL 将结果同时写入 ai_cachecache_type=app8_clue_consolidatedtarget_id=member_id
### 需求 11事件调度与调用链编排
**用户故事:** 作为系统,我需要根据业务事件(消费、备注、任务分配)自动编排 AI 应用调用链,确保执行顺序和数据依赖正确。
#### 验收标准
1. WHEN 消费事件结账单发生时THE Event_Dispatcher SHALL 按严格串行顺序执行:应用 3 → 应用 8 → 应用 7
2. WHEN 消费事件中该结算单有助教参与时THE Event_Dispatcher SHALL 在应用 8 完成后额外执行:应用 4 → 应用 5
3. WHEN 备注提交事件发生时THE Event_Dispatcher SHALL 按串行顺序执行:应用 6 → 应用 8
4. WHEN 任务分配事件(优先召回/高优先召回发生时THE Event_Dispatcher SHALL 执行:应用 4 → 应用 5直接读取应用 8 已有缓存)
5. THE Event_Dispatcher SHALL 确保消费事件链中应用 7 等待应用 8 完成后再启动,保证读到本次消费触发的最新线索
6. THE Event_Dispatcher SHALL 确保消费事件链中应用 4 等待应用 3 → 应用 8 完成后再执行,确保读到本次消费的最新线索
7. IF 调用链中某个应用调用失败THEN THE Event_Dispatcher SHALL 记录错误日志,后续应用使用已有缓存继续执行(不阻塞整条链)
### 需求 12AI 缓存读写 API
**用户故事:** 作为前端,我需要通过 API 读取各 AI 应用的缓存结果,以便在对应页面展示 AI 分析内容。
#### 验收标准
1. THE AI_Cache_Service SHALL 提供按 cache_type + site_id + target_id 查询最新缓存结果的 API
2. THE AI_Cache_Service SHALL 支持以下前端消费场景:应用 2 结果展示在 board-finance 财务看板、应用 4/5 结果展示在 task-detail 任务详情页、应用 6 的 score 以打星方式展示在备注卡片、应用 7 结果展示在 customer-detail 客户详情页、应用 8 结果通过 member_retention_clue 表展示
3. THE AI_Cache_Service SHALL 对每个 (cache_type, site_id, target_id) 组合保留最近 500 条记录,超过时删除最旧的
4. WHEN 写入新缓存记录后THE AI_Cache_Service SHALL 异步检查并清理超限记录
5. THE AI_Cache_Service SHALL 对所有查询和写入操作执行 site_id 隔离
### 需求 13AI 调用记录持久化
**用户故事:** 作为系统,所有 AI 对话(含用户主动和系统自动调用)都要持久化记录,以便追溯和统计。
#### 验收标准
1. THE AI_Integration_System SHALL 对所有 8 个应用的每次 AI 调用创建 ai_conversations 记录,包含 conversation_id、app_id、user_id系统调用时为系统标识、nickname、site_id
2. THE AI_Integration_System SHALL 对每次 AI 调用的输入和输出分别写入 ai_messages包含 roleuser/assistant/system、content、tokens_used、created_at
3. THE AI_Integration_System SHALL 在 ai_conversations 中记录 source_page 和 source_contextJSON标识调用来源
### 需求 14百炼技术方案确认
**用户故事:** 作为开发者,我需要确认百炼 API 的流式返回技术方案和 JSON 输出最佳实践,以便正确实现封装层。
#### 验收标准
1. THE AI_Integration_System SHALL 查阅百炼官方文档确认流式返回的技术方案SSE vs WebSocket
2. THE AI_Integration_System SHALL 确认百炼 API 的 JSON 输出模式配置方式response_format 参数或 System Prompt 约束)
3. THE AI_Integration_System SHALL 基于确认结果输出技术方案文档,作为 Bailian_Client 实现的依据
---
## 范围说明
### P5-A 阶段(本 spec 覆盖)
| 任务 | 对应需求 | 说明 |
|------|---------|------|
| T1 | 需求 1 | 建表ai_conversations + ai_messages + ai_cache |
| T2 | 需求 2 | 百炼 API 统一封装层 |
| T3 | 需求 3 | 应用 1 通用对话 SSE |
| T5 | 需求 4 | 应用 2 财务洞察Prompt 已确定) |
| T6-骨架 | 需求 5 | 应用 3 触发机制 + 调用框架 |
| T7-骨架 | 需求 6 | 应用 4 触发机制 + 调用框架 |
| T8-骨架 | 需求 7 | 应用 5 联动框架 |
| T9-骨架 | 需求 8 | 应用 6 触发机制 + 调用框架 |
| T10-骨架 | 需求 9 | 应用 7 触发机制 + 调用框架 |
| T11 | 需求 10 | 应用 8 维客线索整理Prompt 已确定) |
| T12 | 需求 12 | AI 缓存读写 API |
| T13 | 需求 14 | 百炼技术方案确认 |
| — | 需求 11 | 事件调度与调用链编排(贯穿 T6-T11 |
| — | 需求 13 | AI 调用记录持久化(贯穿所有应用) |
### P5-B 阶段(不在本 spec 范围)
以下任务将分散到对应页面的开发 spec 中完成:
- T4页面内容文本化工具 → 随 P6-P9 各页面逐步实现
- T6-完整:应用 3 Prompt JSON 细化 → P9-T1customer-detail API
- T7-完整:应用 4 Prompt JSON 细化 → P6-T4task-detail API
- T8-完整:应用 5 Prompt JSON 细化 → P6-T4task-detail API
- T9-完整:应用 6 Prompt JSON 细化 → P9-T1customer-detail API
- T10-完整:应用 7 Prompt JSON 细化 → P9-T1customer-detail API

View File

@@ -0,0 +1,322 @@
# 实现计划P5 AI 集成层miniapp-ai-integration
## 概述
基于 P5-A 阶段设计,在 `apps/backend/app/ai/` 新建 AI 模块,实现百炼 API 封装、SSE 对话、事件调度、缓存服务、8 个 AI 应用(其中 App2/App8 含完整 PromptApp3/4/5/6/7 仅骨架)。每个任务增量构建,最终通过路由和事件调度器串联所有组件。
## 任务
- [x] 1. 数据库表结构与基础模块搭建
- [x] 1.1 创建 DDL 迁移脚本,在 `biz` schema 下建表 `ai_conversations``ai_messages``ai_cache`
- 按设计文档中的 DDL 创建三张表包含所有字段、CHECK 约束、索引
- DDL 文件放置于 `db/zqyy_app/migrations/` 目录,日期前缀命名
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 1.2 创建 AI 模块目录结构和 Pydantic Schema
- 创建 `apps/backend/app/ai/` 目录及 `__init__.py`
- 创建 `apps/backend/app/ai/apps/` 子目录及 `__init__.py`
- 创建 `apps/backend/app/ai/prompts/` 子目录及 `__init__.py`
-`apps/backend/app/ai/schemas.py` 中定义所有 Pydantic 模型:
- `ChatStreamRequest`message, source_page, page_context, screen_content
- `SSEEvent`type, content, conversation_id, tokens_used, message
- `CacheTypeEnum`7 个枚举值)
- `ClueItem`category, summary, detail, emoji
- `ConsolidatedClueItem`(含 providers
- `App2InsightItem``App4Result``App5TacticsItem``App6Result``App7Result`
- `App2Result``App3Result``App8Result`
- _需求: 4.4, 5.2, 5.3, 6.3, 7.3, 8.2, 8.3, 9.2, 10.4, 10.5_
- [x] 1.3 编写属性测试AI 应用输出 JSON 结构验证
- **Property 8: AI 应用输出 JSON 结构验证**
- 使用 hypothesis 生成随机 JSON验证各应用 Pydantic 模型的解析和校验
- 验证 App3 category ∈ {客户基础, 消费习惯, 玩法偏好}App6/8 category ∈ 6 个枚举值
- 测试文件:`tests/test_p5_ai_integration_properties.py`
- **验证: 需求 4.4, 5.2, 5.3, 5.4, 6.3, 7.3, 8.2, 8.3, 8.4, 9.2, 10.4, 10.5**
- [x] 2. 百炼 API 统一封装层BailianClient
- [x] 2.1 实现 BailianClient 核心逻辑
- 文件:`apps/backend/app/ai/bailian_client.py`
- 使用 `openai` Python SDK`base_url` 指向百炼端点
- 实现 `chat_stream`流式AsyncGenerator[str, None]
- 实现 `chat_json`(非流式,返回 tuple[dict, int]
- 实现 `_inject_current_time`(首条消息注入 current_time
- 实现 `_call_with_retry`(指数退避,最多 3 次1s→2s→4s
- 定义异常类:`BailianApiError``BailianJsonParseError``BailianAuthError`
- 环境变量:`BAILIAN_API_KEY``BAILIAN_BASE_URL``BAILIAN_MODEL`
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
- [x] 2.2 编写属性测试:双模式调用一致性
- **Property 1: BailianClient 双模式调用一致性**
- Mock 百炼 API验证 `chat_stream` 返回非空 chunk 序列,`chat_json` 返回可解析 JSON + 正整数 tokens_used
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.1, 2.3, 2.4**
- [x] 2.3 编写属性测试:指数退避重试策略
- **Property 2: 指数退避重试策略**
- Mock 可控失败次数的 API验证重试间隔为 base_interval × 2^(n-1),超过 max_retries 抛出 BailianApiError
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.2**
- [x] 2.4 编写属性测试JSON 解析失败错误处理
- **Property 3: JSON 解析失败错误处理**
- Mock 返回非法 JSON 字符串,验证 `chat_json` 抛出 BailianJsonParseError
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.5**
- [x] 2.5 编写属性测试current_time 注入不变量
- **Property 4: current_time 注入不变量**
- 纯函数测试,随机消息列表,验证首条消息注入 current_timeISO 格式精确到秒),其余消息不变
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.6**
- [x] 3. 对话记录持久化服务ConversationService
- [x] 3.1 实现 ConversationService
- 文件:`apps/backend/app/ai/conversation_service.py`
- `create_conversation`:创建 ai_conversations 记录,系统调用时 user_id='system'
- `add_message`:写入 ai_messages 记录role, content, tokens_used
- `get_conversations`:按 user_id + site_id 查询created_at DESC分页page_size=20
- `get_messages`:按 conversation_id 查询所有消息
- _需求: 3.2, 3.4, 3.5, 3.7, 13.1, 13.2, 13.3_
- [x] 3.2 编写属性测试AI 调用记录持久化 round-trip
- **Property 5: AI 调用记录持久化 round-trip**
- 使用 test_zqyy_app 数据库,随机 app_id 和消息内容,验证写入后查询一致
- 测试文件:`apps/backend/tests/test_ai_conversation.py`
- **验证: 需求 3.2, 3.4, 3.5, 13.1, 13.2, 13.3**
- [x] 3.3 编写属性测试:历史对话列表排序与分页
- **Property 6: 历史对话列表排序与分页**
- 使用 test_zqyy_app 数据库,随机时间戳创建对话,验证返回严格降序且每页 ≤ page_size
- 测试文件:`apps/backend/tests/test_ai_conversation.py`
- **验证: 需求 3.7**
- [x] 4. AI 缓存读写服务AICacheService
- [x] 4.1 实现 AICacheService
- 文件:`apps/backend/app/ai/cache_service.py`
- `get_latest`:按 (cache_type, site_id, target_id) 查询最新记录
- `get_history`查询历史记录created_at DESC默认 limit=2用于 Prompt reference
- `write_cache`:写入缓存记录,写入后异步清理超限记录
- `_cleanup_excess`:保留最近 500 条,删除最旧的
- _需求: 12.1, 12.2, 12.3, 12.4, 12.5_
- [x] 4.2 编写属性测试:缓存写入 round-trip
- **Property 7: 缓存写入 round-trip**
- 使用 test_zqyy_app 数据库,随机 cache_type、target_id、result_json验证写入后查询一致
- 测试文件:`apps/backend/tests/test_ai_cache.py`
- **验证: 需求 4.7, 5.6, 6.6, 7.5, 8.6, 9.5, 10.10**
- [x] 4.3 编写属性测试:缓存查询 site_id 隔离
- **Property 13: 缓存查询 site_id 隔离**
- 使用 test_zqyy_app 数据库,写入 site_id=A 的记录,以 site_id=B 查询应返回空
- 测试文件:`apps/backend/tests/test_ai_cache.py`
- **验证: 需求 12.1, 12.5**
- [x] 4.4 编写属性测试:缓存保留上限
- **Property 14: 缓存保留上限**
- 使用 test_zqyy_app 数据库,批量写入 >500 条记录,验证清理后 ≤ 500
- 测试文件:`apps/backend/tests/test_ai_cache.py`
- **验证: 需求 12.3**
- [x] 5. 检查点 - 基础服务验证
- 确保所有测试通过ask the user if questions arise.
- 验证 BailianClient、ConversationService、AICacheService 三个核心服务可独立工作
- [x] 6. 应用 1 通用对话 SSE 端点
- [x] 6.1 实现 App1 Chat 核心逻辑
- 文件:`apps/backend/app/ai/apps/app1_chat.py`
- 每次进入 chat 页面新建 ai_conversations 记录(不复用)
- 首条消息注入页面上下文source_page、page_context、screen_content
- 用户消息立即写入 ai_messagesrole=user
- 流式返回完成后写入完整 assistant 回复(含 tokens_used
- 通过 `biz_params.user_prompt_params` 传入 User_ID、Role、Nickname
- 上下文注入框架留接口(页面文本化工具 P5-B 实现)
- _需求: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.8_
- [x] 6.2 实现 SSE 路由端点
- 文件:`apps/backend/app/routers/xcx_ai_chat.py`
- `POST /api/ai/chat/stream`SSE 协议推送Content-Type: text/event-stream
- SSE 事件格式chunk / done / error
- `GET /api/ai/conversations`:历史对话列表(分页,每页 20 条)
- `GET /api/ai/conversations/{conversation_id}/messages`:对话消息列表
- JWT 认证,从 token 提取 user_id、site_id、nickname、role
- 注册路由到 FastAPI app
- _需求: 3.1, 3.7_
- [x] 6.3 编写单元测试SSE 端点
- 验证 SSE Content-Type 和事件格式chunk/done/error
- 验证未认证返回 401、空消息返回 422
- 测试文件:`apps/backend/tests/test_ai_chat.py`
- _需求: 3.1_
- [x] 7. 应用 2 财务洞察(完整 Prompt
- [x] 7.1 实现 App2 Finance Prompt 模板
- 文件:`apps/backend/app/ai/prompts/app2_finance_prompt.py`
- 完整 Prompt 包含当期和上期收入结构table_fee=table_charge_money、assistant_pd=assistant_pd_money、assistant_cx=assistant_cx_money、goods=goods_money、recharge=充值 pay_amount settle_type=5
- 包含储值资产、费用汇总、平台结算数据
- 使用 items_sum 口径,禁止 consume_money
- _需求: 4.5, 4.6_
- [x] 7.2 实现 App2 Finance 核心逻辑
- 文件:`apps/backend/app/ai/apps/app2_finance.py`
- 8 个时间维度独立调用this_month, last_month, this_week, last_week, last_3_months, this_quarter, last_quarter, last_6_months
- 营业日分界点 08:00`BUSINESS_DAY_START_HOUR` 环境变量)
- 每次调用结果写入 ai_cachecache_type=app2_financetarget_id=时间维度编码)
- 每次调用创建 ai_conversations + ai_messages 记录
- 返回结构化 JSONinsights 数组seq + title + body
- _需求: 4.1, 4.2, 4.3, 4.4, 4.7_
- [x] 7.3 编写单元测试App2 时间维度计算
- 验证 8 个时间维度编码的计算逻辑(营业日分界点 08:00
- 验证 Prompt 使用 items_sum 口径字段映射
- 测试文件:`apps/backend/tests/test_ai_app2.py`
- _需求: 4.3, 4.6_
- [x] 8. 应用 3/4/5/6/7 骨架实现
- [x] 8.1 实现 App3 Clue 骨架
- 文件:`apps/backend/app/ai/apps/app3_clue.py`
- `run` 函数:构建 Prompt → 调用百炼 → 写入 conversation + cache
- `build_prompt`:留接口,返回占位 Prompt标注待细化字段consumption_records 等待 P9-T1
- 线索 category 限定 3 个枚举值providers 标记为"系统"
- 使用 items_sum 口径
- Prompt reference 包含 App6 线索 + 最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app3_cluetarget_id=member_id
- _需求: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8_
- [x] 8.2 实现 App4 Analysis 骨架
- 文件:`apps/backend/app/ai/apps/app4_analysis.py`
- `build_prompt`留接口service_history、assistant_info 待 P6-T4
- Prompt reference 包含 App8 最新 + 最近 2 套历史(附 generated_at
- 缓存不存在时 reference 传空对象,标注"暂无历史线索"
- 结果写入 ai_cachecache_type=app4_analysistarget_id=`{assistant_id}_{member_id}`
- _需求: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7_
- [x] 8.3 实现 App5 Tactics 骨架
- 文件:`apps/backend/app/ai/apps/app5_tactics.py`
- 接收 App4 完整返回结果作为 Prompt 中的 task_suggestion 字段
- `build_prompt`留接口service_history、assistant_info 随 App4 同步在 P6-T4
- Prompt reference 包含最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app5_tacticstarget_id=`{assistant_id}_{member_id}`
- _需求: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_
- [x] 8.4 实现 App6 Note 骨架
- 文件:`apps/backend/app/ai/apps/app6_note.py`
- `build_prompt`留接口consumption_data 待 P9-T1
- 返回 score1-10+ clues 数组category 限定 6 个枚举值
- 线索提供者标记为当前备注提供人
- 评分规则6 分为标准分
- Prompt reference 包含 App3 线索 + 最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app6_note_analysistarget_id=member_idscore 存入 ai_cache.score
- _需求: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8_
- [x] 8.5 实现 App7 Customer 骨架
- 文件:`apps/backend/app/ai/apps/app7_customer.py`
- `build_prompt`留接口objective_data 待 P9-T1
- 使用 items_sum 口径
- 对主观信息标注【来源XXX请甄别信息真实性】
- Prompt reference 包含最新 + 最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app7_customer_analysistarget_id=member_id
- _需求: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7_
- [x] 8.6 编写属性测试Prompt reference 历史注入
- **Property 9: Prompt reference 历史注入**
- Mock 缓存数据,验证各应用 build_prompt 的 reference 字段包含正确的缓存结果和 generated_at 时间戳
- 缓存不存在时 reference 为空对象
- 测试文件:`apps/backend/tests/test_ai_apps_prompt.py`
- **验证: 需求 5.8, 6.4, 6.5, 7.2, 7.4, 8.8, 9.7**
- [x] 9. 应用 8 维客线索整理(完整 Prompt+ ClueWriter
- [x] 9.1 实现 App8 Consolidation Prompt 模板
- 文件:`apps/backend/app/ai/prompts/app8_consolidation_prompt.py`
- 完整 Prompt接收 App3 和 App6 全部线索(附 generated_at整合去重
- 分类标签限定 6 个枚举值(与 member_retention_clue CHECK 约束一致)
- 合并相似线索(多提供者逗号分隔),其余原文返回,最小改动原则
- _需求: 10.3, 10.4, 10.5, 10.6_
- [x] 9.2 实现 ClueWriter 全量替换逻辑
- 集成在 `apps/backend/app/ai/apps/app8_consolidation.py`
- DELETE source IN ('ai_consumption', 'ai_note') → INSERT 新线索(事务)
- 字段映射emoji+summary 拼接、providers→recorded_by_name、source 判断逻辑
- recorded_by_assistant_id 填 NULL
- 人工线索source='manual')不受影响
- _需求: 10.7, 10.8, 10.9_
- [x] 9.3 实现 App8 Consolidation 核心逻辑
- 文件:`apps/backend/app/ai/apps/app8_consolidation.py`
- `run` 函数:构建 Prompt → 调用百炼 → 写入 conversation + cache + member_retention_clue
- 结果同时写入 ai_cachecache_type=app8_clue_consolidatedtarget_id=member_id
- _需求: 10.1, 10.2, 10.10_
- [x] 9.4 编写属性测试ClueWriter 全量替换不变量
- **Property 12: ClueWriter 全量替换不变量**
- 使用 test_zqyy_app 数据库,随机线索列表 + 预置人工线索
- 验证AI 线索数量 = new_clues 数量、人工线索不变、recorded_by_assistant_id=NULL、summary=emoji+空格+原始 summary
- 测试文件:`apps/backend/tests/test_ai_clue_writer.py`
- **验证: 需求 10.7, 10.8, 10.9**
- [x] 10. 检查点 - 应用层验证
- 确保所有测试通过ask the user if questions arise.
- 验证 8 个应用的 run 函数可独立调用Mock 百炼 API
- [x] 11. 事件调度与调用链编排AIDispatcher
- [x] 11.1 实现 AIDispatcher 核心逻辑
- 文件:`apps/backend/app/ai/dispatcher.py`
- `handle_consumption_event`App3 → App8 → App7+ App4 → App5 如有助教)
- `handle_note_event`App6 → App8
- `handle_task_assign_event`App4 → App5读已有 App8 缓存)
- `_run_chain`:串行执行调用链,某步失败记录日志后继续
- 容错:失败应用记录错误日志 + 写入失败 conversation后续应用使用已有缓存
- 整条链后台异步执行,不阻塞业务请求
- _需求: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_
- [x] 11.2 集成事件触发点
-`trigger_scheduler.fire_event()` 中注册 AI 事件处理器
- 消费事件consumption_settled`ai_dispatcher.handle_consumption_event`
- 备注事件note_created`ai_dispatcher.handle_note_event`
- 任务分配事件task_assigned`ai_dispatcher.handle_task_assign_event`
- _需求: 5.1, 6.1, 6.2, 7.1, 8.1, 9.1, 11.1, 11.2, 11.3, 11.4_
- [x] 11.3 编写属性测试:事件调用链顺序正确性
- **Property 10: 事件调用链顺序正确性**
- Mock 所有应用,记录调用序列,验证四种事件链的严格顺序
- 测试文件:`apps/backend/tests/test_ai_dispatcher.py`
- **验证: 需求 11.1, 11.2, 11.3, 11.4, 11.5, 11.6**
- [x] 11.4 编写属性测试:调用链容错不变量
- **Property 11: 调用链容错不变量**
- Mock 随机应用失败,验证后续应用继续执行且失败应用有错误日志
- 测试文件:`apps/backend/tests/test_ai_dispatcher.py`
- **验证: 需求 11.7**
- [x] 12. 缓存查询路由与环境配置
- [x] 12.1 实现缓存查询路由
- 文件:`apps/backend/app/routers/xcx_ai_cache.py`
- `GET /api/ai/cache/{cache_type}?target_id=xxx`:查询最新缓存
- JWT 认证site_id 从 token 提取强制过滤
- 注册路由到 FastAPI app
- _需求: 12.1, 12.2, 12.5_
- [x] 12.2 新增环境变量配置
-`.env.template` 中添加 `BAILIAN_API_KEY``BAILIAN_BASE_URL``BAILIAN_MODEL``BUSINESS_DAY_START_HOUR`
- 在后端配置加载逻辑中读取这些变量,缺失时报错
- _需求: 2.1, 14.1, 14.2_
- [x] 13. 百炼技术方案确认文档 ✅
- [x] 13.1 输出百炼技术方案确认文档
- 文件:`docs/reports/bailian-technical-solution.md`
- 确认流式返回方案OpenAI 兼容 SSE
- 确认 JSON 输出模式response_format + System Prompt 约束)
- 确认 SDK 选择openai Python SDK + base_url 指向百炼)
- 作为 BailianClient 实现的依据
- _需求: 14.1, 14.2, 14.3_
- [x] 14. 最终检查点 - 全量验证 ✅
- 全部 9 个测试文件、62 个测试用例通过2026-03-09
- 验证所有路由注册正确、事件触发点集成完毕、环境变量配置完整
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP 交付
- 每个任务引用具体需求编号以确保可追溯
- 属性测试验证设计文档中定义的 14 个正确性属性
- 使用 test_zqyy_app 测试库执行数据库相关测试,禁止连接正式库
- App3/4/5/6/7 的 Prompt 细化将在 P5-B 阶段P6/P9 对应任务)中完成

View File

@@ -0,0 +1,252 @@
# 设计文档ETL 全流程前后端联调etl-fullstack-integration
## 概述
本 Spec 是一个运维联调任务,不涉及新功能开发。目标是验证 `admin-web-console` Spec 产出的前后端代码在真实环境下的端到端正确性,同时收集性能数据。
核心流程:
1. 启动后端 + 前端服务
2. 通过 API 登录获取 JWT
3. 提交全流程 ETL 任务api_full, full_window, force-full, 全选常用任务, 自定义窗口 2025-11-01~当前时间, 30天切分, 全部门店)
4. 实时监控执行过程,捕获错误/警告
4. 执行完成后进行黑盒数据一致性测试(全链路检查器 `scripts/ops/etl_consistency_check.py` + FlowRunner 内置 `ConsistencyChecker`
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. 黑盒数据一致性测试
│ ├── 全链路检查器scripts/ops/etl_consistency_check.py
│ │ ├── API JSON vs ODS字段完整性 + 值采样比对
│ │ ├── ODS vs DWD行数 + 字段映射 + 值比对(含 EX 表合并)
│ │ ├── DWD vs DWS聚合表行数 + 数值列健全性检查
│ │ └── 白名单机制ETL_META_COLS / SCD2_COLS / 空字符串≡None
│ ├── FlowRunner 内置检查quality/consistency_checker.py自动执行
│ └── etl-data-consistency Hook可选手动触发
└── 5. 报告生成
└── 输出到 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": "", # 补充当前时间
"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`
- 计时解析:从日志中提取时间戳,计算阶段耗时
## 黑盒数据一致性测试
### 两套检查工具的定位
| 工具 | 路径 | 触发方式 | 覆盖范围 | 适用场景 |
|------|------|---------|---------|---------|
| 全链路检查器 | `scripts/ops/etl_consistency_check.py` | 手动运行 / `etl-data-consistency` Hook | API→ODS→DWD→DWS 四层全链路 | 联调后独立全面验证(本 Spec 主要使用) |
| FlowRunner 内置检查 | `apps/etl/connectors/feiqiu/quality/consistency_checker.py` | FlowRunner 自动调用 `_run_post_consistency_check()` | API→ODS 字段 + ODS→DWD 映射/值 | ETL 执行后自动轻量检查 |
联调场景下FlowRunner 在 ETL 执行完成后已自动运行内置检查并输出报告到 `ETL_REPORT_ROOT`。联调脚本额外运行全链路检查器,覆盖 DWD→DWS 聚合验证和更深入的值采样比对。
### etl-data-consistency Hook
`.kiro/hooks/etl-data-consistency.kiro.hook` 提供手动触发入口,执行 `scripts/ops/etl_consistency_check.py`。联调任务 5 也可通过此 Hook 替代手动运行脚本。
### 白名单机制
全链路检查器在值比对时使用三层白名单过滤,避免 ETL 框架自动填充的列和已知等价差异产生误报:
#### 1. ETL 元数据列白名单(`ETL_META_COLS`
不参与 API↔ODS 值比对的列:
```python
ETL_META_COLS = {"source_file", "source_endpoint", "fetched_at", "payload", "content_hash"}
```
这些列由 ETL 框架在 ODS 落库时自动填充API 源数据中不存在。
#### 2. SCD2 管理列白名单(`SCD2_COLS`
不参与 ODS↔DWD 值比对的列:
```python
SCD2_COLS = {
"valid_from", "valid_to", "is_current", "etl_loaded_at", "etl_batch_id",
"scd2_start_time", "scd2_end_time", "scd2_is_current", "scd2_version",
}
```
这些列由 DWD 层 SCD2 逻辑自动维护ODS 源数据中不存在。
#### 3. 空字符串 vs None 等价规则(`_values_differ()`
API 返回空字符串 `""` 而数据库存储为 `None` 时,视为等价(不算差异),但标记为 `whitelist`
```python
# API 空字符串 "" vs DB None → 白名单(等价但标记)
if api_val is not None and ods_val is None:
if isinstance(api_val, str) and api_val.strip() == "":
return False, "whitelist"
```
报告中白名单差异以折叠 `<details>` 块展示,不计入失败统计。
#### 4. FlowRunner 内置检查的白名单
`consistency_checker.py` 使用类似但略有不同的白名单:
- `ODS_META_COLUMNS`:与 `ETL_META_COLS` 相同,额外包含 `record_index`
- `KNOWN_NO_SOURCE`:按表配置的已知无源字段(如 `dwd.dim_member.update_time`),标记为已知无源而非报错
### 调用方式
联调脚本在 ETL 全流程执行完成后,运行全链路检查器:
```bash
cd C:\NeoZQYY
uv run python scripts/ops/etl_consistency_check.py
```
脚本自动完成:
1.`LOG_ROOT` 找到最近一次 ETL 日志,解析执行的任务列表
2.`FETCH_ROOT` 读取 API JSON 落盘文件
3. 连接数据库(`PG_DSN`),逐表逐字段比对:
- API JSON vs ODS字段完整性 + 值采样比对(随机 5 条)
- ODS vs DWD行数 + 字段映射 + 值比对(含 EX 表合并)
- DWD vs DWS聚合表行数 + 数值列健全性NULL 率、负值、min/max
4. 输出 Markdown 报告到 `ETL_REPORT_ROOT`
### 检查内容
| 检查类型 | 对比对象 | 检查项 | 白名单处理 |
|---------|---------|--------|-----------|
| API vs ODS | API JSON 缓存 vs ODS 表 | 字段完整性 + 值采样比对5 条记录) | `ETL_META_COLS` 排除空字符串≡None |
| ODS vs DWD | ODS 表 vs DWD 表(含 EX 表) | 行数对比 + 字段映射 + 值比对 | `SCD2_COLS` 排除空字符串≡None |
| DWD vs DWS | DWD 源表 vs DWS 聚合表 | 行数非空 + 数值列健全性NULL 率、负值、min/max | 无DWS 为聚合结果,不做逐行值比对) |
### 报告格式
全链路检查器输出 Markdown 报告,包含:
1. ETL 执行概览(任务列表、成功/失败/跳过统计)
2. API↔ODS 数据一致性(逐表逐字段值比对,白名单差异折叠展示)
3. ODS↔DWD 数据一致性(行数对比 + 映射验证 + 值采样,含字段级统计)
4. DWD↔DWS 数据一致性DWS 表概览 + 数值列健全性检查)
5. 异常汇总与建议
### 参考数据
`dataflow-field-completion` 的实际执行结果API vs ODS 22/22 通过ODS vs DWD 38/42 通过。本次联调执行 api_full 全流程后,预期结果应与此一致或更优(因为联调包含最新的字段补全)。
## 报告格式
报告输出为 Markdown 文件,路径:`{SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md`
```markdown
# ETL 全流程联调报告
## 执行概要
- 任务参数:...
- 开始时间 / 结束时间 / 总时长
- 退出码 / 最终状态
## 性能报告
- 各窗口切片耗时对比表
- Top-5 耗时阶段
- 总体吞吐量估算
## 黑盒测试报告
- API vs ODSX/Y 张表通过(白名单差异 N 处)
- ODS vs DWDX/Y 张表通过(白名单差异 N 处)
- DWD vs DWSX 张表有数据 / Y 张总计,异常指标 N 个
- 失败表清单及差异明细
## DEBUG 报告(如有)
- 错误摘要
- 警告摘要
- 相关日志片段
- 可能的原因分析
```
## 正确性属性
本 Spec 为运维联调任务,不涉及新功能代码开发,因此不定义形式化的属性测试。验证通过以下方式进行:
- 服务健康检查通过
- 任务提交成功并开始执行
- 执行完成后退出码和日志符合预期
- 黑盒数据一致性测试通过(全链路检查器覆盖 API→ODS→DWD→DWS 四层,白名单差异不计入失败)
- 报告文件成功生成(含性能报告和黑盒测试报告)
## 测试策略
本 Spec 本身就是一次集成测试。不额外编写单元测试或属性测试。验证标准:
- 后端 API 响应正确
- ETL CLI 子进程正常启动和执行
- 日志正确捕获和推送
- 黑盒数据一致性测试通过(全链路检查器 API→ODS→DWD→DWS + FlowRunner 内置检查)
- 报告文件正确生成到 ETL_REPORT_ROOT全链路检查报告和 SYSTEM_LOG_ROOT联调综合报告
### 黑盒测试验证标准
- API vs ODS所有已采集的 ODS 表字段完整性和值采样检查通过(白名单差异不计入失败)
- ODS vs DWD所有已配置映射的表行数和值比对检查通过SCD2 列排除,白名单差异不计入失败)
- DWD vs DWS所有 DWS 聚合表行数非空,数值列无异常(高 NULL 率、金额负值等)
- 全链路检查报告 `consistency_check_<timestamp>.md` 成功生成到 ETL_REPORT_ROOT
- 综合联调报告中包含黑盒测试结果摘要(含白名单差异统计)

View File

@@ -0,0 +1,85 @@
# 需求文档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~当前时间 + 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黑盒数据一致性测试
**用户故事:** 作为开发者,我希望在 ETL 全流程执行完成后,以黑盒视角对比数据上下游的字段差异和值差异,验证数据从 API 到 ODS 再到 DWD 再到 DWS 的完整性和正确性。
#### 验收标准
1. WHEN ETL 全流程执行完成后, THE 联调脚本 SHALL 运行全链路检查器 `scripts/ops/etl_consistency_check.py`,执行 API→ODS→DWD→DWS 四层数据一致性检查
2. WHEN 执行 API vs ODS 检查时, THE 检查器 SHALL 对比 API JSON 落盘数据与 ODS 落库数据的字段完整性和值采样(随机 5 条记录的关键字段),覆盖所有已采集的 ODS 表
3. WHEN 执行 ODS vs DWD 检查时, THE 检查器 SHALL 对比 ODS 数据与 DWD 落库数据的行数、字段映射正确性和值一致性(含 EX 表合并比对)
4. WHEN 执行 DWD vs DWS 检查时, THE 检查器 SHALL 验证 DWS 聚合表的行数非空、数值列健全性NULL 率、负值、min/max标注异常指标
5. WHEN 值比对遇到白名单场景时, THE 检查器 SHALL 将 ETL 元数据列(`source_file``source_endpoint``fetched_at``payload``content_hash`)和 SCD2 管理列排除在值比对之外,并将 API 空字符串 `""` vs DB `None` 视为等价(标记为 whitelist
6. WHEN 黑盒测试完成后, THE 检查器 SHALL 输出 Markdown 报告到 `ETL_REPORT_ROOT` 环境变量指定的目录
7. WHEN 黑盒测试报告生成后, THE 报告 SHALL 包含每张表的检查结果、差异明细(含白名单差异折叠展示)、通过/失败状态、字段级统计、以及汇总统计
### 需求 6联调报告输出
**用户故事:** 作为开发者,我希望联调完成后获得一份综合报告,包含执行情况、性能数据、黑盒测试结果和可能的 DEBUG 信息。
#### 验收标准
1. THE 报告 SHALL 包含:执行概要(任务参数、开始/结束时间、总时长、退出码)
2. THE 报告 SHALL 包含性能报告各阶段耗时、窗口切片耗时对比、Top-5 瓶颈)
3. THE 报告 SHALL 包含黑盒测试结果摘要API vs ODS 通过数/总数、ODS vs DWD 通过数/总数、DWD vs DWS 表概览、失败表清单、白名单差异数量)
4. IF 执行过程中出现错误或警告, THEN THE 报告 SHALL 包含 DEBUG 报告(错误摘要、相关日志片段、可能的原因分析)
5. THE 报告 SHALL 输出到 `SYSTEM_LOG_ROOT` 环境变量指定的目录

View File

@@ -0,0 +1,131 @@
# 实现计划ETL 全流程前后端联调etl-fullstack-integration
## 概述
基于 `admin-web-console` 已完成的前后端代码,进行端到端联调验证。全程使用 Playwright 浏览器模拟真实用户操作(登录、配置、提交、监控),不直接调用 API。通过管理后台 UI 提交 api_full 全流程 ETL 任务(自定义窗口 2025-11-01~当前时间30天切分force-full全选常用任务实时监控执行过程收集性能数据执行黑盒数据一致性测试全链路检查器 `scripts/ops/etl_consistency_check.py` + FlowRunner 内置 `ConsistencyChecker`),最终生成综合报告。
## 任务
- [x] 1. 服务启动与健康检查
- [x] 1.1 启动后端服务(`apps/backend/`uvicorn :8000确认 API 可达
- 使用 `controlPwshProcess` 启动 `uvicorn app.main:app --host 0.0.0.0 --port 8000`cwd 为 `apps/backend/`
- 等待服务就绪,验证 `http://localhost:8000/docs` 可访问
- _Requirements: 1.1_
- [x] 1.2 启动前端服务(`apps/admin-web/`pnpm dev :5173确认页面可访问
- 使用 `controlPwshProcess` 启动 `pnpm dev`cwd 为 `apps/admin-web/`
- 等待 Vite 就绪,验证 `http://localhost:5173` 可访问
- _Requirements: 1.2_
- [x] 1.3 浏览器登录与健康检查
- 使用 Playwright 打开 `http://localhost:5173`,应自动跳转到 `/login`
- 在登录表单中输入用户名 `admin`、密码 `admin123`,点击登录按钮
- 验证登录成功后跳转到任务配置页面(`/`
- 确认侧边栏导航菜单正常渲染任务配置、任务管理、ETL 状态、数据库、日志、环境配置、运维面板)
- _Requirements: 1.3, 1.4, 1.5_
- [-] 2. 浏览器操作:任务配置与提交
- [x] 2.1 在任务配置页面填写全流程参数
- 在任务配置页面(`/`),选择 Flow 为 `api_full`API → ODS → DWD → DWS → INDEX
- 选择处理模式为 `full_window`(全窗口)
- 设置时间窗口模式为"自定义",填入开始时间 `2025-11-01`、结束时间 当前时间
- 设置窗口切分为"按天",切分天数为 `30`
- 勾选 `force_full`(强制全量)
- 在任务选择区域,全选 `is_common=True` 的常用任务(共 41 个)
- 确认 CLI 命令预览区域显示完整参数(--flow api_full --processing-mode full_window --window-start ... --window-end ... --window-split day --window-split-days 30 --force-full --tasks ...
- _Requirements: 2.1_
- [x] 2.2 通过浏览器提交任务执行
- 点击"直接执行"按钮SendOutlined 图标),触发 `POST /api/execution/run`
- 确认页面显示任务提交成功的提示消息
- 记录返回的 execution_id从页面响应或跳转中获取
- _Requirements: 2.2, 2.4_
- [x] 3. 浏览器操作:执行监控与 DEBUG
- [x] 3.1 在任务管理页面监控执行状态
- 导航到"任务管理"页面(`/task-manager`),点击侧边栏"任务管理"菜单
- 在"队列"Tab 中确认刚提交的任务状态为 `running`
- 点击 running 状态的任务行,打开 WebSocket 实时日志流抽屉
- 持续观察日志输出,每 30 秒检查一次页面状态
- 检测日志中的 ERROR / CRITICAL / Traceback / Exception / WARNING 关键字
- 如果连续 30 分钟无新日志输出,报告超时警告
- 任务完成success/failed/cancelled时停止监控
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
- [x] 3.2 对执行过程中发现的错误/警告进行 DEBUG 分析
- 从日志流中收集所有 ERROR 和 WARNING 日志行及其上下文
- 分析错误类型API 超时、数据库连接、数据质量、配置问题等
- 如果任务失败,切换到"历史"Tab 查看完整执行详情和日志
- 记录 DEBUG 发现到报告中
- _Requirements: 3.2, 3.5_
- [x] 4. 性能计时与报告生成
- [x] 4.1 从浏览器获取执行日志,提取精细计时数据
- 在"任务管理"→"历史"Tab 中,点击已完成的任务查看执行详情
- 通过 `GET /api/execution/{id}/logs` 获取完整日志(可通过浏览器或 API 辅助)
- 从日志中提取每个窗口切片30天的开始/结束时间
- 计算每个切片的耗时
- 识别 ODS / DWD / DWS / INDEX 各阶段的耗时
- 标注 Top-5 耗时最长的阶段/任务
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 4.2 生成综合联调报告,输出到 SYSTEM_LOG_ROOT
- 报告包含:执行概要(参数、时间、退出码)
- 报告包含性能报告各切片耗时对比、Top-5 瓶颈)
- 报告包含DEBUG 报告(如有错误/警告)
- 黑盒测试结果摘要将在任务 5.3 中追加
- 输出路径:`{SYSTEM_LOG_ROOT}/{date}__etl_integration_report.md`
- 路径通过 `SYSTEM_LOG_ROOT` 环境变量获取,缺失时报错
- _Requirements: 6.1, 6.2, 6.4, 6.5_
- [x] 5. 黑盒数据一致性测试
- [x] 5.1 运行全链路检查器,执行 API→ODS→DWD→DWS 四层数据一致性检查
- 运行 `uv run python scripts/ops/etl_consistency_check.py`cwd 为项目根目录 `C:\NeoZQYY`
- 脚本自动从 `LOG_ROOT` 找到最近一次 ETL 日志,解析本次执行的任务列表
- 脚本自动从 `FETCH_ROOT` 读取 API JSON 落盘文件
- 脚本连接数据库(`PG_DSN`),逐表逐字段比对:
- API JSON vs ODS字段完整性 + 值采样比对(随机 5 条记录),`ETL_META_COLS` 白名单列排除
- ODS vs DWD行数对比 + 字段映射 + 值比对(含 EX 表合并),`SCD2_COLS` 白名单列排除
- DWD vs DWS聚合表行数非空检查 + 数值列健全性NULL 率、负值、min/max
- 白名单处理API 空字符串 `""` vs DB `None` 视为等价,标记为 whitelist不计入失败
- 报告自动输出到 `ETL_REPORT_ROOT`(环境变量,缺失时报错)
- 备选触发方式:可通过 `etl-data-consistency` Hook 手动触发(效果等同)
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
- [x] 5.2 检查 FlowRunner 内置一致性报告
- FlowRunner 在 ETL 执行完成后已自动调用 `_run_post_consistency_check()` 生成报告到 `ETL_REPORT_ROOT`
- 确认内置报告已生成,检查 API vs ODS 和 ODS vs DWD 的通过/失败统计
- 内置检查使用 `ODS_META_COLUMNS` 白名单(含 `record_index`)和 `KNOWN_NO_SOURCE` 按表白名单
- 对比两份报告的结论是否一致(全链路检查器 vs FlowRunner 内置检查)
- _Requirements: 5.1, 5.2, 5.3_
- [x] 5.3 将黑盒测试结果摘要写入综合联调报告
- 在任务 4.2 生成的联调报告中追加"黑盒测试报告"章节
- 包含API vs ODS 通过数/总数、ODS vs DWD 通过数/总数、DWD vs DWS 表概览
- 包含:白名单差异数量统计、失败表清单
- 引用全链路检查报告的完整路径
- _Requirements: 6.3_
- [x] 6. 服务清理
- [x] 6.1 关闭浏览器,停止后端和前端服务,清理资源
- 关闭 Playwright 浏览器实例
- 停止 uvicorn 后端进程(`controlPwshProcess` stop
- 停止 pnpm dev 前端进程(`controlPwshProcess` stop
- 报告联调完成状态
## 说明
- 本 Spec 为运维联调任务,不涉及新功能代码开发
- 不编写属性测试或单元测试,联调本身即为集成验证
- **全程使用 Playwright 浏览器模拟真实用户操作**:登录、页面导航、表单填写、按钮点击、日志查看等均通过浏览器完成
- **黑盒测试使用两套工具**
- 全链路检查器 `scripts/ops/etl_consistency_check.py`:覆盖 API→ODS→DWD→DWS 四层,联调主要使用
- FlowRunner 内置 `ConsistencyChecker``quality/consistency_checker.py`ETL 执行后自动运行,覆盖 API→ODS + ODS→DWD
- **白名单机制**`ETL_META_COLS`ODS 元数据列)、`SCD2_COLS`SCD2 管理列排除在值比对之外API 空字符串 `""` vs DB `None` 视为等价
- **`etl-data-consistency` Hook**`.kiro/hooks/etl-data-consistency.kiro.hook`)可作为手动触发全链路检查的替代方式
- 黑盒测试在 ETL 全流程执行完成后、服务清理前执行,确保数据库中有最新数据可供对比
- 全选常用任务 = 任务注册表中 `is_common=True` 的所有任务(共 41 个)
- "全部门店":当前系统仅有 site_id=1默认管理员绑定如需多门店需逐个执行
- 监控允许空闲等待,最长 30 分钟无新日志才报超时
- 报告输出路径遵循 export-paths 规范:全链路检查报告输出到 `ETL_REPORT_ROOT`,联调综合报告输出到 `SYSTEM_LOG_ROOT`
- 全链路检查器需要 `PG_DSN``FETCH_ROOT``LOG_ROOT``ETL_REPORT_ROOT` 环境变量,缺失时报错

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,177 @@
# 需求文档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 在导航栏或状态栏显示执行中的视觉指示
### 需求 11租户管理员账号管理
**用户故事:** 作为系统管理员Operator我需要在系统管理后台中创建和管理租户管理员账号以便租户管理员能登录独立的租户管理后台`apps/tenant-admin/`进行用户审核、Excel 上传等操作。
#### 验收标准
1. WHEN Operator 打开租户管理员管理页面, THE Admin_Web SHALL 展示所有租户管理员账号列表,包含用户名、所属租户、管辖球房列表、账号状态(启用/禁用)、创建时间
2. WHEN Operator 创建租户管理员账号, THE Backend_API SHALL 接受用户名、初始密码、所属租户标识、管辖球房 ID 列表(`site_id` 数组),并在 `auth` Schema 中创建对应记录,密码以 bcrypt 哈希存储
3. WHEN Operator 编辑租户管理员账号, THE Backend_API SHALL 允许修改管辖球房列表、账号状态(启用/禁用),以及重置密码
4. WHEN Operator 禁用某租户管理员账号, THE Backend_API SHALL 将该账号状态设为禁用,该管理员后续登录租户管理后台时 SHALL 被拒绝
5. WHEN Operator 为租户管理员分配球房, THE Backend_API SHALL 验证球房 ID`site_id`)在 `auth.site_code_mapping` 中存在,不存在时返回 422 错误
6. THE Backend_API SHALL 确保同一用户名不可重复创建(唯一约束)
7. WHEN Operator 查看某租户管理员详情, THE Admin_Web SHALL 展示该管理员管辖的球房列表及每个球房的球房代码(`site_code`)和名称
### 需求 12球房编号管理
**用户故事:** 作为系统管理员Operator我需要在系统管理后台中为每个门店`site_id`)分配球房编号(`site_code`),以便小程序用户申请时通过球房编号定位到对应门店。
#### 验收标准
1. WHEN Operator 打开球房编号管理页面, THE Admin_Web SHALL 展示 `auth.site_code_mapping` 中所有球房编号映射列表,包含球房编号(`site_code`)、门店 ID`site_id`)、创建时间
2. WHEN Operator 新增球房编号映射, THE Backend_API SHALL 接受 `site_code`格式2 字母 + 3 数字,如 `AB123`)和 `site_id`BIGINT验证格式正确后写入 `auth.site_code_mapping`
3. IF 提交的 `site_code` 已存在, THEN THE Backend_API SHALL 返回 409 冲突错误
4. IF 提交的 `site_id` 已绑定其他 `site_code`, THEN THE Backend_API SHALL 返回 409 冲突错误(`site_code``site_id` 一对一)
5. WHEN Operator 编辑球房编号映射, THE Backend_API SHALL 允许修改 `site_code`(需验证新编号不与其他记录冲突)
6. WHEN Operator 删除球房编号映射, THE Backend_API SHALL 检查是否有用户申请引用该 `site_code`,若有则拒绝删除并提示关联数据存在
7. THE Admin_Web SHALL 在球房编号管理页面提供搜索功能,支持按 `site_code``site_id` 搜索
### 需求 13Task_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 @@
{"specId": "98a585de-82d9-4bbd-bed8-179208c12f8b", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,422 @@
# 设计文档业务日分割点机制Business Day Cutoff
## 概述
本设计将全系统的统计时间口径从自然日切换为以可配置小时值(默认 08:00为分割点的营业日。影响范围覆盖六个层面
1. **配置层**`.env` 新增 `BUSINESS_DAY_START_HOUR`ETL `AppConfig` 和后端 `config.py` 同步加载
2. **共享工具层**`packages/shared``datetime_utils.py` 扩展 `business_day_range``business_week_range``business_month_range` 三个范围函数
3. **ETL DWS 层**`BaseDwsTask.iter_dwd_rows``DATE()` 替换为 `biz_date_sql_expr`18 个具体 DWS 任务的 SQL 全面改造
4. **后端 API 层**:新增 `/api/config/business-day` 端点,时间范围查询统一使用 `business_*_range` 函数
5. **数据库层**:新增 PostgreSQL `biz_date()` 函数,物化视图迁移
6. **前端展示层**:管理后台日期选择器标注营业日口径,小程序透传后端数据
### 设计决策
1. **单一配置源**`BUSINESS_DAY_START_HOUR` 仅在根 `.env` 定义一次ETL 通过 `AppConfig`、后端通过 `config.py`、数据库通过迁移脚本参数化读取,避免多处硬编码导致不一致。
2. **共享包作为唯一逻辑实现**:所有营业日归属计算集中在 `packages/shared/datetime_utils.py`ETL 和后端均从此导入,禁止各子系统重复实现。
3. **SQL 表达式生成器模式**`biz_date_sql_expr(col, hour)` 生成 `DATE(col - INTERVAL 'N hours')` 字符串DWS 任务在 SQL 拼接时调用,避免在 Python 侧逐行转换。
4. **BaseDwsTask 基类统一改造**`iter_dwd_rows` 的日期过滤从 `DATE(col)` 改为 `biz_date_sql_expr(col)`,所有子类自动继承,减少逐任务修改量。
5. **物化视图通过迁移脚本重建**:物化视图的时间过滤条件无法动态参数化,需通过 SQL 迁移脚本 DROP + CREATE 重建。
6. **历史数据重算采用 CLI 批量模式**:提供独立重算脚本,复用正式 ETL 任务逻辑(相同 `Business_Day_Cutoff` 配置),按日期窗口分批执行。
## 架构
```mermaid
graph TD
subgraph 配置层
ENV[".env<br/>BUSINESS_DAY_START_HOUR=8"]
ENV --> AC[AppConfig<br/>app.business_day_start_hour]
ENV --> BC[Backend config.py<br/>BUSINESS_DAY_START_HOUR]
end
subgraph 共享工具层
DU["datetime_utils.py<br/>business_date / business_*_range<br/>biz_date_sql_expr"]
end
subgraph ETL DWS 层
BDT["BaseDwsTask<br/>iter_dwd_rowsbiz_date_sql_expr<br/>get_time_window_range"]
BDT --> FT["FinanceBaseTask / FinanceDailyTask<br/>FinanceRechargeTask / FinanceDiscountTask<br/>FinanceIncomeTask"]
BDT --> AT["AssistantDailyTask / AssistantMonthlyTask<br/>AssistantFinanceTask / AssistantCustomerTask<br/>AssistantOrderContributionTask"]
BDT --> MT["MemberVisitTask / MemberConsumptionTask"]
BDT --> GT["GoodsStockDailyTask / WeeklyTask / MonthlyTask"]
BDT --> IT["SpendingPowerIndexTask / MemberIndexBase"]
BDT --> MV["MvRefreshTask"]
end
subgraph 后端 API 层
API["/api/config/business-day<br/>GET → business_day_start_hour"]
TR["时间范围计算<br/>business_day_range / week_range / month_range"]
end
subgraph 数据库层
PGF["biz_date(timestamptz, int)<br/>PostgreSQL 函数"]
MVR["物化视图重建<br/>迁移脚本"]
end
subgraph 前端
AW["Admin_Web<br/>日期选择器标注"]
MP["Miniprogram<br/>透传后端数据"]
end
AC --> BDT
AC --> DU
BC --> API
BC --> TR
DU --> BDT
DU --> TR
API --> AW
API --> MP
PGF --> MVR
```
## 组件与接口
### 1. 共享时间工具(`packages/shared/src/neozqyy_shared/datetime_utils.py`
现有函数(已实现):
- `business_date(dt, day_start_hour) -> date`
- `business_month(dt, day_start_hour) -> date`
- `business_week_monday(dt, day_start_hour) -> date`
- `biz_date_sql_expr(col, day_start_hour) -> str`
新增函数:
```python
def business_day_range(biz_date: date, day_start_hour: int = 8) -> tuple[datetime, datetime]:
"""返回给定营业日的精确时间戳范围 [start, end)。
start = biz_date 当天 day_start_hour:00
end = biz_date 次日 day_start_hour:00
"""
def business_week_range(week_monday: date, day_start_hour: int = 8) -> tuple[datetime, datetime]:
"""返回给定营业周(周一)的精确时间戳范围 [start, end)。
start = week_monday 当天 day_start_hour:00
end = week_monday + 7天 day_start_hour:00
"""
def business_month_range(month_first: date, day_start_hour: int = 8) -> tuple[datetime, datetime]:
"""返回给定营业月(首日)的精确时间戳范围 [start, end)。
start = month_first 当天 day_start_hour:00
end = 次月1日 day_start_hour:00
"""
```
所有 `*_range` 函数返回的时间戳带 `Asia/Shanghai` 时区信息(使用 `SHANGHAI_TZ`)。
### 2. ETL 配置层(`apps/etl/connectors/feiqiu/config/`
**已完成**(代码库中已存在):
- `defaults.py``"app": {"business_day_start_hour": 8}`
- `env_parser.py``"BUSINESS_DAY_START_HOUR": ("app.business_day_start_hour",)`
**需新增**`settings.py``_validate` 方法增加范围校验:
```python
# 在 _validate 中新增
hour = cfg["app"].get("business_day_start_hour", 8)
if not isinstance(hour, int) or not (0 <= hour <= 23):
raise SystemExit("app.business_day_start_hour 必须为 023 的整数")
```
### 3. 后端配置层(`apps/backend/app/config.py`
新增模块级常量:
```python
BUSINESS_DAY_START_HOUR: int = int(get("BUSINESS_DAY_START_HOUR", "8"))
```
### 4. 后端配置查询 API`apps/backend/app/routers/business_day.py`
```python
router = APIRouter(prefix="/api/config", tags=["业务配置"])
@router.get("/business-day")
async def get_business_day_config():
"""返回当前营业日分割点配置。"""
return {"business_day_start_hour": config.BUSINESS_DAY_START_HOUR}
```
无需认证(公开配置),前端启动时调用一次缓存。
### 5. BaseDwsTask 基类改造
**`iter_dwd_rows` 改造**
```python
def iter_dwd_rows(self, table_name, columns, start_date, end_date,
date_col="created_at", ...):
cutoff = self.config.get("app.business_day_start_hour", 8)
biz_expr = biz_date_sql_expr(date_col, cutoff)
where_parts = [f"{biz_expr} >= %s", f"{biz_expr} <= %s"]
# ... 其余逻辑不变
```
**`get_time_window_range` 改造**
当前方法返回 `TimeRange(start=date, end=date)`,改造后语义不变(仍返回 `date` 范围),但内部使用 `business_date` 计算 `base_date` 的营业日归属:
```python
def get_time_window_range(self, window, base_date=None):
if base_date is None:
from neozqyy_shared.datetime_utils import now_shanghai, business_date
cutoff = self.config.get("app.business_day_start_hour", 8)
base_date = business_date(now_shanghai(), cutoff)
# ... 其余逻辑使用 base_date已是营业日
```
### 6. 各 DWS 任务 SQL 改造模式
所有任务的 SQL 改造遵循统一模式:
```sql
-- 改造前
DATE(pay_time) AS stat_date
WHERE DATE(pay_time) >= %s AND DATE(pay_time) <= %s
GROUP BY DATE(pay_time)
-- 改造后cutoff_hour=8 时)
DATE(pay_time - INTERVAL '8 hours') AS stat_date
WHERE DATE(pay_time - INTERVAL '8 hours') >= %s AND DATE(pay_time - INTERVAL '8 hours') <= %s
GROUP BY DATE(pay_time - INTERVAL '8 hours')
```
任务从 `self.config.get("app.business_day_start_hour", 8)` 读取 cutoff 值,调用 `biz_date_sql_expr(col, cutoff)` 生成表达式。
**受影响任务清单**18 个):
| 任务 | 主要时间列 | 聚合粒度 |
|------|-----------|---------|
| FinanceBaseTask | pay_time | 日 |
| FinanceDailyTask | pay_time | 日 |
| FinanceRechargeTask | pay_time | 日 |
| FinanceDiscountTask | pay_time | 日 |
| FinanceIncomeTask | pay_time | 日 |
| AssistantDailyTask | start_use_time | 日 |
| AssistantOrderContributionTask | pay_time, start_use_time | 日 |
| AssistantCustomerTask | start_use_time | 日 |
| AssistantMonthlyTask | (基于日度数据) | 月 |
| AssistantFinanceTask | start_use_time | 日 |
| MemberVisitTask | pay_time, start_use_time, ledger_end_time | 日 |
| MemberConsumptionTask | pay_time, create_time | 日 |
| GoodsStockDailyTask | fetched_at | 日 |
| GoodsStockWeeklyTask | fetched_at | 周 |
| GoodsStockMonthlyTask | fetched_at | 月 |
| SpendingPowerIndexTask | pay_time | 日 |
| MemberIndexBase | pay_time | 日 |
| MvRefreshTask | (物化视图刷新) | - |
### 7. 数据库层
**新增 PostgreSQL 函数**(迁移脚本 `db/etl_feiqiu/migrations/2026-03-XX__add_biz_date_function.sql`
```sql
CREATE OR REPLACE FUNCTION dws.biz_date(ts timestamptz, cutoff_hour int DEFAULT 8)
RETURNS date AS $$
SELECT (ts - make_interval(hours => cutoff_hour))::date;
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE;
```
**物化视图重建**(迁移脚本 `db/etl_feiqiu/migrations/2026-03-XX__rebuild_mv_with_biz_date.sql`
物化视图 `mv_dws_finance_daily_summary_l1..l4``mv_dws_assistant_daily_detail_l1..l4` 的时间过滤条件从 `CURRENT_DATE` 改为 `dws.biz_date(NOW())` 或等效表达式。
### 8. 前端适配
**Admin_Web**
- 日期选择器组件旁增加 Tooltip 或文字标注:`营业日:{HH}:00 起`
- 通过 `/api/config/business-day` 获取 `business_day_start_hour`,启动时请求一次存入全局状态
- 降级策略API 不可用时使用默认值 8`console.warn` 输出警告
**Miniprogram**
- 不直接使用 cutoff 值,所有统计数据由后端 API 按营业日口径返回
- 无需前端改造,仅确认后端 API 返回的数据已是营业日口径
### 9. 历史数据重算
提供 `scripts/ops/rebuild_dws_biz_date.py` 脚本:
```python
# 伪代码
for task_cls in ALL_DWS_TASKS:
for date_window in split_by_month(history_start, history_end):
task = task_cls(config)
task.run(window_start=date_window.start, window_end=date_window.end)
```
- 复用正式 ETL 任务逻辑,确保与正式运行使用相同的 `Business_Day_Cutoff`
- 按月分窗口执行,避免单次事务过大
- 执行前后记录行数对比到日志
- 支持 `--dry-run` 模式预览影响范围
### 10. 运维脚本排查
| 脚本 | 涉及的 DATE() 调用 | 处理方式 |
|------|-------------------|---------|
| `scripts/ops/export_bug_report.py` | `DATE(trash_time)`, `DATE(create_time)`, `DATE(start_use_time)` | 替换为 `biz_date_sql_expr` 生成的表达式 |
| `scripts/ops/etl_consistency_check.py` | 日期比较逻辑 | 评估后按需替换 |
| `apps/etl/.../debug_blackbox.py` | `::date` 类型转换 | 替换为 `biz_date()` 函数调用 |
| `apps/etl/.../run_update.py` | `.date()``datetime.combine` | 替换为 `business_date()` + `business_day_range()` |
## 数据模型
### 配置数据
```
BUSINESS_DAY_START_HOUR: int (023, 默认 8)
```
存储位置:
-`.env``BUSINESS_DAY_START_HOUR=8`
- ETL`AppConfig.config["app"]["business_day_start_hour"]`
- 后端:`config.BUSINESS_DAY_START_HOUR`
### 时间工具函数签名
```python
# 输入/输出类型
business_date(dt: datetime, day_start_hour: int = 8) -> date
business_month(dt: datetime, day_start_hour: int = 8) -> date
business_week_monday(dt: datetime, day_start_hour: int = 8) -> date
business_day_range(biz_date: date, day_start_hour: int = 8) -> tuple[datetime, datetime]
business_week_range(week_monday: date, day_start_hour: int = 8) -> tuple[datetime, datetime]
business_month_range(month_first: date, day_start_hour: int = 8) -> tuple[datetime, datetime]
biz_date_sql_expr(col: str, day_start_hour: int = 8) -> str
```
### 数据库函数
```sql
dws.biz_date(ts timestamptz, cutoff_hour int DEFAULT 8) RETURNS date
-- 等价于 Python 的 business_date用于 SQL 查询和物化视图
```
### API 响应模型
```json
// GET /api/config/business-day
{
"business_day_start_hour": 8
}
```
### DWS 表影响
所有 DWS 表的 `stat_date` 字段语义从"自然日"变为"营业日"。表结构不变,仅数据内容因重算而变化。
## 正确性属性
*属性Property是在系统所有合法执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
### Property 1: 营业日归属往返一致性Round-Trip
*对任意* datetime `dt` 和任意合法的 `day_start_hour` h023`business_day_range(business_date(dt, h), h)` 返回的范围 `[start, end)` 应满足 `start <= dt < end`
**Validates: Requirements 2.9, 11.1**
### Property 2: 营业月与营业日一致性
*对任意* datetime `dt` 和任意合法的 `day_start_hour` h023`business_month(dt, h)` 应等于 `business_date(dt, h).replace(day=1)`
**Validates: Requirements 2.10, 11.2**
### Property 3: 营业周与营业日一致性
*对任意* datetime `dt` 和任意合法的 `day_start_hour` h023`business_week_monday(dt, h)` 应等于 `business_date(dt, h) - timedelta(days=business_date(dt, h).weekday())`,且结果的 `weekday()` 始终为 0周一
**Validates: Requirements 2.11, 11.3**
### Property 4: 营业日归属单调性
*对任意* 两个 datetime `dt1 < dt2` 和任意合法的 `day_start_hour` h023`dt1``dt2` 都在同一个 `business_day_range(d, h)` 范围内,则 `business_date(dt1, h) == business_date(dt2, h)`。等价表述:`business_date(dt, h)` 关于 `dt` 是单调非递减的。
**Validates: Requirements 11.9**
### Property 5: 时间范围长度不变量
*对任意* date `d` 和任意合法的 `day_start_hour` h023
- `business_day_range(d, h)` 返回的 `(start, end)` 满足 `end - start == timedelta(hours=24)`
- `business_week_range(monday, h)` 返回的 `(start, end)` 满足 `end - start == timedelta(days=7)`
**Validates: Requirements 11.6, 11.7**
### Property 6: SQL 表达式生成幂等性
*对任意* 列名 `col` 和任意合法的 `day_start_hour` h023`biz_date_sql_expr(col, h)` 多次调用应返回完全相同的字符串。
**Validates: Requirements 11.4**
### Property 7: 非法配置值拒绝
*对任意* 不在 023 范围内的整数值 `v`,当 `BUSINESS_DAY_START_HOUR` 设为 `v` 时,`AppConfig.load()` 应抛出 `SystemExit`
**Validates: Requirements 1.3**
### Property 8: 合法配置值正确加载
*对任意* 023 范围内的整数值 `v`,当 `BUSINESS_DAY_START_HOUR` 环境变量设为 `v` 时,`AppConfig.load()``cfg.get("app.business_day_start_hour")` 应返回 `v`
**Validates: Requirements 3.4**
## 错误处理
| 场景 | 处理方式 |
|------|---------|
| `BUSINESS_DAY_START_HOUR` 值超出 023 | `AppConfig._validate` 抛出 `SystemExit`,明确提示合法范围 |
| `BUSINESS_DAY_START_HOUR` 环境变量缺失 | 使用默认值 8不报错 |
| `BUSINESS_DAY_START_HOUR` 值为非整数字符串 | `env_parser._coerce_env` 保持字符串,`_validate` 阶段类型检查失败抛出 `SystemExit` |
| 后端 `/api/config/business-day` 不可用 | Admin_Web 使用默认值 8`console.warn` 输出警告 |
| 历史数据重算脚本执行失败 | 按月窗口回滚当前批次,记录错误日志,继续下一窗口或中止(由 `--fail-fast` 参数控制) |
| 物化视图迁移脚本执行失败 | 标准 PostgreSQL 事务回滚,迁移脚本幂等设计(`CREATE OR REPLACE` |
| `business_day_range` 等函数收到非法 `day_start_hour` | 函数内部不做校验(调用方负责),依赖 AppConfig 加载阶段的前置校验 |
## 测试策略
### 属性测试Property-Based Testing
使用 `hypothesis` 库,测试文件位于 `tests/test_property_business_day_cutoff.py`
每个属性测试最少运行 100 次迭代,使用 `@settings(max_examples=200)` 配置。
生成策略:
- `day_start_hour``st.integers(min_value=0, max_value=23)`
- `dt``st.datetimes(min_value=datetime(2020, 1, 1), max_value=datetime(2030, 12, 31))`(避免极端日期)
- `biz_date``st.dates(min_value=date(2020, 1, 1), max_value=date(2030, 12, 31))`
每个测试函数以注释标注对应的设计属性:
```python
# Feature: business-day-cutoff, Property 1: 营业日归属往返一致性
@given(dt=st.datetimes(...), h=st.integers(0, 23))
@settings(max_examples=200)
def test_business_date_round_trip(dt, h):
...
# Feature: business-day-cutoff, Property 2: 营业月与营业日一致性
# Feature: business-day-cutoff, Property 3: 营业周与营业日一致性
# Feature: business-day-cutoff, Property 4: 营业日归属单调性
# Feature: business-day-cutoff, Property 5: 时间范围长度不变量
# Feature: business-day-cutoff, Property 6: SQL 表达式生成幂等性
# Feature: business-day-cutoff, Property 7: 非法配置值拒绝
# Feature: business-day-cutoff, Property 8: 合法配置值正确加载
```
### 单元测试
单元测试覆盖属性测试不适合的场景:
- **边界示例**`day_start_hour=8`07:59:59 归属前一天08:00:00 归属当天
- **默认值行为**`BUSINESS_DAY_START_HOUR` 缺失时 AppConfig 返回 8
- **API 端点**`/api/config/business-day` 返回正确 JSON 格式
- **SQL 表达式格式**`biz_date_sql_expr("pay_time", 8)` 返回 `DATE(pay_time - INTERVAL '8 hours')`
- **月末边界**1月31日 07:00 归属1月30日营业日`business_month` 返回1月1日
### 测试配置
- 属性测试库:`hypothesis`(已在项目 `pyproject.toml` 中声明)
- 每个属性测试对应设计文档中的一个 Property由单个 `@given` 装饰的测试函数实现
- 运行命令:`cd C:\NeoZQYY && pytest tests/test_property_business_day_cutoff.py -v`

View File

@@ -0,0 +1,186 @@
# 需求文档业务日分割点机制Business Day Cutoff
## 简介
引入"业务日分割点"机制,将全系统的统计时间口径从自然日/自然周/自然月切换为以可配置的小时值(默认 08:00为分割点的营业日/营业周/营业月。影响范围覆盖配置层、共享包、ETL 层ODS→DWD→DWS、后端 API 层、前端展示层(管理后台、小程序)及数据库层。
## 术语表
- **Business_Day_Cutoff**营业日分割点一个整数小时值023定义一个"业务日"的起始时刻。默认值为 8即 08:00
- **Business_Date**:营业日,从当天 `Business_Day_Cutoff` 时刻到次日 `Business_Day_Cutoff` 时刻的时间段所归属的日期。`Business_Day_Cutoff` 之前的时间戳归属前一天。
- **Business_Week**:营业周,从周一 `Business_Day_Cutoff` 到次周一 `Business_Day_Cutoff` 的时间段。
- **Business_Month**营业月从当月1日 `Business_Day_Cutoff` 到次月1日 `Business_Day_Cutoff` 的时间段。
- **Shared_DateTime_Utils**`packages/shared/src/neozqyy_shared/datetime_utils.py`,跨子系统共享的时间工具模块。
- **AppConfig**ETL 配置管理器(`apps/etl/connectors/feiqiu/config/settings.py`),通过 `AppConfig.load()` 加载配置。
- **Backend_Config**:后端配置模块(`apps/backend/app/config.py`),从 `.env` 加载环境变量。
- **DWS_Task**DWS 层聚合任务,从 DWD 事实表读取数据并按时间维度聚合写入 DWS 汇总表。
- **biz_date_sql_expr**`Shared_DateTime_Utils` 中生成 PostgreSQL 营业日归属 SQL 表达式的函数。
- **stat_date**DWS 汇总表中的统计日期字段,存储的是 Business_Date 而非自然日期。
- **Admin_Web**:管理后台前端(`apps/admin-web/`React + Vite + Ant Design
- **Miniprogram**:微信小程序前端(`apps/miniprogram/`)。
## 需求
### 需求 1环境变量配置
**用户故事:** 作为运维人员,我希望通过 `.env` 环境变量配置营业日分割点小时值,以便在不修改代码的情况下调整统计时间口径。
#### 验收标准
1. THE Root_Env SHALL 定义 `BUSINESS_DAY_START_HOUR` 环境变量,值为 023 的整数,默认值为 8
2. THE Env_Template SHALL 同步包含 `BUSINESS_DAY_START_HOUR` 的定义及注释说明(日/周/月统计的分割语义)
3. WHEN `BUSINESS_DAY_START_HOUR` 的值不在 023 范围内时, THEN THE AppConfig SHALL 在加载阶段抛出 `SystemExit` 错误并给出明确提示
4. WHEN `BUSINESS_DAY_START_HOUR` 环境变量缺失时, THE AppConfig SHALL 使用默认值 8
### 需求 2共享时间工具函数
**用户故事:** 作为开发者,我希望有一组经过充分测试的共享时间工具函数,以便所有子系统使用统一的营业日归属逻辑。
#### 验收标准
1. THE Shared_DateTime_Utils SHALL 提供 `business_date(dt, day_start_hour)` 函数,将任意时间戳归属到对应的 Business_Date
2. THE Shared_DateTime_Utils SHALL 提供 `business_month(dt, day_start_hour)` 函数,将任意时间戳归属到对应的 Business_Month 首日
3. THE Shared_DateTime_Utils SHALL 提供 `business_week_monday(dt, day_start_hour)` 函数,将任意时间戳归属到对应的 Business_Week 的周一日期
4. THE Shared_DateTime_Utils SHALL 提供 `biz_date_sql_expr(col, day_start_hour)` 函数,生成 PostgreSQL 营业日归属 SQL 表达式(形如 `DATE(col - INTERVAL 'N hours')`
5. WHEN `day_start_hour` 参数未传入时, THE Shared_DateTime_Utils SHALL 使用 `DEFAULT_BUSINESS_DAY_START_HOUR`(值为 8作为默认值
6. THE Shared_DateTime_Utils SHALL 提供 `business_day_range(biz_date, day_start_hour)` 函数,返回给定 Business_Date 对应的精确时间戳范围 `(start_dt, end_dt)`,即 `(biz_date 当天 day_start_hour:00, biz_date 次日 day_start_hour:00)`
7. THE Shared_DateTime_Utils SHALL 提供 `business_week_range(week_monday, day_start_hour)` 函数,返回给定 Business_Week 周一对应的精确时间戳范围
8. THE Shared_DateTime_Utils SHALL 提供 `business_month_range(month_first, day_start_hour)` 函数,返回给定 Business_Month 首日对应的精确时间戳范围
9. FOR ALL 合法的 datetime 输入, `business_date` 的输出 SHALL 满足:`business_day_range(business_date(dt, h), h)[0] <= dt < business_day_range(business_date(dt, h), h)[1]`(往返一致性)
10. FOR ALL 合法的 datetime 输入, `business_month(dt, h)` SHALL 等于 `business_date(dt, h).replace(day=1)`(月归属与日归属一致性)
11. FOR ALL 合法的 datetime 输入, `business_week_monday(dt, h)` SHALL 等于 `business_date(dt, h) - timedelta(days=business_date(dt, h).weekday())`(周归属与日归属一致性)
### 需求 3ETL 配置层集成
**用户故事:** 作为 ETL 开发者,我希望 `AppConfig` 正确加载并传播 `BUSINESS_DAY_START_HOUR`,以便 ETL 任务能获取到配置的分割点值。
#### 验收标准
1. THE AppConfig SHALL 在 `app.business_day_start_hour` 路径下存储 `BUSINESS_DAY_START_HOUR` 的整数值
2. THE Env_Parser SHALL 将环境变量 `BUSINESS_DAY_START_HOUR` 映射到 `app.business_day_start_hour` 配置路径
3. THE AppConfig_Defaults SHALL 将 `app.business_day_start_hour` 的默认值设为 8
4. WHEN AppConfig 加载完成后, THE AppConfig SHALL 通过 `cfg.get("app.business_day_start_hour")` 返回正确的整数值
### 需求 4ETL DWS 层聚合逻辑
**用户故事:** 作为数据分析师,我希望 DWS 层的所有日度/周度/月度聚合统计都基于营业日口径,以便统计结果与门店实际营业周期一致。
#### 验收标准
1. WHEN DWS_Task 从 DWD 表提取数据时, THE DWS_Task SHALL 使用 `biz_date_sql_expr` 替代 `DATE()` 进行日期归属计算
2. WHEN DWS_Task 按日聚合时, THE DWS_Task SHALL 使用 `DATE(timestamp_col - INTERVAL 'N hours')` 作为 `stat_date` 的分组依据,其中 N 为 `Business_Day_Cutoff`
3. WHEN DWS_Task 按月聚合时, THE DWS_Task SHALL 使用 Business_Month 口径当月1日 cutoff 到次月1日 cutoff
4. WHEN DWS_Task 按周聚合时, THE DWS_Task SHALL 使用 Business_Week 口径(周一 cutoff 到次周一 cutoff
5. THE BaseDwsTask.iter_dwd_rows SHALL 使用 `biz_date_sql_expr` 替代 `DATE()` 进行日期过滤
6. THE BaseDwsTask.get_time_window_range SHALL 返回基于 Business_Date 口径的时间范围
7. WHILE ETL 任务运行期间, THE DWS_Task SHALL 从 `AppConfig` 读取 `app.business_day_start_hour` 值,禁止硬编码
### 需求 5受影响的 DWS 任务全面排查
**用户故事:** 作为项目负责人,我希望所有使用 `DATE()` 进行时间归属的 DWS 任务都被排查并改造,确保无遗漏。
#### 验收标准
1. THE FinanceBaseTask SHALL 将所有 `DATE(pay_time)` 替换为 `biz_date_sql_expr("pay_time", cutoff_hour)` 生成的表达式
2. THE FinanceDailyTask SHALL 使用 Business_Date 口径提取和聚合结账单、团购核销、充值等数据
3. THE FinanceRechargeTask SHALL 将 `DATE(pay_time)` 替换为营业日归属表达式
4. THE FinanceDiscountTask SHALL 使用 Business_Date 口径聚合优惠明细
5. THE FinanceIncomeTask SHALL 使用 Business_Date 口径聚合收入结构
6. THE AssistantDailyTask SHALL 使用 Business_Date 口径聚合助教日度明细
7. THE AssistantOrderContributionTask SHALL 将 `DATE(pay_time)` 替换为营业日归属表达式
8. THE AssistantCustomerTask SHALL 将 `DATE(start_use_time)` 替换为营业日归属表达式
9. THE AssistantMonthlyTask SHALL 使用 Business_Month 口径聚合助教月度汇总
10. THE AssistantFinanceTask SHALL 使用 Business_Date 口径聚合助教财务分析
11. THE MemberVisitTask SHALL 将 `DATE(pay_time)``DATE(start_use_time)``DATE(ledger_end_time)` 替换为营业日归属表达式
12. THE MemberConsumptionTask SHALL 将 `DATE(pay_time)``DATE(create_time)` 替换为营业日归属表达式
13. THE GoodsStockDailyTask SHALL 将 `DATE(fetched_at)` 替换为营业日归属表达式
14. THE GoodsStockWeeklyTask SHALL 使用 Business_Week 口径聚合库存周报
15. THE GoodsStockMonthlyTask SHALL 使用 Business_Month 口径聚合库存月报
16. THE SpendingPowerIndexTask SHALL 将 `DATE(pay_time)` 替换为营业日归属表达式
17. THE MemberIndexBase SHALL 将 `DATE(pay_time)` 替换为营业日归属表达式
18. THE MvRefreshTask SHALL 确保物化视图刷新的时间过滤条件使用 Business_Date 口径
### 需求 6后端 API 层时间范围计算
**用户故事:** 作为后端开发者,我希望后端 API 在处理"今日/本周/本月"等时间范围查询时使用营业日口径,以便前端展示的数据与 DWS 统计一致。
#### 验收标准
1. THE Backend_Config SHALL 加载 `BUSINESS_DAY_START_HOUR` 环境变量并暴露为模块级常量
2. WHEN 后端 API 需要计算"今日"时间范围时, THE Backend SHALL 使用 `business_day_range` 函数计算从当天 cutoff 到次日 cutoff 的时间戳范围
3. WHEN 后端 API 需要计算"本周"时间范围时, THE Backend SHALL 使用 `business_week_range` 函数计算从本周一 cutoff 到次周一 cutoff 的时间戳范围
4. WHEN 后端 API 需要计算"本月"时间范围时, THE Backend SHALL 使用 `business_month_range` 函数计算从本月1日 cutoff 到次月1日 cutoff 的时间戳范围
5. THE Backend SHALL 从 `Shared_DateTime_Utils` 导入时间工具函数,禁止在后端重复实现营业日逻辑
### 需求 7前端展示层适配
**用户故事:** 作为前端开发者,我希望管理后台和小程序在展示日期选择器和统计数据时,能正确反映营业日口径,避免用户困惑。
#### 验收标准
1. WHEN Admin_Web 展示日期选择器时, THE Admin_Web SHALL 在日期选择器旁标注营业日口径说明(如"营业日08:00 起"
2. WHEN Admin_Web 展示"今日统计"时, THE Admin_Web SHALL 显示的时间范围为当天 cutoff 到次日 cutoff
3. WHEN Miniprogram 展示统计数据时, THE Miniprogram SHALL 使用后端 API 返回的基于营业日口径的数据
4. THE Admin_Web SHALL 通过后端 API 获取 `BUSINESS_DAY_START_HOUR` 配置值,禁止前端硬编码
5. IF 后端 API 返回的 `BUSINESS_DAY_START_HOUR` 配置值不可用, THEN THE Admin_Web SHALL 使用默认值 8 并在控制台输出警告
### 需求 8后端配置查询 API
**用户故事:** 作为前端开发者,我希望有一个 API 端点能返回当前的营业日分割点配置,以便前端动态获取并展示。
#### 验收标准
1. THE Backend SHALL 提供一个 API 端点返回当前 `BUSINESS_DAY_START_HOUR` 的值
2. WHEN 前端请求该端点时, THE Backend SHALL 返回包含 `business_day_start_hour` 字段的 JSON 响应
3. THE Backend SHALL 确保该端点的响应值与 ETL 层使用的 `BUSINESS_DAY_START_HOUR` 值一致(均来源于同一 `.env` 配置)
### 需求 9数据库层适配
**用户故事:** 作为 DBA我希望数据库中的物化视图和 SQL 函数使用营业日口径,以便直接查询数据库时也能获得正确的统计结果。
#### 验收标准
1. WHEN 物化视图使用 `date_trunc``CURRENT_DATE` 进行时间过滤时, THE Migration_Script SHALL 将其替换为基于 `Business_Day_Cutoff` 的表达式
2. THE Migration_Script SHALL 提供一个 PostgreSQL 函数 `biz_date(timestamptz, int)` 用于在 SQL 中直接计算营业日归属
3. WHEN 迁移脚本执行后, THE Database SHALL 确保所有物化视图的时间过滤条件使用营业日口径
4. THE Migration_Script SHALL 使用日期前缀命名(如 `2026-03-XX__add_biz_date_function.sql`),遵循项目迁移脚本规范
### 需求 10数据迁移与历史数据兼容
**用户故事:** 作为运维人员,我希望引入营业日机制后,历史数据能被正确重算,确保统计连续性。
#### 验收标准
1. THE Migration_Plan SHALL 提供 DWS 历史数据重算脚本,按营业日口径重新聚合所有受影响的 DWS 表
2. WHEN 历史数据重算执行时, THE Rebuild_Script SHALL 使用与正式 ETL 任务相同的 `Business_Day_Cutoff` 配置值
3. THE Migration_Plan SHALL 记录重算前后的数据行数对比,用于验证重算正确性
4. IF 重算过程中发生错误, THEN THE Rebuild_Script SHALL 回滚到重算前的状态并记录错误日志
### 需求 11属性测试覆盖
**用户故事:** 作为测试工程师,我希望营业日归属逻辑有完整的属性测试覆盖,确保边界条件和不变量得到验证。
#### 验收标准
1. THE Property_Test SHALL 验证 `business_date` 的往返一致性:对任意 datetime dt`business_day_range(business_date(dt, h), h)` 的范围包含 dt
2. THE Property_Test SHALL 验证 `business_month``business_date` 的一致性:`business_month(dt, h) == business_date(dt, h).replace(day=1)`
3. THE Property_Test SHALL 验证 `business_week_monday``business_date` 的一致性:`business_week_monday(dt, h).weekday() == 0`(结果始终为周一)
4. THE Property_Test SHALL 验证 `biz_date_sql_expr` 的幂等性:对同一输入参数多次调用返回相同结果
5. THE Property_Test SHALL 验证边界条件cutoff 时刻恰好在分割点上的时间戳归属当天,分割点前一秒归属前一天
6. THE Property_Test SHALL 验证 `business_day_range` 返回的范围恰好为 24 小时
7. THE Property_Test SHALL 验证 `business_week_range` 返回的范围恰好为 7 天168 小时)
8. THE Property_Test SHALL 使用 hypothesis 库生成随机 datetime 和 day_start_hour023进行测试
9. FOR ALL `day_start_hour`023, THE Property_Test SHALL 验证 `business_date` 函数的单调性:若 dt1 < dt2 且两者在同一 Business_Date 范围内,则 `business_date(dt1, h) == business_date(dt2, h)`
### 需求 12运维脚本中的 DATE() 排查
**用户故事:** 作为运维人员,我希望 `scripts/ops/` 和 ETL `scripts/` 中使用 `DATE()` 的运维脚本也被排查,确保调试和排查工具与正式统计口径一致。
#### 验收标准
1. THE Ops_Scripts SHALL 排查 `scripts/ops/export_bug_report.py` 中的 `DATE(trash_time)``DATE(create_time)``DATE(start_use_time)` 调用,评估是否需要替换为营业日归属表达式
2. THE Ops_Scripts SHALL 排查 `scripts/ops/etl_consistency_check.py` 中的日期比较逻辑
3. THE ETL_Scripts SHALL 排查 `apps/etl/connectors/feiqiu/scripts/debug/debug_blackbox.py` 中的 `::date` 类型转换
4. THE ETL_Scripts SHALL 排查 `apps/etl/connectors/feiqiu/scripts/run_update.py` 中的 `.date()` 调用和 `datetime.combine` 逻辑
5. WHEN 运维脚本用于与 DWS 数据对比验证时, THE Ops_Scripts SHALL 使用与 DWS 任务相同的营业日归属逻辑

View File

@@ -0,0 +1,260 @@
# 实现计划业务日分割点机制Business Day Cutoff
## 概述
将全系统统计时间口径从自然日切换为以可配置小时值(默认 08:00为分割点的营业日。实现顺序配置层 → 共享工具层 → ETL 层 → 后端 API 层 → 数据库层 → 前端适配 → 历史数据重算 → 运维脚本排查。
## 任务
- [x] 1. 配置层:环境变量与配置加载
- [x] 1.1 在根 `.env``.env.template` 中新增 `BUSINESS_DAY_START_HOUR=8`
- `.env` 新增 `BUSINESS_DAY_START_HOUR=8`
- `.env.template` 新增带注释的定义,说明日/周/月统计分割语义
- _Requirements: 1.1, 1.2_
- [x] 1.2 在 `AppConfig._validate` 中新增 `business_day_start_hour` 范围校验
-`apps/etl/connectors/feiqiu/config/settings.py``_validate` 方法中增加校验
- 值不在 023 范围内或非整数时抛出 `SystemExit`
- 环境变量缺失时使用默认值 8已由 `defaults.py` 保证)
- _Requirements: 1.3, 1.4, 3.1, 3.2, 3.3, 3.4_
- [x] 1.3 在后端 `apps/backend/app/config.py` 中新增 `BUSINESS_DAY_START_HOUR` 常量
- 新增 `BUSINESS_DAY_START_HOUR: int = int(get("BUSINESS_DAY_START_HOUR", "8"))`
- _Requirements: 6.1_
- [x] 2. 共享时间工具层:新增 range 函数
- [x] 2.1 在 `packages/shared/src/neozqyy_shared/datetime_utils.py` 中实现三个 range 函数
- 实现 `business_day_range(biz_date, day_start_hour)``tuple[datetime, datetime]`
- 实现 `business_week_range(week_monday, day_start_hour)``tuple[datetime, datetime]`
- 实现 `business_month_range(month_first, day_start_hour)``tuple[datetime, datetime]`
- 所有返回值带 `Asia/Shanghai` 时区(使用 `SHANGHAI_TZ`
- 默认 `day_start_hour=8`
- _Requirements: 2.6, 2.7, 2.8, 2.5_
- [x] 2.2 编写属性测试营业日归属往返一致性Property 1
- **Property 1: 营业日归属往返一致性Round-Trip**
- `business_day_range(business_date(dt, h), h)` 的范围 `[start, end)` 应满足 `start <= dt < end`
- 使用 `hypothesis``@given(dt=st.datetimes(...), h=st.integers(0, 23))``@settings(max_examples=200)`
- 测试文件:`tests/test_property_business_day_cutoff.py`
- **Validates: Requirements 2.9, 11.1**
- [x] 2.3 编写属性测试营业月与营业日一致性Property 2
- **Property 2: 营业月与营业日一致性**
- `business_month(dt, h) == business_date(dt, h).replace(day=1)`
- **Validates: Requirements 2.10, 11.2**
- [x] 2.4 编写属性测试营业周与营业日一致性Property 3
- **Property 3: 营业周与营业日一致性**
- `business_week_monday(dt, h) == business_date(dt, h) - timedelta(days=business_date(dt, h).weekday())`,且结果 `weekday() == 0`
- **Validates: Requirements 2.11, 11.3**
- [x] 2.5 编写属性测试营业日归属单调性Property 4
- **Property 4: 营业日归属单调性**
-`dt1 < dt2` 且两者在同一 `business_day_range` 范围内,则 `business_date(dt1, h) == business_date(dt2, h)`
- **Validates: Requirements 11.9**
- [x] 2.6 编写属性测试时间范围长度不变量Property 5
- **Property 5: 时间范围长度不变量**
- `business_day_range(d, h)``end - start == timedelta(hours=24)`
- `business_week_range(monday, h)``end - start == timedelta(days=7)`
- **Validates: Requirements 11.6, 11.7**
- [x] 2.7 编写属性测试SQL 表达式生成幂等性Property 6
- **Property 6: SQL 表达式生成幂等性**
- `biz_date_sql_expr(col, h)` 多次调用返回完全相同的字符串
- **Validates: Requirements 11.4**
- [x] 2.8 编写属性测试边界条件验证Property 1 补充)
- cutoff 时刻恰好在分割点上的时间戳归属当天,分割点前一秒归属前一天
- 使用 hypothesis 生成 `day_start_hour`,构造边界时间戳验证
- **Validates: Requirements 11.5**
- [x] 2.9 编写单元测试:共享时间工具函数
- 测试 `day_start_hour=8` 时 07:59:59 归属前一天、08:00:00 归属当天
- 测试 `biz_date_sql_expr("pay_time", 8)` 返回 `DATE(pay_time - INTERVAL '8 hours')`
- 测试月末边界1月31日 07:00 归属1月30日`business_month` 返回1月1日
- 测试默认值行为:不传 `day_start_hour` 时使用 8
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8_
- [x] 3. Checkpoint — 共享工具层验证
- 确保所有属性测试和单元测试通过,运行 `pytest tests/test_property_business_day_cutoff.py -v`,如有问题请向用户确认。
- [x] 4. ETL 配置层集成与 BaseDwsTask 基类改造
- [x] 4.1 在 `AppConfig._validate` 中新增 `business_day_start_hour` 范围校验(与 1.2 合并实现)
- 确认 `defaults.py``env_parser.py` 已有配置映射(设计文档标注"已完成"
- 仅需在 `settings.py``_validate` 中增加校验逻辑
- _Requirements: 3.1, 3.2, 3.3, 3.4_
- [x] 4.2 编写属性测试非法配置值拒绝Property 7
- **Property 7: 非法配置值拒绝**
- 对任意不在 023 范围内的整数值,`AppConfig.load()` 应抛出 `SystemExit`
- **Validates: Requirements 1.3**
- [x] 4.3 编写属性测试合法配置值正确加载Property 8
- **Property 8: 合法配置值正确加载**
- 对任意 023 范围内的整数值 `v``AppConfig.load()``cfg.get("app.business_day_start_hour")` 应返回 `v`
- **Validates: Requirements 3.4**
- [x] 4.4 改造 `BaseDwsTask.iter_dwd_rows`,将 `DATE()` 替换为 `biz_date_sql_expr`
-`self.config.get("app.business_day_start_hour", 8)` 读取 cutoff 值
- 调用 `biz_date_sql_expr(date_col, cutoff)` 生成 SQL 表达式
- 替换 WHERE 子句中的 `DATE(col)``biz_date_sql_expr` 生成的表达式
- _Requirements: 4.1, 4.5, 4.7_
- [x] 4.5 改造 `BaseDwsTask.get_time_window_range`,使用 `business_date` 计算营业日归属
- `base_date` 为 None 时使用 `business_date(now_shanghai(), cutoff)` 计算
- 确保返回的 `TimeRange` 基于营业日口径
- _Requirements: 4.6_
- [x] 5. DWS 任务 SQL 改造(财务类)
- [x] 5.1 改造 FinanceBaseTask`DATE(pay_time)``biz_date_sql_expr("pay_time", cutoff)`
- 从 config 读取 cutoff替换所有 `DATE(pay_time)` 为营业日表达式
- 包括 SELECT、WHERE、GROUP BY 中的所有出现
- _Requirements: 5.1_
- [x] 5.2 改造 FinanceDailyTask使用 Business_Date 口径
- 替换结账单、团购核销、充值等数据的日期归属
- _Requirements: 5.2_
- [x] 5.3 改造 FinanceRechargeTask`DATE(pay_time)` → 营业日归属表达式
- _Requirements: 5.3_
- [x] 5.4 改造 FinanceDiscountTask使用 Business_Date 口径聚合优惠明细
- _Requirements: 5.4_
- [x] 5.5 改造 FinanceIncomeTask使用 Business_Date 口径聚合收入结构
- _Requirements: 5.5_
- [x] 6. DWS 任务 SQL 改造(助教类)
- [x] 6.1 改造 AssistantDailyTask`DATE(start_use_time)` → 营业日归属表达式
- _Requirements: 5.6_
- [x] 6.2 改造 AssistantOrderContributionTask`DATE(pay_time)` → 营业日归属表达式
- _Requirements: 5.7_
- [x] 6.3 改造 AssistantCustomerTask`DATE(start_use_time)` → 营业日归属表达式
- _Requirements: 5.8_
- [x] 6.4 改造 AssistantMonthlyTask使用 Business_Month 口径聚合月度汇总
- _Requirements: 5.9_
- [x] 6.5 改造 AssistantFinanceTask使用 Business_Date 口径聚合助教财务分析
- _Requirements: 5.10_
- [x] 7. DWS 任务 SQL 改造(会员类 + 商品库存类 + 指标类)
- [x] 7.1 改造 MemberVisitTask`DATE(pay_time)``DATE(start_use_time)``DATE(ledger_end_time)` → 营业日归属表达式
- _Requirements: 5.11_
- [x] 7.2 改造 MemberConsumptionTask`DATE(pay_time)``DATE(create_time)` → 营业日归属表达式
- _Requirements: 5.12_
- [x] 7.3 改造 GoodsStockDailyTask`DATE(fetched_at)` → 营业日归属表达式
- _Requirements: 5.13_
- [x] 7.4 改造 GoodsStockWeeklyTask使用 Business_Week 口径聚合库存周报
- _Requirements: 5.14_
- [x] 7.5 改造 GoodsStockMonthlyTask使用 Business_Month 口径聚合库存月报
- _Requirements: 5.15_
- [x] 7.6 改造 SpendingPowerIndexTask`DATE(pay_time)` → 营业日归属表达式
- _Requirements: 5.16_
- [x] 7.7 改造 MemberIndexBase`DATE(pay_time)` → 营业日归属表达式
- _Requirements: 5.17_
- [x] 7.8 改造 MvRefreshTask确保物化视图刷新的时间过滤条件使用 Business_Date 口径
- _Requirements: 5.18_
- [x] 8. Checkpoint — ETL DWS 层改造验证
- 确保所有 18 个 DWS 任务的 SQL 改造完成,`DATE()` 调用已全部替换为 `biz_date_sql_expr` 生成的表达式。运行 ETL 单元测试 `cd apps/etl/connectors/feiqiu && pytest tests/unit -v`,如有问题请向用户确认。
- [x] 9. 后端 API 层
- [x] 9.1 创建 `apps/backend/app/routers/business_day.py`,实现 `/api/config/business-day` 端点
- 创建 `APIRouter(prefix="/api/config", tags=["业务配置"])`
- 实现 `GET /business-day` 返回 `{"business_day_start_hour": config.BUSINESS_DAY_START_HOUR}`
- 无需认证(公开配置)
- _Requirements: 8.1, 8.2, 8.3_
- [x] 9.2 在后端主路由中注册 `business_day` 路由
-`apps/backend/app/main.py` 或路由注册文件中 `include_router`
- _Requirements: 8.1_
- [x] 9.3 后端时间范围查询统一使用 `business_*_range` 函数
- 在后端需要计算"今日/本周/本月"时间范围的 API 中,导入并使用 `business_day_range``business_week_range``business_month_range`
-`Shared_DateTime_Utils` 导入,禁止重复实现
- _Requirements: 6.2, 6.3, 6.4, 6.5_
- [x] 9.4 编写单元测试:后端配置查询 API
- 测试 `/api/config/business-day` 返回正确 JSON 格式
- 测试返回值与 `config.BUSINESS_DAY_START_HOUR` 一致
- _Requirements: 8.1, 8.2, 8.3_
- [x] 10. 数据库层PostgreSQL 函数与物化视图迁移
- [x] 10.1 创建迁移脚本:新增 `dws.biz_date()` PostgreSQL 函数
- 文件:`db/etl_feiqiu/migrations/2026-03-XX__add_biz_date_function.sql`
- `CREATE OR REPLACE FUNCTION dws.biz_date(ts timestamptz, cutoff_hour int DEFAULT 8) RETURNS date`
- 标记为 `IMMUTABLE PARALLEL SAFE`
- _Requirements: 9.2, 9.4_
- [x] 10.2 创建迁移脚本:重建物化视图使用 `biz_date()` 函数
- 文件:`db/etl_feiqiu/migrations/2026-03-XX__rebuild_mv_with_biz_date.sql`
-`mv_dws_finance_daily_summary_l1..l4``mv_dws_assistant_daily_detail_l1..l4``CURRENT_DATE` / `date_trunc` 替换为 `dws.biz_date(NOW())`
- 使用 `DROP MATERIALIZED VIEW IF EXISTS` + `CREATE MATERIALIZED VIEW` 重建
- _Requirements: 9.1, 9.3_
- [x] 11. 前端适配
- [x] 11.1 Admin_Web日期选择器旁标注营业日口径说明
- 在日期选择器组件旁增加 Tooltip 或文字标注:`营业日:{HH}:00 起`
- 通过 `/api/config/business-day` 获取 `business_day_start_hour`,启动时请求一次存入全局状态
- 降级策略API 不可用时使用默认值 8`console.warn` 输出警告
- _Requirements: 7.1, 7.2, 7.4, 7.5_
- [x] 11.2 Miniprogram确认后端 API 返回的数据已是营业日口径
- 小程序不直接使用 cutoff 值,所有统计数据由后端 API 按营业日口径返回
- 确认无需前端改造,仅验证后端 API 数据正确性
- _Requirements: 7.3_
- [x] 12. Checkpoint — 后端 + 数据库 + 前端验证
- 确保后端 API 端点可用、迁移脚本语法正确、前端标注正常显示。如有问题请向用户确认。
- [x] 13. 历史数据重算脚本
- [x] 13.1 创建 `scripts/ops/rebuild_dws_biz_date.py` 历史数据重算脚本
- 复用正式 ETL 任务逻辑,使用相同的 `Business_Day_Cutoff` 配置
- 按月分窗口执行,避免单次事务过大
- 执行前后记录行数对比到日志
- 支持 `--dry-run` 模式预览影响范围
- 支持 `--fail-fast` 参数控制错误时中止或继续
- 错误时回滚当前批次并记录错误日志
- _Requirements: 10.1, 10.2, 10.3, 10.4_
- [x] 14. 运维脚本排查与改造
- [x] 14.1 排查并改造 `scripts/ops/export_bug_report.py`
-`DATE(trash_time)``DATE(create_time)``DATE(start_use_time)` 替换为 `biz_date_sql_expr` 生成的表达式
- _Requirements: 12.1, 12.5_
- [x] 14.2 排查并改造 `scripts/ops/etl_consistency_check.py`
- 评估日期比较逻辑,按需替换为营业日归属表达式
- _Requirements: 12.2, 12.5_
- [x] 14.3 排查并改造 `apps/etl/connectors/feiqiu/scripts/debug/debug_blackbox.py`
-`::date` 类型转换替换为 `biz_date()` 函数调用
- _Requirements: 12.3_
- [x] 14.4 排查并改造 `apps/etl/connectors/feiqiu/scripts/run_update.py`
-`.date()``datetime.combine` 替换为 `business_date()` + `business_day_range()`
- _Requirements: 12.4, 12.5_
- [x] 15. Final Checkpoint — 全量验证
- 确保所有属性测试通过:`cd C:\NeoZQYY && pytest tests/test_property_business_day_cutoff.py -v`
- 确保 ETL 单元测试通过:`cd apps/etl/connectors/feiqiu && pytest tests/unit -v`
- 确认所有 12 项需求的验收标准均有对应任务覆盖
- 如有问题请向用户确认。
## 备注
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
- 每个任务引用了具体的需求编号,确保可追溯性
- 属性测试验证设计文档中的 8 个正确性属性
- Checkpoint 任务确保增量验证,及时发现问题
- 设计文档标注 `defaults.py``env_parser.py` 已完成,任务 4.1 仅需增加校验逻辑

View File

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

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 @@
{"generationMode": "requirements-first"}

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 @@
{"specId": "7e1dc63d-3dbd-4462-a43c-9ecaa9b1dd07", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,440 @@
# 设计文档DWD 业务全景梳理
## 概述
本设计文档描述如何系统梳理飞球 ETL 的 DWD 层全部业务数据,产出 5 份全景分析文档。这是一个纯文档梳理任务,不涉及代码改动,核心挑战在于:如何在"数据本源优先"的强制准则下,高效、准确地完成 7 个业务域、约 30 张表(含扩展表)的字段语义验证和跨表关联分析。
### 设计目标
1. 定义可重复执行的梳理方法论,确保每张表的分析过程一致
2. 设计每份文档的结构模板,确保产出物格式统一
3. 规划数据验证的技术方案,确保结论可追溯
4. 设计文档间的引用和关联机制,避免信息重复
### 范围
- 输入:`test_etl_feiqiu` 数据库 DWD schema 的全部表和数据
- 参考:现有 BD 手册(`apps/etl/connectors/feiqiu/docs/database/DWD/`)、已有分析报告(`docs/reports/`
- 产出5 份全景文档,输出到 `docs/reports/`
### DWD 表全景
根据现有 BD 手册DWD 层包含以下表:
| 类型 | 表数量 | 表名 |
|------|--------|------|
| 维度表(主表) | 9 | `dim_site`, `dim_table`, `dim_assistant`, `dim_member`, `dim_member_card_account`, `dim_tenant_goods`, `dim_store_goods`, `dim_goods_category`, `dim_groupbuy_package` |
| 维度表(扩展表) | 8 | 上述除 `dim_goods_category` 外均有 `_ex` 扩展表 |
| 事实表(主表) | 13 | `dwd_settlement_head`, `dwd_payment`, `dwd_table_fee_log`, `dwd_table_fee_adjust`, `dwd_assistant_service_log`, `dwd_member_balance_change`, `dwd_recharge_order`, `dwd_refund`, `dwd_groupbuy_redemption`, `dwd_platform_coupon_redemption`, `dwd_store_goods_sale`, `dwd_goods_stock_summary`, `dwd_goods_stock_movement` |
| 事实表(扩展表) | 11 | 除 `dwd_payment`, `dwd_goods_stock_summary`, `dwd_goods_stock_movement` 外均有 `_ex` 扩展表 |
共计约 43 张表。
## 架构
### 整体梳理流程
梳理工作分为两个阶段:基础层(表结构与字段语义)和全景层(跨表关联分析)。基础层产出为全景层的输入。
```mermaid
flowchart TD
subgraph Phase1["阶段一:基础层梳理"]
A[枚举 DWD 全部表] --> B[按业务域分组]
B --> C[逐表执行智能聚焦分析]
C --> D[字段分类筛选]
D --> E[业务关键字段倒推验证]
E --> F[产出DWD 表结构与字段语义总览]
end
subgraph Phase2["阶段二:全景层梳理"]
F --> G[业务全景:消费产生机制]
F --> H[账务全景:结算与支付]
F --> I[财务全景:收入与对账]
F --> J[维度表与主数据全景]
end
subgraph Validation["贯穿:数据验证"]
K[information_schema 查表结构]
L[SQL 查询验证值域分布]
M[交叉查询验证关联关系]
N[对账公式全量验证]
end
C -.-> K
E -.-> L
E -.-> M
H -.-> N
```
### 信息流向
```mermaid
flowchart LR
subgraph 输入源
DB[(test_etl_feiqiu<br/>DWD schema)]
BD[现有 BD 手册<br/>宏观参考]
RPT[已有分析报告<br/>待验证假设]
end
subgraph 梳理过程
DB -->|information_schema| META[表结构元数据]
DB -->|SQL 查询| DATA[实际数据验证]
BD -->|宏观层可参考| REF[参考起点]
RPT -->|字段级需验证| HYP[待验证假设]
end
subgraph 产出
META --> DOC1[文档1: 表结构总览]
DATA --> DOC1
REF --> DOC1
HYP --> DOC1
DOC1 --> DOC2[文档2: 业务全景]
DOC1 --> DOC3[文档3: 账务全景]
DOC1 --> DOC4[文档4: 财务全景]
DOC1 --> DOC5[文档5: 维度表全景]
end
```
## 组件与接口
### 组件一:单表智能聚焦分析器
对每张 DWD 表执行标准化的分析流程,产出该表的字段语义报告。
#### 分析流程(每张表)
```
步骤 1: 表结构获取
→ 查询 information_schema.columns 获取列名、类型、nullable
→ 禁止参考 db/ 目录下的 DDL .sql 文件
步骤 2: 字段分类筛选
→ 查询全表空字段SELECT column WHERE ALL NULL标记为"空字段-跳过"
→ 识别 ETL 管理字段_etl_loaded_at, _etl_batch_id 等),简要标注
→ 识别含义透明字段id, site_id, created_at 等),仅列出
→ 剩余为"业务关键字段",进入深度验证
步骤 3: 业务关键字段倒推验证
→ 从含义明确的字段出发(如 id, site_id, total_amount
→ 通过 JOIN / 聚合对比 / 值域交叉推断不确定字段
→ 金额字段MIN/MAX/AVG/中位数/NULL占比 + 交叉验证
→ 枚举字段DISTINCT 值 + 频次分布
→ 关联 IDJOIN 验证关联完整性
步骤 4: 偏差检测
→ 对比现有 BD 手册的字段描述
→ 标注一致/偏差/错误
```
#### 输出格式(每张表)
```markdown
### {table_name}
**业务职责**:一句话描述
**数据状态**{行数} 行,时间范围 {min_date} ~ {max_date}
**主键**{pk_fields}
**关联表**{related_tables with join fields}
#### 业务关键字段
| 字段名 | 类型 | 验证状态 | 语义说明 | 值域/分布 |
|--------|------|----------|----------|-----------|
| ... | ... | ✅/⚠️/❌ | ... | ... |
#### 空字段(附录)
{列出全 NULL 的字段名}
#### 偏差记录
{与现有文档不一致的地方}
```
### 组件二:全景文档生成器
基于单表分析结果,按业务视角组织跨表关联分析。
#### 全景文档通用模板
```markdown
# {全景文档标题}
> 数据来源test_etl_feiqiu (DWD schema)
> 验证日期:{date}
> 数据时间范围:{min_date} ~ {max_date}
## 目录
{自动生成}
## 正文
{按业务逻辑组织的分析内容}
{每个关键结论标注验证状态:✅ 已验证 / ⚠️ 部分验证 / ❌ 未验证}
## 附录
### 验证 SQL
{关键验证查询}
### 数据样例
{来自测试库的真实数据}
```
### 组件三:数据验证引擎
贯穿整个梳理过程的验证机制。
#### 验证类型
| 验证类型 | 方法 | 适用场景 |
|----------|------|----------|
| 值域验证 | MIN/MAX/AVG/MEDIAN/NULL% | 金额字段、数值字段 |
| 枚举验证 | DISTINCT + COUNT | 状态字段、类型字段 |
| 关联验证 | LEFT JOIN + NULL 检查 | 外键关联完整性 |
| 等式验证 | SUM 对比 | 对账公式F1~F6 等) |
| 交叉验证 | 多表 JOIN + 聚合对比 | 跨表金额一致性 |
| 边界验证 | WHERE = 0 / < 0 / IS NULL | 异常值业务含义 |
#### 验证结果标注规范
- ✅ 已验证:附验证 SQL 摘要或结果统计
- ⚠️ 部分验证:附已知例外数量和分类
- ❌ 未验证:附原因(数据不足/无法关联/逻辑不明)
- ⚠️ 警告:经多次交叉验证仍无法对齐的数据关系
## 数据模型
### DWD 表按业务域分组
本次梳理将 DWD 层全部表按 7 个业务域组织,每个域内的表构成一个分析单元。
```mermaid
erDiagram
%% 结算域
dwd_settlement_head ||--o{ dwd_payment : "order_settle_id"
dwd_settlement_head ||--o{ dwd_table_fee_log : "order_settle_id"
dwd_settlement_head ||--o{ dwd_store_goods_sale : "order_settle_id"
dwd_settlement_head ||--o{ dwd_assistant_service_log : "order_settle_id"
dwd_settlement_head ||--o{ dwd_platform_coupon_redemption : "order_settle_id"
dwd_settlement_head ||--o{ dwd_refund : "order_settle_id"
%% 台桌域
dim_table ||--o{ dwd_table_fee_log : "table_id"
dwd_table_fee_log ||--o{ dwd_table_fee_adjust : "table_fee_log_id"
%% 助教域(作废判断已内聚到 dwd_assistant_service_log_ex.is_trash
dim_assistant ||--o{ dwd_assistant_service_log : "assistant_id"
%% 会员域
dim_member ||--o{ dim_member_card_account : "member_id"
dim_member ||--o{ dwd_member_balance_change : "member_id"
dim_member_card_account ||--o{ dwd_recharge_order : "tenant_member_card_id"
%% 团购域
dim_groupbuy_package ||--o{ dwd_groupbuy_redemption : "groupbuy_package_id"
%% 商品域
dim_goods_category ||--o{ dim_tenant_goods : "category_id"
dim_tenant_goods ||--o{ dim_store_goods : "tenant_goods_id"
dim_store_goods ||--o{ dwd_store_goods_sale : "site_goods_id"
dim_store_goods ||--o{ dwd_goods_stock_summary : "site_goods_id"
dim_store_goods ||--o{ dwd_goods_stock_movement : "site_goods_id"
%% 门店维度
dim_site ||--o{ dwd_settlement_head : "site_id"
```
### 业务域与表映射
| 业务域 | 事实表 | 维度表 | 核心关联 |
|--------|--------|--------|----------|
| 结算 | `dwd_settlement_head`(+ex), `dwd_payment`, `dwd_refund`(+ex) | — | 结算单是所有消费的汇总入口 |
| 台桌 | `dwd_table_fee_log`(+ex), `dwd_table_fee_adjust`(+ex) | `dim_table`(+ex) | 台费计费流水 → 台费调整 |
| 助教 | `dwd_assistant_service_log`(+ex) | `dim_assistant`(+ex) | 助教服务流水(作废通过 `_ex.is_trash` 判断) |
| 会员 | `dwd_member_balance_change`(+ex), `dwd_recharge_order`(+ex) | `dim_member`(+ex), `dim_member_card_account`(+ex) | 充值 → 余额变动 |
| 团购 | `dwd_groupbuy_redemption`(+ex), `dwd_platform_coupon_redemption`(+ex) | `dim_groupbuy_package`(+ex) | 团购核销 → 平台券核销 |
| 商品 | `dwd_store_goods_sale`(+ex) | `dim_tenant_goods`(+ex), `dim_store_goods`(+ex), `dim_goods_category` | 商品销售流水 |
| 库存 | `dwd_goods_stock_summary`, `dwd_goods_stock_movement` | (复用商品域维度表) | 库存汇总 + 变动流水 |
### 5 份文档的数据依赖关系
```mermaid
flowchart TD
DOC1["文档1: DWD 表结构与字段语义总览<br/>覆盖全部 43 张表"]
DOC1 --> DOC2["文档2: 业务全景<br/>消费产生机制"]
DOC1 --> DOC3["文档3: 账务全景<br/>结算与支付流水"]
DOC1 --> DOC4["文档4: 财务全景<br/>收入确认与对账"]
DOC1 --> DOC5["文档5: 维度表与主数据全景<br/>全部维度表"]
DOC2 -->|消费构成| DOC3
DOC2 -->|消费金额| DOC4
DOC3 -->|支付渠道| DOC4
DOC5 -->|维度关联| DOC2
DOC5 -->|维度关联| DOC3
```
### 文档产出路径与文件名
| 序号 | 文档 | 文件名 | 路径 |
|------|------|--------|------|
| 1 | DWD 表结构与字段语义总览 | `dwd-table-structure-overview.md` | `docs/reports/` |
| 2 | 业务全景:消费产生机制 | `dwd-business-panorama.md` | `docs/reports/` |
| 3 | 账务全景:结算与支付流水 | `dwd-accounting-panorama.md` | `docs/reports/` |
| 4 | 财务全景:收入确认与对账 | `dwd-financial-panorama.md` | `docs/reports/` |
| 5 | 维度表与主数据全景 | `dwd-dimension-panorama.md` | `docs/reports/` |
### 文档间引用规范
- 文档间使用相对路径引用:`[表结构总览](./dwd-table-structure-overview.md#table_name)`
- 引用已有分析报告:`[消费金额口径分析](./consume-money-caliber-deep-analysis.md#章节名)`
- 引用 BD 手册:`[BD 手册](../../apps/etl/connectors/feiqiu/docs/database/DWD/main/BD_manual_xxx.md)`
- 引用结论时标注验证状态和来源文档
## 正确性属性
*属性Property是一种在系统所有合法执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1: DWD 表覆盖完整性
*对于* DWD schema`test_etl_feiqiu.dwd`)中 `information_schema.tables` 返回的任意表,产出的文档集合中必须包含对该表的描述段落(表名出现在某份文档的标题或表格中)。
**Validates: Requirements 1.1, 5.1**
### Property 2: 主键标注准确性
*对于* DWD schema 中的任意表,文档中记录的主键字段集合必须与 `information_schema.table_constraints` + `key_column_usage` 查询返回的实际主键约束一致。
**Validates: Requirements 1.5**
### Property 3: 业务环节数据佐证
*对于* 业务全景文档文档2中描述的任意业务环节段落该段落必须包含至少一个来自测试库的数据样例以代码块或表格形式呈现的查询结果
**Validates: Requirements 2.6**
### Property 4: 对账公式验证一致性
*对于* 账务全景文档文档3中列出的任意对账公式文档中标注的成立率和例外数量必须与在 `test_etl_feiqiu` 全量数据上执行该公式验证 SQL 的实际结果一致。
**Validates: Requirements 3.5, 6.2**
### Property 5: 文档元数据完整性
*对于* 产出的任意全景文档,文档开头必须包含:(a) 数据来源标注(`test_etl_feiqiu`)、(b) 验证日期、(c) 数据时间范围(最早和最晚记录的时间)。
**Validates: Requirements 6.4**
### Property 6: 文档输出路径正确性
*对于* 本次梳理产出的任意文档文件,其路径必须位于 `docs/reports/` 目录下。
**Validates: Requirements 7.1**
### Property 7: 文档模板一致性
*对于* 产出的任意全景文档,其结构必须包含以下模板元素:标题、数据来源与验证日期块、目录、正文、附录(含验证 SQL 或数据样例)。
**Validates: Requirements 7.3**
### Property 8: 内部链接格式
*对于* 产出文档中的任意内部链接(指向本项目其他 markdown 文件的链接),链接必须使用相对路径格式(以 `./``../` 开头),且目标文件实际存在。
**Validates: Requirements 7.5**
## 错误处理
本任务是纯文档梳理,不涉及运行时代码。"错误"主要指梳理过程中遇到的数据异常和验证失败。
### 数据异常处理策略
| 异常场景 | 处理方式 | 文档标注 |
|----------|----------|----------|
| 表无数据0 行) | 跳过字段语义验证,仅记录表结构 | `❌ 未验证:表无数据` |
| 数据量不足(<10 行) | 执行有限验证,标注样本量不足 | `⚠️ 部分验证:仅 N 行数据` |
| 字段全 NULL | 标记为空字段,不展开分析 | 附录中列出字段名 |
| 对账公式不成立 | 分析例外案例,量化影响范围 | `⚠️ 成立率 X%,例外 N 笔` |
| 交叉验证矛盾 | 记录矛盾细节,标注为不确定 | `⚠️ 警告:无法对齐` |
| 现有文档与数据不一致 | 以数据为准,记录偏差 | `偏差记录` 段落 |
### 不确定性升级机制
当遇到以下情况时,必须在文档中以 `⚠️ 警告` 醒目标记:
1. 经过 ≥3 次不同角度的交叉验证仍无法确认的字段含义
2. 对账公式成立率 < 95% 且无法归因的例外
3. 金额字段的计算关系无法通过任何已知公式解释
4. 枚举值在现有文档中未记录且无法通过数据推断含义
警告内容须包含:已尝试的验证方法、无法确认的具体原因、建议的后续验证方向。
## 测试策略
### 测试方法说明
本任务的产出物是 markdown 文档而非代码,因此传统的单元测试和属性测试需要适配为"文档正确性验证"。
### 属性测试Property-Based Testing
使用 Python + `hypothesis` 库,针对设计文档中定义的正确性属性编写验证脚本。
**测试库**`hypothesis`(项目已有,见 `tests/` 目录)
**最低迭代次数**100 次(对于涉及随机采样的属性)
**测试位置**`tests/` 目录Monorepo 级属性测试)
#### 属性测试实现方案
| Property | 测试方法 | 实现思路 |
|----------|----------|----------|
| P1: 表覆盖完整性 | 查询 information_schema → 解析文档 → 比对 | 从数据库获取全部 DWD 表名,解析 5 份文档提取提及的表名,验证覆盖率 = 100% |
| P2: 主键标注准确性 | 查询 PK 约束 → 解析文档 → 比对 | 对于随机采样的 N 张表,比对文档中的主键与数据库实际主键 |
| P3: 业务环节数据佐证 | 解析文档段落 → 检查数据样例 | 解析业务全景文档的每个业务环节段落,验证包含代码块或数据表格 |
| P4: 对账公式验证一致性 | 提取公式 → 执行 SQL → 比对成立率 | 对于文档中的每个对账公式,重新执行验证 SQL比对成立率 |
| P5: 文档元数据完整性 | 解析文档头部 → 检查必要字段 | 对于每份文档,检查开头是否包含数据来源、验证日期、时间范围 |
| P6: 文档路径正确性 | 列出产出文件 → 检查路径 | 验证所有产出文件位于 `docs/reports/` |
| P7: 文档模板一致性 | 解析文档结构 → 检查模板元素 | 对于每份文档,检查是否包含标题、元数据块、目录、正文、附录 |
| P8: 内部链接格式 | 正则提取链接 → 检查格式和目标 | 提取所有 markdown 链接,验证使用相对路径且目标文件存在 |
#### 属性测试标签格式
每个属性测试必须包含注释标签:
```python
# Feature: dwd-business-panorama, Property 1: DWD 表覆盖完整性
# Feature: dwd-business-panorama, Property 2: 主键标注准确性
# ...
```
### 单元测试(示例测试)
针对 prework 中标记为 `yes - example` 的验收标准:
| 验收标准 | 测试内容 |
|----------|----------|
| 1.6 SCD2 字段标注 | 检查 `dim_member` 文档中是否标注了 `scd2_start_time`, `scd2_end_time`, `scd2_is_current`, `scd2_version` |
| 2.2 消费类目覆盖 | 检查业务全景文档中是否包含"台费"、"商品消费"、"助教服务"、"灯控电费"四个关键词 |
| 2.5 团购三层价格 | 检查文档中是否提及 `sale_price``pl_coupon_sale_amount``coupon_amount` |
| 2.7 Mermaid 流程图 | 检查业务全景文档中是否包含 ` ```mermaid` 代码块 |
| 3.7 consume_money 三种口径 | 检查文档中是否包含口径 A、B、C 的描述 |
| 4.5 对账矩阵 | 检查财务全景文档中是否包含矩阵格式的表格 |
| 7.2 5 份文档产出 | 检查 `docs/reports/` 下是否存在 5 个指定文件名 |
| 7.4 Mermaid 图表 | 检查每份文档中是否包含至少一个 Mermaid 代码块 |
### 边界条件测试
| 验收标准 | 边界场景 |
|----------|----------|
| 1.7 表无数据 | 验证无数据表在文档中有"数据不足"标注 |
### 测试执行
```bash
# 属性测试Monorepo 级)
cd C:\NeoZQYY && pytest tests/test_dwd_panorama_properties.py -v
# 单元测试
cd C:\NeoZQYY && pytest tests/test_dwd_panorama_examples.py -v
```

View File

@@ -0,0 +1,144 @@
# 需求文档DWD 业务全景梳理
## 简介
从 DWD 层出发,系统梳理飞球 ETL 对台球厅所有业务数据的记录方式。不仅覆盖每个字段的含义,还要搞清楚字段间的关联性,最终产出覆盖业务全景、账务全景、财务全景三个维度的分析文档。
现有的 `consume-money-caliber-deep-analysis.md``consumption-cases-analysis.md` 已深入分析了结算/支付相关的 DWD 表,但仅从支付订单金额角度无法搞清楚整个球房的业务、财务、账务全貌。本 SPEC 旨在补全剩余的业务域(台桌、助教、会员、团购、商品、库存),并将所有域串联成三个全景视图。
## ⚠️ 核心准则:数据本源优先(强制)
**启动本 SPEC 的根本原因**:项目中现有的所有文档(包括 `consume-money-caliber-deep-analysis.md``consumption-cases-analysis.md`、BD 手册、ETL 代码注释等)都可能存在纰漏、过期、遗漏或与数据库现实不符的情况。因此本次梳理必须遵循以下强制准则:
### 信息可信度分层
| 层级 | 可信度 | 说明 | 使用方式 |
|------|--------|------|----------|
| 宏观/直观层 | ✅ 可参考 | 表的业务归属、业务域划分、主要关联方向、流程大框架等直观明显的信息 | 直接参考作为起点,无需逐一验证 |
| 字段级/数据关联层 | ⚠️ 必须验证 | 字段语义、金额计算规则、枚举值含义、跨表关联关系等细节 | 慎之又慎,必须通过测试库数据关联做推理验证,不可直接采信 |
### 强制准则
1. **数据库是唯一真相源(字段级)**:字段级别的结论必须以测试库(`test_etl_feiqiu`)的实际表结构和数据为准。表结构信息必须通过查询 `information_schema``pg_catalog` 获取,禁止参考 `db/` 目录下的 DDL `.sql` 文件。宏观层面的信息(表的职责、业务域归属、流程大框架)可参考现有文档作为起点
2. **先查后写,禁止臆断**:描述任何字段语义、业务规则、金额关系之前,必须先通过 SQL 查询验证实际数据分布和值域。未经查询验证的结论禁止写入文档
3. **刨根问底,通过数据关联做推理**:对每个金额字段、状态枚举、关联关系,不能停留在"看起来是什么",必须通过交叉查询、边界案例、异常值分析确认其真实业务含义。从含义明确的字段出发,通过数据关联倒推不确定的字段
4. **现有文档作为假设而非事实**:引用现有文档的字段级结论时,必须标注为"待验证假设",并在验证后标注实际结果(一致/偏差/错误)
5. **偏差必须显式记录**:当验证发现现有文档与数据库现实不一致时,必须在新文档中明确记录偏差内容、偏差原因(如果能推断)、以及修正后的正确描述
6. **无数据不下结论**:如果测试库中某张表无数据或数据量不足以支撑结论,必须明确标注"数据不足,无法验证",禁止基于推测填充内容
7. **不确定性必须显式警告**:对于以下情况,必须在文档中以醒目的 `⚠️ 警告` 标记:(a) 经过长时间推理和多次交叉验证仍无法对齐的数据关系;(b) 根据现有数据和依据无法确认的字段含义或业务规则。警告内容须说明:已尝试的验证方法、无法确认的具体原因、建议的后续验证方向
## 术语表
- **DWD_层**Data Warehouse Detail 层ETL 管道中的明细数据层,存储经过清洗和标准化的业务事实和维度数据
- **全景文档**:按特定视角(业务/账务/财务)组织的、覆盖所有相关 DWD 表及其字段关联的分析文档
- **梳理器**:执行本 SPEC 任务的分析人员或脚本,负责读取 DWD 表结构和数据并产出文档
- **测试库**PostgreSQL 数据库 `test_etl_feiqiu`,包含 DWD 层的实际数据,用于验证文档描述的准确性
- **业务域**DWD 表按业务功能的分组,包括:结算、台桌、助教、会员、团购、商品、库存
- **主表/扩展表**DWD 层的表设计模式,主表存核心字段,`_ex` 扩展表存补充字段,通过主键 1:1 关联
- **维度表**:以 `dim_` 前缀命名的 DWD 表存储缓慢变化的主数据SCD2 模式)
- **事实表**:以 `dwd_` 前缀命名的 DWD 表,存储业务事件流水(增量写入)
- **字段语义**:字段在实际业务中的真实含义,可能与 DDL 注释不一致(如 `point_amount` 实际是线上收款而非积分)
- **对账公式**:用于校验数据一致性的等式关系,如 F1消费构成、F2收支平衡
## 需求
### 需求 1DWD 表结构与字段语义梳理(智能聚焦策略)
**用户故事:** 作为数据分析人员,我希望获得每张 DWD 表中有业务意义的字段的准确语义说明,以便理解数据如何记录业务事实。
**梳理策略说明:** `apps/etl/connectors/feiqiu/docs/database/DWD/` 下已有各表的文档,宏观层面(表的职责、业务域归属、主要关联关系)可作为参考起点。但字段级别的语义必须通过数据关联倒推验证,不可直接采信。梳理时采用"智能聚焦"策略,而非逐字段全量罗列。
#### 验收标准
1. THE 梳理器 SHALL 覆盖 DWD 层全部 7 个业务域(结算、台桌、助教、会员、团购、商品、库存)的所有主表和扩展表,以现有 DWD 文档为宏观参考起点
2. THE 梳理器 SHALL 对每张表先执行字段分类筛选:(a) 查询全表空字段(全部为 NULL 的列),标记为"空字段-跳过"(b) 识别含义明确的基础字段(如 `id``site_id``created_at`),简要标注即可;(c) 聚焦于业务关键字段(金额、状态、类型、关联 ID进行深度验证
3. WHEN 梳理业务关键字段时THE 梳理器 SHALL 采用"倒推法"先从含义明确的字段出发通过数据关联JOIN、聚合对比、值域交叉推断不确定字段的真实含义
4. WHEN 发现字段的实际业务含义与现有 DWD 文档或 DDL 注释不一致时THE 梳理器 SHALL 明确标注偏差内容并给出基于测试库数据验证的修正说明
5. THE 梳理器 SHALL 标注每张表的主键、外键关联、以及与其他 DWD 表的关联方式(关联字段和关联类型如 1:1、1:N
6. THE 梳理器 SHALL 标注每张维度表的 SCD2 生效/失效字段和当前记录标识字段
7. IF 测试库中某张表无数据或数据量不足以验证字段语义THEN THE 梳理器 SHALL 在文档中标注该表的数据状态并说明无法验证的字段
8. THE 梳理器 SHALL 主动忽略以下字段类别,不在文档中详细展开:(a) 全表 NULL 的空字段(仅在附录中列出字段名);(b) ETL 管理字段(如 `_etl_loaded_at``_etl_batch_id`),仅简要说明用途;(c) 含义完全透明且无歧义的字段(如 `id``created_at``updated_at`),仅在表结构概览中列出
### 需求 2业务全景文档——消费是怎么产生的
**用户故事:** 作为门店经营者,我希望搞清楚台球厅的消费是怎么来的、价格怎么报的、优惠怎么产生的,以便理解整个消费链路。
#### 验收标准
1. THE 全景文档 SHALL 描述从顾客开台到结算的完整业务流程,标注每个环节涉及的 DWD 表和关键字段
2. THE 全景文档 SHALL 覆盖以下消费类目的产生机制:台费(含多台桌合并)、商品消费、助教服务(陪打/超休)、灯控电费
3. THE 全景文档 SHALL 描述台费的计价规则,包括:台桌类型与单价的关系、计时方式、台费折扣(`dwd_table_fee_adjust`)的触发条件和计算方式
4. THE 全景文档 SHALL 描述优惠的产生机制,包括:平台团购券(美团/抖音)的核销流程、会员折扣的计算方式、台费调整(`adjust_amount`)的业务场景
5. THE 全景文档 SHALL 描述团购券的三层价格体系(顾客支付给平台的 `sale_price`、平台结算给门店的 `pl_coupon_sale_amount`、门店实际抵扣的 `coupon_amount`),并标注每层价格对应的 DWD 表和字段
6. WHEN 描述某个业务环节时THE 全景文档 SHALL 提供至少一个来自测试库的真实数据样例作为佐证
7. THE 全景文档 SHALL 以 Mermaid 流程图展示从开台到结算的完整数据流向
### 需求 3账务全景文档——客户怎么结算的
**用户故事:** 作为门店财务人员,我希望搞清楚客户的每一笔消费是通过什么方式结算的、支付流水怎么记录的,以便进行对账和核销。
#### 验收标准
1. THE 全景文档 SHALL 描述所有支付渠道及其在 DWD 层的记录方式,包括:线上收款(微信/支付宝)、现金、储值卡余额(含礼品卡/充值卡)、平台团购券
2. THE 全景文档 SHALL 描述 `dwd_payment` 表与 `dwd_settlement_head` 的关联方式,以及 `payment_method` 枚举值与实际支付渠道的对应关系
3. THE 全景文档 SHALL 描述会员储值卡体系,包括:充值流程(`dwd_recharge_order`)、余额变动记录(`dwd_member_balance_change`)、本金/赠送金额的分账逻辑
4. THE 全景文档 SHALL 描述退款流程,包括:结算退款、充值退款、转账退款的触发场景和在 DWD 层的记录方式
5. THE 全景文档 SHALL 列出所有已验证的对账公式F1~F6、R1~R3、RF1~RF2、B1~B4标注每个公式的适用范围、成立率、以及已知的例外情况
6. WHEN 描述支付方式推断逻辑时THE 全景文档 SHALL 提供完整的推断规则(因 `settlement_head_ex.payment_method` 全部为 0 不可用)
7. THE 全景文档 SHALL 描述 `consume_money` 字段的三种历史口径A/B/C及其时间线标注当前生效的口径
### 需求 4财务全景文档——收入确认与对账
**用户故事:** 作为门店管理者,我希望搞清楚门店的收入如何确认、各渠道的资金如何对账,以便进行财务分析和经营决策。
#### 验收标准
1. THE 全景文档 SHALL 描述门店收入的构成,按收入来源分类:台费收入、商品收入、助教服务收入、充值收入
2. THE 全景文档 SHALL 描述每种收入来源对应的 DWD 表、关键金额字段、以及从 DWD 到 DWS 的聚合路径
3. THE 全景文档 SHALL 描述平台团购券场景下的收入确认逻辑:门店实际收入 = `pl_coupon_sale_amount`(平台结算额),而非 `coupon_amount`(券面值抵扣额),差额为门店补贴
4. THE 全景文档 SHALL 描述储值卡充值场景的资金流向:充值收款 → 余额入账(本金+赠送)→ 消费扣款 → 退款(如有),标注每个环节的 DWD 表和金额字段
5. THE 全景文档 SHALL 提供按支付渠道的对账矩阵,列出每种支付渠道在 DWD 层涉及的表和字段,以及跨表校验的公式
6. THE 全景文档 SHALL 标注已知的数据质量问题和对账例外(如助教券的支付缺口、商品消费未覆盖等),并给出影响范围的量化评估
### 需求 5维度表与主数据全景
**用户故事:** 作为数据开发人员,我希望搞清楚 DWD 层所有维度表的结构和业务含义,以便在 DWS 聚合时正确关联维度。
#### 验收标准
1. THE 全景文档 SHALL 覆盖所有 DWD 维度表(`dim_site``dim_table``dim_assistant``dim_member``dim_member_card_account``dim_tenant_goods``dim_store_goods``dim_goods_category``dim_groupbuy_package`),包括主表和扩展表
2. THE 全景文档 SHALL 描述每张维度表与事实表的关联方式,标注关联字段和关联基数
3. THE 全景文档 SHALL 描述会员体系的数据结构,包括:会员档案(`dim_member`)、储值卡账户(`dim_member_card_account`)、会员等级/标签的记录方式
4. THE 全景文档 SHALL 描述商品体系的数据结构,包括:商品分类树(`dim_goods_category`)、租户商品(`dim_tenant_goods`)与门店商品(`dim_store_goods`)的关系、库存相关表(`dwd_goods_stock_summary``dwd_goods_stock_movement`)的结构
5. THE 全景文档 SHALL 描述团购套餐维度(`dim_groupbuy_package`)的结构,包括:套餐与券种的关系、价格体系(面值/售价/门店结算价)
### 需求 6数据验证与文档可信度保障
**用户故事:** 作为数据分析人员,我希望文档中的每个结论都经过测试库数据验证,以避免使用过期或错误的信息。
#### 验收标准
1. WHEN 梳理任何 DWD 表的字段语义时THE 梳理器 SHALL 通过查询测试库(`test_etl_feiqiu`)验证字段的实际值分布,确认语义描述的准确性。禁止仅凭 DDL 注释或现有文档描述字段含义
2. WHEN 文档引用对账公式时THE 梳理器 SHALL 提供该公式在测试库全量数据上的验证结果(成立率、例外数量和分类)
3. IF 验证过程中发现现有文档(`consume-money-caliber-deep-analysis.md``consumption-cases-analysis.md`、BD 手册、ETL 代码注释等的结论与测试库数据不一致THEN THE 梳理器 SHALL 在新文档中明确标注修正内容,包括:原文档的描述、实际数据的表现、偏差原因分析
4. THE 梳理器 SHALL 在每份全景文档的开头标注数据验证日期和测试库数据的时间范围
5. THE 全景文档 SHALL 对每个关键结论标注验证状态:✅ 已验证(附验证 SQL 或结果摘要)、⚠️ 部分验证(附已知例外)、❌ 未验证(附原因)
6. THE 梳理器 SHALL 对每个金额字段执行以下深度验证流程:(a) 查询值域分布MIN/MAX/AVG/中位数/NULL 占比);(b) 与关联字段交叉验证(如 `total_amount` 是否等于各子项之和);(c) 检查边界案例(零值、负值、极端值)的业务含义
7. WHEN 引用现有文档的任何结论时THE 梳理器 SHALL 将其标注为"待验证假设",并在验证后更新为实际结果(一致 ✅ / 偏差 ⚠️ / 错误 ❌),附偏差说明
### 需求 7文档产出与组织
**用户故事:** 作为项目成员,我希望全景文档按统一格式组织并落在正确的路径下,以便团队查阅和后续维护。
#### 验收标准
1. THE 梳理器 SHALL 将所有全景文档输出到 `docs/reports/` 目录下
2. THE 梳理器 SHALL 产出以下文档(文件名待确认):
- DWD 表结构与字段语义总览
- 业务全景:消费产生机制
- 账务全景:结算与支付流水
- 财务全景:收入确认与对账
- 维度表与主数据全景
3. THE 全景文档 SHALL 使用统一的文档模板,包含:标题、数据来源与验证日期、目录、正文、附录(验证 SQL、数据样例
4. THE 全景文档 SHALL 在适当位置使用 Mermaid 图表ER 图、流程图、时序图)辅助说明数据关系和业务流程
5. WHEN 全景文档引用其他文档的结论时THE 梳理器 SHALL 使用相对路径链接到源文档,并标注引用的具体章节

View File

@@ -0,0 +1,277 @@
# 实施计划DWD 业务全景梳理
## 概述
将 DWD 业务全景梳理的设计方案转化为可执行的任务序列。任务按两阶段组织基础层文档1表结构总览→ 全景层文档2~5数据验证贯穿全程。属性测试使用 Python + hypothesis放置在 `tests/` 目录。
## Tasks
- [x] 1. 阶段一DWD 表结构与字段语义总览文档1
- [x] 1.1 枚举 DWD 全部表并按业务域分组
- 查询 `test_etl_feiqiu.dwd``information_schema.tables` 获取全部表名
- 按 7 个业务域(结算、台桌、助教、会员、团购、商品、库存)分组
- 查询每张表的行数和时间范围,确认数据状态
- 创建 `docs/reports/dwd-table-structure-overview.md` 文件骨架(含模板元素:标题、元数据块、目录、正文、附录)
- _Requirements: 1.1, 7.1, 7.2, 7.3_
- [x] 1.2 结算域表梳理dwd_settlement_head/ex, dwd_payment, dwd_refund/ex
- 对每张表执行单表智能聚焦分析information_schema 获取列信息 → 字段分类筛选(空字段/ETL字段/透明字段/业务关键字段)→ 业务关键字段倒推验证
- 金额字段执行深度验证值域分布MIN/MAX/AVG/中位数/NULL占比+ 交叉验证
- 枚举字段执行 DISTINCT + 频次分布
- 对比现有 BD 手册标注偏差
- 将结果写入文档1的结算域章节
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.8, 6.1, 6.6_
- [x] 1.3 台桌域表梳理dim_table/ex, dwd_table_fee_log/ex, dwd_table_fee_adjust/ex
- 同 1.2 的分析流程
- 重点验证台费计价相关字段、台费调整的触发条件
- 标注 dim_table 的 SCD2 字段
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 6.1, 6.6_
- [x] 1.4 助教域表梳理dim_assistant/ex, dwd_assistant_service_log/ex
- 同 1.2 的分析流程
- 重点验证助教服务类型枚举、`_ex.is_trash` 作废标记的业务含义
- 标注 dim_assistant 的 SCD2 字段
- 注意:`dwd_assistant_trash_event` 已于 2026-02-22 DROP作废判断改用 `dwd_assistant_service_log_ex.is_trash`
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 6.1, 6.6_
- [x] 1.5 会员域表梳理dim_member/ex, dim_member_card_account/ex, dwd_member_balance_change/ex, dwd_recharge_order/ex
- 同 1.2 的分析流程
- 重点验证储值卡本金/赠送金额分账、余额变动类型枚举
- 标注 dim_member 和 dim_member_card_account 的 SCD2 字段
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 6.1, 6.6_
- [x] 1.6 团购域表梳理dim_groupbuy_package/ex, dwd_groupbuy_redemption/ex, dwd_platform_coupon_redemption/ex
- 同 1.2 的分析流程
- 重点验证团购三层价格体系sale_price / pl_coupon_sale_amount / coupon_amount
- 标注 dim_groupbuy_package 的 SCD2 字段
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.8, 6.1, 6.6_
- [x] 1.7 商品域与库存域表梳理dim_tenant_goods/ex, dim_store_goods/ex, dim_goods_category, dwd_store_goods_sale/ex, dwd_goods_stock_summary, dwd_goods_stock_movement
- 同 1.2 的分析流程
- 重点验证商品分类树结构、租户商品与门店商品的关系
- 标注维度表的 SCD2 字段dim_goods_category 无扩展表)
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 6.1, 6.6_
- [x] 1.8 完善文档1跨表关联关系汇总与文档收尾
- 汇总所有表的主键、外键关联、关联类型1:1, 1:N
- 补充 Mermaid ER 图展示跨域关联
- 补充附录:空字段汇总、验证 SQL 汇总
- 添加文档间引用链接(相对路径格式)
- 确认文档模板完整性(标题、元数据块、目录、正文、附录)
- _Requirements: 1.5, 6.4, 7.3, 7.4, 7.5_
- [x] 2. 检查点 - 文档1 完成确认
- 确认文档1`docs/reports/dwd-table-structure-overview.md`)已覆盖全部 42 张表(含新发现的 dim_staff/dim_staff_ex
- 确认每张表的业务关键字段均已通过测试库数据验证
- 确认偏差记录完整point_amount 等偏差已标注)
- 第9章门店维度有占位符待后续补充不阻塞全景文档
- [x] 3. 阶段二A业务全景文档文档2
- [x] 3.1 创建文档2骨架并梳理消费产生链路
- 创建 `docs/reports/dwd-business-panorama.md`,使用全景文档通用模板
- 描述从开台到结算的完整业务流程,标注每个环节涉及的 DWD 表和关键字段
- 以 Mermaid 流程图展示从开台到结算的完整数据流向
- 基于文档1的字段语义引用而非重复描述
- _Requirements: 2.1, 2.7, 6.4, 7.3_
- [x] 3.2 梳理各消费类目的产生机制
- 台费(含多台桌合并):计价规则、台桌类型与单价关系、计时方式
- 台费折扣dwd_table_fee_adjust触发条件和计算方式
- 商品消费:商品销售流水的记录方式
- 助教服务(陪打/超休):服务类型和计费方式
- 灯控电费:记录方式
- 每个环节提供至少一个测试库真实数据样例
- _Requirements: 2.2, 2.3, 2.6_
- [x] 3.3 梳理优惠与团购机制
- 平台团购券(美团/抖音)核销流程
- 会员折扣计算方式
- 台费调整adjust_amount的业务场景
- 团购券三层价格体系sale_price / pl_coupon_sale_amount / coupon_amount标注每层价格对应的 DWD 表和字段
- 每个环节提供测试库数据样例
- _Requirements: 2.4, 2.5, 2.6_
- [x] 3.4 文档2收尾附录与引用
- 补充附录(验证 SQL、数据样例
- 添加文档间引用链接
- 确认文档模板完整性
- _Requirements: 7.3, 7.4, 7.5_
- [x] 4. 阶段二B账务全景文档文档3
- [x] 4.1 创建文档3骨架并梳理支付渠道
- 创建 `docs/reports/dwd-accounting-panorama.md`,使用全景文档通用模板
- 描述所有支付渠道及其在 DWD 层的记录方式(线上收款、现金、储值卡余额、平台团购券)
- 描述 dwd_payment 与 dwd_settlement_head 的关联方式
- 描述 payment_method 枚举值与实际支付渠道的对应关系
- 描述支付方式推断逻辑(因 settlement_head_ex.payment_method 全部为 0 不可用)
- _Requirements: 3.1, 3.2, 3.6, 6.4, 7.3_
- [x] 4.2 梳理会员储值卡体系与退款流程
- 充值流程dwd_recharge_order
- 余额变动记录dwd_member_balance_change
- 本金/赠送金额分账逻辑
- 退款流程:结算退款、充值退款、转账退款的触发场景和 DWD 记录方式
- _Requirements: 3.3, 3.4_
- [x] 4.3 梳理对账公式与 consume_money 口径
- 列出所有已验证的对账公式F1~F6、R1~R3、RF1~RF2、B1~B4
- 对每个公式在 test_etl_feiqiu 全量数据上执行验证,标注成立率和例外情况
- 描述 consume_money 字段的三种历史口径A/B/C及时间线标注当前生效口径
- _Requirements: 3.5, 3.7, 6.2_
- [x] 4.4 文档3收尾附录与引用
- 补充附录(验证 SQL、对账公式验证结果
- 添加文档间引用链接
- 确认文档模板完整性
- _Requirements: 7.3, 7.4, 7.5_
- [x] 5. 检查点 - 文档2和文档3完成确认
- 确认文档2覆盖全部消费类目和优惠机制台费/商品/助教/灯控/团购券/会员折扣)
- 确认文档3覆盖全部支付渠道和对账公式F1/F2/B1-B3/consume_money三种口径
- 确认所有关键结论标注了验证状态
- 确认文档3覆盖全部支付渠道和对账公式
- 确认所有关键结论标注了验证状态
- Ensure all tests pass, ask the user if questions arise.
- [x] 6. 阶段二C财务全景文档文档4
- [x] 6.1 创建文档4骨架并梳理收入构成
- 创建 `docs/reports/dwd-financial-panorama.md`,使用全景文档通用模板
- 按收入来源分类描述门店收入构成:台费收入、商品收入、助教服务收入、充值收入
- 描述每种收入来源对应的 DWD 表、关键金额字段、从 DWD 到 DWS 的聚合路径
- _Requirements: 4.1, 4.2, 6.4, 7.3_
- [x] 6.2 梳理团购收入确认与储值卡资金流向
- 团购券场景收入确认逻辑:门店实际收入 = pl_coupon_sale_amount差额为门店补贴
- 储值卡充值资金流向:充值收款 → 余额入账(本金+赠送)→ 消费扣款 → 退款
- 标注每个环节的 DWD 表和金额字段
- _Requirements: 4.3, 4.4_
- [x] 6.3 构建对账矩阵与数据质量评估
- 按支付渠道构建对账矩阵:每种支付渠道涉及的 DWD 表和字段、跨表校验公式
- 标注已知数据质量问题和对账例外(助教券支付缺口、商品消费未覆盖等)
- 给出影响范围的量化评估
- _Requirements: 4.5, 4.6_
- [x] 6.4 文档4收尾附录与引用
- 补充附录(验证 SQL、对账矩阵详细数据
- 添加文档间引用链接引用文档2的消费构成、文档3的支付渠道
- 确认文档模板完整性
- _Requirements: 7.3, 7.4, 7.5_
- [x] 7. 阶段二D维度表与主数据全景文档5
- [x] 7.1 创建文档5骨架并梳理门店与台桌维度
- 创建 `docs/reports/dwd-dimension-panorama.md`,使用全景文档通用模板
- 梳理 dim_site/ex门店维度结构、SCD2 字段、与事实表的关联
- 梳理 dim_table/ex台桌维度结构、台桌类型枚举、与台费流水的关联
- _Requirements: 5.1, 5.2, 6.4, 7.3_
- [x] 7.2 梳理会员体系与助教维度
- 会员档案dim_member/ex会员等级/标签记录方式
- 储值卡账户dim_member_card_account/ex账户类型、余额字段
- 助教维度dim_assistant/ex助教类型、服务能力
- 描述维度表与事实表的关联方式,标注关联字段和基数
- _Requirements: 5.2, 5.3_
- [x] 7.3 梳理商品体系与团购维度
- 商品分类树dim_goods_category分类层级结构
- 租户商品dim_tenant_goods/ex与门店商品dim_store_goods/ex的关系
- 库存相关表dwd_goods_stock_summary, dwd_goods_stock_movement的结构
- 团购套餐维度dim_groupbuy_package/ex套餐与券种关系、价格体系
- _Requirements: 5.4, 5.5_
- [x] 7.4 文档5收尾Mermaid ER 图与附录
- 补充 Mermaid ER 图展示全部维度表与事实表的关联关系
- 补充附录(验证 SQL、SCD2 字段汇总)
- 添加文档间引用链接
- 确认文档模板完整性
- _Requirements: 5.1, 5.2, 7.3, 7.4, 7.5_
- [x] 8. 检查点 - 全部5份文档完成确认
- 确认 5 份文档均已创建在 `docs/reports/` 目录下
- 确认文档间引用链接格式正确(相对路径)且目标文件存在
- 确认每份文档包含完整模板元素(标题、元数据块、目录、正文、附录)
- Ensure all tests pass, ask the user if questions arise.
- [x] 9. 属性测试与示例测试
- [x] 9.1 创建属性测试文件骨架与公共工具
- 创建 `tests/test_dwd_panorama_properties.py`
- 实现公共工具函数:读取文档内容、解析 markdown 结构、提取表名、提取链接
- 配置 hypothesis settingsmin_examples=100
- 使用 `load_dotenv` 加载根 `.env`,通过 `TEST_DB_DSN` 连接测试库
- _Requirements: 6.1_
- [x] 9.2 编写 Property 1: DWD 表覆盖完整性
- **Property 1: DWD 表覆盖完整性**
- 查询 information_schema.tables 获取 DWD schema 全部表名
- 解析 5 份文档提取提及的表名
- 验证覆盖率 = 100%
- **Validates: Requirements 1.1, 5.1**
- [x] 9.3 编写 Property 2: 主键标注准确性
- **Property 2: 主键标注准确性**
- 对随机采样的表,查询 information_schema.table_constraints + key_column_usage 获取实际主键
- 解析文档1中该表的主键标注
- 验证一致性
- **Validates: Requirements 1.5**
- [x] 9.4 编写 Property 3: 业务环节数据佐证
- **Property 3: 业务环节数据佐证**
- 解析业务全景文档文档2的每个业务环节段落
- 验证每个段落包含至少一个代码块或数据表格
- **Validates: Requirements 2.6**
- [x] 9.5 编写 Property 4: 对账公式验证一致性
- **Property 4: 对账公式验证一致性**
- 提取账务全景文档文档3中的对账公式和标注的成立率
- 重新执行验证 SQL比对成立率
- **Validates: Requirements 3.5, 6.2**
- [x] 9.6 编写 Property 5: 文档元数据完整性
- **Property 5: 文档元数据完整性**
- 对每份全景文档检查开头是否包含数据来源test_etl_feiqiu、验证日期、数据时间范围
- **Validates: Requirements 6.4**
- [x] 9.7 编写 Property 6: 文档输出路径正确性
- **Property 6: 文档输出路径正确性**
- 验证所有产出文件位于 `docs/reports/` 目录下
- **Validates: Requirements 7.1**
- [x] 9.8 编写 Property 7: 文档模板一致性
- **Property 7: 文档模板一致性**
- 对每份文档,检查是否包含标题、元数据块、目录、正文、附录
- **Validates: Requirements 7.3**
- [x] 9.9 编写 Property 8: 内部链接格式
- **Property 8: 内部链接格式**
- 正则提取所有 markdown 内部链接
- 验证使用相对路径格式(以 `./``../` 开头)且目标文件存在
- **Validates: Requirements 7.5**
- [x] 9.10 编写示例测试
- 创建 `tests/test_dwd_panorama_examples.py`
- SCD2 字段标注检查dim_member 文档中包含 scd2_start_time 等字段
- 消费类目覆盖检查:业务全景文档包含"台费"、"商品消费"、"助教服务"、"灯控电费"
- 团购三层价格检查:文档包含 sale_price、pl_coupon_sale_amount、coupon_amount
- Mermaid 流程图检查:业务全景文档包含 mermaid 代码块
- consume_money 三种口径检查:文档包含口径 A、B、C
- 对账矩阵检查:财务全景文档包含矩阵格式表格
- 5 份文档产出检查docs/reports/ 下存在 5 个指定文件名
- 每份文档 Mermaid 图表检查
- 无数据表标注检查(边界条件)
- _Requirements: 1.6, 1.7, 2.2, 2.5, 2.7, 3.5, 3.7, 4.5, 7.2, 7.4_
- [x] 10. 最终检查点 - 全部完成确认
- 运行全部属性测试:`cd C:\NeoZQYY && pytest tests/test_dwd_panorama_properties.py -v`
- 运行全部示例测试:`cd C:\NeoZQYY && pytest tests/test_dwd_panorama_examples.py -v`
- 确认 5 份文档内容完整、验证状态标注齐全
- Ensure all tests pass, ask the user if questions arise.
## Notes
- 标记 `*` 的任务为可选,可跳过以加速 MVP
- 每个任务引用了具体的需求编号,确保可追溯
- 检查点任务确保增量验证
- 属性测试验证文档的结构正确性,示例测试验证具体内容覆盖
- 所有数据库查询必须使用测试库 `test_etl_feiqiu`,通过 `TEST_DB_DSN` 连接
- 文档1是其他4份文档的基础必须先完成阶段一再进入阶段二

View File

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

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 @@
{"specId": "cd79656c-9c23-4470-a147-d402b5f4b50b", "workflowType": "requirements-first", "specType": "feature"}

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