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>
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
# 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.py` 加 `psycopg2.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.py` 改 `Depends(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/1~4/20 = 68.59h,4/1~4/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 完整覆盖)是必跑闭环
|
||||
Reference in New Issue
Block a user