Long 1 тиждень тому
батько
коміт
6baf9fc2a8
33 змінених файлів з 1006 додано та 142 видалено
  1. 21 0
      fs-common/src/main/java/com/fs/common/utils/PinYinUtil.java
  2. 123 24
      fs-company/src/main/java/com/fs/company/controller/qw/QwMsgController.java
  3. 97 17
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  4. 5 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  5. 2 2
      fs-service/src/main/java/com/fs/course/param/FsCourseLinkMiniParam.java
  6. 2 2
      fs-service/src/main/java/com/fs/course/param/FsCourseListBySidebarParam.java
  7. 6 4
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  8. 3 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  9. 8 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  10. 84 10
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  11. 27 0
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogIMVO.java
  12. 11 8
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  13. 84 40
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  14. 5 0
      fs-service/src/main/java/com/fs/qw/domain/QwSession.java
  15. 11 2
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  16. 6 0
      fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java
  17. 20 2
      fs-service/src/main/java/com/fs/qw/mapper/QwSessionMapper.java
  18. 16 0
      fs-service/src/main/java/com/fs/qw/service/IQwMsgService.java
  19. 186 28
      fs-service/src/main/java/com/fs/qw/service/impl/QwMsgServiceImpl.java
  20. 35 0
      fs-service/src/main/java/com/fs/qw/vo/QwContactVO.java
  21. 15 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxRoomHeaderDTO.java
  22. 15 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxRoomHeaderResp.java
  23. 20 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxWorkChatId2RoomIdDTO.java
  24. 15 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxWorkChatId2RoomIdResp.java
  25. 1 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxWorkMessageDTO.java
  26. 20 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxWorkRoomId2ChatIdDTO.java
  27. 24 0
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java
  28. 36 0
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java
  29. 1 1
      fs-service/src/main/resources/application-config-dev.yml
  30. 21 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  31. 32 0
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml
  32. 25 0
      fs-service/src/main/resources/mapper/qw/QwGroupChatMapper.xml
  33. 29 2
      fs-service/src/main/resources/mapper/qw/QwSessionMapper.xml

+ 21 - 0
fs-common/src/main/java/com/fs/common/utils/PinYinUtil.java

@@ -91,6 +91,27 @@ public class PinYinUtil {
         }
     }
 
+    /**
+     * 获取字符串首字母
+     */
+    public static String getFirstLetter(String str) {
+        if (str == null || str.isEmpty()) {
+            return "";
+        }
+
+        // 去除前后空格
+        str = str.trim();
+        char firstChar = str.charAt(0);
+
+        char firstLetter = Char2Initial(firstChar);
+        if (Character.isLetter(firstLetter)) {
+            return String.valueOf(firstLetter).toUpperCase();
+        }
+
+        return "";
+    }
+
+
     public static void main(String[] args) throws Exception {
         System.out.println(cn2py("重庆重视发展IT行业,大多数外企,如,IBM等进驻山城"));
     }

+ 123 - 24
fs-company/src/main/java/com/fs/company/controller/qw/QwMsgController.java

@@ -8,28 +8,32 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.exception.CustomException;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.param.FsCourseLinkMiniParam;
 import com.fs.course.param.FsCourseListBySidebarParam;
+import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsCourseListBySidebarVO;
 import com.fs.course.vo.FsCourseVideoListBySidebarVO;
+import com.fs.course.vo.FsCourseWatchLogIMVO;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwExternalContactInfo;
-import com.fs.qw.domain.QwMsg;
-import com.fs.qw.domain.QwUser;
+import com.fs.his.domain.FsStoreOrder;
+import com.fs.his.domain.FsUserOperationLog;
+import com.fs.his.service.IFsStoreOrderService;
+import com.fs.his.service.IFsUserOperationLogService;
+import com.fs.his.vo.FsStoreOrderListVO;
+import com.fs.his.vo.FsUserOperationLogVo;
+import com.fs.qw.domain.*;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
-import com.fs.qw.service.IQwExternalContactInfoService;
-import com.fs.qw.service.IQwExternalContactService;
-import com.fs.qw.service.IQwMsgService;
-import com.fs.qw.service.IQwUserService;
+import com.fs.qw.service.*;
 import com.fs.qw.vo.QwContactListVO;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwMessageListVO;
 import com.fs.statis.service.IStatisticsService;
 import com.fs.statistics.dto.WatchCourseStatisticsDTO;
@@ -42,9 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 
 /**
  * 企微聊天记录Controller
@@ -73,6 +75,14 @@ public class QwMsgController extends BaseController
     private IFsUserCourseService fsUserCourseService;
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private IFsCourseWatchLogService watchLogService;
+    @Autowired
+    private IFsUserOperationLogService userOperationLogService;
+    @Autowired
+    private IQwSessionService sessionService;
 
     /**
      * 查询企微聊天记录列表
@@ -156,13 +166,10 @@ public class QwMsgController extends BaseController
     @GetMapping("/conversationList/{userId}")
     @ApiOperation("获取会话")
     public R conversations(@PathVariable("userId")Long qwUserId){
+        startPage();
         List<QwContactListVO> list = qwMsgService.selectQwConversationByUserId(qwUserId);
-        for (QwContactListVO contract:list) {
-            if(StringUtils.isEmpty(contract.getDisplayName())){
-                contract.setDisplayName("群聊");
-            }
-        }
-        return R.ok().put("data",list);
+        PageInfo<QwContactListVO> result = new PageInfo<>(list);
+        return R.ok().put("data", result);
     }
     //根据会话获取消息
     @GetMapping("/getQwMessageListBySession")
@@ -190,15 +197,17 @@ public class QwMsgController extends BaseController
         return R.ok().put("data",data);
     }
 
+    @ApiOperation("获取外部联系人详情")
     @GetMapping("/getQwExternalContactDetails")
     public R getQwExternalContactDetails(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId){
         return R.ok().put("data", qwExternalContactService.getQwExternalContactDetailsById(qwExternalContactId));
     }
 
     @GetMapping("/getQwUserInfo")
-    @ApiOperation("获取企微用户信息")
+    @ApiOperation("获取外部联系人用户信息")
     public R getQwUserInfo(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId){
-        if(qwExternalContactId == null) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if(externalContact == null) {
             throw new CustomException("企微外部联系人id不能为空!");
         }
 
@@ -206,6 +215,8 @@ public class QwMsgController extends BaseController
         if (contactInfo==null){
             contactInfo = new QwExternalContactInfo();
             contactInfo.setExternalContactId(qwExternalContactId);
+            contactInfo.setName(externalContact.getName());
+            contactInfo.setSex(externalContact.getGender() == 1 ? "男" : externalContact.getGender() ==  2 ? "女" : "未知");
             qwExternalContactInfoService.insertQwExternalContactInfo(contactInfo);
         }
 
@@ -241,10 +252,12 @@ public class QwMsgController extends BaseController
     @PostMapping("/getFsCourseListBySidebar")
     @ApiOperation("获取视频课程下拉列表 侧边栏")
     public R getFsCourseListBySidebar(@RequestBody FsCourseListBySidebarParam param) {
+        QwSession qwSession = sessionService.selectQwSessionBySessionId(param.getSessionId());
+        if (qwSession == null) {
+            return R.error("会话不存在");
+        }
 
-        QwExternalContact externalContact = qwExternalContactService.getById(param.getExtId());
-        QwUser qwUser = qwUserService.selectQwUserById(externalContact.getQwUserId());
-
+        QwUser qwUser = qwUserService.selectQwUserById(Long.parseLong(qwSession.getQwUserId()));
         if (qwUser == null || qwUser.getCompanyId() == null) {
             return R.error("员工未绑定 销售公司 或 未获取到员工信息,请重试!");
         }
@@ -284,10 +297,96 @@ public class QwMsgController extends BaseController
             return R.error("视频id不能为空");
         }
 
-        if (Objects.isNull(param.getExtId())){
-            return R.error("客户id不能为空");
+        if (Objects.isNull(param.getSessionId())){
+            return R.error("会话ID不能为空");
         }
 
         return fsUserCourseVideoService.createMiniLinkByQwIm(param);
     }
+
+    // 获取联系人列表
+    @GetMapping("/contactList/{userId}")
+    @ApiOperation("获取联系人列表")
+    public TableDataInfo contacts(@PathVariable("userId") Long qwUserId) {
+        startPage();
+        List<QwContactVO> list  = qwMsgService.contactListByQwUserId(qwUserId);
+        return getDataTable(list);
+    }
+
+    // 获取群组列表
+    @GetMapping("/groupList/{userId}")
+    @ApiOperation("获取群组列表")
+    public TableDataInfo groups(@PathVariable("userId") Long qwUserId) {
+        startPage();
+        List<QwContactVO> list  = qwMsgService.groupListByQwUserId(qwUserId);
+        return getDataTable(list);
+    }
+
+    // 获取会话ID
+    @GetMapping("/getConversationId")
+    @ApiOperation("获取会话ID")
+    public R getConversationId(@RequestParam Long qwUserId, @RequestParam String id, @RequestParam Boolean isGroup) {
+        return R.ok().put("data", qwMsgService.getConversationIdById(qwUserId, id, isGroup));
+    }
+
+    @ApiOperation("获取外部联系人订单列表")
+    @GetMapping("/getQwExternalContactOrderList")
+    public TableDataInfo getQwExternalContactOrderList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        List<FsStoreOrder> orderList = new ArrayList<>();
+        if (externalContact.getFsUserId() != null) {
+            FsStoreOrder params = new FsStoreOrder();
+            params.setUserId(externalContact.getFsUserId());
+            params.setCompanyId(externalContact.getCompanyId());
+            params.setCompanyUserId(externalContact.getCompanyUserId());
+
+            startPage();
+            orderList = storeOrderService.selectFsStoreOrderList(params);
+        }
+
+        return getDataTable(orderList);
+    }
+
+    @ApiOperation("获取外部联系人看课记录列表")
+    @GetMapping("/getQwExternalContactWatchLogList")
+    public TableDataInfo getQwExternalContactWatchLogList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("externalContactId", externalContact.getId());
+        params.put("companyId", externalContact.getCompanyId());
+        params.put("companyUserId", externalContact.getCompanyUserId());
+
+        startPage();
+        List<FsCourseWatchLogIMVO> logList = watchLogService.selectWatchLogIMVOListByMap(params);
+        return getDataTable(logList);
+    }
+
+    @ApiOperation("获取外部联系人访问记录列表")
+    @GetMapping("/getQwExternalContactVisitList")
+    public TableDataInfo getQwExternalContactVisitList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        List<FsUserOperationLogVo> logList = new ArrayList<>();
+        if (externalContact.getFsUserId() != null) {
+            FsUserOperationLog params = new FsUserOperationLog();
+            params.setUserId(externalContact.getFsUserId());
+
+            startPage();
+            logList = userOperationLogService.selectFsUserOperationLogByList(params);
+        }
+
+        return getDataTable(logList);
+    }
+
 }

+ 97 - 17
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -303,10 +303,16 @@ public class QwMsgController {
                 break;
             case 102000:
                 WxWorkMessageDTO wxWorkMessageDTO = JSON.parseObject(wxWorkMsgResp.getJson(), WxWorkMessageDTO.class);
-                if (wxWorkMessageDTO.getIs_room()!=0){
+                if (wxWorkMessageDTO.getReferid()!=0){
                     break;
                 }
-                if (wxWorkMessageDTO.getReferid()!=0){
+                if (wxWorkMessageDTO.getIs_room()!=0){
+                    try {
+                        // 接收处理群消息
+                        processRoomMessage(wxWorkMsgResp, wxWorkMessageDTO, id, serverId);
+                    } catch (Exception e) {
+                        log.error("接收处理群消息失败 err: {}", e.getMessage(), e);
+                    }
                     break;
                 }
 
@@ -423,23 +429,23 @@ public class QwMsgController {
 
                 // 处理文本消息
                 if (wxWorkMessageDTO.getMsgtype() == 2 || wxWorkMessageDTO.getMsgtype() == 0) {
-                    processTextMessage(id, userId, wxWorkMessageDTO.getContent(), wxWorkMsgResp, sendType);
+                    processTextMessage(id, userId, wxWorkMessageDTO.getContent(), wxWorkMsgResp, sendType, false, null, null);
                 }
                 // 语音消息
                 if (wxWorkMessageDTO.getMsgtype() == 16) {
-                    processVoiceMessage(serverId, wxWorkMessageDTO.getContent(), wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                    processVoiceMessage(serverId, wxWorkMessageDTO.getContent(), wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType, false, null, null);
                 }
                 // 图片消息
                 if (wxWorkMessageDTO.getMsgtype() == 101){
-                    processImageMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                    processImageMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType, false, null, null);
                 }
                 // gif 表情消息
                 if (wxWorkMessageDTO.getMsgtype() == 104){
-                    processEmotionDynamicMessage(wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                    processEmotionDynamicMessage(wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType, false, null, null);
                 }
                 // 小程序消息
                 if (wxWorkMessageDTO.getMsgtype() == 78) {
-                    processMiniAppMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                    processMiniAppMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType, false, null, null);
                 }
 
                 break;
@@ -573,10 +579,13 @@ public class QwMsgController {
      * @param content           消息内容
      * @param wxWorkMsgResp     回调信息对象
      * @param sendType          发送者类型 1客户 2销售
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
      */
-    private void processTextMessage(Long id, Long userId, String content, WxWorkMsgResp wxWorkMsgResp, Integer sendType) {
+    private void processTextMessage(Long id, Long userId, String content, WxWorkMsgResp wxWorkMsgResp, Integer sendType, boolean isRoom, String chatId, String chatAvatar) {
         // 保存聊天消息
-        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 1);
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 1, isRoom, chatId, chatAvatar);
         QwImSocket.broadcast(message);
     }
 
@@ -589,8 +598,11 @@ public class QwMsgController {
      * @param id                企微用户ID
      * @param userId            消息发送者ID
      * @param sendType          发送者类型 1客户 2销售
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
      */
-    private void processVoiceMessage(Long serverId, String content, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, Integer sendType) {
+    private void processVoiceMessage(Long serverId, String content, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, Integer sendType, boolean isRoom, String chatId, String chatAvatar) {
         String voiceFileName = IdUtils.fastSimpleUUID() + ".silk";
         WxWorkResponseDTO<String> fileUrlResp =
                 aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getVoice_id(), wxWorkMessageDTO.getAes_key(), 5, voiceFileName, wxWorkMessageDTO.getVoice_size(), serverId);
@@ -620,7 +632,7 @@ public class QwMsgController {
         json.put("content", content);
 
         // 保存聊天消息
-        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, json.toString(), wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 4);
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, json.toString(), wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 4, isRoom, chatId, chatAvatar);
         QwImSocket.broadcast(message);
     }
 
@@ -632,8 +644,11 @@ public class QwMsgController {
      * @param id                企微用户ID
      * @param userId            消息发送者ID
      * @param sendType          发送者类型 1客户 2销售
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
      */
-    private void processImageMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, Integer sendType) {
+    private void processImageMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, Integer sendType, boolean isRoom, String chatId, String chatAvatar) {
         String fileName = IdUtils.fastSimpleUUID() + ".jpg";
         WxWorkResponseDTO<String> fileUrlResp =
                 aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getFile_id(), wxWorkMessageDTO.getAes_key(), wxWorkMessageDTO.getOpenim_cdn_authkey(), fileName, wxWorkMessageDTO.getFile_size(), serverId);
@@ -644,7 +659,7 @@ public class QwMsgController {
 
         String content = fileUrlResp.getData();
         // 保存聊天消息
-        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 2);
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 2, isRoom, chatId, chatAvatar);
         QwImSocket.broadcast(message);
     }
 
@@ -655,11 +670,14 @@ public class QwMsgController {
      * @param id                企微用户ID
      * @param userId            消息发送者ID
      * @param sendType          发送者类型 1客户 2销售
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
      */
-    private void processEmotionDynamicMessage(WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, int sendType) {
+    private void processEmotionDynamicMessage(WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, int sendType, boolean isRoom, String chatId, String chatAvatar) {
         String content = wxWorkMessageDTO.getUrl();
         // 保存聊天消息
-        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 3);
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 3, isRoom, chatId, chatAvatar);
         QwImSocket.broadcast(message);
     }
 
@@ -671,8 +689,11 @@ public class QwMsgController {
      * @param id                企微用户ID
      * @param userId            消息发送者ID
      * @param sendType          发送者类型 1客户 2销售
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
      */
-    private void processMiniAppMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, int sendType) {
+    private void processMiniAppMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, int sendType, boolean isRoom, String chatId, String chatAvatar) {
         String thumbName = IdUtils.fastSimpleUUID() + ".jpg";
         WxWorkResponseDTO<String> fileUrlResp =
                 aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getThumbFileId(), wxWorkMessageDTO.getThumbAESKey(), 1, thumbName, wxWorkMessageDTO.getSize(), serverId);
@@ -691,8 +712,67 @@ public class QwMsgController {
         json.put("thumbnail", fileUrlResp.getData());
 
         // 保存聊天消息
-        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, json.toString(), wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 5);
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, json.toString(), wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 5, isRoom, chatId, chatAvatar);
         QwImSocket.broadcast(message);
     }
 
+    /**
+     * 处理群消息
+     */
+    private void processRoomMessage(WxWorkMsgResp wxWorkMsgResp, WxWorkMessageDTO wxWorkMessageDTO, Long qwUserId, Long serverId) {
+        log.debug("接收群消息 msg: {}", JSON.toJSONString(wxWorkMessageDTO));
+        Long receiver = wxWorkMessageDTO.getReceiver();
+        Long sender = wxWorkMessageDTO.getSender();
+
+        // 1客户 2销售
+        int sendType = 2;
+
+        // 消息发送者用户ID
+        Long userId = receiver;
+        if (receiver != 0 && 2000000000000000L - receiver > 0){
+            sendType = 1;
+            userId = sender;
+        }
+
+        WxWorkRoomId2ChatIdDTO roomId2ChatIdDTO = new WxWorkRoomId2ChatIdDTO();
+        roomId2ChatIdDTO.setUuid(wxWorkMsgResp.getUuid());
+        roomId2ChatIdDTO.setRoom_id(wxWorkMessageDTO.getRoom_conversation_id());
+        WxWorkResponseDTO<WxWorkChatId2RoomIdResp> roomId2ChatIdResp = wxWorkService.roomId2ChatId(roomId2ChatIdDTO, serverId);
+        if (roomId2ChatIdResp.getErrcode() != 0) {
+            log.warn("接收群消息  rooId2ChatId失败: {}", roomId2ChatIdResp.getErrmsg());
+            return;
+        }
+        String chatId = roomId2ChatIdResp.getData().getChatid();
+
+        String chatAvatar = "";
+        WxRoomHeaderDTO roomHeaderDTO = new WxRoomHeaderDTO();
+        roomHeaderDTO.setRoomid(roomId2ChatIdResp.getData().getRoom_id());
+        roomHeaderDTO.setUuid(wxWorkMsgResp.getUuid());
+        WxWorkResponseDTO<WxRoomHeaderResp> roomHeaderResp = wxWorkService.wxRoomHeader(roomHeaderDTO, serverId);
+        if (roomHeaderResp.getErrcode() == 0) {
+            chatAvatar = roomHeaderResp.getData().getImage_url();
+        }
+
+        // 处理文本消息
+        if (wxWorkMessageDTO.getMsgtype() == 2 || wxWorkMessageDTO.getMsgtype() == 0) {
+            processTextMessage(qwUserId, userId, wxWorkMessageDTO.getContent(), wxWorkMsgResp, sendType, true, chatId, chatAvatar);
+        }
+        // 语音消息
+        if (wxWorkMessageDTO.getMsgtype() == 16) {
+            processVoiceMessage(serverId, wxWorkMessageDTO.getContent(), wxWorkMessageDTO, wxWorkMsgResp, qwUserId, userId, sendType, true, chatId, chatAvatar);
+        }
+        // 图片消息
+        if (wxWorkMessageDTO.getMsgtype() == 101){
+            processImageMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, qwUserId, userId, sendType, true, chatId, chatAvatar);
+        }
+        // gif 表情消息
+        if (wxWorkMessageDTO.getMsgtype() == 104){
+            processEmotionDynamicMessage(wxWorkMessageDTO, wxWorkMsgResp, qwUserId, userId, sendType, true, chatId, chatAvatar);
+        }
+        // 小程序消息
+        if (wxWorkMessageDTO.getMsgtype() == 78) {
+            processMiniAppMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, qwUserId, userId, sendType, true, chatId, chatAvatar);
+        }
+    }
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -522,4 +522,9 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      * 查询训练营看课人数
      */
     Integer getUserCountByCampId(@Param("trainingCampId") Long trainingCampId);
+
+    /**
+     * 查询用户看课记录
+     */
+    List<FsCourseWatchLogIMVO> selectWatchLogIMVOListByMap(@Param("params") Map<String, Object> params);
 }

+ 2 - 2
fs-service/src/main/java/com/fs/course/param/FsCourseLinkMiniParam.java

@@ -26,8 +26,8 @@ public class FsCourseLinkMiniParam {
     */
     private Long fsUserId;
     /**
-     * 外部联系人主键
+     * sessionId
      */
-    private Long extId;
+    private Long sessionId;
 
 }

+ 2 - 2
fs-service/src/main/java/com/fs/course/param/FsCourseListBySidebarParam.java

@@ -36,7 +36,7 @@ public class FsCourseListBySidebarParam implements Serializable {
     */
     private String externalUserId;
     /**
-     * 外部联系人主键ID
+     * 会话ID
      */
-    private Long extId;
+    private Long sessionId;
 }

+ 6 - 4
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -3,10 +3,7 @@ package com.fs.course.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.*;
-import com.fs.course.vo.FsCourseOverVO;
-import com.fs.course.vo.FsCourseUserStatisticsListVO;
-import com.fs.course.vo.FsCourseWatchLogListVO;
-import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
+import com.fs.course.vo.*;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 
@@ -134,4 +131,9 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
     void scheduleBatchUpdateToDatabaseIsOpen();
 
     List<FsCourseWatchLogListVO> selectFsCourseWatchLogListVOexport(FsCourseWatchLogListParam param);
+
+    /**
+     * 查询用户看课记录
+     */
+    List<FsCourseWatchLogIMVO> selectWatchLogIMVOListByMap(Map<String, Object> params);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -196,5 +196,8 @@ public interface IFsUserCourseVideoService
      */
     List<FsUserCourseVideoChooseVO> getChooseCourseVideoListByMap(Map<String, Object> params);
 
+    /**
+     * 企微聊天创建小程序链接
+     */
     R createMiniLinkByQwIm(FsCourseLinkMiniParam param);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -1109,4 +1109,12 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return list;
     }
 
+    /**
+     * 查询用户看课记录
+     */
+    @Override
+    public List<FsCourseWatchLogIMVO> selectWatchLogIMVOListByMap(Map<String, Object> params) {
+        return fsCourseWatchLogMapper.selectWatchLogIMVOListByMap(params);
+    }
+
 }

+ 84 - 10
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -49,8 +49,10 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwSession;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwSessionMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwExternalContactService;
@@ -229,6 +231,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     @Autowired
     ConfigUtil configUtil;
+    @Autowired
+    private QwSessionMapper sessionMapper;
 
 
 
@@ -2809,15 +2813,19 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         return fsUserCourseVideoMapper.getChooseCourseVideoListByMap(params);
     }
 
+    /**
+     * 企微聊天创建小程序链接
+     */
     @Override
     public R createMiniLinkByQwIm(FsCourseLinkMiniParam param) {
-        QwExternalContact qwExternalContact = qwExternalContactMapper.selectById(param.getExtId());
-        if (Objects.isNull(qwExternalContact)) {
-            return R.error("客户不存在");
+        QwSession qwSession = sessionMapper.selectQwSessionBySessionId(param.getSessionId());
+        if (qwSession == null){
+            return R.error("会话不存在");
         }
-        QwUser qwUser = qwUserMapper.selectById(qwExternalContact.getQwUserId());
+
+        QwUser qwUser = qwUserMapper.selectQwUserById(Long.valueOf(qwSession.getQwUserId()));
         if (Objects.isNull(qwUser) || Objects.isNull(qwUser.getCompanyId()) || Objects.isNull(qwUser.getCompanyUserId())){
-            return R.error("员工未绑定 销售公司  销售 请先绑定");
+            return R.error("员工未绑定销售公司或销售请先绑定");
         }
 
         QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(qwUser.getCorpId());
@@ -2825,12 +2833,78 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
             return R.error().put("msg","企业不存在,请联系管理员");
         }
 
-        //看课记录
-        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), qwExternalContact.getFsUserId(), qwUser, qwExternalContact.getId());
+        if (qwSession.getIsRoom() == 0){
+            QwExternalContact qwExternalContact = qwExternalContactMapper.selectById(qwSession.getQwExtId());
+            if (Objects.isNull(qwExternalContact)) {
+                return R.error("客户不存在");
+            }
 
-        //生成小程序链接
-        String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, qwExternalContact.getId(),2,null, 0);
-        return R.ok().put("data", linkByMiniApp);
+            //看课记录
+            addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), qwExternalContact.getFsUserId(), qwUser, qwExternalContact.getId());
+
+            //生成小程序链接
+            String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, qwExternalContact.getId(),2,null, 0);
+            return R.ok().put("data", linkByMiniApp);
+        } else {
+            List<QwExternalContact> contacts = qwExternalContactMapper.selectGroupContactByChatIdAndQwUserId(qwSession.getChatId(), qwUser.getId());
+
+            if (contacts.isEmpty()) {
+                return R.error("群组客户不存在");
+            }
+
+            contacts.forEach(contact -> {
+                //看课记录
+                addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), contact.getFsUserId(), qwUser, contact.getId());
+            });
+
+            //生成小程序链接
+            String linkByMiniApp = createLinkByMiniApp(qwUser.getId(), qwUser.getCompanyId(), qwUser.getCompanyUserId(), param.getCourseId(), param.getVideoId(), qwUser.getCorpId(), qwSession.getChatId());
+            return R.ok().put("data", linkByMiniApp);
+        }
+    }
+
+    /**
+     * 小程序-发群链接
+     */
+    private String createLinkByMiniApp(Long qwUserId, Long companyId, Long companyUserId, Long courseId, Long videoId, String corpId, String chatId) {
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(companyId);
+        link.setQwUserId(qwUserId);
+        link.setCompanyUserId(companyUserId);
+        link.setVideoId(videoId);
+        link.setCorpId(corpId);
+        link.setCourseId(courseId);
+        link.setChatId(chatId);
+        link.setLinkType(3); //小程序
+        link.setUNo(UUID.randomUUID().toString());
+        link.setIsRoom(1);
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(new Date());
+        link.setProjectCode(cloudHostProper.getProjectCode());
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link,courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = miniappRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(0);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        link.setUpdateTime(updateTime);
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
     }
 
 }

+ 27 - 0
fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogIMVO.java

@@ -0,0 +1,27 @@
+package com.fs.course.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class FsCourseWatchLogIMVO {
+
+    @ApiModelProperty("记录ID")
+    private Long logId;
+
+    @ApiModelProperty("小节封面")
+    private String thumbnail;
+
+    @ApiModelProperty("看课时长")
+    private Long duration;
+
+    @ApiModelProperty("记录类型 1看课中 2完课 3待看课 4看课中断")
+    private Integer logType;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+}

+ 11 - 8
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -41,13 +41,16 @@ public interface AiHookService {
     /**
      * 保存企微聊天信息
      *
-     * @param qwUserId 企微用户ID
-     * @param userId   用户ID
-     * @param content  聊天内容
-     * @param uuid     UUID
-     * @param sendType 发送者类型 1用户 2客服
-     * @param json     消息json
-     * @param msgType  消息类型 1文本 2图片 3动态表情 4语音 5小程序
+     * @param qwUserId      企微用户ID
+     * @param userId        用户ID
+     * @param content       聊天内容
+     * @param uuid          UUID
+     * @param sendType      发送者类型 1用户 2客服
+     * @param json          消息json
+     * @param msgType       消息类型 1文本 2图片 3动态表情 4语音 5小程序
+     * @param isRoom        是否群聊
+     * @param chatId        会话ID(群聊才有)
+     * @param chatAvatar    群头像(群聊才有)
      */
-    QwMessageListVO saveQwMsg(Long qwUserId, Long userId, String content, String uuid, int sendType, String json, int msgType);
+    QwMessageListVO saveQwMsg(Long qwUserId, Long userId, String content, String uuid, int sendType, String json, int msgType, boolean isRoom, String chatId, String chatAvatar);
 }

+ 84 - 40
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -3,11 +3,10 @@ package com.fs.fastGpt.service.impl;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
 import com.fs.common.annotation.Excel;
-import com.fs.common.config.FSConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.PinYinUtil;
 import com.fs.company.domain.CompanyConfig;
 import com.fs.company.mapper.CompanyConfigMapper;
 import com.fs.config.ai.AiHostProper;
@@ -38,11 +37,9 @@ import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.dto.TracesDTO;
 import com.fs.his.enums.ShipperCodeEnum;
-import com.fs.his.mapper.FsStoreMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsStoreOrderService;
-import com.fs.im.dto.OpenImMsgDTO;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.qw.domain.*;
 import com.fs.qw.enums.MsgType;
@@ -65,7 +62,6 @@ import com.fs.wxwork.dto.*;
 import com.fs.wxwork.service.WxWorkService;
 import com.vdurmont.emoji.EmojiParser;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.Nullable;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -74,7 +70,6 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
 import java.lang.reflect.Field;
 import java.time.DayOfWeek;
 import java.time.LocalDate;
@@ -173,6 +168,8 @@ public class AiHookServiceImpl implements AiHookService {
     private QwSessionMapper qwSessionMapper;
     @Autowired
     private QwMsgMapper qwMsgMapper;
+    @Autowired
+    private QwGroupChatMapper qwGroupChatMapper;
 
     private static final String AI_REPLY = "AI_REPLY:";
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
@@ -2079,17 +2076,20 @@ public class AiHookServiceImpl implements AiHookService {
     /**
      * 保存企微聊天信息
      *
-     * @param qwUserId 企微用户ID
-     * @param userId   用户ID
-     * @param content  聊天内容
-     * @param uuid     UUID
-     * @param sendType 发送者类型 1用户 2客服
-     * @param json     消息json
-     * @param msgType  消息类型 1文本 2图片 3动态表情 4语音
+     * @param qwUserId      企微用户ID
+     * @param userId        用户ID
+     * @param content       聊天内容
+     * @param uuid          UUID
+     * @param sendType      发送者类型 1用户 2客服
+     * @param json          消息json
+     * @param msgType       消息类型 1文本 2图片 3动态表情 4语音 5小程序
+     * @param isRoom        是否群聊
+     * @param chatId        会话ID(群聊才有)
+     * @param chatAvatar    群头像(群聊才有)
      */
     @Transactional(rollbackFor = Exception.class)
     @Override
-    public QwMessageListVO saveQwMsg(Long qwUserId, Long userId, String content, String uuid, int sendType, String json, int msgType) {
+    public QwMessageListVO saveQwMsg(Long qwUserId, Long userId, String content, String uuid, int sendType, String json, int msgType, boolean isRoom, String chatId, String chatAvatar) {
         // 查询企微用户
         QwUser qwUser = qwUserService.selectQwUserById(qwUserId);
         if (Objects.isNull(qwUser)){
@@ -2099,33 +2099,71 @@ public class AiHookServiceImpl implements AiHookService {
 
         // 查询外部联系人
         QwExternalContact qwExternalContact = getExternalContact(userId, uuid, qwUser.getServerId(), qwUser.getCorpId(), qwUser.getQwUserId());
-        if (Objects.isNull(qwExternalContact)){
+        if (Objects.isNull(qwExternalContact) && !isRoom) {
             log.warn("外部联系人不存在 userId: {}, uuid: {}, serverId: {}, corpId: {}, qwUserId: {}", userId, uuid, qwUser.getServerId(), qwUser.getCorpId(), qwUser.getQwUserId());
             return null;
         }
 
         // 查询会话
-        QwSession qwSession = qwSessionMapper.selectQwSessionByExtIdAndQwUserId(qwExternalContact.getId(), qwUser.getId());
-        if (qwSession == null) {
-            qwSession = new QwSession();
-            String chatId = UUID.randomUUID().toString();
-            qwSession.setChatId(chatId);
-            qwSession.setCorpId(qwUser.getCorpId());
-            qwSession.setQwExtWxId(String.valueOf(userId));
-            qwSession.setQwExtId(qwExternalContact.getId().toString());
-            qwSession.setQwUserId(qwUser.getId().toString());
-            qwSession.setStatus(1);
-            qwSession.setAvatar(qwExternalContact.getAvatar());
-            qwSession.setNickName(qwExternalContact.getName());
-            qwSession.setCompanyId(qwUser.getCompanyId());
-            qwSession.setCompanyUserId(qwUser.getCompanyUserId());
-            qwSession.setCreateTime(new Date());
-            qwSession.setUpdateTime(new Date());
-            qwSessionMapper.insertQwSession(qwSession);
-        }else {
-            qwSession.setUpdateTime(new Date());
-            qwSession.setNickName(qwExternalContact.getName());
-            qwSessionMapper.updateQwSession(qwSession);
+        QwSession qwSession;
+        if (isRoom) {
+            QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(chatId);
+            if (Objects.isNull(qwGroupChat)){
+                log.warn("群聊不存在 userId: {}, uuid: {}, serverId: {}, corpId: {}, qwUserId: {}, chatId: {}", userId, uuid, qwUser.getServerId(), qwUser.getCorpId(), qwUser.getQwUserId(), chatId);
+                return null;
+            }
+
+            qwSession = qwSessionMapper.selectQwSessionByChatIdAndQwUserId(chatId, qwUser.getId());
+            String firstLetter = PinYinUtil.getFirstLetter(qwGroupChat.getName());
+            if (qwSession == null) {
+                qwSession = new QwSession();
+                qwSession.setChatId(chatId);
+                qwSession.setCorpId(qwGroupChat.getCorpId());
+                qwSession.setQwUserId(qwUser.getId().toString());
+                qwSession.setStatus(1);
+                qwSession.setAvatar(chatAvatar);
+                qwSession.setNickName(qwGroupChat.getName());
+                qwSession.setCompanyId(qwUser.getCompanyId());
+                qwSession.setCreateTime(new Date());
+                qwSession.setUpdateTime(new Date());
+                qwSession.setIsRoom(1);
+                qwSession.setFirstLetter(firstLetter);
+                qwSessionMapper.insertQwSession(qwSession);
+            }else {
+                qwSession.setUpdateTime(new Date());
+                qwSession.setQwUserId(qwUser.getId().toString());
+                qwSession.setNickName(qwGroupChat.getName());
+                qwSession.setAvatar(chatAvatar);
+                qwSession.setFirstLetter(firstLetter);
+                qwSessionMapper.updateQwSession(qwSession);
+            }
+        } else {
+            qwSession = qwSessionMapper.selectQwSessionByExtIdAndQwUserId(qwExternalContact.getId(), qwUser.getId());
+            String firstLetter = PinYinUtil.getFirstLetter(qwExternalContact.getName());
+            if (qwSession == null) {
+                qwSession = new QwSession();
+                chatId = UUID.randomUUID().toString();
+                qwSession.setChatId(chatId);
+                qwSession.setCorpId(qwUser.getCorpId());
+                qwSession.setQwExtWxId(String.valueOf(userId));
+                qwSession.setQwExtId(qwExternalContact.getId().toString());
+                qwSession.setQwUserId(qwUser.getId().toString());
+                qwSession.setStatus(1);
+                qwSession.setAvatar(qwExternalContact.getAvatar());
+                qwSession.setNickName(qwExternalContact.getName());
+                qwSession.setCompanyId(qwUser.getCompanyId());
+                qwSession.setCompanyUserId(qwUser.getCompanyUserId());
+                qwSession.setCreateTime(new Date());
+                qwSession.setUpdateTime(new Date());
+                qwSession.setIsRoom(0);
+                qwSession.setFirstLetter(firstLetter);
+                qwSessionMapper.insertQwSession(qwSession);
+            }else {
+                qwSession.setUpdateTime(new Date());
+                qwSession.setNickName(qwExternalContact.getName());
+                qwSession.setFirstLetter(firstLetter);
+                qwSessionMapper.updateQwSession(qwSession);
+            }
         }
 
         // 保存聊天消息
@@ -2139,10 +2177,16 @@ public class AiHookServiceImpl implements AiHookService {
         qwMsg.setMsgJson(json);
         qwMsg.setStatus(0);
         qwMsg.setQwUserId(qwSession.getQwUserId());
-        qwMsg.setQwExtId(qwSession.getQwExtId());
-        qwMsg.setAvatar(qwExternalContact.getAvatar());
-        qwMsg.setNickName(qwExternalContact.getName());
         qwMsg.setCreateTime(new Date());
+
+        if (Objects.isNull(qwExternalContact)) {
+            qwMsg.setAvatar(qwUser.getAvatar());
+            qwMsg.setNickName(qwUser.getQwUserName());
+        } else {
+            qwMsg.setQwExtId(qwExternalContact.getId().toString());
+            qwMsg.setAvatar(qwExternalContact.getAvatar());
+            qwMsg.setNickName(qwExternalContact.getName());
+        }
         qwMsgMapper.insertQwMsg(qwMsg);
         log.debug("保存企微聊天记录 msgId: {}", qwMsg.getMsgId());
 
@@ -2150,7 +2194,7 @@ public class AiHookServiceImpl implements AiHookService {
         QwMessageListVO listVO = new QwMessageListVO();
         QWFromUser qwFromUser = new QWFromUser();
         if (sendType == 1) {
-            qwFromUser.setId(Long.parseLong(qwMsg.getQwExtId()));
+            qwFromUser.setId(qwExternalContact.getId());
             qwFromUser.setAvatar(qwMsg.getAvatar());
             qwFromUser.setDisplayName(qwMsg.getNickName());
         }else if(sendType == 2){

+ 5 - 0
fs-service/src/main/java/com/fs/qw/domain/QwSession.java

@@ -65,5 +65,10 @@ public class QwSession extends BaseEntity implements Serializable
 
     private String qwExtWxId;//企微外部联系人wxid
 
+    /** 是否群聊 0否 1是 **/
+    private Integer isRoom;
+
+    /** 首字母 **/
+    private String firstLetter;
 
 }

+ 11 - 2
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -506,8 +506,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
 
     void updateQwExternalContactIsRePlyById(@Param("id")Long id);
 
-    @Select("SELECT id,external_user_id,name,avatar,remark,description,fs_user_id FROM qw_external_contact " +
-            " WHERE id = #{qwExternalContactId}")
+    @Select("SELECT * FROM qw_external_contact WHERE id = #{qwExternalContactId}")
     QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId);
 
     /**
@@ -516,4 +515,14 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
      * @return  Boolean
      */
     Boolean getBuyStatusByExtId(@Param("qwExternalContactId") Long qwExternalContactId);
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    List<QwContactVO> getContactListByQwUserId(@Param("qwUserId") Long qwUserId);
+
+    /**
+     * 查询群组指定企微关联外部联系人
+     */
+    List<QwExternalContact> selectGroupContactByChatIdAndQwUserId(@Param("chatId") String chatId, @Param("qwUserId") Long qwUserId);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java

@@ -2,6 +2,7 @@ package com.fs.qw.mapper;
 
 import com.fs.qw.domain.QwGroupChat;
 import com.fs.qw.param.QwGroupChatParam;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwGroupChatOptionsVO;
 import com.fs.qw.vo.QwGroupChatTransferVO;
 import com.fs.qw.vo.QwGroupChatVO;
@@ -128,4 +129,9 @@ public interface QwGroupChatMapper
      * @return  list
      */
     List<QwGroupChatTransferVO> selectQwGroupChatTransferList(QwGroupChatParam qwGroupChat);
+
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    List<QwContactVO> getGroupListByQwUserId(@Param("qwUserId") Long qwUserId);
 }

+ 20 - 2
fs-service/src/main/java/com/fs/qw/mapper/QwSessionMapper.java

@@ -64,8 +64,10 @@ public interface QwSessionMapper extends BaseMapper<QwSession>
      */
     public int deleteQwSessionBySessionIds(Long[] sessionIds);
 
-
-    @Select("select * from qw_session where qw_ext_id = #{qwExtId} and qw_user_id = #{qwUserId}")
+    /**
+     * 查询单聊会话
+     */
+    @Select("select * from qw_session where qw_ext_id = #{qwExtId} and qw_user_id = #{qwUserId} and is_room = 0")
     QwSession selectQwSessionByExtIdAndQwUserId(@Param("qwExtId") Long qwExtId, @Param("qwUserId") Long id);
 
     /**
@@ -74,4 +76,20 @@ public interface QwSessionMapper extends BaseMapper<QwSession>
      * @return  list
      */
     List<QwContactListVO> selectContactListByQwUserId(@Param("qwUserId") Long qwUserId);
+
+    /**
+     * 查询群聊会话
+     */
+    @Select("select * from qw_session where chat_id = #{chatId} and qw_user_id = #{qwUserId} and is_room = 1")
+    QwSession selectQwSessionByChatIdAndQwUserId(@Param("chatId") String chatId, @Param("qwUserId") Long id);
+
+    /**
+     * 根据群组ID查询会话
+     */
+    QwSession selectQwSessionByGroupChatId(@Param("groupChatId") String groupChatId);
+
+    /**
+     * 根据外部联系人ID查询会话
+     */
+    QwSession selectQwSessionByContactId(@Param("contactId") Long contactId);
 }

+ 16 - 0
fs-service/src/main/java/com/fs/qw/service/IQwMsgService.java

@@ -8,6 +8,7 @@ import com.fs.qw.domain.QwUser;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
 import com.fs.qw.vo.QwContactListVO;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwHookApi.vo.QwHookMsgVO;
 
@@ -96,4 +97,19 @@ public interface IQwMsgService extends IService<QwMsg>
     R sendMsg(QwMsgSendParam param);
 
     QwContactListVO selectQwSessionBycId(String conversationId, Long qwUserId);
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    List<QwContactVO> contactListByQwUserId(Long qwUserId);
+
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    List<QwContactVO> groupListByQwUserId(Long qwUserId);
+
+    /**
+     * 获取会话ID
+     */
+    QwContactListVO getConversationIdById(Long qwUserid, String id, Boolean isGroup);
 }

+ 186 - 28
fs-service/src/main/java/com/fs/qw/service/impl/QwMsgServiceImpl.java

@@ -8,7 +8,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PinYinUtil;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyMiniapp;
@@ -21,23 +23,25 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.qw.Bean.MsgBean;
 import com.fs.qw.domain.*;
 import com.fs.qw.enums.MsgType;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.mapper.QwMsgMapper;
-import com.fs.qw.mapper.QwSessionMapper;
-import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
 import com.fs.qw.service.IQwMsgService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.vo.QwContactListVO;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwHookApi.vo.QwHookMsgVO;
 import com.fs.wxwork.dto.*;
 import com.fs.wxwork.service.WxWorkService;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -46,6 +50,7 @@ import java.util.stream.Collectors;
  * @author fs
  * @date 2024-12-17
  */
+@Slf4j
 @Service
 public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements IQwMsgService {
     @Autowired
@@ -67,6 +72,12 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
     private CompanyMiniappMapper companyMiniappMapper;
     @Autowired
     private FsCoursePlaySourceConfigMapper playSourceConfigMapper;
+    @Autowired
+    private QwExternalContactMapper externalContactMapper;
+    @Autowired
+    private QwGroupChatMapper groupChatMapper;
+    @Autowired
+    private RedissonClient redissonClient;
 
     /**
      * 查询企微聊天记录
@@ -279,12 +290,6 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             return R.error("会话不存在");
         }
 
-        // 外部联系人
-        QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(Long.valueOf(qwSession.getQwExtId()));
-        if (Objects.isNull(qwExternalContact)) {
-            return R.error("联系人不存在");
-        }
-
         // 企微用户
         QwUser qwUser = qwUserMapper.selectQwUserById(Long.parseLong(qwSession.getQwUserId()));
         if (Objects.isNull(qwUser)) {
@@ -293,19 +298,39 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
         Long serverId = qwUser.getServerId();
         String uuid = qwUser.getUid();
-        String openId = qwExternalContact.getExternalUserId();
         String sCorpId = qwUser.getCorpId();
-
-        WxWorkUserId2VidDTO params = new WxWorkUserId2VidDTO();
-        params.setOpenid(Collections.singletonList(openId));
-        params.setUuid(uuid);
-        params.setScorpid(sCorpId);
-        WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> listWxWorkResponseDTO = wxWorkService.UserId2Vid(params, serverId);
-
-        if (listWxWorkResponseDTO.getErrcode() != 0) {
-            return R.error(listWxWorkResponseDTO.getErrmsg());
+        boolean isRoom = qwSession.getIsRoom() == 1;
+
+        long sendUserId;
+        QwExternalContact qwExternalContact = null;
+        if (isRoom) {
+            WxWorkChatId2RoomIdDTO chatId2RoomIdDTO = new WxWorkChatId2RoomIdDTO();
+            chatId2RoomIdDTO.setUuid(uuid);
+            chatId2RoomIdDTO.setChatid(qwSession.getChatId());
+            WxWorkResponseDTO<WxWorkChatId2RoomIdResp> chatId2RoomIdResp = wxWorkService.chatId2RoomId(chatId2RoomIdDTO, serverId);
+            if (chatId2RoomIdResp.getErrcode() != 0) {
+                return R.error(chatId2RoomIdResp.getErrmsg());
+            }
+            sendUserId = chatId2RoomIdResp.getData().getRoom_id();
+        } else {
+            // 外部联系人
+            qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(Long.valueOf(qwSession.getQwExtId()));
+            if (Objects.isNull(qwExternalContact)) {
+                return R.error("联系人不存在");
+            }
+            String openId = qwExternalContact.getExternalUserId();
+            WxWorkUserId2VidDTO params = new WxWorkUserId2VidDTO();
+            params.setOpenid(Collections.singletonList(openId));
+            params.setUuid(uuid);
+            params.setScorpid(sCorpId);
+            WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> listWxWorkResponseDTO = wxWorkService.UserId2Vid(params, serverId);
+
+            if (listWxWorkResponseDTO.getErrcode() != 0) {
+                return R.error(listWxWorkResponseDTO.getErrmsg());
+            }
+            sendUserId = listWxWorkResponseDTO.getData().get(0).getUser_id();
         }
-        long sendUserId = listWxWorkResponseDTO.getData().get(0).getUser_id();
+
         String msgJson;
 
         MsgType msgType = MsgType.getMsgType(param.getMsgType());
@@ -314,7 +339,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             WxWorkSendTextMsgDTO textMsgDTO = new WxWorkSendTextMsgDTO();
             textMsgDTO.setUuid(uuid);
             textMsgDTO.setSend_userid(sendUserId);
-            textMsgDTO.setIsRoom(false);
+            textMsgDTO.setIsRoom(isRoom);
             textMsgDTO.setContent(param.getContent());
             WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> msgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(textMsgDTO, serverId);
 
@@ -339,7 +364,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             WxwSendCDNImgMsgDTO imgMsgDTO = new WxwSendCDNImgMsgDTO();
             imgMsgDTO.setUuid(uuid);
             imgMsgDTO.setSend_userid(sendUserId);
-            imgMsgDTO.setIsRoom(false);
+            imgMsgDTO.setIsRoom(isRoom);
             imgMsgDTO.setCdnkey(data.getCdn_key());
             imgMsgDTO.setAeskey(data.getAes_key());
             imgMsgDTO.setMd5(data.getMd5());
@@ -401,7 +426,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             appMsgDTO.setMd5(data.getMd5());
             appMsgDTO.setAeskey(data.getAes_key());
             appMsgDTO.setFileSize(data.getSize());
-            appMsgDTO.setIsRoom(false);
+            appMsgDTO.setIsRoom(isRoom);
             WxWorkResponseDTO<WxWorkSendAppMsgRespDTO> appMsgResp = wxWorkService.SendAppMsg(appMsgDTO, serverId);
             if (appMsgResp.getErrcode() != 0) {
                 return R.error(appMsgResp.getErrmsg());
@@ -433,10 +458,16 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         qwMsg.setMsgJson(msgJson);
         qwMsg.setStatus(0);
         qwMsg.setQwUserId(qwSession.getQwUserId());
-        qwMsg.setQwExtId(qwSession.getQwExtId());
-        qwMsg.setAvatar(qwExternalContact.getAvatar());
-        qwMsg.setNickName(qwExternalContact.getName());
         qwMsg.setCreateTime(new Date());
+
+        if (Objects.isNull(qwExternalContact)) {
+            qwMsg.setAvatar(qwUser.getAvatar());
+            qwMsg.setNickName(qwUser.getQwUserName());
+        } else {
+            qwMsg.setQwExtId(qwExternalContact.getId().toString());
+            qwMsg.setAvatar(qwExternalContact.getAvatar());
+            qwMsg.setNickName(qwExternalContact.getName());
+        }
         qwMsgMapper.insertQwMsg(qwMsg);
 
         // 组装返回消息结构
@@ -505,8 +536,10 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             lambdaQueryWrapper.last("limit 1");
             List<QwMsg> qwMsgs = qwMsgMapper.selectList(lambdaQueryWrapper);
             if (CollectionUtil.isEmpty(qwMsgs)){
+                listVO.setType("text");
                 listVO.setLastContent("");
-                listVO.setLastSendTime(new Date().getTime());
+                listVO.setLastSendTime(null);
+                listVO.setUnread(0);
                 qwContactListVOS.add(listVO);
                 break;
             }
@@ -605,4 +638,129 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         listVO.setUnread(0);
         return listVO;
     }
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    @Override
+    public List<QwContactVO> contactListByQwUserId(Long qwUserId) {
+        return externalContactMapper.getContactListByQwUserId(qwUserId);
+    }
+
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    @Override
+    public List<QwContactVO> groupListByQwUserId(Long qwUserId) {
+        return groupChatMapper.getGroupListByQwUserId(qwUserId);
+    }
+
+    /**
+     * 获取会话ID
+     */
+    @Override
+    public QwContactListVO getConversationIdById(Long qwUserId, String id, Boolean isGroup) {
+
+        QwUser qwUser = qwUserMapper.selectQwUserById(qwUserId);
+        if (qwUser == null) {
+            throw new ServiceException("企微用户不存在");
+        }
+
+        QwSession qwSession;
+        if (isGroup) {
+            qwSession = qwSessionMapper.selectQwSessionByGroupChatId(id);
+            if (qwSession == null) {
+                QwGroupChat qwGroupChat = groupChatMapper.selectQwGroupChatByChatId(id);
+                if (qwGroupChat == null) {
+                    throw new ServiceException("群聊不存在");
+                }
+
+                RLock lock = redissonClient.getLock("addSession" + id + qwUser);
+                try {
+                    boolean tryLock = lock.tryLock(2, 3, TimeUnit.SECONDS);
+                    if (!tryLock) {
+                        throw new ServiceException("操作太频繁,请稍后再试");
+                    }
+
+                    qwSession = qwSessionMapper.selectQwSessionByGroupChatId(id);
+                    if (qwSession == null) {
+                        String firstLetter = PinYinUtil.getFirstLetter(qwGroupChat.getName());
+                        qwSession = new QwSession();
+                        qwSession.setChatId(qwGroupChat.getChatId());
+                        qwSession.setCorpId(qwGroupChat.getCorpId());
+                        qwSession.setQwUserId(qwUserId.toString());
+                        qwSession.setStatus(1);
+                        qwSession.setNickName(qwGroupChat.getName());
+                        qwSession.setCompanyId(qwGroupChat.getCompanyId());
+                        qwSession.setCreateTime(new Date());
+                        qwSession.setUpdateTime(new Date());
+                        qwSession.setIsRoom(1);
+                        qwSession.setFirstLetter(firstLetter);
+                        qwSessionMapper.insertQwSession(qwSession);
+                    }
+                } catch (InterruptedException e) {
+                    log.warn("获取锁失败 err: {}", e.getMessage(), e);
+                    throw new ServiceException("操作太频繁,请稍后再试");
+                } finally {
+                    lock.unlock();
+                }
+            }
+        } else {
+            qwSession = qwSessionMapper.selectQwSessionByContactId(Long.parseLong(id));
+            if (qwSession == null) {
+                QwExternalContact qwExternalContact = externalContactMapper.selectQwExternalContactById(Long.valueOf(id));
+                if (qwExternalContact == null) {
+                    throw new ServiceException("外部联系人不存在");
+                }
+
+                RLock lock = redissonClient.getLock("addSession" + id + qwUser);
+                try {
+                    boolean tryLock = lock.tryLock(2, 3, TimeUnit.SECONDS);
+                    if (!tryLock) {
+                        throw new ServiceException("操作太频繁,请稍后再试");
+                    }
+
+                    qwSession = qwSessionMapper.selectQwSessionByContactId(Long.parseLong(id));
+                    if (qwSession == null) {
+                        String firstLetter = PinYinUtil.getFirstLetter(qwExternalContact.getName());
+                        qwSession = new QwSession();
+                        String chatId = UUID.randomUUID().toString();
+                        qwSession.setChatId(chatId);
+                        qwSession.setCorpId(qwUser.getCorpId());
+                        qwSession.setQwExtId(qwExternalContact.getId().toString());
+                        qwSession.setQwUserId(qwUser.getId().toString());
+                        qwSession.setStatus(1);
+                        qwSession.setAvatar(qwExternalContact.getAvatar());
+                        qwSession.setNickName(qwExternalContact.getName());
+                        qwSession.setCompanyId(qwUser.getCompanyId());
+                        qwSession.setCompanyUserId(qwUser.getCompanyUserId());
+                        qwSession.setCreateTime(new Date());
+                        qwSession.setUpdateTime(new Date());
+                        qwSession.setIsRoom(0);
+                        qwSession.setFirstLetter(firstLetter);
+                        qwSessionMapper.insertQwSession(qwSession);
+                    }
+                } catch (InterruptedException e) {
+                    log.warn("获取锁失败 err: {}", e.getMessage(), e);
+                    throw new ServiceException("操作太频繁,请稍后再试");
+                } finally {
+                    lock.unlock();
+                }
+            }
+        }
+
+        QwContactListVO qwContactListVO = new QwContactListVO();
+        qwContactListVO.setId(qwSession.getSessionId());
+        qwContactListVO.setConversationId(String.valueOf(qwSession.getSessionId()));
+        qwContactListVO.setExtId(qwSession.getQwExtId());
+        qwContactListVO.setAvatar(qwSession.getAvatar());
+        qwContactListVO.setDisplayName(qwSession.getNickName());
+        qwContactListVO.setIndex(qwSession.getFirstLetter());
+        qwContactListVO.setIsGroup(isGroup);
+        qwContactListVO.setUnread(0);
+        qwContactListVO.setLastSendTime(new Date().getTime());
+        qwContactListVO.setLastContent("");
+
+        return qwContactListVO;
+    }
 }

+ 35 - 0
fs-service/src/main/java/com/fs/qw/vo/QwContactVO.java

@@ -0,0 +1,35 @@
+package com.fs.qw.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class QwContactVO {
+
+    @ApiModelProperty("唯一ID")
+    private String id;
+
+    @ApiModelProperty("会话ID")
+    private Long conversationId;
+
+    @ApiModelProperty("名称")
+    private String displayName;
+
+    @ApiModelProperty("头像")
+    private String avatar;
+
+    @ApiModelProperty("通讯录索引,传入字母或数字进行排序,索引可以显示自定义文字“[1]群组”")
+    private String index;
+
+    @ApiModelProperty("未读消息数")
+    private Integer unread;
+
+    @ApiModelProperty("最近一条消息的时间戳,13位毫秒")
+    private Long lastSendTime;
+
+    @ApiModelProperty("最近一条消息的内容")
+    private String lastContent;
+
+    @ApiModelProperty("是否群聊")
+    private Boolean isGroup;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxRoomHeaderDTO.java

@@ -0,0 +1,15 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+@Data
+public class WxRoomHeaderDTO {
+    /**
+     * UUID
+     */
+    private String uuid;
+    /**
+     * RoomId
+     */
+    private Long roomid;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxRoomHeaderResp.java

@@ -0,0 +1,15 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+@Data
+public class WxRoomHeaderResp {
+    /**
+     * 群头像
+     */
+    private String image_url;
+    /**
+     * RoomId
+     */
+    private Long roomid;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxWorkChatId2RoomIdDTO.java

@@ -0,0 +1,20 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+@Data
+public class WxWorkChatId2RoomIdDTO {
+
+    /**
+     * UUID
+     */
+    private String uuid;
+    /**
+     * 不传默认当前账号登录企业
+     */
+    private Long corpid;
+    /**
+     * ChatId
+     */
+    private String chatid;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxWorkChatId2RoomIdResp.java

@@ -0,0 +1,15 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+@Data
+public class WxWorkChatId2RoomIdResp {
+    /**
+     * RoomID
+     */
+    private Long room_id;
+    /**
+     * ChatId
+     */
+    private String chatid;
+}

+ 1 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxWorkMessageDTO.java

@@ -20,6 +20,7 @@ public class WxWorkMessageDTO {
     private Integer msgtype;         // 对应 "msgtype": 2 (消息类型 0 和 2文本消息)
     private String recordwording;    //通话时长
     private Integer recordtype;         //通话5
+    private Long room_conversation_id; // 群ChatID
 
     // 图片
     private String file_id;

+ 20 - 0
fs-service/src/main/java/com/fs/wxwork/dto/WxWorkRoomId2ChatIdDTO.java

@@ -0,0 +1,20 @@
+package com.fs.wxwork.dto;
+
+import lombok.Data;
+
+@Data
+public class WxWorkRoomId2ChatIdDTO {
+
+    /**
+     * uuid
+     */
+    private String uuid;
+    /**
+     * 企业id 不传默认当前账号登录企业
+     */
+    private Long corpid;
+    /**
+     * 房间ID
+     */
+    private Long room_id;
+}

+ 24 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java

@@ -238,4 +238,28 @@ public interface WxWorkService {
      * @return  WxWorkResponseDTO
      */
     WxWorkResponseDTO<WxCdnUploadImgLinkResp> cdnUploadImgLink(WxCdnUploadImgLinkDTO param, Long serverId);
+
+    /**
+     * roomId转chatId
+     * @param param     参数
+     * @param serverId  服务器ID
+     * @return          WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<WxWorkChatId2RoomIdResp> roomId2ChatId(WxWorkRoomId2ChatIdDTO param, Long serverId);
+
+    /**
+     * chatId转RoomId
+     * @param param     参数
+     * @param serverId  服务器ID
+     * @return          WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<WxWorkChatId2RoomIdResp> chatId2RoomId(WxWorkChatId2RoomIdDTO param, Long serverId);
+
+    /**
+     * 获取群头像
+     * @param param     参数
+     * @param serverId  服务器ID
+     * @return          WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<WxRoomHeaderResp> wxRoomHeader(WxRoomHeaderDTO param, Long serverId);
 }

+ 36 - 0
fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java

@@ -323,4 +323,40 @@ public class WxWorkServiceImpl implements WxWorkService {
         String url = getUrl(serverId) + "/CdnUploadImgLink";
         return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxCdnUploadImgLinkResp>>() {});
     }
+
+    /**
+     * roomId转chatId
+     * @param param     参数
+     * @param serverId  服务器ID
+     * @return          WxWorkResponseDTO
+     */
+    @Override
+    public WxWorkResponseDTO<WxWorkChatId2RoomIdResp> roomId2ChatId(WxWorkRoomId2ChatIdDTO param, Long serverId) {
+        String url = getUrl(serverId) + "/RoomIdToChatId";
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkChatId2RoomIdResp>>() {});
+    }
+
+    /**
+     * chatId转RoomId
+     * @param param     参数
+     * @param serverId  服务器ID
+     * @return          WxWorkResponseDTO
+     */
+    @Override
+    public WxWorkResponseDTO<WxWorkChatId2RoomIdResp> chatId2RoomId(WxWorkChatId2RoomIdDTO param, Long serverId) {
+        String url = getUrl(serverId) + "/ChatIdToRoomId";
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkChatId2RoomIdResp>>() {});
+    }
+
+    /**
+     * 获取群头像
+     * @param param     参数
+     * @param serverId  服务器ID
+     * @return          WxWorkResponseDTO
+     */
+    @Override
+    public WxWorkResponseDTO<WxRoomHeaderResp> wxRoomHeader(WxRoomHeaderDTO param, Long serverId) {
+        String url = getUrl(serverId) + "/WxRoomHeader";
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxRoomHeaderResp>>() {});
+    }
 }

+ 1 - 1
fs-service/src/main/resources/application-config-dev.yml

@@ -103,7 +103,7 @@ cloud_host:
 headerImg:
   imgUrl: https://jz-cos-1356808054.cos.ap-chengdu.myqcloud.com/fs/20250515/0877754b59814ea8a428fa3697b20e68.png
 ipad:
-  ipadUrl: https://927d296b385a.ngrok-free.app
+  ipadUrl: https://contrastably-magnetooptic-gladys.ngrok-free.dev
   aiApi: http://152.136.202.157:3000/api
   voiceApi:
   commonApi:

+ 21 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -839,4 +839,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where ucp.training_camp_id = #{trainingCampId}
     </select>
 
+    <select id="selectWatchLogIMVOListByMap" resultType="com.fs.course.vo.FsCourseWatchLogIMVO">
+        select
+            fcwl.log_id             as logId,
+            fucv.thumbnail          as thumbnail,
+            fcwl.duration           as duration,
+            fcwl.log_type           as logType,
+            fcwl.create_time        as createTime
+        from fs_course_watch_log fcwl
+        left join fs_user_course_video fucv on fucv.video_id = fcwl.video_id
+        where fcwl.send_type = 2
+        <if test="params.externalContactId != null">
+            and fcwl.qw_external_contact_id = #{params.externalContactId}
+        </if>
+        <if test="params.companyId != null">
+            and fcwl.company_id = #{params.companyId}
+        </if>
+        <if test="params.companyUserId != null">
+            and fcwl.company_user_id = #{params.companyUserId}
+        </if>
+    </select>
+
 </mapper>

+ 32 - 0
fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

@@ -660,5 +660,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where ec.id = #{qwExternalContactId}
     </select>
 
+    <select id="getContactListByQwUserId" resultType="com.fs.qw.vo.QwContactVO">
+        select
+            qec.id                                  as id,
+            qs.session_id                           as conversationId,
+            ifnull(qec.name, qs.avatar)             as displayName,
+            ifnull(qec.avatar, qs.avatar)           as avatar,
+            ifnull(qs.first_letter, '未定义')        as `index`,
+            0                                       as unread,
+            unix_timestamp(qm.create_time) * 1000   as lastSendTime,
+            qm.content                              as lastContent,
+            false                                   as isGroup
+        from qw_external_contact qec
+        left join qw_session qs on qec.id = qs.qw_ext_id and qs.is_room = 0
+        left join (
+            select
+                session_id,
+                content,
+                create_time,
+                row_number() over (partition by session_id order by create_time desc) as rn
+            from qw_msg
+        ) qm on qm.session_id = qs.session_id and qm.rn = 1
+        where qec.qw_user_id = #{qwUserId}
+    </select>
+
+    <select id="selectGroupContactByChatIdAndQwUserId" resultType="com.fs.qw.domain.QwExternalContact">
+        select
+            qec.*
+        from qw_external_contact qec
+        inner join qw_group_chat_user qgcu on qec.external_user_id = qgcu.user_id and qgcu.type = 2 and qgcu.is_out = 1
+        inner join qw_group_chat qgc on qgc.chat_id = qgcu.chat_id
+        where qgc.chat_id = #{chatId} and qec.qw_user_id = #{qwUserId}
+    </select>
 
 </mapper>

+ 25 - 0
fs-service/src/main/resources/mapper/qw/QwGroupChatMapper.xml

@@ -208,4 +208,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{chatId}
         </foreach>
     </delete>
+
+    <select id="getGroupListByQwUserId" resultType="com.fs.qw.vo.QwContactVO">
+        select
+            qgc.chat_id                             as id,
+            qs.session_id                           as conversationId,
+            ifnull(qgc.name, qs.avatar)             as displayName,
+            qs.avatar                               as avatar,
+            ifnull(qs.first_letter, '未定义')        as `index`,
+            0                                       as unread,
+            unix_timestamp(qm.create_time) * 1000   as lastSendTime,
+            qm.content                              as lastContent,
+            true                                    as isGroup
+        from qw_group_chat qgc
+        inner join qw_user qu on qu.qw_user_id = qgc.owner
+        left join qw_session qs on qgc.chat_id = qs.chat_id and qs.is_room = 1
+        left join (
+            select
+                session_id,
+                content,
+                create_time,
+                row_number() over (partition by session_id order by create_time desc) as rn
+            from qw_msg
+        ) qm on qm.session_id = qs.session_id and qm.rn = 1
+        where qu.id = #{qwUserId}
+    </select>
 </mapper>

+ 29 - 2
fs-service/src/main/resources/mapper/qw/QwSessionMapper.xml

@@ -19,10 +19,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="nickName"    column="nick_name"    />
         <result property="avatar"    column="avatar"    />
         <result property="companyUserId"    column="company_user_id"    />
+        <result property="isRoom"    column="is_room"    />
+        <result property="firstLetter"    column="first_letter"    />
     </resultMap>
 
     <sql id="selectQwSessionVo">
-        select session_id,qw_ext_wx_id, chat_id, qw_ext_id, corp_id, qw_user_id, create_time, update_time, status, company_id, user_type, nick_name, avatar, company_user_id from qw_session
+        select session_id,qw_ext_wx_id, chat_id, qw_ext_id, corp_id, qw_user_id, create_time, update_time, status, company_id, user_type, nick_name, avatar, company_user_id, is_room, first_letter from qw_session
     </sql>
 
     <select id="selectQwSessionList" parameterType="QwSession" resultMap="QwSessionResult">
@@ -38,6 +40,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="nickName != null  and nickName != ''"> and nick_name like concat( #{nickName}, '%')</if>
             <if test="avatar != null  and avatar != ''"> and avatar = #{avatar}</if>
             <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="isRoom != null "> and is_room = #{isRoom}</if>
+            <if test="firstLetter != null "> and first_letter = #{firstLetter}</if>
         </where>
     </select>
 
@@ -48,12 +52,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectContactListByQwUserId" resultType="com.fs.qw.vo.QwContactListVO">
         select
-            s.qw_user_id as id,
+            s.session_id as id,
             s.qw_ext_id extId,
             s.avatar,
             s.session_id conversationId,
             s.nick_name displayName,
             ec.comment_status isBlack,
+            s.is_room isGroup,
             if(u.qw_repeat = 1 OR u.user_repeat = 1, true, false) isRepeat
         from qw_session s
         left join qw_external_contact ec on s.qw_ext_id = ec.id
@@ -78,6 +83,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="nickName != null">nick_name,</if>
             <if test="avatar != null">avatar,</if>
             <if test="companyUserId != null">company_user_id,</if>
+            <if test="isRoom != null">is_room,</if>
+            <if test="firstLetter != null">first_letter,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="chatId != null">#{chatId},</if>
@@ -93,6 +100,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="nickName != null">#{nickName},</if>
             <if test="avatar != null">#{avatar},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="isRoom != null">#{isRoom},</if>
+            <if test="firstLetter != null">#{firstLetter},</if>
          </trim>
     </insert>
 
@@ -112,6 +121,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="nickName != null">nick_name = #{nickName},</if>
             <if test="avatar != null">avatar = #{avatar},</if>
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="isRoom != null">is_room = #{isRoom},</if>
+            <if test="firstLetter != null">first_letter = #{firstLetter},</if>
         </trim>
         where session_id = #{sessionId}
     </update>
@@ -126,4 +137,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{sessionId}
         </foreach>
     </delete>
+
+    <select id="selectQwSessionByGroupChatId" resultType="com.fs.qw.domain.QwSession">
+        select
+            qs.*
+        from qw_session qs
+        inner join qw_group_chat qgc on qs.chat_id = qgc.chat_id
+        where qgc.chat_id = #{groupChatId}
+    </select>
+
+    <select id="selectQwSessionByContactId" resultType="com.fs.qw.domain.QwSession">
+        select
+            qs.*
+        from qw_session qs
+        inner join qw_external_contact qec on qs.qw_ext_id = qec.id
+        where qec.id = #{contactId}
+    </select>
 </mapper>