# 在线接诊(兽医)— 技术方案 > 依据:`在线接诊(兽医)功能需求.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 |