Selaa lähdekoodia

Merge remote-tracking branch 'origin/master'

三七 2 päivää sitten
vanhempi
commit
5c28cc4482
44 muutettua tiedostoa jossa 2487 lisäystä ja 32 poistoa
  1. 1 0
      fs-ai-call-task/src/main/resources/logback.xml
  2. 1 0
      fs-cid-workflow/src/main/resources/logback.xml
  3. 51 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java
  4. 250 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  5. 15 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  6. 4 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java
  7. 9 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  8. 2 0
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  9. 9 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java
  10. 3 2
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  11. 45 9
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  12. 86 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  13. 3 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java
  14. 14 14
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  15. 79 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneVO.java
  16. 4 2
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  17. 137 0
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java
  18. 26 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionBaseRequest.java
  19. 27 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionCreateResponse.java
  20. 9 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionDeleteResponse.java
  21. 15 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetRequest.java
  22. 63 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetResponse.java
  23. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListRequest.java
  24. 19 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListResponse.java
  25. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionPriority.java
  26. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionRange.java
  27. 9 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionUpdateResponse.java
  28. 92 0
      fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionAssistantMapper.java
  29. 6 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  30. 100 0
      fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionAssistantService.java
  31. 18 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  32. 767 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java
  33. 20 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  34. 146 0
      fs-service/src/main/java/com/fs/qw/utils/UniqueStringUtil.java
  35. 65 0
      fs-service/src/main/java/com/fs/qw/vo/AcquisitionAssistantDetailVO.java
  36. 25 0
      fs-service/src/main/java/com/fs/qwApi/config/QwApiConfig.java
  37. 50 0
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml
  38. 2 0
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  39. 2 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml
  40. 211 0
      fs-service/src/main/resources/mapper/qw/QwAcquisitionAssistantMapper.xml
  41. 11 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml
  42. 41 0
      fs-user-app/src/main/java/com/fs/app/controller/CustomerLinkWeChatController.java
  43. 1 0
      fs-wx-api/src/main/resources/logback.xml
  44. 1 0
      fs-wx-task/src/main/resources/logback.xml

+ 1 - 0
fs-ai-call-task/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-ai-call-task/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->

+ 1 - 0
fs-cid-workflow/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-cid-workflow/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->

+ 51 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java

@@ -2,6 +2,11 @@ package com.fs.company.controller.company;
 
 import java.util.ArrayList;
 import java.util.List;
+
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -60,6 +65,52 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
         }
     }
 
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/listByCallerIdAndRoboticId")
+    public TableDataInfo listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+
+    }
+
+
+    /**
+     * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/groupList")
+    public TableDataInfo groupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticCallPhoneLogCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
+    }
+
+    /**
+     * 导出调用日志_ai打电话列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphonelog:export')")
+    @Log(title = "调用日志_ai打电话", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+        ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneVO> util = new ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneVO>(CompanyVoiceRoboticCallLogCallPhoneVO.class);
+        return util.exportExcel(list, "调用日志_ai打电话数据");
+    }
+
 //    /**
 //     * 导出调用日志_ai打电话列表
 //     */

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

+ 15 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -115,6 +115,21 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private List<Long> callerIds;
 
+    @TableField(exist = false)
+    private String roboticName;
+
+    @TableField(exist = false)
+    private Integer totalRecordCount;
+
+    @TableField(exist = false)
+    private Integer successCount;
+
+    @TableField(exist = false)
+    private Integer failCount;
+
+    @TableField(exist = false)
+    private Integer runningCount;
+
     public static CompanyVoiceRoboticCallLogCallphone initCallLog( String runParam, Long keyId, Long taskId,Long companyId) {
         CompanyVoiceRoboticCallLogCallphone log = new CompanyVoiceRoboticCallLogCallphone();
         log.callerId = keyId;

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

@@ -88,4 +88,8 @@ public class CompanyWxClient extends BaseEntityTow
     /** 是否微信 */
     @Excel(name = "加微类型1个微2企微(防止add_type被占用)")
     private Integer isWeCom;
+    /**
+     * 投流来源id
+     */
+    private String traceId;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -4,6 +4,8 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -75,4 +77,11 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * @return 当天通话次数,如果没有记录返回0
      */
     int countTodayCallsByBusinessId(@Param("businessId") Long businessId);
+
+    List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+
+
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java

@@ -162,5 +162,7 @@ public class EntryCustomerParam {
     private Integer sceneType;
     //对话图
     private String dialogue;
+    //投流id
+    private String traceId;
 
 }

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

@@ -3,6 +3,9 @@ package com.fs.company.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
 /**
  * 调用日志_ai打电话Service接口
@@ -73,4 +76,10 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
      * @return
      */
     List<Long> getCallerIdsByCustomerId(Long customerId);
+
+    List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 }

+ 3 - 2
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -97,9 +97,10 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
     void finishAddWxByCallees(Set<Long> roboticIds);
 
     /**
-     *
+     * 入流程执行
      * @param taskId
      * @param crmCustomerId
+     * @param traceId
      */
-    void addNewExec4Task(Long taskId,Long crmCustomerId);
+    void addNewExec4Task(Long taskId,Long crmCustomerId,String traceId);
 }

+ 45 - 9
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -2,12 +2,10 @@ package com.fs.company.service.impl;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
@@ -19,28 +17,30 @@ import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.param.getDialogMapDomain;
 import com.fs.aicall.service.AiCallService;
 import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
-import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
-import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.vo.CidConfigVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.DictVO;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.store.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.impl.SysDictTypeServiceImpl;
 import com.fs.voice.constant.Constant;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
@@ -77,6 +77,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
+    @Autowired
+    SysDictTypeServiceImpl sysDictTypeService;
     /**
      * 查询调用日志_ai打电话
      *
@@ -308,7 +310,17 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 companyVoiceRoboticCallLog.setCallCreateTime(createTime);
                 Long answerTime = result.getCallEndTime();
                 companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                companyVoiceRoboticCallLog.setIntention(result.getIntent());
+                String intention = result.getIntent();
+                String intentf = null;
+                List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+                if (!isPositiveInteger(intention)) {
+                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
+                    if (firstDict.isPresent()) {
+                        SysDictData sysDictData = firstDict.get();
+                        intentf = sysDictData.getDictValue();
+                    }
+                }
+                companyVoiceRoboticCallLog.setIntention(intentf);
                 companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
                 BigDecimal callCharge = cidConfigVO.getCallCharge();
                 //
@@ -367,4 +379,28 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     public List<Long> getCallerIdsByCustomerId(Long customerId) {
         return companyVoiceRoboticCalleesMapper.getCallerIdsByCustomerId(customerId);
     }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
+    }
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount() {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogCount();
+    }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+    }
+    /**
+     * 判断整数
+     *
+     * @param str
+     * @return
+     */
+    public boolean isPositiveInteger(String str) {
+        return str != null && str.matches("[1-9]\\d*");
+    }
 }

+ 86 - 5
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -129,6 +129,13 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     final int BATCH_SIZE = 1500;
 
+    /** EasyCall intent 意向度重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String EASYCALL_INTENT_RETRY_KEY = "easycall:intent:retry:";
+    /** intent 意向度等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int EASYCALL_INTENT_MAX_RETRY = 5;
+    /** 每次重试等待时长(毫秒) */
+    private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
+
     /**
      * 查询机器人外呼任务
      *
@@ -819,18 +826,90 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         log.info("进入easyCall外呼结果查询结果callPhoneRes:{}", JSON.toJSONString(callPhoneRes));
+        // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+            String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                doRetryCallerResult4EasyCall(result, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 intent 为空(意向未知)兜底继续处理
+                log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // intent 已有值,直接正常处理
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 延迟重试处理 EasyCall 外呼回调(等待 intent 意向度异步评估完成)
+     * 每次重试前等待 {@link #EASYCALL_INTENT_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryCallerResult4EasyCall(CdrDetailVo result, int currentRetry) {
+        try {
+            Thread.sleep(EASYCALL_INTENT_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("easyCall intent重试等待被中断, uuid={}", result.getUuid());
+            return;
+        }
+        log.info("easyCall intent重试第{}次开始, uuid={}", currentRetry, result.getUuid());
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if (null == callPhoneRes) {
+            log.error("easyCall intent重试时仍未查询到外呼结果, uuid={}", result.getUuid());
+            return;
+        }
+        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+            // intent 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall intent仍未评估完成,uuid={},第{}次继续延迟重试", result.getUuid(), retryCount + 1);
+                doRetryCallerResult4EasyCall(result, retryCount + 1);
+            } else {
+                log.warn("easyCall intent在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // intent 已评估完成,正常处理
+        log.info("easyCall intent重试第{}次成功获取到意向度={},uuid={}", currentRetry, callPhoneRes.getIntent(), result.getUuid());
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 执行 EasyCall 外呼回调核心业务处理(推送对话内容、更新通话日志)
+     * 供 {@link #callerResult4EasyCall} 和重试逻辑统一调用
+     */
+    private void doHandleEasyCallResult(EasyCallCallPhoneVO callPhoneRes) {
         //等待数据信息
         JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
         String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
         if (StringUtils.isBlank(cacheString)) {
-            log.error("easyCall外呼回调缓存信息缺失:{}", JSON.toJSONString(result));
+            log.error("easyCall外呼回调缓存信息缺失, uuid={}", callPhoneRes.getUuid());
             return;
         }
         JSONObject cacheInfo = JSONObject.parseObject(cacheString);
         pushDialogContent4EasyCall(cacheInfo, callPhoneRes);
         CompanyVoiceRoboticCallees callee = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
         companyVoiceRoboticCallLogCallphoneService.asyncHandleCalleeCallBackResult4EasyCall(callPhoneRes, callee);
-        System.out.println(callPhoneRes);
+        log.info("easyCall外呼回调业务处理完成, uuid={}, intent={}", callPhoneRes.getUuid(), callPhoneRes.getIntent());
     }
 
     public void pushDialogContent(PushIIntentionResult result) {
@@ -1145,7 +1224,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     /**
      * 添加用户到场景任务
      */
-    public void addNewExec4Task(Long taskId, Long crmCustomerId) {
+    public void addNewExec4Task(Long taskId, Long crmCustomerId,String traceId) {
         //保存callees表数据
         CompanyVoiceRobotic companyVoiceRobotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(taskId);
         CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(crmCustomerId);
@@ -1154,6 +1233,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         client.setRoboticId(taskId);
         client.setCustomerId(crmCustomerId);
         client.setIsWeCom(companyVoiceRobotic.getIsWeCom());
+        client.setTraceId(traceId);
         companyWxClientServiceImpl.insertCompanyWxClient(client);
 
         CompanyVoiceRoboticCallees callee = new CompanyVoiceRoboticCallees();
@@ -1203,7 +1283,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         }
         //写入业务表数据
         CompanyVoiceRoboticBusiness companyVoiceRoboticBusiness = buildTaskBussiness4SceneTask(companyVoiceRobotic, callee);
-        //初始化流程表 todo
+        //初始化流程表
         initWorkflows4SceneTask(companyVoiceRobotic,companyVoiceRoboticBusiness);
 
     }
@@ -1750,7 +1830,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 targetExec.setCurrentNodeKey(nodeInfoVo.getTargetNodeKey());
                 targetExec.setCurrentNodeName(nodeInfoVo.getNodeName());
                 targetExec.setCurrentNodeType(NodeTypeEnum.fromCode(nodeInfoVo.getNodeType()).getValue());
-                targetExec.setStatus(ExecutionStatusEnum.FAILURE.getValue());
+                targetExec.setStatus(ExecutionStatusEnum.INTERRUPT.getValue());
                 targetExec.setStartTime(now);
                 targetExec.setVariables(variables.toJSONString());
                 targetExec.setBusinessKey(startExec.getBusinessKey());
@@ -1782,6 +1862,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         int rows = companyAiWorkflowExecMapper.insertBatchInfo(workflowExecs);
         if(rows > 0){
             workflowExecLogBatchInsert(startExecList);//第一节点
+            workflowExecs.stream().forEach(a->a.setStatus(ExecutionStatusEnum.FAILURE.getValue()));
             workflowExecLogBatchInsert(workflowExecs);//第二节点
         }
         workflowExecs.clear();

+ 3 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java

@@ -179,6 +179,9 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
         workflow.setCreateTime(now);
         workflow.setStartNodeKey(source.getStartNodeKey());
         workflow.setEndNodeKey(source.getEndNodeKey());
+        workflow.setCompanyId(source.getCompanyId());
+        workflow.setCompanyUserId(source.getCompanyUserId());
+
         companyWorkflowMapper.insertCompanyWorkflow(workflow);
         Long newWorkflowId = workflow.getWorkflowId();
 

+ 14 - 14
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -53,19 +53,19 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
      * @return
      */
     //@Override
-    public R entryCustomer(String param) {
-        try {
-            String decryptParam = CryptoUtil.decrypt(param);
-            if (StringUtils.isBlank(decryptParam)) {
-                return R.error("参数错误");
-            }
-            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
-            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
-        } catch (Exception ex) {
-            log.error("录入客户异常", ex);
-        }
-        return R.ok().put("result", "录入成功");
-    }
+//    public R entryCustomer(String param) {
+//        try {
+//            String decryptParam = CryptoUtil.decrypt(param);
+//            if (StringUtils.isBlank(decryptParam)) {
+//                return R.error("参数错误");
+//            }
+//            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
+//            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
+//        } catch (Exception ex) {
+//            log.error("录入客户异常", ex);
+//        }
+//        return R.ok().put("result", "录入成功");
+//    }
 
     @Override
     @Async("crmCustomerExecutor")
@@ -111,7 +111,7 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
             CompanyVoiceRobotic companySceneTasks = getCompanySceneTask(data);
             if(null != companySceneTasks){
                 //场景任务存在 加入场景任务队列
-                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId());
+                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId(),data.getTraceId());
             }
         }
     }

+ 79 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneVO.java

@@ -0,0 +1,79 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class CompanyVoiceRoboticCallLogCallPhoneVO {
+
+    /** $column.columnComment */
+    private Long logId;
+
+    /** 任务id */
+    @Excel(name = "任务id")
+    private Long roboticId;
+
+    @Excel(name = "任务名称")
+    private String roboticName;
+
+    /** caller_id */
+    @Excel(name = "caller_id")
+    private Long callerId;
+
+    /** 记录调用时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "记录调用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date runTime;
+
+    /** 调用参数 */
+    @Excel(name = "调用参数")
+    private String runParam;
+
+    /** 执行结果 */
+    @Excel(name = "执行结果")
+    private String result;
+
+    /** 执行状态:1、执行中,2、执行成功,3、执行失败 */
+    @Excel(name = "执行状态:1、执行中,2、执行成功,3、执行失败")
+    private Integer status;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    @Excel(name = "公司名称")
+    private String companyName;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+    @Excel(name = "销售名称")
+    private String companyUserName;
+
+    /** 客户号码 */
+    @Excel(name = "客户号码")
+    private String callerNum;
+
+    /** 话术号码 */
+    @Excel(name = "话术号码")
+    private String calleeNum;
+
+    @Excel(name = "客户类型")
+    private String intention;
+
+    /** 通话时长 */
+    @Excel(name = "通话时长(秒)")
+    private String callTime;
+
+    @Excel(name = "录音地址")
+    private String recordPath;
+
+    /** 花费金额 */
+    @Excel(name = "花费金额")
+    private BigDecimal cost;
+
+}

+ 4 - 2
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -190,7 +190,9 @@ public class CrmCustomer extends BaseEntity
     private String shopName;
     // 平台名称
     private String platformName;
-
-
+    /**
+     * 投流来源id
+     */
+    private String traceId;
 
 }

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

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

@@ -0,0 +1,92 @@
+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);
+
+
+    /**
+     *  查询所有未逻辑删除的pageParam
+     * */
+    List<String> selectAllPageParams();
+}

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

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

@@ -0,0 +1,767 @@
+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() : "全企业";
+    }
+
+    /**
+     * 生成唯一的页面参数(针对2000条数据的优化版本)
+     */
+    private String generateUniquePageParam() {
+        // 获取所有已存在的pageParam(只取需要的字段)
+        List<String> existingParams = qwAcquisitionAssistantMapper.selectAllPageParams();
+        //使用Set,提高查找效率 O(1)
+        Set<String> paramSet = new HashSet<>(existingParams);
+
+        int maxAttempts = 10; // 设置最大尝试次数
+        int attempt = 0;
+
+        while (attempt < maxAttempts) {
+            // 生成6位随机码
+            String candidate = UniqueStringUtil.generateTimeBasedUnique(6);
+
+            // 使用Set的contains方法,O(1)复杂度
+            if (!paramSet.contains(candidate)) {
+                log.debug("生成页面参数成功: {}, 尝试次数: {}", candidate, attempt + 1);
+                return candidate;
+            }
+
+            attempt++;
+            log.debug("页面参数 {} 已存在,重新生成,第{}次尝试", candidate, attempt);
+        }
+
+        // 如果多次尝试都失败,使用+1随机数方案
+        String finalParam = UniqueStringUtil.generateTimeBasedUnique(7);
+        log.warn("多次尝试后使用7位参数: {}", finalParam);
+        return finalParam;
+    }
+
+    // ==================== 获取详情方法 ====================
+
+    @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 =generateUniquePageParam();
+
+        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);
+        //TODO重新生成页面参数,需要修改对应redis缓存
+        String oldPageParam = existAssistant.getPageParam();
+        String oldKey = QW_ACQUISITION_URL_KEY_PREFIX + oldPageParam;
+        redisCache.deleteObject(oldKey);
+        String newPageParam =generateUniquePageParam();
+        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";
 }

+ 50 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticCallLogCallphoneMapper.xml

@@ -195,4 +195,54 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND callphone.create_time >= CURDATE()
         AND callphone.create_time &lt; CURDATE() + INTERVAL 1 DAY
     </select>
+
+    <select id="selectCompanyVoiceRoboticCallPhoneLogGroupList" resultType="com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone" parameterType="com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone">
+        select
+        robotic_id,
+        cvr.name as robotic_name,
+        count(1) as totalRecordCount,
+        sum(case when status = 1 then 1 else 0 end) as runningCount,
+        sum(case when status = 2 then 1 else 0 end) as successCount,
+        sum(case when status = 3 then 1 else 0 end) as failCount
+        from company_voice_robotic_call_log_callphone t1
+        left join company_voice_robotic cvr on cvr.id = t1.robotic_id
+        <where>
+            <if test="roboticId != null">and robotic_id = #{roboticId}</if>
+        </where>
+        group by robotic_id
+    </select>
+    <select id="selectCompanyVoiceRoboticCallPhoneLogCount" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCount">
+        select
+            count(1) as recordCount,
+            sum(case when status = 2 then 1 else 0 end) as successRecordCount,
+            sum(case when run_time &gt;= CURDATE() and run_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY) then 1 else 0 end) as todayCount,
+            sum(case when status = 2 and run_time &gt;= CURDATE() and run_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY) then 1 else 0 end) as todaySuccessCount
+        from company_voice_robotic_call_log_callphone
+    </select>
+
+
+    <select id="listByRoboticId" resultType="com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO" parameterType="CompanyVoiceRoboticCallLogCallphone">
+        SELECT
+        t1.*,
+        t2.company_name,
+        t3.nick_name as companyUserName
+        FROM company_voice_robotic_call_log_callphone t1
+        left join company t2 on t1.company_id = t2.company_id
+        left join company_user t3 on t3.user_id = t1.company_user_id
+        where 1=1
+        <if test="roboticId != null">and t1.robotic_id = #{roboticId}</if>
+        <if test="callerId != null">and t1.caller_id = #{callerId}</if>
+        <if test="callerIds != null and callerIds.size() > 0">
+            AND t1.caller_id IN
+            <foreach collection='callerIds' item='item' open='(' separator=',' close=')'>
+                #{item}
+            </foreach>
+        </if>
+        <if test="callerNum != null and callerNum != ''">
+            and t1.caller_num like concat('%', #{callerNum}, '%')
+        </if>
+
+    </select>
+
+
 </mapper>

+ 2 - 0
fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml

@@ -94,6 +94,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="addTime != null">add_time,</if>
             <if test="remark != null">remark,</if>
             <if test="createTime != null">create_time,</if>
+            <if test="traceId != null">trace_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="roboticWxId != null">#{roboticWxId},</if>
@@ -105,6 +106,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="addTime != null">#{addTime},</if>
             <if test="remark != null">#{remark},</if>
             <if test="createTime != null">#{createTime},</if>
+            <if test="traceId != null">#{traceId},</if>
          </trim>
     </insert>
 

+ 2 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -171,6 +171,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="goodsName != null">goods_name,</if>
             <if test="goodsSpecification != null">goods_specification,</if>
             <if test="shopName != null">shop_name,</if>
+            <if test="traceId != null">trace_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="customerCode != null">#{customerCode},</if>
@@ -225,6 +226,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="goodsName != null">#{goodsName},</if>
             <if test="goodsSpecification != null">#{goodsSpecification},</if>
             <if test="shopName != null">#{shopName},</if>
+            <if test="traceId != null">#{traceId},</if>
          </trim>
     </insert>
 

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

@@ -0,0 +1,211 @@
+<?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>
+
+    <!-- 查询所有pageParam -->
+    <select id="selectAllPageParams" resultType="java.lang.String">
+        SELECT page_param
+        FROM qw_acquisition_assistant
+        WHERE del_flag = '0'
+    </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);
+    }
+}

+ 1 - 0
fs-wx-api/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="groupNo" source="group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-ai-websocket/${groupNo}/logs" />
     <!-- 日志输出格式 -->

+ 1 - 0
fs-wx-task/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-wx-task/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->