package com.iailab.module.system.service.sms; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import com.iailab.framework.common.core.KeyValue; import com.iailab.framework.common.enums.CommonStatusEnum; import com.iailab.framework.common.enums.UserTypeEnum; import com.iailab.framework.datapermission.core.annotation.DataPermission; import com.iailab.module.system.framework.sms.core.client.SmsClient; import com.iailab.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; import com.iailab.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import com.iailab.module.system.dal.dataobject.sms.SmsChannelDO; import com.iailab.module.system.dal.dataobject.sms.SmsTemplateDO; import com.iailab.module.system.dal.dataobject.user.AdminUserDO; import com.iailab.module.system.mq.message.sms.SmsSendMessage; import com.iailab.module.system.mq.producer.sms.SmsProducer; import com.iailab.module.system.service.member.MemberService; import com.iailab.module.system.service.user.AdminUserService; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.iailab.module.system.enums.ErrorCodeConstants.*; /** * 短信发送 Service 发送的实现 * * @author iailab */ @Service @Slf4j public class SmsSendServiceImpl implements SmsSendService { @Resource private AdminUserService adminUserService; @Resource private MemberService memberService; @Resource private SmsChannelService smsChannelService; @Resource private SmsTemplateService smsTemplateService; @Resource private SmsLogService smsLogService; @Resource private SmsProducer smsProducer; @Override @DataPermission(enable = false) // 发送短信时,无需考虑数据权限 public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { // 如果 mobile 为空,则加载用户编号对应的手机号 if (StrUtil.isEmpty(mobile)) { AdminUserDO user = adminUserService.getUser(userId); if (user != null) { mobile = user.getMobile(); } } // 执行发送 return sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); } @Override public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { // 如果 mobile 为空,则加载用户编号对应的手机号 if (StrUtil.isEmpty(mobile)) { mobile = memberService.getMemberUserMobile(userId); } // 执行发送 return sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); } @Override public Long sendSingleSms(String mobile, Long userId, Integer userType, String templateCode, Map templateParams) { // 校验短信模板是否合法 SmsTemplateDO template = validateSmsTemplate(templateCode); // 校验短信渠道是否合法 SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId()); // 校验手机号码是否存在 mobile = validateMobile(mobile); // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志 List> newTemplateParams = buildTemplateParams(template, templateParams); // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); // 发送 MQ 消息,异步执行发送短信 if (isSend) { smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), template.getApiTemplateId(), newTemplateParams); } return sendLogId; } @VisibleForTesting SmsChannelDO validateSmsChannel(Long channelId) { // 获得短信模板。考虑到效率,从缓存中获取 SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); // 短信模板不存在 if (channelDO == null) { throw exception(SMS_CHANNEL_NOT_EXISTS); } return channelDO; } @VisibleForTesting SmsTemplateDO validateSmsTemplate(String templateCode) { // 获得短信模板。考虑到效率,从缓存中获取 SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode); // 短信模板不存在 if (template == null) { throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS); } return template; } /** * 将参数模板,处理成有序的 KeyValue 数组 *

* 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说 腾讯云 * * @param template 短信模板 * @param templateParams 原始参数 * @return 处理后的参数 */ @VisibleForTesting List> buildTemplateParams(SmsTemplateDO template, Map templateParams) { return template.getParams().stream().map(key -> { Object value = templateParams.get(key); if (value == null) { throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key); } return new KeyValue<>(key, value); }).collect(Collectors.toList()); } @VisibleForTesting public String validateMobile(String mobile) { if (StrUtil.isEmpty(mobile)) { throw exception(SMS_SEND_MOBILE_NOT_EXISTS); } return mobile; } @Override public void doSendSms(SmsSendMessage message) { // 获得渠道对应的 SmsClient 客户端 SmsClient smsClient = smsChannelService.getSmsClient(message.getChannelId()); Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); // 发送短信 try { SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(), message.getApiTemplateId(), message.getTemplateParams()); smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(), sendResponse.getApiCode(), sendResponse.getApiMsg(), sendResponse.getApiRequestId(), sendResponse.getSerialNo()); } catch (Throwable ex) { log.error("[doSendSms][发送短信异常,日志编号({})]", message.getLogId(), ex); smsLogService.updateSmsSendResult(message.getLogId(), false, "EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null); } } @Override public void receiveSmsStatus(String channelCode, String text) throws Throwable { // 获得渠道对应的 SmsClient 客户端 SmsClient smsClient = smsChannelService.getSmsClient(channelCode); Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode); // 解析内容 List receiveResults = smsClient.parseSmsReceiveStatus(text); if (CollUtil.isEmpty(receiveResults)) { return; } // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); } }