1.今日项目实战的事项流程
2.今日工作安排
- 修复项目bug
- 理解发送短信工具类需求并设计表结构
- 编写工具类并提供feign调用功能
- 优化短信工具类实现多通道和负载均衡
3.【任务五】:修复redis中登陆验证码到期不删除的Bug
- 从master分支拉取代码,在本地创建bug-fix分支。
- 本地运行,修复代码bug。
- 提交分支代码,合并分支代码到bug-fix-conflict分支。
- 解决冲突后提交。
查看一下源代码:
debug一下:
可以看到key的ttl还剩8秒,还需要找到setKey的源代码。
刷新首页,可以发现:
原来以为key没有设置ttl,现在是设置了ttl,key还会长期存在。
只能请求老师帮助,发现是我的bug修复分支是在develop分支上创建的,需要在master分支上创建才行。可见我对分支的概念理解还不深入,一直认为开发代码只是能在develop分支上。忙活了大半天,一看小丑竟是我自己。
再看redis的key:
从master分支创建bug修复分支:
修改代码,将一天时间改为一分钟:
再运行:
bug修复了。提交代码:
切换到远程bug-fix-confict分支,再合并:
出现代码冲突:
最后push:
4. 短信微服务
短信微服务是一个独立的微服务,主要负责短信的发送,其他微服务可调用此微服务的接口进行短信发送。具体需求如下:
● 提供发送Feign接口,支持单次或批量发送短信
● 支持发送验证码、通知两种类型的短信
● 需要保存发送记录
4.1 【任务六】:理解发短信工具类需求并设计表结构
- 理解短信工具类微服务需求
- 根据需求设计表结构(设计完成后和答案对比一下)
参考答案:https://blog.csdn.net/2301_79965602/article/details/133304836
CREATE TABLE `sl_sms_record` (`id` bigint NOT NULL COMMENT '短信发送记录id',`send_channel_id` bigint NOT NULL COMMENT '发送通道id,对应sl_sms_third_channel的主键',`batch_id` bigint NOT NULL COMMENT '发送批次id,用于判断这些数据是同一批次发送的',`app_name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT '发起发送请求的微服务名称,如:sl-express-ms-work',`mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',`sms_content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '短信内容,一般为json格式的参数数据,用于填充短信模板中的占位符参数',`status` int NOT NULL COMMENT '发送状态,1:成功,2:失败',`created` datetime NOT NULL COMMENT '创建时间',`updated` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,KEY `created` (`created`) USING BTREE,KEY `batch_id` (`batch_id`) USING BTREE,KEY `mobile` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='短信发送记录';CREATE TABLE `sl_sms_third_channel` (`id` bigint NOT NULL COMMENT '主键id',`sms_type` int NOT NULL COMMENT '短信类型,1:验证类型短信,2:通知类型短信',`content_type` int NOT NULL COMMENT '内容类型,1:文字短信,2:语音短信',`sms_code` int NOT NULL COMMENT '短信code,短信微服务发放的code,与sms_code是一对多的关系',`template_code` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT '第三方平台模板code',`send_channel` int NOT NULL COMMENT '第三方短信平台码',`sign_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '签名',`sms_priority` int NOT NULL COMMENT '数字越大优先级越高',`account` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT '三方平台对应的账户信息,如:accessKeyId、accessKeySecret等,以json格式存储,使用时自行解析',`status` int NOT NULL COMMENT '通道状态1:使用 中,2:已经停用',`created` datetime NOT NULL COMMENT '创建时间',`updated` datetime NOT NULL COMMENT '更新时间',`is_delete` bit(1) NOT NULL COMMENT '是否删除',PRIMARY KEY (`id`) USING BTREE,KEY `created` (`created`) USING BTREE,KEY `sms_priority` (`sms_priority`),KEY `index_type` (`sms_type`,`content_type`,`sms_code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='短信发送通道';
4.2 【任务七】:编写短信工具类、并提供feign调用功能
- 实现短信工具类,参考文档:https://help.aliyun.com/document_detail/215759.html?spm=a2c4g.212725.0.0.67704c82Umxncl
- 实现短信工具类的Feign接口调用
发送短信工具类:
package com.sl.ms.sms.utils;import com.aliyun.teaopenapi.models.Config;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import lombok.extern.slf4j.Slf4j;import static com.aliyun.teautil.Common.toJSONString;import java.util.concurrent.ExecutionException;/*** @ClassName SendSms* @Description 阿里云sms, 发送短信工具类* @Author 孙克旭* @Date 2024/12/25 15:42*/
@Slf4j
public class SendSms {private Client client;public SendSms(String accessKeyId, String accessKeySecret, String endpoint) {this.createClient(accessKeyId, accessKeySecret, endpoint);}/*** 创建短信发送客户端** @return* @throws Exception*/public void createClient(String accessKeyId, String accessKeySecret, String endpoint) {try {Config config = new Config()// 配置 AccessKey ID,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。.setAccessKeyId(accessKeyId)// 配置 AccessKey Secret,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。.setAccessKeySecret(accessKeySecret);// 配置 Endpointconfig.endpoint = endpoint;this.client = new Client(config);} catch (Exception e) {throw new RuntimeException("创建短信发送客户端异常", e);}}/*** 发送短信** @param signName* @param templateCode* @param templateParam* @return* @throws ExecutionException* @throws InterruptedException*/public boolean sendCode(String phoneNumbers, String signName, String templateCode, String templateParam) {try {// 构造请求对象,请填入请求参数值SendSmsRequest sendSmsRequest = new SendSmsRequest().setPhoneNumbers(phoneNumbers).setSignName(signName).setTemplateCode(templateCode).setTemplateParam(templateParam);// 获取响应对象SendSmsResponse sendSmsResponse = this.client.sendSms(sendSmsRequest);// 响应包含服务端响应的 body 和 headersString result = toJSONString(sendSmsResponse);log.info("发送结果:{}", result);String message = sendSmsResponse.getBody().getMessage();return "OK".equals(message);} catch (Exception e) {throw new RuntimeException("发送失败", e);}}public static void main(String[] args) throws Exception {
// String phoneNumbers = AliConstant.PHONE_NUMBERS;
// String signName = AliConstant.SIGN_NAME;
// String captchaTemplateCode = AliConstant.CAPTCHA_TEMPLATE_CODE;
// String captchaTemplateParam = String.format(AliConstant.CAPTCHA_TEMPLATE_PARAM, "1256");
// SendSms sendSms = new SendSms();
// boolean result = sendSms.sendCode(phoneNumbers, signName, captchaTemplateCode, captchaTemplateParam);
// System.out.println(result);}
}
发送短信Service层实现接口:
package com.sl.ms.sms.service.impl;import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.oss.model.JsonFormat;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.sl.ms.sms.dto.SendResultDTO;
import com.sl.ms.sms.dto.SmsInfoDTO;
import com.sl.ms.sms.entity.SmsRecordEntity;
import com.sl.ms.sms.entity.SmsThirdChannelEntity;
import com.sl.ms.sms.enums.SendStatusEnum;
import com.sl.ms.sms.enums.SmsContentTypeEnum;
import com.sl.ms.sms.enums.SmsExceptionEnum;
import com.sl.ms.sms.enums.SmsTypeEnum;
import com.sl.ms.sms.handler.HandlerFactory;
import com.sl.ms.sms.handler.SmsSendHandler;
import com.sl.ms.sms.mapper.SlSmsRecordMapper;
import com.sl.ms.sms.mapper.SlSmsThirdChannelMapper;
import com.sl.ms.sms.service.ISlSmsThirdChannelService;
import com.sl.ms.sms.service.RouteService;
import com.sl.ms.sms.service.SmsService;
import com.sl.ms.sms.utils.SendSms;
import com.sl.transport.common.exception.SLException;
import com.sl.transport.common.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;import static com.aliyun.teautil.Common.toJSONString;@Slf4j
@Service
public class SmsServiceImpl extends ServiceImpl<SlSmsRecordMapper, SmsRecordEntity> implements SmsService {Gson gson = new Gson();Random random = new Random();@Resourceprivate RouteService routeService;@Autowiredprivate ISlSmsThirdChannelService smsThirdChannelService;@Overridepublic List<SendResultDTO> sendSms(SmsInfoDTO smsInfoDTO) {// TODO 参数校验 1.数据校验 2.接口幂等性校验//根据请求参数查找发送短信的必要参数//短信类型SmsTypeEnum smsType = smsInfoDTO.getSmsType();//内容类型SmsContentTypeEnum contentType = smsInfoDTO.getContentType();//短信code,有可能为空String smsCode = smsInfoDTO.getSmsCode();//appNameString appName = smsInfoDTO.getAppName();//查询数据库表:第三方通道,查看通道表中符合发送短信条件的通道,按照优先级降序排序,取第一个SmsThirdChannelEntity smsThirdChannel = smsThirdChannelService.lambdaQuery().select(SmsThirdChannelEntity::getId, SmsThirdChannelEntity::getTemplateCode, SmsThirdChannelEntity::getSignName, SmsThirdChannelEntity::getAccount).eq(SmsThirdChannelEntity::getStatus, "1").eq(SmsThirdChannelEntity::getSmsType, smsType).eq(SmsThirdChannelEntity::getContentType, contentType).eq(smsCode != null, SmsThirdChannelEntity::getSmsCode, smsCode).orderByDesc(SmsThirdChannelEntity::getSmsPriority).one();if (smsThirdChannel == null) {throw new RuntimeException(SmsExceptionEnum.SMS_CHANNEL_DOES_NOT_EXIST.getValue());}
// //路由短信发送通道
// SmsThirdChannelEntity smsThirdChannelEntity = routeService.route(smsInfoDTO.getSmsType(),
// smsInfoDTO.getContentType(), smsInfoDTO.getSmsCode());
// if (ObjectUtil.isEmpty(smsThirdChannel)) {
// throw new SLException(SmsExceptionEnum.SMS_CHANNEL_DOES_NOT_EXIST);
// }
// //获取service
// SmsSendHandler smsSendHandler = HandlerFactory.get(smsThirdChannelEntity.getSendChannel(), SmsSendHandler.class);
// if (ObjectUtil.isEmpty(smsSendHandler)) {
// throw new SLException(SmsExceptionEnum.SMS_CHANNEL_DOES_NOT_EXIST);
// }//生成批次id,一次请求的所有手机号都在同一批次long batchId = Math.abs(random.nextLong());//封装发送短信对象集合数据//模板codeString templateCode = smsThirdChannel.getTemplateCode();//todo 短信内容应该是按照smsCode的值来判断业务类型的,进而选择合适的json格式String smsContent = String.format("{\"code\":\"%s\"}", smsInfoDTO.getSmsContent());//签名String signName = smsThirdChannel.getSignName();// id、secret、endpointString account = smsThirdChannel.getAccount();JsonObject jsonObj = gson.fromJson(account, JsonObject.class);String accessKeyId = jsonObj.get("accessKeyId").getAsString();String accessKeySecret = jsonObj.get("accessKeySecret").getAsString();String endpoint = jsonObj.get("endpoint").getAsString();//获取手机号List<String> mobiles = smsInfoDTO.getMobiles();//创建发送短信客户端SendSms sendSms = new SendSms(accessKeyId, accessKeySecret, endpoint);List<SendResultDTO> list = new ArrayList<>();//通道idLong id = smsThirdChannel.getId();for (String mobile : mobiles) {//发送短信boolean b = sendSms.sendCode(mobile, signName, templateCode, smsContent);SendStatusEnum statue = b ? SendStatusEnum.SUCCESS : SendStatusEnum.FAIL;SmsRecordEntity smsRecordEntity = new SmsRecordEntity(id, batchId, appName, mobile, smsContent, statue);//入库save(smsRecordEntity);list.add(new SendResultDTO(smsRecordEntity.getId(), mobile, appName, batchId, statue));}log.info(list.toString());return list;}}
feign接口:
package com.sl.ms.sms.feign;import com.sl.ms.sms.dto.SendResultDTO;
import com.sl.ms.sms.dto.SmsInfoDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;import javax.validation.Valid;
import java.util.List;/*** @ClassName FeignClient* @Description feign客户端* @Author 孙克旭* @Date 2024/12/26 16:17*/
@RestController
@org.springframework.cloud.openfeign.FeignClient(value = "sl-express-ms-sms")
public interface SmsFeignClient {/*** 发送信息** @param smsInfoDTO* @return*/@PostMapping("/sms/send")List<SendResultDTO> send(@Valid @RequestBody SmsInfoDTO smsInfoDTO);}
测试feign接口:
返回的状态值是2,表示发送失败,再看发送信息的result:
这是阿里云的流量限制。feign接口测试成功。
4.3 【任务八】:短信工具类优化,实现多通道模式(使用简单工厂模式实现),并实现负载均衡(可以使用随机数实现)
- 短信工具类优化,实现多通道模式,也就是不仅仅使用阿里云实现,同时要提供其他的短信平台的实现。
- 实现多通道(多平台)负载均衡
2.1 实现多通道(多平台可以去阿里云云市场查找)负载均衡(轮询、随机、权重,实现一种即可)
云市场参考链接:参考链接
具体实现:
- 创建三方平台枚举类,用来表示不同的平台
- 创建自定义注解,注解属性包含枚举,用来表示不同的平台
- 创建路由Service接口,接口包含路由方法
3.1 路由方法查询数据库获取发送短信通道列表
3.2 实现通道的负载均衡(建议使用随机实现) - 编写简单工厂类(不了解简单工厂的话自行搜索),用来获取实际的短信发送通道
4.1 将短信发送通道通过Ioc存入Spring容器中,并添加自定义注解
4.2 解析Spring容器中短信发送通道类中的注解来确定具体的短信发送通道并返回(如何获取容器中的bean可以参考hutool工具类 参考链接) - 创建短信通道接口类,声明发送短信方法
- 参考短信工具类,创建短信通道实现类,实现短信通道接口类,编写具体的实现方法
- 组合上述步骤,完成调用
注意:(开发过程中实现多通道负载均衡就好,阿里云短信必须发送成功,其他平台不要求必须能发送成功,实现负载均衡就好。)
路由选择:
package com.sl.ms.sms.service.impl;import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sl.ms.sms.entity.SmsThirdChannelEntity;
import com.sl.ms.sms.enums.SmsContentTypeEnum;
import com.sl.ms.sms.enums.SmsTypeEnum;
import com.sl.ms.sms.mapper.SlSmsThirdChannelMapper;
import com.sl.ms.sms.service.ISlSmsThirdChannelService;
import com.sl.ms.sms.service.RouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.Random;@Service
public class RouteServiceImpl extends ServiceImpl<SlSmsThirdChannelMapper, SmsThirdChannelEntity> implements RouteService {@Autowiredprivate ISlSmsThirdChannelService smsThirdChannelService;/*** 目前只根据优先级进行路由选出前五,然后随机选择渠道** @param smsTypeEnum 短信类型* @param smsContentTypeEnum 内容类型* @param smsCode 短信code,短信微服务发放的code,与sms_code是一对多的关系* @return 选中的发送通道信息*/@Overridepublic SmsThirdChannelEntity route(SmsTypeEnum smsTypeEnum, SmsContentTypeEnum smsContentTypeEnum, String smsCode) {//取前符合条件的五条记录List<SmsThirdChannelEntity> list = smsThirdChannelService.lambdaQuery().select(SmsThirdChannelEntity::getId, SmsThirdChannelEntity::getTemplateCode, SmsThirdChannelEntity::getSignName, SmsThirdChannelEntity::getAccount).eq(SmsThirdChannelEntity::getStatus, "1").eq(SmsThirdChannelEntity::getSmsType, smsTypeEnum).eq(SmsThirdChannelEntity::getContentType, smsContentTypeEnum).eq(SmsThirdChannelEntity::getSmsCode, smsCode).orderByDesc(SmsThirdChannelEntity::getSmsPriority).last("limit 5").list();//查询数据//随机选择Random random = new Random();int randomNumber = random.nextInt(5);return list.get(randomNumber);}
}
这个任务不太会做,我再沉淀沉淀……