依据:
在线接诊(兽医)功能需求.md(v1.1);关联AI诊断(兽医、机构)功能需求-草稿.md、原型
本文档版本:1.1(评审补充约定,见 §3.4、§4.6、§5、§8)
| 项 | 说明 |
|---|---|
| 整体 | 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 诊断共用)
兽医在线接诊与 AI 诊断 共用两张表,以 consult_type 隔离;禁止混查。
consult_type |
模块 | 会话参与方 | 后台新建会话 |
|---|---|---|---|
1 |
在线接诊(兽医) | 问诊人(牧民)↔ 接诊兽医 | 无(移动端创建/续聊) |
2 |
AI 诊断(兽医、机构) | 提问人(当前登录用户)↔ AI 助手 | 有(后台「新增会话」) |
3 |
专家在线问诊(预留) | 问诊人 ↔ 专家 | 移动端(另模块) |
| 能力 | 兽医在线接诊 | 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,兽医列表不可见 | 不适用(无接诊方) |
消息落库 → 更新 session.last_message_*
→ WebSocket 推送至:
- 兽医后台:/topic/consult/vet/{vetUserId}
- 移动端问诊人:/topic/consult/user/{askerUserId}
- AI 提问人:/topic/consult/ai/{askerUserId}
客户端收到推送后刷新当前会话消息与(若相关)左侧列表摘要。
| # | 说明 | 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 内实现。
biz_consult_session(问诊会话)| 字段 | 类型 | 非空 | 说明 |
|---|---|---|---|
id |
bigint(20) |
Y | 主键 |
consult_type |
tinyint(4) |
Y | 1兽医在线 2AI 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。
biz_consult_message(问诊消息)| 字段 | 类型 | 非空 | 说明 |
|---|---|---|---|
id |
bigint(20) |
Y | 主键 |
session_id |
bigint(20) |
Y | 会话 ID |
sender_role |
tinyint(4) |
Y | 1问诊人/提问人 2接诊人/兽医 3AI 助手 |
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。
默认列表(未点搜索):
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 同会话)。| # | 约定项 | 本期实现 |
|---|---|---|
| 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 同一迭代交付;缺移动端则后台仅可联调/造数,不作为上线验收 |
其他说明
oper_log(后续迭代)。consult_type=2 时 receiver_provider_id 必须为 NULL,禁止写入兽医资源 ID,避免误占 uk_vet_session 语义。统一响应: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 |
rows[] 字段sessionId、askerName、askerAvatar、lastMessagePreview、lastMessageTime(前端格式化为「M月D日」)、sessionTitle(可选)。
rows[] / 推送 payload 字段messageId、sessionId、senderRole、senderName、msgType、content、mediaDuration、sendTime。
senderRole:1 问诊人(左气泡)、2 兽医(右气泡)。
{
"msgType": 1,
"content": "建议先隔离观察"
}
图片/视频/语音:msgType 为 2/3/4,content 为上传接口返回的 URL。
msg 示例)| 场景 | 文案 |
|---|---|
| 未绑定兽医 | 未绑定兽医资源,无法接诊 |
| 越权会话 | 无权查看该会话 |
| 会话不可见 | 会话不存在或已删除 |
| 文本为空 | 请输入消息内容 |
| 媒体为空 | 请上传文件 |
| 文件不合规 | 文件格式或大小不符合要求 |
| 项 | 兽医 | AI |
|---|---|---|
| 列表 Path | .../vet/session/list |
.../ai/session/list |
| 新建会话 | — | POST .../ai/session |
| 发送 | 人发 → 人收 | 人发 → AI 处理 → AI 消息入库 |
vet_visible |
使用 | 不使用 |
| 用户操作 | 请求参数 | 列表范围 |
|---|---|---|
| 进入页面 / 重置 | searchMode=false(或不传);askerName、contentKeyword 清空 |
仅最近 3 个月(§3.4 #2) |
| 点击搜索 | searchMode=true;可带 askerName、contentKeyword(均可空,但至少一项非空否则提示「请输入筛选条件」) |
全量可见会话(仍 vet_visible=1),可含 3 个月外 |
| 仅输入筛选项未点搜索 | 同默认 | 仍仅 3 个月(§3.4 #1) |
| 项 | 说明 |
|---|---|
| 协议 | 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 携带即可)。
1. POST /common/upload(multipart)
2. 校验后缀与大小(§3.4 #7)
3. POST .../message Body: { msgType, content: url, mediaDuration? }
发送失败时 HTTP 返回错误,msg 见 §4.4;不向聊天区插入乐观消息。
| 菜单名 | 权限标识 | 路由 component |
|---|---|---|
| 在线接诊(兽医) | diseaseTreatment:vetOnlineConsult:list |
diseaseTreatment/onlineConsult/vet/index |
按钮:query、send。角色 100(兽医)绑定;机构/专家无此菜单。
| # | 说明 | 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 核心逻辑。
| 项 | 说明 |
|---|---|
| 越权 | 所有 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 / BizConsultMessageMapperVetOnlineConsultServiceImplConsultWebSocketHandler + 推送服务| 编号 | 场景 | 通过标准 |
|---|---|---|
| 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 会话不出现在接诊列表 |
sql/biz_consult_session.sql、sql/biz_consult_message.sqlConsultSessionRules + Mapper/XML(含 searchMode、3 个月窗、摘要写入)VetOnlineConsultController + Service + STOMP + Redis 推送AiOnlineConsultController(关联模块,可次迭代;须 receiver_provider_id=NULL)| 版本 | 说明 |
|---|---|
| 1.0 | 初稿:共用问诊 IM 表;兽医接口;WebSocket≤1s;3 个月默认可搜;vet_visible;AI 诊断关联路径;不绑定预约 |
| 1.1 | 评审补充:searchMode 与搜索/重置;3 个月自然日;摘要规则;askerAvatar;消息分页;STOMP+Redis;上传规范;移动端同迭代;验收用例 AC-01~07 |