Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

wangzl 4 месяцев назад
Родитель
Сommit
87a88e326c
34 измененных файлов с 508 добавлено и 304 удалено
  1. 5 0
      tr-modules/tr-module-mobile/pom.xml
  2. 91 52
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/ServerEventListenerImpl.java
  3. 0 2
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/config/ImServerService.java
  4. 60 14
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/config/ServerEventCallbackHandler.java
  5. 12 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/constant/IMRabbitMQConstant.java
  6. 11 44
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/controller/ImMsgReceivedController.java
  7. 0 80
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/controller/ImMsgSendController.java
  8. 43 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/controller/vo/ImMsgReceivedVO.java
  9. 14 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImLoginSuccessDTO.java
  10. 2 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImMsgReceivedDTO.java
  11. 5 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImMsgReceivedQueryDTO.java
  12. 13 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImMsgUnreadTotalCountQueryDTO.java
  13. 2 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/MsgDTO.java
  14. 3 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/mapper/ImMsgReceivedMapper.java
  15. 6 2
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/po/ImGroupUserPO.java
  16. 3 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/po/ImMsgReceivedPO.java
  17. 5 1
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/repository/ImGroupUserRepository.java
  18. 21 1
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/repository/ImMsgReceivedRepository.java
  19. 5 31
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/service/IImMsgReceivedService.java
  20. 93 61
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/service/impl/ImMsgReceivedServiceImpl.java
  21. 32 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/service/impl/ImMsgSendServiceImpl.java
  22. 20 0
      tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/utils/UserUtils.java
  23. 3 0
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/app/controller/vo/DoctorClinicRoomVO.java
  24. 1 1
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/config/RabbitMQConfig.java
  25. 1 1
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/enums/RabbitMQConstant.java
  26. 7 1
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/po/BizInfusionClinicPO.java
  27. 3 0
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/service/IBizDeviceService.java
  28. 24 4
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/service/impl/BizDeviceServiceImpl.java
  29. 5 6
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/web/controller/BizDeviceController.java
  30. 3 0
      tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/wx/controller/vo/BizWxAppletClinicDetailVO.java
  31. 7 2
      tr-modules/tr-module-smartFollowUp/src/main/resources/mapper/smart/BizClinicRoomMapper.xml
  32. 3 0
      tr-test/src/main/resources/application-doc.yml
  33. 4 0
      tr-test/src/main/resources/application.yml
  34. 1 1
      tr-test/src/main/resources/log4j2.xml

+ 5 - 0
tr-modules/tr-module-mobile/pom.xml

@@ -47,6 +47,11 @@
             <artifactId>tr-module-system-api</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-eventbus</artifactId>
+        </dependency>
+
         <!-- MobileIMSDK的核心库(此jar没有上传到mave中央库,请注意本地引用路径哦) -->
         <dependency>
             <groupId>com.x52im</groupId>

+ 91 - 52
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/ServerEventListenerImpl.java

@@ -1,15 +1,18 @@
 package cn.tr.module.mobile;
 
+import cn.hutool.core.lang.Pair;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import cn.tr.module.mobile.config.ServerEventCallbackHandler;
+import cn.tr.module.mobile.dto.ImLoginSuccessDTO;
 import cn.tr.module.mobile.dto.MsgDTO;
+import cn.tr.module.mobile.utils.UserUtils;
 import io.netty.channel.Channel;
 import lombok.extern.slf4j.Slf4j;
 import net.x52im.mobileimsdk.server.event.ServerEventListener;
 import net.x52im.mobileimsdk.server.processor.OnlineProcessor;
 import net.x52im.mobileimsdk.server.protocal.Protocal;
 import net.x52im.mobileimsdk.server.protocal.s.PKickoutInfo;
-
 /**
  * @author wangzl
  * @description: TODO
@@ -31,6 +34,11 @@ public class ServerEventListenerImpl implements ServerEventListener {
     @Override
     public int onUserLoginVerify(String userId, String token, String extra, Channel session) {
         log.debug("【DEBUG_回调通知】正在调用回调方法:OnVerifyUserCallBack...(extra=" + extra + ")");
+        // 校验userId格式必须是"字符串-字符串"的格式
+        if (userId == null || !userId.matches("^[^-]+-[^-]+-[^-]+$")) {
+            log.warn("用户登录验证失败,userId格式不正确: {}", userId);
+            return 1025; // 返回自定义错误码,表示userId格式错误
+        }
         return 0;
     }
     /**
@@ -45,6 +53,9 @@ public class ServerEventListenerImpl implements ServerEventListener {
     @Override
     public void onUserLoginSucess(String userId, String extra, Channel session) {
         log.debug("【IM_回调通知onUserLoginSucess】用户:" + userId + " 上线了!");
+        ImLoginSuccessDTO loginUser = parseCompoundUserId(userId);
+        ServerEventCallbackHandler.onUserLoginSuccess.accept(loginUser);
+
     }
     /**
      * 用户退出登录回调方法定义(可理解为下线通知回调)。
@@ -70,22 +81,8 @@ public class ServerEventListenerImpl implements ServerEventListener {
      */
     @Override
     public boolean onTransferMessage4C2SBefore(Protocal p, Channel session) {
-        String dataContent = p.getDataContent();
         log.debug("收到C2S消息前处理: from={}, to={}, fp={}, typeu={}, dataContent={}",
-                p.getFrom(), p.getTo(), p.getFp(), p.getTypeu(), dataContent);
-
-        if(!JSONUtil.isTypeJSON(dataContent)){
-            log.warn("收到的C2S消息不是有效的JSON格式: {}", dataContent);
-            return false;
-        }
-        try {
-            MsgDTO msg = JSONUtil.toBean(dataContent, MsgDTO.class);
-            log.debug("解析C2S消息成功: clinicId={}, type={}, role={}",
-                    msg.getClinicId(), msg.getType(), msg.getRole());
-        } catch (Exception e) {
-            log.error("解析C2S消息失败: dataContent={}, error={}", dataContent, e.getMessage());
-            return false;
-        }
+                p.getFrom(), p.getTo(), p.getFp(), p.getTypeu(),p.getDataContent());
         return true;
     }
     /**
@@ -99,27 +96,12 @@ public class ServerEventListenerImpl implements ServerEventListener {
      */
     @Override
     public boolean onTransferMessage4C2CBefore(Protocal p, Channel session) {
-        String dataContent = p.getDataContent();
         log.debug("收到C2C消息前处理: from={}, to={}, fp={}, typeu={}, dataContent={}",
-                p.getFrom(), p.getTo(), p.getFp(), p.getTypeu(), dataContent);
-
-        if(!JSONUtil.isTypeJSON(dataContent)){
-            log.warn("收到的C2C消息不是有效的JSON格式: {}", dataContent);
-            return false;
-        }
-        try {
-            MsgDTO msg = JSONUtil.toBean(dataContent, MsgDTO.class);
-            log.debug("解析C2C消息成功: clinicId={}, type={}, role={}",
-                    msg.getClinicId(), msg.getType(), msg.getRole());
-        } catch (Exception e) {
-            log.error("解析C2C消息失败: dataContent={}, error={}", dataContent, e.getMessage());
-            return false;
-        }
+                p.getFrom(), p.getTo(), p.getFp(), p.getTypeu(),p.getDataContent());
         return true;
     }
     /**
      * 收到客户端发送给“服务端”的数据回调通知(即:消息路径为“C2S”的消息).
-     *
      * @param p 消息/指令的完整协议包对象
      * @param session 此客户端连接对应的 netty “会话”
      * @return true表示本方法已成功处理完成,否则表示未处理成功。此返回值目前框架中并没有特殊意义,仅作保留吧
@@ -128,27 +110,35 @@ public class ServerEventListenerImpl implements ServerEventListener {
      */
     @Override
     public boolean onTransferMessage4C2S(Protocal p, Channel session) {
+        // 发送者uid
+        ImLoginSuccessDTO loginUser = parseCompoundUserId(p.getFrom());
+
         // 接收者uid
         String userId = p.getTo();
-        // 发送者uid
-        String from_user_id = p.getFrom();
         // 消息或指令内容
         String dataContent = p.getDataContent();
+        if(!JSONUtil.isTypeJSON(dataContent)){
+            return false;
+        }
         // 消息或指令指纹码(即唯一ID)
         String fingerPrint = p.getFp();
         // 【重要】用户定义的消息或指令协议类型(开发者可据此类型来区分具体的消息或指令)
         int typeu = p.getTypeu();
         try {
+            if(!validateMsgDTO(dataContent)){
+                return false;
+            }
             MsgDTO msg = JSONUtil.toBean(dataContent, MsgDTO.class);
-            msg.setFromUserId(from_user_id);
+            UserUtils.setCurrentUser(loginUser.getUserId(),loginUser.getTenantId());
+            msg.setFromUserId(loginUser.getUserId());
             msg.setToUserId(userId);
             msg.setMsgId(fingerPrint);
+            msg.setTenantId(loginUser.getTenantId());
             ServerEventCallbackHandler.onTransferMessage4C2S.accept(msg);
         }catch (Exception e){
-            log.error("【IM_回调通知】[typeu=" + typeu + "]收到了客户端" + from_user_id + "发给服务端的消息:str=" + dataContent);
+            log.error("【IM_回调通知】[typeu=" + typeu + "]收到了客户端" + p.getFrom() + "发给服务端的消息:str=" + dataContent);
         }
-
-        log.debug("【DEBUG_回调通知】[typeu=" + typeu + "]收到了客户端" + from_user_id + "发给服务端的消息:str=" + dataContent);
+        log.debug("【DEBUG_回调通知】[typeu=" + typeu + "]收到了客户端" + p.getFrom() + "发给服务端的消息:str=" + dataContent);
         return true;
     }
     /**
@@ -184,7 +174,6 @@ public class ServerEventListenerImpl implements ServerEventListener {
     /**
      * 服务端在进行消息发送时,当对方在线但实时发送失败、以及其它各种问题导致消息并没能正常发出时,将无条件走本回调通知。
      *
-     *
      * @param p 消息/指令的完整协议包对象
      * @return true表示应用层已经处理了离线消息,否则表示应用层没有处理
      * @see Protocal
@@ -199,19 +188,8 @@ public class ServerEventListenerImpl implements ServerEventListener {
         String from_user_id = p.getFrom();
         // 消息或指令内容
         String dataContent = p.getDataContent();
-        // 消息或指令指纹码(即唯一ID)
-        String fingerPrint = p.getFp();
-        // 【重要】用户定义的消息或指令协议类型(开发者可据此类型来区分具体的消息或指令)
+        // 【重要】用户定义的消息或指令协议类型(开发者可据此类型来区分具体的消息或指令)
         int typeu = p.getTypeu();
-        try {
-            MsgDTO msg = JSONUtil.toBean(dataContent, MsgDTO.class);
-            msg.setFromUserId(from_user_id);
-            msg.setToUserId(userId);
-            msg.setMsgId(fingerPrint);
-            ServerEventCallbackHandler.onTransferMessage_RealTimeSendFaild.accept(msg);
-        }catch (Exception e){
-            log.error("【IM_回调通知】[typeu=" + typeu + "]收到了客户端" + from_user_id + "发给客户端" + userId + "的消息:str=" + dataContent);
-        }
         log.debug("【DEBUG_回调通知】[typeu=" + typeu + "]客户端" + from_user_id + "发给客户端" + userId + "的消息:str=" + dataContent + ",因实时发送没有成功,需要上层应用作离线处理哦,否则此消息将被丢弃.");
         return false;
     }
@@ -225,4 +203,65 @@ public class ServerEventListenerImpl implements ServerEventListener {
     public void onTransferMessage4C2C_AfterBridge(Protocal p){
         // 默认本方法可
     }
-}
+
+
+    private boolean validateMsgDTO(String msgContent) {
+        if(!JSONUtil.isTypeJSON(msgContent)){
+            return false;
+        }
+        MsgDTO msg=JSONUtil.toBean(msgContent, MsgDTO.class);
+        if (msg == null) {
+            return false;
+        }
+        // 校验MsgDTO每个字段都不为空
+        if (StrUtil.isEmpty(msg.getClinicId())) {
+            return false;
+        }
+
+        if (StrUtil.isEmpty(msg.getType())) {
+            return false;
+        }
+
+        if (StrUtil.isEmpty(msg.getContent())) {
+            return false;
+        }
+
+        if (StrUtil.isEmpty(msg.getRole())) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 解析复合用户ID,从格式为 "tenantId-userId-clinicId" 的字符串中提取租户ID、用户ID和诊所ID
+     *
+     * 在多租户系统中,为了区分不同租户、不同诊所下的同名用户,用户ID采用复合格式,
+     * 即 tenantId+"-"+userId+"-"+clinicId 的形式。
+     * 该方法用于将这种复合格式的用户ID解析为独立的租户ID、用户ID和诊所ID三部分。
+     *
+     * @param compoundUserId 复合用户ID,格式为 tenantId+"-"+userId+"-"+clinicId,例如 "tenant1-user123-clinic456"
+     * @return Pair<String, String> 键值对,其中:
+     *         - Key: 用户ID (userId)
+     *         - Value: 包含租户ID和诊所ID的组合字符串,格式为 tenantId+"-"+clinicId
+     *         如果输入参数为空或不包含足够的"-"分隔符,则返回(null, null)
+     *
+     * @see Pair
+     *
+     * @example
+     * <pre>
+     * parseCompoundUserId("tenant1-user123-clinic456") returns Pair.of("user123", "tenant1-clinic456")
+     * parseCompoundUserId("invalidFormat") returns Pair.of(null, null)
+     * parseCompoundUserId(null) returns Pair.of(null, null)
+     * </pre>
+     */
+    private ImLoginSuccessDTO parseCompoundUserId(String compoundUserId) {
+        ImLoginSuccessDTO result = null;
+        if (compoundUserId != null && compoundUserId.contains("-")) {
+            String[] parts = compoundUserId.split("-", 3); // 分割成3部分
+            if (parts.length >= 3) {
+                result = ImLoginSuccessDTO.of(parts[0], parts[1],parts[2]); // userId, tenantId-clinicId
+            }
+        }
+        return result;
+    }
+}

+ 0 - 2
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/config/ImServerService.java

@@ -59,7 +59,6 @@ public class ImServerService {
                     properties.getTcpPort(),
                     properties.getUdpPort(),
                     properties.getWebSocketPort());
-
         } catch (Exception e) {
             log.error("MobileIMSDK 服务端启动失败", e);
             throw new RuntimeException("IM 服务启动失败", e);
@@ -70,7 +69,6 @@ public class ImServerService {
         if (serverLauncher != null) {
             try {
                 serverLauncher.shutdown();
-                Runtime.getRuntime().removeShutdownHook(new Thread(() -> serverLauncher.shutdown()));
                 log.info("MobileIMSDK 服务端已停止");
             } catch (Exception e) {
                 log.error("MobileIMSDK 服务端停止失败", e);

+ 60 - 14
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/config/ServerEventCallbackHandler.java

@@ -1,10 +1,16 @@
 package cn.tr.module.mobile.config;
 
+import cn.tr.module.mobile.dto.ImLoginSuccessDTO;
 import cn.tr.module.mobile.dto.MsgDTO;
+import lombok.extern.slf4j.Slf4j;
 import net.x52im.mobileimsdk.server.protocal.Protocal;
 
 import java.nio.channels.Channel;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * ServerEventCallbackHandler 回调处理器配置类
@@ -18,22 +24,9 @@ import java.util.function.Consumer;
  *     return null;
  * };
  */
+@Slf4j
 public class ServerEventCallbackHandler {
 
-    /**
-     * 处理实时发送失败的消息回调函数
-     *
-     * 当服务端在进行消息发送时,对方在线但实时发送失败、以及其它各种问题导致消息并没能正常发出时,
-     * 将无条件调用此回调函数。
-     *
-     * 函数参数:
-     * - p: 消息/指令的完整协议包对象 {@link Protocal}
-     *
-     * 函数返回值:
-     * - true表示应用层已经处理了离线消息,否则表示应用层没有处理
-     */
-    public static Consumer<MsgDTO> onTransferMessage_RealTimeSendFaild = p -> {};
-
     /**
      * 处理客户端发送给服务端的消息回调函数
      *
@@ -63,4 +56,57 @@ public class ServerEventCallbackHandler {
      * - Void类型,无实际返回值意义
      */
     public static Consumer<MsgDTO> onTransferMessage4C2C = p->{};
+
+
+
+    /**
+     * 处理服务端发送给客户端成功消息的回调函数
+     *
+     * 当服务端发送消息给客户端成功时,会调用此回调函数。
+     *
+     * 函数参数:
+     * - p: 消息/指令的完整协议包对象 {@link Protocal}
+     */
+    public static Consumer<MsgDTO> onTransfer4S2CMessageSuccess = p->{
+        // 处理服务端发送给客户端成功消息的逻辑
+       log.debug("服务端发送消息成功: from=" + p.getFromUserId() + ", to=" + p.getToUserId() +
+                ", msgId=" + p.getMsgId() + ", content=" + p.getContent());
+    };
+
+
+    /**
+     * 获取群组下所有userId的回调函数
+     *
+     * 当需要获取指定群组下的所有用户ID时,会调用此回调函数。
+     *
+     * 函数参数:
+     * - groupId: 群组ID
+     * 函数返回值:
+     * - List<String>: 群组下所有用户的ID列表
+     */
+    public static Function<String, List<String>> onGetGroupUserIds = groupId -> {
+        // 默认实现,实际使用时应该由业务方实现具体逻辑
+        log.debug("获取群组 {} 下的所有用户ID", groupId);
+        // 实际实现应该查询数据库或缓存获取群组成员列表
+        return Collections.emptyList();
+    };
+
+
+    /**
+     * 用户登录成功的回调函数
+     *
+     * 当用户成功登录系统时,会调用此回调函数。
+     *
+     * 函数参数:
+     * - loginInfo: 登录用户信息,包含用户ID、诊所ID等信息
+     * 函数返回值:
+     * - void: 无返回值
+     */
+    public static Consumer<ImLoginSuccessDTO> onUserLoginSuccess = loginInfo -> {
+        // 默认实现,实际使用时应该由业务方实现具体逻辑
+        log.debug("用户登录成功: userId={}, clinicId={}",
+                loginInfo.getUserId(), loginInfo.getClinicId());
+    };
+
+
 }

+ 12 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/constant/IMRabbitMQConstant.java

@@ -0,0 +1,12 @@
+package cn.tr.module.mobile.constant;
+
+
+public interface IMRabbitMQConstant {
+    String EXCHANGE_IM_MSG="im_msg_exchange";
+
+    String ROUTING_KEY_IM_MSG="im_msg_routing_key";
+
+    String QUEUE_IM_MSG="im_msg";
+
+
+}

+ 11 - 44
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/controller/ImMsgReceivedController.java

@@ -1,25 +1,20 @@
 package cn.tr.module.mobile.controller;
 
+import cn.tr.module.mobile.controller.vo.ImMsgReceivedVO;
+import cn.tr.module.mobile.dto.ImMsgUnreadTotalCountQueryDTO;
 import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
-import cn.dev33.satoken.annotation.SaCheckPermission;
-import cn.tr.core.validation.Insert;
-import cn.tr.core.validation.Update;
 import cn.tr.core.pojo.CommonResult;
 import lombok.AllArgsConstructor;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
-import cn.tr.module.mobile.dto.ImMsgReceivedDTO;
 import cn.tr.module.mobile.service.IImMsgReceivedService;
 import cn.tr.module.mobile.dto.ImMsgReceivedQueryDTO;
-import java.util.*;
 import cn.tr.plugin.mybatis.base.BaseController;
 import org.springframework.web.bind.annotation.*;
-import cn.tr.module.api.sys.log.annotation.OperateLog;
 import cn.tr.core.pojo.TableDataInfo;
 /**
  * 发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。控制器
@@ -27,7 +22,7 @@ import cn.tr.core.pojo.TableDataInfo;
  * @author lf
  * @date  2025/08/20 10:14
  */
-@Api(tags = "发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。")
+@Api(tags = "聊天室消息表")
 @RestController
 @RequestMapping("/mobile/msgReceived")
 @AllArgsConstructor
@@ -36,45 +31,17 @@ public class ImMsgReceivedController extends BaseController{
     private final IImMsgReceivedService imMsgReceivedService;
 
     @ApiOperationSupport(author = "lf",order = 1)
-    @ApiOperation(value="根据条件查询发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。",notes = "权限: 无")
+    @ApiOperation(value="根据条件查询发送消息表(分页)",notes = "权限: 无")
     @PostMapping("/query/page")
-    public TableDataInfo<ImMsgReceivedDTO> selectList(@RequestBody ImMsgReceivedQueryDTO query) {
+    public TableDataInfo<ImMsgReceivedVO> selectList(@RequestBody@Validated ImMsgReceivedQueryDTO query) {
         startPage();
         return getDataTable(imMsgReceivedService.selectImMsgReceivedList(query));
     }
 
-    @ApiOperationSupport(author = "lf",order = 2)
-    @ApiOperation(value = "根据id查询发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。",notes = "权限: mobile:msgReceived:query")
-    @GetMapping("/detail/{id}")
-    @SaCheckPermission("mobile:msgReceived:query")
-    public CommonResult<ImMsgReceivedDTO> findById(@PathVariable("id") String id){
-        return CommonResult.success(imMsgReceivedService.selectImMsgReceivedById(id));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 3)
-    @ApiOperation(value="添加发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。",notes = "权限: mobile:msgReceived:add")
-    @PostMapping("/add")
-    @OperateLog
-    @SaCheckPermission("mobile:msgReceived:add")
-    public CommonResult<Boolean> add(@RequestBody@Validated(Insert.class) ImMsgReceivedDTO source) {
-        return CommonResult.success(imMsgReceivedService.insertImMsgReceived(source));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 4)
-    @ApiOperation(value="通过主键id编辑发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。",notes = "权限: mobile:msgReceived:edit")
-    @PostMapping("/edit")
-    @OperateLog
-    @SaCheckPermission("mobile:msgReceived:edit")
-    public CommonResult<Boolean> edit(@RequestBody@Validated(Update.class) ImMsgReceivedDTO source) {
-        return CommonResult.success(imMsgReceivedService.updateImMsgReceivedById(source));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 5)
-    @ApiOperation(value="删除发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。",notes = "权限: mobile:msgReceived:remove")
-    @PostMapping("/removeByIds")
-    @OperateLog
-    @SaCheckPermission("mobile:msgReceived:remove")
-    public CommonResult<Integer> delete(@RequestBody Collection<String> ids) {
-        return CommonResult.success(imMsgReceivedService.removeImMsgReceivedByIds(ids));
+    @ApiOperationSupport(author = "lf",order = 1)
+    @ApiOperation(value="查看用户未读消息数量",notes = "权限: 无")
+    @PostMapping("/unread/totalCount")
+    public CommonResult<Long> selectUnReadCount(@RequestBody@Validated ImMsgUnreadTotalCountQueryDTO query) {
+        return CommonResult.success(imMsgReceivedService.selectUndReadTotalCount(query));
     }
-}
+}

+ 0 - 80
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/controller/ImMsgSendController.java

@@ -1,80 +0,0 @@
-package cn.tr.module.mobile.controller;
-
-import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
-import cn.dev33.satoken.annotation.SaCheckPermission;
-import cn.tr.core.validation.Insert;
-import cn.tr.core.validation.Update;
-import cn.tr.core.pojo.CommonResult;
-import lombok.AllArgsConstructor;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RestController;
-import cn.tr.module.mobile.dto.ImMsgSendDTO;
-import cn.tr.module.mobile.service.IImMsgSendService;
-import cn.tr.module.mobile.dto.ImMsgSendQueryDTO;
-import java.util.*;
-import cn.tr.plugin.mybatis.base.BaseController;
-import org.springframework.web.bind.annotation.*;
-import cn.tr.module.api.sys.log.annotation.OperateLog;
-import cn.tr.core.pojo.TableDataInfo;
-/**
- * 推送消息表,保存某个用户收到了哪些消息。控制器
- *
- * @author lf
- * @date  2025/08/20 10:15
- */
-@Api(tags = "推送消息表,保存某个用户收到了哪些消息。")
-@RestController
-@RequestMapping("/mobile/msgSend")
-@AllArgsConstructor
-public class ImMsgSendController extends BaseController{
-
-    private final IImMsgSendService imMsgSendService;
-
-    @ApiOperationSupport(author = "lf",order = 1)
-    @ApiOperation(value="根据条件查询推送消息表,保存某个用户收到了哪些消息。",notes = "权限: 无")
-    @PostMapping("/query/page")
-    public TableDataInfo<ImMsgSendDTO> selectList(@RequestBody ImMsgSendQueryDTO query) {
-        startPage();
-        return getDataTable(imMsgSendService.selectImMsgSendList(query));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 2)
-    @ApiOperation(value = "根据id查询推送消息表,保存某个用户收到了哪些消息。",notes = "权限: mobile:msgSend:query")
-    @GetMapping("/detail/{id}")
-    @SaCheckPermission("mobile:msgSend:query")
-    public CommonResult<ImMsgSendDTO> findById(@PathVariable("id") String id){
-        return CommonResult.success(imMsgSendService.selectImMsgSendById(id));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 3)
-    @ApiOperation(value="添加推送消息表,保存某个用户收到了哪些消息。",notes = "权限: mobile:msgSend:add")
-    @PostMapping("/add")
-    @OperateLog
-    @SaCheckPermission("mobile:msgSend:add")
-    public CommonResult<Boolean> add(@RequestBody@Validated(Insert.class) ImMsgSendDTO source) {
-        return CommonResult.success(imMsgSendService.insertImMsgSend(source));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 4)
-    @ApiOperation(value="通过主键id编辑推送消息表,保存某个用户收到了哪些消息。",notes = "权限: mobile:msgSend:edit")
-    @PostMapping("/edit")
-    @OperateLog
-    @SaCheckPermission("mobile:msgSend:edit")
-    public CommonResult<Boolean> edit(@RequestBody@Validated(Update.class) ImMsgSendDTO source) {
-        return CommonResult.success(imMsgSendService.updateImMsgSendById(source));
-    }
-
-    @ApiOperationSupport(author = "lf",order = 5)
-    @ApiOperation(value="删除推送消息表,保存某个用户收到了哪些消息。",notes = "权限: mobile:msgSend:remove")
-    @PostMapping("/removeByIds")
-    @OperateLog
-    @SaCheckPermission("mobile:msgSend:remove")
-    public CommonResult<Integer> delete(@RequestBody Collection<String> ids) {
-        return CommonResult.success(imMsgSendService.removeImMsgSendByIds(ids));
-    }
-}

+ 43 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/controller/vo/ImMsgReceivedVO.java

@@ -0,0 +1,43 @@
+package cn.tr.module.mobile.controller.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.ToString;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。传输对象
+ *
+ * @author lf
+ * @date  2025/08/20 10:14
+ **/
+@Data
+@ApiModel("消息表")
+@ToString
+public class ImMsgReceivedVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    @ApiModelProperty(value = "消息id", position = 1)
+    private String msgId;
+
+    @ApiModelProperty(value = "消息发送者", position = 2)
+    private String msgFrom;
+
+    @ApiModelProperty(value = "消息发送者角色", position = 2)
+    private String msgFromRole;
+
+    @ApiModelProperty(value = "临床id", position = 4)
+    private String groupId;
+
+    @ApiModelProperty(value = "消息内容", position = 7)
+    private String msgContent;
+
+    @ApiModelProperty(value = "服务端收到消息的时间", position = 8)
+    private Date sendTime;
+
+    @ApiModelProperty(value = "消息类型", position = 9)
+    private String msgType;
+}

+ 14 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImLoginSuccessDTO.java

@@ -0,0 +1,14 @@
+package cn.tr.module.mobile.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor(staticName = "of")
+public class ImLoginSuccessDTO {
+    private String tenantId;
+
+    private String userId;
+
+    private String clinicId;
+}

+ 2 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImMsgReceivedDTO.java

@@ -53,4 +53,6 @@ public class ImMsgReceivedDTO extends BaseDTO  {
     @ApiModelProperty(value = "0、未送达 1、送达", position = 10)
     private Integer delivered;
 
+    @ApiModelProperty(value = "消息发送者角色", position = 2)
+    private String msgFromRole;
 }

+ 5 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImMsgReceivedQueryDTO.java

@@ -4,6 +4,8 @@ import lombok.ToString;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
 import java.util.*;
 /**
  * 发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。查询参数
@@ -16,4 +18,7 @@ import java.util.*;
 @ToString
 public class ImMsgReceivedQueryDTO  {
     private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "手术id不能为空")
+    private String clinicId;
 }

+ 13 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/ImMsgUnreadTotalCountQueryDTO.java

@@ -0,0 +1,13 @@
+package cn.tr.module.mobile.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class ImMsgUnreadTotalCountQueryDTO {
+    @ApiModelProperty(value = "用户id",required = true)
+    @NotBlank(message = "用户id不能为空")
+    private String userId;
+}

+ 2 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/dto/MsgDTO.java

@@ -24,4 +24,6 @@ public class MsgDTO {
     private String fromUserId;
 
     private String toUserId;
+
+    private String tenantId;
 }

+ 3 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/mapper/ImMsgReceivedMapper.java

@@ -1,5 +1,6 @@
 package cn.tr.module.mobile.mapper;
 
+import cn.tr.module.mobile.controller.vo.ImMsgReceivedVO;
 import cn.tr.module.mobile.po.ImMsgReceivedPO;
 import cn.tr.module.mobile.dto.ImMsgReceivedDTO;
 import org.mapstruct.Mapper;
@@ -25,4 +26,6 @@ public interface ImMsgReceivedMapper {
 
     List<ImMsgReceivedPO> convertPOList(List<ImMsgReceivedDTO> source);
 
+    List<ImMsgReceivedVO> convertVOList(List<ImMsgReceivedPO> source);
+
 }

+ 6 - 2
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/po/ImGroupUserPO.java

@@ -2,6 +2,7 @@ package cn.tr.module.mobile.po;
 
 
 import cn.tr.plugin.mybatis.pojo.BasePO;
+import cn.tr.plugin.mybatis.pojo.TenantPO;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.annotations.ApiModelProperty;
@@ -19,7 +20,7 @@ import java.util.*;
 @TableName(value="im_group_user",autoResultMap = true)
 @EqualsAndHashCode(callSuper = true)
 @ToString
-public class ImGroupUserPO extends BasePO {
+public class ImGroupUserPO extends TenantPO {
 
     /** 群id */
     @TableId
@@ -27,6 +28,9 @@ public class ImGroupUserPO extends BasePO {
     private String groupId;
 
     /** 发送者 */
-    @ApiModelProperty(value = "发送者", position = 2)
+    @ApiModelProperty(value = "用户id", position = 2)
     private String userId;
+
+    @ApiModelProperty("消息未读数量")
+    private Long unreadCount;
 }

+ 3 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/po/ImMsgReceivedPO.java

@@ -61,4 +61,7 @@ public class ImMsgReceivedPO extends BasePO {
     /** 0、未送达 1、送达 */
     @ApiModelProperty(value = "0、未送达 1、送达", position = 10)
     private Integer delivered;
+
+    @ApiModelProperty(value = "消息发送者角色", position = 2)
+    private String msgFromRole;
 }

+ 5 - 1
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/repository/ImGroupUserRepository.java

@@ -2,6 +2,8 @@ package cn.tr.module.mobile.repository;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
 import org.springframework.stereotype.Repository;
 import cn.tr.module.mobile.po.ImGroupUserPO;
 /**
@@ -13,4 +15,6 @@ import cn.tr.module.mobile.po.ImGroupUserPO;
 @Repository
 @Mapper
 public interface ImGroupUserRepository extends BaseMapper<ImGroupUserPO> {
-}
+    @Update("update im_group_user set unread_count = 0 where group_id =#{clinicId} and user_id=#{userId}")
+    void readAllMsg(@Param("clinicId") String clinicId, @Param("userId") String userId);
+}

+ 21 - 1
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/repository/ImMsgReceivedRepository.java

@@ -1,9 +1,15 @@
 package cn.tr.module.mobile.repository;
 
+import cn.tr.module.mobile.dto.ImMsgUnreadTotalCountQueryDTO;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 import org.springframework.stereotype.Repository;
 import cn.tr.module.mobile.po.ImMsgReceivedPO;
+
+import java.util.List;
+
 /**
  * 发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。Mapper接口
  *
@@ -13,4 +19,18 @@ import cn.tr.module.mobile.po.ImMsgReceivedPO;
 @Repository
 @Mapper
 public interface ImMsgReceivedRepository extends BaseMapper<ImMsgReceivedPO> {
-}
+    @Select("SELECT bcrdu.user_id AS merged_column\n" +
+            "FROM biz_clinic_room AS bcr\n" +
+            "LEFT JOIN biz_clinic_room_doctor_user AS bcrdu ON bcrdu.clinic_room_id = bcr.id\n" +
+            "LEFT JOIN biz_clinic_room_wx_user AS bcrwu ON bcrwu.clinic_room_id = bcr.id\n" +
+            "WHERE bcr.id = #{clinicId}\n" +
+            "  AND bcrdu.user_id IS NOT NULL\n" +
+            "UNION\n" +
+            "SELECT bcrwu.wx_user_id AS merged_column\n" +
+            "FROM biz_clinic_room AS bcr\n" +
+            "LEFT JOIN biz_clinic_room_doctor_user AS bcrdu ON bcrdu.clinic_room_id = bcr.id\n" +
+            "LEFT JOIN biz_clinic_room_wx_user AS bcrwu ON bcrwu.clinic_room_id = bcr.id\n" +
+            "WHERE bcr.id = #{clinicId}\n" +
+            "  AND bcrwu.wx_user_id IS NOT NULL")
+    List<String> selectAllUserId(@Param("clinicId") String clinicId);
+}

+ 5 - 31
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/service/IImMsgReceivedService.java

@@ -1,7 +1,10 @@
 package cn.tr.module.mobile.service;
 
+import cn.tr.module.mobile.controller.vo.ImMsgReceivedVO;
 import cn.tr.module.mobile.dto.ImMsgReceivedDTO;
 import cn.tr.module.mobile.dto.ImMsgReceivedQueryDTO;
+import cn.tr.module.mobile.dto.ImMsgUnreadTotalCountQueryDTO;
+
 import java.util.*;
 
 /**
@@ -18,37 +21,8 @@ public interface IImMsgReceivedService{
      * @author   lf
      * @date      2025/08/20 10:14
      */
-    List<ImMsgReceivedDTO> selectImMsgReceivedList(ImMsgReceivedQueryDTO query);
+    List<ImMsgReceivedVO> selectImMsgReceivedList(ImMsgReceivedQueryDTO query);
 
-    /**
-     * 根据id查询发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-     * @param    id 主键id
-     * @author   lf
-     * @date      2025/08/20 10:14
-     */
-    ImMsgReceivedDTO selectImMsgReceivedById(String id);
 
-    /**
-     * 编辑发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-     * @param   source 编辑实体类
-     * @author  lf
-     * @date     2025/08/20 10:14
-     */
-    boolean updateImMsgReceivedById(ImMsgReceivedDTO source);
-
-    /**
-     * 新增发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-     * @param   source 新增实体类
-     * @author lf
-     * @date  2025/08/20 10:14
-     */
-    boolean insertImMsgReceived(ImMsgReceivedDTO source);
-
-    /**
-     * 删除发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。详情
-     * @param  ids 删除主键集合
-     * @author lf
-     * @date    2025/08/20 10:14
-     */
-    int removeImMsgReceivedByIds(Collection<String> ids);
+    Long selectUndReadTotalCount(ImMsgUnreadTotalCountQueryDTO query);
 }

+ 93 - 61
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/service/impl/ImMsgReceivedServiceImpl.java

@@ -1,21 +1,30 @@
 package cn.tr.module.mobile.service.impl;
 
-import cn.tr.core.exception.TRExcCode;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import cn.tr.core.tenant.TenantContextHolder;
 import cn.tr.module.mobile.config.ServerEventCallbackHandler;
+import cn.tr.module.mobile.controller.vo.ImMsgReceivedVO;
+import cn.tr.module.mobile.dto.ImMsgUnreadTotalCountQueryDTO;
+import cn.tr.module.mobile.dto.MsgDTO;
+import cn.tr.module.mobile.po.ImGroupUserPO;
+import cn.tr.module.mobile.repository.ImGroupUserRepository;
+import cn.tr.module.mobile.utils.UserUtils;
+import net.x52im.mobileimsdk.server.network.MBObserver;
+import net.x52im.mobileimsdk.server.processor.OnlineProcessor;
+import net.x52im.mobileimsdk.server.protocal.Protocal;
+import net.x52im.mobileimsdk.server.protocal.ProtocalFactory;
+import net.x52im.mobileimsdk.server.utils.LocalSendHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import cn.hutool.core.collection.CollectionUtil;
-import org.springframework.transaction.annotation.Transactional;
-import cn.tr.core.exception.ServiceException;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import cn.tr.module.mobile.repository.ImMsgReceivedRepository;
 import cn.tr.module.mobile.po.ImMsgReceivedPO;
-import cn.tr.module.mobile.dto.ImMsgReceivedDTO;
 import cn.tr.module.mobile.dto.ImMsgReceivedQueryDTO;
 import java.util.*;
 import cn.tr.module.mobile.service.IImMsgReceivedService;
 import cn.tr.module.mobile.mapper.ImMsgReceivedMapper;
-
 import javax.annotation.PostConstruct;
 
 /**
@@ -28,9 +37,13 @@ import javax.annotation.PostConstruct;
 public class ImMsgReceivedServiceImpl implements IImMsgReceivedService {
     @Autowired
     private ImMsgReceivedRepository baseRepository;
+
+    @Autowired
+    private ImGroupUserRepository groupUserRepository;
+
     @PostConstruct
     public void init(){
-        ServerEventCallbackHandler.onTransferMessage4C2C=msgDTO -> {
+        ServerEventCallbackHandler.onTransferMessage4C2S= msgDTO -> {
             ImMsgReceivedPO p = baseRepository.selectById(msgDTO.getMsgId());
             if(p!=null){
                 return;
@@ -43,72 +56,91 @@ public class ImMsgReceivedServiceImpl implements IImMsgReceivedService {
             receivedPO.setMsgContent(msgDTO.getContent());
             receivedPO.setSendTime(new Date());
             receivedPO.setMsgType(msgDTO.getType());
+            receivedPO.setMsgFromRole(msgDTO.getRole());
             baseRepository.insert(receivedPO);
+            //发送群组消息
+            List<String> userIds = ServerEventCallbackHandler.onGetGroupUserIds.apply(msgDTO.getClinicId());
+            CollectionUtil.removeAny(userIds,msgDTO.getFromUserId());
+            for (String userId : userIds) {
+                msgDTO.setToUserId(userId);
+                try {
+                    sendMsg(msgDTO);
+                }catch (Exception e){
+                }
+            }
+        };
+
+        ServerEventCallbackHandler.onUserLoginSuccess= loginInfo -> {
+            TenantContextHolder.setIgnore(Boolean.TRUE);
+            groupUserRepository.readAllMsg(loginInfo.getClinicId(), loginInfo.getUserId());
+            TenantContextHolder.setIgnore(Boolean.FALSE);
         };
+
+        ServerEventCallbackHandler.onGetGroupUserIds=clinicId->baseRepository.selectAllUserId(clinicId);
+        ;
+    }
+
+    public void sendMsg(MsgDTO msg) throws Exception {
+        String toUser = UserUtils.formatUserId(msg);
+        if(OnlineProcessor.isOnline(toUser)) {
+            final Protocal p = ProtocalFactory.createCommonData(JSONUtil.toJsonStr(msg), "0", toUser, true, msg.getMsgId(), -1);
+            MBObserver resultObserver = new MBObserver() {
+                public void update(boolean b, Object o) {
+                    if (b) {
+                        //更新已读消息
+                        ServerEventCallbackHandler.onTransfer4S2CMessageSuccess.accept(msg);
+                    }
+                }
+            };
+            LocalSendHelper.sendData(p, resultObserver);
+        }else {
+            //不在线的时候更新未读
+            ImGroupUserPO groupUserPO = groupUserRepository.selectOne(new LambdaQueryWrapper<ImGroupUserPO>()
+                    .eq(ImGroupUserPO::getUserId, msg.getToUserId())
+                    .eq(ImGroupUserPO::getGroupId, msg.getClinicId())
+                    .last("limit 1"));
+            UserUtils.setCurrentUser(toUser,msg.getTenantId());
+            if(ObjectUtil.isNull(groupUserPO)){
+                groupUserPO=new ImGroupUserPO();
+                groupUserPO.setGroupId(msg.getClinicId());
+                groupUserPO.setUserId( msg.getToUserId());
+                groupUserPO.setUnreadCount(1L);
+                groupUserPO.setTenantId(msg.getTenantId());
+                groupUserRepository.insert(groupUserPO);
+            }else {
+                groupUserPO.setUnreadCount(groupUserPO.getUnreadCount()+1);
+                groupUserRepository.updateById(groupUserPO);
+            }
+        }
     }
 
 
     /**
-    * 根据条件查询发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-    * @param    query 查询参数
-    * @author   lf
-    * @date      2025/08/20 10:14
-    */
+     * 根据条件查询发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
+     * @param    query 查询参数
+     * @author   lf
+     * @date      2025/08/20 10:14
+     */
     @Override
-    public List<ImMsgReceivedDTO> selectImMsgReceivedList(ImMsgReceivedQueryDTO query){
-        return ImMsgReceivedMapper.INSTANCE.convertDtoList(
+    public List<ImMsgReceivedVO> selectImMsgReceivedList(ImMsgReceivedQueryDTO query){
+        return ImMsgReceivedMapper.INSTANCE.convertVOList(
                 baseRepository.selectList(new LambdaQueryWrapper<ImMsgReceivedPO>()
+                        .eq(ImMsgReceivedPO::getGroupId,query.getClinicId())
+                        .orderByDesc(ImMsgReceivedPO::getSendTime)
                 )
         );
     };
 
-    /**
-    * 根据id查询发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-    * @param    id 主键id
-    * @author   lf
-    * @date      2025/08/20 10:14
-    */
     @Override
-    public ImMsgReceivedDTO selectImMsgReceivedById(String id){
-        return ImMsgReceivedMapper.INSTANCE.convertDto(baseRepository.selectById(id));
-    };
-
-    /**
-    * 编辑发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-    * @param   source 编辑实体类
-    * @author  lf
-    * @date     2025/08/20 10:14
-    */
-    @Transactional(rollbackFor = Exception.class)
-    @Override
-    public boolean updateImMsgReceivedById(ImMsgReceivedDTO source){
-            return baseRepository.updateById(ImMsgReceivedMapper.INSTANCE.convertPO(source))!=0;
-    };
-
-    /**
-    * 新增发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。
-    * @param   source 新增实体类
-    * @author lf
-    * @date  2025/08/20 10:14
-    */
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public boolean insertImMsgReceived(ImMsgReceivedDTO source){
-        return baseRepository.insert(ImMsgReceivedMapper.INSTANCE.convertPO(source))!=0;
-    };
-
-    /**
-    * 删除发送消息表,保存某个用户发送了哪些消息,用于复现用户聊天场景(消息漫游功能需要)。详情
-    * @param  ids 删除主键集合
-    * @author lf
-    * @date    2025/08/20 10:14
-    */
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public int removeImMsgReceivedByIds(Collection<String> ids){
-        if(CollectionUtil.isEmpty(ids)){
-            throw new ServiceException(TRExcCode.SYSTEM_ERROR_B0001,"请选择要删除的数据");
+    public Long selectUndReadTotalCount(ImMsgUnreadTotalCountQueryDTO query){
+        List<ImGroupUserPO> imGroupUserList = groupUserRepository.selectList(new LambdaQueryWrapper<ImGroupUserPO>()
+                .eq(ImGroupUserPO::getUserId, query.getUserId()));
+        if(CollectionUtil.isEmpty(imGroupUserList)){
+            return 0L;
         }
-        return baseRepository.deleteBatchIds(ids);
-    };
-}
+        return imGroupUserList.stream()
+                .map(ImGroupUserPO::getUnreadCount)
+                .filter(Objects::nonNull)
+                .reduce(0L,Long::sum);
+    }
+}

+ 32 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/service/impl/ImMsgSendServiceImpl.java

@@ -1,6 +1,16 @@
 package cn.tr.module.mobile.service.impl;
 
+import cn.hutool.json.JSONUtil;
 import cn.tr.core.exception.TRExcCode;
+import cn.tr.module.mobile.config.ServerEventCallbackHandler;
+import cn.tr.module.mobile.dto.MsgDTO;
+import cn.tr.module.mobile.po.ImMsgReceivedPO;
+import cn.tr.module.mobile.utils.UserUtils;
+import net.x52im.mobileimsdk.server.network.MBObserver;
+import net.x52im.mobileimsdk.server.processor.OnlineProcessor;
+import net.x52im.mobileimsdk.server.protocal.Protocal;
+import net.x52im.mobileimsdk.server.protocal.ProtocalFactory;
+import net.x52im.mobileimsdk.server.utils.LocalSendHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import cn.hutool.core.collection.CollectionUtil;
@@ -14,6 +24,9 @@ import cn.tr.module.mobile.dto.ImMsgSendQueryDTO;
 import java.util.*;
 import cn.tr.module.mobile.service.IImMsgSendService;
 import cn.tr.module.mobile.mapper.ImMsgSendMapper;
+
+import javax.annotation.PostConstruct;
+
 /**
  * 推送消息表,保存某个用户收到了哪些消息。Service接口实现类
  *
@@ -25,6 +38,25 @@ public class ImMsgSendServiceImpl implements IImMsgSendService {
     @Autowired
     private ImMsgSendRepository baseRepository;
 
+    @PostConstruct
+    public void init(){
+        ServerEventCallbackHandler.onTransfer4S2CMessageSuccess=msgDTO -> {
+            ImMsgSendPO p = baseRepository.selectById(msgDTO.getMsgId());
+            if(p!=null){
+                return;
+            }
+            UserUtils.setCurrentUser(msgDTO.getFromUserId(),msgDTO.getTenantId());
+            ImMsgSendPO sendPO = new ImMsgSendPO();
+            sendPO.setMsgId(msgDTO.getMsgId());
+            sendPO.setMsgFrom(msgDTO.getFromUserId());
+            sendPO.setMsgTo(msgDTO.getToUserId());
+            sendPO.setGroupId(msgDTO.getClinicId());
+            sendPO.setMsgContent(msgDTO.getContent());
+            sendPO.setSendTime(new Date());
+            sendPO.setMsgType(msgDTO.getType());
+            baseRepository.insert(sendPO);
+        };
+    }
 
     /**
     * 根据条件查询推送消息表,保存某个用户收到了哪些消息。

+ 20 - 0
tr-modules/tr-module-mobile/src/main/java/cn/tr/module/mobile/utils/UserUtils.java

@@ -0,0 +1,20 @@
+package cn.tr.module.mobile.utils;
+
+import cn.tr.module.mobile.dto.MsgDTO;
+import cn.tr.plugin.security.bo.UserLoginInfoBO;
+import cn.tr.plugin.security.context.LoginUserContextHolder;
+
+public class UserUtils {
+    public static void setCurrentUser(String userId,String tenantId) {
+        UserLoginInfoBO userLoginInfoBO = new UserLoginInfoBO();
+        userLoginInfoBO.setUserId(userId);
+        userLoginInfoBO.setTenantId(tenantId);
+        LoginUserContextHolder.setUser(userLoginInfoBO);
+    }
+
+
+
+    public static String formatUserId(MsgDTO msg){
+        return msg.getTenantId()+"-"+msg.getToUserId()+"-"+ msg.getClinicId();
+    }
+}

+ 3 - 0
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/app/controller/vo/DoctorClinicRoomVO.java

@@ -79,4 +79,7 @@ public class DoctorClinicRoomVO implements Serializable {
 
     @ApiModelProperty(value = "加减档提示")
     private String warnFlow;
+
+    @ApiModelProperty("未读消息数量")
+    private Long unreadCount;
 }

+ 1 - 1
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/config/RabbitMQConfig.java

@@ -20,7 +20,7 @@ public class RabbitMQConfig {
     @Bean
     public Queue patientMonitorQueue() {
         // 设置队列不持久化,并且在没有消费者时自动删除
-        return new Queue(RabbitMQConstant.QUEUE_PATIENT_MONITOR, false, false, true);
+        return new Queue(RabbitMQConstant.QUEUE_PATIENT_MONITOR, Boolean.FALSE, false, Boolean.TRUE);
     }
 
     @Bean

+ 1 - 1
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/enums/RabbitMQConstant.java

@@ -8,4 +8,4 @@ public interface RabbitMQConstant {
     String ROUTING_KEY_PATIENT_MONITOR = "patient.monitor";
 
     String QUEUE_PATIENT_MONITOR = "patient.monitor.queue";
-}
+}

+ 7 - 1
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/po/BizInfusionClinicPO.java

@@ -34,7 +34,13 @@ public class BizInfusionClinicPO extends BasePO {
     @ApiModelProperty(value = "手术id", position = 3)
     private String clinicId;
 
-    /** 绑定类型 */
+    /**
+     * {@link cn.tr.module.smart.common.enums.InfusionBindType}
+     * 绑定类型
+     * */
     @ApiModelProperty(value = "绑定类型", position = 4)
     private String type;
+
+    @ApiModelProperty("设备id")
+    private String deviceId;
 }

+ 3 - 0
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/service/IBizDeviceService.java

@@ -3,6 +3,7 @@ package cn.tr.module.smart.common.service;
 import cn.tr.module.smart.common.dto.BizDeviceDTO;
 import cn.tr.module.smart.common.dto.BizDeviceQueryDTO;
 import cn.tr.module.smart.web.dto.BizDeviceAlarmInfoDTO;
+import cn.tr.module.smart.web.dto.BizDeviceBindClinicDTO;
 import cn.tr.module.smart.web.vo.BizDeviceDetailVO;
 
 import java.util.List;
@@ -39,4 +40,6 @@ public interface IBizDeviceService {
      * @date 2025/8/11
      */
     List<BizDeviceAlarmInfoDTO> queryAlarmInfo(String infusionId);
+
+    Boolean bindClinic(BizDeviceBindClinicDTO source);
 }

+ 24 - 4
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/common/service/impl/BizDeviceServiceImpl.java

@@ -5,19 +5,19 @@ import cn.tr.core.exception.ServiceException;
 import cn.tr.core.exception.TRExcCode;
 import cn.tr.module.smart.common.dto.BizDeviceDTO;
 import cn.tr.module.smart.common.dto.BizDeviceQueryDTO;
+import cn.tr.module.smart.common.enums.InfusionBindType;
 import cn.tr.module.smart.common.mapper.BizDeviceAlarmMapper;
 import cn.tr.module.smart.common.mapper.BizInfusionHistoryMapper;
 import cn.tr.module.smart.common.po.BizDeviceAlarmPO;
 import cn.tr.module.smart.common.po.BizDevicePO;
 import cn.tr.module.smart.common.po.BizInfusionClinicPO;
-import cn.tr.module.smart.common.po.BizInfusionHistoryPO;
 import cn.tr.module.smart.common.repository.BizDeviceAlarmRepository;
 import cn.tr.module.smart.common.repository.BizDeviceRepository;
 import cn.tr.module.smart.common.repository.BizInfusionClinicRepository;
-import cn.tr.module.smart.common.repository.BizInfusionHistoryRepository;
 import cn.tr.module.smart.common.service.IBizDeviceService;
 import cn.tr.module.smart.web.dto.BizDeviceAlarmInfoDTO;
 import cn.tr.module.smart.web.dto.BizDeviceAndClinicDetailQueryDTO;
+import cn.tr.module.smart.web.dto.BizDeviceBindClinicDTO;
 import cn.tr.module.smart.web.vo.BizDeviceAndClinicDetailVO;
 import cn.tr.module.smart.web.vo.BizDeviceDetailVO;
 import cn.tr.module.smart.web.vo.BizDeviceInfoVO;
@@ -25,7 +25,7 @@ import cn.tr.module.smart.web.vo.BizInfusionInfoVO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-
+import org.springframework.transaction.annotation.Transactional;
 import java.util.List;
 
 /**
@@ -41,6 +41,9 @@ public class BizDeviceServiceImpl implements IBizDeviceService {
 
     @Autowired
     private BizDeviceAlarmRepository bizDeviceAlarmRepository;
+
+    @Autowired
+    private BizInfusionClinicRepository infusionClinicRepository;
     /**
      * @param query 查询参数
      * @description: 根据条件查询泵设备列表
@@ -111,4 +114,21 @@ public class BizDeviceServiceImpl implements IBizDeviceService {
         return BizDeviceAlarmMapper.INSTANCE.convertAlarmInfoDtoList(bizDeviceAlarmPOS);
     }
 
-}
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean bindClinic(BizDeviceBindClinicDTO source) {
+        BizDevicePO device = bizDeviceRepository.selectById(source.getDeviceId());
+        if (ObjectUtil.isNull(device)) {
+            throw new ServiceException(TRExcCode.SYSTEM_ERROR_B0001, "泵信息不存在");
+        }
+        String infusionId = device.getInfusionId();
+        infusionClinicRepository.delete(new LambdaQueryWrapper<BizInfusionClinicPO>()
+                .eq(BizInfusionClinicPO::getInfusionId,infusionId));
+        BizInfusionClinicPO infusionClinicPO = new BizInfusionClinicPO();
+        infusionClinicPO.setType(InfusionBindType.manualBind);
+        infusionClinicPO.setClinicId(source.getClinicId());
+        infusionClinicPO.setInfusionId(infusionId);
+        infusionClinicPO.setDeviceId(source.getDeviceId());
+        return infusionClinicRepository.insert(infusionClinicPO)!=0;
+    }
+}

+ 5 - 6
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/web/controller/BizDeviceController.java

@@ -75,11 +75,10 @@ public class BizDeviceController extends BaseController {
     }
 
 
-    @ApiOperationSupport(author = "lf", order = 5)
+    @ApiOperationSupport(author = "lf", order = 6)
     @ApiOperation(value = "绑定手术信息", notes = "权限: 无")
-    @GetMapping("/bindClinic}")
-    public CommonResult<String> bindClinic(@RequestBody@Validated BizDeviceBindClinicDTO source) {
-        return CommonResult.success(null);
+    @PostMapping("/bindClinic}")
+    public CommonResult<Boolean> bindClinic(@RequestBody@Validated BizDeviceBindClinicDTO source) {
+        return CommonResult.success(bizDeviceService.bindClinic(source));
     }
-
-}
+}

+ 3 - 0
tr-modules/tr-module-smartFollowUp/src/main/java/cn/tr/module/smart/wx/controller/vo/BizWxAppletClinicDetailVO.java

@@ -90,4 +90,7 @@ public class BizWxAppletClinicDetailVO implements Serializable {
 
     @ApiModelProperty("术后问卷id")
     private String postQuestionGroupId;
+
+    @ApiModelProperty("未读消息数量")
+    private Long unreadCount;
 }

+ 7 - 2
tr-modules/tr-module-smartFollowUp/src/main/resources/mapper/smart/BizClinicRoomMapper.xml

@@ -51,6 +51,7 @@
         <result property="warnWillFinished" column="warn_will_finished"/>
         <result property="warnAnalgesicPoor" column="warn_analgesic_poor"/>
         <result property="warnFlow" column="warn_flow"/>
+        <result property="unreadCount" column="unread_count"/>
     </resultMap>
 
 
@@ -94,7 +95,8 @@
         bp.card_no as card_no,
         bcr.last_before_question_time as last_before_question_time,
         bcr.last_pain_assessment_time as last_pain_assessment_time,
-        bcr.last_after_question_time as last_after_question_time
+        bcr.last_after_question_time as last_after_question_time,
+        GREATEST(COALESCE(igu.unread_count, 0), 0) as unread_count
         from biz_clinic_room as bcr
         join biz_patient as bp on bp.id = bcr.patient_id
         join biz_clinic_room_wx_user as bcrwu on bcrwu.clinic_room_id = bcr.id
@@ -102,6 +104,7 @@
         left join sys_user as su on bcrmu.user_id = su.id
         left join biz_question_answer as bqa on bqa.clinic_id = bcr.id
         left join biz_dept as bd on bd.id=bcr.dept_id
+        left join (select * from im_group_user where user_id=  #{query.userId,jdbcType=VARCHAR}) as igu on igu.group_id=bcr.id
         <where>
             and bcr.deleted=0 and bcrwu.wx_user_id = #{query.userId}
             <if test="query.patientCode != null and query.patientCode != ''">
@@ -131,11 +134,13 @@
         bih.warn_low_battery as warn_low_battery,
         bih.warn_will_finished as warn_will_finished,
         bih.warn_flow as warn_flow,
-        bih.warn_analgesic_poor as warn_analgesic_poor
+        bih.warn_analgesic_poor as warn_analgesic_poor,
+        GREATEST(COALESCE(igu.unread_count, 0), 0) as unread_count
         FROM biz_clinic_room bcr
         join biz_clinic_room_doctor_user bcrmu on bcr.id = bcrmu.clinic_room_id
         left join biz_infusion_clinic as bic on bic.clinic_id = bcr.id
         left join biz_infusion_history as bih on bih.id=bic.infusion_id
+        left join (select * from im_group_user where user_id=  #{source.currentUserId,jdbcType=VARCHAR}) as igu on igu.group_id=bcr.id
         <where>
             and bcr.deleted = 0
             <if test="source.clinicStatus != null and source.clinicStatus != ''">

+ 3 - 0
tr-test/src/main/resources/application-doc.yml

@@ -26,17 +26,20 @@ knife4j:
         api-rule-resources:
           - cn.tr.module.smart.web
           - cn.tr.module.smart.common
+          - cn.tr.module.mobile
       smartApplet:
         group-name: 智慧随访-小程序端
         api-rule: package
         api-rule-resources:
           - cn.tr.module.smart.wx
+          - cn.tr.module.mobile
       smartDoctor:
         group-name: 智慧随访-医生端
         api-rule: package
         api-rule-resources:
           - cn.tr.module.smart.app
           - cn.tr.module.smart.common
+          - cn.tr.module.mobile
       tianai:
         group-name: 图片验证码
         api-rule: package

+ 4 - 0
tr-test/src/main/resources/application.yml

@@ -93,6 +93,10 @@ tr:
       - sys_export_row
 #      数据对接模块
       - joint_datasource
+#     即时通讯模块
+      - im_msg_group
+      - im_msg_received
+      - im_msg_send
       - biz_infusion_clinic
     ignore-urls:
       - /oauth2/psw/token

+ 1 - 1
tr-test/src/main/resources/log4j2.xml

@@ -37,7 +37,7 @@
     <Loggers>
         <logger name="org.springframework" level="INFO"></logger>
         <logger name="org.mybatis" level="INFO"></logger>
-
+        <logger name="net.x52im.mobileimsdk" level="WARN"></logger>
         <Root level="INFO">
             <AppenderRef ref="Console"/>
             <AppenderRef ref="RollingFile"/>