Files
Neo-ZQYY/docs/prd/specs/P20-runtime-context-sandbox.md
Neo 5d4da0ae8c docs(spec): F1-5b D1-D4 P20 SPEC 同步 + audit dashboard 刷新 (W1)
F1-5b 大量代码改动落地后,同步 P20 SPEC 反映现状,防止文档与代码偏差。

D1 §6 与 ETL 影子衔接:
- 新增 §6.1 "F1-5b 收益":app 视图业务日上界裁剪范围 + 后端读取层
  + 写入层 sandbox 隔离 + 业务架构边界(zqyy_app 永不建 RLS) + 跨连接器扩展性

D2 §10 跨模块覆盖矩阵更新:
- §10.1 后端服务层:5 个 ? 项核实更新为 X 或 —,各项补 commit 引用
  * task_generator / coach_service / customer_service / dispatcher / admin_service
- §10.2 AI 提示词:app8_consolidate ? 标"F1-5b 范围外,Wave 2 / F1-6 audit"
- §10.3 小程序页面:新增 coach-service-records 行(MP-5);board-* 系列
  ? → —(后端走 board_service);customer-detail 备注补 MP-3 + MP-4

D3 §11 已知遗漏:
- §11.1 设计共识:新增 zqyy_app 永不建 RLS(A4) + batch_id 命名规约(A5)
- §11.2 已知 hack:补 F1-5b T3 间接覆盖说明
- 新增 §11.3 F1-5b 已收口的 11 项遗留 hack ✓
- 新增 §11.4 推迟到 F1-6 沙箱时光机阶段 B 的 4 项 
- 新增 §11.5 推迟到 F1-7+ 阶段 C 的 3 项 

D4 §15 变更记录 + §15.1 收益总结 + §12 任务清单:
- §15 新增 4 行(F1-5a 走查 / F1-5b Wave A / Wave B / 沙箱时光机 spec)
- 新增 §15.1 F1-5b 收益总结:7 大类已落地 + 业务价值 + 未落地指引
- §12 任务清单:T11/T12/T13 F1-5b 三批次摘要 + T18/T19 F1-6/F1-7+ 排期
- audit dashboard 自动刷新(scripts/audit/gen_audit_dashboard.py)
  扫描 165 条审计记录(含本次 F1-5b 全部 commit)

无代码改动,纯文档同步。F1-6 启动可直接引用 sandbox-replay-engine-spec
+ P20 SPEC §11.4/§11.5 排期登记。

审计:docs/audit/changes/2026-05-05__wave1_f1_5b_d1234_spec_sync.md

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

672 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P20Runtime Context 沙箱 — 虚拟时间机制
> 版本v1.0 草案 | 日期2026-05-04 | 来源:基于 2026-05-01 / 2026-05-02 落地代码与审计反推
> SPEC slug`runtime-context-sandbox`
> 状态:**追溯型 SPEC**(功能已实现并通过单环境 e2e 验证;本文档将"已发生的事实"沉淀为权威规约,**深入测试与跨模块收口未完成**,详见同目录 `docs/_overview/04a-feedback/P0-7-runtime-context-todos.md`
---
## 1. 需求概述
### 1.1 问题背景
NeoZQYY 平台与真实门店运营强耦合在球房停业期、AI 演示、新功能演练、跨年度回放等场景下,必须在**不污染真实数据**的前提下,让全栈系统假装"今天是某个历史日期"。
直接修改服务器系统时钟或在每个调用点单独传日期参数,存在两个根本风险:
- **可见性失控**:业务读取仍然命中"sandbox_date 之后"的真实最新数据,让演示与真实数据混合,沙箱失去意义。
- **写入污染**演示中产生的任务、AI 缓存、运行日志会与真实运营数据共存且无法区分,回收成本极高。
### 1.2 解决方案
引入一套**按门店隔离**的「业务运行上下文RuntimeContext
- 数据库层新增 `biz.site_runtime_context` 单门店一行的状态表,可在 `live``sandbox` 之间切换。
- 7 张业务/AI 表新增 `runtime_mode` + `sandbox_instance_id` 两列做**写入隔离**;唯一索引扩入这两列,允许 live 与 sandbox 共存。
- 后端封装 `RuntimeContext` 抽象,所有业务时钟读取统一通过 `as_runtime_today_param` / `as_runtime_now_param` 等 helper禁止 `date.today()` / `NOW()` 直接出现在业务窗口语义中。
- ETL 库通过会话级 GUC `app.current_business_date` + `app.business_date_now()` 函数 + 39 个 RLS 视图业务日上界,实现**读取层不看未来**。
- admin-web 提供超级管理员可见的切换页面;小程序通过 `/api/xcx/runtime/clock` 获取业务时钟,停止在前端用 `new Date()` 推算业务年月。
### 1.3 沙箱不影响项(设计共识)
下列内容**始终按真实系统时间运行**,不被 sandbox 切换影响:
- 写入时间戳(`created_at` / `updated_at` / `finished_at`
- 调度元数据(`scheduled_tasks.next_run_at` / `last_run_at`
- AI tokens 计费、限流、熔断窗口
- AI 调用真实记录(`biz.ai_run_logs.created_at`
- 缓存 TTL`biz.ai_cache.expires_at`
- 全局触发器调度(`biz.trigger_jobs``site_id` 列,多门店共用)
- AIDashboard 运维监控指标("今日调用 / 7 天趋势"按真实日期)
---
## 2. 关键交付物
| # | 交付物 | 状态 | 路径 |
|---|---|---|---|
| 1 | 数据库迁移:`biz.site_runtime_context` 表 + 7 表加列 + 索引重构 | 已上 test 库 | `db/zqyy_app/migrations/20260501__runtime_context_sandbox.sql` |
| 2 | ETL 库迁移:`app.business_date_now()` + 39 个 RLS 视图业务日上界 | 已上 test 库 | `db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql` |
| 3 | 后端服务层 `RuntimeContext` + helper | 已合并 | `apps/backend/app/services/runtime_context.py` |
| 4 | 后端管理 API4 个端点) | 已合并 | `apps/backend/app/routers/admin_runtime_context.py` |
| 5 | 后端小程序 API1 个端点) | 已合并 | `apps/backend/app/routers/xcx_runtime_clock.py` |
| 6 | admin-web 切换页面 | 已合并 | `apps/admin-web/src/pages/RuntimeContext.tsx` |
| 7 | 小程序业务时钟工具 | 已合并 | `apps/miniprogram/miniprogram/utils/runtime-clock.ts` |
| 8 | RLS 迁移生成器 | 已合并 | `scripts/ops/gen_rls_business_date_migration.py` |
| 9 | 端到端验证脚本 | 已合并 | `tools/db/verify_sandbox_end_to_end.py``tools/db/verify_admin_web_sandbox.py` |
| 10 | BD_Manual + 一系列变更说明 | 已合并 | `docs/database/BD_Manual_runtime_context_sandbox.md``docs/database/changes/2026-05-01__*``2026-05-02__*` |
| 11 | 生产库执行 | **待执行** | 待运维窗口 |
| 12 | 跨模块完整测试与收口 | **待执行** | 见 `docs/_overview/04a-feedback/P0-7-runtime-context-todos.md` |
---
## 3. 数据模型
### 3.1 `biz.site_runtime_context`(核心状态表)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `site_id` | bigint | PKFK → `biz.sites(site_id)` | 门店 ID |
| `mode` | varchar(20) | NOT NULLDEFAULT `'live'`CHECK `IN ('live','sandbox')` | 运行模式 |
| `sandbox_date` | date | 可空 | sandbox 模式下系统假设的业务日期 |
| `sandbox_instance_id` | varchar(64) | 可空 | sandbox 写入隔离实例 ID格式 `sbx_<24hex>` |
| `ai_mode` | varchar(20) | NOT NULLDEFAULT `'live'`CHECK `IN ('live')` | AI 调用模式(当前仅 live |
| `status` | varchar(20) | NOT NULLDEFAULT `'active'` | 上下文状态 |
| `reason` | text | 可空 | 切换原因(运维/演示备注) |
| `updated_by` | bigint | 可空 | 最近一次切换的 admin user_id |
| `created_at` | timestamptz | NOT NULLDEFAULT now() | 创建时间 |
| `updated_at` | timestamptz | NOT NULLDEFAULT now() | 更新时间API 切换时显式 NOW() |
复合 CHECK`site_runtime_context_sandbox_check`
- `mode='live'``sandbox_date IS NULL``sandbox_instance_id IS NULL`
- `mode='sandbox'``sandbox_date IS NOT NULL``sandbox_instance_id IS NOT NULL`
### 3.2 7 张表新增 runtime 维度列
下列表均新增两列DEFAULT 与 NOT NULL 一致,便于唯一索引覆盖两种模式:
| Schema.Table | 主要用途 |
|---|---|
| `biz.coach_tasks` | 助教任务(召回/回访/关系建设) |
| `biz.coach_task_transfer_log` | 任务转移日志 |
| `biz.coach_task_history` | 任务历史归档 |
| `biz.recall_events` | 消费引发的召回事件 |
| `biz.ai_cache` | AI 应用缓存 |
| `biz.ai_run_logs` | AI 调用明细 |
| `biz.ai_trigger_jobs` | AI 调度记录 |
| 列 | 类型 | DEFAULT | NULL | 说明 |
|---|---|---|---|---|
| `runtime_mode` | varchar(20) | `'live'` | NOT NULL | 写入时所处模式(`live` / `sandbox` |
| `sandbox_instance_id` | varchar(64) | `'live'` | NOT NULL | sandbox 实例 IDlive 模式占位 `'live'` |
### 3.3 索引变更
DROP
| Index | 原唯一性 |
|---|---|
| `biz.idx_coach_tasks_site_assistant_member_type` | UNIQUE |
| `biz.idx_recall_events_site_assistant_member_day` | UNIQUE |
CREATE
| Index | 类型 | 说明 |
|---|---|---|
| `idx_coach_tasks_runtime_unique_active` | UNIQUE部分 `WHERE status='active'` | 唯一键加入 `runtime_mode + sandbox_instance_id`,允许 live 与 sandbox 同时存在同一 (site/assistant/member/task_type) 活跃任务 |
| `idx_recall_events_runtime_site_assistant_member_day` | UNIQUE | 召回事件按当日去重,加入 runtime 维度 |
| `idx_coach_tasks_runtime_assistant_status` | INDEX | 任务列表查询 |
| `idx_ai_cache_runtime_lookup` | INDEX | AI cache 按 `cache_type + site + runtime + target` 查询 |
| `idx_ai_trigger_jobs_runtime_site` | INDEX | AI 触发记录按 `site + runtime + event_type + status` 查询 |
### 3.4 ETL 库会话级 GUC + helper 函数
| 名称 | 类型 | 说明 |
|---|---|---|
| `app.current_business_date` | session GUC字符串 | 业务日 ISO 字符串;未设置时回退 `CURRENT_DATE` |
| `app.current_runtime_mode` | session GUC字符串 | 当前运行模式 `live` / `sandbox` |
| `app.business_date_now()` | STABLE SQL 函数 | 返回 `app.current_business_date` 解析的 date`CURRENT_DATE` |
### 3.5 RLS 视图业务日上界39 个视图)
模式:`<日期列> <= app.business_date_now()`(部分用 `COALESCE(... ::date, '0001-01-01'::date)` 兜底)。
涉及视图分组(详细列表见 `scripts/ops/gen_rls_business_date_migration.py``VIEWS_WITH_BD`
- 财务事实 6 个:`v_dws_finance_area_daily / daily_summary / discount_detail / expense_summary / income_structure / recharge_summary`
- 助教/任务 5 个:`v_assistant_daily / v_dws_assistant_daily_detail / monthly_summary / salary_calc / finance_analysis`
- 客户事实 3 个:`v_dws_member_consumption_summary / visit_detail / winback_index`
- DWD 事实 5 个:`v_dwd_settlement_head / assistant_service_log / recharge_order / store_goods_sale / table_fee_log`
- SCD2 配置 4 个:`v_cfg_assistant_level_price / performance_tier / bonus_rules / index_parameters``effective_from``effective_to` 双向夹住)
- 其他 DWS 16 个:`v_dws_assistant_customer_stats / order_contribution / project_tag / recharge_commission`
跳过项(明确不加上界):
- `v_dim_assistant / v_dim_member / v_dim_member_card_account / v_dim_staff / v_dim_staff_ex / v_dim_table` —— SCD2 维度保留 `scd2_is_current=1` 当前快照语义。
- `v_assistant / v_member / v_site / v_cfg_area_category` —— 无业务日列。
---
## 4. 接口契约
### 4.1 管理面admin-web 调用)
#### 4.1.1 `GET /api/admin/runtime-context/sites`
| 项 | 内容 |
|---|---|
| 权限 | `super_admin` |
| 说明 | 列出所有门店及其当前运行上下文(即使未配置也返回 site 行,运行字段为 NULL |
| 响应 | `RuntimeSiteItem[]` |
`RuntimeSiteItem``{ site_id, site_name, site_code, is_active, mode, sandbox_date, sandbox_instance_id, ai_mode, status, updated_at }`
#### 4.1.2 `GET /api/admin/runtime-context?site_id=<id>`
| 项 | 内容 |
|---|---|
| 权限 | `super_admin` |
| 说明 | 按 site_id 查询当前运行上下文(含 `business_date` / `business_now` 计算结果) |
| 响应 | `RuntimeContextResponse` |
#### 4.1.3 `PATCH /api/admin/runtime-context`
| 项 | 内容 |
|---|---|
| 权限 | `super_admin` |
| 说明 | 切换 live / sandbox。**先 stop runtime activity再 upsert 上下文** |
| 请求 | `RuntimeSwitchRequest = { site_id, mode, sandbox_date?, reset_sandbox?, reason? }` |
| 响应 | `RuntimeSwitchResponse = { context, steps[] }` |
校验:
- `mode='sandbox'``sandbox_date IS NULL` → 422
- `mode='live'``sandbox_date IS NOT NULL` → 422
- `sandbox_date > date.today()` → 422沙箱只允许回放历史禁止"未来"
切换前置动作(写入 `steps[]`
1. `cancel_etl_processes` — 终止当前进程内 ETL 执行(`task_executor.cancel`
2. `cancel_task_queue` — 取消当前门店 pending/running 的 `task_queue` 行(`status='cancelled'`
3. `cancel_ai_runtime` — 取消当前进程内属于该门店的 AI 异步调用链(`get_dispatcher().cancel_running`
4. `cancel_ai_jobs` — 标记该门店 pending/running 的 `biz.ai_trigger_jobs``cancelled`
5. `biz_triggers_unchanged``biz.trigger_jobs` 是全局调度表,不暂停(设计有意保留)
6. `apply_context` — UPSERT `biz.site_runtime_context`
`sandbox_instance_id` 处理:
- 进入 sandbox 且 `reset_sandbox=true`(默认)→ 调用 `new_sandbox_instance_id()` 生成 `sbx_<24hex>`
- 进入 sandbox 且 `reset_sandbox=false` → 沿用原 `sandbox_instance_id`
- 切回 live → 显式置 NULL
### 4.2 通用面
#### 4.2.1 `GET /api/config/runtime-context`
| 项 | 内容 |
|---|---|
| 权限 | 任意已登录用户(不要求 super_admin |
| 说明 | 返回当前用户门店的 RuntimeContext |
| 响应 | `RuntimeContextResponse` |
### 4.3 小程序面
#### 4.3.1 `GET /api/xcx/runtime/clock`
| 项 | 内容 |
|---|---|
| 权限 | `require_approved`(已审核通过的小程序用户) |
| 说明 | 返回当前用户门店的业务时钟。小程序所有"今天""本月"判断必须走此接口 |
| 响应 | `{ mode, business_date, business_year, business_month, business_year_month, business_now, is_sandbox, sandbox_date, sandbox_instance_id }` |
`RuntimeContextResponse``apps/backend/app/schemas/runtime_context.py`
```python
{
"site_id": int,
"mode": "live" | "sandbox",
"business_day_start_hour": int,
"business_date": "YYYY-MM-DD",
"business_now": "ISO 8601",
"sandbox_date": "YYYY-MM-DD" | null,
"sandbox_instance_id": "sbx_..." | null,
"ai_mode": "live",
"status": "active",
"is_sandbox": bool
}
```
---
## 5. 各端读取约定
### 5.1 后端服务层
`apps/backend/app/services/runtime_context.py` 提供如下 helper**所有业务窗口语义必须走 helper禁止 `date.today()` / `datetime.now()` / `CURRENT_DATE` / `NOW()` 直出**
| Helper | 返回 | 用途 |
|---|---|---|
| `get_runtime_context(site_id, conn=None)` | `RuntimeContext` 实例 | 取上下文(异常时降级 live |
| `as_runtime_now_param(site_id, conn=None)` | `datetime` | SQL 中的"业务当前时间"参数 |
| `as_runtime_today_param(site_id, conn=None)` | `date` | SQL 中的"业务今天"参数 |
| `as_runtime_year_month_param(site_id, conn=None)` | `'YYYY-MM'` | 月度报表 |
| `as_runtime_business_now_str(site_id, fmt='%Y-%m-%d %H:%M:%S')` | `str` | AI prompt `current_time` 字段 |
| `task_runtime_filter(site_id, alias='')` | `(sql_clause, params)` | 业务表查询过滤live 用 COALESCE 兜历史 NULL |
| `runtime_insert_columns(site_id, conn=None)` | `(cols, placeholders, values)` | 业务表 INSERT 片段 |
| `runtime_update_assignments(site_id, conn=None)` | `(set_clause, params)` | 业务表 UPDATE 片段 |
| `namespace_ai_target_id(site_id, target_id)` | `str` | sandbox 模式下给 `ai_cache.target_id``sbx_*:` 前缀 |
| `business_date_upper_bound_sql(site_id, column, alias='', cast=None)` | `(sql_clause, params)` | 给查询补 `column <= business_date` 上界live 返回空) |
| `apply_runtime_session_vars(conn, ctx=None, site_id=None)` | None | 在已有连接上 `set_config('app.current_business_date', ...)` + `app.current_runtime_mode` |
#### 已接入 helper 的服务(截至 2026-05-02
- 任务引擎:`task_manager.py` / `task_generator.py` / `task_expiry.py` / `recall_detector.py`
- 看板:`board_service.py` / `coach_service.py` / `customer_service.py` / `chat_service.py` / `performance_service.py`
- ETL FDW`fdw_queries.py``_fdw_context``SET LOCAL app.current_site_id` 之后下发 `app.current_business_date` + `app.current_runtime_mode`
- AI 数据:`ai/data_fetchers/member_data.py` / `assistant_data.py` / `page_context.py`
- AI 缓存:`ai/cache_service.py`(命名空间隔离)
- AI 日志:`ai/run_log_service.py`(写入时带 `runtime_mode + sandbox_instance_id`
- AI prompts`app2_finance_prompt.py` / `app2a_finance_area_prompt.py` / `app3-7` `current_time` 字段
- 路由:`tenant_users.py`SCD2 配置查询用业务日)
### 5.2 ETL 视图层C 方案)
后端在每次直连 ETL 库的事务里执行:
```sql
SET LOCAL app.current_site_id = '<site_id>';
SET LOCAL app.current_business_date = '<YYYY-MM-DD>';
SET LOCAL app.current_runtime_mode = 'live' | 'sandbox';
```
39 个 RLS 视图通过 `app.business_date_now()` 自动裁剪 `<日期列> <= business_date`。live 模式不下发 GUC 时函数回退 `CURRENT_DATE`,行为完全等同于改造前。
### 5.3 小程序
工具:`apps/miniprogram/miniprogram/utils/runtime-clock.ts`
- `getBusinessClock(force=false)`60 秒 in-memory 缓存 + 失败降级本地时间
- `getBusinessYearMonth()` / `getBusinessDate()` 便捷方法
- `clearBusinessClockCache()` 主动失效
已接入页面5 个,**详见 §10 跨模块覆盖矩阵**
| 页面 | 替换点 |
|---|---|
| `pages/performance/performance.ts` | G2 当月预估判断 |
| `pages/performance-records/performance-records.ts` | onLoad / loadData / switchMonth / canGoNext |
| `pages/task-list/task-list.ts` | `isCurrentMonth` 月度判断 |
| `pages/customer-records/customer-records.ts` | onLoad |
| `pages/customer-service-records/customer-service-records.ts` | onLoad |
### 5.4 AI 提示词
App2 / App2a`当前时间` 字段走 `runtime_ctx.business_now`,日期范围(`_calc_date_range`)的 `ref_date` 必须由调用方显式传 `runtime_ctx.business_date`
App3 / 4 / 5 / 6 / 7prompt JSON 里 `current_time` 字段走 `as_runtime_business_now_str(site_id, fmt="%Y-%m-%d %H:%M")`
### 5.5 admin-web
切换页面 `apps/admin-web/src/pages/RuntimeContext.tsx` + API 封装 `apps/admin-web/src/api/runtimeContext.ts`
- 路由:`/settings/runtime-context`(仅 super_admin 可见)
- 表格列:门店 / 运行模式 / 业务日期 / 沙箱实例 / AI 模式 / 更新时间 / 操作
- 切换弹窗:目标模式 disabled、沙箱业务日期 DatePicker`disabledDate: d.isAfter(today, 'day')`)、重置沙箱实例 Switch、操作原因 TextArea
- 提交后弹 Steps 弹窗,逐步显示 6 个 transition step 的 success / skipped / warning
---
## 6. 与 ETL 影子跑数的衔接
`biz.trigger_jobs` 是全局调度(无 `site_id` 列),单门店切沙箱**不暂停**。多门店隔离完全在数据写入层用 `runtime_mode + sandbox_instance_id` 实现。
ETL `task_engine``flow_runner` 并不直接读 `biz.site_runtime_context`ETL 跑批写 ODS/DWD/DWS 仍按真实数据;`task_generator` 等业务任务通过后端服务调用,间接读取 RuntimeContext。
**已知设计权衡**sandbox 模式下若 ETL 在演示中跑了一轮真实数据(如把昨天的真实订单跑入 DWS由于 RLS 视图按 `app.business_date_now() = sandbox_date` 截断,演示侧仍看不到。但 DWS 物理数据已变化,切回 live 后立刻可见。**这是有意设计**:沙箱只裁可见性,不阻断真实跑数。
### 6.1 F1-5b 收益(2026-05-05 D1)
- **app 视图业务日上界裁剪范围**(2026-05-02 迁移 + F1-5b 验收):39 个 `app.v_dws_*` 视图全部加 `WHERE stat_date <= app.business_date_now()`,沙箱模式自动过滤未来数据
- **后端读取层**:`fdw_queries._fdw_context` 在所有 ETL 查询入口注入 GUC(F1-5b A2/A3 完成),保证视图层裁剪生效
- **写入层 sandbox 隔离**:ETL 跑批写入 dwd/dws 不带 sandbox 标记;后端业务表(`biz.coach_tasks` / `biz.ai_run_logs` / `biz.ai_trigger_jobs`)在 sandbox 模式下写入带 `runtime_mode='sandbox' + sandbox_instance_id='sbx_*'`,与 live 行共存可隔离查询
- **业务架构边界澄清**(F1-5b D2 / §11.1):`zqyy_app.app` schema 实为 FDW 外表,业务读写 `biz.*` 走应用层 A 方案,不重复建 RLS 视图层
- **跨连接器扩展性**:Core 层(标准化层)未来支持多门店系统时,DWS / app 视图 / 沙箱裁剪逻辑无需改动,仅 DWD 扩展即可
---
## 7. 安全 / 权限模型
| 操作 | 角色 | 备注 |
|---|---|---|
| 列出门店 + 查上下文 | `super_admin` | `/api/admin/runtime-context/sites``?site_id=` |
| 切换 live / sandbox | `super_admin` | `PATCH /api/admin/runtime-context` |
| 读取自门店上下文 | 任意已登录用户 | `/api/config/runtime-context` |
| 读取业务时钟 | 已审核通过的小程序用户 | `/api/xcx/runtime/clock` |
边界:
- 多门店并行 sandbox 已经支持(每个 site 独立一行,互不影响)
- super_admin 当前可切任意 site不限于自己绑定的门店
- `sandbox_date > date.today()` 服务端 422 拒绝admin-web DatePicker `disabledDate` 兜底
---
## 8. 验收标准AC
| # | 验收点 | 验证方式 |
|---|---|---|
| AC1 | live → sandbox 切换后,小程序看到的"今天"变为 `sandbox_date` | 自动化:`tools/db/verify_sandbox_end_to_end.py`;手工:登录小程序看 performance / task-list 当月判断 |
| AC2 | sandbox 模式下,业务表写入带 `runtime_mode='sandbox' + sandbox_instance_id='sbx_*'`,与 live 行共存 | SQL`SELECT runtime_mode, sandbox_instance_id, COUNT(*) FROM biz.coach_tasks GROUP BY 1,2` |
| AC3 | sandbox 模式下39 个 RLS 视图 `max(<日期列>) <= sandbox_date` | 自动化:`verify_sandbox_end_to_end.py` § 1已 31/31 PASS |
| AC4 | 切回 live 后 `sandbox_date / sandbox_instance_id` 恢复 NULL写入恢复 `('live', 'live')` | SQL`SELECT mode, sandbox_date, sandbox_instance_id FROM biz.site_runtime_context` |
| AC5 | AI promptsApp2/2a 的"当前时间" + App3-7 的 `current_time`)显示 `sandbox_date HH:MM` | admin-web `/logs/ai-run-logs` Drawer 抽屉 |
| AC6 | `biz.trigger_jobs` 切沙箱**不被暂停**(无 `paused_by_sandbox` 状态) | SQL`SELECT status, COUNT(*) FROM biz.trigger_jobs GROUP BY status` |
| AC7 | AIDashboard "今日" 与"7 天趋势"按真实日期,不被沙箱拉到 `sandbox_date` | admin-web `/ai/dashboard` 视觉验证 |
| AC8 | AI cache `target_id` 在 sandbox 模式下加 `sbx_*:` 前缀 | SQL`SELECT target_id FROM biz.ai_cache WHERE target_id LIKE 'sbx_%'` |
| AC9 | `sandbox_date > date.today()` 切换返回 422 | curl PATCH 测试 |
| AC10 | admin-web 非 super_admin 不可见菜单 | 普通账号登录视觉验证 |
| AC11 | 多门店并行 sandboxsite A 切 sandbox 不影响 site B 的 live 行为 | **未验证**(详见 todos |
| AC12 | 跨页时间漂移:小程序在 sandbox 下连续打开多页business_date 一致60s 缓存语义) | **未验证**(详见 todos |
| AC13 | 沙箱写入数据可清理:按 `runtime_mode='sandbox' AND sandbox_instance_id=...` 一键清理 | **未实施**(详见 todos |
---
## 9. 依赖
- **P3** 用户认证super_admin 角色 + JWT aud=`admin`;小程序 `require_approved`
- **P11** 部署上线(迁移与 RLS 视图必须在生产库执行;建议灰度先跑测试库)
- **P17** 助教归属任务引擎(任务表加 runtime 维度索引依赖 P17 的 `(site_id, assistant_id, member_id, task_type, status)` 唯一约束)
- **P18** Admin Task Engine Dashboard沙箱状态在 admin-web 可视化)
---
## 10. 跨模块覆盖矩阵
> 来源:审计文档 + 代码 grep。**X = 已接入****?** = 不确定 / 未验证;**—** = 不需要。
### 10.1 后端服务层
> **F1-5b 收益**(2026-05-05 D2):`?` 项经审计核实更新为 X 或 — ;`customer_service.py` MP-3 上界 + `ai/admin_service.py` A1 改造均已落地 commit。
| 模块 | 读 RuntimeContext | task_runtime_filter | runtime_insert_columns | 业务日上界 SQL |
|---|---|---|---|---|
| `task_manager.py` | X | X | X | X |
| `task_generator.py` | X | X | X | X(F1-5b T3 已覆盖间接调用 site_id 传递) |
| `task_expiry.py` | X | X | — | — |
| `recall_detector.py` | X | X | X | X |
| `board_service.py` | X | X | — | X(MP-2 推迟 F1-6,monthly salary 暂存遗留) |
| `coach_service.py` | X | — | — | X(MP-3 _build_coach_tasks 加业务日上界,commit 96dae0c) |
| `customer_service.py` | X | — | — | X(MP-3 lastService 上界 + ref_date 提至模块顶部) |
| `chat_service.py` | X | — | — | X |
| `performance_service.py` | X | — | — | X |
| `fdw_queries.py` | X(`_fdw_context` GUC + F1-5b A2 异常分支兜底) | — | — | C 方案视图层 |
| `ai/cache_service.py` | X | — | X | — |
| `ai/run_log_service.py` | X | — | X | — |
| `ai/dispatcher.py` | X(间接,通过 context.site_id → run_log_svc) | — | — | —(F1-5b T3 unit test 5/5 PASS) |
| `ai/admin_service.py` | X(F1-5b A1 改造完成,4 处 CURRENT_DATE → business_date) | — | — | X(_get_range_stats / _get_7d_trend / _get_app_distribution 上下界双全 + UI-3 today_calls 分组) |
### 10.2 AI 提示词
| Prompt | `current_time` / 当前时间 | 数据窗口 ref_date |
|---|---|---|
| `app2_finance_prompt.py` | X | X |
| `app2a_finance_area_prompt.py` | X | X |
| `app3_clue_prompt.py` | X | — |
| `app4_analysis_prompt.py` | X | — |
| `app5_tactics_prompt.py` | X | — |
| `app6_note_prompt.py` | X | — |
| `app7_customer_prompt.py` | X | — |
| `app8_consolidate` | ? | ?(F1-5b 范围外,Wave 2 / F1-6 audit) |
### 10.3 小程序页面
> **F1-5b 收益**(2026-05-05 D2):`?` 项经全面 audit 核实(详见 sandbox-replay-engine-spec.md)。
> 走查覆盖范围:`pages/board-* / customer-* / coach-* / performance* / task-list`(共 11 个含月度数据的页面)。
| 页面 | 引入 `runtime-clock` | 备注 |
|---|---|---|
| `performance/performance.ts` | X | G2 月度判断 |
| `performance-records/performance-records.ts` | X | 4 处 |
| `task-list/task-list.ts` | X | 月度判断 |
| `customer-records/customer-records.ts` | X | onLoad |
| `customer-service-records/customer-service-records.ts` | X | onLoad |
| `coach-service-records/coach-service-records.ts` | X | F1-5b MP-5 onLoad + loadData + switchMonth 4 处 new Date() 替换 |
| `board-finance/board-finance.ts` | —(后端走 board_service) | F1-5b MP-1 复核 PASS:actualIncome=66000 现金口径正确,前端无需 runtime-clock |
| `board-customer/board-customer.ts` | —(后端走 board_service) | F1-5b A1/A3 间接覆盖,前端无需 runtime-clock |
| `board-coach/board-coach.ts` | —(后端走 board_service) | F1-5b MP-2 推迟 F1-6,前端无需 runtime-clock |
| `customer-detail/customer-detail.ts` | —(F1-5b MP-3 后端 _build_coach_tasks 上界 + MP-4 id guard) | 操作时间戳保留真实时钟(设计共识) |
| `chat/chat.ts` | — | 同上 |
| `utils/time.ts` | — | 显示文案,保留真实时钟(设计共识) |
### 10.4 ETL 视图
39 个 RLS 视图业务日上界(详见 `scripts/ops/gen_rls_business_date_migration.py``VIEWS_WITH_BD`),跳过 6 个 SCD2 dim 与 4 个无日期列视图。
---
## 11. 已知遗漏 / 未覆盖
### 11.1 设计共识保留(不视为 bug
- `created_at` / `updated_at` / `finished_at` / `next_run_at` 持久化时间戳保留真实系统时间(审计需要)
- AI tokens 计费、限流、熔断、缓存 TTL 按真实时钟运行
- 调度元数据(`scheduled_tasks`, `biz.trigger_jobs`)按真实时钟
- AIDashboard / AIRunLogs 列表按真实写入时间排序
- 小程序 `chat / customer-detail / utils/time.ts` 操作时间戳保留 `new Date()`
- DIM SCD2 维度(`v_dim_*`)保留 `scd2_is_current=1` 当前快照
- **`zqyy_app` 库永不建 RLS 视图层**F1-5b D2 决策 / 2026-05-05 A4
- `zqyy_app.app` schema 实为 FDW 外表(映射 `etl_feiqiu.app.v_*`),非真实视图
- 业务读写 `biz.*` 走**应用层 A 方案**(后端 SQL 显式带 `WHERE site_id=?` + `RuntimeContext.business_date` 上界)
- 若新建 `zqyy_app.app` RLS 视图,会形成"应用层守护 + 视图层守护"两套保护,违反**单一权威源**原则
- GUC 注入路径(F1-5b A3):仅在 `etl_feiqiu` 入口(`fdw_queries._fdw_context`)做,zqyy_app 直连不注入
- **batch_id 命名规约**(F1-5b A5 / 2026-05-05):
- **生成方式**:`uuid.uuid4().hex`(32 字符小写 hex,无连字符)
- **生命周期**:`AdminAIService._batch_store` 内存字典,TTL 10 分钟(estimate→confirm 间隔上限)
- **用途**:
1. estimate 阶段返回给前端,confirm 时回传校验
2. 标注 `ai_run_logs.triggered_by = f"batch:{batch_id}"` (Wave 2 进度查询基础)
3. 隔离 ctx_snapshot:批量执行内全程使用 estimate 时锁定的 RuntimeContext,避免中途切换 sandbox 导致漂移
- **不持久化到 DB**:仅内存存储,后端重启丢失;前端必须在 10 分钟内完成 confirm
- **与 sandbox_instance_id 区别**:
- `batch_id`:批量执行的"会话 ID",生命周期跨 estimate→confirm
- `sandbox_instance_id`:沙箱"实例 ID"(`sbx_<uuid8>`),生命周期跨 sandbox 切换 live 之前
- 一个 batch 可以在 sandbox 模式下执行,此时 ctx_snapshot 保留 `sandbox_instance_id`,batch_id 与之独立
### 11.2 已知 hack
- `coach_service._build_task_groups` 是否带 `site_id` + runtime filter 未在审计中明确验证
- `task_generator` 部分 SQL 是否有业务日上界仅文档提及,未单测覆盖(F1-5b T3 已 mock 覆盖 dispatcher 间接传递,直接 generator 未单测)
- `page_context.py` 7 处直连 ETL 查询依赖 GUCC 方案)兜底,未单独传 `ref_date`
### 11.3 F1-5b 已收口的遗留 hack2026-05-05 D3
-`customer_service._build_coach_tasks` 第一条 SQL 加业务日上界(MP-3 commit 96dae0c)
-`ai/admin_service.py` 4 处 CURRENT_DATE → business_date(F1-5b A1 commit af02446)
-`fdw_queries._fdw_context` 异常分支兜底(F1-5b A2)
-`apply_runtime_session_vars` 在所有 ETL 查询入口统一注入(F1-5b A3)
-`auth.role_permissions` manager 角色移除 view_tasks(BE-1 commit 18fbb2f)
- ✓ ai_run_logs / coach_tasks / ai_trigger_jobs 写入带 runtime 字段(BE-3 / T3 unit 测试覆盖)
- ✓ admin-web AIDashboard / AIRunLogs / AITriggerJobs runtime 全套透出(UI-1/2/3/4/5)
- ✓ 小程序 coach-service-records 业务时钟接入(MP-5)+ coach-detail id guard(MP-4)
- ✓ ETL 连接显式 client_encoding=UTF8 防御 GBK 异常(A6)
- ✓ Excel 修正 3 表加 effective_date schema 准备(MP-2 prep)
### 11.4 推迟到 F1-6 沙箱时光机阶段 B 的待办
详见 **`docs/_overview/sandbox-replay-engine-spec.md`**(F1-5b D3 / 2026-05-05):
-**MP-2 完整实施**:board-coach 月度面板 daily salary 累计(需新建 `dws_assistant_daily_salary` 表 + ETL 改造)
-**ETL Excel 上传 UI 改造**:tenant-admin/ExcelUpload 模块支持 effective_date 列解析 + 模板分发
-**14 个 P1 指标 service 切换**:会员余额 / 60d 消费 / 累计 GMV / 月度新增/流失会员等
-**5 个 P2 指标重算**:RS 关系指数 / 客户黏性 / 任务完成率 / Excel 修正按 effective_date
### 11.5 推迟到 F1-7+ 沙箱时光机阶段 C 的远期目标
- ⏳ 3 个 P3 指标(门店等级评级 / 助教星级 / 累计 KPI 状态算法)
-`biz.sandbox_audit_log` 用户行为审计表(沙箱演练复盘场景)
- ⏳ AI app8_consolidate prompt 业务日接入审计
### 11.6 完整待办指向
跨模块完整收口、深入测试用例、Wave 1 走查必测场景、清理脚本设计 → **`docs/_overview/04a-feedback/P0-7-runtime-context-todos.md`**。
---
## 12. 任务清单
- [x] T1`biz.site_runtime_context` 表与 7 表加列迁移test 库)
- [x] T2后端 `RuntimeContext` 服务 + 4 + 1 个 API 端点
- [x] T3admin-web 切换页面 + 权限校验
- [x] T4AI prompts / data_fetchers 接入业务时钟
- [x] T5fdw_queries `_fdw_context` 下发 GUC
- [x] T6ETL 库 39 个 RLS 视图业务日上界(含 helper 函数)
- [x] T7小程序 5 个页面引入 `getBusinessClock`
- [x] T8`tools/db/verify_sandbox_end_to_end.py` 31/31 PASStest 库 site=2790685415443269 / sandbox=2025-09-01
- [x] T9admin-web Playwright e2e 13/13 PASS
- [x] T10BD_Manual + 6 份 changes 审计
- [x] **T11(F1-5b)**:F1-5a 沙箱 batch-run + 走查 bug fix(2026-05-05 commit 421e193 / 1baa212 / a045625 / 95a4500)
- [x] **T12(F1-5b Wave A)**:架构主体收口 — A1/A2/A3/T1/T2/Hook + admin-web UI-1/2/4 + 小程序 MP-3/5 + MP-1/BE-1(commit af02446 → 87a5e3b)
- [x] **T13(F1-5b Wave B)**:UI-3/5 sandbox 透出 + MP-4 id guard + BE-3/T3 测试回归 + A6 ETL 编码 + MP-2 prep + 沙箱时光机 spec(commit c433757 → 1e803e2)
- [ ] T14生产库 `zqyy_app` + `etl_feiqiu` 迁移执行(原 T11)
- [ ] T15sandbox 数据定期清理脚本(按 `sandbox_instance_id` 限定)
- [ ] T16多门店并行 sandbox 验证脚本
- [ ] T17admin-web 沙箱实例数据浏览页(可选,便于运维查看 `sbx_*` 写入了什么)
- [ ] **T18(F1-6)**:沙箱时光机阶段 B — 14 个 P1 + 5 个 P2 指标(详见 `docs/_overview/sandbox-replay-engine-spec.md`)
- [ ] **T19(F1-7+)**:沙箱时光机阶段 C — 3 个 P3 指标 + sandbox_audit_log 用户行为审计
---
## 13. 已知冲突 / 历史文档与现状的偏差
| 项 | 冲突 | 当前权威 |
|---|---|---|
| `biz.trigger_jobs` 是否按 site 暂停 | `BD_Manual_runtime_context_sandbox.md` § 3.3 / § 4 仍写 "切沙箱时按 site_id 暂停 enabled 行为 paused_by_sandbox";现状代码已经移除该逻辑 | **代码现状**`biz.trigger_jobs` 全局共用,不按 site 暂停。BD_Manual 待修订 |
| Steps 弹窗步骤数量 | `2026-05-02__sandbox_admin_web_manual_checklist.md` 提到 `runtime_context_upserted` / `pending_jobs_cancelled` 等 key代码实际是 `cancel_etl_processes` / `cancel_task_queue` / `cancel_ai_runtime` / `cancel_ai_jobs` / `biz_triggers_unchanged` / `apply_context` | **代码现状**6 个 stepkey 以 `cancel_*` / `biz_triggers_unchanged` / `apply_context` 为准 |
| 小程序覆盖范围 | `2026-05-02__sandbox_no_future_data_plan.md` § B2 计划改 `board-finance.ts` `isCurrentMonthFilter`grep 结果未在 `board-finance.ts` 确认 `runtime-clock` import | **未验证**,归入 todos P1 |
---
## 14. 成果层走查User-facing Acceptance
### 14.1 验证哲学
工程层(代码 / 调用链 / 迁移 / lint / typecheck是必要的根基,但**不是终点**。最终用户看到的页面 / 小程序数据是否符合设计目标,才是项目成功标准。
视角分两层:
| 层级 | 验证目标 | 工具 | 通过标准 |
|---|---|---|---|
| 工程层 | 代码可运行 / 调用链通 / 迁移落库 / lint 过 | tsc / pytest / SQL | 自动化全绿 |
| **成果层** | **页面渲染对 / 数据准确 / 交互流畅 / 角色权限对** | **Playwright + 微信开发者工具** | **逐条手工 + 截图归档** |
### 14.2 admin-web 走查清单Playwright MCP,12 路由)
每条走查 = 设置 sandbox 时间2026-03-01→ 打开页面 → 截图 → 对照"期望展示"。
| # | 路由 | 期望展示 | 走查重点 |
|---|---|---|---|
| 14.2.1 | `/dashboard` | 顶部 sandbox 模式条带高亮 + 数据为 sandbox_date 当时数据 | 切 sandbox 后是否立刻显示"沙箱模式" + 数据是否切到虚拟日 |
| 14.2.2 | `/etl-tasks` | 5 Tab 切换 + 队列 / 历史 / 任务配置 / 触发器 / 状态 | sandbox 模式下 ETL 调度按真实时钟(设计共识)|
| 14.2.3 | `/triggers` (含 ?tab=biz / ?tab=ai) | 触发器列表 / status / cron 编辑 | 沙箱模式下显示"触发器仍按真实时钟运行"提示 |
| 14.2.4 | `/ai/dashboard` | 7 天趋势按真实日期(沙箱不影响)、今日 / 累计 token 按真实 | AC7 已要求,补实地走查截图 |
| 14.2.5 | `/ai/operations` | 8 个 APP + app2a 区域财务派生 列出 / 手动触发 → 沙箱模式下日期参数对 | 触发 1 次 app2_finance,看 AI 输入是否带 sandbox_date |
| 14.2.6 | `/ai/prewarm` | 分组展示 area=all 8 组合 + 8 area 64 组合 = 72 / 沙箱不影响预热范围 | 走查 prewarm 在 sandbox 下是否仍按真实 area 跑 |
| 14.2.7 | `/ai/trigger-jobs` | AI 调度历史 / 分页 / 筛选 | sandbox 影响触发的标记 |
| 14.2.8 | `/tenant-admins` | 租户管理员列表 / 创建 / 重置密码 | sandbox 不应改 auth 状态 |
| 14.2.9 | `/settings/env-config` | 环境变量列表 / 编辑 | 与 sandbox 无关 |
| 14.2.10 | `/settings/runtime-context` | 切 sandbox / 历史日期选择 / 切回 live | 完整切换流程,验证 AC1-AC4 |
| 14.2.11 | `/logs/dev-trace` | (已计划 Drop,可跳过) | — |
| 14.2.12 | `/logs/ai-run-logs` | 历史日志按真实写入时间排序 / Drawer 显示 sandbox_date | AC5 验证 |
| 14.2.13 | `/logs/db-viewer` | SELECT 查询正常 / DDL 拒绝P0-8 修复后) | 验证 P0-8 白名单 |
### 14.3 小程序走查清单(微信开发者工具 MCP,10 页)
前置:打开微信开发者工具 + 启用 9420 自动化端口 + 以教练身份登录。
| # | 页面 | 期望展示 | 走查重点 |
|---|---|---|---|
| 14.3.1 | `task-list` | 任务列表按 sandbox_date 当月生成 | AC1 |
| 14.3.2 | `performance` | 月度统计反映 sandbox_date | AC1 |
| 14.3.3 | `performance-records` | 4 处时间过滤反映 sandbox_date | T7 |
| 14.3.4 | `customer-records` | onLoad 拿 sandbox_date 拉历史 | T7 |
| 14.3.5 | `customer-service-records` | onLoad 拿 sandbox_date 拉历史 | T7 |
| **14.3.6** | **`board-finance`** | **area 5 区域切换 / AI 洞察 12 项指标 / 折线图横轴为虚拟近 7 日** | **P0-3 主体,Wave 1 必修** |
| **14.3.7** | **`board-customer`** | **客户分层基于 sandbox_date 截止状态** | **P0-3** |
| **14.3.8** | **`board-coach`** | **助教绩效反映 sandbox_date 当月累计** | **P0-3** |
| 14.3.9 | `customer-detail` | 操作时间戳保留真实时钟(设计共识) | §11.1 |
| 14.3.10 | `chat` | AI 对话 prompt 含 sandbox_date / 操作时间戳保留真实时钟 | AC5 |
### 14.4 跨页时间漂移走查AC12 实地化)
走查脚本:连续打开 10 个页面,各拉一次 `business-clock`,验证返回值在 60s 缓存窗口内一致。
### 14.5 多角色身份走查(看板收口后由主线主动提醒 Neo
**前置**:看板沙箱接入完成14.3.6 / 14.3.7 / 14.3.8 全部通过)。
**触发提醒**:在 §14.3.6/7/8 全部 PASS 时,在审计中标"提醒 Neo:可以切换用户身份做下一轮走查"。
走查矩阵:
| 身份 | 必走页面 |
|---|---|
| 教练 (coach) | task-list / performance / performance-records / 看板 3 页 |
| 顾问 (consultant) | task-list无看板权限 / customer-records / chat |
| 散客模式memberId=0 | coach-service-records 中"散客无详情"提示 |
| site_admin (admin-web) | /settings/runtime-context |
| tenant_admin (tenant-admin) | tenant-admin 主面板 |
每身份完整走完 + 截图归档。
### 14.6 走查产物归档
每次走查产出一份 `docs/audit/changes/2026-XX-XX__sandbox_acceptance_<wave>.md`,内容:
- 截图清单(按 §14.2 / §14.3 编号)
- 失败项明细(现状 / 期望 / 复现步骤)
- 通过率 / 总耗时
---
## 15. 变更记录
| 日期 | 操作 | 执行人 |
|---|---|---|
| 2026-05-01 | 主迁移产出 + 后端 / admin-web 接入 | Codex / Claude |
| 2026-05-02 | C 方案GUC + 39 视图)+ 小程序接入 + Playwright e2e + 端到端验证 | Cursor + Neo |
| 2026-05-04 | 本 SPEC 草案产出(追溯型) | ClaudeNeo 反馈触发) |
| 2026-05-04 | §14 成果层走查 patch 落入Neo P0-7 反馈,选项 A | Claude |
| 2026-05-05 | F1-5a 沙箱 batch-run + 走查 bug fix(commit 421e193 / 1baa212 / a045625 / 95a4500) | Claude(Neo 复审) |
| 2026-05-05 | **F1-5b Wave A**:架构主体收口 — A1/A2/A3/T1/T2/Hook + admin-web UI-1/2/4 + 小程序 MP-3/5 + MP-1 复核 + BE-1 权限修正(commit af02446 → 87a5e3b) | Claude(Neo 复审) |
| 2026-05-05 | **F1-5b Wave B**:UI-3/5 sandbox 透出 + MP-4 id guard + BE-3/T3 测试回归 + A6 ETL 编码防御 + MP-2 prep(3 stg 表加 effective_date)+ A4/A5 SPEC 登记 + D1-D4 SPEC 同步(commit c433757 → 1e803e2 + 本次 D1-D4 文档) | Claude(Neo 复审) |
| 2026-05-05 | **沙箱时光机模块 spec 产出**(`docs/_overview/sandbox-replay-engine-spec.md`):MP-2 完整实施 + 14 个 P1 + 5 个 P2 指标推迟到 F1-6 阶段 B,3 个 P3 + sandbox_audit_log 推迟到 F1-7+ 阶段 C | Claude(Neo 决策方向 1) |
| 待定 | F1-6 沙箱时光机阶段 B 启动 | Neo + Claude |
| 待定 | 生产库执行 + 跨模块走查 | Neo + 运维 |
### 15.1 F1-5b 收益总结
**已落地能力**(11 个 commit):
1. **后端架构主体收口**:RuntimeContext 在所有 ETL 入口统一注入,admin_service 4 处 CURRENT_DATE → business_date,fdw_queries 异常分支三层兜底
2. **测试回归网**:T1 RuntimeContext API 36 case + T2 batch ctx_snapshot 5 case + BE-3 ai_run_logs 写入 5 case + T3 dispatcher 路径 5 case = **51 case 本地全 PASS**(走 .gitignore:71 不入仓)
3. **admin-web sandbox 全栈透出**:UI-1 列表 runtime 列 + UI-2 详情 Drawer + UI-3 Dashboard 提示 + UI-4 全局徽章 + UI-5 触发任务列表 runtime 列
4. **小程序业务日纵深裁剪**:MP-3 customer-detail lastService 上界 + MP-5 coach-service-records 业务时钟 + MP-4 coach-detail id guard
5. **权限矩阵正确性**:BE-1 manager 角色移除 view_tasks(产品设计修正,DB seed 迁移)
6. **基础设施防御**:A6 ETL 连接显式 client_encoding=UTF8 防 GBK 异常 + post_edit_business_date_check.py PreToolUse hook 防 PR 引回 CURRENT_DATE
7. **沙箱时光机前置**:MP-2 prep 3 stg 表加 effective_date NOT NULL 强约束(zqyy_app 三个 Excel 暂存表)+ 完整模块 spec 产出
**业务价值**:
- 沙箱模式下,小程序所有"daily 累计型"指标(财务流水 / 服务记录 / 客户消费 / 任务列表)严格按 business_date 上界
- admin-web 操作员可一眼看出当前 site sandbox 状态(全局徽章 + Dashboard 提示 + 列表 / 详情 runtime 字段)
- 已知遗留(monthly salary / Excel 修正 daily 截断 / 用户行为审计)有完整 spec + 排期(F1-6/F1-7+)
**未落地能力**(已登记 §11.4 / §11.5):
- MP-2 完整实施(monthly daily salary 累计)→ F1-6
- ETL Excel 上传 UI 改造 → F1-6
- 14 个 P1 指标 service 切换 → F1-6
- sandbox_audit_log 用户行为审计 → F1-7+
详情见 [`sandbox-replay-engine-spec.md`](../../_overview/sandbox-replay-engine-spec.md)。