物流项目第六期(短信微服务——对接阿里云第三方短信服务JAVA代码实现、策略模式 + 工厂模式的应用)

发布于:2025-05-22 ⋅ 阅读:(15) ⋅ 点赞:(0)

 前五期:

物流项目第一期(登录业务)-CSDN博客

物流项目第二期(用户端登录与双token三验证)-CSDN博客

物流项目第三期(统一网关、工厂模式运用)-CSDN博客

 物流项目第四期(运费模板列表实现)-CSDN博客

物流项目第五期(运费计算实现、责任链设计模式运用)-CSDN博客

注:本章节内容很干,大部分都是代码,我会尽可能的在代码加上详细的注释,帮助理解

需求

短信微服务是一个独立的微服务,主要负责短信的发送,其他微服务可调用此微服务的接口进行短信发送。具体需求如下:

  • 提供发送Feign接口,支持单次或批量发送短信
  • 支持发送验证码、通知两种类型的短信
  • 需要保存发送记录
  • 支持多通道发送,并且需要做多通道间的负载均衡

表结构

通道表

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,其他微服务调用时需要传递该参数',
  `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='短信发送通道';

发送记录表

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='短信发送记录';

Entity

@Data
@TableName("sl_sms_third_channel")
public class SmsThirdChannelEntity extends BaseEntity {

    /**
     * 短信类型,1:文字短信,2:语音短信
     */
    private SmsTypeEnum smsType;

    /**
     * 内容类型,1:短信验证码,2:营销短信
     */
    private SmsContentTypeEnum contentType;

    /**
     * 短信code,短信微服务发放的code,与sms_code是一对多的关系
     */
    private String smsCode;

    /**
     * 第三方平台模板code
     */
    private String templateCode;

    /**
     * 第三方短信平台码
     */
    private SendChannelEnum sendChannel;

    /**
     * 签名
     */
    private String signName;

    /**
     * 优先级,数字越大优先级越高
     */
    private Integer smsPriority;

    /**
     * 三方平台对应的账户信息,如:accessKeyId、accessKeySecret等,以json格式存储,使用时自行解析
     */
    private String account;

    /**
     * 通道状态1:使用 中,2:已经停用
     */
    private Integer status;

    /**
     * 是否删除
     */
    private Boolean isDelete;

}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("sl_sms_record")
public class SmsRecordEntity extends BaseEntity {

    /**
     * 发送通道id,对应sl_sms_third_channel的主键
     */
    private Long sendChannelId;

    /**
     * 发送批次id,用于判断这些数据是同一批次发送的
     */
    private Long batchId;

    /**
     * 发起发送请求的微服务名称,如:sl-express-ms-work
     */
    private String appName;

    /**
     * 手机号
     */
    private String mobile;

    /**
     * 短信内容,一般为json格式的参数数据,用于填充短信模板中的占位符参数
     */
    private String smsContent;

    /**
     * 发送状态,1:成功,2:失败
     */
    private SendStatusEnum status;

}

Enum

/**
 * 三方发送平台枚举
 */
public enum SendChannelEnum implements BaseEnum {

    ALI_YUN(1, "阿里云短信平台,详情:https://www.aliyun.com/product/sms"),

    CONSOLE(2, "控制台发送短信");
    //YIMA(2, "yima发送短信");

    @EnumValue
    @JsonValue
    private Integer code;
    private String value;

    SendChannelEnum(Integer code, String value) {
        this.code = code;
        this.value = value;
    }

    @Override
    public Integer getCode() {
        return this.code;
    }

    @Override
    public String getValue() {
        return this.value;
    }

    public static SendChannelEnum codeOf(Integer code) {
        return EnumUtil.getBy(SendChannelEnum::getCode, code);
    }
}
/**
 * 交易枚举
 */
public enum SmsExceptionEnum implements BaseExceptionEnum {

    SMS_CHANNEL_DOES_NOT_EXIST(1001, "短信通道不存在");

    private Integer code;
    private Integer status;
    private String value;

    SmsExceptionEnum(Integer code, String value) {
        this.code = code;
        this.value = value;
        this.status = 500;
    }

    SmsExceptionEnum(Integer code, Integer status, String value) {
        this.code = code;
        this.value = value;
        this.status = status;
    }

    public Integer getCode() {
        return code;
    }

    public String getValue() {
        return this.value;
    }

    @Override
    public Integer getStatus() {
        return this.status;
    }
}

定义 SmsSendHandler 接口

定义 SmsSendHandler 接口及其方法 send 是为了抽象化发送短信的逻辑,使得不同的短信服务提供商(如阿里云、腾讯云等)可以以统一的方式被调用。这样做不仅提高了代码的可维护性,还增强了系统的扩展性和灵活性。

/**
 * SmsSendHandler 接口用于对接第三方短信平台,提供一个标准化的方式来发送短信。
 * 
 * 设计此接口的主要目的是为了实现解耦和模块化:
 * - 解耦:通过定义统一的接口,具体的短信发送逻辑可以在不改变调用方代码的情况下进行修改或替换。
 * - 模块化:允许轻松添加对新的短信服务提供商的支持,只需实现此接口即可。
 * 
 * 此接口特别适用于以下场景:
 * 1. 当你需要支持多个短信服务提供商时,每个提供商都有自己的API和请求格式。
 * 2. 当你希望在运行时动态选择使用哪个短信服务提供商时。
 * 3. 当你需要测试不同的短信服务提供商时,可以通过简单地切换实现类来完成。
 * 
 * 方法 send 的功能描述如下:
 * 
 * @param smsThirdChannelEntity 包含了发送通道的相关信息,例如:
 *                              - 短信类型(如验证码、通知等)
 *                              - 内容类型(如纯文本、富媒体等)
 *                              - 发送渠道(如阿里云、腾讯云等)
 *                              - 签名名称
 *                              - 第三方账号信息(如 Access Key ID, Secret Key 等)
 *                              - 状态(是否启用)
 *                              这些信息对于构建正确的请求至关重要。
 *                              
 * @param smsRecordEntities     待发送的短信记录列表,每个记录包含:
 *                              - 手机号码
 *                              - 短信内容
 *                              - 发送状态(初始为未发送)
 *                              - 创建时间
 *                              - 更新时间
 *                              在发送成功后,需要更新这些记录的状态为成功,并保存到数据库中。
 *                              
 * 发送短信的具体步骤通常包括:
 * 1. 根据 smsThirdChannelEntity 获取对应的第三方短信服务配置。
 * 2. 遍历 smsRecordEntities 列表,构造每个短信的请求参数。
 * 3. 调用第三方短信服务的 API 发送短信。
 * 4. 根据返回结果更新 smsRecordEntities 中每条记录的状态。
 * 5. 将更新后的记录保存到数据库中(假设有一个 DAO 或 Repository 来处理)。
 * 
 * 注意事项:
 * - 实现该接口时,应确保异常处理机制完善,避免因个别短信发送失败而导致整个流程中断。
 * - 如果可能,考虑异步发送短信以提高性能和用户体验。
 * - 对于高并发场景,考虑使用消息队列(如 RabbitMQ、Kafka)来缓冲短信发送请求。
 */
public interface SmsSendHandler {

    /**
     * 发送短信,发送成功后需要修改状态为成功状态。
     *
     * @param smsThirdChannelEntity 发送通道信息
     * @param smsRecordEntities     待发送列表
     */
    void send(SmsThirdChannelEntity smsThirdChannelEntity, List<SmsRecordEntity> smsRecordEntities);

}

 对接阿里云第三方短信平台

/**
 * AliYunSmsSendHandler 是一个实现了 SmsSendHandler 接口的具体类,
 * 用于通过阿里云短信服务发送短信。该类负责处理短信发送的逻辑,
 * 包括解析请求参数、构造HTTP请求、发送请求以及处理响应。
 */
@Slf4j // Lombok 注解,用于简化日志记录对象的创建
@Component // Spring 注解,标识该类为一个Spring管理的bean
@SendChannel(type = SendChannelEnum.ALI_YUN) // 自定义注解,用于标识该处理器适用于阿里云短信通道
public class AliYunSmsSendHandler implements SmsSendHandler {

    /**
     * 定义短信模板,其中包含占位符 {code},用于替换实际验证码。
     */
    private static final String TEMPLATE = "您的验证码是:{code},请勿泄露。";

    /**
     * 阿里云短信内容的最大字符限制建议为100以内,超出该长度可能会导致发送失败。
     */
    private static final int MAX_CONTENT_LENGTH = 100;

    /**
     * ObjectMapper 实例,用于将Java对象转换为JSON字符串或反之。
     */
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 发送短信的核心方法。
     *
     * @param smsThirdChannelEntity 包含第三方平台的配置信息,如AppCode、模板ID等。
     * @param smsRecordEntities     待发送的短信记录列表,每条记录包含手机号码、短信内容等信息。
     */
    @Override
    public void send(SmsThirdChannelEntity smsThirdChannelEntity, List<SmsRecordEntity> smsRecordEntities) {
        // 记录开始发送短信的日志,包括渠道配置信息和待发送短信的数量。
        log.info("开始发送短信,渠道配置:{}", JSONUtil.toJsonStr(smsThirdChannelEntity));
        log.info("待发送短信数量:{}", smsRecordEntities.size());

        // 解析第三方平台的账户配置信息,获取必要的参数,如AppCode。
        JSONObject accountJson = JSONUtil.parseObj(smsThirdChannelEntity.getAccount());
        String appCode = accountJson.getStr("appCode");

        // 定义阿里云短信服务的API地址、路径和请求方法。
        String host = "https://send.market.alicloudapi.com";
        String path = "/sms/send";
        String method = "POST";

        // 构造HTTP请求的公共头部信息,包括授权信息和内容类型。
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", "APPCODE " + appCode);
        headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");

        // 公共查询参数(这里为空)
        Map<String, String> querys = new HashMap<>();

        // 遍历每条短信记录进行发送
        for (SmsRecordEntity record : smsRecordEntities) {
            try {
                // 解析原始短信内容(假设是JSON格式字符串),提取出需要替换到模板中的参数。
                Map<String, String> params = parseContentToMap(record.getSmsContent());

                // 将提取出的参数转换为JSON字符串,作为最终的短信内容。
                String contentJson = JSONUtil.toJsonStr(params);

                // 构造HTTP请求的body部分,包含短信内容、模板ID和接收手机号码。
                Map<String, String> bodys = new HashMap<>();
                bodys.put("content", contentJson); // 使用 JSON 格式的 content 字段
                bodys.put("templateid", smsThirdChannelEntity.getTemplateCode()); // 模板 ID
                bodys.put("mobile", record.getMobile()); // 手机号

                // 发起HTTP POST请求,发送短信。
                HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);

                // 获取并记录短信平台返回的响应内容。
                String responseBody = EntityUtils.toString(response.getEntity());
                log.info("短信平台返回响应:{}", responseBody);

                // 解析JSON响应,判断短信是否发送成功。
                JsonNode jsonNode = objectMapper.readTree(responseBody);
                boolean isSuccess = "OK".equals(jsonNode.path("status").asText());

                if (isSuccess) {
                    // 如果短信发送成功,则更新记录状态为成功。
                    record.setStatus(SendStatusEnum.SUCCESS);
                } else {
                    // 如果短信发送失败,则更新记录状态为失败,并记录警告日志。
                    record.setStatus(SendStatusEnum.FAIL);
                    log.warn("短信发送失败,手机号:{},响应内容:{}", record.getMobile(), responseBody);
                }

            } catch (Exception e) {
                // 处理发送过程中可能发生的异常,更新记录状态为失败,并记录错误日志。
                record.setStatus(SendStatusEnum.FAIL);
                log.error("短信发送出现异常,手机号:{}", record.getMobile(), e);
            }

            // 更新短信记录的状态(这部分假定你有一个 DAO 或 Repository 来处理)。
            // 注意:在实际应用中,这里应调用相应的持久化方法来保存修改后的记录。
            // smsRecordRepository.save(record);
        }
    }

    /**
     * 解析原始短信内容(假设是JSON格式字符串),提取出需要替换到模板中的参数。
     *
     * @param contentJson 原始短信内容,通常是一个JSON格式的字符串。
     * @return 包含所有需要替换到模板中的参数的Map对象。
     */
    private Map<String, String> parseContentToMap(String contentJson) {
        Map<String, String> map = new HashMap<>();
        try {
            // 解析JSON字符串,提取出所有的键值对。
            Map<String, Object> parsed = JSONUtil.parseObj(contentJson).entrySet().stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString()));
            for (Map.Entry<String, Object> entry : parsed.entrySet()) {
                map.put(entry.getKey(), entry.getValue().toString());
            }
        } catch (Exception e) {
            // 如果解析失败,则记录警告日志,并提供默认值。
            log.warn("解析短信内容失败:{}", contentJson);
            map.put("code", "未知");
        }
        return map;
    }
}

实现短信渠道处理工厂类

为什么要设计这样一个 HandlerFactory

✅ 1. 统一获取处理器的入口

无论你是通过:

  • 枚举(SendChannelEnum)
  • 字符串("ALI_YUN")
  • 整数编码(1001)

你都可以通过一个工厂类拿到对应的处理类,避免到处写 if-elseswitch-case 来判断渠道。


✅ 2. 实现解耦,增强扩展性

  • 业务层只需要知道渠道类型(比如来自数据库或请求参数),无需关心具体是哪个类在干活;
  • 新增短信服务商时,只需新增一个 SmsSendHandler 实现类并加上 @SendChannel 注解即可;
  • 不需要修改原有代码,符合开闭原则。

✅ 3. 支持多种参数来源

实际项目中,渠道信息可能来自:

  • 数据库字段(int code)
  • 接口请求参数(String channelName)
  • 内部枚举(SendChannelEnum)

这个工厂类都能兼容,非常灵活。


✅ 4. 提高可维护性和可测试性

  • 所有处理器统一管理;
  • 方便进行 Mock 测试;
  • 逻辑清晰,易于排查问题。

📦 总结一句话:

HandlerFactory 是一个基于自定义注解和 Spring 容器的短信处理器选择工厂,它让系统可以根据不同的短信渠道自动匹配到对应的实现类,实现了解耦、统一调度、高扩展性的架构设计目标。

/**
 * 短信渠道处理器工厂类
 *
 * 用于根据短信发送渠道(如阿里云、腾讯云等)动态获取对应的 SmsSendHandler 实现类。
 * 这是典型的【策略模式】 + 【工厂模式】结合使用,实现了解耦和统一调度。
 */
public class HandlerFactory {

    /**
     * 私有化构造函数,防止外部实例化此类。
     * 因为这是一个工具类/工厂类,不需要也不应该被 new 出来。
     */
    private HandlerFactory() {
        // 空构造方法,仅用于阻止实例化
    }

    /**
     * 根据短信渠道枚举类型获取对应的短信发送处理器
     *
     * @param sendChannelEnum 指定的短信渠道枚举(如 ALI_YUN, TX_YUN)
     * @param handler         目标处理器接口类型(如 SmsSendHandler.class)
     * @return 返回匹配的处理器实例,若无匹配则返回 null
     */
    public static <T> T get(SendChannelEnum sendChannelEnum, Class<T> handler) {

        // 从 Spring 容器中获取所有实现了指定接口的 Bean
        Map<String, T> beans = SpringUtil.getBeansOfType(handler);

        // 遍历所有找到的 Bean
        for (Map.Entry<String, T> entry : beans.entrySet()) {

            // 获取当前 Bean 的类对象
            Class<?> beanClass = entry.getValue().getClass();

            // 查看该类上是否有 @SendChannel 注解
            SendChannel sendChannelAnnotation = beanClass.getAnnotation(SendChannel.class);

            // 如果存在注解,并且其 type() 值与传入的 sendChannelEnum 匹配,则返回该 Bean
            if (ObjectUtil.isNotEmpty(sendChannelAnnotation)
                    && ObjectUtil.equal(sendChannelEnum, sendChannelAnnotation.type())) {
                return entry.getValue();
            }
        }

        // 没有找到匹配的处理器,返回 null
        return null;
    }

    /**
     * 通过字符串形式的渠道名称获取短信发送处理器
     *
     * 示例:"ALI_YUN" -> SendChannelEnum.ALI_YUN
     *
     * @param payChannel 渠道名称字符串(注意大小写需一致)
     * @param handler      目标处理器接口类型
     * @return 对应的处理器实例
     */
    public static <T> T get(String payChannel, Class<T> handler) {
        // 将字符串转换为枚举后调用主方法
        return get(SendChannelEnum.valueOf(payChannel), handler);
    }

    /**
     * 通过整数编码获取短信发送处理器
     *
     * 示例:1001 -> SendChannelEnum.ALI_YUN
     *
     * @param code    渠道编码(由业务定义,如数据库字段)
     * @param handler 目标处理器接口类型
     * @return 对应的处理器实例
     */
    public static <T> T get(Integer code, Class<T> handler) {
        // 调用 SendChannelEnum.codeOf(code) 方法将编码转为枚举
        return get(SendChannelEnum.codeOf(code), handler);
    }

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented //标记注解
public @interface SendChannel {

    SendChannelEnum type();

}

短信选择

设计意图与作用

1. 解耦与模块化
  • 该方法实现了短信发送渠道的选择逻辑,使得具体的路由逻辑独立于其他业务逻辑,便于维护和扩展。
  • 通过接口 RouteService 和其实现类 RouteServiceImpl,实现了良好的模块化设计,方便后续添加新的路由规则或修改现有规则。
2. 灵活的选择策略
  • 通过设置优先级和随机选择机制,可以在多个符合条件的渠道中灵活选择一个最优或随机的渠道进行短信发送。
  • 这种设计特别适合需要负载均衡或多渠道备份的场景。

 

 

/**
 * RouteServiceImpl 是一个服务层实现类,用于根据短信类型、内容类型和短信编码路由到合适的短信发送渠道。
 * 它扩展了 MyBatis-Plus 提供的 ServiceImpl 基础实现,并实现了自定义的 RouteService 接口。
 */
@Service // 标识此类为 Spring 管理的 Bean,可以被自动扫描并注入到其他组件中
public class RouteServiceImpl extends ServiceImpl<SmsThirdChannelMapper, SmsThirdChannelEntity> implements RouteService {

    /**
     * 根据给定的短信类型、内容类型和短信编码选择一个合适的短信发送渠道。
     *
     * @param smsTypeEnum         短信类型(如验证码、通知等)
     * @param smsContentTypeEnum  短信内容类型(如纯文本、富媒体等)
     * @param smsCode             短信编码,通常对应某个特定模板或业务场景
     * @return 匹配的 SmsThirdChannelEntity 实例,如果未找到匹配项则返回 null
     */
    @Override
    public SmsThirdChannelEntity route(SmsTypeEnum smsTypeEnum, SmsContentTypeEnum smsContentTypeEnum, String smsCode) {
        // 创建 LambdaQueryWrapper 来构建查询条件,使用 lambda 表达式来避免硬编码字段名
        LambdaQueryWrapper<SmsThirdChannelEntity> queryWrapper = Wrappers.<SmsThirdChannelEntity>lambdaQuery()
                .eq(SmsThirdChannelEntity::getSmsType, smsTypeEnum) // 短信类型匹配
                .eq(SmsThirdChannelEntity::getContentType, smsContentTypeEnum) // 内容类型匹配
                .eq(SmsThirdChannelEntity::getSmsCode, smsCode) // 短信编码匹配
                .eq(SmsThirdChannelEntity::getStatus, 1) // 只选择启用状态的渠道
                .orderByDesc(SmsThirdChannelEntity::getSmsPriority) // 按优先级降序排列
                .last("LIMIT 5"); // 限制结果集大小为5条记录

        // 执行查询操作,获取符合条件的 SmsThirdChannelEntity 列表
        List<SmsThirdChannelEntity> smsThirdChannelEntities = super.list(queryWrapper);

        // 如果没有找到任何符合条件的渠道,则返回 null
        if (CollUtil.isEmpty(smsThirdChannelEntities)) {
            return null;
        }

        // 随机选择一个符合条件的渠道(从列表中随机选择一个索引)
        int index = RandomUtil.randomInt(0, CollUtil.size(smsThirdChannelEntities));

        // 返回随机选择的渠道实体
        return CollUtil.get(smsThirdChannelEntities, index);
    }
}

短信发送业务

这个类的作用与设计意图

✅ 1. 统一短信发送流程

  • 从接收请求、路由渠道、组装数据、发送短信、记录日志、持久化数据,整个过程都在一个方法里完成;
  • 保证了短信发送的完整性、一致性。

✅ 2. 职责分离清晰

  • RouteService 负责选路;
  • HandlerFactory 负责找具体实现;
  • SmsSendHandler 负责具体发送;
  • SmsServiceImpl 作为协调者串联各个组件;
  • 遵循单一职责原则,提高可维护性。

✅ 3. 高扩展性

  • 新增短信平台?只需:
    • 添加新的 SmsSendHandler 实现类;
    • 加上 @SendChannel 注解;
    • 系统自动识别,无需改动已有代码;

✅ 4. 便于监控和追溯

  • 每条短信都有唯一批次 ID;
  • 所有发送记录都会入库;
  • 方便后续做统计分析、失败重试、客户投诉核查等工作。

✅ 5. 容错机制

  • 渠道找不到时抛出明确异常;
  • 发送失败会记录日志;
  • 可结合定时任务进行失败短信重发。

📦 总结一句话:

SmsServiceImpl 是整个短信发送业务的核心协调者,它整合了短信路由、处理器选择、短信记录生成、调用发送接口、持久化等功能,是短信服务模块中最核心的服务类之一。它的存在使得短信发送功能变得结构清晰、易于扩展、便于维护和监控。

 

/**
 * SmsServiceImpl 是短信发送服务的具体实现类。
 *
 * 它继承自 MyBatis-Plus 的 ServiceImpl,具备操作数据库的能力(如保存短信记录),
 * 并实现了自定义接口 SmsService,对外暴露统一的短信发送方法。
 */
@Slf4j // Lombok 提供的日志工具注解,自动创建 log 对象
@Service // 标识为 Spring Bean,可被自动注入使用
public class SmsServiceImpl extends ServiceImpl<SmsRecordMapper, SmsRecordEntity> implements SmsService {

    /**
     * 注入路由服务类,用于根据短信类型、内容类型等信息选择合适的短信渠道。
     */
    @Resource
    private RouteService routeService;

    /**
     * 短信发送主入口方法。
     *
     * @param smsInfoDTO 包含短信发送所需的基本参数,如手机号列表、短信内容、短信类型等
     * @return 返回发送结果 DTO 列表,包含每个手机号的发送状态等信息
     */
    @Override
    public List<SendResultDTO> send(SmsInfoDTO smsInfoDTO) {

        // 1️⃣ 记录开始日志,方便调试和追踪请求
        log.info("开始路由短信通道,参数:smsType={}, contentType={}, smsCode={}",
                smsInfoDTO.getSmsType(), smsInfoDTO.getContentType(), smsInfoDTO.getSmsCode());

        // 2️⃣ 调用路由服务,根据短信类型、内容类型、短信编码获取对应的短信渠道配置
        SmsThirdChannelEntity smsThirdChannelEntity = routeService.route(
                smsInfoDTO.getSmsType(),
                smsInfoDTO.getContentType(),
                smsInfoDTO.getSmsCode()
        );

        // 3️⃣ 如果未找到匹配的短信发送渠道,抛出异常并记录错误日志
        if (ObjectUtil.isEmpty(smsThirdChannelEntity)) {
            log.error("未能找到短信通道,查询参数为:smsType={}, contentType={}, smsCode={}",
                    smsInfoDTO.getSmsType(), smsInfoDTO.getContentType(), smsInfoDTO.getSmsCode());
            throw new SLException(SmsExceptionEnum.SMS_CHANNEL_DOES_NOT_EXIST);
        }

        // 4️⃣ 找到短信渠道后,记录该渠道信息
        log.info("找到短信通道实体: {}", smsThirdChannelEntity);

        // 5️⃣ 使用 HandlerFactory 获取与当前渠道对应的短信发送处理器(如阿里云、腾讯云)
        SmsSendHandler smsSendHandler = HandlerFactory.get(smsThirdChannelEntity.getSendChannel(), SmsSendHandler.class);

        // 6️⃣ 如果没有找到对应的处理器,说明系统中没有支持该渠道的实现,抛出异常
        if (ObjectUtil.isEmpty(smsSendHandler)) {
            log.info("开始路由短信通道,参数:smsType={}, contentType={}, smsCode={}",
                    smsInfoDTO.getSmsType(), smsInfoDTO.getContentType(), smsInfoDTO.getSmsCode());
            throw new SLException(SmsExceptionEnum.SMS_CHANNEL_DOES_NOT_EXIST);
        }

        // 7️⃣ 生成唯一批次 ID,用于标识本次发送任务(可用于后续日志追踪或查数据)
        long batchId = IdWorker.getId();

        // 8️⃣ 构建短信发送记录列表 SmsRecordEntity,每条记录对应一个手机号
        List<SmsRecordEntity> smsRecordEntities = StreamUtil.of(smsInfoDTO.getMobiles())
                .map(mobile -> SmsRecordEntity.builder()
                        .batchId(batchId) // 批次 ID,同一时间发送的所有短信共用同一个批次号
                        .appName(smsInfoDTO.getAppName()) // 来源应用名,用于区分哪个微服务发起的请求
                        .smsContent(smsInfoDTO.getSmsContent()) // 短信内容,可能是 JSON 字符串
                        .sendChannelId(smsThirdChannelEntity.getId()) // 所使用的短信渠道 ID
                        .mobile(mobile) // 当前处理的手机号
                        .status(SendStatusEnum.FAIL) // 默认发送状态为失败,发送成功后再改为成功
                        .build())
                .collect(Collectors.toList());

        // 9️⃣ 调用具体的短信发送处理器执行发送逻辑(比如调用阿里云 API)
        smsSendHandler.send(smsThirdChannelEntity, smsRecordEntities);

        // 🔟 将短信发送记录批量写入数据库,用于后期审计、排查、统计等用途
        super.saveBatch(smsRecordEntities);

        // 🔪 最后将实体类列表转换为返回值 DTO 列表,屏蔽数据库字段细节
        return BeanUtil.copyToList(smsRecordEntities, SendResultDTO.class);
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到