feat(ai): F1-5b Wave A 中段 沙箱业务日全栈架构主体收口 (W1)

完成 F1-5b 任务:
- T1 RuntimeContext unit 测试基础(36 case PASS,本地不入仓走 .gitignore:71)
- A1 admin_service.py 4 处 CURRENT_DATE → business_date 改造
  - _get_range_stats / _get_7d_trend / _get_app_distribution
  - 上下界双全(下界 - 6 days + 上界 < + 1 day,Step 4b 暴露原 PR
    上界缺失,sandbox=4-20 时 trend_7d 漏 4-21~5-01 数据 → 修补)
  - 全局聚合 list_trigger_jobs / get_budget 保留 CURRENT_DATE
    (Neo D 决策选 A: 多 site 时全局无单一业务日)
- A2 fdw_queries:113 / 2552 异常分支兜底 + 三层 fallback + warning
  - conn=None 也尝试 get_runtime_context(自开 conn)
  - RuntimeContext 不可用降级真实 today + logger.warning
- A3 _fdw_context docstring 显式登记唯一 ETL 入口架构契约
  (D2 完整且统一: 所有 ETL 视图查询通过 _fdw_context 自动 SET 三个
   GUC: site_id / business_date / runtime_mode)
- 防御 hook post_edit_business_date_check.py
  Wave 2 后续 PR 引回 CURRENT_DATE / date.today() 即提醒

双口径验证(§3.1 4a + 4b):
- 4a live: dashboard trend_7d 2 条 4-30~5-01 (真实今天)
- 4b sandbox=2026-04-20: trend_7d 1 条仅 4-20 (业务日上界生效硬证据)
- pytest test_runtime_context 36/36 全过

未完(下一批 Wave A): T2 integration / UI-1/2/4 / MP-3/5 / MP-1 / BE-1
F1-5b-tasks.md 新增 + audit 记录已就位

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-05 15:01:51 +08:00
parent a045625d48
commit af02446740
6 changed files with 780 additions and 13 deletions

View File

@@ -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),
}
}))

View File

@@ -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": [

View File

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

View File

@@ -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)
# 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
else:
from datetime import date as _date
bd_str = _date.today().isoformat()
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()

View File

@@ -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_<slug>.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_<slug>.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 复审。

View File

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