# 2026-05-02 沙箱「不看未来」彻底改造(A+B+C 全做) ## 目标 让 sandbox 真正模拟"设定历史日 sandbox_date 当时所有数据状态"—— 后台读取层、AI prompts、**小程序**业务看板/绩效/客户/任务页全部按 business_date 截断, 不再读取 sandbox_date 之后的真实生产数据。 > **端的归类(重要更正 2026-05-02)**: > - **小程序** 才是业务看板(`board-finance / board-customer / board-coach`)和绩效/客户/任务页面所在, > 是沙箱「不看未来」的主要受益方。 > - **admin-web** 是开发/运维向,**不展示业务看板**;沙箱在它这边主要表现为 `RuntimeContext` 开关、 > `AIDashboard / AIOperations / AIRunLogs / TaskManager / TriggerManager` 等管理页能看到 sandbox 实例下的 > AI 调用、任务写入与触发记录是隔离的(但 AI 计费/调度时间仍按真实系统时间,不受沙箱影响)。 > - **tenant-admin** 几乎不涉及业务数据展示,本轮基本不在沙箱范围。 ## 总览:三层方案 | 层 | 范围 | 方法 | 状态 | |---|------|------|------| | **A 文档/UI** | admin-web、BD_Manual | 顶部 Alert + 路线章节,提示"读取层修复进行中" | ✅ | | **B 应用层** | backend service / AI prompts & fetchers / fdw_queries / 小程序 | 时间锚替换为 RuntimeContext.business_date / business_now,SQL 补上界 | ✅ | | **C 数据层** | etl_feiqiu app schema RLS 视图 | 引入 GUC ``app.current_business_date`` + ``app.business_date_now()`` 函数 + 关键视图 WHERE 上界 | ✅ | ## 关键改动 ### A 层 - `apps/admin-web/src/pages/RuntimeContext.tsx` — 顶部 Alert 增加"读取层修复进行中"+ plan 链接。 - `docs/database/BD_Manual_runtime_context_sandbox.md` — 第 7 节新增"读取层不看未来路线"。 ### B 层 (后端) - `apps/backend/app/services/runtime_context.py` 新增 helpers: - `as_runtime_year_month_param(site_id) -> 'YYYY-MM'` - `as_runtime_business_now_str(site_id, fmt) -> str` - `business_date_upper_bound_sql(site_id, column, alias, cast)` 返回 SQL 片段 - `apply_runtime_session_vars(conn, ctx | site_id)` 设置 GUC(C 层基础) - AI prompts:app3/4/5/6/7 的 `current_time` 改用 `as_runtime_business_now_str`,不再 `datetime.now()`。 - AI data_fetchers: - `member_data._query_consumption_records` / `_query_visit_info` 接受 `ref_date`,所有窗口加业务日上界。 - `assistant_data._fetch_assistant_info_sync` / `_fetch_service_history_sync` 用业务日。 - `page_context._text_board_finance/customer/coach/customer_service_records` 全部上界化。 - 所有直连 ETL 库的 cursor 在 `SET LOCAL app.current_site_id` 之后再下发 `app.current_business_date`,供 RLS 视图 GUC 读取。 - service: - `board_service._batch_coach_details` 接受 ref_date,60 天消费窗口按业务日截。 - `chat_service._get_consumption_30d` / `_get_visit_count_30d` 业务日 30 天窗口。 - `coach_service.get_coach_detail` / `_build_history_months` 用业务日年月。 - `customer_service` 60 天助教统计上界化。 - `task_generator` 转移子流程的 `now` 改用 business_now。 - `task_manager.batch_query_for_task_list` / `build_performance_summary` / 任务详情 60 天窗口全部业务日。 - `tenant_users.py` SCD2 配置(cfg_assistant_level_price)用业务日。 - **fdw_queries**(关键修复): - `_fdw_context` 进入事务后下发 `app.current_business_date` + `app.current_runtime_mode` GUC。 - **客户看板「最近到店」bug 修复**:`get_last_visit_days` / `batch_query_for_task_list`(last_visit 计算)改为 ETL `last_consume_date` + `business_date - last_consume_date` 实时计算,不再依赖 ETL 预计算的 `days_since_last`,沙箱场景与 ETL 跑批延迟下都能正确显示"距上次到店 N 天"。 - `get_customer_board_recent` / `get_customer_board_recharge` / `get_customer_board_freq60` / `get_customer_board_recall` / `_get_weekly_visits_batch` / `get_coach_60d_stats` / `batch_query_for_task_list` 60 天窗口 / SCD2 配置等全部用业务日。 ### B 层 (小程序) - `apps/backend/app/routers/xcx_runtime_clock.py` 新增端点 `GET /api/xcx/runtime/clock`,返回 mode/business_date/business_year/business_month/business_year_month/business_now/is_sandbox/sandbox_date。 - `apps/miniprogram/miniprogram/services/api.ts` 增加 `fetchRuntimeClock`。 - `apps/miniprogram/miniprogram/utils/runtime-clock.ts`(新增)—— 60s 缓存 + 失败降级到本地时间。 - 关键页面切换为业务时钟: - `pages/performance/performance.ts` —— G2 当月预估判断 - `pages/performance-records/performance-records.ts` —— onLoad / loadData / switchMonth - `pages/task-list/task-list.ts` —— 月度判断 - `pages/customer-records/customer-records.ts` —— onLoad - `pages/customer-service-records/customer-service-records.ts` —— onLoad ### C 层 (RLS 视图) - 新增迁移 `db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql`: - 注册 STABLE SQL 函数 `app.business_date_now()`:从 GUC `app.current_business_date` 读取业务日,未设置时回退 `CURRENT_DATE`。 - **21 个视图**重写 WHERE,加 `<日期列> <= app.business_date_now()`: - 财务事实 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 配置 2 个:`v_cfg_assistant_level_price / performance_tier` - 列签名通过 `pg_get_viewdef` 实时从测试库读取,确保 `CREATE OR REPLACE VIEW` 不会因列签名漂移而失败。 - 生成脚本:`scripts/ops/gen_rls_business_date_migration.py`(可重复执行)。 - DDL 同步:`docs/database/ddl/etl_feiqiu__app.sql`、`db/etl_feiqiu/schemas/app.sql` 已同步。 ## 验证 ### 测试库迁移结果 ``` test site_id = 2790685415443269 live: max(stat_date)=2026-04-27, count=2439 sandbox(=2025-09-01): max(stat_date) finance_area_daily = 2025-09-01, count=432 max(visit_date) member_visit = 2025-09-01 max(create_time::date) settlement = 2025-09-01 RESULT: PASS ``` live 模式行为不变;sandbox 模式下所有事实视图严格不返回 sandbox_date 之后的数据。 ### 静态检查 - 后端 99 个改动文件 AST 解析全部通过。 - 前端 admin-web、小程序关键页面 lint 无新增错误。 ## 兼容性 / 回滚 - live 模式下 GUC 不设置 → `app.business_date_now()` 回退 `CURRENT_DATE`,行为完全等同于改造前。 - 回滚:`DROP FUNCTION app.business_date_now() CASCADE;`(视图会一并被 DROP),然后重新执行 `db/etl_feiqiu/schemas/app.sql` 即可恢复 live 行为。 - B 层 / 小程序的时间锚替换全部走 RuntimeContext(fail-soft 降级 live),不影响生产链路。 ## 已知未覆盖 - **page_context.py** 中 7 处直连 ETL 的查询,已加 SQL 上界(B 层),但部分位置依赖 GUC(C 层)即可,未单独传 ref_date。 - 写入时间戳(`created_at`、`updated_at`、`finished_at`、调度 `last_run_at`、ai_run_logs 写入)保持系统真实时间,**不应**被沙箱影响(这是审计/运行时元数据),保留现状。 - 小程序 chat / customer-detail 页面用于"展示当前操作时间"的 `new Date()` 保留(与会话/操作记录关联)。 - AI 调度的预算计算、限流仍按真实系统时间。 - DIM SCD2 维度(v_dim_assistant / v_dim_member / v_dim_member_card_account / v_dim_staff / v_dim_staff_ex / v_dim_table)保留 ``scd2_is_current=1`` 当前快照语义,未按 sandbox_date 重建历史维度行;如需"sandbox 当时维度状态"另行评估。 ## 2026-05-02 后续追加 ### B-2 / C 层补强 - 18 个非关键视图补业务日上界(详见 `gen_rls_business_date_migration.py` 的 `VIEWS_WITH_BD`):覆盖 `v_cfg_bonus_rules` / `v_cfg_index_parameters` 两个配置维度,及 16 个 DWS 业务事实/汇总(如 `v_dws_assistant_customer_stats`、`v_dws_member_assistant_intimacy`、`v_dws_finance_board_cache`、`v_finance_daily` 等)。**总计 39 个 RLS 视图带业务日上界**。 - 端到端验证:`tools/db/verify_sandbox_end_to_end.py` 一键跑 live + sandbox(2025-09-01) 对比,输出 `2026-05-02__sandbox_e2e_verify_report.md`。本轮结果 31/31 PASS。 - 注意:脚本里测的 `get_customer_board_recent / recharge / freq60 / recall` 是 `fdw_queries` 函数,**实际服务的是小程序 `board-customer`**,不是 admin-web。验证脚本同时覆盖 RLS 视图层(21+18=39 个视图),与端无关。 ### log 警告止血(独立于沙箱) - `apps/etl/connectors/feiqiu/tasks/dws/finance_area_daily.py`: 拓宽 `_is_all_only_area`,把 `补时长N`/`虚拟台N` 编号变体、`area_name=None & table_id 不空` 都归入 INFO(不再 WARNING),消除噪音。 - `apps/etl/connectors/feiqiu/tasks/dws/task_engine.py`: ETL → backend HTTP `_TIMEOUT` 由 `(5, 30)` 改 `(10, 600)`,与 `flow_runner` 对齐,止血 30s 读超时。**根因(同步长任务+30s timeout)已记录,长期方案是 `/api/internal/run-job` 改异步入队,待后续 PR。** ## 相关文件清单 - 主迁移:`db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql` - 生成器:`scripts/ops/gen_rls_business_date_migration.py` - 端到端验证:`tools/db/verify_sandbox_end_to_end.py` - 验证报告:`docs/database/changes/2026-05-02__sandbox_e2e_verify_report.md` - 文档:本文件 + `docs/database/BD_Manual_runtime_context_sandbox.md`