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

益寿缘-销售端-新增获客链接功能

cgp 4 часов назад
Родитель
Сommit
8b3c3b0af7
22 измененных файлов с 1789 добавлено и 0 удалено
  1. 262 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  2. 133 0
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java
  3. 26 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionBaseRequest.java
  4. 27 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionCreateResponse.java
  5. 9 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionDeleteResponse.java
  6. 15 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetRequest.java
  7. 62 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetResponse.java
  8. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListRequest.java
  9. 18 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListResponse.java
  10. 15 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionPriority.java
  11. 15 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionRange.java
  12. 9 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionUpdateResponse.java
  13. 77 0
      fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionAssistantMapper.java
  14. 9 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  15. 96 0
      fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionAssistantService.java
  16. 27 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  17. 639 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java
  18. 29 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  19. 64 0
      fs-service/src/main/java/com/fs/qw/vo/AcquisitionAssistantDetailVO.java
  20. 25 0
      fs-service/src/main/java/com/fs/qwApi/config/QwApiConfig.java
  21. 188 0
      fs-service/src/main/resources/mapper/qw/QwAcquisitionAssistantMapper.xml
  22. 28 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

+ 262 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java

@@ -0,0 +1,262 @@
+package com.fs.company.controller.qw;
+
+import java.util.Collections;
+import java.util.List;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.chat.config.QwConfig;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyConfig;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.service.ICompanyConfigService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.acquisition.AcquisitionGetResponse;
+import com.fs.qw.dto.acquisition.AcquisitionListResponse;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.AcquisitionAssistantDetailVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
+
+/**
+ * 企微-获客链接管理Controller
+ *
+ * @author fs
+ * @date 2026-03-16
+ */
+@Slf4j
+@RestController
+@RequestMapping("/qw/acquisitionAssistant")
+public class QwAcquisitionAssistantController extends BaseController {
+    @Autowired
+    private IQwAcquisitionAssistantService qwAcquisitionAssistantService;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    /**
+     * 查询企微-获客链接管理列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(QwAcquisitionAssistant qwAcquisitionAssistant) {
+        startPage();
+        List<QwAcquisitionAssistant> list = qwAcquisitionAssistantService.selectQwAcquisitionAssistantList(qwAcquisitionAssistant);
+        return getDataTable(list);
+    }
+
+    /**
+     * 从企微同步获客链接列表(全量)
+     * 手动点击同步按钮时调用
+     */
+    @PostMapping("/syncList")
+    public AjaxResult syncList(@RequestParam String corpId) {
+        try {
+
+            QwCompany qwCompany = getQwCompany(corpId);
+
+            // 调用同步服务
+            String result = qwAcquisitionAssistantService.syncListFromQw(qwCompany.getCorpId(), qwCompany.getOpenSecret());
+
+            return AjaxResult.success(result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 分页获取企微列表(直接调用企微接口)
+     * 用于查看企微原始数据
+     */
+    @GetMapping("/qwList")
+    public AjaxResult getQwList(@RequestParam(required = false) Integer limit,
+                                @RequestParam(required = false) String cursor,
+                                @RequestParam String corpId) {
+        try {
+            QwCompany qwCompany = getQwCompany(corpId);
+            // 调用企微列表接口
+            AcquisitionListResponse response = qwAcquisitionAssistantService.getQwList(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), limit, cursor);
+
+            return AjaxResult.success(response);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据linkId直接获取详情
+     *
+     * @param linkId 企微链接ID
+     */
+    @GetMapping("/getDetailByLinkId/{linkId}")
+    public AjaxResult getDetailByLinkId(@PathVariable String linkId) {
+        try {
+            QwAcquisitionAssistant acquisitionAssistant = new QwAcquisitionAssistant();
+            acquisitionAssistant.setLinkId(linkId);
+            List<QwAcquisitionAssistant> qwAcquisitionAssistants = qwAcquisitionAssistantService.selectQwAcquisitionAssistantList(acquisitionAssistant);
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistants.get(0).getCorpId());
+            // 调用获取详情服务
+            AcquisitionAssistantDetailVO detailVo = qwAcquisitionAssistantService.getDetailWithQw(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), linkId);
+
+            return AjaxResult.success("获取成功", detailVo);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 新增企微-获客链接管理
+     */
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody QwAcquisitionAssistant qwAcquisitionAssistant) {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            qwAcquisitionAssistant.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.createWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+            return AjaxResult.success("创建成功", result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/qwUserList")
+    public AjaxResult getQwUserList(@RequestBody QwUser qwUser) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<QwUser> qwUserList = qwUserService.selectQwUserListByAcquisition(qwUser);
+        if (qwUserList != null && !qwUserList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwUserList);
+        } else {
+            return AjaxResult.error("未找到用户");
+        }
+    }
+
+    /**
+     * 获取企微用户主体列表
+     */
+    @PostMapping("/qwUserCompanyList")
+    public AjaxResult getQwUserCompanyList(@RequestBody QwCompany qwCompany) {
+        qwCompany.setStatus(1L);//启用状态的主体
+        List<QwCompany> qwCompanyList = qwCompanyService.selectQwCompanyList(qwCompany);
+        if (qwCompanyList != null && !qwCompanyList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwCompanyList);
+        } else {
+            return AjaxResult.error("未找到主体");
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @GetMapping("/getQwUserListByIds")
+    public AjaxResult getQwUserListByIds(@RequestBody List<Long> qwUserTableIds) {
+        if (qwUserTableIds == null || qwUserTableIds.isEmpty()) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+        // 限制最多500个,避免性能问题
+        if (qwUserTableIds.size() > 500) {
+            qwUserTableIds = qwUserTableIds.subList(0, 500);
+        }
+        List<QwUser> qwUserList = qwUserService.selectQwUserListByIds(qwUserTableIds);
+        if (qwUserList != null && !qwUserList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwUserList);
+        } else {
+            return AjaxResult.error("未找到用户");
+        }
+    }
+
+    /**
+     * 修改企微-获客链接
+     */
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody QwAcquisitionAssistant qwAcquisitionAssistant) {
+        try {
+            // 参数校验
+            if (qwAcquisitionAssistant.getId() == null) {
+                return AjaxResult.error("ID不能为空");
+            }
+            if (StringUtils.isEmpty(qwAcquisitionAssistant.getLinkId())) {
+                return AjaxResult.error("链接ID不能为空");
+            }
+
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+            qwAcquisitionAssistant.setUpdateBy(String.valueOf(loginUser.getUser().getUserId()));
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.updateWithQw(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+
+            return AjaxResult.success("修改成功", result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除企微-获客链接
+     *
+     * @param id 本地记录ID
+     */
+    @GetMapping("/delete/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        try {
+            // 先查询本地记录,获取linkId
+            QwAcquisitionAssistant assistant = qwAcquisitionAssistantService.selectQwAcquisitionAssistantById(id);
+            if (assistant == null) {
+                return AjaxResult.error("获客链接不存在");
+            }
+            QwCompany qwCompany = getQwCompany(assistant.getCorpId());
+            // 调用删除服务
+            qwAcquisitionAssistantService.deleteWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), assistant);
+
+            return AjaxResult.success("删除成功");
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    private QwCompany getQwCompany(String corpId) {
+        if (StringUtils.isBlank(corpId)) {
+            log.error("获客链接管理参数异常:{}", corpId);
+            throw new CustomException("未找到企业微信主体");
+        }
+        QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
+        if (qwCompany == null) {
+            log.error("获客链接管理-企微主体获取异常:{}", corpId);
+            throw new CustomException("未找到企业微信主体");
+        }
+        return qwCompany;
+    }
+}

+ 133 - 0
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java

@@ -0,0 +1,133 @@
+package com.fs.qw.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import com.alibaba.fastjson.JSON;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企微-获客链接管理对象 qw_acquisition_assistant
+ * 
+ * @author fs
+ * @date 2026-03-16
+ */
+
+@Data
+public class QwAcquisitionAssistant extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 获客链接ID */
+    private String linkId;
+
+    /** 获客链接名称 */
+    private String linkName;
+
+    /** 获客链接URL */
+    private String url;
+
+    /** 获客链接scheme */
+    private String scheme;
+
+    /** 创建时间(企微返回的时间) */
+    private Date qwCreateTime;
+
+    /** 是否无需验证,默认为true */
+    private Boolean skipVerify=true;
+
+//    /**
+//     * 是否标记客户添加来源为该应用创建的获客链接, 默认值为true; 仅对「营销获客」应用生效
+//     * */
+//    private Boolean markSource=true;
+
+    /** 优先分配类型(0-不启用 1-全企业范围内优先分配给有好友关系的 2-指定范围内优先分配有好友关系的) */
+    private Integer priorityType;
+
+    /** 关联的成员列表,JSON数组格式 */
+    private String userList;
+
+    /** 关联的部门列表,JSON数组格式 */
+    private String departmentList;
+
+    /** 优先分配成员列表,JSON数组格式 */
+    private String priorityUserList;
+
+    /** qw_user表的主键id列表,JSON数组格式 */
+    private String qwUserTableIdList;
+
+    /** 使用范围描述(用于展示) */
+    private String rangeDesc;
+
+    /** 状态(0-已删除 1-正常 2-已失效) */
+    private Integer status;
+
+    /** 最后同步时间 */
+    private Date syncTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 删除标志(0-正常 1-已删除) */
+    private String delFlag;
+
+    // ==================== 非数据库字段,用于辅助操作 ====================
+
+    /** 成员列表(用于接收前端参数) */
+    private List<String> userListParam;
+
+    /** 部门列表(用于接收前端参数) */
+    private List<Long> departmentListParam;
+
+    /** 优先分配成员列表(用于接收前端参数) */
+    private List<String> priorityUserListParam;
+
+    /** 主体corpId */
+    private String corpId;
+
+    /**
+     * 将参数列表转换为JSON字符串
+     */
+    public void buildJsonFields() 
+    {
+        if (userListParam != null) {
+            this.userList = JSON.toJSONString(userListParam);
+        }
+        if (departmentListParam != null) {
+            this.departmentList = JSON.toJSONString(departmentListParam);
+        }
+        if (priorityUserListParam != null) {
+            this.priorityUserList = JSON.toJSONString(priorityUserListParam);
+        }
+    }
+
+    /**
+     * 解析JSON字段到参数列表
+     */
+    public void parseJsonFields() 
+    {
+        if (StringUtils.isNotBlank(this.userList)) {
+            this.userListParam = JSON.parseArray(this.userList, String.class);
+        }
+        if (StringUtils.isNotBlank(this.departmentList)) {
+            this.departmentListParam = JSON.parseArray(this.departmentList, Long.class);
+        }
+        if (StringUtils.isNotBlank(this.priorityUserList)) {
+            this.priorityUserListParam = JSON.parseArray(this.priorityUserList, String.class);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "QwAcquisitionAssistant{" +
+                "id=" + id +
+                ", linkId='" + linkId + '\'' +
+                ", linkName='" + linkName + '\'' +
+                ", status=" + status +
+                '}';
+    }
+}

+ 26 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionBaseRequest.java

@@ -0,0 +1,26 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+@Data
+public class AcquisitionBaseRequest {
+    
+    @JSONField(name = "link_id")      // 编辑时必填,创建时不填
+    private String linkId;
+    
+    @JSONField(name = "link_name")    // 创建时必填,编辑时可选
+    private String linkName;
+    
+    private AcquisitionRange range;    // 范围
+    
+    @JSONField(name = "skip_verify")
+    private Boolean skipVerify;
+    
+    @JSONField(name = "priority_option")
+    private AcquisitionPriority priorityOption;
+
+//    标记来源功能仅对「营销获客」应用生效
+//    @JSONField(name = "mark_source")
+//    private Boolean markSource;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionCreateResponse.java

@@ -0,0 +1,27 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+@Data
+public class AcquisitionCreateResponse {
+    
+    private Integer errcode;
+    private String errmsg;
+    
+    private LinkInfo link;  // 创建成功返回链接信息
+    
+    @Data
+    public static class LinkInfo {
+        @JSONField(name = "link_id")
+        private String linkId;
+        
+        @JSONField(name = "link_name")
+        private String linkName;
+        
+        private String url;
+        
+        @JSONField(name = "create_time")
+        private Long createTime;
+    }
+}

+ 9 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionDeleteResponse.java

@@ -0,0 +1,9 @@
+package com.fs.qw.dto.acquisition;
+
+import lombok.Data;
+
+@Data
+public class AcquisitionDeleteResponse {
+    private Integer errcode;
+    private String errmsg;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetRequest.java

@@ -0,0 +1,15 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 获取获客链接详情请求DTO
+ */
+@Data
+public class AcquisitionGetRequest {
+    
+    /** 获客链接id */
+    @JSONField(name = "link_id")
+    private String linkId;
+}

+ 62 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetResponse.java

@@ -0,0 +1,62 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class AcquisitionGetResponse {
+
+    private Integer errcode;
+    private String errmsg;
+
+    @JSONField(name = "link")
+    private LinkDetail link;
+
+    @JSONField(name = "range")
+    private RangeInfo range;
+
+    @JSONField(name = "priority_option")
+    private PriorityInfo priorityOption;
+
+    @Data
+    public static class LinkDetail {
+        @JSONField(name = "link_name")
+        private String linkName;
+
+        private String url;
+
+        @JSONField(name = "create_time")
+        private Long createTime;
+
+        @JSONField(name = "skip_verify")
+        private Boolean skipVerify;
+    }
+
+    @Data
+    public static class RangeInfo {
+        @JSONField(name = "user_list")
+        private List<String> userList;
+
+        @JSONField(name = "department_list")
+        private List<Integer> departmentList;
+
+        // 可以添加构造方法
+        public RangeInfo() {
+        }
+
+        public RangeInfo(List<String> userList, List<Integer> departmentList) {
+            this.userList = userList;
+            this.departmentList = departmentList;
+        }
+    }
+
+    @Data
+    public static class PriorityInfo {
+        @JSONField(name = "priority_type")
+        private Integer priorityType;
+
+        @JSONField(name = "priority_userid_list")
+        private List<String> priorityUseridList;
+    }
+}

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

@@ -0,0 +1,16 @@
+package com.fs.qw.dto.acquisition;
+
+import lombok.Data;
+
+/**
+ * 获取获客链接列表请求DTO(调用企微接口用)
+ */
+@Data
+public class AcquisitionListRequest {
+    
+    /** 返回的最大记录数,最大值100 */
+    private Integer limit;
+    
+    /** 分页游标 */
+    private String cursor;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListResponse.java

@@ -0,0 +1,18 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class AcquisitionListResponse {
+    
+    private Integer errcode;
+    private String errmsg;
+    
+    @JSONField(name = "link_id_list")
+    private List<String> linkIdList;
+    
+    @JSONField(name = "next_cursor")
+    private String nextCursor;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionPriority.java

@@ -0,0 +1,15 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class AcquisitionPriority {
+    
+    @JSONField(name = "priority_type")
+    private Integer priorityType;
+    
+    @JSONField(name = "priority_userid_list")
+    private List<String> priorityUseridList;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionRange.java

@@ -0,0 +1,15 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class AcquisitionRange {
+    
+    @JSONField(name = "user_list")
+    private List<String> userList;
+    
+    @JSONField(name = "department_list")
+    private List<Integer> departmentList;
+}

+ 9 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionUpdateResponse.java

@@ -0,0 +1,9 @@
+package com.fs.qw.dto.acquisition;
+
+import lombok.Data;
+
+@Data
+public class AcquisitionUpdateResponse {
+    private Integer errcode;
+    private String errmsg;
+}

+ 77 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionAssistantMapper.java

@@ -0,0 +1,77 @@
+package com.fs.qw.mapper;
+
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import java.util.List;
+
+/**
+ * 企微-获客链接管理Mapper接口
+ * 
+ * @author fs
+ * @date 2026-03-16
+ */
+public interface QwAcquisitionAssistantMapper 
+{
+    /**
+     * 查询企微-获客链接管理
+     * 
+     * @param id 企微-获客链接管理主键
+     * @return 企微-获客链接管理
+     */
+    public QwAcquisitionAssistant selectQwAcquisitionAssistantById(Long id);
+
+    /**
+     * 根据linkId查询
+     * 
+     * @param linkId 获客链接ID
+     * @return 企微-获客链接管理
+     */
+    public QwAcquisitionAssistant selectQwAcquisitionAssistantByLinkId(String linkId);
+
+    /**
+     * 查询企微-获客链接管理列表
+     * 
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 企微-获客链接管理集合
+     */
+    public List<QwAcquisitionAssistant> selectQwAcquisitionAssistantList(QwAcquisitionAssistant qwAcquisitionAssistant);
+
+    /**
+     * 新增企微-获客链接管理
+     * 
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 结果
+     */
+    public int insertQwAcquisitionAssistant(QwAcquisitionAssistant qwAcquisitionAssistant);
+
+    /**
+     * 修改企微-获客链接管理
+     * 
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 结果
+     */
+    public int updateQwAcquisitionAssistant(QwAcquisitionAssistant qwAcquisitionAssistant);
+
+    /**
+     * 删除企微-获客链接管理
+     * 
+     * @param id 企微-获客链接管理主键
+     * @return 结果
+     */
+    public int deleteQwAcquisitionAssistantById(Long id);
+
+    /**
+     * 批量删除企微-获客链接管理
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteQwAcquisitionAssistantByIds(Long[] ids);
+
+    /**
+     * 更新状态
+     * 
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 结果
+     */
+    public int updateStatus(QwAcquisitionAssistant qwAcquisitionAssistant);
+}

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

@@ -470,4 +470,13 @@ public interface QwUserMapper extends BaseMapper<QwUser>
 
     List<QwUser> selectQwUserByTest();
 
+    /**
+     *  根据企微ID集合查询企微用户
+     * */
+    List<QwUser> selectQwUserListByQwUserIds(List<String> qwUserIds);
+
+    /**
+     *  根据主键ID集合查询企微用户
+     * */
+    List<QwUser> selectQwUserListByIds(List<Long> ids);
 }

+ 96 - 0
fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionAssistantService.java

@@ -0,0 +1,96 @@
+package com.fs.qw.service;
+
+import com.fs.common.exception.CustomException;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.dto.acquisition.AcquisitionGetResponse;
+import com.fs.qw.dto.acquisition.AcquisitionListResponse;
+import com.fs.qw.vo.AcquisitionAssistantDetailVO;
+
+import java.util.List;
+
+/**
+ * 企微-获客链接管理Service接口
+ * 
+ * @author fs
+ * @date 2026-03-16
+ */
+public interface IQwAcquisitionAssistantService 
+{
+    /**
+     * 查询企微-获客链接管理列表
+     *
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 企微-获客链接管理集合
+     */
+    public List<QwAcquisitionAssistant> selectQwAcquisitionAssistantList(QwAcquisitionAssistant qwAcquisitionAssistant);
+
+    /**
+     * 从企微同步获客链接列表(全量拉取所有详情)
+     * @param corpid 企业ID
+     * @param corpsecret 应用密钥
+     * @return 同步结果统计
+     */
+    public String syncListFromQw(String corpid, String corpsecret);
+
+    /**
+     * 获取企微原始列表(分页)
+     * @param corpid 企业ID
+     * @param corpsecret 应用密钥
+     * @param limit 每页数量(最大100)
+     * @param cursor 分页游标
+     * @return 企微返回的列表数据
+     */
+    public AcquisitionListResponse getQwList(String corpid, String corpsecret, Integer limit, String cursor);
+    /**
+     * 获取获客链接详情(从企微实时查询)
+     * @param corpid 企业ID
+     * @param corpsecret 应用密钥
+     * @param linkId 获客链接ID
+     * @return 企微返回的详情数据
+     * @throws CustomException 当调用企微API失败时抛出
+     */
+    public AcquisitionAssistantDetailVO getDetailWithQw(String corpid, String corpsecret, String linkId);
+
+    /**
+     * 根据linkId同步单个获客链接详情到本地
+     * @param corpid 企业ID
+     * @param corpsecret 应用密钥
+     * @param linkId 获客链接ID
+     */
+    public void syncDetailToLocal(String corpid, String corpsecret, String linkId);
+
+    /**
+     * 创建获客链接
+     *
+     * @param corpid 企业ID
+     * @param corpsecret 应用密钥
+     * @param assistant 获客链接信息
+     * @return 创建成功的完整对象(包含企微返回的link_id、url等)
+     */
+    public QwAcquisitionAssistant createWithQw(String corpid,String corpsecret,QwAcquisitionAssistant assistant);
+
+    /**
+     * 修改企微-获客链接管理
+     *
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 结果
+     */
+    public QwAcquisitionAssistant updateWithQw(String corpId, String secret, QwAcquisitionAssistant qwAcquisitionAssistant);
+
+    /**
+     * 删除获客链接
+     * @param corpid 企业ID
+     * @param corpsecret 应用密钥
+     * @param assistant 获客链接信息(必须包含linkId)
+     * @throws CustomException 当调用企微API失败时抛出
+     */
+    public void deleteWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant);
+
+    /**
+     * 查询企微-获客链接管理
+     *
+     * @param id 企微-获客链接管理主键
+     * @return 企微-获客链接管理
+     */
+    public QwAcquisitionAssistant selectQwAcquisitionAssistantById(Long id);
+}

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

@@ -202,4 +202,31 @@ public interface IQwUserService
 
     List<QwOptionsVO> selectQwCompanyListOptionsVOBySys();
 
+    /**
+     * 获客链接---查询企微用户列表
+     *
+     * @param qwUser 企微用户
+     * @return 企微用户列表
+     *
+     * */
+    public List<QwUser> selectQwUserListByAcquisition(QwUser qwUser);
+
+
+    /**
+     * 获客链接---查询企微用户列表
+     *
+     * @param qwUserIds 企微用户id集合
+     * @return 企微用户列表
+     *
+     * */
+    public List<QwUser> selectQwUserListByQwUserIds(List<String> qwUserIds);
+
+    /**
+     * 获客链接---查询企微用户列表
+     *
+     * @param ids 企微用户主键id集合
+     * @return 企微用户列表
+     *
+     * */
+    public List<QwUser> selectQwUserListByIds(List<Long> ids);
 }

+ 639 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java

@@ -0,0 +1,639 @@
+package com.fs.qw.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.fastgptApi.util.HttpUtil;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.dto.acquisition.*;
+import com.fs.qw.mapper.QwAcquisitionAssistantMapper;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.vo.AcquisitionAssistantDetailVO;
+import com.fs.qwApi.config.QwApiConfig;
+import com.fs.wx.kf.service.IWeixinKfService;
+import com.fs.wx.kf.vo.WeixinKfTokenVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 企微-获客链接管理Service业务层处理
+ */
+@Slf4j
+@Service
+public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistantService {
+
+    @Autowired
+    private QwAcquisitionAssistantMapper qwAcquisitionAssistantMapper;
+
+    @Autowired
+    private IWeixinKfService weixinKfService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+
+
+    // 获客链接管理-企微的ACCESS_TOKEN的key
+    private static final String QW_ACQUISITION_KEY_PREFIX = "qw:acquisition:key:";
+
+    /**
+     * 获取access_token并返回完整URL
+     */
+    private String buildApiUrl(String corpid, String corpsecret, String apiPath) {
+        WeixinKfTokenVO token = getAccessToken(corpid, corpsecret);
+
+        if (token == null || StringUtils.isEmpty(token.getAccess_token())) {
+            log.error("获取access_token失败, corpid:{}", corpid);
+            throw new CustomException("获取企业微信access_token失败");
+        }
+
+        return apiPath + "?access_token=" + token.getAccess_token();
+    }
+
+    /**
+     * 获取access_token(使用实际有效期)
+     */
+    private WeixinKfTokenVO getAccessToken(String corpid, String corpsecret) {
+        String key = QW_ACQUISITION_KEY_PREFIX + corpid;
+
+        // 1. 先从缓存获取
+        WeixinKfTokenVO token = null;
+        try {
+            Object cacheObj = redisCache.getCacheObject(key);
+            if (cacheObj instanceof WeixinKfTokenVO) {
+                token = (WeixinKfTokenVO) cacheObj;
+                log.debug("从缓存获取token成功, corpid:{}", corpid);
+            }
+        } catch (Exception e) {
+            log.warn("从缓存获取token异常, 将重新获取, corpid:{}", corpid);
+        }
+
+        // 2. 缓存中没有,则重新获取
+        if (token == null) {
+            log.info("缓存中没有token,重新获取, corpid:{}", corpid);
+            try {
+                // 获取新token
+                token = weixinKfService.getToken(corpid, corpsecret);
+
+                // 存入缓存,使用企业微信返回的实际有效期
+                if (token != null && StringUtils.isNotEmpty(token.getAccess_token())) {
+                    Integer expiresIn = token.getExpires_in();
+                    if (expiresIn == null) {
+                        // 如果返回中没有expires_in,默认2小时
+                        expiresIn = 7200;
+                        log.warn("token返回中没有expires_in字段,使用默认值7200秒");
+                    }
+
+                    // 实际缓存时间比有效期稍短(提前5分钟过期),避免边界问题
+                    Integer cacheExpire = expiresIn - 300; // 提前5分钟
+                    if (cacheExpire <= 0) {
+                        cacheExpire = expiresIn;
+                    }
+
+                    redisCache.setCacheObject(key, token, cacheExpire, TimeUnit.SECONDS);
+                    log.info("token缓存成功, corpid:{}, 有效期:{}秒, 缓存时间:{}秒",
+                            corpid, expiresIn, cacheExpire);
+                }
+            } catch (Exception e) {
+                log.error("获取token失败, corpid:{}, error:{}", corpid, e.getMessage());
+                return null;
+            }
+        }
+
+        return token;
+    }
+
+    /**
+     * 调用企微API并统一处理返回结果
+     */
+    private <T> T callQwApi(String url, Object request, Class<T> responseClass, String operationName) {
+        log.info("调用企微{},参数:{}", operationName, JSON.toJSONString(request));
+        String result = HttpUtil.sendAuthPost(request, url);
+        log.info("企微{}响应:{}", operationName, result);
+
+        if (StringUtils.isEmpty(result)) {
+            throw new CustomException("调用企微API失败,返回为空");
+        }
+
+        T response = JSON.parseObject(result, responseClass);
+
+        // 通过反射获取errcode(所有响应都有这个字段)
+        try {
+            java.lang.reflect.Field errcodeField = responseClass.getDeclaredField("errcode");
+            errcodeField.setAccessible(true);
+            Integer errcode = (Integer) errcodeField.get(response);
+
+            if (errcode != null && errcode != 0) {
+                java.lang.reflect.Field errmsgField = responseClass.getDeclaredField("errmsg");
+                errmsgField.setAccessible(true);
+                String errmsg = (String) errmsgField.get(response);
+                throw new CustomException("企微" + operationName + "失败:" + errmsg);
+            }
+        } catch (Exception e) {
+            if (e instanceof CustomException) {
+                throw (CustomException) e;
+            }
+            log.error("解析企微响应失败", e);
+        }
+
+        return response;
+    }
+
+    /**
+     * 生成范围描述
+     */
+    private String generateRangeDesc(QwAcquisitionAssistant assistant) {
+        StringBuilder desc = new StringBuilder();
+        if (assistant.getUserListParam() != null && !assistant.getUserListParam().isEmpty()) {
+            desc.append(assistant.getUserListParam().size()).append("名成员");
+        }
+        if (assistant.getDepartmentListParam() != null && !assistant.getDepartmentListParam().isEmpty()) {
+            if (desc.length() > 0) desc.append(" + ");
+            desc.append(assistant.getDepartmentListParam().size()).append("个部门");
+        }
+        return desc.length() > 0 ? desc.toString() : "未设置范围";
+    }
+
+    /**
+     * 生成scheme
+     */
+    private String generateScheme(String url) {
+        if (StringUtils.isNotEmpty(url)) {
+            try {
+                String encodedUrl = java.net.URLEncoder.encode(url, "UTF-8");
+                return "weixin://biz/ww/profile/" + encodedUrl;
+            } catch (Exception e) {
+                log.warn("生成scheme失败", e);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 设置本地通用字段
+     */
+    private void setLocalFields(QwAcquisitionAssistant assistant, boolean isCreate) {
+        if (isCreate) {
+            assistant.setStatus(1);
+            assistant.setDelFlag("0");
+            assistant.setCreateTime(new Date());
+        } else {
+            assistant.setUpdateTime(new Date());
+        }
+        assistant.setSyncTime(new Date());
+        assistant.buildJsonFields();
+        assistant.setRangeDesc(generateRangeDesc(assistant));
+
+        if (StringUtils.isEmpty(assistant.getScheme())) {
+            assistant.setScheme(generateScheme(assistant.getUrl()));
+        }
+    }
+
+    // ==================== 列表相关方法 ====================
+
+    @Override
+    public AcquisitionListResponse getQwList(String corpid, String corpsecret, Integer limit, String cursor) {
+        AcquisitionListRequest request = new AcquisitionListRequest();
+        if (limit != null) {
+            request.setLimit(limit > 100 ? 100 : limit);
+        }
+        request.setCursor(cursor);
+
+        String url = buildApiUrl(corpid, corpsecret, QwApiConfig.listAcquisition);
+        return callQwApi(url, request, AcquisitionListResponse.class, "获取获客链接列表");
+    }
+
+    /**
+     * 查询企微-获客链接管理列表
+     *
+     * @param qwAcquisitionAssistant 企微-获客链接管理
+     * @return 企微-获客链接管理
+     */
+    @Override
+    public List<QwAcquisitionAssistant> selectQwAcquisitionAssistantList(QwAcquisitionAssistant qwAcquisitionAssistant) {
+        List<QwAcquisitionAssistant> list = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantList(qwAcquisitionAssistant);
+        if (CollectionUtils.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+        // 解析JSON字段
+        for (QwAcquisitionAssistant assistant : list) {
+            assistant.parseJsonFields();
+        }
+        return list;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String syncListFromQw(String corpid, String corpsecret) {
+        String cursor = null;
+        int totalCount = 0;
+        int successCount = 0;
+        int pageNum = 1;
+
+        log.info("开始同步企微获客链接列表");
+
+        do {
+            log.info("正在同步第{}页", pageNum);
+            AcquisitionListResponse listResponse = getQwList(corpid, corpsecret, 100, cursor);
+
+            List<String> linkIdList = listResponse.getLinkIdList();
+            if (linkIdList == null || linkIdList.isEmpty()) {
+                break;
+            }
+
+            totalCount += linkIdList.size();
+
+            for (String linkId : linkIdList) {
+                try {
+                    AcquisitionAssistantDetailVO detail = getDetailWithQw(corpid, corpsecret, linkId);
+                    QwAcquisitionAssistant assistant = convertToLocal(linkId, detail);
+
+                    QwAcquisitionAssistant existData = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantByLinkId(linkId);
+
+                    if (existData != null) {
+                        assistant.setId(existData.getId());
+                        setLocalFields(assistant, false);
+                        qwAcquisitionAssistantMapper.updateQwAcquisitionAssistant(assistant);
+                    } else {
+                        setLocalFields(assistant, true);
+                        assistant.setCorpId(corpid);// 对于新增的获客链接默认corpId为当前传入的corpId
+                        qwAcquisitionAssistantMapper.insertQwAcquisitionAssistant(assistant);
+                    }
+
+                    successCount++;
+                } catch (Exception e) {
+                    log.error("同步链接详情失败,linkId:{}", linkId, e);
+                }
+            }
+
+            cursor = listResponse.getNextCursor();
+            pageNum++;
+
+        } while (StringUtils.isNotEmpty(cursor));
+
+        String resultMsg = String.format("同步完成,总链接数:%d,成功同步:%d", totalCount, successCount);
+        log.info(resultMsg);
+        return resultMsg;
+    }
+
+    /**
+     * 将企微详情转换为本地实体
+     */
+    private QwAcquisitionAssistant convertToLocal(String linkId, AcquisitionAssistantDetailVO detail) {
+        QwAcquisitionAssistant assistant = new QwAcquisitionAssistant();
+        assistant.setLinkId(linkId);
+
+        assistant.setLinkName(detail.getLinkName());
+        assistant.setUrl(detail.getUrl());
+        assistant.setQwCreateTime(DateUtils.getNowDate());
+        assistant.setSkipVerify(detail.getSkipVerify());
+        //assistant.setMarkSource(detail.getLink().getMarkSource());
+        assistant.setUserList(JSON.toJSONString(detail.getUserList()));
+        assistant.setDepartmentList(JSON.toJSONString(detail.getDepartmentList()));
+
+        assistant.setPriorityType(detail.getPriorityType());
+        assistant.setPriorityUserList(JSON.toJSONString(detail.getPriorityUserList()));
+
+        return assistant;
+    }
+
+    /**
+     * 构建统一的VO对象
+     */
+    private AcquisitionAssistantDetailVO buildDetailVO(QwAcquisitionAssistant localData,
+                                                       AcquisitionGetResponse qwDetail,
+                                                       QwCompany qwCompany,
+                                                       String linkId) {
+        AcquisitionAssistantDetailVO vo = new AcquisitionAssistantDetailVO();
+
+        // 处理企微API返回的数据
+        if (qwDetail != null) {
+            // 基础信息
+            if (qwDetail.getLink() != null) {
+                AcquisitionGetResponse.LinkDetail link = qwDetail.getLink();
+                vo.setLinkId(linkId);
+                vo.setLinkName(link.getLinkName());
+                vo.setUrl(link.getUrl());
+                vo.setSkipVerify(link.getSkipVerify() != null ? link.getSkipVerify() : true);
+                vo.setQwCreateTime(link.getCreateTime());
+            }
+
+            // 范围信息处理
+            if (qwDetail.getRange() != null) {
+                AcquisitionGetResponse.RangeInfo range = qwDetail.getRange();
+
+                // 将userList和departmentList组合成JSON字符串
+                Map<String, Object> rangeUserListMap = new HashMap<>();
+                Map<String, Object> rangeDepartmentListMap = new HashMap<>();
+                rangeUserListMap.put("userList", range.getUserList() != null ? range.getUserList() : new ArrayList<>());
+                rangeDepartmentListMap.put("departmentList", range.getDepartmentList() != null ? range.getDepartmentList() : new ArrayList<>());
+                vo.setUserList(JSON.toJSONString(rangeUserListMap));
+                vo.setDepartmentList(JSON.toJSONString(rangeDepartmentListMap));
+
+                // 生成范围描述
+                vo.setRangeDesc(buildRangeDesc(range));
+            }
+
+            // 优先分配信息处理
+            if (qwDetail.getPriorityOption() != null) {
+                AcquisitionGetResponse.PriorityInfo priority = qwDetail.getPriorityOption();
+                vo.setPriorityType(priority.getPriorityType() != null ? priority.getPriorityType() : 0);
+
+                // 将优先分配成员列表转换为JSON字符串
+                if (priority.getPriorityUseridList() != null) {
+                    vo.setPriorityUserList(JSON.toJSONString(priority.getPriorityUseridList()));
+                } else {
+                    vo.setPriorityUserList("[]");
+                }
+            }
+        }
+
+        // 补充本地数据
+        if (localData != null) {
+            // 如果企微数据中没有的字段,使用本地数据
+            if (vo.getLinkId() == null) vo.setLinkId(localData.getLinkId());
+            if (vo.getLinkName() == null) vo.setLinkName(localData.getLinkName());
+            if (vo.getUrl() == null) vo.setUrl(localData.getUrl());
+            if (vo.getSkipVerify() == null) vo.setSkipVerify(localData.getSkipVerify());
+            if (vo.getUserList() == null) vo.setUserList(localData.getUserList());
+            if (vo.getPriorityType() == null) vo.setPriorityType(localData.getPriorityType());
+            if (vo.getPriorityUserList() == null) vo.setPriorityUserList(localData.getPriorityUserList());
+            if (vo.getQwUserTableIdList() == null) vo.setQwUserTableIdList(localData.getQwUserTableIdList());
+            if (vo.getRangeDesc() == null) vo.setRangeDesc(localData.getRangeDesc());
+
+            // 本地记录特有的字段
+            vo.setId(localData.getId());
+            vo.setStatus(localData.getStatus());
+            vo.setSyncTime(localData.getSyncTime());
+            vo.setRemark(localData.getRemark());
+            vo.setCorpId(localData.getCorpId());
+            vo.setScheme(localData.getScheme());
+        } else {
+            // 纯企微数据,设置默认值
+            vo.setStatus(1);
+            vo.setCorpId(qwCompany.getCorpId());
+            if (vo.getPriorityType() == null) vo.setPriorityType(0);
+            if (vo.getPriorityUserList() == null) vo.setPriorityUserList("[]");
+            if (vo.getUserList() == null) vo.setUserList("{\"userList\":[]}");
+            if (vo.getDepartmentList() == null) vo.setDepartmentList("{\"departmentList\":[]}");
+            if (vo.getRangeDesc() == null) vo.setRangeDesc("全企业");
+        }
+
+        return vo;
+    }
+
+    /**
+     * 生成范围描述 - 确保方法签名正确
+     */
+    private String buildRangeDesc(AcquisitionGetResponse.RangeInfo range) {
+        if (range == null) {
+            return "全企业";
+        }
+
+        List<String> userList = range.getUserList();
+        List<Integer> departmentList = range.getDepartmentList();
+
+        StringBuilder desc = new StringBuilder();
+
+        if (userList != null && !userList.isEmpty()) {
+            desc.append("成员(").append(userList.size()).append("人)");
+        }
+
+        if (departmentList != null && !departmentList.isEmpty()) {
+            if (desc.length() > 0) {
+                desc.append("、");
+            }
+            desc.append("部门(").append(departmentList.size()).append("个)");
+        }
+
+        return desc.length() > 0 ? desc.toString() : "全企业";
+    }
+
+    // ==================== 获取详情方法 ====================
+
+    @Override
+    public AcquisitionAssistantDetailVO getDetailWithQw(String corpid, String corpsecret, String linkId) {
+        if (StringUtils.isEmpty(linkId)) {
+            throw new CustomException("链接ID不能为空");
+        }
+        // 1. 查询本地记录
+        QwAcquisitionAssistant query = new QwAcquisitionAssistant();
+        query.setLinkId(linkId);
+        List<QwAcquisitionAssistant> localRecords = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantList(query);
+        QwAcquisitionAssistant localData = localRecords.isEmpty() ? null : localRecords.get(0);
+
+        // 2. 获取企业信息
+        String corpId = localData != null ? localData.getCorpId() : null;
+        QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
+        if (qwCompany == null) {
+            throw new CustomException("未找到企业配置信息");
+        }
+
+        // 3. 调用企微API获取详情
+        AcquisitionGetResponse qwDetail = null;
+        try {
+            AcquisitionGetRequest request = new AcquisitionGetRequest();
+            request.setLinkId(linkId);
+
+            String url = buildApiUrl(corpid, corpsecret, QwApiConfig.getAcquisition);
+            qwDetail = callQwApi(url, request, AcquisitionGetResponse.class, "获取获客链接详情");
+        } catch (Exception e) {
+            log.error("调用企微API失败", e);
+            // 如果API调用失败,至少返回本地数据
+        }
+        // 4. 构建VO对象
+        return buildDetailVO(localData, qwDetail, qwCompany, linkId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncDetailToLocal(String corpid, String corpsecret, String linkId) {
+        AcquisitionAssistantDetailVO detail = getDetailWithQw(corpid, corpsecret, linkId);
+        QwAcquisitionAssistant assistant = convertToLocal(linkId, detail);
+
+        QwAcquisitionAssistant existData = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantByLinkId(linkId);
+
+        if (existData != null) {
+            assistant.setId(existData.getId());
+            setLocalFields(assistant, false);
+            qwAcquisitionAssistantMapper.updateQwAcquisitionAssistant(assistant);
+        } else {
+            setLocalFields(assistant, true);
+            qwAcquisitionAssistantMapper.insertQwAcquisitionAssistant(assistant);
+        }
+    }
+
+    // ==================== 新增方法 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public QwAcquisitionAssistant createWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant) {
+        // 参数校验
+        if (StringUtils.isEmpty(assistant.getLinkName())) {
+            throw new CustomException("链接名称不能为空");
+        }
+        if (assistant.getLinkName().length() > 30) {
+            throw new CustomException("链接名称不能超过30个字符");
+        }
+
+        // 构建请求
+        JSONObject request = new JSONObject();  // 改用JSONObject以便灵活构建
+        request.put("link_name", assistant.getLinkName());
+        request.put("skip_verify", assistant.getSkipVerify());
+        //request.put("mark_source", assistant.getMarkSource());
+
+        // 构建range对象 - 即使没有值也要传空对象
+        JSONObject range = new JSONObject();
+
+        // 如果有成员列表
+        if (assistant.getUserListParam() != null && !assistant.getUserListParam().isEmpty()) {
+            range.put("user_list", assistant.getUserListParam());
+        }
+
+        // 如果有部门列表
+        if (assistant.getDepartmentListParam() != null && !assistant.getDepartmentListParam().isEmpty()) {
+            range.put("department_list", assistant.getDepartmentListParam());
+        }
+
+        // 无论是否有值,都添加range字段
+        request.put("range", range);
+
+        // 构建priority_option - 可选
+        if (assistant.getPriorityType() != null && assistant.getPriorityType() > 0) {
+            JSONObject priorityOption = new JSONObject();
+            priorityOption.put("priority_type", assistant.getPriorityType());
+
+            // 如果priority_type为2,需要指定priority_userid_list
+            if (assistant.getPriorityType() == 2) {
+                if (assistant.getPriorityUserListParam() == null || assistant.getPriorityUserListParam().isEmpty()) {
+                    throw new CustomException("优先分配类型为指定范围内时,优先分配成员不能为空");
+                }
+                priorityOption.put("priority_userid_list", assistant.getPriorityUserListParam());
+            }
+
+            request.put("priority_option", priorityOption);
+        }
+
+        // 调用企微API
+        String url = buildApiUrl(corpid, corpsecret, QwApiConfig.createAcquisition);
+
+        AcquisitionCreateResponse response = callQwApi(url, request, AcquisitionCreateResponse.class, "创建获客链接");
+
+        // 设置企微返回的数据
+        if (response.getLink() != null) {
+            assistant.setLinkId(response.getLink().getLinkId());
+            assistant.setUrl(response.getLink().getUrl());
+            if (response.getLink().getCreateTime() != null) {
+                assistant.setQwCreateTime(new Date(response.getLink().getCreateTime() * 1000));
+            }
+        }
+
+        // 设置本地字段并保存
+        setLocalFields(assistant, true);
+        qwAcquisitionAssistantMapper.insertQwAcquisitionAssistant(assistant);
+
+        return assistant;
+    }
+
+    // ==================== 编辑方法 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public QwAcquisitionAssistant updateWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant) {
+        // 参数校验
+        if (assistant.getId() == null) {
+            throw new CustomException("ID不能为空");
+        }
+        if (StringUtils.isEmpty(assistant.getLinkId())) {
+            throw new CustomException("链接ID不能为空");
+        }
+
+        // 查询本地是否存在
+        QwAcquisitionAssistant existAssistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(assistant.getId());
+        if (existAssistant == null) {
+            throw new CustomException("获客链接不存在");
+        }
+
+        // 构建请求
+        AcquisitionBaseRequest request = new AcquisitionBaseRequest();
+        request.setLinkId(assistant.getLinkId());
+        if (StringUtils.isNotEmpty(assistant.getLinkName())) {
+            request.setLinkName(assistant.getLinkName());
+        }
+        request.setSkipVerify(assistant.getSkipVerify());
+
+        //request.setMarkSource(assistant.getMarkSource());
+
+        // 调用企微API
+        String url = buildApiUrl(corpid, corpsecret, QwApiConfig.updateAcquisition);
+        AcquisitionUpdateResponse response = callQwApi(url, request, AcquisitionUpdateResponse.class, "更新获客链接");
+
+        // 更新本地字段
+        setLocalFields(assistant, false);
+
+        int rows = qwAcquisitionAssistantMapper.updateQwAcquisitionAssistant(assistant);
+        if (rows <= 0) {
+            throw new CustomException("本地数据更新失败");
+        }
+
+        return assistant;
+    }
+
+    // ==================== 删除方法 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant) {
+        // 参数校验
+        if (assistant == null || assistant.getId() == null) {
+            throw new CustomException("参数不能为空");
+        }
+        if (StringUtils.isEmpty(assistant.getLinkId())) {
+            throw new CustomException("链接ID不能为空");
+        }
+
+        // 查询本地是否存在
+        QwAcquisitionAssistant existAssistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(assistant.getId());
+        if (existAssistant == null) {
+            throw new CustomException("获客链接不存在");
+        }
+
+        // 构建删除请求
+        JSONObject request = new JSONObject();
+        request.put("link_id", assistant.getLinkId());
+
+        // 调用企微API
+        String url = buildApiUrl(corpid, corpsecret, QwApiConfig.deleteAcquisition);
+        AcquisitionDeleteResponse response = callQwApi(url, request, AcquisitionDeleteResponse.class, "删除获客链接");
+
+        // 删除本地记录
+        int rows = qwAcquisitionAssistantMapper.deleteQwAcquisitionAssistantById(assistant.getId());
+        if (rows <= 0) {
+            throw new CustomException("本地数据删除失败");
+        }
+    }
+
+    // ==================== 查询方法 ====================
+
+    @Override
+    public QwAcquisitionAssistant selectQwAcquisitionAssistantById(Long id) {
+        QwAcquisitionAssistant assistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(id);
+        if (assistant != null) {
+            assistant.parseJsonFields();
+        }
+        return assistant;
+    }
+}

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

@@ -53,6 +53,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
 
 import java.io.*;
 import java.net.URL;
@@ -1556,6 +1557,34 @@ public class QwUserServiceImpl implements IQwUserService
         return qwUserMapper.selectQwCompanyListOptionsVOBySys();
     }
 
+    @Override
+    public List<QwUser> selectQwUserListByAcquisition(QwUser qwUser) {
+        qwUser.setIsDel(0);// 只查询未删除的
+        List<QwUser> qwUserList = qwUserMapper.selectQwUserList(qwUser);
+        if (CollectionUtils.isEmpty(qwUserList)){
+            return Collections.emptyList();
+        }
+        return qwUserList;
+    }
+
+    @Override
+    public List<QwUser> selectQwUserListByQwUserIds(List<String> qwUserIds) {
+        List<QwUser> qwUserList = qwUserMapper.selectQwUserListByQwUserIds(qwUserIds);
+        if (CollectionUtils.isEmpty(qwUserList)){
+            return Collections.emptyList();
+        }
+        return qwUserList;
+    }
+
+    @Override
+    public List<QwUser> selectQwUserListByIds(List<Long> ids) {
+        List<QwUser> qwUserList = qwUserMapper.selectQwUserListByIds(ids);
+        if (CollectionUtils.isEmpty(qwUserList)){
+            return Collections.emptyList();
+        }
+        return qwUserList;
+    }
+
 
     /**
      * 构建查询条件

+ 64 - 0
fs-service/src/main/java/com/fs/qw/vo/AcquisitionAssistantDetailVO.java

@@ -0,0 +1,64 @@
+package com.fs.qw.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import java.util.Date;
+
+/**
+ * 获客链接详情VO - 返回给前端的统一格式
+ */
+@Data
+public class AcquisitionAssistantDetailVO {
+    
+    // 本地记录ID
+    private Long id;
+    
+    // 企业ID
+    private String corpId;
+    
+    // 企微链接ID
+    private String linkId;
+    
+    // 链接名称
+    private String linkName;
+    
+    // 链接URL
+    private String url;
+    
+    // 链接Scheme
+    private String scheme;
+    
+    // 是否无需验证
+    private Boolean skipVerify;
+    
+    // 优先分配类型(0:不启用,1:全企业,2:指定范围内)
+    private Integer priorityType;
+    
+    // 关联成员列表(JSON字符串:{"userList":[]})
+    private String userList;
+
+    // 关联成员部门列表(JSON字符串:{"departmentList":[]})
+    private String departmentList;
+
+    //关联成员qw_user表的主键id列表
+    private String qwUserTableIdList;
+
+    // 优先分配成员列表(JSON字符串:{"priority_userid_list":["tom","lisi"]})
+    private String priorityUserList;
+    
+    // 使用范围描述
+    private String rangeDesc;
+    
+    // 状态(1:正常,2:已失效)
+    private Integer status;
+    
+    // 企微创建时间
+    private Long qwCreateTime;
+    
+    // 最后同步时间
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date syncTime;
+    
+    // 备注
+    private String remark;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/qwApi/config/QwApiConfig.java

@@ -331,4 +331,29 @@ public interface QwApiConfig {
     String sendMsg="https://qyapi.weixin.qq.com/cgi-bin/message/send";
 
     String openidToExternalUserid="https://qyapi.weixin.qq.com/cgi-bin/corpgroup/unionid_to_external_userid";
+
+    /**
+     * 获取获客链接列表
+     * */
+    String listAcquisition="https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/list_link";
+
+    /**
+     * 获取获客链接详情
+     * */
+    String getAcquisition="https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get";
+
+    /**
+     * 创建获客链接
+     * */
+    String createAcquisition="https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/create_link";
+
+    /**
+     * 编辑获客链接
+     * */
+    String updateAcquisition="https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/update_link";
+
+    /**
+     * 删除获客链接
+     * */
+    String deleteAcquisition="https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/delete_link";
 }

+ 188 - 0
fs-service/src/main/resources/mapper/qw/QwAcquisitionAssistantMapper.xml

@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwAcquisitionAssistantMapper">
+
+    <resultMap type="QwAcquisitionAssistant" id="QwAcquisitionAssistantResult">
+        <id     property="id"               column="id"               />
+        <result property="linkId"            column="link_id"           />
+        <result property="linkName"          column="link_name"         />
+        <result property="url"               column="url"               />
+        <result property="scheme"            column="scheme"            />
+        <result property="qwCreateTime"       column="qw_create_time"    />
+        <result property="skipVerify"         column="skip_verify"       />
+        <result property="priorityType"       column="priority_type"     />
+        <result property="userList"           column="user_list"         />
+        <result property="departmentList"     column="department_list"   />
+        <result property="priorityUserList"   column="priority_user_list"/>
+        <result property="qwUserTableIdList"       column="qw_user_table_id_list"/>
+        <result property="rangeDesc"          column="range_desc"        />
+        <result property="remark"             column="remark"            />
+        <result property="createBy"           column="create_by"         />
+        <result property="createTime"         column="create_time"       />
+        <result property="updateBy"           column="update_by"         />
+        <result property="updateTime"         column="update_time"       />
+        <result property="delFlag"            column="del_flag"          />
+        <result property="corpId"            column="corp_id"          />
+        <result property="syncTime"            column="sync_time"          />
+    </resultMap>
+
+    <sql id="selectQwAcquisitionAssistantVo">
+        select id, link_id, link_name, url, scheme, qw_create_time, skip_verify,
+               mark_source, priority_type, user_list, department_list, priority_user_list,
+               qw_user_table_id_list,range_desc, status, remark,
+               create_by, create_time, update_by, update_time, del_flag,corp_id,sync_time
+        from qw_acquisition_assistant
+    </sql>
+
+    <!-- 根据ID查询 -->
+    <select id="selectQwAcquisitionAssistantById" parameterType="Long" resultMap="QwAcquisitionAssistantResult">
+        <include refid="selectQwAcquisitionAssistantVo"/>
+        where id = #{id}
+    </select>
+
+    <!-- 根据linkId查询 -->
+    <select id="selectQwAcquisitionAssistantByLinkId" parameterType="String" resultMap="QwAcquisitionAssistantResult">
+        <include refid="selectQwAcquisitionAssistantVo"/>
+        where link_id = #{linkId} and del_flag = '0'
+    </select>
+
+    <!-- 查询列表 -->
+    <select id="selectQwAcquisitionAssistantList" parameterType="QwAcquisitionAssistant" resultMap="QwAcquisitionAssistantResult">
+        <include refid="selectQwAcquisitionAssistantVo"/>
+        <where>
+            del_flag = '0'
+            <if test="linkId != null and linkId != ''">
+                and link_id = #{linkId}
+            </if>
+            <if test="linkName != null and linkName != ''">
+                and link_name like concat('%', #{linkName}, '%')
+            </if>
+            <if test="skipVerify != null">
+                and skip_verify = #{skipVerify}
+            </if>
+            <if test="priorityType != null">
+                and priority_type = #{priorityType}
+            </if>
+            <if test="rangeDesc != null and rangeDesc != ''">
+                and range_desc like concat('%', #{rangeDesc}, '%')
+            </if>
+            <if test="status != null">
+                and status = #{status}
+            </if>
+            <if test="corpId != null">
+                and corp_id = #{corpId}
+            </if>
+            <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
+                and date_format(qw_create_time, '%y%m%d') &gt;= date_format(#{params.beginTime}, '%y%m%d')
+            </if>
+            <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
+                and date_format(qw_create_time, '%y%m%d') &lt;= date_format(#{params.endTime}, '%y%m%d')
+            </if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <!-- 新增 -->
+    <insert id="insertQwAcquisitionAssistant" parameterType="QwAcquisitionAssistant" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_acquisition_assistant
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="linkId != null">link_id,</if>
+            <if test="linkName != null">link_name,</if>
+            <if test="url != null">url,</if>
+            <if test="scheme != null">scheme,</if>
+            <if test="qwCreateTime != null">qw_create_time,</if>
+            <if test="skipVerify != null">skip_verify,</if>
+            <if test="priorityType != null">priority_type,</if>
+            <if test="userList != null">user_list,</if>
+            <if test="departmentList != null">department_list,</if>
+            <if test="priorityUserList != null">priority_user_list,</if>
+            <if test="qwUserTableIdList != null">qw_user_table_id_list,</if>
+            <if test="rangeDesc != null">range_desc,</if>
+            <if test="status != null">status,</if>
+            <if test="remark != null">remark,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="delFlag != null">del_flag,</if>
+            <if test="corpId != null">corp_id,</if>
+            <if test="syncTime != null">sync_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="linkId != null">#{linkId},</if>
+            <if test="linkName != null">#{linkName},</if>
+            <if test="url != null">#{url},</if>
+            <if test="scheme != null">#{scheme},</if>
+            <if test="qwCreateTime != null">#{qwCreateTime},</if>
+            <if test="skipVerify != null">#{skipVerify},</if>
+            <if test="priorityType != null">#{priorityType},</if>
+            <if test="userList != null">#{userList},</if>
+            <if test="departmentList != null">#{departmentList},</if>
+            <if test="priorityUserList != null">#{priorityUserList},</if>
+            <if test="qwUserTableIdList != null">#{qwUserTableIdList},</if>
+            <if test="rangeDesc != null">#{rangeDesc},</if>
+            <if test="status != null">#{status},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="delFlag != null">#{delFlag},</if>
+            <if test="corpId != null">#{corpId},</if>
+            <if test="syncTime != null">#{syncTime},</if>
+        </trim>
+    </insert>
+
+    <!-- 修改 -->
+    <update id="updateQwAcquisitionAssistant" parameterType="QwAcquisitionAssistant">
+        update qw_acquisition_assistant
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="linkName != null">link_name = #{linkName},</if>
+            <if test="url != null">url = #{url},</if>
+            <if test="scheme != null">scheme = #{scheme},</if>
+            <if test="qwCreateTime != null">qw_create_time = #{qwCreateTime},</if>
+            <if test="skipVerify != null">skip_verify = #{skipVerify},</if>
+            <if test="priorityType != null">priority_type = #{priorityType},</if>
+            <if test="userList != null">user_list = #{userList},</if>
+            <if test="departmentList != null">department_list = #{departmentList},</if>
+            <if test="priorityUserList != null">priority_user_list = #{priorityUserList},</if>
+            <if test="qwUserTableIdList != null">qw_user_table_id_list = #{qwUserTableIdList},</if>
+            <if test="rangeDesc != null">range_desc = #{rangeDesc},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="corpId != null">corp_id = #{corpId},</if>
+            <if test="syncTime != null">sync_time = #{syncTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <!-- 更新状态 -->
+    <update id="updateStatus" parameterType="QwAcquisitionAssistant">
+        update qw_acquisition_assistant
+        set status = #{status},
+            update_time = sysdate()
+        where id = #{id}
+    </update>
+
+    <!-- 删除(逻辑删除) -->
+    <delete id="deleteQwAcquisitionAssistantById" parameterType="Long">
+        update qw_acquisition_assistant
+        set del_flag = '1', status = 0
+        where id = #{id}
+    </delete>
+
+    <!-- 批量删除(逻辑删除) -->
+    <delete id="deleteQwAcquisitionAssistantByIds" parameterType="String">
+        update qw_acquisition_assistant
+        set del_flag = '1', status = 0
+        where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+</mapper>

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

@@ -276,4 +276,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where status = 0 and server_status = 1
     </select>
 
+    <select id="selectQwUserListByQwUserIds" parameterType="java.util.List" resultMap="QwUserResult">
+        <include refid="selectQwUserVo"/>
+        <where>
+            <if test="companyId != null">
+                and company_id = #{companyId}
+            </if>
+            and qw_user_id in
+            <foreach collection="list" item="item" open="(" separator="," close=")">
+                #{item}
+            </foreach>
+            and is_del = 0
+        </where>
+    </select>
+
+    <select id="selectQwUserListByIds" parameterType="java.util.List" resultMap="QwUserResult">
+        <include refid="selectQwUserVo"/>
+        <where>
+            <if test="companyId != null">
+                and company_id = #{companyId}
+            </if>
+            and id in
+            <foreach collection="list" item="item" open="(" separator="," close=")">
+                #{item}
+            </foreach>
+            and is_del = 0
+        </where>
+    </select>
+
 </mapper>