init: 项目初始提交 - NeoZQYY Monorepo 完整代码

This commit is contained in:
Neo
2026-02-15 14:58:14 +08:00
commit ded6dfb9d8
769 changed files with 182616 additions and 0 deletions

30
.env.template Normal file
View 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
View 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
View File

View File

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

View 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”>

View 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"
}

View 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"
}

View File

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

View File

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

View File

@@ -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/ShanghaiYYYY-MM-DDPromptPrompt-ID + ≤5 行摘录或原文Direct cause必要性 + 修改方案简介)\n\n如果没有发生表结构变更例如仅修改注释在变更日志文档中写一条简短说明\"无结构性变更\"(同样要带日期 + Prompt-ID。"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "db-schema-doc-enforcer"
}

View File

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

View File

@@ -0,0 +1,15 @@
{
"enabled": true,
"name": "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- 子代理必须按需调用 skillsteering-readme-maintainer、change-annotation-audit、bd-manual-db-docs仅在满足触发条件时。\n- 子代理结束后,必须把 .kiro/.audit_state.json 中 audit_required 置为 false或清空文件以停止后续提醒。\n- 审计落盘完成后,必须执行 `python scripts/gen_audit_dashboard.py` 刷新审计一览表docs/audit/audit_dashboard.md。\n- 你的最终回复必须是「极短回执」,只包含:\n 1) 是否完成yes/no\n 2) 写了哪些文件(文件列表)\n 3) 如果失败下一步怎么做1~2 条)"
},
"workspaceFolderName": "NeoZQYY",
"shortName": "audit"
}

View 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

View 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
}

View 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
View File

@@ -0,0 +1,4 @@
{
"mcpServers": {
}
}

View 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`

View File

@@ -0,0 +1,27 @@
# Schema 变更日志Schema Change Log
- 日期Asia/ShanghaiYYYY-MM-DD
- Prompt-ID
- 原始原因Prompt 摘录/原文):
- 直接原因(必要性 + 方案简介):
- 影响的 Schema
- 变更摘要(一句话):
## 变更明细
- 新增:
- 修改:
- 删除:
## 影响范围
- ETL
- 后端 API
- 小程序:
## 回滚要点
- DDL 回滚:
- 数据回填/迁移注意事项:
## 验证 SQL至少 3 条)
1)
2)
3)

View File

@@ -0,0 +1,22 @@
# <schema>.<table>
## 表用途Purpose
- 该表代表什么业务对象/过程
## 字段Columns
| 字段名 | 类型 | 可空 | 默认值 | 约束/键 | 说明(含口径) |
|---|---|---:|---|---|---|
> 金额类字段必须注明:币种、精度、舍入/截断规则、是否允许负数。
## 索引Indexes
- 索引名 / 字段 / 是否唯一 / 备注
## 约束与外键Constraints & FKs
- 约束名 / 定义 / 备注
## 数据不变量Invariants
- 例如:状态机枚举范围、唯一性、跨字段一致性约束(如有)
## 变更历史Change History
- YYYY-MM-DD | Prompt-ID | 直接原因 | 变更摘要

View 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_CHANGELOGPer-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`

View File

@@ -0,0 +1,19 @@
# 变更审计记录Change Audit Record
- 日期/时间Asia/Shanghai
- Prompt-ID
- 原始原因Prompt 原文或 ≤5 行摘录):
- 直接原因(必要性 + 修改方案简介):
## 变更范围Changed
- 模块/接口/表/关键文件:
## 风险与回滚Risk & Rollback
- 风险点:
- 回滚要点:
## 验证Verification
- 至少 1 条可执行验证方式(测试/SQL/联调):
## 文件清单Files changed
- ...

View File

@@ -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: ...
---
## Pythondocstring/块注释)
"""
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: ...

View 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`

View File

@@ -0,0 +1,23 @@
# Steering & README 同步清单(逻辑改动必查)
## product.md产品/口径)
- 业务定义/指标口径/字段含义是否改变?
- 涉及金额的精度/舍入/阈值规则是否改变?
- 角色/权限模型是否改变?
## tech.md技术/运行)
- 新增/变更依赖(框架、库、驱动)?
- 配置项/环境变量/端口/服务启动方式是否改变?
- 数据访问边界ETL 库 vs 业务库)是否改变?
- 性能/一致性/幂等/重试策略是否改变?
## structure.md结构/职责)
- 新增目录/模块?
- 模块职责或边界是否重新划分?
- 新增集成点(队列、定时任务、外部系统)?
## README.md使用/联调)
- 本地启动步骤是否改变?
- 新增/变更配置项(.env 等)?
- API 契约是否变化(路径、参数、返回、错误码)?
- 小程序联调步骤是否变化?

View File

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

View 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 次迭代
- 每个测试需注释引用对应的设计属性编号

View 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/` 下为每张表提供表级文档
### 需求 2DDL 文件与数据库实际状态对比同步
**用户故事:** 作为数据开发人员,我希望 DDL 文件与数据库实际表结构保持一致,以便 DDL 文件可作为可信的 schema 参考。
#### 验收标准
1. WHEN 对比 ODS 层 DDL 文件(`database/schema_ODS_doc.sql`)与数据库 `billiards_ods` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项(缺失表、多余表、字段差异、类型差异、约束差异)
2. WHEN 对比 DWD 层 DDL 文件(`database/schema_dwd_doc.sql`)与数据库 `billiards_dwd` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项
3. WHEN 对比 DWS 层 DDL 文件(`database/schema_dws.sql`)与数据库 `billiards_dws` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项
4. WHEN 对比 ETL_Admin 层 DDL 文件(`database/schema_etl_admin.sql`)与数据库 `etl_admin` schema 实际表结构时THE 对比脚本 SHALL 输出所有差异项
5. WHEN 发现差异时THE DDL 文件 SHALL 以数据库实际状态为准进行修正
6. WHEN DDL 文件被修正后THE 变更记录 SHALL 在对应层的 `changes/` 目录下生成一份差异说明文档
### 需求 3补全 ODS 层表级文档
**用户故事:** 作为数据开发人员,我希望 ODS 层每张表都有完整的表级文档,以便理解原始数据结构和来源。
#### 验收标准
1. THE ODS 表级文档 SHALL 为 `billiards_ods` schema 中的每张 ODS 表生成一份 Markdown 文档,存放于 `docs/bd_manual/ODS/main/`
2. THE ODS 表级文档 SHALL 遵循与 DWD/DWS 表级文档一致的格式,包含:表信息表格、字段说明表格、使用说明(含 SQL 示例)、可回溯性信息
3. WHEN ODS 表的字段含有 COMMENT 注释时THE 表级文档 SHALL 将 COMMENT 中的说明、示例、JSON 字段映射信息提取并填入字段说明
4. THE ODS 表级文档的表信息 SHALL 包含 Schema、表名、主键、数据来源API 端点或文件)、说明
5. THE ODS 表级文档 SHALL 包含 ETL 元数据字段(`content_hash``source_file``source_endpoint``fetched_at``payload`)的统一说明
6. THE ODS 表级文档的文件命名 SHALL 遵循 `BD_manual_{表名}.md` 格式
### 需求 4建立 API JSON → ODS 字段映射文档
**用户故事:** 作为数据开发人员,我希望有一份清晰的 API 响应字段到 ODS 表字段的映射文档,以便理解数据从 API 到 ODS 的转换逻辑。
#### 验收标准
1. THE 映射文档 SHALL 为每个 API 端点与其对应的 ODS 表建立一份映射文件,存放于 `docs/bd_manual/ODS/mappings/`
2. THE 映射文档 SHALL 包含以下信息API 端点路径、对应 ODS 表名、JSON 响应路径(如 `data.tenantMemberInfos`)、每个字段的 JSON 路径到 ODS 列名的映射
3. WHEN 字段存在类型转换或值处理逻辑时THE 映射文档 SHALL 记录转换规则(如时间格式转换、枚举值映射、金额精度处理)
4. THE 映射文档 SHALL 标注 ETL 补充字段(`content_hash``source_file``source_endpoint``fetched_at``payload`)的生成逻辑
5. THE 映射文档 SHALL 参考 `models/parsers.py` 中的解析逻辑和 `docs/api-reference/` 中的端点文档作为信息来源
6. THE 映射文档的文件命名 SHALL 遵循 `mapping_{API端点名}_{ODS表名}.md` 格式
### 需求 5建立 ODS 数据字典
**用户故事:** 作为数据开发人员,我希望有一份 ODS 层的数据字典汇总文档,以便快速查阅所有 ODS 表的概览信息。
#### 验收标准
1. THE ODS 数据字典 SHALL 创建于 `docs/dictionary/ods_tables_dictionary.md`
2. THE ODS 数据字典 SHALL 列出所有 ODS 表的概览信息,包含:表名、中文说明、主键、记录数、数据来源
3. THE ODS 数据字典 SHALL 遵循与现有 DWD/DWS 数据字典一致的格式

View File

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

View File

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

View 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` 位置包含重定向说明

View 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 的条目和说明

View 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
- 每个任务引用了具体的需求编号以便追溯
- 检查点用于增量验证
- 属性测试验证通用正确性属性,单元测试验证具体例子和边界情况

View File

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

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -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_UPscale=2
- 创建 `packages/shared/src/neozqyy_shared/datetime_utils.py`:时区转换、日期范围计算
- 创建 `packages/shared/tests/__init__.py`
- _Requirements: 12.1, 12.2_
- [~] 10.2 生成 packages/shared/pyproject.toml
- 声明包名 `neozqyy-shared`最小依赖python-dateutil、tzdata
- _Requirements: 12.3_
- [~] 10.3 编写配置优先级属性测试
- **Property 3: 配置优先级 - .env.local 覆盖**
- **Validates: Requirements 4.3**
- [~] 10.4 编写必需配置缺失检测属性测试
- **Property 4: 必需配置缺失检测**
- **Validates: Requirements 4.4**
- [ ] 11. 检查点 - 全局验证
- 验证 uv workspace 依赖解析:在根目录运行 `uv sync`
- 验证 ETL 单元测试:在 `apps/etl/pipelines/feiqiu/` 下运行 `pytest tests/unit`
- Ensure all tests pass, ask the user if questions arise.
- [ ] 12. RLS 与多门店隔离验证
- [~] 12.1 编写 RLS 按 site_id 隔离属性测试
- **Property 11: RLS 按 site_id 隔离**
- **Validates: Requirements 13.2**
- 需要集成测试环境test_etl_feiqiu 数据库)
- [ ] 13. 最终检查点
- 确保所有文件已创建、所有 README 已编写、所有 DDL 语法正确
- Ensure all tests pass, ask the user if questions arise.
## 备注
- 标记 `*` 的任务为可选测试任务,可跳过以加速 MVP
- 每个任务引用具体需求编号,确保可追溯
- 检查点确保增量验证,避免问题累积
- 属性测试验证通用正确性,单元测试验证具体边界情况
- 文件复制操作需要用户在终端手动执行涉及跨目录操作Kiro 负责生成目标文件内容

View File

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

View File

@@ -0,0 +1,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` 生成器:随机树结构(限制深度和宽度)
- 文件树生成器:构造临时目录结构用于扫描器测试

View 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/` 目录是否存在,若不存在则创建该目录

View 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 次迭代
- 单元测试验证具体示例和边界情况,属性测试验证通用正确性

View File

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

View 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 1data_source 参数决定执行路径
*对于任意* 任务代码和任意 `data_source`online/offline/hybridTaskExecutor 执行该任务时,抓取阶段执行当且仅当 `data_source``online``hybrid`,入库阶段执行当且仅当 `data_source``offline``hybrid`
**验证:需求 1.2**
### Property 2成功任务推进游标
*对于任意* 非工具类任务,当任务执行成功且返回包含有效 `window`(含 `start``end`的结果时CursorManager.advance 应被调用且参数与返回的窗口一致。
**验证:需求 1.3**
### Property 3失败任务标记 FAIL 并重新抛出
*对于任意* 非工具类任务当任务执行过程中抛出异常时RunTracker 应被更新为 FAIL 状态,且该异常应被重新抛出给调用方。
**验证:需求 1.4**
### Property 4工具类任务由元数据决定
*对于任意* 任务代码TaskExecutor 是否跳过游标管理和运行记录,取决于 TaskRegistry 中该任务的 `requires_db_config` 元数据。当 `requires_db_config=False` 时跳过,否则执行完整生命周期。
**验证:需求 1.6, 4.2**
### Property 5管道名称→层列表映射
*对于任意* 有效的管道名称PipelineRunner 解析出的层列表应与 `PIPELINE_LAYERS` 字典中的定义完全一致。
**验证:需求 2.1**
### Property 6processing_mode 控制执行流程
*对于任意* processing_mode 值,增量 ETL 执行当且仅当模式包含 `increment`(即 `increment_only``increment_verify`),校验流程执行当且仅当模式包含 `verify`(即 `verify_only``increment_verify`)。
**验证:需求 2.3, 2.4**
### Property 7管道结果汇总完整性
*对于任意* 一组任务执行结果PipelineRunner 返回的汇总字典应包含 `status``pipeline``layers``results` 字段,且 `results` 列表长度等于实际执行的任务数。
**验证:需求 2.6**
### Property 8TaskRegistry 元数据 round-trip
*对于任意* 任务代码、任务类和元数据组合requires_db_config、layer、task_type注册后通过 `get_metadata` 查询应返回相同的元数据值。
**验证:需求 4.1**
### Property 9TaskRegistry 向后兼容默认值
*对于任意* 使用旧接口(仅 task_code 和 task_class注册的任务查询元数据应返回 `requires_db_config=True``layer=None``task_type="etl"`
**验证:需求 4.4**
### Property 10按层查询任务
*对于任意* 注册了 `layer` 元数据的任务集合,`get_tasks_by_layer(layer)` 返回的任务代码集合应等于所有 `layer` 匹配的已注册任务代码集合。
**验证:需求 4.3**
### Property 11pipeline_flow → data_source 映射一致性
*对于任意*`pipeline_flow`FULL/FETCH_ONLY/INGEST_ONLY映射到 `data_source` 的结果应与预定义映射表一致FULL→hybrid、FETCH_ONLY→online、INGEST_ONLY→offline。同样配置键 `pipeline.flow` 应自动映射到 `run.data_source`
**验证:需求 8.1, 8.2, 8.3, 5.2, 8.4**
## 错误处理
### TaskExecutor 错误处理
- 任务执行异常:更新 RunTracker 状态为 FAIL含 error_message然后重新抛出异常
- 游标推进失败:记录错误日志,不影响任务结果(任务本身已成功)
- 任务配置不存在:返回 `{"status": "SKIP"}` 结果,不抛异常
### PipelineRunner 错误处理
- 单个任务失败:记录错误,继续执行后续任务(与当前行为一致)
- 校验框架未安装:返回 `{"status": "SKIPPED"}` 并记录警告
- 无效管道名称:抛出 `ValueError`
### CLI 错误处理
- 配置加载失败:`SystemExit` 并输出错误信息
- 资源创建失败:`SystemExit` 并输出错误信息
- 执行过程异常:记录错误日志,`finally` 块确保资源释放,返回非零退出码
### 弃用警告
- 使用 Python `warnings.warn(DeprecationWarning)` 发出弃用警告
- 同时在日志中记录映射详情,便于运维排查
## 测试策略
### 单元测试
使用 `pytest` + 现有的 `FakeDB`/`FakeAPI` 测试工具(`tests/unit/task_test_utils.py`)。
**TaskExecutor 测试**
- 注入 mock 依赖FakeDB、FakeAPI、mock CursorManager、mock RunTracker
- 验证成功/失败/跳过三种路径
- 验证工具类任务不触发游标/运行记录
- 验证 data_source 参数正确控制抓取/入库阶段
**PipelineRunner 测试**
- 注入 mock TaskExecutor
- 验证不同 processing_mode 下的执行流程
- 验证管道→层→任务的解析链
**TaskRegistry 测试**
- 验证元数据注册和查询
- 验证向后兼容(无元数据注册)
- 验证按层查询
**配置兼容性测试**
- 验证旧键→新键映射
- 验证优先级规则
- 验证默认值变更
### 属性测试
使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
每个属性测试必须用注释标注对应的设计属性编号:
```python
# Feature: scheduler-refactor, Property 8: TaskRegistry 元数据 round-trip
```
**属性测试覆盖**
- Property 1: data_source 参数决定执行路径
- Property 2: 成功任务推进游标
- Property 3: 失败任务标记 FAIL 并重新抛出
- Property 4: 工具类任务由元数据决定
- Property 5: 管道名称→层列表映射
- Property 6: processing_mode 控制执行流程
- Property 7: 管道结果汇总完整性
- Property 8: TaskRegistry 元数据 round-trip
- Property 9: TaskRegistry 向后兼容默认值
- Property 10: 按层查询任务
- Property 11: pipeline_flow → data_source 映射一致性

View File

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

View File

@@ -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
View 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

View File

@@ -0,0 +1,27 @@
---
inclusion: always
---
# GovernanceLite
## 目的
在“上下文压缩很致命”的前提下,保留最小硬约束:**任何逻辑改动必须可追溯、可验证、可回滚**。
## 何时必须审计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`

View File

@@ -0,0 +1,18 @@
---
inclusion: always
---
# 语言与编码规范(强制)
## 输出语言
- 默认所有“说明性文字”一律使用简体中文对话回复、文档内容、代码注释、README/ADR/变更说明等)。
- 允许保留英文的部分:
- 代码标识符(类名/函数名/变量名/接口名/库名/命令名)不翻译
- 第三方工具的原始 CLI 输出/报错原文不篡改(可在原文后补充中文解释)
## 文档与注释
- 新增/修改的文档必须与代码变更同步更新
- 注释只写“为什么/边界/假设”,避免复述代码
## 编码与字符集
- 仓库内所有文本文件统一 UTF-8无 BOM。
- 禁止出现 GBK/Big5 混用;若发现历史文件,先转码再重构

22
.kiro/steering/product.md Normal file
View 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`

View 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 标记注释

View 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
View 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 一个精简版 .md25 个)
│ │ ├── 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 个精简版 .mdendpoints/ 说明从"25 个"更正为"24 个"
- 风险与验证: 纯文档结构描述变更,无运行时影响
-->

60
.kiro/steering/tech.md Normal file
View 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
View File

@@ -0,0 +1,9 @@
tmp/
.hypothesis/
node_modules/
__pycache__/
.pytest_cache/
*.pyc
logs/
samples/
.git/

8
NeoZQYY.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

53
README.md Normal file
View 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六层 Schemameta/ods/dwd/core/dws/app
- FastAPI + uvicorn
- PySide6桌面 GUI
- Donut + TDesign微信小程序

17
apps/README.md Normal file
View 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
View File

0
apps/backend/.gitkeep Normal file
View File

41
apps/backend/README.md Normal file
View 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-sharedworkspace 引用)
## Roadmap
- [ ] 用户管理与微信登录
- [ ] RBAC 权限中间件
- [ ] 任务审批流 API
- [ ] FDW 数据查询接口(助教业绩、财务日报等)

View File

View 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")

View 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
View File

@@ -0,0 +1,22 @@
"""
NeoZQYY 后端 API 入口
基于 FastAPI 构建,为微信小程序提供 RESTful API。
OpenAPI 文档自动生成于 /docsSwagger UI和 /redocReDoc
"""
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"}

View File

View File

View File

View 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 }

View File

15
apps/etl/README.md Normal file
View File

@@ -0,0 +1,15 @@
# apps/etl/
## 作用说明
ETL 数据管线集合。每个上游数据源对应 `pipelines/` 下的一个子目录,当前仅有飞球平台(`feiqiu`)。管线负责从 SaaS API 抽取数据,经 ODS→DWD→Core→DWS 逐层处理后落库。
## 内部结构
- `pipelines/feiqiu/` — 飞球平台 ETLapi、cli、config、loaders、models、orchestration、scd、tasks、utils、quality、tests
## Roadmap
- 将通用抽取/加载逻辑抽离为 `etl_sdk` 共享包,供多管线复用
- 将各平台 API 客户端拆分为独立 `connectors` 包,实现可插拔数据源接入
- 新增管线时在 `pipelines/` 下创建同构子目录

View File

View 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 []

View 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)]

View 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

View 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

View 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())

View 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"

View 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

View File

@@ -0,0 +1,3 @@
{
"tasks": {}
}

View 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]

View 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

View 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

View 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()

View 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 文档逐表比对;多轮修复比对脚本 bugskip_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
- **摘要**:用会员消费统计 APIQueryMemberConsumptionStatistics替换权限配置查询 APIrole_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)

View 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 编码,中文撰写

View 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/ 子目录说明
- 风险与验证: 纯文档变更,无运行时影响
-->

File diff suppressed because it is too large Load Diff

View 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"
}
]

View File

@@ -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 条为视频 URLoss 存储路径)
含义:助教个人视频介绍地址。
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 为与灯控设备联动预留的字段,目前在该门店未实际启用。

View File

@@ -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_passwordWiFi 账号和密码。
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", "1" 等。
含义:台桌名称/编号,供人阅读。
关系:
与台桌列表中的 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 条,但已经有完整的门店、桌台、助教、时长、金额、原因字段。
说明设计不是临时补的,而是参照完整流水表(助教流水)设计的一张配套表,只是当前时间范围内“废除事件”确实不多。

View File

@@ -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 结果
-->

View File

@@ -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枚举。
观测值(本样本):
189 条
411 条
与 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=2startNum=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
在 库存现状.json20251110_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 永远对应负数 changeNumstockType=4 永远对应正数。
说明系统在设计时,不是单纯依赖 changeNum 的正负来判断业务含义,而是:
用 stockType 表示业务场景(销售出库/盘点/入库等),
用 changeNum 的正负表达实际的增或减。
其它可能的 stockType如报损出库/盘亏/退货等)本批样本中未出现,但结构已经预留可扩展。
价格在本表中是“静态快照”,而不是动态计算字段
对同一个 siteGoodsId所有记录的 price 一致,表明:
price 是当时商品价格的快照副本。
真实的“标准价/进价/零售价”仍以商品档案为准,只是在库存变动记录中复制一份方便报表使用。
这一设计避免了之后价格调整导致历史库存记录无法按当时价格还原的问题。
操作员信息体现“人工 vs 系统”两类来源
大部分记录由“收银员”操作,说明库存减少主要来自前台销售。
个别记录由“系统”操作,说明系统本身会根据某些规则自动生成库存变动记录(例如盘点差异自动入库/出库、库存初始化等)。
结构上不需要额外字段即可从 operatorName 粗略判断记录来源。
与商品分类强绑定,方便结构化报表
通过 goodsCategoryId / goodsSecondCategoryId这张库存变动明细表可以非常方便地按“饮料/香烟/小食”等分类对库存变动进行结构化分析。
虽然你不希望做“大数据/盈利分析”,但从结构角度看,这两个字段是后续任意统计的关键维度。

View File

@@ -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 或负数:
036次、-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导出那一刻的库存快照。
二者不一定相等(有部分记录存在差 14 的差值),说明结构上清晰区分了查询区间和当前状态。
汇总粒度清晰:每个 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