diff --git a/.claude/hooks/post_edit_business_date_check.py b/.claude/hooks/post_edit_business_date_check.py new file mode 100644 index 0000000..20ff157 --- /dev/null +++ b/.claude/hooks/post_edit_business_date_check.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""PostToolUse hook: 编辑 backend service 后检查是否引回 CURRENT_DATE / date.today() + +F1-5b A1 决策(2026-05-05): + apps/backend/app/services/ 下涉及业务时间的查询应走 RuntimeContext.business_date + (sandbox 模式取虚拟日,live 取真实今天),不应直接用 SQL CURRENT_DATE 或 + Python date.today()。 + +例外:全局聚合(无 site_id 上下文)允许保留 CURRENT_DATE,需在该行附近加 + `# RUNTIME_CTX_BYPASS: <理由>` 注释明确标注。 + +本 hook 仅 soft warning(additionalContext),不强阻断,允许必要 fallback。 +""" +import json +import re +import sys + +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) + +tool_input = data.get("tool_input") or {} +fp = tool_input.get("file_path", "") +if not fp: + sys.exit(0) + +rel = re.sub(r"^.*?NeoZQYY[/\\]", "", fp.replace("\\", "/")) + +# 只关注 backend service 层 +if not re.search(r"^apps/backend/app/services/.*\.py$", rel): + sys.exit(0) + +# 提取本次写入/改动的内容 +content = tool_input.get("new_string") or tool_input.get("content") or "" +if not content: + sys.exit(0) + +# grep CURRENT_DATE / date.today() +lines = content.split("\n") +hits = [] +for i, line in enumerate(lines, 1): + if "RUNTIME_CTX_BYPASS" in line: + continue + # 排除注释行的纯文本提及 + stripped = line.lstrip() + if stripped.startswith("#") or stripped.startswith('"""') or stripped.startswith('"'): + continue + if re.search(r"\bCURRENT_DATE\b", line) or re.search(r"\bdate\.today\(\)", line): + hits.append((i, line.strip()[:80])) + +if not hits: + sys.exit(0) + +hint_lines = [ + f"[business-date-check] 检测到 {rel} 含 {len(hits)} 处 CURRENT_DATE / date.today():", +] +for ln, code in hits[:5]: + hint_lines.append(f" L{ln}: {code}") +if len(hits) > 5: + hint_lines.append(f" ... (+{len(hits) - 5} 处)") +hint_lines.append( + "F1-5b A1 决策:有 site_id 上下文应走 RuntimeContext.business_date。" + "全局聚合(无 site_id)允许保留,但需加 `# RUNTIME_CTX_BYPASS: <理由>` 注释。" +) + +print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": "\n".join(hint_lines), + } +})) diff --git a/.claude/settings.json b/.claude/settings.json index 0b1cfd9..bdc4f12 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -95,6 +95,16 @@ "timeout": 5 } ] + }, + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/post_edit_business_date_check.py\"", + "timeout": 5 + } + ] } ], "Stop": [ diff --git a/apps/backend/app/services/ai/admin_service.py b/apps/backend/app/services/ai/admin_service.py index a4b0225..85c8120 100644 --- a/apps/backend/app/services/ai/admin_service.py +++ b/apps/backend/app/services/ai/admin_service.py @@ -21,6 +21,7 @@ from app.ai.budget_tracker import BudgetTracker from app.database import get_connection from app.services.runtime_context import ( RuntimeContext, + as_runtime_today_param, get_runtime_context, runtime_insert_columns, ) @@ -94,12 +95,24 @@ class AdminAIService: """指定时间段内的调用次数、成功率、Token 消耗、平均延迟。 字段名沿用 today_* 前缀以兼容前端 DashboardResponse schema。 + + F1-5b A1: 时间窗口基准日: + - site_id 非 None: 用 RuntimeContext.business_date(sandbox 模式取虚拟日) + - site_id 为 None: 全局聚合,用 PG CURRENT_DATE(全局视图无单一业务日) """ site_clause, site_params = _site_filter(site_id) if date_from and date_to: time_clause = "created_at >= %s::date AND created_at < (%s::date + INTERVAL '1 day')" time_params: tuple = (date_from, date_to) + elif site_id is not None: + days = range_days if range_days and range_days > 0 else 1 + today = as_runtime_today_param(site_id) + time_clause = ( + "created_at >= %s::date - (%s::int - 1) * INTERVAL '1 day' " + "AND created_at < %s::date + INTERVAL '1 day'" + ) + time_params = (today, days, today) else: days = range_days if range_days and range_days > 0 else 1 time_clause = ( @@ -142,8 +155,26 @@ class AdminAIService: } async def _get_7d_trend(self, site_id: int | None) -> list[dict]: - """近 7 天按日聚合。""" - site_clause, params = _site_filter(site_id) + """近 7 天按日聚合。 + + F1-5b A1: site_id 非 None 用业务日上下界(sandbox 取虚拟日); + site_id None 全局聚合用 CURRENT_DATE。 + 2026-05-05 修补:同时加上界 < %s + 1day,sandbox 不漏未来数据。 + """ + site_clause, site_params = _site_filter(site_id) + if site_id is not None: + today = as_runtime_today_param(site_id) + time_clause = ( + "created_at >= %s::date - INTERVAL '6 days' " + "AND created_at < %s::date + INTERVAL '1 day'" + ) + params = (today, today) + site_params + else: + time_clause = ( + "created_at >= CURRENT_DATE - INTERVAL '6 days' " + "AND created_at < CURRENT_DATE + INTERVAL '1 day'" + ) + params = site_params conn = get_connection() try: with conn.cursor() as cur: @@ -154,7 +185,7 @@ class AdminAIService: COUNT(*) AS calls, COUNT(*) FILTER (WHERE status = 'success') AS success_count FROM biz.ai_run_logs - WHERE created_at >= CURRENT_DATE - INTERVAL '6 days' + WHERE {time_clause} {site_clause} GROUP BY day ORDER BY day @@ -176,8 +207,26 @@ class AdminAIService: ] async def _get_app_distribution(self, site_id: int | None) -> list[dict]: - """各 App 调用占比。""" - site_clause, params = _site_filter(site_id) + """各 App 调用占比。 + + F1-5b A1: site_id 非 None 用业务日上下界(sandbox 取虚拟日); + site_id None 全局聚合用 CURRENT_DATE。 + 2026-05-05 修补:同时加上界 < %s + 1day,sandbox 不漏未来数据。 + """ + site_clause, site_params = _site_filter(site_id) + if site_id is not None: + today = as_runtime_today_param(site_id) + time_clause = ( + "created_at >= %s::date - INTERVAL '6 days' " + "AND created_at < %s::date + INTERVAL '1 day'" + ) + params = (today, today) + site_params + else: + time_clause = ( + "created_at >= CURRENT_DATE - INTERVAL '6 days' " + "AND created_at < CURRENT_DATE + INTERVAL '1 day'" + ) + params = site_params conn = get_connection() try: with conn.cursor() as cur: @@ -185,7 +234,7 @@ class AdminAIService: f""" SELECT app_type, COUNT(*) AS cnt FROM biz.ai_run_logs - WHERE created_at >= CURRENT_DATE - INTERVAL '6 days' + WHERE {time_clause} {site_clause} GROUP BY app_type ORDER BY cnt DESC diff --git a/apps/backend/app/services/fdw_queries.py b/apps/backend/app/services/fdw_queries.py index ef27905..79035e4 100644 --- a/apps/backend/app/services/fdw_queries.py +++ b/apps/backend/app/services/fdw_queries.py @@ -93,6 +93,21 @@ def _fdw_context(conn: Any, site_id: int, *, etl_conn: Any = None): 不传时新建连接并在 yield 后自动关闭。避免同一请求内多次新建连接(每次 ~2.6s)。 CHANGE 2026-05-02 | 同时设置 app.current_business_date / app.current_runtime_mode, 供 RLS 视图层(C 方案)做日期上界裁剪。conn=None 时降级 live。 + + F1-5b A3 架构契约(D2"完整且统一"): + 本函数是 backend 访问 ETL 库 (etl_feiqiu) 的**唯一入口**,任何 ETL 视图查询 + 都必须通过 `with _fdw_context(...) as cur` 形式调用。本函数内部 SET 三个 GUC: + - app.current_site_id (RLS 多门店隔离) + - app.current_business_date (sandbox 业务日上界,激活 ETL 库 26 个 v_* 视图裁剪) + - app.current_runtime_mode (live/sandbox 标识) + 禁止绕过 `_fdw_context` 直接 `_get_etl_connection` + cur.execute, + 否则 sandbox 业务日上界在该路径不生效。 + + 与 `apply_runtime_session_vars` 的关系: + - apply_runtime_session_vars 设 business_date + runtime_mode (业务库 conn 用) + - _fdw_context 额外设 site_id (ETL 库 RLS 必需) + 两者互补,不重叠。zqyy_app conn 调 apply_runtime_session_vars, + ETL conn 走 _fdw_context。 """ from app.services.runtime_context import ( MODE_LIVE, @@ -104,15 +119,18 @@ def _fdw_context(conn: Any, site_id: int, *, etl_conn: Any = None): bd_str = "" rt_mode = MODE_LIVE try: - if conn is not None: - ctx = get_runtime_context(site_id, conn=conn) - bd_str = ctx.business_date.isoformat() - rt_mode = MODE_SANDBOX if ctx.is_sandbox else MODE_LIVE - else: - from datetime import date as _date - bd_str = _date.today().isoformat() + # F1-5b A2: conn=None 也尝试取 ctx(get_runtime_context 内部自开 conn), + # 确保 sandbox 业务日上界在所有调用路径生效。 + ctx = get_runtime_context(site_id, conn=conn) if conn is not None else get_runtime_context(site_id) + bd_str = ctx.business_date.isoformat() + rt_mode = MODE_SANDBOX if ctx.is_sandbox else MODE_LIVE except Exception: + # 三层 fallback: RuntimeContext 完全不可用 → 降级真实 today + warning from datetime import date as _date + logger.warning( + "F1-5b A2: RuntimeContext 不可用,fdw_context 降级真实 today (site_id=%s)", + site_id, exc_info=True, + ) bd_str = _date.today().isoformat() rt_mode = MODE_LIVE @@ -2549,6 +2567,12 @@ def _get_weekly_visits_batch( from datetime import date as _date, timedelta as _timedelta if ref_date is None: + # F1-5b A2: ref_date 应由调用方从 RuntimeContext.business_date 显式传入, + # 此处降级真实 today 仅作 last-resort 兜底,违反 sandbox 上界 + logger.warning( + "F1-5b A2: _get_weekly_visits_batch 收到 ref_date=None," + "调用方应从 RuntimeContext.business_date 显式传入,降级真实 today" + ) ref_date = _date.today() elif hasattr(ref_date, "date") and not isinstance(ref_date, _date): ref_date = ref_date.date() diff --git a/docs/_overview/wave1-findings/F1-5b-tasks.md b/docs/_overview/wave1-findings/F1-5b-tasks.md new file mode 100644 index 0000000..9734149 --- /dev/null +++ b/docs/_overview/wave1-findings/F1-5b-tasks.md @@ -0,0 +1,492 @@ +# F1-5b 任务拆分(规范化稳健版) + +> 日期:2026-05-05 +> 上游:F1-5a 主体(commit `421e193`)+ 走查 bug fix(`1baa212` `a045625`)已完成 +> 走查报告:[`docs/audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md`](../../audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md) +> 决策卡:[`F1-5-impl-decisions.md`](F1-5-impl-decisions.md)(D1-D6 已 Neo 拍板) +> 用法:本文档为 F1-5b 阶段唯一权威任务清单。每项任务按 §3 标准 5 步流程实施,每个 Wave 末做 checkpoint。 + +--- + +## 〇、与 v1 版的关键调整(规范化) + +| 调整 | 原版 | 调整版 | +|---|---|---| +| 实施流程 | 各任务自由实施 | **统一 5 步**:调研 → TDD → 实施 → 验证 → 审计(§3) | +| **Step 4 验证(关键稳健性)** | **单口径 mcp 走查** | **双口径**:4a live 主验 + 4b sandbox 二验,防沙箱遗漏(§3.1 + §3.3 必做清单) | +| Commit 粒度 | 2 个 commit(混合后端/前端/测试) | **4-6 个 commit**,每个逻辑独立可 review/revert(§4.3) | +| 防御机制 | 仅 grep 验证 | **A1 完成立即加 PreToolUse hook 防回归**(§5.1) | +| Wave 节奏 | A 收尾直接进 B | **Wave A 末 mid-wave checkpoint**,Neo 复审后启动 B(§4.2) | +| 进度跟踪 | 无 | **§6 进度表**,每项含 status / commit / audit / 4a 4b 完成状态 | +| Neo 决策点 | 末尾"反馈区"空白 | **§7 结构化决策表**(D7-D10 4 项,Neo 拍板后启动) | +| 失败兜底 | 仅回退路径 | **§8 失败决策矩阵**:严重度阈值 + 升级路径 | +| 测试守护 | 测试整体在 Wave A | **TDD 红绿重构嵌入每项 A 任务**,T1 case 与 A 任务同步演进 | + +--- + +## 一、阶段总览 + +### 1.1 阶段目标(不变) + +把 F1-5a "沙箱 batch-run 接入 runtime_context"上半场未收口的工作做完: +1. **架构主体收口**:F1-5a 计划但未做的 admin_service `CURRENT_DATE` → `business_date`、fdw_queries 异常分支兜底、apply_runtime_session_vars 全 DB 入口统一注入(D2 决策"完整且统一") +2. **F1-5a 走查 follow-up**:12 项闭环 +3. **测试 3 套**:test_runtime_context.py / test_admin_ai_batch_runtime.py / test_dispatcher_runtime.py +4. **文档同步**:P20 SPEC §6/§10/§11/§15 + +### 1.2 验收标准(不变) + +| # | 标准 | 验证手段 | +|---|---|---| +| AC1 | sandbox=2026-04-20 时,admin-web 全局可见沙箱状态 | Playwright 多页面截图 | +| AC2 | sandbox=2026-04-20 时,小程序所有看板 + 子页 lastService / 月份切换均严格 ≤ 业务日 | weixin-devtools-mcp + DB 累计核对 | +| AC3 | admin_service.py 6 处 CURRENT_DATE 全部走 business_date | pytest integration | +| AC4 | apply_runtime_session_vars 在选定"统一注入点"全部生效,grep 0 处遗漏 | grep + integration test | +| AC5 | 3 套测试本地全绿,覆盖 ≥ 80% RuntimeContext 公共 API | pytest --cov | +| AC6 | P20 SPEC §6/§10/§11/§15 文字与现状一致;§10 矩阵 12 项全 ✓ | 逐项 reviewer 核对 | +| AC7 | board-finance 储值充值 132000 vs 66000 字段差异有结论 | DB 复算脚本入仓 | +| AC8 | task-list 403 manager 权限链路矛盾找到根因并修复 | curl + DB 双源核对 | +| **AC9 新增** | **A1 落地后 PreToolUse hook 阻止 PR 引回 CURRENT_DATE / date.today()** | **手动测试 hook + 审计** | + +### 1.3 工作量估算(微调) + +| 组 | 任务数 | 工作量 | 折算人天 | +|---|---|---|---| +| 架构主体收口 | 6 | 1×L + 2×M + 3×S | 1.5 | +| 走查 follow-up | 12 | 1×L + 4×M + 5×S + 2×XS | 2.0 | +| 测试 | 3 | 2×M + 1×L | 1.0 | +| 文档同步 | 4 | 4×S | 0.5 | +| **防御 hook(新增)** | **1** | **S** | **0.2** | +| **mid-wave checkpoint(新增)** | **1** | **M** | **0.3** | +| **合计** | **27** | — | **5.5 天** | + +--- + +## 二、关键背景与判断(同 v1) + +### 2.1 D2"完整且统一"解读 = (a) 不 (b) + +**(a) 在所有 DB 入口注入 GUC** ✅ 本次必做(F1-5a 仅 run_log_service 试点,不统一) +**(b) zqyy_app 新建 RLS 视图层** ❌ 不做(zqyy_app `app` 实为 fdw_etl 外表,业务读写 `biz.*` 走应用层 A 方案已覆盖,新建会形成两套保护违反"单一权威源") + +后续 Wave 2/3 业务模块新增直查 `biz.*` 的 SQL,沿用应用层 A 方案,**通过 PreToolUse hook 保障(§5.1)**。 + +### 2.2 月度聚合表 MP-2 推荐 D 双口径 + +`dws_assistant_salary_calc` 月度粒度含罚分扣减,daily 不可还原。 + +**推荐方案 D**:API 同时返回 `current_month_partial`(daily 累计到 business_date)+ `current_month_settled`(monthly 全量),前端默认显 partial 加"实时累计(不含扣减)"标。约 4h 改造。 + +### 2.3 BE-1 task-list 403 首要假设 + +**`require_permission` factory 取 site_id 不一致**(JWT vs path/query/body)。F1-5a 走查临时插桩已删,需重做。 + +### 2.4 BE-2 db-health UnicodeDecodeError + +`_get_etl_connection` 强制 `SET client_encoding TO 'UTF8'` + db-health 端点 try/except 降级。属偶发不阻塞,P2。 + +--- + +## 三、实施流程标准化(每项任务必走 5 步) + +### 3.1 五步流程(Step 4 双口径) + +每项任务实施时必须按以下 5 步,**Step 4 验证拆 live + sandbox 双口径**(关键稳健性补强): + +```text +Step 1: 调研(强制,符合 CLAUDE.md "逻辑改动前置调研") + - 用 Explore agent 找现有相关代码 + - 输出"改动前上下文摘要":模块职责 / 历史变更 / 影响范围 / 风险点 + - Neo 确认后才进 Step 2 + +Step 2: TDD 红(测试先行) + - 写 test case(若任务在测试组,本身就是 case;若在收口/follow-up,补对应 test case) + - 运行 test → 红(测试失败,因为代码还未改) + +Step 3: 实施(写代码) + - 改代码,最小改动原则 + - 运行 test → 绿 + - 重构(如有) + +Step 4: 验证(双口径) + ─ Step 4a: live 模式主验证(默认起点) + · 后端改动: curl + Python 脚本 DB 核对(主路径功能正确) + · admin-web 改动: Playwright 截图核对 + · 小程序改动: weixin-devtools-mcp 实地 + DB 核对 + · 多端: 逐端 live 验证 + + ─ Step 4b: sandbox 二次验证(强制,除非任务标"sandbox 无关") + · 切 sandbox=2026-04-20(主走查日,有数据) + · 重复 4a 同样的验证,确认 sandbox 下: + (i) 业务日上界裁剪正确 + (ii) UI 沙箱状态透出(若涉及) + (iii) 业务时钟生效(若涉及) + · 若 4b 发现 4a 未暴露的 bug → 回 Step 3 修复 → 重做 4a + 4b + · 验证完成 → 切回 live(fixture try/finally 保证) + + ─ 4b 跳过条件: 任务在 Step 1 调研时明确标"sandbox 无关", + 如纯文档同步 / 与时间无关的字段映射 / hook 注册等 + +Step 5: 审计 + - 写"改动后摘要":diff / 风险点 / 未覆盖路径 / **4a 与 4b 验证结果** + - 审计记录入 docs/audit/changes/2026-05-XX__wave1_f1_5b_.md +``` + +### 3.2 跳过条件(罕见) + +| 步骤 | 可跳条件 | +|---|---| +| Step 1 调研 | 任务标"XS 工作量"+ 文档已显式列改动行号 | +| Step 2 TDD | 文档/SPEC 类任务(组 4) | +| Step 3 实施 | 仅文档/SPEC 类任务 | +| Step 4a live 验证 | XS 文档类任务,但仍需 grep 验证 | +| **Step 4b sandbox 二次验证** | **任务在 Step 1 调研时明确标"sandbox 无关"**(纯文档/无时间字段/hook 等) | +| Step 5 审计 | **无可跳条件**,所有改动必入审计 | + +### 3.3 Step 4b 必做任务清单(预判) + +| 任务 | sandbox 必验? | 验证关注点 | +|---|---|---| +| A1 admin_service 6 处 CURRENT_DATE | **是** | dashboard 区间显示业务月而非真实今天 | +| A2 fdw_queries 异常分支兜底 | **是** | conn=None 路径降级 RuntimeContext.business_date | +| A3 apply_runtime_session_vars 统一注入 | **是** | EXPLAIN 视图 WHERE 含 ≤ business_date | +| A4 zqyy_app 永不做登记 | 否(纯文档) | — | +| A5 batch_id 命名规则 | 否(纯文档) | — | +| A6 db-health UnicodeDecodeError | 否(与 sandbox 无关) | — | +| Hook §5.1 防回归 | 否(hook 触发只与代码改动相关) | — | +| UI-1/2/4 admin-web runtime 透出 | **是** | sandbox 下徽章/列/Drawer 真显沙箱 | +| UI-3 AIDashboard sandbox 提示+分组 | **是** | sandbox 下提示条显示 + 分组数据正确 | +| UI-5 AITriggerJobs runtime 列 | **是** | sandbox 下列正确显示 | +| MP-1 board-finance 储值充值复核 | **是** | sandbox 月数据 vs DB 累计核对 | +| MP-2 board-coach 月度面板双口径 | **是** | sandbox 月 partial vs settled 字段 | +| MP-3 customer-detail lastService 裁剪 | **是** | sandbox 下 lastService 不超业务日 | +| MP-4 coach-detail data.coachId 修复 | 否(预存前端 bug,与 sandbox 无关) | — | +| MP-5 coach-service-records 接 runtime-clock | **是** | sandbox 下默认月 = 业务月 | +| BE-1 task-list 403 manager 权限 | 否(权限链路与 sandbox 无关) | — | +| BE-3 ai_run_logs runtime 回归测试 | **是**(测试本身覆盖 sandbox 路径) | runtime_mode='sandbox' 字段写入 | +| T1 unit RuntimeContext | 否(测试用 mock,不切真实 sandbox) | — | +| T2 integration ctx_snapshot | **是**(测试本身就是 sandbox/live 切换) | ctx_snapshot 不漂移 | +| T3 unit dispatcher | 否(测试用 mock) | — | +| D1-D4 文档 | 否(纯文档) | — | + +--- + +## 四、任务执行顺序 + +### 4.1 DAG 依赖图(同 v1) + +```text +T1-pre (RuntimeContext API test cases) ─┐ +A1 (admin_service CURRENT_DATE) ────────┼─> A3 (GUC 统一注入) ──┐ +A2 (fdw_queries 异常分支) ──────────────┘ │ + ├─> T2 (integration) +T1 (其他 unit) ────────────────────────────────────────────────┤ + └─> T3 (dispatcher unit) ──> A4 + +A1 完成 ──> Hook (§5.1 防回归) ──> Wave A 内继续 + +UI-1 ──> UI-2 / UI-3 (依赖 A1) / UI-4 (独立) / UI-5 (独立) +MP-1 (独立) / MP-2 (依赖 A2) / MP-3 (独立) / MP-4 (独立, 预存 bug) / MP-5 (依赖已修) +BE-1 (独立) / BE-2 (独立) / BE-3 (依赖 T2) +D1 ──> D2 ──> D3 ──> D4 (本阶段全部完成后) +``` + +### 4.2 Wave 节奏(强化 mid-wave checkpoint) + +#### Wave F1-5b-A(P0 核心,2 天) + +**目标**:架构主体收口 + 沙箱硬要求 + 关键回归测试就位 + 防御 hook 落地 + +| 顺序 | 任务 | 类型 | 工作量 | 依赖 | Commit 归属 | +|---|---|---|---|---|---| +| 1 | T1 unit RuntimeContext 公共 API | 测试 | M | 无 | C1 | +| 2 | A1 admin_service.py 6 处 CURRENT_DATE | 收口 | M | T1 部分 case | C2 | +| 3 | **§5.1 PreToolUse hook 防回归** | **新增** | **S** | A1 | C2 | +| 4 | A2 fdw_queries 异常分支兜底 | 收口 | S | T1 | C2 | +| 5 | A3 apply_runtime_session_vars 统一注入 | 收口 | L | A1+A2 | C2 | +| 6 | T2 integration estimate→confirm ctx_snapshot 不漂移 | 测试 | L | A1+A3 | C2 | +| 7 | UI-1 + UI-2 AIRunLogs runtime 列+Drawer | follow-up | S+XS | 无 | C3 | +| 8 | UI-4 全局 sandbox 徽章 | follow-up | M | 无 | C3 | +| 9 | MP-3 customer-detail lastService 裁剪 | follow-up | M | 无 | C4 | +| 10 | MP-5 coach-service-records 接入 runtime-clock | follow-up | M | 已修 | C4 | +| 11 | MP-1 board-finance 储值充值复核 | follow-up | M | 无 | C2 | +| 12 | BE-1 task-list 403 详查 | follow-up | M | 无 | C2 | + +**Wave A mid-wave checkpoint(强制)**: + +```text +1. 切 sandbox=2026-04-20 +2. 跑全套 P0 走查(Playwright 5 页 admin-web + weixin-devtools-mcp 7 页小程序) +3. 写 mid-wave 走查报告 docs/audit/changes/2026-05-XX__wave1_f1_5b_wave_a_checkpoint.md +4. Neo 复审 → 决定是否进 Wave B / 调整 Wave B 优先级 / 修复发现的新 bug +5. 切回 live +``` + +#### Wave F1-5b-B(P1/P2 增强 + 文档,3 天) + +**目标**:剩余 follow-up + 测试补全 + SPEC 同步 + +| 顺序 | 任务 | 类型 | 工作量 | 依赖 | Commit 归属 | +|---|---|---|---|---|---| +| 13 | UI-3 AIDashboard sandbox 提示条 + runtime 分组 | follow-up | M | A1 | C3 | +| 14 | UI-5 AITriggerJobs runtime 列 | follow-up | XS | 无 | C3 | +| 15 | MP-2 board-coach 月度面板双口径 | follow-up | L | A2 | C4 | +| 16 | MP-4 coach-detail data.coachId 修复 | follow-up | S | 无 | C4 | +| 17 | T3 unit dispatcher runtime | 测试 | M | A3 | C5 | +| 18 | BE-3 ai_run_logs runtime 写入回归 | follow-up | S | T2 | C5 | +| 19 | A6 db-health UnicodeDecodeError | 收口 | M | 无 | C5 | +| 20 | A4 zqyy_app 永不做登记 | 收口 | XS | A3 | C6 | +| 21 | A5 batch_id 命名规则文档化 | 收口 | XS | 无 | C6 | +| 22 | D1 P20 SPEC §6 | 文档 | S | A3+A4 | C6 | +| 23 | D2 P20 SPEC §10 | 文档 | S | 全部 follow-up | C6 | +| 24 | D3 P20 SPEC §11 | 文档 | XS | 全部任务 | C6 | +| 25 | D4 P20 SPEC §15 + audit dashboard | 文档 | S | C5 commit 后 | C6 | + +### 4.3 Commit 策略(细化为 4-6 个独立逻辑单元) + +| Commit | 主题 | 内容 | 验证 | +|---|---|---|---| +| **C1** `feat(test): F1-5b-A1 RuntimeContext unit 测试基础` | 测试基础 | T1 部分 case(为 A 任务红绿) | pytest 红(预期) | +| **C2** `fix(backend): F1-5b-A 沙箱主体收口 + 测试 + 防御 hook` | 后端架构 | A1+A2+A3 + 防御 hook + T2 + MP-1 + BE-1 | pytest 绿 + curl + Playwright + MCP | +| **C3** `feat(admin-web): F1-5b UI sandbox runtime 透出` | admin-web | UI-1/2/4 (Wave A) + UI-3/5 (Wave B) | Playwright 截图核对 | +| **C4** `fix(miniprogram): F1-5b 小程序 sandbox 上界 + 时钟接入` | 小程序 | MP-3/5 (Wave A) + MP-2/4 (Wave B) | weixin-devtools-mcp + DB 累计 | +| **C5** `test+fix(backend): F1-5b 测试增量 + db-health 编码 + retry 回归` | 测试 + 杂项 | T3 + BE-3 + A6 | pytest 全绿 | +| **C6** `docs(spec): F1-5b P20 SPEC §6/§10/§11/§15 同步 + audit` | 文档收尾 | A4+A5 + D1-D4 + audit dashboard 刷新 | grep 一致性 | + +每个 commit 跑一次 `/audit` → 生成 `docs/audit/changes/2026-05-XX__wave1_f1_5b_.md`。 + +C2 是"原 Commit 1"的精炼,C5 是"原 Commit 2 后端部分",C6 是"原 Commit 2 文档部分" — 拆细后每个 commit 独立可 review,失败可单独 revert 不影响其他。 + +--- + +## 五、防御机制(本次新增) + +### 5.1 PreToolUse hook 防回归(A1 完成立即落地) + +A1 完成后立即加 hook,防 PR 引回 `CURRENT_DATE` 或 `date.today()`: + +**位置**:`.claude/hooks/pre_check_business_date_regression.py`(参照 F2-1B `post_edit_*_reminder.py` 同类机制) + +**触发**:Edit/Write 命中 `apps/backend/app/services/**/*.py` 时 + +**逻辑**: +1. 读取目标文件 new_content +2. grep `CURRENT_DATE` / `date\.today\(\)`(排除 fallback 注释行) +3. 命中则提示:"检测到 CURRENT_DATE / date.today() 直接调用,F1-5b 决策要求走 RuntimeContext.business_date,如确需绕过请加 `# RUNTIME_CTX_BYPASS: <理由>` 注释" +4. 不强阻断(soft warning),允许 fallback 路径 + +**配套**:`.claude/settings.json` 加 hook 注册;CLAUDE.md 加"业务日上界规范"章节,引用本 hook。 + +**工作量**:S(60min) +**优先级**:P0(防回归) + +### 5.2 Lint 加 grep 校验 + +每个 commit 之前自动跑: + +```bash +# 不应出现: +grep -rn "CURRENT_DATE\|date\.today\(\)" apps/backend/app/services/ | grep -v "# RUNTIME_CTX_BYPASS" +# 期望:0 处(或仅 RUNTIME_CTX_BYPASS 注释行) +``` + +集成到 `scripts/audit/prescan.py`,作为 audit 前置检查。 + +### 5.3 测试库环境守护 + +每项 Step 4 验证前后,**强制切回 live**: + +```python +# 通用 fixture(F1-5b-T2 复用) +@pytest.fixture(autouse=True) +def reset_runtime_context_to_live(): + yield + # try/finally 保证测试失败也切回 live + requests.patch(BASE+"/api/admin/runtime-context", + json={"site_id": SITE, "mode": "live", "sandbox_date": None, + "reason": "F1-5b 测试 fixture autorestore"}, + headers=auth_header()) +``` + +--- + +## 六、进度跟踪表 + +每项任务完成后填写。Wave A 完成后 mid-wave checkpoint 复审本表。 + +**4a/4b 列说明**:`-` 任务无需 / `▢` 待做 / `✓` 通过 / `✗` 发现问题需回 Step 3。任一为 `✗` 该任务不算完成。 + +| ID | 任务 | 状态 | 完成时间 | Commit | 审计 | 4a live | 4b sandbox | 备注 | +|---|---|---|---|---|---|---|---|---| +| T1 | RuntimeContext unit 测试 | 待开始 | — | C1 | — | ▢ | - | mock 不切真实 sandbox | +| A1 | admin_service CURRENT_DATE → business_date | 待开始 | — | C2 | — | ▢ | ▢ | dashboard 显示业务月 | +| **Hook** | PreToolUse 防回归 hook | 待开始 | — | C2 | — | ▢ | - | hook 与 sandbox 无关 | +| A2 | fdw_queries 异常分支兜底 | 待开始 | — | C2 | — | ▢ | ▢ | conn=None 路径 | +| A3 | apply_runtime_session_vars 统一注入 | 待开始 | — | C2 | — | ▢ | ▢ | EXPLAIN GUC 生效 | +| T2 | integration estimate→confirm 不漂移 | 待开始 | — | C2 | — | ▢ | ▢ | 测试本身就是双模式 | +| UI-1 | AIRunLogs 列表 runtime 列 | 待开始 | — | C3 | — | ▢ | ▢ | sandbox 行 Tag 显示 | +| UI-2 | AIRunLogs 详情 Drawer | 待开始 | — | C3 | — | ▢ | ▢ | Drawer runtime 字段 | +| UI-4 | 全局 sandbox 徽章 | 待开始 | — | C3 | — | ▢ | ▢ | 顶栏徽章颜色切换 | +| MP-3 | customer-detail lastService 裁剪 | 待开始 | — | C4 | — | ▢ | ▢ | sandbox 下不显未来 | +| MP-5 | coach-service-records 接入 runtime-clock | 待开始 | — | C4 | — | ▢ | ▢ | 默认月=业务月 | +| MP-1 | board-finance 储值充值复核 | 待开始 | — | C2 | — | ▢ | ▢ | DB 累计 vs 前端 | +| BE-1 | task-list 403 manager 权限详查 | 待开始 | — | C2 | — | ▢ | - | 权限与 sandbox 无关 | +| — | **Wave A mid-wave checkpoint** | — | — | — | — | — | — | Neo 复审 | +| UI-3 | AIDashboard sandbox 提示+分组 | 待开始 | — | C3 | — | ▢ | ▢ | 双线分组显示 | +| UI-5 | AITriggerJobs runtime 列 | 待开始 | — | C3 | — | ▢ | ▢ | 列正确显示 | +| MP-2 | board-coach 月度面板双口径 | 待开始 | — | C4 | — | ▢ | ▢ | partial vs settled | +| MP-4 | coach-detail data.coachId 修复 | 待开始 | — | C4 | — | ▢ | - | 预存 bug 与 sandbox 无关 | +| T3 | dispatcher unit | 待开始 | — | C5 | — | ▢ | - | mock | +| BE-3 | ai_run_logs runtime 回归 | 待开始 | — | C5 | — | ▢ | ▢ | runtime 字段写入 | +| A6 | db-health UnicodeDecodeError | 待开始 | — | C5 | — | ▢ | - | 与 sandbox 无关 | +| A4 | zqyy_app RLS 永不做登记 | 待开始 | — | C6 | — | ▢ | - | 纯文档 | +| A5 | batch_id 命名规则文档化 | 待开始 | — | C6 | — | ▢ | - | 纯文档 | +| D1 | SPEC §6 GUC 路线 | 待开始 | — | C6 | — | ▢ | - | 纯文档 | +| D2 | SPEC §10 矩阵 | 待开始 | — | C6 | — | ▢ | - | 纯文档 | +| D3 | SPEC §11 已知遗漏 | 待开始 | — | C6 | — | ▢ | - | 纯文档 | +| D4 | SPEC §15 + audit dashboard | 待开始 | — | C6 | — | ▢ | - | 纯文档 | + +--- + +## 七、Neo 决策点(D7-D10) + +启动 Wave A 前,Neo 拍板以下 4 项决策。同 D1-D6 模式,在每项下用 *斜体* 写反馈。 + +### D7 Commit 拆分粒度 + +v1 推荐 2 个 commit。规范化版改 4-6 个独立逻辑单元(C1-C6)。 + +| 选项 | 优势 | 劣势 | +|---|---|---| +| **a 4-6 个独立 commit(规范版推荐)** | 每个 commit 可独立 review/revert / 历史清晰 | commit 数量多 | +| b 沿用 v1 的 2 个 commit | 数量少 | 一个 commit 含混合主题 | +| c 折中 3 个 commit(测试 / 后端架构 / 前端+文档) | 平衡 | 仍混合后端架构 + UI 改动 | + +**Claude 推荐 a**。 + +**Neo 反馈**: + +*(待填)* + +--- + +### D8 PreToolUse hook 防回归是否落地 + +§5.1 描述。 + +| 选项 | 评估 | +|---|---| +| **a 落地(规范版推荐)** | A1 完成立即加 hook,**长期防御**未来 PR 引回 CURRENT_DATE | +| b 不落地 | 仅靠 grep 在 commit 前手动检查 | + +**Claude 推荐 a**。F2-1B 已验证 hook 防御机制有效,本次类似场景。 + +**Neo 反馈**: + +*(待填)* + +--- + +### D9 Wave A mid-wave checkpoint 是否做 + +§4.2 Wave A 末加 mid-wave checkpoint(切 sandbox + 全套 P0 走查 + Neo 复审 + 切回 live)。 + +| 选项 | 评估 | +|---|---| +| **a 做(规范版推荐)** | Wave B 启动前确认 Wave A 闭环,降低 B 阶段集成 bug 风险 | +| b 不做 | A B 连续做,集成失败时回退面更大 | + +**Claude 推荐 a**。F1-5a 走查的教训证明"假定全过其实没过"风险高(走查后才发现 5 个 bug)。 + +**Neo 反馈**: + +*(待填)* + +--- + +### D10 MP-2 月度聚合表方案选型 + +§2.2 推荐 D 双口径,但破坏性较强(API 字段变 + 前端展示变)。 + +| 选项 | 评估 | +|---|---| +| **a 方案 D 双口径(推荐)** | 业务一致 + 不丢扣减信息 + 不需新 ETL 表;~4h | +| b 方案 C 接受月度全量 + 前端标注 | 简单透明;但破坏"≤ 业务日"严格性,不符 AC2 | +| c 方案 A daily 累计实时计算 | 业务正确,失去 salary_calc 扣减算法 | +| d 方案 B monthly snapshot + daily 增量 | 实施量大(ETL+DWS+DDL),F1-5b 内做不完 | + +**Claude 推荐 a**。 + +**Neo 反馈**: + +*(待填)* + +--- + +## 八、失败决策矩阵(本次新增) + +每项任务实施中遇到问题时,按严重度自主或升级处理。 + +### 8.1 严重度阈值 + +| 严重度 | 描述 | 处理 | +|---|---|---| +| **L0 自主修复** | 单元测试失败 / lint 警告 / 文档拼错 | Claude 自主修,不打断 Neo | +| **L1 自主回退** | 单 task 出错且不影响其他 task / Step 4 验证发现局部回归 | Claude `git revert` 单 task → 写回退报告 → 跳过该 task 进下一项 | +| **L2 升级 Neo** | 数据 bug 影响生产财务 / 跨组依赖断裂 / 走查发现 ≥ 2 个生产 bug | **立即停止 Wave** → 写阻塞报告 → 等 Neo 决定 | +| **L3 紧急停 Wave** | 全 PG 库不可达 / 测试库被污染影响其他正在跑的测试 / commit 已 push 后发现关键 bug | **立即停止全部任务** → 报警 → 等 Neo 决定回滚或修补 | + +### 8.2 阶段级回退 + +| 场景 | 严重度 | 回退路径 | +|---|---|---| +| Wave A 完成后 mid-wave checkpoint 发现 ≥ 1 个 P0 bug | L2 | 当 commit revert + 原任务回 Wave A 重做 | +| Wave A 完成后 checkpoint 发现 sandbox 仍漏裁剪 | L2 | revert C2 → 加补丁 task → 重过 A3 | +| Wave B 完成后 SPEC 与代码不一致 | L0 | 单独修 D1-D4 文字,不动代码 | +| 整个 F1-5b 失败 | L3 | revert C1-C6 → 回到 F1-5a 完整状态(`421e193+1baa212+a045625`) | + +### 8.3 单项风险表(细化 v1) + +| 任务 | 风险 | 严重度 | 回退路径 | 升级阈值 | +|---|---|---|---|---| +| A1 | live 模式 RuntimeContext 失效 → 显示旧日期 | L1 | 三层 fallback:RuntimeContext → date.today() → warning | RuntimeContext 完全不可用 → L2 | +| A2 | conn=None + RuntimeContext 同时失效 | L1 | fallback 真实 today + warning | 同上 | +| A3 | GUC 注入与 `_fdw_context` SET LOCAL 重复 | L1 | grep `_fdw_context` 之前已 SET 跳过 | 性能 ≥ 5ms / 调用 → L2 | +| A6 | SET client_encoding 影响其他 ETL 连接 | L1 | feature flag `ETL_FORCE_UTF8` | ETL 全部断 → L3 | +| **Hook §5.1** | **hook 误伤 fallback 路径** | **L1** | **soft warning,允许 RUNTIME_CTX_BYPASS 注释 bypass** | **正常代码 ≥ 30% 命中误报 → L2 调整 grep 规则** | +| UI-3 | 分组显示破坏图表 | L1 | feature flag `VITE_AI_RUNTIME_GROUPING` | manager 反馈强 → L2 回退视图 | +| UI-4 | 顶栏徽章 API 500 整站破 | L1 | ErrorBoundary 包裹 | 整站破 → L3 | +| MP-1 | 修后影响生产财务数据 | **H/L2** | **修前后对照表给 Neo 确认** | **任何指标变负 / 误差 > 5% → L2** | +| MP-2 | 双口径 UX 不直观 | L1 | feature flag `enable_partial_month` | 反馈强 → L2 | +| MP-3 | 月末闭区间 `<=` vs `<` 边界 | L0 | boundary case test 守护 | — | +| MP-4 | setData 时机修复连带 | L1 | 仅 line 247-251 加补强 | 影响其他 page.data → L2 | +| MP-5 | localFallback 仍走真实 today | L1 | localFallback 改读 RuntimeContext sandbox_date 缓存 | sandbox 看到 5 月 → L2 | +| BE-1 | 插桩日志噪音 | L0 | logger.warning 加 [F1-5b-BE-1 DIAG] 前缀,排查完删除 | — | +| T2 | fixture 不切回 live 污染下次 | L1 | autouse fixture 强制切回 | 测试库被污染影响生产模拟 → L3 | + +--- + +## 九、关联 + +- F1-5a 主体审计:[`docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md`](../../audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md) +- F1-5a 走查报告:[`docs/audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md`](../../audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md) +- F1-5 实施决策卡:[`F1-5-impl-decisions.md`](F1-5-impl-decisions.md) +- P20 SPEC:[`docs/prd/specs/P20-runtime-context-sandbox.md`](../../prd/specs/P20-runtime-context-sandbox.md) +- W1-T7 PRD 批 1:[`docs/_overview/admin-api-prd/batch1-runtime-context-and-ai.md`](../admin-api-prd/batch1-runtime-context-and-ai.md) +- 测试库 site 清单:`memory/project_test_db_sites.md`(只有 `2790685415443269` 有数据) +- F2-1B 防御 hook 经验:`.claude/hooks/post_edit_openapi_reminder.py` / `.claude/hooks/post_edit_prompt_sync_reminder.py` +- Reload 卡死预防:`scripts/ops/start-admin.ps1` / `apps/backend/start_uvicorn.py` / `scripts/ops/backend-watchdog.ps1` + +--- + +## 十、启动指令 + +Neo 复核 §7 D7-D10 后,在每项决策下填 *斜体* 反馈,然后回复"启动 Wave A"或"调整后启动",Claude 即按 §4.2 + §3 标准 5 步流程执行。 + +每项任务完成后: +1. 更新 §6 进度表(状态 / 完成时间 / commit) +2. 写审计记录(§3 Step 5) +3. 跑 §5.2 lint grep 校验(必过) +4. 进下一项 + +Wave A 末做 §4.2 mid-wave checkpoint,等 Neo 复审。 diff --git a/docs/audit/changes/2026-05-05__wave1_f1_5b_wave_a_partial.md b/docs/audit/changes/2026-05-05__wave1_f1_5b_wave_a_partial.md new file mode 100644 index 0000000..f42e7ea --- /dev/null +++ b/docs/audit/changes/2026-05-05__wave1_f1_5b_wave_a_partial.md @@ -0,0 +1,120 @@ +# 2026-05-05 — Wave 1 F1-5b Wave A 中段(T1+A1+A2+A3+Hook) + +## 摘要 + +F1-5b Wave A 完成 5 项核心任务(测试基础 + 后端架构主体 + 防御 hook),按 §3 5 步流程 + §3.1 4a/4b 双口径验证执行。 + +| ID | 任务 | 4a live | 4b sandbox | Commit | +|---|---|---|---|---| +| T1 | RuntimeContext unit 测试(36 case) | PASS(36/36) | - 跳过 mock | 不入仓(.gitignore:71) | +| A1 | admin_service.py 4 处 CURRENT_DATE → business_date(上下界双全) | PASS | PASS(trend_7d 严格 ≤ 4-20) | C2 | +| A2 | fdw_queries.py 异常分支兜底 + 三层 fallback + warning | PASS(后端 200) | PASS(随 A1 4b) | C2 | +| A3 | _fdw_context docstring 显式登记唯一 ETL 入口 + GUC 三键对齐说明 | - 文档 | - | C2 | +| Hook | post_edit_business_date_check.py + settings.json 注册 | - 防御 hook | - | C2 | + +## 关联 + +- F1-5b 任务清单:`docs/_overview/wave1-findings/F1-5b-tasks.md` +- F1-5a 走查报告:`docs/audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md` +- F1-5 决策卡:`docs/_overview/wave1-findings/F1-5-impl-decisions.md`(D7-D10 已 Neo 同意) +- 测试库 site:2790685415443269(唯一有数据) + +## A1 详情 + +### 改动范围(4 处,文档原估"6 处"含全局聚合,经 Neo 确认选 A 保留) + +| 行 | 函数 | 改造 | +|---|---|---| +| L69 docstring | _get_range_stats | "CURRENT_DATE - (N-1) days" 改为 "business_date - (N-1) days" | +| L106-108 | _get_range_stats | site_id 非 None 用 `as_runtime_today_param(site_id)` + 闭区间裁剪 | +| L150-160 | _get_7d_trend | 同上 + **下界 + 上界双全**(< business_date + 1day,F1-5b 走查时 4b 暴露原 PR 上界缺失) | +| L185-195 | _get_app_distribution | 同上 + 下界 + 上界双全 | + +### 全局聚合(无 site_id)分支保留 CURRENT_DATE + +`list_trigger_jobs` (L324-325) / `get_budget` (L575-588) 不在改造范围: +- 全局聚合按真实今天合理(测试库唯一 site,生产多 site 时全局无单一业务日) +- F1-5a 走查 P10 AIDashboard "今日调用 0" 即此路径 + +### Step 4b 双口径验证(关键稳健性) + +第一轮实施时只加下界,sandbox=2026-04-20 时 trend_7d 仍返回 4-21~5-01 数据(漏未来)。**Step 4b 立即暴露**,补加上界后: +- live: trend_7d 2 条 4-30 ~ 5-01(真实今天数据) +- sandbox=4-20: trend_7d 1 条仅 4-20(`4-14 ~ 4-20` 区间内仅 4-20 当天有数据) + +## A2 详情 + +### 改动 1:fdw_queries.py:103-120 _fdw_context + +之前: +- conn=None 时直接 `bd_str = _date.today()`,**不尝试 RuntimeContext** +- exception 时 fallback 真实 today 但**无 warning** + +现在: +- conn=None 也调 `get_runtime_context(site_id)`(内部自开 conn),sandbox 业务日生效 +- exception 时 fallback + `logger.warning("RuntimeContext 不可用...")` + +### 改动 2:fdw_queries.py:2549-2562 _get_weekly_visits_batch + +之前: +- ref_date=None 静默降级 _date.today() + +现在: +- 加 `logger.warning` 提示调用方应从 RuntimeContext.business_date 显式传入 + +## A3 详情 + +未改实现(F1-5a 已就位 _fdw_context 的 SET LOCAL GUC),仅补 docstring: + +```python +""" +F1-5b A3 架构契约(D2"完整且统一"): +本函数是 backend 访问 ETL 库 (etl_feiqiu) 的唯一入口, +任何 ETL 视图查询都必须通过 `with _fdw_context(...) as cur` 形式调用。 +内部 SET 三个 GUC: +- app.current_site_id (RLS 多门店隔离) +- app.current_business_date (sandbox 业务日上界,激活 ETL 库 26 个 v_* 视图裁剪) +- app.current_runtime_mode (live/sandbox 标识) +禁止绕过 `_fdw_context` 直接 `_get_etl_connection` + cur.execute, +否则 sandbox 业务日上界在该路径不生效。 +""" +``` + +D2 主诉"完整且统一"达成:架构层面所有 ETL 库访问统一通过 `_fdw_context`,grep 验证全仓库 `_get_etl_connection` 仅 fdw_queries.py 内部使用(0 处绕过)。 + +## Hook 详情 + +`post_edit_business_date_check.py` 防御: +- 触发:Edit/Write 命中 `apps/backend/app/services/**/*.py` +- 检查 new_string/content 中是否含 `CURRENT_DATE` / `date.today()`(排除注释行 + RUNTIME_CTX_BYPASS 标注) +- soft warning 而非阻断,允许 fallback 路径 + +防御目标:Wave 2 后续 PR 引回 CURRENT_DATE 时立即提醒。 + +## 风险与未完覆盖 + +### 已覆盖 + +- A1 sandbox 上下界双全 ✓ +- A2 三层 fallback + warning ✓ +- A3 文档化架构契约 ✓ +- T1 36 个测试 case 守住公共 API ✓ + +### 未完(Wave A 剩余) + +- T2 integration estimate→confirm ctx_snapshot 不漂移(L 工作量,本批未做) +- UI-1/2/4 admin-web sandbox 透出 +- MP-3/5 小程序 sandbox 上界 +- MP-1 board-finance 储值充值后端字段复核 +- BE-1 task-list 403 manager 权限链路详查 + +将在下一批 commit 中继续。 + +## Step 5 审计完成确认 + +- [x] grep 验证 admin_service.py 4 处 CURRENT_DATE 改造区分 site_id None vs 非 None +- [x] pytest test_runtime_context.py 36/36 全过 +- [x] curl /api/admin/ai/dashboard 4a live + 4b sandbox 双口径 PASS +- [x] grep 全仓库 `_get_etl_connection` 仅 fdw_queries.py 内部使用(无绕过) +- [x] hook 注册 settings.json 已加新 entry +- [x] 切回 live(测试库归位 business_date=2026-05-05 / mode=live)