cgp 1 dzień temu
rodzic
commit
5bcd4739ab
27 zmienionych plików z 565 dodań i 153 usunięć
  1. 17 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyController.java
  2. 43 2
      fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java
  3. 12 0
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  4. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  5. 4 0
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  6. 9 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  7. 12 0
      fs-service/src/main/java/com/fs/company/vo/SimpleCompanyVo.java
  8. 16 0
      fs-service/src/main/java/com/fs/qw/dto/QwUserQueryDto.java
  9. 30 0
      fs-service/src/main/java/com/fs/qw/dto/SearchMsgRequest.java
  10. 8 0
      fs-service/src/main/java/com/fs/qw/mapper/QwConversationMessageMapper.java
  11. 1 3
      fs-service/src/main/java/com/fs/qw/mapper/QwConversationParticipantMapper.java
  12. 6 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  13. 13 2
      fs-service/src/main/java/com/fs/qw/service/ICorporateWeChatSpaceService.java
  14. 7 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  15. 34 53
      fs-service/src/main/java/com/fs/qw/service/impl/ConversationSyncService.java
  16. 209 71
      fs-service/src/main/java/com/fs/qw/service/impl/ICorporateWeChatSpaceServiceImpl.java
  17. 36 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwProgramInvoker.java
  18. 10 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  19. 5 5
      fs-service/src/main/java/com/fs/qw/utils/WeChatSpaceUtil.java
  20. 1 0
      fs-service/src/main/java/com/fs/qw/vo/QwOptionsVO.java
  21. 16 0
      fs-service/src/main/java/com/fs/qw/vo/QwUserListVo.java
  22. 15 0
      fs-service/src/main/java/com/fs/qw/vo/SearchResultVO.java
  23. 7 0
      fs-service/src/main/resources/mapper/company/CompanyMapper.xml
  24. 14 6
      fs-service/src/main/resources/mapper/qw/QwConversationMessageMapper.xml
  25. 4 7
      fs-service/src/main/resources/mapper/qw/QwConversationParticipantMapper.xml
  26. 4 4
      fs-service/src/main/resources/mapper/qw/QwConversationSyncStateMapper.xml
  27. 30 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

+ 17 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyController.java

@@ -22,6 +22,7 @@ import com.fs.company.service.*;
 import com.fs.company.vo.CompanyCrmVO;
 import com.fs.company.vo.CompanyVO;
 import com.fs.company.vo.CompanyVoiceCallerListVO;
+import com.fs.company.vo.SimpleCompanyVo;
 import com.fs.core.utils.OrderCodeUtils;
 import com.fs.course.config.CourseConfig;
 import com.fs.framework.web.service.TokenService;
@@ -33,6 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -319,4 +321,19 @@ public class CompanyController extends BaseController
     {
         return companyService.saveSidebarCompanyImage(param);
     }
+
+    //根据companyIds查询companyList
+    @PostMapping("/queryCompanyListByCompanyIds")
+    public R queryCompanyListByCompanyIds(@RequestBody List<Long> companyIds) {
+        List<Company> companyList = companyService.queryCompanyListByCompanyIds(companyIds);
+        //只返回 companyId和companyName
+        List<SimpleCompanyVo> simpleCompanyList = new ArrayList<>();
+        companyList.forEach(company -> {
+            SimpleCompanyVo simpleCompany = new SimpleCompanyVo();
+            simpleCompany.setCompanyId(company.getCompanyId());
+            simpleCompany.setCompanyName(company.getCompanyName());
+            simpleCompanyList.add(simpleCompany);
+        });
+        return R.ok().put("companyList", simpleCompanyList);
+    }
 }

+ 43 - 2
fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java

@@ -4,22 +4,27 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.exception.CustomException;
+import com.fs.qw.dto.SearchMsgRequest;
 import com.fs.qw.service.ICorporateWeChatSpaceService;
 import com.fs.qw.vo.QwSessionConfigVo;
+import com.fs.qw.vo.SearchResultVO;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.web.bind.annotation.*;
 
 /**
  * 企业微信专区-统一前端 API 接口
  * */
+@Slf4j
+@RequiredArgsConstructor
 @RestController
 @RequestMapping("/weChatSpace")
-@RequiredArgsConstructor
 public class CorporateWeChatSpaceController extends BaseController {
 
     private final ICorporateWeChatSpaceService weChatSpaceService;
 
-    // 企业微信会话专区中转接口
+    // 会话信息列表
     @GetMapping("/conversations")
     public JSONObject getConversations(
             @RequestParam(defaultValue = "100") long limit,
@@ -36,6 +41,42 @@ public class CorporateWeChatSpaceController extends BaseController {
         return weChatSpaceService.fetchConversations(limit, timeout, cursor, customerId,staffUserId,corpid);
     }
 
+    /**
+     * 关键词搜索会话消息
+     */
+    @PostMapping("/searchMsg")
+    public AjaxResult searchMsg(@RequestBody SearchMsgRequest request) {
+        // 参数校验
+        if (StringUtils.isBlank(request.getCorpId())) {
+            return AjaxResult.error("企业ID不能为空");
+        }
+        if (StringUtils.isBlank(request.getQueryWord()) || request.getQueryWord().length() < 2) {
+            return AjaxResult.error("关键词至少2个字符");
+        }
+        // 若指定了单聊,则员工ID和客户ID不能为空
+        if (request.getChatType() != null && request.getChatType() == 1) {
+            if (StringUtils.isBlank(request.getStaffUserId()) || StringUtils.isBlank(request.getCustomerId())) {
+                return AjaxResult.error("单聊搜索必须提供员工ID和客户ID");
+            }
+        }
+        // 若指定了群聊,则群聊ID不能为空
+        if (request.getChatType() != null && request.getChatType() == 2) {
+            if (StringUtils.isBlank(request.getChatId())) {
+                return AjaxResult.error("群聊搜索必须提供群聊ID");
+            }
+        }
+
+        try {
+            SearchResultVO result = weChatSpaceService.searchMsg(request);
+            return AjaxResult.success(result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            log.error("搜索异常", e);
+            return AjaxResult.error("搜索失败:" + e.getMessage());
+        }
+    }
+
 
     // agentConfig 签名
     @GetMapping("/getAgentConfigSignature")

+ 12 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -25,6 +25,7 @@ import com.fs.framework.manager.AsyncManager;
 import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.QwUserQueryDto;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.param.*;
@@ -32,6 +33,7 @@ import com.fs.qw.service.IQwDeptService;
 import com.fs.qw.service.IQwExternalContactTransferCompanyAuditService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.vo.QwOptionsVO;
+import com.fs.qw.vo.QwUserListVo;
 import com.fs.qw.vo.QwUserVO;
 import com.fs.qw.vo.UpdateSendTypeVo;
 import com.fs.qwApi.domain.QwExternalContactAllListResult;
@@ -1000,4 +1002,14 @@ public class QwUserController extends BaseController {
     {
         return qwUserService.updateQwUserFastGptRoleStatusById(id);
     }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/selectQwUserListByCondition")
+    public R selectQwUserListByCondition(@RequestBody QwUserQueryDto query) {
+        PageHelper.startPage(query.getPageNum(), query.getPageSize());
+        List<QwUserListVo> qwUserList = qwUserService.selectQwUserListByCondition(query);
+        return R.ok().put("data", new PageInfo<>(qwUserList));
+    }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -256,4 +256,6 @@ public interface CompanyMapper
     List<Company> getCompanyList(@Param("corpId") String corpId);
 
     String getGateWayList(@Param("companyId") Long companyId);
+
+    List<Company> queryCompanyListByCompanyIds(@Param("companyIds") List<Long> companyIds);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyService.java

@@ -208,4 +208,8 @@ public interface ICompanyService
     R saveSidebarCompanyImage(SidebarCompanyImageParam param);
 
 
+    /**
+     * 根据公司id集合查询多个公司
+     * */
+    List<Company> queryCompanyListByCompanyIds(List<Long> companyIds);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -2031,4 +2031,13 @@ public class CompanyServiceImpl implements ICompanyService
         return R.ok();
     }
 
+    @Override
+    public List<Company> queryCompanyListByCompanyIds(List<Long> companyIds) {
+        List<Company> companyList =companyMapper.queryCompanyListByCompanyIds(companyIds);
+        if (CollectionUtils.isEmpty(companyList)){
+            return Collections.emptyList();
+        }
+        return companyList;
+    }
+
 }

+ 12 - 0
fs-service/src/main/java/com/fs/company/vo/SimpleCompanyVo.java

@@ -0,0 +1,12 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+@Data
+public class SimpleCompanyVo {
+    /** ID */
+    private Long companyId;
+
+    /** 企业名 */
+    private String companyName;
+}

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

@@ -0,0 +1,16 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+
+@Data
+public class QwUserQueryDto {
+    //company_user表的主键id
+    private Long userId;
+
+    //企微用户昵称(qw_user表的qw_user_name字段)
+    private String qwUserName;
+
+    //分页相关
+    private Integer pageNum;
+    private Integer pageSize;
+}

+ 30 - 0
fs-service/src/main/java/com/fs/qw/dto/SearchMsgRequest.java

@@ -0,0 +1,30 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class SearchMsgRequest {
+    // 必填
+    private String corpId;           // 企业ID
+    private String queryWord;        // 搜索关键词,至少2个字符
+
+    // 搜索范围(可选)
+    private Integer chatType;        // 1:单聊 2:群聊
+    private String staffUserId;      // 员工userid(chatType=1时必填其一)
+    private String customerId;       // 客户external_userid(chatType=1时必填其一)
+    private String chatId;           // 群聊ID(chatType=2时必填)
+    private List<Integer> msgTypeList; // 消息类型列表,如[1,8]
+    private String senderUserId;     // 指定发送者(员工)
+
+    // 时间范围
+    private Long startTime;          // 起始时间戳(秒)
+    private Long endTime;            // 结束时间戳(秒),范围不超过31天
+
+    // 分页
+    private Integer limit = 50;      // 每页数量,最大100
+    private String cursor;           // 翻页游标
+
+    // 可选优化
+    private Boolean skipStopWords;   // 是否跳过停用词,默认false
+}

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

@@ -57,4 +57,12 @@ public interface QwConversationMessageMapper {
                                                         @Param("endTime") Long endTime,
                                                         @Param("offset") Integer offset,
                                                         @Param("limit") Integer limit);
+
+    /**
+     * 批量根据msgid查询消息
+     * @param corpId 企业ID
+     * @param msgIds msgid列表
+     * @return 消息列表
+     */
+    List<QwConversationMessage> selectByMsgIds(@Param("corpId") String corpId, @Param("msgIds") List<String> msgIds);
 }

+ 1 - 3
fs-service/src/main/java/com/fs/qw/mapper/QwConversationParticipantMapper.java

@@ -16,7 +16,5 @@ public interface QwConversationParticipantMapper {
 
     List<QwConversationParticipant> selectByUser(@Param("corpId") String corpId, @Param("userType") Integer userType, @Param("userId") String userId);
 
-    List<QwConversationParticipant> selectByMsgidAndType(@Param("corpId") String corpId,
-                                                         @Param("msgid") String msgid,
-                                                         @Param("participantType") Integer participantType);
+    List<QwConversationParticipant> selectByMsgidAndType(@Param("corpId") String corpId, @Param("msgid") String msgid, @Param("participantType") Integer participantType);
 }

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

@@ -7,6 +7,7 @@ import com.fs.qw.domain.QwWorkTask;
 import com.fs.qw.dto.QwUserByToolDTO;
 import com.fs.qw.dto.QwUserDTO;
 import com.fs.qw.dto.QwUserKeyDTO;
+import com.fs.qw.dto.QwUserQueryDto;
 import com.fs.qw.param.*;
 import com.fs.qw.vo.*;
 import com.fs.qw.vo.sidebar.ExternalContactQwUserVO;
@@ -541,6 +542,11 @@ public interface QwUserMapper extends BaseMapper<QwUser>
      * */
     List<QwUser> selectQwUserListByIds(List<Long> ids);
 
+    /**
+     * 根据条件动态查询企微用户列表(连表查询)
+     */
+    List<QwUserListVo> selectQwUserListByCondition(@Param("query") QwUserQueryDto query);
+
     @Select("select * from qw_user where company_user_id =#{companyUserId} and is_del = 0 order by id asc limit 1 ")
     QwUser selectQwUserconvertSopVoice(@Param("companyUserId") Long companyUserID);
 }

+ 13 - 2
fs-service/src/main/java/com/fs/qw/service/ICorporateWeChatSpaceService.java

@@ -1,7 +1,10 @@
 package com.fs.qw.service;
 
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.exception.CustomException;
+import com.fs.qw.dto.SearchMsgRequest;
 import com.fs.qw.vo.QwSessionConfigVo;
+import com.fs.qw.vo.SearchResultVO;
 
 import java.util.List;
 
@@ -24,10 +27,18 @@ public interface ICorporateWeChatSpaceService {
     /**
      * 获取所有企业微信专区会话配置列表
      */
-    public List<QwSessionConfigVo> getQwSessionConfigList();
+    List<QwSessionConfigVo> getQwSessionConfigList();
 
     /**
      * 根据企业ID获取单个配置
      */
-    public QwSessionConfigVo getQwSessionConfigByCorpid(String corpid);
+    QwSessionConfigVo getQwSessionConfigByCorpid(String corpid);
+
+    /**
+     * 关键词搜索会话消息
+     * @param request 搜索参数(已由Controller完成基本校验)
+     * @return 搜索结果VO
+     * @throws CustomException 业务异常(如配置缺失)
+     */
+    SearchResultVO searchMsg(SearchMsgRequest request);
 }

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

@@ -4,6 +4,7 @@ import com.fs.common.core.domain.R;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.domain.QwWorkTask;
 import com.fs.qw.dto.QwUserKeyDTO;
+import com.fs.qw.dto.QwUserQueryDto;
 import com.fs.qw.param.*;
 import com.fs.qw.vo.*;
 import com.fs.sop.domain.QwSop;
@@ -237,4 +238,10 @@ public interface IQwUserService
      *
      * */
     public List<QwUser> selectQwUserListByIds(List<Long> ids);
+
+
+    /**
+     * 根据条件动态查询企微用户列表
+     */
+    List<QwUserListVo> selectQwUserListByCondition(QwUserQueryDto query);
 }

+ 34 - 53
fs-service/src/main/java/com/fs/qw/service/impl/ConversationSyncService.java

@@ -1,8 +1,8 @@
 package com.fs.qw.service.impl;
 
-import cn.hutool.json.JSONArray;
-import cn.hutool.json.JSONObject;
-import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.utils.DateUtils;
 import com.fs.qw.domain.QwConversationMessage;
 import com.fs.qw.domain.QwConversationParticipant;
@@ -12,14 +12,12 @@ import com.fs.qw.mapper.QwConversationParticipantMapper;
 import com.fs.qw.mapper.QwConversationSyncStateMapper;
 import com.fs.qw.service.ICorporateWeChatSpaceService;
 import com.fs.qw.utils.WeChatSpaceDecryptUtil;
-import com.fs.qw.utils.WeChatSpaceUtil;
 import com.fs.qw.vo.QwSessionConfigVo;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.client.RestTemplate;
 
 import java.util.Date;
 
@@ -40,9 +38,7 @@ public class ConversationSyncService {
     private QwConversationSyncStateMapper stateMapper;
 
     @Autowired
-    private WeChatSpaceUtil weChatSpaceUtil;
-
-    private final RestTemplate restTemplate = new RestTemplate();
+    private QwProgramInvoker programInvoker;
 
     /**
      * 全量同步指定企业的会话记录(一直拉取直到 has_more=0)
@@ -50,7 +46,7 @@ public class ConversationSyncService {
      * - 最大页数 1000
      * - 连续空页 10 次退出
      * - 游标不变检测
-     * - 全局超时 30 分钟
+     * - 全局超时 60 分钟
      */
     @Transactional(rollbackFor = Exception.class)
     public void syncConversationsForCorp(String corpId) {
@@ -85,15 +81,15 @@ public class ConversationSyncService {
         int emptyPageCount = 0;
         String lastCursor = null;
         long startTime = System.currentTimeMillis();
-        long timeoutMillis = 30 * 60 * 1000L; // 30分钟
-        boolean syncError = false; // 标记是否发生错误
+        long timeoutMillis = 60 * 60 * 1000L; // 60分钟
+        boolean syncError = false;
 
         while (hasMore) {
             currentPage++;
 
             // 超时检查
             if (System.currentTimeMillis() - startTime > timeoutMillis) {
-                log.warn("企业 {} 同步超时(>30分钟),强制退出", corpId);
+                log.warn("企业 {} 同步超时(>60分钟),强制退出", corpId);
                 syncError = true;
                 break;
             }
@@ -107,25 +103,30 @@ public class ConversationSyncService {
             // 构建请求
             JSONObject requestData = new JSONObject();
             if (StringUtils.isNotBlank(nextCursor)) {
-                requestData.set("cursor", nextCursor);
+                requestData.put("cursor", nextCursor);
             }
-            requestData.set("limit", 1000);
-
-            JSONObject response = callSyncProgram(config, abilityId, requestData);
-            if (response == null || response.getInt("errcode") != 0) {
+            requestData.put("limit", 1000);
+
+            JSONObject response = programInvoker.callProgram(
+                    config.getCorpid(),
+                    config.getProgramId(),
+                    abilityId,
+                    requestData
+            );
+            if (response == null || response.getIntValue("errcode") != 0) {
                 log.error("拉取会话失败,corpId={}, response={}", corpId, response);
                 syncError = true;
                 break;
             }
 
-            String responseDataStr = response.getStr("response_data");
+            String responseDataStr = response.getString("response_data");
             if (StringUtils.isBlank(responseDataStr)) {
                 log.error("response_data 为空,corpId={}", corpId);
                 syncError = true;
                 break;
             }
-            JSONObject respData = JSONUtil.parseObj(responseDataStr);
-            if (respData.getInt("errcode") != 0) {
+            JSONObject respData = JSON.parseObject(responseDataStr);
+            if (respData.getIntValue("errcode") != 0) {
                 log.error("专区内部错误,corpId={}, respData={}", corpId, respData);
                 syncError = true;
                 break;
@@ -149,8 +150,8 @@ public class ConversationSyncService {
                 log.info("企业 {} 第 {} 页保存 {} 条,累计 {} 条", corpId, currentPage, inserted, totalInserted);
             }
 
-            hasMore = respData.getInt("has_more") == 1;
-            String newCursor = respData.getStr("next_cursor");
+            hasMore = respData.getIntValue("has_more") == 1;
+            String newCursor = respData.getString("next_cursor");
 
             if (StringUtils.isNotBlank(lastCursor) && StringUtils.isNotBlank(newCursor) && lastCursor.equals(newCursor)) {
                 log.warn("企业 {} 游标连续两次相同 [{}],强制退出", corpId, newCursor);
@@ -160,15 +161,14 @@ public class ConversationSyncService {
             lastCursor = newCursor;
             nextCursor = newCursor;
 
-            // 每页成功拉取后更新 cursor(仅更新,不涉及 last_sync_time)
+            // 每页成功拉取后更新 cursor
             updateCursor(corpId, nextCursor, config.getProgramId(), abilityId);
         }
 
-        // 只有成功完成(没有发生错误)才更新最后同步时间
+        // 只有成功完成才更新最后同步时间
         if (!syncError) {
             int updated = stateMapper.updateSyncTime(corpId, DateUtils.getNowDate());
             if (updated == 0) {
-                // 没有记录则插入一条
                 QwConversationSyncState newState = new QwConversationSyncState();
                 newState.setCorpId(corpId);
                 newState.setProgramId(config.getProgramId());
@@ -183,30 +183,11 @@ public class ConversationSyncService {
         }
     }
 
-    private JSONObject callSyncProgram(QwSessionConfigVo config, String abilityId, JSONObject requestData) {
-        String accessToken = weChatSpaceUtil.getAccessToken(config.getCorpid(), config.getAgentSecret());
-        String url = "https://qyapi.weixin.qq.com/cgi-bin/chatdata/sync_call_program?access_token=" + accessToken;
-        JSONObject requestBody = new JSONObject();
-        requestBody.set("program_id", config.getProgramId());
-        requestBody.set("ability_id", abilityId);
-        requestBody.set("request_data", JSONUtil.toJsonStr(requestData));
-        log.info("调用专区接口 - URL: {}", url);
-        log.info("调用专区接口 - 请求体: {}", JSONUtil.toJsonStr(requestBody));
-        try {
-            JSONObject response = restTemplate.postForObject(url, requestBody, JSONObject.class);
-            //log.info("调用专区接口 - 响应: {}", response);
-            return response;
-        } catch (Exception e) {
-            log.error("调用专区接口异常", e);
-            return null;
-        }
-    }
-
     private int saveMessagesToDb(String corpId, JSONArray msgList, String privateKeyPem) {
         int insertCount = 0;
         for (Object obj : msgList) {
             JSONObject msg = (JSONObject) obj;
-            String msgid = msg.getStr("msgid");
+            String msgid = msg.getString("msgid");
             if (messageMapper.existsByCorpIdAndMsgid(corpId, msgid) > 0) {
                 continue;
             }
@@ -216,7 +197,7 @@ public class ConversationSyncService {
                 log.warn("消息 {} 缺少 service_encrypt_info,跳过", msgid);
                 continue;
             }
-            String encryptedKey = encryptInfo.getStr("encrypted_secret_key");
+            String encryptedKey = encryptInfo.getString("encrypted_secret_key");
             if (StringUtils.isBlank(encryptedKey)) {
                 log.warn("消息 {} 缺少 encrypted_secret_key,跳过", msgid);
                 continue;
@@ -238,16 +219,16 @@ public class ConversationSyncService {
                 log.warn("消息 {} 缺少 sender,跳过", msgid);
                 continue;
             }
-            record.setSenderType(sender.getInt("type"));
-            record.setSenderId(sender.getStr("id"));
-            record.setChatid(msg.getStr("chatid"));
-            record.setMsgtype(msg.getInt("msgtype"));
+            record.setSenderType(sender.getIntValue("type"));
+            record.setSenderId(sender.getString("id"));
+            record.setChatid(msg.getString("chatid"));
+            record.setMsgtype(msg.getIntValue("msgtype"));
             record.setSendTime(msg.getLong("send_time"));
             record.setEncryptedSecretKey(encryptedKey);
             record.setSecretKey(secretKey);
-            record.setPublicKeyVer(encryptInfo.getInt("public_key_ver"));
+            record.setPublicKeyVer(encryptInfo.getIntValue("public_key_ver"));
             if (msg.containsKey("extra_info")) {
-                record.setExtraInfo(JSONUtil.toJsonStr(msg.getJSONObject("extra_info")));
+                record.setExtraInfo(JSON.toJSONString(msg.getJSONObject("extra_info")));
             }
             messageMapper.insert(record);
 
@@ -256,7 +237,7 @@ public class ConversationSyncService {
             if (receivers != null) {
                 for (Object rObj : receivers) {
                     JSONObject r = (JSONObject) rObj;
-                    insertParticipant(corpId, msgid, 2, r.getInt("type"), r.getStr("id"));
+                    insertParticipant(corpId, msgid, 2, r.getIntValue("type"), r.getString("id"));
                 }
             }
             insertCount++;

+ 209 - 71
fs-service/src/main/java/com/fs/qw/service/impl/ICorporateWeChatSpaceServiceImpl.java

@@ -7,12 +7,13 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.exception.CustomException;
 import com.fs.qw.domain.QwConversationMessage;
 import com.fs.qw.domain.QwConversationParticipant;
+import com.fs.qw.dto.SearchMsgRequest;
 import com.fs.qw.mapper.QwConversationMessageMapper;
 import com.fs.qw.mapper.QwConversationParticipantMapper;
 import com.fs.qw.service.ICorporateWeChatSpaceService;
-import com.fs.qw.utils.WeChatSpaceDecryptUtil;
 import com.fs.qw.utils.WeChatSpaceUtil;
 import com.fs.qw.vo.QwSessionConfigVo;
+import com.fs.qw.vo.SearchResultVO;
 import com.fs.system.service.ISysConfigService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -24,6 +25,7 @@ import lombok.RequiredArgsConstructor;
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
@@ -45,6 +47,9 @@ public class ICorporateWeChatSpaceServiceImpl implements ICorporateWeChatSpaceSe
     @Autowired
     private WeChatSpaceUtil weChatSpaceUtil;
 
+    @Autowired
+    private QwProgramInvoker qwProgramInvoker;
+
     private final RestTemplate restTemplate = new RestTemplate();
 
     private final ConcurrentHashMap<String, String> consumedCodes = new ConcurrentHashMap<>();
@@ -118,7 +123,7 @@ public class ICorporateWeChatSpaceServiceImpl implements ICorporateWeChatSpaceSe
     @Override
     public JSONObject getAgentConfigSignature(String url,String corpid) {
         QwSessionConfigVo qwSessionConfig = getQwSessionConfigByCorpid(corpid);
-        return weChatSpaceUtil.generateAgentConfigSignature(qwSessionConfig.getCorpid(), qwSessionConfig.getAgentSecret(), qwSessionConfig.getAgentid(), url);
+        return weChatSpaceUtil.generateAgentConfigSignature(qwSessionConfig.getCorpid(), qwSessionConfig.getAgentid(), url);
     }
 
     @Override
@@ -136,7 +141,7 @@ public class ICorporateWeChatSpaceServiceImpl implements ICorporateWeChatSpaceSe
         }
         QwSessionConfigVo qwSessionConfig = getQwSessionConfigByCorpid(corpid);
         try {
-            String accessToken = weChatSpaceUtil.getAccessToken(qwSessionConfig.getCorpid(), qwSessionConfig.getAgentSecret());
+            String accessToken = weChatSpaceUtil.getAccessToken(qwSessionConfig.getCorpid());
             String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token="
                     + accessToken + "&code=" + code;
             JSONObject resp = restTemplate.getForObject(url, JSONObject.class);
@@ -192,81 +197,214 @@ public class ICorporateWeChatSpaceServiceImpl implements ICorporateWeChatSpaceSe
                 .orElseThrow(() -> new CustomException("未找到corpid为 " + corpid + " 的配置"));
     }
 
+    // ========== 关键词搜索 ==========
+    @Override
+    public SearchResultVO searchMsg(SearchMsgRequest request) {
+        // 1. 获取企业配置,并找到 search_msg 能力ID
+        QwSessionConfigVo config = getQwSessionConfigByCorpid(request.getCorpId());
+        String abilityId = config.getAbilityIds().stream()
+                .filter(item -> "invokeSearchMsg".equals(item.getKey()))
+                .map(QwSessionConfigVo.AbilityItem::getValue)
+                .findFirst()
+                .orElseThrow(() -> new CustomException("未配置 invokeSearchMsg 能力,请在企微后台添加"));
+
+        // 2. 构造请求数据
+        JSONObject requestData = new JSONObject();
+        requestData.put("query_word", request.getQueryWord());
+
+        // 构造 chat_info
+        if (request.getChatType() != null) {
+            JSONObject chatInfo = new JSONObject();
+            chatInfo.put("chat_type", request.getChatType());
+
+            if (request.getChatType() == 1) { // 单聊
+                JSONArray idList = new JSONArray();
+                idList.add(new JSONObject().fluentPut("open_userid", request.getStaffUserId()));
+                idList.add(new JSONObject().fluentPut("external_userid", request.getCustomerId()));
+                chatInfo.put("id_list", idList);
+            } else if (request.getChatType() == 2) { // 群聊
+                chatInfo.put("chat_id", request.getChatId());
+            }
 
-    private JSONArray processMessages(JSONArray msgList, String customerId, String staffUserId, QwSessionConfigVo qwConfig) {
-        return msgList.parallelStream()
-                .map(obj -> (JSONObject) obj)
-                .filter(msg -> isMessageRelatedToUsers(msg, customerId, staffUserId))
-                .map(msg -> decryptAndFormatMessage(msg, qwConfig))
-                .filter(Objects::nonNull)
-                .collect(Collectors.toCollection(JSONArray::new));
-    }
+            if (request.getMsgTypeList() != null && !request.getMsgTypeList().isEmpty()) {
+                chatInfo.put("msg_type_list", request.getMsgTypeList());
+            }
+            if (StringUtils.isNotBlank(request.getSenderUserId())) {
+                chatInfo.put("sender", new JSONObject().fluentPut("open_userid", request.getSenderUserId()));
+            }
+            requestData.put("chat_info", chatInfo);
+        }
+
+        if (request.getStartTime() != null) requestData.put("start_time", request.getStartTime());
+        if (request.getEndTime() != null) requestData.put("end_time", request.getEndTime());
+        if (request.getSkipStopWords() != null) requestData.put("skip_stop_words", request.getSkipStopWords());
+        if (request.getLimit() != null) requestData.put("limit", Math.min(request.getLimit(), 100));
+        if (StringUtils.isNotBlank(request.getCursor())) requestData.put("cursor", request.getCursor());
+
+        // 3. 调用企微 sync_call_program 接口
+        JSONObject response = qwProgramInvoker.callProgram(
+                config.getCorpid(),
+                config.getProgramId(),
+                abilityId,
+                requestData
+        );
+        if (response == null || response.getIntValue("errcode") != 0) {
+            String errMsg = response != null ? response.getString("errmsg") : "网络错误";
+            log.error("调用 search_msg 失败: {}", errMsg);
+            throw new CustomException("搜索失败:" + errMsg);
+        }
 
-    private boolean isMessageRelatedToUsers(JSONObject msg, String customerId, String staffUserId) {
-        // 如果都为空,则不过滤,返回全部
-        if ((customerId == null || customerId.isEmpty()) && (staffUserId == null || staffUserId.isEmpty())) {
-            return true;
+        String responseDataStr = response.getString("response_data");
+        if (StringUtils.isBlank(responseDataStr)) {
+            throw new CustomException("专区返回数据为空");
         }
-        boolean hasCustomer = (customerId == null || customerId.isEmpty());
-        boolean hasStaff = (staffUserId == null || staffUserId.isEmpty());
-
-        JSONObject sender = msg.getJSONObject("sender");
-        JSONArray receivers = msg.getJSONArray("receiver_list");
-
-        // 检查发送者
-        if (sender != null) {
-            String senderId = sender.getString("id");
-            int senderType = sender.getIntValue("type");
-            if (!hasCustomer && senderType == 2 && customerId.equals(senderId)) hasCustomer = true;
-            if (!hasStaff && senderType == 1 && staffUserId.equals(senderId)) hasStaff = true;
+        JSONObject respData = JSON.parseObject(responseDataStr);
+        if (respData.getIntValue("errcode") != 0) {
+            throw new CustomException("搜索失败:" + respData.getString("errmsg"));
         }
-        if (hasCustomer && hasStaff) return true;
-
-        // 检查接收者
-        if (receivers != null) {
-            for (int i = 0; i < receivers.size(); i++) {
-                JSONObject recv = receivers.getJSONObject(i);
-                String recvId = recv.getString("id");
-                int recvType = recv.getIntValue("type");
-                if (!hasCustomer && recvType == 2 && customerId.equals(recvId)) hasCustomer = true;
-                if (!hasStaff && recvType == 1 && staffUserId.equals(recvId)) hasStaff = true;
-                if (hasCustomer && hasStaff) return true;
-            }
+
+        // 4. 提取 msgid 列表
+        JSONArray msgListArray = respData.getJSONArray("msg_list");
+        if (msgListArray == null || msgListArray.isEmpty()) {
+            return new SearchResultVO(new JSONArray(), 0, "");
+        }
+
+        List<String> msgIds = new ArrayList<>();
+        for (Object obj : msgListArray) {
+            JSONObject item = (JSONObject) obj;
+            msgIds.add(item.getString("msgid"));
         }
-        return hasCustomer && hasStaff;
+
+        // 5. 从本地数据库批量查询消息详情
+        List<QwConversationMessage> messages = messageMapper.selectByMsgIds(request.getCorpId(), msgIds);
+        if (messages.isEmpty()) {
+            return new SearchResultVO(new JSONArray(), respData.getIntValue("has_more"), respData.getString("next_cursor"));
+        }
+
+        // 6. 构建返回数据
+        JSONArray resultList = new JSONArray();
+        for (QwConversationMessage msg : messages) {
+            JSONObject item = buildMessageJson(msg, request.getCorpId());
+            resultList.add(item);
+        }
+
+        return new SearchResultVO(resultList, respData.getIntValue("has_more"), respData.getString("next_cursor"));
     }
 
-    private JSONObject decryptAndFormatMessage(JSONObject msg, QwSessionConfigVo qwConfig) {
-        JSONObject result = new JSONObject();
-        try {
-            JSONObject encryptInfo = msg.getJSONObject("service_encrypt_info");
-            if (encryptInfo == null) return null;
-            String encryptedKey = encryptInfo.getString("encrypted_secret_key");
-            if (encryptedKey == null) return null;
-
-            // 解密得到 secretKey
-            String secretKey = WeChatSpaceDecryptUtil.decryptSecretKey(encryptedKey, qwConfig.getPrivateKey());
-
-            // 复制需要返回的字段
-            result.put("msgid", msg.getString("msgid"));
-            result.put("secretKey", secretKey);
-            result.put("sender", msg.get("sender"));
-            result.put("receiver_list", msg.get("receiver_list"));
-            result.put("msgtype", msg.getInteger("msgtype"));
-
-            Long sendTime = msg.getLong("send_time");
-            if (sendTime != null) {
-                String formattedTime = Instant.ofEpochSecond(sendTime)
-                        .atZone(ZoneId.systemDefault())
-                        .toLocalDateTime()
-                        .format(DATE_TIME_FORMATTER);
-                result.put("send_time_str", formattedTime);
-                result.put("send_time", sendTime);
-            }
-            return result;
-        } catch (Exception e) {
-            log.error("解密消息失败, msgid: {}", msg.getString("msgid"), e);
-            return null;
+    /**
+     * 构建前端需要的消息JSON
+     */
+    private JSONObject buildMessageJson(QwConversationMessage msg, String corpId) {
+        JSONObject item = new JSONObject();
+        item.put("msgid", msg.getMsgid());
+        item.put("secretKey", msg.getSecretKey());
+
+        // 发送者
+        JSONObject sender = new JSONObject();
+        sender.put("type", msg.getSenderType());
+        sender.put("id", msg.getSenderId());
+        item.put("sender", sender);
+
+        // 接收者列表
+        List<QwConversationParticipant> receivers = participantMapper.selectByMsgidAndType(corpId, msg.getMsgid(), 2);
+        JSONArray receiverList = new JSONArray();
+        for (QwConversationParticipant p : receivers) {
+            JSONObject recv = new JSONObject();
+            recv.put("type", p.getUserType());
+            recv.put("id", p.getUserId());
+            receiverList.add(recv);
         }
+        item.put("receiver_list", receiverList);
+
+        item.put("msgtype", msg.getMsgtype());
+
+        // 时间
+        if (msg.getSendTime() != null) {
+            item.put("send_time", msg.getSendTime());
+            String formattedTime = Instant.ofEpochSecond(msg.getSendTime())
+                    .atZone(ZoneId.systemDefault())
+                    .toLocalDateTime()
+                    .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            item.put("send_time_str", formattedTime);
+        }
+        return item;
     }
+
+
+//    private JSONArray processMessages(JSONArray msgList, String customerId, String staffUserId, QwSessionConfigVo qwConfig) {
+//        return msgList.parallelStream()
+//                .map(obj -> (JSONObject) obj)
+//                .filter(msg -> isMessageRelatedToUsers(msg, customerId, staffUserId))
+//                .map(msg -> decryptAndFormatMessage(msg, qwConfig))
+//                .filter(Objects::nonNull)
+//                .collect(Collectors.toCollection(JSONArray::new));
+//    }
+//
+//    private boolean isMessageRelatedToUsers(JSONObject msg, String customerId, String staffUserId) {
+//        // 如果都为空,则不过滤,返回全部
+//        if ((customerId == null || customerId.isEmpty()) && (staffUserId == null || staffUserId.isEmpty())) {
+//            return true;
+//        }
+//        boolean hasCustomer = (customerId == null || customerId.isEmpty());
+//        boolean hasStaff = (staffUserId == null || staffUserId.isEmpty());
+//
+//        JSONObject sender = msg.getJSONObject("sender");
+//        JSONArray receivers = msg.getJSONArray("receiver_list");
+//
+//        // 检查发送者
+//        if (sender != null) {
+//            String senderId = sender.getString("id");
+//            int senderType = sender.getIntValue("type");
+//            if (!hasCustomer && senderType == 2 && customerId.equals(senderId)) hasCustomer = true;
+//            if (!hasStaff && senderType == 1 && staffUserId.equals(senderId)) hasStaff = true;
+//        }
+//        if (hasCustomer && hasStaff) return true;
+//
+//        // 检查接收者
+//        if (receivers != null) {
+//            for (int i = 0; i < receivers.size(); i++) {
+//                JSONObject recv = receivers.getJSONObject(i);
+//                String recvId = recv.getString("id");
+//                int recvType = recv.getIntValue("type");
+//                if (!hasCustomer && recvType == 2 && customerId.equals(recvId)) hasCustomer = true;
+//                if (!hasStaff && recvType == 1 && staffUserId.equals(recvId)) hasStaff = true;
+//                if (hasCustomer && hasStaff) return true;
+//            }
+//        }
+//        return hasCustomer && hasStaff;
+//    }
+//
+//    private JSONObject decryptAndFormatMessage(JSONObject msg, QwSessionConfigVo qwConfig) {
+//        JSONObject result = new JSONObject();
+//        try {
+//            JSONObject encryptInfo = msg.getJSONObject("service_encrypt_info");
+//            if (encryptInfo == null) return null;
+//            String encryptedKey = encryptInfo.getString("encrypted_secret_key");
+//            if (encryptedKey == null) return null;
+//
+//            // 解密得到 secretKey
+//            String secretKey = WeChatSpaceDecryptUtil.decryptSecretKey(encryptedKey, qwConfig.getPrivateKey());
+//
+//            // 复制需要返回的字段
+//            result.put("msgid", msg.getString("msgid"));
+//            result.put("secretKey", secretKey);
+//            result.put("sender", msg.get("sender"));
+//            result.put("receiver_list", msg.get("receiver_list"));
+//            result.put("msgtype", msg.getInteger("msgtype"));
+//
+//            Long sendTime = msg.getLong("send_time");
+//            if (sendTime != null) {
+//                String formattedTime = Instant.ofEpochSecond(sendTime)
+//                        .atZone(ZoneId.systemDefault())
+//                        .toLocalDateTime()
+//                        .format(DATE_TIME_FORMATTER);
+//                result.put("send_time_str", formattedTime);
+//                result.put("send_time", sendTime);
+//            }
+//            return result;
+//        } catch (Exception e) {
+//            log.error("解密消息失败, msgid: {}", msg.getString("msgid"), e);
+//            return null;
+//        }
+//    }
 }

+ 36 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwProgramInvoker.java

@@ -0,0 +1,36 @@
+package com.fs.qw.service.impl;
+
+import com.fs.qw.utils.WeChatSpaceUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+import com.alibaba.fastjson.JSONObject;
+@Slf4j
+@Component
+public class QwProgramInvoker {
+    @Autowired
+    private WeChatSpaceUtil weChatSpaceUtil;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public JSONObject callProgram(String corpId, String programId, String abilityId, JSONObject requestData) {
+        String accessToken = weChatSpaceUtil.getAccessToken(corpId);
+        String url = "https://qyapi.weixin.qq.com/cgi-bin/chatdata/sync_call_program?access_token=" + accessToken;
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("program_id", programId);
+        requestBody.put("ability_id", abilityId);
+        requestBody.put("request_data", requestData.toJSONString());
+        log.info("调用数据与智能专区接口:{},请求体: {}",abilityId, requestBody.toJSONString());
+        try {
+            JSONObject response = restTemplate.postForObject(url, requestBody, JSONObject.class);
+            if ("invoke_search_msg".equals(abilityId)) {
+                log.info("搜索接口响应: {}", response);
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("调用 search_msg 接口异常", e);
+            return null;
+        }
+    }
+}

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

@@ -33,6 +33,7 @@ import com.fs.ipad.vo.WxRoomUserListVo;
 import com.fs.qw.domain.*;
 import com.fs.qw.dto.QwUserByToolDTO;
 import com.fs.qw.dto.QwUserKeyDTO;
+import com.fs.qw.dto.QwUserQueryDto;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.mapper.QwWorkTaskMapper;
 import com.fs.qw.param.*;
@@ -1679,6 +1680,15 @@ public class QwUserServiceImpl implements IQwUserService
         return qwUserList;
     }
 
+    @Override
+    public List<QwUserListVo> selectQwUserListByCondition(QwUserQueryDto query) {
+        List<QwUserListVo> qwUserList = qwUserMapper.selectQwUserListByCondition(query);
+        if (CollectionUtils.isEmpty(qwUserList)){
+            return Collections.emptyList();
+        }
+        return qwUserList;
+    }
+
     /**
      * 根据销售公司和企微ID查询企微用户
      */

+ 5 - 5
fs-service/src/main/java/com/fs/qw/utils/WeChatSpaceUtil.java

@@ -32,7 +32,7 @@ public class WeChatSpaceUtil {
     /**
      * 获取企业 access_token(带缓存,由 QwApiService 实现)
      */
-    public String getAccessToken(String corpId, String corpSecret) {
+    public String getAccessToken(String corpId) {
         QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
         String openSecret = qwCompany.getOpenSecret();
         return qwApiService.getToken(corpId, openSecret);
@@ -41,7 +41,7 @@ public class WeChatSpaceUtil {
     /**
      * 获取 agent_ticket(按企业+应用独立缓存)
      */
-    public String getAgentTicket(String corpId, String corpSecret, String agentId) {
+    public String getAgentTicket(String corpId, String agentId) {
         long now = System.currentTimeMillis() / 1000;
         String cacheKey = corpId + "_" + agentId;  // 区分不同企业的不同应用
 
@@ -49,7 +49,7 @@ public class WeChatSpaceUtil {
             return AGENT_TICKET_CACHE.get(cacheKey);
         }
 
-        String accessToken = this.getAccessToken(corpId, corpSecret);
+        String accessToken = this.getAccessToken(corpId);
         String url = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get?access_token=" + accessToken + "&type=agent_config";
         JSONObject resp = restTemplate.getForObject(url, JSONObject.class);
 
@@ -66,9 +66,9 @@ public class WeChatSpaceUtil {
     /**
      * 生成 agentConfig 签名(实例方法)
      */
-    public JSONObject generateAgentConfigSignature(String corpId, String corpSecret, String agentId, String url) {
+    public JSONObject generateAgentConfigSignature(String corpId, String agentId, String url) {
         try {
-            String ticket = this.getAgentTicket(corpId, corpSecret, agentId);
+            String ticket = this.getAgentTicket(corpId, agentId);
             String nonceStr = UUID.randomUUID().toString().replaceAll("-", "");
             String timestamp = Long.toString(System.currentTimeMillis() / 1000);
             String signStr = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url;

+ 1 - 0
fs-service/src/main/java/com/fs/qw/vo/QwOptionsVO.java

@@ -9,4 +9,5 @@ public class QwOptionsVO {
     String qwUserId;
     String corpId;
     String corpName;
+    String companyIds;
 }

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

@@ -0,0 +1,16 @@
+package com.fs.qw.vo;
+
+import lombok.Data;
+
+@Data
+public class QwUserListVo {
+    //id
+    private Long id;
+    private String qwUserId;
+    //员工昵称
+    private String nickName;
+    //企微昵称
+    private String qwUserName;
+    //部门名称
+    private String departmentName;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/vo/SearchResultVO.java

@@ -0,0 +1,15 @@
+package com.fs.qw.vo;
+
+import com.alibaba.fastjson.JSONArray;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SearchResultVO {
+    private JSONArray data;        // 消息列表
+    private Integer hasMore;       // 0-否 1-是
+    private String nextCursor;     // 下一页游标
+}

+ 7 - 0
fs-service/src/main/resources/mapper/company/CompanyMapper.xml

@@ -286,4 +286,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         select GROUP_CONCAT(gateway_id) from company_bind_gateway where company_id = #{companyId}
     </select>
 
+    <select id="queryCompanyListByCompanyIds" resultType="com.fs.company.domain.Company">
+        select company_id,company_name,money,red_package_money from company where is_del= 0 and company_id in
+        <foreach item="companyId" collection="companyIds" open="(" close=")" separator=",">
+            #{companyId}
+        </foreach>
+    </select>
+
 </mapper>

+ 14 - 6
fs-service/src/main/resources/mapper/qw/QwConversationMessageMapper.xml

@@ -20,8 +20,9 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, corp_id, msgid, sender_type, sender_id, chatid, msgtype, send_time,
+        SELECT id, corp_id, msgid, sender_type, sender_id, chatid, msgtype, send_time,
         encrypted_secret_key, secret_key, public_key_ver, extra_info, create_time, update_time
+        FROM qw_conversation_message
     </sql>
 
     <!-- 插入 -->
@@ -82,13 +83,12 @@
 
     <!-- 根据主键查询 -->
     <select id="selectById" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/> FROM qw_conversation_message WHERE id = #{id}
+        <include refid="Base_Column_List"/> WHERE id = #{id}
     </select>
 
     <!-- 根据企业+msgid查询 -->
     <select id="selectByCorpIdAndMsgid" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/>
-        FROM qw_conversation_message
+        <include refid="Base_Column_List"/>
         WHERE corp_id = #{corpId} AND msgid = #{msgid}
     </select>
 
@@ -115,12 +115,20 @@
 
     <!-- 条件分页查询(按时间范围) -->
     <select id="selectListByCondition" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/>
-        FROM qw_conversation_message
+        <include refid="Base_Column_List"/>
         WHERE corp_id = #{corpId}
         <if test="startTime != null">AND send_time >= #{startTime}</if>
         <if test="endTime != null">AND send_time &lt;= #{endTime}</if>
         ORDER BY send_time DESC
         <if test="offset != null and limit != null">LIMIT #{offset}, #{limit}</if>
     </select>
+
+    <select id="selectByMsgIds" resultType="com.fs.qw.domain.QwConversationMessage">
+        <include refid="Base_Column_List"/>
+        WHERE corp_id = #{corpId}
+        AND msgid IN
+        <foreach collection="msgIds" item="msgid" open="(" separator="," close=")">
+            #{msgid}
+        </foreach>
+    </select>
 </mapper>

+ 4 - 7
fs-service/src/main/resources/mapper/qw/QwConversationParticipantMapper.xml

@@ -12,7 +12,7 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, corp_id, msgid, participant_type, user_type, user_id
+        SELECT id, corp_id, msgid, participant_type, user_type, user_id FROM qw_conversation_participant
     </sql>
 
     <!-- 插入参与者记录 -->
@@ -47,21 +47,18 @@
 
     <!-- 根据消息ID查询参与者列表 -->
     <select id="selectByMsgid" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/>
-        FROM qw_conversation_participant
+        <include refid="Base_Column_List"/>
         WHERE corp_id = #{corpId} AND msgid = #{msgid}
     </select>
 
     <!-- 查询用户参与的所有消息ID(用于快速过滤) -->
     <select id="selectByUser" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/>
-        FROM qw_conversation_participant
+        <include refid="Base_Column_List"/>
         WHERE corp_id = #{corpId} AND user_type = #{userType} AND user_id = #{userId}
     </select>
 
     <select id="selectByMsgidAndType" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/>
-        FROM qw_conversation_participant
+        <include refid="Base_Column_List"/>
         WHERE corp_id = #{corpId} AND msgid = #{msgid} AND participant_type = #{participantType}
     </select>
 </mapper>

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

@@ -14,7 +14,8 @@
     </resultMap>
 
     <sql id="Base_Column_List">
-        id, corp_id, program_id, ability_id, `cursor`, last_sync_time, create_time, update_time
+        SELECT id, corp_id, program_id, ability_id, `cursor`, last_sync_time, create_time, update_time
+        FROM qw_conversation_sync_state
     </sql>
 
     <insert id="insert" useGeneratedKeys="true" keyProperty="id">
@@ -60,12 +61,11 @@
     </delete>
 
     <select id="selectById" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/> FROM qw_conversation_sync_state WHERE id = #{id}
+        <include refid="Base_Column_List"/> WHERE id = #{id}
     </select>
 
     <select id="selectByCorpId" resultMap="BaseResultMap">
-        SELECT <include refid="Base_Column_List"/>
-        FROM qw_conversation_sync_state
+        <include refid="Base_Column_List"/>
         WHERE corp_id = #{corpId}
     </select>
 

+ 30 - 0
fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

@@ -358,4 +358,34 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
     </select>
 
+    <select id="selectQwUserListByCondition" resultType="com.fs.qw.vo.QwUserListVo">
+        SELECT
+        q.id,
+        q.qw_user_id AS qwUserId,
+        q.qw_user_name AS qwUserName,
+        <!-- 使用 MAX 聚合函数,防止一对多关联导致数据重复 -->
+        MAX(cu.nick_name) AS nickName,
+        MAX(cd.dept_name) AS departmentName
+        FROM
+        qw_user q
+        LEFT JOIN
+        company_user cu ON FIND_IN_SET(q.id, cu.qw_user_id)
+        LEFT JOIN
+        company_dept cd ON q.department = cd.dept_id
+        AND cd.status = '0'
+        AND cd.del_flag = '0'
+        <where>
+            <!-- 1. 根据 company_user 的主键ID进行连表过滤 -->
+            <if test="query.userId != null">
+                AND cu.user_id = #{query.userId}
+            </if>
+
+            <!-- 2. 根据企微用户昵称进行模糊查询 -->
+            <if test="query.qwUserName != null and query.qwUserName.trim() != ''">
+                AND q.qw_user_name LIKE CONCAT('%', #{query.qwUserName}, '%')
+            </if>
+        </where>
+        GROUP BY q.id
+    </select>
+
 </mapper>