org.quartz-scheduler-spring-boot-starter-mail 是一个用于在 Spring Boot 应用中集成 Quartz Scheduler 和邮件发送功能

本文详细介绍Quartz调度器的集群配置与实践,包括Quartz的特性、集群的好处及其实现方式,通过示例代码展示了如何在Spring Boot环境中配置Quartz集群,并利用数据库和Terracotta实现高可用性和负载均衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Quartz Scheduler 是一个开源的作业调度框架,支持多种类型的作业。以下是一些常见的作业类型:

  1. 简单作业:这种作业只执行一次,通常用于简单的任务,如发送一封电子邮件或执行一个简单的计算。
  2. 重复作业:这种作业可以按照指定的时间间隔重复执行,例如每天、每周或每月执行一次。
  3. Cron 表达式作业:这种作业使用 Cron 表达式来定义作业的执行时间,可以非常灵活地设置复杂的调度计划。
  4. 监听器作业:这种作业可以监听特定的事件,并在事件发生时执行相应的操作。
  5. 持久化作业:这种作业可以在服务器重启后继续执行,确保任务不会因为服务器故障而丢失。
  6. 非持久化作业:这种作业在服务器关闭后不会保存,适用于临时性的任务。
  7. 状态监控作业:这种作业可以监控其他作业的执行状态,并在需要时进行干预或调整。
  8. 数据驱动作业:这种作业可以根据外部数据源(如数据库)中的数据动态生成和调度。
  9. 远程作业:这种作业可以通过网络与其他系统交互,实现分布式任务调度。
  10. 集群作业:这种作业可以在多个节点上同时运行,提高系统的可靠性和可扩展性。

Quartz Scheduler 的强大之处在于它的灵活性和可扩展性,能够适应各种复杂的调度需求。

org.quartz-scheduler-spring-boot-starter-mail 是一个用于在 Spring Boot 应用中集成 Quartz Scheduler 和邮件发送功能的 starter。Quartz 是一个开源的作业调度框架,它可以帮助你在 Java 应用程序中执行定时任务。而这个 starter 则简化了将 Quartz 与邮件发送功能结合使用的过程。

以下是一个简单的介绍:

1. 引入依赖

首先,你需要在你的 pom.xml 文件中添加以下依赖:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-scheduler-spring-boot-starter-mail</artifactId>
    <version>2.3.0</version>
</dependency>

2. 配置邮件属性

application.propertiesapplication.yml 文件中配置邮件服务器的相关属性,例如:

spring.mail.host=smtp.example.com
spring.mail.port=587
spring.mail.username=your_email@example.com
spring.mail.password=your_password
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

3. 创建邮件任务

创建一个类来实现具体的邮件发送逻辑,并使用 Quartz 的注解来定义任务的触发器和调度策略。例如:

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;

@Component
public class EmailJob implements Job {

    @Autowired
    private JavaMailSender mailSender;

    @Override
    public void execute(JobExecutionContext context) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo("recipient@example.com");
        message.setSubject("Test Email");
        message.setText("This is a test email sent by Quartz Scheduler.");
        mailSender.send(message);
    }
}

4. 配置任务调度

application.propertiesapplication.yml 文件中配置 Quartz 的任务调度信息,例如:

spring.quartz.job-store-type=memory
spring.quartz.scheduler.instance-name=myScheduler
spring.quartz.scheduler.instance-id=AUTO

# Define the job and its trigger
spring.quartz.jobs[0].job-class=com.example.EmailJob
spring.quartz.jobs[0].cron=0/30 * * * * ?

5. 启动应用

启动你的 Spring Boot 应用,Quartz 会根据你配置的调度策略自动执行邮件发送任务。

org.quartz-scheduler-spring-boot-starter-mail

  • Quartz是OpenSymphony开源组织在Job
    scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或
    EJBs。Quartz的最新版本为Quartz 2.3.2。
  • 集群Quartz应用
    Quartz应用能被集群,是水平集群还是垂直集群取决于你自己的需要。集群提供以下好处:
    ·伸缩性
    ·高可用性
    ·负载均衡
    Quartz可以借助关系数据库和JDBC作业存储支持集群。
    Terracotta扩展quartz提供集群功能而不需要数据库支持
    相关工具
    Quartz经常会用到cron表达式,可以使用国外网站cronmaker辅助生成cron表达式。
package programb.abel.quartz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;


@SpringBootApplication
@ImportResource("classpath*:META-INF/spring/*.xml")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }


}

package programb.abel.quartz.config;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.lang.reflect.Method;


public class InvokingJobDetailFactory extends QuartzJobBean {

    /**
     * 计划任务所在类
     */
    private String targetObject;

    /**
     * 具体需要执行的计划任务
     */
    private String targetMethod;

    private ApplicationContext ctx;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        try {
            Object obj = ctx.getBean(targetObject);
            Method m = null;
            try {
                m = obj.getClass().getMethod(targetMethod);
                //调用被代理对象的方法
                m.invoke(obj);
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            throw new JobExecutionException(e);
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext) {
        this.ctx = applicationContext;
    }

    public void setTargetObject(String targetObject) {
        this.targetObject = targetObject;
    }

    public void setTargetMethod(String targetMethod) {
        this.targetMethod = targetMethod;
    }
}

package programb.abel.quartz.config;

import java.beans.PropertyVetoException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import com.zaxxer.hikari.HikariDataSource;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;


@Configuration
public class QuartzConfig {

    /**
     * 1.通过name+group获取唯一的jobKey;2.通过groupname来获取其下的所有jobkey
     */
    final static String GROUP_NAME = "QuartzJobGroups";

    @Value("${quartz.scheduler.instanceName}")
    private String quartzInstanceName;

    @Value("${spring.datasource.driverClassName}")
    private String myDSDriver;

    @Value("${spring.datasource.url}")
    private String myDSUrl;

    @Value("${spring.datasource.username}")
    private String myDSUser;

    @Value("${spring.datasource.password}")
    private String myDSPassword;

    @Value("${org.quartz.dataSource.myDS.maxConnections}")
    private int myDSMaxConnections;

    /**
     * 设置属性
     *
     * @return
     * @throws IOException
     */
    private Properties quartzProperties() throws IOException {
        Properties prop = new Properties();
        // 调度标识名 集群中每一个实例都必须使用相同的名称
        prop.put("quartz.scheduler.instanceName", quartzInstanceName);
        // ID设置为自动获取 每一个必须不同
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        // 禁用quartz软件更新
        prop.put("org.quartz.scheduler.skipUpdateCheck", "true");
        prop.put("org.quartz.scheduler.jmx.export", "true");


        // 数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
        prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
        // 数据保存方式为数据库持久化
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        // 数据库别名 随便取
        prop.put("org.quartz.jobStore.dataSource", "quartzDataSource");
        //prop.put("org.quartz.jobStore.dataSource", "myDS");
        // 表的前缀,默认QRTZ_
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
        // 是否加入集群
        prop.put("org.quartz.jobStore.isClustered", "true");

        // 调度实例失效的检查时间间隔
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "20000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        // 信息保存时间 ms 默认值60秒
        prop.put("org.quartz.jobStore.misfireThreshold", "120000");
        prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");
        prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE");

        // 程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        // 定线程数,至少为1(无默认值)(一般设置为1-100之间的整数合适)
        prop.put("org.quartz.threadPool.threadCount", "10");
        // 设置线程的优先级(最大为java.lang.Thread.MAX_PRIORITY 10,最小为Thread.MIN_PRIORITY 1,默认为5)
        prop.put("org.quartz.threadPool.threadPriority", "5");
        prop.put("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "true");

        prop.put("org.quartz.plugin.triggHistory.class", "org.quartz.plugins.history.LoggingJobHistoryPlugin");
        prop.put("org.quartz.plugin.shutdownhook.class", "org.quartz.plugins.management.ShutdownHookPlugin");
        prop.put("org.quartz.plugin.shutdownhook.cleanShutdown", "true");

        //#自定义连接池
        //org.quartz.dataSource.myDS.connectionProvider.class=com.poly.pay.schedule.DruidConnectionProvider

        return prop;
    }

    /**
     * 数据源
     *
     * @return
     * @throws PropertyVetoException
     */
    @Bean
    public HikariDataSource createDataSource() throws PropertyVetoException {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(myDSUrl);
        dataSource.setDriverClassName(myDSDriver);
        dataSource.setUsername(myDSUser);
        dataSource.setPassword(myDSPassword);
        dataSource.setMaximumPoolSize(myDSMaxConnections);
        return dataSource;
    }


    /**
     * 创建触发器工厂
     *
     * @param jobDetail
     * @param cronExpression
     * @return
     */
    private static CronTriggerFactoryBean cronTriggerFactoryBean(JobDetail jobDetail, String cronExpression) {
        CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
        factoryBean.setJobDetail(jobDetail);
        factoryBean.setCronExpression(cronExpression);
        return factoryBean;
    }


/****************************************************以下配置需要注意******************************************************/


    /**
     * 调度工厂
     * 此处配置需要调度的触发器 例如 executeJobTrigger
     *
     * @param executeJobTrigger
     * @return
     * @throws IOException
     * @throws PropertyVetoException
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("executeJobTrigger") Trigger executeJobTrigger) throws IOException, PropertyVetoException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        // this allows to update triggers in DB when updating settings in config file:
        //用于quartz集群,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
        factory.setOverwriteExistingJobs(true);
        //用于quartz集群,加载quartz数据源
        //factory.setDataSource(dataSource);
        //QuartzScheduler 延时启动,应用启动完10秒后 QuartzScheduler 再启动
        //factory.setStartupDelay(10);
        //用于quartz集群,加载quartz数据源配置
        factory.setAutoStartup(true);
        factory.setQuartzProperties(quartzProperties());
        factory.setApplicationContextSchedulerContextKey("applicationContext");
        factory.setDataSource(createDataSource());
        //注册触发器
        Trigger[] triggers = {executeJobTrigger};
        factory.setTriggers(triggers);

        return factory;
    }


    /**
     * 加载触发器
     *
     * 新建触发器进行job 的调度  例如 executeJobDetail
     * @param jobDetail
     * @return
     */
    @Bean(name = "executeJobTrigger")
    public CronTriggerFactoryBean executeJobTrigger(@Qualifier("executeJobDetail") JobDetail jobDetail) {
        //每天凌晨3点执行
        return cronTriggerFactoryBean(jobDetail, "0 1 0 * * ? ");
    }


    /**
     * 加载job
     *
     * 新建job 类用来代理
     *
     *
     * @return
     */
    @Bean
    public JobDetailFactoryBean executeJobDetail() {
        return createJobDetail(InvokingJobDetailFactory.class, GROUP_NAME, "executeJob");
    }


    /**
     * 执行规则job工厂
     *
     * 配置job 类中需要定时执行的 方法  execute
     * @param jobClass
     * @param groupName
     * @param targetObject
     * @return
     */
    private static JobDetailFactoryBean createJobDetail(Class<? extends Job> jobClass,
                                                        String groupName,
                                                        String targetObject) {
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(jobClass);
        factoryBean.setDurability(true);
        factoryBean.setRequestsRecovery(true);
        factoryBean.setGroup(groupName);
        Map<String, String> map = new HashMap<>();
        map.put("targetMethod", "execute");
        map.put("targetObject", targetObject);
        factoryBean.setJobDataAsMap(map);
        return factoryBean;
    }

}
package programb.abel.quartz.job;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;


@Service
public class ExecuteJob {
    private static final Logger logger = LoggerFactory.getLogger(ExecuteJob.class);

    /**
     * 方法名在quartz定义
     */
    public void execute() {
        System.out.println("定时任务执行了。。。。。");

    }
}

## tomcat\u914D\u7F6E
server.port=8090
#server.tomcat.maxHttpHeaderSize=8192
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.abel.encoding=UTF-8
# tomcat\u6700\u5927\u7EBF\u7A0B\u6570\uFF0C\u9ED8\u8BA4\u4E3A200  
server.tomcat.max-threads=800
# session\u6700\u5927\u8D85\u65F6\u65F6\u95F4(\u5206\u949F)\uFF0C\u9ED8\u8BA4\u4E3A30
server.session-timeout=60

## spring \u914D\u7F6E
spring.application.name=springboot-Quartz
application.main=programb.abel.quartz.Application


## \u4E3B\u6570\u636E\u6E90\uFF0C\u9ED8\u8BA4\u7684
spring.datasource.url=jdbc:mysql://localhost:3306/quartz?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.driverClassName=com.mysql.jdbc.Driver

## \u8FDE\u63A5\u6C60\u914D\u7F6E
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#\u6700\u5C0F\u7A7A\u95F2\u8FDE\u63A5
spring.datasource.hikari.minimum-idle=10
#\u8FDE\u63A5\u6C60\u4E2D\u5141\u8BB8\u7684\u6700\u5927\u8FDE\u63A5\u6570\u3002\u7F3A\u7701\u503C\uFF1A10\uFF1B\u63A8\u8350\u7684\u516C\u5F0F\uFF1A((core_count * 2) + effective_spindle_count)
spring.datasource.hikari.maximum-pool-size=30
#spring.datasource.hikari.auto-commit=true
#\u4E00\u4E2A\u8FDE\u63A5idle\u72B6\u6001\u7684\u6700\u5927\u65F6\u957F\uFF08\u6BEB\u79D2\uFF09\uFF0C\u8D85\u65F6\u5219\u88AB\u91CA\u653E\uFF08retired\uFF09\uFF0C\u7F3A\u7701:10\u5206\u949F\u3002minimumIdle<maximumPoolSize\u65F6\u751F\u6548
spring.datasource.hikari.idle-timeout=120000
#\u81EA\u5B9A\u4E49\u8FDE\u63A5\u6C60\u540D
#spring.datasource.hikari.pool-name=DatebookHikariCP
#\u4E00\u4E2A\u8FDE\u63A5\u7684\u751F\u547D\u65F6\u957F\uFF08\u6BEB\u79D2\uFF09\uFF0C\u8D85\u65F6\u800C\u4E14\u6CA1\u88AB\u4F7F\u7528\u5219\u88AB\u91CA\u653E\uFF08retired\uFF09\uFF0C\u7F3A\u7701:30\u5206\u949F\uFF0C\u5EFA\u8BAE\u8BBE\u7F6E\u6BD4\u6570\u636E\u5E93\u8D85\u65F6\u65F6\u957F\u5C1130\u79D2\uFF0C\u53C2\u8003MySQL wait_timeout\u53C2\u6570\uFF08show variables like '%timeout%';\uFF09
spring.datasource.hikari.max-lifetime=1800000
#\u7B49\u5F85\u8FDE\u63A5\u6C60\u5206\u914D\u8FDE\u63A5\u7684\u6700\u5927\u65F6\u957F\uFF08\u6BEB\u79D2\uFF09\uFF0C\u8D85\u8FC7\u8FD9\u4E2A\u65F6\u957F\u8FD8\u6CA1\u53EF\u7528\u7684\u8FDE\u63A5\u5219\u53D1\u751FSQLException\uFF0C \u7F3A\u7701:30\u79D2
spring.datasource.hikari.connection-timeout=30000
#\u6307\u5B9A\u9A8C\u8BC1\u8FDE\u63A5\u6709\u6548\u6027\u7684\u8D85\u65F6\u65F6\u95F4\uFF0C\u9ED8\u8BA4\u662F5\u79D2
spring.datasource.hikari.validation-timeout=3000
spring.datasource.hikari.connection-test-query=SELECT 1

# \u8C03\u5EA6\u6807\u8BC6\u540D \u96C6\u7FA4\u4E2D\u6BCF\u4E00\u4E2A\u5B9E\u4F8B\u90FD\u5FC5\u987B\u4F7F\u7528\u76F8\u540C\u7684\u540D\u79F0
quartz.scheduler.instanceName=QuartScheduler
# \u5141\u8BB8\u6700\u5927\u8FDE\u63A5
org.quartz.dataSource.myDS.maxConnections=10

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <contextName>springboot-quartz</contextName>

    <!-- 控制台输入日志信息 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%method - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 当日日志归档文件 -->
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--过期日志转存的文件名格式 -->
            <FileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}</FileNamePattern>
            <!-- 日志保留天数 -->
            <MaxHistory>60</MaxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%method - %msg%n</Pattern>
        </layout>
    </appender>

    <!--<logger name="com.apache.ibatis" level="TRACE"/>-->
    <logger name="com.apache.ibatis" level="DEBUG"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>

    <!-- com.alibaba.dubbo是dubbo服务的包,在info以下的级别会产生大量的启动日志,调成WARN减少日志输出 -->
    <logger name="com.alibaba.dubbo" level="INFO"/>

    <root level="INFO">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>programb.abel.quartz</groupId>
    <artifactId>springboot-Quartz</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--quartz-->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <!--因为quartz 需要有Spring context 所有引入mail包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>3.2.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bol5261

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值