소스 검색

渠道活码-全体在线-限制员工加客户数量/从益寿元迁移了渠道活码

三七 2 일 전
부모
커밋
7c19607887
23개의 변경된 파일1730개의 추가작업 그리고 176개의 파일을 삭제
  1. 21 0
      fs-admin/src/main/java/com/fs/qw/qwTask/qwTask.java
  2. 47 49
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  3. 449 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionLinkInfoController.java
  4. 94 4
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  5. 2 2
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  6. 5 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  7. 13 4
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java
  8. 47 0
      fs-service/src/main/java/com/fs/qw/domain/QwContactAcquisitionUser.java
  9. 9 0
      fs-service/src/main/java/com/fs/qw/domain/QwExternalContact.java
  10. 62 0
      fs-service/src/main/java/com/fs/qw/mapper/QwContactAcquisitionUserMapper.java
  11. 3 6
      fs-service/src/main/java/com/fs/qw/mapper/QwContactWayMapper.java
  12. 20 32
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  13. 19 5
      fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionAssistantService.java
  14. 62 0
      fs-service/src/main/java/com/fs/qw/service/IQwContactAcquisitionUserService.java
  15. 2 0
      fs-service/src/main/java/com/fs/qw/service/IQwContactWayService.java
  16. 40 0
      fs-service/src/main/java/com/fs/qw/service/impl/AsyncQwContactWayService.java
  17. 321 12
      fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java
  18. 91 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwContactAcquisitionUserServiceImpl.java
  19. 113 6
      fs-service/src/main/java/com/fs/qw/service/impl/QwContactWayServiceImpl.java
  20. 183 35
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  21. 24 15
      fs-service/src/main/java/com/fs/qw/vo/AcquisitionAssistantDetailVO.java
  22. 86 0
      fs-service/src/main/resources/mapper/qw/QwContactAcquisitionUserMapper.xml
  23. 17 1
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

+ 21 - 0
fs-admin/src/main/java/com/fs/qw/qwTask/qwTask.java

@@ -49,6 +49,11 @@ public class qwTask {
     @Autowired
     private IQwUserService qwUserService;
 
+    @Autowired
+    private IQwAcquisitionAssistantService qwAcquisitionAssistantService;
+
+    @Autowired
+    private IQwContactWayService contactWayService;
 
     @Autowired
     private FsStatisSalerWatchService fsStatisSalerWatchService;
@@ -380,4 +385,20 @@ public class qwTask {
     public  void  syncMultiDimensionStatistics(){
         finishCourseStatisticsSyncService.syncMultiDimensionStatistics();
     }
+
+    /**
+     * 每天重置 获客链接里的 员工账号可添加次数
+     */
+    public void resetAcquisitionLinkUserLimit() {
+        qwAcquisitionAssistantService.resetAcquisitionLinkUserLimit();
+
+    }
+
+    /**
+     * 每天重置 渠道活码里的 员工账号可添加次数
+     */
+    public void resetQwContactWayUserLimit() {
+        contactWayService.resetQwContactWayUserLimit();
+
+    }
 }

+ 47 - 49
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java

@@ -1,27 +1,28 @@
 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 java.util.Collections;
+import java.util.List;
+
 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.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
 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;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
 
 /**
  * 企微-获客链接管理Controller
@@ -56,48 +57,30 @@ public class QwAcquisitionAssistantController extends BaseController {
     }
 
     /**
-     * 从企微同步获客链接列表(全量)
-     * 手动点击同步按钮时调用
-     */
-    @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) {
+    @GetMapping("/sendAcquisitionMessage/{id}/{phone}")
+    public AjaxResult sendAcquisitionMessage(@PathVariable Long id,@PathVariable String phone) {
         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());
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser==null||loginUser.getCompany()==null){
+                throw new CustomException("请登录");
+            }
+            SendMsgLogBo sendMsgLogBo=new SendMsgLogBo();
+            sendMsgLogBo.setCompanyId(loginUser.getCompany().getCompanyId());
+            sendMsgLogBo.setCompanyUserId(loginUser.getCompany().getUserId());
+            validatePhone(phone);
+            SendResultDetailDTO sendResultDetailDTO = qwAcquisitionAssistantService.sendMessageAcquisition(phone, id,sendMsgLogBo);
+            if (sendResultDetailDTO.isSuccess()){
+                return AjaxResult.success("发送成功");
+            }else {
+                return AjaxResult.error(sendResultDetailDTO.getFailReason());
+            }
         } catch (Exception e) {
-            return AjaxResult.error("系统异常:" + e.getMessage());
+            log.error("发送失败:" + e.getMessage());
+            return AjaxResult.error("网络异常,请稍后再试");
         }
     }
-
     /**
      * 根据linkId直接获取详情
      *
@@ -124,8 +107,9 @@ public class QwAcquisitionAssistantController extends BaseController {
         try {
             LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
             qwAcquisitionAssistant.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
+            Long companyId = loginUser.getUser().getCompanyId();
             QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
-            QwAcquisitionAssistant result = qwAcquisitionAssistantService.createWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.createWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant,companyId);
             return AjaxResult.success("创建成功", result);
         } catch (CustomException e) {
             return AjaxResult.error(e.getMessage());
@@ -200,7 +184,7 @@ public class QwAcquisitionAssistantController extends BaseController {
             qwAcquisitionAssistant.setUpdateBy(String.valueOf(loginUser.getUser().getUserId()));
             QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
             QwAcquisitionAssistant result = qwAcquisitionAssistantService.updateWithQw(
-                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant,loginUser.getUser().getCompanyId());
 
             return AjaxResult.success("修改成功", result);
         } catch (CustomException e) {
@@ -247,4 +231,18 @@ public class QwAcquisitionAssistantController extends BaseController {
         }
         return qwCompany;
     }
-}
+
+    /**
+     * 校验电话
+     */
+    private void validatePhone(String phone) {
+        if (StringUtils.isEmpty(phone)) {
+            throw new CustomException("发送短信的电话号码不能为空", 400);
+        }
+
+        // 支持手机号或固话
+        if (!phone.matches("^1[3-9]\\d{9}$") && !phone.matches("^\\d{3,4}-?\\d{7,8}$")) {
+            throw new CustomException("电话号码格式不正确,请输入正确的手机号或固定电话", 400);
+        }
+    }
+}

+ 449 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionLinkInfoController.java

@@ -0,0 +1,449 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.mapper.CompanyUserRoleMapper;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
+import com.fs.qw.dto.IpadBlindAddDto;
+import com.fs.qw.service.IQwAcquisitionLinkInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * 获客链接-号码链接生成记录Controller
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+@Slf4j
+@RestController
+@RequestMapping("/qw/linkInfo")
+public class QwAcquisitionLinkInfoController extends BaseController
+{
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private CompanyUserRoleMapper roleMapper;
+
+    @Autowired
+    private IQwAcquisitionLinkInfoService qwAcquisitionLinkInfoService;
+
+    // 定义手机号正则表达式
+    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
+    /**
+     * 查询获客链接-号码链接生成记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:linkInfo:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        qwAcquisitionLinkInfo.setCreateBy(loginUser.getUser().getUserId());
+        //管理员查看所有数据
+        Long isAdmin = roleMapper.companyUserIsAdmin(loginUser.getUser().getUserId());
+        if (isAdmin != null) {
+            qwAcquisitionLinkInfo.setCreateBy(null);
+        }
+        startPage();
+        List<QwAcquisitionLinkInfo> list = qwAcquisitionLinkInfoService.selectQwAcquisitionLinkInfoList(qwAcquisitionLinkInfo);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出获客链接-号码链接生成记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:linkInfo:export')")
+    @Log(title = "获客链接-号码链接生成记录", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, QwAcquisitionLinkInfo qwAcquisitionLinkInfo) throws IOException {
+        List<QwAcquisitionLinkInfo> list = qwAcquisitionLinkInfoService.selectQwAcquisitionLinkInfoList(qwAcquisitionLinkInfo);
+        ExcelUtil<QwAcquisitionLinkInfo> util = new ExcelUtil<QwAcquisitionLinkInfo>(QwAcquisitionLinkInfo.class);
+        util.exportExcel(response, list, "获客链接-号码链接生成记录数据");
+    }
+
+    /**
+     * 获取获客链接-号码链接生成记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('qw:linkInfo:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(qwAcquisitionLinkInfoService.selectQwAcquisitionLinkInfoById(id));
+    }
+
+    /**
+     * 新增获客链接-号码链接生成记录
+     */
+    @PreAuthorize("@ss.hasPermi('qw:linkInfo:add')")
+    @Log(title = "获客链接-号码链接生成记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
+    {
+        return toAjax(qwAcquisitionLinkInfoService.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo));
+    }
+
+    /**
+     * 修改获客链接-号码链接生成记录
+     */
+    @PreAuthorize("@ss.hasPermi('qw:linkInfo:edit')")
+    @Log(title = "获客链接-号码链接生成记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
+    {
+        return toAjax(qwAcquisitionLinkInfoService.updateQwAcquisitionLinkInfo(qwAcquisitionLinkInfo));
+    }
+
+    /**
+     * 删除获客链接-号码链接生成记录
+     */
+    @PreAuthorize("@ss.hasPermi('qw:linkInfo:remove')")
+    @Log(title = "获客链接-号码链接生成记录", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(qwAcquisitionLinkInfoService.deleteQwAcquisitionLinkInfoByIds(ids));
+    }
+
+    /**
+     * 发送获客链接短信
+     */
+    @GetMapping("/sendMessageLink/{id}/{phone}")
+    public AjaxResult sendMessageLink(@PathVariable Long id,@PathVariable String phone) {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser==null||loginUser.getCompany()==null){
+                throw new CustomException("请登录");
+            }
+            SendMsgLogBo sendMsgLogBo=new SendMsgLogBo();
+            sendMsgLogBo.setCompanyId(loginUser.getCompany().getCompanyId());
+            sendMsgLogBo.setCompanyUserId(loginUser.getCompany().getUserId());
+            validatePhone(phone);
+            SendResultDetailDTO sendResultDetailDTO = qwAcquisitionLinkInfoService.sendMessageLink(phone, id,sendMsgLogBo);
+            if (sendResultDetailDTO.isSuccess()){
+                return AjaxResult.success("发送成功");
+            }else {
+                return AjaxResult.error(sendResultDetailDTO.getFailReason());
+            }
+        } catch (Exception e) {
+            log.error("发送失败:" + e.getMessage());
+            return AjaxResult.error("网络异常,请稍后再试");
+        }
+    }
+
+    /**
+     * 提取链接 - 根据手机号生成短链接文本
+     *
+     * @param params 包含 id, phone, url 的参数
+     * @return 生成的文本内容
+     */
+    @PostMapping("/extractLink")
+    public AjaxResult extractLink(@RequestBody Map<String, Object> params) {
+        try {
+            // 参数校验
+            Long qwAcquisitionAssistantId = null;
+            String phone = null;
+            String url = null;
+
+            if (params.get("id") != null) {
+                qwAcquisitionAssistantId = Long.valueOf(params.get("id").toString());
+            }
+            if (params.get("phone") != null) {
+                phone = params.get("phone").toString();
+            }
+            if (params.get("url") != null) {
+                url = params.get("url").toString();
+            }
+
+            if (qwAcquisitionAssistantId == null) {
+                return AjaxResult.error("获客链接ID不能为空");
+            }
+            if (StringUtils.isEmpty(phone)) {
+                return AjaxResult.error("手机号码不能为空");
+            }
+            if (StringUtils.isEmpty(url)) {
+                return AjaxResult.error("获客链接URL不能为空");
+            }
+
+            // 验证手机号格式
+            if (!phone.matches("^1[3-9]\\d{9}$")) {
+                return AjaxResult.error("请输入正确的11位手机号码");
+            }
+
+            // 获取当前登录用户信息
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser == null || loginUser.getCompany() == null) {
+                throw new CustomException("请登录");
+            }
+            // 调用服务层方法生成短链接文本
+            String resultText = qwAcquisitionLinkInfoService.extractLink(qwAcquisitionAssistantId, phone, url,loginUser.getCompany().getCompanyId());
+
+            return AjaxResult.success(resultText);
+
+        } catch (CustomException e) {
+            log.error("提取链接失败: {}", e.getMessage());
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            log.error("提取链接异常", e);
+            return AjaxResult.error("服务器内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 提取链接 -正常的获客
+     *
+     * @param params 包含 id, url 的参数
+     * @return 生成的文本内容
+     */
+    @PostMapping("/extractLinkNol")
+    public R extractLinkNol(@RequestBody Map<String, Object> params) {
+        try {
+            // 参数校验
+            Long qwAcquisitionAssistantId = null;
+            String phone = null;
+            String url = null;
+
+            if (params.get("id") != null) {
+                qwAcquisitionAssistantId = Long.valueOf(params.get("id").toString());
+            }
+
+            if (params.get("url") != null) {
+                url = params.get("url").toString();
+            }
+
+            if (qwAcquisitionAssistantId == null) {
+                return R.error("获客链接ID不能为空");
+            }
+            if (StringUtils.isEmpty(url)) {
+                return R.error("获客链接URL不能为空");
+            }
+
+            // 获取当前登录用户信息
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser == null || loginUser.getCompany() == null) {
+                throw new CustomException("请登录");
+            }
+
+            // 调用服务层方法生成短链接文本
+            return qwAcquisitionLinkInfoService.extractLinkNol(qwAcquisitionAssistantId, url,loginUser.getCompany().getCompanyId());
+
+        } catch (CustomException e) {
+            log.error("提取链接失败: {}", e.getMessage());
+            return R.error(e.getMessage());
+        } catch (Exception e) {
+            log.error("提取链接异常", e);
+            return R.error("服务器内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 批量生成多手机号短链
+     * */
+    @PostMapping("/batchCreateMessageLink")
+    public AjaxResult batchCreateMessageLink(@RequestParam("file") MultipartFile file,
+                                             @RequestParam("qwAcquisitionAssistantId") Long qwAcquisitionAssistantId,
+                                             @RequestParam("qwAcquisitionAssistantUrl") String qwAcquisitionAssistantUrl) {
+
+        // 获取当前登录用户信息
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser == null || loginUser.getCompany() == null) {
+            throw new CustomException("请登录");
+        }
+        // 1. 参数校验
+        if (file == null || file.isEmpty()) {
+            return AjaxResult.error("上传的文件不能为空");
+        }
+        if (qwAcquisitionAssistantId == null) {
+            return AjaxResult.error("获客链接ID不能为空");
+        }
+        if (qwAcquisitionAssistantUrl == null || qwAcquisitionAssistantUrl.trim().isEmpty()) {
+            return AjaxResult.error("获客链接URL不能为空");
+        }
+
+        // 2. 检查文件类型
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".xls")) {
+            return AjaxResult.error("仅支持上传 .xls 格式的文件");
+        }
+
+        try {
+            // 3. 读取Excel文件并解析出电话号码列表
+            List<String> phoneList = readPhonesFromXls(file.getInputStream());
+            if (CollectionUtils.isEmpty(phoneList)) {
+                return AjaxResult.error("上传的Excel文件中未找到有效的电话号码数据");
+            }
+            if (phoneList.size()>500) {
+                return AjaxResult.error("单次上传的号码不能超过500个");
+            }
+            // 4. 构建DTO对象
+            BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO = new BatchAddAcquisitionLinkDTO();
+            batchAddAcquisitionLinkDTO.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+            batchAddAcquisitionLinkDTO.setQwAcquisitionAssistantUrl(qwAcquisitionAssistantUrl);
+            batchAddAcquisitionLinkDTO.setPhoneList(phoneList);
+            batchAddAcquisitionLinkDTO.setCreateBy(loginUser.getCompany().getUserId());
+            batchAddAcquisitionLinkDTO.setCompanyId(loginUser.getCompany().getCompanyId());
+            // 5. 调用服务层方法处理
+            int count = qwAcquisitionLinkInfoService.batchCreateMessageLink(batchAddAcquisitionLinkDTO);
+
+            return AjaxResult.success("成功处理 " + count + " 条记录");
+
+        } catch (Exception e) {
+            log.error("上传Excel并生成短链失败", e);
+            return AjaxResult.error("服务器内部错误: " + e.getMessage());
+        }
+    }
+
+    /**
+     * iPad获客链接加好友
+     *
+     * @param dto 请求参数
+     * @return 操作结果
+     */
+    @PostMapping("/ipadBlindAdd")
+    public AjaxResult ipadBlindAdd(@Valid @RequestBody IpadBlindAddDto dto) {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            if (loginUser == null || loginUser.getCompany() == null) {
+                throw new CustomException("请登录");
+            }
+            SendMsgLogBo sendMsgLogBo = new SendMsgLogBo();
+            sendMsgLogBo.setCompanyId(loginUser.getCompany().getCompanyId());
+            sendMsgLogBo.setCompanyUserId(loginUser.getCompany().getUserId());
+            validatePhone(dto.getPhone());
+            // 调用业务逻辑
+            qwAcquisitionLinkInfoService.ipadBlindAdd(dto, sendMsgLogBo);
+            return AjaxResult.success("添加成功");
+
+        } catch (Exception e) {
+            log.error("iPad盲加失败", e);
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 校验电话
+     */
+    private void validatePhone(String phone) {
+        if (StringUtils.isEmpty(phone)) {
+            throw new CustomException("发送短信的电话号码不能为空", 400);
+        }
+
+        // 支持手机号或固话
+        if (!phone.matches("^1[3-9]\\d{9}$") && !phone.matches("^\\d{3,4}-?\\d{7,8}$")) {
+            throw new CustomException("电话号码格式不正确,请输入正确的手机号或固定电话", 400);
+        }
+    }
+    /**
+     * 从.xls文件流中读取电话号码列表
+     * 假设第一行为表头,第一列开始为电话号码
+     * @param inputStream 文件输入流
+     * @return 电话号码列表
+     * @throws IOException
+     */
+    public static List<String> readPhonesFromXls(InputStream inputStream) throws IOException {
+        List<String> phoneList = new ArrayList<>();
+        HSSFWorkbook workbook = null;
+
+        try {
+            workbook = new HSSFWorkbook(inputStream);
+            HSSFSheet sheet = workbook.getSheetAt(0); // 获取第一个Sheet
+
+            int lastRowNum = sheet.getLastRowNum();
+
+            // 从第二行开始遍历 (i=1),第一行是表头
+            for (int i = 1; i <= lastRowNum; i++) {
+                HSSFRow row = sheet.getRow(i);
+                if (row != null) {
+                    // 修改:获取第一列 (index=0) 的单元格
+                    HSSFCell cell = row.getCell(0);
+                    if (cell != null) {
+                        // 获取单元格内容并转为字符串
+                        String phoneValue = getCellValueAsString(cell);
+                        if (phoneValue != null && !phoneValue.trim().isEmpty()) {
+                            // 进行格式校验
+                            if (PHONE_PATTERN.matcher(phoneValue.trim()).matches()) {
+                                phoneList.add(phoneValue.trim());
+                            } else {
+                                log.warn("发现格式不正确的电话号码,已跳过: {}", phoneValue.trim());
+                            }
+                        }
+                    }
+                }
+            }
+        } finally {
+            if (workbook != null) {
+                try {
+                    workbook.close();
+                } catch (IOException e) {
+                    log.error("关闭Excel工作簿时发生错误", e);
+                }
+            }
+        }
+
+        return phoneList;
+    }
+
+    /**
+     * 将HSSFCell的值转为String
+     * @param cell 单元格
+     * @return 单元格的字符串值
+     */
+    private static String getCellValueAsString(HSSFCell cell) {
+        if (cell == null) {
+            return null;
+        }
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                // 如果是数字,通常电话号码是字符串,这里转为长整型再转为字符串,避免科学计数法
+                if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
+                    return String.valueOf(cell.getDateCellValue());
+                } else {
+                    Double numericValue = cell.getNumericCellValue();
+                    // 尝试转为Long,适用于大部分电话号码
+                    return String.valueOf(numericValue.longValue());
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                return cell.getCellFormula();
+            case BLANK:
+                return "";
+            case ERROR:
+                return "ERROR";
+            default:
+                return "";
+        }
+    }
+}

+ 94 - 4
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -25,15 +25,17 @@ import com.fs.his.service.IFsInquiryOrderService;
 import com.fs.his.service.IFsIntegralCountService;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.utils.qrcode.QRCodeUtils;
-import com.fs.qw.domain.QwCompany;
-import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwIpadServerLog;
-import com.fs.qw.domain.QwUser;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.QwContactWayMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwRestrictionPushRecordMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.QwUpdateContactWayParam;
 import com.fs.qw.service.*;
+import com.fs.qw.service.impl.AsyncQwContactWayService;
 import com.fs.qwApi.domain.QwExternalContactResult;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.domain.inner.ExternalContact;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.mapper.QwSopMapper;
@@ -50,6 +52,7 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 import com.fs.app.task.qwTask;
 
@@ -169,6 +172,93 @@ public class CommonController {
 
     @Autowired
     private IFsIntegralCountService integralCountService;
+    @Autowired
+    private QwContactWayMapper qwContactWayMapper;
+
+    @Autowired
+    RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private AsyncQwContactWayService asyncQwContactWayService;
+
+    @Autowired
+    private IQwContactWayService contactWayService;
+
+    @GetMapping("/resetQwContactWayUserLimit")
+    public void resetQwContactWayUserLimit()  {
+        contactWayService.resetQwContactWayUserLimit();
+    }
+    @GetMapping("/qwContactWay")
+    public void qwContactWay(String userID) throws Exception {
+        String state="way:wwce259dbd92b6f0e7:256";
+        String corpId="wwce259dbd92b6f0e7";
+        if (state != null && state != "") {
+            String s = "way:" + corpId + ":";
+            if (state.contains(s)) {
+                String substring = state.substring(state.indexOf(s) + s.length());
+                QwContactWay qwContactWay = qwContactWayMapper.selectQwContactWayById(Long.parseLong(substring));
+                if (qwContactWay != null) {
+                    if (qwContactWay.getUserType() == 1 && qwContactWay.getIsUserLimit() == 1) {
+
+                        String userLimitJson = qwContactWay.getUserLimitJson();
+                        List<QwContactWayUser> qwContactWayUsers = JSON.parseArray(userLimitJson, QwContactWayUser.class);
+
+                        String wayLimit = "qwContactWayLimit:" + corpId  + ":" + userID;
+
+                        QwContactWayUser matchedUser = qwContactWayUsers.stream()
+                                .filter(u -> userID.equals(u.getUserId()))
+                                .findFirst()
+                                .orElse(null);
+
+                        if (matchedUser != null) {
+                            Long currentCount = redisTemplate.opsForValue().increment(wayLimit, 1);
+                            if (currentCount == null) {
+                                currentCount = 1L;
+                            }
+
+                            if(currentCount == 1L){
+                                long expireSeconds = getSecondsUntilMidnight();
+                                redisTemplate.expire(wayLimit, expireSeconds, java.util.concurrent.TimeUnit.SECONDS);
+                            }
+                            if(currentCount >= matchedUser.getLimitCount()){
+                                log.error("用户{}已达到每日添加上限{}/{}", userID, currentCount, matchedUser.getLimitCount());
+
+                                boolean needUpdate = false;
+
+                                List<String> userIdList = JSON.parseArray(qwContactWay.getUserIds(), String.class);
+
+                                if (userIdList != null && !userIdList.isEmpty() && userIdList.contains(userID) && !userIdList.contains(qwContactWay.getSpareUserIds())) {
+                                    //移除渠道活码里面的人员-保留 备选人员
+                                    userIdList.remove(userID);
+                                    //移除渠道活码里面的人员-保留 备选人员
+                                    if (userIdList.isEmpty()){
+                                        List<String> newUserIdList =new ArrayList<>();
+                                        newUserIdList.add(JSON.parseObject(qwContactWay.getSpareUserIds(), String.class));
+                                        qwContactWay.setUserIds(JSON.toJSONString(newUserIdList));
+                                    }else {
+                                        qwContactWay.setUserIds(JSON.toJSONString(userIdList));
+                                    }
+                                    needUpdate = true;
+                                }
+
+                                if (needUpdate) {
+                                    asyncQwContactWayService.executeUpdateContactWay(qwContactWay,userID);
+                                }
+                            }
+                        }
+
+                    }
+                }
+
+            }
+        }
+    }
+
+    private long getSecondsUntilMidnight() {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime midnight = now.toLocalDate().plusDays(1).atStartOfDay();
+        return java.time.Duration.between(now, midnight).getSeconds();
+    }
 
 
     @GetMapping("/testTime")

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

@@ -176,10 +176,10 @@ public interface CompanyUserMapper
             "    OR FIND_IN_SET(#{maps.deptId}, cd.ancestors))")
     List<CompanyUserQwVO> getUserListByDeptIdToQwUserid(@Param("maps") CompanyUser user);
 
-    @Select("select * from  qw_user  where corp_id=#{corpId} and company_id=#{companyId}")
+    @Select("select * from  qw_user  where corp_id=#{corpId} and company_id=#{companyId} and company_user_id is not null and is_del=0 ")
     List<QwUserVO> selectCompanyQwUserList(@Param("corpId") String corpId,@Param("companyId")Long companyId);
 
-    @Select("select * from  qw_user  where corp_id=#{corpId} and company_id=#{companyId} and company_user_id=#{userId}")
+    @Select("select * from  qw_user  where corp_id=#{corpId} and company_id=#{companyId} and company_user_id=#{userId} and is_del=0 ")
     List<QwUserVO> selectCompanyQwUserListByMy(@Param("corpId") String corpId,@Param("companyId")Long companyId,@Param("userId")Long userId);
 
     @Select("<script>" +

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

@@ -1669,11 +1669,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
 
-        //拿取业务id
-        List<Long> businessIds = records.stream()
-                .map(WorkflowExecRecordVo::getBusinessId)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+//        //拿取业务id
+//        List<Long> businessIds = records.stream()
+//                .map(WorkflowExecRecordVo::getBusinessId)
+//                .filter(Objects::nonNull)
+//                .collect(Collectors.toList());
 
 //        if (!businessIds.isEmpty()) {
 //            List<CompanyVoiceRoboticBusiness> businesses = companyVoiceRoboticBusinessMapper.selectList(new LambdaQueryWrapper<CompanyVoiceRoboticBusiness>()

+ 13 - 4
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java

@@ -1,6 +1,7 @@
 package com.fs.qw.domain;
 
 import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 import org.apache.commons.lang3.StringUtils;
@@ -10,7 +11,7 @@ import java.util.List;
 
 /**
  * 企微-获客链接管理对象 qw_acquisition_assistant
- * 
+ *
  * @author fs
  * @date 2026-03-16
  */
@@ -96,7 +97,7 @@ public class QwAcquisitionAssistant extends BaseEntity
     /**
      * 将参数列表转换为JSON字符串
      */
-    public void buildJsonFields() 
+    public void buildJsonFields()
     {
         if (userListParam != null) {
             this.userList = JSON.toJSONString(userListParam);
@@ -112,7 +113,7 @@ public class QwAcquisitionAssistant extends BaseEntity
     /**
      * 解析JSON字段到参数列表
      */
-    public void parseJsonFields() 
+    public void parseJsonFields()
     {
         if (StringUtils.isNotBlank(this.userList)) {
             this.userListParam = JSON.parseArray(this.userList, String.class);
@@ -134,4 +135,12 @@ public class QwAcquisitionAssistant extends BaseEntity
                 ", status=" + status +
                 '}';
     }
-}
+
+    /** 员工添加上限 */
+    @Excel(name = "员工添加上限")
+    private String userLimitJson;
+
+    /** 备用员工 */
+    @Excel(name = "备用员工")
+    private String spareUserIds;
+}

+ 47 - 0
fs-service/src/main/java/com/fs/qw/domain/QwContactAcquisitionUser.java

@@ -0,0 +1,47 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+/**
+ * 获客链接限制对象 qw_contact_acquisition_user
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+@Data
+public class QwContactAcquisitionUser {
+
+    /** id */
+    private Long id;
+
+    /** 获客链接的id */
+    @Excel(name = "获客链接的id")
+    private String linkId;
+
+    /** 企微员工id */
+    @Excel(name = "企微员工id")
+    private Long qwUserId;
+
+    /** 企微员工id */
+    @Excel(name = "企微员工id")
+    private String userId;
+
+    /** 添加总数 */
+    @Excel(name = "添加总数")
+    private Long limitCount;
+
+    /** 剩余可添加数 */
+    @Excel(name = "剩余可添加数")
+    private Long dayCount;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 企业CorpID */
+    @Excel(name = "企业CorpID")
+    private String corpId;
+
+
+}

+ 9 - 0
fs-service/src/main/java/com/fs/qw/domain/QwExternalContact.java

@@ -152,5 +152,14 @@ public class QwExternalContact extends BaseEntity
     // 广告链路唯一id
     private String traceId;
 
+    //小程序用户手机号
+    private String fsUserPhone;
+
+    // 添加来源类型:1-普通添加,2-渠道活码,3-获客链接,4-APP联系方式
+    private Integer addSourceType;
+
+    //获客链接主键id
+    private Long qwAcquisitionAssistantId;
+
 
 }

+ 62 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwContactAcquisitionUserMapper.java

@@ -0,0 +1,62 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.QwContactAcquisitionUser;
+
+import java.util.List;
+
+/**
+ * 获客链接限制Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface QwContactAcquisitionUserMapper extends BaseMapper<QwContactAcquisitionUser>{
+    /**
+     * 查询获客链接限制
+     *
+     * @param id 获客链接限制主键
+     * @return 获客链接限制
+     */
+    QwContactAcquisitionUser selectQwContactAcquisitionUserById(Long id);
+
+    /**
+     * 查询获客链接限制列表
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 获客链接限制集合
+     */
+    List<QwContactAcquisitionUser> selectQwContactAcquisitionUserList(QwContactAcquisitionUser qwContactAcquisitionUser);
+
+    /**
+     * 新增获客链接限制
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 结果
+     */
+    int insertQwContactAcquisitionUser(QwContactAcquisitionUser qwContactAcquisitionUser);
+
+    /**
+     * 修改获客链接限制
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 结果
+     */
+    int updateQwContactAcquisitionUser(QwContactAcquisitionUser qwContactAcquisitionUser);
+
+    /**
+     * 删除获客链接限制
+     *
+     * @param id 获客链接限制主键
+     * @return 结果
+     */
+    int deleteQwContactAcquisitionUserById(Long id);
+
+    /**
+     * 批量删除获客链接限制
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteQwContactAcquisitionUserByIds(Long[] ids);
+}

+ 3 - 6
fs-service/src/main/java/com/fs/qw/mapper/QwContactWayMapper.java

@@ -22,12 +22,6 @@ import java.util.Map;
 @Repository
 public interface QwContactWayMapper
 {
-    /**
-     * 查询企微活码
-     *
-     * @param id 企微活码主键
-     * @return 企微活码
-     */
     public QwContactWay selectQwContactWayById(Long id);
 
     /**
@@ -54,6 +48,9 @@ public interface QwContactWayMapper
      */
     public int updateQwContactWay(QwContactWay qwContactWay);
 
+    @Update("UPDATE qw_contact_way SET user_ids = #{userIds} WHERE id = #{id}")
+    int updateUserIdsById(@Param("id") Long id, @Param("userIds") String userIds);
+
     /**
      * 删除企微活码
      *

+ 20 - 32
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -611,39 +611,27 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     public List<String> selectQwExternalContactMandatoryRegistration();
 
     @Select("<script>" +
-            "SELECT t1.id,t2.valid_fs_user_id as fs_user_id \n" +
-            "FROM qw_external_contact t1\n" +
-            "JOIN (\n" +
-            "    SELECT \n" +
-            "        external_user_id,\n" +
-            "        corp_id,\n" +
-            "        MAX(fs_user_id) as valid_fs_user_id\n" +
-            "    FROM qw_external_contact\n" +
-            "    WHERE external_user_id IS NOT NULL \n" +
-            "        AND corp_id IS NOT NULL \n" +
-            "        AND corp_id = #{corpId}\n" +
-            "        AND fs_user_id IS NOT NULL\n" +
-            "        AND create_time >='2025-10-01 00:00:00' \n" +
-            "    GROUP BY external_user_id, corp_id\n" +
+            "SELECT \n" +
+            "  t1.id, \n" +
+            "  t2.valid_fs_user_id AS fs_user_id \n" +
+            "FROM qw_external_contact t1 \n" +
+            "JOIN ( \n" +
+            "  SELECT \n" +
+            "    external_user_id, \n" +
+            "    corp_id, \n" +
+            "    MAX(fs_user_id) AS valid_fs_user_id \n" +
+            "  FROM qw_external_contact \n" +
+            "  WHERE external_user_id IS NOT NULL \n" +
+            "    AND corp_id = #{corpId} \n" +
+            "    AND create_time >= '2025-10-01 00:00:00' \n" +
+            "  GROUP BY external_user_id, corp_id \n" +
+            "  HAVING COUNT(*) >= 2 \n" +
+            "    AND SUM(fs_user_id IS NOT NULL) >= 1 \n" +
+            "    AND SUM(fs_user_id IS NULL) >= 1 \n" +
             ") t2 ON t1.external_user_id = t2.external_user_id \n" +
-            "    AND t1.corp_id = t2.corp_id\n" +
-            "JOIN (\n" +
-            "    SELECT \n" +
-            "        external_user_id,\n" +
-            "        corp_id\n" +
-            "    FROM qw_external_contact\n" +
-            "    WHERE external_user_id IS NOT NULL \n" +
-            "        AND corp_id IS NOT NULL \n" +
-            "        AND corp_id = #{corpId}\n" +
-            "        AND create_time >='2025-10-01 00:00:00' \n" +
-            "    GROUP BY external_user_id, corp_id\n" +
-            "    HAVING COUNT(*) >= 2\n" +
-            "        AND SUM(CASE WHEN fs_user_id IS NOT NULL THEN 1 ELSE 0 END) >= 1\n" +
-            "        AND SUM(CASE WHEN fs_user_id IS NULL THEN 1 ELSE 0 END) >= 1\n" +
-            ") t3 ON t1.external_user_id = t3.external_user_id \n" +
-            "    AND t1.corp_id = t3.corp_id\n" +
-            "WHERE t1.fs_user_id IS NULL\n" +
-            "    AND t1.corp_id = #{corpId}\n" +
+            "  AND t1.corp_id = t2.corp_id \n" +
+            "WHERE t1.fs_user_id IS NULL \n" +
+            "  AND t1.corp_id = #{corpId} " +
             "</script>")
     public List<QwMandatoryRegistrParam> selectQwExternalContactMandatoryRegistrationByIds(@Param("corpId") String corpId);
 

+ 19 - 5
fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionAssistantService.java

@@ -1,6 +1,8 @@
 package com.fs.qw.service;
 
 import com.fs.common.exception.CustomException;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
 import com.fs.qw.domain.QwAcquisitionAssistant;
 import com.fs.qw.dto.acquisition.AcquisitionListResponse;
 import com.fs.qw.vo.AcquisitionAssistantDetailVO;
@@ -9,11 +11,11 @@ import java.util.List;
 
 /**
  * 企微-获客链接管理Service接口
- * 
+ *
  * @author fs
  * @date 2026-03-16
  */
-public interface IQwAcquisitionAssistantService 
+public interface IQwAcquisitionAssistantService
 {
     /**
      * 查询企微-获客链接管理列表
@@ -23,6 +25,17 @@ public interface IQwAcquisitionAssistantService
      */
     public List<QwAcquisitionAssistant> selectQwAcquisitionAssistantList(QwAcquisitionAssistant qwAcquisitionAssistant);
 
+
+    /**
+     * 发送获客链接短信
+     *
+     * @param phone 待发送短信的手机号
+     * @param qwAcquisitionId 获客链接Id
+     * @return 结果
+     */
+    public SendResultDetailDTO sendMessageAcquisition(String phone, Long qwAcquisitionId, SendMsgLogBo sendMsgLogBo);
+
+
     /**
      * 从企微同步获客链接列表(全量拉取所有详情)
      * @param corpid 企业ID
@@ -65,7 +78,7 @@ public interface IQwAcquisitionAssistantService
      * @param assistant 获客链接信息
      * @return 创建成功的完整对象(包含企微返回的link_id、url等)
      */
-    public QwAcquisitionAssistant createWithQw(String corpid,String corpsecret,QwAcquisitionAssistant assistant);
+    public QwAcquisitionAssistant createWithQw(String corpid,String corpsecret,QwAcquisitionAssistant assistant,Long companyId);
 
     /**
      * 修改企微-获客链接管理
@@ -73,7 +86,7 @@ public interface IQwAcquisitionAssistantService
      * @param qwAcquisitionAssistant 企微-获客链接管理
      * @return 结果
      */
-    public QwAcquisitionAssistant updateWithQw(String corpId, String secret, QwAcquisitionAssistant qwAcquisitionAssistant);
+    public QwAcquisitionAssistant updateWithQw(String corpId, String secret, QwAcquisitionAssistant qwAcquisitionAssistant,Long companyId);
 
     /**
      * 删除获客链接
@@ -97,4 +110,5 @@ public interface IQwAcquisitionAssistantService
      * */
     public String selectQwAcquisitionUrlByPageParam(String pageParam);
 
-}
+    public void resetAcquisitionLinkUserLimit();
+}

+ 62 - 0
fs-service/src/main/java/com/fs/qw/service/IQwContactAcquisitionUserService.java

@@ -0,0 +1,62 @@
+package com.fs.qw.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.qw.domain.QwContactAcquisitionUser;
+
+import java.util.List;
+
+/**
+ * 获客链接限制Service接口
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+public interface IQwContactAcquisitionUserService extends IService<QwContactAcquisitionUser>{
+    /**
+     * 查询获客链接限制
+     *
+     * @param id 获客链接限制主键
+     * @return 获客链接限制
+     */
+    QwContactAcquisitionUser selectQwContactAcquisitionUserById(Long id);
+
+    /**
+     * 查询获客链接限制列表
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 获客链接限制集合
+     */
+    List<QwContactAcquisitionUser> selectQwContactAcquisitionUserList(QwContactAcquisitionUser qwContactAcquisitionUser);
+
+    /**
+     * 新增获客链接限制
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 结果
+     */
+    int insertQwContactAcquisitionUser(QwContactAcquisitionUser qwContactAcquisitionUser);
+
+    /**
+     * 修改获客链接限制
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 结果
+     */
+    int updateQwContactAcquisitionUser(QwContactAcquisitionUser qwContactAcquisitionUser);
+
+    /**
+     * 批量删除获客链接限制
+     *
+     * @param ids 需要删除的获客链接限制主键集合
+     * @return 结果
+     */
+    int deleteQwContactAcquisitionUserByIds(Long[] ids);
+
+    /**
+     * 删除获客链接限制信息
+     *
+     * @param id 获客链接限制主键
+     * @return 结果
+     */
+    int deleteQwContactAcquisitionUserById(Long id);
+}

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

@@ -83,4 +83,6 @@ public interface IQwContactWayService
     void addWatchLogIfNeeded(Integer videoId, Integer courseId,
                                      String qwUserId, String companyUserId,
                                      String companyId, String externalId,Integer watchType);
+
+    public void resetQwContactWayUserLimit();
 }

+ 40 - 0
fs-service/src/main/java/com/fs/qw/service/impl/AsyncQwContactWayService.java

@@ -0,0 +1,40 @@
+package com.fs.qw.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.qw.domain.QwContactWay;
+import com.fs.qw.mapper.QwContactWayMapper;
+import com.fs.qw.param.QwUpdateContactWayParam;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.service.QwApiService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class AsyncQwContactWayService {
+    @Autowired
+    private QwApiService qwApiService;
+    @Autowired
+    private QwContactWayMapper qwContactWayMapper;
+
+    @Async("scheduledExecutorService")
+    public void executeUpdateContactWay(QwContactWay qwContactWay,String userID) {
+        QwUpdateContactWayParam qwUpdateContactWayParam = new QwUpdateContactWayParam();
+        qwUpdateContactWayParam.setConfig_id(qwContactWay.getConfigId());
+        qwUpdateContactWayParam.setUser(JSON.parseArray(qwContactWay.getUserIds(), String.class));
+
+        QwResult qwResult = qwApiService.updateContactWay(qwUpdateContactWayParam, qwContactWay.getCorpId());
+        if (qwResult.getErrcode() == 0) {
+
+            qwContactWayMapper.updateQwContactWay(qwContactWay);
+            log.info("已从获客链接{}的可用列表中移除用户{}", qwContactWay.getId(), userID);
+
+        } else {
+            log.error("修改渠道活码 失败!" + qwUpdateContactWayParam + ":" + qwResult.getErrmsg());
+        }
+    }
+}

+ 321 - 12
fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionAssistantServiceImpl.java

@@ -2,30 +2,47 @@ package com.fs.qw.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
+import com.fs.common.service.ISmsService;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanySms;
+import com.fs.company.domain.CompanySmsTemp;
+import com.fs.company.service.ICompanySmsService;
+import com.fs.company.service.ICompanySmsTempService;
 import com.fs.fastgptApi.util.HttpUtil;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
 import com.fs.qw.domain.QwAcquisitionAssistant;
 import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwContactAcquisitionUser;
 import com.fs.qw.dto.acquisition.*;
+import com.fs.qw.enums.SmsLogType;
 import com.fs.qw.mapper.QwAcquisitionAssistantMapper;
 import com.fs.qw.service.IQwAcquisitionAssistantService;
 import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwContactAcquisitionUserService;
 import com.fs.qw.utils.UniqueStringUtil;
 import com.fs.qw.vo.AcquisitionAssistantDetailVO;
 import com.fs.qwApi.config.QwApiConfig;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.param.QwLinkCreateParam;
+import com.fs.qwApi.service.QwApiService;
 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.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * 企微-获客链接管理Service业务层处理
@@ -37,6 +54,9 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
     @Autowired
     private QwAcquisitionAssistantMapper qwAcquisitionAssistantMapper;
 
+    @Autowired
+    private QwAcquisitionAssistantMapper acquisitionAssistantMapper;
+
     @Autowired
     private IWeixinKfService weixinKfService;
 
@@ -46,6 +66,25 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
     @Autowired
     private IQwCompanyService qwCompanyService;
 
+    @Autowired
+    private ICompanySmsTempService smsTempService;
+
+    @Autowired
+    private ICompanySmsService companySmsService;
+
+    @Autowired
+    private ISmsService smsService;
+
+    @Autowired
+    private IQwContactAcquisitionUserService insertQwContactWayUser;
+
+
+    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private QwApiService qwApiService;
+
 
     // 获客链接管理-企微的ACCESS_TOKEN的key
     private static final String QW_ACQUISITION_KEY_PREFIX = "qw:acquisition:key:";
@@ -54,6 +93,13 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
     // 获客链接-页面参数-url的key
     private static final String QW_ACQUISITION_URL_KEY_PREFIX = "qw:acquisition:url:key:";
 
+    //获客链接短信模板code
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
+
+
+    //访问链接域名(待优化)
+    private static final String  LINK_DOMAIN = "";
+
     /**
      * 获取access_token并返回完整URL
      */
@@ -241,6 +287,147 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
         return list;
     }
 
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public SendResultDetailDTO sendMessageAcquisition(String phone, Long qwAcquisitionId, SendMsgLogBo sendMsgLogBo) {
+        log.info("发送获客链接短信,号码:{}", phone);
+
+        // 1. 获取短信模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_LINK_TEMPLATE_CODE);
+        if (temp == null) {
+            log.info("获客链接-未找到短信模板:{}", SMS_LINK_TEMPLATE_CODE);
+            throw new CustomException("获客链接-未找到短信模板");
+        }
+
+        // 2. 获取获客链接信息
+        String originalContent = temp.getContent();
+        QwAcquisitionAssistant acquisitionAssistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
+        if (acquisitionAssistant == null) {
+            log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
+            throw new CustomException("获客链接-未找到获客链接信息");
+        }
+
+        // 3. 构建短信内容
+        String replaceText = LINK_DOMAIN + acquisitionAssistant.getPageParam();
+        String content = originalContent.replace("${sms.friendLink}", replaceText);
+
+        // 4. 计算需要的短信条数
+        Integer needCount = calculateSmsCount(content);
+
+        // 5. 【乐观锁扣减,支持重试】
+        int maxRetries = 3;
+
+        for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
+            // 先查缓存快速检查余额
+            Long balance = companySmsService.getBalance(2L);
+            if (balance == null || balance < needCount) {
+                log.warn("短信余额不足, companyId=2, balance={}, needCount={}", balance, needCount);
+                return new SendResultDetailDTO(false, "短信余额不足", null);
+            }
+
+            // 获取最新数据(含version)
+            CompanySms latestSms = companySmsService.selectCompanySms(2L);
+            if (latestSms == null) {
+                throw new CustomException("公司短信配置不存在");
+            }
+
+            // 再次确认余额(DB为准)
+            if (latestSms.getRemainSmsCount() < needCount) {
+                log.warn("短信余额不足, companyId=2, remainCount={}, needCount={}",
+                        latestSms.getRemainSmsCount(), needCount);
+                return new SendResultDetailDTO(false, "短信余额不足", null);
+            }
+
+            Long version = latestSms.getVersion() != null ? latestSms.getVersion() : 0L;
+            int updateCount = companySmsService.decrementRemainSmsCountWithVersion(2L, needCount, version);
+
+            if (updateCount > 0) {
+                log.info("乐观锁扣减成功, needCount={}, version={}", needCount, version);
+                // 扣减成功,跳出循环
+                break;
+            }
+
+            log.warn("乐观锁扣减失败,第{}次重试", retryCount + 1);
+
+            // 最后一次重试失败,返回错误
+            if (retryCount == maxRetries - 1) {
+                return new SendResultDetailDTO(false, "系统繁忙,请稍后重试", null);
+            }
+
+            try {
+                Thread.sleep(50L * (retryCount + 1));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return new SendResultDetailDTO(false, "操作被中断", null);
+            }
+        }
+
+        // 6. 更新缓存
+        companySmsService.updateCacheBalance(2L, -needCount);
+
+        try {
+            sendMsgLogBo.setQwAcquisitionId(acquisitionAssistant.getId());
+
+            // 7. 发送短信
+            R r = smsService.simpleSmsSend(phone, content, temp, SmsLogType.ACQUISITION_LINK, sendMsgLogBo);
+
+            if (r != null && "200".equals(String.valueOf(r.get("code")))) {
+                log.info("短信发送成功, phone={}, needCount={}", phone, needCount);
+                return new SendResultDetailDTO(true, null, null);
+            } else {
+                // 8. 发送失败,退还余额
+                String msg = r != null && r.get("msg") != null ? r.get("msg").toString() : "未知错误";
+                log.warn("短信发送失败,退还余额, phone={}, needCount={}", phone, needCount);
+                rollbackBalance(needCount, maxRetries);
+                return new SendResultDetailDTO(false, msg, null);
+            }
+        } catch (Exception e) {
+            log.error("发送异常,退还余额, phone=" + phone, e);
+            rollbackBalance(needCount, maxRetries);
+            return new SendResultDetailDTO(false, e.getMessage(), null);
+        }
+    }
+
+    //根据短信文字内容计算短信条数
+    private int calculateSmsCount(String content) {
+        if (content == null) return 1;
+        int counts = content.length() / 67;
+        if (content.length() % 67 > 0) {
+            counts = counts + 1;
+        }
+        if (counts == 0) {
+            counts = 1;
+        }
+        return counts;
+    }
+
+    /**
+     * 退还余额(带重试)
+     */
+    private void rollbackBalance(int needCount, int maxRetries) {
+        for (int i = 0; i < maxRetries; i++) {
+            try {
+                CompanySms current = companySmsService.selectCompanySms(2L);
+                if (current == null) {
+                    log.error("退还短信余额失败,公司短信配置不存在");
+                    return;
+                }
+                Long currentVersion = current.getVersion() != null ? current.getVersion() : 0L;
+                int rollbackCount = companySmsService.incrementRemainSmsCountWithVersion(2L, needCount, currentVersion);
+                if (rollbackCount > 0) {
+                    companySmsService.updateCacheBalance(2L, needCount);
+                    log.info("退还余额成功, needCount={}", needCount);
+                    return;
+                }
+                Thread.sleep(50L);
+            } catch (Exception e) {
+                log.error("退还短信余额异常,第{}次重试", i + 1, e);
+            }
+        }
+        log.error("退还短信余额失败,需要人工处理, companyId=2, needCount={}", needCount);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public String syncListFromQw(String corpid, String corpsecret) {
@@ -387,6 +574,8 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
             vo.setRemark(localData.getRemark());
             vo.setCorpId(localData.getCorpId());
             vo.setScheme(localData.getScheme());
+            vo.setUserLimitJson(localData.getUserLimitJson());
+            vo.setSpareUserIds(localData.getSpareUserIds());
         } else {
             // 纯企微数据,设置默认值
             vo.setStatus(1);
@@ -518,7 +707,7 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public QwAcquisitionAssistant createWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant) {
+    public QwAcquisitionAssistant createWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant,Long companyId) {
         // 参数校验
         if (StringUtils.isEmpty(assistant.getLinkName())) {
             throw new CustomException("链接名称不能为空");
@@ -580,8 +769,9 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
         if (response.getLink().getCreateTime() != null) {
             assistant.setQwCreateTime(new Date(response.getLink().getCreateTime() * 1000));
         }
-
-
+//        assistant.setQwCreateTime(new Date());
+//        assistant.setUrl("6666666666");
+//        assistant.setLinkId("123121");
         //如果这个随机参数已存在,则重新生成
         String randomParam =generateUniquePageParam();
 
@@ -592,12 +782,23 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
 
         // 设置本地字段并保存
         setLocalFields(assistant, true);
-        qwAcquisitionAssistantMapper.insertQwAcquisitionAssistant(assistant);
+        acquisitionAssistantMapper.insertQwAcquisitionAssistant(assistant);
+
+        String userLimitJson = assistant.getUserLimitJson();
+        List<QwContactAcquisitionUser> acquisitionUserList = JSON.parseArray(userLimitJson, QwContactAcquisitionUser.class);
+        for (QwContactAcquisitionUser acquisitionUser : acquisitionUserList) {
+            acquisitionUser.setDayCount(acquisitionUser.getLimitCount());
+            acquisitionUser.setLinkId(assistant.getLinkId());
+            acquisitionUser.setCompanyId(companyId);
+            acquisitionUser.setCorpId(corpid);
+            insertQwContactWayUser.insertQwContactAcquisitionUser(acquisitionUser);
+        }
+
 
         // ========== 缓存URL,便于后续通过pageParam访问 ==========
         try {
             String cacheKey = QW_ACQUISITION_URL_KEY_PREFIX + randomParam;
-            Integer cacheExpire = 10; // 默认缓存10天
+            Integer cacheExpire = 2; // 默认缓存2
             redisCache.setCacheObject(cacheKey, friendUrl, cacheExpire, TimeUnit.DAYS);
             log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomParam, friendUrl);
         } catch (Exception e) {
@@ -605,13 +806,14 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
             log.error("获客链接URL缓存失败, pageParam: {}", randomParam, e);
         }
         return assistant;
+
     }
 
     // ==================== 编辑方法 ====================
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public QwAcquisitionAssistant updateWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant) {
+    public QwAcquisitionAssistant updateWithQw(String corpid, String corpsecret, QwAcquisitionAssistant assistant,Long companyId) {
         // 参数校验
         if (assistant.getId() == null) {
             throw new CustomException("ID不能为空");
@@ -621,7 +823,7 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
         }
 
         // 查询本地是否存在
-        QwAcquisitionAssistant existAssistant = qwAcquisitionAssistantMapper.selectQwAcquisitionAssistantById(assistant.getId());
+        QwAcquisitionAssistant existAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(assistant.getId());
         if (existAssistant == null) {
             throw new CustomException("获客链接不存在");
         }
@@ -634,30 +836,60 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
         }
         request.setSkipVerify(Boolean.parseBoolean(assistant.getSkipVerify()));
 
+        AcquisitionRange range=new AcquisitionRange();
+        range.setUserList(assistant.getUserListParam());
+        request.setRange(range);
         //request.setMarkSource(assistant.getMarkSource());
 
-        // 调用企微API
+//         调用企微API
         String qwApiUrl = buildApiUrl(corpid, corpsecret, QwApiConfig.updateAcquisition);
         AcquisitionUpdateResponse response = callQwApi(qwApiUrl, request, AcquisitionUpdateResponse.class, "更新获客链接");
 
         // 更新本地字段
         setLocalFields(assistant, false);
-        //TODO重新生成页面参数,需要修改对应redis缓存
+        //重新生成页面参数,需要修改对应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
+        Integer cacheExpire = 2;//默认缓存2
         redisCache.setCacheObject(newKey, existAssistant.getUrl(), cacheExpire, TimeUnit.DAYS);
 
         assistant.setPageParam(newPageParam);
 
-        int rows = qwAcquisitionAssistantMapper.updateQwAcquisitionAssistant(assistant);
+        int rows = acquisitionAssistantMapper.updateQwAcquisitionAssistant(assistant);
         if (rows <= 0) {
             throw new CustomException("本地数据更新失败");
         }
 
+        //先根据linkId查询出所有ID,再删除
+        List<QwContactAcquisitionUser> userList = insertQwContactWayUser.list(
+                new LambdaQueryWrapper<QwContactAcquisitionUser>()
+                        .eq(QwContactAcquisitionUser::getLinkId, existAssistant.getLinkId()));
+
+        if (!CollectionUtils.isEmpty(userList)) {
+            List<Long> ids = userList.stream()
+                    .map(QwContactAcquisitionUser::getId)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+
+            if (!ids.isEmpty()) {
+                insertQwContactWayUser.removeByIds(ids);
+            }
+        }
+
+        String userLimitJson = assistant.getUserLimitJson();
+        List<QwContactAcquisitionUser> acquisitionUserList = JSON.parseArray(userLimitJson, QwContactAcquisitionUser.class);
+        for (QwContactAcquisitionUser acquisitionUser : acquisitionUserList) {
+            acquisitionUser.setDayCount(acquisitionUser.getLimitCount());
+            acquisitionUser.setLinkId(existAssistant.getLinkId());
+            acquisitionUser.setCompanyId(companyId);
+            acquisitionUser.setCorpId(corpid);
+            insertQwContactWayUser.insertQwContactAcquisitionUser(acquisitionUser);
+        }
+
+
         return assistant;
     }
 
@@ -764,4 +996,81 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
 
         return friendUrl;
     }
-}
+
+    @Override
+    public void resetAcquisitionLinkUserLimit() {
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 开始重置获客链接用户每日添加额度 ======");
+
+        try {
+            QwAcquisitionAssistant queryParam = new QwAcquisitionAssistant();
+            queryParam.setDelFlag("0");
+            List<QwAcquisitionAssistant> assistants = acquisitionAssistantMapper.selectQwAcquisitionAssistantList(queryParam);
+
+            if (assistants == null || assistants.isEmpty()) {
+                log.info("没有需要处理的获客链接");
+                return;
+            }
+
+            int processedCount = 0;
+            int restoredUserCount = 0;
+
+            for (QwAcquisitionAssistant assistant : assistants) {
+                try {
+                    if (assistant.getUserLimitJson() != null && !assistant.getUserLimitJson().isEmpty()) {
+                        com.alibaba.fastjson.JSONArray userLimitArray = com.alibaba.fastjson.JSON.parseArray(assistant.getUserLimitJson());
+
+                        List<String> restoredUserList = new java.util.ArrayList<>();
+                        List<Long> restoredQwUserTableIdList = new java.util.ArrayList<>();
+
+                        for (int i = 0; i < userLimitArray.size(); i++) {
+                            com.alibaba.fastjson.JSONObject userLimit = userLimitArray.getJSONObject(i);
+                            String userId = userLimit.getString("userId");
+                            Long qwUserId = userLimit.getLong("qwUserId");
+
+                            restoredUserList.add(userId);
+                            if (qwUserId != null) {
+                                restoredQwUserTableIdList.add(qwUserId);
+                            }
+                        }
+
+                        assistant.setUserList(com.alibaba.fastjson.JSON.toJSONString(restoredUserList));
+                        assistant.setQwUserTableIdList(com.alibaba.fastjson.JSON.toJSONString(restoredQwUserTableIdList));
+
+
+                        // 构建请求
+                        QwLinkCreateParam linkCreateParam=new QwLinkCreateParam();
+                        linkCreateParam.setLink_id(assistant.getLinkId());
+
+                        QwLinkCreateParam.Range range=new QwLinkCreateParam.Range();
+                        range.setUser_list(JSON.parseArray(assistant.getUserList(), String.class));
+                        linkCreateParam.setRange(range);
+
+                        //调用企微API
+                        QwResult qwResult = qwApiService.linkUpdate(linkCreateParam, assistant.getCorpId());
+                        if (qwResult.getErrcode() == 0) {
+                              acquisitionAssistantMapper.updateQwAcquisitionAssistant(assistant);
+                            restoredUserCount += restoredUserList.size();
+                            processedCount++;
+
+                        }else {
+                            log.error("修改渠道活码 失败!"+linkCreateParam+":"+qwResult.getErrmsg());
+                        }
+
+
+
+                        log.info("获客链接{}已恢复{}个用户的添加额度", assistant.getLinkId(), restoredUserList.size());
+                    }
+                } catch (Exception e) {
+                    log.error("处理获客链接{}时出错", assistant.getId(), e);
+                }
+            }
+
+            long endTimeMillis = System.currentTimeMillis();
+            log.info("====== 获客链接用户额度重置完成,处理{}个链接,恢复{}个用户,耗时 {} 毫秒 ======",
+                    processedCount, restoredUserCount, (endTimeMillis - startTimeMillis));
+        } catch (Exception e) {
+            log.error("重置获客链接用户额度任务执行失败", e);
+        }
+    }
+}

+ 91 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwContactAcquisitionUserServiceImpl.java

@@ -0,0 +1,91 @@
+package com.fs.qw.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.qw.domain.QwContactAcquisitionUser;
+import com.fs.qw.mapper.QwContactAcquisitionUserMapper;
+import com.fs.qw.service.IQwContactAcquisitionUserService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 获客链接限制Service业务层处理
+ *
+ * @author fs
+ * @date 2026-04-20
+ */
+@Service
+public class QwContactAcquisitionUserServiceImpl extends ServiceImpl<QwContactAcquisitionUserMapper, QwContactAcquisitionUser> implements IQwContactAcquisitionUserService {
+
+    /**
+     * 查询获客链接限制
+     *
+     * @param id 获客链接限制主键
+     * @return 获客链接限制
+     */
+    @Override
+    public QwContactAcquisitionUser selectQwContactAcquisitionUserById(Long id)
+    {
+        return baseMapper.selectQwContactAcquisitionUserById(id);
+    }
+
+    /**
+     * 查询获客链接限制列表
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 获客链接限制
+     */
+    @Override
+    public List<QwContactAcquisitionUser> selectQwContactAcquisitionUserList(QwContactAcquisitionUser qwContactAcquisitionUser)
+    {
+        return baseMapper.selectQwContactAcquisitionUserList(qwContactAcquisitionUser);
+    }
+
+    /**
+     * 新增获客链接限制
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 结果
+     */
+    @Override
+    public int insertQwContactAcquisitionUser(QwContactAcquisitionUser qwContactAcquisitionUser)
+    {
+        return baseMapper.insertQwContactAcquisitionUser(qwContactAcquisitionUser);
+    }
+
+    /**
+     * 修改获客链接限制
+     *
+     * @param qwContactAcquisitionUser 获客链接限制
+     * @return 结果
+     */
+    @Override
+    public int updateQwContactAcquisitionUser(QwContactAcquisitionUser qwContactAcquisitionUser)
+    {
+        return baseMapper.updateQwContactAcquisitionUser(qwContactAcquisitionUser);
+    }
+
+    /**
+     * 批量删除获客链接限制
+     *
+     * @param ids 需要删除的获客链接限制主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwContactAcquisitionUserByIds(Long[] ids)
+    {
+        return baseMapper.deleteQwContactAcquisitionUserByIds(ids);
+    }
+
+    /**
+     * 删除获客链接限制信息
+     *
+     * @param id 获客链接限制主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwContactAcquisitionUserById(Long id)
+    {
+        return baseMapper.deleteQwContactAcquisitionUserById(id);
+    }
+}

+ 113 - 6
fs-service/src/main/java/com/fs/qw/service/impl/QwContactWayServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.qw.service.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.BeanCopyUtils;
 import com.fs.common.core.domain.R;
@@ -33,6 +34,7 @@ import com.fs.voice.utils.StringUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -64,6 +66,10 @@ public class QwContactWayServiceImpl implements IQwContactWayService
 
     @Autowired
     private FsCourseWatchLogMapper  watchLogMapper;
+
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
     /**
      * 查询企微活码
      *
@@ -73,7 +79,19 @@ public class QwContactWayServiceImpl implements IQwContactWayService
     @Override
     public QwContactWay selectQwContactWayById(Long id)
     {
-        return qwContactWayMapper.selectQwContactWayById(id);
+        QwContactWay qwContactWay = qwContactWayMapper.selectQwContactWayById(id);
+
+        String userLimitJson = qwContactWay.getUserLimitJson();
+        List<QwContactWayUser> wayUserList = JSON.parseArray(userLimitJson, QwContactWayUser.class);
+        for (QwContactWayUser  wayUsers: wayUserList){
+            String wayLimit = "qwContactWayLimit:" + qwContactWay.getCorpId()  + ":" + wayUsers.getUserId();
+            Long num = Long.valueOf(Objects.requireNonNull(redisTemplate.opsForValue().get(wayLimit)));
+            wayUsers.setDayCount(num);
+        }
+
+        qwContactWay.setUserLimitJson(JSON.toJSONString(wayUserList));
+
+        return qwContactWay;
     }
 
     /**
@@ -292,10 +310,16 @@ public class QwContactWayServiceImpl implements IQwContactWayService
         for (Long id : ids) {
             QwContactWay qwContactWay1 = qwContactWayMapper.selectQwContactWayById(id);
             QwGetContactWayParam qwGetContactWayParam = new QwGetContactWayParam();
-            qwGetContactWayParam.setConfig_id(qwContactWay1.getConfigId());
-            QwResult qwResult = qwApiService.delContactWay(qwGetContactWayParam, qwContactWay1.getCorpId());
-
-            if (qwResult.getErrcode()==0){
+            if (!StringUtil.strIsNullOrEmpty(qwContactWay1.getConfigId())){
+                qwGetContactWayParam.setConfig_id(qwContactWay1.getConfigId());
+                QwResult qwResult = qwApiService.delContactWay(qwGetContactWayParam, qwContactWay1.getCorpId());
+                if (qwResult.getErrcode()==0){
+                    QwContactWay qwContactWay = new QwContactWay();
+                    qwContactWay.setId(id);
+                    qwContactWay.setIsDel(2);
+                    qwContactWayMapper.updateQwContactWay(qwContactWay);
+                }
+            }else {
                 QwContactWay qwContactWay = new QwContactWay();
                 qwContactWay.setId(id);
                 qwContactWay.setIsDel(2);
@@ -315,7 +339,26 @@ public class QwContactWayServiceImpl implements IQwContactWayService
     @Override
     public int deleteQwContactWayById(Long id)
     {
-        return qwContactWayMapper.deleteQwContactWayById(id);
+
+        QwContactWay qwContactWay1 = qwContactWayMapper.selectQwContactWayById(id);
+        QwGetContactWayParam qwGetContactWayParam = new QwGetContactWayParam();
+        if (!StringUtil.strIsNullOrEmpty(qwContactWay1.getConfigId())){
+            qwGetContactWayParam.setConfig_id(qwContactWay1.getConfigId());
+            QwResult qwResult = qwApiService.delContactWay(qwGetContactWayParam, qwContactWay1.getCorpId());
+            if (qwResult.getErrcode()==0){
+                QwContactWay qwContactWay = new QwContactWay();
+                qwContactWay.setId(id);
+                qwContactWay.setIsDel(2);
+                return   qwContactWayMapper.updateQwContactWay(qwContactWay);
+            }
+        }else {
+            QwContactWay qwContactWay = new QwContactWay();
+            qwContactWay.setId(id);
+            qwContactWay.setIsDel(2);
+           return   qwContactWayMapper.updateQwContactWay(qwContactWay);
+        }
+        return 1;
+//        return qwContactWayMapper.deleteQwContactWayById(id);
     }
 
     @Override
@@ -525,6 +568,8 @@ public class QwContactWayServiceImpl implements IQwContactWayService
         watchLogMapper.insertOrUpdateFsCourseWatchLog(watchLog);
     }
 
+
+
     @Override
     public List<QwWayStatisticsListVO> QwWayStatisticsListVO(QwStatisticsParam param) {
         return qwContactWayMapper.QwWayStatisticsListVO(param);
@@ -556,4 +601,66 @@ public class QwContactWayServiceImpl implements IQwContactWayService
 
         }
     }
+
+    @Override
+    public void resetQwContactWayUserLimit() {
+
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 开始重置渠道活码用户每日添加额度 ======");
+
+        try {
+            QwContactWay contactWay = new QwContactWay();
+            contactWay.setIsDel(0);
+            contactWay.setIsUserLimit(1);
+            List<QwContactWay> contactWayList = qwContactWayMapper.selectQwContactWayList(contactWay);
+
+            if (contactWayList == null || contactWayList.isEmpty()) {
+                log.info("没有需要处理的渠道活码");
+                return;
+            }
+
+            int processedCount = 0;
+            int restoredUserCount = 0;
+
+            for (QwContactWay qwContactWay : contactWayList) {
+                try {
+                    String userLimitJson = qwContactWay.getUserLimitJson();
+                    if (StringUtils.isNotEmpty(userLimitJson)) {
+                        List<QwContactWayUser> qwContactWayUsers = JSON.parseArray(userLimitJson, QwContactWayUser.class);
+                        List<String> restoredUserList = new ArrayList<>();
+                        for (QwContactWayUser qwContactWayUser : qwContactWayUsers) {
+                            restoredUserList.add(qwContactWayUser.getUserId());
+                        }
+
+                        QwUpdateContactWayParam qwUpdateContactWayParam = new QwUpdateContactWayParam();
+                        qwUpdateContactWayParam.setConfig_id(qwContactWay.getConfigId());
+                        qwUpdateContactWayParam.setUser(restoredUserList);
+
+                        QwResult qwResult = qwApiService.updateContactWay(qwUpdateContactWayParam, qwContactWay.getCorpId());
+
+                        if (qwResult.getErrcode()==0) {
+                            qwContactWayMapper.updateUserIdsById(qwContactWay.getId(), JSON.toJSONString(restoredUserList));
+                            restoredUserCount += restoredUserList.size();
+                            processedCount++;
+                        }else {
+                            log.error("重置获客链接 失败:{},参数 :{}",qwResult.getErrmsg(),qwContactWay);
+                        }
+
+
+                    }
+                } catch (Exception e) {
+                    log.error("处理获客链接{}时出错", qwContactWay.getId(), e);
+                }
+            }
+
+
+
+            long endTimeMillis = System.currentTimeMillis();
+            log.info("====== 获客链接用户额度重置完成,处理{}个链接,恢复{}个用户,耗时 {} 毫秒 ======",
+                    processedCount, restoredUserCount, (endTimeMillis - startTimeMillis));
+        } catch (Exception e) {
+            log.error("重置获客链接用户额度任务执行失败", e);
+        }
+
+    }
 }

+ 183 - 35
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -178,7 +178,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     @Autowired
     private QwSopMapper qwSopMapper;
     @Autowired
-    RedisTemplate<String, String> redisTemplate;
+    private RedisTemplate<String, String> redisTemplate;
     @Autowired
     private FsUserMapper fsUserMapper;
     @Autowired
@@ -222,6 +222,12 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     @Autowired
     private CloudHostProper cloudHostProper;
 
+    @Autowired
+    QwAcquisitionAssistantMapper acquisitionAssistantMapper;
+
+    @Autowired
+    private AsyncQwContactWayService asyncQwContactWayService;
+
     Logger logger = LoggerFactory.getLogger(getClass());
     @Autowired
     private CompanyWxAccountMapper companyWxAccountMapper;
@@ -2318,53 +2324,68 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         QwExternalContact contact = qwExternalContact;
         QwUser qwUser = qwUserMapper.selectQwUserByCorpIdAndUserId(corpId, userID);
 
+        // 渠道活码里有欢迎语 所有欢迎语优先
         if (state != null && state != "") {
             String s = "way:" + corpId + ":";
             if (state.contains(s)) {
-                if (welcomeCode != null && welcomeCode != "") {
-                    String substring = state.substring(state.indexOf(s) + s.length());
-                    QwContactWay qwContactWay = qwContactWayMapper.selectQwContactWayById(Long.parseLong(substring));
-                    logger.info("qwContactWay:" + qwContactWay);
-                    if (qwContactWay != null) {
-                        isWay = true;
-                        wayId = qwContactWay;
-                        if (qwContactWay.getIsWelcome() != null && qwContactWay.getIsWelcome() == 1) {
-                            Boolean isClose = true;
-                            if (wayId.getIsSpanWelcome() == 1) {
-                                ExternalContact externalContact = externalContactResult.getExternal_contact();
-                                String name = externalContact.getName();
-                                String closeWelcomeWord = wayId.getCloseWelcomeWord();
-                                if (closeWelcomeWord != null && closeWelcomeWord.length() > 0) {
-                                    List<String> strings = JSON.parseArray(closeWelcomeWord, String.class);
-                                    for (String string : strings) {
-                                        if (name.contains(string)) {
-                                            isClose = false;
-                                            break;
-                                        }
-                                    }
-                                }
+                String substring = state.substring(state.indexOf(s) + s.length());
+                QwContactWay qwContactWay = qwContactWayMapper.selectQwContactWayById(Long.parseLong(substring));
+                if (qwContactWay != null) {
+                    if (qwContactWay.getUserType() == 1 && qwContactWay.getIsUserLimit() == 1) {
+
+                        String userLimitJson = qwContactWay.getUserLimitJson();
+                        List<QwContactWayUser> qwContactWayUsers = JSON.parseArray(userLimitJson, QwContactWayUser.class);
+
+                        String wayLimit = "qwContactWayLimit:" + corpId  + ":" + userID;
+
+                        QwContactWayUser matchedUser = qwContactWayUsers.stream()
+                                .filter(u -> userID.equals(u.getUserId()))
+                                .findFirst()
+                                .orElse(null);
+
+                        if (matchedUser != null) {
+                            Long currentCount = redisTemplate.opsForValue().increment(wayLimit, 1);
+                            if (currentCount == null) {
+                                currentCount = 1L;
                             }
-                            if (qwContactWay.getIsWelcome() == 1 && isClose) {
-                                isSend = qwContactWayService.sendWelcomeMsg(qwContactWay, corpId, welcomeCode, qwUser, contact.getId());
+
+                            if(currentCount == 1L){
+                                long expireSeconds = getSecondsUntilMidnight();
+                                redisTemplate.expire(wayLimit, expireSeconds, java.util.concurrent.TimeUnit.SECONDS);
                             }
-                        }
-                        if (qwContactWay.getUserType() == 1 && qwContactWay.getIsUserLimit() == 1) {
-                            QwContactWayUser qwContactWayUser = qwContactWayUserMapper.selectQwContactWayUserByUserIdAndCompanyId(userID, corpId);
-                            if (qwContactWayUser != null) {
-                                qwContactWayUser.setDayCount(qwContactWayUser.getDayCount() - 1);
-                                qwContactWayUserMapper.updateQwContactWayUser(qwContactWayUser);
-                                if (qwContactWayUser.getDayCount() <= 0) {
-                                    //超过限制
-                                    qwContactWayService.updateQwContactWayBYLimit(qwContactWayUser.getWayId());
+                            if(currentCount >= matchedUser.getLimitCount()){
+                                log.error("用户{}已达到每日添加上限{}/{}", userID, currentCount, matchedUser.getLimitCount());
+
+                                boolean needUpdate = false;
+
+                                List<String> userIdList = JSON.parseArray(qwContactWay.getUserIds(), String.class);
+
+                                if (userIdList != null && !userIdList.isEmpty() && userIdList.contains(userID) && !userIdList.contains(qwContactWay.getSpareUserIds())) {
+                                    //移除渠道活码里面的人员-保留 备选人员
+                                    userIdList.remove(userID);
+                                    //移除渠道活码里面的人员-保留 备选人员
+                                    if (userIdList.isEmpty()){
+                                        List<String> newUserIdList =new ArrayList<>();
+                                        newUserIdList.add(JSON.parseObject(qwContactWay.getSpareUserIds(), String.class));
+                                        qwContactWay.setUserIds(JSON.toJSONString(newUserIdList));
+                                    }else {
+                                        qwContactWay.setUserIds(JSON.toJSONString(userIdList));
+                                    }
+                                    needUpdate = true;
                                 }
 
+                                if (needUpdate) {
+                                    asyncQwContactWayService.executeUpdateContactWay(qwContactWay,userID);
+                                }
                             }
                         }
+
                     }
                 }
+
             }
         }
-
+        // 欢迎语优先
         if (isSend && welcomeCode != null && welcomeCode != "") {
             if (qwUser != null) {
                 // 查询成员的欢迎语以及欢迎图片
@@ -2460,6 +2481,124 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
             }
         }
 
+        // 这里获客链接 没有欢迎语,延后
+        if (state != null && state != "") {
+            String userPhone = "up:";
+            String linkState = "link:";
+            if (state.contains(userPhone)) {
+                log.error("获客链接添加外部联系人小程序用户手机号");
+                try {
+                    //获取手机号数据
+                    String phone = state.substring(state.indexOf(userPhone) + userPhone.length());
+                    //更新外部联系人小程序用户手机号数据
+                    QwExternalContact updateContact = new QwExternalContact();
+                    updateContact.setId(contact.getId());
+                    updateContact.setFsUserPhone(phone);
+                    qwExternalContactMapper.updateQwExternalContact(updateContact);
+                } catch (Exception e) {
+                    log.error("获客链接添加外部联系人小程序用户手机号失败,错误信息:{}",e.getMessage());
+                }
+            }
+
+            if (state.contains(linkState)){
+                try {
+                    log.info("获客链接添加外部联系人");
+                    String linkId = state.substring(state.indexOf(linkState) + linkState.length());
+                    QwAcquisitionAssistant qwAcquisitionAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(Long.valueOf(linkId));
+
+                    if (qwAcquisitionAssistant != null && StringUtils.isNotBlank(qwAcquisitionAssistant.getUserLimitJson())) {
+                        try {
+                            qwExternalContact.setQwAcquisitionAssistantId(qwAcquisitionAssistant.getId());//对于获客链接加好友回调需要保存获客助手id
+                            String cacheKey = "qwAcquisition:" + corpId + ":" + linkId + ":" + userID;
+
+                            com.alibaba.fastjson.JSONArray userLimitArray = com.alibaba.fastjson.JSON.parseArray(qwAcquisitionAssistant.getUserLimitJson());
+                            for (int i = 0; i < userLimitArray.size(); i++) {
+                                com.alibaba.fastjson.JSONObject userLimit = userLimitArray.getJSONObject(i);
+                                String limitUserId = userLimit.getString("userId");
+                                Integer limitCount = userLimit.getInteger("limitCount");
+                                Integer qwUserId = userLimit.getInteger("qwUserId");
+
+                                if (userID.equals(limitUserId)) {
+                                    Long currentCount = redisTemplate.opsForValue().increment(cacheKey, 1);
+
+                                    if (currentCount == null) {
+                                        currentCount = 1L;
+                                    }
+
+                                    if (currentCount==1L){
+                                        long expireSeconds = getSecondsUntilMidnight();
+                                        redisTemplate.expire(cacheKey, expireSeconds, java.util.concurrent.TimeUnit.SECONDS);
+                                    }
+
+                                    if (currentCount >= limitCount) {
+                                        log.error("用户{}已达到每日添加上限{}/{}", userID, currentCount, limitCount);
+
+//                                    redisTemplate.delete(cacheKey);
+
+                                        List<String> currentUserList = com.alibaba.fastjson.JSON.parseArray(qwAcquisitionAssistant.getUserList(), String.class);
+                                        if (currentUserList == null && StringUtils.isNotBlank(qwAcquisitionAssistant.getUserList())) {
+                                            currentUserList = com.alibaba.fastjson.JSON.parseArray(qwAcquisitionAssistant.getUserList(), String.class);
+                                        }
+
+                                        List<Long> currentQwUserTableIdList = null;
+                                        if (StringUtils.isNotBlank(qwAcquisitionAssistant.getQwUserTableIdList())) {
+                                            currentQwUserTableIdList = com.alibaba.fastjson.JSON.parseArray(qwAcquisitionAssistant.getQwUserTableIdList(), Long.class);
+                                        }
+
+                                        boolean needUpdate = false;
+
+                                        if (currentUserList != null && currentUserList.contains(userID)) {
+                                            currentUserList.remove(userID);
+                                            qwAcquisitionAssistant.setUserList(com.alibaba.fastjson.JSON.toJSONString(currentUserList));
+                                            needUpdate = true;
+                                        }
+
+                                        if (currentQwUserTableIdList != null && currentQwUserTableIdList.contains(Long.valueOf(qwUserId))) {
+                                            currentQwUserTableIdList.remove(Long.valueOf(qwUserId));
+                                            qwAcquisitionAssistant.setQwUserTableIdList(com.alibaba.fastjson.JSON.toJSONString(currentQwUserTableIdList));
+                                            needUpdate = true;
+                                        }
+
+                                        if (needUpdate) {
+                                            // 构建请求
+                                            QwLinkCreateParam linkCreateParam=new QwLinkCreateParam();
+                                            linkCreateParam.setLink_id(qwAcquisitionAssistant.getLinkId());
+
+                                            QwLinkCreateParam.Range range=new QwLinkCreateParam.Range();
+//                                        range.setUser_list(currentUserList);
+                                            range.setUser_list(JSON.parseArray(qwAcquisitionAssistant.getUserList(), String.class));
+                                            linkCreateParam.setRange(range);
+
+                                            //调用企微API
+                                            QwResult qwResult = qwApiService.linkUpdate(linkCreateParam, corpId);
+                                            if (qwResult.getErrcode() == 0) {
+                                                acquisitionAssistantMapper.updateQwAcquisitionAssistant(qwAcquisitionAssistant);
+                                                log.info("已从获客链接{}的可用列表中移除用户{}", linkId, userID);
+                                            }else {
+                                                logger.error("修改渠道活码 失败!"+linkCreateParam+":"+qwResult.getErrmsg());
+                                            }
+
+                                        }
+
+                                    } else {
+                                        log.info("用户{}今日已添加客户数:{}/{}", userID, currentCount, limitCount);
+                                    }
+                                    break;
+                                }
+                            }
+                        } catch (Exception e) {
+                            log.error("处理获客链接用户限额失败", e);
+                        }
+                    }
+                }catch (Exception e){
+                    log.error("处理获客链接用户限额失败", e);
+                }
+
+
+
+            }
+        }
+
         ExternalContact externalContact = externalContactResult.getExternal_contact();
         List<FollowUser> followUsers = externalContactResult.getFollow_user();
         logger.info("外部联系人的情况里面0:" + userID + ":" + externalUserID + ":" + followUsers + ":" + externalContactResult.getErrmsg());
@@ -2921,6 +3060,12 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         }
     }
 
+    private long getSecondsUntilMidnight() {
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime midnight = now.toLocalDate().plusDays(1).atStartOfDay();
+        return java.time.Duration.between(now, midnight).getSeconds();
+    }
+
     public void checkHaveQwSop(Set<String> combinedTagsSet, QwUser qwUser, String corpId, List<String> combinedTagsList,
                                String userID, String externalUserID, ExternalContact externalContact, QwExternalContact contact,
                                LocalDate currentDate, LocalTime localTime) {
@@ -3325,6 +3470,9 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
             contact1.setTransferStatus(1);
             contact1.setExternalUserId(transferLogListByCheck.getExternalUserId());
             qwExternalContactMapper.updateQwExternalContactByUseridTransfer(contact1);
+
+            //同步转接记录里的fs_user_id
+            qwExternalContact.setFsUserId(transferLogListByCheck.getFsUserId());
         }
 
         //上面存过了,这里就更新

+ 24 - 15
fs-service/src/main/java/com/fs/qw/vo/AcquisitionAssistantDetailVO.java

@@ -1,6 +1,7 @@
 package com.fs.qw.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
 import lombok.Data;
 
 import java.util.Date;
@@ -10,31 +11,31 @@ import java.util.Date;
  */
 @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;
 
@@ -46,20 +47,28 @@ public class AcquisitionAssistantDetailVO {
 
     // 优先分配成员列表(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;
-}
+
+    /** 员工添加上限 */
+    @Excel(name = "员工添加上限")
+    private String userLimitJson;
+
+    /** 备用员工 */
+    @Excel(name = "备用员工")
+    private String spareUserIds;
+}

+ 86 - 0
fs-service/src/main/resources/mapper/qw/QwContactAcquisitionUserMapper.xml

@@ -0,0 +1,86 @@
+<?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.QwContactAcquisitionUserMapper">
+
+    <resultMap type="QwContactAcquisitionUser" id="QwContactAcquisitionUserResult">
+        <result property="id"    column="id"    />
+        <result property="linkId"    column="link_id"    />
+        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="limitCount"    column="limit_count"    />
+        <result property="dayCount"    column="day_count"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="corpId"    column="corp_id"    />
+    </resultMap>
+
+    <sql id="selectQwContactAcquisitionUserVo">
+        select id, link_id, qw_user_id, user_id, limit_count, day_count, company_id, corp_id from qw_contact_acquisition_user
+    </sql>
+
+    <select id="selectQwContactAcquisitionUserList" parameterType="QwContactAcquisitionUser" resultMap="QwContactAcquisitionUserResult">
+        <include refid="selectQwContactAcquisitionUserVo"/>
+        <where>
+            <if test="linkId != null "> and link_id = #{linkId}</if>
+            <if test="qwUserId != null "> and qw_user_id = #{qwUserId}</if>
+            <if test="userId != null  and userId != ''"> and user_id = #{userId}</if>
+            <if test="limitCount != null "> and limit_count = #{limitCount}</if>
+            <if test="dayCount != null "> and day_count = #{dayCount}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+        </where>
+    </select>
+
+    <select id="selectQwContactAcquisitionUserById" parameterType="Long" resultMap="QwContactAcquisitionUserResult">
+        <include refid="selectQwContactAcquisitionUserVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwContactAcquisitionUser" parameterType="QwContactAcquisitionUser" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_contact_acquisition_user
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="linkId != null">link_id,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="limitCount != null">limit_count,</if>
+            <if test="dayCount != null">day_count,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="corpId != null">corp_id,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="linkId != null">#{linkId},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="limitCount != null">#{limitCount},</if>
+            <if test="dayCount != null">#{dayCount},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="corpId != null">#{corpId},</if>
+         </trim>
+    </insert>
+
+    <update id="updateQwContactAcquisitionUser" parameterType="QwContactAcquisitionUser">
+        update qw_contact_acquisition_user
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="linkId != null">link_id = #{linkId},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="limitCount != null">limit_count = #{limitCount},</if>
+            <if test="dayCount != null">day_count = #{dayCount},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="corpId != null">corp_id = #{corpId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwContactAcquisitionUserById" parameterType="Long">
+        delete from qw_contact_acquisition_user where id = #{id}
+    </delete>
+
+    <delete id="deleteQwContactAcquisitionUserByIds" parameterType="String">
+        delete from qw_contact_acquisition_user where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 17 - 1
fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

@@ -44,10 +44,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="lastWatchTime"    column="last_watch_time"    />
         <result property="registerTime"    column="register_time"    />
         <result property="isReply"    column="is_reply"    />
+        <result property="fsUserPhone"    column="fs_user_phone"    />
+        <result property="addSourceType"    column="add_source_type"    />
+        <result property="qwAcquisitionAssistantId"    column="qw_acquisition_assistant_id"    />
     </resultMap>
 
     <sql id="selectQwExternalContactVo">
-        select id,qw_user_id,register_time,state,way_id,stage_status,first_time,open_id,is_interact,level, unionid, user_id,transfer_time,loss_time,del_time,transfer_num, external_user_id,transfer_status,status,create_time, name, avatar, type, gender, remark, description, tag_ids, remark_mobiles, remark_corp_name, add_way, oper_userid, corp_id, company_id, company_user_id, customer_id, fs_user_id,is_reply from qw_external_contact
+        select id,qw_user_id,register_time,state,way_id,stage_status,first_time,open_id,is_interact,level, unionid,
+               user_id,transfer_time,loss_time,del_time,transfer_num, external_user_id,transfer_status,status,create_time,
+               name, avatar, type, gender, remark, description, tag_ids, remark_mobiles, remark_corp_name, add_way, oper_userid,
+               corp_id, company_id, company_user_id, customer_id, fs_user_id,is_reply,fs_user_phone,add_source_type,qw_acquisition_assistant_id
+        from qw_external_contact
     </sql>
 
     <select id="selectQwExternalContactList" parameterType="QwExternalContact" resultMap="QwExternalContactResult">
@@ -275,6 +282,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="levelType != null">level_type,</if>
             <if test="firstTime != null">first_time,</if>
             <if test="registerTime != null">register_time,</if>
+            <if test="addSourceType != null">add_source_type,</if>
+            <if test="qwAcquisitionAssistantId != null">qw_acquisition_assistant_id,</if>
+            <if test="registerTime != null">register_time,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="id != null">#{id},</if>
@@ -314,6 +324,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="levelType != null">#{levelType},</if>
             <if test="firstTime != null">#{firstTime},</if>
             <if test="registerTime != null">#{registerTime},</if>
+            <if test="addSourceType != null">#{addSourceType},</if>
+            <if test="qwAcquisitionAssistantId != null">#{qwAcquisitionAssistantId},</if>
+            <if test="registerTime != null">#{registerTime},</if>
          </trim>
     </insert>
 
@@ -356,6 +369,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="levelType != null">level_type = #{levelType},</if>
             <if test="firstTime != null">first_time = #{firstTime},</if>
             <if test="registerTime != null">register_time = #{registerTime},</if>
+            <if test="fsUserPhone != null">fs_user_phone = #{fsUserPhone},</if>
+            <if test="addSourceType != null">add_source_type = #{addSourceType},</if>
+            <if test="qwAcquisitionAssistantId != null">qw_acquisition_assistant_id = #{qwAcquisitionAssistantId},</if>
         </trim>
         where id = #{id}
     </update>