# AI 诊断（兽医、机构）— 技术方案

> 依据：`AI诊断(兽医、机构)功能需求.md`（v1.1）；关联 `在线接诊(兽医)技术方案.md`（v1.1）、`在线接诊(兽医)功能需求.md`  
> **本文档版本：1.6**（`biz_diagnosis_user` 使用用户表，见 **§3.0**）

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **整体** | RuoYi **v3.9.2**（**springboot2** 分支）单体后端 + 若依 **Vue2** 管理端 + 移动端 |
| **运行时** | JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **问诊 IM 表** | **复用** `biz_consult_session`、`biz_consult_message`（与在线接诊共用，见 **§2**）；另增 `biz_diagnosis_user` 记录提问人首次/末次使用时间（**§3.0**） |
| **近实时** | 复用 **Redis Pub/Sub** `consult:push` + STOMP（与在线接诊 **§5.1** 一致）；提问落库即推；AI 回复落库后推，**≤ 1 秒** |
| **文件** | `POST /common/upload`；消息存 URL |
| **AI 能力（对话框）** | 管理端/移动端 **直连或经本服务转发** `GET /v1/models`、`POST /v1/chat/completions`（`com.ruoyi.web.kb.controller.KbOpenAiProxyController`，配置 `ruoyi.kb`）；详见 `doc/大模型/大模型网关转发接口说明.md`、前端方案 §4 |
| **AI 能力（服务端兜底）** | `ConsultAiGateway`：仅当落库请求 **未带** `aiReplyContent` 时由服务端调模型；**超时 60s**、**非流式**整段回复 |
| **后台准入** | 若依菜单 + `diseaseTreatment:aiOnlineConsult:*` 权限；**不校验** `biz_medical_resource` 绑定 |
| **预约/接诊** | **不关联** `biz_service_appointment`、在线接诊接口 |

**后端分层（AI 诊断）**

| 层 | 职责 |
| --- | --- |
| `Controller` | 后台 `.../onlineConsult/ai`；移动端 `.../app/consult/ai` |
| `Service` | 列表/新建/消息/提问；调用 AI 生成回复；`hide` |
| `ConsultAiGateway` | 文本/图片调用模型；视频/语音返回固定引导文案 |
| `Mapper` + XML | 扩展 `BizConsultSessionMapper`（`consult_type=2` 查询）；`BizDiagnosisUserMapper`（**§3.0**） |
| `support` | 复用 `ConsultSessionRules`、`ConsultThreeMonthsWindow`、`ConsultMessagePreview`、`VetConsultValidation`；新增 `AiConsultValidation` |

**代码包**：`com.ruoyi.web.modules.diagnosis`（与在线接诊、医疗资源同域）  
**大模型转发包**：`com.ruoyi.web.kb`（与知识库共用 `ruoyi.kb`，见 **§1.2**）  
**DDL**：`sql/biz_consult_session.sql`、`sql/biz_consult_message.sql`（会话/消息，与在线接诊共用）；`sql/biz_diagnosis_user.sql`（AI 诊断使用用户，**§3.0**）

### 1.2 大模型 OpenAI 兼容转发（`/v1`）

与知识库同属大模型网关体系，实现类不在 `diagnosis` 包而在 `kb` 包：

| 对外路径 | 实现 | 用途 |
| --- | --- | --- |
| `GET /v1/models` | `KbOpenAiProxyController` → `KbOpenAiProxyServiceImpl` | 模型列表（原样转发上游） |
| `POST /v1/chat/completions` | 同上；`stream:false` JSON，`stream:true` SSE | 管理端 AI 诊断、移动端 AI 助手 **对话框问答** |

| 项 | 说明 |
| --- | --- |
| **鉴权（本服务）** | **须若依登录 Token**（`KbOpenAiProxyController` 无 `@Anonymous`）；未登录 **401** |
| **鉴权（上游）** | 服务端 `ruoyi.kb.api-key` 访问上游，**不**使用客户端 `Authorization` |
| **配置** | `ruoyi.kb`（`scheme`/`host`/`port`/`api-key`/`stream-read-timeout-ms`） |
| **与业务接口关系** | 会话 CRUD、消息落库仍走 `.../onlineConsult/ai/**`、`.../app/consult/ai/**`；前端大模型返回后 `POST .../message` 带 `aiReplyContent`，**不**再走 `ConsultAiGateway` |
| **设计/测试** | `doc/大模型/大模型网关转发接口说明.md`、`doc/大模型/大模型网关转发测试用例.md` |

---

## 2. 关联方案设计（AI 诊断 / 在线接诊）

### 2.1 统一数据模型（`consult_type` 隔离）

| `consult_type` | 模块 | `receiver_provider_id` | `vet_visible` 语义（本期） |
| --- | --- | --- | --- |
| `1` | 在线接诊（兽医） | 兽医 `biz_medical_resource.id` | 牧民删除后对**兽医**不可见 |
| **`2`** | **AI 诊断** | **必须为 NULL** | 复用字段：提问人删除后对**本人**列表不可见（`0`） |
| `3` | 专家问诊（预留） | 专家资源 ID | 待定 |

**禁止**：AI 列表/消息 SQL 出现 `consult_type=1`；接诊 SQL 出现 `consult_type=2`。

### 2.2 能力对照

| 能力 | 在线接诊（兽医） | AI 诊断（本文档） |
| --- | --- | --- |
| Base Path | `/diseaseTreatment/onlineConsult/vet` | `/diseaseTreatment/onlineConsult/ai` |
| 角色 | 兽医（100） | 兽医（100）+ 机构（102 等） |
| 列表筛选 | `askerName`、`contentKeyword` | **`contentKeyword` 仅** |
| 新建会话 | 无 | **`POST /session`** |
| 发送 | 人 → 人 | 人 → **AI 服务** → AI 消息入库 |
| 列表标题字段 | `askerName` | **`sessionTitle`** |
| 移动端 Path | `/app/onlineConsult` | `/app/consult/ai` |

### 2.3 推送频道（共用基础设施）

```text
提问/AI 回复落库 → 更新 last_message_* 
                → Redis consult:push 
                → STOMP /topic/consult/ai/{askerUserId}
```

Payload 同在线接诊 **§4.2**（`senderRole`：1 提问人，**3** AI 助手）。

### 2.4 与在线接诊代码复用

| 复用项 | 说明 |
| --- | --- |
| `biz_consult_*` 表 | 同表异 `consult_type` |
| `ConsultMessagePreview` | 摘要 50 字 / `[图片]` 等 |
| `ConsultThreeMonthsWindow` | 3 个月自然日 |
| `ConsultRedisPushService` | 推送 |
| `BizConsultMessageMapper` | 消息插入、分页查询 |
| `VetConsultValidation.validateSendBody` | 发送体校验（AI 模块复用） |

**独立实现**：`BizAiOnlineConsultServiceImpl`、`BizAiOnlineConsultController`、`ConsultAiGateway`、`selectAiSessionList` 等 Mapper 方法。

### 2.5 与「我的预约」

无表关联、无接口校验。

---

## 3. 数据库设计

会话/消息在共用表上按 `consult_type=2` 读写；另增 **AI 诊断使用用户** 表记录提问人首次/末次使用时间。

### 3.0 表 `biz_diagnosis_user`（AI 诊断使用用户）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | `bigint(20)` | 主键；与提问人 `asker_user_id` 一致 |
| `first_use_time` | `datetime` | 开始使用 AI 诊断时间 |
| `last_use_time` | `datetime` | 最后一次使用 AI 诊断时间 |

**写入规则**

| 时机 | 行为 |
| --- | --- |
| `POST .../session` 新建会话 | 若 `user_id` 不存在则 **INSERT**，`first_use_time`、`last_use_time` 均为当前时间 |
| 更新 `biz_consult_session.last_message_time` | 同步 **UPDATE** 对应 `asker_user_id` 的 `last_use_time`（与 `last_message_time` 一致） |

**DDL**：`sql/biz_diagnosis_user.sql`。

### 3.1 表 `biz_consult_session`（AI 会话写入约定）

| 字段 | AI 会话取值 |
| --- | --- |
| `consult_type` | **2** |
| `asker_user_id` / `asker_name` | 当前登录用户 |
| `receiver_user_id` | **NULL** |
| `receiver_provider_id` | **NULL**（禁止写入兽医/机构资源 ID） |
| `receiver_name` | **NULL** |
| `session_title` | 新建时 **「新会话」**；首条有效文本提问后更新为摘要（≤50 字） |
| `real_session_id` | **varchar(64)**，可为空；大模型网关 `session_id`，由 `POST .../message` 写入/更新，供多轮 `session_id` 回传 |
| `last_message_time` / `last_message_preview` | 每次提问或 AI 回复后更新 |
| `vet_visible` | 默认 **1**；提问人 `hide` 后置 **0** |

**索引使用**：`idx_asker (consult_type, asker_user_id, last_message_time)`。

**多会话**：同一 `asker_user_id` 可有多行 `consult_type=2`（**不**使用 `uk_vet_session` 限制 AI；该唯一键仅对 `receiver_provider_id` 非空的兽医会话生效）。

### 3.2 表 `biz_consult_message`

| `sender_role` | 含义 |
| --- | --- |
| **1** | 提问人（兽医/机构/牧民） |
| **3** | **AI 助手**（`sender_user_id=0`，`sender_name=AI助手`） |

在线接诊使用的 **2**（接诊兽医）在 AI 会话中**不出现**。

| 字段 | 说明 |
| --- | --- |
| `ai_category` | **varchar(32)**，可为空；仅 **提问人消息**（`sender_role=1`）写入；AI 回复为 `NULL`。纯数字时须为 `1`～`4`（与 `AiConsultConstants` 分类一致），亦可存业务编码字符串 |
| `cost_time` | **bigint**，可为空；大模型对话耗时（**毫秒**），仅 **AI 消息**（`sender_role=3`）写入；来自 `/v1/chat/completions` 响应头 **`X-COST`** |

**已有库迁移**：`sql/biz_diagnosis_user.sql`（或 `sql/alter_rename_biz_ai_diagnosis_user_to_biz_diagnosis_user.sql`）；`sql/alter_biz_consult_session_real_session_id.sql`、`sql/alter_biz_consult_message_ai_category.sql`、`sql/alter_biz_consult_message_cost_time.sql`。

### 3.3 AI 列表查询语义

**默认列表**（`searchMode=false`）：

```sql
-- 时间字段见 §3.5 推荐 SQL（COALESCE 或创建时写入 last_message_time）
WHERE consult_type = 2
  AND asker_user_id = #{askerUserId}
  AND vet_visible = 1
  AND COALESCE(last_message_time, create_time) >= #{threeMonthsAgo}
ORDER BY COALESCE(last_message_time, create_time) DESC
```

**搜索**（`searchMode=true`）：

- `vet_visible = 1`；**不限制** 3 个月。  
- `contentKeyword` 非空时匹配 `session_title`、`last_message_preview`、消息文本子查询（含 AI 回复文本）。

### 3.4 实现约定（对齐功能需求 v1.1）

| # | 约定项 | 实现要点 |
| --- | --- | --- |
| 1 | 3 个月窗 | `ConsultThreeMonthsWindow.calcThreeMonthsAgo()`，`Asia/Shanghai` |
| 2 | 搜索/重置 | 仅 `searchMode=true` 超窗；空关键字 →「请输入筛选条件」 |
| 3 | 摘要 | `ConsultMessagePreview.build` |
| 4 | 首问标题 | 首条 `msg_type=1` 提问成功后 `update session_title` |
| 5 | 消息分页 | 未传 `beforeId` 取最新 50 条，响应升序 |
| 6 | 隐藏 | `updateSessionVisible` → `vet_visible=0`；管理端/移动端 `POST .../hide`，仅 `asker_user_id=当前用户` |
| 7 | AI 超时 | `ConsultAiGateway` 调用 **60s** 超时，失败不插入 AI 消息 |
| 8 | 非流式 | 模型返回整段文本后一次 `insert` + 推送 |

### 3.5 实现约定补充（v1.1 已确认）

| # | 约定项 | 本期实现 |
| --- | --- | --- |
| 1 | **空会话进列表** | `POST /session` 创建时同步写入 `last_message_time = create_time`（或列表 SQL 使用 `COALESCE(last_message_time, create_time)` 参与 3 个月过滤与排序），保证「新会话」**立即**出现在默认列表 |
| 2 | **HTTP 读超时** | `POST .../message` 同步等待 AI 最长 **60s**；Tomcat/网关 **read timeout ≥ 65s**，避免先于 AI 调用返回 504 |
| 3 | **视频/语音固定回复** | `ConsultAiGateway` 常量文案（写入 AI 消息）：`已收到视频，请补充文字描述症状。` / `已收到语音，请补充文字描述。` |
| 4 | **图片识图失败** | 模型不可用或识图失败时，**仍插入**一条 AI 文本消息（`sender_role=3`），内容为：`图片识别暂不可用，请补充文字描述症状。`；**不**仅返回 HTTP 错误而无 AI 气泡 |
| 5 | **移动端搜索** | `GET /app/consult/ai/session/list` 支持与后台相同参数：`contentKeyword`、`searchMode`（规则同 **§3.3**） |
| 6 | **AI 上下文** | 本期 **单轮**：每次仅将**当前提问**（文本或图片 URL + 可选说明）送模型；**不带**历史多轮上下文（多轮对话产品化另迭代） |
| 7 | **同会话并发** | 同一会话在 AI 处理中（服务端标记或分布式锁）**拒绝**第二次 `POST .../message`，返回「AI 正在回复中，请稍候」 |
| 8 | **免责声明** | `POST /session` 与首次 `GET /session/list` 响应附带字段 `disclaimer`（常量，与功能需求 §5.4 #6 一致）；前端必须展示 |
| 9 | **`vet_visible` 口径** | 以**本文档 §2.1** 为准：AI 会话复用该字段表示**提问人列表可见**；在线接诊技术方案中「AI 不适用」表述作废 |

**列表 SQL 推荐（含空会话）**：

```sql
WHERE consult_type = 2
  AND asker_user_id = #{askerUserId}
  AND vet_visible = 1
  AND COALESCE(last_message_time, create_time) >= #{threeMonthsAgo}
ORDER BY COALESCE(last_message_time, create_time) DESC
```

---

## 4. 接口设计（后台 AI 诊断）

统一响应：`AjaxResult` / `TableDataInfo`。

**权限（示例）**：`diseaseTreatment:aiOnlineConsult:list|query|send|add|remove`

**Base Path**：`/diseaseTreatment/onlineConsult/ai`

**准入**：登录 + 接口权限；列表/新建按 `asker_user_id = 当前用户` 隔离，**不**调用 `VetProviderResolver` / `OrgProviderResolver` 校验医疗资源绑定。

| # | 说明 | Method | URI | 权限 | 要点 |
| --- | --- | --- | --- | --- |
| 4.1 | 历史会话列表 | GET | `/session/list` | `...:list` | Query：`pageNum`、`pageSize`（默认 20）、`contentKeyword`、`searchMode`；`asker_user_id` 服务端注入 |
| 4.2 | 新增会话 | POST | `/session` | `...:add` | 创建 `consult_type=2` 空会话；`session_title=新会话`；`last_message_time=create_time`（**§3.5 #1**）；返回 `sessionId`、`disclaimer` |
| 4.3 | 历史消息 | GET | `/session/{sessionId}/messages` | `...:query` | 校验 `consult_type=2` + `asker_user_id` + `vet_visible=1`；`beforeId`、`pageSize`（默认 50） |
| 4.4 | 提问（触发 AI） | POST | `/session/{sessionId}/message` | `...:send` | Body：`msgType`、`content`、`mediaDuration?`、`aiReplyContent?`、`realSessionId?`、`aiCategory?`、`costTime?`（毫秒，来自 `X-COST`）；落库时更新 `last_message_*` 与 **`update_time`**；流程见 **§5** |
| 4.5 | 隐藏会话 | POST | `/session/{sessionId}/hide` | `...:remove` | 管理端隐藏 AI 诊断会话；`vet_visible=0`；仅本人会话；隐藏后列表/消息/提问不可用 |

### 4.1 列表 `rows[]` 字段

`sessionId`、`sessionTitle`、`lastMessagePreview`、`lastMessageTime`、`realSessionId`（大模型网关会话 ID，可为空）。

### 4.2 消息 `rows[]` / 推送字段

`messageId`、`sessionId`、`senderRole`（1 提问人 / **3** AI）、`senderName`、`msgType`、`content`、`mediaDuration`、`aiCategory`（可为空）、`sendTime`。

### 4.3 提问 Body 示例

**服务端调 `ConsultAiGateway`（无 `aiReplyContent`）**

```json
{
  "msgType": 1,
  "content": "牛不吃草、精神沉郁，请问可能原因？"
}
```

**前端大模型直连后落库（推荐）**

```json
{
  "msgType": 1,
  "content": "牛不吃草、精神沉郁，请问可能原因？",
  "aiReplyContent": "可能原因包括…",
  "realSessionId": "gateway-session-001",
  "aiCategory": "2",
  "costTime": 1523
}
```

| Body 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `msgType` | int | Y | 1 文本 2 图 3 视频 4 语音 |
| `content` | string | 条件 | 文本或媒体 URL |
| `mediaDuration` | int | N | 音视频秒数 |
| `aiReplyContent` | string | N | 非空时服务端**仅落库**，不调 `ConsultAiGateway` |
| `realSessionId` | string | N | 大模型 `session_id`；写入 `biz_consult_session.real_session_id`（别名 `real_session_id`、`session_id`、`llmSessionId`） |
| `aiCategory` | string | N | 写入提问人消息 `ai_category`（别名 `ai_category`、`category`）；最长 32 |
| `costTime` | long | N | 写入 **AI 消息** `cost_time`（别名 `cost_time`、`xCost`）；毫秒，来自大模型响应头 `X-COST` |

**提问成功 `data`**

```json
{
  "userMessage": { "id": 1, "senderRole": 1, "aiCategory": "2", "...": "..." },
  "aiMessage": { "id": 2, "senderRole": 3, "...": "..." },
  "realSessionId": "gateway-session-001"
}
```

### 4.4 业务错误（`msg` 示例）

| 场景 | 文案 |
| --- | --- |
| 越权会话 | 无权查看该会话 |
| 会话不可见 | 会话不存在或已删除 |
| 搜索为空 | 请输入筛选条件 |
| 文本为空 | 请输入消息内容 |
| 媒体为空 | 请上传文件 |
| 文件不合规 | 文件格式或大小不符合要求 |
| AI 超时 | AI 回复超时，请稍后重试 |
| AI 失败 | AI 回复失败，请稍后重试 |
| AI 处理中 | AI 正在回复中，请稍候 |
| AI 提问分类无效 | AI提问分类无效 / AI提问分类长度不能超过32个字符 |

---

## 5. 提问与 AI 回复流程（核心）

```text
1. 校验会话归属（type=2, asker=当前用户, vet_visible=1）
2. 若会话 AI 处理中 → 拒绝（§3.5 #7）
3. 校验 Body（VetConsultValidation、AiConsultValidation.validateAiCategory）
4. 若 Body.realSessionId 非空 → 更新 biz_consult_session.real_session_id
5. 标记会话 processing=1（内存/Redis 锁）
6. INSERT 提问消息（sender_role=1，含 ai_category 若有）
7. 更新 session.last_message_*（提问侧摘要）
8. 推送 NEW_MESSAGE（提问）→ 前端立即展示
9a. 若 aiReplyContent 非空：直接作为 AI 文本，跳过 ConsultAiGateway（前端直连大模型落库通道）
9b. 否则调用 ConsultAiGateway（≤60s，单轮 §3.5 #6）：
   - msg_type=1：文本问答
   - msg_type=2：图文；失败 → §3.5 #4 固定 AI 文案仍入库
   - msg_type=3/4：§3.5 #3 固定引导文案入库
10. 成功：INSERT AI 消息（sender_role=3，ai_category=NULL）
11. 更新 session.last_message_*；首问更新 session_title
12. 推送 NEW_MESSAGE（AI 回复，落库后 ≤1s）
13. 释放 processing；响应 data：{ userMessage, aiMessage, realSessionId? }
14. 网关硬失败（非图片软失败且无 aiReplyContent）：不写 AI 消息；HTTP 错误 + §4.4 文案
```

### 5.1 同步、超时与展示策略（v1.1 已确认）

| 项 | 约定 |
| --- | --- |
| HTTP | `POST .../message` **同步阻塞**等待 AI，最长 **60s**；读超时 **≥65s**（§3.5 #2） |
| 响应体 | 成功时返回 `userMessage` + `aiMessage`；前端发送后即可展示提问，等待态文案「AI 正在思考…」 |
| 推送 | AI 消息落库后 **≤1s** 经 `/topic/consult/ai/{userId}` 推送（与 HTTP 响应**双通道**，以先到者刷新为准） |
| 非流式 | 不做 SSE/逐字；整段 `content` 一次入库（§3.5 #6、功能需求 §5.4 #10） |
| 免责声明 | `disclaimer` 由 **`POST /session`**、**`GET /session/list`** 返回，前端**必须展示**（§3.5 #8） |

---

## 6. 移动端接口

**Base Path**：`/app/consult/ai`  
**鉴权**：登录 Token；**不**要求绑定医疗资源（牧民可用）。

| # | 说明 | Method | URI | 要点 |
| --- | --- | --- | --- | --- |
| M1 | 新建会话 | POST | `/session` | Body 可选 `category`：`1`～`4`；返回 `sessionId`、`promptTemplate`、`disclaimer`；`last_message_time=create_time` |
| M2 | 会话列表 | GET | `/session/list` | `asker_user_id=当前用户`；`contentKeyword`、`searchMode`（**§3.5 #5**）；分页；默认 3 个月 |
| M3 | 历史消息 | GET | `/session/{id}/messages` | 同 4.3 |
| M4 | 提问 | POST | `/session/{id}/message` | 同 4.4 |
| M5 | 隐藏会话 | POST | `/session/{id}/hide` | `vet_visible=0`；管理端列表同步不展示 |

**快捷入口（§5.4 #4）**：`M1` + `category` → 新建会话（**不**复用当前会话）+ 返回模板字符串，由客户端填入输入框。

**模板示例（服务端常量）**

| category | 预填模板（示例） |
| --- | --- |
| 1 | 想了解一些养殖技巧，我的情况是： |
| 2 | 想咨询疾病诊疗问题，症状描述： |
| 3 | 想了解用药指导，具体情况： |
| 4 | 想咨询设备操作问题，设备/现象： |

---

## 7. 近实时、上传、菜单

与在线接诊技术方案 **§5.1、§5.2** 一致；订阅主题为 `/topic/consult/ai/{userId}`。

| 菜单名 | 权限标识 | 路由 component |
| --- | --- | --- |
| AI 诊断（兽医、机构） | `diseaseTreatment:aiOnlineConsult:list` | `diseaseTreatment/onlineConsult/ai/index` |

按钮：`query`、`send`、`add`（新建会话）、`remove`（**隐藏会话**）。角色 **100**、**102**（机构，以平台为准）。权限 SQL：`sql/biz_ai_online_consult_remove_perm.sql`。

---

## 8. 实现要点与类清单

| 项 | 说明 |
| --- | --- |
| 越权 | `sessionId` 操作必须 `consult_type=2` 且 `asker_user_id=当前用户` |
| AI 会话创建 | `receiver_*` 全 NULL；`insert` 后 `session_title=新会话` |
| 后台准入 | 权限注解；无医疗资源绑定校验 |
| 与接诊隔离 | Mapper 方法命名区分 `selectAi*` / `selectVet*` |
| 配置项 | `application.yml`：`consult.ai.endpoint`、`apiKey`、`timeoutMs=60000`（示例键名） |

**建议新增类**

- `AiConsultValidation`（搜索空条件等）  
- `ConsultAiGateway` / `ConsultAiGatewayImpl`  
- `IBizAiOnlineConsultService` / `BizAiOnlineConsultServiceImpl`  
- `BizAiOnlineConsultController`  
- `BizAppAiConsultController`  
- `BizConsultSessionMapper.selectAiSessionList` / `insertAiSession` / `selectAiSessionByIdAndAsker`（XML 补充）

**可继续复用在线接诊已实现的**

- `ConsultSessionRules`、`ConsultRedisPushService`、`BizConsultMessageMapper`

---

## 9. 验收要点

| 编号 | 场景 | 通过标准 |
| --- | --- | --- |
| AI-01 | 新建会话 | 后台 `POST /session` 返回新 `sessionId`；列表出现「新会话」 |
| AI-02 | 提问与回复 | 发送文本后 ≤60s 内返回 AI 消息；落库后 ≤1s 界面可见 |
| AI-03 | 默认/搜索 | 同在线接诊 AC-02、AC-03（`consult_type=2`） |
| AI-04 | 隐藏 | 移动端/管理端 `hide` 后列表无该会话 |
| AI-05 | 隔离 | 接诊列表无 AI 会话；AI 列表无接诊会话 |
| AI-06 | 机构/兽医 | 具备权限即可 list/create，不要求绑定医疗资源 |
| AI-07 | 视频/语音 | 上传后 AI 回复为引导补文字文案，非解析内容 |
| AI-08 | 快捷入口 | 点「疾病诊疗」等 → 新会话 + 预填模板 |
| AI-09 | 空会话列表 | `POST /session` 后未提问，左侧仍可见「新会话」 |
| AI-10 | 处理中拒发 | AI 未返回前再次发送 →「AI 正在回复中，请稍候」 |
| AI-11 | 图片失败 | 识图失败仍有 AI 引导气泡（§3.5 #4） |

---

## 10. 交付清单

- [ ] Mapper/XML：`selectAiSessionList` 等（`consult_type=2`）  
- [ ] `ConsultAiGateway` + 配置  
- [ ] `BizAiOnlineConsultServiceImpl` + 后台 Controller  
- [ ] `BizAppAiConsultController`（M1～M5）  
- [ ] 菜单权限 SQL（**§7**）  
- [ ] 单元测试 + MockMvc（AI 列表/新建/提问 Mock Gateway）  
- [ ] 与在线接诊联调：推送、摘要、3 个月窗一致  

---

## 11. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：共用 biz_consult_*；consult_type=2；后台/移动端接口；AI 回复流程与 60s 超时；关联在线接诊 v1.1；vet_visible 复用为列表可见 |
| 1.1 | 评审补充：§3.5 空会话列表、HTTP 超时、固定文案、图片软失败、移动端搜索、单轮上下文、处理中拒发、免责声明必返；§5.1 双通道；验收 AI-09～11 |
| 1.2 | 补充 §1.2：`KbOpenAiProxyController` 转发 `/v1/models`、`/v1/chat/completions`；区分对话框直连/转发与 `ConsultAiGateway` 兜底 |
| 1.3 | §1.2：`/v1/**` 须若依 Token，取消匿名访问 |
| 1.4 | 管理端 **§4.5** `POST /session/{id}/hide` 隐藏 AI 诊断会话；权限 `...:remove` |
| 1.5 | **§3.1** `biz_consult_session.real_session_id`；**§3.2** `biz_consult_message.ai_category`（varchar32）；**§4.3** 落库 Body/响应字段；**§5** 前端 `aiReplyContent` 落库通道 |
| 1.6 | **§3.0** `biz_diagnosis_user`：新建会话写入首次用户、更新 `last_message_time` 同步 `last_use_time` |
