Files
Neo-ZQYY/docs/database/changes/2026-05-02__sandbox_no_future_data_plan.md
Neo caf179a5da feat: 2026-04-15~05-02 累积变更基线 — AI 重构 + Runtime Context + DWS 修复
涵盖(每条对应已存的审计记录):
- AI 模块拆分:apps/backend/app/ai/apps -> prompts/(8 个 APP + app2a 派生)
  audit: 2026-04-20__ai-module-complete.md
- admin-web AI 管理套件:AIDashboard / AIOperations / AIRunLogs / AITriggers / TriggerManager
  audit: 2026-04-21__admin-web-ai-management-suite.md
- App2 财务洞察 prompt v3 -> v5.1 + 小程序 AI 接入(chat / board-finance)
  audit: 2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md
- App2 prewarm 全过滤器 + AI 触发器 cron reschedule
  audit: 2026-04-21__app2-finance-prewarm-all-filters.md
  migration: 20260420_ai_trigger_jobs_and_app2_prewarm.sql / 20260421_app2_prewarm_cron_reschedule.sql
- AppType 联合类型对齐 + adminAiAppTypes.test.ts
  audit: 2026-04-30__admin_web_ai_app_type_alignment.md
- DashScope tokens_used 提取修复
  audit: 2026-04-30__backend_dashscope_tokens_used_extraction.md
- App3 线索完整详情 prompt
  audit: 2026-05-01__backend_app3_full_detail_prompt.md
- Runtime Context 沙箱(5-1~5-2 主线):
  - 后端 schema/service + admin_runtime_context / xcx_runtime_clock 两个 router
  - admin-web RuntimeContext.tsx + miniprogram runtime-clock.ts
  - migration: 20260501__runtime_context_sandbox.sql
  - tools/db/verify_admin_web_sandbox.py + verify_sandbox_end_to_end.py
  - database/changes: 7 份 sandbox_* 验证报告
- 飞球 DWS 修复:finance_area_daily 区域汇总 + task_engine 调整
  + RLS 视图业务日上界(migration 20260502 + scripts/ops/gen_rls_business_date_migration.py)

合规:
- .gitignore 启用 tmp/ 排除
- 不入仓:apps/etl/connectors/feiqiu/.env(API_TOKEN secret,本地修改保留)

待验证清单:
- docs/audit/changes/2026-05-04__cumulative_baseline_pending_verification.md
  每个主题的功能完整性 / 上线验证几乎都未收口,按优先级 P0~P3 逐一处理
2026-05-04 02:30:19 +08:00

17 KiB
Raw Blame History

沙箱「不看未来」完整修复清单

日期2026-05-02 关联:changes/2026-05-01__runtime_context_sandbox.md 关联代码:

  • 后端:apps/backend/app/services/runtime_context.pyfdw_queries.pyboard_service.pytask_*.pyrecall_detector.pychat_service.pycoach_service.pycustomer_service.pyperformance_service.pyai/data_fetchers/*ai/prompts/*
  • 数据库:db/etl_feiqiu/schemas/app.sqlDWS/DWD/DIM RLS 视图)
  • 小程序:apps/miniprogram/miniprogram/pages/performance*task-listboard-finance 等 风险等级:(核心业务读取层广泛假设「真实今天」) 状态:方案待用户确认;实施前不动业务代码

一、问题陈述

R1 RuntimeContext 业务日期沙箱 的初版只解决了 写入隔离sandbox 行带 runtime_mode='sandbox' + sandbox_instance_id='sbx_*',与 live 数据共存但不污染。

但读取层仍大量使用 真实系统时间,导致 sandbox 模式下:

  • get_finance_board 区间按 business_dateOK但 prompts 内部的辅助 ETL 查询用 _calc_date_range(time) 漏传 ref_date,退回 date.today(),会拉「真实今天」的数据。
  • AI data_fetchersmember_data / assistant_data / page_contextSQL 写死 CURRENT_DATE - INTERVAL '60 days' 等。
  • App3-7 prompt current_time 字段是 datetime.now(),与沙箱业务时钟不一致。
  • fdw_queries.py 大量 CURRENT_DATEORDER BY stat_date DESC LIMIT N,无业务日上界。
  • 小程序 performance / performance-records 用本地 Date 推算 year/month 直接传给后端,绕过 RuntimeContext。

后果sandbox 演示「以 2026-03-15 视角重放」时,看板/任务/AI 输出实际混合了截至真实今天的最新数据,纯净度被破坏。


二、修复策略A + B + C 三层)

A 层:文档与 UI 警告(最轻,先上)

不改业务代码,只让用户清楚当前沙箱边界。

文件 改动
apps/admin-web/src/pages/RuntimeContext.tsx Alert 中追加「当前沙箱可能仍读取部分真实近期数据」的警告,并提示完整修复进度
docs/database/BD_Manual_runtime_context_sandbox.md 新增「读取层局限与逐步修复路线」章节
docs/database/changes/2026-05-02__sandbox_no_future_data_plan.md 本文件,作为修复路线图

B 层:后端 + 小程序代码层修复(核心)

按调用链分两组:

B1 · 后端服务层时间锚替换 + SQL 上界

date.today() / datetime.now() / CURRENT_DATE / NOW() 在「业务窗口」语义里换成 as_runtime_today_param / as_runtime_now_param / task_runtime_filter 或参数化业务日;查询 DWS/DWD/FDW 时补 stat_date <= business_date / pay_time <= business_now 等上界。

调度元数据、审计、token TTL、写库 updated_at = NOW() 这类非业务窗口保持原状。

文件 行号 现状 改法
apps/backend/app/services/board_service.py 37 today = ref_date or date.today() 调用方必须传 ref_date=runtime_ctx.business_date(如 prompts 漏传需补)
同上 500 SQL 写死 create_time >= CURRENT_DATE - INTERVAL '60 days' create_time BETWEEN %s AND %s,参数为 business_date - 60dbusiness_now
apps/backend/app/services/task_generator.py 231/845/884 datetime.now(timezone.utc) 业务窗口处改 as_runtime_now_param(site_id)run_started_at(运行记录)保留真实时间
同上 873 直连 dwd.dim_assistant(非 RLS 视图) 切换 app.v_dim_assistant 或加 sandbox 上界
apps/backend/app/services/task_expiry.py 63 注释 expires_at < NOW();实际已用 as_runtime_now_param 仅更新注释,无代码改动
apps/backend/app/services/task_manager.py 680-682 datetime.now(timezone.utc).year/month 用作工资月 as_runtime_today_param(site_id).year/month
同上 819-820 datetime.now() 计算年月 同上
同上 1199-1202 today = date.today(); cutoff_60d = today - 60d today = as_runtime_today_param(site_id)
apps/backend/app/services/coach_service.py 150/716 datetime.date.today() as_runtime_today_param
同上 198-207/744-756 biz.coach_tasks 查询无 task_runtime_filter task_runtime_filter(site_id)
同上 550-566 _build_task_groups 未带 site_id 补 site_id 过滤 + runtime filter
apps/backend/app/services/customer_service.py 516 CURRENT_DATE - INTERVAL '60 days' 参数化为 business_date - 60d
apps/backend/app/services/performance_service.py 508-518 / 532-534 _calc_date_range ref 来自 next_month_start,未对齐沙箱 参数链路改为 business_date 推导
apps/backend/app/services/chat_service.py 195 NOW() - 3 days 限制对话上下文 business_now - 3 days
同上 692/709 CURRENT_DATE - 30 days business_date - 30 days
同上 602 写消息 NOW() 写消息保留真实时间(持久化时钟应跟真实)
apps/backend/app/services/fdw_queries.py 196-200 / 489 / 567-568 / 650-651 / 688-689 / 924 / 1012-1016 / 等 大量 CURRENT_DATEORDER BY stat_date DESC LIMIT 无上界 函数签名增加 business_date / business_now 参数SQL 加 stat_date <= %s / create_time <= %s
apps/backend/app/services/ai/admin_service.py 86-87 / 137 / 304-305 / 546-551 AI 调用统计窗口 CURRENT_DATE super-admin 后台是否也按门店沙箱口径——需产品确认。默认建议保留真实时间
apps/backend/app/services/recall_detector.py 157-164 / 174-195 app.v_dws_* 查询无 stat_date 上界 stat_date <= business_date
apps/backend/app/services/scheduler.py / trigger_scheduler.py 调度元数据(任务下次运行时间) 不动:调度本身按真实时钟工作
apps/backend/app/routers/xcx_auth.py ~334-348 _dt.now().year/monthget_salary_calc as_runtime_today_param(user.site_id).year/month
apps/backend/app/routers/admin_runtime_context.py 111 sandbox_date > date.today() 校验 保留sandbox_date 的「未来」语义就是相对真实日历

AI prompts / data_fetchers

文件 行号 改法
apps/backend/app/ai/prompts/app2_finance_prompt.py 817-818 / 841-846 _calc_date_range(time, ref_date=runtime_ctx.business_date)
apps/backend/app/ai/prompts/app2a_finance_area_prompt.py 466-468 同上
apps/backend/app/ai/prompts/app3_clue_prompt.py 65-66 current_time = as_runtime_now_param(site_id)
apps/backend/app/ai/prompts/app4_analysis_prompt.py 82-83 同上
apps/backend/app/ai/prompts/app5_tactics_prompt.py 82-83 同上
apps/backend/app/ai/prompts/app6_note_prompt.py 79-80 同上
apps/backend/app/ai/prompts/app7_customer_prompt.py 79-80 同上
apps/backend/app/ai/dispatcher.py 259/330 去重键的 date.today() 改为 as_runtime_today_param(site_id),确保沙箱 vs live 去重不互相污染
apps/backend/app/ai/data_fetchers/member_data.py 166/211/280/326/377-378 CURRENT_DATE → 参数;date.today()business_dateORDER BY ... DESC LIMIT N 加上界
apps/backend/app/ai/data_fetchers/assistant_data.py 92/105-106/212-213 同上
apps/backend/app/ai/data_fetchers/page_context.py 140/154/218/243/364/413/415-416/465/467-468/518-519/602-603 同上;ORDER BY DESC LIMIT N 全部加 <= business_now / business_date 上界
apps/backend/app/ai/cache_service.py 83/279 expires_at > now() 与 TTL → 保留真实时钟(缓存 TTL 是真实时间维度)
apps/backend/app/ai/run_log_service.py 115/143/165/195-196/211-212 run log finished_at 真实时钟;窗口聚合按真实时钟(运维口径)

B2 · 小程序绕过点修复

让小程序停止用本地 Dateyear/month 直接传给后端;改为后端从 RuntimeContext 决定。

文件 行号 改法
apps/miniprogram/miniprogram/pages/performance/performance.ts 121-127 / 133-135 不再传 year/month,调用 fetchPerformanceOverview() 让后端按 business_date 决定;或先 fetchRuntimeContext() 拿业务年月
apps/miniprogram/miniprogram/pages/performance-records/performance-records.ts 62-63 / 87-91 / 143-147 / 258-262 初始化用后端 business_date.year/monthcanGoNext 上界改用 business_date
apps/miniprogram/miniprogram/pages/task-list/task-list.ts 389-411 isCurrentMonth 通过后端返回字段或 runtimeContext.business_date 计算
apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts 17 isCurrentMonthFilter 同上
apps/miniprogram/miniprogram/services/api.ts (新增) fetchRuntimeContext() 包装 /api/config/runtime-context,返回 { business_date, business_now, is_sandbox };缓存到全局 store
utils/time.ts 全文 不改:相对时间/IM 时间/截止日全是显示文案,按真实本地时间合理
chat.ts 367/407、customer-detail.ts 216-219 等乐观 UI 时间戳 不改:仅 UI 兜底,最终以后端时间为准

B 层后端公共改造点

为减少散点改动,建议在 apps/backend/app/services/runtime_context.py 加两个工具:

def runtime_window_upper_bound(site_id, conn=None) -> tuple[date, datetime]:
    """返回 (business_date, business_now) 用作 SQL 上界。"""

def runtime_year_month(site_id, conn=None) -> tuple[int, int]:
    """返回沙箱业务年月,用于绩效报表。"""

fdw_queries.py 函数签名增加可选 business_datebusiness_now 参数;调用方按需传入。

C 层ETL RLS 视图业务日上界(最彻底)

利用现有 app.current_site_id 模式,引入 app.current_business_date 会话变量,在 app.v_* 视图层加上界。后端 _fdw_context 增加 SET LOCAL app.current_business_date = %ssandbox 模式下传 business_datelive 模式下不设置或设置 9999-12-31

C 方案 SQL 模式

-- 时间事实表(含 stat_date / pay_time / create_time
CREATE OR REPLACE VIEW app.v_dws_finance_daily_summary AS
SELECT ...
FROM dws.dws_finance_daily_summary
WHERE site_id = current_setting('app.current_site_id')::bigint
  AND stat_date <= COALESCE(
        NULLIF(current_setting('app.current_business_date', true), '')::date,
        '9999-12-31'::date
      );

-- 维度表(含 scd2_effective_from
CREATE OR REPLACE VIEW app.v_dim_member AS
SELECT ...
FROM dwd.dim_member
WHERE register_site_id = current_setting('app.current_site_id')::bigint
  AND COALESCE(scd2_effective_from, '0001-01-01'::date) <= COALESCE(
        NULLIF(current_setting('app.current_business_date', true), '')::date,
        '9999-12-31'::date
      );

current_setting('app.current_business_date', true) 第二个参数 true 表示「未设置时返回空字符串而非报错」,配合 NULLIF + COALESCE 实现:

  • live 模式下 app.current_business_date 未设置 → 上界为 9999-12-31 → 等同无限制
  • sandbox 模式下后端 SET LOCAL app.current_business_date = '2026-03-15' → 视图自动截断

C 方案涉及范围

db/etl_feiqiu/schemas/app.sql 共 49 个 RLS 视图:

类型 视图模式 上界字段
维度表 SCD2 v_dim_*10 个) scd2_effective_fromcreated_at
DWD 事实表 v_dwd_*6 个) create_timepay_time
DWS 日粒度 v_dws_*_daily*(约 10 个) stat_date
DWS 月粒度 v_dws_*_monthly*(约 5 个) stat_month
DWS 索引/聚合 v_dws_*_index stat_date
配置表 v_cfg_* 一般取「最新有效」,沙箱可保留真实最新(配置不该回放)

需要按视图逐个判断时间上界字段。建议分批:

  1. C-1:财务相关 v_dws_finance_*5 视图)。
  2. C-2:助教/任务相关 v_dws_assistant_*v_dws_member_assistant_*(约 12 视图)。
  3. C-3DWD 事实表 v_dwd_*6 视图)。
  4. C-4:维度表 SCD2 v_dim_*10 视图,需配合 SCD2 字段)。
  5. C-5:配置表 v_cfg_*(一般保留真实最新,但确认是否需要按 effective_to)。

每批按 RLS 双 schema 规则同时改 dws.v_*app.v_*

C 方案后端改造

# apps/backend/app/database.py 或 fdw_queries.py 内
def get_etl_readonly_connection(site_id, business_date=None):
    conn = ...
    with conn.cursor() as cur:
        cur.execute("SET default_transaction_read_only = on")
        cur.execute("SET LOCAL app.current_site_id = %s", (str(site_id),))
        if business_date is not None:
            cur.execute("SET LOCAL app.current_business_date = %s", (str(business_date),))
    ...

_fdw_context 同样改造:默认从 get_runtime_context(site_id)business_datesandbox 模式自动 SET。


三、推荐实施顺序

步骤 工作量 价值 依赖
A1 admin-web Alert 警告升级 0.5h 立即让用户知道当前局限
A2 BD_Manual + 本修复路线图 0.5h 后续工作可见
B1-后端 prompts ref_date 漏传补齐 1h 立即修复 App2/App2a 漏洞
B1-后端 runtime helpers + service 层关键路径 4h 修 task/board/coach/customer/performance 主链路
B1-后端 fdw_queries 上界改造 6h 收口最大公约数 上一步
B1-后端 AI data_fetchers + prompts current_time 3h AI 链路对齐沙箱
B2-小程序 performance/year-month 改后端权威 2h 小程序绕过点收口 B1 后端 runtime helpers
C-1 财务视图 RLS 上界 2h 双 schema 规则DDL 同步 B 层验证通过
C-2 助教/任务视图 RLS 上界 3h 同上 同上
C-3 DWD RLS 上界 2h 同上 同上
C-4 维度 SCD2 上界 3h 历史维度回放精度 同上
C-5 配置表评估(多数不改) 0.5h 同上
同步主 DDL + 双 schema + DDL 副本 1h 保证仓库 ddl 与测试库一致 C 完成

总估时:约 28-30 小时,单人执行;强烈建议分 4-5 个 PRA、B-后端、B-小程序、C 财务视图、C 其他视图。


四、风险与开发约束

  1. live 行为不能变:所有 SQL 上界用 COALESCE(... , '9999-12-31') 形式live 不设变量时等同无限制。
  2. 双 schema 规则db/etl_feiqiu/schemas/app.sqldws.v_* 必须同时更改。
  3. DDL 副本同步:每批 C 改完跑 python tools/db/gen_consolidated_ddl.py,把 docs/database/ddl/etl_feiqiu__app.sql 等同步进 git。
  4. 真实时钟字段保留updated_at = NOW()finished_at = NOW(timezone.utc)、缓存 TTL expires_at、调度 next_run_at、AI run_logs 写入时间——这些保留真实时钟
  5. 去重键dispatcher / cache 的「当日去重」key 需带 runtime_mode + business_date,避免 sandbox 与 live 互相污染。
  6. 配置表沙箱语义v_cfg_* 一般取「最新有效」;如需历史回放,需要单独评估 scd2_effective_to 上界。
  7. 测试:每批改完都要在 test_zqyy_app + test_etl_feiqiu 上运行至少一次端到端 sandbox 切换 + 看板抽查 + AI 触发。

五、未覆盖项(需用户确认或单独立项)

  • 生产数据库执行:本修复在 test 库通过后才能上生产;窗口需运维约定。
  • 写入沙箱数据归零:长期使用沙箱后会积累 sandbox 行任务、AI cache、run logs、recall events应有按 runtime_mode='sandbox' AND sandbox_instance_id=... 的清理脚本。
  • 小程序 utils/time.ts当前不改如需把「相对时间」也按沙箱算如沙箱日下「3 天前」按沙箱日推算),属于另一议题。
  • 跨多门店 sandbox:当前未限制同时多门店进入 sandbox若并行需求需要约定每个 site 独立 RuntimeContext + 各自上界(架构已支持)。
  • Admin-Web AI 调用统计 / 监控页:是否按真实时间口径,需产品决定。

六、实施建议

  1. 本文档已落地docs/database/changes/2026-05-02__sandbox_no_future_data_plan.md),作为后续多 PR 的统一目录。
  2. 不立即动业务代码;等用户确认范围后开始。
  3. 优先级建议A → B-后端关键路径task_generator、board_service、prompts ref_date 漏传)→ B-AI prompts current_time → B-小程序 performance → B-fdw_queries → C-财务视图 → 其他 C。
  4. 每个 PR 自带单测sandbox 模式下 SQL 不返回 sandbox_date 之后的数据。