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