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

销售端新增接入获客链接功能

cgp 4 дней назад
Родитель
Сommit
c512652433
24 измененных файлов с 2058 добавлено и 0 удалено
  1. 250 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  2. 137 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. 63 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. 19 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListResponse.java
  10. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionPriority.java
  11. 16 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. 86 0
      fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionAssistantMapper.java
  14. 6 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  15. 100 0
      fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionAssistantService.java
  16. 18 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  17. 733 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java
  18. 20 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  19. 146 0
      fs-service/src/main/java/com/fs/qw/utils/UniqueStringUtil.java
  20. 65 0
      fs-service/src/main/java/com/fs/qw/vo/AcquisitionAssistantDetailVO.java
  21. 25 0
      fs-service/src/main/java/com/fs/qwApi/config/QwApiConfig.java
  22. 204 0
      fs-service/src/main/resources/mapper/qw/QwAcquisitionAssistantMapper.xml
  23. 11 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml
  24. 41 0
      fs-user-app/src/main/java/com/fs/app/controller/CustomerLinkWeChatController.java

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

@@ -0,0 +1,250 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.acquisition.AcquisitionListResponse;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
+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 java.util.Collections;
+import java.util.List;
+
+/**
+ * 企微-获客链接管理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 {
+            // 调用获取详情服务
+            AcquisitionAssistantDetailVO detailVo = qwAcquisitionAssistantService.getDetailWithQw(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("未找到主体");
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/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;
+    }
+}

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

@@ -0,0 +1,137 @@
+package com.fs.qw.domain;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.BaseEntity;
+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 String skipVerify;
+
+//    /**
+//     * 是否标记客户添加来源为该应用创建的获客链接, 默认值为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;
+
+    /** 页面参数 */
+    private String pageParam;
+
+    /**
+     * 将参数列表转换为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;
+}

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

@@ -0,0 +1,63 @@
+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 String 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;
+}

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

@@ -0,0 +1,19 @@
+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;
+}

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

@@ -0,0 +1,16 @@
+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;
+}

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

@@ -0,0 +1,16 @@
+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;
+}

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

@@ -0,0 +1,86 @@
+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);
+
+    /**
+     * 根据pageParam查询
+     *
+     * @param pageParam
+     * @return
+     */
+    String selectQwAcquisitionUrlByPageParam(String pageParam);
+}

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

@@ -520,4 +520,10 @@ public interface QwUserMapper extends BaseMapper<QwUser>
     List<QwUser> selectQwUserByQwUserIds(@Param("list") List<String> userIdsLong);
 
     QwUserVO getQwUserCompanyInfo(@Param("qwUserId") Long qwUserId);
+
+
+    /**
+     *  根据主键ID集合查询企微用户
+     * */
+    List<QwUser> selectQwUserListByIds(List<Long> ids);
 }

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

@@ -0,0 +1,100 @@
+package com.fs.qw.service;
+
+import com.fs.common.exception.CustomException;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+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 linkId 获客链接ID
+     * @return 企微返回的详情数据
+     * @throws CustomException 当调用企微API失败时抛出
+     */
+    public AcquisitionAssistantDetailVO getDetailWithQw(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);
+
+    /**
+     * 根据页面参数查询获客链接url
+     * */
+    public String selectQwAcquisitionUrlByPageParam(String pageParam);
+
+}

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

@@ -211,4 +211,22 @@ public interface IQwUserService
     R updateQwUserFastGptRoleStatusById(Long id);
 
     QwUserVO getQwUserCompanyInfo(Long qwUserId);
+
+    /**
+     * 获客链接---查询企微用户列表
+     *
+     * @param qwUser 企微用户
+     * @return 企微用户列表
+     *
+     * */
+    public List<QwUser> selectQwUserListByAcquisition(QwUser qwUser);
+
+    /**
+     * 获客链接---查询企微用户列表
+     *
+     * @param ids 企微用户主键id集合
+     * @return 企微用户列表
+     *
+     * */
+    public List<QwUser> selectQwUserListByIds(List<Long> ids);
 }

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

@@ -0,0 +1,733 @@
+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.utils.UniqueStringUtil;
+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:";
+
+
+    // 获客链接-页面参数-url的key
+    private static final String QW_ACQUISITION_URL_KEY_PREFIX = "qw:acquisition:url: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 qwApiUrl, Object request, Class<T> responseClass, String operationName) {
+        log.info("调用企微{},参数:{}", operationName, JSON.toJSONString(request));
+        String result = HttpUtil.sendAuthPost(request, qwApiUrl);
+        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 qwApiUrl) {
+        if (StringUtils.isNotEmpty(qwApiUrl)) {
+            try {
+                String encodedUrl = java.net.URLEncoder.encode(qwApiUrl, "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(DateUtils.getNowDate());
+        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 qwApiUrl = buildApiUrl(corpid, corpsecret, QwApiConfig.listAcquisition);
+        return callQwApi(qwApiUrl, 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(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());
+                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 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 qwApiUrl = buildApiUrl(qwCompany.getCorpId(), qwCompany.getOpenSecret(), QwApiConfig.getAcquisition);
+            qwDetail = callQwApi(qwApiUrl, 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(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", Boolean.parseBoolean(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 qwApiUrl = buildApiUrl(corpid, corpsecret, QwApiConfig.createAcquisition);
+
+        AcquisitionCreateResponse response = callQwApi(qwApiUrl, request, AcquisitionCreateResponse.class, "创建获客链接");
+        if (response.getLink() == null || StringUtils.isEmpty(response.getLink().getUrl())) {
+            log.error("企微创建获客链接成功但未返回URL,response: {}", JSON.toJSONString(response));
+            throw new CustomException("创建获客链接成功但未获取到访问URL");
+        }
+        /// 设置企微返回的数据
+        String friendUrl = response.getLink().getUrl();
+        assistant.setUrl(friendUrl);  // 保存企微返回的URL
+        assistant.setLinkId(response.getLink().getLinkId());
+
+        if (response.getLink().getCreateTime() != null) {
+            assistant.setQwCreateTime(new Date(response.getLink().getCreateTime() * 1000));
+        }
+
+        //生成随机网页参数编码(字母+数字)
+        String randomParam = UniqueStringUtil.generateShortUnique();
+        assistant.setPageParam(randomParam);
+
+        // 生成scheme
+        assistant.setScheme(generateScheme(friendUrl));
+
+        // 设置本地字段并保存
+        setLocalFields(assistant, true);
+        qwAcquisitionAssistantMapper.insertQwAcquisitionAssistant(assistant);
+
+        // ========== 缓存URL,便于后续通过pageParam访问 ==========
+        try {
+            String cacheKey = QW_ACQUISITION_URL_KEY_PREFIX + randomParam;
+            Integer cacheExpire = 10; // 默认缓存10天
+            redisCache.setCacheObject(cacheKey, friendUrl, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomParam, friendUrl);
+        } catch (Exception e) {
+            // 缓存失败不影响主流程,但需要记录日志
+            log.error("获客链接URL缓存失败, pageParam: {}", randomParam, e);
+        }
+        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(Boolean.parseBoolean(assistant.getSkipVerify()));
+
+        //request.setMarkSource(assistant.getMarkSource());
+
+        // 调用企微API
+        String qwApiUrl = buildApiUrl(corpid, corpsecret, QwApiConfig.updateAcquisition);
+        AcquisitionUpdateResponse response = callQwApi(qwApiUrl, request, AcquisitionUpdateResponse.class, "更新获客链接");
+
+        // 更新本地字段
+        setLocalFields(assistant, false);
+        //重新生成页面参数,需要修改对应redis缓存
+        String oldPageParam = existAssistant.getPageParam();
+        String oldKey = QW_ACQUISITION_URL_KEY_PREFIX + oldPageParam;
+        redisCache.deleteObject(oldKey);
+        String newPageParam = UniqueStringUtil.generateShortUnique();
+        String newKey = QW_ACQUISITION_URL_KEY_PREFIX + newPageParam;
+        Integer cacheExpire = 10;//默认缓存10天
+        redisCache.setCacheObject(newKey, existAssistant.getUrl(), cacheExpire, TimeUnit.DAYS);
+
+        assistant.setPageParam(newPageParam);
+
+        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不能为空");
+        }
+
+        // 查询本地是否存在(获取完整的记录,包括pageParam)
+        QwAcquisitionAssistant existAssistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(assistant.getId());
+        if (existAssistant == null) {
+            throw new CustomException("获客链接不存在");
+        }
+
+        // 构建删除请求
+        JSONObject request = new JSONObject();
+        request.put("link_id", assistant.getLinkId());
+
+        // 调用企微API
+        String qwApiUrl = buildApiUrl(corpid, corpsecret, QwApiConfig.deleteAcquisition);
+        AcquisitionDeleteResponse response = callQwApi(qwApiUrl, request, AcquisitionDeleteResponse.class, "删除获客链接");
+
+        // ========== 删除Redis缓存 ==========
+        try {
+            // 1. 删除pageParam对应的URL缓存
+            if (StringUtils.isNotEmpty(existAssistant.getPageParam())) {
+                String urlCacheKey = QW_ACQUISITION_URL_KEY_PREFIX + existAssistant.getPageParam();
+                redisCache.deleteObject(urlCacheKey);
+                log.info("删除获客链接URL缓存成功, pageParam: {}, key: {}",
+                        existAssistant.getPageParam(), urlCacheKey);
+            }
+        } catch (Exception e) {
+            // 缓存删除失败不应该影响主流程,但需要记录日志
+            log.error("删除获客链接缓存失败, id: {}, linkId: {}, pageParam: {}",
+                    existAssistant.getId(), existAssistant.getLinkId(),
+                    existAssistant.getPageParam(), e);
+        }
+
+        // 删除本地记录
+        int rows = qwAcquisitionAssistantMapper.deleteQwAcquisitionAssistantById(assistant.getId());
+        if (rows <= 0) {
+            throw new CustomException("本地数据删除失败");
+        }
+
+        log.info("获客链接删除成功, id: {}, linkId: {}, pageParam: {}",
+                existAssistant.getId(), existAssistant.getLinkId(),
+                existAssistant.getPageParam());
+    }
+
+    // ==================== 查询方法 ====================
+
+    @Override
+    public QwAcquisitionAssistant selectQwAcquisitionAssistantById(Long id) {
+        QwAcquisitionAssistant assistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(id);
+        if (assistant != null) {
+            assistant.parseJsonFields();
+        }
+        return assistant;
+    }
+
+    @Override
+    public String selectQwAcquisitionUrlByPageParam(String pageParam) {
+        String key = QW_ACQUISITION_URL_KEY_PREFIX + pageParam;
+        String friendUrl = null;
+
+        try {
+            Object cacheObj = redisCache.getCacheObject(key);
+            if (cacheObj instanceof String) {
+                friendUrl = (String) cacheObj;
+                // 处理缓存空值的情况
+                if ("NULL".equals(friendUrl)) {
+                    return null;
+                }
+                log.debug("从缓存获取获客链接url成功,pageParam:{}", pageParam);
+                return friendUrl;
+            }
+        } catch (Exception e) {
+            log.warn("从缓存获取获客链接url异常, 将重新获取, pageParam:{}", pageParam);
+        }
+
+        // 缓存中没有,查询数据库
+        friendUrl = qwAcquisitionAssistantMapper.selectQwAcquisitionUrlByPageParam(pageParam);
+
+        // 缓存处理(包括空值缓存)
+        if (friendUrl == null) {
+            int nullCacheExpire = 10; // 10秒
+            redisCache.setCacheObject(key, "NULL", nullCacheExpire, TimeUnit.SECONDS);
+            log.info("获客链接URL不存在,缓存空值10秒, pageParam:{}", pageParam);
+            return null;
+        } else {
+            // 正常值仍缓存10天
+            Integer cacheExpire = 10;
+            redisCache.setCacheObject(key, friendUrl, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam:{}", pageParam);
+        }
+
+        return friendUrl;
+    }
+}

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

@@ -59,6 +59,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 import java.io.*;
 import java.net.URL;
@@ -1648,6 +1649,25 @@ public class QwUserServiceImpl implements IQwUserService
         return qwUserMapper.getQwUserCompanyInfo( qwUserId);
     }
 
+    @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> selectQwUserListByIds(List<Long> ids) {
+        List<QwUser> qwUserList = qwUserMapper.selectQwUserListByIds(ids);
+        if (CollectionUtils.isEmpty(qwUserList)){
+            return Collections.emptyList();
+        }
+        return qwUserList;
+    }
+
     /**
      * 根据销售公司和企微ID查询企微用户
      */

+ 146 - 0
fs-service/src/main/java/com/fs/qw/utils/UniqueStringUtil.java

@@ -0,0 +1,146 @@
+package com.fs.qw.utils;
+
+import java.security.SecureRandom;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 生成不重复的随机字符串工具类
+ * 用于生成网页参数编码、短链接等场景
+ */
+public class UniqueStringUtil {
+    
+    // 字符集:数字+大小写字母
+    private static final String CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+    private static final int BASE = CHAR_SET.length();
+    
+    // 安全随机数生成器
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+    
+    // 自增序列(保证同一毫秒内的唯一性)
+    private static final AtomicLong SEQUENCE = new AtomicLong(1);
+    
+    // 用于检查重复的缓存(可选,用于调试)
+    private static final ConcurrentHashMap<String, Boolean> GENERATED_CACHE = new ConcurrentHashMap<>();
+    
+    /**
+     * 生成指定长度的不重复随机字符串
+     * @param length 字符串长度
+     * @return 不重复的随机字符串
+     */
+    public static String generateUnique(int length) {
+        if (length <= 0 || length > 64) {
+            throw new IllegalArgumentException("Length must be between 1 and 64");
+        }
+        
+        String result;
+        do {
+            result = generateRandomString(length);
+        } while (GENERATED_CACHE.putIfAbsent(result, Boolean.TRUE) != null);
+        
+        return result;
+    }
+    
+    /**
+     * 生成带时间戳的随机字符串(确保唯一性)
+     * @param length 字符串长度
+     * @return 唯一字符串
+     */
+    public static String generateTimeBasedUnique(int length) {
+        // 时间戳部分(毫秒级,转换为36进制)
+        String timePart = Long.toString(System.currentTimeMillis(), 36);
+        
+        // 序列号部分(转换为62进制)
+        long seq = SEQUENCE.getAndIncrement();
+        if (seq >= Long.MAX_VALUE / 2) {
+            SEQUENCE.set(1);
+        }
+        String seqPart = toBase62(seq);
+        
+        // 随机部分
+        String randomPart = generateRandomString(length - Math.min(timePart.length() + seqPart.length(), length));
+        
+        // 组合
+        String result = (timePart + seqPart + randomPart).substring(0, length);
+        
+        return result;
+    }
+    
+    /**
+     * 生成短字符串(8-16位,适合做网页参数)
+     * @return 唯一短字符串
+     */
+    public static String generateShortUnique() {
+        return generateTimeBasedUnique(12);
+    }
+    
+    /**
+     * 生成长字符串(32位,类似UUID)
+     * @return 唯一长字符串
+     */
+    public static String generateLongUnique() {
+        return generateTimeBasedUnique(32);
+    }
+    
+    /**
+     * 生成随机字符串
+     */
+    private static String generateRandomString(int length) {
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            int index = SECURE_RANDOM.nextInt(BASE);
+            sb.append(CHAR_SET.charAt(index));
+        }
+        return sb.toString();
+    }
+    
+    /**
+     * 将数字转换为62进制字符串
+     */
+    private static String toBase62(long number) {
+        if (number == 0) return "0";
+        
+        StringBuilder sb = new StringBuilder();
+        long num = number;
+        
+        while (num > 0) {
+            int remainder = (int) (num % 62);
+            sb.append(CHAR_SET.charAt(remainder));
+            num = num / 62;
+        }
+        
+        return sb.reverse().toString();
+    }
+    
+    /**
+     * 生成批量唯一字符串
+     */
+    public static String[] generateBatch(int count, int length) {
+        String[] results = new String[count];
+        for (int i = 0; i < count; i++) {
+            results[i] = generateTimeBasedUnique(length);
+        }
+        return results;
+    }
+    
+    public static void main(String[] args) {
+        System.out.println("=== 测试生成唯一字符串 ===");
+        
+        // 测试生成10个短字符串
+        System.out.println("\n生成10个短字符串(12位):");
+        for (int i = 0; i < 10; i++) {
+            System.out.println(generateShortUnique());
+        }
+        
+        // 测试生成长字符串
+        System.out.println("\n生成长字符串(32位):");
+        System.out.println(generateLongUnique());
+        
+        // 测试批量生成
+        System.out.println("\n批量生成5个16位字符串:");
+        String[] batch = generateBatch(5, 16);
+        for (String s : batch) {
+            System.out.println(s);
+        }
+    }
+}

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

@@ -0,0 +1,65 @@
+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 String 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";
 }

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

@@ -0,0 +1,204 @@
+<?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"          />
+        <result property="pageParam"            column="page_param"          />
+    </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,page_param
+        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="pageParam != null">
+                and page_param = #{pageParam}
+            </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>
+
+    <select id="selectQwAcquisitionUrlByPageParam" resultType="java.lang.String" parameterType="java.lang.String">
+        SELECT url
+        FROM qw_acquisition_assistant
+        WHERE page_param = #{pageParam}
+          AND status = 1
+          AND del_flag = '0'
+        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>
+            <if test="pageParam != null">page_param,</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>
+            <if test="pageParam != null">#{pageParam},</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>
+            <if test="pageParam != null">page_param = #{pageParam},</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>

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

@@ -340,4 +340,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where qu.id = #{qwUserId}
     </select>
 
+    <select id="selectQwUserListByIds" parameterType="java.util.List" resultMap="QwUserResult">
+        <include refid="selectQwUserVo"/>
+        <where>
+            and id in
+            <foreach collection="list" item="item" open="(" separator="," close=")">
+                #{item}
+            </foreach>
+            and is_del = 0
+        </where>
+    </select>
+
 </mapper>

+ 41 - 0
fs-user-app/src/main/java/com/fs/app/controller/CustomerLinkWeChatController.java

@@ -0,0 +1,41 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 获客链接+微信
+ */
+@Slf4j
+@RestController
+@RequestMapping(value="/app/friendLinkWeChat")
+public class CustomerLinkWeChatController extends AppBaseController {
+
+    @Autowired
+    private IQwAcquisitionAssistantService qwAcquisitionAssistantService;
+
+    @GetMapping("/goToLink/{pageParam}")
+    @CrossOrigin
+    public AjaxResult goToLink(@PathVariable String pageParam) {
+        // 参数校验
+        if (StringUtils.isBlank(pageParam)) {
+            return AjaxResult.error("参数错误:页面参数不能为空");
+        }
+
+        // 获取URL
+        String url = qwAcquisitionAssistantService.selectQwAcquisitionUrlByPageParam(pageParam);
+
+        // 判断URL是否有效
+        if (StringUtils.isBlank(url)) {
+            log.info("无效的pageParam: {}", pageParam);
+            return AjaxResult.error("链接无效或已过期");
+        }
+
+        // 返回成功结果,包含URL
+        return AjaxResult.success("获取成功", url);
+    }
+}