فهرست منبع

企微聊天代码调整

wangxy 1 هفته پیش
والد
کامیت
158c2abf02
42فایلهای تغییر یافته به همراه1678 افزوده شده و 88 حذف شده
  1. 2 1
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  2. 21 0
      fs-common/src/main/java/com/fs/common/utils/PinYinUtil.java
  3. 22 0
      fs-company/src/main/java/com/fs/company/controller/param/QwExternalContactEditTagDTO.java
  4. 358 17
      fs-company/src/main/java/com/fs/company/controller/qw/QwMsgController.java
  5. 4 0
      fs-qw-api-msg/pom.xml
  6. 169 3
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  7. 101 0
      fs-qw-api-msg/src/main/java/com/fs/app/socket/QwImSocket.java
  8. 22 0
      fs-qw-api-msg/src/main/java/com/fs/app/socket/configurator/QwImConfigurator.java
  9. 160 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/AudioUtils.java
  10. 2 1
      fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java
  11. 17 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/WebSocketConfig.java
  12. 9 0
      fs-service/src/main/java/com/fs/course/param/FsCourseLinkMiniParam.java
  13. 10 0
      fs-service/src/main/java/com/fs/course/param/FsCourseListBySidebarParam.java
  14. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  15. 30 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  16. 27 0
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogIMVO.java
  17. 27 0
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  18. 160 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  19. 3 0
      fs-service/src/main/java/com/fs/qw/domain/QwUser.java
  20. 27 0
      fs-service/src/main/java/com/fs/qw/enums/MsgType.java
  21. 18 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  22. 8 0
      fs-service/src/main/java/com/fs/qw/mapper/QwSessionMapper.java
  23. 5 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  24. 6 0
      fs-service/src/main/java/com/fs/qw/param/QwMsgSendParam.java
  25. 2 0
      fs-service/src/main/java/com/fs/qw/param/QwSessionParam.java
  26. 17 0
      fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java
  27. 16 0
      fs-service/src/main/java/com/fs/qw/service/IQwMsgService.java
  28. 5 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  29. 27 8
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  30. 266 27
      fs-service/src/main/java/com/fs/qw/service/impl/QwMsgServiceImpl.java
  31. 7 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  32. 10 1
      fs-service/src/main/java/com/fs/qw/vo/QwContactListVO.java
  33. 35 0
      fs-service/src/main/java/com/fs/qw/vo/QwContactVO.java
  34. 5 0
      fs-service/src/main/java/com/fs/qw/vo/QwMessageListVO.java
  35. 0 22
      fs-service/src/main/java/com/fs/statis/param/WatchCourseStatisticsParam.java
  36. 0 1
      fs-service/src/main/java/com/fs/statis/service/impl/StatisticsServiceImpl.java
  37. 14 0
      fs-service/src/main/java/com/fs/wxwork/dto/WxWorkMessageDTO.java
  38. 1 1
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkService.java
  39. 2 2
      fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java
  40. 26 0
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml
  41. 16 0
      fs-service/src/main/resources/mapper/qw/QwSessionMapper.xml
  42. 16 4
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

+ 2 - 1
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -19,5 +19,6 @@ public enum DataSourceType
      * 从库
      */
     SLAVE,
-    SopREAD
+    SopREAD,
+    SHARDING
 }

+ 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等进驻山城"));
     }

+ 22 - 0
fs-company/src/main/java/com/fs/company/controller/param/QwExternalContactEditTagDTO.java

@@ -0,0 +1,22 @@
+package com.fs.company.controller.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@ApiModel("外部联系人标签调整")
+@Data
+public class QwExternalContactEditTagDTO {
+
+    @NotNull(message = "企微外部联系人Id不能为空")
+    @ApiModelProperty("企微外部联系人ID")
+    private Long qwExternalContactId;
+
+    @NotEmpty(message = "企微标签Id不能为空")
+    @ApiModelProperty("企微标签ID集合")
+    private List<Long> tagIds;
+}

+ 358 - 17
fs-company/src/main/java/com/fs/company/controller/qw/QwMsgController.java

@@ -1,23 +1,45 @@
 package com.fs.company.controller.qw;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.common.annotation.DataSource;
 import com.fs.common.annotation.Log;
+import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.enums.DataSourceType;
+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.company.controller.param.QwExternalContactEditTagDTO;
+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.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.FsUserOperationLogVo;
+import com.fs.qw.domain.*;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
-import com.fs.qw.service.IQwMsgService;
-import com.fs.qw.vo.QwContactListVO;
-import com.fs.qw.vo.QwMessageListVO;
+import com.fs.qw.service.*;
+import com.fs.qw.vo.*;
+import com.fs.statis.service.IStatisticsService;
+import com.fs.statistics.dto.WatchCourseStatisticsDTO;
+import com.fs.statistics.param.WatchCourseStatisticsParam;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -26,8 +48,8 @@ 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.*;
+import java.util.stream.Collectors;
 
 /**
  * 企微聊天记录Controller
@@ -44,6 +66,30 @@ public class QwMsgController extends BaseController
     private IQwMsgService qwMsgService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private IQwExternalContactInfoService qwExternalContactInfoService;
+    @Autowired
+    private IQwExternalContactService qwExternalContactService;
+    @Autowired
+    private IQwUserService qwUserService;
+    @Autowired
+    private IStatisticsService statisticsService;
+    @Autowired
+    private IFsUserCourseService fsUserCourseService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private IFsCourseWatchLogService watchLogService;
+    @Autowired
+    private IFsUserOperationLogService userOperationLogService;
+    @Autowired
+    private IQwSessionService sessionService;
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+    @Autowired
+    private IQwTagService tagService;
 
     /**
      * 查询企微聊天记录列表
@@ -120,24 +166,28 @@ public class QwMsgController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         //员工userId
         Long userId = loginUser.getUser().getUserId();
-        List<QwUser> qwUsers = qwMsgService.qwUserList(userId);
-        return R.ok().put("data",qwUsers);
+        return R.ok().put("data", qwUserService.selectQwUserVoListByCompanyUserId(userId));
     }
     //获取会话
     @GetMapping("/conversationList/{userId}")
     @ApiOperation("获取会话")
-    public R conversations(@PathVariable("userId")Long qwUserId){
-        List<QwContactListVO> list = qwMsgService.selectQwConversationByUserId(qwUserId);
-        for (QwContactListVO contract:list) {
-            if(StringUtils.isEmpty(contract.getDisplayName())){
-                contract.setDisplayName("群聊");
-            }
-        }
-        return R.ok().put("data",list);
+    public R conversations(@PathVariable("userId") Long qwUserId,
+                           @RequestParam(required = false) Boolean removeBlack,
+                           @RequestParam(required = false) Boolean removeRepeat){
+        Map<String, Object> params = new HashMap<>();
+        params.put("qwUserId", qwUserId.toString());
+        params.put("removeBlack", removeBlack);
+        params.put("removeRepeat", removeRepeat);
+
+        startPage();
+        List<QwContactListVO> list = qwMsgService.selectQwConversationByMap(params);
+        PageInfo<QwContactListVO> result = new PageInfo<>(list);
+        return R.ok().put("data", result);
     }
     //根据会话获取消息
     @GetMapping("/getQwMessageListBySession")
     @ApiOperation("根据会话获取消息")
+    @DataSource(DataSourceType.SHARDING)
     public R getQwMessageListBySession(QwSessionParam param){
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<QwMsg> list = qwMsgService.selectQwMsgBySession(param);
@@ -148,7 +198,9 @@ public class QwMsgController extends BaseController
         return R.ok().put("data",listPageInfo);
     }
 
+    @ApiOperation("发送企微消息")
     @PostMapping("/sendMsg")
+    @DataSource(DataSourceType.SHARDING)
     public R sendMsg(@RequestBody QwMsgSendParam param){
         return qwMsgService.sendMsg(param);
     }
@@ -156,8 +208,297 @@ public class QwMsgController extends BaseController
 
     //获取用户单条会话
     @GetMapping("/getSession")
+    @DataSource(DataSourceType.SHARDING)
     public R getSession(QwSessionParam param){
         QwContactListVO data = qwMsgService.selectQwSessionBycId(param.getConversationId(),param.getUserId());
         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("获取外部联系人用户信息")
+    public R getQwUserInfo(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId){
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if(externalContact == null) {
+            throw new CustomException("企微外部联系人id不能为空!");
+        }
+
+        QwExternalContactInfo contactInfo = qwExternalContactInfoService.selectQwExternalContactInfoByExternalContactId(qwExternalContactId);
+        if (contactInfo==null){
+            contactInfo = new QwExternalContactInfo();
+            contactInfo.setExternalContactId(qwExternalContactId);
+            contactInfo.setName(externalContact.getName());
+            contactInfo.setSex(externalContact.getGender() == 1 ? "男" : externalContact.getGender() ==  2 ? "女" : "未知");
+            qwExternalContactInfoService.insertQwExternalContactInfo(contactInfo);
+        }
+
+        // 已购状态
+        Boolean isBuy = qwExternalContactService.getBuyStatusByExtId(qwExternalContactId);
+        contactInfo.setIsBuy(isBuy ? "是" : "否");
+
+        return R.ok().put("moreInfo",contactInfo);
+    }
+
+    @PostMapping("/updateQwUserInfo")
+    @ApiOperation("更新企微用户信息")
+    public R updateQwUserInfo(@RequestBody QwExternalContactInfo qwExternalContactInfo){
+        if(qwExternalContactInfo.getExternalContactId() == null) {
+            throw new CustomException("企微外部联系人id不能为空!");
+        }
+        qwExternalContactInfoService.updateQwExternalContactInfoByExternalContactId(qwExternalContactInfo);
+        return R.ok();
+    }
+
+    @PostMapping("/course/watch")
+    @ApiOperation("查询看课记录")
+    public R queryCourseWatchStatistics(@RequestBody WatchCourseStatisticsParam param) {
+        if(param.getQwExternalContactId() == null) {
+            throw new CustomException("外部联系人id为空!");
+        }
+
+        WatchCourseStatisticsDTO watchCourseStatisticsDTO = statisticsService.queryWatchCourse(param);
+
+        return R.ok().put("data",watchCourseStatisticsDTO);
+    }
+
+    @PostMapping("/getFsCourseListBySidebar")
+    @ApiOperation("获取视频课程下拉列表 侧边栏")
+    public R getFsCourseListBySidebar(@RequestBody FsCourseListBySidebarParam param) {
+        QwSession qwSession = sessionService.selectQwSessionBySessionId(param.getSessionId());
+        if (qwSession == null) {
+            return R.error("会话不存在");
+        }
+
+        QwUser qwUser = qwUserService.selectQwUserById(Long.parseLong(qwSession.getQwUserId()));
+        if (qwUser == null || qwUser.getCompanyId() == null) {
+            return R.error("员工未绑定 销售公司 或 未获取到员工信息,请重试!");
+        }
+        param.setCompanyId(qwUser.getCompanyId());
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsCourseListBySidebarVO> fsCourseListBySidebar = fsUserCourseService.getFsCourseListBySidebar(param);
+        PageInfo<FsCourseListBySidebarVO> result = new PageInfo<>(fsCourseListBySidebar);
+        return R.ok().put("data", result);
+    }
+
+    @PostMapping("/getFsCourseVideoListBySidebar")
+    @ApiOperation("获取视频课程的课节下拉列表 侧边栏")
+    public R getFsCourseVideoListBySidebar(@RequestBody FsCourseListBySidebarParam param) {
+
+        if (param.getCourseId()==null){
+            return R.error("课程id不能为空");
+        }
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsCourseVideoListBySidebarVO> videoListBySidebar = fsUserCourseVideoService.getFsCourseVideoListBySidebar(param);
+        PageInfo<FsCourseVideoListBySidebarVO> result = new PageInfo<>(videoListBySidebar);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 创建 发客户小程序
+     */
+    @RepeatSubmit
+    @PostMapping("/createMiniLink")
+    public R createMiniLink(@RequestBody FsCourseLinkMiniParam param) {
+
+        if (Objects.isNull(param.getCourseId())){
+            return R.error("课程id不能为空");
+        }
+        if (Objects.isNull(param.getVideoId())){
+            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, @RequestParam(required = false) String name) {
+        startPage();
+        List<QwContactVO> list  = qwMsgService.contactListByQwUserId(qwUserId, name);
+        return getDataTable(list);
+    }
+
+    // 获取群组列表
+    @GetMapping("/groupList/{userId}")
+    @ApiOperation("获取群组列表")
+    public TableDataInfo groups(@PathVariable("userId") Long qwUserId, @RequestParam(required = false) String name) {
+        startPage();
+        List<QwContactVO> list  = qwMsgService.groupListByQwUserId(qwUserId, name);
+        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);
+    }
+
+    @DataSource(DataSourceType.SHARDING)
+    @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);
+    }
+
+    @ApiOperation("获取外部联系人标签列表")
+    @GetMapping("/getQwExternalContactTagList")
+    public TableDataInfo getQwExternalContactTagList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+        String tagIds = externalContact.getTagIds();
+
+        List<QwTagVO> tagList = new ArrayList<>();
+        if (StringUtils.isBlank(tagIds) || "[]".equals(tagIds)) {
+            return getDataTable(tagList);
+        }
+
+        List<String> ids = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+
+        startPage();
+        tagList = tagService.selectQwTagVOListByTagIds(ids);
+        return getDataTable(tagList);
+    }
+
+    @ApiOperation("获取企微主体标签列表")
+    @GetMapping("/getCorpTagList")
+    public TableDataInfo getCorpTagList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId,
+                                        @RequestParam(required = false) String name) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            return getDataTable(new ArrayList<>());
+        }
+
+        startPage();
+        QwTagGroup params = new QwTagGroup();
+        params.setName(name);
+        params.setCorpId(externalContact.getCorpId());
+        List<QwTagGroupListVO> list = qwTagGroupService.selectQwTagGroupListVO(params);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("外部联系人添加标签")
+    @Log(title = "添加标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addQwExternalContactTag")
+    public R addQwExternalContactTag(@RequestBody QwExternalContactEditTagDTO param) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(param.getQwExternalContactId());
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        List<QwTag> addTags = tagService.selectQwTagListByCorpIdAndIds(param.getTagIds(), externalContact.getCorpId());
+        if (addTags.isEmpty()) {
+            throw new ServiceException("添加标签不能为空");
+        }
+
+        List<String> addTagList = new ArrayList<>();
+        if (StringUtils.isNotBlank(externalContact.getTagIds())) {
+            List<String> oldTags = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+            addTagList = addTags.stream().map(QwTag::getTagId).filter(tagId -> !oldTags.contains(tagId)).collect(Collectors.toList());
+        }
+
+        if (addTagList.isEmpty()) {
+            throw new ServiceException("添加标签不能为空");
+        }
+
+        qwExternalContactService.addQwExternalContactTag(externalContact.getId(), addTagList);
+        return R.ok();
+    }
+
+    @ApiOperation("外部联系人删除标签")
+    @Log(title = "删除标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/delQwExternalContactTag")
+    public R delQwExternalContactTag(@RequestBody QwExternalContactEditTagDTO param) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(param.getQwExternalContactId());
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        if (StringUtils.isBlank(externalContact.getTagIds())) {
+            throw new ServiceException("外部联系人不存在标签");
+        }
+
+        List<QwTag> delTags = tagService.selectQwTagListByCorpIdAndIds(param.getTagIds(), externalContact.getCorpId());
+        if (delTags.isEmpty()) {
+            throw new ServiceException("删除标签不能为空");
+        }
+
+        List<String> oldTags = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+        List<String> delTagList = delTags.stream().map(QwTag::getTagId).filter(oldTags::contains).collect(Collectors.toList());
+        if (delTagList.isEmpty()) {
+            throw new ServiceException("删除标签不能为空");
+        }
+
+        qwExternalContactService.delQwExternalContactTag(externalContact.getId(), delTagList);
+        return R.ok();
+    }
+
 }

+ 4 - 0
fs-qw-api-msg/pom.xml

@@ -117,6 +117,10 @@
             <artifactId>vosk</artifactId>
             <version>0.3.32</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.fs</groupId>
             <artifactId>fs-qw-api</artifactId>

+ 169 - 3
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -2,8 +2,11 @@ package com.fs.app.controller;
 
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
+import com.fs.app.socket.QwImSocket;
+import com.fs.app.util.AudioUtils;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.uuid.IdUtils;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.AiHookService;
@@ -23,6 +26,7 @@ import com.fs.qw.service.IQwExternalContactService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.service.IQwUserVideoService;
 import com.fs.qw.service.IQwUserVoiceLogService;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.mapper.SopUserLogsInfoMapper;
 import com.fs.sop.params.GetQwSopLogsByJsApiParam;
@@ -253,6 +257,7 @@ public class QwMsgController {
                 qwUser.setId(id);
                 qwUser.setVid(jsonObject.get("Vid").toString());
                 qwUser.setIpadStatus(1);
+                qwUser.setAvatar(jsonObject.get("avatar").toString());
                 qwUserMapper.updateQwUser(qwUser);
                 log.info("id:{}, 存Vid", id);
                 redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),104001,10, TimeUnit.MINUTES);
@@ -308,14 +313,26 @@ public class QwMsgController {
                 if (wxWorkMessageDTO.getReferid()!=0){
                     break;
                 }
+
+
+                Long receiver = wxWorkMessageDTO.getReceiver();
+                Long sender = wxWorkMessageDTO.getSender();
+
+                // 1客户 2销售
+                int sendType = 2;
+
+                // 消息发送者用户ID
+                Long userId = receiver;
+                if (2000000000000000L - receiver > 0){
+                    sendType = 1;
+                    userId = sender;
+                }
                 if (wxWorkMessageDTO.getMsgtype()==2||wxWorkMessageDTO.getMsgtype()==0||wxWorkMessageDTO.getMsgtype()==16||wxWorkMessageDTO.getMsgtype() == 101||wxWorkMessageDTO.getMsgtype() == 104){
 
                     String content = wxWorkMessageDTO.getContent();
                     log.info("id:{}, 接收人:"+wxWorkMessageDTO.getReceiver(), id);
                     log.info("id:{}, 发送人:"+wxWorkMessageDTO.getSender(), id);
                     log.info("id:{}, 内容:"+content, id);
-                    Long receiver = wxWorkMessageDTO.getReceiver();
-                    Long sender = wxWorkMessageDTO.getSender();
                     if(wxWorkMessageDTO.getMsgtype()==16){
                         WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
                         ste.setMsgid(wxWorkMessageDTO.getMsg_id());
@@ -390,7 +407,6 @@ public class QwMsgController {
                     if (wxWorkMessageDTO.getRecordtype()==null){
                         break;
                     }
-                    Long receiver = wxWorkMessageDTO.getReceiver();
                     Long extId=null;
                     long totalSeconds=0L;
                     if (2000000000000000L-receiver>0){
@@ -436,6 +452,27 @@ public class QwMsgController {
                     qwUserVoiceLogService.addQuUserVoiceByIpadCallback(id,extId,recordType,totalSeconds,wxWorkMsgResp.getUuid());
                 }
 
+                // 处理文本消息
+                if (wxWorkMessageDTO.getMsgtype() == 2 || wxWorkMessageDTO.getMsgtype() == 0) {
+                    processTextMessage(id, userId, wxWorkMessageDTO.getContent(), wxWorkMsgResp, sendType);
+                }
+                // 语音消息
+                if (wxWorkMessageDTO.getMsgtype() == 16) {
+                    processVoiceMessage(serverId, wxWorkMessageDTO.getContent(), wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                }
+                // 图片消息
+                if (wxWorkMessageDTO.getMsgtype() == 101){
+                    processImageMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                }
+                // gif 表情消息
+                if (wxWorkMessageDTO.getMsgtype() == 104){
+                    processEmotionDynamicMessage(wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                }
+                // 小程序消息
+                if (wxWorkMessageDTO.getMsgtype() == 78) {
+                    processMiniAppMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp, id, userId, sendType);
+                }
+
                 break;
 
         }
@@ -560,4 +597,133 @@ public class QwMsgController {
         }
     }
 
+    /**
+     * 处理文本消息
+     * @param id                企微用户ID
+     * @param userId            消息发送者ID
+     * @param content           消息内容
+     * @param wxWorkMsgResp     回调信息对象
+     * @param sendType          发送者类型 1客户 2销售
+     */
+    private void processTextMessage(Long id, Long userId, String content, WxWorkMsgResp wxWorkMsgResp, Integer sendType) {
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 1);
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 处理语音消息
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param content           翻译后的内容
+     * @param wxWorkMsgResp     回调信息对象
+     * @param id                企微用户ID
+     * @param userId            消息发送者ID
+     * @param sendType          发送者类型 1客户 2销售
+     */
+    private void processVoiceMessage(Long serverId, String content, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, Integer sendType) {
+        String voiceFileName = IdUtils.fastSimpleUUID() + ".silk";
+        WxWorkResponseDTO<String> fileUrlResp =
+                aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getVoice_id(), wxWorkMessageDTO.getAes_key(), 5, voiceFileName, wxWorkMessageDTO.getVoice_size(), serverId);
+        if (fileUrlResp.getErrcode() != 0) {
+            log.warn("获取语音地址失败: {}", fileUrlResp.getErrmsg());
+            return;
+        }
+
+        // silk转map3
+        String url = AudioUtils.convertSilk2Mp3(fileUrlResp.getData());
+        if (StringUtils.isBlank(url)) {
+            log.warn("转换silk语音格式失败");
+            return;
+        }
+
+        // 转换内容为空时再尝试一次
+        if (StringUtils.isBlank(content)) {
+            WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
+            ste.setMsgid(wxWorkMessageDTO.getMsg_id());
+            ste.setUuid(wxWorkMsgResp.getUuid());
+            WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO> dto = wxWorkService.SpeechToTextEntity(ste, serverId);
+            content = dto.getData().getText();
+        }
+
+        com.alibaba.fastjson.JSONObject json = new com.alibaba.fastjson.JSONObject();
+        json.put("url", url);
+        json.put("content", content);
+
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, json.toString(), wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 4);
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 处理图片消息
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param id                企微用户ID
+     * @param userId            消息发送者ID
+     * @param sendType          发送者类型 1客户 2销售
+     */
+    private void processImageMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, Integer sendType) {
+        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);
+        if (fileUrlResp.getErrcode() != 0) {
+            log.warn("获取图片地址失败: {}", fileUrlResp.getErrmsg());
+            return;
+        }
+
+        String content = fileUrlResp.getData();
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 2);
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 处理动态表情消息
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param id                企微用户ID
+     * @param userId            消息发送者ID
+     * @param sendType          发送者类型 1客户 2销售
+     */
+    private void processEmotionDynamicMessage(WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, int sendType) {
+        String content = wxWorkMessageDTO.getUrl();
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, content, wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 3);
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 小程序消息处理
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param id                企微用户ID
+     * @param userId            消息发送者ID
+     * @param sendType          发送者类型 1客户 2销售
+     */
+    private void processMiniAppMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, Long id, Long userId, int sendType) {
+        String thumbName = IdUtils.fastSimpleUUID() + ".jpg";
+        WxWorkResponseDTO<String> fileUrlResp =
+                aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getThumbFileId(), wxWorkMessageDTO.getThumbAESKey(), 1, thumbName, wxWorkMessageDTO.getSize(), serverId);
+        if (fileUrlResp.getErrcode() != 0) {
+            log.warn("获取图片地址失败: {}", fileUrlResp.getErrmsg());
+            return;
+        }
+
+        JSONObject json = new JSONObject();
+        json.put("appid", wxWorkMessageDTO.getAppid());
+        json.put("appName", wxWorkMessageDTO.getAppName());
+        json.put("weappIconUrl", wxWorkMessageDTO.getAppid());
+        json.put("desc", wxWorkMessageDTO.getDesc());
+        json.put("pagepath", wxWorkMessageDTO.getPagepath());
+        json.put("title", wxWorkMessageDTO.getTitle());
+        json.put("thumbnail", fileUrlResp.getData());
+
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(id, userId, json.toString(), wxWorkMsgResp.getUuid(), sendType, wxWorkMsgResp.getJson(), 5);
+        QwImSocket.broadcast(message);
+    }
+
 }

+ 101 - 0
fs-qw-api-msg/src/main/java/com/fs/app/socket/QwImSocket.java

@@ -0,0 +1,101 @@
+package com.fs.app.socket;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.app.socket.configurator.QwImConfigurator;
+import com.fs.qw.vo.QwMessageListVO;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+@ServerEndpoint(value = "/qwImSocket/{companyId}", configurator = QwImConfigurator.class)
+@Component
+public class QwImSocket {
+
+    private static final ConcurrentHashMap<Long, CopyOnWriteArraySet<Session>> companySessions = new ConcurrentHashMap<>();
+
+    /**
+     * 连接建立成功调用的方法
+     * @param session   连接会话
+     * @param companyId 公司ID
+     */
+    @OnOpen
+    public void onOpen(Session session, @PathParam("companyId") Long companyId) {
+        // 将当前会话加入到会话池中
+        companySessions.computeIfAbsent(companyId, k -> new CopyOnWriteArraySet<>()).add(session);
+    }
+
+    /**
+     * 连接关闭调用的方法
+     * @param session   连接会话
+     * @param companyId 公司ID
+     */
+    @OnClose
+    public void onClose(Session session, @PathParam("companyId") Long companyId) {
+        // 从会话池中移除当前会话
+        CopyOnWriteArraySet<Session> sessions = companySessions.get(companyId);
+        if (sessions != null) {
+            sessions.remove(session);
+            // 如果直播间没人了,可以移除该直播间
+            if (sessions.isEmpty()) {
+                companySessions.remove(companyId);
+            }
+        }
+    }
+
+    /**
+     * 发生错误时调用的方法
+     * @param session   连接会话
+     * @param companyId 公司ID
+     * @param error     错误对象
+     */
+    @OnError
+    public void onError(Session session, @PathParam("companyId") Long companyId, Throwable error) {
+        System.err.println("发生错误!会话ID: " + session.getId());
+        CopyOnWriteArraySet<Session> sessions = companySessions.get(companyId);
+        if (sessions != null) {
+            sessions.remove(session);
+            // 如果直播间没人了,可以移除该直播间
+            if (sessions.isEmpty()) {
+                companySessions.remove(companyId);
+            }
+        }
+    }
+
+    /**
+     * 群发消息
+     * @param message   要发送的消息
+     */
+    public static void broadcast(QwMessageListVO message) {
+        if (Objects.isNull(message)) {
+            return;
+        }
+
+        String msg = JSON.toJSONString(message);
+        CopyOnWriteArraySet<Session> sessions = companySessions.get(message.getCompanyId());
+        if (sessions != null) {
+            for (Session session : sessions) {
+                if (session.isOpen()) {
+                    try {
+                        session.getBasicRemote().sendText(msg);
+                    } catch (IOException e) {
+                        System.err.println("发送消息给会话[" + session.getId() + "]失败: " + e.getMessage());
+                        // 移除无效会话
+                        sessions.remove(session);
+                    }
+                } else {
+                    sessions.remove(session); // 移除已关闭的会话
+                }
+            }
+        }
+    }
+
+}

+ 22 - 0
fs-qw-api-msg/src/main/java/com/fs/app/socket/configurator/QwImConfigurator.java

@@ -0,0 +1,22 @@
+package com.fs.app.socket.configurator;
+
+import com.fs.app.exception.FSException;
+
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class QwImConfigurator extends ServerEndpointConfig.Configurator {
+
+    @Override
+    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+        Map<String, List<String>> parameterMap = request.getParameterMap();
+        List<String> token = parameterMap.get("token");
+        if (Objects.isNull(token)) {
+            throw new FSException("Unauthorized access to WebSocket endpoint.");
+        }
+    }
+}

+ 160 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/AudioUtils.java

@@ -0,0 +1,160 @@
+package com.fs.app.util;
+
+import com.fs.common.utils.uuid.IdUtils;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.springframework.http.HttpStatus;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class AudioUtils {
+
+    /**
+     * silk转换为mp3
+     * @param silkUrl silk语音链接地址
+     * @return  mp3链接地址
+     */
+    public static String convertSilk2Mp3(String silkUrl) {
+        String uniqueId = IdUtils.fastSimpleUUID();
+        Path uploadDirPath = Paths.get(System.getProperty("java.io.tmpdir"), "/");
+        Path downloadedSilkFilePath = uploadDirPath.resolve(uniqueId + ".silk");
+        Path pcmFilePath = uploadDirPath.resolve(uniqueId + ".pcm");
+        Path mp3FilePath = uploadDirPath.resolve(uniqueId + ".mp3");
+
+        try {
+            // 1. 从网络下载 SILK 文件
+            downloadFile(silkUrl, downloadedSilkFilePath);
+            log.debug("SILK file downloaded to: {}", downloadedSilkFilePath);
+
+            // 2. 使用 silk-v3-decoder 解码 SILK 到 PCM
+            List<String> silkDecodeCommand = new ArrayList<>();
+            silkDecodeCommand.add("silk_v3_decoder");
+            silkDecodeCommand.add(downloadedSilkFilePath.toString());
+            silkDecodeCommand.add(pcmFilePath.toString());
+
+            ProcessBuilder silkDecoderPb = new ProcessBuilder(silkDecodeCommand);
+            silkDecoderPb.redirectErrorStream(true); // 将错误流合并到标准输出
+            Process silkDecoderProcess = silkDecoderPb.start();
+
+            String silkDecoderOutput = readInputStreamToString(silkDecoderProcess.getInputStream());
+
+            boolean silkDecoderExited = silkDecoderProcess.waitFor(60, TimeUnit.SECONDS);
+            if (!silkDecoderExited || silkDecoderProcess.exitValue() != 0) {
+                log.error("silk conversion failed or timed out. error: {}", silkDecoderOutput);
+                return null;
+            }
+            log.debug("SILK decoder to PCM successfully.");
+
+            // 3. 使用 FFmpeg 将 PCM 转码为 MP3
+            Process ffmpegProcess = getFfmpegProcess(pcmFilePath, mp3FilePath);
+            String ffmpegOutput = readInputStreamToString(ffmpegProcess.getInputStream());
+
+            boolean ffmpegExited = ffmpegProcess.waitFor(120, TimeUnit.SECONDS);
+            if (!ffmpegExited || ffmpegProcess.exitValue() != 0) {
+                log.error("ffmpeg conversion failed or timed out. error: {}", ffmpegOutput);
+                return null;
+            }
+            log.debug("ffmpeg conversion to MP3 successfully.");
+
+            // 4. 上传oss
+            String fileName = mp3FilePath.getFileName().toString();
+            String suffix = fileName.substring(fileName.lastIndexOf("."));
+            CloudStorageService storage = OSSFactory.build();
+            return storage.uploadSuffix(Files.newInputStream(mp3FilePath), suffix);
+
+        } catch (IOException | InterruptedException | NullPointerException e) {
+            log.error("Conversion error: {}", e.getMessage());
+            return null;
+        } finally {
+            // 清理临时文件 (重要!)
+            try {
+                if (Files.exists(downloadedSilkFilePath)) Files.delete(downloadedSilkFilePath);
+                if (Files.exists(pcmFilePath)) Files.delete(pcmFilePath);
+                if (Files.exists(mp3FilePath)) Files.delete(mp3FilePath);
+            } catch (IOException e) {
+                log.error("Error cleaning up temporary files:: {}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 执行ffmpeg
+     * @param pcmFilePath   pcm文件
+     * @param mp3FilePath   mp3地址
+     * @return  process
+     * @throws IOException exception
+     */
+    private static Process getFfmpegProcess(Path pcmFilePath, Path mp3FilePath) throws IOException {
+        List<String> ffmpegCommand = new ArrayList<>();
+        ffmpegCommand.add("ffmpeg");
+        ffmpegCommand.add("-y");
+        ffmpegCommand.add("-f");
+        ffmpegCommand.add("s16le");
+        ffmpegCommand.add("-ar");
+        ffmpegCommand.add("24000"); // 注意:这里假设是 24kHz,如果你的 SILK 文件是其他采样率,请调整
+        ffmpegCommand.add("-ac");
+        ffmpegCommand.add("1");
+        ffmpegCommand.add("-i");
+        ffmpegCommand.add(pcmFilePath.toString());
+        ffmpegCommand.add(mp3FilePath.toString());
+
+        ProcessBuilder ffmpegPb = new ProcessBuilder(ffmpegCommand);
+        ffmpegPb.redirectErrorStream(true);
+        return ffmpegPb.start();
+    }
+
+    /**
+     * 处理文件流
+     * @param is 输入流
+     * @return  输出
+     * @throws IOException exception
+     */
+    private static String readInputStreamToString(InputStream is) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int len;
+        while ((len = is.read(buffer)) != -1) {
+            bos.write(buffer, 0, len);
+        }
+        return bos.toString("UTF-8"); // 使用 UTF-8 编码
+    }
+
+    /**
+     * 下载网络文件
+     * @param fileUrl       网络文件
+     * @param destination   临时文件
+     * @throws IOException  exception
+     */
+    private static void downloadFile(String fileUrl, Path destination) throws IOException {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault();
+             CloseableHttpResponse response = httpClient.execute(new HttpGet(fileUrl));
+             InputStream inputStream = response.getEntity().getContent();
+             FileOutputStream outputStream = new FileOutputStream(destination.toFile())) {
+
+            if (response.getStatusLine().getStatusCode() != HttpStatus.OK.value()) {
+                throw new IOException("Failed to download file from " + fileUrl + ", HTTP Status: " + response.getStatusLine().getStatusCode());
+            }
+
+            byte[] buffer = new byte[4096];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+        }
+    }
+}

+ 2 - 1
fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -106,7 +106,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/**/*.html",
                         "/**/*.css",
                         "/**/*.js",
-                        "/profile/**"
+                        "/profile/**",
+                        "/qwImSocket/**"
                 ).permitAll()
 
                 .antMatchers("/**").anonymous()

+ 17 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/WebSocketConfig.java

@@ -0,0 +1,17 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+public class WebSocketConfig {
+    /**
+     * ServerEndpointExporter 作用
+     * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
+     */
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

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

@@ -25,5 +25,14 @@ public class FsCourseLinkMiniParam {
     * 客户的小程序id
     */
     private Long fsUserId;
+    /**
+     * 外部联系人主键
+     */
+    private Long extId;
+
+    /**
+     * sessionId
+     */
+    private Long sessionId;
 
 }

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

@@ -41,4 +41,14 @@ public class FsCourseListBySidebarParam implements Serializable {
      * 用于过滤用户当日应该看课的视频id
      */
     private List<Long> videoIds;
+
+    /**
+     * 外部联系人主键ID
+     */
+    private Long extId;
+
+    /**
+     * 会话ID
+     */
+    private Long sessionId;
 }

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

@@ -151,6 +151,9 @@ public interface IFsUserCourseVideoService
      */
     ResponseResult<FsUserCourseVideoLinkDetailsVO> getLinkCourseVideoDetails(FsUserCourseVideoLinkParam param);
 
+
+    R addWatchLogByLink(FsUserCourseAddCompanyUserParam param);
+
     /**
      * 更新看课时长
      * @param param 入参
@@ -241,4 +244,6 @@ public interface IFsUserCourseVideoService
     R getVideoInfoByVid();
 
     void updateMediaPublishStatus(String vid);
+
+    R createMiniLinkByQwIm(FsCourseLinkMiniParam param);
 }

+ 30 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -2408,6 +2408,12 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
         return ResponseResult.ok(vo);
     }
 
+    @Override
+    public R addWatchLogByLink(FsUserCourseAddCompanyUserParam param) {
+
+        return null;
+    }
+
     @Override
     public R updateWatchDurationWx(FsUserCourseVideoUParam param) {
         //临时短链不做记录
@@ -3836,5 +3842,29 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             }
         }
     }
+
+    @Override
+    public R createMiniLinkByQwIm(FsCourseLinkMiniParam param) {
+        QwExternalContact qwExternalContact = qwExternalContactMapper.selectById(param.getExtId());
+        if (Objects.isNull(qwExternalContact)) {
+            return R.error("客户不存在");
+        }
+        QwUser qwUser = qwUserMapper.selectById(qwExternalContact.getQwUserId());
+        if (Objects.isNull(qwUser) || Objects.isNull(qwUser.getCompanyId()) || Objects.isNull(qwUser.getCompanyUserId())){
+            return R.error("员工未绑定 销售公司 或 销售 请先绑定");
+        }
+
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(qwUser.getCorpId());
+        if (Objects.isNull(qwCompany)) {
+            return R.error().put("msg","企业不存在,请联系管理员");
+        }
+
+        //看课记录
+        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), qwExternalContact.getFsUserId(), qwUser, qwExternalContact.getId(),2);
+
+        //生成小程序链接
+        String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, qwExternalContact.getId(),2,null, 0);
+        return R.ok().put("data", linkByMiniApp);
+    }
 }
 

+ 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;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -2,6 +2,7 @@ package com.fs.fastGpt.service;
 
 import com.fs.common.core.domain.R;
 import com.fs.im.vo.OpenImMsgCallBackVO;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwHookApi.vo.QwHookVO;
 import com.fs.wxwork.dto.WxWorkResponseDTO;
 
@@ -25,4 +26,30 @@ public interface AiHookService {
     void expireAiMsg();
 
     WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, String authKey, String fileName, Integer fileSize, Long serverId);
+
+    /**
+     * 获取文件地址
+     * @param uuid      uuid
+     * @param fileId    fileId
+     * @param aesKey    aesKey
+     * @param fileType  fileType
+     * @param fileName  fileName
+     * @param fileSize  fileSize
+     * @param serverId  serverId
+     * @return  WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, Integer fileType, String fileName, Integer fileSize, Long serverId);
+
+    /**
+     * 保存企微聊天信息
+     *
+     * @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小程序
+     */
+    QwMessageListVO saveQwMsg(Long qwUserId, Long userId, String content, String uuid, int sendType, String json, int msgType);
 }

+ 160 - 0
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -45,9 +45,11 @@ 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;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwAutoTagsRulesTags;
 import com.fs.qw.service.*;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwApi.domain.QwResult;
 import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.param.QwSendMsgParam;
@@ -70,7 +72,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 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;
@@ -165,6 +169,10 @@ public class AiHookServiceImpl implements AiHookService {
     private IFastGptChatReplaceTextService fastGptChatReplaceTextService;
     @Autowired
     private ICrmMsgService crmMsgService;
+    @Autowired
+    private QwSessionMapper qwSessionMapper;
+    @Autowired
+    private QwMsgMapper qwMsgMapper;
 
     private static final String AI_REPLY = "AI_REPLY:";
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
@@ -2063,6 +2071,20 @@ public class AiHookServiceImpl implements AiHookService {
                     }
                 }
             }
+            // 客服进行回复后就转人工10分钟
+            if(fastGptChatSession != null){
+                Calendar calendar = Calendar.getInstance();
+                //定时任务会处理10分钟以内的,所以设置20分钟
+                calendar.add(Calendar.MINUTE, 30);
+                Date expireTime = calendar.getTime();
+
+                FastGptChatSession chatSession = new FastGptChatSession();
+                chatSession.setLastTime(expireTime);
+                chatSession.setIsArtificial(1);
+                chatSession.setSessionId(fastGptChatSession.getSessionId());
+
+                fastGptChatSessionMapper.updateFastGptChatSession(chatSession);
+            }
         }
         return R.ok();
     }
@@ -2117,4 +2139,142 @@ public class AiHookServiceImpl implements AiHookService {
         return wxWorkService.downloadWeChatFile(weChatFileDTO, serverId);
     }
 
+    /**
+     * 获取文件地址
+     * @param uuid      uuid
+     * @param fileId    fileId
+     * @param aesKey    aesKey
+     * @param fileType  fileType
+     * @param fileName  fileName
+     * @param fileSize  fileSize
+     * @param serverId  serverId
+     * @return  WxWorkResponseDTO
+     */
+    @Override
+    public WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, Integer fileType, String fileName, Integer fileSize, Long serverId) {
+        WxDownloadFileDTO downloadFileDTO = new WxDownloadFileDTO();
+        downloadFileDTO.setUuid(uuid);
+        downloadFileDTO.setFileid(fileId);
+        downloadFileDTO.setAes_key(aesKey);
+        downloadFileDTO.setFiletype(fileType);
+        downloadFileDTO.setFile_name(fileName);
+        downloadFileDTO.setSize(fileSize);
+        return wxWorkService.downloadFile(downloadFileDTO, serverId);
+    }
+
+    /**
+     * 保存企微聊天信息
+     *
+     * @param qwUserId 企微用户ID
+     * @param userId   用户ID
+     * @param content  聊天内容
+     * @param uuid     UUID
+     * @param sendType 发送者类型 1用户 2客服
+     * @param json     消息json
+     * @param msgType  消息类型 1文本 2图片 3动态表情 4语音
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public QwMessageListVO saveQwMsg(Long qwUserId, Long userId, String content, String uuid, int sendType, String json, int msgType) {
+        // 查询企微用户
+        QwUser qwUser = qwUserService.selectQwUserById(qwUserId);
+        if (Objects.isNull(qwUser)){
+            log.warn("企微用户不存在 qwUserId: {}", qwUserId);
+            return null;
+        }
+
+        // 查询外部联系人
+        QwExternalContact qwExternalContact = getExternalContact(userId, uuid, qwUser.getServerId(), qwUser.getCorpId(), qwUser.getQwUserId());
+        if (Objects.isNull(qwExternalContact)){
+            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);
+        }
+
+        // 保存聊天消息
+        QwMsg qwMsg = new QwMsg();
+        qwMsg.setContent(content);
+        qwMsg.setSessionId(qwSession.getSessionId());
+        qwMsg.setSendType(sendType);
+        qwMsg.setCompanyId(qwUser.getCompanyId());
+        qwMsg.setCompanyUserId(qwUser.getCompanyUserId());
+        qwMsg.setMsgType(msgType);
+        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());
+        qwMsgMapper.insertQwMsg(qwMsg);
+        log.debug("保存企微聊天记录 msgId: {}", qwMsg.getMsgId());
+
+        // 组装返回消息结构
+        QwMessageListVO listVO = new QwMessageListVO();
+        QWFromUser qwFromUser = new QWFromUser();
+        if (sendType == 1) {
+            qwFromUser.setId(Long.parseLong(qwMsg.getQwExtId()));
+            qwFromUser.setAvatar(qwMsg.getAvatar());
+            qwFromUser.setDisplayName(qwMsg.getNickName());
+        }else if(sendType == 2){
+            qwFromUser.setId(Long.parseLong(qwMsg.getQwUserId()));
+            qwFromUser.setDisplayName(qwUser.getQwUserName());
+            qwFromUser.setAvatar(qwUser.getAvatar());
+        }
+
+        listVO.setCompanyId(qwUser.getCompanyId());
+        String type = "text";
+        MsgType messageType = MsgType.getMsgType(msgType);
+        if (Objects.nonNull(messageType)){
+            type = messageType.getValue();
+        }
+
+        listVO.setType(type);
+        listVO.setStatus("succeed");
+        listVO.setExtId(qwMsg.getQwExtId());
+        listVO.setFromUser(qwFromUser);
+        listVO.setSendTime(qwMsg.getCreateTime().getTime());
+        listVO.setId(qwMsg.getMsgId().toString());
+        listVO.setContent(qwMsg.getContent());
+        listVO.setToContactId(String.valueOf(qwSession.getSessionId()));
+        listVO.setAppKey(qwUser.getAppKey());
+        return listVO;
+    }
+
+    /**
+     * 查询外部联系人
+     * @param userId    用户ID
+     * @param uuid      UUID
+     * @param serverId  服务ID
+     * @param corpId    企微ID
+     * @param qwUserId  企微用户ID
+     * @return  QwExternalContact
+     */
+    private QwExternalContact getExternalContact(Long userId, String uuid, Long serverId, String corpId, String qwUserId) {
+        return qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(getExtId(userId, uuid, serverId), corpId, qwUserId);
+    }
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/qw/domain/QwUser.java

@@ -28,6 +28,9 @@ public class QwUser extends BaseEntity
     @Excel(name = "企微用户名")
     private String qwUserName;
 
+    /** 头像 **/
+    private String avatar;
+
     /** 所属部门id */
     @Excel(name = "所属部门id")
     private String department;

+ 27 - 0
fs-service/src/main/java/com/fs/qw/enums/MsgType.java

@@ -0,0 +1,27 @@
+package com.fs.qw.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+@Getter
+public enum MsgType {
+    TEXT(1, "text"),
+    IMAGE(2, "image"),
+    EMOTION_DYNAMIC(3, "emotionDynamic"),
+    VOICE(4, "voice"),
+    MINI_PROGRAM(5, "miniprogram"),
+    ;
+
+    private final Integer code;
+    private final String value;
+
+    MsgType(Integer code, String value) {
+        this.code = code;
+        this.value = value;
+    }
+
+    public static MsgType getMsgType(Integer code) {
+        return Stream.of(values()).filter(t -> t.getCode().equals(code)).findFirst().orElse(null);
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -497,6 +497,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
 
     List<QwExternalContact> selectExternalByFsUserIds(@Param("userIds")List<Long> userIds);
 
+
     @Select({"<script> " +
             "SELECT qe.remark FROM qw_external_contact qe " +
             "where qe.company_user_id = #{companyUserId} and qe.fs_user_id = #{userId}" +
@@ -520,6 +521,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
 
     void updateQwExternalContactIsRePlyById(@Param("id")Long id);
 
+
     @Select("SELECT max(id) FROM qw_external_contact where status <>'4' and unionid is NULL")
     Long selectSyncMaxId();
 
@@ -544,4 +546,20 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     List<QwExternalContact> getGroupChatUserByChatIdAndUserName(@Param("userId")String userId,@Param("userName")String userName,@Param("corpId") String corpId,@Param("chatId") String chatId);
     @Select("select * from qw_external_contact where unionid = #{unionID} order by create_time asc limit 1 ")
     QwExternalContact selectQwExternalByUnionID(String unionId);
+
+    @Select("SELECT id,external_user_id,name,avatar,remark,description,fs_user_id FROM qw_external_contact " +
+            " WHERE id = #{qwExternalContactId}")
+    QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId);
+
+    /**
+     * 根据外部联系人ID查询用户是否已购产品
+     * @param qwExternalContactId   外部联系人ID
+     * @return  Boolean
+     */
+    Boolean getBuyStatusByExtId(@Param("qwExternalContactId") Long qwExternalContactId);
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    List<QwContactVO> getContactListByQwUserId(@Param("qwUserId") Long qwUserId, @Param("name") String name);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwSessionMapper.java

@@ -2,6 +2,7 @@ package com.fs.qw.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.qw.domain.QwSession;
+import com.fs.qw.vo.QwContactListVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
@@ -66,4 +67,11 @@ public interface QwSessionMapper extends BaseMapper<QwSession>
 
     @Select("select * from qw_session where qw_ext_id = #{qwExtId} and qw_user_id = #{qwUserId}")
     QwSession selectQwSessionByExtIdAndQwUserId(@Param("qwExtId") Long qwExtId, @Param("qwUserId") Long id);
+
+    /**
+     * 根据企微用户ID查询会话列表
+     * @param qwUserId  企微用户ID
+     * @return  list
+     */
+    List<QwContactListVO> selectContactListByQwUserId(@Param("qwUserId") Long qwUserId);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -469,4 +469,9 @@ public interface QwUserMapper extends BaseMapper<QwUser>
             "</script>")
     List<QwUser> selectQwUserByIds(@Param("qwUserIdList") List<Long> qwUserIdList);
 
+    /**
+     * 根据销售ID查询企微用户列表
+     */
+    List<QwUserVO> selectQwUserVoListByCompanyUserId(@Param("companyUserId") Long companyUserId);
+
 }

+ 6 - 0
fs-service/src/main/java/com/fs/qw/param/QwMsgSendParam.java

@@ -9,5 +9,11 @@ public class QwMsgSendParam implements Serializable {
     private Long sessionId;
     private String content;//内容
     private String appKey;
+    // 消息类型 1文本 2图片 3动态表情 4语音 5小程序
+    private Integer msgType;
+    // 小程序标题
+    private String title;
+    // 小程序图片
+    private String image;
 
 }

+ 2 - 0
fs-service/src/main/java/com/fs/qw/param/QwSessionParam.java

@@ -7,4 +7,6 @@ import lombok.Data;
 public class QwSessionParam extends BaseQueryParam {
     private String conversationId;
     private Long userId;//企微id
+    // 消息ID
+    private Long msgId;
 }

+ 17 - 0
fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java

@@ -249,8 +249,10 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
      */
     List<QwUserDelLossLogVO> selectQwUserDelLossLogList(QwUserDelLossLogParam param);
 
+
     void updateQwExternalContactStatusById(QwExternalContact qwExternalContact);
 
+
     R getRepeat(RepeatParam param);
     List<QwExternalContactVO> selectQwExternalContactListVONewSys(QwExternalContactParam qwExternalContact);
     /**
@@ -259,4 +261,19 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
     QwExternalContact selectQwUserListVOByQwUserIdAndCorpIdAndExternalUserId(ExternalContactParam externalContactParam);
 
     List<QwExternalContact> selectQwUserAndLevel(List<Long> qwUserIdList, List<String> levelList, boolean isReg);
+
+    /**
+     * 根据id查询外部联系人信息
+     * @param qwExternalContactId id
+     * @return QwExternalContact
+     */
+    QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId);
+
+    /**
+     * 根据外部联系人ID查询用户是否已购产品
+     * @param qwExternalContactId   外部联系人ID
+     * @return  Boolean
+     */
+    Boolean getBuyStatusByExtId(Long qwExternalContactId);
+
 }

+ 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, String name);
+
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    List<QwContactVO> groupListByQwUserId(Long qwUserId, String name);
+
+    /**
+     * 获取会话ID
+     */
+    QwContactListVO getConversationIdById(Long qwUserid, String id, Boolean isGroup);
 }

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

@@ -203,4 +203,9 @@ public interface IQwUserService
     List<Long> selectDeptByParentId(Long deptId,String cropId);
 
     List<QwUser> selectQwUserByIds(List<Long> qwUserIdList);
+
+    /**
+     * 根据销售ID查询企微用户列表
+     */
+    List<QwUserVO> selectQwUserVoListByCompanyUserId(Long companyUserId);
 }

+ 27 - 8
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -2213,12 +2213,12 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     public void insertQwExternalContactByExternalUserId(String externalUserID, String userID, Long companyId, String corpId, String state, String welcomeCode) throws ParseException {
 
 
-//        String qwApiExternal=redisCache.getCacheObject("qwApiExternal:"+userID+":"+corpId+":"+externalUserID);
-//        if (!StringUtil.strIsNullOrEmpty(qwApiExternal)){
-//            return;
-//        }else {
-//            redisCache.setCacheObject("qwApiExternal:"+userID+":"+corpId+":"+externalUserID ,"1",10, TimeUnit.MINUTES);
-//        }
+        String qwApiExternal=redisCache.getCacheObject("qwApiExternal:"+userID+":"+corpId+":"+externalUserID);
+        if (!StringUtil.strIsNullOrEmpty(qwApiExternal)){
+            return;
+        }else {
+            redisCache.setCacheObject("qwApiExternal:"+userID+":"+corpId+":"+externalUserID ,"1",10, TimeUnit.MINUTES);
+        }
 
         // 获取当前日期(只包含年月日)
         LocalDate currentDate = LocalDate.now();
@@ -2230,7 +2230,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         QwContactWay wayId = null;
         //先入客户
         QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-        boolean isNewQwExternalContact = qwExternalContact == null;
+        boolean isNewQwExternalContact = qwExternalContact == null ? true : false;
         qwExternalContact = qwExternalContact == null ? new QwExternalContact() : qwExternalContact;
         qwExternalContact.setUserId(userID); // 设置属于用户ID
         qwExternalContact.setExternalUserId(externalUserID); // 设置外部联系人ID
@@ -2312,7 +2312,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
                                     }
                                 }
                             }
-                            if (qwContactWay.getIsWelcome() == 1 && isClose) {
+                            if (qwContactWay.getIsSpanWelcome() == 1 && isClose) {
                                 isSend = qwContactWayService.sendWelcomeMsg(qwContactWay, corpId, welcomeCode,qwUser,contact.getId());
                             }
                         }
@@ -5978,4 +5978,23 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     }
 
 
+    /**
+     * 根据id查询外部联系人信息
+     * @param qwExternalContactId id
+     * @return QwExternalContact
+     */
+    @Override
+    public QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId) {
+        return qwExternalContactMapper.getQwExternalContactDetailsById(qwExternalContactId);
+    }
+
+    /**
+     * 根据外部联系人ID查询用户是否已购产品
+     * @param qwExternalContactId   外部联系人ID
+     * @return  Boolean
+     */
+    @Override
+    public Boolean getBuyStatusByExtId(Long qwExternalContactId) {
+        return qwExternalContactMapper.getBuyStatusByExtId(qwExternalContactId);
+    }
 }

+ 266 - 27
fs-service/src/main/java/com/fs/qw/service/impl/QwMsgServiceImpl.java

@@ -2,16 +2,25 @@ package com.fs.qw.service.impl;
 
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.http.HttpRequest;
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
 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.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyMiniapp;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyMiniappMapper;
+import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.his.config.FsSysConfig;
 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;
@@ -21,8 +30,11 @@ 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 org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -50,6 +62,14 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
     @Autowired
     private ConfigUtil configUtil;
+    @Autowired
+    private WxWorkService wxWorkService;
+    @Autowired
+    private CompanyMiniappMapper companyMiniappMapper;
+    @Autowired
+    private QwExternalContactMapper externalContactMapper;
+    @Autowired
+    private FsCoursePlaySourceConfigMapper playSourceConfigMapper;
 
     /**
      * 查询企微聊天记录
@@ -139,7 +159,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                     qwSession.setQwUserId(qwuser.getId().toString());
                     qwSession.setStatus(1);
                     qwSession.setAvatar(qwExternalContact.getAvatar());
-                    qwSession.setNickName(qwExternalContact.getRemark());
+                    qwSession.setNickName(qwExternalContact.getName());
                     qwSession.setCompanyId(qwuser.getCompanyId());
                     qwSession.setCompanyUserId(qwuser.getCompanyUserId());
                     qwSession.setCreateTime(new Date());
@@ -147,7 +167,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                     qwSessionMapper.insertQwSession(qwSession);
                 }else {
                     qwSession.setUpdateTime(new Date());
-                    qwSession.setNickName(qwExternalContact.getRemark());
+                    qwSession.setNickName(qwExternalContact.getName());
                     qwSessionMapper.updateQwSession(qwSession);
                 }
                 QwMsg qwMsg = new QwMsg();
@@ -161,7 +181,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                 qwMsg.setQwUserId(qwSession.getQwUserId());
                 qwMsg.setQwExtId(qwSession.getQwExtId());
                 qwMsg.setAvatar(qwExternalContact.getAvatar());
-                qwMsg.setNickName(qwExternalContact.getRemark());
+                qwMsg.setNickName(qwExternalContact.getName());
                 qwMsg.setCreateTime(new Date());
                 if (qwMsgMapper.insertQwMsg(qwMsg) > 0) {
                     //发送socket
@@ -235,7 +255,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             //客服发送
             qwFromUser.setId(Long.parseLong(qwMsg.getQwUserId()));
             qwFromUser.setDisplayName(user.getQwUserName());
-            qwFromUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20241231/22a765a96da247d1b83ea94fef438a41.png");
+            qwFromUser.setAvatar(user.getAvatar());
             msg.setFromUser(qwFromUser);
             sendSocket("receiveMsg",JSONObject.toJSONString(msg),user.getAppKey());
         }
@@ -244,12 +264,199 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
     @Override
     public R sendMsg(QwMsgSendParam param) {
-        FsSysConfig config = configUtil.getSysConfig();
-        String domainName = config.getHookUrl();
-        HttpRequest.post(domainName+"/app/qwmsg/sendMsg")
-                .body(JSON.toJSONString(param),"application/json;charset=UTF-8")
-                .execute().body();
-        return R.ok();
+        if (StringUtils.isBlank(param.getContent())) {
+            return R.error("消息内容不能为空");
+        }
+
+        if (Objects.isNull(param.getSessionId())) {
+            return R.error("会话ID不能为空");
+        }
+
+        if (Objects.isNull(param.getMsgType())) {
+            return R.error("消息类型不能为空");
+        }
+
+        // 查询会话
+        QwSession qwSession = qwSessionMapper.selectQwSessionBySessionId(param.getSessionId());
+        if (Objects.isNull(qwSession)) {
+            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)) {
+            return R.error("用户不存在");
+        }
+
+        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());
+        }
+        long sendUserId = listWxWorkResponseDTO.getData().get(0).getUser_id();
+        String msgJson;
+
+        MsgType msgType = MsgType.getMsgType(param.getMsgType());
+        // 发送消息  文本
+        if (MsgType.TEXT == msgType) {
+            WxWorkSendTextMsgDTO textMsgDTO = new WxWorkSendTextMsgDTO();
+            textMsgDTO.setUuid(uuid);
+            textMsgDTO.setSend_userid(sendUserId);
+            textMsgDTO.setIsRoom(false);
+            textMsgDTO.setContent(param.getContent());
+            WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> msgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(textMsgDTO, serverId);
+
+            if (msgRespDTOWxWorkResponseDTO.getErrcode() != 0) {
+                return R.error(msgRespDTOWxWorkResponseDTO.getErrmsg());
+            }
+            msgJson = JSONObject.toJSONString(textMsgDTO);
+        }
+
+        // 图片
+        else if (MsgType.IMAGE == msgType) {
+            WxCdnUploadImgLinkDTO linkDTO = new WxCdnUploadImgLinkDTO();
+            linkDTO.setUuid(uuid);
+            linkDTO.setUrl(param.getContent());
+            WxWorkResponseDTO<WxCdnUploadImgLinkResp> imgLinkResp = wxWorkService.cdnUploadImgLink(linkDTO, serverId);
+            if (imgLinkResp.getErrcode() != 0) {
+                return R.error(imgLinkResp.getErrmsg());
+            }
+            WxCdnUploadImgLinkResp data = imgLinkResp.getData();
+
+            // 发送图片消息
+            WxwSendCDNImgMsgDTO imgMsgDTO = new WxwSendCDNImgMsgDTO();
+            imgMsgDTO.setUuid(uuid);
+            imgMsgDTO.setSend_userid(sendUserId);
+            imgMsgDTO.setIsRoom(false);
+            imgMsgDTO.setCdnkey(data.getCdn_key());
+            imgMsgDTO.setAeskey(data.getAes_key());
+            imgMsgDTO.setMd5(data.getMd5());
+            imgMsgDTO.setFileSize(data.getSize());
+            WxWorkResponseDTO<WxwSendCDNImgMsgRespDTO> imgMsgResp = wxWorkService.SendCDNImgMsg(imgMsgDTO, serverId);
+            if (imgMsgResp.getErrcode() != 0) {
+                return R.error(imgMsgResp.getErrmsg());
+            }
+            msgJson = JSONObject.toJSONString(imgMsgDTO);
+        }
+        // 小程序
+        else if (MsgType.MINI_PROGRAM == msgType) {
+            String pagepath = param.getContent();
+
+            // 查询公司对应小程序配置
+            CompanyMiniapp miniappParams = new CompanyMiniapp();
+            miniappParams.setCompanyId(qwUser.getCompanyId());
+            List<CompanyMiniapp> companyMiniapps = companyMiniappMapper.selectCompanyMiniappList(miniappParams);
+            if (companyMiniapps == null || companyMiniapps.isEmpty()) {
+                return R.error("用户所属销售公司主备小程序未配置");
+            }
+
+            FsCoursePlaySourceConfig config = null;
+            companyMiniapps.sort(Comparator.comparing(CompanyMiniapp::getType));
+            for (CompanyMiniapp companyMiniapp : companyMiniapps) {
+                if (config == null) {
+                    Wrapper<FsCoursePlaySourceConfig> queryWrapper = Wrappers.<FsCoursePlaySourceConfig>lambdaQuery()
+                            .eq(FsCoursePlaySourceConfig::getAppid, companyMiniapp.getAppId())
+                            .eq(FsCoursePlaySourceConfig::getIsDel, 0).last("limit 1");
+                    config = playSourceConfigMapper.selectOne(queryWrapper);
+                }
+            }
+
+            if (config == null) {
+                return R.error("用户所属销售公司主备小程序配置错误");
+            }
+
+            String img = StringUtils.isNotBlank(param.getImage()) ? param.getImage() : config.getImg();
+            WxCdnUploadImgLinkDTO linkDTO = new WxCdnUploadImgLinkDTO();
+            linkDTO.setUuid(uuid);
+            linkDTO.setUrl(img);
+            WxWorkResponseDTO<WxCdnUploadImgLinkResp> imgLinkResp = wxWorkService.cdnUploadImgLink(linkDTO, serverId);
+            if (imgLinkResp.getErrcode() != 0) {
+                return R.error(imgLinkResp.getErrmsg());
+            }
+            WxCdnUploadImgLinkResp data = imgLinkResp.getData();
+
+            // 发送小程序消息
+            WxWorkSendAppMsgDTO appMsgDTO = new WxWorkSendAppMsgDTO();
+            appMsgDTO.setUuid(uuid);
+            appMsgDTO.setSend_userid(sendUserId);
+            appMsgDTO.setDesc(param.getTitle());
+            appMsgDTO.setTitle(config.getName());
+            appMsgDTO.setWeappIconUrl(img);
+            appMsgDTO.setPagepath(pagepath);
+            appMsgDTO.setUsername(config.getOriginalId() + "@app");
+            appMsgDTO.setAppid(config.getAppid());
+            appMsgDTO.setCdnkey(data.getCdn_key());
+            appMsgDTO.setMd5(data.getMd5());
+            appMsgDTO.setAeskey(data.getAes_key());
+            appMsgDTO.setFileSize(data.getSize());
+            appMsgDTO.setIsRoom(false);
+            WxWorkResponseDTO<WxWorkSendAppMsgRespDTO> appMsgResp = wxWorkService.SendAppMsg(appMsgDTO, serverId);
+            if (appMsgResp.getErrcode() != 0) {
+                return R.error(appMsgResp.getErrmsg());
+            }
+
+            JSONObject json = new JSONObject();
+            json.put("appid", config.getAppid());
+            json.put("appName", config.getName());
+            json.put("weappIconUrl", img);
+            json.put("desc", param.getTitle());
+            json.put("pagepath", pagepath);
+            json.put("title", param.getTitle());
+            json.put("thumbnail", img);
+
+            msgJson = json.toJSONString();
+            param.setContent(msgJson);
+        } else {
+            return R.error("暂不支持的消息类型");
+        }
+
+        // 消息保存本地数据库
+        QwMsg qwMsg = new QwMsg();
+        qwMsg.setContent(param.getContent());
+        qwMsg.setSessionId(qwSession.getSessionId());
+        qwMsg.setSendType(2);
+        qwMsg.setCompanyId(qwUser.getCompanyId());
+        qwMsg.setCompanyUserId(qwUser.getCompanyUserId());
+        qwMsg.setMsgType(param.getMsgType());
+        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());
+        qwMsgMapper.insertQwMsg(qwMsg);
+
+        // 组装返回消息结构
+        QwMessageListVO listVO = new QwMessageListVO();
+        QWFromUser qwFromUser = new QWFromUser();
+        qwFromUser.setId(Long.parseLong(qwMsg.getQwUserId()));
+        qwFromUser.setDisplayName(qwUser.getQwUserName());
+        qwFromUser.setAvatar(qwUser.getAvatar());
+        listVO.setType(msgType.getValue());
+        listVO.setStatus("succeed");
+        listVO.setFromUser(qwFromUser);
+        listVO.setSendTime(qwMsg.getCreateTime().getTime());
+        listVO.setId(qwMsg.getMsgId().toString());
+        listVO.setContent(qwMsg.getContent());
+        listVO.setToContactId(String.valueOf(param.getSessionId()));
+        listVO.setAppKey(qwUser.getAppKey());
+        return R.ok().put("data", listVO);
     }
 
     @Override
@@ -288,23 +495,17 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
     @Override
     public List<QwContactListVO> selectQwConversationByUserId(Long userId) {
-        LambdaQueryWrapper<QwSession> sessionWrapper = new LambdaQueryWrapper<>();
-        sessionWrapper.eq(QwSession::getQwUserId, userId);
-        sessionWrapper.orderByDesc(QwSession::getUpdateTime);
-        List<QwSession> qwSessions = qwSessionMapper.selectList(sessionWrapper);
-        if (CollectionUtil.isEmpty(qwSessions)){
-            return Collections.EMPTY_LIST;
+        // 查询会话列表
+        List<QwContactListVO> contactList = qwSessionMapper.selectContactListByQwUserId(userId);
+        if (contactList.isEmpty()) {
+            return new ArrayList<>();
         }
+
         ArrayList<QwContactListVO> qwContactListVOS = new ArrayList<>();
-        for (QwSession qwSession : qwSessions) {
-            QwContactListVO listVO = new QwContactListVO();
-            listVO.setId(userId);
-            listVO.setAvatar(qwSession.getAvatar());
-            listVO.setConversationId(qwSession.getSessionId().toString());
-            listVO.setDisplayName(qwSession.getNickName());
-//            listVO.setIndex(qwSession.getNickName().substring(0, 1));
+        for (QwContactListVO listVO : contactList) {
             LambdaQueryWrapper<QwMsg> lambdaQueryWrapper = new LambdaQueryWrapper<>();
-            lambdaQueryWrapper.eq(QwMsg::getSessionId, qwSession.getSessionId());
+            lambdaQueryWrapper.select(QwMsg.class, q -> !q.getColumn().equals("remark"));
+            lambdaQueryWrapper.eq(QwMsg::getSessionId, Integer.parseInt(listVO.getConversationId()));
             lambdaQueryWrapper.orderByDesc(QwMsg::getMsgId);
             lambdaQueryWrapper.last("limit 1");
             List<QwMsg> qwMsgs = qwMsgMapper.selectList(lambdaQueryWrapper);
@@ -314,6 +515,19 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                 qwContactListVOS.add(listVO);
                 break;
             }
+            QwMsg qwMsg = qwMsgs.get(0);
+            if (qwMsg.getMsgType() == 1) {
+                listVO.setType("text");
+            } else if (qwMsg.getMsgType() == 2) {
+                listVO.setType("image");
+            } else if (qwMsg.getMsgType() == 3) {
+                listVO.setType("emotionDynamic");
+            } else if (qwMsg.getMsgType() == 4) {
+                listVO.setType("voice");
+            } else if (qwMsg.getMsgType() == 5) {
+                listVO.setType("miniprogram");
+            }
+            listVO.setMsgId(qwMsg.getMsgId());
             listVO.setLastContent(qwMsgs.get(0).getContent());
             listVO.setLastSendTime(qwMsgs.get(0).getCreateTime().getTime());
             listVO.setUnread(0);
@@ -325,7 +539,11 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
     @Override
     public List<QwMsg> selectQwMsgBySession(QwSessionParam param) {
         LambdaQueryWrapper<QwMsg> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+        lambdaQueryWrapper.select(QwMsg.class, q -> !q.getColumn().equals("remark"));
         lambdaQueryWrapper.eq(QwMsg::getSessionId, param.getConversationId());
+        if (Objects.nonNull(param.getMsgId())) {
+            lambdaQueryWrapper.gt(QwMsg::getMsgId, param.getMsgId());
+        }
         lambdaQueryWrapper.orderByDesc(QwMsg::getMsgId);
         List<QwMsg> records = qwMsgMapper.selectList(lambdaQueryWrapper);
         return records;
@@ -337,7 +555,12 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         List<QwMessageListVO> qwMessageVOS = new ArrayList<>();
         for (QwMsg record : list) {
             QwMessageListVO listVO = new QwMessageListVO();
-            listVO.setType("text");
+            String type = "text";
+            MsgType msgType = MsgType.getMsgType(record.getMsgType());
+            if (Objects.nonNull(record.getMsgType())) {
+                type = msgType.getValue();
+            }
+            listVO.setType(type);
             listVO.setStatus("succeed");
             QWFromUser qwFromUser = new QWFromUser();
             //用户发送
@@ -348,8 +571,9 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             }else if(record.getSendType() == 2){
                 qwFromUser.setId(Long.parseLong(record.getQwUserId()));
                 qwFromUser.setDisplayName(user.getQwUserName());
-                qwFromUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20241231/22a765a96da247d1b83ea94fef438a41.png");
+                qwFromUser.setAvatar(user.getAvatar());
             }
+            listVO.setExtId(record.getQwExtId());
             listVO.setFromUser(qwFromUser);
             listVO.setSendTime(record.getCreateTime().getTime());
             listVO.setId(record.getMsgId().toString());
@@ -385,4 +609,19 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         listVO.setUnread(0);
         return listVO;
     }
+
+    @Override
+    public List<QwContactVO> contactListByQwUserId(Long qwUserId, String name) {
+        return externalContactMapper.getContactListByQwUserId(qwUserId, name);
+    }
+
+    @Override
+    public List<QwContactVO> groupListByQwUserId(Long qwUserId, String name) {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public QwContactListVO getConversationIdById(Long qwUserid, String id, Boolean isGroup) {
+        return null;
+    }
 }

+ 7 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1624,4 +1624,11 @@ public class QwUserServiceImpl implements IQwUserService
         }
         return ""; // 如果没有扩展名,返回空字符串
     }
+    /**
+     * 根据销售ID查询企微用户列表
+     */
+    @Override
+    public List<QwUserVO> selectQwUserVoListByCompanyUserId(Long companyUserId) {
+        return qwUserMapper.selectQwUserVoListByCompanyUserId(companyUserId);
+    }
 }

+ 10 - 1
fs-service/src/main/java/com/fs/qw/vo/QwContactListVO.java

@@ -14,5 +14,14 @@ public class QwContactListVO {
     private Long roomId;
     private Long lastSendTime;
     private String lastContent;
-
+    // 消息ID
+    private Long msgId;
+    // 消息类型
+    private String type;
+    // 外部联系人ID
+    private String extId;
+    // 是否黑粉
+    private Boolean isBlack;
+    // 是否重粉
+    private Boolean isRepeat;
 }

+ 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;
+}

+ 5 - 0
fs-service/src/main/java/com/fs/qw/vo/QwMessageListVO.java

@@ -1,5 +1,6 @@
 package com.fs.qw.vo;
 
+import com.alibaba.fastjson.annotation.JSONField;
 import com.fs.qw.domain.QWFromUser;
 import com.fs.qw.domain.QwMsg;
 import lombok.Data;
@@ -19,6 +20,10 @@ public class QwMessageListVO {
     private Integer duration; //时长
     private String toContactId;
     private QWFromUser fromUser;
+    private String appKey;
+    @JSONField(serialize = false)
+    private Long companyId;
+    private String extId;
 
     //获取fromUser
     public  QWFromUser getQwFromUser(Long senderId,QwMsg qwMsg){

+ 0 - 22
fs-service/src/main/java/com/fs/statis/param/WatchCourseStatisticsParam.java

@@ -1,22 +0,0 @@
- package com.fs.statis.param;
-
-import lombok.Data;
-
-import java.io.Serializable;
-
- /**
-  * 看课统计参数
-  */
- @Data
- public class WatchCourseStatisticsParam implements Serializable {
-
-     /**
-      * 0 七天
-      * 1 30天
-      */
-     private Integer type;
-     /**
-      * 企微外部联系人id
-      */
-     private Long qwExternalContactId;
- }

+ 0 - 1
fs-service/src/main/java/com/fs/statis/service/impl/StatisticsServiceImpl.java

@@ -21,7 +21,6 @@ import com.fs.qw.service.IQwIpadServerService;
 import com.fs.statis.StatisticsRedisConstant;
 import com.fs.statis.dto.*;
 import com.fs.statis.mapper.ConsumptionBalanceMapper;
-import com.fs.statis.param.WatchCourseStatisticsParam;
 import com.fs.statis.service.IStatisticsService;
 import com.fs.statis.service.utils.TrendDataFiller;
 

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

@@ -20,6 +20,8 @@ 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 revoke_ref_appinfo; //撤回消息的appinfo
 
     // 图片
     private String file_id;
@@ -43,4 +45,16 @@ public class WxWorkMessageDTO {
     String desc;//视频号描述
     String extras;//视频号扩展字段
     String objectNonceId;//视频号扩展字段
+
+    // 小程序
+    private String appid;
+    private String appName;
+    private String username;
+    private String weappIconUrl;
+    private String thumbFileId;
+    private String thumbMD5;
+    private String thumbAESKey;
+    private Integer size;
+    private String title;
+    private String pagepath;
 }

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

@@ -36,7 +36,7 @@ public interface WxWorkService {
      * @param param 参数
      * @return QwWorkResponseDTO
      */
-    WxWorkSendAppMsgRespDTO SendAppMsg(WxWorkSendAppMsgDTO param,Long serverId);
+    WxWorkResponseDTO<WxWorkSendAppMsgRespDTO> SendAppMsg(WxWorkSendAppMsgDTO param,Long serverId);
 
     /**
      * 发送链接消息

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

@@ -69,9 +69,9 @@ public class WxWorkServiceImpl implements WxWorkService {
     }
 
     @Override
-    public WxWorkSendAppMsgRespDTO SendAppMsg(WxWorkSendAppMsgDTO param,Long serverId) {
+    public WxWorkResponseDTO<WxWorkSendAppMsgRespDTO> SendAppMsg(WxWorkSendAppMsgDTO param,Long serverId) {
         String url = getUrl(serverId) + "/SendAppMsg";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkSendAppMsgRespDTO>() {
+        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO<WxWorkSendAppMsgRespDTO>>() {
         });
     }
 

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

@@ -655,6 +655,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <include refid="selectQwExternalContactVo"/>
         where fs_user_id = #{userId} and company_user_id = #{companyUserId}
     </select>
+
+    <select id="getBuyStatusByExtId" resultType="java.lang.Boolean">
+        select IF(u.pay_count > 0, true, false)
+        from qw_external_contact ec
+                 left join fs_user u on ec.fs_user_id = u.user_id
+        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(nullif(qs.first_letter, ''), '未定义')        as `index`,
+        0                                                   as unread,
+        qs.last_send_time                                   as lastSendTime,
+        qs.last_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
+        where qec.qw_user_id = #{qwUserId}
+        <if test="name != null and name != ''">
+            and qec.name like concat('%', #{name}, '%')
+        </if>
+    </select>
+
     <select id="selectQwExternalContactListVONewSys" resultType="com.fs.qw.vo.QwExternalContactVO">
             select ec.*, qu.qw_user_name, qd.dept_name as departmentName
             from qw_external_contact ec

+ 16 - 0
fs-service/src/main/resources/mapper/qw/QwSessionMapper.xml

@@ -46,6 +46,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where session_id = #{sessionId}
     </select>
 
+    <select id="selectContactListByQwUserId" resultType="com.fs.qw.vo.QwContactListVO">
+        select
+            s.qw_user_id as id,
+            s.qw_ext_id extId,
+            s.avatar,
+            s.session_id conversationId,
+            s.nick_name displayName,
+            ec.comment_status isBlack,
+            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
+        left join fs_user u on ec.fs_user_id = u.user_id
+        where s.qw_user_id = #{qwUserId}
+        order by s.update_time desc
+    </select>
+
     <insert id="insertQwSession" parameterType="QwSession" useGeneratedKeys="true" keyProperty="sessionId">
         insert into qw_session
         <trim prefix="(" suffix=")" suffixOverrides=",">

+ 16 - 4
fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

@@ -34,10 +34,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="serverStatus"    column="server_status"    />
         <result property="isAuto"    column="is_auto"    />
         <result property="videoGetStatus"    column="video_get_status"    />
+        <result property="avatar"    column="avatar"    />
     </resultMap>
 
     <sql id="selectQwUserVo">
-        select id,is_auto, video_get_status, qw_user_id,server_id,server_status,ipad_status,config_id,vid,uid,contact_way,app_key, qw_user_name, department, openid, company_id, company_user_id, corp_id, status, is_del, welcome_text, welcome_image, is_send_msg,app_key,qw_hook_id,fastGpt_role_id,login_status,tool_status,login_code_url,version from qw_user
+        select id,is_auto, video_get_status, qw_user_id,server_id,server_status,ipad_status,config_id,vid,uid,contact_way,app_key, qw_user_name, department, openid, company_id, company_user_id, corp_id, status, is_del, welcome_text, welcome_image, is_send_msg,app_key,qw_hook_id,fastGpt_role_id,login_status,tool_status,login_code_url,version,avatar from qw_user
         </sql>
 
     <select id="selectQwUserList" parameterType="QwUser" resultMap="QwUserResult">
@@ -62,6 +63,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="loginCodeUrl != null "> and login_code_url = #{loginCodeUrl}</if>
             <if test="version != null "> and version = #{version}</if>
             <if test="isAuto != null "> and is_auto = #{isAuto}</if>
+            <if test="avatar != null "> and avatar = #{avatar}</if>
         </where>
     </select>
 
@@ -115,6 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="serverStatus != null">server_status,</if>
             <if test="isAuto != null">is_auto,</if>
             <if test="videoGetStatus != null">video_get_status,</if>
+            <if test="avatar != null">avatar,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="qwUserId != null">#{qwUserId},</if>
@@ -143,6 +146,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="serverId != null">#{serverId},</if>
             <if test="isAuto != null">#{isAuto},</if>
             <if test="videoGetStatus != null">#{videoGetStatus},</if>
+            <if test="avatar != null">#{avatar},</if>
          </trim>
     </insert>
 
@@ -177,6 +181,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="serverStatus != null">server_status = #{serverStatus},</if>
             <if test="isAuto != null">is_auto = #{isAuto},</if>
             <if test="videoGetStatus != null">video_get_status = #{videoGetStatus},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
         </trim>
         where id = #{id}
     </update>
@@ -269,9 +274,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             (qw_user_id = #{query.qwUserId} AND corp_id = #{query.corpId})
         </foreach>
     </select>
-    <select id="selectQwCompanyListOptionsVOBySys" resultType="com.fs.qw.vo.QwOptionsVO">
-        select corp_id as dictValue,corp_name as dictLabel from qw_company
-    </select>
 
     <select id="selectQwUserListVOByCompanyIdAndCorpIdAndNickName" resultType="com.fs.qw.vo.QwUserVO">
         select
@@ -304,4 +306,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         SELECT dept_id FROM sub_dept
     </select>
 
+    <select id="selectQwUserVoListByCompanyUserId" resultType="com.fs.qw.vo.QwUserVO">
+        select
+            qu.*,
+            qc.corp_name
+        from qw_user qu
+                 inner join qw_company qc on qu.corp_id = qc.corp_id
+        where qu.is_del = 0 and qu.app_key is not null
+          and qu.company_user_id = #{companyUserId}
+    </select>
+
 </mapper>