短信接入

这一步我们只需打开支付宝,扫码登录,如果是第一次登录,可能还需要进行实名认证(如果不是第一次则不需要)

我们选择个人认证,认证完成后,重新在使用支付宝扫码登录即可

登录成功后,在阿里云首页,找到短信服务点击进入,准备开通并购买短信服务

image-20250426161215887

首先开通短信服务,但是必须首先下载钉钉,扫码加群,会送免费套餐包,如果不扫码无法开通短信服务

image-20250426161951378

在调用阿里云API时您需要使用AccessKey完成身份验证。AccessKey包括AccessKey ID和AccessKey Secret,需要一起使用。具体如下:

  • AccessKey ID:用于标识用户。
  • AccessKey Secret:用于验证用户的密钥。AccessKey Secret必须保密。

处于安全的考虑,创建RAM账号。这里的一个RAM账号相当于你的一个子账号,每个账号可以限定其访问的资源,不同的资源可以创建不同RAM账号来访问,所以我们可以创建RAM账号,并给它赋予短信资源访问权限

首先进入进入工作台,准备创建RAM用户

image-20250426163215187

接着进入创建RAM用户界面

image-20250426163515583
image-20250426164106552
image-20250426165012091
image-20250426165447942
image-20250426170211468

到此为止,就完成了RAM账号的短信服务授权,这样依赖我们的AccessKey才会生效

在真正使用短信服务前还需要申请资质,申请签名,以及申请模版,因为目前已经不支持测试用途的申请,所以我们了解即可

image-20250426181257536

申请资质

主要就是提交身份证明,证明短信服务使用方的资质,并给这个资质起一个名字。这里需要说明的是我们可以创建多个资质,举个例子比如王道这家公司有背单词APP,刷题APP,而且这两个APP都会用到短信功能,我们就需要为两个APP分别申请资质。

申请签名

申请好了资质之后,我们可以给资质绑定签名,所谓短信签名就是短信发送方属性的一种标识。一条完整的短信是有短信签名+短信内容组成。

image-20250426183647848

申请模版

使用阿里云的SMS发送短信的内容,并不是随便定义的,只能基于模版生成。

尊敬的${name},您今天学习的单词数量为${count},不要懒惰

name和count是两个占位符,真正发送的短信内容,可以变化的部分只有占位符的值,比如上面短信内容中,name被替换为徐凤年,count被替换为20

但是很遗憾,因为工信部对于短信的管理愈发严格了,所以没有经过企业实名认证的模版,现在已经用不了了,因此我们只能使用阿里云专门提供的测试模版(发送验证码的模版来发送),但是我们可以将测试模版中的验证码,替换为用户当天剩余的应该记忆的单词数量= 用户每天计划学习的单词数量 – 当天已经复习的单词数量(user_rev_statistics)

image-20250526071735588

所以大家在项目中关于发送短信的配置如下:

sms:
    access-key-id: 大家自己的AccessKey ID
    access-key-secret: 大家自己的AccessKey Secret
    sign: 阿里云短信测试
    remind-template-code: SMS_154950909
    end-point: dysmsapi.aliyuncs.com

同时还需要注意的是,对于测试模版中的占位符${code}有一个要求,必须是4-6位的数值字符串,因此我们需要注意,针对剩余的应该记忆的单词数量,我们应该做一个格式化:

 // 假如vocCount就是当天用户剩余的应该记忆的单词数量,将其变为宽度为4位的数值字符串,用该值替换占位符${code}的值
 String vocCountStr = String.format("%04d", vocCount);

注意,因为我们是开发测试,要发送短信有一定的限制,必须绑定自己的手机号,才可能对绑定过的手机号发送短信

image-20250526071651926

引入依赖

想要使用发送短信的功能,我们需要引入短信发送的SDK,如果是maven工程直接引入maven依赖即可

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>dysmsapi20170525</artifactId>
    <version>3.1.0</version>
</dependency>

基于SDK发送短信

// 构造请求对象
SendSmsRequest sendSmsRequest = new SendSmsRequest()
            // 发送短信的目标手机号
            .setPhoneNumbers("1390000****")
            // 设置签名
            .setSignName("自己申请的签名")
            // 设置申请的模版对应的模版码,相当于指定发送断行的模版
            .setTemplateCode("模版码")
            // TemplateParam 为序列化后的 JSON 字符串。其中\"表示转义后的双引号。
            // name和count属性的值分别表示模版中,name和count占位符的值
            .setTemplateParam("{\"name\":\"张三\",\"count'\":\"1390000****\"}"); 

// 创建配置类对象
Config config = new Config();
// 指定accesskeyid
config.setAccessKeyId(accessKeyId);
// 指定accesskeySecret
config.setAccessKeySecret(accessKeySecret);
// 创建发送请求客户端
Client client = new Client(config);

// 发送 API 请求
SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);

当然,在我们的项目中,短信相关的配置已经定义好了配置类和工具类,我们只需要调用使用即可。

image-20250426194625061

在用户的APP的我的页面中,用户可以设置学习提醒功能的打开关闭,以及发送提醒的时间(时间是诸如21:30 这样的只包含小时和分钟的时间),我们要实现的功能就是,在满足一定的条件的情况下,在用户设置的提醒时间,给用户发送提醒背单词的短信,其基本实现思路如下:

整个发送提醒短信业务的实现分成三个组成部分:

  • 整点执行的定时任务: 主要负责筛选出在近1小时内满足发送提醒短信的用户,并生成短信,放入延迟队列
  • 延迟队列: 负责暂存待发送的消息
  • 发送短信的线程: 负责不停地从延迟队列中取出消息来发送

在这里需要解释一下,我们是如何保证在用户指定的时间将短信发送出去的呢? 这主要依赖于延迟队列的特性

  • 放入延迟队列的元素都需要指定一个特殊的属性——延迟时间,且所有元素在队列中会以延迟时间排序,延迟时间小的在队头,延迟时间大的在队尾
  • 延迟时间代表了放入延迟队列的元素出队列的条件——只有达到了该元素的延迟时间,队头元素才能出队列(因为队列按延迟时间有序,所以取元素还是只能取队头元素)
  • 如果取队头元素(延迟时间最小的元素)时,它还没到达延迟时间,那么获取元素的线程会被阻塞,直到取到队头元素,线程才会被唤醒,并获取到队头元素
  • 如果队列为空时,从队列中获取队头元素,获取元素的线程同样会阻塞,直到有新的元素添加到延迟队列,且队头元素的延迟时间到,获取元素的线程才会被唤醒,并获取队头元素

所以,结合延迟队列的以上特征,我们只需要保证生成短信的定时任务在向延迟队列放入待发送的短信(延迟队列元素)时,将其延迟时间指定为待用户设置的短信提醒时间即可,只有到了短信应该发送的时间,它才能被发送短信的线程取出来发送,这样就可以达到我们想要的效果了。

生成短信定时任务

生成短信的定时任务的执行时机为整点执行,它的核心任务是筛选出未来一个小时内,满足发送提醒短信条件的用户,并生成待发送的短信放入延迟队列,这里的一定条件具体如下:

  • 用户设置的提醒时间在未来的一小时内(包括整点的那一分钟)
  • 用户今天尚未签到
  • 用户最近连续签到天数>=指定天数(在配置文件中指定,默认3天)
# 学习提醒配置
user:
  remind:
    days: 3   #当前连续签到天数最少天数
    span: 5   #发送短信的时间间隔
    description: '3天及3天以上(由于短信成本较高,用户不会每天收到短信提醒)'
  • 用户最近一个周内没有发送过提醒短信(控制短信发送频率)

关于生成短信的定时任务我们需要注意的是:

  • 查询的时候使用循环+分页查询,因为可能用户的数据量较大
  • 学习提醒开关,提醒时间涉及表user_reamind
  • 判断签到以及最近连续签到涉及表user_checkin
  • 判断最近一个星期内是否发送涉及表user_remind_message
  • 向延迟队列中存取元素的方法和普通队列没有区别

延迟队列

对于延迟队列而言,其存取元素的方法与普通队列基本没有差异,对于使用者而言,最大的差别在于元素具有了延迟时间,如何指定元素的延迟时间呢?我们只需要让队列元素所属的类继承Delayed接口接口

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

在我们的项目中,封装短信消息的类为SmsDelayMessage定义如下:

@Data
public class SmsDelayMessage implements Delayed {

    Long userId;
    Integer vocCount;
    // 用户设置的提醒时间
    LocalTime remindTime;
    String userName;
    String phoneNumber;

    /**
        可以认为: 
        1. 当getDelay方法返回0说明延迟时间到
        2. 当getDelay方法返回负数说明已经过了延迟时间
        以上两种情况元素都可以被取出,为了达到元素在到达提醒时间之后可以被取出的目的,
        所以返回的延迟时间为: 提醒时间 - 当前时间
    */
    @Override
    public long getDelay(@NotNull TimeUnit unit) {
        LocalTime now = LocalTime.now();
        Duration between = Duration.between(remindTime, now);
        return between.toMinutes();
    }

    /**
        compareTo方法来自于Comparable接口,主要定义了延迟队列中元素的
        排序规则,我们这里就以延迟时间的大小来排序
    */
    @Override
    public int compareTo(@NotNull Delayed other) {
        Long substraction
            = getDelay(TimeUnit.MINUTES) - other.getDelay(TimeUnit.MINUTES);
        return substraction.intValue();
    }
}

同时,为了保证定时任务和消息发送线程访能访问到同一个延迟队列,所以我们需要一定义一个配置类,将延迟队列放入Spring容器中,这一点一定要注意!

发送短信线程

我们可以通过配置类中的init方法,实现在应用启动时,创建并启动线程的工作:

@Component
@Slf4j
public class SmsConsumer {

    // 延迟队列
    @Resource
    DelayQueue<SmsDelayMessage> delayQueue;

    // 消息发送工具类对象
    @Resource
    SmsSender smsSender;

    // mapper用来在发送短信后,插入发送短信的记录
    @Resource
    UserRemindMessageMapper userRemindMessageMapper;


    /**
        这里@PstConstruct注解的作用为,让Spring在创建完对象后,自动调用该方法
        来完成对象的初始化(也就是说该方法会被Spring自动调用)
    */
    @PostConstruct
    public void init() {
        log.info("start to consume message...");
        
        // 启动线程(也可以自己查阅资料使用其他方式启动线程)
        new Thread(() -> {
            while (true) {
                
                // 获取延迟队列中的短信消息对象(注意如果没有或者队头元素延迟时间未到会在这里阻塞)
                
                // 获取到短信消息后,发送
                
                // 在数据库添加消息发送记录                
                
            }
        })
    }

}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇