Files
Neo-ZQYY/docs/audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md
Neo 95a4500c75 chore(ops): reload 卡死三层预防 + F1-5a 完整走查报告
reload 卡死三层预防(走查中遭遇 uvicorn graceful shutdown 死等触发):
- Layer 1 (apps/backend/start_uvicorn.py 新): 把 reload-excludes
  封装在 Python 字符串内,ps1 命令行只有字面路径,根治 PowerShell
  PSNativeCommandArgumentPassing 在不同 profile 下 wildcard 展开
  行为差异(数组 splatting 和 --% 都不稳)。同时显式设
  timeout-graceful-shutdown=5,5 秒强杀防死等
- Layer 2 (scripts/ops/backend-watchdog.ps1 新): 自主 socket 探针
  (TcpClient + 手写 HTTP/1.1 GET,Connection: close)规避 .NET
  HttpClient pool 复用 + 系统代理误报;3s × 3 = 9s 触发重启;
  进程链 kill 至 pwsh 后端窗口(关闭原窗口);3 次/小时上限自停
- Layer 3 (scripts/ops/start-admin.ps1): 启动时拉起 watchdog,
  菜单 [4] 仅重启后端选项,主菜单退出时一并 kill 看门狗

CLAUDE.md: 新增"后端 reload 卡死预防(强制)"章节,
分级文件风险表 + SOP + 启动菜单速查

走查报告(应查尽查严肃版):
- 后端 6 个改造点 PASS(P1-P4 + GUC + ai_run_logs runtime 字段)
- admin-web 7 页 Playwright 实地走查 → 5 项 UI 不完整登记 F1-5b
- 小程序看板 tab 7 页 weixin-devtools-mcp 实地 + DB 数据核对 →
  board-finance 5/6 项上界裁剪吻合;board-customer 业务日生效;
  board-coach 月度聚合表设计盲区;5 项 sandbox 覆盖盲区登记 F1-5b
- 8 张走查截图归档 docs/audit/changes/screenshots/2026-05-05_f1_5a_walkthrough/

audit_dashboard 刷新到 153 条审计

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:53:08 +08:00

13 KiB

2026-05-05 — Wave 1 F1-5a 完整走查(应查尽查版)

摘要

针对 commit 421e193(F1-5a 沙箱 batch-run 接入 runtime_context)的应查尽查严肃走查:

  • 9 项 PASS:F1-5a 改造点 + 小程序看板核心页面 sandbox 上界裁剪验证通过
  • 3 个 BUG 已修:xcx_runtime_clock require_approved 括号丢失;retry_trigger_job payload Json wrap;reload 卡死三层预防
  • 10 项 F1-5b/F2 待办登记:admin-web 4 项 UI 不完整 + 小程序 5 项 sandbox 覆盖盲区 + 后端 1 项 manager 权限链路

走查时长 ~3 小时(含 reload 卡死调试 + watchdog 修复)。 本次走查不再是仅后端最小验证,涵盖:

  • 后端 6 个改造点 + 数据库 + HTTP 端到端
  • admin-web 7 个相关页面(Playwright 实地走查)
  • 小程序看板 tab 7 个相关页面(weixin-devtools-mcp 实地走查 + DB 数据核对)
  • 修复阻塞 bug 后重新走查

关联

  • 改造 commit:421e193 fix(ai): F1-5a 沙箱 batch-run 接入 runtime_context (W1 / 阶段 A 主体)
  • 改造审计:docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md
  • 决策依据:docs/_overview/wave1-findings/F1-5-impl-decisions.md
  • 测试库 site 清单:memory/project_test_db_sites.md(只有 2790685415443269 有数据)

走查环境

时间 2026-05-05 03:15 ~ 06:15(含 reload 调试时间)
后端 http://127.0.0.1:8000(本地 uvicorn,改造后 start_uvicorn.py 启动)
admin-web http://localhost:5173
小程序 weixin-devtools-mcp ws://127.0.0.1:9420(店长 manager 身份)
数据库 test_zqyy_app + test_etl_feiqiu
测试 site 2790685415443269(朗朗桌球 LL0001 — 唯一有数据 site)
沙箱日期(主走查) 2026-04-20(4 月有数据,不是停业日)

A 段 — 后端走查(已 PASS,详见前次报告)

Step 1-6 后端走查(初版 PASS)

  • Step 1:PATCH /api/admin/runtime-context 切沙盒 OK
  • Step 2:DB 校验 site_runtime_context OK
  • Step 3:batch-run estimate + confirm 链路 OK
  • Step 4:AIRunLogService.create_log 写入 runtime_mode='sandbox' OK
  • Step 5:apply_runtime_session_vars GUC 双 key + bind_to_session=True 激活 OK
  • Step 6:切回 live + 残留检查 OK

P1 A6 ai_run_logs 索引(补执行 migration + 3 条校验 PASS)

发现 migration 20260505__ai_run_logs_runtime_index.sql 从未在测试库执行。补执行后 3 条校验:

校验 结果
pg_indexes 存在 PASS — idx_ai_run_logs_runtime_site 物理就位
EXPLAIN 启用 PASS — 查询计划用到目标索引
索引大小 PASS — 80 kB(表 1280 kB)

P2/P3 A3 prompt ref_date(month/week/lastMonth 全 PASS)

切 sandbox=2026-03-01,核对 _calc_date_range 改造前后差异:

枚举 改造前(default date.today()) 改造后(显式 business_date) 判定
month (2026-05-01, 2026-05-05) (2026-03-01, 2026-03-01) PASS
week (2026-05-04, 2026-05-05) (2026-02-23, 2026-03-01) PASS
lastMonth (2026-04-01, 2026-04-30) (2026-02-01, 2026-02-28) PASS

grep 确认 app2_finance_prompt.py 2 处 + app2a_finance_area_prompt.py 1 处 = 3 处全就位

P4 A4 retry_trigger_job runtime 列写入 + 发现 payload BUG

直接调 service 层发现 psycopg2.ProgrammingError: can't adapt type 'dict':

  • jsonb 列读出是 dict,INSERT 时未 psycopg2.extras.Json() wrap
  • 生产环境点击"重试"必 500,F1-5a 走查前未发现
  • 修复:apps/backend/app/services/ai/admin_service.pypsycopg2.extras import + Json wrap
  • v2 直接 SQL 复现验证 PASS:
    • runtime_mode='sandbox' / sandbox_instance_id 一致 ✓
    • payload {'foo':'bar','n':42} 完整保留 ✓

Reload 卡死三层预防(P4b 调试副产物)

走查中遭遇 uvicorn reload 卡死(Waiting for background tasks to complete)。补充三层防护:

Layer 改造 文件
1 --timeout-graceful-shutdown 5 5 秒强杀 apps/backend/start_uvicorn.py (新建)
2 backend-watchdog.ps1 自主 socket 探针 + 进程链 kill 至 pwsh + 3 次/小时上限自停 scripts/ops/backend-watchdog.ps1
3 start-admin.ps1 启动菜单 [4] 仅重启后端 scripts/ops/start-admin.ps1

根治 wildcard 展开问题:把 --reload-exclude tests/* 等 wildcard 字符串封装在 start_uvicorn.py 的 Python 字符串里,ps1 命令行只有字面路径,PowerShell shell 不接触 wildcard,根治 PSNativeCommandArgumentPassing 不一致行为

B 段 — admin-web 走查(Playwright 实地)

P5-P8 PASS 项

步骤 页面 结果
P5 端口 5173 / 5174 区分确认 PASS
P6 admin / admin123 登录到 dashboard PASS
P7 /settings/runtime-context 切沙盒 UI PASS — 2 门店列表渲染 + sandbox/live 状态标识
P8 /ai/operations Sandbox Alert(F1-5a A5) PASS — 完整显示业务日 + sandbox_instance_id + 影响范围 + 切回 live 链接

P9-P12 F1-5a UI 改造覆盖不全(4 项登记 F1-5b)

编号 页面 问题
F1-5b-UI-1 /logs/ai-run-logs 列表 列定义无 runtime_mode / sandbox_instance_id(后端有写入,前端不展示)
F1-5b-UI-2 /logs/ai-run-logs 详情 Drawer 同样无 runtime 字段
F1-5b-UI-3 /ai/dashboard AIDashboard 无 sandbox 提示条 + 统计未按 runtime_mode 分组
F1-5b-UI-4 全局状态栏(顶栏 + 底 footer) 无 sandbox 徽章 — 离开 AIOperations 看不到当前是 sandbox 还是 live

P12 补走查

  • AITriggerJobs(/ai/trigger-jobs):空列表,列定义同样无 runtime 列(F1-5b-UI-5 同类问题)

C 段 — 小程序看板 tab 走查(weixin-devtools-mcp 实地 + DB 数据核对)

切沙箱到 2026-04-20(4 月有完整数据,不是停业日)。

修复阻塞 BUG 1:xcx_runtime_clock 500

发现小程序 /api/xcx/runtime/clock 返回 500:

  • xcx_runtime_clock.py:49 AttributeError: 'function' object has no attribute 'site_id'
  • 根因:Depends(require_approved) 把 factory 函数当依赖,而 require_approved 是 factory 必须 () 调用
  • 修复:apps/backend/app/routers/xcx_runtime_clock.pyDepends(require_approved())
  • 验证:小程序调返回 200 + 完整 sandbox ctx ✓

这个 bug 阻塞了 sandbox 在小程序所有页面生效 — 因为 getBusinessClock() 一直 500 后降级 localFallback() 用真实今天。

看板 tab 主页(P13 看板核心)

board-finance(财务)— PASS 上界裁剪 5/6 ✓

DB 直查 4 月每天 → 累计两组:4/1~4/20 上界 vs 4/1~4/30 全月,对比前端:

指标 4/1~4/20 上界 4/1~4/30 全月 前端 判定
发生额 290193.22 362967.36 290193.22 上界吻合 ✓
优惠 109917.76 140522.68 109917.76 上界吻合 ✓
确认收入 180275.46 222444.68 180275.46 上界吻合 ✓
现金流入 184555.30 211927.58 184555.30 上界吻合 ✓
现金支出 0 0 0 上界吻合 ✓
储值充值 132000 133996 66000 ⚠ 字段差异(F1-5b-MP-1 复核)

recharge_total vs recharge_cash 字段差异需复核 backend SQL。

board-customer(客户)— PASS 业务日生效 ✓

陈腾鑫(memberId=2799207124305669):

  • 前端 elapsedDays = 15
  • DB 最后 visit_time = 2026-04-05 04:07(visit_date=4-4 但 visit_time::date=4-5)
  • 算式:business_date(4-20) - last_visit_date(4-5) = 15
  • 反证(真实今天):5-5 - 4-5 = 30

board-coach(助教)— ⚠ 月度聚合表设计盲区(F1-5b-MP-2)

喵喵(assistant_id=3148987180059141):

  • 前端 perfHours = 73.54
  • DB dws_assistant_salary_calc.effective_hours = 73.54 完全吻合
  • salary_calc月度聚合表(salary_month=2026-04-01),无 business_date 上界切片字段
  • daily 累计 4/14/20 = 68.59h,4/14/30 = 77.02h —都不等于 73.54
  • 结论:salary_calc 内部用罚分扣减算法,月度表设计上无法按业务日切片
  • F1-5b 需考虑:月度面板 sandbox 上界设计(daily 累计?或 etl 跑月中 snapshot?)

子页

customer-detail — ⚠ coachTasks.lastService 超上界(F1-5b-MP-3)

陈腾鑫详情页 coachTasks[0] = 盈盈,lastService = "05-01 01:12":

  • 当前 sandbox=2026-04-20
  • 5-1 数据不该出现在管理员视图(违反 sandbox 业务日上界)
  • 后端 xcx_customers/{id} 端点未对 coachTasks.lastService 做 business_date 裁剪

coach-detail — ✗ 预存 bug(F1-5b-MP-4)

pages/coach-detail/coach-detail 加载失败:

  • options.coachId = "3148987180059141" ✓
  • 但 page.data.coachId = "" ✗
  • onLoad 没把 options 写到 data.coachId,导致 fetchCoachDetail 拿空字符串
  • 与 F1-5a 无关,但走查发现登记

customer-records — ✓ 数据合理

陈腾鑫 4 月:visitCount=1,records=[2026-04-05] — sandbox=4-20 业务日内,合理。

coach-service-records — ⚠ runtime-clock 未接入(F1-5b-MP-5)

URL 传 ?year=2026&month=4,但页面 monthLabel = "2026年5月":

  • URL query 被忽略
  • 默认用 new Date() 取当月而非业务时钟
  • getBusinessClock() 应该已就位(xcx_runtime_clock bug 已修),但本页未调用
  • F1-5b 需在本页 onLoad 调用 getBusinessClock() 取业务月

performance-records — ✗ manager 视角无效(预存,无关 F1-5a)

manager 不是 coach,API 返回错误。本页是 coach 视角,与本次走查无关。

D 段 — 走查中遭遇但暂记的问题

task-list 403 权限不足

manager 角色调 /api/xcx/tasks/api/xcx/performance 返回 403。

诊断结果:

  • DB 直查 auth.user_site_roles + role_permissions:user 8778 在 site 2790685415443269 下有 5 个权限(view_tasks / view_board / view_board_coach / view_board_customer / view_board_finance)
  • 直接 await get_user_permissions(8778, 2790685415443269) 返回 5 个权限
  • 但 HTTP 路径仍 403 — 矛盾
  • 临时插入 logger.warning + detail debug 但 Neo 让聚焦看板,未深查
  • 已回滚 debug 插桩,登记 F1-5b/F2 详查

不阻塞看板走查(看板用 view_board_* 权限,这些都正常工作)。

显式合规清单(A1-A6)

改造点 走查方式 结果
A1 _run_batch 真 executor + lazy dispatcher 注入 step 3b HTTP estimate+confirm PASS(lazy 注入 + Semaphore + ValueError 兜底)
A2 ctx_snapshot 抓取/取出无漂移 step 3a/3b 间不漂移 PASS
A3 prompt 3 处 ref_date(app2_finance + app2a) P2/P3 _calc_date_range 单元 + grep 3 处 PASS
A4 retry_trigger_job runtime 列写入 P4 v2 SQL 复现 + payload bug fix PASS + bug fix
A5 admin-web AIOperations sandbox Alert P8 Playwright 实地 PASS
A6 ai_run_logs 索引 P1 补 migration + 3 校验 PASS
GUC bind_to_session 机制 step 5 双 key + 一站式 PASS
ai_run_logs runtime 字段写入 step 4 字段一致 PASS
xcx_runtime_clock 端点(F1-5a 间接依赖) 修 bug 后 200 ✓ fix + PASS
board-finance sandbox 上界裁剪 5/6 项 DB 累计核对 PASS
board-customer 业务日生效 陈腾鑫 elapsedDays = bd - last PASS

F1-5b/F2 待办清单(10 项)

admin-web (5 项)

# 描述
UI-1 AIRunLogs 列表加 runtime_mode / sandbox_instance_id
UI-2 AIRunLogs 详情 Drawer 加 runtime 字段
UI-3 AIDashboard 加 sandbox 提示条 + 统计按 runtime_mode 分组
UI-4 全局状态栏 sandbox 徽章
UI-5 AITriggerJobs 列表加 runtime 列

小程序 (5 项)

# 描述
MP-1 board-finance 储值充值字段差异(132000 vs 66000)复核 backend SQL
MP-2 board-coach 月度聚合面板 sandbox 上界设计(月度聚合表 vs daily 累计选型)
MP-3 customer-detail coachTasks.lastService 加 business_date 上界裁剪
MP-4 coach-detail data.coachId 未从 options 赋值(预存 bug)
MP-5 coach-service-records 接入 runtime-clock,onLoad 用业务月而非真实今天

后端(其他,1 项)

# 描述
BE-1 task-list 403 manager 权限链路矛盾(DB 5 权限 vs HTTP 路径 missing 矛盾)详查

走查中已修复的 BUG(3 个)

编号 文件 修复
BUG-1 apps/backend/app/routers/xcx_runtime_clock.py:29 Depends(require_approved)Depends(require_approved())
BUG-2 apps/backend/app/services/ai/admin_service.py:393 retry_trigger_job 的 payload 加 psycopg2.extras.Json() wrap
BUG-3 scripts/ops/start-admin.ps1 + apps/backend/start_uvicorn.py + scripts/ops/backend-watchdog.ps1 reload 卡死三层预防

走查脚本归档

_DEL/walkthrough_f1_5a/ 下:

  • step3-step6 — 后端 step 系列
  • step_p1-step_p4* — admin-web HTTP 验证
  • step_p4b_http_retry — retry HTTP 端到端
  • verify_finance_v4.py — board-finance 上界核对
  • verify_customer_v2.py — board-customer 陈腾鑫核对
  • verify_coach_v3.py / v5.py — board-coach salary_calc 核对
  • check_perms*.py — manager 权限调查
  • print_argv.py — wildcard 展开测试辅助

脚本仅一次性走查辅助,不入仓(_DEL/ 已 .gitignore 排除)。 F1-5b/F2 阶段会把同等覆盖以 pytest 形式入仓。

后续行动

  1. 本次走查完成后立即 commit:permission.py debug 已回滚 / start-admin.ps1 + watchdog + start_uvicorn.py / xcx_runtime_clock fix / admin_service payload Json wrap / 走查报告 + dashboard 刷新
  2. F1-5b 启动:本走查发现的 11 项待办,与 F1-5a 主体改造一并形成 F1-5b 阶段任务
  3. P11 上线前:UI-1 ~ UI-5(admin-web sandbox 透出)+ MP-1 ~ MP-5(小程序 sandbox 完整覆盖)是必跑闭环