本文面向完全没有 IM(即时通讯)经验的前端开发者 ,用生活化的类比手把手带你对接腾讯云 IM。教程以一个「医患一对一问诊」的真实场景为例,覆盖初始化登录、收发文本消息、发送自定义消息(影像/检验报告卡片)、授权流程、常见坑点与调试技巧。
注:本文仅仅是知识分享与个人学习,不包含任何涉密内容
文档范围与结构说明(重要)
- 本文当前覆盖 医生端(移动端 H5) ,代码示例参考自真实项目
doctor-h5-view,基于@tencentcloud/chat(新版 Chat SDK)。- 患者端实现已在第 4 章完整展开,与医生端(第 1--3 章)共用第 0 章共享基座与第 5 章职责边界。
- 第 0 章「共享基座」与第 5 章「职责边界」为医生端 / 患者端共用,两端必须对齐。
- 文档中凡标注
【待确认:患者端】的区域,为在真实患者端仓库落地时需与后端接口核对的细节。
目录
- [共享基座:腾讯 IM 黑话、SDK、登录、自定义消息信封(两端共用)](#共享基座:腾讯 IM 黑话、SDK、登录、自定义消息信封(两端共用))
- [医生端实现(模块 A)](#医生端实现(模块 A))
- [医生端 ------ 会话、历史消息与收发文本](#医生端 —— 会话、历史消息与收发文本)
- [医生端 ------ 报告授权卡片(影像/检验)全流程](#医生端 —— 报告授权卡片(影像/检验)全流程)
- [患者端实现(模块 B)](#患者端实现(模块 B))
- 两端职责、数据交互与接口边界(两端共用)
- 常见坑点与调试建议
- 小结与学习路线
0. 共享基座(两端共用)
0.1 先搞懂几个"黑话"
腾讯 IM(即时通讯)是腾讯云提供的一套"聊天基础设施",可类比成一家专门帮人盖"聊天邮局"的公司:你不用自己搭 WebSocket、消息存储、离线推送,只要"租用"邮局,再在前端装一个"收发室软件"(SDK)。
| 黑话 | 生活化类比 | 它是什么(本项目对应) |
|---|---|---|
| SDKAppID | 邮局的"门牌号" | 腾讯分配的应用唯一 ID,写在配置里(非秘密) |
| UserID | 用户的"工号" | 医生端形如 doctor_<instanceUserId>,患者端形如 patient_<userId>;由各自后端生成 |
| UserSig | 用户的"临时门禁卡" | 后端用密钥签发的加密串,前端绝不能持有 SecretKey |
| Chat SDK | 收发室"对讲机" | 本项目用 @tencentcloud/chat(tim-js-sdk 已为旧版),导入 ChatSDK |
| Conversation | "聊天窗口" | 本项目医患问诊用 GROUP 群会话 (conversationType: 'GROUP'),群 ID = 订单号 orderNo |
| Message | 一封"信件" | 文本、图片、自定义消息都是 Message |
| 自定义消息(Custom Message) | 能塞任意 JSON 的"信封" | 普通信封装文字;自定义消息在前缀 TIMCustomElem 下让你塞任意结构、前端自己画卡片 |
💡 一句话:
SDKAppID=邮局,UserID=是谁,UserSig=门禁卡,ChatSDK=对讲机,Message=信,自定义消息=能塞任意内容的信封。
0.2 依赖安装
bash
# 新版 Web SDK(本项目使用)
pnpm add @tencentcloud/chat
# 发图片/文件需要上传插件(按需)
pnpm add @tencentcloud/chat-upload-plugin
⚠️ 文档旧版示例中的
tim-js-sdk/tim-upload-plugin为已被取代的旧包,请勿在新项目中使用。
0.3 最小配置
ts
// im/config.ts ------ 把"门牌号"放这里
export const SDKAppID = 16000xxxx; // 替换成你的
export const IM_SERVER = "xxx"; // 你们后端 IM 相关接口域名
0.4 初始化 SDK 单例(两端共用思路)
ts
// 关键约束:整个应用只创建一次 Chat 实例,存成单例
import { ChatSDK } from "@tencentcloud/chat";
import { SDKAppID } from "./config";
// 全局单例(注意不要每次进页面都 create 一遍,否则监听会重复、消息重复)
export const tim = ChatSDK.getInstance();
tim.setSDKAppID(SDKAppID);
🔑 坑点 1 :实例必须单例。
ChatSDK.getInstance()本身就是单例工厂;若自行new或重复setSDKAppID,会出现多实例"打架"、消息监听重复触发。
0.5 登录(刷"门禁卡"进门)
ts
// 1) 先找你们后端要 userSig
const { data } = await getImParams(); // 后端接口 hospital/chatRoom/getImParams
const userSig = data.userSig;
const userID = data.userID; // 医生端形如 doctor_xxx
// 2) 登录
await tim.login({ userID, userSig });
🔑 坑点 2 :
userSig有有效期 (默认 180 天,后端可配)。登录报错70001多为签名过期或 UserID 不匹配。上线后需做"登录失效 → 重新获取 userSig → 重新登录"的兜底。必须等login的 Promise resolve 之后再发消息 ,否则报"未登录"。建议用SDK_READY事件 / 全局imReadyflag 守门。
0.6 自定义消息"标准信封"(两端强一致约定)
本项目自定义消息采用双层信封 结构。新版 SDK 中 message.payload.data 已是已解析对象 (无需再 JSON.parse,旧版 tim-js-sdk 才需要),其标准结构为:
ts
// 自定义消息 payload.data 的标准信封
{
type: string, // 路由类型,决定渲染哪种卡片(见下方"消息 type 字典表")
data: string, // 业务子类型/标识,部分卡片用此字段进一步分流
description: string, // 卡片展示用的简要文案
extension: string, // 扩展字段(JSON 字符串),真正业务参数放这里
}
- 真正业务参数(如
reportType、authType、url、patientId、orderId)放在extension内(字符串化的 JSON)。 - 渲染侧先按
type分流,再按data/extension取业务字段。
⚠️ 与旧版的关键差异 :旧
tim-js-sdk的payload.data是字符串 ,需JSON.parse;新版@tencentcloud/chat已自动解析为对象。切换 SDK 时此处极易出错。
0.7 自定义消息 type 字典表(两端共用,必须对齐)
| type(外层) | data / 子类型 | 由哪端发出 | 由哪端渲染 | 说明 |
|---|---|---|---|---|
TIMTextElem |
--- | 医生/患者 | 两端 | 文本消息 |
TIMImageElem |
--- | 医生/患者 | 两端 | 图片消息 |
PRECONSULTATION |
--- | 系统/医生 | 医生 | 预问诊卡片 |
LIVE_STATUS_NOTIFY |
--- | 系统 | 医生 | 音视频通话状态 |
AUDIO_STATUS_NOTIFY |
--- | 系统 | 医生 | 语音状态 |
CUSTOM_VIDEO / CUSTOM_AUDIO |
--- | 医生 | 医生 | 音视频消息 |
COUNSEL_ASSESS |
--- | 系统 | 医生 | 问诊评价 |
TW_RPLIST |
--- | 系统 | 医生 | 处方列表 |
HOSPITAL_PASS_ADVICE |
--- | 医生 | 医生 | 住院证 |
CASE_ADVICE |
--- | 医生 | 医生 | 病历建议 |
IMAGING_REPORT_AUTH_REQUEST |
IMAGING_REPORT_AUTH_REQUEST |
医生 | 患者 | 影像报告授权请求(医生端发送、患者端 PatientAuthDialog 弹窗渲染) |
IMAGING_REPORT_AUTH_APPROVE |
IMAGING_REPORT_AUTH_APPROVE |
患者 | 医生 | 影像授权同意(患者端发、医生端 ImagingReportAuthCard 渲染) |
IMAGING_REPORT_AUTH_REJECT |
IMAGING_REPORT_AUTH_REJECT |
患者 | 医生 | 影像授权拒绝(患者端发、医生端渲染) |
LAB_TEST_REPORT_AUTH_REQUEST |
LAB_TEST_REPORT_AUTH_REQUEST |
医生 | 患者 | 检验报告授权请求(医生端发送、患者端弹窗渲染) |
LAB_TEST_REPORT_AUTH_APPROVE |
LAB_TEST_REPORT_AUTH_APPROVE |
患者 | 医生 | 检验授权同意(患者端发、医生端渲染) |
LAB_TEST_REPORT_AUTH_REJECT |
LAB_TEST_REPORT_AUTH_REJECT |
患者 | 医生 | 检验授权拒绝(患者端发、医生端渲染) |
FILE-PDF |
--- | 医生/患者 | 两端 | PDF 文件 |
OFFLINE_SOURCE |
--- | 系统 | 医生 | 离线资源 |
CUSTOM_QUOTE |
--- | 医生/患者 | 两端 | 引用回复 |
TW_QUESTION / MEDICAL_RECORD / OFFLINE_MEDICAL_RECORD |
--- | 系统/医生 | 医生 | 各类业务卡片 |
SYS_PLAIN / SEND_URGENT_MESSAGE / TW_WAIT_FOR_FINISH |
--- | 系统 | 医生 | 系统提示 |
📌 此表为医生端实现中实际出现的类型集合;患者端补全时,需把患者端实际收发/渲染的类型补进同一张表,并与医生端对齐。
0.7.1 授权请求 extension 字段契约表(两端强一致)
授权类自定义消息(*_AUTH_REQUEST)的 extension 是 JSON 字符串,以下字段为医生端发送、患者端 authHelper.parseAuthRequestData 解析的共同契约 ,缺字段会导致患者端弹窗显示 undefined:
| extension 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
authType |
"IMAGING_REPORT" | "LAB_TEST_REPORT" |
是 | 决定报告类别与配色 |
authTitle |
string | 是 | 弹窗标题(如"影像报告"),缺省回退"报告" |
doctorName |
string | 是 | 申请医生姓名 |
patientName |
string | 否 | 患者姓名 |
consultationId |
string | 否 | 问诊 ID |
orderId |
string | 否 | 订单 ID(= 群 groupID) |
timestamp |
number | 否 | 请求时间戳,用于响应回带 originalTimestamp |
⚠️ 医生端发请求时务必写入
authType/authTitle/doctorName;患者端parseAuthRequestData对缺失字段不做强校验,漏传会在 UI 出现undefined,联调时重点核对上表。
0.8 消息渲染分发思路(两端共用)
消息进入渲染层后,按 message.type(如 TIMTextElem / TIMCustomElem)先分流元素类型;对自定义消息再读 payload.data.type 决定具体卡片组件。医生端实现见 src/pages/chat/components/ChatContent.vue,其外壳用 message.flow(in=对方发来 / out=自己发出)决定左右布局。
1. 医生端实现(模块 A)
模块 A 覆盖医生端移动端 H5 的完整链路,对应仓库:doctor-h5-view → src/pages/chat/* + src/store/modules/chat/*。
1.1 分层结构
typescript
im.ts ← SDK 单例 + 事件监听 + 登录 + 消息构造封装
store/modules/chat ← Pinia store:消息列表、当前会话、授权 flag
pages/chat/index.vue ← 会话页:拉取历史、启动授权轮询定时器、组装参数
components/
ChatContent.vue ← 消息列表渲染分发(按 type/flow)
ChatFooter.vue ← 底部输入 + 更多功能入口
moreFunctions.vue ← "更多"面板:发起报告授权、查看报告
messageType/
ImagingReportAuthCard.vue ← 影像/检验授权卡片渲染
...(文本/图片/其他卡片)
utils/authHelper.ts ← 解析/构造授权响应消息的辅助(患者端复用)
📌 建议业务组件不要直接拼 SDK ,统一通过
im.ts提供的getTim()+createCustomMessage封装收发,降低耦合。
1.2 初始化与登录(医生端)
ts
// src/pages/chat/im.ts(简化)
import { ChatSDK } from "@tencentcloud/chat";
import { getImParams } from "@/api/chat";
let _tim: any = null;
export function getTim() {
if (!_tim) {
_tim = ChatSDK.getInstance();
_tim.setSDKAppID(SDKAppID);
bindListeners(_tim); // 监听 MESSAGE_RECEIVED / CONV_LIST_UPDATED / KICKED_OUT / ERROR 等
}
return _tim;
}
export async function initDoctorIm() {
const { data } = await getImParams(); // 后端下发 userID + userSig
await getTim().login({ userID: data.userID, userSig: data.userSig });
}
事件监听要点(对应 im.ts 的 bindListeners):
ts
tim.on(ChatSDK.EVENT.MESSAGE_RECEIVED, (event) => {
// event.data 是消息数组,推入 Pinia chatStore 的消息列表
chatStore[ChatActionTypes.PUSH_NEW_MESSAGES](event.data);
});
tim.on(ChatSDK.EVENT.CONV_LIST_UPDATED, (event) => { /* 更新会话列表 */ });
tim.on(ChatSDK.EVENT.KICKED_OUT, () => { /* 多端登录被踢,提示并重新登录 */ });
tim.on(ChatSDK.EVENT.ERROR, (event) => { /* SDK 错误/网络异常兜底 */ });
tim.on(ChatSDK.EVENT.SDK_READY, () => { imReady = true; });
🔑 坑点 3 :监听要在
login之前注册,否则可能漏收登录前的事件。网络断线后 SDK 会自动重连并补推离线消息,但ERROR事件需自行兜底提示。
2. 医生端 ------ 会话、历史消息与收发文本
2.1 进入会话、拉历史消息(分页)
ts
// 医生端用 GROUP 会话,conversationID 来自路由;群 ID = orderNo
const conversationID = route.query.conversationID as string;
// 拉历史消息(向下分页加载,避免长会话一次性拉取卡顿)
async function optimizedGetMessageList(conversationID: string) {
const tim = getTim();
const { data } = await tim.getMessageList({ conversationID, count: 15 });
// data.messageList 是历史消息;data.nextReqMessageID 用于向上翻页
chatStore[ChatActionTypes.SET_MESSAGE_LIST](data.messageList);
nextReqMessageID.value = data.nextReqMessageID;
}
// 继续上拉加载更早消息:tim.getMessageList({ conversationID, nextReqMessageID, count })
📌 性能 :长会话务必分页(
count控制每页条数 +nextReqMessageID翻页),并在 UI 上做下拉加载更多,避免一次性渲染几千条消息导致卡顿。
2.2 收发文本消息
ts
// 发送文本(在 ChatFooter.vue 输入框回车/发送事件里)
async function sendText(text: string) {
const tim = getTim();
const message = tim.createTextMessage({
to: route.query.groupID as string, // 群 ID(orderNo)
conversationType: ChatSDK.TYPES.CONV_GROUP,
payload: { text },
});
const res = await tim.sendMessage(message);
// 发送成功后 res.data.message 立即可 push 到本地列表做"自己可见"
chatStore[ChatActionTypes.PUSH_NEW_MESSAGES]([res.data.message]);
}
接收文本无需单独写代码------MESSAGE_RECEIVED 监听已统一把对端消息推入列表,ChatContent 按 type === 'TIMTextElem' 渲染。
🔑 坑点 4 :
msg.type区分元素类型:文本=TIMTextElem、图片=TIMImageElem、自定义=TIMCustomElem。渲染前必须按type分流,否则图片会被当文字显示。
3. 医生端 ------ 报告授权卡片(影像/检验)全流程
这是医生端最核心、也最容易和文档理解产生偏差的模块。务必理解双通道模型 :IM 卡片 = 用户交互通道;业务接口 flag = 权限真相来源。
3.1 医生发起授权请求(发送自定义消息)
医生在 moreFunctions.vue 点击"影像报告 / 检验报告" → 调用 requestImagingReportAuth 业务接口发起授权申请 → 同时用 IM 发送一张授权请求卡片给群(患者端会收到并弹窗)。
ts
// moreFunctions.vue(简化)
async function handleReportAuth(reportType: "imaging" | "lab") {
const tim = getTim();
// 1) 先调业务接口登记授权申请(拿到授权记录,后端据此置 flag)
await requestImagingReportAuth({
reportType,
authType: reportType === "imaging" ? "IMAGING_REPORT" : "LAB_TEST_REPORT",
groupId: route.query.groupID,
// ... patientId / orderId 等
});
// 2) 用 IM 发一张授权请求卡片(自定义消息)
const message = tim.createCustomMessage({
to: route.query.groupID as string,
conversationType: ChatSDK.TYPES.CONV_GROUP,
payload: {
data: {
type: reportType === "imaging"
? "IMAGING_REPORT_AUTH_REQUEST"
: "LAB_TEST_REPORT_AUTH_REQUEST",
data: reportType === "imaging"
? "IMAGING_REPORT_AUTH_REQUEST"
: "LAB_TEST_REPORT_AUTH_REQUEST",
description: reportType === "imaging" ? "影像报告授权申请" : "检验报告授权申请",
extension: JSON.stringify({
reportType,
authType: reportType === "imaging" ? "IMAGING_REPORT" : "LAB_TEST_REPORT",
// 需要的业务参数由后端填充或前端带入
}),
},
},
});
const res = await tim.sendMessage(message);
chatStore[ChatActionTypes.PUSH_NEW_MESSAGES]([res.data.message]);
}
3.2 授权状态:靠"轮询业务接口"而非"IM 回传"
这是与旧文档理解差异最大的一点(务必注意):
- 患者端同意后,医生端不是 靠收到
_APPROVE卡片消息自动开权限; - 医生端
index.vue通过setInterval每 5 秒轮询后端queryConsultationListByConversationID接口 ,读取imagingReportFlag/labReportFlag字段,再chatStore.setAuth(flag1, flag2)控制"查看报告"按钮是否可点。
ts
// src/pages/chat/index.vue(核心逻辑)
const startAuthTimer = () => {
if (authTimer.value) clearInterval(authTimer.value);
authTimer.value = window.setInterval(async () => {
try {
const res = await queryConsultationListByConversationID({
orderNoList: [route.query.groupID as string],
});
if (res.code === 200 && res.data && res.data[0]) {
// 权限真相来源:后端 flag
chatStore.setAuth(res.data[0].imagingReportFlag, res.data[0].labReportFlag);
// 同步回填最新 counselDetail(用于取报告时 patientResp.idNo 为最新)
const currentConv = chatStore.currentConversation as any;
if (currentConv) currentConv.counselDetail = res.data[0];
}
} catch (error) {
console.error("定时查询授权状态失败:", error);
}
}, 5000);
};
onMounted(() => { startAuthTimer(); });
onUnmounted(() => { if (authTimer.value) clearInterval(authTimer.value); }); // 必须清理!
📌 IM
_APPROVE/_REJECT卡片在医生端仅作"状态展示" ,真正的权限开关在后端 flag。联调时若医生端看不到"查看报告",先查后端counselDetailList返回的 flag,而非 IM 消息。
3.3 渲染授权卡片
ImagingReportAuthCard.vue 根据 message.flow(in/out)+ payload.data.type 渲染不同态:医生自己发的是"请求态",患者回的 APPROVE/REJECT 是"结果态"。注意卡片内部对 payload.data.data 的二次判断(见 ChatContent.vue 中对 message.payload.data.data 的分流),与 0.6 节的"双层信封"约定一致。
3.4 查看报告(走业务 HTTP 接口,不走 IM)
授权通过后,医生点"查看报告"调用业务接口拿带时效的 URL,而非 IM 传文件:
ts
// moreFunctions.vue(简化)
async function viewReport(reportType: "imaging" | "lab") {
if (reportType === "imaging") {
// 影像胶片登录 URL(带 expireTime / expireInSecond 时效)
const { data } = await getFilmLoginUrl({
idCardNo: patientResp.idCardNo,
idCardType: patientResp.idCardType,
});
// data 含 url / expireTime / expireInSecond
// 若 URL 已过期(expireInSecond 到期),需重新调接口刷新
openUrl(data.url);
} else {
// 检验报告列表
const { data } = await queryReportList({ /* orderId / patientId ... */ });
// 渲染报告列表
}
}
📌 报告 PDF/影像二进制不通过 IM 传输,IM 只负责"通知 + 授权交互"。这是重要的"接口边界":IM = 消息通道,业务 HTTP = 数据与权限。
3.5 相关接口清单(医生端 · 业务 HTTP)
| 接口(前端函数) | 路径 | 用途 |
|---|---|---|
getImParams |
hospital/chatRoom/getImParams |
获取医生 userID + userSig |
queryConsultationListByConversationID |
hospital/chatRoom/counselDetailList |
轮询授权 flag、消息剩余次数、患者信息 |
requestImagingReportAuth |
hospital/consultation/requestAuth |
发起影像/检验授权申请 |
checkAuthStatus |
hospital/consultation/checkAuthStatus |
查询授权状态(备用,实际用轮询 flag) |
getImagingReportList |
hospital/consultation/getImagingReportList |
影像报告列表 |
queryReportList |
patient/prescription/queryReportList |
检验报告列表 |
getFilmLoginUrl |
patient/prescription/getFilmLoginUrl |
影像胶片登录 URL(带时效) |
queryOrderDetailByRoomNo |
patient/consultation/queryOrderDetailByRoomNo |
订单/患者详情 |
4. 患者端实现(模块 B)
📌 本章定位 :与第 1--3 章共用第 0 章的"共享基座"约定------新版
@tencentcloud/chat、单例ChatSDK.getInstance()、GROUP 群会话(groupID=orderNo)、自定义消息"双层信封"(新版 SDK 中payload.data已是解析后的对象,勿再JSON.parse)。患者端的核心职责与医生端呈镜像对称:
- 医生端发起
*_AUTH_REQUEST、并渲染 患者回的*_APPROVE/_REJECT;- 患者端接收
*_AUTH_REQUEST并弹窗、点击后发回*_APPROVE/_REJECT。- 两端的"授权真相"都在后端 flag(
imagingReportFlag/labReportFlag),IM 仅做交互通道(见第 3.2 / 5.2 节)。两端共用同一套
authHelper.ts(解析/构造授权消息)与PatientAuthDialog.vue(授权弹窗),保证信封强一致。
4.1 患者端 SDK 初始化与登录
思路与医生端第 1.2 节完全共用 同一套 @tencentcloud/chat 单例封装(见第 0.4 / 0.5 节)。
ts
// 患者端复用与医生端完全相同的单例封装思路
import { ChatSDK } from "@tencentcloud/chat";
import { getPatientImParams } from "@/api/xxx"; // 获取患者 userSig 的接口
export async function initPatientIm() {
const { data } = await getPatientImParams();
// userID 形如 patient_<userId>,由后端生成;userSig 由后端签发
await ChatSDK.getInstance().login({
userID: data.userID,
userSig: data.userSig,
});
}
- 依赖 :与医生端一致,使用
@tencentcloud/chat(tim-js-sdk已为旧版,勿用)。 - userID 形态 :
patient_<userId>(与医生端doctor_<instanceUserId>对称)。 - userSig 获取接口 :患者端对应
getPatientImParams(对应医生端getImParams),后端返回userID + userSig。 - 单例位置 :
ChatSDK.getInstance()(与医生端同一工厂,勿重复create/new)。
🔑 患者端与医生端在同一个 GROUP 群会话 (groupID=orderNo)里,因此只要各自登录成功,就能互相收发消息,无需额外"加好友/建群"逻辑。
login的 Promise resolve 之后(或SDK_READY事件触发后)再发消息。
4.2 患者端接收授权请求卡片
患者端在 MESSAGE_RECEIVED 监听中识别授权请求,调用 authHelper.isAuthRequestMessage + parseAuthRequestData 解析,再用 PatientAuthDialog 弹窗展示。
ts
import {
isAuthRequestMessage,
parseAuthRequestData,
} from "@/utils/authHelper";
import { ref } from "vue";
import PatientAuthDialog from "@/components/PatientAuthDialog.vue";
const authDialogRef = ref<InstanceType<typeof PatientAuthDialog>>();
function onMessageReceived(event: { data: Message[] }) {
for (const msg of event.data) {
// 仅患者端需要弹窗响应医生端发来的 *_AUTH_REQUEST
if (isAuthRequestMessage(msg)) {
// 解析 extension 中的业务字段(authType/doctorName/consultationId/orderId 等)
const parsed = parseAuthRequestData(msg);
console.log("收到授权请求:", parsed);
// 弹出患者授权对话框(show 方法由 defineExpose 暴露)
authDialogRef.value?.show(msg);
}
}
}
PatientAuthDialog.vue 读取的展示字段来自 payload.data.extension(新版 SDK 中 payload.data 已是对象,勿 JSON.parse,与第 0.6 节一致):
| extension 字段 | 含义 | 来源(由哪端写入) |
|---|---|---|
doctorName |
申请医生姓名 | 医生端发请求时写入 |
authTitle |
报告标题(如"影像报告") | 医生端发请求时写入 |
patientName |
患者姓名 | 医生端发请求时写入 |
consultationId / orderId |
问诊/订单 ID(= 群 groupID) | 医生端发请求时写入 |
authType |
IMAGING_REPORT / LAB_TEST_REPORT |
医生端发请求时写入,决定弹窗图标/标题配色 |
📌 由哪端发出 / 由哪端渲染 :
*_AUTH_REQUEST由医生端发出 (见第 3.1 节),由患者端渲染 (PatientAuthDialog弹窗)。与字典表第 0.7 节一致。
4.3 患者端授权响应(同意/拒绝)
PatientAuthDialog.vue 的"同意 / 取消"按钮 → 调用 authHelper.respondToAuthRequest 构造响应消息并回发群组。authHelper.ts 的核心逻辑:
ts
// src/utils/authHelper.ts 关键逻辑(两端共用)
export function createAuthResponseMessage(originalMessage, responseType) {
const authData = parseAuthRequestData(originalMessage);
const dataType =
authData.authType === "IMAGING_REPORT"
? "IMAGING_REPORT_AUTH" // 注意:这里是 IMAGING_REPORT_AUTH(不是 _REQUEST)
: "LAB_TEST_REPORT_AUTH";
return {
data: `${dataType}_${responseType}`, // => "IMAGING_REPORT_AUTH_APPROVE" / "_REJECT"
description: `${authData.authTitle}授权${responseType === "APPROVE" ? "同意" : "拒绝"}`,
extension: JSON.stringify({
authType: authData.authType,
doctorName: authData.doctorName,
orderId: authData.orderId,
response: responseType,
originalTimestamp: authData.timestamp,
responseTimestamp: Date.now(),
}),
};
}
// 患者端调用(PatientAuthDialog handleApprove/handleReject)
respondToAuthRequest(props.message, "APPROVE", props.sendMsgCallback);
// sendMsgCallback("custom", payload) 内部用 tim.createCustomMessage + sendMessage 发回群组
患者端 sendMsgCallback 的实现体(回发群组的关键封装,须保证 to = 当前 orderNo 群 ID、conversationType = CONV_GROUP):
ts
// 患者端 sendMsgCallback 实现(对照医生端 3.1 的 createCustomMessage)
function sendMsgCallback(_elemType, payload) {
const tim = ChatSDK.getInstance();
const message = tim.createCustomMessage({
to: currentGroupID, // = orderNo,与医生端同一会话
conversationType: ChatSDK.TYPES.CONV_GROUP,
payload: { data: payload }, // 双层信封:payload.data = { type, data, description, extension }
});
return tim.sendMessage(message);
}
两端对齐校验(已确认一致):
- 患者端发出
data = IMAGING_REPORT_AUTH_APPROVE/IMAGING_REPORT_AUTH_REJECT - 医生端
ImagingReportAuthCard.vue判定data === "IMAGING_REPORT_AUTH_APPROVE"为已同意、=== "IMAGING_REPORT_AUTH_REJECT"为已拒绝 ✓
📌 由哪端发出 / 由哪端渲染 :
*_APPROVE/_REJECT由患者端发出 、由医生端渲染 (ImagingReportAuthCard.vue结果态)。这与字典表第 0.7 节完全对齐。⚠️ 接口边界提醒 :患者端发回
_APPROVE/_REJECT后,医生端"查看报告"按钮的真正可点状态仍由后端imagingReportFlag/labReportFlag轮询驱动(见第 3.2 节)。即:IM 响应消息是"交互闭环",后端 flag 是"权限真相"。患者端无需关心 flag,只需保证响应消息格式正确。
4.4 患者端查看报告
患者端查看报告不通过医生端那套 getFilmLoginUrl/queryReportList(那是医生端调"全市报告共享"的接口),而应走患者自有系统的报告接口。
- 若患者端需要"在聊天里点开报告卡片看报告",其入口与渲染组件需患者端自行实现(医生端
ImagingReportAuthCard.vue的"查看报告"逻辑仅供医生端参考,不可直接照搬,因为idCardNo来源、报告归属均不同)。 - 建议:患者端报告数据接口与医生端在第 3.5 节清单中明确区分,避免误调对方权限的接口。
- 报告 PDF/影像二进制不通过 IM 传输,IM 只负责"通知 + 授权交互"。这与第 0 章"接口边界三原则"一致。
📌 由哪端发出 / 由哪端渲染 :报告数据由患者侧业务接口提供;IM 仅承载"授权请求卡片"与"授权状态"通知。与字典表 / 第 5 章矩阵「查看报告」行一致。
4.5 患者端相关接口清单(对照医生端 3.5)
| 接口(前端函数) | 用途 | 与医生端关系 |
|---|---|---|
getPatientImParams |
获取患者 userSig | 仅患者端(对应医生端 getImParams) |
IM 发送 *_APPROVE/_REJECT 自定义消息 |
回发授权响应 | 仅患者端发出 ,医生端渲染 (对照医生端 3.1 发 *_AUTH_REQUEST) |
| (患者端报告接口) | 拉取患者本人报告 | 仅患者端,与医生端 queryReportList/getFilmLoginUrl 区分 |
| IM 授权请求/响应消息 | 复用 authHelper.ts + PatientAuthDialog.vue |
两端共用工具代码 |
📌 与医生端第 3.5 节对照:医生端
requestImagingReportAuth(发起授权)、queryConsultationListByConversationID(轮询 flag)、getFilmLoginUrl/queryReportList(查看报告)均为医生端侧或共用;患者端侧多为对称或独立实现,勿混用。
4.6 患者端审阅结论与两端对账点
对照第 0 章共享约定,患者端(按本章约定实现)应满足:
- SDK 与单例一致 :
@tencentcloud/chat+ChatSDK.getInstance(),与医生端第 1.2 节一致 ✓ - 消息信封一致 :
payload.data已是对象(勿JSON.parse),响应消息走data/description/extension双层信封,与第 0.6 节一致 ✓ - type 字典对齐 :响应
data与医生端判定值一致 ✓(见 4.3) - 对接易错点(需联调复核) :
- 患者端
sendMsgCallback必须保证to = currentGroupID(= orderNo)、conversationType = CONV_GROUP,否则消息发到错误会话,医生端收不到。 authHelper.parseAuthRequestData依赖extension中存在doctorName/patientName/consultationId/orderId,若医生端发请求时漏传任一字段,患者端弹窗会显示undefined------这是两端最易出错的"字段契约"点,详见第 0.7.1 节契约表。- 性能:患者端授权弹窗为事件驱动、非轮询,无轮询性能风险(与医生端 5s 轮询形成对比,属合理分工)。
- 患者端
5. 两端职责、数据交互与接口边界(两端共用)
5.1 职责边界矩阵
| 维度 | 医生端(第 1--3 章) | 患者端(第 4 章) | 边界 / 共享约定 |
|---|---|---|---|
| IM 登录身份 | doctor_<instanceUserId> |
patient_<userId> |
userSig 由各自后端 getImParams / getPatientImParams 下发;同一 ChatSDK.getInstance() 单例 |
| 会话载体 | GROUP 群(groupID=orderNo) | 同一 GROUP | 会话 ID 即订单号,两端共用 |
| 发起授权请求 | moreFunctions 发 *_AUTH_REQUEST |
接收卡片 → PatientAuthDialog 弹窗,点击后回发 *_APPROVE/_REJECT |
请求由医生端发出/患者端渲染;响应由患者端发出/医生端渲染(见 4.2 / 4.3)。⚠️ 患者端真实仓库目前走 replyAuthRequest 业务接口回传授权结果、未发 IM 消息,见 4.6 备注 |
| 授权状态真相 | 轮询业务接口 imagingReportFlag/labReportFlag |
不持有 flag,只回传响应消息 | 权限开关在后端,不在 IM;IM 响应是交互闭环,flag 是权限真相 |
| 查看报告 | 调 getFilmLoginUrl/queryReportList(全市报告共享) |
报告数据在患者自有系统(独立接口,不走医生端那套) | 报告数据走 HTTP,不走 IM |
| 消息渲染 | ChatContent.vue 按 flow+type 分发 |
镜像实现 | message.flow(in/out) 决定左右;自定义消息按 type 字典分发(见 0.7) |
5.2 接口边界三原则(必须牢记)
- IM 只做"消息通道":通知、授权请求/响应、卡片展示。不承载报告 PDF/影像二进制、不承载权限判定。
- 业务 HTTP 接口做"数据与权限":报告 URL、授权 flag、用户信息。医生端"能否看报告"由后端 flag 决定,而非 IM 消息到达。
- 自定义消息信封两端强一致 :
type字典、extension字段命名由第 0 章统一定义,改动需两端同步评审。
6. 常见坑点与调试建议
6.1 坑点速查表
| # | 坑点 | 现象 | 解决 |
|---|---|---|---|
| 1 | 实例非单例 | 消息重复、监听乱 | ChatSDK.getInstance() 单例,勿重复 create/new |
| 2 | userSig 过期/不匹配 | 登录报 70001 | 后端重签,前端做失效重试 |
| 3 | 未登录就发消息 | sendMessage 报错 |
login resolve 后再发,用 imReady/SDK_READY 守门 |
| 4 | 用 msg.type 判断错 |
卡片/文字显示错乱 | 文本=TIMTextElem,图片=TIMImageElem,自定义=TIMCustomElem |
| 5 | 自定义消息 payload 结构错 | 对端解析崩 | 遵循第 0.6 节"双层信封";新版 payload.data 已是对象,勿再 JSON.parse |
| 6 | 忘记先注册监听 | 收不到消息 | login 前先 tim.on(MESSAGE_RECEIVED,...) |
| 7 | 离开页面没清定时器 | 内存泄漏、轮询堆积 | onUnmounted 里 clearInterval 授权轮询 |
| 8 | C2C / GROUP 搞混 | 消息发错会话 | 本项目统一用 CONV_GROUP,conversationType 要一致 |
| 9 | 字段无版本兼容 | 老版本解析报错 | 用"类型白名单"判断,旧协议留兼容分支 |
| 10 | 两端 data 结构不一致 | 一端卡片空白 | 约定统一 JSON 结构,两端共用枚举(见 0.7) |
| 11 | 授权状态误以为由 IM 驱动 | 医生端看不到"查看报告" | 查后端 counselDetailList 的 flag,而非 IM 消息 |
| 12 | 报告 URL 过期未刷新 | 打开报告 403/失效 | 用 expireInSecond 判断,过期重新调 getFilmLoginUrl |
6.2 调试建议
- 看 IM 日志 :
tim.setLogLevel(0)输出详细日志,定位登录/收发问题。 - 先"回显"验证 :发送成功后把
res.data.message直接 push 列表,确认"自己能看见"再查对端。 - 控制台 REST API 联调:后端没好时,可用腾讯 IM 控制台手动发自定义消息,验证前端渲染。
- 打印原始 payload :收到自定义消息先
console.log(message.payload),确认data是对象(新版)还是字符串(旧版)。 - 断点 + DevTools :卡片不显示时,90% 是
type/data分流没命中,断点看真实值。 - 真机联调:移动端涉及推送/离线,务必真机跑一遍。
- 授权轮询排查 :医生端看不到权限变更,优先在 Network 面板看
counselDetailList返回的imagingReportFlag/labReportFlag是否为1。
7. 小结与学习路线
你现在已经掌握了腾讯 IM 前端对接的完整骨架(医生端):
typescript
开通服务拿 SDKAppID/SecretKey
↓
后端签发 UserSig(getImParams)
↓
ChatSDK.getInstance() 单例 + tim.on 监听事件
↓
tim.login 登录(守门 imReady)
↓
getMessageList 分页拉历史
↓
createTextMessage / sendMessage 收发文本
↓
createCustomMessage 发授权卡片(双层信封,type 字典对齐)
↓
ChatContent 按 type/flow 渲染 + 5秒轮询后端 flag 控制权限
↓
查看报告走业务 HTTP(getFilmLoginUrl/queryReportList)
下一步建议:
- 官方文档:腾讯云 IM Web 端 SDK
- 重点看
ChatSDK.TYPES、ChatSDK.EVENT.MESSAGE_RECEIVED等常量
IM 对接不难,难的是"事件驱动思维 "------消息不是函数返回值,而是被动推给你的事件;更难的是分清"IM 通道"与"业务接口"的边界。把这两点想通,剩下都是 API 调用而已。祝你对接顺利!