腾讯 IM 前端对接小白教程:医患一对一聊天(含自定义报告卡片)

本文面向完全没有 IM(即时通讯)经验的前端开发者 ,用生活化的类比手把手带你对接腾讯云 IM。教程以一个「医患一对一问诊」的真实场景为例,覆盖初始化登录、收发文本消息、发送自定义消息(影像/检验报告卡片)、授权流程、常见坑点与调试技巧。

注:本文仅仅是知识分享与个人学习,不包含任何涉密内容

文档范围与结构说明(重要)

  • 本文当前覆盖 医生端(移动端 H5) ,代码示例参考自真实项目 doctor-h5-view,基于 @tencentcloud/chat(新版 Chat SDK)
  • 患者端实现已在第 4 章完整展开,与医生端(第 1--3 章)共用第 0 章共享基座与第 5 章职责边界。
  • 第 0 章「共享基座」与第 5 章「职责边界」为医生端 / 患者端共用,两端必须对齐。
  • 文档中凡标注 【待确认:患者端】 的区域,为在真实患者端仓库落地时需与后端接口核对的细节。

目录

  1. [共享基座:腾讯 IM 黑话、SDK、登录、自定义消息信封(两端共用)](#共享基座:腾讯 IM 黑话、SDK、登录、自定义消息信封(两端共用))
  2. [医生端实现(模块 A)](#医生端实现(模块 A))
  3. [医生端 ------ 会话、历史消息与收发文本](#医生端 —— 会话、历史消息与收发文本)
  4. [医生端 ------ 报告授权卡片(影像/检验)全流程](#医生端 —— 报告授权卡片(影像/检验)全流程)
  5. [患者端实现(模块 B)](#患者端实现(模块 B))
  6. 两端职责、数据交互与接口边界(两端共用)
  7. 常见坑点与调试建议
  8. 小结与学习路线

0. 共享基座(两端共用)

0.1 先搞懂几个"黑话"

腾讯 IM(即时通讯)是腾讯云提供的一套"聊天基础设施",可类比成一家专门帮人盖"聊天邮局"的公司:你不用自己搭 WebSocket、消息存储、离线推送,只要"租用"邮局,再在前端装一个"收发室软件"(SDK)。

黑话 生活化类比 它是什么(本项目对应)
SDKAppID 邮局的"门牌号" 腾讯分配的应用唯一 ID,写在配置里(非秘密)
UserID 用户的"工号" 医生端形如 doctor_<instanceUserId>,患者端形如 patient_<userId>由各自后端生成
UserSig 用户的"临时门禁卡" 后端用密钥签发的加密串,前端绝不能持有 SecretKey
Chat SDK 收发室"对讲机" 本项目用 @tencentcloud/chattim-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 });

🔑 坑点 2userSig有效期 (默认 180 天,后端可配)。登录报错 70001 多为签名过期或 UserID 不匹配。上线后需做"登录失效 → 重新获取 userSig → 重新登录"的兜底。必须等 login 的 Promise resolve 之后再发消息 ,否则报"未登录"。建议用 SDK_READY 事件 / 全局 imReady flag 守门。

0.6 自定义消息"标准信封"(两端强一致约定)

本项目自定义消息采用双层信封 结构。新版 SDK 中 message.payload.data 已是已解析对象 (无需再 JSON.parse,旧版 tim-js-sdk 才需要),其标准结构为:

ts 复制代码
// 自定义消息 payload.data 的标准信封
{
  type: string,        // 路由类型,决定渲染哪种卡片(见下方"消息 type 字典表")
  data: string,        // 业务子类型/标识,部分卡片用此字段进一步分流
  description: string, // 卡片展示用的简要文案
  extension: string,   // 扩展字段(JSON 字符串),真正业务参数放这里
}
  • 真正业务参数(如 reportTypeauthTypeurlpatientIdorderId)放在 extension 内(字符串化的 JSON)。
  • 渲染侧先按 type 分流,再按 data / extension 取业务字段。

⚠️ 与旧版的关键差异 :旧 tim-js-sdkpayload.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.flowin=对方发来 / out=自己发出)决定左右布局。


1. 医生端实现(模块 A)

模块 A 覆盖医生端移动端 H5 的完整链路,对应仓库:doctor-h5-viewsrc/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.tsbindListeners):

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' 渲染。

🔑 坑点 4msg.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/chattim-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 章共享约定,患者端(按本章约定实现)应满足:

  1. SDK 与单例一致@tencentcloud/chat + ChatSDK.getInstance(),与医生端第 1.2 节一致 ✓
  2. 消息信封一致payload.data 已是对象(勿 JSON.parse),响应消息走 data/description/extension 双层信封,与第 0.6 节一致 ✓
  3. type 字典对齐 :响应 data 与医生端判定值一致 ✓(见 4.3)
  4. 对接易错点(需联调复核)
    • 患者端 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.vueflow+type 分发 镜像实现 message.flow(in/out) 决定左右;自定义消息按 type 字典分发(见 0.7)

5.2 接口边界三原则(必须牢记)

  1. IM 只做"消息通道":通知、授权请求/响应、卡片展示。不承载报告 PDF/影像二进制、不承载权限判定。
  2. 业务 HTTP 接口做"数据与权限":报告 URL、授权 flag、用户信息。医生端"能否看报告"由后端 flag 决定,而非 IM 消息到达。
  3. 自定义消息信封两端强一致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 离开页面没清定时器 内存泄漏、轮询堆积 onUnmountedclearInterval 授权轮询
8 C2C / GROUP 搞混 消息发错会话 本项目统一用 CONV_GROUPconversationType 要一致
9 字段无版本兼容 老版本解析报错 用"类型白名单"判断,旧协议留兼容分支
10 两端 data 结构不一致 一端卡片空白 约定统一 JSON 结构,两端共用枚举(见 0.7)
11 授权状态误以为由 IM 驱动 医生端看不到"查看报告" 查后端 counselDetailList 的 flag,而非 IM 消息
12 报告 URL 过期未刷新 打开报告 403/失效 expireInSecond 判断,过期重新调 getFilmLoginUrl

6.2 调试建议

  1. 看 IM 日志tim.setLogLevel(0) 输出详细日志,定位登录/收发问题。
  2. 先"回显"验证 :发送成功后把 res.data.message 直接 push 列表,确认"自己能看见"再查对端。
  3. 控制台 REST API 联调:后端没好时,可用腾讯 IM 控制台手动发自定义消息,验证前端渲染。
  4. 打印原始 payload :收到自定义消息先 console.log(message.payload),确认 data 是对象(新版)还是字符串(旧版)。
  5. 断点 + DevTools :卡片不显示时,90% 是 type / data 分流没命中,断点看真实值。
  6. 真机联调:移动端涉及推送/离线,务必真机跑一遍。
  7. 授权轮询排查 :医生端看不到权限变更,优先在 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 对接不难,难的是"事件驱动思维 "------消息不是函数返回值,而是被动推给你的事件;更难的是分清"IM 通道"与"业务接口"的边界。把这两点想通,剩下都是 API 调用而已。祝你对接顺利!

相关推荐
深念Y6 小时前
07-SSR水合问题实战排查与修复记录
前端·vue·vite·nuxt·ssr·csr·水合
深念Y7 小时前
NativeScript 移动端开发踩坑记录
前端·ui·vue·安卓·移动端·native·原生
avi91111 天前
【】js不同颜色(Vue 框架)今时今日2026年学编程入门(10月1日)
前端·vue.js·vue·vue框架·前端入门·vue入门·html上传
上海心泾国际物流有限公司1 天前
去年秋天,我在闵行区为一批冷链货找仓库
经验分享·笔记·健康医疗·交通物流
宠友信息2 天前
社区类源码开发实践中的仿小红书系统技术要点分析
java·spring boot·redis·mysql·uni-app·vue·内容运营
南城以南溫暖如初1472 天前
从零搭建24小时自助健身系统:技术选型与核心模块实战
java·spring boot·redis·mysql·vue·mybatis
jkyy20142 天前
从商品售卖到健康服务,数字化重构大健康品牌会员运营底层逻辑
大数据·人工智能·信息可视化·健康医疗
_xaboy2 天前
开源表单设计器 FcDesigner 保存表单教程:toJson parseJson 回显
低代码·开源·vue·表单·fcdesigner
钛态3 天前
Vite 中的 CSS 工程化:从 CSS Modules 到 UnoCSS 的渐进式迁移
前端·vue·react·web