# BD_Manual：biz Schema AI 表（对话记录 + 消息 + 缓存 + 运行日志 + 调度记录）

> 目标库：`test_zqyy_app`（通过 `APP_DB_DSN` 连接）
> 迁移脚本：
>   - `db/zqyy_app/migrations/2026-03-08__create_ai_tables.sql`（初始建表）
>   - `db/zqyy_app/migrations/2026-03-20__rns14_chat_module_extend.sql`（RNS1.4 CHAT 扩展）
>   - `db/zqyy_app/migrations/2026-03-22__p14_ai_module.sql`（P14 DashScope 迁移 + 调度器完善）
>   - `db/zqyy_app/migrations/2026-03-23__p15_ai_monitoring.sql`（P15 AI 监控后台 — alert_status + BRIN 索引）
> 关联 SPEC：`05-miniapp-ai-integration`（P5 AI 集成层）、`rns1-chat-integration`（RNS1.4 CHAT 对齐与联调收尾）、`P14-ai-dashscope-migration`（P14 DashScope 迁移）、`ai-monitoring-testing`（P15 AI 监控后台）

---

## 1. 变更说明

### 新增表（5 张）

| # | 表名 | 用途 | 字段数（初始→当前） | 来源 |
|---|------|------|---------------------|------|
| 1 | `biz.ai_conversations` | AI 对话记录：每次 AI 调用（用户主动或系统自动）创建一条 | 8 → 14 | P5 |
| 2 | `biz.ai_messages` | AI 消息记录：对话中的每条消息（输入/输出/系统） | 6 → 7 | P5 |
| 3 | `biz.ai_cache` | AI 应用缓存：各应用的结构化输出结果 | 9 → 10 | P5 |
| 4 | `biz.ai_run_logs` | **P14 新增** — AI 运行记录：每次 DashScope API 调用的详细日志 | 14 → 15 | P14, P15 |
| 5 | `biz.ai_trigger_jobs` | **P14 新增** — 调度运行记录：每次 AI 事件触发的编排执行记录 | 13 | P14 |

### RNS1.4 CHAT 模块扩展字段（2026-03-20）

| # | 表名 | 新增字段 | 用途 |
|---|------|---------|------|
| 1 | `biz.ai_conversations` | `context_type`, `context_id`, `title`, `last_message`, `last_message_at` | 多入口对话复用 + 历史列表展示与排序 |
| 2 | `biz.ai_messages` | `reference_card` | 引用卡片 JSON（客户概览等结构化上下文数据） |

### P14 DashScope 迁移扩展字段（2026-03-22）

| # | 表名 | 新增字段 | 用途 |
|---|------|---------|------|
| 1 | `biz.ai_conversations` | `session_id` | 百炼 session_id（格式 `conv_{id}_{ts}`），仅 App1 使用 |
| 2 | `biz.ai_cache` | `status` | 缓存状态：valid / expired / invalidated / generating |

### P15 AI 监控后台扩展（2026-03-23）

| # | 表名 | 变更类型 | 说明 |
|---|------|---------|------|
| 1 | `biz.ai_run_logs` | 新增字段 `alert_status` | 告警处理状态：NULL / pending / acknowledged / ignored |
| 2 | `biz.ai_run_logs` | 新增约束 `chk_ai_run_logs_alert_status` | CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')) |
| 3 | `biz.ai_run_logs` | 新增部分索引 `idx_ai_run_logs_alert` | (alert_status, created_at DESC) WHERE status IN ('failed', 'timeout', 'circuit_open') — 告警列表查询 |
| 4 | `biz.ai_run_logs` | 新增 BRIN 索引 `idx_ai_run_logs_created_brin` | BRIN (created_at) WITH (pages_per_range = 32) — Dashboard 聚合优化 |
| 5 | `biz.ai_run_logs` | 回填 | 已有 status IN ('failed','timeout','circuit_open') 的记录 alert_status 设为 'pending' |

### 表字段明细

#### biz.ai_conversations（14 字段）

| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `user_id` | VARCHAR(50) | NOT NULL | 用户 ID，系统自动调用时为 `system` |
| `nickname` | VARCHAR(100) | NOT NULL DEFAULT '' | 用户昵称 |
| `app_id` | VARCHAR(30) | NOT NULL | 应用标识：app1_chat / app2_finance / app3_clue / app4_analysis / app5_tactics / app6_note / app7_customer / app8_consolidation |
| `site_id` | BIGINT | NOT NULL | 门店 ID（多门店隔离） |
| `source_page` | VARCHAR(100) | 可空 | 来源页面标识 |
| `source_context` | JSONB | 可空 | 页面上下文 JSON |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `context_type` | VARCHAR(20) | 可空 | **RNS1.4 新增** — 对话关联上下文类型：task / customer / coach / general |
| `context_id` | VARCHAR(50) | 可空 | **RNS1.4 新增** — 关联上下文 ID |
| `title` | VARCHAR(200) | 可空 | **RNS1.4 新增** — 对话标题 |
| `last_message` | TEXT | 可空 | **RNS1.4 新增** — 最后一条消息内容摘要（截断至100字） |
| `last_message_at` | TIMESTAMPTZ | 可空 | **RNS1.4 新增** — 最后消息时间 |
| `session_id` | VARCHAR(100) | 可空 | **P14 新增** — 百炼 session_id，格式 `conv_{conversation_id}_{created_timestamp}`，仅 App1 使用 |

#### biz.ai_messages（7 字段）

| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `conversation_id` | BIGINT | NOT NULL, FK → `biz.ai_conversations(id)` ON DELETE CASCADE | 关联对话 |
| `role` | VARCHAR(10) | NOT NULL, CHECK IN ('user', 'assistant', 'system') | 消息角色 |
| `content` | TEXT | NOT NULL | 消息内容 |
| `tokens_used` | INTEGER | 可空 | 本条消息消耗的 token 数 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `reference_card` | JSONB | 可空 | **RNS1.4 新增** — 引用卡片 JSON：`{type, title, summary, data}`，用于展示客户概览等结构化上下文数据 |

#### biz.ai_cache（10 字段）

| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `cache_type` | VARCHAR(30) | NOT NULL, CHECK 7 个枚举值 | 缓存类型（见下方枚举） |
| `site_id` | BIGINT | NOT NULL | 门店 ID（多门店隔离） |
| `target_id` | VARCHAR(100) | NOT NULL | 目标 ID，含义因 cache_type 而异 |
| `result_json` | JSONB | NOT NULL | 结构化输出结果 |
| `score` | INTEGER | 可空 | 评分：仅应用 6 使用（1-10 分） |
| `triggered_by` | VARCHAR(100) | 可空 | 触发来源标识 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 创建时间 |
| `expires_at` | TIMESTAMPTZ | 可空 | 可选过期时间 |
| `status` | VARCHAR(20) | DEFAULT 'valid', CHECK | **P14 新增** — 缓存状态：valid（有效）/ expired（已过期）/ invalidated（手动失效）/ generating（生成中） |

### cache_type 枚举值与 target_id 约定

| cache_type | target_id 格式 | 示例 |
|---|---|---|
| `app2_finance` | 时间维度编码 | `this_month`, `last_week` |
| `app3_clue` | member_id | `12345` |
| `app4_analysis` | `{assistant_id}_{member_id}` | `100_12345` |
| `app5_tactics` | `{assistant_id}_{member_id}` | `100_12345` |
| `app6_note_analysis` | member_id | `12345` |
| `app7_customer_analysis` | member_id | `12345` |
| `app8_clue_consolidated` | member_id | `12345` |

### 约束与索引

| 表 | 约束/索引名 | 类型 | 说明 |
|----|-----------|------|------|
| `ai_conversations` | `idx_ai_conv_user_site` | INDEX | `(user_id, site_id, created_at DESC)` — 用户历史对话列表查询 |
| `ai_conversations` | `idx_ai_conv_app_site` | INDEX | `(app_id, site_id, created_at DESC)` — 按应用查询对话 |
| `ai_conversations` | `idx_ai_conv_context` | INDEX（条件） | **RNS1.4** — `(user_id, site_id, context_type, context_id, last_message_at DESC) WHERE context_type IS NOT NULL` |
| `ai_conversations` | `idx_ai_conv_last_msg` | INDEX | **RNS1.4** — `(user_id, site_id, last_message_at DESC NULLS LAST)` |
| `ai_messages` | FK `conversation_id` | FK | → `biz.ai_conversations(id)` ON DELETE CASCADE |
| `ai_messages` | `chk_ai_msg_role` | CHECK | `role IN ('user', 'assistant', 'system')` |
| `ai_messages` | `idx_ai_msg_conv` | INDEX | `(conversation_id, created_at)` — 对话消息列表 |
| `ai_cache` | `chk_ai_cache_type` | CHECK | 7 个枚举值 |
| `ai_cache` | `chk_ai_cache_status` | CHECK | **P14** — `status IN ('valid', 'expired', 'invalidated', 'generating')` |
| `ai_cache` | `idx_ai_cache_lookup` | INDEX | `(cache_type, site_id, target_id, created_at DESC)` — 查询最新缓存 |
| `ai_cache` | `idx_ai_cache_cleanup` | INDEX | `(cache_type, site_id, target_id, created_at)` — 清理超限记录 |
| `ai_run_logs` | `idx_ai_run_logs_site_app` | INDEX | **P14** — `(site_id, app_type)` |
| `ai_run_logs` | `idx_ai_run_logs_created` | INDEX | **P14** — `(created_at)` — Token 预算聚合 |
| `ai_run_logs` | `idx_ai_run_logs_status` | INDEX | **P14** — `(status)` |
| `ai_run_logs` | `chk_ai_run_logs_alert_status` | CHECK | **P15** — `alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored')` |
| `ai_run_logs` | `idx_ai_run_logs_alert` | INDEX（部分） | **P15** — `(alert_status, created_at DESC) WHERE status IN ('failed', 'timeout', 'circuit_open')` — 告警列表查询优化 |
| `ai_run_logs` | `idx_ai_run_logs_created_brin` | BRIN INDEX | **P15** — `BRIN (created_at) WITH (pages_per_range = 32)` — Dashboard 聚合查询优化，适合按时间顺序插入的表 |
| `ai_trigger_jobs` | `idx_ai_trigger_jobs_site` | INDEX | **P14** — `(site_id, event_type)` |
| `ai_trigger_jobs` | `idx_ai_trigger_jobs_dedup` | INDEX（条件） | **P14** — `(event_type, member_id, site_id, created_at) WHERE status NOT IN ('skipped_duplicate')` — 去重 |
| `ai_trigger_jobs` | `idx_ai_trigger_jobs_status` | INDEX | **P14** — `(status)` |

#### biz.ai_run_logs（15 字段，P14 新增 + P15 扩展）

| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `app_type` | VARCHAR(30) | NOT NULL | 应用类型：app1_chat / app2_finance / ... / app8_consolidate |
| `trigger_type` | VARCHAR(20) | NOT NULL | 触发类型：user / scheduled / event / forced |
| `member_id` | BIGINT | 可空 | 会员 ID |
| `request_prompt` | TEXT | 可空 | 请求 prompt（截断前 2000 字符） |
| `response_text` | TEXT | 可空 | 响应文本 |
| `tokens_used` | INTEGER | DEFAULT 0 | 消耗 token 数 |
| `latency_ms` | INTEGER | 可空 | 调用延迟（毫秒） |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'pending' | 状态：pending / running / success / failed / timeout / budget_exceeded |
| `error_message` | TEXT | 可空 | 错误信息 |
| `session_id` | VARCHAR(100) | 可空 | 百炼 session_id（仅 App1） |
| `alert_status` | VARCHAR(20) | DEFAULT NULL, CHECK | **P15 新增** — 告警处理状态：NULL（非告警记录）/ pending（待处理）/ acknowledged（已确认）/ ignored（已忽略）。仅 status IN ('failed','timeout','circuit_open') 的记录才设置此字段 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT now() | 创建时间 |
| `finished_at` | TIMESTAMPTZ | 可空 | 完成时间 |

#### biz.ai_trigger_jobs（13 字段，P14 新增）

| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | BIGSERIAL | PK | 自增主键 |
| `site_id` | BIGINT | NOT NULL | 门店 ID |
| `event_type` | VARCHAR(30) | NOT NULL | 事件类型：consumption / dws_completed / note_created / task_assigned |
| `connector_type` | VARCHAR(30) | DEFAULT 'feiqiu' | 连接器类型 |
| `member_id` | BIGINT | 可空 | 会员 ID |
| `payload` | JSONB | 可空 | 附加数据 |
| `status` | VARCHAR(20) | NOT NULL DEFAULT 'pending' | 状态：pending / running / completed / failed / skipped_duplicate / budget_exceeded |
| `is_forced` | BOOLEAN | DEFAULT false | 是否强制执行（跳过去重检查） |
| `app_chain` | VARCHAR(100) | 可空 | 调用链描述，如 `app3→app8→app7` |
| `started_at` | TIMESTAMPTZ | 可空 | 开始时间 |
| `finished_at` | TIMESTAMPTZ | 可空 | 完成时间 |
| `error_message` | TEXT | 可空 | 错误信息 |
| `created_at` | TIMESTAMPTZ | NOT NULL DEFAULT now() | 创建时间 |

---

## 2. 兼容性影响

| 组件 | 影响 |
|------|------|
| ETL 任务 | **P14 新增**：DWS 任务完成后通过 `utils/ai_trigger.py` 发送 HTTP 触发事件到后端 `ai_trigger_jobs`，失败不中断 ETL 流程 |
| 后端 API | 直接依赖。P5 AI 模块（`apps/backend/app/ai/`）基于这些表实现对话持久化、缓存读写、SSE 流式对话等功能 |
| 后端 API（RNS1.4） | **直接依赖**。CHAT 模块依赖 `ai_conversations` 的 5 个 RNS1.4 字段实现多入口对话复用 |
| 后端 API（P14） | **直接依赖**。`ai_run_logs` 用于 Token 预算聚合（BudgetTracker）；`ai_trigger_jobs` 用于事件去重和调度记录；`ai_conversations.session_id` 用于百炼会话管理；`ai_cache.status` 用于缓存状态控制 |
| 小程序 | 间接依赖。小程序通过后端 AI API 间接使用对话和缓存数据 |
| 管理后台 | **P15 直接依赖**。admin-web AI 监控后台（4 个页面）依赖 `ai_run_logs.alert_status` 实现告警管理（确认/忽略），依赖 BRIN 索引优化 Dashboard 聚合查询性能 |
| `member_retention_clue` | 间接关联。App8 结果同时写入 `ai_cache` 和 `member_retention_clue` 表（P14 实现幂等 DELETE+INSERT） |

---

## 3. 回滚策略

### 3d. 回滚 P15 AI 监控后台（2026-03-23 迁移）

按逆序 DROP 新增索引、约束和字段：

```sql
BEGIN;
DROP INDEX IF EXISTS biz.idx_ai_run_logs_created_brin;
DROP INDEX IF EXISTS biz.idx_ai_run_logs_alert;
ALTER TABLE biz.ai_run_logs DROP CONSTRAINT IF EXISTS chk_ai_run_logs_alert_status;
ALTER TABLE biz.ai_run_logs DROP COLUMN IF EXISTS alert_status;
COMMIT;
```

注意：
- 回滚后 admin-web AI 监控后台的告警管理功能将不可用
- Dashboard 聚合查询性能可能下降（失去 BRIN 索引）
- 回滚不影响 P14 的核心功能（调度器、预算追踪等）

### 3c. 回滚 P14 DashScope 迁移（2026-03-22 迁移）

按逆序 DROP 新增表和字段：

```sql
-- 删除 P14 新增约束和字段
ALTER TABLE biz.ai_cache DROP CONSTRAINT IF EXISTS chk_ai_cache_status;
ALTER TABLE biz.ai_cache DROP COLUMN IF EXISTS status;
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS session_id;

-- 删除 P14 新增表
DROP TABLE IF EXISTS biz.ai_trigger_jobs;
DROP TABLE IF EXISTS biz.ai_run_logs;
```

注意：
- 回滚后 P14 AI 调度器（dispatcher）、Token 预算追踪（BudgetTracker）将无法正常工作
- `ai_run_logs` 和 `ai_trigger_jobs` 中的数据将丢失，需先备份
- 回滚不影响 P5 和 RNS1.4 的功能

### 3a. 回滚 RNS1.4 CHAT 扩展（2026-03-20 迁移）

按逆序 DROP 新增索引和字段：

```sql
-- 删除 RNS1.4 新增索引
DROP INDEX IF EXISTS biz.idx_ai_conv_context;
DROP INDEX IF EXISTS biz.idx_ai_conv_last_msg;

-- 回退 ai_messages 新增字段
ALTER TABLE biz.ai_messages DROP COLUMN IF EXISTS reference_card;

-- 回退 ai_conversations 新增字段（逆序）
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS last_message_at;
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS last_message;
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS title;
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS context_id;
ALTER TABLE biz.ai_conversations DROP COLUMN IF EXISTS context_type;
```

注意：
- 回滚后 CHAT 模块（xcx_chat 路由、chat_service）将无法正常工作
- 如果新字段中已有数据，需先备份再执行回滚
- 回滚不影响 P5 初始建表的 8 个原始字段

### 3b. 回滚 P5 初始建表（2026-03-08 迁移）

按逆序 DROP（CASCADE 处理外键依赖）：

```sql
-- 删除索引
DROP INDEX IF EXISTS biz.idx_ai_cache_cleanup;
DROP INDEX IF EXISTS biz.idx_ai_cache_lookup;
DROP INDEX IF EXISTS biz.idx_ai_msg_conv;
DROP INDEX IF EXISTS biz.idx_ai_conv_app_site;
DROP INDEX IF EXISTS biz.idx_ai_conv_user_site;

-- 删除表（ai_messages 依赖 ai_conversations，需先删或用 CASCADE）
DROP TABLE IF EXISTS biz.ai_cache CASCADE;
DROP TABLE IF EXISTS biz.ai_messages CASCADE;
DROP TABLE IF EXISTS biz.ai_conversations CASCADE;
```

注意：
- `ai_messages` 通过 FK 依赖 `ai_conversations`，CASCADE 会级联删除消息
- 如果表中已有数据，需先备份再执行回滚
- 回滚不会删除 `biz` Schema 本身

---

## 4. 验证 SQL

### 4a. P5 初始建表验证

```sql
-- 1. 验证 3 张 AI 表全部存在
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'biz'
  AND table_name IN ('ai_conversations', 'ai_messages', 'ai_cache')
ORDER BY table_name;
-- 预期：返回 3 行（ai_cache, ai_conversations, ai_messages）

-- 2. 验证 ai_conversations 字段数量和关键字段
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
ORDER BY ordinal_position;
-- 预期：返回 14 行（P5 原始 8 + RNS1.4 新增 5 + P14 session_id = 14）

-- 3. 验证 ai_messages 的外键和 CHECK 约束
SELECT conname, contype, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.ai_messages'::regclass
ORDER BY conname;
-- 预期：包含 chk_ai_msg_role（CHECK role IN ...）和 ai_messages_conversation_id_fkey（FK → ai_conversations）

-- 4. 验证 ai_cache 的 CHECK 约束（7 个枚举值）
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.ai_cache'::regclass AND contype = 'c';
-- 预期：返回 2 行（chk_ai_cache_type 含 7 个枚举值，chk_ai_cache_status 含 4 个状态值）

-- 5. 验证 P5 初始索引全部存在（5 个）
SELECT indexname
FROM pg_indexes
WHERE schemaname = 'biz'
  AND indexname IN (
    'idx_ai_conv_user_site', 'idx_ai_conv_app_site',
    'idx_ai_msg_conv',
    'idx_ai_cache_lookup', 'idx_ai_cache_cleanup'
  )
ORDER BY indexname;
-- 预期：返回 5 行
```

### 4b. RNS1.4 CHAT 扩展验证

```sql
-- 6. 验证 ai_conversations 新增 5 个字段存在
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
  AND column_name IN ('context_type', 'context_id', 'title', 'last_message', 'last_message_at');
-- 预期：返回 5 行
--   context_type    | character varying | 20
--   context_id      | character varying | 50
--   title           | character varying | 200
--   last_message    | text              | NULL
--   last_message_at | timestamp with time zone | NULL

-- 7. 验证 ai_messages 新增 reference_card 字段存在
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_messages'
  AND column_name = 'reference_card';
-- 预期：返回 1 行，data_type = 'jsonb'

-- 8. 验证 RNS1.4 新增 2 个索引存在
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_conversations'
  AND indexname IN ('idx_ai_conv_context', 'idx_ai_conv_last_msg');
-- 预期：返回 2 行
--   idx_ai_conv_context  — 含 WHERE context_type IS NOT NULL 条件
--   idx_ai_conv_last_msg — (user_id, site_id, last_message_at DESC NULLS LAST)
```

### 4c. P14 DashScope 迁移验证

```sql
-- 9. 验证 ai_run_logs 表存在且字段正确
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_run_logs'
ORDER BY ordinal_position;
-- 预期：返回 15 行

-- 10. 验证 ai_trigger_jobs 表存在且字段正确
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_trigger_jobs'
ORDER BY ordinal_position;
-- 预期：返回 13 行

-- 11. 验证 ai_run_logs 索引（含 PK）
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_run_logs';
-- 预期：6 行（PK + 3 P14 索引 + 2 P15 索引）

-- 12. 验证 ai_trigger_jobs 索引（含去重部分索引）
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_trigger_jobs';
-- 预期：4 行（PK + 3 个索引）

-- 13. 验证 ai_conversations.session_id 字段
SELECT column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_conversations'
  AND column_name = 'session_id';
-- 预期：1 行，varchar(100)

-- 14. 验证 ai_cache.status 字段 + CHECK 约束
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_cache'
  AND column_name = 'status';
-- 预期：1 行，varchar(20)，default 'valid'

SELECT conname FROM pg_constraint
WHERE conrelid = 'biz.ai_cache'::regclass AND conname = 'chk_ai_cache_status';
-- 预期：1 行
```

### 4d. P15 AI 监控后台验证

```sql
-- 15. 验证 ai_run_logs.alert_status 字段存在
SELECT column_name, data_type, character_maximum_length, column_default
FROM information_schema.columns
WHERE table_schema = 'biz' AND table_name = 'ai_run_logs'
  AND column_name = 'alert_status';
-- 预期：1 行，varchar(20)，default NULL

-- 16. 验证 alert_status CHECK 约束
SELECT conname, pg_get_constraintdef(oid) AS constraint_def
FROM pg_constraint
WHERE conrelid = 'biz.ai_run_logs'::regclass
  AND conname = 'chk_ai_run_logs_alert_status';
-- 预期：1 行，CHECK (alert_status IS NULL OR alert_status IN ('pending', 'acknowledged', 'ignored'))

-- 17. 验证 P15 新增索引（部分索引 + BRIN 索引）
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_run_logs'
  AND indexname IN ('idx_ai_run_logs_alert', 'idx_ai_run_logs_created_brin');
-- 预期：2 行
--   idx_ai_run_logs_alert       — 含 WHERE status IN ('failed', 'timeout', 'circuit_open')
--   idx_ai_run_logs_created_brin — USING brin

-- 18. 验证 ai_run_logs 总索引数（P14 3个 + P15 2个 + PK = 6）
SELECT indexname FROM pg_indexes
WHERE schemaname = 'biz' AND tablename = 'ai_run_logs';
-- 预期：6 行

-- 19. 验证回填结果：失败/超时/熔断记录的 alert_status 应为 'pending'
SELECT COUNT(*) AS unset_alerts
FROM biz.ai_run_logs
WHERE status IN ('failed', 'timeout', 'circuit_open')
  AND alert_status IS NULL;
-- 预期：0（所有失败记录已回填为 'pending'）
```

### P15 admin API 查询模式说明

P15 admin-web AI 监控后台引入以下典型查询模式：

| 查询场景 | SQL 模式 | 使用索引 |
|---------|---------|---------|
| Dashboard 今日统计 | `SELECT COUNT(*), AVG(latency_ms) FROM ai_run_logs WHERE created_at >= 今日零点` | `idx_ai_run_logs_created_brin` |
| Dashboard 7天趋势 | `SELECT date_trunc('day', created_at), COUNT(*) FROM ai_run_logs WHERE created_at >= 7天前 GROUP BY 1` | `idx_ai_run_logs_created_brin` |
| 告警列表 | `SELECT * FROM ai_run_logs WHERE status IN ('failed','timeout','circuit_open') AND alert_status = 'pending' ORDER BY created_at DESC` | `idx_ai_run_logs_alert` |
| 告警确认/忽略 | `UPDATE ai_run_logs SET alert_status = 'acknowledged' WHERE id = ?` | PK |
| 调用记录分页 | `SELECT * FROM ai_run_logs WHERE site_id = ? AND app_type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?` | `idx_ai_run_logs_site_app` |
| Token 预算聚合 | `SELECT SUM(tokens_used) FROM ai_run_logs WHERE created_at >= 今日零点 AND status = 'success'` | `idx_ai_run_logs_created` (B-tree) |
