springboot集成Quartz任务调度

概述:Quartz是功能强大的开源作业调度库,几乎可以集成到任何Java应用程序中-从最小的独立应用程序到最大的电子商务系统。Quartz可用于创建简单或复杂的计划,以执行数以万计,数以万计的工作;任务定义为标准Java组件的作业,它们实际上可以执行您可以编写的所有内容。Quartz Scheduler包含许多企业级功能,例如对JTA事务和集群的支持。
Quartz是免费使用的,并根据Apache 2.0许可获得许可。

以上是在Quartz官网上复制的。废话不多说,执行上代码。

提前准备工作,自己到quartz官网上下载源码,里面带有不同数据库版本的脚本进行初始化sql脚本
,我这快使用的是quartz-2.3.0-SNAPSHOT版本,数据库使用的postgresql 11

脚本路径为:quartz-2.3.0-distribution.tar\quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore

这块根据自己使用的数据库进行选择即可。

1、pom代码

<!--quartz定时调度依赖-->
  <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-quartz</artifactId>
   </dependency>

版本是根据springboot框架自动获取的,我这块用的是2.0.4.RELEASE

2、Quartz配置类

import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

import javax.sql.DataSource;

/**
 * Quartz任务调度配置器
 */
@Configuration
@EnableScheduling
public class QuartzConfiguration {
    /**
     * 继承org.springframework.scheduling.quartz.SpringBeanJobFactory
     * 实现任务实例化方式
     */
    public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
            ApplicationContextAware {

        private transient AutowireCapableBeanFactory beanFactory;

        @Override
        public void setApplicationContext(final ApplicationContext context) {
            beanFactory = context.getAutowireCapableBeanFactory();
        }

        /**
         * 将job实例交给spring ioc托管
         * 我们在job实例实现类内可以直接使用spring注入的调用被spring ioc管理的实例
         *
         * @param bundle
         * @return
         * @throws Exception
         */
        @Override
        protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
            final Object job = super.createJobInstance(bundle);
            /**
             * 将job实例交付给spring ioc
             */
            beanFactory.autowireBean(job);
            return job;
        }
    }

    /**
     * 配置任务工厂实例
     *
     * @return
     */
    @Bean
    public JobFactory jobFactory() {
        /**
         * 采用自定义任务工厂 整合spring实例来完成构建任务*/
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        return jobFactory;
    }

    /**
     * 配置任务调度器
     * 使用项目数据源作为quartz数据源
     *
     * @param jobFactory 自定义配置任务工厂
     * @param dataSource 数据源实例
     * @return
     * @throws Exception
     */
    @Bean(destroyMethod = "destroy", autowire = Autowire.NO)
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        //将spring管理job自定义工厂交由调度器维护
        schedulerFactoryBean.setJobFactory(jobFactory);
        //设置覆盖已存在的任务
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        //项目启动完成后,等待2秒后开始执行调度器初始化
        schedulerFactoryBean.setStartupDelay(2);
        //设置调度器自动运行
        schedulerFactoryBean.setAutoStartup(true);
        //设置数据源,使用与项目统一数据源
        schedulerFactoryBean.setDataSource(dataSource);
        //设置上下文spring bean name
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
        //设置配置文件位置
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
        return schedulerFactoryBean;
    }
}

3、Quartz配置文件quartz.properties

# 在集群中每个实例都必须有一个唯一的instanceId,但是应该有一个相同的instanceName【默认“QuartzScheduler”】
org.quartz.scheduler.instanceName = schedulerFactoryBean
# 只有在”org.quartz.scheduler.instanceId”设置为”AUTO”的时候才使用该属性设置
# 默认情况下,“org.quartz.simpl.SimpleInstanceIdGenerator”是基于instanceId和时间戳来自动生成的
org.quartz.scheduler.instanceId = AUTO
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#驱动程序代表理解不同数据库系统的特定方言,许多数据库使用StdJDBCDelegate
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
#数据库中表的前缀
org.quartz.jobStore.tablePrefix = QRTZ_
# 为了指示JDBCJobStore所有的JobDataMaps中的值都是字符串,并且能以“名字-值”对的方式存储而不是以复杂对象的序列化形式存储在BLOB字段中,应该设置为true(缺省方式)
org.quartz.jobStore.useProperties = false
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
# 并发个数
org.quartz.threadPool.threadCount = 10
# 优先级
org.quartz.threadPool.threadPriority = 5

基本上Quartz的配置完成,下面开始些正在的实现操作。

4、TestController实现

package com.joe.oauth.quartz.controller;

import com.joe.oauth.quartz.domin.TaskScheduleVo;
import com.joe.oauth.quartz.job.TestJob1;
import com.joe.oauth.quartz.service.QuartzService;
import com.joe.oauth.quartz.util.CronUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Calendar;

@RestController
public class TestController {

    @Autowired
    private QuartzService quartzService;
    @RequestMapping("/addjob")
    public void startJob(String jobName) {
        TaskScheduleVo taskScheduleVo = new TaskScheduleVo();
        Calendar now = Calendar.getInstance();
        taskScheduleVo.setJobType(1);
        taskScheduleVo.setHour(now.get(Calendar.HOUR_OF_DAY));
        taskScheduleVo.setMinute(now.get(Calendar.MINUTE)+1);//分
        taskScheduleVo.setSecond(0);//秒
        //组装cron表达式
        String cron = CronUtil.createCronExpression(taskScheduleVo);
        quartzService.addJob(TestJob1.class, jobName, "test", "0/10 * * * * ?");
    }
    
    @RequestMapping("/updatejob")
    public void updatejob(String jobName) {
            quartzService.updateJob(jobName, "test", "0/10 * * * * ?");
    }
    
    @RequestMapping("/deletejob")
    public void deletejob(String jobName) {
            quartzService.deleteJob(jobName, "test");
    }
    
    @RequestMapping("/pauseJob")
    public void pauseJob(String jobName) {
            quartzService.pauseJob(jobName, "test");
    }
    
    @RequestMapping("/resumeJob")
    public void resumeJob(String jobName) {
            quartzService.resumeJob(jobName, "test");
    }
    
    @RequestMapping("/queryAllJob")
    public Object queryAllJob() {
            return quartzService.queryAllJob();
    }
    

    @RequestMapping("/queryRunJob")
    public Object queryRunJob() {
            return quartzService.queryRunJob();
    }
}

5、Job的业务层

package com.joe.oauth.quartz.job;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.Date;

@PersistJobDataAfterExecution //参数持久化,这块暂时没有传入参数,加上该属性不影响
@DisallowConcurrentExecution// 不允许并发执行
public class TestJob1 extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException {
        System.out.println(new Date() + "    job执行");
    }

}

6、QuartzService实现

package com.joe.oauth.quartz.service;

import org.quartz.*;
import org.quartz.DateBuilder.IntervalUnit;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.*;

@Service
public class QuartzService {

    @Autowired
    private Scheduler scheduler;

    @PostConstruct
    public void startScheduler() {
        try {
            scheduler.start();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 增加一个job
     * 
     * @param jobClass
     *            任务实现类
     * @param jobName
     *            任务名称
     * @param jobGroupName
     *            任务组名
     * @param jobTime
     *            时间表达式 (这是每隔多少秒为一次任务)
     * @param jobTimes
     *            运行的次数 (<0:表示不限次数)
     */
    public void addJob(Class<? extends QuartzJobBean> jobClass, String jobName, String jobGroupName, int jobTime,
            int jobTimes) {
        try {
            JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName)// 任务名称和组构成任务key
                    .build();
            // 使用simpleTrigger规则
            Trigger trigger = null;
            if (jobTimes < 0) {
                trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName)
                        .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(1).withIntervalInSeconds(jobTime))
                        .startNow().build();
            } else {
                trigger = TriggerBuilder
                        .newTrigger().withIdentity(jobName, jobGroupName).withSchedule(SimpleScheduleBuilder
                                .repeatSecondlyForever(1).withIntervalInSeconds(jobTime).withRepeatCount(jobTimes))
                        .startNow().build();
            }
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 增加一个job
     * 
     * @param jobClass
     *            任务实现类
     * @param jobName
     *            任务名称
     * @param jobGroupName
     *            任务组名
     * @param jobTime
     *            时间表达式 (如:0/5 * * * * ? )
     */
    public void addJob(Class<? extends QuartzJobBean> jobClass, String jobName, String jobGroupName, String jobTime) {
        try {
            // 创建jobDetail实例,绑定Job实现类
            // 指明job的名称,所在组的名称,以及绑定job类
            JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName)// 任务名称和组构成任务key
                    .build();
            // 定义调度触发规则
            // 使用cornTrigger规则
            Trigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName)// 触发器key
                    .startAt(DateBuilder.futureDate(1, IntervalUnit.SECOND))
                    .withSchedule(CronScheduleBuilder.cronSchedule(jobTime)).startNow().build();
            // 把作业和触发器注册到任务调度中
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 修改 一个job的 时间表达式
     * 
     * @param jobName
     * @param jobGroupName
     * @param jobTime
     */
    public void updateJob(String jobName, String jobGroupName, String jobTime) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule(jobTime)).build();
            // 重启触发器
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 删除任务一个job
     * 
     * @param jobName
     *            任务名称
     * @param jobGroupName
     *            任务组名
     */
    public void deleteJob(String jobName, String jobGroupName) {
        try {
            scheduler.deleteJob(new JobKey(jobName, jobGroupName));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 暂停一个job
     * 
     * @param jobName
     * @param jobGroupName
     */
    public void pauseJob(String jobName, String jobGroupName) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
            scheduler.pauseJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 恢复一个job
     * 
     * @param jobName
     * @param jobGroupName
     */
    public void resumeJob(String jobName, String jobGroupName) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
            scheduler.resumeJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 立即执行一个job
     * 
     * @param jobName
     * @param jobGroupName
     */
    public void runAJobNow(String jobName, String jobGroupName) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
            scheduler.triggerJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取所有计划中的任务列表
     * 
     * @return
     */
    public List<Map<String, Object>> queryAllJob() {
        List<Map<String, Object>> jobList = null;
        try {
            GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
            Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
            jobList = new ArrayList<Map<String, Object>>();
            for (JobKey jobKey : jobKeys) {
                List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                for (Trigger trigger : triggers) {
                    Map<String, Object> map = new HashMap<>();
                    map.put("jobName", jobKey.getName());
                    map.put("jobGroupName", jobKey.getGroup());
                    map.put("description", "触发器:" + trigger.getKey());
                    Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                    map.put("jobStatus", triggerState.name());
                    if (trigger instanceof CronTrigger) {
                        CronTrigger cronTrigger = (CronTrigger) trigger;
                        String cronExpression = cronTrigger.getCronExpression();
                        map.put("jobTime", cronExpression);
                    }
                    jobList.add(map);
                }
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return jobList;
    }

    /**
     * 获取所有正在运行的job
     * 
     * @return
     */
    public List<Map<String, Object>> queryRunJob() {
        List<Map<String, Object>> jobList = null;
        try {
            List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
            jobList = new ArrayList<Map<String, Object>>(executingJobs.size());
            for (JobExecutionContext executingJob : executingJobs) {
                Map<String, Object> map = new HashMap<String, Object>();
                JobDetail jobDetail = executingJob.getJobDetail();
                JobKey jobKey = jobDetail.getKey();
                Trigger trigger = executingJob.getTrigger();
                map.put("jobName", jobKey.getName());
                map.put("jobGroupName", jobKey.getGroup());
                map.put("description", "触发器:" + trigger.getKey());
                Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                map.put("jobStatus", triggerState.name());
                if (trigger instanceof CronTrigger) {
                    CronTrigger cronTrigger = (CronTrigger) trigger;
                    String cronExpression = cronTrigger.getCronExpression();
                    map.put("jobTime", cronExpression);
                }
                jobList.add(map);
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return jobList;
    }

}

7、CronUtil工具类

package com.joe.oauth.quartz.util;

import com.joe.oauth.quartz.domin.TaskScheduleVo;

public class CronUtil {

    /**
     *
     *方法摘要:构建Cron表达式
     *@param  taskScheduleVo
     *@return String
     */
    public static String createCronExpression(TaskScheduleVo taskScheduleVo){
        StringBuffer cronExp = new StringBuffer("");

        if(null == taskScheduleVo.getJobType()) {
            System.out.println("执行周期未配置" );//执行周期未配置
        }

        if (null != taskScheduleVo.getSecond()
                && null == taskScheduleVo.getMinute()
                && null == taskScheduleVo.getHour()){
            //每隔几秒
            if (taskScheduleVo.getJobType().intValue() == 0) {
                cronExp.append("0/").append(taskScheduleVo.getSecond());
                cronExp.append(" ");
                cronExp.append("* ");
                cronExp.append("* ");
                cronExp.append("* ");
                cronExp.append("* ");
                cronExp.append("?");
            }

        }

        if (null != taskScheduleVo.getSecond()
                && null != taskScheduleVo.getMinute()
                && null == taskScheduleVo.getHour()){
            //每隔几分钟
            if (taskScheduleVo.getJobType().intValue() == 4) {
                cronExp.append("* ");
                cronExp.append("0/").append(taskScheduleVo.getMinute());
                cronExp.append(" ");
                cronExp.append("* ");
                cronExp.append("* ");
                cronExp.append("* ");
                cronExp.append("?");
            }

        }

        if (null != taskScheduleVo.getSecond()
                && null != taskScheduleVo.getMinute()
                && null != taskScheduleVo.getHour()) {
            //秒
            cronExp.append(taskScheduleVo.getSecond()).append(" ");
            //分
            cronExp.append(taskScheduleVo.getMinute()).append(" ");
            //小时
            cronExp.append(taskScheduleVo.getHour()).append(" ");

            //每天
            if(taskScheduleVo.getJobType().intValue() == 1){
                cronExp.append("* ");//日
                cronExp.append("* ");//月
                cronExp.append("?");//周
            }

            //按每周
            else if(taskScheduleVo.getJobType().intValue() == 3){
                //一个月中第几天
                cronExp.append("? ");
                //月份
                cronExp.append("* ");
                //周
                Integer[] weeks = taskScheduleVo.getDayOfWeeks();
                for(int i = 0; i < weeks.length; i++){
                    if(i == 0){
                        cronExp.append(weeks[i]);
                    } else{
                        cronExp.append(",").append(weeks[i]);
                    }
                }

            }

            //按每月
            else if(taskScheduleVo.getJobType().intValue() == 2){
                //一个月中的哪几天
                Integer[] days = taskScheduleVo.getDayOfMonths();
                for(int i = 0; i < days.length; i++){
                    if(i == 0){
                        cronExp.append(days[i]);
                    } else{
                        cronExp.append(",").append(days[i]);
                    }
                }
                //月份
                cronExp.append(" * ");
                //周
                cronExp.append("?");
            }

        }
        else {
            System.out.println("时或分或秒参数未配置" );//时或分或秒参数未配置
        }
        return cronExp.toString();
    }

    /**
     *
     *方法摘要:生成计划的详细描述
     *@param  taskScheduleVo
     *@return String
     */
    public static String createDescription(TaskScheduleVo taskScheduleVo){
        StringBuffer description = new StringBuffer("");
        //计划执行开始时间
//      Date startTime = taskScheduleVo.getScheduleStartTime();

        if (null != taskScheduleVo.getSecond()
                && null != taskScheduleVo.getMinute()
                && null != taskScheduleVo.getHour()) {
            //按每天
            if(taskScheduleVo.getJobType().intValue() == 1){
                description.append("每天");
                description.append(taskScheduleVo.getHour()).append("时");
                description.append(taskScheduleVo.getMinute()).append("分");
                description.append(taskScheduleVo.getSecond()).append("秒");
                description.append("执行");
            }

            //按每周
            else if(taskScheduleVo.getJobType().intValue() == 3){
                if(taskScheduleVo.getDayOfWeeks() != null && taskScheduleVo.getDayOfWeeks().length > 0) {
                    String days = "";
                    for(int i : taskScheduleVo.getDayOfWeeks()) {
                        days += "周" + i;
                    }
                    description.append("每周的").append(days).append(" ");
                }
                if (null != taskScheduleVo.getSecond()
                        && null != taskScheduleVo.getMinute()
                        && null != taskScheduleVo.getHour()) {
                    description.append(",");
                    description.append(taskScheduleVo.getHour()).append("时");
                    description.append(taskScheduleVo.getMinute()).append("分");
                    description.append(taskScheduleVo.getSecond()).append("秒");
                }
                description.append("执行");
            }

            //按每月
            else if(taskScheduleVo.getJobType().intValue() == 2){
                //选择月份
                if(taskScheduleVo.getDayOfMonths() != null && taskScheduleVo.getDayOfMonths().length > 0) {
                    String days = "";
                    for(int i : taskScheduleVo.getDayOfMonths()) {
                        days += i + "号";
                    }
                    description.append("每月的").append(days).append(" ");
                }
                description.append(taskScheduleVo.getHour()).append("时");
                description.append(taskScheduleVo.getMinute()).append("分");
                description.append(taskScheduleVo.getSecond()).append("秒");
                description.append("执行");
            }

        }
        return description.toString();
    }

    //参考例子
    public static void main(String[] args) {
        //执行时间:每天的12时12分12秒 start
        TaskScheduleVo taskScheduleVo = new TaskScheduleVo();

        taskScheduleVo.setJobType(0);//按每秒
        taskScheduleVo.setSecond(30);
        String cronExp = createCronExpression(taskScheduleVo);
        System.out.println(cronExp);

        taskScheduleVo.setJobType(4);//按每分钟
        taskScheduleVo.setMinute(8);
        String cronExpp = createCronExpression(taskScheduleVo);
        System.out.println(cronExpp);

        taskScheduleVo.setJobType(1);//按每天
        Integer hour = 12; //时
        Integer minute = 12; //分
        Integer second = 12; //秒
        taskScheduleVo.setHour(hour);
        taskScheduleVo.setMinute(minute);
        taskScheduleVo.setSecond(second);
        String cropExp = createCronExpression(taskScheduleVo);
        System.out.println(cropExp + ":" + createDescription(taskScheduleVo));
        //执行时间:每天的12时12分12秒 end

        taskScheduleVo.setJobType(3);//每周的哪几天执行
        Integer[] dayOfWeeks = new Integer[3];
        dayOfWeeks[0] = 1;
        dayOfWeeks[1] = 2;
        dayOfWeeks[2] = 3;
        taskScheduleVo.setDayOfWeeks(dayOfWeeks);
        cropExp = createCronExpression(taskScheduleVo);
        System.out.println(cropExp + ":" + createDescription(taskScheduleVo));

        taskScheduleVo.setJobType(2);//每月的哪几天执行
        Integer[] dayOfMonths = new Integer[3];
        dayOfMonths[0] = 1;
        dayOfMonths[1] = 21;
        dayOfMonths[2] = 13;
        taskScheduleVo.setDayOfMonths(dayOfMonths);
        cropExp = createCronExpression(taskScheduleVo);
        System.out.println(cropExp + ":" + createDescription(taskScheduleVo));

    }

}

8、Cron时间实体

package com.joe.oauth.quartz.domin;

import lombok.Data;

@Data
public class TaskScheduleVo {

    /**
     * 所选作业类型:
     * 0  -> 每秒
     * 4  -> 每分
     * 1  -> 每天
     * 3  -> 每周
     * 2  -> 每月
     */
    Integer jobType;

    /**月*/
    Integer[] dayOfMonths;

    /**天*/
    Integer[] dayOfWeeks;

    /**秒  */
    Integer second;

    /**分  */
    Integer minute;

    /**时  */
    Integer hour;

}

9、执行结果如下:

在这里插入图片描述
这块我设置的间隔10秒执行异常任务。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值