Files
Neo-ZQYY/docs/prd/specs/P20-runtime-context-sandbox.md
Neo c6453829a6 docs(prd): 新增 P20 Runtime Context 沙箱 SPEC + §14 成果层走查
补全 Runtime Context / 虚拟时间机制的独立 SPEC,基于现有代码 +
2026-05-01/02 审计记录追溯型起草(Neo P0-7 反馈触发)。

§1-§13 工程层规范:
- 数据模型 (biz.site_runtime_context + 7 表 runtime 维度列)
- 5 个 API 端点(管理面 + 通用面 + 小程序面)
- 各端读取约定(后端服务层 / ETL 视图层 / 小程序 / AI 提示词 / admin-web)
- 跨模块覆盖矩阵 + 13 条 AC + 任务清单 T1-T15
- 已知冲突清单 (BD_Manual vs 代码现状)

§14 成果层走查 (Neo P0-7 §15 patch 落入,选项 A):
- admin-web Playwright 12 路由走查清单
- 小程序微信开发者工具 10 页走查清单
- 跨页时间漂移 (AC12 实地化)
- 多角色身份走查 (看板收口后主线主动提醒切身份)
- 走查产物归档约定

参考: docs/_overview/04a-feedback/P0-7-spec-acceptance-layer-check.md
2026-05-04 07:38:01 +08:00

31 KiB
Raw Blame History

P20Runtime Context 沙箱 — 虚拟时间机制

版本v1.0 草案 | 日期2026-05-04 | 来源:基于 2026-05-01 / 2026-05-02 落地代码与审计反推 SPEC slugruntime-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 单门店一行的状态表,可在 livesandbox 之间切换。
  • 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
  • 缓存 TTLbiz.ai_cache.expires_at
  • 全局触发器调度(biz.trigger_jobssite_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.pytools/db/verify_admin_web_sandbox.py
10 BD_Manual + 一系列变更说明 已合并 docs/database/BD_Manual_runtime_context_sandbox.mddocs/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()

复合 CHECKsite_runtime_context_sandbox_check

  • mode='live'sandbox_date IS NULLsandbox_instance_id IS NULL
  • mode='sandbox'sandbox_date IS NOT NULLsandbox_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 解析的 dateCURRENT_DATE

3.5 RLS 视图业务日上界39 个视图)

模式:<日期列> <= app.business_date_now()(部分用 COALESCE(... ::date, '0001-01-01'::date) 兜底)。

涉及视图分组(详细列表见 scripts/ops/gen_rls_business_date_migration.pyVIEWS_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_parameterseffective_fromeffective_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_jobscancelled
  5. biz_triggers_unchangedbiz.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 }

RuntimeContextResponseapps/backend/app/schemas/runtime_context.py

{
  "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_idsbx_*: 前缀
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 FDWfdw_queries.py_fdw_contextSET 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 promptsapp2_finance_prompt.py / app2a_finance_area_prompt.py / app3-7 current_time 字段
  • 路由:tenant_users.pySCD2 配置查询用业务日)

5.2 ETL 视图层C 方案)

后端在每次直连 ETL 库的事务里执行:

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、沙箱业务日期 DatePickerdisabledDate: 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_engineflow_runner 并不直接读 biz.site_runtime_contextETL 跑批写 ODS/DWD/DWS 仍按真实数据;task_generator 等业务任务通过后端服务调用,间接读取 RuntimeContext。

已知设计权衡sandbox 模式下若 ETL 在演示中跑了一轮真实数据(如把昨天的真实订单跑入 DWS由于 RLS 视图按 app.business_date_now() = sandbox_date 截断,演示侧仍看不到。但 DWS 物理数据已变化,切回 live 后立刻可见。这是有意设计:沙箱只裁可见性,不阻断真实跑数。


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 行共存 SQLSELECT 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') SQLSELECT 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 状态) SQLSELECT 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_*: 前缀 SQLSELECT 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 后端服务层

模块 读 RuntimeContext task_runtime_filter runtime_insert_columns 业务日上界 SQL
task_manager.py X X X X
task_generator.py X X X ?
task_expiry.py X X
recall_detector.py X X X X
board_service.py X X X
coach_service.py X ? X
customer_service.py X X
chat_service.py X X
performance_service.py X X
fdw_queries.py X_fdw_context GUC C 方案视图层
ai/cache_service.py X X
ai/run_log_service.py X X
ai/dispatcher.py ?
ai/admin_service.py ?

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

10.3 小程序页面

页面 引入 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
board-finance/board-finance.ts ? 设计文档要求改 isCurrentMonthFilter,未在 grep 结果中确认
board-customer/board-customer.ts ? 未确认
board-coach/board-coach.ts ? 未确认
customer-detail/customer-detail.ts 操作时间戳保留真实时钟(设计共识)
chat/chat.ts 同上
utils/time.ts 显示文案,保留真实时钟(设计共识)

10.4 ETL 视图

39 个 RLS 视图业务日上界(详见 scripts/ops/gen_rls_business_date_migration.pyVIEWS_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 当前快照

11.2 已知 hack

  • coach_service._build_task_groups 是否带 site_id + runtime filter 未在审计中明确验证
  • task_generator 部分 SQL 是否有业务日上界仅文档提及,未单测覆盖
  • page_context.py 7 处直连 ETL 查询依赖 GUCC 方案)兜底,未单独传 ref_date

11.3 完整待办指向

跨模块完整收口、深入测试用例、Wave 1 走查必测场景、清理脚本设计 → docs/_overview/04a-feedback/P0-7-runtime-context-todos.md


12. 任务清单

  • T1biz.site_runtime_context 表与 7 表加列迁移test 库)
  • T2后端 RuntimeContext 服务 + 4 + 1 个 API 端点
  • T3admin-web 切换页面 + 权限校验
  • T4AI prompts / data_fetchers 接入业务时钟
  • T5fdw_queries _fdw_context 下发 GUC
  • T6ETL 库 39 个 RLS 视图业务日上界(含 helper 函数)
  • T7小程序 5 个页面引入 getBusinessClock
  • T8tools/db/verify_sandbox_end_to_end.py 31/31 PASStest 库 site=2790685415443269 / sandbox=2025-09-01
  • T9admin-web Playwright e2e 13/13 PASS
  • T10BD_Manual + 6 份 changes 审计
  • T11生产库 zqyy_app + etl_feiqiu 迁移执行
  • T12跨模块完整测试 / Wave 1 走查(详见 todos
  • T13sandbox 数据定期清理脚本(按 sandbox_instance_id 限定)
  • T14多门店并行 sandbox 验证脚本
  • T15admin-web 沙箱实例数据浏览页(可选,便于运维查看 sbx_* 写入了什么)

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 isCurrentMonthFiltergrep 结果未在 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
待定 生产库执行 + 跨模块走查 Neo + 运维