init: 项目初始提交 - NeoZQYY Monorepo 完整代码
This commit is contained in:
30
.env.template
Normal file
30
.env.template
Normal file
@@ -0,0 +1,30 @@
|
||||
# ============================================================
|
||||
# NeoZQYY Monorepo 公共环境配置模板
|
||||
# 复制为 .env 后填入实际值
|
||||
# ============================================================
|
||||
|
||||
# ---------- 数据库(公共) ----------
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
|
||||
# ---------- ETL 数据库(etl_feiqiu) ----------
|
||||
ETL_DB_NAME=etl_feiqiu
|
||||
|
||||
# ---------- 业务数据库(zqyy_app) ----------
|
||||
APP_DB_NAME=zqyy_app
|
||||
|
||||
# ---------- 时区 ----------
|
||||
TIMEZONE=Asia/Shanghai
|
||||
|
||||
# ---------- 门店标识 ----------
|
||||
STORE_ID=
|
||||
|
||||
# ---------- 上游 API ----------
|
||||
API_BASE_URL=
|
||||
API_TOKEN=
|
||||
|
||||
# ---------- 日志 ----------
|
||||
LOG_LEVEL=INFO
|
||||
LOG_DIR=logs
|
||||
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# ===== 临时与缓存 =====
|
||||
tmp/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
logs/
|
||||
|
||||
# ===== 环境配置(保留模板) =====
|
||||
.env
|
||||
.env.local
|
||||
!.env.template
|
||||
|
||||
# ===== Node =====
|
||||
node_modules/
|
||||
|
||||
# ===== Python 虚拟环境 =====
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# ===== infra 敏感文件 =====
|
||||
infra/**/*.key
|
||||
infra/**/*.pem
|
||||
infra/**/*.secret
|
||||
|
||||
# ===== IDE =====
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# ===== 分发/构建产物 =====
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
0
.kiro/.gitkeep
Normal file
0
.kiro/.gitkeep
Normal file
4
.kiro/.last_prompt_id.json
Normal file
4
.kiro/.last_prompt_id.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"at": "2026-02-15T06:00:50.2089232+08:00",
|
||||
"prompt_id": "P20260215-060050"
|
||||
}
|
||||
37
.kiro/agents/audit-writer.md
Normal file
37
.kiro/agents/audit-writer.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: audit-writer
|
||||
description: Run post-change audit + docs sync for NeoZQYY Monorepo; write audit artifacts; return a very short receipt only.
|
||||
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 原文(用于溯源)
|
||||
- 通过项目实际文件内容判断是否“逻辑改动”
|
||||
|
||||
## 何时需要做“重型后处理”
|
||||
满足任一即执行审计收口(否则只输出“无逻辑改动/无需审计”,并清除待审计标记):
|
||||
- 改动文件命中 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) 若是逻辑改动:
|
||||
- 按需调用 skill:
|
||||
- steering-readme-maintainer(同步 product/tech/structure-lite/README)
|
||||
- change-annotation-audit(写 docs/audit/changes/... + AI_CHANGELOG + CHANGE 注释)
|
||||
- bd-manual-db-docs(仅当 DB schema 变更)
|
||||
3) 完成后把 `.kiro/.audit_state.json` 的 `audit_required` 置为 false(或清空 reasons/changed_files/last_reminded_at)
|
||||
|
||||
## 输出(强制极短回执)
|
||||
你最终只允许输出 3 段信息:
|
||||
- done: yes/no
|
||||
- files_written: <按行列出相对路径>
|
||||
- next_step: <若失败给 1~2 条;成功则写 “commit when ready”>
|
||||
15
.kiro/hooks/audit-flagger.kiro.hook
Normal file
15
.kiro/hooks/audit-flagger.kiro.hook
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
.kiro/hooks/audit-reminder.kiro.hook
Normal file
15
.kiro/hooks/audit-reminder.kiro.hook
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
.kiro/hooks/change-impact-review.kiro.hook
Normal file
15
.kiro/hooks/change-impact-review.kiro.hook
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"enabled": false,
|
||||
"name": "change-impact-review(Steering + 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- Prompt(Prompt-ID + 摘录)\n- 直接原因(必要性 + 方案简介)\n- 变更摘要(改了什么:模块/函数/接口/字段等)\n- 风险与验证(回归范围 + 验证方法/测试点/SQL/联调步骤)\n对每一处“逻辑变更”的代码块,必须在变更附近添加内联 CHANGE 标记注释,至少说明:\n- 变更意图(intent)\n- 关键假设(assumptions)\n- 边界条件/资金口径/精度与舍入规则(若相关)\n- 关联 Prompt(Prompt-ID 或摘录)以及必要的验证提示\n\n硬性规则:如果涉及数据库 schema 或表结构变更,必须同步更新 docs/database/ 下对应的表结构文档。"
|
||||
},
|
||||
"workspaceFolderName": "NeoZQYY",
|
||||
"shortName": "change-impact-review"
|
||||
}
|
||||
15
.kiro/hooks/db-docs-sync.kiro.hook
Normal file
15
.kiro/hooks/db-docs-sync.kiro.hook
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
22
.kiro/hooks/db-schema-doc-enforcer.kiro.hook
Normal file
22
.kiro/hooks/db-schema-doc-enforcer.kiro.hook
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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/Shanghai,YYYY-MM-DD);Prompt(Prompt-ID + ≤5 行摘录或原文);Direct cause(必要性 + 修改方案简介)\n\n如果没有发生表结构变更(例如仅修改注释),在变更日志文档中写一条简短说明:\"无结构性变更\"(同样要带日期 + Prompt-ID)。"
|
||||
},
|
||||
"workspaceFolderName": "NeoZQYY",
|
||||
"shortName": "db-schema-doc-enforcer"
|
||||
}
|
||||
15
.kiro/hooks/prompt-audit-log.kiro.hook
Normal file
15
.kiro/hooks/prompt-audit-log.kiro.hook
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
15
.kiro/hooks/run-audit-writer.kiro.hook
Normal file
15
.kiro/hooks/run-audit-writer.kiro.hook
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"name": "Manual: Run /audit (via audit-writer subagent)",
|
||||
"description": "按需触发:启动 audit-writer 子代理执行变更影响审查+文档同步+审计落盘,完成后自动刷新审计一览表,并仅回传极短回执。",
|
||||
"version": "2",
|
||||
"when": {
|
||||
"type": "userTriggered"
|
||||
},
|
||||
"then": {
|
||||
"type": "askAgent",
|
||||
"prompt": "立刻启动名为 audit-writer 的子代理来执行「后处理写入/审计收口」流程。\n\n约束:\n- 子代理自行使用 git status/diff 与 .kiro/.last_prompt_id.json 中最新 Prompt-ID 作为溯源;不要依赖主对话上下文。\n- 子代理必须按需调用 skill:steering-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 条)"
|
||||
},
|
||||
"workspaceFolderName": "NeoZQYY",
|
||||
"shortName": "audit"
|
||||
}
|
||||
128
.kiro/scripts/audit_flagger.ps1
Normal file
128
.kiro/scripts/audit_flagger.ps1
Normal file
@@ -0,0 +1,128 @@
|
||||
# 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
|
||||
72
.kiro/scripts/audit_reminder.ps1
Normal file
72
.kiro/scripts/audit_reminder.ps1
Normal file
@@ -0,0 +1,72 @@
|
||||
$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
|
||||
}
|
||||
61
.kiro/scripts/prompt_audit_log.ps1
Normal file
61
.kiro/scripts/prompt_audit_log.ps1
Normal file
@@ -0,0 +1,61 @@
|
||||
$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
|
||||
}
|
||||
4
.kiro/settings/mcp.json
Normal file
4
.kiro/settings/mcp.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
}
|
||||
}
|
||||
41
.kiro/skills/bd-manual-db-docs/SKILL.md
Normal file
41
.kiro/skills/bd-manual-db-docs/SKILL.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: bd-manual-db-docs
|
||||
description: 当 PostgreSQL schema/表结构发生变化时,用于将变更以审计友好的方式落盘到 docs/database/(含变更原因、影响、回滚与验证 SQL)。
|
||||
---
|
||||
|
||||
# 目的
|
||||
保证数据库结构变化可追溯、可审计、可回滚,并与 ETL/后端/小程序字段映射保持一致。
|
||||
|
||||
# 触发条件
|
||||
- 迁移脚本/DDL 修改(新增/删除/改表、字段、类型、默认值、非空、约束、索引、外键)
|
||||
- ORM/Schema 定义变更导致实际 DB 结构变化
|
||||
- 手工执行 DDL(需用 manualTrigger hook 或本 Skill 补齐文档)
|
||||
|
||||
# 输出要求(必须全部满足)
|
||||
所有输出必须落盘到:`docs/database/`
|
||||
|
||||
至少包含:
|
||||
1) Schema Change Log(变更日志条目)
|
||||
2) Table Structure Doc(涉及表的结构文档更新)
|
||||
3) Rollback & Verification(回滚要点 + 至少 3 条验证 SQL)
|
||||
4) 溯源:日期 + Prompt-ID/Prompt 摘录 + Direct cause(必要性 + 方案简介)
|
||||
|
||||
# 工作流
|
||||
## 1) 识别结构性变化
|
||||
- 列出新增/修改/删除的对象:schema/table/column/index/constraint/fk
|
||||
- 明确变更前后差异(before/after)
|
||||
|
||||
## 2) 更新变更日志(Schema Change Log)
|
||||
- 在对应 schema 目录下追加一条变更记录(模板见 assets/schema-changelog-template.md)
|
||||
|
||||
## 3) 更新表结构文档(Table Structure Doc)
|
||||
- 每张受影响的表都要更新(模板见 assets/table-structure-template.md)
|
||||
- 同步字段含义/口径说明,尤其是金额类字段:精度、币种、舍入
|
||||
|
||||
## 4) 回滚与验证
|
||||
- 写清楚 DDL 回滚路径(必要时提供反向迁移)
|
||||
- 写至少 3 条验证 SQL(含约束/索引/关键字段检查)
|
||||
|
||||
# 模板
|
||||
- `assets/schema-changelog-template.md`
|
||||
- `assets/table-structure-template.md`
|
||||
@@ -0,0 +1,27 @@
|
||||
# Schema 变更日志(Schema Change Log)
|
||||
|
||||
- 日期(Asia/Shanghai,YYYY-MM-DD):
|
||||
- Prompt-ID:
|
||||
- 原始原因(Prompt 摘录/原文):
|
||||
- 直接原因(必要性 + 方案简介):
|
||||
- 影响的 Schema:
|
||||
- 变更摘要(一句话):
|
||||
|
||||
## 变更明细
|
||||
- 新增:
|
||||
- 修改:
|
||||
- 删除:
|
||||
|
||||
## 影响范围
|
||||
- ETL:
|
||||
- 后端 API:
|
||||
- 小程序:
|
||||
|
||||
## 回滚要点
|
||||
- DDL 回滚:
|
||||
- 数据回填/迁移注意事项:
|
||||
|
||||
## 验证 SQL(至少 3 条)
|
||||
1)
|
||||
2)
|
||||
3)
|
||||
@@ -0,0 +1,22 @@
|
||||
# <schema>.<table>
|
||||
|
||||
## 表用途(Purpose)
|
||||
- 该表代表什么业务对象/过程
|
||||
|
||||
## 字段(Columns)
|
||||
| 字段名 | 类型 | 可空 | 默认值 | 约束/键 | 说明(含口径) |
|
||||
|---|---|---:|---|---|---|
|
||||
|
||||
> 金额类字段必须注明:币种、精度、舍入/截断规则、是否允许负数。
|
||||
|
||||
## 索引(Indexes)
|
||||
- 索引名 / 字段 / 是否唯一 / 备注
|
||||
|
||||
## 约束与外键(Constraints & FKs)
|
||||
- 约束名 / 定义 / 备注
|
||||
|
||||
## 数据不变量(Invariants)
|
||||
- 例如:状态机枚举范围、唯一性、跨字段一致性约束(如有)
|
||||
|
||||
## 变更历史(Change History)
|
||||
- YYYY-MM-DD | Prompt-ID | 直接原因 | 变更摘要
|
||||
37
.kiro/skills/change-annotation-audit/SKILL.md
Normal file
37
.kiro/skills/change-annotation-audit/SKILL.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: change-annotation-audit
|
||||
description: 对每次修改强制生成审计记录(docs/audit/changes/...),并在每个被改文件写 AI_CHANGELOG、在逻辑变更处写 CHANGE 标记注释(包含日期、Prompt 与直接原因)。
|
||||
---
|
||||
|
||||
# 目的
|
||||
把“为什么改、怎么改、怎么验”固化到可审计产物中,满足资金相关项目的严谨性要求。
|
||||
|
||||
# 触发条件
|
||||
- 任何对代码或文档的实质修改(非纯格式化)
|
||||
- 特别是:逻辑改动、资金口径改动、接口契约改动、DB 结构改动
|
||||
|
||||
# 必须产物(缺一不可)
|
||||
1) `docs/audit/changes/<YYYY-MM-DD>__<slug>.md`
|
||||
2) 每个被修改文件内的 `AI_CHANGELOG` 条目
|
||||
3) 每个逻辑变更附近的 `CHANGE` 标记注释
|
||||
|
||||
# 工作流
|
||||
## 1) Prompt 溯源
|
||||
- 确认本次修改有 Prompt-ID(来自 prompt_log.md)
|
||||
- 若没有,先补写 Prompt-ID,再继续
|
||||
|
||||
## 2) 写审计记录(Per-change)
|
||||
使用模板:`assets/audit-record-template.md`
|
||||
- 必须写:原始原因(Prompt)、直接原因、改动方案简介、文件清单、风险/回滚/验证
|
||||
|
||||
## 3) 写文件内 AI_CHANGELOG(Per-file)
|
||||
- 对每个修改的文件追加一条 AI_CHANGELOG
|
||||
- 选择适合语言/文件类型的注释风格(模板见 assets/file-changelog-templates.md)
|
||||
|
||||
## 4) 写 CHANGE 标记(Block-level)
|
||||
- 对每处逻辑变更,必须在附近写 CHANGE 标记
|
||||
- 必须包含:intent、assumptions、边界条件(金额/舍入/精度)、验证提示
|
||||
|
||||
# 模板
|
||||
- `assets/audit-record-template.md`
|
||||
- `assets/file-changelog-templates.md`
|
||||
@@ -0,0 +1,19 @@
|
||||
# 变更审计记录(Change Audit Record)
|
||||
|
||||
- 日期/时间(Asia/Shanghai):
|
||||
- Prompt-ID:
|
||||
- 原始原因(Prompt 原文或 ≤5 行摘录):
|
||||
- 直接原因(必要性 + 修改方案简介):
|
||||
|
||||
## 变更范围(Changed)
|
||||
- 模块/接口/表/关键文件:
|
||||
|
||||
## 风险与回滚(Risk & Rollback)
|
||||
- 风险点:
|
||||
- 回滚要点:
|
||||
|
||||
## 验证(Verification)
|
||||
- 至少 1 条可执行验证方式(测试/SQL/联调):
|
||||
|
||||
## 文件清单(Files changed)
|
||||
- ...
|
||||
@@ -0,0 +1,48 @@
|
||||
# 文件内 AI_CHANGELOG 与 CHANGE 标记模板
|
||||
|
||||
## 通用 AI_CHANGELOG(建议放在文件头部或“变更记录”小节)
|
||||
- 2026-02-13 | Prompt: P20260213-101530(摘录:...)| Direct cause:... | Summary:... | Verify:...
|
||||
|
||||
---
|
||||
|
||||
## Markdown / 文档(放在文档末尾或“变更记录”小节)
|
||||
### AI_CHANGELOG
|
||||
- YYYY-MM-DD | Prompt: P...(摘录:...)| Direct cause:... | Summary:... | Verify:...
|
||||
|
||||
---
|
||||
|
||||
## JS/TS(块注释)
|
||||
/*
|
||||
AI_CHANGELOG
|
||||
- YYYY-MM-DD | Prompt: P...(摘录:...)| Direct cause:... | Summary:... | Verify:...
|
||||
*/
|
||||
|
||||
// [CHANGE P...] intent: ...
|
||||
// assumptions: ...
|
||||
// edge cases / money semantics: ...
|
||||
// verify: ...
|
||||
|
||||
---
|
||||
|
||||
## Python(docstring/块注释)
|
||||
"""
|
||||
AI_CHANGELOG
|
||||
- YYYY-MM-DD | Prompt: P...(摘录:...)| Direct cause:... | Summary:... | Verify:...
|
||||
"""
|
||||
|
||||
# [CHANGE P...] intent: ...
|
||||
# assumptions: ...
|
||||
# edge cases / money semantics: ...
|
||||
# verify: ...
|
||||
|
||||
---
|
||||
|
||||
## SQL(块注释 + 行注释)
|
||||
/*
|
||||
AI_CHANGELOG
|
||||
- YYYY-MM-DD | Prompt: P...(摘录:...)| Direct cause:... | Summary:... | Verify:...
|
||||
*/
|
||||
-- [CHANGE P...] intent: ...
|
||||
-- assumptions: ...
|
||||
-- money semantics: precision/rounding/currency ...
|
||||
-- verify: ...
|
||||
38
.kiro/skills/steering-readme-maintainer/SKILL.md
Normal file
38
.kiro/skills/steering-readme-maintainer/SKILL.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: steering-readme-maintainer
|
||||
description: 当发生业务/ETL/API/鉴权/小程序交互等逻辑改动时,用于执行变更影响审查并同步更新 product/tech/structure/README 与审计记录。
|
||||
---
|
||||
|
||||
# 目的
|
||||
将“逻辑改动→文档同步→审计留痕”流程标准化,减少漏更与口径漂移风险(资金相关场景优先保证可追溯与可复算)。
|
||||
|
||||
# 触发条件(何时调用本 Skill)
|
||||
- 修改了业务规则/计算口径/资金处理(精度、舍入、阈值等)
|
||||
- 修改了 ETL/SQL 清洗聚合映射逻辑
|
||||
- 修改了 API 行为(返回结构、错误码、鉴权/权限)
|
||||
- 修改了小程序关键交互流程(校验、状态机、关键字段)
|
||||
|
||||
# 工作流(必须按顺序执行)
|
||||
## 1) 分类:是否属于“逻辑改动”
|
||||
- 若不是逻辑改动:写明“无逻辑改动”,并说明为何(例如仅格式化/拼写修正/注释调整)。
|
||||
- 若是逻辑改动:进入下一步。
|
||||
|
||||
## 2) Steering 与 README 同步(逐项评估)
|
||||
- `.kiro/steering/product.md`:业务定义/口径/资金规则是否变化?
|
||||
- `.kiro/steering/tech.md`:技术栈/运行方式/依赖/部署假设是否变化?
|
||||
- `.kiro/steering/structure-lite.md(摘要)/ .kiro/steering/structure.md(仅在目录树/边界变化时)`:目录/模块边界/职责是否变化?
|
||||
- `README.md`:运行方式、配置、环境变量、接口契约、联调步骤是否变化?
|
||||
|
||||
> 规则:如果“对读者理解系统行为”有帮助,就应更新;不要为了追求“少改文档”而拒绝同步。
|
||||
|
||||
## 3) 输出审计友好摘要(对话回复/审计记录都需要)
|
||||
- Changed:改了哪些模块/接口/表/关键文件
|
||||
- Why:原始原因(Prompt-ID + 摘录)与直接原因(必要性 + 方案简介)
|
||||
- Risk:风险点与回归范围
|
||||
- Verify:建议的验证步骤(测试/SQL/联调)
|
||||
|
||||
## 4) 联动硬规则检查
|
||||
- 如果涉及 DB schema/表结构变化:必须同步更新 `docs/database/`(见 skill `bd-manual-db-docs`)。
|
||||
|
||||
# 资产(可复制模板/清单)
|
||||
见:`assets/steering-update-checklist.md`
|
||||
@@ -0,0 +1,23 @@
|
||||
# Steering & README 同步清单(逻辑改动必查)
|
||||
|
||||
## product.md(产品/口径)
|
||||
- 业务定义/指标口径/字段含义是否改变?
|
||||
- 涉及金额的精度/舍入/阈值规则是否改变?
|
||||
- 角色/权限模型是否改变?
|
||||
|
||||
## tech.md(技术/运行)
|
||||
- 新增/变更依赖(框架、库、驱动)?
|
||||
- 配置项/环境变量/端口/服务启动方式是否改变?
|
||||
- 数据访问边界(ETL 库 vs 业务库)是否改变?
|
||||
- 性能/一致性/幂等/重试策略是否改变?
|
||||
|
||||
## structure.md(结构/职责)
|
||||
- 新增目录/模块?
|
||||
- 模块职责或边界是否重新划分?
|
||||
- 新增集成点(队列、定时任务、外部系统)?
|
||||
|
||||
## README.md(使用/联调)
|
||||
- 本地启动步骤是否改变?
|
||||
- 新增/变更配置项(.env 等)?
|
||||
- API 契约是否变化(路径、参数、返回、错误码)?
|
||||
- 小程序联调步骤是否变化?
|
||||
1
.kiro/specs/bd-manual-docs-consolidation/.config.kiro
Normal file
1
.kiro/specs/bd-manual-docs-consolidation/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
311
.kiro/specs/bd-manual-docs-consolidation/design.md
Normal file
311
.kiro/specs/bd-manual-docs-consolidation/design.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# 设计文档:数据库文档整理与补全
|
||||
|
||||
## 概述
|
||||
|
||||
本特性对 `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 次迭代
|
||||
- 每个测试需注释引用对应的设计属性编号
|
||||
80
.kiro/specs/bd-manual-docs-consolidation/requirements.md
Normal file
80
.kiro/specs/bd-manual-docs-consolidation/requirements.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 需求文档
|
||||
|
||||
## 简介
|
||||
|
||||
整理和补全飞球 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/` 下为每张表提供表级文档
|
||||
|
||||
### 需求 2:DDL 文件与数据库实际状态对比同步
|
||||
|
||||
**用户故事:** 作为数据开发人员,我希望 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 数据字典一致的格式
|
||||
111
.kiro/specs/bd-manual-docs-consolidation/tasks.md
Normal file
111
.kiro/specs/bd-manual-docs-consolidation/tasks.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 实施计划:数据库文档整理与补全
|
||||
|
||||
## 概述
|
||||
|
||||
按照"目录结构 → 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 运行对比脚本对比四个 schema(ODS、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 文档
|
||||
1
.kiro/specs/docs-optimization/.config.kiro
Normal file
1
.kiro/specs/docs-optimization/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
229
.kiro/specs/docs-optimization/design.md
Normal file
229
.kiro/specs/docs-optimization/design.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# 设计文档:文档体系整理与优化
|
||||
|
||||
## 概述
|
||||
|
||||
本设计针对飞球 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` 位置包含重定向说明
|
||||
56
.kiro/specs/docs-optimization/requirements.md
Normal file
56
.kiro/specs/docs-optimization/requirements.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 需求文档:文档体系整理与优化
|
||||
|
||||
## 简介
|
||||
|
||||
对飞球 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 的条目和说明
|
||||
100
.kiro/specs/docs-optimization/tasks.md
Normal file
100
.kiro/specs/docs-optimization/tasks.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 实施计划:文档体系整理与优化
|
||||
|
||||
## 概述
|
||||
|
||||
基于设计文档,将实施分为四个阶段:新增文档目录与骨架文件、业务规则文档迁移、审计一览表生成脚本开发、文档总索引更新。所有任务聚焦于文件创建/修改和 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
|
||||
- 每个任务引用了具体的需求编号以便追溯
|
||||
- 检查点用于增量验证
|
||||
- 属性测试验证通用正确性属性,单元测试验证具体例子和边界情况
|
||||
1
.kiro/specs/etl-task-documentation/.config.kiro
Normal file
1
.kiro/specs/etl-task-documentation/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
285
.kiro/specs/etl-task-documentation/design.md
Normal file
285
.kiro/specs/etl-task-documentation/design.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# 设计文档:ETL 任务说明文档
|
||||
|
||||
## 概述
|
||||
|
||||
本设计描述如何为飞球 ETL 系统生成一套完整的任务说明文档,放置于 `docs/etl_tasks/` 目录下。文档以 Markdown 格式编写,按数据层(ODS / DWD / DWS / INDEX / Utility)分文件组织,并提供一个总览 README 作为入口。
|
||||
|
||||
文档的目标读者是开发者和运维人员,需要覆盖:
|
||||
- 每个任务的代码标识、Python 类、数据来源与目标
|
||||
- Extract / Transform / Load 各阶段的处理逻辑
|
||||
- CLI 参数与管道执行方式
|
||||
- BaseTask 公共机制
|
||||
|
||||
## 架构
|
||||
|
||||
文档为纯静态 Markdown 文件,不涉及运行时代码变更。整体结构如下:
|
||||
|
||||
```
|
||||
docs/etl_tasks/
|
||||
├── README.md # 总览:任务清单 + 跳转链接 + 执行方式
|
||||
├── ods_tasks.md # ODS 层任务详解
|
||||
├── dwd_tasks.md # DWD 层任务详解
|
||||
├── dws_tasks.md # DWS 层任务详解
|
||||
├── index_tasks.md # INDEX 层任务详解
|
||||
├── utility_tasks.md # 工具类任务详解
|
||||
└── base_task_mechanism.md # BaseTask 公共机制与执行参数
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
README["README.md<br/>总览入口"] --> ODS["ods_tasks.md"]
|
||||
README --> DWD["dwd_tasks.md"]
|
||||
README --> DWS["dws_tasks.md"]
|
||||
README --> IDX["index_tasks.md"]
|
||||
README --> UTL["utility_tasks.md"]
|
||||
README --> BASE["base_task_mechanism.md"]
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
本 spec 不涉及代码组件。产出物为 7 个 Markdown 文件,各文件的内容范围如下:
|
||||
|
||||
### README.md(总览)
|
||||
|
||||
| 章节 | 内容 |
|
||||
|------|------|
|
||||
| 系统简介 | 飞球 ETL 系统概述、数据流向(API → ODS → DWD → DWS) |
|
||||
| 任务清单 | 按层分组的表格:任务代码、Python 类、简要说明、跳转链接 |
|
||||
| 管道类型 | 7 种管道(api_ods / api_ods_dwd / api_full / ods_dwd / dwd_dws / dwd_dws_index / dwd_index)的层组合 |
|
||||
| 处理模式 | increment_only / verify_only / increment_verify 的区别 |
|
||||
| 数据源模式 | online / offline / hybrid 的区别 |
|
||||
| CLI 参数速查 | 所有 CLI 参数的表格(参数名、类型、默认值、说明) |
|
||||
| 常见命令示例 | 典型使用场景的命令行示例 |
|
||||
|
||||
### ods_tasks.md(ODS 层)
|
||||
|
||||
每个 ODS 任务一个小节,包含:
|
||||
- 任务代码与 Python 类
|
||||
- API 端点与请求参数
|
||||
- 字段解析逻辑(transform 阶段)
|
||||
- 目标 ODS 表与写入策略
|
||||
- 特殊说明(如分页、去重、content_hash 等)
|
||||
|
||||
需区分两种 ODS 任务模式:
|
||||
1. 独立任务类(如 OrdersTask、MembersTask):继承 BaseTask,有独立的 E/T/L 实现
|
||||
2. 通用 ODS 任务(由 `ods_tasks.py` 中 OdsTaskSpec + BaseOdsTask 动态生成):通过声明式配置定义端点、列映射等
|
||||
|
||||
已注册的 ODS 任务(14 个独立 + N 个通用):
|
||||
|
||||
| 任务代码 | Python 类 | API 端点 | 目标表 |
|
||||
|----------|-----------|----------|--------|
|
||||
| ORDERS | OrdersTask | /Site/GetAllOrderSettleList | billiards_ods.fact_order |
|
||||
| PAYMENTS | PaymentsTask | 支付相关端点 | billiards_ods.fact_payment |
|
||||
| MEMBERS | MembersTask | /MemberProfile/GetTenantMemberList | billiards_ods.dim_member |
|
||||
| PRODUCTS | ProductsTask | 商品相关端点 | billiards_ods 商品表 |
|
||||
| TABLES | TablesTask | 台桌相关端点 | billiards_ods 台桌表 |
|
||||
| ASSISTANTS | AssistantsTask | 助教相关端点 | billiards_ods 助教表 |
|
||||
| PACKAGES_DEF | PackagesDefTask | 套餐相关端点 | billiards_ods 套餐表 |
|
||||
| REFUNDS | RefundsTask | 退款相关端点 | billiards_ods 退款表 |
|
||||
| COUPON_USAGE | CouponUsageTask | 优惠券相关端点 | billiards_ods 优惠券表 |
|
||||
| INVENTORY_CHANGE | InventoryChangeTask | 库存变动端点 | billiards_ods 库存表 |
|
||||
| TOPUPS | TopupsTask | 充值相关端点 | billiards_ods 充值表 |
|
||||
| TABLE_DISCOUNT | TableDiscountTask | 台费折扣端点 | billiards_ods 折扣表 |
|
||||
| ASSISTANT_ABOLISH | AssistantAbolishTask | 助教取消端点 | billiards_ods 取消表 |
|
||||
| LEDGER | LedgerTask | 台账端点 | billiards_ods 台账表 |
|
||||
|
||||
通用 ODS 任务由 `ODS_TASK_CLASSES` 字典动态注册,每个任务通过 `OdsTaskSpec` 声明:
|
||||
- `endpoint`:API 端点路径
|
||||
- `table`:目标 ODS 表名
|
||||
- `columns`:列定义列表(ColumnSpec)
|
||||
- `page_size`、`data_path`、`list_key`:分页参数
|
||||
- `pk_columns`:主键列
|
||||
- `snapshot_mode`:快照模式(content_hash 去重)
|
||||
|
||||
### dwd_tasks.md(DWD 层)
|
||||
|
||||
DWD 层有 5 个已注册任务:
|
||||
|
||||
| 任务代码 | Python 类 | 说明 |
|
||||
|----------|-----------|------|
|
||||
| DWD_LOAD_FROM_ODS | DwdLoadTask | 核心装载任务:遍历 TABLE_MAP,维度走 SCD2,事实走增量 |
|
||||
| TICKET_DWD | TicketDwdTask | 结账小票明细 → fact_order / fact_order_goods / fact_table_usage / fact_assistant_service |
|
||||
| PAYMENTS_DWD | PaymentsDwdTask | ODS 支付记录 → fact_payment |
|
||||
| MEMBERS_DWD | MembersDwdTask | ODS 会员记录 → dim_member |
|
||||
| DWD_QUALITY_CHECK | DwdQualityTask | ODS 与 DWD 行数/金额核对,输出 JSON 报表 |
|
||||
|
||||
核心任务 DWD_LOAD_FROM_ODS 的处理逻辑:
|
||||
- TABLE_MAP 定义了 40+ 对 DWD→ODS 表映射
|
||||
- 维度表(dim_*):检测 SCD2 列是否存在,有则执行 SCD2 合并(关闭旧版+插入新版),无则执行 Type1 Upsert
|
||||
- 事实表(dwd_*、fact_*):按 fetched_at 水位线增量插入,支持 upsert 或 insert-only
|
||||
- FACT_MAPPINGS 定义了列名映射(ODS 驼峰命名 → DWD 下划线命名)
|
||||
- 每张表独立事务,单表失败不影响后续表
|
||||
|
||||
SCD2 处理流程:
|
||||
1. 从 ODS 取最新快照(DISTINCT ON 按业务主键 + fetched_at DESC)
|
||||
2. 与 DWD 当前版本(scd2_is_current=1)逐列对比
|
||||
3. 有变更:关闭旧版(scd2_end_time=now, scd2_is_current=0)+ 插入新版(version+1)
|
||||
4. 无变更:跳过
|
||||
|
||||
### dws_tasks.md(DWS 层)
|
||||
|
||||
DWS 层有 15 个已注册任务,按业务域分组:
|
||||
|
||||
**助教业绩域:**
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 粒度 |
|
||||
|----------|-----------|--------|------|
|
||||
| DWS_ASSISTANT_DAILY | AssistantDailyTask | dws_assistant_daily_detail | 日期+助教 |
|
||||
| DWS_ASSISTANT_MONTHLY | AssistantMonthlyTask | dws_assistant_monthly_summary | 月份+助教 |
|
||||
| DWS_ASSISTANT_CUSTOMER | AssistantCustomerTask | dws_assistant_customer_stats | 日期+助教+会员 |
|
||||
| DWS_ASSISTANT_SALARY | AssistantSalaryTask | dws_assistant_salary_calc | 月份+助教 |
|
||||
| DWS_ASSISTANT_FINANCE | AssistantFinanceTask | dws_assistant_finance_analysis | 日期+助教 |
|
||||
|
||||
**会员分析域:**
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 粒度 |
|
||||
|----------|-----------|--------|------|
|
||||
| DWS_MEMBER_CONSUMPTION | MemberConsumptionTask | dws_member_consumption_summary | 日期+会员 |
|
||||
| DWS_MEMBER_VISIT | MemberVisitTask | dws_member_visit_detail | 日期+会员+结账单 |
|
||||
|
||||
**财务统计域:**
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 粒度 |
|
||||
|----------|-----------|--------|------|
|
||||
| DWS_FINANCE_DAILY | FinanceDailyTask | dws_finance_daily_summary | 日期 |
|
||||
| DWS_FINANCE_RECHARGE | FinanceRechargeTask | dws_finance_recharge_summary | 日期 |
|
||||
| DWS_FINANCE_INCOME_STRUCTURE | FinanceIncomeStructureTask | dws_finance_income_structure | 日期+收入类型 |
|
||||
| DWS_FINANCE_DISCOUNT_DETAIL | FinanceDiscountDetailTask | dws_finance_discount_detail | 日期+折扣类型 |
|
||||
|
||||
**运维任务:**
|
||||
|
||||
| 任务代码 | Python 类 | 说明 |
|
||||
|----------|-----------|------|
|
||||
| DWS_BUILD_ORDER_SUMMARY | DwsBuildOrderSummaryTask | 构建订单汇总中间表 |
|
||||
| DWS_RETENTION_CLEANUP | DwsRetentionCleanupTask | 按时间分层清理历史数据 |
|
||||
| DWS_MV_REFRESH_FINANCE_DAILY | DwsMvRefreshFinanceDailyTask | 刷新财务日报物化视图 |
|
||||
| DWS_MV_REFRESH_ASSISTANT_DAILY | DwsMvRefreshAssistantDailyTask | 刷新助教日报物化视图 |
|
||||
|
||||
所有 DWS 任务继承 BaseDwsTask,共享以下机制:
|
||||
- 时间分层范围计算(TimeLayer: LAST_2_DAYS / LAST_1_MONTH / LAST_3_MONTHS / LAST_6_MONTHS / ALL)
|
||||
- 配置缓存(ConfigCache):业绩档位、等级价格、奖金规则、区域分类、技能类型
|
||||
- delete-before-insert 更新策略(按日期范围先删后插,保证幂等)
|
||||
- bulk_insert / upsert 写入方法
|
||||
|
||||
### index_tasks.md(INDEX 层)
|
||||
|
||||
INDEX 层有 4 个已注册任务:
|
||||
|
||||
| 任务代码 | Python 类 | 目标表 | 指数类型 |
|
||||
|----------|-----------|--------|----------|
|
||||
| DWS_WINBACK_INDEX | WinbackIndexTask | dws_member_winback_index | WBI(回流指数) |
|
||||
| DWS_NEWCONV_INDEX | NewconvIndexTask | dws_member_newconv_index | NCI(新客转化指数) |
|
||||
| DWS_RELATION_INDEX | RelationIndexTask | dws_relation_index | RS(关系指数) |
|
||||
| DWS_ML_MANUAL_IMPORT | MlManualImportTask | dws_ml_manual_ledger | ML(手动台账导入) |
|
||||
|
||||
所有指数任务继承 BaseIndexTask,共享:
|
||||
- 参数从 `billiards_dws.cfg_index_parameters` 表加载
|
||||
- 百分位历史记录(PercentileHistory)
|
||||
- 标准化的指数计算流程
|
||||
|
||||
### utility_tasks.md(工具类)
|
||||
|
||||
| 任务代码 | Python 类 | 用途 |
|
||||
|----------|-----------|------|
|
||||
| INIT_ODS_SCHEMA | InitOdsSchemaTask | 执行 ODS + etl_admin DDL,创建必要目录 |
|
||||
| INIT_DWD_SCHEMA | InitDwdSchemaTask | 执行 DWD DDL |
|
||||
| INIT_DWS_SCHEMA | InitDwsSchemaTask | 执行 DWS DDL |
|
||||
| MANUAL_INGEST | ManualIngestTask | 从本地 JSON 文件手动入库到 ODS |
|
||||
| ODS_JSON_ARCHIVE | OdsJsonArchiveTask | 归档 ODS JSON 文件 |
|
||||
| CHECK_CUTOFF | CheckCutoffTask | 检查数据截止时间 |
|
||||
| SEED_DWS_CONFIG | SeedDwsConfigTask | 初始化 DWS 配置种子数据 |
|
||||
| DATA_INTEGRITY_CHECK | DataIntegrityTask | 数据完整性校验 |
|
||||
|
||||
### base_task_mechanism.md(公共机制)
|
||||
|
||||
覆盖内容:
|
||||
- BaseTask 模板方法流程(execute → build_context → [分段] → extract → transform → load → commit)
|
||||
- TaskContext 字段说明
|
||||
- 时间窗口计算逻辑(优先级:手动覆盖 > 游标 > 闲忙时段默认值)
|
||||
- 窗口分段(build_window_segments)
|
||||
- TaskRegistry 注册方式与元数据(TaskMeta: task_class, requires_db_config, layer, task_type)
|
||||
- PipelineRunner 管道执行流程
|
||||
- 校验框架(Verifier)概述
|
||||
|
||||
## 数据模型
|
||||
|
||||
本 spec 不涉及数据模型变更。文档中引用的数据模型均为现有系统中的表结构,包括:
|
||||
|
||||
**ODS 层表**(billiards_ods schema):
|
||||
- settlement_records, table_fee_transactions, assistant_service_records, member_profiles, payment_transactions, refund_transactions 等 20+ 表
|
||||
|
||||
**DWD 层表**(billiards_dwd schema):
|
||||
- 维度表:dim_site, dim_table, dim_assistant, dim_member, dim_member_card_account, dim_tenant_goods, dim_store_goods, dim_goods_category, dim_groupbuy_package(各含 _ex 扩展表)
|
||||
- 事实表:dwd_settlement_head, dwd_table_fee_log, dwd_table_fee_adjust, dwd_store_goods_sale, dwd_assistant_service_log, dwd_assistant_trash_event, dwd_member_balance_change, dwd_groupbuy_redemption, dwd_platform_coupon_redemption, dwd_recharge_order, dwd_payment, dwd_refund(各含 _ex 扩展表)
|
||||
|
||||
**DWS 层表**(billiards_dws schema):
|
||||
- 助教域:dws_assistant_daily_detail, dws_assistant_monthly_summary, dws_assistant_customer_stats, dws_assistant_salary_calc, dws_assistant_finance_analysis
|
||||
- 会员域:dws_member_consumption_summary, dws_member_visit_detail
|
||||
- 财务域:dws_finance_daily_summary, dws_finance_recharge_summary, dws_finance_income_structure, dws_finance_discount_detail
|
||||
- 指数域:dws_member_winback_index, dws_member_newconv_index, dws_relation_index, dws_ml_manual_ledger
|
||||
- 配置表:cfg_index_parameters, cfg_skill_type, cfg_performance_tier, cfg_level_price, cfg_bonus_rule, cfg_area_category
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
由于本 spec 的产出物是文档(Markdown 文件),而非运行时代码,正确性属性主要关注文档的完整性和一致性——即文档是否覆盖了所有已注册任务。
|
||||
|
||||
Property 1: ODS 任务文档覆盖完整性
|
||||
*对于所有*在 `task_registry.py` 中注册且 layer="ODS" 的任务代码,`ods_tasks.md` 中应包含该任务代码的说明章节,并列出其目标表。
|
||||
**Validates: Requirements 2.1, 2.4**
|
||||
|
||||
Property 2: DWD 任务文档覆盖完整性
|
||||
*对于所有*在 `task_registry.py` 中注册且 layer="DWD" 的任务代码,`dwd_tasks.md` 中应包含该任务代码的说明章节,并列出其源表和目标表。
|
||||
**Validates: Requirements 3.1**
|
||||
|
||||
Property 3: DWS 任务文档覆盖完整性
|
||||
*对于所有*在 `task_registry.py` 中注册且 layer="DWS" 的任务代码,`dws_tasks.md` 中应包含该任务代码的说明章节,并标注其更新策略。
|
||||
**Validates: Requirements 4.1, 4.4**
|
||||
|
||||
Property 4: INDEX 任务文档覆盖完整性
|
||||
*对于所有*在 `task_registry.py` 中注册且 layer="INDEX" 的任务代码,`index_tasks.md` 中应包含该任务代码的说明章节。
|
||||
**Validates: Requirements 5.1**
|
||||
|
||||
Property 5: Utility 任务文档覆盖完整性
|
||||
*对于所有*在 `task_registry.py` 中注册且 task_type="utility" 的任务代码,`utility_tasks.md` 中应包含该任务代码的说明章节。
|
||||
**Validates: Requirements 6.1**
|
||||
|
||||
Property 6: CLI 参数文档覆盖完整性
|
||||
*对于所有*在 `cli/main.py` 的 `parse_args()` 中定义的 CLI 参数,`README.md` 或 `base_task_mechanism.md` 中应包含该参数的说明。
|
||||
**Validates: Requirements 7.1**
|
||||
|
||||
Property 7: 管道类型文档覆盖完整性
|
||||
*对于所有*在 `PipelineRunner.PIPELINE_LAYERS` 中定义的管道类型,`README.md` 中应包含该管道类型的层组合说明。
|
||||
**Validates: Requirements 7.2**
|
||||
|
||||
## 错误处理
|
||||
|
||||
本 spec 为文档生成任务,不涉及运行时错误处理。文档编写过程中需注意:
|
||||
|
||||
1. 若源代码中的任务类或注册信息发生变更,文档可能过时——应在 README.md 中注明"最后更新日期"和"基于代码版本"
|
||||
2. 若某个任务的 API 端点或参数无法从代码中直接读取(如动态配置),应在文档中标注"参见配置文件"
|
||||
|
||||
## 测试策略
|
||||
|
||||
**单元测试(示例验证):**
|
||||
- 验证所有 7 个 Markdown 文件存在于 `docs/etl_tasks/` 目录下
|
||||
- 验证 README.md 包含指向其他 6 个文件的链接
|
||||
- 验证每个分层文件中包含对应层的所有已注册任务代码
|
||||
|
||||
**属性测试(覆盖完整性验证):**
|
||||
- 使用 pytest 编写脚本,从 `task_registry.py` 动态读取已注册任务列表
|
||||
- 解析对应的 Markdown 文件,检查每个任务代码是否出现在文档中
|
||||
- 从 `cli/main.py` 解析 CLI 参数列表,检查文档中是否覆盖
|
||||
- 属性测试库:pytest(本项目已使用),配合 parametrize 实现参数化验证
|
||||
- 每个属性测试标注对应的设计属性编号
|
||||
|
||||
**测试标注格式:**
|
||||
```python
|
||||
# Feature: etl-task-documentation, Property 1: ODS 任务文档覆盖完整性
|
||||
def test_ods_task_coverage():
|
||||
...
|
||||
```
|
||||
|
||||
由于本 spec 的产出物是静态文档,属性测试的核心价值在于确保文档与代码的一致性,防止文档遗漏任务。测试应在文档生成后运行一次即可,无需持续集成。
|
||||
119
.kiro/specs/etl-task-documentation/requirements.md
Normal file
119
.kiro/specs/etl-task-documentation/requirements.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 需求文档:ETL 任务说明文档
|
||||
|
||||
## 简介
|
||||
|
||||
为飞球 ETL 系统(etl-billiards)生成一份完整的任务说明文档,覆盖 ODS、DWD、DWS、INDEX 四层所有已注册任务的逻辑、执行方式、参数含义及处理流程。文档面向开发者和运维人员,放置于 `docs/etl_tasks/` 目录下。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **ETL_System**:飞球 ETL 系统,负责从上游 API 抽取数据并经 ODS → DWD → DWS 三层处理
|
||||
- **Task_Document**:本次生成的 ETL 任务说明文档
|
||||
- **ODS**:操作数据存储层(Operational Data Store),保留 API 原始 payload
|
||||
- **DWD**:明细数据层(Data Warehouse Detail),清洗后的维度表和事实表
|
||||
- **DWS**:数据服务层(Data Warehouse Service),汇总统计表
|
||||
- **INDEX**:指数算法层,基于 DWS 数据计算自定义业务指数
|
||||
- **BaseTask**:所有 ETL 任务的基类,提供 Extract → Transform → Load 模板方法
|
||||
- **TaskRegistry**:任务注册表,维护任务代码与任务类的映射关系
|
||||
- **TaskContext**:运行期上下文,包含 store_id、时间窗口等信息
|
||||
- **Pipeline**:管道,定义多层任务的执行顺序(如 api_ods、api_full、dwd_dws 等)
|
||||
- **Loader**:加载器,负责将转换后的数据写入目标表(upsert/insert)
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:文档结构与组织
|
||||
|
||||
**用户故事:** 作为开发者,我希望文档按数据层分章节组织,以便快速定位特定层的任务说明。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 包含一个总览文件(`README.md`),列出所有层及其任务清单,并提供跳转链接
|
||||
2. THE Task_Document SHALL 按 ODS、DWD、DWS、INDEX、Utility 五个分类分别生成独立的 Markdown 文件
|
||||
3. THE Task_Document SHALL 放置于 `docs/etl_tasks/` 目录下
|
||||
4. WHEN 新增或删除任务时,THE Task_Document SHALL 通过总览文件的任务清单反映当前已注册任务的完整列表
|
||||
|
||||
### 需求 2:ODS 层任务说明
|
||||
|
||||
**用户故事:** 作为开发者,我希望了解每个 ODS 任务的 API 端点、参数、解析逻辑和目标表,以便排查数据抓取问题。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 为每个 ODS 任务列出任务代码、对应的 Python 类、源 API 端点
|
||||
2. THE Task_Document SHALL 说明每个 ODS 任务的 extract 阶段调用的 API 参数及其含义
|
||||
3. THE Task_Document SHALL 说明每个 ODS 任务的 transform 阶段的字段解析和类型转换逻辑
|
||||
4. THE Task_Document SHALL 说明每个 ODS 任务的 load 阶段的目标表名和写入策略(upsert/insert)
|
||||
5. THE Task_Document SHALL 区分"独立 ODS 任务"(如 OrdersTask)和"通用 ODS 任务"(由 ODS_TASK_CLASSES 动态生成)两种模式
|
||||
6. THE Task_Document SHALL 说明通用 ODS 任务的 OdsTaskSpec 配置结构(端点、表名、列映射、分页参数等)
|
||||
|
||||
### 需求 3:DWD 层任务说明
|
||||
|
||||
**用户故事:** 作为开发者,我希望了解 DWD 层任务如何从 ODS 读取数据并清洗装载到维度表和事实表,以便理解数据血缘。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 为每个 DWD 任务列出任务代码、Python 类、源 ODS 表和目标 DWD 表
|
||||
2. THE Task_Document SHALL 说明 DWD_LOAD_FROM_ODS 任务的 TABLE_MAP 映射关系及维度/事实分流逻辑
|
||||
3. THE Task_Document SHALL 说明维度表的 SCD2 处理方式(生效区间、变更检测、历史版本管理)
|
||||
4. THE Task_Document SHALL 说明事实表的增量装载方式(水位线、去重、冲突处理)
|
||||
5. THE Task_Document SHALL 说明 DWD_QUALITY_CHECK 任务的行数/金额核对逻辑和报表输出格式
|
||||
6. THE Task_Document SHALL 说明 TICKET_DWD、PAYMENTS_DWD、MEMBERS_DWD 三个独立 DWD 任务各自的处理特点
|
||||
|
||||
### 需求 4:DWS 层任务说明
|
||||
|
||||
**用户故事:** 作为开发者,我希望了解 DWS 层每个汇总任务的业务含义、数据来源和计算规则,以便验证业务报表的正确性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 为每个 DWS 任务列出任务代码、Python 类、目标表、主键和统计粒度
|
||||
2. THE Task_Document SHALL 说明每个 DWS 任务的数据来源表(DWD 层的哪些表)
|
||||
3. THE Task_Document SHALL 说明每个 DWS 任务的核心业务计算规则(如工资计算公式、业绩档位、排名逻辑等)
|
||||
4. THE Task_Document SHALL 说明每个 DWS 任务的更新策略(delete-before-insert 或 upsert)
|
||||
5. THE Task_Document SHALL 说明物化视图刷新任务(MV_REFRESH)的分层刷新机制和配置方式
|
||||
6. THE Task_Document SHALL 说明数据保留清理任务(RETENTION_CLEANUP)的时间分层策略和配置参数
|
||||
|
||||
### 需求 5:INDEX 层任务说明
|
||||
|
||||
**用户故事:** 作为开发者,我希望了解指数算法任务的计算逻辑和参数含义,以便调优指数模型。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 为每个 INDEX 任务列出任务代码、Python 类、目标表和指数类型
|
||||
2. THE Task_Document SHALL 说明每个指数的计算公式或算法概要(WBI/NCI/RS/ML)
|
||||
3. THE Task_Document SHALL 说明指数参数的配置来源(cfg_index_parameters 表)和参数含义
|
||||
4. THE Task_Document SHALL 说明 ML_MANUAL_IMPORT 任务的 Excel 导入逻辑和模板格式
|
||||
|
||||
### 需求 6:工具类任务说明
|
||||
|
||||
**用户故事:** 作为运维人员,我希望了解 Schema 初始化、手动入库等工具类任务的用途和使用方式。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 为每个工具类任务列出任务代码、Python 类和用途说明
|
||||
2. THE Task_Document SHALL 说明 INIT_ODS_SCHEMA、INIT_DWD_SCHEMA、INIT_DWS_SCHEMA 三个初始化任务执行的 DDL 文件和创建的目录
|
||||
3. THE Task_Document SHALL 说明 MANUAL_INGEST 任务的文件匹配规则、JSON 解析逻辑和入库流程
|
||||
4. THE Task_Document SHALL 说明 ODS_JSON_ARCHIVE 任务的归档策略
|
||||
5. THE Task_Document SHALL 说明 CHECK_CUTOFF 和 DATA_INTEGRITY_CHECK 任务的校验逻辑
|
||||
|
||||
### 需求 7:执行方式与参数说明
|
||||
|
||||
**用户故事:** 作为运维人员,我希望了解如何通过 CLI 和管道模式执行任务,以及各参数的含义。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 说明 CLI 入口(`python -m cli.main`)的所有参数及其含义
|
||||
2. THE Task_Document SHALL 说明管道类型(api_ods、api_ods_dwd、api_full、ods_dwd、dwd_dws、dwd_dws_index、dwd_index)各自包含的层和执行顺序
|
||||
3. THE Task_Document SHALL 说明处理模式(increment_only、verify_only、increment_verify)的区别和适用场景
|
||||
4. THE Task_Document SHALL 说明时间窗口参数(window-start、window-end、window-split、lookback-hours、overlap-seconds)的计算逻辑
|
||||
5. THE Task_Document SHALL 说明数据源模式(online、offline、hybrid)的区别
|
||||
6. THE Task_Document SHALL 提供常见使用场景的命令示例
|
||||
|
||||
### 需求 8:BaseTask 与公共机制说明
|
||||
|
||||
**用户故事:** 作为开发者,我希望了解任务基类的模板方法和公共机制,以便开发新任务时遵循统一模式。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Task_Document SHALL 说明 BaseTask 的 Execute → Extract → Transform → Load 模板方法流程
|
||||
2. THE Task_Document SHALL 说明 TaskContext 的字段含义(store_id、window_start、window_end、window_minutes、cursor)
|
||||
3. THE Task_Document SHALL 说明时间窗口的计算逻辑(游标优先、闲忙时段、手动覆盖)
|
||||
4. THE Task_Document SHALL 说明窗口分段(build_window_segments)的切分策略
|
||||
5. THE Task_Document SHALL 说明任务注册表(TaskRegistry)的注册方式和元数据结构(layer、task_type、requires_db_config)
|
||||
125
.kiro/specs/etl-task-documentation/tasks.md
Normal file
125
.kiro/specs/etl-task-documentation/tasks.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# 实施计划:ETL 任务说明文档
|
||||
|
||||
## 概述
|
||||
|
||||
基于对 `tasks/`、`loaders/`、`orchestration/`、`cli/` 目录下源代码的分析,生成 7 个 Markdown 文档文件,放置于 `docs/etl_tasks/` 目录下。每个任务按照设计文档中定义的结构和内容范围编写。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 创建 `docs/etl_tasks/base_task_mechanism.md`
|
||||
- 说明 BaseTask 的 execute → extract → transform → load 模板方法流程
|
||||
- 说明 TaskContext 字段含义(store_id、window_start、window_end、window_minutes、cursor)
|
||||
- 说明时间窗口计算逻辑(手动覆盖 > 游标 > 闲忙时段默认值)
|
||||
- 说明窗口分段(build_window_segments)切分策略
|
||||
- 说明 TaskRegistry 注册方式与 TaskMeta 元数据结构(layer、task_type、requires_db_config)
|
||||
- 说明 PipelineRunner 管道执行流程与校验框架概述
|
||||
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_
|
||||
|
||||
- [x] 2. 创建 `docs/etl_tasks/ods_tasks.md`
|
||||
- [x] 2.1 编写独立 ODS 任务说明(14 个任务:ORDERS、PAYMENTS、MEMBERS、PRODUCTS、TABLES、ASSISTANTS、PACKAGES_DEF、REFUNDS、COUPON_USAGE、INVENTORY_CHANGE、TOPUPS、TABLE_DISCOUNT、ASSISTANT_ABOLISH、LEDGER)
|
||||
- 每个任务列出:任务代码、Python 类、API 端点、请求参数、字段解析逻辑、目标表、写入策略
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
||||
- [x] 2.2 编写通用 ODS 任务说明(BaseOdsTask + OdsTaskSpec 模式)
|
||||
- 说明 OdsTaskSpec 配置结构(endpoint、table、columns、pk_columns、snapshot_mode 等)
|
||||
- 说明 BaseOdsTask 的通用 execute 流程(API 调用、schema-aware 插入、content_hash 去重、软删除标记)
|
||||
- 列出由 ODS_TASK_CLASSES 动态注册的所有任务
|
||||
- _Requirements: 2.5, 2.6_
|
||||
|
||||
- [x] 3. 创建 `docs/etl_tasks/dwd_tasks.md`
|
||||
- [x] 3.1 编写 DWD_LOAD_FROM_ODS 核心任务说明
|
||||
- 列出完整的 TABLE_MAP 映射表(DWD 表 → ODS 表)
|
||||
- 说明维度/事实分流逻辑(dim_* 走 SCD2 或 Type1 Upsert,其余走增量插入)
|
||||
- 说明 SCD2 处理流程(最新快照选取、变更检测、版本关闭与新建)
|
||||
- 说明事实表增量装载(fetched_at 水位线、upsert/insert-only、FACT_MAPPINGS 列映射)
|
||||
- _Requirements: 3.2, 3.3, 3.4_
|
||||
- [x] 3.2 编写独立 DWD 任务说明(TICKET_DWD、PAYMENTS_DWD、MEMBERS_DWD)
|
||||
- 每个任务列出:源 ODS 表、目标 DWD 表、处理特点
|
||||
- _Requirements: 3.6_
|
||||
- [x] 3.3 编写 DWD_QUALITY_CHECK 任务说明
|
||||
- 说明行数/金额核对逻辑、金额列自动扫描规则、JSON 报表输出格式
|
||||
- _Requirements: 3.5_
|
||||
|
||||
- [x] 4. 创建 `docs/etl_tasks/dws_tasks.md`
|
||||
- [x] 4.1 编写 BaseDwsTask 公共机制说明
|
||||
- 说明时间分层(TimeLayer)、配置缓存(ConfigCache)、delete-before-insert 策略、bulk_insert/upsert 方法
|
||||
- _Requirements: 4.1_
|
||||
- [x] 4.2 编写助教业绩域任务说明(5 个任务)
|
||||
- DWS_ASSISTANT_DAILY:日度服务明细聚合,数据来源、聚合维度、输出字段
|
||||
- DWS_ASSISTANT_MONTHLY:月度汇总,业绩档位计算、排名逻辑、新人封顶规则
|
||||
- DWS_ASSISTANT_CUSTOMER:助教-客户关系统计
|
||||
- DWS_ASSISTANT_SALARY:工资计算公式(基础工资+提成+奖金+扣款)
|
||||
- DWS_ASSISTANT_FINANCE:助教收支分析(收入 vs 日均成本、毛利率)
|
||||
- _Requirements: 4.2, 4.3, 4.4_
|
||||
- [x] 4.3 编写会员分析域任务说明(2 个任务)
|
||||
- DWS_MEMBER_CONSUMPTION:会员消费汇总、客户分层
|
||||
- DWS_MEMBER_VISIT:会员到店明细、服务时长、折扣计算
|
||||
- _Requirements: 4.2, 4.3, 4.4_
|
||||
- [x] 4.4 编写财务统计域任务说明(4 个任务)
|
||||
- DWS_FINANCE_DAILY:财务日报(结算汇总、团购、充值、赠卡消费、费用、平台)
|
||||
- DWS_FINANCE_RECHARGE:充值统计(首充/续充、现金/赠送、卡余额)
|
||||
- DWS_FINANCE_INCOME_STRUCTURE:收入结构分析(按类型、按区域)
|
||||
- DWS_FINANCE_DISCOUNT_DETAIL:折扣明细统计
|
||||
- _Requirements: 4.2, 4.3, 4.4_
|
||||
- [x] 4.5 编写运维任务说明(4 个任务)
|
||||
- DWS_BUILD_ORDER_SUMMARY:订单汇总中间表构建
|
||||
- DWS_RETENTION_CLEANUP:时间分层清理策略、配置参数(enabled、layer、tables、table_layers)
|
||||
- DWS_MV_REFRESH_FINANCE_DAILY / DWS_MV_REFRESH_ASSISTANT_DAILY:物化视图分层刷新机制、L1-L4 层级、配置方式
|
||||
- _Requirements: 4.5, 4.6_
|
||||
|
||||
- [x] 5. 创建 `docs/etl_tasks/index_tasks.md`
|
||||
- 编写 BaseIndexTask 公共机制(参数加载、百分位历史)
|
||||
- 编写 4 个指数任务说明:WBI(回流指数)、NCI(新客转化指数)、RS(关系指数)、ML(手动台账导入)
|
||||
- 说明 cfg_index_parameters 配置表结构和参数含义
|
||||
- 说明 ML_MANUAL_IMPORT 的 Excel 模板格式和导入逻辑
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
||||
|
||||
- [x] 6. 创建 `docs/etl_tasks/utility_tasks.md`
|
||||
- 编写 8 个工具类任务说明:INIT_ODS_SCHEMA、INIT_DWD_SCHEMA、INIT_DWS_SCHEMA、MANUAL_INGEST、ODS_JSON_ARCHIVE、CHECK_CUTOFF、SEED_DWS_CONFIG、DATA_INTEGRITY_CHECK
|
||||
- 每个任务列出:用途、执行的 DDL/操作、配置参数
|
||||
- 重点说明 MANUAL_INGEST 的文件匹配规则和入库流程
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
|
||||
- [x] 7. 创建 `docs/etl_tasks/README.md`(总览)
|
||||
- 编写系统简介和数据流向图(API → ODS → DWD → DWS)
|
||||
- 编写按层分组的任务清单表格(任务代码、Python 类、简要说明、跳转链接)
|
||||
- 编写管道类型说明(7 种管道的层组合)
|
||||
- 编写处理模式说明(increment_only / verify_only / increment_verify)
|
||||
- 编写数据源模式说明(online / offline / hybrid)
|
||||
- 编写 CLI 参数速查表(所有参数的表格)
|
||||
- 编写常见命令示例
|
||||
- _Requirements: 1.1, 1.2, 1.3, 7.1, 7.2, 7.3, 7.5, 7.6_
|
||||
|
||||
- [x] 8. 检查点 - 验证文档完整性
|
||||
- 确认 7 个文件全部存在于 `docs/etl_tasks/` 目录下
|
||||
- 确认 README.md 中的任务清单覆盖所有已注册任务
|
||||
- 确认各分层文件中的任务代码与 task_registry.py 一致
|
||||
- Ensure all files are valid Markdown, ask the user if questions arise.
|
||||
|
||||
- [x] 9. 编写文档覆盖完整性验证脚本
|
||||
- [x] 9.1 编写 ODS 任务覆盖验证
|
||||
- **Property 1: ODS 任务文档覆盖完整性**
|
||||
- **Validates: Requirements 2.1, 2.4**
|
||||
- [x] 9.2 编写 DWD 任务覆盖验证
|
||||
- **Property 2: DWD 任务文档覆盖完整性**
|
||||
- **Validates: Requirements 3.1**
|
||||
- [x] 9.3 编写 DWS 任务覆盖验证
|
||||
- **Property 3: DWS 任务文档覆盖完整性**
|
||||
- **Validates: Requirements 4.1, 4.4**
|
||||
- [x] 9.4 编写 INDEX 和 Utility 任务覆盖验证
|
||||
- **Property 4: INDEX 任务文档覆盖完整性**
|
||||
- **Property 5: Utility 任务文档覆盖完整性**
|
||||
- **Validates: Requirements 5.1, 6.1**
|
||||
- [x] 9.5 编写 CLI 参数和管道类型覆盖验证
|
||||
- **Property 6: CLI 参数文档覆盖完整性**
|
||||
- **Property 7: 管道类型文档覆盖完整性**
|
||||
- **Validates: Requirements 7.1, 7.2**
|
||||
|
||||
- [x] 10. 最终检查点
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## 说明
|
||||
|
||||
- 任务标记 `*` 的为可选项,可跳过以加快 MVP 进度
|
||||
- 每个任务引用了具体的需求编号以便追溯
|
||||
- 检查点确保增量验证
|
||||
- 文档编写顺序:先写公共机制(task 1),再按层写各任务(task 2-6),最后写总览(task 7)
|
||||
1
.kiro/specs/monorepo-migration/.config.kiro
Normal file
1
.kiro/specs/monorepo-migration/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
553
.kiro/specs/monorepo-migration/design.md
Normal file
553
.kiro/specs/monorepo-migration/design.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# 设计文档:Monorepo 迁移
|
||||
|
||||
## 概述
|
||||
|
||||
本设计将现有单一 ETL 仓库(`FQ-ETL`)迁移为 Monorepo 单体仓库(`NeoZQYY`),采用一次性搬迁策略。核心设计原则:
|
||||
|
||||
1. **最小破坏性**:ETL 整体平移,保持内部结构不变,仅调整外部引用
|
||||
2. **分层隔离**:通过 uv workspace 实现 Python 包依赖隔离,通过 `.env` 分层实现配置隔离
|
||||
3. **数据库重组**:从现有 4 个 schema(billiards_ods/billiards_dwd/billiards_dws/etl_admin)重组为 6 层 schema(meta/ods/dwd/core/dws/app)
|
||||
4. **渐进式扩展**:第一阶段只建必要骨架,未来扩展点记录在 Roadmap 中
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "NeoZQYY Monorepo"
|
||||
subgraph "apps/"
|
||||
ETL["apps/etl/pipelines/feiqiu/"]
|
||||
Backend["apps/backend/ (FastAPI)"]
|
||||
Mini["apps/miniprogram/ (Donut+TDesign)"]
|
||||
Admin["apps/admin-web/ (未来)"]
|
||||
end
|
||||
|
||||
subgraph "packages/"
|
||||
Shared["packages/shared/"]
|
||||
end
|
||||
|
||||
subgraph "gui/"
|
||||
GUI["gui/ (PySide6,过渡期)"]
|
||||
end
|
||||
|
||||
subgraph "db/"
|
||||
ETLDB["db/etl_feiqiu/"]
|
||||
AppDB["db/zqyy_app/"]
|
||||
FDW["db/fdw/"]
|
||||
end
|
||||
end
|
||||
|
||||
ETL --> Shared
|
||||
Backend --> Shared
|
||||
GUI --> Shared
|
||||
Backend --> AppDB
|
||||
ETL --> ETLDB
|
||||
AppDB -.->|postgres_fdw 只读| ETLDB
|
||||
```
|
||||
|
||||
### 数据流架构
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
API["上游 SaaS API"] --> ODS["ods (原始数据)"]
|
||||
ODS --> DWD["dwd (main+EX 明细)"]
|
||||
DWD --> Core["core (统一最小字段集)"]
|
||||
DWD --> DWS["dws (汇总/工资)"]
|
||||
Core --> DWS
|
||||
DWS --> App["app (视图+RLS)"]
|
||||
App -.->|FDW 只读映射| ZqyyApp["zqyy_app DB"]
|
||||
ZqyyApp --> FastAPI["FastAPI 后端"]
|
||||
FastAPI --> MiniApp["微信小程序"]
|
||||
|
||||
subgraph "etl_feiqiu DB"
|
||||
Meta["meta (调度/游标)"]
|
||||
ODS
|
||||
DWD
|
||||
Core
|
||||
DWS
|
||||
App
|
||||
end
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 1. 目录结构生成器(Scaffold)
|
||||
|
||||
负责创建 Monorepo 完整目录结构和基础配置文件。
|
||||
|
||||
**输入**:目标路径 `C:\NeoZQYY\`
|
||||
**输出**:完整目录树 + README.md + 配置文件
|
||||
|
||||
**关键行为**:
|
||||
- 创建所有一级和二级目录
|
||||
- 为每个一级目录生成 README.md(作用 + 结构 + Roadmap)
|
||||
- 生成 `.gitignore`、`.kiroignore`、`.env.template`
|
||||
- 初始化 Git 仓库
|
||||
|
||||
### 2. uv Workspace 配置
|
||||
|
||||
**根 `pyproject.toml`**:
|
||||
```toml
|
||||
[project]
|
||||
name = "neozqyy"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"apps/etl/pipelines/feiqiu",
|
||||
"apps/backend",
|
||||
"packages/shared",
|
||||
"gui",
|
||||
]
|
||||
```
|
||||
|
||||
**子项目 `pyproject.toml` 模式**(以 ETL 为例):
|
||||
```toml
|
||||
[project]
|
||||
name = "etl-feiqiu"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"psycopg2-binary>=2.9.0",
|
||||
"requests>=2.28.0",
|
||||
"python-dateutil>=2.8.0",
|
||||
"tzdata>=2023.0",
|
||||
"python-dotenv",
|
||||
"openpyxl>=3.1.0",
|
||||
"neozqyy-shared",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
neozqyy-shared = { workspace = true }
|
||||
```
|
||||
|
||||
### 3. 配置隔离机制
|
||||
|
||||
**分层加载顺序**:
|
||||
```
|
||||
根 .env(公共配置)→ 应用 .env.local(私有覆盖)→ 环境变量 → CLI 参数
|
||||
```
|
||||
|
||||
**实现方式**:
|
||||
- 现有 `AppConfig` 的 `DEFAULTS < ENV < CLI` 模式保持不变
|
||||
- 新增:在 `load_env_overrides()` 中先加载根 `.env`,再加载应用级 `.env.local`
|
||||
- 冲突策略:应用级优先(后加载覆盖先加载)
|
||||
- 缺失检测:在 `_validate()` 中检查必需项,报告缺失项名称
|
||||
|
||||
### 4. ETL 平移策略
|
||||
|
||||
**平移范围**:
|
||||
| 源路径 | 目标路径 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `api/` | `apps/etl/pipelines/feiqiu/api/` | API 客户端 |
|
||||
| `cli/` | `apps/etl/pipelines/feiqiu/cli/` | CLI 入口 |
|
||||
| `config/` | `apps/etl/pipelines/feiqiu/config/` | 配置 |
|
||||
| `loaders/` | `apps/etl/pipelines/feiqiu/loaders/` | 加载器 |
|
||||
| `models/` | `apps/etl/pipelines/feiqiu/models/` | 模型 |
|
||||
| `orchestration/` | `apps/etl/pipelines/feiqiu/orchestration/` | 调度 |
|
||||
| `scd/` | `apps/etl/pipelines/feiqiu/scd/` | SCD2 |
|
||||
| `tasks/` | `apps/etl/pipelines/feiqiu/tasks/` | 任务 |
|
||||
| `utils/` | `apps/etl/pipelines/feiqiu/utils/` | 工具 |
|
||||
| `quality/` | `apps/etl/pipelines/feiqiu/quality/` | 质量检查 |
|
||||
| `tests/` | `apps/etl/pipelines/feiqiu/tests/` | 测试 |
|
||||
| `database/*.sql` | `db/etl_feiqiu/schemas/` | DDL |
|
||||
| `database/migrations/` | `db/etl_feiqiu/migrations/` | 迁移脚本 |
|
||||
| `database/seed_*.sql` | `db/etl_feiqiu/seeds/` | 种子数据 |
|
||||
| `gui/` | `gui/` | GUI(顶层) |
|
||||
|
||||
**import 路径策略**:
|
||||
- ETL 内部使用相对 import(`from .config.settings import AppConfig`)或保持现有绝对 import
|
||||
- `pyproject.toml` 中设置 `pythonpath`,使 `apps/etl/pipelines/feiqiu/` 为 Python 路径根
|
||||
- `pytest.ini` 同步更新 `pythonpath = .`
|
||||
- 目标:ETL 内部代码零修改或最小修改
|
||||
|
||||
### 5. 小程序平移策略
|
||||
|
||||
**平移范围**:
|
||||
| 源路径 | 目标路径 |
|
||||
|--------|----------|
|
||||
| `C:\ZQYY\XCX\`(除 Prototype) | `apps/miniprogram/` |
|
||||
| `C:\ZQYY\XCX\Prototype\` | `docs/h5_ui/` |
|
||||
|
||||
小程序为独立前端项目(Donut + TDesign),不涉及 Python 依赖管理,直接复制即可。
|
||||
|
||||
|
||||
### 6. 数据库 Schema 重组(etl_feiqiu)
|
||||
|
||||
**现有 → 新 schema 映射**:
|
||||
|
||||
| 现有 Schema | 新 Schema | 说明 |
|
||||
|-------------|-----------|------|
|
||||
| `etl_admin` | `meta` | 调度、游标、运行记录 |
|
||||
| `billiards_ods` | `ods` | ODS 原始数据,结构不变 |
|
||||
| `billiards_dwd` | `dwd` | DWD 明细,保留 main+EX 拆分 |
|
||||
| (新增) | `core` | 统一维度/事实最小字段集 |
|
||||
| `billiards_dws` | `dws` | DWS 汇总,结构不变 |
|
||||
| (新增) | `app` | 面向外部的视图/函数 + RLS |
|
||||
|
||||
**core schema 设计原则**:
|
||||
- 仅包含跨系统共享的最小字段集(如会员 ID、姓名、手机号、状态)
|
||||
- 维度表从 DWD 维度表提取核心字段
|
||||
- 事实表从 DWD 事实表提取核心度量
|
||||
- 第一版保持精简,后续按需扩展
|
||||
|
||||
**app schema 设计原则**:
|
||||
- 以视图(VIEW)封装 DWS/Core 层数据
|
||||
- 所有视图启用 RLS,以 `site_id` 过滤
|
||||
- 提供函数接口供 FDW 映射使用
|
||||
- 不存储实际数据,仅做访问层
|
||||
|
||||
**RLS 实现方案**:
|
||||
```sql
|
||||
-- 创建应用角色
|
||||
CREATE ROLE app_reader;
|
||||
|
||||
-- 在 app schema 的视图上启用 RLS
|
||||
ALTER TABLE app.v_member_summary ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建策略:根据会话变量 app.current_site_id 过滤
|
||||
CREATE POLICY site_isolation ON app.v_member_summary
|
||||
FOR SELECT TO app_reader
|
||||
USING (site_id = current_setting('app.current_site_id')::bigint);
|
||||
```
|
||||
|
||||
### 7. 业务数据库设计(zqyy_app)
|
||||
|
||||
**核心表**:
|
||||
- `users`:用户账户(微信 OpenID、手机号、角色)
|
||||
- `roles` / `permissions`:RBAC 权限模型
|
||||
- `user_roles`:用户-角色关联
|
||||
- `tasks`:任务管理(审批流)
|
||||
- `approvals`:审批记录
|
||||
|
||||
**FDW 映射**:
|
||||
```sql
|
||||
-- 在 zqyy_app 中创建外部服务器
|
||||
CREATE SERVER etl_feiqiu_server
|
||||
FOREIGN DATA WRAPPER postgres_fdw
|
||||
OPTIONS (host 'localhost', dbname 'etl_feiqiu', port '5432');
|
||||
|
||||
-- 创建用户映射
|
||||
CREATE USER MAPPING FOR app_user
|
||||
SERVER etl_feiqiu_server
|
||||
OPTIONS (user 'app_reader', password '***');
|
||||
|
||||
-- 导入 app schema 的外部表
|
||||
IMPORT FOREIGN SCHEMA app
|
||||
FROM SERVER etl_feiqiu_server
|
||||
INTO fdw_etl;
|
||||
```
|
||||
|
||||
**约束**:FDW 映射为只读,`zqyy_app` 不存储 ETL 数据副本。
|
||||
|
||||
### 8. FastAPI 后端骨架
|
||||
|
||||
**项目结构**:
|
||||
```
|
||||
apps/backend/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI 入口
|
||||
│ ├── config.py # 配置加载
|
||||
│ ├── database.py # 数据库连接
|
||||
│ ├── routers/ # 路由模块
|
||||
│ │ └── __init__.py
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ └── __init__.py
|
||||
│ └── schemas/ # Pydantic 模型
|
||||
│ └── __init__.py
|
||||
├── tests/
|
||||
│ └── __init__.py
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**关键配置**:
|
||||
- 连接 `zqyy_app` 数据库(通过 FDW 访问 ETL 数据)
|
||||
- OpenAPI 文档自动生成(FastAPI 内置)
|
||||
- 依赖 `packages/shared` 获取通用工具
|
||||
|
||||
### 9. 共享包(packages/shared)
|
||||
|
||||
**模块划分**:
|
||||
```
|
||||
packages/shared/
|
||||
├── src/
|
||||
│ └── neozqyy_shared/
|
||||
│ ├── __init__.py
|
||||
│ ├── enums.py # 字段枚举定义
|
||||
│ ├── money.py # 金额精度工具(CNY, numeric(2))
|
||||
│ └── datetime_utils.py # 时间处理工具
|
||||
├── tests/
|
||||
│ └── __init__.py
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**提取来源**:
|
||||
- `enums.py`:从 ETL 的 `models/` 中提取通用枚举
|
||||
- `money.py`:金额四舍五入、格式化(`Decimal` + `ROUND_HALF_UP`,scale=2)
|
||||
- `datetime_utils.py`:时区转换、日期范围计算(从 `utils/` 提取)
|
||||
|
||||
### 10. .kiro 迁移
|
||||
|
||||
**迁移内容**:
|
||||
- 复制 `.kiro/steering/` 到 Monorepo
|
||||
- 更新 `product.md`:从单一 ETL 视角扩展为 Monorepo 全局视角
|
||||
- 更新 `tech.md`:新增 FastAPI、uv workspace、Donut+TDesign 等技术栈
|
||||
- 更新 `structure-lite.md`:反映 Monorepo 目录结构和模块边界
|
||||
- 更新路径引用:所有 steering 文件中的路径适配新结构
|
||||
|
||||
## 数据模型
|
||||
|
||||
### etl_feiqiu 数据库(六层 Schema)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
META {
|
||||
bigint run_id PK
|
||||
text task_code
|
||||
timestamptz started_at
|
||||
timestamptz ended_at
|
||||
text status
|
||||
jsonb result_summary
|
||||
}
|
||||
|
||||
META ||--o{ ODS : "调度触发"
|
||||
|
||||
ODS {
|
||||
bigint id PK
|
||||
text content_hash PK
|
||||
jsonb payload
|
||||
text source_endpoint
|
||||
timestamptz fetched_at
|
||||
}
|
||||
|
||||
ODS ||--o{ DWD : "清洗装载"
|
||||
|
||||
DWD {
|
||||
bigint id PK
|
||||
timestamptz scd2_start_time
|
||||
timestamptz scd2_end_time
|
||||
int scd2_is_current
|
||||
int scd2_version
|
||||
}
|
||||
|
||||
DWD ||--o{ CORE : "提取最小字段集"
|
||||
|
||||
CORE {
|
||||
bigint id PK
|
||||
text name
|
||||
bigint site_id
|
||||
}
|
||||
|
||||
DWD ||--o{ DWS : "汇总聚合"
|
||||
CORE ||--o{ DWS : "汇总聚合"
|
||||
|
||||
DWS {
|
||||
bigint id PK
|
||||
date stat_date
|
||||
numeric amount
|
||||
bigint site_id
|
||||
}
|
||||
|
||||
DWS ||--o{ APP : "视图封装"
|
||||
|
||||
APP {
|
||||
text view_name
|
||||
text rls_policy
|
||||
}
|
||||
```
|
||||
|
||||
### zqyy_app 数据库
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS {
|
||||
bigint id PK
|
||||
text wx_openid UK
|
||||
text mobile
|
||||
text nickname
|
||||
int status
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
ROLES {
|
||||
int id PK
|
||||
text name UK
|
||||
text description
|
||||
}
|
||||
|
||||
USER_ROLES {
|
||||
bigint user_id FK
|
||||
int role_id FK
|
||||
}
|
||||
|
||||
PERMISSIONS {
|
||||
int id PK
|
||||
text resource
|
||||
text action
|
||||
}
|
||||
|
||||
ROLE_PERMISSIONS {
|
||||
int role_id FK
|
||||
int permission_id FK
|
||||
}
|
||||
|
||||
USERS ||--o{ USER_ROLES : "拥有"
|
||||
ROLES ||--o{ USER_ROLES : "分配给"
|
||||
ROLES ||--o{ ROLE_PERMISSIONS : "包含"
|
||||
PERMISSIONS ||--o{ ROLE_PERMISSIONS : "授予"
|
||||
|
||||
FDW_ETL_VIEWS {
|
||||
text foreign_table_name
|
||||
text source_schema
|
||||
text mapping_type
|
||||
}
|
||||
```
|
||||
|
||||
### 配置分层模型
|
||||
|
||||
```
|
||||
优先级(低 → 高):
|
||||
┌─────────────────────────────┐
|
||||
│ 根 .env(公共配置模板) │ DB_HOST, DB_PORT, TIMEZONE
|
||||
├─────────────────────────────┤
|
||||
│ 应用 .env.local(私有覆盖) │ DB_NAME, DB_PASSWORD, API_TOKEN
|
||||
├─────────────────────────────┤
|
||||
│ 环境变量 │ 运行时覆盖
|
||||
├─────────────────────────────┤
|
||||
│ CLI 参数 │ 最高优先级
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性是系统在所有有效执行中都应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: README.md 结构完整性
|
||||
|
||||
*对于任意* Monorepo 一级目录,其 README.md 文件应存在且包含"作用说明"、"结构描述"和"Roadmap"三个段落。
|
||||
|
||||
**Validates: Requirements 1.5**
|
||||
|
||||
### Property 2: Python 子项目配置完整性
|
||||
|
||||
*对于任意* uv workspace 声明的 Python 子项目成员,该子项目目录下应存在独立的 `pyproject.toml` 文件,且文件中包含 `[project]` 段落。
|
||||
|
||||
**Validates: Requirements 3.2**
|
||||
|
||||
### Property 3: 配置优先级 - .env.local 覆盖
|
||||
|
||||
*对于任意*配置项名称和两个不同的值,当根 `.env` 和应用 `.env.local` 都定义了该配置项时,配置加载器返回的值应等于 `.env.local` 中的值。
|
||||
|
||||
**Validates: Requirements 4.3**
|
||||
|
||||
### Property 4: 必需配置缺失检测
|
||||
|
||||
*对于任意*必需配置项,当所有配置层级(.env、.env.local、环境变量、CLI)均未提供该项时,配置加载器应抛出错误,且错误信息中包含该缺失配置项的名称。
|
||||
|
||||
**Validates: Requirements 4.4**
|
||||
|
||||
### Property 5: 文件迁移完整性
|
||||
|
||||
*对于任意*源-目标目录映射关系(ETL 业务代码、database 文件、tests 目录),源目录中的每个文件在目标目录的对应位置都应存在且内容一致。
|
||||
|
||||
**Validates: Requirements 5.1, 5.2, 5.3**
|
||||
|
||||
### Property 6: Schema 表定义迁移完整性
|
||||
|
||||
*对于任意*现有数据库 schema(billiards_ods、billiards_dws)中的表,新 schema(ods、dws)的 DDL 文件中应包含该表的 CREATE TABLE 定义。
|
||||
|
||||
**Validates: Requirements 7.3, 7.6**
|
||||
|
||||
### Property 7: Core schema 最小字段集
|
||||
|
||||
*对于任意* core schema 中的表,其字段数量应严格少于对应 dwd schema 中同名(或对应)表的字段数量。
|
||||
|
||||
**Validates: Requirements 7.5**
|
||||
|
||||
### Property 8: 测试数据库结构一致性
|
||||
|
||||
*对于任意*生产数据库(etl_feiqiu、zqyy_app)中的 schema 和表定义,对应的测试数据库(test_etl_feiqiu、test_zqyy_app)中应存在相同的 schema 和表结构。
|
||||
|
||||
**Validates: Requirements 9.1, 9.2**
|
||||
|
||||
### Property 9: Steering 文件路径更新
|
||||
|
||||
*对于任意* `.kiro/steering/` 目录下的文件,文件内容中不应包含旧仓库路径引用(如 `FQ-ETL`、`C:\ZQYY\FQ-ETL`)。
|
||||
|
||||
**Validates: Requirements 10.2**
|
||||
|
||||
### Property 10: 业务表 site_id 字段存在性
|
||||
|
||||
*对于任意* app schema 中的业务视图和 dws/core schema 中的业务表,其定义中应包含 `site_id` 字段。
|
||||
|
||||
**Validates: Requirements 13.1**
|
||||
|
||||
### Property 11: RLS 按 site_id 隔离
|
||||
|
||||
*对于任意* app schema 中启用了 RLS 的视图,当会话变量 `app.current_site_id` 设置为某个门店 ID 时,查询结果应仅包含该 `site_id` 的数据行。
|
||||
|
||||
**Validates: Requirements 13.2**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 配置错误
|
||||
- **缺失必需配置**:启动时立即报错,列出所有缺失项名称,不启动服务
|
||||
- **配置值格式错误**:报告具体的配置项路径和期望格式
|
||||
- **.env 文件不存在**:使用默认值继续,不报错(.env.template 仅为模板)
|
||||
|
||||
### 迁移错误
|
||||
- **源文件不存在**:记录警告日志,继续迁移其他文件,最终汇总报告缺失文件列表
|
||||
- **目标目录已存在**:提示用户确认是否覆盖,默认不覆盖
|
||||
- **import 路径修复失败**:记录错误日志,标记需要手动修复的文件
|
||||
|
||||
### 数据库错误
|
||||
- **Schema 创建失败**:回滚当前 schema 的所有 DDL,报告失败原因
|
||||
- **FDW 连接失败**:记录错误日志,不影响本地表的正常使用
|
||||
- **RLS 策略创建失败**:回滚策略创建,报告受影响的表
|
||||
|
||||
### 测试数据库错误
|
||||
- **结构不一致**:提供 diff 工具比较生产与测试库结构差异
|
||||
- **数据迁移失败**:回滚到迁移前状态,报告失败的表和原因
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 测试框架
|
||||
- **单元测试**:`pytest`(Python 子项目)
|
||||
- **属性测试**:`hypothesis`(Python 属性测试库)
|
||||
- 每个属性测试配置最少 100 次迭代
|
||||
|
||||
### 单元测试覆盖
|
||||
|
||||
1. **Scaffold 测试**:验证目录创建、文件生成的具体示例
|
||||
2. **配置加载器测试**:验证分层加载、冲突处理、缺失检测的边界情况
|
||||
3. **迁移脚本测试**:验证文件复制、路径映射的具体场景
|
||||
4. **DDL 语法测试**:验证生成的 SQL 语法正确性
|
||||
|
||||
### 属性测试覆盖
|
||||
|
||||
每个属性测试必须引用设计文档中的属性编号:
|
||||
|
||||
- **Feature: monorepo-migration, Property 1: README.md 结构完整性** — 验证所有一级目录 README 包含必需段落
|
||||
- **Feature: monorepo-migration, Property 2: Python 子项目配置完整性** — 验证所有 workspace 成员有 pyproject.toml
|
||||
- **Feature: monorepo-migration, Property 3: 配置优先级** — 生成随机配置项,验证 .env.local 覆盖行为
|
||||
- **Feature: monorepo-migration, Property 4: 必需配置缺失检测** — 生成随机必需项组合,验证缺失报错
|
||||
- **Feature: monorepo-migration, Property 5: 文件迁移完整性** — 验证源-目标文件映射的完整性
|
||||
- **Feature: monorepo-migration, Property 6: Schema 表定义迁移完整性** — 验证现有表在新 DDL 中存在
|
||||
- **Feature: monorepo-migration, Property 7: Core schema 最小字段集** — 验证 core 表字段数少于 dwd
|
||||
- **Feature: monorepo-migration, Property 8: 测试数据库结构一致性** — 验证测试库与生产库结构相同
|
||||
- **Feature: monorepo-migration, Property 9: Steering 文件路径更新** — 验证无旧路径残留
|
||||
- **Feature: monorepo-migration, Property 10: 业务表 site_id 存在性** — 验证业务表包含 site_id
|
||||
- **Feature: monorepo-migration, Property 11: RLS 隔离** — 验证 RLS 按 site_id 过滤(集成测试)
|
||||
|
||||
### 集成测试
|
||||
|
||||
- **ETL 运行验证**:在新目录结构下运行 `pytest tests/unit`,确保所有现有测试通过
|
||||
- **数据库 Schema 验证**:在测试数据库上执行 DDL,验证 schema 创建成功
|
||||
- **FDW 连接验证**:验证 zqyy_app 通过 FDW 可读取 etl_feiqiu 的 app schema 数据
|
||||
- **uv workspace 验证**:运行 `uv sync`,验证所有子项目依赖正确解析
|
||||
186
.kiro/specs/monorepo-migration/requirements.md
Normal file
186
.kiro/specs/monorepo-migration/requirements.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# 需求文档:Monorepo 迁移
|
||||
|
||||
## 简介
|
||||
|
||||
将现有台球厅运营助手项目从单一 ETL 仓库(`FQ-ETL`)扩展为 Monorepo 单体仓库(`NeoZQYY`),整合 ETL 管线、微信小程序后端、小程序前端、管理后台等多个子项目。迁移采用一次性搬迁策略,不保留 Git 历史,所有架构决策已在前期讨论中确认。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Monorepo**:单体仓库,多个子项目共存于同一 Git 仓库中
|
||||
- **ETL_Pipeline**:数据抽取-转换-加载管线,负责从上游 SaaS API 抓取数据并逐层处理
|
||||
- **ODS**:操作数据存储层(Operational Data Store),保留源 payload
|
||||
- **DWD**:明细数据层(Data Warehouse Detail),清洗后的明细数据
|
||||
- **DWS**:数据服务层(Data Warehouse Service),汇总与聚合数据
|
||||
- **Core**:统一维度/事实最小字段集层,位于 DWD 与 DWS 之间
|
||||
- **App_Schema**:应用层 schema,提供视图/函数 + RLS 供外部访问
|
||||
- **Meta_Schema**:元数据层 schema,存储 ETL 调度、游标、运行记录
|
||||
- **FDW**:PostgreSQL 外部数据包装器(Foreign Data Wrapper),用于跨库只读映射
|
||||
- **uv_Workspace**:Python 包管理工具 uv 的 workspace 模式,管理多包依赖
|
||||
- **RLS**:行级安全策略(Row Level Security),用于多门店数据隔离
|
||||
- **SCD2**:缓慢变化维度类型 2(Slowly Changing Dimension Type 2),维度历史追踪
|
||||
- **etl_feiqiu**:飞球平台 ETL 数据库实例名
|
||||
- **zqyy_app**:业务应用数据库实例名(用户/权限/任务/审批)
|
||||
- **site_id**:门店标识字段,用于多门店数据隔离
|
||||
- **Scaffold**:项目骨架,包含目录结构、配置文件、README 等基础设施
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:Monorepo 骨架搭建
|
||||
|
||||
**用户故事:** 作为开发者,我希望在 `C:\NeoZQYY\` 创建完整的 Monorepo 目录结构和基础配置,以便所有子项目有统一的组织方式和开发规范。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Scaffold 初始化执行时,THE Scaffold SHALL 在 `C:\NeoZQYY\` 下创建以下一级目录:`apps/`、`gui/`、`packages/`、`db/`、`docs/`、`infra/`、`scripts/`、`samples/`、`tests/`、`tmp/`、`.kiro/`
|
||||
2. WHEN Scaffold 初始化执行时,THE Scaffold SHALL 在 `apps/` 下创建子目录:`etl/`(含 `pipelines/feiqiu/`)、`backend/`、`miniprogram/`、`admin-web/`
|
||||
3. WHEN Scaffold 初始化执行时,THE Scaffold SHALL 在 `db/` 下创建子目录:`etl_feiqiu/`(含 `schemas/`、`migrations/`、`seeds/`)、`zqyy_app/`(含 `schemas/`、`migrations/`、`seeds/`)、`fdw/`
|
||||
4. WHEN Scaffold 初始化执行时,THE Scaffold SHALL 在 `docs/` 下创建子目录:`prd/`、`contracts/`(含 `openapi/`、`schemas/`、`data_dictionary/`)、`permission_matrix/`、`architecture/`、`database/`、`h5_ui/`、`ops/`、`audit/`、`roadmap/`
|
||||
5. THE Scaffold SHALL 为每个一级目录生成 `README.md` 文件,包含该目录的作用说明、内部结构描述和 Roadmap 段落
|
||||
6. WHEN 某个功能"暂不实施但未来必须做"时,THE Scaffold SHALL 将该内容记录在对应目录 `README.md` 的 Roadmap 段落中
|
||||
|
||||
### 需求 2:Git 仓库与版本控制配置
|
||||
|
||||
**用户故事:** 作为开发者,我希望新 Monorepo 有正确的 Git 配置,以便代码版本管理规范且安全。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Git 仓库初始化时,THE Scaffold SHALL 创建新的 Git 仓库,不迁移旧仓库历史
|
||||
2. THE Scaffold SHALL 生成 `.gitignore` 文件,排除 `tmp/`、`__pycache__/`、`.env`(非模板)、`*.pyc`、`.hypothesis/`、`.pytest_cache/`、`logs/`、`node_modules/`、虚拟环境目录等
|
||||
3. THE Scaffold SHALL 生成 `.kiroignore` 文件,排除不需要 Kiro 索引的目录
|
||||
|
||||
### 需求 3:Python 包管理与 uv Workspace 配置
|
||||
|
||||
**用户故事:** 作为开发者,我希望使用 `pyproject.toml` + `uv` workspace 管理多包依赖,以便各子项目的依赖隔离且可统一管理。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Scaffold SHALL 在 Monorepo 根目录生成 `pyproject.toml`,配置 uv workspace 并声明所有 Python 子项目成员
|
||||
2. THE Scaffold SHALL 为每个 Python 子项目(`apps/etl/pipelines/feiqiu/`、`apps/backend/`、`packages/shared/`、`gui/`)生成独立的 `pyproject.toml`
|
||||
3. WHEN 子项目声明对 `packages/shared` 的依赖时,THE uv_Workspace SHALL 通过 workspace 路径引用解析该依赖
|
||||
|
||||
### 需求 4:环境配置隔离
|
||||
|
||||
**用户故事:** 作为开发者,我希望公共配置和各应用私有配置分层管理,以便敏感信息不泄露且配置不冲突。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Scaffold SHALL 在 Monorepo 根目录生成 `.env.template` 文件,包含公共配置项(数据库主机、端口等非敏感信息)的模板
|
||||
2. WHEN 各应用需要私有配置时,THE Scaffold SHALL 支持在应用目录下放置 `.env.local` 文件覆盖公共配置
|
||||
3. IF 公共 `.env` 与应用 `.env.local` 存在同名配置项且值冲突,THEN THE 配置加载器 SHALL 以应用级 `.env.local` 的值为准
|
||||
4. IF 必需的配置项在所有层级均缺失,THEN THE 配置加载器 SHALL 在启动时报告明确的错误信息,指出缺失的配置项名称
|
||||
|
||||
### 需求 5:ETL 项目平移
|
||||
|
||||
**用户故事:** 作为开发者,我希望将现有 ETL 项目整体平移到 Monorepo 中,以便 ETL 功能在新仓库中正常运行。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN ETL 平移执行时,THE 迁移脚本 SHALL 将 `C:\ZQYY\FQ-ETL` 的业务代码(`api/`、`cli/`、`config/`、`loaders/`、`models/`、`orchestration/`、`scd/`、`tasks/`、`utils/`、`quality/`)复制到 `apps/etl/pipelines/feiqiu/`
|
||||
2. WHEN ETL 平移执行时,THE 迁移脚本 SHALL 将 `database/` 目录的 DDL、seed、migration 文件迁移到 `db/etl_feiqiu/` 对应子目录
|
||||
3. WHEN ETL 平移执行时,THE 迁移脚本 SHALL 将 `tests/` 目录复制到 `apps/etl/pipelines/feiqiu/tests/`
|
||||
4. WHEN ETL 平移完成后,THE ETL_Pipeline SHALL 通过 `pytest tests/unit` 验证所有单元测试通过
|
||||
5. IF ETL 内部存在需要调整的 import 路径,THEN THE 迁移脚本 SHALL 更新这些路径以适配新目录结构
|
||||
6. WHEN ETL 平移执行时,THE 迁移脚本 SHALL 将现有 `gui/` 目录迁移到 Monorepo 顶层 `gui/`
|
||||
|
||||
### 需求 6:小程序前端平移
|
||||
|
||||
**用户故事:** 作为开发者,我希望将微信小程序项目迁移到 Monorepo 中,以便前端代码与后端统一管理。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 小程序平移执行时,THE 迁移脚本 SHALL 将 `C:\ZQYY\XCX` 的项目文件复制到 `apps/miniprogram/`
|
||||
2. WHEN 小程序平移执行时,THE 迁移脚本 SHALL 将 `C:\ZQYY\XCX\Prototype` 目录复制到 `docs/h5_ui/`
|
||||
3. WHEN 小程序平移完成后,THE 小程序项目 SHALL 保持原有的 Donut + TDesign 技术栈配置不变
|
||||
|
||||
### 需求 7:数据库 Schema 重组(etl_feiqiu)
|
||||
|
||||
**用户故事:** 作为数据工程师,我希望将 ETL 数据库重组为六层 schema 架构,以便数据分层清晰、职责明确。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 数据库迁移 SHALL 为 `etl_feiqiu` 数据库创建六个 schema:`meta`、`ods`、`dwd`、`core`、`dws`、`app`
|
||||
2. WHEN `meta` schema 创建时,THE DDL SHALL 包含 ETL 调度、游标、运行记录相关表(从现有 `etl_admin` schema 迁移)
|
||||
3. WHEN `ods` schema 创建时,THE DDL SHALL 包含现有 `billiards_ods` 的所有表定义
|
||||
4. WHEN `dwd` schema 创建时,THE DDL SHALL 保留现有 main + EX 拆分模式(因字段量大)
|
||||
5. WHEN `core` schema 创建时,THE DDL SHALL 仅包含统一维度表和事实表的最小字段集
|
||||
6. WHEN `dws` schema 创建时,THE DDL SHALL 包含现有 `billiards_dws` 的汇总表定义(助教业绩、财务日报、工资计算等)
|
||||
7. WHEN `app` schema 创建时,THE DDL SHALL 创建面向外部访问的视图和函数,并配置 RLS 策略以 `site_id` 隔离多门店数据
|
||||
8. THE 数据库迁移 SHALL 将所有 DDL 文件存放在 `db/etl_feiqiu/schemas/` 目录下,每个 schema 一个独立文件
|
||||
|
||||
### 需求 8:业务数据库设计(zqyy_app)
|
||||
|
||||
**用户故事:** 作为后端开发者,我希望有独立的业务数据库存储用户、权限、任务、审批等应用数据,以便业务逻辑与 ETL 数据解耦。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 数据库迁移 SHALL 为 `zqyy_app` 数据库创建用户管理、权限控制、任务管理、审批流程相关的表结构
|
||||
2. THE 数据库迁移 SHALL 将 `zqyy_app` 的 DDL 文件存放在 `db/zqyy_app/schemas/` 目录下
|
||||
3. WHEN `zqyy_app` 需要访问 ETL 数据时,THE FDW 配置 SHALL 通过 `postgres_fdw` 将 `etl_feiqiu` 的 `app` schema 映射为 `zqyy_app` 中的外部表
|
||||
4. THE FDW 配置 SHALL 以只读方式映射,`zqyy_app` 不存储 ETL 数据的副本
|
||||
5. THE FDW 配置文件 SHALL 存放在 `db/fdw/` 目录下
|
||||
|
||||
### 需求 9:测试数据库镜像
|
||||
|
||||
**用户故事:** 作为开发者,我希望有与生产结构完全一致的测试数据库,以便在不影响生产数据的情况下进行开发和测试。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 数据库迁移 SHALL 创建 `test_etl_feiqiu` 数据库,其 schema 结构与 `etl_feiqiu` 完全一致
|
||||
2. THE 数据库迁移 SHALL 创建 `test_zqyy_app` 数据库,其 schema 结构与 `zqyy_app` 完全一致
|
||||
3. WHEN 测试数据库创建完成后,THE 迁移脚本 SHALL 提供从现有 `LLZQ-test` 数据库迁移测试数据到新结构的脚本
|
||||
4. WHEN 生产数据库 schema 发生变更时,THE 测试数据库 SHALL 同步应用相同的迁移脚本以保持结构一致
|
||||
|
||||
### 需求 10:.kiro 配置迁移与 Steering 更新
|
||||
|
||||
**用户故事:** 作为开发者,我希望 Kiro IDE 的配置和 steering 文件适配 Monorepo 结构,以便 AI 辅助开发在新仓库中正常工作。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN .kiro 迁移执行时,THE 迁移脚本 SHALL 将现有 `.kiro/steering/` 文件复制到 Monorepo 的 `.kiro/steering/`
|
||||
2. WHEN .kiro 迁移完成后,THE Steering 文件 SHALL 更新所有路径引用以反映 Monorepo 目录结构
|
||||
3. WHEN .kiro 迁移完成后,THE Steering 文件 SHALL 更新 `product.md`、`tech.md`、`structure.md` 为 Monorepo 视角的内容
|
||||
|
||||
### 需求 11:FastAPI 后端骨架
|
||||
|
||||
**用户故事:** 作为后端开发者,我希望有 FastAPI 项目骨架,以便快速开始小程序后端 API 的开发。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 后端骨架创建时,THE Scaffold SHALL 在 `apps/backend/` 下生成 FastAPI 项目结构,包含入口文件、路由目录、中间件目录、配置文件
|
||||
2. THE 后端骨架 SHALL 配置 OpenAPI 文档自动生成
|
||||
3. THE 后端骨架 SHALL 配置数据库连接模块,支持连接 `zqyy_app` 数据库
|
||||
4. THE 后端骨架 SHALL 包含独立的 `pyproject.toml`,声明 FastAPI 及相关依赖
|
||||
|
||||
### 需求 12:共享包基础结构
|
||||
|
||||
**用户故事:** 作为开发者,我希望有统一的共享包存放跨项目复用的工具代码,以便避免代码重复。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 共享包创建时,THE Scaffold SHALL 在 `packages/shared/` 下生成 Python 包结构,包含 `__init__.py` 和 `pyproject.toml`
|
||||
2. THE 共享包 SHALL 提取并包含以下通用工具模块:字段枚举定义、金额精度处理工具(CNY,numeric(2))、时间处理工具
|
||||
3. WHEN ETL 或后端项目引用共享包时,THE uv_Workspace SHALL 通过 workspace 路径依赖解析 `packages/shared`
|
||||
|
||||
### 需求 13:多门店数据隔离
|
||||
|
||||
**用户故事:** 作为系统架构师,我希望在同一数据库内通过 RLS 实现多门店数据隔离,以便未来扩展到多门店场景。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE 数据库设计 SHALL 在所有业务表中包含 `site_id` 字段标识门店归属
|
||||
2. WHEN RLS 策略启用时,THE 数据库 SHALL 根据当前会话的 `site_id` 参数自动过滤查询结果,仅返回该门店的数据
|
||||
3. WHEN 每个门店运行 ETL 时,THE ETL_Pipeline SHALL 作为独立进程执行,使用该门店的 `site_id` 标识数据
|
||||
|
||||
### 需求 14:基础设施配置管理
|
||||
|
||||
**用户故事:** 作为运维人员,我希望基础设施配置纳入版本控制,以便环境配置可追溯、可复现。
|
||||
|
||||
### 需求15:避免影响kiro性能,完成Monorepo后,根据文件和目录结构。编辑.kiroignore
|
||||
|
||||
验收标准:完善
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Scaffold SHALL 在 `infra/` 下创建 `jump_proxy/`、`tailscale/`、`firewall/` 子目录
|
||||
2. THE infra 目录 SHALL 纳入 Git 版本控制
|
||||
3. IF infra 目录中包含敏感配置文件,THEN THE `.gitignore` SHALL 排除这些敏感文件,同时保留非敏感的配置模板
|
||||
215
.kiro/specs/monorepo-migration/tasks.md
Normal file
215
.kiro/specs/monorepo-migration/tasks.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# 实施计划:Monorepo 迁移
|
||||
|
||||
## 概述
|
||||
|
||||
将现有 ETL 仓库迁移为 Monorepo 单体仓库,分 7 个阶段执行。每个阶段包含具体的代码/文件操作任务,按依赖顺序排列。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. Monorepo 骨架搭建
|
||||
- [x] 1.1 在 `C:\NeoZQYY\` 创建完整目录结构
|
||||
- 创建所有一级目录:`apps/`、`gui/`、`packages/`、`db/`、`docs/`、`infra/`、`scripts/`、`samples/`、`tests/`、`tmp/`、`.kiro/`
|
||||
- 创建 `apps/` 子目录:`etl/pipelines/feiqiu/`、`backend/`、`miniprogram/`、`admin-web/`
|
||||
- 创建 `db/` 子目录:`etl_feiqiu/schemas/`、`etl_feiqiu/migrations/`、`etl_feiqiu/seeds/`、`zqyy_app/schemas/`、`zqyy_app/migrations/`、`zqyy_app/seeds/`、`fdw/`
|
||||
- 创建 `docs/` 子目录:`prd/`、`contracts/openapi/`、`contracts/schemas/`、`contracts/data_dictionary/`、`permission_matrix/`、`architecture/`、`database/`、`h5_ui/`、`ops/`、`audit/`、`roadmap/`
|
||||
- 创建 `infra/` 子目录:`jump_proxy/`、`tailscale/`、`firewall/`
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 14.1_
|
||||
|
||||
- [x] 1.2 生成所有一级目录的 README.md
|
||||
- 每个 README 包含:作用说明、内部结构描述、Roadmap 段落
|
||||
- 一级目录列表:`apps/`、`gui/`、`packages/`、`db/`、`docs/`、`infra/`、`scripts/`、`samples/`、`tests/`
|
||||
- `apps/etl/README.md` 的 Roadmap 记录未来 sdk/connectors 拆分计划
|
||||
- `packages/README.md` 的 Roadmap 记录 etl_sdk、authz、data_contracts 候选
|
||||
- `db/README.md` 的 Roadmap 记录 FDW 演进计划
|
||||
- _Requirements: 1.5, 1.6_
|
||||
|
||||
- [x] 1.3 编写 README 结构完整性属性测试
|
||||
- **Property 1: README.md 结构完整性**
|
||||
- **Validates: Requirements 1.5**
|
||||
|
||||
- [x] 1.4 初始化 Git 仓库并生成版本控制配置
|
||||
- 在 `C:\NeoZQYY\` 执行 `git init`
|
||||
- 生成 `.gitignore`:排除 `tmp/`、`__pycache__/`、`.env`、`*.pyc`、`.hypothesis/`、`.pytest_cache/`、`logs/`、`node_modules/`、虚拟环境目录、`infra/` 下的敏感文件
|
||||
- 生成 `.kiroignore`
|
||||
- _Requirements: 2.1, 2.2, 2.3, 14.2, 14.3_
|
||||
|
||||
- [x] 1.5 配置 pyproject.toml 和 uv workspace
|
||||
- 生成根 `pyproject.toml`,声明 workspace 成员:`apps/etl/pipelines/feiqiu`、`apps/backend`、`packages/shared`、`gui`
|
||||
- 为每个 Python 子项目生成独立 `pyproject.toml`
|
||||
- _Requirements: 3.1, 3.2_
|
||||
|
||||
- [x] 1.6 编写 Python 子项目配置完整性属性测试
|
||||
- **Property 2: Python 子项目配置完整性**
|
||||
- **Validates: Requirements 3.2**
|
||||
|
||||
- [x] 1.7 生成环境配置模板
|
||||
- 生成根 `.env.template`,包含公共配置项模板(DB_HOST、DB_PORT、TIMEZONE 等)
|
||||
- _Requirements: 4.1_
|
||||
|
||||
- [x] 2. 检查点 - 骨架验证
|
||||
- 确保所有目录和文件已创建,ask the user if questions arise.
|
||||
|
||||
- [x] 3. ETL 项目平移
|
||||
- [x] 3.1 复制 ETL 业务代码到 Monorepo
|
||||
- 将 `C:\ZQYY\FQ-ETL` 的 `api/`、`cli/`、`config/`、`loaders/`、`models/`、`orchestration/`、`scd/`、`tasks/`、`utils/`、`quality/` 复制到 `apps/etl/pipelines/feiqiu/`
|
||||
- 将 `tests/` 复制到 `apps/etl/pipelines/feiqiu/tests/`
|
||||
- 将 `requirements.txt`、`pytest.ini`、`run_etl.bat`、`run_etl.sh` 复制到 `apps/etl/pipelines/feiqiu/`
|
||||
- _Requirements: 5.1, 5.3_
|
||||
|
||||
- [x] 3.2 迁移数据库文件到 db/etl_feiqiu/
|
||||
- 将 `database/schema_*.sql` 复制到 `db/etl_feiqiu/schemas/`
|
||||
- 将 `database/migrations/` 复制到 `db/etl_feiqiu/migrations/`
|
||||
- 将 `database/seed_*.sql` 复制到 `db/etl_feiqiu/seeds/`
|
||||
- 将 `database/connection.py`、`database/operations.py`、`database/base.py` 保留在 ETL 内部(`apps/etl/pipelines/feiqiu/database/`)
|
||||
- _Requirements: 5.2_
|
||||
|
||||
- [x] 3.3 迁移 GUI 到顶层
|
||||
- 将 `C:\ZQYY\FQ-ETL\gui/` 复制到 `C:\NeoZQYY\gui/`
|
||||
- 生成 `gui/pyproject.toml`,声明 PySide6 依赖
|
||||
- _Requirements: 5.6_
|
||||
|
||||
- [x] 3.4 调整 ETL 的 pyproject.toml 和 pytest.ini
|
||||
- 更新 `apps/etl/pipelines/feiqiu/pyproject.toml`,从 `requirements.txt` 提取依赖
|
||||
- 更新 `apps/etl/pipelines/feiqiu/pytest.ini`,设置 `pythonpath = .`
|
||||
- _Requirements: 5.4, 5.5_
|
||||
|
||||
- [x] 3.5 编写文件迁移完整性属性测试
|
||||
- **Property 5: 文件迁移完整性**
|
||||
- **Validates: Requirements 5.1, 5.2, 5.3**
|
||||
|
||||
- [x] 4. 检查点 - ETL 平移验证
|
||||
- 在 `apps/etl/pipelines/feiqiu/` 下运行 `pytest tests/unit`,确保所有单元测试通过
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 5. 小程序前端平移
|
||||
- [x] 5.1 复制小程序项目到 Monorepo
|
||||
- 将 `C:\ZQYY\XCX\`(除 Prototype 目录)复制到 `apps/miniprogram/`
|
||||
- 将 `C:\ZQYY\XCX\Prototype\` 复制到 `docs/h5_ui/`
|
||||
- 生成 `apps/miniprogram/README.md`
|
||||
- _Requirements: 6.1, 6.2, 6.3_
|
||||
|
||||
- [x] 6. 数据库 Schema 重组
|
||||
- [x] 6.1 编写 etl_feiqiu 六层 Schema DDL
|
||||
- 创建 `db/etl_feiqiu/schemas/meta.sql`:从现有 `etl_admin` schema 迁移调度、游标、运行记录表
|
||||
- 创建 `db/etl_feiqiu/schemas/ods.sql`:从现有 `billiards_ods` 迁移所有表定义,schema 名改为 `ods`
|
||||
- 创建 `db/etl_feiqiu/schemas/dwd.sql`:从现有 `billiards_dwd` 迁移,保留 main+EX 拆分
|
||||
- 创建 `db/etl_feiqiu/schemas/core.sql`:设计统一维度/事实最小字段集表
|
||||
- 创建 `db/etl_feiqiu/schemas/dws.sql`:从现有 `billiards_dws` 迁移汇总表
|
||||
- 创建 `db/etl_feiqiu/schemas/app.sql`:创建面向外部的视图 + RLS 策略(以 `site_id` 隔离)
|
||||
- _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7, 7.8_
|
||||
|
||||
- [x] 6.2 编写 Schema 表定义迁移完整性属性测试
|
||||
- **Property 6: Schema 表定义迁移完整性**
|
||||
- **Validates: Requirements 7.3, 7.6**
|
||||
|
||||
- [x] 6.3 编写 Core schema 最小字段集属性测试
|
||||
- **Property 7: Core schema 最小字段集**
|
||||
- **Validates: Requirements 7.5**
|
||||
|
||||
- [x] 6.4 编写 zqyy_app 数据库 Schema DDL
|
||||
- 创建 `db/zqyy_app/schemas/init.sql`:用户表、角色表、权限表、用户角色关联表、任务表、审批表
|
||||
- 所有业务表包含 `site_id` 字段
|
||||
- _Requirements: 8.1, 8.2, 13.1_
|
||||
|
||||
- [x] 6.5 编写 FDW 映射配置
|
||||
- 创建 `db/fdw/setup_fdw.sql`:CREATE SERVER、CREATE USER MAPPING(只读角色)、IMPORT FOREIGN SCHEMA
|
||||
- _Requirements: 8.3, 8.4, 8.5_
|
||||
|
||||
- [x] 6.6 编写业务表 site_id 存在性属性测试
|
||||
- **Property 10: 业务表 site_id 字段存在性**
|
||||
- **Validates: Requirements 13.1**
|
||||
|
||||
- [x] 6.7 编写测试数据库创建脚本
|
||||
- 创建 `db/etl_feiqiu/scripts/create_test_db.sql`:创建 `test_etl_feiqiu`,复用生产 DDL
|
||||
- 创建 `db/zqyy_app/scripts/create_test_db.sql`:创建 `test_zqyy_app`,复用生产 DDL
|
||||
- 创建 `db/scripts/migrate_test_data.sql`:从 `LLZQ-test` 迁移测试数据的脚本
|
||||
- _Requirements: 9.1, 9.2, 9.3_
|
||||
|
||||
- [x] 6.8 编写测试数据库结构一致性属性测试
|
||||
- **Property 8: 测试数据库结构一致性**
|
||||
- **Validates: Requirements 9.1, 9.2**
|
||||
|
||||
- [x] 7. 检查点 - 数据库 Schema 验证
|
||||
- 确保所有 DDL 文件语法正确,ask the user if questions arise.
|
||||
|
||||
|
||||
- [ ] 8. .kiro 迁移与 Steering 更新
|
||||
- [-] 8.1 复制 .kiro/steering/ 到 Monorepo
|
||||
- 将 `C:\ZQYY\FQ-ETL\.kiro\steering\` 所有文件复制到 `C:\NeoZQYY\.kiro\steering\`
|
||||
- 将 `C:\ZQYY\FQ-ETL\.kiro\specs\` 复制到 `C:\NeoZQYY\.kiro\specs\`(包含本 spec)
|
||||
- _Requirements: 10.1_
|
||||
|
||||
- [~] 8.2 更新 Steering 文件为 Monorepo 视角
|
||||
- 更新 `product.md`:从单一 ETL 扩展为 Monorepo 全局视角(ETL + 后端 + 小程序 + GUI)
|
||||
- 更新 `tech.md`:新增 FastAPI、uv workspace、Donut+TDesign 技术栈
|
||||
- 更新 `structure-lite.md`:反映 Monorepo 目录结构和模块边界
|
||||
- 更新所有 steering 文件中的路径引用,移除旧仓库路径(`FQ-ETL`、`C:\ZQYY\FQ-ETL`)
|
||||
- _Requirements: 10.2, 10.3_
|
||||
|
||||
- [~] 8.3 编写 Steering 文件路径更新属性测试
|
||||
- **Property 9: Steering 文件路径更新**
|
||||
- **Validates: Requirements 10.2**
|
||||
|
||||
- [ ] 9. FastAPI 后端骨架
|
||||
- [~] 9.1 创建 FastAPI 项目结构
|
||||
- 创建 `apps/backend/app/__init__.py`、`main.py`、`config.py`、`database.py`
|
||||
- 创建 `apps/backend/app/routers/__init__.py`
|
||||
- 创建 `apps/backend/app/middleware/__init__.py`
|
||||
- 创建 `apps/backend/app/schemas/__init__.py`
|
||||
- 创建 `apps/backend/tests/__init__.py`
|
||||
- `main.py` 中配置 FastAPI 实例,启用 OpenAPI 文档自动生成
|
||||
- `database.py` 中配置 `zqyy_app` 数据库连接
|
||||
- _Requirements: 11.1, 11.2, 11.3_
|
||||
|
||||
- [~] 9.2 生成 apps/backend/pyproject.toml
|
||||
- 声明 FastAPI、uvicorn、psycopg2-binary、neozqyy-shared 等依赖
|
||||
- 配置 uv workspace 源引用 `neozqyy-shared`
|
||||
- _Requirements: 11.4_
|
||||
|
||||
- [~] 9.3 生成 apps/backend/README.md
|
||||
- 包含作用说明、项目结构、启动方式、Roadmap
|
||||
- _Requirements: 1.5_
|
||||
|
||||
- [ ] 10. 共享包搭建
|
||||
- [~] 10.1 创建 packages/shared 包结构
|
||||
- 创建 `packages/shared/src/neozqyy_shared/__init__.py`
|
||||
- 创建 `packages/shared/src/neozqyy_shared/enums.py`:字段枚举定义(从 ETL models/ 提取通用枚举)
|
||||
- 创建 `packages/shared/src/neozqyy_shared/money.py`:金额精度工具(Decimal + ROUND_HALF_UP,scale=2)
|
||||
- 创建 `packages/shared/src/neozqyy_shared/datetime_utils.py`:时区转换、日期范围计算
|
||||
- 创建 `packages/shared/tests/__init__.py`
|
||||
- _Requirements: 12.1, 12.2_
|
||||
|
||||
- [~] 10.2 生成 packages/shared/pyproject.toml
|
||||
- 声明包名 `neozqyy-shared`,最小依赖(python-dateutil、tzdata)
|
||||
- _Requirements: 12.3_
|
||||
|
||||
- [~] 10.3 编写配置优先级属性测试
|
||||
- **Property 3: 配置优先级 - .env.local 覆盖**
|
||||
- **Validates: Requirements 4.3**
|
||||
|
||||
- [~] 10.4 编写必需配置缺失检测属性测试
|
||||
- **Property 4: 必需配置缺失检测**
|
||||
- **Validates: Requirements 4.4**
|
||||
|
||||
- [ ] 11. 检查点 - 全局验证
|
||||
- 验证 uv workspace 依赖解析:在根目录运行 `uv sync`
|
||||
- 验证 ETL 单元测试:在 `apps/etl/pipelines/feiqiu/` 下运行 `pytest tests/unit`
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [ ] 12. RLS 与多门店隔离验证
|
||||
- [~] 12.1 编写 RLS 按 site_id 隔离属性测试
|
||||
- **Property 11: RLS 按 site_id 隔离**
|
||||
- **Validates: Requirements 13.2**
|
||||
- 需要集成测试环境(test_etl_feiqiu 数据库)
|
||||
|
||||
- [ ] 13. 最终检查点
|
||||
- 确保所有文件已创建、所有 README 已编写、所有 DDL 语法正确
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
|
||||
- 每个任务引用具体需求编号,确保可追溯
|
||||
- 检查点确保增量验证,避免问题累积
|
||||
- 属性测试验证通用正确性,单元测试验证具体边界情况
|
||||
- 文件复制操作需要用户在终端手动执行(涉及跨目录操作),Kiro 负责生成目标文件内容
|
||||
1
.kiro/specs/repo-audit/.config.kiro
Normal file
1
.kiro/specs/repo-audit/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
424
.kiro/specs/repo-audit/design.md
Normal file
424
.kiro/specs/repo-audit/design.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# 设计文档:仓库治理只读审计
|
||||
|
||||
## 概述
|
||||
|
||||
本设计描述三个 Python 审计脚本的实现方案,用于对 etl-billiards 仓库进行只读分析并生成三份 Markdown 报告。脚本仅读取文件系统和源代码,不连接数据库、不修改任何现有文件,仅在 `docs/audit/repo/` 目录下输出报告。
|
||||
|
||||
审计脚本采用模块化设计:一个共享的仓库扫描器负责遍历文件系统,三个独立的分析器分别生成文件清单、流程树和文档对齐报告。
|
||||
|
||||
## 架构
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[scripts/audit/run_audit.py<br/>审计主入口] --> B[scripts/audit/scanner.py<br/>仓库扫描器]
|
||||
A --> C[scripts/audit/inventory_analyzer.py<br/>文件清单分析器]
|
||||
A --> D[scripts/audit/flow_analyzer.py<br/>流程树分析器]
|
||||
A --> E[scripts/audit/doc_alignment_analyzer.py<br/>文档对齐分析器]
|
||||
|
||||
B --> F[文件系统<br/>只读遍历]
|
||||
C --> G[docs/audit/repo/file_inventory.md]
|
||||
D --> H[docs/audit/repo/flow_tree.md]
|
||||
E --> I[docs/audit/repo/doc_alignment.md]
|
||||
|
||||
C --> B
|
||||
D --> B
|
||||
E --> B
|
||||
```
|
||||
|
||||
### 执行流程
|
||||
|
||||
1. `run_audit.py` 作为主入口,初始化扫描器并依次调用三个分析器
|
||||
2. `scanner.py` 递归遍历仓库,构建文件元信息列表(路径、大小、类型)
|
||||
3. 各分析器接收扫描结果,执行各自的分析逻辑,输出 Markdown 报告
|
||||
4. 所有报告写入 `docs/audit/repo/` 目录
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 1. 仓库扫描器 (`scripts/audit/scanner.py`)
|
||||
|
||||
负责递归遍历仓库文件系统,返回结构化的文件元信息。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class FileEntry:
|
||||
"""单个文件/目录的元信息"""
|
||||
rel_path: str # 相对于仓库根目录的路径
|
||||
is_dir: bool # 是否为目录
|
||||
size_bytes: int # 文件大小(目录为 0)
|
||||
extension: str # 文件扩展名(小写,含点号)
|
||||
is_empty_dir: bool # 是否为空目录
|
||||
|
||||
EXCLUDED_PATTERNS: list[str] = [
|
||||
".git", "__pycache__", ".pytest_cache",
|
||||
"*.pyc", ".kiro",
|
||||
]
|
||||
|
||||
def scan_repo(root: Path, exclude: list[str] = EXCLUDED_PATTERNS) -> list[FileEntry]:
|
||||
"""递归扫描仓库,返回所有文件和目录的元信息列表"""
|
||||
...
|
||||
```
|
||||
|
||||
### 2. 文件清单分析器 (`scripts/audit/inventory_analyzer.py`)
|
||||
|
||||
对扫描结果进行用途分类和处置标签分配。
|
||||
|
||||
```python
|
||||
# 用途分类枚举
|
||||
class Category(str, Enum):
|
||||
CORE_CODE = "核心代码"
|
||||
CONFIG = "配置"
|
||||
DATABASE_DEF = "数据库定义"
|
||||
TEST = "测试"
|
||||
DOCS = "文档"
|
||||
SCRIPTS = "脚本工具"
|
||||
GUI = "GUI"
|
||||
BUILD_DEPLOY = "构建与部署"
|
||||
LOG_OUTPUT = "日志与输出"
|
||||
TEMP_DEBUG = "临时与调试"
|
||||
OTHER = "其他"
|
||||
|
||||
# 处置标签枚举
|
||||
class Disposition(str, Enum):
|
||||
KEEP = "保留"
|
||||
CANDIDATE_DELETE = "候选删除"
|
||||
CANDIDATE_ARCHIVE = "候选归档"
|
||||
NEEDS_REVIEW = "待确认"
|
||||
|
||||
@dataclass
|
||||
class InventoryItem:
|
||||
"""清单条目"""
|
||||
rel_path: str
|
||||
category: Category
|
||||
disposition: Disposition
|
||||
description: str
|
||||
|
||||
def classify(entry: FileEntry) -> InventoryItem:
|
||||
"""根据路径、扩展名等规则对单个文件/目录进行分类和标签分配"""
|
||||
...
|
||||
|
||||
def build_inventory(entries: list[FileEntry]) -> list[InventoryItem]:
|
||||
"""批量分类所有文件条目"""
|
||||
...
|
||||
|
||||
def render_inventory_report(items: list[InventoryItem], repo_root: str) -> str:
|
||||
"""生成 Markdown 格式的文件清单报告"""
|
||||
...
|
||||
```
|
||||
|
||||
**分类规则(按优先级从高到低)**:
|
||||
|
||||
| 路径模式 | 用途分类 | 默认处置 |
|
||||
|---------|---------|---------|
|
||||
| `tmp/` 下所有文件 | 临时与调试 | 候选删除/候选归档 |
|
||||
| `logs/`、`export/` 下的运行时产出 | 日志与输出 | 候选归档 |
|
||||
| `*.lnk`、`*.rar` 文件 | 其他 | 候选删除 |
|
||||
| 空目录(如 `Deleded & backup/`) | 其他 | 候选删除 |
|
||||
| `tasks/`、`loaders/`、`scd/`、`orchestration/`、`quality/`、`models/`、`utils/`、`api/` | 核心代码 | 保留 |
|
||||
| `config/` | 配置 | 保留 |
|
||||
| `database/*.sql`、`database/migrations/` | 数据库定义 | 保留 |
|
||||
| `database/*.py` | 核心代码 | 保留 |
|
||||
| `tests/` | 测试 | 保留 |
|
||||
| `docs/` | 文档 | 保留 |
|
||||
| `scripts/` 下的 `.py` 文件 | 脚本工具 | 保留/待确认 |
|
||||
| `gui/` | GUI | 保留 |
|
||||
| `setup.py`、`build_exe.py`、`*.bat`、`*.sh`、`*.ps1` | 构建与部署 | 保留 |
|
||||
| 根目录散落文件(`Prompt用.md`、`Untitled`、`fix_symbols.py` 等) | 其他 | 待确认 |
|
||||
|
||||
### 3. 流程树分析器 (`scripts/audit/flow_analyzer.py`)
|
||||
|
||||
通过静态分析 Python 源码的 `import` 语句和类继承关系,构建从入口到末端模块的调用树。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class FlowNode:
|
||||
"""流程树节点"""
|
||||
name: str # 节点名称(模块名/类名/函数名)
|
||||
source_file: str # 所在源文件路径
|
||||
node_type: str # 类型:entry/module/class/function
|
||||
children: list["FlowNode"]
|
||||
|
||||
def parse_imports(filepath: Path) -> list[str]:
|
||||
"""使用 ast 模块解析 Python 文件的 import 语句,返回被导入的本地模块列表"""
|
||||
...
|
||||
|
||||
def build_flow_tree(repo_root: Path, entry_file: str) -> FlowNode:
|
||||
"""从指定入口文件出发,递归追踪 import 链,构建流程树"""
|
||||
...
|
||||
|
||||
def find_orphan_modules(repo_root: Path, all_entries: list[FileEntry], reachable: set[str]) -> list[str]:
|
||||
"""找出未被任何入口直接或间接引用的 Python 模块"""
|
||||
...
|
||||
|
||||
def render_flow_report(trees: list[FlowNode], orphans: list[str], repo_root: str) -> str:
|
||||
"""生成 Markdown 格式的流程树报告(含 Mermaid 图和缩进文本)"""
|
||||
...
|
||||
```
|
||||
|
||||
**入口点识别**:
|
||||
- CLI 入口:`cli/main.py` → `main()` 函数
|
||||
- GUI 入口:`gui/main.py` → `main()` 函数
|
||||
- 批处理入口:`run_etl.bat`、`run_gui.bat`、`run_ods.bat` → 解析其中的 `python` 命令
|
||||
- 运维脚本:`scripts/*.py` → 各自的 `if __name__ == "__main__"` 块
|
||||
|
||||
**静态分析策略**:
|
||||
- 使用 Python `ast` 模块解析源文件,提取 `import` 和 `from ... import` 语句
|
||||
- 仅追踪项目内部模块(排除标准库和第三方包)
|
||||
- 通过 `orchestration/task_registry.py` 的注册语句识别所有任务类及其源文件
|
||||
- 通过类继承关系(`BaseTask`、`BaseLoader`、`BaseDwsTask` 等)识别任务和加载器层级
|
||||
|
||||
### 4. 文档对齐分析器 (`scripts/audit/doc_alignment_analyzer.py`)
|
||||
|
||||
检查文档与代码之间的映射关系、过期点、冲突点和缺失点。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class DocMapping:
|
||||
"""文档与代码的映射关系"""
|
||||
doc_path: str # 文档文件路径
|
||||
doc_topic: str # 文档主题
|
||||
related_code: list[str] # 关联的代码文件/模块
|
||||
status: str # 状态:aligned/stale/conflict/orphan
|
||||
|
||||
@dataclass
|
||||
class AlignmentIssue:
|
||||
"""对齐问题"""
|
||||
doc_path: str
|
||||
issue_type: str # stale/conflict/missing
|
||||
description: str
|
||||
related_code: str
|
||||
|
||||
def scan_docs(repo_root: Path) -> list[str]:
|
||||
"""扫描所有文档文件路径"""
|
||||
...
|
||||
|
||||
def extract_code_references(doc_path: Path) -> list[str]:
|
||||
"""从文档中提取代码引用(文件路径、类名、函数名、表名等)"""
|
||||
...
|
||||
|
||||
def check_reference_validity(ref: str, repo_root: Path) -> bool:
|
||||
"""检查文档中的代码引用是否仍然有效"""
|
||||
...
|
||||
|
||||
def find_undocumented_modules(repo_root: Path, documented: set[str]) -> list[str]:
|
||||
"""找出缺少文档的核心代码模块"""
|
||||
...
|
||||
|
||||
def check_ddl_vs_dictionary(repo_root: Path) -> list[AlignmentIssue]:
|
||||
"""比对 DDL 文件与数据字典文档的覆盖度"""
|
||||
...
|
||||
|
||||
def check_api_samples_vs_parsers(repo_root: Path) -> list[AlignmentIssue]:
|
||||
"""比对 API 响应样本与 ODS 表结构/解析器的一致性"""
|
||||
...
|
||||
|
||||
def render_alignment_report(mappings: list[DocMapping], issues: list[AlignmentIssue], repo_root: str) -> str:
|
||||
"""生成 Markdown 格式的文档对齐报告"""
|
||||
...
|
||||
```
|
||||
|
||||
**文档来源识别**:
|
||||
- `docs/` 目录下的 `.md`、`.txt`、`.csv` 文件
|
||||
- 根目录的 `README.md`
|
||||
- `开发笔记/` 目录
|
||||
- 各模块内的 `README.md`(`gui/README.md`、`fetch-test/README.md`)
|
||||
- `.kiro/steering/` 下的引导文件
|
||||
- `docs/test-json-doc/` 下的 API 响应样本及分析文档
|
||||
|
||||
**对齐检查策略**:
|
||||
- 过期点检测:文档中引用的文件路径、类名、函数名在代码中已不存在
|
||||
- 冲突点检测:DDL 中的表/字段定义与数据字典文档不一致;API 样本字段与解析器不匹配
|
||||
- 缺失点检测:核心代码模块(`tasks/`、`loaders/`、`orchestration/` 等)缺少对应文档
|
||||
|
||||
### 5. 审计主入口 (`scripts/audit/run_audit.py`)
|
||||
|
||||
```python
|
||||
def run_audit(repo_root: Path | None = None) -> None:
|
||||
"""执行完整审计流程,生成三份报告到 docs/audit/"""
|
||||
...
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_audit()
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### FileEntry(文件元信息)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `rel_path` | `str` | 相对路径 |
|
||||
| `is_dir` | `bool` | 是否为目录 |
|
||||
| `size_bytes` | `int` | 文件大小 |
|
||||
| `extension` | `str` | 扩展名 |
|
||||
| `is_empty_dir` | `bool` | 是否为空目录 |
|
||||
|
||||
### InventoryItem(清单条目)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `rel_path` | `str` | 相对路径 |
|
||||
| `category` | `Category` | 用途分类 |
|
||||
| `disposition` | `Disposition` | 处置标签 |
|
||||
| `description` | `str` | 简要说明 |
|
||||
|
||||
### FlowNode(流程树节点)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `name` | `str` | 节点名称 |
|
||||
| `source_file` | `str` | 源文件路径 |
|
||||
| `node_type` | `str` | 节点类型 |
|
||||
| `children` | `list[FlowNode]` | 子节点列表 |
|
||||
|
||||
### DocMapping / AlignmentIssue(文档对齐)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `doc_path` | `str` | 文档路径 |
|
||||
| `doc_topic` / `issue_type` | `str` | 主题/问题类型 |
|
||||
| `related_code` | `list[str]` / `str` | 关联代码 |
|
||||
| `status` / `description` | `str` | 状态/描述 |
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性(Property)是指在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是连接人类可读规格说明与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: classify 完整性
|
||||
|
||||
*对于任意* `FileEntry`,`classify` 函数返回的 `InventoryItem` 的 `category` 字段应属于 `Category` 枚举,`disposition` 字段应属于 `Disposition` 枚举,且 `description` 字段为非空字符串。
|
||||
|
||||
**Validates: Requirements 1.2, 1.3**
|
||||
|
||||
### Property 2: 清单渲染完整性
|
||||
|
||||
*对于任意* `InventoryItem` 列表,`render_inventory_report` 生成的 Markdown 文本中,每个条目对应的行应包含该条目的 `rel_path`、`category.value`、`disposition.value` 和 `description` 四个字段。
|
||||
|
||||
**Validates: Requirements 1.4**
|
||||
|
||||
### Property 3: 空目录标记为候选删除
|
||||
|
||||
*对于任意* `is_empty_dir=True` 的 `FileEntry`,`classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_DELETE`。
|
||||
|
||||
**Validates: Requirements 1.5**
|
||||
|
||||
### Property 4: .lnk/.rar 文件标记为候选删除
|
||||
|
||||
*对于任意* 扩展名为 `.lnk` 或 `.rar` 的 `FileEntry`,`classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_DELETE`。
|
||||
|
||||
**Validates: Requirements 1.6**
|
||||
|
||||
### Property 5: tmp/ 下文件处置范围
|
||||
|
||||
*对于任意* `rel_path` 以 `tmp/` 开头的 `FileEntry`,`classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_DELETE` 或 `Disposition.CANDIDATE_ARCHIVE` 之一。
|
||||
|
||||
**Validates: Requirements 1.7**
|
||||
|
||||
### Property 6: 运行时产出目录标记为候选归档
|
||||
|
||||
*对于任意* `rel_path` 以 `logs/` 或 `export/` 开头且非 `__init__.py` 的 `FileEntry`,`classify` 返回的 `disposition` 应为 `Disposition.CANDIDATE_ARCHIVE`。
|
||||
|
||||
**Validates: Requirements 1.8**
|
||||
|
||||
### Property 7: 扫描器排除规则
|
||||
|
||||
*对于任意* 文件树,`scan_repo` 返回的 `FileEntry` 列表中不应包含 `rel_path` 匹配排除模式(`.git`、`__pycache__`、`.pytest_cache`)的条目。
|
||||
|
||||
**Validates: Requirements 1.1**
|
||||
|
||||
### Property 8: 清单按分类分组
|
||||
|
||||
*对于任意* `InventoryItem` 列表,`render_inventory_report` 生成的 Markdown 中,同一 `Category` 的条目应连续出现(即按分类分组排列)。
|
||||
|
||||
**Validates: Requirements 1.10**
|
||||
|
||||
### Property 9: 流程树节点 source_file 有效性
|
||||
|
||||
*对于任意* `FlowNode` 树中的节点,`source_file` 字段应为非空字符串,且对应的文件在仓库中实际存在。
|
||||
|
||||
**Validates: Requirements 2.7**
|
||||
|
||||
### Property 10: 孤立模块检测正确性
|
||||
|
||||
*对于任意* 文件集合和可达模块集合,`find_orphan_modules` 返回的孤立模块列表中的每个模块都不应出现在可达集合中,且可达集合中的每个模块都不应出现在孤立列表中。
|
||||
|
||||
**Validates: Requirements 2.8**
|
||||
|
||||
### Property 11: 过期引用检测
|
||||
|
||||
*对于任意* 文档中提取的代码引用,若该引用指向的文件路径在仓库中不存在,则 `check_reference_validity` 应返回 `False`。
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 12: 缺失文档检测
|
||||
|
||||
*对于任意* 核心代码模块集合和已文档化模块集合,`find_undocumented_modules` 返回的缺失列表应恰好等于核心模块集合与已文档化集合的差集。
|
||||
|
||||
**Validates: Requirements 3.5**
|
||||
|
||||
### Property 13: 统计摘要一致性
|
||||
|
||||
*对于任意* 报告的统计摘要,各分类/标签的计数之和应等于对应条目列表的总长度。
|
||||
|
||||
**Validates: Requirements 4.5, 4.6, 4.7**
|
||||
|
||||
### Property 14: 报告头部元信息
|
||||
|
||||
*对于任意* 报告输出,头部应包含一个符合 ISO 格式的时间戳字符串和仓库根目录路径字符串。
|
||||
|
||||
**Validates: Requirements 4.2**
|
||||
|
||||
### Property 15: 写操作仅限 docs/audit/
|
||||
|
||||
*对于任意* 审计执行过程,所有文件写操作的目标路径应以 `docs/audit/repo/` 为前缀。
|
||||
|
||||
**Validates: Requirements 5.2**
|
||||
|
||||
### Property 16: 文档对齐报告分区完整性
|
||||
|
||||
*对于任意* `render_alignment_report` 的输出,Markdown 文本应包含"映射关系"、"过期点"、"冲突点"、"缺失点"四个分区标题。
|
||||
|
||||
**Validates: Requirements 3.8**
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|---------|
|
||||
| 文件读取权限不足 | 记录警告到报告的"错误"分区,跳过该文件,继续处理 |
|
||||
| Python 源文件语法错误(`ast.parse` 失败) | 记录警告,将该文件标记为"待确认",不中断流程树构建 |
|
||||
| 文档中的代码引用格式无法解析 | 跳过该引用,不产生误报 |
|
||||
| DDL 文件 SQL 语法不规范 | 使用正则提取 `CREATE TABLE` 和列定义,容忍非标准语法 |
|
||||
| `docs/audit/repo/` 目录创建失败 | 抛出异常并终止,因为无法输出报告 |
|
||||
| 编码问题(非 UTF-8 文件) | 尝试 `utf-8` → `gbk` → `latin-1` 回退读取,记录编码警告 |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 测试框架
|
||||
|
||||
- 单元测试与属性测试均使用 `pytest`
|
||||
- 属性测试库:`hypothesis`(Python 生态最成熟的属性测试框架)
|
||||
- 测试文件位于 `tests/unit/test_audit_*.py`
|
||||
|
||||
### 单元测试
|
||||
|
||||
针对具体示例和边界情况:
|
||||
- 扫描器对实际仓库子集的遍历结果
|
||||
- classify 对已知文件路径的分类正确性(如 `tmp/hebing.py` → 临时与调试/候选删除)
|
||||
- 入口点识别对实际仓库的结果
|
||||
- DDL 与数据字典的比对结果
|
||||
- 文件读取失败时的容错行为
|
||||
- `docs/audit/repo/` 目录不存在时的自动创建
|
||||
|
||||
### 属性测试
|
||||
|
||||
每个正确性属性对应一个属性测试,使用 `hypothesis` 生成随机输入:
|
||||
|
||||
- 每个属性测试至少运行 100 次迭代
|
||||
- 每个测试用注释标注对应的设计属性编号
|
||||
- 标注格式:**Feature: repo-audit, Property {N}: {属性标题}**
|
||||
|
||||
**生成器策略**:
|
||||
- `FileEntry` 生成器:随机路径(含各种扩展名、目录层级)、随机大小、随机 is_dir/is_empty_dir
|
||||
- `InventoryItem` 生成器:随机 Category/Disposition 组合、随机描述文本
|
||||
- `FlowNode` 生成器:随机树结构(限制深度和宽度)
|
||||
- 文件树生成器:构造临时目录结构用于扫描器测试
|
||||
90
.kiro/specs/repo-audit/requirements.md
Normal file
90
.kiro/specs/repo-audit/requirements.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# 需求文档:仓库治理只读审计
|
||||
|
||||
## 简介
|
||||
|
||||
对飞球 ETL 系统 (etl-billiards) 仓库进行全面的只读审计分析,产出三份结构化报告:文件/目录清单(含处置建议)、项目流程树(从入口到末端逻辑)、文档对齐报告(文档与代码的映射关系)。本阶段不修改任何文件,所有处置决策留待用户逐一确认后再执行。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **审计脚本 (Audit_Script)**:执行只读分析并生成报告的 Python 脚本集合
|
||||
- **文件清单 (File_Inventory)**:按用途归类的仓库文件与目录列表,每项附带处置标签
|
||||
- **处置标签 (Disposition_Tag)**:对文件/目录的处置建议,取值为:保留、候选删除、候选归档、待确认
|
||||
- **流程树 (Flow_Tree)**:从程序入口出发,沿调用链展开到各子模块/子逻辑的树状结构
|
||||
- **文档对齐报告 (Doc_Alignment_Report)**:文档与代码之间映射关系的分析报告,包含过期点、冲突点、缺失点
|
||||
- **入口 (Entry_Point)**:程序的顶层启动点,如 `cli/main.py`、`gui/main.py`、`scripts/*.py`
|
||||
- **ODS/DWD/DWS**:数据仓库三层架构——操作数据存储层/明细数据层/数据服务层
|
||||
- **SCD2**:缓慢变化维度类型 2,维度表的历史版本管理策略
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:文件与目录清单生成
|
||||
|
||||
**用户故事:** 作为项目维护者,我希望获得一份按用途归类的仓库文件与目录清单,以便了解每个文件的角色并决定其去留。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 审计脚本扫描仓库根目录时,THE Audit_Script SHALL 递归遍历所有文件和目录(排除 `.git/`、`__pycache__/`、`.pytest_cache/` 等运行时缓存目录)
|
||||
2. WHEN 审计脚本处理每个文件或目录时,THE Audit_Script SHALL 将其归入以下用途分类之一:核心代码、配置、数据库定义、测试、文档、脚本工具、GUI、构建与部署、日志与输出、临时与调试、其他
|
||||
3. WHEN 审计脚本完成归类后,THE Audit_Script SHALL 为每个条目分配一个处置标签(保留/候选删除/候选归档/待确认)
|
||||
4. WHEN 审计脚本生成清单时,THE File_Inventory SHALL 包含以下字段:相对路径、用途分类、处置标签、简要说明
|
||||
5. WHEN 审计脚本遇到空目录(如 `database/Deleded & backup/`、`scripts/Deleded & backup/`)时,THE Audit_Script SHALL 将其标记为"候选删除"
|
||||
6. WHEN 审计脚本遇到 `.lnk` 快捷方式文件或 `.rar` 压缩包时,THE Audit_Script SHALL 将其标记为"候选删除"
|
||||
7. WHEN 审计脚本遇到 `tmp/` 目录下的文件时,THE Audit_Script SHALL 逐一评估并标记为"候选删除"或"候选归档"
|
||||
8. WHEN 审计脚本遇到 `logs/`、`export/` 目录下的运行时产出文件时,THE Audit_Script SHALL 将其标记为"候选归档"
|
||||
9. IF 审计脚本无法确定某文件的用途分类,THEN THE Audit_Script SHALL 将其标记为"待确认"并在说明中注明原因
|
||||
10. WHEN 审计脚本完成清单生成后,THE File_Inventory SHALL 以 Markdown 表格格式输出,按用途分类分组排列
|
||||
|
||||
### 需求 2:项目流程树生成
|
||||
|
||||
**用户故事:** 作为项目维护者,我希望获得一份从入口到各子模块的调用流程树,以便理解系统的执行路径和模块依赖关系。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 审计脚本分析项目入口时,THE Audit_Script SHALL 识别以下入口点:`cli/main.py`(CLI 主入口)、`gui/main.py`(GUI 主入口)、`scripts/*.py`(运维脚本)、批处理文件(`run_etl.bat`、`run_gui.bat`、`run_ods.bat` 等)
|
||||
2. WHEN 审计脚本从 CLI 入口展开时,THE Flow_Tree SHALL 追踪以下调用链:CLI 参数解析 → 配置加载 → 调度器初始化 → 任务注册表查询 → 任务执行(Extract → Transform → Load)→ 加载器调用 → 数据库操作
|
||||
3. WHEN 审计脚本从 GUI 入口展开时,THE Flow_Tree SHALL 追踪以下调用链:GUI 主窗口初始化 → 各面板/组件加载 → 后台工作线程 → CLI 命令构建 → 任务执行
|
||||
4. WHEN 审计脚本分析任务模块时,THE Flow_Tree SHALL 区分以下任务类型:ODS 抓取任务、DWD 加载任务、DWS 汇总任务、校验任务、Schema 初始化任务
|
||||
5. WHEN 审计脚本分析加载器模块时,THE Flow_Tree SHALL 区分以下加载器类型:ODS 通用加载器、维度加载器(SCD2)、事实表加载器
|
||||
6. WHEN 审计脚本生成流程树时,THE Flow_Tree SHALL 以缩进文本或 Mermaid 图的形式输出,层级深度至少达到函数/方法级别
|
||||
7. WHEN 审计脚本分析模块依赖时,THE Flow_Tree SHALL 标注每个节点所在的源文件路径
|
||||
8. IF 审计脚本发现存在孤立模块(未被任何入口直接或间接引用的代码文件),THEN THE Flow_Tree SHALL 在报告末尾单独列出这些孤立模块
|
||||
|
||||
### 需求 3:文档对齐报告生成
|
||||
|
||||
**用户故事:** 作为项目维护者,我希望了解现有文档与代码之间的对齐状况,以便识别过期、冲突和缺失的文档。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 审计脚本扫描文档目录时,THE Audit_Script SHALL 识别以下文档来源:`docs/` 目录、`README.md`、`开发笔记/`、各模块内的 `README.md`(如 `gui/README.md`、`fetch-test/README.md`)、`.kiro/steering/` 下的引导文件
|
||||
2. WHEN 审计脚本分析每份文档时,THE Doc_Alignment_Report SHALL 建立文档与代码模块之间的映射关系
|
||||
3. WHEN 审计脚本检测到文档引用了已不存在的代码实体(函数、类、文件路径)时,THE Doc_Alignment_Report SHALL 将该引用标记为"过期点"
|
||||
4. WHEN 审计脚本检测到文档描述与代码实际行为不一致时,THE Doc_Alignment_Report SHALL 将该处标记为"冲突点"
|
||||
5. WHEN 审计脚本检测到核心代码模块缺少对应文档时,THE Doc_Alignment_Report SHALL 将该模块标记为"缺失点"
|
||||
6. WHEN 审计脚本分析 DDL 文件(`database/schema_*.sql`)时,THE Doc_Alignment_Report SHALL 检查数据字典文档(`docs/dwd_main_tables_dictionary.md`、`docs/dws_tables_dictionary.md`)是否覆盖了所有表和字段
|
||||
7. WHEN 审计脚本分析 `docs/test-json-doc/` 下的 API 响应样本时,THE Doc_Alignment_Report SHALL 检查样本字段是否与 ODS 表结构和解析器(`models/parsers.py`)一致
|
||||
8. WHEN 审计脚本完成分析后,THE Doc_Alignment_Report SHALL 以 Markdown 格式输出,包含以下分区:映射关系表、过期点列表、冲突点列表、缺失点列表
|
||||
|
||||
### 需求 4:报告输出与格式规范
|
||||
|
||||
**用户故事:** 作为项目维护者,我希望审计报告以统一、可读的格式输出,以便后续逐项决策和执行。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Audit_Script SHALL 将三份报告输出到 `docs/audit/repo/` 目录下,文件名分别为 `file_inventory.md`、`flow_tree.md`、`doc_alignment.md`
|
||||
2. THE Audit_Script SHALL 在每份报告的头部包含生成时间戳和仓库根目录路径
|
||||
3. WHEN 报告引用代码标识符(类名、函数名、变量名、文件路径)时,THE Audit_Script SHALL 保留英文原文,使用行内代码格式(反引号)
|
||||
4. WHEN 报告包含说明性文字时,THE Audit_Script SHALL 使用简体中文
|
||||
5. THE Audit_Script SHALL 在文件清单报告末尾附加统计摘要:各用途分类的文件数量、各处置标签的文件数量
|
||||
6. THE Audit_Script SHALL 在流程树报告末尾附加统计摘要:入口点数量、任务数量、加载器数量、孤立模块数量
|
||||
7. THE Audit_Script SHALL 在文档对齐报告末尾附加统计摘要:过期点数量、冲突点数量、缺失点数量
|
||||
|
||||
### 需求 5:只读安全保障
|
||||
|
||||
**用户故事:** 作为项目维护者,我希望审计过程不会修改仓库中的任何文件,以确保分析阶段的安全性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Audit_Script SHALL 仅执行文件系统的读取操作(读取文件内容、列出目录、获取文件元信息)
|
||||
2. THE Audit_Script SHALL 仅在 `docs/audit/repo/` 目录下创建新文件,该目录为报告专用输出目录
|
||||
3. IF 审计脚本在执行过程中遇到权限错误或文件读取失败,THEN THE Audit_Script SHALL 在报告中记录该错误并继续处理其余文件
|
||||
4. THE Audit_Script SHALL 在运行前检查 `docs/audit/repo/` 目录是否存在,若不存在则创建该目录
|
||||
118
.kiro/specs/repo-audit/tasks.md
Normal file
118
.kiro/specs/repo-audit/tasks.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 实施计划:仓库治理只读审计
|
||||
|
||||
## 概述
|
||||
|
||||
将设计文档中的审计脚本拆分为增量式编码任务。每个任务构建在前一个任务之上,最终产出可运行的审计工具集。所有脚本位于 `scripts/audit/` 目录,报告输出到 `docs/audit/repo/`。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 搭建审计脚本骨架和数据模型
|
||||
- [x] 1.1 创建 `scripts/audit/__init__.py` 和数据模型定义
|
||||
- 定义 `FileEntry` dataclass(`rel_path`, `is_dir`, `size_bytes`, `extension`, `is_empty_dir`)
|
||||
- 定义 `Category` 和 `Disposition` 枚举
|
||||
- 定义 `InventoryItem` dataclass
|
||||
- 定义 `FlowNode` dataclass
|
||||
- 定义 `DocMapping` 和 `AlignmentIssue` dataclass
|
||||
- _Requirements: 1.2, 1.3, 1.4, 2.7, 3.2, 3.3_
|
||||
|
||||
- [x] 1.2 编写 classify 完整性属性测试
|
||||
- **Property 1: classify 完整性**
|
||||
- **Validates: Requirements 1.2, 1.3**
|
||||
|
||||
- [x] 2. 实现仓库扫描器
|
||||
- [x] 2.1 创建 `scripts/audit/scanner.py`
|
||||
- 实现 `EXCLUDED_PATTERNS` 常量和排除匹配逻辑
|
||||
- 实现 `scan_repo(root, exclude)` 函数:递归遍历文件系统,返回 `list[FileEntry]`
|
||||
- 处理空目录检测(`is_empty_dir`)
|
||||
- 处理文件读取权限错误(跳过并记录)
|
||||
- _Requirements: 1.1, 5.1, 5.3_
|
||||
|
||||
- [x] 2.2 编写扫描器排除规则属性测试
|
||||
- **Property 7: 扫描器排除规则**
|
||||
- **Validates: Requirements 1.1**
|
||||
|
||||
- [x] 3. 实现文件清单分析器
|
||||
- [x] 3.1 创建 `scripts/audit/inventory_analyzer.py`
|
||||
- 实现 `classify(entry: FileEntry) -> InventoryItem` 函数,包含完整分类规则表
|
||||
- 实现 `build_inventory(entries) -> list[InventoryItem]` 批量分类函数
|
||||
- 实现 `render_inventory_report(items, repo_root) -> str` Markdown 渲染函数
|
||||
- 包含统计摘要生成(各分类/标签计数)
|
||||
- 注意:需求 1.8 仅覆盖 `logs/` 和 `export/` 目录(不含 `reports/`)
|
||||
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 4.2, 4.5_
|
||||
|
||||
- [x] 3.2 编写 classify 分类规则属性测试
|
||||
- **Property 3: 空目录标记为候选删除**
|
||||
- **Property 4: .lnk/.rar 文件标记为候选删除**
|
||||
- **Property 5: tmp/ 下文件处置范围**
|
||||
- **Property 6: 运行时产出目录标记为候选归档**(仅 `logs/`、`export/`)
|
||||
- **Validates: Requirements 1.5, 1.6, 1.7, 1.8**
|
||||
|
||||
- [x] 3.3 编写清单渲染属性测试
|
||||
- **Property 2: 清单渲染完整性**
|
||||
- **Property 8: 清单按分类分组**
|
||||
- **Validates: Requirements 1.4, 1.10**
|
||||
|
||||
- [x] 4. 检查点 - 确保文件清单模块测试通过
|
||||
- 确保所有测试通过,如有疑问请向用户确认。
|
||||
|
||||
- [x] 5. 实现流程树分析器
|
||||
- [x] 5.1 创建 `scripts/audit/flow_analyzer.py`
|
||||
- 实现 `parse_imports(filepath)` 函数:使用 `ast` 模块解析 Python 文件的 import 语句
|
||||
- 实现 `build_flow_tree(repo_root, entry_file)` 函数:从入口递归追踪 import 链
|
||||
- 实现 `find_orphan_modules(repo_root, all_entries, reachable)` 函数
|
||||
- 实现 `render_flow_report(trees, orphans, repo_root)` 函数:生成 Mermaid 图和缩进文本
|
||||
- 包含入口点识别逻辑(CLI、GUI、批处理、运维脚本)
|
||||
- 包含任务类型和加载器类型区分逻辑
|
||||
- 包含统计摘要生成
|
||||
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 4.6_
|
||||
|
||||
- [x] 5.2 编写流程树属性测试
|
||||
- **Property 9: 流程树节点 source_file 有效性**
|
||||
- **Property 10: 孤立模块检测正确性**
|
||||
- **Validates: Requirements 2.7, 2.8**
|
||||
|
||||
- [x] 6. 实现文档对齐分析器
|
||||
- [x] 6.1 创建 `scripts/audit/doc_alignment_analyzer.py`
|
||||
- 实现 `scan_docs(repo_root)` 函数:扫描所有文档来源
|
||||
- 实现 `extract_code_references(doc_path)` 函数:从文档提取代码引用
|
||||
- 实现 `check_reference_validity(ref, repo_root)` 函数
|
||||
- 实现 `find_undocumented_modules(repo_root, documented)` 函数
|
||||
- 实现 `check_ddl_vs_dictionary(repo_root)` 函数:DDL 与数据字典比对
|
||||
- 实现 `check_api_samples_vs_parsers(repo_root)` 函数:API 样本与解析器比对
|
||||
- 实现 `render_alignment_report(mappings, issues, repo_root)` 函数
|
||||
- 包含统计摘要生成
|
||||
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 4.7_
|
||||
|
||||
- [x] 6.2 编写文档对齐属性测试
|
||||
- **Property 11: 过期引用检测**
|
||||
- **Property 12: 缺失文档检测**
|
||||
- **Property 16: 文档对齐报告分区完整性**
|
||||
- **Validates: Requirements 3.3, 3.5, 3.8**
|
||||
|
||||
- [x] 7. 检查点 - 确保流程树和文档对齐模块测试通过
|
||||
- 确保所有测试通过,如有疑问请向用户确认。
|
||||
|
||||
- [x] 8. 实现审计主入口和报告输出
|
||||
- [x] 8.1 创建 `scripts/audit/run_audit.py`
|
||||
- 实现 `run_audit(repo_root)` 主函数:依次调用扫描器和三个分析器
|
||||
- 实现 `docs/audit/repo/` 目录检查与创建逻辑
|
||||
- 实现报告头部元信息(时间戳、仓库路径)注入
|
||||
- 实现三份报告的文件写入
|
||||
- 添加 `if __name__ == "__main__"` 入口
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 5.2, 5.4_
|
||||
|
||||
- [x] 8.2 编写报告输出属性测试
|
||||
- **Property 13: 统计摘要一致性**
|
||||
- **Property 14: 报告头部元信息**
|
||||
- **Property 15: 写操作仅限 docs/audit/**
|
||||
- **Validates: Requirements 4.2, 4.5, 4.6, 4.7, 5.2**
|
||||
|
||||
- [x] 9. 最终检查点 - 确保所有测试通过
|
||||
- 确保所有测试通过,如有疑问请向用户确认。
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选,可跳过以加速 MVP 交付
|
||||
- 每个任务引用了具体的需求编号,便于追溯
|
||||
- 属性测试使用 `hypothesis` 库,每个测试至少 100 次迭代
|
||||
- 单元测试验证具体示例和边界情况,属性测试验证通用正确性
|
||||
1
.kiro/specs/scheduler-refactor/.config.kiro
Normal file
1
.kiro/specs/scheduler-refactor/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
462
.kiro/specs/scheduler-refactor/design.md
Normal file
462
.kiro/specs/scheduler-refactor/design.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# 设计文档:ETL 调度器重构
|
||||
|
||||
## 概述
|
||||
|
||||
本次重构将 `ETLScheduler`(约 900 行,职责混乱的"上帝类")拆分为三层清晰的架构:
|
||||
|
||||
1. **CLI 层**(`cli/main.py`):参数解析、配置加载、资源创建与释放
|
||||
2. **PipelineRunner**(`orchestration/pipeline_runner.py`):管道定义、层→任务映射、校验编排
|
||||
3. **TaskExecutor**(`orchestration/task_executor.py`):单任务执行、游标管理、运行记录
|
||||
|
||||
核心设计原则:**单个任务是最小执行单元,管道模式只是"调度拼接"**。每层通过依赖注入接收协作对象,不自行创建资源,便于独立测试。
|
||||
|
||||
## 架构
|
||||
|
||||
### 分层架构图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
CLI["CLI 层<br/>cli/main.py<br/>参数解析 · 配置加载 · 资源管理"]
|
||||
PR["PipelineRunner<br/>orchestration/pipeline_runner.py<br/>管道定义 · 层→任务映射 · 校验编排"]
|
||||
TE["TaskExecutor<br/>orchestration/task_executor.py<br/>单任务执行 · 游标管理 · 运行记录"]
|
||||
TR["TaskRegistry<br/>orchestration/task_registry.py<br/>任务注册 · 元数据查询"]
|
||||
CM["CursorManager"]
|
||||
RT["RunTracker"]
|
||||
DB["DatabaseConnection"]
|
||||
API["APIClient"]
|
||||
|
||||
CLI -->|"创建并注入"| PR
|
||||
CLI -->|"创建并注入"| TE
|
||||
CLI -->|"管理生命周期"| DB
|
||||
CLI -->|"管理生命周期"| API
|
||||
PR -->|"委托执行"| TE
|
||||
PR -->|"查询任务"| TR
|
||||
TE -->|"查询元数据"| TR
|
||||
TE -->|"管理游标"| CM
|
||||
TE -->|"记录运行"| RT
|
||||
TE -->|"使用"| DB
|
||||
TE -->|"使用"| API
|
||||
```
|
||||
|
||||
### 调用流程
|
||||
|
||||
**传统模式**(`--tasks`):
|
||||
```
|
||||
CLI → TaskExecutor.run_tasks([task_codes]) → TaskExecutor._run_single_task() × N
|
||||
```
|
||||
|
||||
**管道模式**(`--pipeline`):
|
||||
```
|
||||
CLI → PipelineRunner.run(pipeline, processing_mode, ...)
|
||||
→ PipelineRunner._resolve_tasks(layers)
|
||||
→ TaskExecutor.run_tasks([resolved_tasks])
|
||||
→ [可选] PipelineRunner._run_verification(layers, ...)
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### TaskExecutor
|
||||
|
||||
负责单任务执行的完整生命周期。从原 `ETLScheduler` 中提取 `_run_single_task`、`_execute_fetch`、`_execute_ingest`、`_execute_ods_record_and_load`、`_run_utility_task` 等方法。
|
||||
|
||||
```python
|
||||
class TaskExecutor:
|
||||
def __init__(
|
||||
self,
|
||||
config: AppConfig,
|
||||
db_ops: DatabaseOperations,
|
||||
api_client: APIClient,
|
||||
cursor_mgr: CursorManager,
|
||||
run_tracker: RunTracker,
|
||||
task_registry: TaskRegistry,
|
||||
logger: logging.Logger,
|
||||
):
|
||||
...
|
||||
|
||||
def run_tasks(
|
||||
self,
|
||||
task_codes: list[str],
|
||||
data_source: str = "hybrid", # online / offline / hybrid
|
||||
) -> list[dict[str, Any]]:
|
||||
"""批量执行任务列表,返回每个任务的结果。"""
|
||||
...
|
||||
|
||||
def run_single_task(
|
||||
self,
|
||||
task_code: str,
|
||||
run_uuid: str,
|
||||
store_id: int,
|
||||
data_source: str = "hybrid",
|
||||
) -> dict[str, Any]:
|
||||
"""执行单个任务的完整生命周期。"""
|
||||
...
|
||||
```
|
||||
|
||||
关键变化:
|
||||
- `data_source` 作为显式参数传入,不再读取 `self.pipeline_flow` 全局状态
|
||||
- 工具类任务判断通过 `TaskRegistry.get_metadata(task_code)` 查询,不再硬编码
|
||||
- 不自行创建 `DatabaseConnection` 或 `APIClient`
|
||||
|
||||
### PipelineRunner
|
||||
|
||||
负责管道编排。从原 `ETLScheduler` 中提取 `run_pipeline_with_verification`、`_run_layer_verification`、`_get_tasks_for_layers` 等方法。
|
||||
|
||||
```python
|
||||
class PipelineRunner:
|
||||
# 管道定义(从 scheduler.py 模块级常量迁移至此)
|
||||
PIPELINE_LAYERS: dict[str, list[str]] = {
|
||||
"api_ods": ["ODS"],
|
||||
"api_ods_dwd": ["ODS", "DWD"],
|
||||
"api_full": ["ODS", "DWD", "DWS", "INDEX"],
|
||||
"ods_dwd": ["DWD"],
|
||||
"dwd_dws": ["DWS"],
|
||||
"dwd_dws_index": ["DWS", "INDEX"],
|
||||
"dwd_index": ["INDEX"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: AppConfig,
|
||||
task_executor: TaskExecutor,
|
||||
task_registry: TaskRegistry,
|
||||
db_conn: DatabaseConnection,
|
||||
api_client: APIClient,
|
||||
logger: logging.Logger,
|
||||
):
|
||||
...
|
||||
|
||||
def run(
|
||||
self,
|
||||
pipeline: str,
|
||||
processing_mode: str = "increment_only",
|
||||
data_source: str = "hybrid",
|
||||
window_start: datetime | None = None,
|
||||
window_end: datetime | None = None,
|
||||
window_split: str | None = None,
|
||||
task_codes: list[str] | None = None,
|
||||
fetch_before_verify: bool = False,
|
||||
verify_tables: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""执行管道,返回汇总结果。"""
|
||||
...
|
||||
|
||||
def _resolve_tasks(self, layers: list[str]) -> list[str]:
|
||||
"""根据层列表解析任务代码,优先查询 TaskRegistry 元数据。"""
|
||||
...
|
||||
|
||||
def _run_verification(self, layers, window_start, window_end, ...):
|
||||
"""执行后置校验(从原 _run_layer_verification 迁移)。"""
|
||||
...
|
||||
```
|
||||
|
||||
### TaskRegistry(增强)
|
||||
|
||||
在现有注册功能基础上增加元数据支持。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TaskMeta:
|
||||
"""任务元数据"""
|
||||
task_class: type
|
||||
requires_db_config: bool = True
|
||||
layer: str | None = None # "ODS" / "DWD" / "DWS" / "INDEX" / None
|
||||
task_type: str = "etl" # "etl" / "utility" / "verification"
|
||||
|
||||
class TaskRegistry:
|
||||
def __init__(self):
|
||||
self._tasks: dict[str, TaskMeta] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
task_code: str,
|
||||
task_class: type,
|
||||
requires_db_config: bool = True,
|
||||
layer: str | None = None,
|
||||
task_type: str = "etl",
|
||||
):
|
||||
"""注册任务类及其元数据。"""
|
||||
self._tasks[task_code.upper()] = TaskMeta(
|
||||
task_class=task_class,
|
||||
requires_db_config=requires_db_config,
|
||||
layer=layer,
|
||||
task_type=task_type,
|
||||
)
|
||||
|
||||
def create_task(self, task_code, config, db_connection, api_client, logger):
|
||||
"""创建任务实例(保持原有接口不变)。"""
|
||||
...
|
||||
|
||||
def get_metadata(self, task_code: str) -> TaskMeta | None:
|
||||
"""查询任务元数据。"""
|
||||
...
|
||||
|
||||
def get_tasks_by_layer(self, layer: str) -> list[str]:
|
||||
"""获取指定层的所有任务代码。"""
|
||||
...
|
||||
|
||||
def is_utility_task(self, task_code: str) -> bool:
|
||||
"""判断是否为工具类任务(不需要游标/运行记录)。"""
|
||||
meta = self.get_metadata(task_code)
|
||||
return meta is not None and not meta.requires_db_config
|
||||
|
||||
def get_all_task_codes(self) -> list[str]:
|
||||
"""获取所有已注册的任务代码(保持原有接口)。"""
|
||||
...
|
||||
```
|
||||
|
||||
### CLI 层重构
|
||||
|
||||
```python
|
||||
# cli/main.py 核心流程伪代码
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config = AppConfig.load(build_cli_overrides(args))
|
||||
|
||||
# 资源创建
|
||||
db_conn = DatabaseConnection(...)
|
||||
api_client = APIClient(...)
|
||||
|
||||
try:
|
||||
# 组装依赖
|
||||
db_ops = DatabaseOperations(db_conn)
|
||||
cursor_mgr = CursorManager(db_conn)
|
||||
run_tracker = RunTracker(db_conn)
|
||||
registry = default_registry
|
||||
|
||||
executor = TaskExecutor(config, db_ops, api_client, cursor_mgr, run_tracker, registry, logger)
|
||||
|
||||
if args.pipeline:
|
||||
runner = PipelineRunner(config, executor, registry, db_conn, api_client, logger)
|
||||
runner.run(
|
||||
pipeline=args.pipeline,
|
||||
processing_mode=args.processing_mode,
|
||||
data_source=resolve_data_source(args),
|
||||
...
|
||||
)
|
||||
else:
|
||||
task_codes = config.get("run.tasks")
|
||||
data_source = resolve_data_source(args)
|
||||
executor.run_tasks(task_codes, data_source=data_source)
|
||||
finally:
|
||||
db_conn.close()
|
||||
```
|
||||
|
||||
### 参数映射
|
||||
|
||||
| 旧参数 | 旧值 | 新参数 | 新值 | 说明 |
|
||||
|--------|------|--------|------|------|
|
||||
| `--pipeline-flow` | `FULL` | `--data-source` | `hybrid` | 在线抓取 + 本地入库 |
|
||||
| `--pipeline-flow` | `FETCH_ONLY` | `--data-source` | `online` | 仅在线抓取落盘 |
|
||||
| `--pipeline-flow` | `INGEST_ONLY` | `--data-source` | `offline` | 仅本地清洗入库 |
|
||||
|
||||
### 静态方法归位
|
||||
|
||||
| 方法 | 原位置 | 新位置 | 理由 |
|
||||
|------|--------|--------|------|
|
||||
| `_map_run_status` | `ETLScheduler` | `RunTracker` | 状态映射是运行记录的职责 |
|
||||
| `_filter_verify_tables` | `ETLScheduler` | `tasks/verification/` 模块 | 校验表过滤是校验模块的职责 |
|
||||
|
||||
## 数据模型
|
||||
|
||||
### TaskMeta(新增)
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TaskMeta:
|
||||
task_class: type # 任务类引用
|
||||
requires_db_config: bool = True # 是否需要数据库任务配置(游标/运行记录)
|
||||
layer: str | None = None # 所属层:"ODS"/"DWD"/"DWS"/"INDEX"/None
|
||||
task_type: str = "etl" # 任务类型:"etl"/"utility"/"verification"
|
||||
```
|
||||
|
||||
### DataSource 枚举
|
||||
|
||||
```python
|
||||
class DataSource(str, Enum):
|
||||
ONLINE = "online" # 仅在线抓取(原 FETCH_ONLY)
|
||||
OFFLINE = "offline" # 仅本地入库(原 INGEST_ONLY)
|
||||
HYBRID = "hybrid" # 抓取 + 入库(原 FULL)
|
||||
```
|
||||
|
||||
### 配置键映射
|
||||
|
||||
| 旧键 | 新键 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `app.timezone` | `app.timezone` | `Asia/Shanghai`(原 `Asia/Shanghai`) |
|
||||
| `pipeline.flow` | `run.data_source` | `hybrid` |
|
||||
| `pipeline.fetch_root` | `io.fetch_root` | `export/JSON` |
|
||||
| `pipeline.ingest_source_dir` | `io.ingest_source_dir` | `""` |
|
||||
|
||||
### 任务执行结果(不变)
|
||||
|
||||
```python
|
||||
# 单任务结果
|
||||
{
|
||||
"task_code": str,
|
||||
"status": str, # "SUCCESS" / "FAIL" / "SKIP"
|
||||
"counts": {
|
||||
"fetched": int,
|
||||
"inserted": int,
|
||||
"updated": int,
|
||||
"skipped": int,
|
||||
"errors": int,
|
||||
},
|
||||
"window": {"start": datetime, "end": datetime, "minutes": int} | None,
|
||||
"dump_dir": str | None,
|
||||
}
|
||||
|
||||
# 管道结果
|
||||
{
|
||||
"status": str,
|
||||
"pipeline": str,
|
||||
"layers": list[str],
|
||||
"results": list[dict], # 各任务结果
|
||||
"verification_summary": dict | None, # 校验汇总
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1:data_source 参数决定执行路径
|
||||
|
||||
*对于任意* 任务代码和任意 `data_source` 值(online/offline/hybrid),TaskExecutor 执行该任务时,抓取阶段执行当且仅当 `data_source` 为 `online` 或 `hybrid`,入库阶段执行当且仅当 `data_source` 为 `offline` 或 `hybrid`。
|
||||
|
||||
**验证:需求 1.2**
|
||||
|
||||
### Property 2:成功任务推进游标
|
||||
|
||||
*对于任意* 非工具类任务,当任务执行成功且返回包含有效 `window`(含 `start` 和 `end`)的结果时,CursorManager.advance 应被调用且参数与返回的窗口一致。
|
||||
|
||||
**验证:需求 1.3**
|
||||
|
||||
### Property 3:失败任务标记 FAIL 并重新抛出
|
||||
|
||||
*对于任意* 非工具类任务,当任务执行过程中抛出异常时,RunTracker 应被更新为 FAIL 状态,且该异常应被重新抛出给调用方。
|
||||
|
||||
**验证:需求 1.4**
|
||||
|
||||
### Property 4:工具类任务由元数据决定
|
||||
|
||||
*对于任意* 任务代码,TaskExecutor 是否跳过游标管理和运行记录,取决于 TaskRegistry 中该任务的 `requires_db_config` 元数据。当 `requires_db_config=False` 时跳过,否则执行完整生命周期。
|
||||
|
||||
**验证:需求 1.6, 4.2**
|
||||
|
||||
### Property 5:管道名称→层列表映射
|
||||
|
||||
*对于任意* 有效的管道名称,PipelineRunner 解析出的层列表应与 `PIPELINE_LAYERS` 字典中的定义完全一致。
|
||||
|
||||
**验证:需求 2.1**
|
||||
|
||||
### Property 6:processing_mode 控制执行流程
|
||||
|
||||
*对于任意* processing_mode 值,增量 ETL 执行当且仅当模式包含 `increment`(即 `increment_only` 或 `increment_verify`),校验流程执行当且仅当模式包含 `verify`(即 `verify_only` 或 `increment_verify`)。
|
||||
|
||||
**验证:需求 2.3, 2.4**
|
||||
|
||||
### Property 7:管道结果汇总完整性
|
||||
|
||||
*对于任意* 一组任务执行结果,PipelineRunner 返回的汇总字典应包含 `status`、`pipeline`、`layers`、`results` 字段,且 `results` 列表长度等于实际执行的任务数。
|
||||
|
||||
**验证:需求 2.6**
|
||||
|
||||
### Property 8:TaskRegistry 元数据 round-trip
|
||||
|
||||
*对于任意* 任务代码、任务类和元数据组合(requires_db_config、layer、task_type),注册后通过 `get_metadata` 查询应返回相同的元数据值。
|
||||
|
||||
**验证:需求 4.1**
|
||||
|
||||
### Property 9:TaskRegistry 向后兼容默认值
|
||||
|
||||
*对于任意* 使用旧接口(仅 task_code 和 task_class)注册的任务,查询元数据应返回 `requires_db_config=True`、`layer=None`、`task_type="etl"`。
|
||||
|
||||
**验证:需求 4.4**
|
||||
|
||||
### Property 10:按层查询任务
|
||||
|
||||
*对于任意* 注册了 `layer` 元数据的任务集合,`get_tasks_by_layer(layer)` 返回的任务代码集合应等于所有 `layer` 匹配的已注册任务代码集合。
|
||||
|
||||
**验证:需求 4.3**
|
||||
|
||||
### Property 11:pipeline_flow → data_source 映射一致性
|
||||
|
||||
*对于任意* 旧 `pipeline_flow` 值(FULL/FETCH_ONLY/INGEST_ONLY),映射到 `data_source` 的结果应与预定义映射表一致:FULL→hybrid、FETCH_ONLY→online、INGEST_ONLY→offline。同样,配置键 `pipeline.flow` 应自动映射到 `run.data_source`。
|
||||
|
||||
**验证:需求 8.1, 8.2, 8.3, 5.2, 8.4**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### TaskExecutor 错误处理
|
||||
|
||||
- 任务执行异常:更新 RunTracker 状态为 FAIL(含 error_message),然后重新抛出异常
|
||||
- 游标推进失败:记录错误日志,不影响任务结果(任务本身已成功)
|
||||
- 任务配置不存在:返回 `{"status": "SKIP"}` 结果,不抛异常
|
||||
|
||||
### PipelineRunner 错误处理
|
||||
|
||||
- 单个任务失败:记录错误,继续执行后续任务(与当前行为一致)
|
||||
- 校验框架未安装:返回 `{"status": "SKIPPED"}` 并记录警告
|
||||
- 无效管道名称:抛出 `ValueError`
|
||||
|
||||
### CLI 错误处理
|
||||
|
||||
- 配置加载失败:`SystemExit` 并输出错误信息
|
||||
- 资源创建失败:`SystemExit` 并输出错误信息
|
||||
- 执行过程异常:记录错误日志,`finally` 块确保资源释放,返回非零退出码
|
||||
|
||||
### 弃用警告
|
||||
|
||||
- 使用 Python `warnings.warn(DeprecationWarning)` 发出弃用警告
|
||||
- 同时在日志中记录映射详情,便于运维排查
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
|
||||
使用 `pytest` + 现有的 `FakeDB`/`FakeAPI` 测试工具(`tests/unit/task_test_utils.py`)。
|
||||
|
||||
**TaskExecutor 测试**:
|
||||
- 注入 mock 依赖(FakeDB、FakeAPI、mock CursorManager、mock RunTracker)
|
||||
- 验证成功/失败/跳过三种路径
|
||||
- 验证工具类任务不触发游标/运行记录
|
||||
- 验证 data_source 参数正确控制抓取/入库阶段
|
||||
|
||||
**PipelineRunner 测试**:
|
||||
- 注入 mock TaskExecutor
|
||||
- 验证不同 processing_mode 下的执行流程
|
||||
- 验证管道→层→任务的解析链
|
||||
|
||||
**TaskRegistry 测试**:
|
||||
- 验证元数据注册和查询
|
||||
- 验证向后兼容(无元数据注册)
|
||||
- 验证按层查询
|
||||
|
||||
**配置兼容性测试**:
|
||||
- 验证旧键→新键映射
|
||||
- 验证优先级规则
|
||||
- 验证默认值变更
|
||||
|
||||
### 属性测试
|
||||
|
||||
使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
|
||||
|
||||
每个属性测试必须用注释标注对应的设计属性编号:
|
||||
```python
|
||||
# Feature: scheduler-refactor, Property 8: TaskRegistry 元数据 round-trip
|
||||
```
|
||||
|
||||
**属性测试覆盖**:
|
||||
- Property 1: data_source 参数决定执行路径
|
||||
- Property 2: 成功任务推进游标
|
||||
- Property 3: 失败任务标记 FAIL 并重新抛出
|
||||
- Property 4: 工具类任务由元数据决定
|
||||
- Property 5: 管道名称→层列表映射
|
||||
- Property 6: processing_mode 控制执行流程
|
||||
- Property 7: 管道结果汇总完整性
|
||||
- Property 8: TaskRegistry 元数据 round-trip
|
||||
- Property 9: TaskRegistry 向后兼容默认值
|
||||
- Property 10: 按层查询任务
|
||||
- Property 11: pipeline_flow → data_source 映射一致性
|
||||
123
.kiro/specs/scheduler-refactor/requirements.md
Normal file
123
.kiro/specs/scheduler-refactor/requirements.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 需求文档:ETL 调度器重构
|
||||
|
||||
## 简介
|
||||
|
||||
当前 `orchestration/scheduler.py`(约 900 行)中的 `ETLScheduler` 类承担了过多职责:单任务执行、管道编排、资源管理。CLI 参数命名混乱(`--pipeline` vs `--pipeline-flow` vs `--processing-mode`),全局状态耦合严重,配置键语义重叠。本次重构将调度器拆分为三层架构(CLI → PipelineRunner → TaskExecutor),重新设计参数命名,消除全局状态依赖,使每层可独立测试。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **TaskExecutor**:任务执行器,负责单个 ETL 任务的执行、游标管理和运行记录
|
||||
- **PipelineRunner**:管道运行器,负责管道定义、层→任务映射、校验编排
|
||||
- **TaskRegistry**:任务注册表,管理所有已注册的任务类及其元数据
|
||||
- **DataSource**:数据源模式,取代原 `pipeline.flow`,表示数据来自在线 API(`online`)、本地 JSON(`offline`)或混合模式(`hybrid`)
|
||||
- **ProcessingMode**:处理模式,控制 ETL 执行策略(仅增量 / 仅校验 / 增量+校验)
|
||||
- **Pipeline**:管道,定义一组按层顺序执行的 ETL 任务集合(如 `api_full` = ODS → DWD → DWS → INDEX)
|
||||
- **CursorManager**:游标管理器,管理任务的时间水位(上次处理到哪里)
|
||||
- **RunTracker**:运行记录器,在 `etl_admin` Schema 中记录每次任务执行的状态和统计
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:架构分层 — TaskExecutor(执行层)
|
||||
|
||||
**用户故事:** 作为开发者,我希望单任务执行逻辑独立封装在 TaskExecutor 中,以便可以脱离管道上下文独立测试和复用。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TaskExecutor SHALL 封装单个任务的完整执行生命周期:创建运行记录、执行任务、更新游标、记录结果
|
||||
2. WHEN TaskExecutor 执行一个任务时,THE TaskExecutor SHALL 接收显式的 `data_source` 参数,而非读取全局状态
|
||||
3. WHEN 任务执行成功且返回有效时间窗口时,THE TaskExecutor SHALL 推进该任务的游标水位
|
||||
4. WHEN 任务执行过程中发生异常时,THE TaskExecutor SHALL 将运行记录状态更新为 FAIL 并重新抛出异常
|
||||
5. THE TaskExecutor SHALL 通过构造函数接收 `db_ops`、`api_client`、`cursor_manager`、`run_tracker`、`task_registry` 等依赖,而非自行创建
|
||||
6. WHEN 执行工具类任务(如 INIT_ODS_SCHEMA)时,THE TaskExecutor SHALL 跳过游标管理和运行记录,直接执行任务
|
||||
|
||||
### 需求 2:架构分层 — PipelineRunner(编排层)
|
||||
|
||||
**用户故事:** 作为开发者,我希望管道编排逻辑独立封装在 PipelineRunner 中,以便管道定义和校验流程可以独立演进。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE PipelineRunner SHALL 根据管道名称解析出需要执行的层列表(如 `api_full` → `["ODS", "DWD", "DWS", "INDEX"]`)
|
||||
2. WHEN PipelineRunner 执行管道时,THE PipelineRunner SHALL 委托 TaskExecutor 逐个执行任务,而非直接操作数据库或 API
|
||||
3. WHEN 处理模式为 `verify_only` 时,THE PipelineRunner SHALL 跳过增量 ETL,仅执行校验流程
|
||||
4. WHEN 处理模式为 `increment_verify` 时,THE PipelineRunner SHALL 先执行增量 ETL,再执行校验流程
|
||||
5. THE PipelineRunner SHALL 根据层列表自动选择对应的任务代码,支持配置覆盖
|
||||
6. WHEN 管道执行完成时,THE PipelineRunner SHALL 汇总所有任务的执行结果并返回统一的结果字典
|
||||
|
||||
### 需求 3:架构分层 — CLI 层重构
|
||||
|
||||
**用户故事:** 作为运维人员,我希望 CLI 参数命名清晰、语义无歧义,以便快速理解和正确使用各种执行模式。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CLI SHALL 将 `--pipeline-flow`(FULL/FETCH_ONLY/INGEST_ONLY)重命名为 `--data-source`(online/offline/hybrid),并保留旧名称作为别名
|
||||
2. THE CLI SHALL 保留 `--pipeline` 参数用于管道模式,保留 `--tasks` 参数用于传统模式
|
||||
3. WHEN 用户同时指定 `--pipeline` 和 `--tasks` 时,THE CLI SHALL 将 `--tasks` 作为管道内的任务过滤器
|
||||
4. THE CLI SHALL 保留 `--processing-mode`(increment_only/verify_only/increment_verify)参数不变
|
||||
5. WHEN 用户使用旧参数名 `--pipeline-flow` 时,THE CLI SHALL 发出弃用警告并将值映射到新的 `--data-source` 参数
|
||||
6. THE CLI SHALL 仅负责参数解析和配置加载,将执行逻辑委托给 PipelineRunner 或 TaskExecutor
|
||||
|
||||
### 需求 4:任务分类元数据化
|
||||
|
||||
**用户故事:** 作为开发者,我希望任务的分类信息(是否需要数据库配置、所属层等)由任务注册表管理,而非硬编码在调度器中。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TaskRegistry SHALL 支持在注册任务时附带元数据(`requires_db_config`、`layer`、`task_type`)
|
||||
2. WHEN TaskExecutor 需要判断任务是否为工具类任务时,THE TaskExecutor SHALL 查询 TaskRegistry 的元数据,而非检查硬编码集合
|
||||
3. WHEN PipelineRunner 需要根据层获取任务列表时,THE PipelineRunner SHALL 查询 TaskRegistry 的 `layer` 元数据
|
||||
4. THE TaskRegistry SHALL 保持向后兼容,无元数据的任务默认为 `requires_db_config=True`、`layer=None`
|
||||
|
||||
### 需求 5:配置键重构
|
||||
|
||||
**用户故事:** 作为运维人员,我希望配置键命名合理、语义清晰,以便正确配置 ETL 系统的运行参数。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE AppConfig SHALL 将 `app.timezone` 默认值从 `Asia/Shanghai` 改为 `Asia/Shanghai`
|
||||
2. THE AppConfig SHALL 将 `pipeline.flow` 配置键重命名为 `run.data_source`,并保留旧键作为兼容别名
|
||||
3. WHEN 配置中同时存在旧键 `pipeline.flow` 和新键 `run.data_source` 时,THE AppConfig SHALL 优先使用新键的值
|
||||
4. THE AppConfig SHALL 将 `pipeline.fetch_root` 和 `pipeline.ingest_source_dir` 移至 `io` 命名空间下(`io.fetch_root`、`io.ingest_source_dir`)
|
||||
|
||||
### 需求 6:资源管理与生命周期
|
||||
|
||||
**用户故事:** 作为开发者,我希望数据库连接和 API 客户端的创建与关闭由 CLI 层统一管理,以便确保资源正确释放。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CLI SHALL 在 `finally` 块中关闭数据库连接和 API 客户端,确保异常情况下资源也能释放
|
||||
2. THE TaskExecutor SHALL 通过依赖注入接收已创建的数据库连接和 API 客户端,而非自行创建
|
||||
3. THE PipelineRunner SHALL 通过依赖注入接收已创建的数据库连接和 API 客户端,而非自行创建
|
||||
4. WHEN CLI 创建资源时,THE CLI SHALL 使用 Python 上下文管理器(`with` 语句)或 `try/finally` 模式管理生命周期
|
||||
|
||||
### 需求 7:静态方法归位
|
||||
|
||||
**用户故事:** 作为开发者,我希望与调度器无关的静态工具方法移至合适的模块,以便保持类的职责单一。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE `_map_run_status` 方法 SHALL 从 ETLScheduler 移至 RunTracker 或独立的工具模块
|
||||
2. THE `_filter_verify_tables` 方法 SHALL 从 ETLScheduler 移至校验相关模块
|
||||
3. WHEN 静态方法被移动后,THE 原调用方 SHALL 更新导入路径以引用新位置
|
||||
|
||||
### 需求 8:向后兼容与过渡
|
||||
|
||||
**用户故事:** 作为运维人员,我希望重构后的系统在过渡期内兼容旧的 CLI 参数和配置键,以便平滑迁移。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户使用旧参数 `--pipeline-flow FULL` 时,THE CLI SHALL 将其等价映射为 `--data-source hybrid` 并发出弃用警告
|
||||
2. WHEN 用户使用旧参数 `--pipeline-flow FETCH_ONLY` 时,THE CLI SHALL 将其等价映射为 `--data-source online` 并发出弃用警告
|
||||
3. WHEN 用户使用旧参数 `--pipeline-flow INGEST_ONLY` 时,THE CLI SHALL 将其等价映射为 `--data-source offline` 并发出弃用警告
|
||||
4. WHEN 配置文件中使用旧键 `pipeline.flow` 时,THE AppConfig SHALL 自动映射到新键 `run.data_source`
|
||||
5. THE 系统 SHALL 在日志中记录所有弃用映射,便于运维人员逐步迁移
|
||||
|
||||
### 需求 9:可测试性
|
||||
|
||||
**用户故事:** 作为开发者,我希望重构后的每一层都可以独立进行单元测试,以便快速验证逻辑正确性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TaskExecutor SHALL 支持通过注入 mock 依赖(FakeDB、FakeAPI)进行单元测试,无需真实数据库
|
||||
2. THE PipelineRunner SHALL 支持通过注入 mock TaskExecutor 进行单元测试,无需执行真实任务
|
||||
3. THE TaskRegistry SHALL 支持在测试中创建独立实例,不依赖全局 `default_registry`
|
||||
4. WHEN 运行单元测试时,THE 测试 SHALL 验证各层之间的交互契约(调用参数、返回值格式)
|
||||
147
.kiro/specs/scheduler-refactor/tasks.md
Normal file
147
.kiro/specs/scheduler-refactor/tasks.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 实现计划:ETL 调度器重构
|
||||
|
||||
## 概述
|
||||
|
||||
将 `ETLScheduler`(~900 行)拆分为 TaskExecutor(执行层)、PipelineRunner(编排层)、增强版 TaskRegistry(元数据),重构 CLI 参数和配置键,保持向后兼容。采用自底向上的实现顺序:先基础组件,再上层编排,最后 CLI 集成。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 增强 TaskRegistry,支持元数据注册与查询
|
||||
- [x] 1.1 扩展 TaskRegistry 类,添加 TaskMeta 数据类和元数据相关方法
|
||||
- 在 `orchestration/task_registry.py` 中添加 `TaskMeta` dataclass(`task_class`、`requires_db_config`、`layer`、`task_type`)
|
||||
- 修改 `register()` 方法签名,增加可选的 `requires_db_config`、`layer`、`task_type` 参数
|
||||
- 添加 `get_metadata()`、`get_tasks_by_layer()`、`is_utility_task()` 方法
|
||||
- 保持 `create_task()` 和 `get_all_task_codes()` 接口不变
|
||||
- _需求: 4.1, 4.4_
|
||||
|
||||
- [x] 1.2 更新所有任务注册调用,添加元数据
|
||||
- 将原 `NO_DB_CONFIG_TASKS` 硬编码集合中的任务标记为 `requires_db_config=False`
|
||||
- 为 ODS 任务添加 `layer="ODS"`,DWD 任务添加 `layer="DWD"`,DWS 任务添加 `layer="DWS"`,INDEX 任务添加 `layer="INDEX"`
|
||||
- 工具类任务标记 `task_type="utility"`,校验类任务标记 `task_type="verification"`
|
||||
- _需求: 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 1.3 编写 TaskRegistry 属性测试
|
||||
- **Property 8: TaskRegistry 元数据 round-trip**
|
||||
- **验证: 需求 4.1**
|
||||
|
||||
- [x] 1.4 编写 TaskRegistry 向后兼容和按层查询属性测试
|
||||
- **Property 9: TaskRegistry 向后兼容默认值**
|
||||
- **Property 10: 按层查询任务**
|
||||
- **验证: 需求 4.4, 4.3**
|
||||
|
||||
- [x] 2. 配置键重构与向后兼容
|
||||
- [x] 2.1 修改 `config/defaults.py` 默认值
|
||||
- 将 `app.timezone` 默认值从 `Asia/Shanghai` 改为 `Asia/Shanghai`
|
||||
- 将 `db.session.timezone` 默认值从 `Asia/Shanghai` 改为 `Asia/Shanghai`
|
||||
- 添加 `run.data_source` 键(默认 `hybrid`)
|
||||
- 将 `pipeline.fetch_root` 和 `pipeline.ingest_source_dir` 复制到 `io.fetch_root` 和 `io.ingest_source_dir`(保留旧键兼容)
|
||||
- _需求: 5.1, 5.2, 5.4_
|
||||
|
||||
- [x] 2.2 在 `config/settings.py` 的 `_normalize()` 中添加兼容映射逻辑
|
||||
- 旧键 `pipeline.flow` → 新键 `run.data_source`(值映射:FULL→hybrid, FETCH_ONLY→online, INGEST_ONLY→offline)
|
||||
- 旧键 `pipeline.fetch_root` → `io.fetch_root`,`pipeline.ingest_source_dir` → `io.ingest_source_dir`
|
||||
- 新键优先:当新旧键同时存在时,使用新键的值
|
||||
- 记录弃用警告日志
|
||||
- _需求: 5.2, 5.3, 5.4, 8.4, 8.5_
|
||||
|
||||
- [x] 2.3 编写配置映射属性测试
|
||||
- **Property 11: pipeline_flow → data_source 映射一致性**
|
||||
- **验证: 需求 8.1, 8.2, 8.3, 5.2, 8.4**
|
||||
|
||||
- [x] 3. 静态方法归位
|
||||
- [x] 3.1 将 `_map_run_status` 移至 RunTracker
|
||||
- 在 `orchestration/run_tracker.py` 中添加 `map_run_status()` 静态方法(从 `ETLScheduler._map_run_status` 复制)
|
||||
- _需求: 7.1_
|
||||
|
||||
- [x] 3.2 将 `_filter_verify_tables` 移至校验模块
|
||||
- 在 `tasks/verification/` 下合适的模块中添加 `filter_verify_tables()` 函数
|
||||
- _需求: 7.2_
|
||||
|
||||
- [x] 4. 检查点 — 确保所有测试通过
|
||||
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 5. 实现 TaskExecutor(执行层)
|
||||
- [x] 5.1 创建 `orchestration/task_executor.py`
|
||||
- 实现 `TaskExecutor` 类,构造函数接收 `config`、`db_ops`、`api_client`、`cursor_mgr`、`run_tracker`、`task_registry`、`logger`
|
||||
- 从 `ETLScheduler` 迁移以下方法:`run_tasks`、`_run_single_task`、`_execute_fetch`、`_execute_ingest`、`_execute_ods_record_and_load`、`_run_utility_task`、`_build_fetch_dir`、`_resolve_ingest_source`、`_counts_from_fetch`、`_load_task_config`、`_maybe_run_integrity_check`、`_attach_run_file_logger`
|
||||
- 将 `data_source` 改为方法参数(替代原 `self.pipeline_flow` 全局状态)
|
||||
- 使用 `self.task_registry.is_utility_task()` 替代硬编码的 `NO_DB_CONFIG_TASKS`
|
||||
- 使用 `RunTracker.map_run_status()` 替代 `self._map_run_status()`
|
||||
- 添加 `DataSource` 枚举类(`online`/`offline`/`hybrid`)
|
||||
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
|
||||
|
||||
- [x] 5.2 编写 TaskExecutor 属性测试
|
||||
- **Property 1: data_source 参数决定执行路径**
|
||||
- **Property 2: 成功任务推进游标**
|
||||
- **Property 3: 失败任务标记 FAIL 并重新抛出**
|
||||
- **Property 4: 工具类任务由元数据决定**
|
||||
- **验证: 需求 1.2, 1.3, 1.4, 1.6, 4.2**
|
||||
|
||||
- [x] 6. 实现 PipelineRunner(编排层)
|
||||
- [x] 6.1 创建 `orchestration/pipeline_runner.py`
|
||||
- 实现 `PipelineRunner` 类,构造函数接收 `config`、`task_executor`、`task_registry`、`db_conn`、`api_client`、`logger`
|
||||
- 将 `PIPELINE_LAYERS` 常量从 `scheduler.py` 迁移至此
|
||||
- 从 `ETLScheduler` 迁移以下方法:`run_pipeline_with_verification`(重命名为 `run`)、`_run_layer_verification`(重命名为 `_run_verification`)、`_get_tasks_for_layers`(重命名为 `_resolve_tasks`)
|
||||
- 使用 `filter_verify_tables()`(已移至校验模块)替代原内联静态方法
|
||||
- 使用 `task_registry.get_tasks_by_layer()` 作为默认任务解析,配置覆盖优先
|
||||
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
|
||||
|
||||
- [x] 6.2 编写 PipelineRunner 属性测试
|
||||
- **Property 5: 管道名称→层列表映射**
|
||||
- **Property 6: processing_mode 控制执行流程**
|
||||
- **Property 7: 管道结果汇总完整性**
|
||||
- **验证: 需求 2.1, 2.3, 2.4, 2.6**
|
||||
|
||||
- [x] 7. 检查点 — 确保所有测试通过
|
||||
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 8. 重构 CLI 层
|
||||
- [x] 8.1 重构 `cli/main.py` 参数解析
|
||||
- 添加 `--data-source` 参数(choices: online/offline/hybrid,默认 hybrid)
|
||||
- 保留 `--pipeline-flow` 作为弃用别名,使用时发出 `DeprecationWarning` 并映射到 `--data-source`
|
||||
- 更新 `build_cli_overrides()` 将 `--data-source` 写入 `run.data_source` 配置键
|
||||
- _需求: 3.1, 3.5, 8.1, 8.2, 8.3_
|
||||
|
||||
- [x] 8.2 重构 `cli/main.py` 的 `main()` 函数
|
||||
- 在 `try/finally` 块中管理 `DatabaseConnection` 和 `APIClient` 的生命周期
|
||||
- 在 `try` 块内组装 `TaskExecutor` 和 `PipelineRunner`(依赖注入)
|
||||
- 管道模式委托 `PipelineRunner.run()`,传统模式委托 `TaskExecutor.run_tasks()`
|
||||
- 添加 `resolve_data_source(args)` 辅助函数处理新旧参数映射
|
||||
- _需求: 3.2, 3.3, 3.4, 3.6, 6.1, 6.4_
|
||||
|
||||
- [x] 8.3 编写 CLI 参数解析单元测试
|
||||
- 测试 `--data-source` 新参数正确解析
|
||||
- 测试 `--pipeline-flow` 旧参数弃用映射
|
||||
- 测试 `--pipeline` + `--tasks` 同时使用时的行为
|
||||
- _需求: 3.1, 3.3, 3.5_
|
||||
|
||||
- [x] 9. 清理旧代码与集成
|
||||
- [x] 9.1 重构 `orchestration/scheduler.py` 为薄包装层
|
||||
- 将 `ETLScheduler` 改为薄包装,内部委托 `TaskExecutor` 和 `PipelineRunner`
|
||||
- 保留 `ETLScheduler` 类名和 `run_tasks()`、`run_pipeline_with_verification()`、`close()` 公共接口,标记为弃用
|
||||
- 确保 GUI 层(`gui/workers/`)等现有调用方无需立即修改
|
||||
- _需求: 8.1, 8.4_
|
||||
|
||||
- [x] 9.2 更新 GUI 工作线程中的调度器引用
|
||||
- 检查 `gui/workers/` 中对 `ETLScheduler` 的使用
|
||||
- 如有直接引用内部方法,更新为使用新的公共接口
|
||||
- _需求: 7.3_
|
||||
|
||||
- [x] 9.3 编写集成测试验证端到端流程
|
||||
- 使用 FakeDB/FakeAPI 验证 CLI → PipelineRunner → TaskExecutor 完整调用链
|
||||
- 验证传统模式和管道模式均正常工作
|
||||
- _需求: 9.4_
|
||||
|
||||
- [x] 10. 最终检查点 — 确保所有测试通过
|
||||
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选测试任务,可跳过以加速 MVP
|
||||
- 每个任务引用了具体的需求编号,确保可追溯性
|
||||
- 检查点确保增量验证,避免问题累积
|
||||
- 属性测试使用 `hypothesis` 库,验证通用正确性属性
|
||||
- 单元测试验证具体示例和边界条件
|
||||
- `ETLScheduler` 保留为薄包装层,确保 GUI 等现有调用方平滑过渡
|
||||
22
.kiro/steering/db-docs.md
Normal file
22
.kiro/steering/db-docs.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
inclusion: fileMatch
|
||||
fileMatchPattern:
|
||||
- "**/migrations/**/*.*"
|
||||
- "**/*.sql"
|
||||
- "**/*schema*.*"
|
||||
- "**/*ddl*.*"
|
||||
- "**/*.prisma"
|
||||
---
|
||||
|
||||
# Database Schema Documentation Rules
|
||||
|
||||
当你修改任何可能影响 PostgreSQL schema/表结构的内容时(迁移脚本/DDL/表定义/ORM 模型):
|
||||
|
||||
1) 必须同步更新 BD 手册目录:
|
||||
docs/database
|
||||
|
||||
2) 文档最低要求:
|
||||
- 变更说明:新增/修改/删除的表、字段、约束、索引
|
||||
- 兼容性:对 ETL、后端 API、小程序字段映射的影响
|
||||
- 回滚策略:如何撤销(DDL 回滚 / 数据回填)
|
||||
- 验证步骤:最少包含 3 条校验 SQL
|
||||
27
.kiro/steering/governance.md
Normal file
27
.kiro/steering/governance.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
inclusion: always
|
||||
---
|
||||
|
||||
# Governance(Lite)
|
||||
|
||||
## 目的
|
||||
在“上下文压缩很致命”的前提下,保留最小硬约束:**任何逻辑改动必须可追溯、可验证、可回滚**。
|
||||
|
||||
## 何时必须审计(Audit Required)
|
||||
满足任一即视为“高风险”,必须运行 `/audit`:
|
||||
- 改动文件命中:`api/`、`cli/`、`config/`、`database/`、`loaders/`、`models/`、`orchestration/`、`scd/`、`tasks/`、`utils/` 或根目录散文件
|
||||
- 出现 DB schema / migration / `*.sql` / `*.prisma` 结构性变更
|
||||
- 业务口径/资金精度与舍入/数据清洗聚合映射/API 契约/鉴权权限/调度游标逻辑发生变化
|
||||
|
||||
## 执行方式(自动判定 + 半自动执行)
|
||||
- 系统会在你提交 prompt 时自动判定“是否待审计”,并在与 Kiro 交互结束时提醒(15 分钟限频)
|
||||
- 用户将手动触发 `/audit`(Manual: Run /audit hook),由 **audit-writer 子代理**执行重型写入
|
||||
- 主对话只接收“极短回执”(done + files_written + next_step),避免审计细节淹没上下文
|
||||
|
||||
## 审计产物(由 /audit 生成)
|
||||
- `docs/audit/changes/<YYYY-MM-DD>__<slug>.md`
|
||||
- 每个被改文件内:`AI_CHANGELOG`
|
||||
- 每处逻辑变更附近:`CHANGE` 注释
|
||||
- DB schema 变更:同步 `docs/database/`
|
||||
|
||||
(详细模板/清单/流程见 skills:`steering-readme-maintainer`、`change-annotation-audit`、`bd-manual-db-docs`)
|
||||
18
.kiro/steering/language-zh.md
Normal file
18
.kiro/steering/language-zh.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
inclusion: always
|
||||
---
|
||||
# 语言与编码规范(强制)
|
||||
|
||||
## 输出语言
|
||||
- 默认:所有“说明性文字”一律使用简体中文(对话回复、文档内容、代码注释、README/ADR/变更说明等)。
|
||||
- 允许保留英文的部分:
|
||||
- 代码标识符(类名/函数名/变量名/接口名/库名/命令名)不翻译
|
||||
- 第三方工具的原始 CLI 输出/报错原文不篡改(可在原文后补充中文解释)
|
||||
|
||||
## 文档与注释
|
||||
- 新增/修改的文档必须与代码变更同步更新
|
||||
- 注释只写“为什么/边界/假设”,避免复述代码
|
||||
|
||||
## 编码与字符集
|
||||
- 仓库内所有文本文件统一 UTF-8,无 BOM。
|
||||
- 禁止出现 GBK/Big5 混用;若发现历史文件,先转码再重构
|
||||
22
.kiro/steering/product.md
Normal file
22
.kiro/steering/product.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 产品概述
|
||||
|
||||
飞球 ETL 系统 (etl-billiards) — 面向台球门店业务的数据仓库 ETL 管线。
|
||||
|
||||
## 功能
|
||||
- 从上游 SaaS API 抽取运营数据(订单、支付、会员、助教、库存等)
|
||||
- 原始数据落地 **ODS**(操作数据存储层),保留源 payload 便于回溯
|
||||
- 清洗装载至 **DWD**(明细数据层),维度走 SCD2,事实按时间增量
|
||||
- 汇总至 **DWS**(数据服务层):助教业绩、财务日报、会员分析、工资计算、自定义指数算法(WBI/NCI/RS/OS/MS/ML)
|
||||
- 提供 **PySide6 桌面 GUI**,支持任务管理、调度配置
|
||||
- 支持在线(API 抓取)和离线(JSON 回放)两种模式
|
||||
|
||||
## 业务上下文
|
||||
- 单租户:一家台球门店(由 `STORE_ID` 标识)
|
||||
- 核心实体:会员(客户)、助教(教练)、台桌、订单、支付、退款、团购套餐、库存
|
||||
- 领域语言以中文为主;代码注释、文档、UI 文案均为中文
|
||||
- 货币:人民币(CNY),金额以 numeric(2) 存储
|
||||
|
||||
## 主要入口
|
||||
- CLI:`python -m cli.main`(主入口)
|
||||
- GUI:`python -m gui.main`
|
||||
- 批处理脚本:`run_etl.bat`、`run_gui.bat`(根目录)、`scripts/run_ods.bat`
|
||||
17
.kiro/steering/steering-readme-maintainer.md
Normal file
17
.kiro/steering/steering-readme-maintainer.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# 变更影响审查与文档同步(手动参考)
|
||||
|
||||
说明:本文件用于“按需加载”的快速参考(可作为 /slash command),详细流程请优先使用 skill:
|
||||
- steering-readme-maintainer
|
||||
|
||||
## 何时使用
|
||||
- 发生业务/资金口径/ETL/接口/鉴权/小程序交互等“逻辑改动”时
|
||||
|
||||
## 快速清单
|
||||
- 是否需要更新 product.md / tech.md / structure.md / README.md / (各子目录下README.md)
|
||||
- 是否需要补齐审计记录 docs/audit/changes/<date>__<slug>.md
|
||||
- 是否需要在每个修改文件写入 AI_CHANGELOG
|
||||
- 是否需要在逻辑变更处加 CHANGE 标记注释
|
||||
33
.kiro/steering/structure-lite.md
Normal file
33
.kiro/steering/structure-lite.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
inclusion: always
|
||||
---
|
||||
|
||||
# 项目结构(Lite)
|
||||
|
||||
目标:在不注入大段目录树的前提下,让 Agent 快速理解“模块边界 + 高风险区”。
|
||||
|
||||
## 关键模块边界(高风险路径 = 变更默认需要审计)
|
||||
- `cli/`:命令行入口与参数/运行模式(影响一键增量、调度参数等)
|
||||
- `config/`:默认值、环境变量解析、AppConfig、调度任务配置(影响运行时假设)
|
||||
- `api/`:外部接口客户端与端点路由(影响抓取/契约/回放)
|
||||
- `database/`:连接、DDL/schema、seed、migrations(影响数据结构与回滚)
|
||||
- `tasks/`:ETL 任务(ODS/DWD/DWS/指数/校验),业务规则主要落在这里
|
||||
- `loaders/`:upsert 与维度/事实装载(影响落库与冲突处理)
|
||||
- `scd/`:SCD2 处理(影响维度历史与生效区间)
|
||||
- `orchestration/`:调度/注册/游标/运行记录(影响增量水位与可重复性)
|
||||
- `models/`:解析与验证器(影响字段校验与转换)
|
||||
- `utils/`:日志、JSON 存储、窗口切分等通用工具(影响全局行为)
|
||||
- 根目录散文件:`.env*`、`pyproject.toml`、`requirements*`、`Makefile`、`README.md` 等(影响运行/依赖/发布)
|
||||
|
||||
## 架构要点(摘要)
|
||||
- 任务模式:每个 ETL 任务继承 `BaseTask`(Extract → Transform → Load),并在 `orchestration/task_registry.py` 注册
|
||||
- 加载器模式:每张目标表一个 Loader,维度/事实分目录;核心是 `upsert()` 与冲突处理策略
|
||||
- 配置分层:DEFAULTS → `.env` → CLI 覆盖;通过 `AppConfig.get("dotted.path")` 访问
|
||||
- 管线流程:`FULL` / `FETCH_ONLY` / `INGEST_ONLY` 由 CLI 或环境变量控制
|
||||
- 调度器:负责游标(水位)与运行记录(增量正确性关键)
|
||||
|
||||
## 编码/命名约定(摘要)
|
||||
- 文件编码:UTF-8
|
||||
- SQL:纯 SQL(非 ORM);迁移脚本放 `database/migrations/`,推荐“日期前缀”命名
|
||||
- 任务:大写蛇形命名(例如 `DWD_LOAD_FROM_ODS`)
|
||||
- 日志:统一经由 `utils/logging_utils.py`
|
||||
124
.kiro/steering/structure.md
Normal file
124
.kiro/steering/structure.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
inclusion: auto
|
||||
name: structure-full
|
||||
description: Full directory tree + architecture patterns. Load only for large refactors, module moves, or changes spanning multiple subsystems.
|
||||
---
|
||||
|
||||
# 项目结构
|
||||
|
||||
```
|
||||
NeoZQYY/ # Monorepo 工作区根目录(C:\NeoZQYY)
|
||||
├── cli/ # CLI 入口(main.py)
|
||||
├── config/ # 配置:默认值、环境变量解析、AppConfig、调度任务配置
|
||||
│ └── scheduled_tasks.json
|
||||
├── api/ # API 客户端(HTTP、本地 JSON 回放、录制)
|
||||
│ └── endpoint_routing.py # 端点路由映射
|
||||
├── database/ # 数据库连接、操作、DDL Schema、种子脚本、迁移
|
||||
│ ├── migrations/ # 迁移脚本(纯 SQL,日期前缀命名)
|
||||
│ ├── schema_*.sql # DDL 定义
|
||||
│ └── seed_*.sql # 种子数据
|
||||
├── tasks/ # ETL 任务实现(按数据层分目录)
|
||||
│ ├── base_task.py # BaseTask 基类,提供 Extract/Transform/Load 模板
|
||||
│ ├── ods/ # ODS 层抓取任务(16 个业务实体 + ods_tasks 工厂)
|
||||
│ ├── dwd/ # DWD 层装载任务(base_dwd_task、维度/事实装载、质量检查)
|
||||
│ ├── dws/ # DWS 汇总与指数任务
|
||||
│ │ └── index/ # 指数计算任务(亲密度、新客转化、召回、关系、赢回)
|
||||
│ ├── utility/ # 工具类任务(Schema 初始化、手动入库、完整性检查、DWS 构建等)
|
||||
│ └── verification/ # ETL 后置校验任务(ODS/DWD/DWS/指数校验器)
|
||||
├── loaders/ # 数据加载器(ODS、维度、事实)
|
||||
│ ├── base_loader.py # BaseLoader 基类,定义 upsert 接口
|
||||
│ ├── ods/ # 通用 ODS 加载器
|
||||
│ ├── dimensions/ # SCD2 维度加载器(会员、助教、商品、台桌、套餐)
|
||||
│ └── facts/ # 事实表加载器(订单、支付、退款、小票、充值等)
|
||||
├── scd/ # SCD2(缓慢变化维度)处理器
|
||||
├── orchestration/ # 调度器、任务注册表、游标管理、运行记录
|
||||
│ ├── pipeline_runner.py # 管线运行器
|
||||
│ ├── task_executor.py # 任务执行器
|
||||
│ ├── task_registry.py # 任务注册表
|
||||
│ ├── scheduler.py # ETL 调度器
|
||||
│ ├── cursor_manager.py # 游标(水位)管理
|
||||
│ └── run_tracker.py # 运行记录追踪
|
||||
├── quality/ # 数据质量检查器(余额一致性、完整性)
|
||||
│ └── integrity_service.py # 完整性检查服务
|
||||
├── models/ # 解析器与验证器
|
||||
├── utils/ # 工具函数:日志、JSON 存储、报告、窗口切分
|
||||
├── gui/ # PySide6 桌面 GUI
|
||||
│ ├── main_window.py
|
||||
│ ├── widgets/ # UI 面板与组件
|
||||
│ ├── workers/ # 后台工作线程
|
||||
│ ├── models/ # GUI 数据模型(任务、调度)
|
||||
│ ├── utils/ # GUI 专用工具(设置、CLI 构建器)
|
||||
│ └── resources/ # 样式表
|
||||
├── scripts/ # 运维/工具脚本
|
||||
│ ├── run_update.py # 一键增量更新入口(ODS → DWD → DWS)
|
||||
│ ├── run_ods.bat # ODS 批处理入口
|
||||
│ ├── audit/ # 仓库审计脚本(扫描器、分析器、报告生成)
|
||||
│ ├── check/ # 数据检查脚本(完整性、ODS 缺口、DWD 服务、内容哈希等)
|
||||
│ ├── db_admin/ # 数据库管理脚本(Excel 导入)
|
||||
│ ├── export/ # 数据导出脚本(指数、团购、亲密度、会员明细等)
|
||||
│ ├── rebuild/ # 数据重建脚本(全量 ODS→DWD 重建)
|
||||
│ └── repair/ # 数据修复脚本(回填、去重、hash 修复、维度修复、索引调优)
|
||||
├── tests/ # 测试套件
|
||||
│ ├── unit/ # 单元测试(FakeDB/FakeAPI,无需真实数据库)
|
||||
│ └── integration/ # 集成测试(需要 TEST_DB_DSN 或真实数据库)
|
||||
├── docs/ # 文档
|
||||
│ ├── CHANGELOG.md # 项目级版本变更历史
|
||||
│ ├── audit/ # 审计产物
|
||||
│ │ ├── changes/ # AI 逐次变更审计记录
|
||||
│ │ ├── repo/ # 仓库审计报告(自动生成)
|
||||
│ │ ├── prompt_logs/ # Prompt 日志(每次 prompt 一个独立文件,按时间戳命名)
|
||||
│ │ └── audit_dashboard.md # 审计一览表(/audit 自动刷新)
|
||||
│ ├── architecture/ # 架构设计文档(系统概览、数据流向)
|
||||
│ ├── business-rules/ # 业务规则文档(指数算法、DWS 口径、SCD2 规则)
|
||||
│ ├── operations/ # 运维文档(环境搭建、调度配置、故障排查)
|
||||
│ ├── database/ # 数据库文档统一目录(ODS/DWD/DWS/ETL_Admin 表手册 + 概览索引)
|
||||
│ │ ├── overview/ # 层级概览 / 速查索引
|
||||
│ │ ├── ODS/ # ODS 层表手册(main/mappings/changes)
|
||||
│ │ ├── DWD/ # DWD 层表手册(main + Ex 扩展)
|
||||
│ │ ├── DWS/ # DWS 层表手册
|
||||
│ │ └── ETL_Admin/ # ETL 管理层表手册
|
||||
│ ├── etl_tasks/ # ETL 任务文档
|
||||
│ ├── requirements/ # 需求文档(功能需求、口径补充、指数 PRD)
|
||||
│ ├── reports/ # 分析报告
|
||||
│ ├── api-reference/ # API 参考文档(标准化)
|
||||
│ │ ├── api_registry.json # API 注册表(25 个端点定义)
|
||||
│ │ ├── summary/ # 每个 API 一个精简版 .md(25 个)
|
||||
│ │ ├── endpoints/ # 每个 API 一个详细版 .md 文档(24 个)
|
||||
│ │ └── samples/ # 最新响应样本(JSON)
|
||||
├── reports/ # 质检输出(JSON,已 gitignore)
|
||||
├── export/ # JSON 落盘与日志(已 gitignore)
|
||||
├── logs/ # 运行日志(已 gitignore)
|
||||
└── .Deleted/ # 已归档/废弃文件(隐藏目录,已 gitignore)
|
||||
```
|
||||
|
||||
## 架构模式
|
||||
|
||||
- **任务模式**:每个 ETL 任务继承 `BaseTask`(Extract → Transform → Load 模板方法),在 `orchestration/task_registry.py` 中注册。
|
||||
- **加载器模式**:每张目标表对应一个加载器,继承 `BaseLoader` 并实现 `upsert()` 方法。维度加载器在 `loaders/dimensions/`,事实加载器在 `loaders/facts/`。
|
||||
- **配置分层**:`DEFAULTS` 字典 → `.env` 覆盖 → CLI 参数覆盖。通过 `AppConfig.get("dotted.path")` 访问。
|
||||
- **管线流程**:`FULL`(抓取 + 入库)、`FETCH_ONLY`(仅抓取)、`INGEST_ONLY`(仅入库)。由 `--pipeline-flow` CLI 参数或 `PIPELINE_FLOW` 环境变量控制。
|
||||
- **调度器**:`ETLScheduler` 编排任务执行,管理游标(水位),在 `etl_admin` Schema 中记录运行状态。
|
||||
- **API 抽象**:`APIClient`(HTTP)、`LocalJsonClient`(离线回放)、`RecordingAPIClient`(抓取 + 落盘)共享相同接口,任务代码无需关心数据来源。
|
||||
|
||||
## 编码约定
|
||||
- 文件编码:UTF-8,文件头加 `# -*- coding: utf-8 -*-`
|
||||
- 日志格式:通过 `utils/logging_utils.py` 统一
|
||||
- 任务代码:大写蛇形命名(如 `DWD_LOAD_FROM_ODS`、`DWS_ASSISTANT_DAILY`)
|
||||
- SQL 文件:纯 SQL,不使用 ORM;通过 `psycopg2` 执行
|
||||
- 数据库操作:批量 upsert + 冲突处理,显式 commit/rollback
|
||||
- 中文注释和文档字符串是正常且预期的
|
||||
|
||||
<!--
|
||||
AI_CHANGELOG:
|
||||
- 日期: 2026-02-13
|
||||
- Prompt: P20260213-171500 — "继续"(Task 3 API 文档全面重构续接)
|
||||
- 直接原因: 新增 docs/api-reference/ 目录替代旧 test-json-doc,需在项目结构文档中反映
|
||||
- 变更摘要: docs/ 树中新增 api-reference/(含 api_registry.json、endpoints/、samples/);test-json-doc 标记为 [已废弃]
|
||||
- 风险与验证: 纯文档结构描述变更,无运行时影响;验证方式:对比实际目录 `ls docs/api-reference/` 确认一致
|
||||
|
||||
- 日期: 2026-02-14
|
||||
- Prompt: P20260214-130000 — 25 个 API 文档归档至 summary/ + 字段分组修正
|
||||
- 直接原因: 新增 summary/ 子目录存放精简版 API 文档,需在项目结构中反映
|
||||
- 变更摘要: api-reference/ 树中新增 summary/(25 个精简版 .md);endpoints/ 说明从"25 个"更正为"24 个"
|
||||
- 风险与验证: 纯文档结构描述变更,无运行时影响
|
||||
-->
|
||||
60
.kiro/steering/tech.md
Normal file
60
.kiro/steering/tech.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 技术栈与构建
|
||||
|
||||
## 语言与运行时
|
||||
- Python 3.10+(测试缓存中观察到 3.13)
|
||||
- 未提交虚拟环境;用户自行管理
|
||||
|
||||
## 核心依赖(requirements.txt)
|
||||
- `psycopg2-binary>=2.9.0` — PostgreSQL 驱动
|
||||
- `requests>=2.28.0` — 上游 API 的 HTTP 客户端
|
||||
- `python-dateutil>=2.8.0` / `tzdata>=2023.0` — 日期解析与时区处理
|
||||
- `python-dotenv` — `.env` 文件加载
|
||||
- `openpyxl>=3.1.0` — Excel 导入导出(DWS 数据)
|
||||
- `PySide6>=6.5.0` — Qt 桌面 GUI 框架
|
||||
- `flask>=2.3` — 可选 Web API
|
||||
- `pyinstaller>=6.0.0` — 可选,仅打包 EXE 时需要
|
||||
|
||||
## 数据库
|
||||
- PostgreSQL(连接远程实例)
|
||||
- Schema:`billiards_ods`(ODS 原始数据)、`billiards_dwd`(明细数据)、`billiards_dws`(汇总数据)、`etl_admin`(调度/运行记录)
|
||||
- DDL 文件位于 `database/schema_*.sql`,种子脚本位于 `database/seed_*.sql`
|
||||
- 迁移脚本位于 `database/migrations/`(纯 SQL,日期前缀命名)
|
||||
|
||||
## 测试
|
||||
- 框架:`pytest`(未固定在 requirements 中,需单独安装)
|
||||
- 配置:`pytest.ini` 设置 `pythonpath = .`
|
||||
- 结构:`tests/unit/`(基于 mock,无需数据库)、`tests/integration/`(需要 `TEST_DB_DSN`)
|
||||
- 测试工具:`tests/unit/task_test_utils.py` 提供 FakeDB/FakeAPI 辅助类
|
||||
|
||||
## 常用命令
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 在线全流程 ETL(抓取 + 入库)
|
||||
python -m cli.main --pg-dsn "$PG_DSN" --store-id "$STORE_ID" --api-token "$API_TOKEN"
|
||||
|
||||
# 运行指定任务
|
||||
python -m cli.main --tasks INIT_ODS_SCHEMA,MANUAL_INGEST --data-source offline
|
||||
|
||||
# 试运行(不写库)
|
||||
python -m cli.main --dry-run --tasks DWD_LOAD_FROM_ODS
|
||||
|
||||
# 单元测试
|
||||
pytest tests/unit
|
||||
|
||||
# 集成测试(需要数据库)
|
||||
TEST_DB_DSN="postgresql://..." pytest tests/integration
|
||||
|
||||
# 启动 GUI
|
||||
python -m gui.main
|
||||
```
|
||||
|
||||
## 配置体系
|
||||
- 分层叠加:`config/defaults.py` < `.env` / 环境变量 < CLI 参数
|
||||
- 配置类:`config.settings.AppConfig`,支持点号路径访问(`config.get("db.dsn")`)
|
||||
- 敏感值(DSN、API Token)放在 `.env` 中,禁止提交
|
||||
|
||||
## 打包
|
||||
- 已移除 EXE 打包支持(`build_exe.py`、`setup.py` 已归档至 `.Deleted/`)
|
||||
- 直接通过 `python -m cli.main` 或 `python -m gui.main` 运行
|
||||
9
.kiroignore
Normal file
9
.kiroignore
Normal file
@@ -0,0 +1,9 @@
|
||||
tmp/
|
||||
.hypothesis/
|
||||
node_modules/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.pyc
|
||||
logs/
|
||||
samples/
|
||||
.git/
|
||||
8
NeoZQYY.code-workspace
Normal file
8
NeoZQYY.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
53
README.md
Normal file
53
README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# NeoZQYY Monorepo
|
||||
|
||||
台球门店运营助手一体化平台,整合 ETL 数据管线、微信小程序后端、小程序前端、管理后台与桌面 GUI。
|
||||
|
||||
## 项目结构
|
||||
|
||||
| 目录 | 说明 |
|
||||
|------|------|
|
||||
| apps/etl/pipelines/feiqiu/ | ETL 数据管线(飞球平台) |
|
||||
| apps/backend/ | FastAPI 后端(小程序 API) |
|
||||
| apps/miniprogram/ | 微信小程序(Donut + TDesign) |
|
||||
| apps/admin-web/ | 管理后台(规划中) |
|
||||
| gui/ | PySide6 桌面 GUI(过渡期) |
|
||||
| packages/shared/ | 跨项目共享包(枚举、金额精度、时间工具) |
|
||||
| db/ | 数据库 DDL、迁移、种子脚本 |
|
||||
| docs/ | 文档(PRD、契约、权限矩阵、架构等) |
|
||||
| infra/ | 基础设施配置 |
|
||||
| scripts/ | 运维/工具脚本 |
|
||||
| samples/ | 示例数据与配置 |
|
||||
| tests/ | Monorepo 级属性测试 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装全部依赖(需要 uv)
|
||||
uv sync
|
||||
|
||||
# 运行 ETL
|
||||
cd apps/etl/pipelines/feiqiu
|
||||
python -m cli.main --pg-dsn "$PG_DSN" --store-id "$STORE_ID" --api-token "$API_TOKEN"
|
||||
|
||||
# 启动后端 API
|
||||
cd apps/backend
|
||||
uvicorn app.main:app --reload
|
||||
|
||||
# 运行 ETL 单元测试
|
||||
cd apps/etl/pipelines/feiqiu
|
||||
pytest tests/unit
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
配置采用分层叠加:根 .env -> 应用 .env.local -> 环境变量 -> CLI 参数。
|
||||
|
||||
参见 .env.template 了解可用配置项。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.10+, uv workspace
|
||||
- PostgreSQL(六层 Schema:meta/ods/dwd/core/dws/app)
|
||||
- FastAPI + uvicorn
|
||||
- PySide6(桌面 GUI)
|
||||
- Donut + TDesign(微信小程序)
|
||||
17
apps/README.md
Normal file
17
apps/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# apps/
|
||||
|
||||
## 作用说明
|
||||
|
||||
应用项目顶层目录,存放所有可独立部署/运行的子项目。当前包含 ETL 数据管线、FastAPI 后端、微信小程序前端,以及预留的管理后台。
|
||||
|
||||
## 内部结构
|
||||
|
||||
- `etl/pipelines/feiqiu/` — 飞球平台 ETL 管线(抽取→清洗→汇总全流程)
|
||||
- `backend/` — FastAPI 后端(小程序 API、权限、审批)
|
||||
- `miniprogram/` — 微信小程序前端(Donut + TDesign)
|
||||
- `admin-web/` — 管理后台(预留,暂未实施)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- 新增更多数据源管线时,在 `etl/pipelines/` 下按平台名创建子目录
|
||||
- `admin-web/` 待产品需求确认后启动
|
||||
0
apps/admin-web/.gitkeep
Normal file
0
apps/admin-web/.gitkeep
Normal file
0
apps/backend/.gitkeep
Normal file
0
apps/backend/.gitkeep
Normal file
41
apps/backend/README.md
Normal file
41
apps/backend/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# apps/backend - FastAPI 后端
|
||||
|
||||
为微信小程序提供 RESTful API,连接 zqyy_app 业务数据库,通过 FDW 只读访问 ETL 数据。
|
||||
|
||||
## 内部结构
|
||||
|
||||
`
|
||||
apps/backend/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI 入口,启用 OpenAPI 文档
|
||||
│ ├── config.py # 配置加载
|
||||
│ ├── database.py # zqyy_app 数据库连接
|
||||
│ ├── routers/ # 路由模块
|
||||
│ ├── middleware/ # 中间件(鉴权、日志等)
|
||||
│ └── schemas/ # Pydantic 请求/响应模型
|
||||
├── tests/ # 后端测试
|
||||
├── pyproject.toml # 依赖声明
|
||||
└── README.md
|
||||
`
|
||||
|
||||
## 启动
|
||||
|
||||
`ash
|
||||
cd apps/backend
|
||||
uvicorn app.main:app --reload
|
||||
`
|
||||
|
||||
API 文档自动生成于 http://localhost:8000/docs
|
||||
|
||||
## 依赖
|
||||
|
||||
- fastapi>=0.100, uvicorn>=0.23
|
||||
- psycopg2-binary>=2.9.0
|
||||
- neozqyy-shared(workspace 引用)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] 用户管理与微信登录
|
||||
- [ ] RBAC 权限中间件
|
||||
- [ ] 任务审批流 API
|
||||
- [ ] FDW 数据查询接口(助教业绩、财务日报等)
|
||||
0
apps/backend/app/__init__.py
Normal file
0
apps/backend/app/__init__.py
Normal file
36
apps/backend/app/config.py
Normal file
36
apps/backend/app/config.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
后端配置加载
|
||||
|
||||
优先级(低 → 高):根 .env → 应用 .env.local → 环境变量
|
||||
敏感值(DSN、Token)禁止提交,仅放在 .env / .env.local 中。
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 根 .env(公共配置)
|
||||
_root_env = Path(__file__).resolve().parents[3] / ".env"
|
||||
load_dotenv(_root_env, override=False)
|
||||
|
||||
# 应用级 .env.local(私有覆盖,优先级更高)
|
||||
_local_env = Path(__file__).resolve().parents[1] / ".env.local"
|
||||
load_dotenv(_local_env, override=True)
|
||||
|
||||
|
||||
def get(key: str, default: str | None = None) -> str | None:
|
||||
"""从环境变量读取配置值。"""
|
||||
return os.getenv(key, default)
|
||||
|
||||
|
||||
# ---- 数据库连接参数 ----
|
||||
DB_HOST: str = get("DB_HOST", "localhost")
|
||||
DB_PORT: str = get("DB_PORT", "5432")
|
||||
DB_USER: str = get("DB_USER", "")
|
||||
DB_PASSWORD: str = get("DB_PASSWORD", "")
|
||||
APP_DB_NAME: str = get("APP_DB_NAME", "zqyy_app")
|
||||
|
||||
# ---- 通用 ----
|
||||
TIMEZONE: str = get("TIMEZONE", "Asia/Shanghai")
|
||||
LOG_LEVEL: str = get("LOG_LEVEL", "INFO")
|
||||
26
apps/backend/app/database.py
Normal file
26
apps/backend/app/database.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
zqyy_app 数据库连接
|
||||
|
||||
使用 psycopg2 直连 PostgreSQL,不引入 ORM。
|
||||
连接参数从环境变量读取(经 config 模块加载)。
|
||||
"""
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extensions import connection as PgConnection
|
||||
|
||||
from app.config import APP_DB_NAME, DB_HOST, DB_PASSWORD, DB_PORT, DB_USER
|
||||
|
||||
|
||||
def get_connection() -> PgConnection:
|
||||
"""
|
||||
获取 zqyy_app 数据库连接。
|
||||
|
||||
调用方负责关闭连接(推荐配合 contextmanager 或 try/finally 使用)。
|
||||
"""
|
||||
return psycopg2.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
dbname=APP_DB_NAME,
|
||||
)
|
||||
22
apps/backend/app/main.py
Normal file
22
apps/backend/app/main.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
NeoZQYY 后端 API 入口
|
||||
|
||||
基于 FastAPI 构建,为微信小程序提供 RESTful API。
|
||||
OpenAPI 文档自动生成于 /docs(Swagger UI)和 /redoc(ReDoc)。
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI(
|
||||
title="NeoZQYY API",
|
||||
description="台球门店运营助手 — 微信小程序后端 API",
|
||||
version="0.1.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/health", tags=["系统"])
|
||||
async def health_check():
|
||||
"""健康检查端点,用于探活和监控。"""
|
||||
return {"status": "ok"}
|
||||
0
apps/backend/app/middleware/__init__.py
Normal file
0
apps/backend/app/middleware/__init__.py
Normal file
0
apps/backend/app/routers/__init__.py
Normal file
0
apps/backend/app/routers/__init__.py
Normal file
0
apps/backend/app/schemas/__init__.py
Normal file
0
apps/backend/app/schemas/__init__.py
Normal file
10
apps/backend/pyproject.toml
Normal file
10
apps/backend/pyproject.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "zqyy-backend"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"neozqyy-shared",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
neozqyy-shared = { workspace = true }
|
||||
0
apps/backend/tests/__init__.py
Normal file
0
apps/backend/tests/__init__.py
Normal file
15
apps/etl/README.md
Normal file
15
apps/etl/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# apps/etl/
|
||||
|
||||
## 作用说明
|
||||
|
||||
ETL 数据管线集合。每个上游数据源对应 `pipelines/` 下的一个子目录,当前仅有飞球平台(`feiqiu`)。管线负责从 SaaS API 抽取数据,经 ODS→DWD→Core→DWS 逐层处理后落库。
|
||||
|
||||
## 内部结构
|
||||
|
||||
- `pipelines/feiqiu/` — 飞球平台 ETL(api、cli、config、loaders、models、orchestration、scd、tasks、utils、quality、tests)
|
||||
|
||||
## Roadmap
|
||||
|
||||
- 将通用抽取/加载逻辑抽离为 `etl_sdk` 共享包,供多管线复用
|
||||
- 将各平台 API 客户端拆分为独立 `connectors` 包,实现可插拔数据源接入
|
||||
- 新增管线时在 `pipelines/` 下创建同构子目录
|
||||
0
apps/etl/pipelines/feiqiu/.gitkeep
Normal file
0
apps/etl/pipelines/feiqiu/.gitkeep
Normal file
0
apps/etl/pipelines/feiqiu/api/__init__.py
Normal file
0
apps/etl/pipelines/feiqiu/api/__init__.py
Normal file
293
apps/etl/pipelines/feiqiu/api/client.py
Normal file
293
apps/etl/pipelines/feiqiu/api/client.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""API客户端:统一封装 POST/重试/分页与列表提取逻辑。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Sequence, Tuple
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from api.endpoint_routing import plan_calls
|
||||
|
||||
DEFAULT_BROWSER_HEADERS = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://pc.ficoo.vip",
|
||||
"Referer": "https://pc.ficoo.vip/",
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"sec-ch-ua": '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-fetch-site": "same-origin",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-dest": "empty",
|
||||
"priority": "u=1, i",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"DNT": "1",
|
||||
}
|
||||
|
||||
DEFAULT_LIST_KEYS: Tuple[str, ...] = (
|
||||
"list",
|
||||
"rows",
|
||||
"records",
|
||||
"items",
|
||||
"dataList",
|
||||
"data_list",
|
||||
"tenantMemberInfos",
|
||||
"tenantMemberCardLogs",
|
||||
"tenantMemberCards",
|
||||
"settleList",
|
||||
"orderAssistantDetails",
|
||||
"assistantInfos",
|
||||
"siteTables",
|
||||
"taiFeeAdjustInfos",
|
||||
"siteTableUseDetailsList",
|
||||
"tenantGoodsList",
|
||||
"packageCouponList",
|
||||
"queryDeliveryRecordsList",
|
||||
"goodsCategoryList",
|
||||
"orderGoodsList",
|
||||
"orderGoodsLedgers",
|
||||
)
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""HTTP API 客户端(默认使用 POST + JSON 请求体)"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
token: str | None = None,
|
||||
timeout: int = 20,
|
||||
retry_max: int = 3,
|
||||
headers_extra: dict | None = None,
|
||||
):
|
||||
self.base_url = (base_url or "").rstrip("/")
|
||||
self.token = self._normalize_token(token)
|
||||
self.timeout = timeout
|
||||
self.retry_max = retry_max
|
||||
self.headers_extra = headers_extra or {}
|
||||
self._session: requests.Session | None = None
|
||||
|
||||
# ------------------------------------------------------------------ HTTP 基础
|
||||
def _get_session(self) -> requests.Session:
|
||||
"""获取或创建带重试的 Session。"""
|
||||
if self._session is None:
|
||||
self._session = requests.Session()
|
||||
|
||||
retries = max(0, int(self.retry_max) - 1)
|
||||
retry = Retry(
|
||||
total=None,
|
||||
connect=retries,
|
||||
read=retries,
|
||||
status=retries,
|
||||
allowed_methods=frozenset(["GET", "POST"]),
|
||||
status_forcelist=(429, 500, 502, 503, 504),
|
||||
backoff_factor=0.5,
|
||||
respect_retry_after_header=True,
|
||||
raise_on_status=False,
|
||||
)
|
||||
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
self._session.mount("http://", adapter)
|
||||
self._session.mount("https://", adapter)
|
||||
self._session.headers.update(self._build_headers())
|
||||
|
||||
return self._session
|
||||
|
||||
def get(self, endpoint: str, params: dict | None = None) -> dict:
|
||||
"""
|
||||
兼容旧名的请求入口(实际以 POST JSON 方式请求)。
|
||||
"""
|
||||
return self._post_json(endpoint, params)
|
||||
|
||||
def _post_json(self, endpoint: str, payload: dict | None = None) -> dict:
|
||||
if not self.base_url:
|
||||
raise ValueError("API base_url 未配置")
|
||||
|
||||
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
||||
sess = self._get_session()
|
||||
resp = sess.post(url, json=payload or {}, timeout=self.timeout)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
self._ensure_success(data)
|
||||
return data
|
||||
|
||||
def _build_headers(self) -> dict:
|
||||
headers = dict(DEFAULT_BROWSER_HEADERS)
|
||||
headers.update(self.headers_extra)
|
||||
if self.token:
|
||||
headers["Authorization"] = self.token
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def _normalize_token(token: str | None) -> str | None:
|
||||
if not token:
|
||||
return None
|
||||
t = str(token).strip()
|
||||
if not t.lower().startswith("bearer "):
|
||||
t = f"Bearer {t}"
|
||||
return t
|
||||
|
||||
@staticmethod
|
||||
def _ensure_success(payload: dict):
|
||||
"""API 返回 code 非 0 时主动抛错,便于上层重试/记录。"""
|
||||
if isinstance(payload, dict) and "code" in payload:
|
||||
code = payload.get("code")
|
||||
if code not in (0, "0", None):
|
||||
msg = payload.get("msg") or payload.get("message") or ""
|
||||
raise ValueError(f"API 返回错误 code={code} msg={msg}")
|
||||
|
||||
# ------------------------------------------------------------------ 分页
|
||||
def _iter_paginated_single(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict | None,
|
||||
page_size: int | None = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | Sequence[str] | None = None,
|
||||
page_start: int = 1,
|
||||
page_end: int | None = None,
|
||||
) -> Iterable[tuple[int, list, dict, dict]]:
|
||||
"""
|
||||
单一 endpoint 的分页迭代器(不包含 recent/former 路由逻辑)。
|
||||
"""
|
||||
base_params = dict(params or {})
|
||||
page = page_start
|
||||
|
||||
while True:
|
||||
page_params = dict(base_params)
|
||||
if page_size is not None:
|
||||
page_params[page_field] = page
|
||||
page_params[size_field] = page_size
|
||||
|
||||
payload = self._post_json(endpoint, page_params)
|
||||
records = self._extract_list(payload, data_path, list_key)
|
||||
|
||||
yield page, records, page_params, payload
|
||||
|
||||
if page_size is None:
|
||||
break
|
||||
if page_end is not None and page >= page_end:
|
||||
break
|
||||
if len(records) < (page_size or 0):
|
||||
break
|
||||
if len(records) == 0:
|
||||
break
|
||||
|
||||
page += 1
|
||||
|
||||
def iter_paginated(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict | None,
|
||||
page_size: int | None = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | Sequence[str] | None = None,
|
||||
page_start: int = 1,
|
||||
page_end: int | None = None,
|
||||
) -> Iterable[tuple[int, list, dict, dict]]:
|
||||
"""
|
||||
分页迭代器:逐页拉取数据并产出 (page_no, records, request_params, raw_response)。
|
||||
page_size=None 时不附带分页参数,仅拉取一次。
|
||||
"""
|
||||
# recent/former 路由:当 params 带时间范围字段时,按“3个月自然月”边界决定走哪个 endpoint,
|
||||
# 跨越边界则拆分为两段请求并顺序产出,确保调用方使用 page_no 命名文件时不会被覆盖。
|
||||
call_plan = plan_calls(endpoint, params)
|
||||
global_page = 1
|
||||
|
||||
for call in call_plan:
|
||||
for _, records, request_params, payload in self._iter_paginated_single(
|
||||
endpoint=call.endpoint,
|
||||
params=call.params,
|
||||
page_size=page_size,
|
||||
page_field=page_field,
|
||||
size_field=size_field,
|
||||
data_path=data_path,
|
||||
list_key=list_key,
|
||||
page_start=page_start,
|
||||
page_end=page_end,
|
||||
):
|
||||
yield global_page, records, request_params, payload
|
||||
global_page += 1
|
||||
|
||||
def get_paginated(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict,
|
||||
page_size: int | None = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | Sequence[str] | None = None,
|
||||
page_start: int = 1,
|
||||
page_end: int | None = None,
|
||||
) -> tuple[list, list]:
|
||||
"""分页获取数据并将所有记录汇总在一个列表中。"""
|
||||
records, pages_meta = [], []
|
||||
|
||||
for page_no, page_records, request_params, response in self.iter_paginated(
|
||||
endpoint=endpoint,
|
||||
params=params,
|
||||
page_size=page_size,
|
||||
page_field=page_field,
|
||||
size_field=size_field,
|
||||
data_path=data_path,
|
||||
list_key=list_key,
|
||||
page_start=page_start,
|
||||
page_end=page_end,
|
||||
):
|
||||
records.extend(page_records)
|
||||
pages_meta.append(
|
||||
{"page": page_no, "request": request_params, "response": response}
|
||||
)
|
||||
|
||||
return records, pages_meta
|
||||
|
||||
# ------------------------------------------------------------------ 响应解析
|
||||
@classmethod
|
||||
def _extract_list(
|
||||
cls, payload: dict | list, data_path: tuple, list_key: str | Sequence[str] | None
|
||||
) -> list:
|
||||
"""根据 data_path/list_key 提取列表结构,兼容常见字段名。"""
|
||||
cur: object = payload
|
||||
|
||||
if isinstance(cur, list):
|
||||
return cur
|
||||
|
||||
for key in data_path:
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(key)
|
||||
else:
|
||||
cur = None
|
||||
if cur is None:
|
||||
break
|
||||
|
||||
if isinstance(cur, list):
|
||||
return cur
|
||||
|
||||
if isinstance(cur, dict):
|
||||
if list_key:
|
||||
keys = (list_key,) if isinstance(list_key, str) else tuple(list_key)
|
||||
for k in keys:
|
||||
if isinstance(cur.get(k), list):
|
||||
return cur[k]
|
||||
|
||||
for k in DEFAULT_LIST_KEYS:
|
||||
if isinstance(cur.get(k), list):
|
||||
return cur[k]
|
||||
|
||||
for v in cur.values():
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
|
||||
return []
|
||||
166
apps/etl/pipelines/feiqiu/api/endpoint_routing.py
Normal file
166
apps/etl/pipelines/feiqiu/api/endpoint_routing.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
“近期记录 / 历史记录(Former)”接口路由规则。
|
||||
|
||||
需求:
|
||||
- 当请求参数包含可定义时间范围的字段时,根据当前时间(北京时间/上海时区)判断:
|
||||
- 3个月(自然月)之前 -> 使用“历史记录”接口
|
||||
- 3个月以内 -> 使用“近期记录”接口
|
||||
- 若时间范围跨越边界 -> 拆分为两段分别请求并合并(由上层分页迭代器顺序产出)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from dateutil import parser as dtparser
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
ROUTING_TZ = ZoneInfo("Asia/Shanghai")
|
||||
RECENT_MONTHS = 3
|
||||
|
||||
|
||||
# 按 `fetch-test/recent_vs_former_report.md` 更新(“无”表示没有历史接口;相同 path 表示同一个接口可查历史)
|
||||
RECENT_TO_FORMER_OVERRIDES: dict[str, str | None] = {
|
||||
"/AssistantPerformance/GetAbolitionAssistant": None,
|
||||
"/Site/GetSiteTableUseDetails": "/Site/GetSiteTableUseDetails",
|
||||
"/GoodsStockManage/QueryGoodsOutboundReceipt": "/GoodsStockManage/QueryFormerGoodsOutboundReceipt",
|
||||
"/Promotion/GetOfflineCouponConsumePageList": "/Promotion/GetOfflineCouponConsumePageList",
|
||||
"/Order/GetRefundPayLogList": None,
|
||||
# 已知特殊
|
||||
"/Site/GetAllOrderSettleList": "/Site/GetFormerOrderSettleList",
|
||||
"/PayLog/GetPayLogListPage": "/PayLog/GetFormerPayLogListPage",
|
||||
}
|
||||
|
||||
|
||||
TIME_WINDOW_KEYS: tuple[tuple[str, str], ...] = (
|
||||
("startTime", "endTime"),
|
||||
("rangeStartTime", "rangeEndTime"),
|
||||
("StartPayTime", "EndPayTime"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindowSpec:
|
||||
start_key: str
|
||||
end_key: str
|
||||
start: datetime
|
||||
end: datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RoutedCall:
|
||||
endpoint: str
|
||||
params: dict
|
||||
|
||||
|
||||
def is_former_endpoint(endpoint: str) -> bool:
|
||||
return "Former" in str(endpoint or "")
|
||||
|
||||
|
||||
def _parse_dt(value: object, tz: ZoneInfo) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
s = str(value).strip()
|
||||
if not s:
|
||||
return None
|
||||
dt = dtparser.parse(s)
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=tz)
|
||||
return dt.astimezone(tz)
|
||||
|
||||
|
||||
def _fmt_dt(dt: datetime, tz: ZoneInfo) -> str:
|
||||
return dt.astimezone(tz).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def extract_window_spec(params: dict | None, tz: ZoneInfo = ROUTING_TZ) -> WindowSpec | None:
|
||||
if not isinstance(params, dict) or not params:
|
||||
return None
|
||||
for start_key, end_key in TIME_WINDOW_KEYS:
|
||||
if start_key in params or end_key in params:
|
||||
start = _parse_dt(params.get(start_key), tz)
|
||||
end = _parse_dt(params.get(end_key), tz)
|
||||
if start and end:
|
||||
return WindowSpec(start_key=start_key, end_key=end_key, start=start, end=end)
|
||||
return None
|
||||
|
||||
|
||||
def derive_former_endpoint(recent_endpoint: str) -> str | None:
|
||||
endpoint = str(recent_endpoint or "").strip()
|
||||
if not endpoint:
|
||||
return None
|
||||
|
||||
if endpoint in RECENT_TO_FORMER_OVERRIDES:
|
||||
return RECENT_TO_FORMER_OVERRIDES[endpoint]
|
||||
|
||||
if is_former_endpoint(endpoint):
|
||||
return endpoint
|
||||
|
||||
idx = endpoint.find("Get")
|
||||
if idx == -1:
|
||||
return endpoint
|
||||
return f"{endpoint[:idx]}GetFormer{endpoint[idx + 3:]}"
|
||||
|
||||
|
||||
def recent_boundary(now: datetime, months: int = RECENT_MONTHS) -> datetime:
|
||||
"""
|
||||
3个月(自然月)边界:取 (now - months) 所在月份的 1 号 00:00:00。
|
||||
"""
|
||||
if now.tzinfo is None:
|
||||
raise ValueError("now 必须为时区时间")
|
||||
base = now - relativedelta(months=months)
|
||||
return base.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
def plan_calls(
|
||||
endpoint: str,
|
||||
params: dict | None,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
tz: ZoneInfo = ROUTING_TZ,
|
||||
months: int = RECENT_MONTHS,
|
||||
) -> list[RoutedCall]:
|
||||
"""
|
||||
根据 endpoint + params 的时间窗口,返回要调用的 endpoint/params 列表(可能拆分为两段)。
|
||||
"""
|
||||
base_params = dict(params or {})
|
||||
if not base_params:
|
||||
return [RoutedCall(endpoint=endpoint, params=base_params)]
|
||||
|
||||
# 若调用方显式传了 Former 接口,则不二次路由。
|
||||
if is_former_endpoint(endpoint):
|
||||
return [RoutedCall(endpoint=endpoint, params=base_params)]
|
||||
|
||||
window = extract_window_spec(base_params, tz)
|
||||
if not window:
|
||||
return [RoutedCall(endpoint=endpoint, params=base_params)]
|
||||
|
||||
former_endpoint = derive_former_endpoint(endpoint)
|
||||
if former_endpoint is None or former_endpoint == endpoint:
|
||||
return [RoutedCall(endpoint=endpoint, params=base_params)]
|
||||
|
||||
now_dt = (now or datetime.now(tz)).astimezone(tz)
|
||||
boundary = recent_boundary(now_dt, months=months)
|
||||
|
||||
start, end = window.start, window.end
|
||||
if end <= boundary:
|
||||
return [RoutedCall(endpoint=former_endpoint, params=base_params)]
|
||||
if start >= boundary:
|
||||
return [RoutedCall(endpoint=endpoint, params=base_params)]
|
||||
|
||||
# 跨越边界:拆分两段(老数据 -> former,新数据 -> recent)
|
||||
p1 = dict(base_params)
|
||||
p1[window.start_key] = _fmt_dt(start, tz)
|
||||
p1[window.end_key] = _fmt_dt(boundary, tz)
|
||||
|
||||
p2 = dict(base_params)
|
||||
p2[window.start_key] = _fmt_dt(boundary, tz)
|
||||
p2[window.end_key] = _fmt_dt(end, tz)
|
||||
|
||||
return [RoutedCall(endpoint=former_endpoint, params=p1), RoutedCall(endpoint=endpoint, params=p2)]
|
||||
|
||||
78
apps/etl/pipelines/feiqiu/api/local_json_client.py
Normal file
78
apps/etl/pipelines/feiqiu/api/local_json_client.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""本地 JSON 客户端,模拟 APIClient 的分页接口,从落盘的 JSON 回放数据。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Tuple
|
||||
|
||||
from api.client import APIClient
|
||||
from utils.json_store import endpoint_to_filename
|
||||
|
||||
|
||||
class LocalJsonClient:
|
||||
"""
|
||||
读取 RecordingAPIClient 生成的 JSON,提供 iter_paginated/get_paginated 接口。
|
||||
"""
|
||||
|
||||
def __init__(self, base_dir: str | Path):
|
||||
self.base_dir = Path(base_dir)
|
||||
if not self.base_dir.exists():
|
||||
raise FileNotFoundError(f"JSON 目录不存在: {self.base_dir}")
|
||||
|
||||
def get_source_hint(self, endpoint: str) -> str:
|
||||
"""Return the JSON file path for this endpoint (for source_file lineage)."""
|
||||
return str(self.base_dir / endpoint_to_filename(endpoint))
|
||||
|
||||
def iter_paginated(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict | None,
|
||||
page_size: int = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | None = None,
|
||||
) -> Iterable[Tuple[int, list, dict, dict]]:
|
||||
file_path = self.base_dir / endpoint_to_filename(endpoint)
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"未找到匹配的 JSON 文件: {file_path}")
|
||||
|
||||
with file_path.open("r", encoding="utf-8") as fp:
|
||||
payload = json.load(fp)
|
||||
|
||||
pages = payload.get("pages")
|
||||
if not isinstance(pages, list) or not pages:
|
||||
pages = [{"page": 1, "request": params or {}, "response": payload}]
|
||||
|
||||
for idx, page in enumerate(pages, start=1):
|
||||
response = page.get("response", {})
|
||||
request_params = page.get("request") or {}
|
||||
page_no = page.get("page") or idx
|
||||
records = APIClient._extract_list(response, data_path, list_key) # type: ignore[attr-defined]
|
||||
yield page_no, records, request_params, response
|
||||
|
||||
def get_paginated(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict,
|
||||
page_size: int = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | None = None,
|
||||
) -> tuple[list, list]:
|
||||
records: list = []
|
||||
pages_meta: list = []
|
||||
for page_no, page_records, request_params, response in self.iter_paginated(
|
||||
endpoint=endpoint,
|
||||
params=params,
|
||||
page_size=page_size,
|
||||
page_field=page_field,
|
||||
size_field=size_field,
|
||||
data_path=data_path,
|
||||
list_key=list_key,
|
||||
):
|
||||
records.extend(page_records)
|
||||
pages_meta.append({"page": page_no, "request": request_params, "response": response})
|
||||
return records, pages_meta
|
||||
195
apps/etl/pipelines/feiqiu/api/recording_client.py
Normal file
195
apps/etl/pipelines/feiqiu/api/recording_client.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""包装 APIClient,将分页响应落盘便于后续本地清洗。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import Any, Iterable, Tuple
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from api.client import APIClient
|
||||
from api.endpoint_routing import plan_calls
|
||||
from utils.json_store import dump_json, endpoint_to_filename
|
||||
|
||||
|
||||
class RecordingAPIClient:
|
||||
"""
|
||||
代理 APIClient,在调用 iter_paginated/get_paginated 时同时把响应写入 JSON 文件。
|
||||
文件名根据 endpoint 生成,写入到指定 output_dir。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_client: APIClient,
|
||||
output_dir: Path | str,
|
||||
task_code: str,
|
||||
run_id: int,
|
||||
write_pretty: bool = False,
|
||||
):
|
||||
self.base = base_client
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.task_code = task_code
|
||||
self.run_id = run_id
|
||||
self.write_pretty = write_pretty
|
||||
self.last_dump: dict[str, Any] | None = None
|
||||
|
||||
# ------------------------------------------------------------------ 公共 API
|
||||
def get_source_hint(self, endpoint: str) -> str:
|
||||
"""Return the JSON dump path for this endpoint (for source_file lineage)."""
|
||||
return str(self.output_dir / endpoint_to_filename(endpoint))
|
||||
|
||||
def iter_paginated(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict | None,
|
||||
page_size: int = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | None = None,
|
||||
) -> Iterable[Tuple[int, list, dict, dict]]:
|
||||
pages: list[dict[str, Any]] = []
|
||||
total_records = 0
|
||||
|
||||
for page_no, records, request_params, response in self.base.iter_paginated(
|
||||
endpoint=endpoint,
|
||||
params=params,
|
||||
page_size=page_size,
|
||||
page_field=page_field,
|
||||
size_field=size_field,
|
||||
data_path=data_path,
|
||||
list_key=list_key,
|
||||
):
|
||||
pages.append({"page": page_no, "request": request_params, "response": response})
|
||||
total_records += len(records)
|
||||
yield page_no, records, request_params, response
|
||||
|
||||
self._dump(endpoint, params, page_size, pages, total_records)
|
||||
|
||||
def get_paginated(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict,
|
||||
page_size: int = 200,
|
||||
page_field: str = "page",
|
||||
size_field: str = "limit",
|
||||
data_path: tuple = ("data",),
|
||||
list_key: str | None = None,
|
||||
) -> tuple[list, list]:
|
||||
records: list = []
|
||||
pages_meta: list = []
|
||||
|
||||
for page_no, page_records, request_params, response in self.iter_paginated(
|
||||
endpoint=endpoint,
|
||||
params=params,
|
||||
page_size=page_size,
|
||||
page_field=page_field,
|
||||
size_field=size_field,
|
||||
data_path=data_path,
|
||||
list_key=list_key,
|
||||
):
|
||||
records.extend(page_records)
|
||||
pages_meta.append({"page": page_no, "request": request_params, "response": response})
|
||||
|
||||
return records, pages_meta
|
||||
|
||||
# ------------------------------------------------------------------ 内部方法
|
||||
def _dump(
|
||||
self,
|
||||
endpoint: str,
|
||||
params: dict | None,
|
||||
page_size: int,
|
||||
pages: list[dict[str, Any]],
|
||||
total_records: int,
|
||||
):
|
||||
filename = endpoint_to_filename(endpoint)
|
||||
path = self.output_dir / filename
|
||||
routing_calls = []
|
||||
try:
|
||||
for call in plan_calls(endpoint, params):
|
||||
routing_calls.append({"endpoint": call.endpoint, "params": call.params})
|
||||
except Exception:
|
||||
routing_calls = []
|
||||
payload = {
|
||||
"task_code": self.task_code,
|
||||
"run_id": self.run_id,
|
||||
"endpoint": endpoint,
|
||||
"params": params or {},
|
||||
"endpoint_routing": {"calls": routing_calls} if routing_calls else None,
|
||||
"page_size": page_size,
|
||||
"pages": pages,
|
||||
"total_records": total_records,
|
||||
"dumped_at": datetime.utcnow().isoformat() + "Z",
|
||||
}
|
||||
dump_json(path, payload, pretty=self.write_pretty)
|
||||
self.last_dump = {
|
||||
"file": str(path),
|
||||
"endpoint": endpoint,
|
||||
"pages": len(pages),
|
||||
"records": total_records,
|
||||
}
|
||||
|
||||
|
||||
def _cfg_get(cfg, key: str, default=None):
|
||||
if isinstance(cfg, dict):
|
||||
cur = cfg
|
||||
for part in key.split("."):
|
||||
if not isinstance(cur, dict) or part not in cur:
|
||||
return default
|
||||
cur = cur[part]
|
||||
return cur
|
||||
getter = getattr(cfg, "get", None)
|
||||
if callable(getter):
|
||||
return getter(key, default)
|
||||
return default
|
||||
|
||||
|
||||
def build_recording_client(
|
||||
cfg,
|
||||
*,
|
||||
task_code: str,
|
||||
output_dir: Path | str | None = None,
|
||||
run_id: int | None = None,
|
||||
write_pretty: bool | None = None,
|
||||
):
|
||||
"""Build RecordingAPIClient from AppConfig or dict config."""
|
||||
base_client = APIClient(
|
||||
base_url=_cfg_get(cfg, "api.base_url") or "",
|
||||
token=_cfg_get(cfg, "api.token"),
|
||||
timeout=int(_cfg_get(cfg, "api.timeout_sec", 20) or 20),
|
||||
retry_max=int(_cfg_get(cfg, "api.retries.max_attempts", 3) or 3),
|
||||
headers_extra=_cfg_get(cfg, "api.headers_extra") or {},
|
||||
)
|
||||
|
||||
if write_pretty is None:
|
||||
write_pretty = bool(_cfg_get(cfg, "io.write_pretty_json", False))
|
||||
|
||||
if run_id is None:
|
||||
run_id = int(time.time())
|
||||
|
||||
if output_dir is None:
|
||||
# CHANGE [2026-02-14] intent: 默认时区从 Asia/Taipei 修正为 Asia/Shanghai,与运营地区一致
|
||||
tz_name = _cfg_get(cfg, "app.timezone", "Asia/Shanghai") or "Asia/Shanghai"
|
||||
tz = ZoneInfo(tz_name)
|
||||
ts = datetime.now(tz).strftime("%Y%m%d-%H%M%S")
|
||||
fetch_root = _cfg_get(cfg, "pipeline.fetch_root") or _cfg_get(cfg, "io.export_root") or "export/JSON"
|
||||
task_upper = str(task_code).upper()
|
||||
output_dir = Path(fetch_root) / task_upper / f"{task_upper}-{run_id}-{ts}"
|
||||
|
||||
return RecordingAPIClient(
|
||||
base_client=base_client,
|
||||
output_dir=output_dir,
|
||||
task_code=str(task_code),
|
||||
run_id=int(run_id),
|
||||
write_pretty=bool(write_pretty),
|
||||
)
|
||||
|
||||
|
||||
# AI_CHANGELOG:
|
||||
# - 日期: 2026-02-14
|
||||
# - Prompt: P20260214-040231(审计收口补录)
|
||||
# - 直接原因: 默认时区 Asia/Taipei 与运营地区(中国大陆)不符
|
||||
# - 变更摘要: build_recording_client 默认时区从 Asia/Taipei 改为 Asia/Shanghai
|
||||
# - 风险与验证: 极低风险,两时区当前 UTC 偏移相同(+08:00)
|
||||
0
apps/etl/pipelines/feiqiu/cli/__init__.py
Normal file
0
apps/etl/pipelines/feiqiu/cli/__init__.py
Normal file
504
apps/etl/pipelines/feiqiu/cli/main.py
Normal file
504
apps/etl/pipelines/feiqiu/cli/main.py
Normal file
@@ -0,0 +1,504 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CLI主入口
|
||||
|
||||
支持两种执行模式:
|
||||
1. 传统模式:指定任务列表直接执行
|
||||
2. 管道模式:指定管道类型和处理模式,执行多层 ETL
|
||||
|
||||
处理模式说明:
|
||||
- increment_only:仅增量 - 只执行增量数据处理
|
||||
- verify_only:校验并修复 - 跳过增量,直接校验数据一致性并自动补齐
|
||||
- 可选 --fetch-before-verify:校验前先从 API 获取数据
|
||||
- increment_verify:增量+校验并修复 - 先增量处理,再校验补齐
|
||||
|
||||
示例:
|
||||
# 传统模式
|
||||
python -m cli.main --tasks ODS_MEMBER,ODS_ORDER
|
||||
|
||||
# 管道模式(仅增量)
|
||||
python -m cli.main --pipeline api_full --processing-mode increment_only
|
||||
|
||||
# 管道模式(校验并修复,跳过增量)
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only
|
||||
|
||||
# 管道模式(校验并修复,校验前先从 API 获取数据)
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only --fetch-before-verify
|
||||
|
||||
# 管道模式(增量+校验并修复)
|
||||
python -m cli.main --pipeline api_full --processing-mode increment_verify
|
||||
|
||||
# 带时间窗口的管道模式
|
||||
python -m cli.main --pipeline api_ods_dwd --window-start "2026-02-01" --window-end "2026-02-02"
|
||||
"""
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from config.settings import AppConfig
|
||||
from orchestration.scheduler import ETLScheduler # 保留,task 9 处理薄包装层
|
||||
|
||||
# 新架构依赖
|
||||
from database.connection import DatabaseConnection
|
||||
from database.operations import DatabaseOperations
|
||||
from orchestration.cursor_manager import CursorManager
|
||||
from orchestration.run_tracker import RunTracker
|
||||
from orchestration.task_registry import default_registry
|
||||
from orchestration.task_executor import TaskExecutor
|
||||
from orchestration.pipeline_runner import PipelineRunner
|
||||
from api.client import APIClient
|
||||
|
||||
# 管道选项
|
||||
PIPELINE_CHOICES = [
|
||||
"api_ods", # API → ODS
|
||||
"api_ods_dwd", # API → ODS → DWD
|
||||
"api_full", # API → ODS → DWD → DWS汇总 → DWS指数
|
||||
"ods_dwd", # ODS → DWD
|
||||
"dwd_dws", # DWD → DWS汇总
|
||||
"dwd_dws_index", # DWD → DWS汇总 → DWS指数
|
||||
"dwd_index", # DWD → DWS指数
|
||||
]
|
||||
|
||||
# 处理模式选项
|
||||
PROCESSING_MODE_CHOICES = [
|
||||
"increment_only", # 仅增量
|
||||
"verify_only", # 校验并修复(跳过增量)
|
||||
"increment_verify", # 增量 + 校验并修复
|
||||
]
|
||||
|
||||
# 时间窗口切分选项
|
||||
WINDOW_SPLIT_CHOICES = ["none", "day", "week", "month"]
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""设置日志(使用统一格式)"""
|
||||
try:
|
||||
from utils.logging_utils import UNIFIED_FORMAT, DATE_FORMAT
|
||||
fmt = UNIFIED_FORMAT
|
||||
datefmt = DATE_FORMAT
|
||||
except ImportError:
|
||||
fmt = "[%(asctime)s] %(levelname)-5s | %(name)s | %(message)s"
|
||||
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format=fmt, datefmt=datefmt)
|
||||
return logging.getLogger("etl_billiards")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="台球场ETL系统",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
示例:
|
||||
# 传统任务模式
|
||||
python -m cli.main --tasks ODS_MEMBER,ODS_ORDER --store-id 1
|
||||
|
||||
# 管道模式(仅增量)
|
||||
python -m cli.main --pipeline api_ods_dwd --processing-mode increment_only
|
||||
|
||||
# 管道模式(校验并修复,跳过增量)
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only
|
||||
|
||||
# 管道模式(校验并修复,先从 API 获取数据)
|
||||
python -m cli.main --pipeline api_full --processing-mode verify_only --fetch-before-verify
|
||||
|
||||
# 管道模式(增量+校验并修复)
|
||||
python -m cli.main --pipeline api_full --processing-mode increment_verify
|
||||
|
||||
# 指定时间窗口
|
||||
python -m cli.main --pipeline api_ods --window-start "2026-02-01" --window-end "2026-02-02"
|
||||
""",
|
||||
)
|
||||
|
||||
# 基本参数
|
||||
parser.add_argument("--store-id", type=int, help="门店ID")
|
||||
parser.add_argument("--tasks", help="任务列表,逗号分隔(传统模式)")
|
||||
parser.add_argument("--dry-run", action="store_true", help="试运行(不提交)")
|
||||
|
||||
# 管道参数(新增)
|
||||
parser.add_argument(
|
||||
"--pipeline",
|
||||
choices=PIPELINE_CHOICES,
|
||||
help="管道类型:api_ods, api_ods_dwd, api_full, ods_dwd, dwd_dws, dwd_dws_index, dwd_index",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--processing-mode",
|
||||
dest="processing_mode",
|
||||
choices=PROCESSING_MODE_CHOICES,
|
||||
default="increment_only",
|
||||
help="处理模式:increment_only(仅增量)/ verify_only(校验并修复)/ increment_verify(增量+校验并修复)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fetch-before-verify",
|
||||
dest="fetch_before_verify",
|
||||
action="store_true",
|
||||
help="校验前先从 API 获取数据(仅在 verify_only 模式下有效)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verify-tables",
|
||||
dest="verify_tables",
|
||||
help="仅校验指定表(逗号分隔),用于单表验证",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-split",
|
||||
dest="window_split",
|
||||
choices=WINDOW_SPLIT_CHOICES,
|
||||
default="none",
|
||||
help="时间窗口切分:none(不切分)/ day / week / month",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lookback-hours",
|
||||
dest="lookback_hours",
|
||||
type=int,
|
||||
default=24,
|
||||
help="回溯小时数(默认24小时)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overlap-seconds",
|
||||
dest="overlap_seconds",
|
||||
type=int,
|
||||
default=3600,
|
||||
help="冗余秒数(默认3600秒=1小时)",
|
||||
)
|
||||
|
||||
# 数据库参数
|
||||
parser.add_argument("--pg-dsn", help="PostgreSQL DSN")
|
||||
parser.add_argument("--pg-host", help="PostgreSQL主机")
|
||||
parser.add_argument("--pg-port", type=int, help="PostgreSQL端口")
|
||||
parser.add_argument("--pg-name", help="PostgreSQL数据库名")
|
||||
parser.add_argument("--pg-user", help="PostgreSQL用户名")
|
||||
parser.add_argument("--pg-password", help="PostgreSQL密码")
|
||||
|
||||
# API参数
|
||||
parser.add_argument("--api-base", help="API基础URL")
|
||||
parser.add_argument("--api-token", "--token", dest="api_token", help="API令牌(Bearer Token)")
|
||||
parser.add_argument("--api-timeout", type=int, help="API超时(秒)")
|
||||
parser.add_argument("--api-page-size", type=int, help="分页大小")
|
||||
parser.add_argument("--api-retry-max", type=int, help="API重试最大次数")
|
||||
|
||||
# 回溯/手动窗口
|
||||
parser.add_argument(
|
||||
"--window-start",
|
||||
dest="window_start",
|
||||
help="固定时间窗口开始(优先级高于游标,例如:2025-07-01 00:00:00)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-end",
|
||||
dest="window_end",
|
||||
help="固定时间窗口结束(优先级高于游标,推荐用月末+1,例如:2025-08-01 00:00:00)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force-window-override",
|
||||
action="store_true",
|
||||
help="强制使用 window_start/window_end,不走 MAX(fetched_at) 兜底",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-split-unit",
|
||||
dest="window_split_unit",
|
||||
help="窗口切分单位(day/week/month/none),默认来自配置 run.window_split.unit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-split-days",
|
||||
dest="window_split_days",
|
||||
type=int,
|
||||
choices=[1, 10, 30],
|
||||
help="按天切分的天数(1/10/30),默认来自配置 run.window_split.days",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-compensation-hours",
|
||||
dest="window_compensation_hours",
|
||||
type=int,
|
||||
help="窗口前后补偿小时数,默认来自配置 run.window_split.compensation_hours",
|
||||
)
|
||||
|
||||
# 目录参数
|
||||
parser.add_argument("--export-root", help="导出根目录")
|
||||
parser.add_argument("--log-root", help="日志根目录")
|
||||
|
||||
# 数据源模式(新参数,替代 --pipeline-flow)
|
||||
parser.add_argument(
|
||||
"--data-source",
|
||||
dest="data_source",
|
||||
choices=["online", "offline", "hybrid"],
|
||||
default=None,
|
||||
help="数据源模式:online(仅在线抓取)/ offline(仅本地入库)/ hybrid(抓取+入库)",
|
||||
)
|
||||
|
||||
# 抓取/清洗管线(--pipeline-flow 已弃用,请使用 --data-source)
|
||||
parser.add_argument("--pipeline-flow", choices=["FULL", "FETCH_ONLY", "INGEST_ONLY"], help="[已弃用] 请使用 --data-source")
|
||||
parser.add_argument("--fetch-root", help="抓取JSON输出根目录")
|
||||
parser.add_argument("--ingest-source", help="本地清洗入库源目录")
|
||||
parser.add_argument("--write-pretty-json", action="store_true", help="抓取JSON美化输出")
|
||||
|
||||
# 运行窗口
|
||||
parser.add_argument("--idle-start", help="闲时窗口开始(HH:MM)")
|
||||
parser.add_argument("--idle-end", help="闲时窗口结束(HH:MM)")
|
||||
parser.add_argument("--allow-empty-advance", action="store_true", help="允许空结果推进窗口")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def resolve_data_source(args) -> str:
|
||||
"""解析 data_source 参数,处理旧参数 --pipeline-flow 的弃用映射。
|
||||
|
||||
优先级:--data-source > --pipeline-flow > 默认值 hybrid
|
||||
"""
|
||||
_FLOW_TO_DATA_SOURCE = {
|
||||
"FULL": "hybrid",
|
||||
"FETCH_ONLY": "online",
|
||||
"INGEST_ONLY": "offline",
|
||||
}
|
||||
|
||||
if args.data_source:
|
||||
return args.data_source
|
||||
|
||||
if args.pipeline_flow:
|
||||
import warnings
|
||||
mapped = _FLOW_TO_DATA_SOURCE.get(args.pipeline_flow.upper(), "hybrid")
|
||||
warnings.warn(
|
||||
f"--pipeline-flow 已弃用,请使用 --data-source {mapped}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return mapped
|
||||
|
||||
return "hybrid" # 默认值
|
||||
|
||||
|
||||
def build_cli_overrides(args) -> dict:
|
||||
"""从命令行参数构建配置覆盖"""
|
||||
overrides = {}
|
||||
|
||||
# 基本信息
|
||||
if args.store_id is not None:
|
||||
overrides.setdefault("app", {})["store_id"] = args.store_id
|
||||
|
||||
# 数据库
|
||||
if args.pg_dsn:
|
||||
overrides.setdefault("db", {})["dsn"] = args.pg_dsn
|
||||
if args.pg_host:
|
||||
overrides.setdefault("db", {})["host"] = args.pg_host
|
||||
if args.pg_port:
|
||||
overrides.setdefault("db", {})["port"] = args.pg_port
|
||||
if args.pg_name:
|
||||
overrides.setdefault("db", {})["name"] = args.pg_name
|
||||
if args.pg_user:
|
||||
overrides.setdefault("db", {})["user"] = args.pg_user
|
||||
if args.pg_password:
|
||||
overrides.setdefault("db", {})["password"] = args.pg_password
|
||||
|
||||
# API
|
||||
if args.api_base:
|
||||
overrides.setdefault("api", {})["base_url"] = args.api_base
|
||||
if args.api_token:
|
||||
overrides.setdefault("api", {})["token"] = args.api_token
|
||||
if args.api_timeout:
|
||||
overrides.setdefault("api", {})["timeout_sec"] = args.api_timeout
|
||||
if args.api_page_size:
|
||||
overrides.setdefault("api", {})["page_size"] = args.api_page_size
|
||||
if args.api_retry_max:
|
||||
overrides.setdefault("api", {}).setdefault("retries", {})["max_attempts"] = args.api_retry_max
|
||||
|
||||
# 目录
|
||||
if args.export_root:
|
||||
overrides.setdefault("io", {})["export_root"] = args.export_root
|
||||
if args.log_root:
|
||||
overrides.setdefault("io", {})["log_root"] = args.log_root
|
||||
|
||||
# 抓取/清洗管线(旧参数保留向后兼容)
|
||||
if args.pipeline_flow:
|
||||
overrides.setdefault("pipeline", {})["flow"] = args.pipeline_flow.upper()
|
||||
|
||||
# 数据源模式(新参数)
|
||||
data_source = resolve_data_source(args)
|
||||
overrides.setdefault("run", {})["data_source"] = data_source
|
||||
if args.fetch_root:
|
||||
overrides.setdefault("pipeline", {})["fetch_root"] = args.fetch_root
|
||||
if args.ingest_source:
|
||||
overrides.setdefault("pipeline", {})["ingest_source_dir"] = args.ingest_source
|
||||
if args.write_pretty_json:
|
||||
overrides.setdefault("io", {})["write_pretty_json"] = True
|
||||
|
||||
# 回溯/手动窗口
|
||||
if args.window_start or args.window_end:
|
||||
overrides.setdefault("run", {}).setdefault("window_override", {})
|
||||
if args.window_start:
|
||||
overrides["run"]["window_override"]["start"] = args.window_start
|
||||
if args.window_end:
|
||||
overrides["run"]["window_override"]["end"] = args.window_end
|
||||
if args.force_window_override:
|
||||
overrides.setdefault("run", {})["force_window_override"] = True
|
||||
if args.window_split_unit:
|
||||
overrides.setdefault("run", {}).setdefault("window_split", {})["unit"] = args.window_split_unit
|
||||
if args.window_split_days is not None:
|
||||
overrides.setdefault("run", {}).setdefault("window_split", {})["days"] = args.window_split_days
|
||||
if args.window_compensation_hours is not None:
|
||||
overrides.setdefault("run", {}).setdefault("window_split", {})[
|
||||
"compensation_hours"
|
||||
] = args.window_compensation_hours
|
||||
|
||||
# 运行窗口
|
||||
if args.idle_start:
|
||||
overrides.setdefault("run", {}).setdefault("idle_window", {})["start"] = args.idle_start
|
||||
if args.idle_end:
|
||||
overrides.setdefault("run", {}).setdefault("idle_window", {})["end"] = args.idle_end
|
||||
if args.allow_empty_advance:
|
||||
overrides.setdefault("run", {})["allow_empty_result_advance"] = True
|
||||
|
||||
# 任务
|
||||
if args.tasks:
|
||||
tasks = [t.strip().upper() for t in args.tasks.split(",") if t.strip()]
|
||||
overrides.setdefault("run", {})["tasks"] = tasks
|
||||
|
||||
return overrides
|
||||
|
||||
def parse_datetime(s: str) -> datetime:
|
||||
"""解析日期时间字符串"""
|
||||
if not s:
|
||||
return None
|
||||
|
||||
formats = [
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y-%m-%d",
|
||||
"%Y/%m/%d %H:%M:%S",
|
||||
"%Y/%m/%d",
|
||||
]
|
||||
|
||||
for fmt in formats:
|
||||
try:
|
||||
return datetime.strptime(s, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
raise ValueError(f"无法解析日期时间: {s}")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数
|
||||
|
||||
资源生命周期由 CLI 层统一管理(try/finally),
|
||||
TaskExecutor / PipelineRunner 通过依赖注入接收已创建的资源。
|
||||
"""
|
||||
logger = setup_logging()
|
||||
args = parse_args()
|
||||
|
||||
try:
|
||||
# 加载配置
|
||||
cli_overrides = build_cli_overrides(args)
|
||||
config = AppConfig.load(cli_overrides)
|
||||
|
||||
logger.info("配置加载完成")
|
||||
logger.info("门店ID: %s", config.get('app.store_id'))
|
||||
|
||||
# ── 创建资源 ──────────────────────────────────────────
|
||||
db_conn = DatabaseConnection(
|
||||
dsn=config["db"]["dsn"],
|
||||
session=config["db"].get("session"),
|
||||
connect_timeout=config["db"].get("connect_timeout_sec"),
|
||||
)
|
||||
api_client = APIClient(
|
||||
base_url=config["api"]["base_url"],
|
||||
token=config["api"]["token"],
|
||||
timeout=config["api"].get("timeout_sec", 20),
|
||||
retry_max=config["api"].get("retries", {}).get("max_attempts", 3),
|
||||
headers_extra=config["api"].get("headers_extra"),
|
||||
)
|
||||
|
||||
try:
|
||||
# ── 组装依赖 ──────────────────────────────────────
|
||||
db_ops = DatabaseOperations(db_conn)
|
||||
cursor_mgr = CursorManager(db_conn)
|
||||
run_tracker = RunTracker(db_conn)
|
||||
registry = default_registry
|
||||
|
||||
executor = TaskExecutor(
|
||||
config, db_ops, api_client,
|
||||
cursor_mgr, run_tracker, registry, logger,
|
||||
)
|
||||
|
||||
data_source = resolve_data_source(args)
|
||||
|
||||
# ── 判断执行模式 ──────────────────────────────────
|
||||
if args.pipeline:
|
||||
# 管道模式
|
||||
logger.info("执行模式: 管道模式")
|
||||
logger.info("管道类型: %s", args.pipeline)
|
||||
logger.info("处理模式: %s", args.processing_mode)
|
||||
|
||||
# 解析时间窗口
|
||||
window_start = None
|
||||
window_end = None
|
||||
|
||||
if args.window_start:
|
||||
window_start = parse_datetime(args.window_start)
|
||||
if args.window_end:
|
||||
window_end = parse_datetime(args.window_end)
|
||||
|
||||
# 如果没有指定时间窗口,使用回溯
|
||||
if window_start is None and window_end is None:
|
||||
from datetime import timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
tz = ZoneInfo(config.get("app.timezone", "Asia/Shanghai"))
|
||||
window_end = datetime.now(tz)
|
||||
window_start = window_end - timedelta(hours=args.lookback_hours)
|
||||
logger.info("使用回溯时间窗口: %s ~ %s", window_start, window_end)
|
||||
|
||||
# 将回溯窗口设置为 window_override,确保 ODS 任务使用指定窗口
|
||||
config.config.setdefault("run", {}).setdefault("window_override", {})
|
||||
config.config["run"]["window_override"]["start"] = window_start
|
||||
config.config["run"]["window_override"]["end"] = window_end
|
||||
|
||||
# 任务过滤器
|
||||
task_codes = None
|
||||
if args.tasks:
|
||||
task_codes = [t.strip().upper() for t in args.tasks.split(",") if t.strip()]
|
||||
|
||||
# 校验表过滤
|
||||
verify_tables = None
|
||||
if args.verify_tables:
|
||||
verify_tables = [t.strip().lower() for t in args.verify_tables.split(",") if t.strip()]
|
||||
|
||||
# 组装 PipelineRunner 并执行
|
||||
runner = PipelineRunner(
|
||||
config, executor, registry,
|
||||
db_conn, api_client, logger,
|
||||
)
|
||||
result = runner.run(
|
||||
pipeline=args.pipeline,
|
||||
processing_mode=args.processing_mode,
|
||||
data_source=data_source,
|
||||
window_start=window_start,
|
||||
window_end=window_end,
|
||||
window_split=args.window_split if args.window_split != "none" else None,
|
||||
task_codes=task_codes,
|
||||
fetch_before_verify=args.fetch_before_verify,
|
||||
verify_tables=verify_tables,
|
||||
)
|
||||
|
||||
logger.info("管道执行完成: %s", result.get("status"))
|
||||
|
||||
else:
|
||||
# 传统模式
|
||||
logger.info("执行模式: 传统模式")
|
||||
task_codes = config.get("run.tasks")
|
||||
logger.info("任务列表: %s", task_codes)
|
||||
|
||||
executor.run_tasks(task_codes, data_source=data_source)
|
||||
|
||||
finally:
|
||||
# 确保资源释放(需求 6.1, 6.4)
|
||||
db_conn.close()
|
||||
|
||||
logger.info("ETL运行完成")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error("ETL运行失败: %s", e, exc_info=True)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
0
apps/etl/pipelines/feiqiu/config/__init__.py
Normal file
0
apps/etl/pipelines/feiqiu/config/__init__.py
Normal file
177
apps/etl/pipelines/feiqiu/config/defaults.py
Normal file
177
apps/etl/pipelines/feiqiu/config/defaults.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""配置默认值定义"""
|
||||
|
||||
DEFAULTS = {
|
||||
"app": {
|
||||
"timezone": "Asia/Shanghai",
|
||||
"store_id": "",
|
||||
"schema_oltp": "billiards",
|
||||
"schema_etl": "etl_admin",
|
||||
},
|
||||
"db": {
|
||||
"dsn": "",
|
||||
"host": "",
|
||||
"port": "",
|
||||
"name": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"connect_timeout_sec": 20,
|
||||
"batch_size": 1000,
|
||||
"session": {
|
||||
"timezone": "Asia/Shanghai",
|
||||
"statement_timeout_ms": 30000,
|
||||
"lock_timeout_ms": 5000,
|
||||
"idle_in_tx_timeout_ms": 600000,
|
||||
},
|
||||
},
|
||||
"api": {
|
||||
"base_url": "https://pc.ficoo.vip/apiprod/admin/v1",
|
||||
"token": None,
|
||||
"timeout_sec": 20,
|
||||
"page_size": 200,
|
||||
"params": {},
|
||||
"retries": {
|
||||
"max_attempts": 3,
|
||||
"backoff_sec": [1, 2, 4],
|
||||
},
|
||||
"headers_extra": {},
|
||||
},
|
||||
"run": {
|
||||
"data_source": "hybrid",
|
||||
"tasks": [
|
||||
"PRODUCTS",
|
||||
"TABLES",
|
||||
"MEMBERS",
|
||||
"ASSISTANTS",
|
||||
"PACKAGES_DEF",
|
||||
"ORDERS",
|
||||
"PAYMENTS",
|
||||
"REFUNDS",
|
||||
"COUPON_USAGE",
|
||||
"INVENTORY_CHANGE",
|
||||
"TOPUPS",
|
||||
"TABLE_DISCOUNT",
|
||||
"ASSISTANT_ABOLISH",
|
||||
"LEDGER",
|
||||
],
|
||||
"dws_tasks": [],
|
||||
"index_tasks": [],
|
||||
"index_lookback_days": 60,
|
||||
"window_minutes": {
|
||||
"default_busy": 30,
|
||||
"default_idle": 180,
|
||||
},
|
||||
"overlap_seconds": 600,
|
||||
"snapshot_missing_delete": True,
|
||||
"snapshot_allow_empty_delete": False,
|
||||
"window_split": {
|
||||
"unit": "day",
|
||||
"days": 10,
|
||||
"compensation_hours": 2,
|
||||
},
|
||||
"idle_window": {
|
||||
"start": "04:00",
|
||||
"end": "16:00",
|
||||
},
|
||||
"allow_empty_result_advance": True,
|
||||
},
|
||||
"io": {
|
||||
"export_root": "export/JSON",
|
||||
"log_root": "export/LOG",
|
||||
"fetch_root": "export/JSON",
|
||||
"ingest_source_dir": "",
|
||||
"manifest_name": "manifest.json",
|
||||
"ingest_report_name": "ingest_report.json",
|
||||
"write_pretty_json": True,
|
||||
"max_file_bytes": 50 * 1024 * 1024,
|
||||
},
|
||||
"pipeline": {
|
||||
# 运行流程:FETCH_ONLY(仅在线抓取落盘)、INGEST_ONLY(本地清洗入库)、FULL(抓取 + 清洗入库)
|
||||
"flow": "FULL",
|
||||
# 在线抓取 JSON 输出根目录(按任务、run_id 与时间自动创建子目录)
|
||||
"fetch_root": "export/JSON",
|
||||
# 本地清洗入库时的 JSON 输入目录(为空则默认使用本次抓取目录)
|
||||
"ingest_source_dir": "",
|
||||
},
|
||||
"clean": {
|
||||
"log_unknown_fields": True,
|
||||
"unknown_fields_limit": 50,
|
||||
"hash_key": {
|
||||
"algo": "sha1",
|
||||
"salt": "",
|
||||
},
|
||||
"strict_numeric": True,
|
||||
"round_money_scale": 2,
|
||||
},
|
||||
"security": {
|
||||
"redact_in_logs": True,
|
||||
"redact_keys": ["token", "password", "Authorization"],
|
||||
"echo_token_in_logs": False,
|
||||
},
|
||||
"ods": {
|
||||
# ODS 离线重建/回放相关(仅开发/运维使用)
|
||||
"json_doc_dir": "export/test-json-doc",
|
||||
"include_files": "",
|
||||
"drop_schema_first": True,
|
||||
},
|
||||
"integrity": {
|
||||
"mode": "history",
|
||||
"history_start": "2025-07-01",
|
||||
"history_end": "",
|
||||
"include_dimensions": True,
|
||||
"auto_check": False,
|
||||
"auto_backfill": False,
|
||||
"compare_content": True,
|
||||
"content_sample_limit": 50,
|
||||
"backfill_mismatch": True,
|
||||
"recheck_after_backfill": True,
|
||||
"ods_task_codes": "",
|
||||
"force_monthly_split": True,
|
||||
},
|
||||
"verification": {
|
||||
"skip_ods_when_fetch_before_verify": True,
|
||||
"ods_use_local_json": True,
|
||||
},
|
||||
"dws": {
|
||||
"monthly": {
|
||||
"allow_history": False,
|
||||
"prev_month_grace_days": 5,
|
||||
"history_months": 0,
|
||||
"new_hire_cap_effective_from": "2026-03-01",
|
||||
"new_hire_cap_day": 25,
|
||||
"new_hire_max_tier_level": 2,
|
||||
},
|
||||
"salary": {
|
||||
"run_days": 5,
|
||||
"allow_out_of_cycle": False,
|
||||
"room_course_price": 138,
|
||||
},
|
||||
},
|
||||
"dwd": {
|
||||
"fact_upsert": True,
|
||||
# 事实表补齐 UPSERT 批量参数(可按锁冲突情况调优)
|
||||
"fact_upsert_batch_size": 1000,
|
||||
"fact_upsert_min_batch_size": 100,
|
||||
"fact_upsert_max_retries": 2,
|
||||
"fact_upsert_retry_backoff_sec": [1, 2, 4],
|
||||
# 仅对事实表 backfill 设置的锁等待超时(None 表示沿用 db.session.lock_timeout_ms)
|
||||
"fact_upsert_lock_timeout_ms": None,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
# 任务代码常量
|
||||
TASK_ORDERS = "ORDERS"
|
||||
TASK_PAYMENTS = "PAYMENTS"
|
||||
TASK_REFUNDS = "REFUNDS"
|
||||
TASK_INVENTORY_CHANGE = "INVENTORY_CHANGE"
|
||||
TASK_COUPON_USAGE = "COUPON_USAGE"
|
||||
TASK_MEMBERS = "MEMBERS"
|
||||
TASK_ASSISTANTS = "ASSISTANTS"
|
||||
TASK_PRODUCTS = "PRODUCTS"
|
||||
TASK_TABLES = "TABLES"
|
||||
TASK_PACKAGES_DEF = "PACKAGES_DEF"
|
||||
TASK_TOPUPS = "TOPUPS"
|
||||
TASK_TABLE_DISCOUNT = "TABLE_DISCOUNT"
|
||||
TASK_ASSISTANT_ABOLISH = "ASSISTANT_ABOLISH"
|
||||
TASK_LEDGER = "LEDGER"
|
||||
213
apps/etl/pipelines/feiqiu/config/env_parser.py
Normal file
213
apps/etl/pipelines/feiqiu/config/env_parser.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""环境变量解析"""
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
|
||||
ENV_MAP = {
|
||||
"TIMEZONE": ("app.timezone",),
|
||||
"STORE_ID": ("app.store_id",),
|
||||
"SCHEMA_OLTP": ("app.schema_oltp",),
|
||||
"SCHEMA_ETL": ("app.schema_etl",),
|
||||
"PG_DSN": ("db.dsn",),
|
||||
"PG_HOST": ("db.host",),
|
||||
"PG_PORT": ("db.port",),
|
||||
"PG_NAME": ("db.name",),
|
||||
"PG_USER": ("db.user",),
|
||||
"PG_PASSWORD": ("db.password",),
|
||||
"PG_CONNECT_TIMEOUT": ("db.connect_timeout_sec",),
|
||||
"API_BASE": ("api.base_url",),
|
||||
"API_TOKEN": ("api.token",),
|
||||
"FICOO_TOKEN": ("api.token",),
|
||||
"API_TIMEOUT": ("api.timeout_sec",),
|
||||
"API_PAGE_SIZE": ("api.page_size",),
|
||||
"API_RETRY_MAX": ("api.retries.max_attempts",),
|
||||
"API_RETRY_BACKOFF": ("api.retries.backoff_sec",),
|
||||
"API_PARAMS": ("api.params",),
|
||||
"EXPORT_ROOT": ("io.export_root",),
|
||||
"LOG_ROOT": ("io.log_root",),
|
||||
"MANIFEST_NAME": ("io.manifest_name",),
|
||||
"INGEST_REPORT_NAME": ("io.ingest_report_name",),
|
||||
"WRITE_PRETTY_JSON": ("io.write_pretty_json",),
|
||||
"RUN_TASKS": ("run.tasks",),
|
||||
"RUN_DWS_TASKS": ("run.dws_tasks",),
|
||||
"RUN_INDEX_TASKS": ("run.index_tasks",),
|
||||
"INDEX_LOOKBACK_DAYS": ("run.index_lookback_days",),
|
||||
"OVERLAP_SECONDS": ("run.overlap_seconds",),
|
||||
"WINDOW_BUSY_MIN": ("run.window_minutes.default_busy",),
|
||||
"WINDOW_IDLE_MIN": ("run.window_minutes.default_idle",),
|
||||
"IDLE_START": ("run.idle_window.start",),
|
||||
"IDLE_END": ("run.idle_window.end",),
|
||||
"IDLE_WINDOW_START": ("run.idle_window.start",),
|
||||
"IDLE_WINDOW_END": ("run.idle_window.end",),
|
||||
"ALLOW_EMPTY_RESULT_ADVANCE": ("run.allow_empty_result_advance",),
|
||||
"ALLOW_EMPTY_ADVANCE": ("run.allow_empty_result_advance",),
|
||||
"SNAPSHOT_MISSING_DELETE": ("run.snapshot_missing_delete",),
|
||||
"SNAPSHOT_ALLOW_EMPTY_DELETE": ("run.snapshot_allow_empty_delete",),
|
||||
"WINDOW_START": ("run.window_override.start",),
|
||||
"WINDOW_END": ("run.window_override.end",),
|
||||
"WINDOW_SPLIT_UNIT": ("run.window_split.unit",),
|
||||
"WINDOW_SPLIT_DAYS": ("run.window_split.days",),
|
||||
"WINDOW_COMPENSATION_HOURS": ("run.window_split.compensation_hours",),
|
||||
"PIPELINE_FLOW": ("pipeline.flow",),
|
||||
"JSON_FETCH_ROOT": ("pipeline.fetch_root",),
|
||||
"JSON_SOURCE_DIR": ("pipeline.ingest_source_dir",),
|
||||
"FETCH_ROOT": ("pipeline.fetch_root",),
|
||||
"INGEST_SOURCE_DIR": ("pipeline.ingest_source_dir",),
|
||||
"INTEGRITY_MODE": ("integrity.mode",),
|
||||
"INTEGRITY_HISTORY_START": ("integrity.history_start",),
|
||||
"INTEGRITY_HISTORY_END": ("integrity.history_end",),
|
||||
"INTEGRITY_INCLUDE_DIMENSIONS": ("integrity.include_dimensions",),
|
||||
"INTEGRITY_AUTO_CHECK": ("integrity.auto_check",),
|
||||
"INTEGRITY_AUTO_BACKFILL": ("integrity.auto_backfill",),
|
||||
"INTEGRITY_COMPARE_CONTENT": ("integrity.compare_content",),
|
||||
"INTEGRITY_CONTENT_SAMPLE_LIMIT": ("integrity.content_sample_limit",),
|
||||
"INTEGRITY_BACKFILL_MISMATCH": ("integrity.backfill_mismatch",),
|
||||
"INTEGRITY_RECHECK_AFTER_BACKFILL": ("integrity.recheck_after_backfill",),
|
||||
"INTEGRITY_ODS_TASK_CODES": ("integrity.ods_task_codes",),
|
||||
"VERIFY_SKIP_ODS_ON_FETCH": ("verification.skip_ods_when_fetch_before_verify",),
|
||||
"VERIFY_ODS_LOCAL_JSON": ("verification.ods_use_local_json",),
|
||||
"DWD_FACT_UPSERT": ("dwd.fact_upsert",),
|
||||
# DWS 月度/薪资配置
|
||||
"DWS_MONTHLY_ALLOW_HISTORY": ("dws.monthly.allow_history",),
|
||||
"DWS_MONTHLY_PREV_GRACE_DAYS": ("dws.monthly.prev_month_grace_days",),
|
||||
"DWS_MONTHLY_HISTORY_MONTHS": ("dws.monthly.history_months",),
|
||||
"DWS_MONTHLY_NEW_HIRE_CAP_EFFECTIVE_FROM": ("dws.monthly.new_hire_cap_effective_from",),
|
||||
"DWS_MONTHLY_NEW_HIRE_CAP_DAY": ("dws.monthly.new_hire_cap_day",),
|
||||
"DWS_MONTHLY_NEW_HIRE_MAX_TIER_LEVEL": ("dws.monthly.new_hire_max_tier_level",),
|
||||
"DWS_SALARY_RUN_DAYS": ("dws.salary.run_days",),
|
||||
"DWS_SALARY_ALLOW_OUT_OF_CYCLE": ("dws.salary.allow_out_of_cycle",),
|
||||
"DWS_SALARY_ROOM_COURSE_PRICE": ("dws.salary.room_course_price",),
|
||||
# ODS 离线回放配置
|
||||
"ODS_JSON_DOC_DIR": ("ods.json_doc_dir",),
|
||||
"ODS_INCLUDE_FILES": ("ods.include_files",),
|
||||
"ODS_DROP_SCHEMA_FIRST": ("ods.drop_schema_first",),
|
||||
}
|
||||
|
||||
|
||||
def _deep_set(d, dotted_keys, value):
|
||||
cur = d
|
||||
for k in dotted_keys[:-1]:
|
||||
cur = cur.setdefault(k, {})
|
||||
cur[dotted_keys[-1]] = value
|
||||
|
||||
|
||||
def _coerce_env(v: str):
|
||||
if v is None:
|
||||
return None
|
||||
s = v.strip()
|
||||
if s.lower() in ("true", "false"):
|
||||
return s.lower() == "true"
|
||||
try:
|
||||
if s.isdigit() or (s.startswith("-") and s[1:].isdigit()):
|
||||
return int(s)
|
||||
except Exception:
|
||||
pass
|
||||
if (s.startswith("{") and s.endswith("}")) or (s.startswith("[") and s.endswith("]")):
|
||||
try:
|
||||
return json.loads(s)
|
||||
except Exception:
|
||||
return s
|
||||
return s
|
||||
|
||||
|
||||
def _strip_inline_comment(value: str) -> str:
|
||||
"""去掉未被引号包裹的内联注释"""
|
||||
result = []
|
||||
in_quote = False
|
||||
quote_char = ""
|
||||
escape = False
|
||||
for ch in value:
|
||||
if escape:
|
||||
result.append(ch)
|
||||
escape = False
|
||||
continue
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
result.append(ch)
|
||||
continue
|
||||
if ch in ("'", '"'):
|
||||
if not in_quote:
|
||||
in_quote = True
|
||||
quote_char = ch
|
||||
elif quote_char == ch:
|
||||
in_quote = False
|
||||
quote_char = ""
|
||||
result.append(ch)
|
||||
continue
|
||||
if ch == "#" and not in_quote:
|
||||
break
|
||||
result.append(ch)
|
||||
return "".join(result).rstrip()
|
||||
|
||||
|
||||
def _unquote_value(value: str) -> str:
|
||||
"""处理引号/原始字符串以及尾随逗号"""
|
||||
trimmed = value.strip()
|
||||
trimmed = _strip_inline_comment(trimmed)
|
||||
trimmed = trimmed.rstrip(",").rstrip()
|
||||
if not trimmed:
|
||||
return trimmed
|
||||
if len(trimmed) >= 2 and trimmed[0] in ("'", '"') and trimmed[-1] == trimmed[0]:
|
||||
return trimmed[1:-1]
|
||||
if (
|
||||
len(trimmed) >= 3
|
||||
and trimmed[0] in ("r", "R")
|
||||
and trimmed[1] in ("'", '"')
|
||||
and trimmed[-1] == trimmed[1]
|
||||
):
|
||||
return trimmed[2:-1]
|
||||
return trimmed
|
||||
|
||||
|
||||
def _parse_dotenv_line(line: str) -> tuple[str, str] | None:
|
||||
"""解析 .env 文件中的单行"""
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
return None
|
||||
if stripped.startswith("export "):
|
||||
stripped = stripped[len("export ") :].strip()
|
||||
if "=" not in stripped:
|
||||
return None
|
||||
key, value = stripped.split("=", 1)
|
||||
key = key.strip()
|
||||
value = _unquote_value(value)
|
||||
return key, value
|
||||
|
||||
|
||||
def _load_dotenv_values() -> dict:
|
||||
"""从项目根目录读取 .env 文件键值"""
|
||||
if os.environ.get("ETL_SKIP_DOTENV") in ("1", "true", "TRUE", "True"):
|
||||
return {}
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
dotenv_path = root / ".env"
|
||||
if not dotenv_path.exists():
|
||||
return {}
|
||||
values: dict[str, str] = {}
|
||||
for line in dotenv_path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
parsed = _parse_dotenv_line(line)
|
||||
if parsed:
|
||||
key, value = parsed
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
def _apply_env_values(cfg: dict, source: dict):
|
||||
for env_key, dotted in ENV_MAP.items():
|
||||
val = source.get(env_key)
|
||||
if val is None:
|
||||
continue
|
||||
v2 = _coerce_env(val)
|
||||
for path in dotted:
|
||||
if path in ("run.tasks", "run.dws_tasks", "run.index_tasks") and isinstance(v2, str):
|
||||
v2 = [item.strip() for item in v2.split(",") if item.strip()]
|
||||
_deep_set(cfg, path.split("."), v2)
|
||||
|
||||
|
||||
def load_env_overrides(defaults: dict) -> dict:
|
||||
cfg = deepcopy(defaults)
|
||||
# 先读取 .env,再读取真实环境变量,确保 CLI 仍然最高优先级
|
||||
_apply_env_values(cfg, _load_dotenv_values())
|
||||
_apply_env_values(cfg, os.environ)
|
||||
return cfg
|
||||
3
apps/etl/pipelines/feiqiu/config/scheduled_tasks.json
Normal file
3
apps/etl/pipelines/feiqiu/config/scheduled_tasks.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"tasks": {}
|
||||
}
|
||||
127
apps/etl/pipelines/feiqiu/config/settings.py
Normal file
127
apps/etl/pipelines/feiqiu/config/settings.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""配置管理主类"""
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
from .defaults import DEFAULTS
|
||||
from .env_parser import load_env_overrides
|
||||
|
||||
# pipeline.flow → run.data_source 值映射
|
||||
_FLOW_TO_DATA_SOURCE = {
|
||||
"FULL": "hybrid",
|
||||
"FETCH_ONLY": "online",
|
||||
"INGEST_ONLY": "offline",
|
||||
}
|
||||
|
||||
class AppConfig:
|
||||
"""应用配置管理器"""
|
||||
|
||||
def __init__(self, config_dict: dict):
|
||||
self.config = config_dict
|
||||
|
||||
@classmethod
|
||||
def load(cls, cli_overrides: dict = None):
|
||||
"""加载配置: DEFAULTS < ENV < CLI"""
|
||||
cfg = load_env_overrides(DEFAULTS)
|
||||
|
||||
if cli_overrides:
|
||||
cls._deep_merge(cfg, cli_overrides)
|
||||
|
||||
# 规范化
|
||||
cls._normalize(cfg)
|
||||
cls._validate(cfg)
|
||||
|
||||
return cls(cfg)
|
||||
|
||||
@staticmethod
|
||||
def _deep_merge(dst, src):
|
||||
"""深度合并字典"""
|
||||
for k, v in src.items():
|
||||
if isinstance(v, dict) and isinstance(dst.get(k), dict):
|
||||
AppConfig._deep_merge(dst[k], v)
|
||||
else:
|
||||
dst[k] = v
|
||||
|
||||
@staticmethod
|
||||
def _normalize(cfg):
|
||||
"""规范化配置"""
|
||||
# 转换 store_id 为整数
|
||||
try:
|
||||
cfg["app"]["store_id"] = int(str(cfg["app"]["store_id"]).strip())
|
||||
except Exception:
|
||||
raise SystemExit("app.store_id 必须为整数")
|
||||
|
||||
# DSN 组装
|
||||
if not cfg["db"]["dsn"]:
|
||||
cfg["db"]["dsn"] = (
|
||||
f"postgresql://{cfg['db']['user']}:{cfg['db']['password']}"
|
||||
f"@{cfg['db']['host']}:{cfg['db']['port']}/{cfg['db']['name']}"
|
||||
)
|
||||
|
||||
# connect_timeout 限定 1-20 秒
|
||||
try:
|
||||
timeout_sec = int(cfg["db"].get("connect_timeout_sec") or 5)
|
||||
except Exception:
|
||||
raise SystemExit("db.connect_timeout_sec 必须为整数")
|
||||
cfg["db"]["connect_timeout_sec"] = max(1, min(timeout_sec, 20))
|
||||
|
||||
# 会话参数
|
||||
cfg["db"].setdefault("session", {})
|
||||
sess = cfg["db"]["session"]
|
||||
sess.setdefault("timezone", cfg["app"]["timezone"])
|
||||
|
||||
for k in ("statement_timeout_ms", "lock_timeout_ms", "idle_in_tx_timeout_ms"):
|
||||
if k in sess and sess[k] is not None:
|
||||
try:
|
||||
sess[k] = int(sess[k])
|
||||
except Exception:
|
||||
raise SystemExit(f"db.session.{k} 需为整数毫秒")
|
||||
|
||||
# ── 旧键 → 新键 兼容映射 ──
|
||||
pipeline = cfg.get("pipeline", {})
|
||||
run = cfg.setdefault("run", {})
|
||||
io = cfg.setdefault("io", {})
|
||||
|
||||
# 1. pipeline.flow → run.data_source
|
||||
# 仅当新键未被显式设置(缺失或仍为默认值 hybrid)时,才用旧键覆盖
|
||||
old_flow = str(pipeline.get("flow", "")).upper()
|
||||
if old_flow in _FLOW_TO_DATA_SOURCE:
|
||||
mapped = _FLOW_TO_DATA_SOURCE[old_flow]
|
||||
if run.get("data_source", "hybrid") == "hybrid" and mapped != "hybrid":
|
||||
run["data_source"] = mapped
|
||||
warnings.warn(
|
||||
f"配置键 pipeline.flow={old_flow} 已弃用,"
|
||||
f"已映射为 run.data_source={mapped}",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# 2. pipeline.fetch_root → io.fetch_root(新键优先)
|
||||
if pipeline.get("fetch_root") and not io.get("fetch_root"):
|
||||
io["fetch_root"] = pipeline["fetch_root"]
|
||||
|
||||
# 3. pipeline.ingest_source_dir → io.ingest_source_dir(新键优先)
|
||||
if pipeline.get("ingest_source_dir") and not io.get("ingest_source_dir"):
|
||||
io["ingest_source_dir"] = pipeline["ingest_source_dir"]
|
||||
|
||||
@staticmethod
|
||||
def _validate(cfg):
|
||||
"""验证必填配置"""
|
||||
missing = []
|
||||
if not cfg["app"]["store_id"]:
|
||||
missing.append("app.store_id")
|
||||
if missing:
|
||||
raise SystemExit("缺少必需配置: " + ", ".join(missing))
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
"""获取配置值(支持点号路径)"""
|
||||
keys = key.split(".")
|
||||
val = self.config
|
||||
for k in keys:
|
||||
if isinstance(val, dict):
|
||||
val = val.get(k)
|
||||
else:
|
||||
return default
|
||||
return val if val is not None else default
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.config[key]
|
||||
0
apps/etl/pipelines/feiqiu/database/__init__.py
Normal file
0
apps/etl/pipelines/feiqiu/database/__init__.py
Normal file
112
apps/etl/pipelines/feiqiu/database/base.py
Normal file
112
apps/etl/pipelines/feiqiu/database/base.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据库操作(批量、RETURNING支持)
|
||||
"""
|
||||
import re
|
||||
from typing import List, Dict, Tuple
|
||||
import psycopg2.extras
|
||||
from .connection import DatabaseConnection
|
||||
|
||||
|
||||
class DatabaseOperations(DatabaseConnection):
|
||||
"""扩展数据库操作(包含批量upsert和returning支持)"""
|
||||
|
||||
def batch_execute(self, sql: str, rows: List[Dict], page_size: int = 1000):
|
||||
"""批量执行SQL(不带RETURNING)"""
|
||||
if not rows:
|
||||
return
|
||||
with self.conn.cursor() as c:
|
||||
psycopg2.extras.execute_batch(c, sql, rows, page_size=page_size)
|
||||
|
||||
def batch_upsert_with_returning(self, sql: str, rows: List[Dict], page_size: int = 1000) -> Tuple[int, int]:
|
||||
"""
|
||||
批量 UPSERT 并统计插入/更新数
|
||||
|
||||
Args:
|
||||
sql: 包含RETURNING子句的SQL
|
||||
rows: 数据行列表
|
||||
page_size: 批次大小
|
||||
|
||||
Returns:
|
||||
(inserted_count, updated_count) 元组
|
||||
"""
|
||||
if not rows:
|
||||
return (0, 0)
|
||||
|
||||
use_returning = "RETURNING" in sql.upper()
|
||||
|
||||
with self.conn.cursor() as c:
|
||||
if not use_returning:
|
||||
psycopg2.extras.execute_batch(c, sql, rows, page_size=page_size)
|
||||
return (0, 0)
|
||||
|
||||
# 优先尝试向量化执行
|
||||
try:
|
||||
inserted, updated = self._execute_with_returning_vectorized(c, sql, rows, page_size)
|
||||
return (inserted, updated)
|
||||
except Exception:
|
||||
# 回退到逐行执行
|
||||
return self._execute_with_returning_row_by_row(c, sql, rows)
|
||||
|
||||
def _execute_with_returning_vectorized(self, cursor, sql: str, rows: List[Dict], page_size: int) -> Tuple[int, int]:
|
||||
"""向量化执行(使用execute_values)"""
|
||||
# 解析VALUES子句
|
||||
m = re.search(r"VALUES\s*\((.*?)\)", sql, flags=re.IGNORECASE | re.DOTALL)
|
||||
if not m:
|
||||
raise ValueError("Cannot parse VALUES clause")
|
||||
|
||||
tpl = "(" + m.group(1) + ")"
|
||||
base_sql = sql[:m.start()] + "VALUES %s" + sql[m.end():]
|
||||
|
||||
ret = psycopg2.extras.execute_values(
|
||||
cursor, base_sql, rows, template=tpl, page_size=page_size, fetch=True
|
||||
)
|
||||
|
||||
if not ret:
|
||||
return (0, 0)
|
||||
|
||||
inserted = 0
|
||||
for rec in ret:
|
||||
flag = self._extract_inserted_flag(rec)
|
||||
if flag:
|
||||
inserted += 1
|
||||
|
||||
return (inserted, len(ret) - inserted)
|
||||
|
||||
def _execute_with_returning_row_by_row(self, cursor, sql: str, rows: List[Dict]) -> Tuple[int, int]:
|
||||
"""逐行执行(回退方案)"""
|
||||
inserted = 0
|
||||
updated = 0
|
||||
|
||||
for r in rows:
|
||||
cursor.execute(sql, r)
|
||||
try:
|
||||
rec = cursor.fetchone()
|
||||
except Exception:
|
||||
rec = None
|
||||
|
||||
flag = self._extract_inserted_flag(rec) if rec else None
|
||||
|
||||
if flag:
|
||||
inserted += 1
|
||||
else:
|
||||
updated += 1
|
||||
|
||||
return (inserted, updated)
|
||||
|
||||
@staticmethod
|
||||
def _extract_inserted_flag(rec) -> bool:
|
||||
"""从返回记录中提取inserted标志"""
|
||||
if isinstance(rec, tuple):
|
||||
return bool(rec[0])
|
||||
elif isinstance(rec, dict):
|
||||
return bool(rec.get("inserted"))
|
||||
else:
|
||||
try:
|
||||
return bool(rec["inserted"])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# 为了向后兼容,提供Pg别名
|
||||
Pg = DatabaseOperations
|
||||
80
apps/etl/pipelines/feiqiu/database/connection.py
Normal file
80
apps/etl/pipelines/feiqiu/database/connection.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接管理器(限制最大连接超时时间)。"""
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
|
||||
class DatabaseConnection:
|
||||
"""封装 psycopg2 连接,支持会话参数和超时保护。"""
|
||||
|
||||
def __init__(self, dsn: str, session: dict = None, connect_timeout: int = None):
|
||||
self._dsn = dsn
|
||||
self._session = session or {}
|
||||
self._connect_timeout = connect_timeout
|
||||
self.conn = self._open_connection()
|
||||
|
||||
def _open_connection(self):
|
||||
"""创建并初始化连接(包含会话参数)。"""
|
||||
timeout_val = self._connect_timeout if self._connect_timeout is not None else 5
|
||||
# 生产环境要求:数据库连接超时不得超过 20 秒。
|
||||
timeout_val = max(1, min(int(timeout_val), 20))
|
||||
|
||||
conn = psycopg2.connect(self._dsn, connect_timeout=timeout_val)
|
||||
conn.autocommit = False
|
||||
|
||||
# 会话参数(时区、语句超时等)
|
||||
if self._session:
|
||||
with conn.cursor() as c:
|
||||
if self._session.get("timezone"):
|
||||
c.execute("SET TIME ZONE %s", (self._session["timezone"],))
|
||||
if self._session.get("statement_timeout_ms") is not None:
|
||||
c.execute(
|
||||
"SET statement_timeout = %s",
|
||||
(int(self._session["statement_timeout_ms"]),),
|
||||
)
|
||||
if self._session.get("lock_timeout_ms") is not None:
|
||||
c.execute(
|
||||
"SET lock_timeout = %s", (int(self._session["lock_timeout_ms"]),)
|
||||
)
|
||||
if self._session.get("idle_in_tx_timeout_ms") is not None:
|
||||
c.execute(
|
||||
"SET idle_in_transaction_session_timeout = %s",
|
||||
(int(self._session["idle_in_tx_timeout_ms"]),),
|
||||
)
|
||||
return conn
|
||||
|
||||
def query(self, sql: str, args=None):
|
||||
"""Execute a query and fetch all rows."""
|
||||
with self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as c:
|
||||
c.execute(sql, args)
|
||||
return c.fetchall()
|
||||
|
||||
def execute(self, sql: str, args=None):
|
||||
"""Execute a SQL statement without returning rows."""
|
||||
with self.conn.cursor() as c:
|
||||
c.execute(sql, args)
|
||||
|
||||
def commit(self):
|
||||
"""Commit current transaction."""
|
||||
self.conn.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""Rollback current transaction."""
|
||||
self.conn.rollback()
|
||||
|
||||
def close(self):
|
||||
"""Safely close the connection."""
|
||||
try:
|
||||
self.conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def ensure_open(self) -> bool:
|
||||
"""确保连接可用,若已关闭则尝试重连。"""
|
||||
try:
|
||||
if getattr(self.conn, "closed", 0):
|
||||
self.conn = self._open_connection()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
107
apps/etl/pipelines/feiqiu/database/operations.py
Normal file
107
apps/etl/pipelines/feiqiu/database/operations.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库批量操作"""
|
||||
import psycopg2.extras
|
||||
import re
|
||||
|
||||
class DatabaseOperations:
|
||||
"""数据库批量操作封装"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
self.conn = connection.conn
|
||||
|
||||
def batch_execute(self, sql: str, rows: list, page_size: int = 1000):
|
||||
"""批量执行SQL"""
|
||||
if not rows:
|
||||
return
|
||||
with self.conn.cursor() as c:
|
||||
psycopg2.extras.execute_batch(c, sql, rows, page_size=page_size)
|
||||
|
||||
def batch_upsert_with_returning(self, sql: str, rows: list,
|
||||
page_size: int = 1000) -> tuple:
|
||||
"""批量UPSERT并返回插入/更新计数"""
|
||||
if not rows:
|
||||
return (0, 0)
|
||||
|
||||
use_returning = "RETURNING" in sql.upper()
|
||||
|
||||
# 不带 RETURNING:直接批量执行即可
|
||||
if not use_returning:
|
||||
with self.conn.cursor() as c:
|
||||
psycopg2.extras.execute_batch(c, sql, rows, page_size=page_size)
|
||||
return (0, 0)
|
||||
|
||||
# 尝试向量化执行(execute_values + fetch returning)
|
||||
vectorized_failed = False
|
||||
m = re.search(r"VALUES\s*\((.*?)\)", sql, flags=re.IGNORECASE | re.DOTALL)
|
||||
if m:
|
||||
tpl = "(" + m.group(1) + ")"
|
||||
base_sql = sql[:m.start()] + "VALUES %s" + sql[m.end():]
|
||||
try:
|
||||
with self.conn.cursor() as c:
|
||||
ret = psycopg2.extras.execute_values(
|
||||
c, base_sql, rows, template=tpl, page_size=page_size, fetch=True
|
||||
)
|
||||
if not ret:
|
||||
return (0, 0)
|
||||
inserted = sum(1 for rec in ret if self._is_inserted(rec))
|
||||
return (inserted, len(ret) - inserted)
|
||||
except Exception:
|
||||
# 向量化失败后,事务通常处于 aborted 状态,需要先 rollback 才能继续执行。
|
||||
vectorized_failed = True
|
||||
|
||||
if vectorized_failed:
|
||||
try:
|
||||
self.conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 回退:逐行执行
|
||||
inserted = 0
|
||||
updated = 0
|
||||
with self.conn.cursor() as c:
|
||||
for r in rows:
|
||||
c.execute(sql, r)
|
||||
try:
|
||||
rec = c.fetchone()
|
||||
except Exception:
|
||||
rec = None
|
||||
|
||||
if self._is_inserted(rec):
|
||||
inserted += 1
|
||||
else:
|
||||
updated += 1
|
||||
|
||||
return (inserted, updated)
|
||||
|
||||
@staticmethod
|
||||
def _is_inserted(rec) -> bool:
|
||||
"""判断是否为插入操作"""
|
||||
if rec is None:
|
||||
return False
|
||||
if isinstance(rec, tuple):
|
||||
return bool(rec[0])
|
||||
if isinstance(rec, dict):
|
||||
return bool(rec.get("inserted"))
|
||||
return False
|
||||
|
||||
# --- 透传辅助方法 -------------------------------------------------
|
||||
def commit(self):
|
||||
"""提交事务(委托给底层连接)"""
|
||||
self._connection.commit()
|
||||
|
||||
def rollback(self):
|
||||
"""回滚事务(委托给底层连接)"""
|
||||
self._connection.rollback()
|
||||
|
||||
def query(self, sql: str, args=None):
|
||||
"""执行查询并返回结果"""
|
||||
return self._connection.query(sql, args)
|
||||
|
||||
def execute(self, sql: str, args=None):
|
||||
"""执行任意 SQL"""
|
||||
self._connection.execute(sql, args)
|
||||
|
||||
def cursor(self):
|
||||
"""暴露原生 cursor,供特殊操作使用"""
|
||||
return self.conn.cursor()
|
||||
198
apps/etl/pipelines/feiqiu/docs/CHANGELOG.md
Normal file
198
apps/etl/pipelines/feiqiu/docs/CHANGELOG.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 项目变更日志(CHANGELOG)
|
||||
|
||||
> 基于 `docs/audit/changes/` 审计记录整理的项目级版本变更历史。
|
||||
> 按日期倒序排列,每条记录包含日期、变更摘要和影响范围。
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-15
|
||||
|
||||
### 审计目录整合与 API 文档字段归类修正
|
||||
|
||||
- **摘要**:将 `docs/ai_audit/` 旧目录内容统一归入 `docs/audit/`;修正 5 个 API 参考文档(summary/)的字段归类错误;为 3 个已有逻辑变更文件补全 AI_CHANGELOG 注释
|
||||
- **影响范围**:文档(`docs/api-reference/summary/`、`docs/audit/`)、代码注释(`tasks/base_task.py`、`quality/`)
|
||||
- **风险**:极低(纯文档重组与注释补录)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-15__audit-consolidation-doc-reorg.md)
|
||||
|
||||
### docs/bd_manual + docs/dictionary → docs/database 合并
|
||||
|
||||
- **摘要**:将分散的 `docs/bd_manual/` 和 `docs/dictionary/` 合并为统一路径 `docs/database/`,按数据层(ODS/DWD/DWS/ETL_Admin/overview)分目录;更新所有路径引用(脚本、steering、hooks)
|
||||
- **影响范围**:文档路径(`docs/database/`)、脚本(`scripts/validate_bd_manual.py`)、配置(`.kiro/steering/`、`.kiro/hooks/`、`.kiro/skills/`)
|
||||
- **风险**:极低(纯路径重组,无运行时代码变更)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-15__docs-database-merge.md)
|
||||
|
||||
### docs/index + docs/开发笔记 清理与路径整合
|
||||
|
||||
- **摘要**:将 `docs/index/index_algorithm_cn.md` 移至 `docs/database/DWS/`;从 `docs/开发笔记/` 分拣 6 个有价值文件至 `docs/requirements/`,删除过期内容;更新引用和测试
|
||||
- **影响范围**:文档路径(`docs/database/DWS/`、`docs/requirements/`)、脚本(`scripts/audit/doc_alignment_analyzer.py`)、测试(`tests/unit/test_audit_doc_alignment.py`)
|
||||
- **风险**:低(纯文档重组 + 脚本路径更新)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-15__docs-devnotes-index-cleanup.md)
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-14
|
||||
|
||||
### API 文档归档至 summary/ + 字段分组修正
|
||||
|
||||
- **摘要**:25 个 API 参考文档从根目录移至 `docs/api-reference/summary/`;修正 7 个文档的"响应字段详解"章节字段归类错误(非时间字段混入"时间"组等)
|
||||
- **影响范围**:文档(`docs/api-reference/summary/`、`docs/api-reference/README.md`)
|
||||
- **风险**:极低(纯文档分组调整)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__api-doc-reorg-field-grouping.md)
|
||||
|
||||
### API vs ODS 比对 v3-fixed
|
||||
|
||||
- **摘要**:重写比对脚本,改用 API 参考文档(.md)的"响应字段详解"章节作为主要字段来源(替代 JSON 样本),解决 v3 中条件性字段误报问题;22 张 ODS 表全部比对,API 独有字段 0 个
|
||||
- **影响范围**:脚本(`scripts/run_compare_v3_fixed.py`)、报告(`docs/reports/`)
|
||||
- **风险**:极低(纯分析脚本和报告)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__api-ods-comparison-v3-fixed.md)
|
||||
|
||||
### API vs ODS 逐表比对 v3
|
||||
|
||||
- **摘要**:从 JSON 样本直接提取字段并与数据库实际列精确比对,逐表重做(替代 v2 不准确的结果)
|
||||
- **影响范围**:脚本(`scripts/run_compare_v3.py`、`scripts/compare_api_ods_v3.py`)、报告(`docs/reports/`)
|
||||
- **风险**:极低(纯分析脚本和报告)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__api-ods-comparison-v3.md)
|
||||
|
||||
### API 参数校对 + ODS 设计方案输出
|
||||
|
||||
- **摘要**:验证 25 个 API 的 .md 文档请求体参数与 API.txt 一致性(全部通过);输出 tenant_member_balance_overview 的主表+子表 ODS 设计方案(待用户确认后执行)
|
||||
- **影响范围**:文档(`docs/ai_audit/`)
|
||||
- **风险**:极低(纯审计日志 + 对话输出)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__api-param-audit-ods-design.md)
|
||||
|
||||
### 删除 DWD 层 settle_list 冗余列
|
||||
|
||||
- **摘要**:删除 `dwd_settlement_head_ex.settle_list` JSONB 列(与 ODS `payload` 中的 `settleList` 重复);同步移除 DWD 加载映射
|
||||
- **影响范围**:数据库(`billiards_dwd.dwd_settlement_head_ex`)、ETL(`tasks/dwd/dwd_load_task.py`)、迁移脚本
|
||||
- **风险**:中(DB schema 变更,需确认下游无引用)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__drop-dwd-settle-list.md)
|
||||
|
||||
### 删除 ODS 层 settlelist 冗余列
|
||||
|
||||
- **摘要**:删除 `settlement_records` 和 `recharge_settlements` 的 `settlelist` jsonb 列(与 `payload` 列数据重复);DWD 加载改为从 `payload->'settleList'` 提取
|
||||
- **影响范围**:数据库(`billiards_ods`)、ETL(`tasks/dwd/dwd_load_task.py`)、脚本、报告
|
||||
- **风险**:中(DB schema 变更,历史 `payload IS NULL` 的行将永久丢失 settleList)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__drop-ods-settlelist.md)
|
||||
|
||||
### DWS 基类 bugfix — 绩效档位兜底 + safe_decimal 异常捕获
|
||||
|
||||
- **摘要**:修复 `get_performance_tier()` 在封顶场景下返回 None 的 bug;修复 `safe_decimal()` 未捕获 `decimal.InvalidOperation` 的问题;修复 3 处测试 bug
|
||||
- **影响范围**:业务代码(`tasks/dws/base_dws_task.py`)、测试(`tests/unit/test_dws_tasks.py`)
|
||||
- **风险**:低(防御性修复,不改变正常路径行为)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__dws-bugfix-tier-safedecimal.md)
|
||||
|
||||
### 全量 JSON 刷新 + MD 文档补全 + 数据路径修正
|
||||
|
||||
- **摘要**:全部 24 个 JSON 样本刷新为 100 条数据;10 个 MD 文档补全共 39 个缺失字段;修正 `api_registry.json` 中 17 个端点的 `data_path`
|
||||
- **影响范围**:文档(`docs/api-reference/`)、脚本(`scripts/refresh_json_and_audit.py`)、报告
|
||||
- **风险**:极低(纯文档和脚本变更)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__json-refresh-md-patch.md)
|
||||
|
||||
### JSON 样本 vs MD 文档全面排查
|
||||
|
||||
- **摘要**:编写比对脚本验证 24 个表的 .md 文档与 JSON 样本字段一致性,全部通过(4 个表有条件性字段差异属正常)
|
||||
- **影响范围**:脚本(`scripts/check_json_vs_md.py`)、报告(`docs/reports/`)
|
||||
- **风险**:极低(纯分析脚本和报告)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__json-vs-md-audit.md)
|
||||
|
||||
### 废弃独立 ODS/DWD 任务代码清理 + 文档同步
|
||||
|
||||
- **摘要**:清理 14 个废弃独立 ODS 任务和 3 个废弃 DWD 任务的残留引用(注册表重复循环、测试工具废弃定义、过时文档);重写 `docs/etl_tasks/` 文档
|
||||
- **影响范围**:调度(`orchestration/task_registry.py`)、测试工具(`tests/unit/task_test_utils.py`)、文档(`docs/etl_tasks/`)、配置(`.kiro/steering/tech.md`)
|
||||
- **风险**:中(task_registry.py 是核心入口,需确认 52 个任务全部正确注册)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__legacy-ods-dwd-cleanup.md)
|
||||
|
||||
### MD 占位符修正 + 临时文件清理
|
||||
|
||||
- **摘要**:修正 5 个 API 文档中 v2 脚本自动插入的占位符描述为正式中文说明;合并/去重字段;清理 25 个临时 JSON 文件和 3 个临时脚本
|
||||
- **影响范围**:文档(`docs/api-reference/`)、报告
|
||||
- **风险**:极低(纯文档修正)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__md-placeholder-fix-cleanup.md)
|
||||
|
||||
### ODS 清理与文档标注
|
||||
|
||||
- **摘要**:删除 ODS 层 2 个全 NULL 冗余列(`option_name`、`able_site_transfer`);4 个 API 独有字段标记"暂不入 ODS";补充 8 个 tableProfile 展开字段文档
|
||||
- **影响范围**:数据库(`billiards_ods`)、DDL(`database/schema_ODS_doc.sql`)、文档、脚本、报告
|
||||
- **风险**:低(删除的列全 NULL,无数据丢失)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__ods-cleanup-doc-update.md)
|
||||
|
||||
### ODS vs Summary 字段比对
|
||||
|
||||
- **摘要**:编写脚本直接查询 PostgreSQL `billiards_ods` schema 与 25 个 summary MD 文档逐表比对;多轮修复比对脚本 bug(skip_words 误过滤、siteProfile 误跳过等),最终完全匹配 17 张表
|
||||
- **影响范围**:脚本(`scripts/compare_ods_vs_summary_v2.py`)、报告
|
||||
- **风险**:极低(纯分析脚本)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__ods-vs-summary-comparison.md)
|
||||
|
||||
### api/recording_client.py 默认时区修正
|
||||
|
||||
- **摘要**:将 `build_recording_client` 默认时区从 `Asia/Taipei` 改为 `Asia/Shanghai`,语义更准确(实际偏移量无差异)
|
||||
- **影响范围**:API 客户端(`api/recording_client.py`)
|
||||
- **风险**:极低(两个时区当前 UTC 偏移相同)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__recording-client-timezone-fix.md)
|
||||
|
||||
### 替换 role_area_association 为 member_consumption_statistics
|
||||
|
||||
- **摘要**:用会员消费统计 API(QueryMemberConsumptionStatistics)替换权限配置查询 API(role_area_association);新建 JSON 样本和参考文档;输出 2 个新表的 ODS 设计方案
|
||||
- **影响范围**:文档(`docs/api-reference/`)
|
||||
- **风险**:极低(纯文档变更)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__replace-role-area-new-api-doc.md)
|
||||
|
||||
### skip_words 误过滤 remark 业务字段修复
|
||||
|
||||
- **摘要**:修复比对脚本中 `skip_words` 误过滤 `remark`/`note`/`type` 等真实业务字段的问题;最终用 Markdown 表格分隔行检测替代 skip_words 方案;修复 siteProfile 子节跳过逻辑和 goodsCategoryList 包装器忽略
|
||||
- **影响范围**:脚本(`scripts/compare_ods_vs_summary_v2.py`)、报告
|
||||
- **风险**:极低(纯分析脚本)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-14__skip-words-remark-fix.md)
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-13
|
||||
|
||||
### API vs ODS 对比 v2
|
||||
|
||||
- **摘要**:重写比对脚本(v1 存在嵌套结构解析 bug),从 API 参考文档提取字段与 PostgreSQL `billiards_ods` 实际列比对;22 张 ODS 表全部对齐,0 张漂移
|
||||
- **影响范围**:脚本(`scripts/compare_api_ods_v2.py`)、报告(`docs/reports/`)
|
||||
- **风险**:极低(纯分析脚本 + 报告文档)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__api-ods-comparison-v2.md)
|
||||
|
||||
### API JSON 字段 vs ODS 表列对比
|
||||
|
||||
- **摘要**:编写 Python 脚本查询 `billiards_ods` 的 `information_schema.columns`,与 API 参考文档做 camelCase→snake_case 归一化匹配;22 张 ODS 表全部对齐,无需 ALTER
|
||||
- **影响范围**:脚本(`scripts/compare_api_ods.py`)、报告(`docs/reports/`)、迁移脚本(空操作)
|
||||
- **风险**:低(纯分析工具,不修改数据库)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__api-ods-comparison.md)
|
||||
|
||||
### API 参考文档批量生成(第二批 6 个)
|
||||
|
||||
- **摘要**:按标杆文档格式生成 6 个高质量 API 参考文档(member_profiles、member_stored_value_cards、member_balance_changes、platform_coupon_redemption_records、group_buy_packages、group_buy_redemption_records)
|
||||
- **影响范围**:文档(`docs/api-reference/`)
|
||||
- **风险**:极低(纯文档变更)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__api-reference-batch2.md)
|
||||
|
||||
### API 参考文档全面重构
|
||||
|
||||
- **摘要**:对 23+ API 文档进行全面重构,创建结构化的 `docs/api-reference/` 目录体系(endpoints/、samples/);生成 25 个端点文档、24 个响应样本、标准化 API 注册表;废弃旧 `test-json-doc` 目录
|
||||
- **影响范围**:文档(`docs/api-reference/`)、配置(`.kiro/steering/structure.md`)
|
||||
- **风险**:极低(纯文档生成和目录结构调整)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__api-reference-overhaul.md)
|
||||
|
||||
### BD_Manual 文档整理与 DDL 同步
|
||||
|
||||
- **摘要**:修复 DDL 对比脚本 bug;同步 ODS/DWD/DWS 三层共 13 项 DDL 差异;生成 ODS 23 张表的表级文档、映射文档和数据字典;创建 BD_Manual 根索引和文档验证脚本
|
||||
- **影响范围**:DDL(`database/schema_ODS_doc.sql`、`database/schema_dws.sql`)、脚本(`scripts/compare_ddl_db.py`、`scripts/validate_bd_manual.py`)、文档(`docs/bd_manual/`、`docs/dictionary/`)、测试
|
||||
- **风险**:中(DDL 文件修正,虽未变更数据库结构但被其他脚本引用)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__bd-manual-docs-consolidation-ddl-sync.md)
|
||||
|
||||
### API 字段漂移报告修正更新
|
||||
|
||||
- **摘要**:用正确的 `limit` 参数重新调用 3 个端点(settlement_records、recharge_settlements、payment_transactions),更新字段漂移报告;发现 5 个新增字段(电费、商户券/平台券相关)
|
||||
- **影响范围**:报告(`docs/reports/`)
|
||||
- **风险**:极低(纯文档更新)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__field-drift-report-update.md)
|
||||
|
||||
### 移除旧版指数(RECALL/INTIMACY)+ ML last-touch 清理
|
||||
|
||||
- **摘要**:彻底删除已被 WBI+NCI 替代的 RecallIndexTask 和已被 RelationIndexTask 替代的 IntimacyIndexTask;修复 WBI `STOP_HIGH_BALANCE` 评分 bug;移除 ML last-touch 备用路径;清理 GUI、调度注册、数据库对象和文档中的所有引用
|
||||
- **影响范围**:业务代码(`tasks/dws/index/`)、调度(`orchestration/task_registry.py`)、GUI(`gui/`)、数据库(DDL + seed + 迁移脚本)、文档、测试
|
||||
- **风险**:中(不可逆的 DROP TABLE,但用户确认不需要向后兼容)
|
||||
- **详情**:[审计记录](audit/changes/2026-02-13__remove-legacy-index-cleanup.md)
|
||||
31
apps/etl/pipelines/feiqiu/docs/README.md
Normal file
31
apps/etl/pipelines/feiqiu/docs/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# docs/ — 项目文档
|
||||
|
||||
## 子目录索引
|
||||
|
||||
| 目录 / 文件 | 内容 |
|
||||
|-------------|------|
|
||||
| [`architecture/`](architecture/README.md) | 架构设计文档 — 系统整体架构、数据流向(ODS→DWD→DWS)、模块交互关系 |
|
||||
| [`api-reference/`](api-reference/) | API 参考文档(25 个端点的标准化文档 + JSON 样本) |
|
||||
| [`audit/`](audit/README.md) | 审计统一目录(AI 变更审计 + 仓库审计报告) |
|
||||
| [`audit/changes/`](audit/changes/) | AI 逐次变更审计记录 |
|
||||
| [`audit/repo/`](audit/repo/) | 仓库审计报告(由 `scripts/audit/` 自动生成:文件清单、调用流、文档对齐) |
|
||||
| [`audit/audit_dashboard.md`](audit/audit_dashboard.md) | 审计一览表 — 基于审计源记录自动生成的汇总视图(时间线 + 模块索引) |
|
||||
| [`business-rules/`](business-rules/README.md) | 业务规则文档 — 指数算法、DWS 口径定义、SCD2 处理规则等业务逻辑 |
|
||||
| [`database/`](database/README.md) | 数据库文档统一目录 — 层级概览 + ODS/DWD/DWS/ETL_Admin 表级文档 |
|
||||
| [`database/overview/`](database/overview/) | 层级概览 / 速查索引(表清单、主键、记录数、业务域分类) |
|
||||
| [`database/ODS/`](database/ODS/) | ODS 层表手册(main/mappings/changes) |
|
||||
| [`database/DWD/`](database/DWD/) | DWD 层表手册(main 表 + Ex 扩展表) |
|
||||
| [`database/DWS/`](database/DWS/) | DWS 层表手册(助教、财务、会员、指数等) |
|
||||
| [`database/ETL_Admin/`](database/ETL_Admin/) | ETL 管理层表手册(etl_cursor/etl_run/etl_task) |
|
||||
| [`etl_tasks/`](etl_tasks/README.md) | ETL 任务文档(ODS/DWD/DWS/指数任务说明与机制) |
|
||||
| [`operations/`](operations/README.md) | 运维文档 — 环境搭建指南、调度配置说明、故障排查手册 |
|
||||
| [`reports/`](reports/) | 分析报告(数据质量、一致性检查等输出) |
|
||||
| [`requirements/`](requirements/) | 需求文档(功能需求、口径补充、指数 PRD 等) |
|
||||
| [`CHANGELOG.md`](CHANGELOG.md) | 项目级版本变更历史(日期、变更摘要、影响范围) |
|
||||
|
||||
## 维护约定
|
||||
|
||||
- 代码变更涉及表结构或口径时,同步更新 `database/`
|
||||
- 审计一览表通过 `python scripts/gen_audit_dashboard.py` 重新生成,不要手动编辑
|
||||
- 审计报告通过 `python -m scripts.audit.run_audit` 重新生成,不要手动编辑
|
||||
- 文档统一 UTF-8 编码,中文撰写
|
||||
149
apps/etl/pipelines/feiqiu/docs/api-reference/README.md
Normal file
149
apps/etl/pipelines/feiqiu/docs/api-reference/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# API 参考文档
|
||||
|
||||
> 飞球 ETL 系统上游 SaaS API 的标准化文档。
|
||||
> 自动生成于 2026-02-13,基于实时 API 调用 + 本地 JSON 样本。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
docs/api-reference/
|
||||
├── README.md # 本文件(索引)
|
||||
├── api_registry.json # API 注册表(标准化参数存储)
|
||||
├── _api_call_results.json # API 调用结果(字段提取)
|
||||
├── summary/ # 每个 API 一个精简版 .md 文档(字段表 + 跨表关联)
|
||||
│ ├── assistant_accounts_master.md
|
||||
│ ├── ...(共 25 个)
|
||||
│ └── tenant_member_balance_overview.md
|
||||
├── endpoints/ # 每个 API 一个详细版 .md 文档(完整字段分析)
|
||||
│ ├── assistant_accounts_master.md
|
||||
│ ├── ...(共 24 个)
|
||||
│ └── tenant_member_balance_overview.md
|
||||
└── samples/ # 每个 API 的响应样本(Top-5 最全记录 JSON)
|
||||
├── assistant_accounts_master.json
|
||||
├── ...
|
||||
└── tenant_member_balance_overview.json
|
||||
```
|
||||
|
||||
## API 总览(25 个接口)
|
||||
|
||||
### 人事管理
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [SearchAssistantInfo](endpoints/assistant_accounts_master.md) | 助教账号主数据 | `assistant_accounts_master` | 61 |
|
||||
| [GetOrderAssistantDetails](endpoints/assistant_service_records.md) | 助教服务流水 | `assistant_service_records` | 64 |
|
||||
| [GetAbolitionAssistant](endpoints/assistant_cancellation_records.md) | 助教撤销记录 | `assistant_cancellation_records` | 13 |
|
||||
|
||||
### 订单与结算
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [GetAllOrderSettleList](endpoints/settlement_records.md) | 结账记录 | `settlement_records` | 92 |
|
||||
| [GetSiteTableOrderDetails](endpoints/table_fee_transactions.md) | 台费流水 | `table_fee_transactions` | 39 |
|
||||
| [GetTaiFeeAdjustList](endpoints/table_fee_discount_records.md) | 台费优惠记录 | `table_fee_discount_records` | 20 |
|
||||
| [GetOrderSettleTicketNew](endpoints/settlement_ticket_details.md) | 结账小票明细 | `settlement_ticket_details` | ⚠️ 不可用 |
|
||||
|
||||
### 支付与退款
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [GetPayLogListPage](endpoints/payment_transactions.md) | 支付流水 | `payment_transactions` | 11 |
|
||||
| [GetRefundPayLogList](endpoints/refund_transactions.md) | 退款流水 | `refund_transactions` | 32 |
|
||||
| [GetRechargeSettleList](endpoints/recharge_settlements.md) | 充值结算记录 | `recharge_settlements` | 92 |
|
||||
|
||||
### 会员
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [GetTenantMemberList](endpoints/member_profiles.md) | 会员档案 | `member_profiles` | 15 |
|
||||
| [GetTenantMemberCardList](endpoints/member_stored_value_cards.md) | 会员储值卡 | `member_stored_value_cards` | 68 |
|
||||
| [GetMemberCardBalanceChange](endpoints/member_balance_changes.md) | 会员余额变动 | `member_balance_changes` | 25 |
|
||||
|
||||
### 优惠券与团购
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [GetOfflineCouponConsumePageList](endpoints/platform_coupon_redemption_records.md) | 平台券核销记录 | `platform_coupon_redemption_records` | 26 |
|
||||
| [QueryPackageCouponList](endpoints/group_buy_packages.md) | 团购套餐定义 | `group_buy_packages` | 35 |
|
||||
| [GetSiteTableUseDetails](endpoints/group_buy_redemption_records.md) | 团购核销记录 | `group_buy_redemption_records` | 43 |
|
||||
|
||||
### 商品与库存
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [QueryTenantGoods](endpoints/tenant_goods_master.md) | 租户商品主数据 | `tenant_goods_master` | 31 |
|
||||
| [GetGoodsSalesList](endpoints/store_goods_sales_records.md) | 门店商品销售记录 | `store_goods_sales_records` | 50 |
|
||||
| [GetGoodsInventoryList](endpoints/store_goods_master.md) | 门店商品库存主数据 | `store_goods_master` | 45 |
|
||||
| [QueryPrimarySecondaryCategory](endpoints/stock_goods_category_tree.md) | 商品分类树 | `stock_goods_category_tree` | 2 |
|
||||
| [QueryGoodsOutboundReceipt](endpoints/goods_stock_movements.md) | 库存出入库流水 | `goods_stock_movements` | 19 |
|
||||
| [GetGoodsStockReport](endpoints/goods_stock_summary.md) | 库存汇总报表 | `goods_stock_summary` | 14 |
|
||||
|
||||
### 台桌
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [GetSiteTables](endpoints/site_tables_master.md) | 台桌主数据 | `site_tables_master` | 25 |
|
||||
|
||||
### 会员统计与总览(待建 ODS 表)
|
||||
|
||||
| API | 中文名 | ODS 表 | 字段数 |
|
||||
|-----|--------|--------|--------|
|
||||
| [QueryMemberConsumptionStatistics](summary/member_consumption_statistics.md) | 会员消费统计 | `member_consumption_statistics`(待建) | 11 |
|
||||
| [TenantMemberBalanceOverview](summary/tenant_member_balance_overview.md) | 会员余额总览 | `tenant_member_balance_overview`(待建) | 9 |
|
||||
|
||||
## 关键发现
|
||||
|
||||
### 分页参数差异
|
||||
- 大多数端点接受 `page` + `limit`
|
||||
- `GetAllOrderSettleList`、`GetRechargeSettleList`、`GetPayLogListPage` 拒绝 `pageSize`/`pageNo`(HTTP 1400),必须用 `limit`
|
||||
- `limit` 最大值为 100
|
||||
|
||||
### 特殊参数格式
|
||||
- `GetGoodsInventoryList` 的 `siteId` 必须为数组格式 `[sid]`
|
||||
- `GetGoodsSalesList` 需要 `isSalesBind`/`goodsSalesType` 业务过滤参数
|
||||
- `QueryPackageCouponList` 的 `areaId` 为数组格式
|
||||
|
||||
### 响应结构差异
|
||||
- 大多数端点:`{code, data: {list: [...], total}}`
|
||||
- `settlement_records` / `recharge_settlements`:`{code, data: {settleList: [{siteProfile, settleList: {...}}]}}`
|
||||
- `stock_goods_category_tree`:`{code, data: {goodsCategoryList: [...]}}`
|
||||
- `payment_transactions` / `refund_transactions`:记录中嵌套 `siteProfile` 对象
|
||||
|
||||
## 与旧文档的关系
|
||||
|
||||
旧文档位于 `docs/test-json-doc/`(已废弃),包含:
|
||||
- `*.json` — 本地 JSON 样本文件(仍可用于离线回放)
|
||||
- `*-Analysis.md` — 详细字段分析文档(内容已迁移至本目录各端点文档的"详细字段分析"章节)
|
||||
|
||||
新文档优势:
|
||||
- 标准化结构(请求参数表 + 响应字段表 + 详细分析)
|
||||
- `api_registry.json` 提供机器可读的 API 定义
|
||||
- `samples/` 目录提供最新响应样本
|
||||
|
||||
|
||||
<!--
|
||||
AI_CHANGELOG:
|
||||
- 日期: 2026-02-13
|
||||
- Prompt: P20260213-171500 — API 文档全面重构
|
||||
- 直接原因: 创建 API 参考文档索引
|
||||
- 变更摘要: 新建 README.md,包含 25 个 API 端点索引表、关键发现、与旧文档的关系说明
|
||||
- 风险与验证: 纯文档,无运行时影响
|
||||
|
||||
- 日期: 2026-02-14
|
||||
- Prompt: P20260214-060000 / P20260214-061000 — 全量 JSON 刷新 + MD 文档补全 + 数据路径修正
|
||||
- 直接原因: api_registry.json 中 17 个 data_path 修正为实际 API 返回路径;24 个 .md 文档新增响应数据路径行;10 个文档补全 39 个缺失字段
|
||||
- 变更摘要: api_registry.json data_path 修正(本文件为 JSON 不支持注释,changelog 记录于此)
|
||||
- 风险与验证: 纯文档变更,api_registry.json 仅被 scripts/ 下比对脚本读取,不影响 ETL 运行时
|
||||
|
||||
- 日期: 2026-02-14
|
||||
- Prompt: P20260214-083000 — 替换 role_area_association 为 member_consumption_statistics + tenant_member_balance_overview ODS 表标注
|
||||
- 直接原因: role_area_association 非业务数据,替换为 QueryMemberConsumptionStatistics;同步更新索引表和 api_registry.json
|
||||
- 变更摘要: 索引表"新增 API"章节替换为"会员统计与总览";api_registry.json 替换 role_area_association 条目、tenant_member_balance_overview ods_table 从 null 改为表名
|
||||
- 风险与验证: 纯文档变更;验证:python -c "import json; d=json.load(open('docs/api-reference/api_registry.json')); print(len(d))" → 25
|
||||
|
||||
- 日期: 2026-02-14
|
||||
- Prompt: P20260214-130000 — 25 个 API 文档归档至 summary/ + 字段分组修正
|
||||
- 直接原因: 25 个精简版 API 文档从根目录移至 summary/ 子目录,README 目录结构需同步更新
|
||||
- 变更摘要: 目录结构树中新增 summary/ 子目录说明
|
||||
- 风险与验证: 纯文档变更,无运行时影响
|
||||
-->
|
||||
4360
apps/etl/pipelines/feiqiu/docs/api-reference/_api_call_results.json
Normal file
4360
apps/etl/pipelines/feiqiu/docs/api-reference/_api_call_results.json
Normal file
File diff suppressed because it is too large
Load Diff
654
apps/etl/pipelines/feiqiu/docs/api-reference/api_registry.json
Normal file
654
apps/etl/pipelines/feiqiu/docs/api-reference/api_registry.json
Normal file
@@ -0,0 +1,654 @@
|
||||
[
|
||||
{
|
||||
"id": "assistant_accounts_master",
|
||||
"name_zh": "助教账号主数据",
|
||||
"module": "PersonnelManagement",
|
||||
"action": "SearchAssistantInfo",
|
||||
"method": "POST",
|
||||
"ods_table": "assistant_accounts_master",
|
||||
"description": "查询门店下所有助教账号的基础信息(人事档案维表)",
|
||||
"body": {
|
||||
"workStatusEnum": 0,
|
||||
"dingTalkSynced": 0,
|
||||
"leaveId": 0,
|
||||
"criticismStatus": 0,
|
||||
"signStatus": -1,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.assistantInfos"
|
||||
},
|
||||
{
|
||||
"id": "settlement_records",
|
||||
"name_zh": "结账记录",
|
||||
"module": "Site",
|
||||
"action": "GetAllOrderSettleList",
|
||||
"method": "POST",
|
||||
"ods_table": "settlement_records",
|
||||
"description": "查询门店结账(台费+商品+助教)汇总记录",
|
||||
"body": {
|
||||
"settleType": 0,
|
||||
"rangeStartTime": "2026-02-01 08:00:00",
|
||||
"rangeEndTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"siteTableAreaIdList": [],
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"rangeStartTime",
|
||||
"rangeEndTime"
|
||||
],
|
||||
"data_path": "data.settleList"
|
||||
},
|
||||
{
|
||||
"id": "assistant_service_records",
|
||||
"name_zh": "助教服务流水",
|
||||
"module": "AssistantPerformance",
|
||||
"action": "GetOrderAssistantDetails",
|
||||
"method": "POST",
|
||||
"ods_table": "assistant_service_records",
|
||||
"description": "查询助教服务明细(含订单关联、计费、确认状态)",
|
||||
"body": {
|
||||
"siteId": 2790685415443269,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"IsConfirm": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.orderAssistantDetails"
|
||||
},
|
||||
{
|
||||
"id": "assistant_cancellation_records",
|
||||
"name_zh": "助教撤销记录",
|
||||
"module": "AssistantPerformance",
|
||||
"action": "GetAbolitionAssistant",
|
||||
"method": "POST",
|
||||
"ods_table": "assistant_cancellation_records",
|
||||
"description": "查询助教服务被撤销/取消的记录",
|
||||
"body": {
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.abolitionAssistants"
|
||||
},
|
||||
{
|
||||
"id": "table_fee_transactions",
|
||||
"name_zh": "台费流水",
|
||||
"module": "Site",
|
||||
"action": "GetSiteTableOrderDetails",
|
||||
"method": "POST",
|
||||
"ods_table": "table_fee_transactions",
|
||||
"description": "查询台桌开台/结账的台费订单明细",
|
||||
"body": {
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"isSaleManUser": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.siteTableUseDetailsList"
|
||||
},
|
||||
{
|
||||
"id": "table_fee_discount_records",
|
||||
"name_zh": "台费优惠记录",
|
||||
"module": "Site",
|
||||
"action": "GetTaiFeeAdjustList",
|
||||
"method": "POST",
|
||||
"ods_table": "table_fee_discount_records",
|
||||
"description": "查询台费调整/优惠明细",
|
||||
"body": {
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.taiFeeAdjustInfos"
|
||||
},
|
||||
{
|
||||
"id": "payment_transactions",
|
||||
"name_zh": "支付流水",
|
||||
"module": "PayLog",
|
||||
"action": "GetPayLogListPage",
|
||||
"method": "POST",
|
||||
"ods_table": "payment_transactions",
|
||||
"description": "查询支付日志(含在线/线下/余额等多种支付方式)",
|
||||
"body": {
|
||||
"StartPayTime": "2026-02-01 08:00:00",
|
||||
"EndPayTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"OnlinePayChannel": 0,
|
||||
"paymentMethod": 0,
|
||||
"relateType": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"StartPayTime",
|
||||
"EndPayTime"
|
||||
],
|
||||
"data_path": "data.list"
|
||||
},
|
||||
{
|
||||
"id": "refund_transactions",
|
||||
"name_zh": "退款流水",
|
||||
"module": "Order",
|
||||
"action": "GetRefundPayLogList",
|
||||
"method": "POST",
|
||||
"ods_table": "refund_transactions",
|
||||
"description": "查询退款记录明细",
|
||||
"body": {
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.list"
|
||||
},
|
||||
{
|
||||
"id": "platform_coupon_redemption_records",
|
||||
"name_zh": "平台券核销记录",
|
||||
"module": "Promotion",
|
||||
"action": "GetOfflineCouponConsumePageList",
|
||||
"method": "POST",
|
||||
"ods_table": "platform_coupon_redemption_records",
|
||||
"description": "查询线下/平台优惠券核销明细",
|
||||
"body": {
|
||||
"couponChannel": 0,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"couponUseStatus": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.list"
|
||||
},
|
||||
{
|
||||
"id": "tenant_goods_master",
|
||||
"name_zh": "租户商品主数据",
|
||||
"module": "TenantGoods",
|
||||
"action": "QueryTenantGoods",
|
||||
"method": "POST",
|
||||
"ods_table": "tenant_goods_master",
|
||||
"description": "查询租户级商品定义(含成本、折扣、状态)",
|
||||
"body": {
|
||||
"costPriceType": 0,
|
||||
"ableDiscount": -1,
|
||||
"tenantGoodsStatus": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.tenantGoodsList"
|
||||
},
|
||||
{
|
||||
"id": "store_goods_sales_records",
|
||||
"name_zh": "门店商品销售记录",
|
||||
"module": "TenantGoods",
|
||||
"action": "GetGoodsSalesList",
|
||||
"method": "POST",
|
||||
"ods_table": "store_goods_sales_records",
|
||||
"description": "查询门店商品销售明细(含绑定销售、独立销售)",
|
||||
"body": {
|
||||
"isSalesBind": 0,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"goodsSalesType": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.orderGoodsLedgers"
|
||||
},
|
||||
{
|
||||
"id": "store_goods_master",
|
||||
"name_zh": "门店商品库存主数据",
|
||||
"module": "TenantGoods",
|
||||
"action": "GetGoodsInventoryList",
|
||||
"method": "POST",
|
||||
"ods_table": "store_goods_master",
|
||||
"description": "查询门店商品库存列表(含分类、状态、库存量)",
|
||||
"body": {
|
||||
"goodsSecondCategoryId": [],
|
||||
"goodsState": 0,
|
||||
"enableStatus": 0,
|
||||
"siteId": [
|
||||
2790685415443269
|
||||
],
|
||||
"existsGoodsStock": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.orderGoodsList"
|
||||
},
|
||||
{
|
||||
"id": "stock_goods_category_tree",
|
||||
"name_zh": "商品分类树",
|
||||
"module": "TenantGoodsCategory",
|
||||
"action": "QueryPrimarySecondaryCategory",
|
||||
"method": "POST",
|
||||
"ods_table": "stock_goods_category_tree",
|
||||
"description": "查询商品一级/二级分类树",
|
||||
"body": null,
|
||||
"pagination": null,
|
||||
"time_range": false,
|
||||
"data_path": "data.goodsCategoryList"
|
||||
},
|
||||
{
|
||||
"id": "goods_stock_movements",
|
||||
"name_zh": "库存出入库流水",
|
||||
"module": "GoodsStockManage",
|
||||
"action": "QueryGoodsOutboundReceipt",
|
||||
"method": "POST",
|
||||
"ods_table": "goods_stock_movements",
|
||||
"description": "查询商品出入库单据明细",
|
||||
"body": {
|
||||
"siteId": 2790685415443269,
|
||||
"stockType": 0,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.queryDeliveryRecordsList"
|
||||
},
|
||||
{
|
||||
"id": "member_profiles",
|
||||
"name_zh": "会员档案",
|
||||
"module": "MemberProfile",
|
||||
"action": "GetTenantMemberList",
|
||||
"method": "POST",
|
||||
"ods_table": "member_profiles",
|
||||
"description": "查询门店会员基础信息列表",
|
||||
"body": {
|
||||
"isMemberInBlackList": 0,
|
||||
"status_Revoked": 0,
|
||||
"isBindOrg": 0,
|
||||
"registerSource": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.tenantMemberInfos"
|
||||
},
|
||||
{
|
||||
"id": "member_stored_value_cards",
|
||||
"name_zh": "会员储值卡",
|
||||
"module": "MemberProfile",
|
||||
"action": "GetTenantMemberCardList",
|
||||
"method": "POST",
|
||||
"ods_table": "member_stored_value_cards",
|
||||
"description": "查询会员储值卡列表(含余额、折扣、状态)",
|
||||
"body": {
|
||||
"siteId": 2790685415443269,
|
||||
"cardPhysicsType": 0,
|
||||
"status": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.tenantMemberCards"
|
||||
},
|
||||
{
|
||||
"id": "recharge_settlements",
|
||||
"name_zh": "充值结算记录",
|
||||
"module": "Site",
|
||||
"action": "GetRechargeSettleList",
|
||||
"method": "POST",
|
||||
"ods_table": "recharge_settlements",
|
||||
"description": "查询充值结算汇总记录",
|
||||
"body": {
|
||||
"settleType": 0,
|
||||
"paymentMethod": 0,
|
||||
"rangeStartTime": "2026-02-01 08:00:00",
|
||||
"rangeEndTime": "2026-02-13 08:00:00",
|
||||
"siteId": 2790685415443269,
|
||||
"isFirst": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"rangeStartTime",
|
||||
"rangeEndTime"
|
||||
],
|
||||
"data_path": "data.settleList"
|
||||
},
|
||||
{
|
||||
"id": "member_balance_changes",
|
||||
"name_zh": "会员余额变动",
|
||||
"module": "MemberProfile",
|
||||
"action": "GetMemberCardBalanceChange",
|
||||
"method": "POST",
|
||||
"ods_table": "member_balance_changes",
|
||||
"description": "查询会员储值卡余额变动明细",
|
||||
"body": {
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"fromType": 0,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.tenantMemberCardLogs"
|
||||
},
|
||||
{
|
||||
"id": "group_buy_packages",
|
||||
"name_zh": "团购套餐定义",
|
||||
"module": "PackageCoupon",
|
||||
"action": "QueryPackageCouponList",
|
||||
"method": "POST",
|
||||
"ods_table": "group_buy_packages",
|
||||
"description": "查询团购/套餐券定义列表",
|
||||
"body": {
|
||||
"areaId": [],
|
||||
"commonShowStatus": 1,
|
||||
"offlineCouponChannel": 0,
|
||||
"systemGroupType": 1,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.packageCouponList"
|
||||
},
|
||||
{
|
||||
"id": "group_buy_redemption_records",
|
||||
"name_zh": "团购核销记录",
|
||||
"module": "Site",
|
||||
"action": "GetSiteTableUseDetails",
|
||||
"method": "POST",
|
||||
"ods_table": "group_buy_redemption_records",
|
||||
"description": "查询团购券/套餐券核销明细",
|
||||
"body": {
|
||||
"siteId": 2790685415443269,
|
||||
"offlineCouponChannel": 0,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"page": 1,
|
||||
"limit": 100,
|
||||
"queryType": 1
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.siteTableUseDetailsList"
|
||||
},
|
||||
{
|
||||
"id": "goods_stock_summary",
|
||||
"name_zh": "库存汇总报表",
|
||||
"module": "TenantGoods",
|
||||
"action": "GetGoodsStockReport",
|
||||
"method": "POST",
|
||||
"ods_table": "goods_stock_summary",
|
||||
"description": "查询商品库存汇总报表",
|
||||
"body": {
|
||||
"siteId": 2790685415443269,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.list"
|
||||
},
|
||||
{
|
||||
"id": "site_tables_master",
|
||||
"name_zh": "台桌主数据",
|
||||
"module": "Table",
|
||||
"action": "GetSiteTables",
|
||||
"method": "POST",
|
||||
"ods_table": "site_tables_master",
|
||||
"description": "查询门店台桌列表(含区域、状态、虚拟桌)",
|
||||
"body": {
|
||||
"showStatus": 0,
|
||||
"virtualTableType": -1,
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": false,
|
||||
"data_path": "data.siteTables"
|
||||
},
|
||||
{
|
||||
"id": "settlement_ticket_details",
|
||||
"name_zh": "结账小票明细",
|
||||
"module": "Order",
|
||||
"action": "GetOrderSettleTicketNew",
|
||||
"method": "POST",
|
||||
"ods_table": "settlement_ticket_details",
|
||||
"description": "查询结账小票明细(暂不可用)",
|
||||
"body": null,
|
||||
"pagination": null,
|
||||
"time_range": false,
|
||||
"data_path": null,
|
||||
"skip": true
|
||||
},
|
||||
{
|
||||
"id": "member_consumption_statistics",
|
||||
"name_zh": "会员消费统计",
|
||||
"module": "MemberProfile",
|
||||
"action": "QueryMemberConsumptionStatistics",
|
||||
"method": "POST",
|
||||
"ods_table": "member_consumption_statistics",
|
||||
"description": "按门店维度统计会员卡的消费、充值、退款等金额汇总,可按 cardTypeId 筛选卡种",
|
||||
"body": {
|
||||
"cardTypeId": 2793249295533893,
|
||||
"startTime": "2026-02-01 08:00:00",
|
||||
"endTime": "2026-02-13 08:00:00",
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
},
|
||||
"pagination": {
|
||||
"type": "page_limit",
|
||||
"page_key": "page",
|
||||
"limit_key": "limit",
|
||||
"max_limit": 100
|
||||
},
|
||||
"time_range": true,
|
||||
"time_keys": [
|
||||
"startTime",
|
||||
"endTime"
|
||||
],
|
||||
"data_path": "data.memberConsumptionStatisticsList"
|
||||
},
|
||||
{
|
||||
"id": "tenant_member_balance_overview",
|
||||
"name_zh": "会员余额总览",
|
||||
"module": "MemberProfile",
|
||||
"action": "TenantMemberBalanceOverview",
|
||||
"method": "POST",
|
||||
"ods_table": "tenant_member_balance_overview",
|
||||
"description": "查询各类会员卡统计一览(余额汇总)",
|
||||
"body": null,
|
||||
"pagination": null,
|
||||
"time_range": false,
|
||||
"data_path": "data"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,811 @@
|
||||
# 助教账号主数据(SearchAssistantInfo)
|
||||
|
||||
> 自动生成于 2026-02-13 | 数据来源:本地 JSON 样本
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 接口路径 | `PersonnelManagement/SearchAssistantInfo` |
|
||||
| 完整 URL | `https://pc.ficoo.vip/apiprod/admin/v1/PersonnelManagement/SearchAssistantInfo` |
|
||||
| 请求方法 | `POST` |
|
||||
| Content-Type | `application/json` |
|
||||
| 鉴权方式 | Bearer Token(`Authorization` 头) |
|
||||
| ODS 对应表 | `assistant_accounts_master` |
|
||||
| 分页方式 | `page` + `limit`(最大 100) |
|
||||
| 时间范围 | 不需要 |
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 示例值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `workStatusEnum` | int | `0` | 工作状态(0=全部) |
|
||||
| `dingTalkSynced` | int | `0` | 钉钉同步状态(0=全部) |
|
||||
| `leaveId` | int | `0` | 离职状态(0=全部) |
|
||||
| `criticismStatus` | int | `0` | 投诉状态(0=全部) |
|
||||
| `signStatus` | int | `-1` | 签署状态(-1=全部) |
|
||||
| `page` | int | `1` | 页码(从 1 开始) |
|
||||
| `limit` | int | `100` | 每页条数(最大 100) |
|
||||
|
||||
## 响应字段(共 61 个)
|
||||
|
||||
| # | 字段名 | 类型 | 示例值 |
|
||||
|---|--------|------|--------|
|
||||
| 1 | `job_num` | string | '' |
|
||||
| 2 | `shop_name` | string | '朗朗桌球' |
|
||||
| 3 | `group_id` | int | 0 |
|
||||
| 4 | `group_name` | string | '' |
|
||||
| 5 | `staff_profile_id` | int | 0 |
|
||||
| 6 | `ding_talk_synced` | int | 1 |
|
||||
| 7 | `entry_type` | int | 1 |
|
||||
| 8 | `team_name` | string | '1组' |
|
||||
| 9 | `entry_sign_status` | int | 0 |
|
||||
| 10 | `resign_sign_status` | int | 0 |
|
||||
| 11 | `system_role_id` | int | 10 |
|
||||
| 12 | `criticism_status` | int | 1 |
|
||||
| 13 | `salary_grant_enabled` | int | 2 |
|
||||
| 14 | `leave_status` | int | 1 |
|
||||
| 15 | `id` | int | 2947562271297029 |
|
||||
| 16 | `allow_cx` | int | 1 |
|
||||
| 17 | `assistant_no` | string | '31' |
|
||||
| 18 | `assistant_status` | int | 1 |
|
||||
| 19 | `avatar` | string | 'https://oss.ficoo.vip/maUiImages/images/defaultAvatar.png' |
|
||||
| 20 | `birth_date` | string | '0001-01-01 00:00:00' |
|
||||
| 21 | `charge_way` | int | 2 |
|
||||
| 22 | `create_time` | string | '2025-11-02 15:55:26' |
|
||||
| 23 | `cx_unit_price` | float | 0.0 |
|
||||
| 24 | `end_time` | string | '2025-12-01 08:00:00' |
|
||||
| 25 | `entry_time` | string | '2025-11-02 08:00:00' |
|
||||
| 26 | `gender` | int | 0 |
|
||||
| 27 | `height` | float | 0.0 |
|
||||
| 28 | `introduce` | string | '' |
|
||||
| 29 | `is_delete` | int | 0 |
|
||||
| 30 | `is_guaranteed` | int | 1 |
|
||||
| 31 | `is_team_leader` | int | 0 |
|
||||
| 32 | `last_table_id` | int | 0 |
|
||||
| 33 | `last_table_name` | string | '' |
|
||||
| 34 | `level` | int | 20 |
|
||||
| 35 | `light_equipment_id` | string | '' |
|
||||
| 36 | `light_status` | int | 2 |
|
||||
| 37 | `mobile` | string | '15119679931' |
|
||||
| 38 | `nickname` | string | '小然' |
|
||||
| 39 | `online_status` | int | 1 |
|
||||
| 40 | `order_trade_no` | int | 0 |
|
||||
| 41 | `pd_unit_price` | float | 0.0 |
|
||||
| 42 | `person_org_id` | int | 2947562271215109 |
|
||||
| 43 | `real_name` | string | '张静然' |
|
||||
| 44 | `resign_time` | string | '2025-11-03 08:00:00' |
|
||||
| 45 | `serial_number` | int | 0 |
|
||||
| 46 | `show_sort` | int | 31 |
|
||||
| 47 | `show_status` | int | 1 |
|
||||
| 48 | `site_id` | int | 2790685415443269 |
|
||||
| 49 | `site_light_cfg_id` | int | 0 |
|
||||
| 50 | `staff_id` | int | 0 |
|
||||
| 51 | `start_time` | string | '2025-11-01 08:00:00' |
|
||||
| 52 | `team_id` | int | 2792011585884037 |
|
||||
| 53 | `tenant_id` | int | 2790683160709957 |
|
||||
| 54 | `update_time` | string | '2025-11-03 18:32:07' |
|
||||
| 55 | `user_id` | int | 2947562270838277 |
|
||||
| 56 | `video_introduction_url` | string | '' |
|
||||
| 57 | `weight` | float | 0.0 |
|
||||
| 58 | `work_status` | int | 2 |
|
||||
| 59 | `assistant_grade` | float | 0.0 |
|
||||
| 60 | `sum_grade` | float | 0.0 |
|
||||
| 61 | `get_grade_times` | int | 0 |
|
||||
|
||||
## 详细字段分析
|
||||
|
||||
> 以下内容迁移自旧版 `assistant_accounts_master-Analysis.md`,包含字段的业务含义、枚举值、跨表关联等详细说明。
|
||||
|
||||
一、文件整体定位与结构
|
||||
|
||||
业务含义(内容类型)
|
||||
|
||||
该文件是 “助教账号/人事档案维表”,记录的是某门店下所有助教(含管理类账号)的账号配置、人事状态、可见性、计费策略等基础信息。
|
||||
|
||||
每条记录对应 一名助教账号,是一张典型的“维度表”(在数据模型中,与“助教流水”等事实表通过 id / user_id / team_id / site_id 等字段关联)。
|
||||
|
||||
二、记录级字段详解(按逻辑分组)
|
||||
1. 主键 / 账号身份类字段
|
||||
|
||||
id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:助教账号主键 ID,在“助教流水.json”中对应 site_assistant_id。
|
||||
|
||||
作用:所有与助教相关的事实表(助教流水、助教排班等)都会通过这个 ID 关联到该维表。
|
||||
|
||||
user_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:系统级“用户账号 ID”,通常对应登录账号。
|
||||
|
||||
关联:
|
||||
|
||||
在“助教流水.json”中有同名字段 user_id,与此完全一致。
|
||||
|
||||
用途:用于统一人员在不同角色/模块下的账号,区别于岗位级的 id。
|
||||
|
||||
assistant_no
|
||||
|
||||
类型:string
|
||||
|
||||
观测值:'1' ~ '39' 等编号,重复时对应不同助教(编号不唯一)。
|
||||
|
||||
含义(结合字段名推测):助教工号 / 编号,便于业务侧识别。
|
||||
|
||||
关联:在“助教流水.json”中有 assistantNo,与此字段对应。
|
||||
|
||||
job_num
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全为 ''(空字符串)。
|
||||
|
||||
含义:备用工号字段,目前未在该门店启用。
|
||||
|
||||
serial_number
|
||||
|
||||
类型:int
|
||||
|
||||
观测:部分为 0,部分是较大的整数(例如 2738, 2698, 2534…)。
|
||||
|
||||
含义(推测):系统内部生成的序列号或排序标识,用于全局排序或迁移。
|
||||
|
||||
2. 个人基础信息字段
|
||||
|
||||
real_name
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教真实姓名,如“何海婷”“梁婷婷”等。
|
||||
|
||||
关联:在“助教流水.json”的 assistantName 与此一致。
|
||||
|
||||
nickname
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教在前台展示的昵称,如“佳怡”“周周”“球球”等。
|
||||
|
||||
用途:与真实姓名区分,用于顾客侧展示。如在助教流水中 nickname 就是这个值。
|
||||
|
||||
gender
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测值:
|
||||
|
||||
0 × 40
|
||||
|
||||
1 × 1
|
||||
|
||||
2 × 9
|
||||
|
||||
含义(结合常见约定与值分布推测):
|
||||
|
||||
0:未填/保密
|
||||
|
||||
1:男
|
||||
|
||||
2:女
|
||||
|
||||
birth_date
|
||||
|
||||
类型:string,时间格式。
|
||||
|
||||
观测值:
|
||||
|
||||
大部分为 "0001-01-01 00:00:00"(显然是默认无效日期)
|
||||
|
||||
少量为真实日期,如 "2007-01-14 00:00:00" 等。
|
||||
|
||||
含义:助教出生日期。
|
||||
|
||||
mobile
|
||||
|
||||
类型:string
|
||||
|
||||
观测:11 位手机号,每个账号基本唯一。
|
||||
|
||||
含义:助教手机号,用于登录绑定、通知、钉钉同步等。
|
||||
|
||||
avatar
|
||||
|
||||
类型:string
|
||||
|
||||
观测:
|
||||
|
||||
大量为默认头像 URL,如 .../defaultAvatar.png
|
||||
|
||||
少量为具体头像图片 URL。
|
||||
|
||||
含义:助教头像地址。
|
||||
|
||||
introduce
|
||||
|
||||
类型:string
|
||||
|
||||
观测:当前导出中全部为空字符串。
|
||||
|
||||
含义:个人简介文案,预留给助教自我介绍使用。
|
||||
|
||||
video_introduction_url
|
||||
|
||||
类型:string
|
||||
|
||||
观测:
|
||||
|
||||
49 条为 ''
|
||||
|
||||
1 条为视频 URL(oss 存储路径)
|
||||
|
||||
含义:助教个人视频介绍地址。
|
||||
|
||||
height
|
||||
|
||||
类型:float
|
||||
|
||||
观测:
|
||||
|
||||
多数为 0.0,少量为 163.0, 166.0, 167.0, 165.0, 170.0 等。
|
||||
|
||||
含义:身高(单位:厘米)。0 表示未填写。
|
||||
|
||||
weight
|
||||
|
||||
类型:float
|
||||
|
||||
观测:
|
||||
|
||||
多数为 0.0
|
||||
|
||||
少量为 55.0, 90.0, 100.0 等。
|
||||
|
||||
含义:体重(单位:公斤)。0 表示未填写。
|
||||
|
||||
3. 组织、团队与门店维度字段
|
||||
|
||||
tenant_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:所有记录相同。
|
||||
|
||||
含义:品牌/租户 ID,对应“非球科技”系统中该商户的唯一标识。
|
||||
|
||||
用途:多门店时用来区分不同商户。
|
||||
|
||||
site_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:所有记录相同。
|
||||
|
||||
含义:门店 ID,对应本次数据的这家球房(朗朗桌球)。
|
||||
|
||||
关联:与其它 JSON(台费流水、库存、销售等)中的 site_id 一致。
|
||||
|
||||
shop_name
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全部为 "朗朗桌球"。
|
||||
|
||||
含义:门店名称,冗余字段,用于展示。
|
||||
|
||||
team_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:所有记录同一个值(唯一团队)。
|
||||
|
||||
含义:助教所属团队 ID。
|
||||
|
||||
关联:在“助教流水.json”中 assistant_team_id 与此一致。
|
||||
|
||||
team_name
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全部为 "1组"。
|
||||
|
||||
含义:团队名称,展示用,和 team_id 一一对应。
|
||||
|
||||
group_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全部为 0。
|
||||
|
||||
含义(推测):上层“分组 ID”预留字段(例如集团/事业部),本门店未使用。
|
||||
|
||||
group_name
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全部为 ''。
|
||||
|
||||
含义:group_id 对应的名称,目前为空。
|
||||
|
||||
person_org_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:每条记录一个不同的 ID。
|
||||
|
||||
含义:人事组织 ID,通常表示“某某门店-助教部-某小组”等层级组织。
|
||||
|
||||
关联:
|
||||
|
||||
在“助教流水.json”中同名字段 person_org_id 与此一致。
|
||||
|
||||
用途:用于人力组织维度统计、权限控制。
|
||||
|
||||
staff_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全部为 0。
|
||||
|
||||
含义(推测):预留给“人事系统员工 ID”的字段,目前未接入或未启用。
|
||||
|
||||
staff_profile_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全部为 0。
|
||||
|
||||
含义(推测):人事档案 ID,与第三方 HR 系统或内部员工档案集成使用,当前未启用。
|
||||
|
||||
4. 等级、计费与薪资配置字段
|
||||
|
||||
level
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测值:
|
||||
|
||||
10 × 24
|
||||
|
||||
20 × 18
|
||||
|
||||
30 × 4
|
||||
|
||||
40 × 3
|
||||
|
||||
8 × 1
|
||||
|
||||
含义(结合“助教流水中的 assistant_level / levelName 推测”):
|
||||
|
||||
8:助教管理/管理员(和流水里的 "助教管理" 对应)
|
||||
|
||||
10:初级助教
|
||||
|
||||
20:中级助教
|
||||
|
||||
30:高级助教
|
||||
|
||||
40:更高等级(可能是“资深/专家”,该等级在流水里暂未出现)。
|
||||
|
||||
关联:在“助教流水.json”里以 assistant_level+levelName 体现。
|
||||
|
||||
assistant_grade
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全部为 0.0。
|
||||
|
||||
含义(推测):助教综合评分(员工维度的平均分 snapshot),当前尚未启用评分。
|
||||
|
||||
sum_grade
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全为 0.0。
|
||||
|
||||
含义:评分总和,用于计算平均分(assistant_grade = sum_grade / get_grade_times),当前为 0。
|
||||
|
||||
get_grade_times
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:累计被评分次数。
|
||||
|
||||
charge_way
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 2。
|
||||
|
||||
含义(推测):计费方式:
|
||||
|
||||
2 代表当前门店为“计时收费”,其他值(1、3 等)可能对应按局、按课时等,当前未出现。
|
||||
|
||||
pd_unit_price
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全为 0.0。
|
||||
|
||||
含义(推测):某种标准单价(例如“普通时段单价”),这里未在账号上配置(实际单价在助教商品或套餐配置中)。
|
||||
|
||||
cx_unit_price
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全为 0.0。
|
||||
|
||||
含义(推测):促销时段的单价,本门店未在账号表层面设置。
|
||||
|
||||
allow_cx
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(从字段名推测):
|
||||
|
||||
是否允许此助教参与“促销价(促销=促销/促销场)”:
|
||||
|
||||
1:允许参与促销计费。
|
||||
|
||||
其他值(未出现)可能为不允许。
|
||||
|
||||
is_guaranteed
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(从字段名推测):是否配置“保底薪酬/保底时长”:
|
||||
|
||||
1:有保底规则。
|
||||
|
||||
其他值可能表示无保底。
|
||||
|
||||
salary_grant_enabled
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 2。
|
||||
|
||||
含义(推测):薪资发放配置开关:
|
||||
|
||||
2:一种固定含义(例如“参与薪资发放方案”或相反),具体码值需看系统配置。
|
||||
|
||||
仅从这份数据无法区分是否“启用/禁用”,只能确认这是一个薪酬相关开关字段。
|
||||
|
||||
5. 入职 / 离职 / 考勤签署相关字段
|
||||
|
||||
entry_time
|
||||
|
||||
类型:string
|
||||
|
||||
观测:各类日期 "2025-07-16 08:00:00", "2025-09-01 08:00:00" 等。
|
||||
|
||||
含义:入职时间。
|
||||
|
||||
resign_time
|
||||
|
||||
类型:string
|
||||
|
||||
观测:
|
||||
|
||||
对在职员工:类似 "2225-11-01 17:57:41" 这类非常未来的年份,显然是“占位默认值”。
|
||||
|
||||
对已离职员工:正常的近时间,如 "2025-10-13 08:00:00" 等。
|
||||
|
||||
含义:离职日期;使用“远未来日期”作为“未离职”的占位。
|
||||
|
||||
entry_type
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(推测):入职类型:
|
||||
|
||||
1:正式入职。
|
||||
|
||||
其他值可能表示实习、兼职等,当前未出现。
|
||||
|
||||
entry_sign_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义(推测):入职协议/合同签署状态:
|
||||
|
||||
0:未签署。
|
||||
|
||||
其他值可能表示已签署(目前未启用电子签功能)。
|
||||
|
||||
resign_sign_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义(推测):离职协议签署状态,类似上面。
|
||||
|
||||
leave_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:
|
||||
|
||||
0 × 21
|
||||
|
||||
1 × 29
|
||||
|
||||
结合 work_status 和 resign_time 可以明确判断:
|
||||
|
||||
0:在职(resign_time 为 2225 年占位)
|
||||
|
||||
1:已离职(resign_time 为真实近日期)
|
||||
|
||||
work_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:
|
||||
|
||||
当 leave_status = 0 时,work_status = 1
|
||||
|
||||
当 leave_status = 1 时,work_status = 2
|
||||
|
||||
推断含义:
|
||||
|
||||
1:在岗/可排班
|
||||
|
||||
2:离岗/停止安排(与离职状态挂钩)。
|
||||
|
||||
6. 账号启用、展示与在线状态字段
|
||||
|
||||
assistant_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:
|
||||
|
||||
1 × 48
|
||||
|
||||
2 × 2
|
||||
|
||||
含义(推测):账号启用状态:
|
||||
|
||||
1:启用
|
||||
|
||||
2:停用 / 冻结(这两条仍处于 leave_status = 0,说明未离职但账号被禁用)。
|
||||
|
||||
show_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(推测):前台展示状态:
|
||||
|
||||
1:在助教选择界面展示。
|
||||
|
||||
其他值可能是不展示。
|
||||
|
||||
show_sort
|
||||
|
||||
类型:int
|
||||
|
||||
观测:多值,如 1, 3, 7, 9, 10, 11, 12, 16, 21, 25, 30, 36, 38, 39, 100 等。
|
||||
|
||||
含义:前台展示排序权重,值越小/越大对应不同的排序策略(当前看起来与 assistant_no 有一定对应关系)。
|
||||
|
||||
online_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(推测):在线状态;当前门店所有助教账号均为在线状态。
|
||||
|
||||
is_delete
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:逻辑删除标记:
|
||||
|
||||
0:未删除
|
||||
|
||||
1:已逻辑删除(数据保留,前台不可见)。
|
||||
|
||||
7. 评价与投诉相关字段
|
||||
|
||||
criticism_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:
|
||||
|
||||
1 × 49
|
||||
|
||||
2 × 1
|
||||
|
||||
含义(推测):投诉/差评状态:
|
||||
|
||||
1:无投诉或正常
|
||||
|
||||
2:有投诉记录。
|
||||
|
||||
assistant_grade / sum_grade / get_grade_times
|
||||
|
||||
已在上文等级部分说明:
|
||||
|
||||
当前全部为 0,表示该门店尚未产生助教评价数据,但字段结构已经做好。
|
||||
|
||||
8. 时间元数据与最近服务记录
|
||||
|
||||
create_time
|
||||
|
||||
类型:string
|
||||
|
||||
含义:账号创建时间。
|
||||
|
||||
update_time
|
||||
|
||||
类型:string
|
||||
|
||||
含义:账号最近一次被修改的时间(例如修改等级、昵称等)。
|
||||
|
||||
start_time
|
||||
|
||||
类型:string
|
||||
|
||||
观测:多为整月开始,如 "2025-07-01 08:00:00", "2025-09-01 08:00:00" 等。
|
||||
|
||||
含义(推测):当前配置生效的开始日期。
|
||||
|
||||
end_time
|
||||
|
||||
类型:string
|
||||
|
||||
观测:对应结束日期,如 "2025-08-01 08:00:00", "2025-10-01 08:00:00" 等。
|
||||
|
||||
含义:当前配置生效的结束日期(例如一个周期性的排班/合同周期)。
|
||||
|
||||
last_table_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:
|
||||
|
||||
大多为 0
|
||||
|
||||
少量为实际台桌 ID。
|
||||
|
||||
含义:该助教最近一次服务的球台 ID。
|
||||
|
||||
last_table_name
|
||||
|
||||
类型:string
|
||||
|
||||
观测:大多为 '',少量为 "TV", "888" 等。
|
||||
|
||||
含义:最近服务球台名称(展示用)。
|
||||
|
||||
last_update_name
|
||||
|
||||
类型:string
|
||||
|
||||
观测:如 "助教管理员:黄月柳", "管理员:郑丽珊"。
|
||||
|
||||
含义:最近修改该账号配置的管理员名称。
|
||||
|
||||
order_trade_no
|
||||
|
||||
类型:int
|
||||
|
||||
观测:
|
||||
|
||||
绝大多数为 0
|
||||
|
||||
少量为非 0 的订单号。
|
||||
|
||||
含义(推测):该助教最近一次关联的订单号,用于快速跳转或回溯最近服务行为。
|
||||
|
||||
9. 灯控、钉钉等系统集成相关字段
|
||||
|
||||
ding_talk_synced
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(从字段名推测):是否已同步至钉钉:
|
||||
|
||||
1:已同步
|
||||
|
||||
其他值:未同步/错误等。
|
||||
|
||||
site_light_cfg_id
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:门店灯控配置 ID,本门店未在助教账号维度启用。
|
||||
|
||||
light_equipment_id
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全为 ''。
|
||||
|
||||
含义:灯控设备 ID,如果开启“助教开台自动控制灯”,会通过该字段关联到灯控硬件。
|
||||
|
||||
light_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 2。
|
||||
|
||||
含义(推测):灯光控制状态,如 1=启用控制、2=不启用 或相反。
|
||||
|
||||
由于所有记录是同一个值,只能确认这是一个预留状态字段。
|
||||
|
||||
10. 其他标志字段
|
||||
|
||||
is_team_leader
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:是否为团队长/组长:
|
||||
|
||||
0:普通助教
|
||||
|
||||
1:团队长(当前门店未指定团队长)。
|
||||
|
||||
三、与其他 JSON 的字段级关联(从结构角度)
|
||||
|
||||
仍然只从“结构 / 关联键”角度说明,不做任何经营或盈利分析:
|
||||
|
||||
与《助教流水.json》的关联
|
||||
|
||||
助教流水.site_assistant_id ↔ 助教账号.id
|
||||
|
||||
助教流水.user_id ↔ 助教账号.user_id
|
||||
|
||||
助教流水.assistant_team_id ↔ 助教账号.team_id
|
||||
|
||||
助教流水.person_org_id ↔ 助教账号.person_org_id
|
||||
|
||||
助教流水.assistant_level ↔ 助教账号.level(以及 levelName)
|
||||
|
||||
助教流水.nickname ↔ 助教账号.nickname
|
||||
|
||||
说明:助教流水是事实表,这个文件是对应的助教维表。
|
||||
|
||||
与门店维度 / 其它业务表
|
||||
|
||||
所有表的 tenant_id、site_id 一致,说明这些记录全部属于同一商户、同一门店。
|
||||
|
||||
台费流水、销售记录、库存变化等表通过 site_id、shop_name 共享门店维。
|
||||
|
||||
与订单相关表(小票、结账)
|
||||
|
||||
此文件中的 order_trade_no 仅是“最近订单号”的影子值,真正的订单明细仍以订单表、小票详情中的 order_trade_no 和 orderSettleId 为主。
|
||||
|
||||
在“助教流水”中,order_trade_no、order_settle_id 与助教账号并无直接外键关系,而是通过“助教流水”这张桥接事实表关联起来。
|
||||
|
||||
与外部系统(钉钉 / 灯控)
|
||||
|
||||
ding_talk_synced / staff_profile_id / staff_id 等为与企业内部人事系统、钉钉等集成预留的字段。
|
||||
|
||||
site_light_cfg_id / light_equipment_id / light_status 为与灯控设备联动预留的字段,目前在该门店未实际启用。
|
||||
@@ -0,0 +1,444 @@
|
||||
# 助教撤销记录(GetAbolitionAssistant)
|
||||
|
||||
> 自动生成于 2026-02-13 | 数据来源:本地 JSON 样本
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 接口路径 | `AssistantPerformance/GetAbolitionAssistant` |
|
||||
| 完整 URL | `https://pc.ficoo.vip/apiprod/admin/v1/AssistantPerformance/GetAbolitionAssistant` |
|
||||
| 请求方法 | `POST` |
|
||||
| Content-Type | `application/json` |
|
||||
| 鉴权方式 | Bearer Token(`Authorization` 头) |
|
||||
| ODS 对应表 | `assistant_cancellation_records` |
|
||||
| 分页方式 | `page` + `limit`(最大 100) |
|
||||
| 时间范围 | 需要(startTime / endTime) |
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 示例值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `startTime` | string | `"2026-02-01 08:00:00"` | 查询起始时间 |
|
||||
| `endTime` | string | `"2026-02-13 08:00:00"` | 查询结束时间 |
|
||||
| `siteId` | int | `2790685415443269` | 门店 ID |
|
||||
| `page` | int | `1` | 页码(从 1 开始) |
|
||||
| `limit` | int | `100` | 每页条数(最大 100) |
|
||||
|
||||
## 响应字段(共 13 个)
|
||||
|
||||
| # | 字段名 | 类型 | 示例值 |
|
||||
|---|--------|------|--------|
|
||||
| 1 | `siteProfile` | object | {'id': 2790685415443269, 'org_id': 2790684179467077, 'sho... |
|
||||
| 2 | `createTime` | string | '2025-11-09 19:23:29' |
|
||||
| 3 | `id` | int | 2957675849518789 |
|
||||
| 4 | `siteId` | int | 2790685415443269 |
|
||||
| 5 | `tableAreaId` | int | 2791963816579205 |
|
||||
| 6 | `tableId` | int | 2793016660660357 |
|
||||
| 7 | `tableArea` | string | 'C区' |
|
||||
| 8 | `tableName` | string | 'C1' |
|
||||
| 9 | `assistantOn` | string | '27' |
|
||||
| 10 | `assistantName` | string | '泡芙' |
|
||||
| 11 | `pdChargeMinutes` | int | 214 |
|
||||
| 12 | `assistantAbolishAmount` | float | 5.83 |
|
||||
| 13 | `trashReason` | string | '' |
|
||||
|
||||
## 详细字段分析
|
||||
|
||||
> 以下内容迁移自旧版 `assistant_cancellation_records-Analysis.md`,包含字段的业务含义、枚举值、跨表关联等详细说明。
|
||||
|
||||
1. 门店相关字段
|
||||
1.1 siteProfile
|
||||
|
||||
类型:对象(Object)
|
||||
|
||||
含义:门店信息快照。
|
||||
|
||||
结构:包含以下子字段(26 个左右):
|
||||
|
||||
id:门店 ID(与其他 JSON 中的 site_id 一致)。
|
||||
|
||||
org_id:组织 ID(总部/品牌组织)。
|
||||
|
||||
shop_name:店名,如“朗朗桌球”。
|
||||
|
||||
avatar:门店头像图片 URL。
|
||||
|
||||
business_tel:门店电话。
|
||||
|
||||
full_address:详细地址。
|
||||
|
||||
address:简化地址描述。
|
||||
|
||||
longitude / latitude:经纬度。
|
||||
|
||||
tenant_site_region_id:区域行政编码。
|
||||
|
||||
tenant_id:租户 ID(品牌商户 ID)。
|
||||
|
||||
auto_light:是否自动控灯(0/1 枚举)。
|
||||
|
||||
attendance_distance:考勤打卡范围。
|
||||
|
||||
wifi_name / wifi_password:WiFi 账号和密码。
|
||||
|
||||
customer_service_qrcode / customer_service_wechat:客服二维码、微信号。
|
||||
|
||||
fixed_pay_qrCode:固定收款码。
|
||||
|
||||
prod_env:环境标记(测试/生产等)。
|
||||
|
||||
light_status / light_type / light_token:灯控相关配置。
|
||||
|
||||
site_type:门店类型枚举。
|
||||
|
||||
site_label:门店标签(如“A”)。
|
||||
|
||||
attendance_enabled:是否启用考勤(0/1)。
|
||||
|
||||
shop_status:门店状态(1=营业等)。
|
||||
|
||||
特点:
|
||||
|
||||
与其他 JSON 中的 siteProfile 完全同构,是冗余的门店信息快照。
|
||||
|
||||
真正的主键是 siteProfile.id,且与同记录的 siteId 一致。
|
||||
|
||||
1.2 siteId
|
||||
|
||||
类型:整数(long)
|
||||
|
||||
观测:所有记录均为同一值 2790685415443269。
|
||||
|
||||
含义:门店 ID,即该废除记录所在门店。
|
||||
|
||||
关联:
|
||||
|
||||
与 siteProfile.id 一致。
|
||||
|
||||
与其他 JSON 中所有 site_id 字段相同(同一门店的数据)。
|
||||
|
||||
2. 台桌维度字段
|
||||
|
||||
这几个字段描述废除发生在哪张桌、哪个区域。
|
||||
|
||||
2.1 tableId
|
||||
|
||||
类型:整数(long)
|
||||
|
||||
含义:球台/桌子的 ID。
|
||||
|
||||
关联:
|
||||
|
||||
对应 “台桌列表.json” 中的 id 字段。
|
||||
|
||||
用于定位具体哪一张台桌上发生了助教废除。
|
||||
|
||||
2.2 tableName
|
||||
|
||||
类型:字符串(string)
|
||||
|
||||
示例值:
|
||||
|
||||
"C1", "C2", "B9", "VIP1", "A4", "666", "董事办", "补时长5", "M1" 等。
|
||||
|
||||
含义:台桌名称/编号,供人阅读。
|
||||
|
||||
关系:
|
||||
|
||||
与台桌列表中的 table_name 或 table_no 文本一致。
|
||||
|
||||
作为冗余字段存在,即使不联表也能看出是哪个桌。
|
||||
|
||||
2.3 tableAreaId
|
||||
|
||||
类型:整数(long)
|
||||
|
||||
示例:2791963816579205 等。
|
||||
|
||||
含义:台桌所在区域 ID。
|
||||
|
||||
关联:
|
||||
|
||||
应对应“区域配置表”的主键(本次导出未包含该表)。
|
||||
|
||||
与其他 JSON 中出现的 tableAreaId(比如台费流水、助教流水里的区域字段)是一致的。
|
||||
|
||||
2.4 tableArea
|
||||
|
||||
类型:字符串(string)
|
||||
|
||||
示例值:
|
||||
|
||||
"C区", "B区", "A区", "VIP包厢", "K包", "补时长", "666"。
|
||||
|
||||
含义:台桌所属区域名称。
|
||||
|
||||
说明:
|
||||
|
||||
用于展示和报表分区。
|
||||
|
||||
与 tableAreaId 一起从“区域维表”中可以查出区域层级信息(本次数据未导出该表)。
|
||||
|
||||
3. 助教维度字段
|
||||
|
||||
反映是哪一个助教被废除。
|
||||
|
||||
3.1 assistantOn
|
||||
|
||||
类型:字符串(string)
|
||||
|
||||
观测值(本次 15 条记录中):
|
||||
|
||||
'2', '4', '9', '16', '23', '27', '52', '15', '99'
|
||||
|
||||
含义:助教编号(工号/序号)。
|
||||
|
||||
说明:
|
||||
|
||||
虽然是字符串,但内容上是纯数字,实际是编号,不是业务金额。
|
||||
|
||||
与 助教流水.json 中的 assistantNo 字段是一致的。
|
||||
|
||||
与“助教账号1/2.json” 中的 assistant_no 字段相同,用于识别哪位助教。
|
||||
|
||||
枚举性质:
|
||||
|
||||
在当前门店范围内,assistantOn 实际上是枚举集合(有限个编号)。
|
||||
|
||||
具体编号-姓名的映射关系在“助教账号”表中定义,不在本文件中。
|
||||
|
||||
3.2 assistantName
|
||||
|
||||
类型:字符串(string)
|
||||
|
||||
观测值(本次 15 条中):
|
||||
|
||||
'佳怡'、'璇子'、'周周'、'球球'、'泡芙'、'婉婉'、'小柔'、'七七'、'Amy'
|
||||
|
||||
含义:助教姓名/对外展示名称。
|
||||
|
||||
关系:
|
||||
|
||||
与“助教账号”档案中的 real_name / nickname 对应。
|
||||
|
||||
与 助教流水.json 里的 assistantName 字段一致。
|
||||
|
||||
注意:
|
||||
|
||||
这是被废除的那位助教,不是顾客姓名。
|
||||
|
||||
4. 时间与时长字段
|
||||
4.1 createTime
|
||||
|
||||
类型:字符串(string),格式 YYYY-MM-DD HH:MM:SS
|
||||
|
||||
示例:"2025-11-09 19:23:29"
|
||||
|
||||
含义:这条“助教废除记录”被创建的时间,即系统正式记录“废除”操作的时刻。
|
||||
|
||||
与其他时间字段关系:
|
||||
|
||||
在 助教流水.json 里有 create_time / ledger_start_time / ledger_end_time 等,本字段通常会落在这些时间点之后,表示在某次服务计时过程后发生了废除操作。
|
||||
|
||||
数据特征:
|
||||
|
||||
所有记录都有非空时间,精确到秒。
|
||||
|
||||
4.2 pdChargeMinutes
|
||||
|
||||
类型:整数(int)
|
||||
|
||||
示例值:214, 10800, 3602, 3600, 2379, 14400, 10605, 10608, 10611, 0 等。
|
||||
|
||||
含义(结构层面):
|
||||
|
||||
“已发生的计费时长(分钟)”,即这次助教服务在被废除前已经累计了多少分钟。
|
||||
|
||||
特点:
|
||||
|
||||
单位是“分钟”,不是秒。
|
||||
|
||||
绝大部分是较大的整数(如 10800 分钟这样的数字,显然系统里有异常/默认值,具体业务含义要结合上下文看,这里只从字段命名和类型说明)。
|
||||
|
||||
也有 0 的情况,表示发生废除时尚未有有效计费时间产生(例如刚排钟就撤销)。
|
||||
|
||||
与其他字段的关系(结构层面推断,不做业务结论):
|
||||
|
||||
这类字段很可能用于后续计算“应退时长”或“扣费时长”。
|
||||
|
||||
对应 助教流水 里关于 real_use_seconds、income_seconds 的记录,可用来判断“废除时已经消耗了多少时间”。
|
||||
|
||||
5. 金额字段
|
||||
5.1 assistantAbolishAmount
|
||||
|
||||
类型:浮点数(float)
|
||||
|
||||
示例值:
|
||||
|
||||
5.83, 570.0, 108.06, 190.0, 71.37, 392.0, 465.44, 318.24, 318.33, 以及 0.0。
|
||||
|
||||
含义(结构层面):
|
||||
|
||||
与“助教废除”关联的金额字段。字面上是“助教废除金额”。
|
||||
|
||||
可以理解为本次废除操作对应的一笔金额数值(是扣除、退还、补差,由业务规则决定,这里不做盈利/收益分析)。
|
||||
|
||||
特点:
|
||||
|
||||
为浮点数,单位为元。
|
||||
|
||||
存在 0 值,表示这条废除记录没有产生额外金额变动(纯记录操作)。
|
||||
|
||||
可能的用途(从字段角色角度,而不是结论):
|
||||
|
||||
后续在账务模块中,可以用 assistantAbolishAmount 这类字段与其他表(如退款记录、余额变更记录)进行金额对账和逻辑匹配。
|
||||
|
||||
6. 废除原因字段
|
||||
6.1 trashReason
|
||||
|
||||
类型:字符串(string)
|
||||
|
||||
当前数据观测:所有 15 条记录都是空字符串 ""。
|
||||
|
||||
含义:
|
||||
|
||||
用于记录“废除原因”的文本描述,例如“顾客临时有事取消”“录入错误”“更换助教”等。
|
||||
|
||||
特点:
|
||||
|
||||
可为空字符串,说明系统允许不填原因。
|
||||
|
||||
从结构上看,这是一个自由文本字段,不是枚举,不会做严格约束。
|
||||
|
||||
与其他字段的关系:
|
||||
|
||||
当配合 is_trash(在 助教流水.json 中)使用时,trashReason 可以为那条流水提供“为什么被废除”的说明。
|
||||
|
||||
本表专门记录“废除事件”的列表,因此 trashReason 是这张表记录的重要附加信息。
|
||||
|
||||
三、字段之间的结构关系与外部关联
|
||||
|
||||
虽然本文件字段不多,但从字段设计可以看出它在整个系统中的位置。这里只从“字段结构”和“关联键”的角度说明,不做业务/盈利分析。
|
||||
|
||||
1. 与门店表 / 全局维度的关系
|
||||
|
||||
siteId 与 siteProfile.id 一致,且与其他 JSON 中的 site_id 一致。
|
||||
|
||||
说明:这是典型的“门店维度外键 + 冗余快照”设计:
|
||||
|
||||
siteId 作为外键;
|
||||
|
||||
siteProfile 作为冗余快照,方便直接展示店名、地址等。
|
||||
|
||||
2. 与台桌列表(台桌列表.json)的关系
|
||||
|
||||
对应关系:
|
||||
|
||||
tableId ↔ 台桌列表中的 id
|
||||
|
||||
tableName ↔ 台桌列表中的 table_name / table_no
|
||||
|
||||
tableAreaId ↔ 台桌列表中的 area_id(通过区域表可以进一步找到区域名称)
|
||||
|
||||
tableArea ↔ 台桌列表中的 area_name
|
||||
|
||||
结论(结构层面):
|
||||
|
||||
助教废除.json 中关于桌台的四个字段,是对“台桌维度”信息的引用 + 冗余快照。
|
||||
|
||||
当用 tableId 去联查台桌列表时,可以获取更多静态信息(如台费设置、是否可预约等),本文件只保留了最基础的桌号和区域。
|
||||
|
||||
3. 与助教档案 / 助教流水的关系
|
||||
|
||||
对助教的标识字段:
|
||||
|
||||
assistantOn ↔ 助教流水中的 assistantNo ↔ 助教账号中的 assistant_no
|
||||
|
||||
assistantName ↔ 助教流水中的 assistantName ↔ 助教账号中的姓名字段
|
||||
|
||||
结构上的含义:
|
||||
|
||||
助教废除.json 可以看作是“助教服务流水”的一个特殊子集:只记录被废除的部分。
|
||||
|
||||
在 助教流水.json 中,存在字段 is_trash、trash_reason 等,它们从主流水视角记录“此条流水已经被废除”这一状态。
|
||||
|
||||
在 助教废除.json 中,則以“废除事件”为主视角,列出每一次废除操作的明细(在哪张桌、哪个助教、多少分钟、金额多少)。
|
||||
|
||||
需要注意的结构事实:
|
||||
|
||||
助教废除.json 里 没有 订单号类字段(例如 order_trade_no、order_assistant_id),因此如果要从“废除事件”反查到具体哪一条助教流水,目前只能通过组合条件关联,例如:
|
||||
|
||||
相同门店 siteId;
|
||||
|
||||
相同助教 assistantOn + assistantName;
|
||||
|
||||
相同台桌 tableId / tableName;
|
||||
|
||||
相近时间(createTime 对应助教流水的 create_time/ledger_end_time 附近)。
|
||||
|
||||
这说明系统在设计时,把“废除事件”作为独立表存储,但没有在导出中包含可直接联表的订单 ID。结构上就导致“硬外键”缺失,只能做“软匹配”。
|
||||
|
||||
4. 与资金/账户类表的潜在关系(结构层面)
|
||||
|
||||
关键金额字段:
|
||||
|
||||
assistantAbolishAmount 是本表唯一金额字段。
|
||||
|
||||
结合字段命名和位置,可以推断结构关系:
|
||||
|
||||
如果废除操作产生金额变动(例如退还部分费用或扣除违约金),那么在:
|
||||
|
||||
退款记录.json 中可能有对应一笔退款记录;
|
||||
|
||||
余额变更记录.json 中可能有对应一条会员卡余额变动(若退到卡里)。
|
||||
|
||||
这些表中不会直接有 assistantAbolishAmount 字段,但会有金额字段 + 关联 ID,结构上可能通过金额和时间进行逻辑匹配。
|
||||
|
||||
需要强调:
|
||||
|
||||
这里只指出“这个字段在系统里承担的是一个‘金额变量’的角色”,不做盈利/损益层面的任何分析或结论。
|
||||
|
||||
四、本表本身暴露出的结构性线索
|
||||
|
||||
清晰的单一职责
|
||||
助教废除.json 不包含订单号、支付信息、会员信息等字段,只保留:
|
||||
|
||||
门店/桌台维度;
|
||||
|
||||
助教维度;
|
||||
|
||||
时间、分钟数;
|
||||
|
||||
一个金额字段;
|
||||
|
||||
文本原因字段。
|
||||
说明这个表的设计就是“专门记录助教废除事件”的事件表,倾向于作为运营日志或审计用途,而不是主结算表。
|
||||
|
||||
软外键的设计取向
|
||||
|
||||
没有 order_trade_no、order_settle_id 等硬外键字段。
|
||||
|
||||
需要通过“时间 + 助教 + 桌台”的组合条件与 助教流水、订单/结算 进行软关联。
|
||||
|
||||
在迁移或对接新系统时,如果希望建立强外键,建议在新结构中给废除表补充 order_assistant_id 或 order_trade_no 之类的字段,以便直接关联。
|
||||
|
||||
分钟与秒的混用
|
||||
|
||||
pdChargeMinutes 单位是“分钟”;
|
||||
|
||||
在 助教流水.json 中,同类字段如 income_seconds / real_use_seconds 是“秒”。
|
||||
结构层面说明:系统在不同接口/表中用了不同的时间单位。
|
||||
在做结构统一或数据建模时,最好统一为一个单位(全部转为秒或全部转为分钟),否则容易出现比较/汇总混乱。
|
||||
|
||||
废除原因文本未被使用但结构预留完备
|
||||
|
||||
当前 15 条记录中,trashReason 全部为空,说明实际运营中并没有强制填写原因。
|
||||
|
||||
但结构上预留了这个字段,将来如果要做“废除原因统计”或“内部稽核”,无需修改结构,只要要求前台填写即可。
|
||||
|
||||
数量很少但字段完整
|
||||
|
||||
当前总记录数只有 15 条,但已经有完整的门店、桌台、助教、时长、金额、原因字段。
|
||||
|
||||
说明设计不是临时补的,而是参照完整流水表(助教流水)设计的一张配套表,只是当前时间范围内“废除事件”确实不多。
|
||||
@@ -0,0 +1,862 @@
|
||||
# 助教服务流水(GetOrderAssistantDetails)
|
||||
|
||||
> 自动生成于 2026-02-13 | 数据来源:本地 JSON 样本
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 接口路径 | `AssistantPerformance/GetOrderAssistantDetails` |
|
||||
| 完整 URL | `https://pc.ficoo.vip/apiprod/admin/v1/AssistantPerformance/GetOrderAssistantDetails` |
|
||||
| 请求方法 | `POST` |
|
||||
| Content-Type | `application/json` |
|
||||
| 鉴权方式 | Bearer Token(`Authorization` 头) |
|
||||
| ODS 对应表 | `assistant_service_records` |
|
||||
| 分页方式 | `page` + `limit`(最大 100) |
|
||||
| 时间范围 | 需要(startTime / endTime) |
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 示例值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `siteId` | int | `2790685415443269` | 门店 ID |
|
||||
| `startTime` | string | `"2026-02-01 08:00:00"` | 查询起始时间 |
|
||||
| `endTime` | string | `"2026-02-13 08:00:00"` | 查询结束时间 |
|
||||
| `IsConfirm` | int | `0` | 是否已确认(0=全部) |
|
||||
| `page` | int | `1` | 页码(从 1 开始) |
|
||||
| `limit` | int | `100` | 每页条数(最大 100) |
|
||||
|
||||
## 响应字段(共 64 个)
|
||||
|
||||
| # | 字段名 | 类型 | 示例值 |
|
||||
|---|--------|------|--------|
|
||||
| 1 | `assistantNo` | string | '27' |
|
||||
| 2 | `nickname` | string | '泡芙' |
|
||||
| 3 | `levelName` | string | '初级' |
|
||||
| 4 | `assistantName` | string | '何海婷' |
|
||||
| 5 | `tableName` | string | 'S1' |
|
||||
| 6 | `siteProfile` | object | {'id': 2790685415443269, 'org_id': 2790684179467077, 'sho... |
|
||||
| 7 | `skillName` | string | '基础课' |
|
||||
| 8 | `id` | int | 2957913441292165 |
|
||||
| 9 | `order_trade_no` | int | 2957784612605829 |
|
||||
| 10 | `site_id` | int | 2790685415443269 |
|
||||
| 11 | `tenant_id` | int | 2790683160709957 |
|
||||
| 12 | `operator_id` | int | 2790687322443013 |
|
||||
| 13 | `operator_name` | string | '收银员:郑丽珊' |
|
||||
| 14 | `order_settle_id` | int | 2957913171693253 |
|
||||
| 15 | `ledger_name` | string | '27-泡芙' |
|
||||
| 16 | `ledger_group_name` | string | '' |
|
||||
| 17 | `ledger_unit_price` | float | 98.0 |
|
||||
| 18 | `ledger_count` | int | 7592 |
|
||||
| 19 | `ledger_amount` | float | 206.67 |
|
||||
| 20 | `order_pay_id` | int | 0 |
|
||||
| 21 | `create_time` | string | '2025-11-09 23:25:11' |
|
||||
| 22 | `is_delete` | int | 0 |
|
||||
| 23 | `assistant_team_id` | int | 2792011585884037 |
|
||||
| 24 | `assistant_level` | int | 10 |
|
||||
| 25 | `ledger_start_time` | string | '2025-11-09 21:18:18' |
|
||||
| 26 | `ledger_end_time` | string | '2025-11-09 23:24:50' |
|
||||
| 27 | `is_single_order` | int | 1 |
|
||||
| 28 | `order_assistant_id` | int | 2957788717240005 |
|
||||
| 29 | `site_assistant_id` | int | 2946266869435205 |
|
||||
| 30 | `order_assistant_type` | int | 1 |
|
||||
| 31 | `ledger_status` | int | 1 |
|
||||
| 32 | `site_table_id` | int | 2793020259897413 |
|
||||
| 33 | `projected_income` | float | 168.0 |
|
||||
| 34 | `is_not_responding` | int | 0 |
|
||||
| 35 | `income_seconds` | int | 7560 |
|
||||
| 36 | `user_id` | int | 2946266868976453 |
|
||||
| 37 | `trash_applicant_id` | int | 0 |
|
||||
| 38 | `trash_applicant_name` | string | '' |
|
||||
| 39 | `is_trash` | int | 0 |
|
||||
| 40 | `trash_reason` | string | '' |
|
||||
| 41 | `real_use_seconds` | int | 7592 |
|
||||
| 42 | `add_clock` | int | 0 |
|
||||
| 43 | `returns_clock` | int | 0 |
|
||||
| 44 | `is_confirm` | int | 2 |
|
||||
| 45 | `member_discount_amount` | float | 0.0 |
|
||||
| 46 | `manual_discount_amount` | float | 0.0 |
|
||||
| 47 | `service_money` | float | 0.0 |
|
||||
| 48 | `person_org_id` | int | 2946266869336901 |
|
||||
| 49 | `last_use_time` | string | '2025-11-09 23:24:50' |
|
||||
| 50 | `salesman_name` | string | '' |
|
||||
| 51 | `salesman_user_id` | int | 0 |
|
||||
| 52 | `salesman_org_id` | int | 0 |
|
||||
| 53 | `coupon_deduct_money` | float | 0.0 |
|
||||
| 54 | `skill_id` | int | 2790683529513797 |
|
||||
| 55 | `start_use_time` | string | '2025-11-09 21:18:18' |
|
||||
| 56 | `tenant_member_id` | int | 0 |
|
||||
| 57 | `system_member_id` | int | 0 |
|
||||
| 58 | `skill_grade` | int | 0 |
|
||||
| 59 | `service_grade` | int | 0 |
|
||||
| 60 | `composite_grade` | float | 0.0 |
|
||||
| 61 | `sum_grade` | float | 0.0 |
|
||||
| 62 | `get_grade_times` | int | 0 |
|
||||
| 63 | `grade_status` | int | 1 |
|
||||
| 64 | `composite_grade_time` | string | '0001-01-01 00:00:00' |
|
||||
|
||||
## 新增字段(2026-02-14 全量刷新发现)
|
||||
|
||||
以下字段在最新 API 响应(100 条全量遍历)中出现,旧版 JSON 样本中不存在:
|
||||
|
||||
| 字段名 | 类型 | 出现率 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `assistantTeamName` | string | 100/100 | 助教所属团队/组名称(如"1组"),与 `assistant_team_id` 对应的文本冗余字段 |
|
||||
| `real_service_money` | float | 100/100 | 实际服务金额,扣除各类优惠后的助教服务实收金额 |
|
||||
|
||||
## 详细字段分析
|
||||
|
||||
> 以下内容迁移自旧版 `assistant_service_records-Analysis.md`,包含字段的业务含义、枚举值、跨表关联等详细说明。
|
||||
|
||||
二、记录级字段完整清单与说明
|
||||
|
||||
下面按逻辑分组来讲字段。数据类型和枚举值,是根据导出数据实际值推断出来的。
|
||||
|
||||
1. 订单与关联 ID 类字段
|
||||
|
||||
这些字段主要用来跟其他表做关联(订单、支付、会员、助教档案等):
|
||||
|
||||
id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:本条助教流水记录的主键 ID(流水唯一标识)。
|
||||
|
||||
作用:在系统内部唯一定位这一条助教服务记录。
|
||||
|
||||
order_trade_no
|
||||
|
||||
类型:int
|
||||
|
||||
含义:订单交易号,整个订单层面的编号。
|
||||
|
||||
关联:
|
||||
|
||||
与台费流水、门店销售记录、团购套餐流水等表中的同名字段是一致的,用于把 同一笔订单下的各类消费明细(台费/商品/助教/套餐)串起来。
|
||||
|
||||
order_settle_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:订单结算 ID,相当于“结账单号”的内部主键。
|
||||
|
||||
关联:
|
||||
|
||||
与小票详情中的 orderSettleId 对应。
|
||||
|
||||
正常情况下也应对应结账记录表中的结算主键(本次导出结账记录为空,但字段设计明显就是用来关联的)。
|
||||
|
||||
order_assistant_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:订单中“助教项目明细”的内部 ID。
|
||||
|
||||
作用:如果订单里有多条助教项目(比如换助教、多个时间段),此字段唯一标识这一条助教明细。
|
||||
|
||||
order_assistant_type
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测值:1 和 2。
|
||||
|
||||
含义(推测):
|
||||
|
||||
1:常规助教服务(主课/基础课)。
|
||||
|
||||
2:附加类助教服务(如“附加课”),和字段 skillName 的值相对应(本数据里,skillName 有 “基础课”和“附加课” 两类)。
|
||||
|
||||
实际含义以系统内部配置为准,但可以确定是 助教服务类型枚举。
|
||||
|
||||
order_pay_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:关联到“支付记录”的主键 ID。
|
||||
|
||||
作用:可以和支付记录中的 id / relate_id 等字段对应,找到这条助教服务对应的支付流水。
|
||||
|
||||
tenant_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:租户/品牌 ID;你这份数据中是固定值(同一个商户)。
|
||||
|
||||
关联:全库所有表都有,作为“商户维度”的过滤键。
|
||||
|
||||
site_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:门店 ID,本数据中指“朗朗桌球”这一家门店。
|
||||
|
||||
关联:
|
||||
|
||||
与其他所有 JSON 中的 site_id 一致,用于判断记录属于哪家门店。
|
||||
|
||||
与内嵌的 siteProfile.id 一致。
|
||||
|
||||
site_assistant_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:门店维度的助教 ID。
|
||||
|
||||
关联:
|
||||
|
||||
在 助教账号1/2.json 中,字段 id 就是这个 site_assistant_id。
|
||||
|
||||
即:助教流水.site_assistant_id = 助教账号.id → 这是助教档案的外键。
|
||||
|
||||
user_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:助教对应的“用户账号 ID”(系统级用户)。
|
||||
|
||||
关联:
|
||||
|
||||
在助教账号表中有同名字段 user_id,与这里完全一致。
|
||||
|
||||
一般是登录账号的主键,区别于 site_assistant_id(岗位/角色 ID)。
|
||||
|
||||
person_org_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:助教所属“人事组织/部门 ID”。
|
||||
|
||||
关联:
|
||||
|
||||
在助教账号表中同样存在 person_org_id 字段,值完全一致。
|
||||
|
||||
用来做人员组织维度的归属,如“朗朗桌球-助教部”。
|
||||
|
||||
assistant_team_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:助教所属团队 ID。
|
||||
|
||||
特点:当前数据中所有记录都是同一个 team_id。
|
||||
|
||||
关联:
|
||||
|
||||
在助教账号表中有 team_id 字段,对应相同值。
|
||||
|
||||
此字段常用于排班/团队统计。
|
||||
|
||||
tenant_member_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:商户维度会员 ID(门店/品牌内的会员主键)。
|
||||
|
||||
观测值:有不少为 0,表示非会员;非零时与会员档案中的 id 一致。
|
||||
|
||||
关联:
|
||||
|
||||
**会员档案(tenantMemberInfos)**中的 id = 此处的 tenant_member_id。
|
||||
|
||||
用来联表查出对应会员的基本资料。
|
||||
|
||||
system_member_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:系统级会员 ID(全集团统一 ID)。
|
||||
|
||||
观测:大部分非 0 记录,对应会员档案中的 system_member_id。
|
||||
|
||||
关联:
|
||||
|
||||
会员档案中的 system_member_id 字段。
|
||||
|
||||
说明:system_member_id 把一个会员在不同门店/不同卡种的账号串起来;tenant_member_id 则是本商户的那一条记录。
|
||||
|
||||
skill_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:助教服务“课程/技能”ID。
|
||||
|
||||
观测:当前数据中只有一个技能 ID(同一类“基础课/附加课”)。
|
||||
|
||||
关联:应对应某个“课程/技能配置表”的主键(你这次导出里没见那个表)。
|
||||
|
||||
2. 助教维度字段
|
||||
|
||||
这些字段描述“是哪位助教、什么级别、属于哪个组”等:
|
||||
|
||||
assistantNo
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教编号,例如 "27"。
|
||||
|
||||
关联:在助教账号表里也有 assistant_no 字段,对应工号/编号。
|
||||
|
||||
assistantName
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教姓名,如“何海婷”“胡敏”等。
|
||||
|
||||
备注:和助教账号档案里的 real_name 一致。
|
||||
|
||||
nickname
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教对外昵称,如“佳怡”“周周”“球球”等。
|
||||
|
||||
说明:从数据看,这个 nickname 是“助教昵称”,不是顾客昵称(容易混淆)。
|
||||
|
||||
关联:在很多小票、商品名里,会把 “编号-昵称” 组合使用(如 ledger_name = "2-佳怡")。
|
||||
|
||||
assistant_level
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测值与 levelName 对应关系(从数据中直接推出来):
|
||||
|
||||
8 → levelName = "助教管理"(管理角色)
|
||||
|
||||
10 → "初级"
|
||||
|
||||
20 → "中级"
|
||||
|
||||
30 → "高级"
|
||||
|
||||
说明:这是助教级别的数值编码,对应助教账号表中的 level 字段。
|
||||
|
||||
levelName
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教等级名称,与 assistant_level 一一对应(初级/中级/高级/助教管理)。
|
||||
|
||||
备注:属于展示用的冗余字段。
|
||||
|
||||
assistant_team_id
|
||||
|
||||
已在上一节说明(团队 ID)。
|
||||
|
||||
skillName
|
||||
|
||||
类型:string
|
||||
|
||||
观测值:"基础课" 或 "附加课"。
|
||||
|
||||
含义:当前这条助教服务所对应的“课程/技能名称”。
|
||||
|
||||
当 order_assistant_type = 1 时,多为“基础课”。
|
||||
|
||||
当 order_assistant_type = 2 时,为“附加课”。
|
||||
|
||||
skill_grade
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义(推测):顾客对“技能表现”的评分(整数或打分等级)。
|
||||
|
||||
当前数据中还未产生评分记录,所以都是默认值 0。
|
||||
|
||||
service_grade
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义(推测):顾客对“服务态度”的评分。
|
||||
|
||||
composite_grade
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全为 0.0。
|
||||
|
||||
含义:综合评分(例如技能+服务加权后的平均分),当前数据没有实际评分。
|
||||
|
||||
sum_grade
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全为 0.0。
|
||||
|
||||
含义:累计评分总和(可能用于计算平均分),当前为 0。
|
||||
|
||||
get_grade_times
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:该条记录对应的评价次数(或该助教被评价次数快照)。
|
||||
|
||||
grade_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 1。
|
||||
|
||||
含义(推测):评价状态,比如:
|
||||
|
||||
1 = 未评价/正常;
|
||||
|
||||
其他值可能表示“已评价”“屏蔽”等,当前数据没有别的值,具体含义需要系统配置表。
|
||||
|
||||
composite_grade_time
|
||||
|
||||
类型:string(时间)
|
||||
|
||||
观测:全为 "0001-01-01 00:00:00"。
|
||||
|
||||
含义(推测):最近一次评价时间/综合评分更新时间。现在都是默认“无效时间”。
|
||||
|
||||
3. 桌台 / 门店维度字段
|
||||
|
||||
tableName
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教服务所在的球台名称(如 "A17"、"S1")。
|
||||
|
||||
关联:
|
||||
|
||||
与台桌列表中的 table_name / table_no 对应(通过 site_table_id)。
|
||||
|
||||
site_table_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:球台 ID。
|
||||
|
||||
关联:
|
||||
|
||||
对应台桌列表中的 id 字段,表示具体是哪一张桌。
|
||||
|
||||
siteProfile
|
||||
|
||||
类型:object
|
||||
|
||||
含义:门店信息快照,包括 id、shop_name、address 等,和其他 JSON 里的 siteProfile 一致。
|
||||
|
||||
作用:冗余门店信息,方便查看(而不是每次都联表看门店档案)。
|
||||
|
||||
4. 时间 / 时长相关字段
|
||||
4.1 时间点(字符串时间)
|
||||
|
||||
create_time
|
||||
|
||||
类型:string,格式 YYYY-MM-DD HH:MM:SS
|
||||
|
||||
含义:这条助教流水记录创建时间(一般接近结算/下单时间)。
|
||||
|
||||
start_use_time
|
||||
|
||||
类型:string
|
||||
|
||||
含义:助教实际开始服务时间。
|
||||
|
||||
特点:正常情况下与 ledger_start_time 相同。
|
||||
|
||||
last_use_time
|
||||
|
||||
类型:string
|
||||
|
||||
含义:最后一次使用(实际服务)时间。
|
||||
|
||||
特点:正常结束时与 ledger_end_time 相同;如果服务还未真正开始或立即结束,开始/结束时间可能相同。
|
||||
|
||||
ledger_start_time
|
||||
|
||||
类型:string
|
||||
|
||||
含义:台账层面记录的开始时间。
|
||||
|
||||
说明:与 start_use_time 在当前数据中完全一致,可以视为“计费起始时间”。
|
||||
|
||||
ledger_end_time
|
||||
|
||||
类型:string
|
||||
|
||||
含义:台账层面的结束时间。
|
||||
|
||||
说明:与 last_use_time 一致,可以视为“计费结束时间”。对于 real_use_seconds = 0 的记录,开始和结束时间相同,说明只是预约/录入,并未实际服务。
|
||||
|
||||
4.2 时长(秒)
|
||||
|
||||
这几个字段单位都是“秒”。
|
||||
|
||||
income_seconds
|
||||
|
||||
类型:int
|
||||
|
||||
含义:计费秒数 / 应计收入对应的时间。
|
||||
|
||||
特点:
|
||||
|
||||
值基本是 60 的倍数(2700、3600、7200、10800 等),即按分钟整点计费的秒数。
|
||||
|
||||
用这个字段配合 ledger_unit_price 计算应计金额(原价或折扣价)。
|
||||
|
||||
real_use_seconds
|
||||
|
||||
类型:int
|
||||
|
||||
含义:实际使用时长(秒)。
|
||||
|
||||
特点:
|
||||
|
||||
大多数情况下,real_use_seconds ≈ ledger_count(有少量 ±1 秒差)。
|
||||
|
||||
对于还没真正消费的记录,该值为 0,表示“已预约/已排钟但还没消耗”。
|
||||
|
||||
ledger_count
|
||||
|
||||
类型:int
|
||||
|
||||
含义:台账记录的计时总秒数。
|
||||
|
||||
特点:
|
||||
|
||||
正常结束的记录中,与 real_use_seconds 基本一致。
|
||||
|
||||
可以理解为“本条助教服务真正消耗的总时长(秒)”。
|
||||
|
||||
add_clock
|
||||
|
||||
类型:int(秒)
|
||||
|
||||
观测值:多为 0,有少量为 240, 300, 420, 600, 900, 2400, 2700, 3600, 32400 等。
|
||||
|
||||
含义(推测):加钟秒数,即在原有预约/服务基础上临时追加的时长。
|
||||
|
||||
说明:值均为 60 的倍数(分钟级加钟),如 600 秒=10 分钟。
|
||||
|
||||
returns_clock
|
||||
|
||||
类型:int(秒)
|
||||
|
||||
观测:全部为 0。
|
||||
|
||||
含义(推测):退钟秒数(取消加钟或提前结束退回的时间)。
|
||||
|
||||
当前数据里没有退钟场景,所以全为 0,但字段设计已经预留。
|
||||
|
||||
5. 金额与折扣相关字段
|
||||
|
||||
ledger_unit_price
|
||||
|
||||
类型:float
|
||||
|
||||
含义:助教服务 标准单价(通常是标价:每小时、每节课的单价)。
|
||||
|
||||
特点:如 98.0、108.0、190.0 等。
|
||||
|
||||
ledger_amount
|
||||
|
||||
类型:float
|
||||
|
||||
含义:按标准单价计算出来的应收金额(近似 = ledger_unit_price × income_seconds / 3600)。
|
||||
|
||||
说明:从数据看,这个金额对应“按原价计费”的金额,未扣除各种优惠。
|
||||
|
||||
projected_income
|
||||
|
||||
类型:float
|
||||
|
||||
含义:实际结算计入门店的金额(已经考虑折扣、卡权益、券等后的结果)。
|
||||
|
||||
从数据:projected_income 明显低于 ledger_amount,说明中间有折扣,但折扣的明细并不全由下面几个字段体现(很多是卡权益内生折扣)。
|
||||
|
||||
coupon_deduct_money
|
||||
|
||||
类型:float
|
||||
|
||||
观测值:大多数为 0.0,有少量记录为 195.73、431.1 等。
|
||||
|
||||
含义:由“优惠券/代金券/团购券”等 直接抵扣到这条助教服务上的金额。
|
||||
|
||||
说明:
|
||||
|
||||
当 >0 时,表明这一条助教服务使用了券抵扣了这么多金额。
|
||||
|
||||
与平台验券记录 / 团购套餐流水中的券相关字段联动。
|
||||
|
||||
manual_discount_amount
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全部为 0.0。
|
||||
|
||||
含义:收银员手动给予的减免金额(人工改价)。
|
||||
|
||||
当前导出时间段内暂未出现手动打折的情况。
|
||||
|
||||
member_discount_amount
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全部为 0.0。
|
||||
|
||||
含义:由会员卡折扣产生的优惠金额。
|
||||
|
||||
说明:尽管字段里是 0,但实际折扣可能已经体现在 projected_income 与 ledger_amount 的差额中,这里只是未单独拆出。
|
||||
|
||||
service_money
|
||||
|
||||
类型:float
|
||||
|
||||
观测:全部为 0.0。
|
||||
|
||||
含义(推测):用于记录与助教结算的金额(平台预留的“成本/分成”字段)。
|
||||
|
||||
当前数据中未启用这个机制,所以全为 0。
|
||||
|
||||
6. 状态 / 标志字段
|
||||
|
||||
ledger_status
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全部为 1。
|
||||
|
||||
含义(推测):助教流水记录状态:
|
||||
|
||||
1:正常有效。
|
||||
|
||||
其他值(例如 0、2)可能对应“未结算”“已作废”等,当前数据未出现。
|
||||
|
||||
is_confirm
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全部为 2。
|
||||
|
||||
含义(推测):确认状态,例如:
|
||||
|
||||
1:待确认;
|
||||
|
||||
2:已确认 / 已完成。
|
||||
|
||||
从全部为 2 推断:导出时选的是已经确认的流水。
|
||||
|
||||
is_single_order
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全部为 1。
|
||||
|
||||
含义(推测):是否单独订单:
|
||||
|
||||
1:本助教服务作为单独订单结算(或单独拆项)。
|
||||
|
||||
0:与其他项目(台费、商品)一起打包在综合订单里。
|
||||
|
||||
当前门店显然采用“助教单独结算”的模式,故全为 1。
|
||||
|
||||
is_not_responding
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义(推测):是否存在“爽约/未响应”情况:
|
||||
|
||||
0:正常;
|
||||
|
||||
1:有爽约等异常情况。
|
||||
|
||||
当前时间段没有记录被标记为爽约。
|
||||
|
||||
is_trash
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:是否已废除/作废:
|
||||
|
||||
0:正常有效;
|
||||
|
||||
1:已废除(对应“助教废除.json”里的记录)。
|
||||
|
||||
一旦为 1,一般会配合 trash_reason 等字段,并在“助教废除”表中有对应记录。
|
||||
|
||||
is_delete
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测:全为 0。
|
||||
|
||||
含义:逻辑删除标志。
|
||||
|
||||
0:未删除;
|
||||
|
||||
1:已删除(逻辑删除,历史保留)。
|
||||
|
||||
和 is_trash 不同:is_trash 表示业务上的“废除”,is_delete 表示系统级删除。
|
||||
|
||||
7. 会员/顾客维度(在本表中的影子)
|
||||
|
||||
system_member_id / tenant_member_id
|
||||
|
||||
已在“关联 ID 类字段”里说明:
|
||||
|
||||
system_member_id 对应会员在整个系统的唯一 ID;
|
||||
|
||||
tenant_member_id 对应当前租户中的会员 ID(会员档案的主键 id)。
|
||||
|
||||
注意:这份助教流水里没有直接出现“顾客姓名”字段,只通过这两个 ID 与会员档案、储值卡等表关联。
|
||||
|
||||
8. 员工 / 销售人员相关字段
|
||||
|
||||
salesman_name
|
||||
|
||||
类型:string
|
||||
|
||||
含义:关联的“营业员/销售员姓名”,用于提成归属。
|
||||
|
||||
观测:本数据中多数为空字符串,说明助教流水没有配置单独的营业员。
|
||||
|
||||
salesman_user_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:营业员用户 ID。
|
||||
|
||||
观测:多为 0,代表未指定。
|
||||
|
||||
salesman_org_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:营业员所属组织/部门 ID。
|
||||
|
||||
观测:多为 0。
|
||||
|
||||
operator_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:操作员 ID(录入/结算这条助教服务的员工)。
|
||||
|
||||
关联:可与员工/账号表对应(本次导出未单独给员工表,但其他 JSON 里多处出现该 ID)。
|
||||
|
||||
operator_name
|
||||
|
||||
类型:string
|
||||
|
||||
含义:操作员姓名,与 operator_id 一起使用,便于直接阅读。
|
||||
|
||||
user_id
|
||||
|
||||
已在“关联 ID 类字段”说明:助教的系统用户 ID,与助教账号表中的 user_id 一致。
|
||||
|
||||
person_org_id
|
||||
|
||||
同样在上文说明:助教所属人事组织 ID。
|
||||
|
||||
9. 作废 / 废除相关字段
|
||||
|
||||
这几个字段在当前数据中值都为 0 或空串,但从命名可以看出专门用于记录“助教废除”的信息,与 助教废除.json 表配合使用。
|
||||
|
||||
trash_applicant_id
|
||||
|
||||
类型:int
|
||||
|
||||
含义:提出废除申请的员工 ID(通常是操作员/管理员)。
|
||||
|
||||
当前数据全为 0,因此短期内没有发生废除操作。
|
||||
|
||||
trash_applicant_name
|
||||
|
||||
类型:string
|
||||
|
||||
含义:废除申请人姓名。
|
||||
|
||||
trash_reason
|
||||
|
||||
类型:string
|
||||
|
||||
含义:废除原因(文本说明),例如“顾客取消”“录入错误”等。
|
||||
|
||||
当前数据为空字符串,说明当前导出时间段没有被废除的助教流水记录。
|
||||
|
||||
10. 其他字段
|
||||
|
||||
ledger_group_name
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全部为空字符串。
|
||||
|
||||
含义(推测):助教项目所属的“计费分组/套餐分组名称”,例如某种助教套餐或业务组名称。
|
||||
|
||||
目前未被实际使用。
|
||||
|
||||
三、助教流水与其它 JSON 的关键关系(从字段角度再强调一下)
|
||||
|
||||
虽然你这次提问重点是字段本身,但从字段设计可以看出它在整个系统里的“位置”,这里简要点一下(不做数值分析):
|
||||
|
||||
与助教账号(助教账号1/2.json)
|
||||
|
||||
site_assistant_id ↔ 助教账号表的 id
|
||||
|
||||
user_id ↔ 助教账号表的 user_id
|
||||
|
||||
assistant_team_id ↔ 助教账号表的 team_id
|
||||
|
||||
person_org_id ↔ 助教账号表的 person_org_id
|
||||
|
||||
assistant_level ↔ 助教账号表的 level
|
||||
|
||||
说明:助教流水是“事实表”,助教账号是“维表”。
|
||||
|
||||
与会员档案(会员档案.json)
|
||||
|
||||
system_member_id ↔ 会员档案中的 system_member_id
|
||||
|
||||
tenant_member_id ↔ 会员档案中的 id
|
||||
|
||||
说明:通过这两个字段可以追溯到哪个会员预约/购买了这次助教服务。
|
||||
|
||||
与台桌(台桌列表.json)
|
||||
|
||||
site_table_id ↔ 台桌表中的 id
|
||||
|
||||
tableName ↔ 台桌表中的 table_name/table_no
|
||||
|
||||
说明:标记助教服务在哪张桌上进行。
|
||||
|
||||
与订单/小票(小票详情.json / 结账记录.json)
|
||||
|
||||
order_trade_no、order_settle_id 与其它消费明细(台费、商品、套餐流水)共享,构成一次订单下的不同子项目。
|
||||
|
||||
小票详情中的 orderSettleId 与这里的 order_settle_id 对应。
|
||||
|
||||
与支付/退款(支付记录.json / 退款记录.json)
|
||||
|
||||
order_pay_id 对应支付记录中的 ID 或 relate_id。
|
||||
|
||||
支付记录通过 relate_type 区分是订单支付还是其他业务(如充值);这里的助教流水对应的是订单类支付。
|
||||
|
||||
与助教废除(助教废除.json)
|
||||
|
||||
当 is_trash = 1 时,对应的废除详情(原因、废除时间等)会记录在“助教废除.json”里。
|
||||
|
||||
字段 trash_reason、trash_applicant_id/name 就是废除信息在当前流水记录中的快照。
|
||||
|
||||
|
||||
<!--
|
||||
AI_CHANGELOG:
|
||||
- 日期: 2026-02-14
|
||||
- Prompt: P20260214-103000 — 上下文传递续接,修正占位符描述
|
||||
- 直接原因: v2 脚本自动插入的 `assistantTeamName`、`real_service_money` 占位符描述需替换为正式中文说明
|
||||
- 变更摘要: 将"新发现字段(待补充描述)"替换为"助教所属团队/组名称"和"实际服务金额"
|
||||
- 风险与验证: 纯文档文案修正,无运行时影响;验证:grep "新发现字段" 返回 0 结果
|
||||
-->
|
||||
@@ -0,0 +1,468 @@
|
||||
# 库存出入库流水(QueryGoodsOutboundReceipt)
|
||||
|
||||
> 自动生成于 2026-02-13 | 数据来源:本地 JSON 样本
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 接口路径 | `GoodsStockManage/QueryGoodsOutboundReceipt` |
|
||||
| 完整 URL | `https://pc.ficoo.vip/apiprod/admin/v1/GoodsStockManage/QueryGoodsOutboundReceipt` |
|
||||
| 请求方法 | `POST` |
|
||||
| Content-Type | `application/json` |
|
||||
| 鉴权方式 | Bearer Token(`Authorization` 头) |
|
||||
| ODS 对应表 | `goods_stock_movements` |
|
||||
| 分页方式 | `page` + `limit`(最大 100) |
|
||||
| 时间范围 | 需要(startTime / endTime) |
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 示例值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `siteId` | int | `2790685415443269` | 门店 ID |
|
||||
| `stockType` | int | `0` | 库存类型(0=全部) |
|
||||
| `startTime` | string | `"2026-02-01 08:00:00"` | 查询起始时间 |
|
||||
| `endTime` | string | `"2026-02-13 08:00:00"` | 查询结束时间 |
|
||||
| `page` | int | `1` | 页码(从 1 开始) |
|
||||
| `limit` | int | `100` | 每页条数(最大 100) |
|
||||
|
||||
## 响应字段(共 19 个)
|
||||
|
||||
| # | 字段名 | 类型 | 示例值 |
|
||||
|---|--------|------|--------|
|
||||
| 1 | `siteGoodsStockId` | int | 2957911857581957 |
|
||||
| 2 | `siteGoodsId` | int | 2793026183532613 |
|
||||
| 3 | `siteId` | int | 2790685415443269 |
|
||||
| 4 | `tenantId` | int | 2790683160709957 |
|
||||
| 5 | `stockType` | int | 1 |
|
||||
| 6 | `goodsName` | string | '阿萨姆' |
|
||||
| 7 | `createTime` | string | '2025-11-09 23:23:34' |
|
||||
| 8 | `startNum` | int | 28 |
|
||||
| 9 | `endNum` | int | 27 |
|
||||
| 10 | `changeNum` | int | -1 |
|
||||
| 11 | `unit` | string | '瓶' |
|
||||
| 12 | `price` | float | 8.0 |
|
||||
| 13 | `operatorName` | string | '收银员:郑丽珊' |
|
||||
| 14 | `changeNumA` | int | 0 |
|
||||
| 15 | `startNumA` | int | 0 |
|
||||
| 16 | `endNumA` | int | 0 |
|
||||
| 17 | `remark` | string | '' |
|
||||
| 18 | `goodsCategoryId` | int | 2790683528350539 |
|
||||
| 19 | `goodsSecondCategoryId` | int | 2790683528350540 |
|
||||
|
||||
## 详细字段分析
|
||||
|
||||
> 以下内容迁移自旧版 `goods_stock_movements-Analysis.md`,包含字段的业务含义、枚举值、跨表关联等详细说明。
|
||||
|
||||
二、记录级字段逐一分析(共 19 个)
|
||||
1. 商品与库存标识 / 关联类字段
|
||||
|
||||
siteGoodsStockId
|
||||
|
||||
类型:int
|
||||
|
||||
含义:门店某个“商品库存记录”的主键 ID。
|
||||
|
||||
特点:每条库存变动记录对应一个 siteGoodsStockId,同一个商品可能在不同库存记录中出现(例如不同仓位或不同批次)。
|
||||
|
||||
结构用途:
|
||||
|
||||
与“库存现状/库存汇总”类表(库存现状.json,文件名 20251110_043308_...)中的主键对应。
|
||||
|
||||
用于从“单条变动记录”追溯到该商品当前的整体库存信息。
|
||||
|
||||
siteGoodsId
|
||||
|
||||
类型:int
|
||||
|
||||
含义:门店维度的商品 ID。
|
||||
|
||||
特点:
|
||||
|
||||
同一种商品(例如“农夫山泉苏打水”)在所有库存变化记录中都会使用同一个 siteGoodsId。
|
||||
|
||||
对应商品档案中的主键(门店商品表),在“小票详情.json”和“库存现状.json”等文件中也出现。
|
||||
|
||||
结构关联:
|
||||
|
||||
库存变化记录.siteGoodsId = 库存现状.siteGoodsId
|
||||
|
||||
库存变化记录.siteGoodsId = 小票详情.siteGoodsId
|
||||
|
||||
通过此字段,可以把“库存变化”与“销售/出库明细”以及“当前库存”关联起来。
|
||||
|
||||
siteId
|
||||
|
||||
类型:int
|
||||
|
||||
含义:门店 ID。
|
||||
|
||||
观测:本文件中所有记录的 siteId 都相同,对应“朗朗桌球”这家门店。
|
||||
|
||||
结构作用:
|
||||
|
||||
和其他所有 JSON 中的 siteId 一致,用于在多门店场景下按门店过滤。
|
||||
|
||||
与 siteProfile.id(出现在其他文件中)一致。
|
||||
|
||||
tenantId
|
||||
|
||||
类型:int
|
||||
|
||||
含义:租户/品牌 ID。
|
||||
|
||||
观测:全部记录相同值,说明属于同一商户。
|
||||
|
||||
作用:作为上层品牌维度,与其他表(销售、库存、会员等)保持一致。
|
||||
|
||||
goodsCategoryId
|
||||
|
||||
类型:int
|
||||
|
||||
含义:商品一级分类 ID。
|
||||
|
||||
观测:当前 100 条样本中约有 5 个不同 ID,对应如“酒水类”“食品小吃类”“香烟类”等大类(仅从命名与商品名推断)。
|
||||
|
||||
结构关联:
|
||||
|
||||
在其他 JSON(如商品列表/库存现状)中也出现同名字段,作为商品分类维表的外键。
|
||||
|
||||
实际的分类名称不在本表体现,需要通过分类表或其他视图查询。
|
||||
|
||||
goodsSecondCategoryId
|
||||
|
||||
类型:int
|
||||
|
||||
含义:商品二级分类 ID。
|
||||
|
||||
观测:样本中约有 7 个不同 ID,如饮料中的“矿泉水/功能饮料/碳酸饮料”等。
|
||||
|
||||
结构作用:
|
||||
|
||||
与商品二级分类维表对应,进一步区分商品细类。
|
||||
|
||||
在库存现状或商品档案 JSON 中也出现,用于报表按分类汇总库存。
|
||||
|
||||
2. 商品基本信息字段
|
||||
|
||||
goodsName
|
||||
|
||||
类型:string
|
||||
|
||||
含义:商品名称。
|
||||
|
||||
示例值:
|
||||
|
||||
"农夫山泉苏打水"
|
||||
|
||||
"阿萨姆"
|
||||
|
||||
"哇哈哈矿泉水"
|
||||
|
||||
"鸡翅三个一份"
|
||||
|
||||
"普通扑克"
|
||||
|
||||
"软玉溪", "钻石荷花"(香烟)
|
||||
|
||||
特点:
|
||||
|
||||
对应门店商品表中的 goods_name,为当时的名称快照。
|
||||
|
||||
与 siteGoodsId 一一对应,但保留在变更记录中便于直接阅读,不用再去商品表查。
|
||||
|
||||
unit
|
||||
|
||||
类型:string,枚举。
|
||||
|
||||
观测值(本样本):
|
||||
|
||||
"瓶"、"包"、"盒"、"根"、"个"、"桶"、"份".
|
||||
|
||||
含义:库存计量单位。
|
||||
|
||||
说明:库存数量(startNum、endNum、changeNum)均以这里的单位计数。
|
||||
|
||||
price
|
||||
|
||||
类型:float
|
||||
|
||||
含义:商品单价(单位金额)。
|
||||
|
||||
观测特征:
|
||||
|
||||
常见值:5.0、8.0、15.0、6.0、2.0、10.0、45.0 等。
|
||||
|
||||
对同一个 siteGoodsId,所有记录的 price 完全一致——说明这是该商品在门店的当前单价快照。
|
||||
|
||||
结构作用:
|
||||
|
||||
虽然库存变化记录中并未直接出现金额字段,但通过 price × changeNum 可以算出这次变动对应的金额(如果需要金额层面分析的话)。
|
||||
|
||||
在结构上,这是为后续报表(如按进销存金额统计)预留的关键字段。
|
||||
|
||||
3. 库存数量变动类字段
|
||||
|
||||
startNum
|
||||
|
||||
类型:int
|
||||
|
||||
含义:变动前(这次出入库之前)的库存数量。
|
||||
|
||||
示例:
|
||||
如记录:startNum = 28, changeNum = -1, endNum = 27。
|
||||
|
||||
特点:样本中有 80+ 个不同值,覆盖几十到几百的库存数。
|
||||
|
||||
endNum
|
||||
|
||||
类型:int
|
||||
|
||||
含义:变动后(出入库之后)的库存数量。
|
||||
|
||||
结构关系:
|
||||
|
||||
全部记录满足:
|
||||
endNum = startNum + changeNum
|
||||
这一点在样本中经检验无一例外。
|
||||
|
||||
意义:确保库存变动画账逻辑正确,是库存平衡的核心约束。
|
||||
|
||||
changeNum
|
||||
|
||||
类型:int
|
||||
|
||||
含义:本次库存数量变化值。
|
||||
|
||||
特点及取值:
|
||||
|
||||
常见值:-1、-2、-3、-6、-12、-36 等负数,也有少量正数(如 1、2、12、36 等)。
|
||||
|
||||
数据验证:
|
||||
|
||||
当 changeNum < 0 时,startNum > endNum;
|
||||
|
||||
当 changeNum > 0 时,startNum < endNum。
|
||||
|
||||
结构逻辑:
|
||||
|
||||
在配合 stockType 使用时,正负号对应该变动是“出库还是入库”:
|
||||
|
||||
对 stockType = 1:全部都是负数,代表从库存中扣减(销售或其他出库)。
|
||||
|
||||
对 stockType = 4:全部是正数,代表库存增加(入库/调整)。
|
||||
|
||||
startNumA
|
||||
|
||||
类型:int
|
||||
|
||||
观测:所有记录为 0。
|
||||
|
||||
含义(推测):辅助计量单位的起始库存(例如件/箱等第二单位)。
|
||||
|
||||
当前门店在样本时间段内没有启用多单位库存管理,因此全部为 0。
|
||||
|
||||
endNumA
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全部为 0。
|
||||
|
||||
含义:辅助单位的变动后库存,同样未启用。
|
||||
|
||||
changeNumA
|
||||
|
||||
类型:int
|
||||
|
||||
观测:全部为 0。
|
||||
|
||||
含义:辅助单位的变化量(与 changeNum 对应的第二计量单位变化),当前未使用。
|
||||
|
||||
结论:
|
||||
startNumA / endNumA / changeNumA 是为“一个商品有两种计量单位(如箱与瓶)”而设计的预留字段。
|
||||
目前门店只在单一单位层面管理库存,故全部为 0。
|
||||
|
||||
4. 库存变动类型字段
|
||||
|
||||
stockType
|
||||
|
||||
类型:int,枚举。
|
||||
|
||||
观测值(本样本):
|
||||
|
||||
1:89 条
|
||||
|
||||
4:11 条
|
||||
|
||||
与 changeNum 的联合特征:
|
||||
|
||||
(stockType=1, changeNum<0) 出现 89 次;
|
||||
|
||||
(stockType=4, changeNum>0) 出现 11 次;
|
||||
|
||||
不存在 stockType=1 且 changeNum>0 或 stockType=4 且 changeNum<0 的情况。
|
||||
|
||||
含义(基于数据行为推断):
|
||||
|
||||
1:出库类变动
|
||||
典型情况是销售出库,库存减少 1 或 2;例如顾客点了一瓶饮料,对应一条 stockType=1, changeNum=-1 的记录。
|
||||
|
||||
4:入库/盘盈/调整增加
|
||||
举例:某条记录为 stockType=4, changeNum=2,startNum=13, endNum=15,说明库存被人工或系统增加了 2。
|
||||
|
||||
结构意义:
|
||||
|
||||
用 stockType 区分变动原因大类(销售/退货/盘点/报损等),再由 changeNum 的正负体现增减。
|
||||
|
||||
当前样本里只出现了两个枚举值,但从命名推测,系统中还可能存在其它类型(例如报损出库、盘亏减少等),只是这段时间内未发生。
|
||||
|
||||
5. 操作与时间字段
|
||||
|
||||
createTime
|
||||
|
||||
类型:string,格式 YYYY-MM-DD HH:MM:SS
|
||||
|
||||
含义:这条库存变动记录的创建时间,即发生库存变更的时间点。
|
||||
|
||||
特点:
|
||||
|
||||
样本覆盖 2025-11-09 晚上一段时间,且有多条记录在同一秒内(同桌多商品一起销售时)。
|
||||
|
||||
是库存流水的时间轴关键字段,可与小票时间、台费时间等交叉校验。
|
||||
|
||||
operatorName
|
||||
|
||||
类型:string
|
||||
|
||||
含义:执行此次库存变动的操作人。
|
||||
|
||||
观测值:
|
||||
|
||||
"收银员:郑丽珊":99 条
|
||||
|
||||
"系统":1 条
|
||||
|
||||
说明:
|
||||
|
||||
大部分库存变化由前台收银员操作(录入销售单、小票)触发。
|
||||
|
||||
个别记录由系统自动生成(如自动盘点调整、系统修正等),操作人显示为“系统”。
|
||||
|
||||
6. 备注字段
|
||||
|
||||
remark
|
||||
|
||||
类型:string
|
||||
|
||||
观测:全部为空字符串 ""。
|
||||
|
||||
含义:备注信息,用于手工记录本次变更的特殊原因说明(例如“盘点差异调整”“报损”)。
|
||||
|
||||
当前样本中没有填入任何备注,但字段已预留,适用于盘点或手工调整场景。
|
||||
|
||||
三、与其他 JSON 的结构关联关系(从字段角度)
|
||||
|
||||
仅从字段命名和你这批文件中出现的位置来看,“库存变化记录1.json”在整体系统中的结构位置大致如下:
|
||||
|
||||
与商品档案 / 库存现状
|
||||
|
||||
siteGoodsId:
|
||||
|
||||
在 库存现状.json(20251110_043308_...)中同名出现,对应门店商品库存汇总表。
|
||||
|
||||
在“小票详情.json”(20251110_035904_...)中也有 siteGoodsId,用于标记每条销售明细对应的商品。
|
||||
|
||||
siteGoodsStockId:
|
||||
|
||||
是具体库存记录主键,与库存现状中的记录一一对应。
|
||||
|
||||
goodsCategoryId / goodsSecondCategoryId:
|
||||
|
||||
在商品定义/库存现状 JSON 中同样出现,对应商品分类维表。
|
||||
|
||||
结构链路可以概括为:
|
||||
|
||||
商品档案/库存现状(siteGoodsId, goodsCategoryId...)
|
||||
↕
|
||||
库存变化记录(siteGoodsId, siteGoodsStockId, changeNum...)
|
||||
↕
|
||||
小票详情/销售明细(siteGoodsId, 数量)
|
||||
|
||||
与门店维度
|
||||
|
||||
tenantId / siteId:
|
||||
|
||||
与所有业务 JSON 中的同名字段一致,表示这条库存变动属于哪一个品牌、哪一家门店。
|
||||
|
||||
对你这批数据来说,这两个字段在所有文件中取值固定,都是“非球科技 · 某门店(朗朗桌球)”。
|
||||
|
||||
与操作员/员工信息
|
||||
|
||||
operatorName:
|
||||
|
||||
以字符串形式记录操作员,“收银员:郑丽珊”与其他 JSON 中的操作员信息(如结账记录、小票记录中的 operator_name)一致。
|
||||
|
||||
虽然本表中没有 operatorId,但其他表(如结账记录)有时会记录 ID;可通过姓名+门店,在员工档案或账号表中匹配。
|
||||
|
||||
与销售/出库行为
|
||||
|
||||
当 stockType = 1, changeNum < 0 时,明显是销售导致的库存减少。
|
||||
|
||||
对应的小票/销售明细也会有同一时间点的消费记录(通过 createTime、siteGoodsId、商品名 等组合可以对齐)。
|
||||
|
||||
对“盘点增加/入库类”的记录(stockType = 4, changeNum > 0),则可能与采购入库或盘盈记录关联到其他表。
|
||||
|
||||
四、结构层面的重要线索(不涉及金额/盈利分析)
|
||||
|
||||
从字段设计和样本值可以看出,这个“库存变化记录”表在系统结构上有一些关键特征:
|
||||
|
||||
库存平衡公式显式存在
|
||||
|
||||
所有记录满足:
|
||||
endNum = startNum + changeNum。
|
||||
|
||||
这意味着系统把每一次增减记录为一条流水,而不是只记录最后库存量。
|
||||
|
||||
通过把所有变动记录按时间排序叠加,可以完全重放库存数变化过程。
|
||||
|
||||
统一支持双计量单位但本门店未启用
|
||||
|
||||
startNumA / endNumA / changeNumA 全为 0,说明目前只使用主单位(瓶/包/盒等)。
|
||||
|
||||
但字段已经为“箱/瓶”这种双单位场景预留了结构,可以在未来随时启用。
|
||||
|
||||
库存变动类型(stockType)与变化方向强绑定
|
||||
|
||||
样本中,stockType=1 永远对应负数 changeNum,stockType=4 永远对应正数。
|
||||
|
||||
说明系统在设计时,不是单纯依赖 changeNum 的正负来判断业务含义,而是:
|
||||
|
||||
用 stockType 表示业务场景(销售出库/盘点/入库等),
|
||||
|
||||
用 changeNum 的正负表达实际的增或减。
|
||||
|
||||
其它可能的 stockType(如报损出库/盘亏/退货等)本批样本中未出现,但结构已经预留可扩展。
|
||||
|
||||
价格在本表中是“静态快照”,而不是动态计算字段
|
||||
|
||||
对同一个 siteGoodsId,所有记录的 price 一致,表明:
|
||||
|
||||
price 是当时商品价格的快照副本。
|
||||
|
||||
真实的“标准价/进价/零售价”仍以商品档案为准,只是在库存变动记录中复制一份方便报表使用。
|
||||
|
||||
这一设计避免了之后价格调整导致历史库存记录无法按当时价格还原的问题。
|
||||
|
||||
操作员信息体现“人工 vs 系统”两类来源
|
||||
|
||||
大部分记录由“收银员”操作,说明库存减少主要来自前台销售。
|
||||
|
||||
个别记录由“系统”操作,说明系统本身会根据某些规则自动生成库存变动记录(例如盘点差异自动入库/出库、库存初始化等)。
|
||||
|
||||
结构上不需要额外字段即可从 operatorName 粗略判断记录来源。
|
||||
|
||||
与商品分类强绑定,方便结构化报表
|
||||
|
||||
通过 goodsCategoryId / goodsSecondCategoryId,这张库存变动明细表可以非常方便地按“饮料/香烟/小食”等分类对库存变动进行结构化分析。
|
||||
|
||||
虽然你不希望做“大数据/盈利分析”,但从结构角度看,这两个字段是后续任意统计的关键维度。
|
||||
@@ -0,0 +1,547 @@
|
||||
# 库存汇总报表(GetGoodsStockReport)
|
||||
|
||||
> 自动生成于 2026-02-13 | 数据来源:实时 API
|
||||
|
||||
## 基本信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| 接口路径 | `TenantGoods/GetGoodsStockReport` |
|
||||
| 完整 URL | `https://pc.ficoo.vip/apiprod/admin/v1/TenantGoods/GetGoodsStockReport` |
|
||||
| 请求方法 | `POST` |
|
||||
| Content-Type | `application/json` |
|
||||
| 鉴权方式 | Bearer Token(`Authorization` 头) |
|
||||
| ODS 对应表 | `goods_stock_summary` |
|
||||
| 分页方式 | `page` + `limit`(最大 100) |
|
||||
| 时间范围 | 需要(startTime / endTime) |
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数名 | 类型 | 示例值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `siteId` | int | `2790685415443269` | 门店 ID |
|
||||
| `startTime` | string | `"2026-02-01 08:00:00"` | 查询起始时间 |
|
||||
| `endTime` | string | `"2026-02-13 08:00:00"` | 查询结束时间 |
|
||||
| `page` | int | `1` | 页码(从 1 开始) |
|
||||
| `limit` | int | `100` | 每页条数(最大 100) |
|
||||
|
||||
## 响应字段(共 14 个)
|
||||
|
||||
| # | 字段名 | 类型 | 示例值 |
|
||||
|---|--------|------|--------|
|
||||
| 1 | `siteGoodsId` | int | 3089190204491141 |
|
||||
| 2 | `goodsName` | string | '小合味道' |
|
||||
| 3 | `goodsUnit` | string | '桶' |
|
||||
| 4 | `goodsCategoryId` | int | 2791941988405125 |
|
||||
| 5 | `goodsCategorySecondId` | int | 2793236829620037 |
|
||||
| 6 | `rangeStartStock` | int | 0 |
|
||||
| 7 | `rangeEndStock` | int | 22 |
|
||||
| 8 | `rangeIn` | int | 24 |
|
||||
| 9 | `rangeOut` | int | -2 |
|
||||
| 10 | `rangeInventory` | int | 0 |
|
||||
| 11 | `rangeSale` | int | 2 |
|
||||
| 12 | `rangeSaleMoney` | float | 16.0 |
|
||||
| 13 | `currentStock` | int | 22 |
|
||||
| 14 | `categoryName` | string | '零食' |
|
||||
|
||||
## 详细字段分析
|
||||
|
||||
> 以下内容迁移自旧版 `goods_stock_summary-Analysis.md`,包含字段的业务含义、枚举值、跨表关联等详细说明。
|
||||
|
||||
每个元素就是某个 门店商品(siteGoodsId)在一个查询时间区间内的库存汇总。
|
||||
二、字段分组说明(含类型 / 是否枚举 / 枚举值)
|
||||
1. 商品主键与基本信息
|
||||
1.1 siteGoodsId
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
161 条记录中 161 个唯一值。
|
||||
|
||||
与 “门店商品档案” (20251110_051132_…1.json) 中 orderGoodsList 里的 id 完全一一对应。
|
||||
|
||||
含义:
|
||||
|
||||
门店商品 ID,本库存汇总表的主键,对应某个具体商品在本店的唯一标识。
|
||||
|
||||
关联:
|
||||
|
||||
库存汇总.siteGoodsId = 门店商品档案.id
|
||||
|
||||
也与库存变动记录(库存变化记录1)里的 siteGoodsId 对应(库存流水的外键)。
|
||||
|
||||
1.2 goodsName
|
||||
|
||||
类型:string
|
||||
|
||||
特征:
|
||||
|
||||
每条记录一个商品名,共 161 个不同值(与 siteGoodsId 一一对应)。
|
||||
|
||||
例:"东方树叶", "红烧牛肉面", "薯片" 等。
|
||||
|
||||
含义:
|
||||
|
||||
商品名称,冗余于门店商品档案的 goods_name。
|
||||
|
||||
结构意义:
|
||||
|
||||
方便直接阅读汇总报表,无需再次联表取商品档案。
|
||||
|
||||
1.3 goodsUnit
|
||||
|
||||
类型:string
|
||||
|
||||
特征:
|
||||
|
||||
典型取值(枚举):
|
||||
|
||||
"包":59 条
|
||||
|
||||
"瓶":46 条
|
||||
|
||||
"个":17 条
|
||||
|
||||
"份":13 条
|
||||
|
||||
"根":10 条
|
||||
|
||||
"盒", "杯", "桶", "盘", "支" 等
|
||||
|
||||
与门店商品档案中的 unit 字段完全一致。
|
||||
|
||||
含义:
|
||||
|
||||
商品的计量单位(售卖单位)。
|
||||
|
||||
小结:siteGoodsId + goodsName + goodsUnit 在结构上确定一条“门店商品”的维度信息,和“门店商品档案”的字段是完全对齐的。
|
||||
|
||||
2. 分类维度字段
|
||||
2.1 goodsCategoryId
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,161 条记录中有 9 个不同的 ID。
|
||||
|
||||
每一个 goodsCategoryId 对应 唯一一个 categoryName(一对一)。
|
||||
|
||||
含义:
|
||||
|
||||
一级商品分类 ID。
|
||||
|
||||
枚举映射(由数据直接推得):
|
||||
|
||||
2791941988405125 → "零食"
|
||||
2790683528350539 → "酒水"
|
||||
2792062778003333 → "香烟"
|
||||
2793217944864581 → "其他"
|
||||
2791942087561093 → "雪糕"
|
||||
2790683528350535 → "器材"
|
||||
2793220945250117 → "小吃"
|
||||
2790683528350533 → "槟榔"
|
||||
2790683528350545 → "果盘"
|
||||
|
||||
|
||||
(ID 是系统内部编码,你这边可以当“分类主键”。)
|
||||
|
||||
2.2 goodsCategorySecondId
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,有 14 个不同的 ID。
|
||||
|
||||
每个二级 ID 对应一个更细的类目(比如不同品牌/系列),但名称在本文件中没有给出。
|
||||
|
||||
含义:
|
||||
|
||||
二级(次级)商品分类 ID,是 goodsCategoryId 的下级分类。
|
||||
|
||||
关联:
|
||||
|
||||
在库存变动类 JSON / 商品分类 JSON(之前看到的分类导出)中,有完整的分类树,可通过这些 ID 找回二级分类名称。
|
||||
|
||||
2.3 categoryName
|
||||
|
||||
类型:string
|
||||
|
||||
特征:
|
||||
|
||||
枚举值恰好 9 个,分别是:
|
||||
|
||||
"零食", "酒水", "香烟", "其他", "雪糕", "器材", "小吃", "槟榔", "果盘"
|
||||
|
||||
与 goodsCategoryId 一一对应。
|
||||
|
||||
含义:
|
||||
|
||||
一级分类名称,属于冗余字段,用于直接展示。
|
||||
|
||||
结构结论:
|
||||
|
||||
分类主键:goodsCategoryId(一级)+ goodsCategorySecondId(二级)
|
||||
|
||||
分类名称:categoryName 仅给了一级中文名,二级名需要到分类表/门店商品档案中再查。
|
||||
|
||||
3. 库存数量相关字段(全部为整数)
|
||||
3.1 rangeStartStock
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,有 61 个不同数值。
|
||||
|
||||
示例值:0, 1, 2, 4, 7, 8, 29 ...
|
||||
|
||||
含义:
|
||||
|
||||
查询区间 起始时刻 的库存数量(期初库存)。
|
||||
|
||||
结构作用:
|
||||
|
||||
与下方各类“变动量”一起构成库存平衡公式。
|
||||
|
||||
3.2 rangeEndStock
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,有 61 个不同数值。
|
||||
|
||||
示例值: 0, 1, 5, 7, 8, 16 ...
|
||||
|
||||
含义:
|
||||
|
||||
查询区间 结束时刻 的库存数量(期末库存)。
|
||||
|
||||
3.3 rangeIn
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,多为正整数或 0。
|
||||
|
||||
示例值:0, 30, 90, 450 ...
|
||||
|
||||
含义:
|
||||
|
||||
查询区间内的 入库数量汇总(正值),包括采购入库、调拨入库等。
|
||||
|
||||
3.4 rangeOut
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
有 64 个不同值,且全部为 0 或负数:
|
||||
|
||||
0(36次)、-1、-2、-3、-4、-7、-8、-14、-35 ……
|
||||
|
||||
含义:
|
||||
|
||||
查询区间内的 出库数量汇总,以 负数 表示从库存扣减(出库/销售)。
|
||||
|
||||
结构公式验证(关键):
|
||||
|
||||
对每一条记录,都满足:
|
||||
|
||||
rangeStartStock + rangeIn + rangeInventory + rangeOut = rangeEndStock
|
||||
|
||||
|
||||
即:期初 + 入库 + 盘点调整 + 出库 = 期末
|
||||
当前数据中 rangeInventory 全为 0,那么简化为:
|
||||
rangeStartStock + rangeIn + rangeOut = rangeEndStock。
|
||||
|
||||
3.5 rangeInventory
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
所有 161 条记录均为 0。
|
||||
|
||||
含义:
|
||||
|
||||
查询区间内的 盘点调整净变动量(盘盈–盘亏)。
|
||||
|
||||
当前数据状态:
|
||||
|
||||
这段时间内没有发生盘点或盘点对库存无净影响,所以全为 0。
|
||||
|
||||
结构意义:
|
||||
|
||||
在有盘点的场景,这个字段会承担“非正常出入库”的调整职责,并参与上面的平衡公式。
|
||||
|
||||
3.6 currentStock
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,61 个不同值。
|
||||
|
||||
示例值:0, 1, 2, 3, 4, 5, 6, 7, 10, 14 ...
|
||||
|
||||
大部分记录里,currentStock 与 rangeEndStock 相等;但有 17 条 存在差异(通常是小差值,例如 rangeEndStock=74, currentStock=72)。
|
||||
|
||||
含义(推断):
|
||||
|
||||
导出时刻的实时库存数量。
|
||||
|
||||
与 rangeEndStock 关系:
|
||||
|
||||
rangeEndStock 是“查询时间段结束瞬间”的库存;
|
||||
|
||||
currentStock 是“导出时当前瞬间”的库存。
|
||||
|
||||
这说明:在查询区间之后,可能又发生了一些出入库,导致当前库存与期末库存略有差异。
|
||||
|
||||
结构小结:
|
||||
|
||||
(rangeStartStock, rangeIn, rangeOut, rangeInventory, rangeEndStock) 构成一个严格的库存平衡关系。
|
||||
|
||||
currentStock 则是另一个时间点的库存快照,在结构上属于“附加状态字段”,不参与那个公式。
|
||||
|
||||
4. 销量与销售金额(汇总)
|
||||
4.1 rangeSale
|
||||
|
||||
类型:int
|
||||
|
||||
特征:
|
||||
|
||||
非空,有 65 个不同的整数。
|
||||
|
||||
示例:0, 1, 2, 3, 4, 5, 6, 8, 13, 14 ...
|
||||
|
||||
含义:
|
||||
|
||||
查询区间内,该商品的 销售数量汇总(售出多少“包/瓶/份”等)。
|
||||
|
||||
与 rangeOut 的关系(结构上):
|
||||
|
||||
对绝大多数以“销售出库”为主的商品,rangeOut 的绝对值与 rangeSale 大致一致(也可能有非销售出库,比如报损/调拨),这一点需要结合库存变动明细来判断,但属于业务层逻辑,这里不展开。
|
||||
|
||||
4.2 rangeSaleMoney
|
||||
|
||||
类型:float
|
||||
|
||||
特征:
|
||||
|
||||
非空,有 102 个不同的浮点值。
|
||||
|
||||
示例:0.0, 48.0, 30.0, 40.0, 280.0, 60.0, 50.0, 15.0 ...
|
||||
|
||||
很多数值看起来是整数金额,但用 float 存储,为以后兼容小数价格预留空间。
|
||||
|
||||
含义:
|
||||
|
||||
查询区间内,该商品销售的 金额小计(按商品维度汇总)。
|
||||
|
||||
结构特征(不做业绩解读,只谈结构):
|
||||
|
||||
对于有销量的记录,可以通过简单比例验证:
|
||||
|
||||
单品成交单价 ≈ rangeSaleMoney / rangeSale
|
||||
|
||||
|
||||
如某商品记录:
|
||||
|
||||
rangeSale = 62
|
||||
|
||||
rangeSaleMoney = 744.0
|
||||
|
||||
求比值 ≈ 12.0 → 对应门店商品档案中的 sale_price。
|
||||
|
||||
也就是说:在结构上,rangeSaleMoney 与 “汇总数量 × 单价” 对应关系非常一致,说明这个字段确实是商品维度的销售金额汇总。
|
||||
|
||||
这里我仅确认字段之间的 计量逻辑与结构关系,不做任何“好/坏”的业务评价。
|
||||
|
||||
三、与其它 JSON 的关联关系(结构层面)
|
||||
1. 与“门店商品档案” JSON 的关系
|
||||
|
||||
通过实际比对:
|
||||
|
||||
库存汇总.siteGoodsId = 门店商品档案.orderGoodsList.id
|
||||
|
||||
库存汇总.goodsName = 门店商品档案.goods_name
|
||||
|
||||
库存汇总.goodsUnit = 门店商品档案.unit
|
||||
|
||||
库存汇总.goodsCategoryId = 门店商品档案.goods_category_id
|
||||
|
||||
库存汇总.goodsCategorySecondId = 门店商品档案.goods_second_category_id
|
||||
|
||||
结构含义:
|
||||
|
||||
门店商品档案:静态维度表,包含商品的售价、成本、是否计库存、分类名称等。
|
||||
|
||||
库存汇总:针对同一批 id 做的“某一时间范围内的库存+销量汇总”。
|
||||
|
||||
因此,你可以把“库存汇总”看成是对“门店商品档案”的一个衍生事实表,按照商品维度聚合库存与销售信息。
|
||||
|
||||
2. 与“库存变化记录”(库存流水)的关系
|
||||
|
||||
虽然你这份导出里“库存变化记录”在另外一个 JSON 中(字段里有 siteGoodsId、stockType 等),但从字段名和使用方式可以推断:
|
||||
|
||||
库存变化记录:
|
||||
|
||||
粒度:一条库存变动(一笔入库/出库/盘点等)。
|
||||
|
||||
重要字段:
|
||||
|
||||
siteGoodsId:对应库存汇总中的 siteGoodsId。
|
||||
|
||||
stockType:入库、出库、盘点等类型枚举。
|
||||
|
||||
changeNum:每次变动数量。
|
||||
|
||||
库存汇总:
|
||||
|
||||
粒度:某商品在查询区间内的汇总。
|
||||
|
||||
字段:rangeIn, rangeOut, rangeInventory 等就是对库存变化记录按 siteGoodsId + 时间区间 汇总出来的结果。
|
||||
|
||||
结构关系可以概括为:
|
||||
|
||||
库存变化记录(明细表)
|
||||
↓ 按 siteGoodsId + 时间范围聚合
|
||||
库存汇总(汇总表)
|
||||
|
||||
|
||||
这使得你在需要追查明细时,可以从“汇总 → 明细”下钻。
|
||||
|
||||
3. 与“门店销售记录”的关系
|
||||
|
||||
从字段设计看:
|
||||
|
||||
门店销售记录中有:
|
||||
|
||||
site_goods_id:门店商品 ID
|
||||
|
||||
ledger_amount:单条销售明细金额
|
||||
|
||||
ledger_count:销售数量
|
||||
|
||||
库存汇总中有:
|
||||
|
||||
siteGoodsId:门店商品 ID
|
||||
|
||||
rangeSale:总销售数量(在时间范围内)
|
||||
|
||||
rangeSaleMoney:总销售金额(在时间范围内)
|
||||
|
||||
结构上可以理解为:
|
||||
|
||||
门店销售记录 是 每一个销售明细;
|
||||
|
||||
库存汇总 是在某时间段对这些明细按商品维度做的 汇总。
|
||||
|
||||
两者之间通过 siteGoodsId/site_goods_id 联接,同时需要根据时间条件约束订单时间,这一点在结构上是清晰的。
|
||||
|
||||
4. 与商品分类树(库存变化记录2 / 分类 JSON)的关系
|
||||
|
||||
在之前分类 JSON 中,你有一个分类树结构(有 id, pid, category_name, categoryBoxes 等):
|
||||
|
||||
库存汇总.goodsCategoryId 对应 分类树中的某个一级分类 id。
|
||||
|
||||
库存汇总.goodsCategorySecondId 对应其子分类(分类树中某个 pid=一级分类id 的节点)。
|
||||
|
||||
categoryName 与分类树中的 category_name 对应(一级节点)。
|
||||
|
||||
结构关系:
|
||||
|
||||
分类树 JSON (全局分类维表)
|
||||
↑ ↑
|
||||
goodsCategoryId goodsCategorySecondId
|
||||
↑ ↑
|
||||
库存汇总 (事实表)
|
||||
|
||||
四、结构层面可以注意的一些“关系和约束”
|
||||
|
||||
全部是字段设计/数值关系层面,不涉及盈利或经营分析:
|
||||
|
||||
库存平衡公式存在且逐条成立
|
||||
|
||||
对每一条记录,都可以验证:
|
||||
|
||||
rangeStartStock + rangeIn + rangeInventory + rangeOut = rangeEndStock
|
||||
|
||||
|
||||
当前导出中 rangeInventory = 0,所以简化公式为:
|
||||
|
||||
rangeStartStock + rangeIn + rangeOut = rangeEndStock
|
||||
|
||||
|
||||
严格成立说明:
|
||||
|
||||
系统在生成库存汇总时,确实是从明细出入库数据做了完整计算,而不是凭输入数据临时凑数。
|
||||
|
||||
出库量采用“负数”表示
|
||||
|
||||
rangeOut 不再定义为“出库数量(正数)”,而是直接记 负数。
|
||||
|
||||
好处是:公式中无需写“–出库量”,直接做代数求和。
|
||||
|
||||
这个习惯在后续做数据集成或迁移时需要注意,避免重复取绝对值/重复取负。
|
||||
|
||||
区分“期末库存”与“当前库存”两个时间点
|
||||
|
||||
rangeEndStock:查询时间段的期末库存。
|
||||
|
||||
currentStock:导出那一刻的库存快照。
|
||||
|
||||
二者不一定相等(有部分记录存在差 1–4 的差值),说明结构上清晰区分了查询区间和当前状态。
|
||||
|
||||
汇总粒度清晰:每个 siteGoodsId 仅一条记录
|
||||
|
||||
siteGoodsId 在本文件中不重复,说明这是按商品聚合后的汇总层,没有再分仓库、批次、货位等维度。
|
||||
|
||||
如果未来需要按仓/货位维度汇总,结构可能会出现类似 warehouseId 之类的新字段,从这份数据来看目前没有。
|
||||
|
||||
金额与数量之间存在一致的单价模式
|
||||
|
||||
对于 rangeSale > 0 的商品,rangeSaleMoney / rangeSale 与门店商品档案中的 sale_price 一致,这只是结构上的一致性检查:
|
||||
|
||||
说明 rangeSaleMoney 并不是某种复杂的计算结果,而是“销售数量 × 单价”的汇总。
|
||||
|
||||
这在系统设计上有利于做“金额与数量对账”。
|
||||
|
||||
分类 ID 与中文名称一一对应
|
||||
|
||||
goodsCategoryId 和 categoryName 的关系是一对一,没有出现“同一 categoryName 对应多 ID”的情况。
|
||||
|
||||
这说明在该门店中,一级分类的结构比较干净,没有重复创建多个 ID 对应相同名称的情况;对你的后续系统对接来说,这一层结构相对简单,只需要维护一套映射即可。
|
||||
|
||||
五、小结
|
||||
|
||||
20251110_043308_库存汇总.json 本质上是:
|
||||
|
||||
以 门店商品(siteGoodsId) 为粒度,
|
||||
|
||||
在某个查询时间范围内,对该商品的:
|
||||
|
||||
期初库存(rangeStartStock)
|
||||
|
||||
入库量(rangeIn)
|
||||
|
||||
出库量(rangeOut,负数)
|
||||
|
||||
盘点调整(rangeInventory)
|
||||
|
||||
期末库存(rangeEndStock)
|
||||
|
||||
销售数量(rangeSale)
|
||||
|
||||
销售金额(rangeSaleMoney)
|
||||
做了一次结构化汇总;
|
||||
|
||||
同时给出了当前时点库存快照(currentStock),并冗余了商品名、单位、一级分类名等维度信息。
|
||||
|
||||
在全局数据模型里,它与 门店商品档案 / 库存变动明细 / 门店销售明细 / 分类树 等文件通过主键(siteGoodsId、分类 ID)和时间条件构成一套“明细–汇总–维度”相互嵌套的结构,这对于后续做数据迁移、数据仓库建模或者跨系统字段映射都比较有价值。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user