涵盖(每条对应已存的审计记录): - 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 逐一处理
294 lines
12 KiB
Markdown
294 lines
12 KiB
Markdown
# 数据库变更:业务运行上下文与沙箱隔离
|
||
|
||
> 日期: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 |
|