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>
40 KiB
P20:Runtime 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 | 后端管理 API(4 个端点) | 已合并 | apps/backend/app/routers/admin_runtime_context.py |
| 5 | 后端小程序 API(1 个端点) | 已合并 | 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 | PK,FK → biz.sites(site_id) |
门店 ID |
mode |
varchar(20) | NOT NULL,DEFAULT 'live',CHECK IN ('live','sandbox') |
运行模式 |
sandbox_date |
date | 可空 | sandbox 模式下系统假设的业务日期 |
sandbox_instance_id |
varchar(64) | 可空 | sandbox 写入隔离实例 ID(格式 sbx_<24hex>) |
ai_mode |
varchar(20) | NOT NULL,DEFAULT 'live',CHECK IN ('live') |
AI 调用模式(当前仅 live) |
status |
varchar(20) | NOT NULL,DEFAULT 'active' |
上下文状态 |
reason |
text | 可空 | 切换原因(运维/演示备注) |
updated_by |
bigint | 可空 | 最近一次切换的 admin user_id |
created_at |
timestamptz | NOT NULL,DEFAULT now() | 创建时间 |
updated_at |
timestamptz | NOT NULL,DEFAULT now() | 更新时间(API 切换时显式 NOW()) |
复合 CHECK:site_runtime_context_sandbox_check
mode='live'时sandbox_date IS NULL且sandbox_instance_id IS NULLmode='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 实例 ID;live 模式占位 '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→ 422mode='live'且sandbox_date IS NOT NULL→ 422sandbox_date > date.today()→ 422(沙箱只允许回放历史,禁止"未来")
切换前置动作(写入 steps[]):
cancel_etl_processes— 终止当前进程内 ETL 执行(task_executor.cancel)cancel_task_queue— 取消当前门店 pending/running 的task_queue行(status='cancelled')cancel_ai_runtime— 取消当前进程内属于该门店的 AI 异步调用链(get_dispatcher().cancel_running)cancel_ai_jobs— 标记该门店 pending/running 的biz.ai_trigger_jobs为cancelledbiz_triggers_unchanged—biz.trigger_jobs是全局调度表,不暂停(设计有意保留)apply_context— UPSERTbiz.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):
{
"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-7current_time字段 - 路由:
tenant_users.py(SCD2 配置查询用业务日)
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 / 7:prompt 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.appschema 实为 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 DatePickerdisabledDate兜底
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 prompts(App2/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 | 多门店并行 sandbox:site 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.pyMP-3 上界 +ai/admin_service.pyA1 改造均已落地 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.appschema 实为 FDW 外表(映射etl_feiqiu.app.v_*),非真实视图- 业务读写
biz.*走应用层 A 方案(后端 SQL 显式带WHERE site_id=?+RuntimeContext.business_date上界) - 若新建
zqyy_app.appRLS 视图,会形成"应用层守护 + 视图层守护"两套保护,违反单一权威源原则 - 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 间隔上限) - 用途:
- estimate 阶段返回给前端,confirm 时回传校验
- 标注
ai_run_logs.triggered_by = f"batch:{batch_id}"(Wave 2 进度查询基础) - 隔离 ctx_snapshot:批量执行内全程使用 estimate 时锁定的 RuntimeContext,避免中途切换 sandbox 导致漂移
- 不持久化到 DB:仅内存存储,后端重启丢失;前端必须在 10 分钟内完成 confirm
- 与 sandbox_instance_id 区别:
batch_id:批量执行的"会话 ID",生命周期跨 estimate→confirmsandbox_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.py7 处直连 ETL 查询依赖 GUC(C 方案)兜底,未单独传ref_date
11.3 F1-5b 已收口的遗留 hack(2026-05-05 D3)
- ✓
customer_service._build_coach_tasks第一条 SQL 加业务日上界(MP-3 commit96dae0c) - ✓
ai/admin_service.py4 处 CURRENT_DATE → business_date(F1-5b A1 commitaf02446) - ✓
fdw_queries._fdw_context异常分支兜底(F1-5b A2) - ✓
apply_runtime_session_vars在所有 ETL 查询入口统一注入(F1-5b A3) - ✓
auth.role_permissionsmanager 角色移除 view_tasks(BE-1 commit18fbb2f) - ✓ 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. 任务清单
- T1:
biz.site_runtime_context表与 7 表加列迁移(test 库) - T2:后端
RuntimeContext服务 + 4 + 1 个 API 端点 - T3:admin-web 切换页面 + 权限校验
- T4:AI prompts / data_fetchers 接入业务时钟
- T5:fdw_queries
_fdw_context下发 GUC - T6:ETL 库 39 个 RLS 视图业务日上界(含 helper 函数)
- T7:小程序 5 个页面引入
getBusinessClock - T8:
tools/db/verify_sandbox_end_to_end.py31/31 PASS(test 库 site=2790685415443269 / sandbox=2025-09-01) - T9:admin-web Playwright e2e 13/13 PASS
- T10:BD_Manual + 6 份 changes 审计
- T11(F1-5b):F1-5a 沙箱 batch-run + 走查 bug fix(2026-05-05 commit
421e193/1baa212/a045625/95a4500) - 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) - 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) - T15:sandbox 数据定期清理脚本(按
sandbox_instance_id限定) - T16:多门店并行 sandbox 验证脚本
- T17:admin-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 个 step,key 以 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 草案产出(追溯型) | Claude(Neo 反馈触发) |
| 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):
- 后端架构主体收口:RuntimeContext 在所有 ETL 入口统一注入,admin_service 4 处 CURRENT_DATE → business_date,fdw_queries 异常分支三层兜底
- 测试回归网: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 不入仓)
- admin-web sandbox 全栈透出:UI-1 列表 runtime 列 + UI-2 详情 Drawer + UI-3 Dashboard 提示 + UI-4 全局徽章 + UI-5 触发任务列表 runtime 列
- 小程序业务日纵深裁剪:MP-3 customer-detail lastService 上界 + MP-5 coach-service-records 业务时钟 + MP-4 coach-detail id guard
- 权限矩阵正确性:BE-1 manager 角色移除 view_tasks(产品设计修正,DB seed 迁移)
- 基础设施防御:A6 ETL 连接显式 client_encoding=UTF8 防 GBK 异常 + post_edit_business_date_check.py PreToolUse hook 防 PR 引回 CURRENT_DATE
- 沙箱时光机前置: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+