|
|
@@ -1,9 +1,6 @@
|
|
|
package com.fs.qw.service.impl;
|
|
|
|
|
|
-import java.util.Collections;
|
|
|
-import java.util.HashSet;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Set;
|
|
|
+import java.util.*;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
import com.fs.common.core.domain.R;
|
|
|
@@ -12,7 +9,9 @@ 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;
|
|
|
@@ -21,13 +20,16 @@ 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.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.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 static com.fs.his.utils.PhoneUtil.encryptPhone;
|
|
|
|
|
|
@@ -50,6 +52,9 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
|
|
|
@Autowired
|
|
|
private ICompanySmsTempService smsTempService;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private ICompanySmsService companySmsService;
|
|
|
+
|
|
|
@Autowired
|
|
|
private QwAcquisitionAssistantMapper acquisitionAssistantMapper;
|
|
|
|
|
|
@@ -192,39 +197,98 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
|
|
|
}
|
|
|
|
|
|
@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){
|
|
|
+ if (acquisitionAssistant == null) {
|
|
|
log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
|
|
|
throw new CustomException("获客链接-未找到获客链接信息");
|
|
|
}
|
|
|
+
|
|
|
+ // 3. 生成随机参数并构建短信内容
|
|
|
String randomStr = generateUniqueRandomStr();
|
|
|
- String replaceText=LINK_DOMAIN+randomStr;
|
|
|
+ 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());
|
|
|
-
|
|
|
+ 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("短信发送失败 获客链接id={}, phone={}, msg={}", qwAcquisitionId, phone, msg);
|
|
|
+ log.warn("短信发送失败,退还余额, phone={}, needCount={}", phone, needCount);
|
|
|
+ rollbackBalance(needCount, maxRetries);
|
|
|
return new SendResultDetailDTO(false, msg, null);
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
- log.error("发送异常 获客链接id=" + qwAcquisitionId, e);
|
|
|
+ log.error("发送异常,退还余额, phone=" + phone, e);
|
|
|
+ rollbackBalance(needCount, maxRetries);
|
|
|
return new SendResultDetailDTO(false, e.getMessage(), null);
|
|
|
}
|
|
|
}
|
|
|
@@ -269,52 +333,203 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
public int batchCreateMessageLink(BatchAddAcquisitionLinkDTO batchAddAcquisitionLinkDTO) {
|
|
|
Long qwAcquisitionId = batchAddAcquisitionLinkDTO.getQwAcquisitionAssistantId();
|
|
|
List<String> phoneList = batchAddAcquisitionLinkDTO.getPhoneList();
|
|
|
- int result = 0;
|
|
|
|
|
|
+ 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("获客链接-未找到短信模板");
|
|
|
}
|
|
|
- String originalContent = temp.getContent();
|
|
|
- //获取获客链接管理信息
|
|
|
+
|
|
|
+ // 2. 获取获客链接信息
|
|
|
QwAcquisitionAssistant acquisitionAssistant = acquisitionAssistantMapper.selectQwAcquisitionAssistantById(qwAcquisitionId);
|
|
|
if (acquisitionAssistant == null) {
|
|
|
log.info("获客链接-未找到获客链接id:{}", qwAcquisitionId);
|
|
|
throw new CustomException("获客链接-未找到获客链接信息");
|
|
|
}
|
|
|
- SendMsgLogBo sendMsgLogBo = new SendMsgLogBo();
|
|
|
- sendMsgLogBo.setCompanyUserId(batchAddAcquisitionLinkDTO.getCreateBy());
|
|
|
- sendMsgLogBo.setQwAcquisitionId(acquisitionAssistant.getId());
|
|
|
+
|
|
|
+ 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) {
|
|
|
- try {
|
|
|
- //新增号码-链接生成记录
|
|
|
- String randomStr = generateUniqueRandomStr();
|
|
|
- String replaceText = LINK_DOMAIN + randomStr;
|
|
|
- String content = originalContent.replace("${sms.friendLink}", replaceText);
|
|
|
+ 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();
|
|
|
|
|
|
- R r = smsService.simpleSmsSend(phone, content, temp, SmsLogType.ACQUISITION_LINK, sendMsgLogBo);
|
|
|
+ // 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());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (r != null && "200".equals(String.valueOf(r.get("code")))) {
|
|
|
+ // 6. 批量发送短信
|
|
|
+ List<SendResultInfo> sendResults = new ArrayList<>();
|
|
|
+ int successCount = 0;
|
|
|
|
|
|
- //新增号码-链接生成记录
|
|
|
- int addResult =addAcquisitionLinkInfo(acquisitionAssistant.getId(), phone, acquisitionAssistant.getUrl(), randomStr, sendMsgLogBo.getCompanyUserId());
|
|
|
- result += addResult;
|
|
|
+ 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("短信发送失败 获客链接id={}, phone={}, msg={}", qwAcquisitionId, phone, msg);
|
|
|
+ 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, 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);
|
|
|
}
|
|
|
}
|
|
|
- log.info("批量创建获客链接完成,总计尝试 {}, 成功 {}", phoneList.size(), result);
|
|
|
- return result;
|
|
|
+
|
|
|
+ // 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
|
|
|
@@ -428,4 +643,34 @@ public class QwAcquisitionLinkInfoServiceImpl implements IQwAcquisitionLinkInfoS
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ //根据短信文字内容计算短信条数
|
|
|
+ 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;
|
|
|
+ }
|
|
|
}
|