houzhongjian
2024-09-14 818a0170d8f2950d52cc7300a302356bbc523236
提交 | 用户 | 时间
e7c126 1 package com.iailab.module.system.service.sms;
H 2
3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.exceptions.ExceptionUtil;
5 import cn.hutool.core.lang.Assert;
6 import cn.hutool.core.util.StrUtil;
7 import com.iailab.framework.common.core.KeyValue;
8 import com.iailab.framework.common.enums.CommonStatusEnum;
9 import com.iailab.framework.common.enums.UserTypeEnum;
10 import com.iailab.framework.datapermission.core.annotation.DataPermission;
11 import com.iailab.module.system.framework.sms.core.client.SmsClient;
12 import com.iailab.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
13 import com.iailab.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
14 import com.iailab.module.system.dal.dataobject.sms.SmsChannelDO;
15 import com.iailab.module.system.dal.dataobject.sms.SmsTemplateDO;
16 import com.iailab.module.system.dal.dataobject.user.AdminUserDO;
17 import com.iailab.module.system.mq.message.sms.SmsSendMessage;
18 import com.iailab.module.system.mq.producer.sms.SmsProducer;
19 import com.iailab.module.system.service.member.MemberService;
20 import com.iailab.module.system.service.user.AdminUserService;
21 import com.google.common.annotations.VisibleForTesting;
22 import lombok.extern.slf4j.Slf4j;
23 import org.springframework.stereotype.Service;
24
25 import javax.annotation.Resource;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.stream.Collectors;
29
30 import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
31 import static com.iailab.module.system.enums.ErrorCodeConstants.*;
32
33 /**
34  * 短信发送 Service 发送的实现
35  *
36  * @author iailab
37  */
38 @Service
39 @Slf4j
40 public class SmsSendServiceImpl implements SmsSendService {
41
42     @Resource
43     private AdminUserService adminUserService;
44     @Resource
45     private MemberService memberService;
46     @Resource
47     private SmsChannelService smsChannelService;
48     @Resource
49     private SmsTemplateService smsTemplateService;
50     @Resource
51     private SmsLogService smsLogService;
52
53     @Resource
54     private SmsProducer smsProducer;
55
56     @Override
57     @DataPermission(enable = false) // 发送短信时,无需考虑数据权限
58     public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
59         // 如果 mobile 为空,则加载用户编号对应的手机号
60         if (StrUtil.isEmpty(mobile)) {
61             AdminUserDO user = adminUserService.getUser(userId);
62             if (user != null) {
63                 mobile = user.getMobile();
64             }
65         }
66         // 执行发送
67         return sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams);
68     }
69
70     @Override
71     public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map<String, Object> templateParams) {
72         // 如果 mobile 为空,则加载用户编号对应的手机号
73         if (StrUtil.isEmpty(mobile)) {
74             mobile = memberService.getMemberUserMobile(userId);
75         }
76         // 执行发送
77         return sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams);
78     }
79
80     @Override
81     public Long sendSingleSms(String mobile, Long userId, Integer userType,
82                               String templateCode, Map<String, Object> templateParams) {
83         // 校验短信模板是否合法
84         SmsTemplateDO template = validateSmsTemplate(templateCode);
85         // 校验短信渠道是否合法
86         SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
87
88         // 校验手机号码是否存在
89         mobile = validateMobile(mobile);
90         // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志
91         List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
92
93         // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志
94         Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
95                 && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
96         String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
97         Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
98
99         // 发送 MQ 消息,异步执行发送短信
100         if (isSend) {
101             smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(),
102                     template.getApiTemplateId(), newTemplateParams);
103         }
104         return sendLogId;
105     }
106
107     @VisibleForTesting
108     SmsChannelDO validateSmsChannel(Long channelId) {
109         // 获得短信模板。考虑到效率,从缓存中获取
110         SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId);
111         // 短信模板不存在
112         if (channelDO == null) {
113             throw exception(SMS_CHANNEL_NOT_EXISTS);
114         }
115         return channelDO;
116     }
117
118     @VisibleForTesting
119     SmsTemplateDO validateSmsTemplate(String templateCode) {
120         // 获得短信模板。考虑到效率,从缓存中获取
121         SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode);
122         // 短信模板不存在
123         if (template == null) {
124             throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS);
125         }
126         return template;
127     }
128
129     /**
130      * 将参数模板,处理成有序的 KeyValue 数组
131      * <p>
132      * 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说 <a href="https://cloud.tencent.com/document/product/382/39023">腾讯云</a>
133      *
134      * @param template       短信模板
135      * @param templateParams 原始参数
136      * @return 处理后的参数
137      */
138     @VisibleForTesting
139     List<KeyValue<String, Object>> buildTemplateParams(SmsTemplateDO template, Map<String, Object> templateParams) {
140         return template.getParams().stream().map(key -> {
141             Object value = templateParams.get(key);
142             if (value == null) {
143                 throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key);
144             }
145             return new KeyValue<>(key, value);
146         }).collect(Collectors.toList());
147     }
148
149     @VisibleForTesting
150     public String validateMobile(String mobile) {
151         if (StrUtil.isEmpty(mobile)) {
152             throw exception(SMS_SEND_MOBILE_NOT_EXISTS);
153         }
154         return mobile;
155     }
156
157     @Override
158     public void doSendSms(SmsSendMessage message) {
159         // 获得渠道对应的 SmsClient 客户端
160         SmsClient smsClient = smsChannelService.getSmsClient(message.getChannelId());
161         Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId());
162         // 发送短信
163         try {
164             SmsSendRespDTO sendResponse = smsClient.sendSms(message.getLogId(), message.getMobile(),
165                     message.getApiTemplateId(), message.getTemplateParams());
166             smsLogService.updateSmsSendResult(message.getLogId(), sendResponse.getSuccess(),
167                     sendResponse.getApiCode(), sendResponse.getApiMsg(),
168                     sendResponse.getApiRequestId(), sendResponse.getSerialNo());
169         } catch (Throwable ex) {
170             log.error("[doSendSms][发送短信异常,日志编号({})]", message.getLogId(), ex);
171             smsLogService.updateSmsSendResult(message.getLogId(), false,
172                     "EXCEPTION", ExceptionUtil.getRootCauseMessage(ex), null, null);
173         }
174     }
175
176     @Override
177     public void receiveSmsStatus(String channelCode, String text) throws Throwable {
178         // 获得渠道对应的 SmsClient 客户端
179         SmsClient smsClient = smsChannelService.getSmsClient(channelCode);
180         Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode);
181         // 解析内容
182         List<SmsReceiveRespDTO> receiveResults = smsClient.parseSmsReceiveStatus(text);
183         if (CollUtil.isEmpty(receiveResults)) {
184             return;
185         }
186         // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新
187         receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(),
188                 result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg()));
189     }
190
191 }