# 在线接诊（兽医）— 技术方案

> 依据：`在线接诊(兽医)功能需求.md`（v1.1）；关联 `AI诊断(兽医、机构)功能需求-草稿.md`、原型  
> **本文档版本：1.1**（评审补充约定，见 **§3.4、§4.6、§5、§8**）

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **整体** | RuoYi **v3.9.2**（**springboot2** 分支）单体后端 + 若依 **Vue2** 管理端 + 移动端（App/小程序） |
| **运行时** | JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| **数据库** | MySQL **5.7.39**，InnoDB，`utf8mb4` |
| **近实时** | **STOMP over WebSocket** + 落库后推送（见 **§5**）；端到端 **≤ 1 秒** |
| **文件** | 复用 `POST /common/upload`（见 **§5.2**）；消息表存 URL；音视频**仅文件上传** |
| **兽医资源** | `biz_medical_resource`（`resource_type=1`，`sys_user_id` 绑定兽医） |
| **预约** | **本期不关联** `biz_service_appointment`（不校验预约状态） |

**后端分层（兽医在线接诊）**

| 层 | 职责 |
| --- | --- |
| `Controller` | 会话列表、历史消息、发送消息；`@PreAuthorize` |
| `WebSocket` | 订阅会话频道；新消息推送列表摘要与聊天区 |
| `Service` | 解析当前兽医、会话 scope、`vet_visible` 过滤、消息发送 |
| `Mapper` + XML | `biz_consult_session` / `biz_consult_message` |
| `support` | `ConsultSessionRules`、`VetConsultValidation`、`VetProviderResolver`（复用 diagnosis） |

**代码包**：`com.ruoyi.web.modules.diagnosis`  
**初始化脚本**：`sql/biz_consult_session.sql`、`sql/biz_consult_message.sql`（与 AI 诊断**共用**）

---

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

### 2.1 统一问诊 IM 数据模型

兽医**在线接诊**与 **AI 诊断** 共用两张表，以 **`consult_type`** 隔离；**禁止**混查。

| `consult_type` | 模块 | 会话参与方 | 后台新建会话 |
| --- | --- | --- | --- |
| **`1`** | **在线接诊（兽医）** | 问诊人（牧民）↔ 接诊兽医 | **无**（移动端创建/续聊） |
| **`2`** | **AI 诊断（兽医、机构）** | 提问人（当前登录用户）↔ AI 助手 | **有**（后台「新增会话」） |
| `3` | 专家在线问诊（预留） | 问诊人 ↔ 专家 | 移动端（另模块） |

### 2.2 三端能力对照

| 能力 | 兽医在线接诊 | AI 诊断 |
| --- | --- | --- |
| Base Path | `/diseaseTreatment/onlineConsult/vet` | `/diseaseTreatment/onlineConsult/ai` |
| 使用角色 | 兽医（100） | 兽医 + 机构（102 等） |
| 列表筛选项 | `askerName`、`contentKeyword` | `contentKeyword` |
| 默认列表 | 最近 **3 个月** `last_message_time` | 同左 |
| 超窗检索 | **搜索**命中更早会话 | 同左 |
| 列表主标题 | `askerName` | `sessionTitle` |
| 右侧标题 | `askerName` | 固定「AI 助手」 |
| 发送方 | 人 ↔ 人 | 人 → AI；AI 回复由 **AI 服务**写入（另接） |
| `vet_visible` | 牧民删除后 **0**，兽医列表不可见 | 不适用（无接诊方） |

### 2.3 近实时推送（共用）

```text
消息落库 → 更新 session.last_message_* 
         → WebSocket 推送至：
            - 兽医后台：/topic/consult/vet/{vetUserId}
            - 移动端问诊人：/topic/consult/user/{askerUserId}
            - AI 提问人：/topic/consult/ai/{askerUserId}
```

客户端收到推送后刷新当前会话消息与（若相关）左侧列表摘要。

### 2.4 与「我的预约（兽医）」

- **无表关联**、**无接口校验**预约状态。  
- 预约与问诊 IM **独立产品能力**。

### 2.5 AI 诊断接口摘要（关联实现）

| # | 说明 | Method | URI（示例） |
| --- | --- | --- | --- |
| A1 | 历史会话列表 | GET | `/ai/session/list` |
| A2 | 新增会话 | POST | `/ai/session` |
| A3 | 消息列表 | GET | `/ai/session/{id}/messages` |
| A4 | 用户提问（触发 AI 回复） | POST | `/ai/session/{id}/message` |

AI 回复消息由异步任务调用大模型后 `INSERT`（`sender_role=AI`），推送至提问人；**不在**兽医接诊 Controller 内实现。

---

## 3. 数据库设计

### 3.1 表 `biz_consult_session`（问诊会话）

| 字段 | 类型 | 非空 | 说明 |
| --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 主键 |
| `consult_type` | `tinyint(4)` | Y | **1**兽医在线 **2**AI **3**专家（预留） |
| `asker_user_id` | `bigint(20)` | Y | 问诊人/提问人 `sys_user_id` |
| `asker_name` | `varchar(64)` | Y | 问诊人姓名快照 |
| `receiver_user_id` | `bigint(20)` | N | 接诊人 `sys_user_id`（AI 会话可为 NULL） |
| `receiver_provider_id` | `bigint(20)` | N | 兽医/专家 `biz_medical_resource.id`（AI 为 NULL） |
| `receiver_name` | `varchar(64)` | N | 接诊人姓名快照 |
| `session_title` | `varchar(200)` | N | 问题/问答标题（AI 列表主标题） |
| `real_session_id` | `varchar(64)` | N | 大模型网关 `session_id`（AI 诊断落库写入，见 AI 诊断方案 §3.1） |
| `last_message_time` | `datetime` | N | 最后一条消息时间（列表排序、3 个月窗） |
| `last_message_preview` | `varchar(500)` | N | 列表摘要（文本截断或 `[图片]` 等） |
| `vet_visible` | `tinyint(4)` | Y | **1**兽医可见 **0**牧民删除后对兽医隐藏（仅 `consult_type=1` 使用） |
| `create_time` | `datetime` | N | 创建时间 |
| `update_time` | `datetime` | N | 更新时间 |

**索引**

- `PRIMARY KEY (id)`  
- `UNIQUE KEY uk_vet_session (consult_type, asker_user_id, receiver_provider_id)`（`consult_type=1` 且 `receiver_provider_id` 非空时保证一对一）  
- `KEY idx_receiver (consult_type, receiver_user_id, receiver_provider_id, last_message_time)`  
- `KEY idx_asker (consult_type, asker_user_id, last_message_time)`  
- `KEY idx_vet_visible (consult_type, vet_visible, last_message_time)`

**DDL**：见 `sql/biz_consult_session.sql`。

### 3.2 表 `biz_consult_message`（问诊消息）

| 字段 | 类型 | 非空 | 说明 |
| --- | --- | --- | --- |
| `id` | `bigint(20)` | Y | 主键 |
| `session_id` | `bigint(20)` | Y | 会话 ID |
| `sender_role` | `tinyint(4)` | Y | **1**问诊人/提问人 **2**接诊人/兽医 **3**AI 助手 |
| `sender_user_id` | `bigint(20)` | N | 发送方用户 ID（AI 可为 0） |
| `sender_name` | `varchar(64)` | N | 发送方姓名快照 |
| `msg_type` | `tinyint(4)` | Y | **1**文本 **2**图片 **3**视频 **4**语音 |
| `content` | `text` | N | 文本内容或媒体 URL |
| `media_duration` | `int(11)` | N | 语音/视频时长（秒，可选） |
| `ai_category` | `varchar(32)` | N | AI 提问分类（仅 AI 诊断提问人消息，见 AI 诊断方案 §3.2） |
| `cost_time` | `bigint(20)` | N | 大模型耗时毫秒（仅 AI 诊断 AI 消息，见 AI 诊断方案 §3.2） |
| `send_time` | `datetime` | Y | 发送时间 |
| `create_time` | `datetime` | N | 创建时间 |

**索引**：`PRIMARY KEY (id)`；`KEY idx_session_time (session_id, send_time)`；`KEY idx_session_id (session_id)`。

**DDL**：见 `sql/biz_consult_message.sql`。

### 3.3 兽医列表查询语义

**默认列表**（未点搜索）：

```sql
WHERE consult_type = 1
  AND receiver_provider_id = #{vetProviderId}
  AND vet_visible = 1
  AND last_message_time >= #{threeMonthsAgo}
ORDER BY last_message_time DESC
```

**搜索**（仅当请求参数 **`searchMode=true`** 时生效，见 **§4.6**）：

- 仍 `vet_visible = 1`；**不限制** 3 个月。  
- `askerName` → `asker_name LIKE`（非空时）。  
- `contentKeyword` → 匹配 `last_message_preview`、`session_title`，或子查询 `biz_consult_message.content`（`msg_type=1` 文本，`consult_type=1` 同会话）。

### 3.4 实现约定（v1.1 已确认）

| # | 约定项 | 本期实现 |
| --- | --- | --- |
| 1 | **列表「搜索 / 重置」** | 仅用户点击**搜索**时传 `searchMode=true`；**重置**清空筛选项并传 `searchMode=false`（或未传），恢复默认 3 个月列表。筛选项有值但未点搜索时，**仍按默认 3 个月**查询 |
| 2 | **3 个月时间窗** | 服务器时区（`Asia/Shanghai`）**自然日**：`threeMonthsAgo = 今日 00:00:00` 往前推 **3 个自然月**（含今日当天有最后消息的会话）。例：今日 2026-05-20 → `last_message_time >= 2026-02-20 00:00:00` |
| 3 | **`last_message_preview`** | 发送成功后由服务端写入：`msg_type=1` 取文本去空白后**最多 50 字**；`2/3/4` 固定为 `[图片]` / `[视频]` / `[语音]` |
| 4 | **列表头像** | 列表接口返回 `askerAvatar`：`sys_user.avatar`；为空时前端用**默认占位图**（与原型一致） |
| 5 | **历史消息首次加载** | `GET .../messages` 未传 `beforeId`：按 `send_time DESC` 取最新 **50** 条，响应体内**按 `send_time` 升序**排列供聊天区展示；上拉更早传 `beforeId`（当前页最小 `messageId`），再取 50 条。详情区**不受**列表 3 个月限制 |
| 6 | **WebSocket** | 采用 **STOMP**（Spring WebSocket + SockJS 可选）；事件类型 `NEW_MESSAGE`（含 **§4.2** 字段 + `sessionId`、`lastMessagePreview`、`lastMessageTime` 供左侧列表更新）。**多实例**：推送经 **Redis Pub/Sub** 广播至各节点再下发（若依集群部署时必配） |
| 7 | **媒体上传** | 先 `POST /common/upload`，消息 `content` 存返回 **`url`**。限制：**图片** jpg/jpeg/png/gif，≤ **10MB**；**视频** mp4/mov，≤ **50MB**；**语音** mp3/m4a/wav，≤ **10MB**（与平台统一规范冲突时以平台为准） |
| 8 | **迭代里程碑** | **兽医后台**与**移动端** `M1` 打开会话、`M2` 发消息、`M4` hide **同一迭代交付**；缺移动端则后台仅可联调/造数，不作为上线验收 |

**其他说明**

- **i18n**：后端存原文；界面汉语/藏文由前端 i18n，接口不单独传语言包。  
- **操作审计**：本期不单独建审计表；若平台要求查看/发送留痕，复用若依 `oper_log`（后续迭代）。  
- **AI 会话**：`consult_type=2` 时 **`receiver_provider_id` 必须为 NULL**，禁止写入兽医资源 ID，避免误占 `uk_vet_session` 语义。

---

## 4. 接口设计（兽医在线接诊）

统一响应：RuoYi `AjaxResult` / `TableDataInfo`；WebSocket 见 **§5**。

**权限（示例）**：`diseaseTreatment:vetOnlineConsult:list|query|send`

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

| # | 说明 | Method | URI | 权限 | 要点 |
| --- | --- | --- | --- | --- |
| 4.1 | 接诊会话列表 | GET | `/session/list` | `...:list` | Query：`pageNum`、`pageSize`（默认 20）、`askerName`、`contentKeyword`、**`searchMode`**（**仅「搜索」为 `true`**，见 **§4.6**）；服务端注入 `receiverProviderId`，禁止前端传 |
| 4.2 | 历史消息 | GET | `/session/{sessionId}/messages` | `...:query` | 校验会话归属与 `vet_visible=1`；未传 `beforeId` 取最新 50 条（**§3.4 #5**）；传 `beforeId` 上拉更早；`pageSize` 默认 50；响应 `rows` **升序** |
| 4.3 | 发送消息 | POST | `/session/{sessionId}/message` | `...:send` | Body：`msgType`（1～4）、`content`（文本或上传后的 URL）、`mediaDuration`（可选）；落库后 **WebSocket 推送**；更新 `last_message_*` 与 **`update_time`** |

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

`sessionId`、`askerName`、`askerAvatar`、`lastMessagePreview`、`lastMessageTime`（前端格式化为「M月D日」）、`sessionTitle`（可选）。

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

`messageId`、`sessionId`、`senderRole`、`senderName`、`msgType`、`content`、`mediaDuration`、`sendTime`。

`senderRole`：1 问诊人（左气泡）、2 兽医（右气泡）。

### 4.3 发送 Body 示例

```json
{
  "msgType": 1,
  "content": "建议先隔离观察"
}
```

图片/视频/语音：`msgType` 为 2/3/4，`content` 为上传接口返回的 URL。

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

| 场景 | 文案 |
| --- | --- |
| 未绑定兽医 | 未绑定兽医资源，无法接诊 |
| 越权会话 | 无权查看该会话 |
| 会话不可见 | 会话不存在或已删除 |
| 文本为空 | 请输入消息内容 |
| 媒体为空 | 请上传文件 |
| 文件不合规 | 文件格式或大小不符合要求 |

### 4.5 与 AI 诊断接口差异

| 项 | 兽医 | AI |
| --- | --- | --- |
| 列表 Path | `.../vet/session/list` | `.../ai/session/list` |
| 新建会话 | — | `POST .../ai/session` |
| 发送 | 人发 → 人收 | 人发 → AI 处理 → AI 消息入库 |
| `vet_visible` | 使用 | 不使用 |

### 4.6 列表「搜索 / 重置」交互（与前端约定）

| 用户操作 | 请求参数 | 列表范围 |
| --- | --- | --- |
| 进入页面 / **重置** | `searchMode=false`（或不传）；`askerName`、`contentKeyword` 清空 | 仅最近 **3 个月**（**§3.4 #2**） |
| 点击**搜索** | `searchMode=true`；可带 `askerName`、`contentKeyword`（均可空，但至少一项非空否则提示「请输入筛选条件」） | **全量**可见会话（仍 `vet_visible=1`），可含 3 个月外 |
| 仅输入筛选项未点搜索 | 同默认 | **仍仅 3 个月**（**§3.4 #1**） |

---

## 5. 近实时、上传与部署

### 5.1 WebSocket（STOMP）

| 项 | 说明 |
| --- | --- |
| 协议 | **STOMP over WebSocket**；管理端 SockJS 端点示例 `/ws/consult` |
| 鉴权 | 握手携带与 HTTP 相同 **Token**；服务端绑定 `userId` |
| 兽医订阅 | `/topic/consult/vet/{sysUserId}` |
| 牧民订阅 | `/topic/consult/user/{sysUserId}` |
| 推送事件 | **`NEW_MESSAGE`**：payload = **§4.2** 消息字段 + `sessionId` + `lastMessagePreview` + `lastMessageTime`（左侧列表同步更新） |
| 多实例 | 落库后发布至 **Redis 频道** `consult:push`，各节点订阅后向本机 STOMP 连接推送 |
| 延迟 | 落库 → Redis/本机推送 → 客户端渲染，验收 **≤ 1 s**（**§8**） |

**本期不做**：已读回执、正在输入、消息撤回、独立 `SESSION_SUMMARY` 事件（摘要随 `NEW_MESSAGE` 携带即可）。

### 5.2 媒体上传

```text
1. POST /common/upload（multipart）
2. 校验后缀与大小（§3.4 #7）
3. POST .../message  Body: { msgType, content: url, mediaDuration? }
```

发送失败时 HTTP 返回错误，`msg` 见 **§4.4**；**不**向聊天区插入乐观消息。

### 5.3 菜单与权限（示例）

| 菜单名 | 权限标识 | 路由 component |
| --- | --- | --- |
| 在线接诊（兽医） | `diseaseTreatment:vetOnlineConsult:list` | `diseaseTreatment/onlineConsult/vet/index` |

按钮：`query`、`send`。角色 **100**（兽医）绑定；机构/专家**无**此菜单。

---

## 6. 移动端接口（与兽医后台同迭代）

| # | 说明 | Method | URI（示例） | 要点 |
| --- | --- | --- | --- | --- |
| M1 | 打开/创建兽医会话 | POST | `/app/onlineConsult/session/open` | Body：`vetResourceId`；`consult_type=1`；写入 `receiver_user_id`/`receiver_name`（来自资源绑定账号）；有则返回已有 `sessionId` |
| M2 | 发送消息 | POST | `/app/onlineConsult/session/{id}/message` | 问诊人；`sender_role=1`；落库后同 **§5.1** 推送给兽医 |
| M3 | 会话列表 | GET | `/app/onlineConsult/session/list` | 牧民消息 Tab；按 `asker_user_id=当前用户` |
| M4 | 删除会话（对兽医隐藏） | POST | `/app/onlineConsult/session/{id}/hide` | 校验 `asker_user_id=当前用户`；`vet_visible=0` |

**里程碑（§3.4 #8）**：M1/M2/M4 与兽医后台 **4.1～4.3 + WebSocket** 同版本上线；M3 可与 M1 同批。

移动端控制器：`BizAppOnlineConsultController`（`/app/onlineConsult`），详见 `doc/app/在线问诊/在线问诊接口说明.md`；**复用** `IBizVetOnlineConsultService` 核心逻辑。

---

## 7. 实现要点

| 项 | 说明 |
| --- | --- |
| 越权 | 所有 `sessionId` 操作校验 `consult_type=1` + `receiver_provider_id=当前兽医` + `vet_visible=1` |
| 会话唯一 | `open` 时 `SELECT` 已有会话，避免重复建多条 |
| 媒体 | 发送前走统一上传；校验扩展名与大小（平台规范） |
| 分页 | 见 **§3.4 #5**；聊天区升序；上拉 `beforeId` |
| 摘要更新 | 每次发送成功后更新 `last_message_time`、`last_message_preview`（**§3.4 #3**） |
| 组件路径 | `diseaseTreatment/onlineConsult/vet/index` |
| AI 模块 | 同包 `AiOnlineConsultController`，`consult_type=2`，列表含 `POST` 新建会话 |

**建议类**

- `ConsultSessionRules`（类型、角色、消息类型常量）  
- `BizConsultSessionMapper` / `BizConsultMessageMapper`  
- `VetOnlineConsultServiceImpl`  
- `ConsultWebSocketHandler` + 推送服务  

---

## 8. 验收要点（v1.1）

| 编号 | 场景 | 通过标准 |
| --- | --- | --- |
| AC-01 | 近实时 | 兽医后台与移动端**同时在线**；任一端发文本，对端 **≤ 1 s** 出现消息且左侧摘要更新 |
| AC-02 | 默认列表 | 仅展示 `last_message_time` 在 3 个月窗内的会话；超窗会话**不出现** |
| AC-03 | 搜索超窗 | 对超窗会话点**搜索**（`searchMode=true`）可命中；**重置**后该会话从默认列表消失 |
| AC-04 | 牧民删除 | `hide` 后兽医列表无该会话；详情/发消息返回「会话不存在或已删除」 |
| AC-05 | 越权 | 兽医 A 无法访问兽医 B 的 `sessionId` |
| AC-06 | 媒体 | 图片/视频/语音文件上传发送成功；超大或非法后缀返回 **§4.4** 文案 |
| AC-07 | 与 AI 隔离 | 兽医列表 SQL 仅 `consult_type=1`；AI 会话不出现在接诊列表 |

---

## 9. 交付清单

- [ ] `sql/biz_consult_session.sql`、`sql/biz_consult_message.sql`  
- [ ] `ConsultSessionRules` + Mapper/XML（含 `searchMode`、3 个月窗、摘要写入）  
- [ ] `VetOnlineConsultController` + Service + **STOMP** + **Redis** 推送  
- [ ] 移动端 **M1/M2/M4**（同迭代，**§3.4 #8**）  
- [ ] `AiOnlineConsultController`（关联模块，可次迭代；须 `receiver_provider_id=NULL`）  
- [ ] 菜单权限 SQL（**§5.3**）  
- [ ] 单元测试 + MockMvc + **§8** 联调用例  

---

## 10. 修订记录

| 版本 | 说明 |
| --- | --- |
| 1.0 | 初稿：共用问诊 IM 表；兽医接口；WebSocket≤1s；3 个月默认可搜；vet_visible；AI 诊断关联路径；不绑定预约 |
| 1.1 | 评审补充：searchMode 与搜索/重置；3 个月自然日；摘要规则；askerAvatar；消息分页；STOMP+Redis；上传规范；移动端同迭代；验收用例 AC-01～07 |
