|
|
@@ -0,0 +1,823 @@
|
|
|
+package com.fs.qw.service.impl;
|
|
|
+
|
|
|
+
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+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.his.dto.SendResultDetailDTO;
|
|
|
+import com.fs.qw.bo.SendMsgLogBo;
|
|
|
+import com.fs.qw.domain.QwAcquisitionAssistant;
|
|
|
+import com.fs.qw.domain.QwUser;
|
|
|
+import com.fs.qw.dto.BatchAddAcquisitionLinkDTO;
|
|
|
+import com.fs.qw.dto.IpadBlindAddDto;
|
|
|
+import com.fs.qw.enums.SmsLogType;
|
|
|
+import com.fs.qw.mapper.QwAcquisitionAssistantMapper;
|
|
|
+import com.fs.qw.mapper.QwUserMapper;
|
|
|
+import com.fs.qw.param.QwUserListParam;
|
|
|
+import com.fs.qw.utils.UniqueStringUtil;
|
|
|
+import com.fs.qw.vo.QwUserVO;
|
|
|
+import com.fs.wxwork.dto.WxAddSearchDTO;
|
|
|
+import com.fs.wxwork.dto.WxSearchContactDTO;
|
|
|
+import com.fs.wxwork.dto.WxSearchContactResp;
|
|
|
+import com.fs.wxwork.dto.WxWorkResponseDTO;
|
|
|
+import com.fs.wxwork.service.WxWorkService;
|
|
|
+import lombok.Data;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.collections4.CollectionUtils;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.context.ApplicationContext;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import com.fs.qw.mapper.QwAcquisitionLinkInfoMapper;
|
|
|
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
|
|
|
+import com.fs.qw.service.IQwAcquisitionLinkInfoService;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+
|
|
|
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获客链接-号码链接生成记录Service业务层处理
|
|
|
+ *
|
|
|
+ * @author fs
|
|
|
+ * @date 2026-03-27
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoService
|
|
|
+{
|
|
|
+ @Autowired
|
|
|
+ private ApplicationContext applicationContext;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ISmsService smsService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RedisCache redisCache;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ICompanySmsTempService smsTempService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ICompanySmsService companySmsService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private QwAcquisitionAssistantMapper acquisitionAssistantMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private QwAcquisitionLinkInfoMapper qwAcquisitionLinkInfoMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private QwUserMapper qwUserMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private WxWorkService wxWorkService;
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ // 如果自动注入失败,手动获取
|
|
|
+ if (wxWorkService == null) {
|
|
|
+ wxWorkService = applicationContext.getBean(WxWorkService.class);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //拼接电话号码的链接后缀(这个后面拼接加密后的手机字符串)
|
|
|
+ private static final String LINK_SUFFIX = "?customer_channel=up:";
|
|
|
+ private static final String LINK_SUFFIX_NOL = "?customer_channel=link:";
|
|
|
+
|
|
|
+ // 企微加好友链接-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/";
|
|
|
+ /**
|
|
|
+ * 查询获客链接-号码链接生成记录
|
|
|
+ *
|
|
|
+ * @param id 获客链接-号码链接生成记录主键
|
|
|
+ * @return 获客链接-号码链接生成记录
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public QwAcquisitionLinkInfo selectQwAcquisitionLinkInfoById(Long id)
|
|
|
+ {
|
|
|
+ return qwAcquisitionLinkInfoMapper.selectQwAcquisitionLinkInfoById(id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询获客链接-号码链接生成记录列表
|
|
|
+ *
|
|
|
+ * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
|
|
|
+ * @return 获客链接-号码链接生成记录
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<QwAcquisitionLinkInfo> selectQwAcquisitionLinkInfoList(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
|
|
|
+ {
|
|
|
+ List<QwAcquisitionLinkInfo> resultList = qwAcquisitionLinkInfoMapper.selectQwAcquisitionLinkInfoList(qwAcquisitionLinkInfo);
|
|
|
+ if (CollectionUtils.isEmpty(resultList)){
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ for (QwAcquisitionLinkInfo item : resultList) {
|
|
|
+ if (item.getPhone() != null) {
|
|
|
+ item.setPhone(item.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return resultList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 新增获客链接-号码链接生成记录
|
|
|
+ *
|
|
|
+ * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
|
|
|
+ * @return 结果
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public int insertQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
|
|
|
+ {
|
|
|
+ return qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改获客链接-号码链接生成记录
|
|
|
+ *
|
|
|
+ * @param qwAcquisitionLinkInfo 获客链接-号码链接生成记录
|
|
|
+ * @return 结果
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public int updateQwAcquisitionLinkInfo(QwAcquisitionLinkInfo qwAcquisitionLinkInfo)
|
|
|
+ {
|
|
|
+ //TODO 如果放开编辑就需要更新缓存
|
|
|
+ return qwAcquisitionLinkInfoMapper.updateQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量删除获客链接-号码链接生成记录
|
|
|
+ *
|
|
|
+ * @param ids 需要删除的获客链接-号码链接生成记录主键集合
|
|
|
+ * @return 结果
|
|
|
+ */
|
|
|
+ @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);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除获客链接-号码链接生成记录信息
|
|
|
+ *
|
|
|
+ * @param qwAcquisitionAssistantIds 需要删除的获客链接管理主键ID集合
|
|
|
+ * @return 结果
|
|
|
+ */
|
|
|
+ @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);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除获客链接-号码链接生成记录信息
|
|
|
+ *
|
|
|
+ * @param id 获客链接-号码链接生成记录主键
|
|
|
+ * @return 结果
|
|
|
+ */
|
|
|
+ @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
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public SendResultDetailDTO sendMessageLink(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 = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
|
|
|
+ if (acquisitionAssistant == null) {
|
|
|
+ log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
|
|
|
+ throw new CustomException("获客链接-未找到获客链接信息");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 生成随机参数并构建短信内容
|
|
|
+ String randomStr = generateUniqueRandomStr();
|
|
|
+ String replaceText = LINK_DOMAIN + randomStr;
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ CompanySms latestSms = companySmsService.selectCompanySms(2L);
|
|
|
+ if (latestSms == null) {
|
|
|
+ throw new CustomException("公司短信配置不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ 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());
|
|
|
+
|
|
|
+ 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, sendMsgLogBo.getCompanyUserId());
|
|
|
+ log.info("短信发送成功, phone={}, needCount={}", phone, needCount);
|
|
|
+ return new SendResultDetailDTO(true, null, null);
|
|
|
+ } else {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String selectQwAcquisitionUrlByRandomStr(String randomStr) {
|
|
|
+ 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 {
|
|
|
+ // 正常值仍缓存2天
|
|
|
+ Integer cacheExpire = 2;
|
|
|
+ redisCache.setCacheObject(key, fullLink, cacheExpire, TimeUnit.DAYS);
|
|
|
+ log.info("完整获客链接URL缓存成功, randomStr:{}", randomStr);
|
|
|
+ }
|
|
|
+
|
|
|
+ return fullLink;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ //TODO 多数据批量操作时,当前事务还未提交,在短时间内可能会导致第三方回调的时候查不到需要更新的数据,后续优化
|
|
|
+ //@Transactional(rollbackFor = Exception.class)
|
|
|
+ public int batchCreateMessageLink(BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO) {
|
|
|
+ Long qwAcquisitionId = batchAddAcquisitionLinkDTO.getQwAcquisitionAssistantId();
|
|
|
+ List<String> phoneList = batchAddAcquisitionLinkDTO.getPhoneList();
|
|
|
+
|
|
|
+ if (CollectionUtils.isEmpty(phoneList)) {
|
|
|
+ log.warn("批量发送短信,手机号列表为空");
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 获取短信模板
|
|
|
+ CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(SMS_LINK_TEMPLATE_CODE);
|
|
|
+ if (temp == null) {
|
|
|
+ log.info("获客链接-未找到短信模板:{}", SMS_LINK_TEMPLATE_CODE);
|
|
|
+ throw new CustomException("获客链接-未找到短信模板");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取获客链接信息
|
|
|
+ QwAcquisitionAssistant acquisitionAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
|
|
|
+ if (acquisitionAssistant == null) {
|
|
|
+ log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
|
|
|
+ throw new CustomException("获客链接-未找到获客链接信息");
|
|
|
+ }
|
|
|
+
|
|
|
+ String originalContent = temp.getContent();
|
|
|
+ SendMsgLogBo baseSendMsgLogBo = new SendMsgLogBo();
|
|
|
+ baseSendMsgLogBo.setCompanyUserId(batchAddAcquisitionLinkDTO.getCreateBy());
|
|
|
+ baseSendMsgLogBo.setQwAcquisitionId(acquisitionAssistant.getId());
|
|
|
+
|
|
|
+ // 3. 为每个手机号预生成链接和短信内容
|
|
|
+ List<PhoneMessageInfo> phoneMessageList = new ArrayList<>();
|
|
|
+ for (String phone : phoneList) {
|
|
|
+ String randomStr = generateUniqueRandomStr();
|
|
|
+ String replaceText = LINK_DOMAIN + randomStr;
|
|
|
+ String content = originalContent.replace("${sms.friendLink}", replaceText);
|
|
|
+ Integer needCount = calculateSmsCount(content);
|
|
|
+
|
|
|
+ PhoneMessageInfo info = new PhoneMessageInfo();
|
|
|
+ info.setPhone(phone);
|
|
|
+ info.setContent(content);
|
|
|
+ info.setRandomStr(randomStr);
|
|
|
+ info.setNeedCount(needCount);
|
|
|
+ phoneMessageList.add(info);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 计算总需要条数
|
|
|
+ int totalNeedCount = phoneMessageList.stream().mapToInt(PhoneMessageInfo::getNeedCount).sum();
|
|
|
+
|
|
|
+ // 5. 快速校验总余额是否充足(使用缓存)
|
|
|
+ Long balance = companySmsService.getBalance(2L);
|
|
|
+ if (balance == null || balance < totalNeedCount) {
|
|
|
+ // 缓存可能不准,再查一次DB确认
|
|
|
+ CompanySms companySms = companySmsService.selectCompanySms(2L);
|
|
|
+ if (companySms == null) {
|
|
|
+ throw new CustomException("公司短信配置不存在");
|
|
|
+ }
|
|
|
+ if (companySms.getRemainSmsCount() < totalNeedCount) {
|
|
|
+ throw new CustomException("短信余额不足,总需要:" + totalNeedCount +
|
|
|
+ ",当前余额:" + companySms.getRemainSmsCount());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 批量发送短信
|
|
|
+ List<SendResultInfo> sendResults = new ArrayList<>();
|
|
|
+ int successCount = 0;
|
|
|
+
|
|
|
+ for (PhoneMessageInfo info : phoneMessageList) {
|
|
|
+ try {
|
|
|
+ SendMsgLogBo sendMsgLogBo = new SendMsgLogBo();
|
|
|
+ BeanUtils.copyProperties(baseSendMsgLogBo, sendMsgLogBo);
|
|
|
+
|
|
|
+ R r = smsService.simpleSmsSend(info.getPhone(), info.getContent(), temp,
|
|
|
+ SmsLogType.ACQUISITION_LINK, sendMsgLogBo);
|
|
|
+
|
|
|
+ if (r != null && "200".equals(String.valueOf(r.get("code")))) {
|
|
|
+ addAcquisitionLinkInfo(acquisitionAssistant.getId(), info.getPhone(),
|
|
|
+ acquisitionAssistant.getUrl(), info.getRandomStr(),
|
|
|
+ baseSendMsgLogBo.getCompanyUserId());
|
|
|
+
|
|
|
+ SendResultInfo successResult = new SendResultInfo();
|
|
|
+ successResult.setSuccess(true);
|
|
|
+ successResult.setNeedCount(info.getNeedCount());
|
|
|
+ successResult.setPhone(info.getPhone());
|
|
|
+ sendResults.add(successResult);
|
|
|
+ successCount++;
|
|
|
+
|
|
|
+ log.info("短信发送成功 phone={}, needCount={}", info.getPhone(), info.getNeedCount());
|
|
|
+ } else {
|
|
|
+ String msg = r != null && r.get("msg") != null ? r.get("msg").toString() : "未知错误";
|
|
|
+ log.warn("短信发送失败 phone={}, msg={}", info.getPhone(), msg);
|
|
|
+
|
|
|
+ SendResultInfo failResult = new SendResultInfo();
|
|
|
+ failResult.setSuccess(false);
|
|
|
+ failResult.setNeedCount(0);
|
|
|
+ failResult.setPhone(info.getPhone());
|
|
|
+ failResult.setFailReason(msg);
|
|
|
+ sendResults.add(failResult);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("短信发送异常 phone=" + info.getPhone(), e);
|
|
|
+
|
|
|
+ SendResultInfo failResult = new SendResultInfo();
|
|
|
+ failResult.setSuccess(false);
|
|
|
+ failResult.setNeedCount(0);
|
|
|
+ failResult.setPhone(info.getPhone());
|
|
|
+ failResult.setFailReason(e.getMessage());
|
|
|
+ sendResults.add(failResult);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 计算实际需要扣减的总条数
|
|
|
+ int actualNeedCount = sendResults.stream()
|
|
|
+ .filter(SendResultInfo::isSuccess)
|
|
|
+ .mapToInt(SendResultInfo::getNeedCount)
|
|
|
+ .sum();
|
|
|
+
|
|
|
+ if (actualNeedCount == 0) {
|
|
|
+ log.info("批量发送全部失败,不扣减余额");
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8. 乐观锁扣减,支持重试
|
|
|
+ int maxRetries = 3;
|
|
|
+
|
|
|
+ for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
|
|
|
+ // 快速检查(用缓存)
|
|
|
+ Long reBalance = companySmsService.getBalance(2L);
|
|
|
+ if (reBalance == null || reBalance < actualNeedCount) {
|
|
|
+ // 缓存可能不准,再查一次DB确认
|
|
|
+ CompanySms latestSms = companySmsService.selectCompanySms(2L);
|
|
|
+ if (latestSms == null) {
|
|
|
+ throw new CustomException("公司短信配置不存在");
|
|
|
+ }
|
|
|
+ if (latestSms.getRemainSmsCount() < actualNeedCount) {
|
|
|
+ throw new CustomException("短信余额不足");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取最新数据(含version)
|
|
|
+ CompanySms latestSms = companySmsService.selectCompanySms(2L);
|
|
|
+ if (latestSms == null) {
|
|
|
+ throw new CustomException("公司短信配置不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ Long version = latestSms.getVersion() != null ? latestSms.getVersion() : 0L;
|
|
|
+ int updateCount = companySmsService.decrementRemainSmsCountWithVersion(2L, actualNeedCount, version);
|
|
|
+
|
|
|
+ if (updateCount > 0) {
|
|
|
+ log.info("乐观锁扣减余额成功, companyId=2, needCount={}, version={}", actualNeedCount, version);
|
|
|
+ // 扣减成功,更新缓存
|
|
|
+ companySmsService.updateCacheBalance(2L, -actualNeedCount);
|
|
|
+ log.info("批量创建获客链接完成,总计尝试 {}, 成功 {}, 实际扣减条数 {}",
|
|
|
+ phoneList.size(), successCount, actualNeedCount);
|
|
|
+ return successCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("乐观锁扣减余额失败,第{}次重试, version={}", retryCount + 1, version);
|
|
|
+
|
|
|
+ if (retryCount == maxRetries - 1) {
|
|
|
+ throw new CustomException("扣减余额失败,系统繁忙,请稍后重试");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ Thread.sleep(100L * (retryCount + 1));
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ throw new CustomException("扣减余额被中断");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return successCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 退还余额(带重试)
|
|
|
+ */
|
|
|
+ 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
|
|
|
+ public String extractLink(Long qwAcquisitionAssistantId, String originalPhone, String originalLink,Long createBy) {
|
|
|
+ String randomStr = generateUniqueRandomStr();
|
|
|
+ QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
|
|
|
+ qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
|
|
|
+ qwAcquisitionLinkInfo.setCreateBy(createBy);
|
|
|
+ qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
|
|
|
+ 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 = 2; // 默认缓存2天
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R extractLinkNol(Long qwAcquisitionAssistantId, String originalLink, Long createBy) {
|
|
|
+ String randomStr = generateUniqueRandomStr();
|
|
|
+ QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
|
|
|
+ qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
|
|
|
+ qwAcquisitionLinkInfo.setCreateBy(createBy);
|
|
|
+ qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
|
|
|
+ String linkPlus=originalLink+LINK_SUFFIX_NOL+ qwAcquisitionAssistantId;
|
|
|
+ qwAcquisitionLinkInfo.setLink(linkPlus);
|
|
|
+ qwAcquisitionLinkInfo.setRandomStr(randomStr);
|
|
|
+ int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
|
|
|
+ // ========== 缓存URL,便于后续通过randomStr访问 ==========
|
|
|
+ try {
|
|
|
+ String cacheKey = QW_FRIEND_LINK_URL_KEY + randomStr;
|
|
|
+ Integer cacheExpire = 2; // 默认缓存2天
|
|
|
+ redisCache.setCacheObject(cacheKey, linkPlus, cacheExpire, TimeUnit.DAYS);
|
|
|
+ log.info("获客链接URL缓存成功, pageParam: {}, url: {}", randomStr, linkPlus);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 缓存失败不影响主流程,但需要记录日志
|
|
|
+ log.error("获客链接URL缓存失败, pageParam: {}", randomStr, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ String shortLink = LINK_DOMAIN+randomStr;
|
|
|
+ return R.ok().put("shortLink",shortLink).put("linkPlus",linkPlus);
|
|
|
+ // 返回域名+随机字符串
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void ipadBlindAdd(IpadBlindAddDto dto,SendMsgLogBo sendMsgLogBo) {
|
|
|
+ //获取QwUser信息
|
|
|
+ QwUser qwUser=qwUserMapper.selectQwUserById(dto.getUserId());
|
|
|
+ if (qwUser==null){
|
|
|
+ log.error( "销售用户不存在,qwUserId:{}",dto.getUserId());
|
|
|
+ throw new CustomException("销售用户不存在");
|
|
|
+ }
|
|
|
+ //判断这个销售的ipad是否在线
|
|
|
+ QwUserListParam queryCondition=new QwUserListParam();
|
|
|
+ queryCondition.setCorpId(dto.getCorpId());
|
|
|
+ queryCondition.setQwUserId(qwUser.getQwUserId());
|
|
|
+ List<QwUserVO> qwUserVOS = qwUserMapper.selectQwUserListStaffVO(queryCondition);
|
|
|
+ if (qwUserVOS.isEmpty()){
|
|
|
+ throw new CustomException("此主体不存在该销售用户");
|
|
|
+ }
|
|
|
+ if (qwUserVOS.get(0).getIpadStatus()==0){
|
|
|
+ throw new CustomException("此销售用户ipad已离线,请登录");
|
|
|
+ }
|
|
|
+ // 每次使用时获取代理对象
|
|
|
+ QwAcquisitionLinkInfoServiceImpl self = applicationContext.getBean(QwAcquisitionLinkInfoServiceImpl.class);
|
|
|
+ try {
|
|
|
+ //调用ipad加好友接口
|
|
|
+ WxWorkResponseDTO<String> response = self.qwAddWxInvokeIpad(dto.getPhone(), qwUser.getUid(),
|
|
|
+ qwUser.getServerId(),
|
|
|
+ qwUser.getVid(),
|
|
|
+ qwUser.getQwUserName());
|
|
|
+ if (response != null && response.getErrcode() == 0) {
|
|
|
+ log.info("ipad获客链接加微成功");
|
|
|
+ }else {
|
|
|
+ log.error("ipad获客链接加微失败,错误码:{},错误信息:{}",response.getErrcode(),response.getErrmsg());
|
|
|
+ }
|
|
|
+ }catch (Exception e){
|
|
|
+ //TODO 如果有异常,暂时只做记录
|
|
|
+ log.error("ipad加微异常",e);
|
|
|
+ }
|
|
|
+
|
|
|
+ //调用发送短信接口
|
|
|
+ self.sendMessageLink(dto.getPhone(),dto.getQwAcquisitionAssistantId(),sendMsgLogBo);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 企微加个微调用ipad端
|
|
|
+ * @param mobile 手机号
|
|
|
+ * @param qwUid 企微uid
|
|
|
+ * @param serverId 服务器id
|
|
|
+ * @return String 结果
|
|
|
+ */
|
|
|
+ public WxWorkResponseDTO<String> qwAddWxInvokeIpad(String mobile, String qwUid, Long serverId, String vid, String qwUserName) {
|
|
|
+ if (StringUtils.isBlank(mobile) || StringUtils.isBlank(qwUid) || serverId == null) {
|
|
|
+ log.warn("企微申请加好友任务参数校验失败: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ WxAddSearchDTO wxAddSearchDTO = new WxAddSearchDTO();
|
|
|
+ wxAddSearchDTO.setUuid(qwUid);
|
|
|
+ wxAddSearchDTO.setVid(Long.valueOf(vid));
|
|
|
+ wxAddSearchDTO.setPhone(mobile);
|
|
|
+
|
|
|
+ WxSearchContactDTO contactDTO=new WxSearchContactDTO();
|
|
|
+ contactDTO.setUuid(qwUid);
|
|
|
+ contactDTO.setPhoneNumber(mobile);
|
|
|
+
|
|
|
+
|
|
|
+ WxWorkResponseDTO<WxSearchContactResp> respWxWorkResponseDTO = wxWorkService.searchContact(contactDTO, serverId);
|
|
|
+ WxSearchContactResp.UserList user = respWxWorkResponseDTO.getData().getUserList().stream()
|
|
|
+ .filter(u -> u.getState().equals("2"))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null); // 或者 .orElseThrow(() -> new RuntimeException("未找到指定用户"))
|
|
|
+
|
|
|
+ wxAddSearchDTO.setOptionid(user.getOpenid());
|
|
|
+ wxAddSearchDTO.setTicket(user.getTicket());
|
|
|
+ wxAddSearchDTO.setContent("你好,我是你的专属助手:"+qwUserName+",有什么问题都可以问我哦~");
|
|
|
+
|
|
|
+ WxWorkResponseDTO<String> response = wxWorkService.addSearch(wxAddSearchDTO, serverId);
|
|
|
+ log.debug("企微加微接口调用结果: errcode={}, errmsg={}",
|
|
|
+ response != null ? response.getErrcode() : "null",
|
|
|
+ response != null ? response.getErrmsg() : "null");
|
|
|
+
|
|
|
+ return response;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("企微申请加好友任务请求接口异常: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加链接生成记录
|
|
|
+ * */
|
|
|
+ public int addAcquisitionLinkInfo(Long qwAcquisitionAssistantId,String originalPhone,String originalLink,String randomStr,Long createBy){
|
|
|
+ QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
|
|
|
+ qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
|
|
|
+ qwAcquisitionLinkInfo.setCreateBy(createBy);
|
|
|
+ qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
|
|
|
+ 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 = 2; // 默认缓存2天
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //根据短信文字内容计算短信条数
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 辅助类
|
|
|
+ @Data
|
|
|
+ private static class PhoneMessageInfo {
|
|
|
+ private String phone;
|
|
|
+ private String content;
|
|
|
+ private String randomStr;
|
|
|
+ private Integer needCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Data
|
|
|
+ private static class SendResultInfo {
|
|
|
+ private boolean success;
|
|
|
+ private int needCount;
|
|
|
+ private String phone;
|
|
|
+ private String failReason;
|
|
|
+ }
|
|
|
+}
|