Springboot+Quartz(四)--Quartz在项目中的使用

前面3篇文章介绍了quartz简单的使用以及quartz核心类的作用,这一篇文章主要介绍quartz在项目中的使用。

这篇文章是我之前在项目中的应用,有些业务相对比较复杂,大家看看就好。

这一篇对于quartz的一些类和接口的使用方法可能和上一篇不太一样,但是稍微理解一下那个类的作用是什么,相信大家就可以理解这么做的意义了。附上一篇链接:Quartz核心类详解

1.自定义注解JobUnit

我们通过自定义注解的方法,来完成job的一些信息的初始化,比如cron,job名称和分组

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JobUnit {
    /**
     * @return
     */
    String jobName() default "";

    String jobGroup() default "";

    String jobDesc() default "";

    String jobCorn() default "";

    int misfireInstruction() default CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;

    /**
     * 是否单例,单例情况下以代码配置为主,系统无法修改 jobName+jobGroup,防止生成多个同任务定时器
     *
     * @return
     */
    boolean singleton() default true;
}
2.任务配置单元数据QuartzJobUnit

这个类主要是用来存储job对象的一些信息,方便在QuartzJobManage中使用

public class QuartzJobUnit implements Serializable {

    private static final long serialVersionUID = 7669523527816564621L;

    /**
     * 任务名称
     */
    private String jobName;
    /**
     * 任务分组
     */
    private String jobGroup;
    /**
     * 任务表达式
     */
    private String jobCorn;
    /**
     * 任务描述
     */
    private String description;
    /**
     * 存活时间,单位秒
     */
    private long surviveSecond;
    /**
     * 扩展单元数据
     */
    private Map<String, Object> extraUnit;

    /**
     * 丢失补仓策略
     */
    private int misfireInstruction = CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;
    
	//省略get、set
 }
3.定义Job任务QuartzJob1

这里我们定义一个简单的job任务,名称为QuartzJob1,分组为QuartzJob,设置为每5秒执行一次

@JobUnit(jobName = "QuartzJob1", jobGroup = "QuartzJob", jobCorn = "*/5 * * * * ?", jobDesc = "Quartz学习")
public class QuartzJob1 implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobKey key = jobExecutionContext.getJobDetail().getKey();
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        String jobMsg = jobDataMap.getString("jobMsg");
        String triggerMsg = jobDataMap.getString("triggerMsg");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(jobMsg + "=====" + triggerMsg + sdf.format(new Date()));
    }
}
4.定义调度监听器QuartzCustomSchedulerListener

用来在Scheduler 的生命周期中有关键事件发生时记录日志或者做一些其他事情。

/**
 * 自定义定时任务调度监听器
 * 监控任务情况
 *
 * @Author: yhx
 * @Date: 2019/12/9 23:26 下午
 */
public class QuartzCustomSchedulerListener implements SchedulerListener {

    private static final Logger LOG = LoggerFactory.getLogger(QuartzCustomSchedulerListener.class);

    /**
     * 任务被部署时被执行
     *
     * @param trigger
     */
    @Override
    public void jobScheduled(Trigger trigger) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被部署", trigger.toString());
    }

    /**
     * 任务被卸载时被执行
     *
     * @param triggerKey
     */
    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被卸载", triggerKey.toString());
    }

    /**
     * 任务完成了它的使命,光荣退休时被执行
     *
     * @param trigger
     */
    @Override
    public void triggerFinalized(Trigger trigger) {
        LOG.info("SchedulerListener监听器:任务 [{}] 结束", trigger.toString());
    }

    /**
     * 一个触发器被暂停时被执行
     *
     * @param triggerKey
     */
    @Override
    public void triggerPaused(TriggerKey triggerKey) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被暂停", triggerKey.toString());
    }

    /**
     * 所在组的全部触发器被停止时被执行
     *
     * @param triggerGroup
     */
    @Override
    public void triggersPaused(String triggerGroup) {
        LOG.info("SchedulerListener监听器:任务组 [{}] 被暂停", triggerGroup);

    }

    /**
     * 一个触发器被恢复时被执行
     *
     * @param triggerKey
     */
    @Override
    public void triggerResumed(TriggerKey triggerKey) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被恢复", triggerKey.toString());
    }

    /**
     * 所在组的全部触发器被回复时被执行
     *
     * @param triggerGroup
     */
    @Override
    public void triggersResumed(String triggerGroup) {
        LOG.info("SchedulerListener监听器:任务组 [{}] 被恢复", triggerGroup);
    }

    /**
     * 一个JobDetail被动态添加进来
     *
     * @param jobDetail
     */
    @Override
    public void jobAdded(JobDetail jobDetail) {

    }

    /**
     * 删除时被执行
     *
     * @param jobKey
     */
    @Override
    public void jobDeleted(JobKey jobKey) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被删除", jobKey.toString());
    }

    /**
     * 暂停时被执行
     *
     * @param jobKey
     */
    @Override
    public void jobPaused(JobKey jobKey) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被暂停", jobKey.toString());
    }

    /**
     * 一组任务被暂定时执行
     *
     * @param jobGroup
     */
    @Override
    public void jobsPaused(String jobGroup) {
        LOG.info("SchedulerListener监听器:任务组 [{}] 被暂停", jobGroup);
    }

    /**
     * 恢复时被执行
     *
     * @param jobKey
     */
    @Override
    public void jobResumed(JobKey jobKey) {
        LOG.info("SchedulerListener监听器:任务 [{}] 被恢复", jobKey.toString());
    }

    /**
     * 一组被恢复时执行
     *
     * @param triggerGroup
     */
    @Override
    public void jobsResumed(String triggerGroup) {
        LOG.info("SchedulerListener监听器:任务组 [{}] 被恢复", triggerGroup);
    }

    /**
     * 出现异常时执行
     *
     * @param jobGroup
     * @param e
     */
    @Override
    public void schedulerError(String jobGroup, SchedulerException e) {
        LOG.error("SchedulerListener监听器:jobGroup [{}] deal error: {}", jobGroup, getStackTraceMsg(e.getStackTrace()));
    }

    /**
     * scheduler被设为standBy等候模式时被执行
     */
    @Override
    public void schedulerInStandbyMode() {

    }

    /**
     * scheduler启动时被执行
     */
    @Override
    public void schedulerStarted() {

    }

    /**
     * scheduler正在启动时被执行
     */
    @Override
    public void schedulerStarting() {

    }

    /**
     * scheduler关闭时被执行
     */
    @Override
    public void schedulerShutdown() {

    }

    /**
     * scheduler正在关闭时被执行
     */
    @Override
    public void schedulerShuttingdown() {

    }

    /**
     * scheduler中所有数据包括jobs, triggers和calendars都被清空时被执行
     */
    @Override
    public void schedulingDataCleared() {

    }

    /**
     * 日志栈输出
     *
     * @param stackTraceElements
     * @return
     */
    public String getStackTraceMsg(StackTraceElement[] stackTraceElements) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement stackTraceElem : stackTraceElements) {
            sb.append(stackTraceElem.toString() + "\n");
        }
        return sb.toString();
    }

}
5.定义任务监听器JobMonitorListener

由 Job 在其生命周期中产生的某些关键事件时被调用,可以进行日志记录或者一些操作。

public class JobMonitorListener implements JobListener {

    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    private static final Logger LOGGER = LoggerFactory.getLogger(JobMonitorListener.class);

    @Override
    public String getName() {
        return "JobListenerName";
    }

    /**
     * 定时任务开始执行
     *
     * @param context
     */
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().toString();
        long startTime = System.currentTimeMillis();
        TIME_THREADLOCAL.set(System.currentTimeMillis());
        String executeTime = DateFormatUtils.format(new Date(startTime), "yyyy-MM-dd HH:mm:ss");
        LOGGER.info("Job [{}] is going to start! BeginTime : [{}]", jobName, executeTime);
    }

    /**
     * 定时任务被否决
     *
     * @param context
     */
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        String executeTime = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
        String jobName = context.getJobDetail().getKey().toString();
        LOGGER.warn("Job [{}] is vetoed, DateTime: {}", jobName, executeTime);
    }

    /**
     * 定时任务执行完毕
     *
     * @param context
     * @param jobException
     */
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        long endTime = System.currentTimeMillis();
        long costTime = System.currentTimeMillis() - TIME_THREADLOCAL.get();
        String jobName = context.getJobDetail().getKey().toString();
        String executeTime = DateFormatUtils.format(new Date(endTime), "yyyy-MM-dd HH:mm:ss");
        LOGGER.info("Job [{}] is finished! EndTime: [{}] , Cost time [{}] ms", jobName, executeTime, costTime);
        if (jobException != null && !jobException.getMessage().equals("")) {
            LOGGER.error("job [{}] is execute dateTime [{}] error: {}", jobName, executeTime, getStackTraceMsg(jobException.getStackTrace()));
        }
    }

    /**
     * 日志栈输出
     *
     * @param stackTraceElements
     * @return
     */
    public String getStackTraceMsg(StackTraceElement[] stackTraceElements) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement stackTraceElem : stackTraceElements) {
            sb.append(stackTraceElem.toString() + "\n");
        }
        return sb.toString();
    }
}
6.定义触发器监听器
public class QuartzTriggerListener implements TriggerListener {

    @Override
    public String getName() {
        return "MyTriggerListener";
    }

    /**
     * Trigger被激发 它关联的job即将被运行
     */
    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        System.out.println("Trigger监听器:MyTriggerListener.triggerFired()");
    }

    /**
     * Trigger被激发 它关联的job即将被运行,先执行(1),在执行(2) 如果返回TRUE 那么任务job会被终止
     */
    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        System.out.println("Trigger监听器:MyTriggerListener.vetoJobExecution()");
        return false;
    }

    /**
     * 当Trigger错过被激发时执行,比如当前时间有很多触发器都需要执行,但是线程池中的有效线程都在工作,
     * 那么有的触发器就有可能超时,错过这一轮的触发。
     */
    @Override
    public void triggerMisfired(Trigger trigger) {
        System.out.println("Trigger监听器:MyTriggerListener.triggerMisfired()");
    }

    /**
     * 任务完成时触发
     */
    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
                                Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        System.out.println("Trigger监听器:MyTriggerListener.triggerComplete()");
    }
}
7.项目启动时初始化Scheduler

项目启动时初始化调度器Scheduler,同时注册调度、任务和trigger监听器

@Configuration
public class QuartzConfiguration {
    @Bean(name = "scheduler")
    public Scheduler scheduler() throws Exception {
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 注册监听
        scheduler.getListenerManager().addSchedulerListener(new QuartzCustomSchedulerListener());
        scheduler.getListenerManager().addJobListener(new JobMonitorListener());
        scheduler.getListenerManager().addTriggerListener(new QuartzTriggerListener());
        return scheduler;
    }
}
8.QuartzJobManager管理类

在这个类中,我们根据添加了JobUnit注解的任务来定义任务实例的一些属性特征如名称,组;添加任务数据映射;构建表达式触发器;
最后将任务和触发器一起注册到调度器中。

/**
 * @Author: yhx
 * @Date: 2019/12/9 23:41 下午
 */
@Service
@AutoConfigureAfter(value = {QuartzConfiguration.class})
public class QuartzJobManager {

    private static final Logger LOG = LoggerFactory.getLogger(QuartzJobManager.class);

    @Qualifier(value = "scheduler")
    @Autowired
    private Scheduler scheduler;


    /**
     * 添加任务</br>
     * 同一个任务只能添加一次,时间表达式不一样时会更新定时任务
     *
     * @param jobUnit
     * @param jobClass
     */
    public void addJob(QuartzJobUnit jobUnit, @SuppressWarnings("rawtypes") Class jobClass) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobUnit.getJobName(), jobUnit.getJobGroup());
            if (!scheduler.checkExists(triggerKey)) {
                // 创建JobDetail实例
                JobDetail jobDetail = null;
                // JobDetail 定义任务实例的一些属性特征
                if (StringUtils.isNotBlank(jobUnit.getDescription())) {
                    jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobUnit.getJobName(), jobUnit.getJobGroup()).withDescription(jobUnit.getDescription()).build();
                } else {
                    jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobUnit.getJobName(), jobUnit.getJobGroup()).build();
                }
                // JobDataMap 任务数据映射
                jobDetail.getJobDataMap().put("scheduleJob", jobUnit);
                //表达式调度构建器
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(jobUnit.getJobCorn());
                CronTriggerImpl trigger = (CronTriggerImpl) TriggerBuilder.newTrigger().withIdentity(jobUnit.getJobName(), jobUnit.getJobGroup()).withSchedule(scheduleBuilder).build();
                if (StringUtils.isNotBlank(jobUnit.getDescription())) {
                    trigger.setDescription(jobUnit.getDescription());
                }
                if (jobUnit.getSurviveSecond() > 0L) {
                    trigger.setEndTime(new Date(System.currentTimeMillis() + jobUnit.getSurviveSecond() * 1000));
                }
                trigger.setMisfireInstruction(jobUnit.getMisfireInstruction());
                trigger.setJobDataMap(jobDetail.getJobDataMap());
                // 注册job和调度器
                scheduler.scheduleJob(jobDetail, trigger);
                scheduler.start();
                LOG.info("add quartz job [{}] success!", triggerKey.toString());
            } else {
                CronTriggerImpl trigger = (CronTriggerImpl) scheduler.getTrigger(triggerKey);
                if (!trigger.getCronExpression().equalsIgnoreCase(jobUnit.getJobCorn()) || (StringUtils.isNotBlank(jobUnit.getDescription()) && !jobUnit.getDescription().equals(trigger.getDescription()))) {
                    CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(jobUnit.getJobCorn());
                    trigger = (CronTriggerImpl) trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
                    if (jobUnit.getSurviveSecond() > 0L) {
                        trigger.setEndTime(new Date(System.currentTimeMillis() + jobUnit.getSurviveSecond() * 1000));
                    }
                    if (StringUtils.isNotBlank(jobUnit.getDescription())) {
                        trigger.setDescription(jobUnit.getDescription());
                    }
                    scheduler.rescheduleJob(triggerKey, trigger);
                    LOG.info("update quartz job [{}] success!", triggerKey.toString());
                }
            }
        } catch (SchedulerException e) {
            LOG.error("add quartz job error: {}", e);
        }
    }


    /**
     * 移除任务
     *
     * @param jobUnit
     */
    public void removeJob(QuartzJobUnit jobUnit) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobUnit.getJobName(), jobUnit.getJobGroup());
            scheduler.pauseTrigger(triggerKey);// 停止触发器
            scheduler.unscheduleJob(triggerKey);// 移除触发器
            JobKey jobKey = JobKey.jobKey(jobUnit.getJobName(), jobUnit.getJobGroup());
            scheduler.deleteJob(jobKey);// 删除任务
        } catch (SchedulerException e) {
            //throw new RuntimeException(e);
        }
    }

    /**
     * 停止任务
     *
     * @param jobUnit
     */
    public void pauseJob(QuartzJobUnit jobUnit) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobUnit.getJobName(), jobUnit.getJobGroup());
            scheduler.pauseTrigger(triggerKey);// 停止触发器
        } catch (SchedulerException e) {
            LOG.error("暂定任务失败:", e);
        }
    }

    /**
     * 恢复任务
     *
     * @param jobUnit
     */
    public void resumeJob(QuartzJobUnit jobUnit) {
        try {
            JobKey jobKey = JobKey.jobKey(jobUnit.getJobName(), jobUnit.getJobGroup());
            scheduler.resumeJob(jobKey);// 停止触发器
        } catch (SchedulerException e) {
            //throw new RuntimeException(e);
        }
    }

    /**
     * 开始所有任务
     */
    public void startJobs() {
        try {
            scheduler.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 停止所有任务
     */
    public void shutdownJobs() {
        try {
            if (!scheduler.isShutdown()) {
                scheduler.shutdown();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 立即执行任务
     *
     * @param jobUnit
     */
    public void triggerJob(QuartzJobUnit jobUnit) {
        try {
            JobKey jobKey = JobKey.jobKey(jobUnit.getJobName(), jobUnit.getJobGroup());
            scheduler.triggerJob(jobKey);
        } catch (Exception e) {

        }

    }
}
9.启动类中注册定时任务
@SpringBootApplication
public class QuartzandbatchApplication implements CommandLineRunner {

    private static final Logger LOGGER = LoggerFactory.getLogger(QuartzandbatchApplication.class);

    private static final String QUARTZ_JOB_PKG = "com.yhx.quartzandbatch.quartz.QuartzFrame";

    @Autowired
    private QuartzJobManager quartzJobManager;

    public static void main(String[] args) {
        SpringApplication.run(QuartzandbatchApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        registerQuartzJob();
    }

    /**
     * 注册定时任务
     *
     * @throws Exception
     */
    private void registerQuartzJob() throws Exception {
        LOGGER.info("start to register quartz job...");
        final ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        scanner.addIncludeFilter(new AnnotationTypeFilter(JobUnit.class));
        for (final BeanDefinition resourceBean : scanner.findCandidateComponents(QUARTZ_JOB_PKG)) {
            try {
                final Class resourceClass = getClass().getClassLoader().loadClass(resourceBean.getBeanClassName());
                if (null != resourceClass) {
                    JobUnit jobUnit = (JobUnit) resourceClass.getAnnotation(JobUnit.class);
                    String jobName = jobUnit.jobName();
                    if (StringUtils.isEmpty(jobName)) {
                        jobName = resourceClass.getSimpleName();
                    }
                    QuartzJobUnit quartzJobUnit = new QuartzJobUnit();
                    quartzJobUnit.setJobName(jobName);
                    quartzJobUnit.setJobGroup(jobUnit.jobGroup());
                    quartzJobUnit.setJobCorn(jobUnit.jobCorn());
                    quartzJobUnit.setMisfireInstruction(jobUnit.misfireInstruction());
                    if (!StringUtils.isEmpty(jobUnit.jobDesc())) {
                        quartzJobUnit.setDescription(jobUnit.jobDesc());
                    }
                    quartzJobManager.addJob(quartzJobUnit, resourceClass);
                }
            } catch (final ClassNotFoundException e) {
                LOGGER.error("error to register quartz job bean [{}]", resourceBean.getBeanClassName(), e);
            }
        }
        LOGGER.info("finished register quartz job...");
    }
}
10.当我们运行后发现报错了:

这个因为job任务是交给quartz处理的,而job任务中引入的TestAutowired是spring管理的,他们是2个东西,所以引入的TestAutowired是空。具体原因见第四章

2019-12-10 00:55:00,106 ERROR (JobRunShell.java:211)- Job QuartzJob.QuartzJob1 threw an unhandled Exception: 
java.lang.NullPointerException: null
	at com.yhx.toali.Quartz.QuartzFrame.QuartzJob1.execute(QuartzJob1.java:33)
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
2019-12-10 00:55:00,107 ERROR (QuartzCustomSchedulerListener.java:157)- jobGroup [Job (QuartzJob.QuartzJob1 threw an exception.] deal error: org.quartz.core.JobRunShell.run(JobRunShell.java:213)
org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
11.添加JobFactory
@Component(value = "quartzCustomAdaptableJobFactory")
public class QuartzCustomAdaptableJobFactory extends AdaptableJobFactory {

    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //实例化对象
        Object jobInstance = super.createJobInstance(bundle);
        //进行注入,交给spring管理该bean
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}
12.修改Scheduler的获取方式
@Configuration
@AutoConfigureAfter(value = {QuartzCustomAdaptableJobFactory.class})
public class QuartzConfiguration {
    /*@Bean(name = "scheduler")
    public Scheduler scheduler() throws Exception {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.getListenerManager().addSchedulerListener(new QuartzCustomSchedulerListener());
        scheduler.getListenerManager().addJobListener(new JobMonitorListener());
        scheduler.getListenerManager().addTriggerListener(new QuartzTriggerListener());

        return scheduler;
    }*/

    @Bean(value = "schedulerFactory")
    public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("quartzCustomAdaptableJobFactory") QuartzCustomAdaptableJobFactory quartzCustomAdaptableJobFactory) {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        schedulerFactoryBean.setJobFactory(quartzCustomAdaptableJobFactory);

        return schedulerFactoryBean;
    }

    @Bean(name = "scheduler")
    public Scheduler scheduler(@Qualifier("schedulerFactory") SchedulerFactoryBean schedulerFactory) throws Exception {
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.getListenerManager().addSchedulerListener(new QuartzCustomSchedulerListener());
        scheduler.getListenerManager().addJobListener(new JobMonitorListener());
        scheduler.getListenerManager().addTriggerListener(new QuartzTriggerListener());
        return scheduler;
    }
}
13.注释掉QuartzJobManager中的调度器的启动
 // 注册job和调度器
 scheduler.scheduleJob(jobDetail, trigger);
 // scheduler.start();
14.执行结果:

红框主要是圈出来我主动打的一些日志,包括监听器的记录和job里面执行的内容,以及间隔时间是5秒钟
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值