Files
Neo-ZQYY/docs/database/changes/2026-05-01__runtime_context_sandbox.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

294 lines
12 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.
# 数据库变更:业务运行上下文与沙箱隔离
> 日期2026-05-01
> 迁移脚本:[db/zqyy_app/migrations/20260501__runtime_context_sandbox.sql](../../db/zqyy_app/migrations/20260501__runtime_context_sandbox.sql)
> 关联代码:
> - [apps/backend/app/services/runtime_context.py](../../apps/backend/app/services/runtime_context.py)
> - [apps/backend/app/routers/admin_runtime_context.py](../../apps/backend/app/routers/admin_runtime_context.py)
> - [apps/backend/app/schemas/runtime_context.py](../../apps/backend/app/schemas/runtime_context.py)
> - [apps/admin-web/src/pages/RuntimeContext.tsx](../../apps/admin-web/src/pages/RuntimeContext.tsx)
> - [apps/admin-web/src/api/runtimeContext.ts](../../apps/admin-web/src/api/runtimeContext.ts)
> 涉及库:`zqyy_app`biz schema
> 风险等级:**中**(新增表 + 7 张业务/AI 表加列加索引;旧唯一索引被替换为含 runtime 维度的新索引)
---
## 1 · 变更说明
### 新增表
| Schema.Table | 字段数 | 用途 |
|---|---|---|
| `biz.site_runtime_context` | 9 | 单门店业务运行上下文:`mode=live` 使用真实日期,`mode=sandbox` 使用 `sandbox_date``sandbox_instance_id` 隔离写入 |
`biz.site_runtime_context` 字段:
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `site_id` | bigint | PK / FK → `biz.sites(site_id)` | 门店 ID |
| `mode` | varchar(20) | NOT NULL DEFAULT `live`CHECK `mode IN ('live','sandbox')` | 运行模式 |
| `sandbox_date` | date | 可空 | sandbox 模式下系统假设的业务日期 |
| `sandbox_instance_id` | varchar(64) | 可空 | sandbox 模式写入隔离实例 ID |
| `ai_mode` | varchar(20) | NOT NULL DEFAULT `live`CHECK `ai_mode IN ('live')` | AI 调用模式(当前仅 live沙箱也真实调用 DashScope |
| `status` | varchar(20) | NOT NULL DEFAULT `active` | 上下文状态 |
| `reason` | text | 可空 | 切换原因,便于审计 |
| `updated_by` | bigint | 可空 | 最近一次切换的操作人 |
| `created_at` / `updated_at` | timestamptz | NOT NULL DEFAULT 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`
### 新增列7 张表)
每张表新增两列:
| 列 | 类型 | 默认值 | NULL | 说明 |
|---|---|---|---|---|
| `runtime_mode` | varchar(20) | `'live'` | NOT NULL | 写入时所处模式(`live` / `sandbox` |
| `sandbox_instance_id` | varchar(64) | `'live'` | NOT NULL | sandbox 写入隔离实例 ID`live` 模式记录占位值 `'live'`,便于唯一索引覆盖两种模式 |
涉及表:
| Schema.Table | 用途 |
|---|---|
| `biz.coach_tasks` | 助教任务 |
| `biz.coach_task_transfer_log` | 任务转移日志 |
| `biz.recall_events` | 召回事件 |
| `biz.coach_task_history` | 任务历史 |
| `biz.ai_cache` | AI 缓存 |
| `biz.ai_run_logs` | AI 运行日志 |
| `biz.ai_trigger_jobs` | AI 触发记录 |
### 索引变更
旧索引DROP
| Schema.Index | 原唯一性 | 原表 |
|---|---|---|
| `biz.idx_coach_tasks_site_assistant_member_type` | UNIQUE | `coach_tasks` |
| `biz.idx_recall_events_site_assistant_member_day` | UNIQUE | `recall_events` |
新索引CREATE
| Schema.Index | 类型 | 说明 |
|---|---|---|
| `biz.idx_coach_tasks_runtime_unique_active` | UNIQUE部分索引 `WHERE status='active'` | 唯一键加入 `runtime_mode` + `sandbox_instance_id`,允许 sandbox/live 同时存在同一 (site/assistant/member/task_type) 的活跃任务 |
| `biz.idx_recall_events_runtime_site_assistant_member_day` | UNIQUE | 召回事件唯一键加入 runtime 维度,按 `pay_time` 当日去重 |
| `biz.idx_coach_tasks_runtime_assistant_status` | INDEX | 任务列表按 runtime + 助教 + 状态查询 |
| `biz.idx_ai_cache_runtime_lookup` | INDEX | AI cache 按 cache_type + site + runtime + target 查询 |
| `biz.idx_ai_trigger_jobs_runtime_site` | INDEX | AI 触发记录按 site + runtime + event_type + status 查询 |
### 数据回填
迁移在事务内执行,对 7 张表做:
```sql
UPDATE biz.<table>
SET runtime_mode = 'live', sandbox_instance_id = 'live'
WHERE sandbox_instance_id IS NULL;
```
迁移完成后所有历史行 `runtime_mode='live'``sandbox_instance_id='live'`,与新写入的 live 行保持一致,唯一索引继续生效。
---
## 2 · 兼容性影响
### 对后端
- `apps/backend/app/services/runtime_context.py` 提供 `get_runtime_context``task_runtime_filter``runtime_insert_columns``runtime_update_assignments``as_runtime_now_param``as_runtime_today_param``namespace_ai_target_id`。当 `biz.site_runtime_context` 不存在或查询异常时降级为默认 live保证迁移前不破坏旧行为。
- 已接入文件(写入或查询时考虑 runtime 维度):
- `apps/backend/app/services/task_manager.py`
- `apps/backend/app/services/task_generator.py`
- `apps/backend/app/services/task_expiry.py`
- `apps/backend/app/services/recall_detector.py`
- `apps/backend/app/services/board_service.py`
- `apps/backend/app/ai/cache_service.py`
- `apps/backend/app/ai/run_log_service.py`
- `apps/backend/app/ai/prompts/app2_finance_prompt.py`
- `apps/backend/app/ai/prompts/app2a_finance_area_prompt.py`
- 新增 admin API
- `GET /api/config/runtime-context`:当前用户门店上下文(任意登录用户)。
- `GET /api/admin/runtime-context?site_id=...`:按门店查询(仅 super_admin
- `GET /api/admin/runtime-context/sites`:列出门店与运行上下文(仅 super_admin
- `PATCH /api/admin/runtime-context`:切换 live/sandbox仅 super_admin
### 对 admin-web
- 新增菜单「系统设置 → 业务运行上下文 / 沙箱」,路由 `/settings/runtime-context`,仅 super_admin 可见。
- 切换 sandbox 时仅暂停/恢复 **当前 site_id** 下的 `biz.trigger_jobs`,不影响其他门店。
### 对小程序
- 不直接读 `site_runtime_context`;通过后端 API 间接生效。
- live 模式不改变现有行为sandbox 模式下看板/任务按 `sandbox_date``sandbox_instance_id` 隔离。
### 对预算与监控
- 真实预算、tokens 计数、审计仍按真实系统时间运行,不受沙箱影响。
---
## 3 · 回滚策略
### 前置条件
- 确认无门店处于 `mode='sandbox'`
```sql
SELECT site_id, mode, sandbox_date FROM biz.site_runtime_context WHERE mode='sandbox';
```
- 后端 / admin-web 中 RuntimeContext 相关代码已经撤回或停止依赖(避免 schema DROP 后查询失败)。
### 回滚 SQL
迁移文件末尾提供完整回滚 SQL注释形式。简化版本
```sql
BEGIN;
DROP INDEX IF EXISTS biz.idx_ai_trigger_jobs_runtime_site;
DROP INDEX IF EXISTS biz.idx_ai_cache_runtime_lookup;
DROP INDEX IF EXISTS biz.idx_coach_tasks_runtime_assistant_status;
DROP INDEX IF EXISTS biz.idx_recall_events_runtime_site_assistant_member_day;
DROP INDEX IF EXISTS biz.idx_coach_tasks_runtime_unique_active;
CREATE UNIQUE INDEX idx_recall_events_site_assistant_member_day
ON biz.recall_events
USING btree (site_id, assistant_id, member_id,
(date_trunc('day', pay_time AT TIME ZONE 'Asia/Shanghai')));
CREATE UNIQUE INDEX idx_coach_tasks_site_assistant_member_type
ON biz.coach_tasks
USING btree (site_id, assistant_id, member_id, task_type)
WHERE status = 'active';
ALTER TABLE biz.ai_trigger_jobs DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
ALTER TABLE biz.ai_run_logs DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
ALTER TABLE biz.ai_cache DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
ALTER TABLE biz.coach_task_history DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
ALTER TABLE biz.recall_events DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
ALTER TABLE biz.coach_task_transfer_log DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
ALTER TABLE biz.coach_tasks DROP COLUMN IF EXISTS sandbox_instance_id, DROP COLUMN IF EXISTS runtime_mode;
DROP TABLE IF EXISTS biz.site_runtime_context;
COMMIT;
```
### 数据保护
- 旧唯一索引被替换为更宽的唯一索引(含 runtime 维度),不会因唯一冲突丢数据。
- 回滚需先 DROP 新索引再重建旧索引;旧索引列子集仍唯一,回滚后历史 live 数据满足新约束。
- 所有 7 张表中 sandbox 模式产生的“演练数据”应在切回 live 前清理或保留:迁移层默认不清理,由运维决定。
---
## 4 · 验证 SQL已在 `test_zqyy_app` 通过)
### 验证 1 · 新表与 CHECK 约束
```sql
SELECT conname FROM pg_constraint
WHERE conrelid = 'biz.site_runtime_context'::regclass
AND contype = 'c'
ORDER BY conname;
-- 期望:
-- site_runtime_context_ai_mode_check
-- site_runtime_context_mode_check
-- site_runtime_context_sandbox_check
```
### 验证 2 · 7 张表的 runtime_mode / sandbox_instance_id 列存在
```sql
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'biz'
AND column_name IN ('runtime_mode', 'sandbox_instance_id')
AND table_name IN (
'coach_tasks',
'coach_task_transfer_log',
'recall_events',
'coach_task_history',
'ai_cache',
'ai_run_logs',
'ai_trigger_jobs'
)
ORDER BY table_name, column_name;
-- 期望14 行7 张表 × 2 列)
```
### 验证 3 · 关键索引存在 / 旧索引已 DROP
```sql
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz'
AND indexname IN (
'idx_coach_tasks_runtime_unique_active',
'idx_recall_events_runtime_site_assistant_member_day',
'idx_coach_tasks_runtime_assistant_status',
'idx_ai_cache_runtime_lookup',
'idx_ai_trigger_jobs_runtime_site'
)
ORDER BY indexname;
-- 期望5 行
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz'
AND indexname IN (
'idx_coach_tasks_site_assistant_member_type',
'idx_recall_events_site_assistant_member_day'
);
-- 期望0 行
```
### 验证 4 · CHECK 约束生效
```sql
INSERT INTO biz.site_runtime_context
(site_id, mode, sandbox_date, sandbox_instance_id)
VALUES (-1, 'sandbox', NULL, NULL);
-- 期望:失败,触发 site_runtime_context_sandbox_check
```
### 验证 5 · 历史数据回填
```sql
SELECT
SUM(CASE WHEN runtime_mode = 'live' THEN 1 ELSE 0 END) AS live_cnt,
SUM(CASE WHEN runtime_mode IS NULL THEN 1 ELSE 0 END) AS null_cnt
FROM biz.ai_cache;
-- 期望null_cnt = 0
```
---
## 5 · 关联变更
| 关联项 | 状态 | 说明 |
|---|---|---|
| 后端 RuntimeContext 服务 / 路由 | 已实施 | 见上文文件清单 |
| admin-web 沙箱设置页面 | 已实施 | `/settings/runtime-context`(仅 super_admin |
| 业务/AI 服务接入 runtime 过滤 | 已实施 | 任务、看板、AI cache、run logs 等 |
| 切换前停止 ETL/AI 队列 | 已实施 | `_stop_runtime_activity` |
| 暂停/恢复 `biz.trigger_jobs` | 已实施 | 已按 `site_id` 隔离 |
| 主 DDL 同步 | 待执行 | `PYTHONUTF8=1 python tools/db/gen_consolidated_ddl.py` 后同步 `db/zqyy_app/schemas/biz.sql` |
| 表级 BD_Manual | 已实施 | [BD_Manual_runtime_context_sandbox.md](../BD_Manual_runtime_context_sandbox.md) |
| 生产库执行 | ⏳ | 上线前由运维按窗口执行 |
---
## 6 · 变更记录
| 日期 | 操作 | 执行人 |
|---|---|---|
| 2026-05-01 | 迁移脚本产出 | Codex / Claude |
| 2026-05-02 | 测试库 `test_zqyy_app` 执行 + 5 项验证通过 | Cursor + Neo |
| 2026-05-02 | 修复 `trigger_jobs` 暂停/恢复按 site_id 隔离admin-web 新增沙箱设置页面 | Cursor + Neo |
| 待定 | 生产库 `zqyy_app` 执行 | Neo |