Ver Fonte

益寿缘-销售端-优化获客链接组成部分

cgp há 10 horas atrás
pai
commit
634e28eebb

+ 256 - 9
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionLinkInfoController.java

@@ -1,19 +1,24 @@
 package com.fs.company.controller.qw;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.http.HttpServletResponse;
 
+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.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.poi.hssf.usermodel.HSSFCell;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -22,20 +27,32 @@ import com.fs.qw.domain.QwAcquisitionLinkInfo;
 import com.fs.qw.service.IQwAcquisitionLinkInfoService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
-
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.InputStream;
+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 IQwAcquisitionLinkInfoService qwAcquisitionLinkInfoService;
 
+    // 定义手机号正则表达式
+    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
     /**
      * 查询获客链接-号码链接生成记录列表
      */
@@ -102,4 +119,234 @@ public class QwAcquisitionLinkInfoController extends BaseController
     {
         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);
+
+            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());
+        }
+    }
+
+    /**
+     * 批量生成多手机号短链
+     * */
+    @PostMapping("/batchCreateMessageLink")
+    public AjaxResult batchCreateMessageLink(@RequestParam("file") MultipartFile file,
+                                             @RequestParam("qwAcquisitionAssistantId") Long qwAcquisitionAssistantId,
+                                             @RequestParam("qwAcquisitionAssistantUrl") String qwAcquisitionAssistantUrl) {
+
+        // 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文件中未找到有效的电话号码数据");
+            }
+
+            // 4. 构建DTO对象
+            BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO = new BatchAddAcquisitionLinkDTO();
+            batchAddAcquisitionLinkDTO.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+            batchAddAcquisitionLinkDTO.setQwAcquisitionAssistantUrl(qwAcquisitionAssistantUrl);
+            batchAddAcquisitionLinkDTO.setPhoneList(phoneList);
+
+            // 5. 调用服务层方法处理
+            int count = qwAcquisitionLinkInfoService.batchCreateMessageLink(batchAddAcquisitionLinkDTO);
+
+            return AjaxResult.success("成功处理 " + count + " 条记录");
+
+        } catch (Exception e) {
+            log.error("上传Excel并生成短链失败", 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 "";
+        }
+    }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionLinkInfo.java

@@ -33,4 +33,9 @@ public class QwAcquisitionLinkInfo extends BaseEntity {
      */
     private String phone;
 
+    /**
+     * 随机字符串
+     */
+    private String randomStr;
+
 }

+ 24 - 0
fs-service/src/main/java/com/fs/qw/dto/BatchAddAcquisitionLinkDTO.java

@@ -0,0 +1,24 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class BatchAddAcquisitionLinkDTO {
+
+    /**
+     * 获客链接管理主键ID
+     */
+    private Long qwAcquisitionAssistantId;
+
+    /**
+     * 获客链接管理url
+     */
+    private String qwAcquisitionAssistantUrl;
+
+    /**
+     * 客户电话列表
+     */
+    private List<String> phoneList;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwAcquisitionLinkInfoMapper.java

@@ -67,4 +67,24 @@ public interface QwAcquisitionLinkInfoMapper
      * @return 结果
      */
     public int deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(@Param("qwAcquisitionAssistantIds") Long[] qwAcquisitionAssistantIds);
+
+    /**
+     * 查询已存在的随机字符串
+     * */
+    List<String> selectAllRandomStr();
+
+    /**
+     * 根据随机字符串查询获客链接
+     * */
+    String selectQwAcquisitionUrlByRandomStr(String randomStr);
+
+    /**
+     *  根据主键id列表批量查询获客链接记录列表
+     * */
+    List<QwAcquisitionLinkInfo> selectAcquisitionLinkInfoListByIds(Long[] ids);
+
+    /**
+     *  根据获客链接管理主键id列表批量查询获客链接记录列表
+     * */
+    List<QwAcquisitionLinkInfo> selectAcquisitionLinkInfoListByAcquisitionAssistantIds(Long[] qwAcquisitionAssistantIds);
 }

+ 26 - 2
fs-service/src/main/java/com/fs/qw/service/IQwAcquisitionLinkInfoService.java

@@ -1,7 +1,11 @@
 package com.fs.qw.service;
 
 import java.util.List;
+
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
 import com.fs.qw.domain.QwAcquisitionLinkInfo;
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
 
 /**
  * 获客链接-号码链接生成记录Service接口
@@ -68,7 +72,27 @@ public interface IQwAcquisitionLinkInfoService
     public int deleteQwAcquisitionLinkInfoById(Long id);
 
     /**
-     * 添加链接生成记录
+     * 发送获客链接短信
+     *
+     * @param phone 待发送短信的手机号
+     * @param qwAcquisitionId 获客链接Id
+     * @return 结果
+     */
+    public SendResultDetailDTO sendMessageLink(String phone, Long qwAcquisitionId, SendMsgLogBo sendMsgLogBo);
+
+    /**
+     * 根据页面路径参数查询完整获客链接url
+     * */
+    public String selectQwAcquisitionUrlByRandomStr(String randomStr);
+
+
+    /**
+     * 批量生成获客链接短链
+     * */
+    public int batchCreateMessageLink(BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO);
+
+    /**
+     * 根据手机号生成单个获客链接
      * */
-    public int buildQwAcquisitionLinkInfoAdd(Long qwAcquisitionAssistantId,String originalPhone,String originalLink);
+    String extractLink(Long qwAcquisitionAssistantId, String originalPhone, String originalLink);
 }

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

@@ -119,7 +119,7 @@ public class QwAcquisitionAssistantServiceImpl implements IQwAcquisitionAssistan
             if (r != null && "200".equals(String.valueOf(r.get("code")))) {
 
                 //新增号码-链接生成记录
-                linkInfoService.buildQwAcquisitionLinkInfoAdd(acquisitionAssistant.getId(), phone, acquisitionAssistant.getUrl());
+                //linkInfoService.buildQwAcquisitionLinkInfoAdd(acquisitionAssistant.getId(), phone, acquisitionAssistant.getUrl());
 
                 return new SendResultDetailDTO(true, null, null);
             } else {

+ 276 - 3
fs-service/src/main/java/com/fs/qw/service/impl/QwAcquisitionLinkInfoServiceImpl.java

@@ -1,6 +1,27 @@
 package com.fs.qw.service.impl;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+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.StringUtils;
+import com.fs.company.domain.CompanySmsTemp;
+import com.fs.company.service.ICompanySmsTempService;
+import com.fs.his.dto.SendResultDetailDTO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.mapper.QwAcquisitionAssistantMapper;
+import com.fs.qw.utils.UniqueStringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.qw.mapper.QwAcquisitionLinkInfoMapper;
@@ -15,15 +36,36 @@ import static com.fs.his.utils.PhoneUtil.encryptPhone;
  * @author fs
  * @date 2026-03-27
  */
+@Slf4j
 @Service
 public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoService
 {
+    @Autowired
+    private ISmsService smsService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ICompanySmsTempService smsTempService;
+
+    @Autowired
+    private QwAcquisitionAssistantMapper acquisitionAssistantMapper;
+
     @Autowired
     private QwAcquisitionLinkInfoMapper qwAcquisitionLinkInfoMapper;
 
-    //组装完整链接后缀(这个后面拼接加密后的手机字符串)
+    //拼接电话号码的链接后缀(这个后面拼接加密后的手机字符串)
     private static final String  LINK_SUFFIX = "?customer_channel=up:";
 
+    // 企微加好友链接-url的key
+    private static final String QW_FRIEND_LINK_URL_KEY = "qw_friend_link_url:";
+
+    //获客链接短信模板code
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
+
+    //访问链接域名
+    private static final String  LINK_DOMAIN = "https://c.ysyd.top/";
     /**
      * 查询获客链接-号码链接生成记录
      *
@@ -81,6 +123,12 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
     @Override
     public int deleteQwAcquisitionLinkInfoByIds(Long[] ids)
     {
+        if (ids.length == 0){
+            return 0;
+        }
+        List<QwAcquisitionLinkInfo> toBeDeletedList = qwAcquisitionLinkInfoMapper.selectAcquisitionLinkInfoListByIds(ids);
+        //循环删除 Redis 缓存
+        batchDeleteLinkCatch(toBeDeletedList);
         return qwAcquisitionLinkInfoMapper.deleteQwAcquisitionLinkInfoByIds(ids);
     }
 
@@ -92,6 +140,12 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
      */
     @Override
     public int deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(Long[] qwAcquisitionAssistantIds){
+        if (qwAcquisitionAssistantIds.length == 0){
+            return 0;
+        }
+        List<QwAcquisitionLinkInfo> toBeDeletedList = qwAcquisitionLinkInfoMapper.selectAcquisitionLinkInfoListByAcquisitionAssistantIds(qwAcquisitionAssistantIds);
+        //循环删除 Redis 缓存
+        batchDeleteLinkCatch(toBeDeletedList);
         return qwAcquisitionLinkInfoMapper.deleteQwAcquisitionLinkInfoByQwAcquisitionAssistantIds(qwAcquisitionAssistantIds);
     }
 
@@ -104,13 +158,164 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
     @Override
     public int deleteQwAcquisitionLinkInfoById(Long id)
     {
+        QwAcquisitionLinkInfo existAcquisitionLinkInfo = qwAcquisitionLinkInfoMapper.selectQwAcquisitionLinkInfoById(id);
+        if (existAcquisitionLinkInfo==null){
+            throw new CustomException("数据不存在");
+        }
+        // ========== 删除Redis缓存 ==========
+        try {
+            // 1. 删除pageParam对应的URL缓存
+            if (StringUtils.isNotEmpty(existAcquisitionLinkInfo.getRandomStr())) {
+                String urlCacheKey = QW_FRIEND_LINK_URL_KEY + existAcquisitionLinkInfo.getRandomStr();
+                redisCache.deleteObject(urlCacheKey);
+                log.info("删除获客链接URL缓存成功, pageParam: {}, key: {}",
+                        existAcquisitionLinkInfo.getRandomStr(), urlCacheKey);
+            }
+        } catch (Exception e) {
+            // 缓存删除失败不应该影响主流程,但需要记录日志
+            log.error("删除获客链接缓存失败, id: {}, qwAcquisitionAssistantId: {}, pageParam: {}",
+                    existAcquisitionLinkInfo.getId(), existAcquisitionLinkInfo.getQwAcquisitionAssistantId(),
+                    existAcquisitionLinkInfo.getRandomStr(), e);
+        }
         return qwAcquisitionLinkInfoMapper.deleteQwAcquisitionLinkInfoById(id);
     }
 
+    @Override
+    public SendResultDetailDTO sendMessageLink(String phone, Long qwAcquisitionId, SendMsgLogBo sendMsgLogBo) {
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_LINK_TEMPLATE_CODE);
+        if (temp == null) {
+            log.info("获客链接-未找到短信模板:{}", SMS_LINK_TEMPLATE_CODE);
+            throw new CustomException("获客链接-未找到短信模板");
+        }
+        String originalContent = temp.getContent();
+        //获取获客链接管理信息
+        QwAcquisitionAssistant acquisitionAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
+        if (acquisitionAssistant == null){
+            log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
+            throw new CustomException("获客链接-未找到获客链接信息");
+        }
+        String randomStr = generateUniqueRandomStr();
+        String replaceText=LINK_DOMAIN+randomStr;
+        String content = originalContent.replace("${sms.friendLink}", replaceText);
+        try {
+            sendMsgLogBo.setQwAcquisitionId(acquisitionAssistant.getId());
+            R r = smsService.simpleSmsSend(phone, content, temp, SmsLogType.ACQUISITION_LINK, sendMsgLogBo);
+
+            if (r != null && "200".equals(String.valueOf(r.get("code")))) {
+
+                //新增号码-链接生成记录
+                addAcquisitionLinkInfo(acquisitionAssistant.getId(), phone, acquisitionAssistant.getUrl(),randomStr);
+
+                return new SendResultDetailDTO(true, null, null);
+            } else {
+                String msg = r != null && r.get("msg") != null ? r.get("msg").toString() : "未知错误";
+                log.warn("短信发送失败 获客链接id={}, phone={}, msg={}", qwAcquisitionId, phone, msg);
+                return new SendResultDetailDTO(false, msg, null);
+            }
+        } catch (Exception e) {
+            log.error("发送异常 获客链接id=" + qwAcquisitionId, e);
+            return new SendResultDetailDTO(false, e.getMessage(), null);
+        }
+    }
+
+    @Override
+    public String selectQwAcquisitionUrlByRandomStr(String randomStr) {
+        log.error("-------------------------------------进入selectQwAcquisitionUrlByRandomStr-----------------------------------");
+        String key = QW_FRIEND_LINK_URL_KEY + randomStr;
+        String fullLink = null;
+
+        try {
+            Object cacheObj = redisCache.getCacheObject(key);
+            if (cacheObj instanceof String) {
+                fullLink = (String) cacheObj;
+                // 处理缓存空值的情况
+                if ("NULL".equals(fullLink)) {
+                    return null;
+                }
+                log.debug("从缓存获取完整获客链接url成功,randomStr:{}", randomStr);
+                return fullLink;
+            }
+        } catch (Exception e) {
+            log.warn("从缓存获取完整获客链接url异常, 将重新获取, randomStr:{}", randomStr);
+        }
+
+        // 缓存中没有,查询数据库
+        fullLink = qwAcquisitionLinkInfoMapper.selectQwAcquisitionUrlByRandomStr(randomStr);
+
+        // 缓存处理(包括空值缓存)
+        if (fullLink == null) {
+            int nullCacheExpire = 10; // 10秒
+            redisCache.setCacheObject(key, "NULL", nullCacheExpire, TimeUnit.SECONDS);
+            log.info("完整获客链接URL不存在,缓存空值10秒, randomStr:{}", randomStr);
+            return null;
+        } else {
+            // 正常值仍缓存10天
+            Integer cacheExpire = 10;
+            redisCache.setCacheObject(key, fullLink, cacheExpire, TimeUnit.DAYS);
+            log.info("完整获客链接URL缓存成功, randomStr:{}", randomStr);
+        }
+
+        return fullLink;
+    }
+
+    @Override
+    public int batchCreateMessageLink(BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO) {
+        Long qwAcquisitionAssistantId = batchAddAcquisitionLinkDTO.getQwAcquisitionAssistantId();
+        String qwAcquisitionAssistantUrl = batchAddAcquisitionLinkDTO.getQwAcquisitionAssistantUrl();
+        List<String> phoneList = batchAddAcquisitionLinkDTO.getPhoneList();
+        int result = 0;
+        for (String phone : phoneList) {
+            try {
+                //新增号码-链接生成记录
+                String randomStr = generateUniqueRandomStr();
+                int addResult = addAcquisitionLinkInfo(qwAcquisitionAssistantId, phone, qwAcquisitionAssistantUrl, randomStr);
+                result += addResult;
+                // 可以在这里根据 addResult 判断单次是否成功,并记录日志
+                if (addResult > 0) {
+                    log.debug("成功为手机号 {} 创建获客链接", phone);
+                } else {
+                    log.warn("为手机号 {} 创建获客链接失败", phone);
+                }
+            } catch (Exception e) {
+                // 捕获异常,记录错误,但不中断整个循环
+                log.error("为手机号 {} 创建获客链接时发生异常", phone, e);
+                // 可以选择在此处继续下一次循环,或者根据业务要求决定是否中断
+            }
+        }
+        log.info("批量创建获客链接完成,总计尝试 {}, 成功 {}", phoneList.size(), result);
+        return result;
+    }
+
+    @Override
+    public String extractLink(Long qwAcquisitionAssistantId, String originalPhone, String originalLink) {
+        String randomStr = generateUniqueRandomStr();
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
+        //加密手机号
+        String phonePlus = encryptPhone(originalPhone);
+        String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        // ========== 缓存URL,便于后续通过randomStr访问 ==========
+        try {
+            String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
+            Integer cacheExpire = 10; // 默认缓存10天
+            redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
+        } catch (Exception e) {
+            // 缓存失败不影响主流程,但需要记录日志
+            log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
+        }
+        // 返回域名+随机字符串
+        return LINK_DOMAIN+randomStr;
+    }
+
     /**
      * 添加链接生成记录
      * */
-    public int buildQwAcquisitionLinkInfoAdd(Long qwAcquisitionAssistantId,String originalPhone,String originalLink){
+    public int addAcquisitionLinkInfo(Long qwAcquisitionAssistantId,String originalPhone,String originalLink,String randomStr){
         QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
         qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
         qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
@@ -118,6 +323,74 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
         String phonePlus = encryptPhone(originalPhone);
         String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
         qwAcquisitionLinkInfo.setLink(linkPlus);
-        return qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        // ========== 缓存URL,便于后续通过randomStr访问 ==========
+        try {
+            String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
+            Integer cacheExpire = 10; // 默认缓存10天
+            redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
+            log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
+        } catch (Exception e) {
+            // 缓存失败不影响主流程,但需要记录日志
+            log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
+        }
+        return addResult;
+    }
+
+
+    /**
+     * 生成唯一的页面参数
+     */
+    private String generateUniqueRandomStr() {
+        // 获取所有已存在的pageParam(只取需要的字段)
+        List<String> existingParams = qwAcquisitionLinkInfoMapper.selectAllRandomStr();
+        //使用Set,提高查找效率 O(1)
+        Set<String> paramSet = new HashSet<>(existingParams);
+
+        int maxAttempts = 10; // 设置最大尝试次数
+        int attempt = 0;
+
+        while (attempt < maxAttempts) {
+            // 生成7位随机码
+            String candidate = UniqueStringUtil.generateTimeBasedUnique(7);
+
+            // 使用Set的contains方法,O(1)复杂度
+            if (!paramSet.contains(candidate)) {
+                log.debug("生成页面参数成功: {}, 尝试次数: {}", candidate, attempt + 1);
+                return candidate;
+            }
+
+            attempt++;
+            log.debug("页面参数 {} 已存在,重新生成,第{}次尝试", candidate, attempt);
+        }
+
+        // 如果多次尝试都失败,使用+1随机数方案
+        String finalParam = UniqueStringUtil.generateTimeBasedUnique(8);
+        log.warn("多次尝试后使用7位参数: {}", finalParam);
+        return finalParam;
+    }
+
+    /**
+     *  批量删除缓存
+     * */
+    private void batchDeleteLinkCatch(List<QwAcquisitionLinkInfo> toBeDeletedList) {
+        if (!toBeDeletedList.isEmpty()) {
+            for (QwAcquisitionLinkInfo record : toBeDeletedList) {
+                try {
+                    // 检查 randomStr 是否为空,避免处理无效的缓存 key
+                    if (StringUtils.isNotEmpty(record.getRandomStr())) {
+                        String urlCacheKey = QW_FRIEND_LINK_URL_KEY + record.getRandomStr();
+                        redisCache.deleteObject(urlCacheKey);
+                        log.info("批量删除获客链接URL缓存成功, randomStr: {}, key: {}",
+                                record.getRandomStr(), urlCacheKey);
+                    }
+                } catch (Exception e) {
+                    // 缓存删除失败不应影响主数据库删除流程,但需要记录日志
+                    log.error("批量删除获客链接缓存失败, id: {}, qwAcquisitionAssistantId: {}, randomStr: {}",
+                            record.getId(), record.getQwAcquisitionAssistantId(), record.getRandomStr(), e);
+                }
+            }
+        }
     }
 }

+ 37 - 1
fs-service/src/main/resources/mapper/qw/QwAcquisitionLinkInfoMapper.xml

@@ -14,10 +14,11 @@
         <result property="updateBy" column="update_by"/>
         <result property="updateTime" column="update_time"/>
         <result property="remark" column="remark"/>
+        <result property="randomStr" column="random_str"/>
     </resultMap>
 
     <sql id="selectQwAcquisitionLinkInfoVo">
-        select id, qw_acquisition_assistant_id, link, phone, create_by, create_time, update_by, update_time, remark from qw_acquisition_link_info
+        select id, qw_acquisition_assistant_id, link, phone, create_by, create_time, update_by, update_time, remark,random_str from qw_acquisition_link_info
     </sql>
 
     <select id="selectQwAcquisitionLinkInfoList" parameterType="com.fs.qw.domain.QwAcquisitionLinkInfo" resultMap="QwAcquisitionLinkInfoResult">
@@ -35,6 +36,9 @@
             <if test="remark != null and remark != ''">
                 and remark like concat('%', #{remark}, '%')
             </if>
+            <if test="randomStr != null and randomStr != ''">
+                and random_str like concat('%', #{randomStr}, '%')
+            </if>
         </where>
     </select>
 
@@ -43,6 +47,29 @@
         where id = #{id}
     </select>
 
+    <select id="selectAllRandomStr" resultType="java.lang.String">
+        select random_str from qw_acquisition_link_info
+    </select>
+    <select id="selectQwAcquisitionUrlByRandomStr" resultType="java.lang.String">
+        select link from qw_acquisition_link_info where random_str = #{randomStr}
+    </select>
+
+    <select id="selectAcquisitionLinkInfoListByIds" resultMap="QwAcquisitionLinkInfoResult">
+        select id, qw_acquisition_assistant_id, link, phone, create_by, create_time, update_by, update_time, remark,random_str from qw_acquisition_link_info where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <select id="selectAcquisitionLinkInfoListByAcquisitionAssistantIds" resultMap="QwAcquisitionLinkInfoResult">
+        select id, qw_acquisition_assistant_id, link, phone, create_by, create_time, update_by, update_time, remark,random_str
+        from qw_acquisition_link_info
+        where qw_acquisition_assistant_id in
+        <foreach item="id" collection="qwAcquisitionAssistantIds" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
     <insert id="insertQwAcquisitionLinkInfo" parameterType="com.fs.qw.domain.QwAcquisitionLinkInfo" useGeneratedKeys="true" keyProperty="id">
         insert into qw_acquisition_link_info
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -70,6 +97,9 @@
             <if test="remark != null and remark != ''">
                 remark,
             </if>
+            <if test="randomStr != null and randomStr != ''">
+                random_str,
+            </if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="qwAcquisitionAssistantId != null">
@@ -96,6 +126,9 @@
             <if test="remark != null and remark != ''">
                 #{remark},
             </if>
+            <if test="randomStr != null and randomStr != ''">
+                #{randomStr},
+            </if>
         </trim>
     </insert>
 
@@ -120,6 +153,9 @@
             <if test="remark != null and remark != ''">
                 remark = #{remark},
             </if>
+            <if test="randomStr != null and randomStr != ''">
+                random_str = #{randomStr},
+            </if>
         </trim>
         where id = #{id}
     </update>

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

@@ -1,7 +1,7 @@
 package com.fs.app.controller;
 
 import com.fs.common.core.domain.AjaxResult;
-import com.fs.qw.service.IQwAcquisitionAssistantService;
+import com.fs.qw.service.IQwAcquisitionLinkInfoService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
 public class CustomerLinkWeChatController extends AppBaseController {
 
     @Autowired
-    private IQwAcquisitionAssistantService qwAcquisitionAssistantService;
+    private IQwAcquisitionLinkInfoService acquisitionLinkInfoService;
 
     @GetMapping("/goToLink/{pageParam}")
     @CrossOrigin
@@ -26,8 +26,8 @@ public class CustomerLinkWeChatController extends AppBaseController {
             return AjaxResult.error("参数错误:页面参数不能为空");
         }
 
-        // 获取URL
-        String url = qwAcquisitionAssistantService.selectQwAcquisitionUrlByPageParam(pageParam);
+        // 获取完整链接URL
+        String url = acquisitionLinkInfoService.selectQwAcquisitionUrlByRandomStr(pageParam);
 
         // 判断URL是否有效
         if (StringUtils.isBlank(url)) {