SpringBoot整合Quartz实现定时任务动态自定义和可视化

最近项目需要使用动态的定时任务,而且还需要在后台进行可视化管理动态任务。这里其实有两个实现思路:第一个就是xxlJob动态任务、第二个就是Quartz框架,这两个的技术路线与框架都是很成熟的技术,都是可以提供动态可支持的定时任务,而且都支持分布式开发。大体的区别就是在于:xxlJob对于分布式和微服务的模式更加的友好,自身就提供了可管理任务的可视化界面,而且配置与使用以及开大起来都是相对便捷一些。Quartz其实也可以用于分布式,但是微服务的话就不推荐使用了。如果你的项目是模块化项目或者是多个项目组成的那就可以使用Quartz这个技术,我个人认为用起来比较舒服,能满足动态定时的基本操作,实现起来也不是很难。接下来就介绍一下springboot怎么整合Quartz实现动态定时任务。

第一步:依然是常规化的导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
        </dependency>

第二步:在.yml文件中增加配置

quartz:
#    job-store-type: jdbc
#    properties:
#      org:
#        quartz:
#          scheduler:
#            instanceName: clusteredScheduler
#            instanceId: AUTO
#          jobStore:
#            selectWithLockSQL: SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?
#            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
#            tablePrefix: QRTZ_
#            isClustered: false # 打开集群配置
#            clusterCheckinInterval: 2000 # 设置集群检查间隔20s
#            useProperties: false
#          threadPool:
#            class: org.quartz.simpl.SimpleThreadPool
#            threadCount: 10
#            threadPriority: 5
#            threadsInheritContextClassLoaderOfInitializingThread: true

导入之后把注释去掉,其他的都不用改,需要注意一点tablePrefix这个配置,这个是Quartz需要用到的表的表名前缀,最好不要改,当前你想改也是可以的,但是改起来会很麻烦,要改很多的文件。

第三步:导入需要用到的表(直接从资源里面下载,导入到数据库中即可)

简单介绍一下表的作用

qrtz开头的表示Quartz动态任务调用时的必须要用到的表,千万不要随意增减字段,有调度任务表,时间表达式存储表,正在运行任务的表等等。下面的这个sys_schedule表,里面的字段不要删,但是你可以增加你想要的字段。这里面记录了你自己系统里面发起任务的数据。相当于前端form表单提交的任务数据就会保存到这个表里面,导入这个表后,使用工具生成最基本的实体类,controller类等基本类,跟平时开发是一样的。但需要特别注意的是,我们需要设置数据库忽略大小写的配置,在mysql配置文件中增加一个属性就行,网上很多。

第四步:代码配置Task任务:

@DisallowConcurrentExecution
public abstract class Task implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        run();
        System.out.println("任务在" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + " 时运行");
    }


    public abstract void run();
}

这是一个抽象类,实现了Job任务类,当前端调用你的controller层方法时,任务触发会先执行这个execute这个方法,这个方法里面会先用这个run方法,关键点就是这个run的抽象方法是我们自己定义的。以后你想实现什么定时任务,就需要要继承这个Task类并且实现这个run方法,就可调用你的代码了,例如下面:

@DisallowConcurrentExecution
public class TestSchedule extends Task {

    @Override
    public void run() {
        System.out.println("=============定时任务被调用了====================");
    }


}

在这个实现了父类的run方法里面写你的具体逻辑。每写一个定时任务类,你都要继承这个Task类,实现run方法。

第五步:核心任务调度的contorller与services层的调用任务代码,直接粘进行用即可。

 /**
     * 暂停任务
     */
    @PostMapping("stop")
    public ResponseJson stop(SysSchedule scheduleJob) {
        sysScheduleService.stopJob(scheduleJob);
        return buildSuccessResult("暂停成功!");
    }


    /**
     * 立即运行一次
     */
    @PostMapping("startNow")
    public ResponseJson stratNow(SysSchedule scheduleJob) {
        sysScheduleService.startNowJob(scheduleJob);
        return buildSuccessResult("运行成功");
    }

    /**
     * 恢复
     */
    @PostMapping("resume")
    public ResponseJson resume(SysSchedule scheduleJob, RedirectAttributes redirectAttributes) {
        sysScheduleService.restartJob(scheduleJob);
        //恢复之后,立即触发一次激活定时任务,不然定时任务有可能不会执行
        sysScheduleService.startNowJob(scheduleJob);
        return buildSuccessResult("恢复成功");
    }

    /**
     * 批量删除定时任务
     */
    @DeleteMapping("delete")
    public ResponseJson deleteAll(String ids, RedirectAttributes redirectAttributes) {
        String idArray[] = ids.split(",");
        for (String id : idArray) {
            sysScheduleService.deleteByIds(sysScheduleService.getById(id));
        }
        return buildSuccessResult("删除成功");
    }
@Transactional(readOnly = false)
    @Override
    public void stopJob(SysSchedule SysSchedule) {
        JobKey key = new JobKey(SysSchedule.getName(), SysSchedule.getSchGroup());
        try {
            scheduler.pauseJob(key);
            SysSchedule.setStatus("0");
            super.updateById(SysSchedule);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Transactional(readOnly = false)
    @Override
    public void startNowJob(SysSchedule SysSchedule) {
        JobKey key = new JobKey(SysSchedule.getName(), SysSchedule.getSchGroup());
        try {
            scheduler.triggerJob(key);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }


    /**
     * 恢复任务
     */
    @Transactional(readOnly = false)
    @Override
    public void restartJob(SysSchedule SysSchedule) {
        JobKey key = new JobKey(SysSchedule.getName(), SysSchedule.getSchGroup());
        try {
            scheduler.resumeJob(key);
            SysSchedule.setStatus("1");
            super.updateById(SysSchedule);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Transactional(readOnly = false)
    @Override
    public void saveJob(SysSchedule SysSchedule) {
        SysSchedule t = this.getById(SysSchedule.getId());
        if (Objects.nonNull(t)) {
            JobKey key = new JobKey(t.getName(), t.getSchGroup());
            try {
                scheduler.deleteJob(key);
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
        this.add(SysSchedule);
        super.save(SysSchedule);
    }

    @Override
    public void deleteByIds(SysSchedule scheduleJob) {
        JobKey key = new JobKey(scheduleJob.getName(), scheduleJob.getSchGroup());
        try {
            scheduler.deleteJob(key);
            super.removeById(scheduleJob);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }

    }
@Transactional(readOnly = false)
    public void add(SysSchedule SysSchedule) {
        Class job = null;
        try {
            job = Class.forName(SysSchedule.getClassname());
        } catch (ClassNotFoundException e1) {
            e1.printStackTrace();
        }
        JobDetail jobDetail = JobBuilder.newJob(job).withIdentity(SysSchedule.getName(), SysSchedule.getSchGroup())
                .build();
        jobDetail.getJobDataMap().put("SysSchedule", SysSchedule);

        // 表达式调度构建器(可判断创建SimpleScheduleBuilder)
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(SysSchedule.getExpression());

        // 按新的cronExpression表达式构建一个新的trigger
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(SysSchedule.getName(), SysSchedule.getSchGroup()).withSchedule(scheduleBuilder).build();
        try {
            scheduler.scheduleJob(jobDetail, trigger);
            JobKey key = new JobKey(SysSchedule.getName(), SysSchedule.getSchGroup());
            if (SysSchedule.getStatus().equals("0")) {
                scheduler.pauseJob(key);
            } else {
                scheduler.resumeJob(key);
            }

        } catch (SchedulerException e) {
            e.printStackTrace();
        }

    }

    /**
     * 获取所有JobDetail
     *
     * @return 结果集合
     */
    public List<JobDetail> getJobs() {
        try {
            GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
            Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
            List<JobDetail> jobDetails = new ArrayList<JobDetail>();
            for (JobKey key : jobKeys) {
                jobDetails.add(scheduler.getJobDetail(key));
            }
            return jobDetails;
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取所有计划中的任务
     *
     * @return 结果集合
     */
    public List<SysSchedule> getAllSysSchedule() {
        List<SysSchedule> SysScheduleList = new ArrayList<SysSchedule>();
        ;
        GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
        try {
            Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
            for (JobKey jobKey : jobKeys) {
                List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                for (Trigger trigger : triggers) {
                    SysSchedule SysSchedule = new SysSchedule();
                    SysSchedule.setName(jobKey.getName());
                    SysSchedule.setSchGroup(jobKey.getGroup());
                    Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                    SysSchedule.setStatus(triggerState.name());
                    //获取要执行的定时任务类名
                    JobDetail jobDetail = scheduler.getJobDetail(jobKey);
                    SysSchedule.setClassname(jobDetail.getJobClass().getName());
                    //判断trigger
                    if (trigger instanceof SimpleTrigger) {
                        SimpleTrigger simple = (SimpleTrigger) trigger;
                        SysSchedule.setExpression("重复次数:" + (simple.getRepeatCount() == -1 ?
                                "无限" : simple.getRepeatCount()) + ",重复间隔:" + (simple.getRepeatInterval() / 1000L));
                        SysSchedule.setDescription(simple.getDescription());
                    }
                    if (trigger instanceof CronTrigger) {
                        CronTrigger cron = (CronTrigger) trigger;
                        SysSchedule.setExpression(cron.getCronExpression());
                        SysSchedule.setDescription(cron.getDescription() == null ? ("触发器:" + trigger.getKey()) : cron.getDescription());
                    }
                    SysScheduleList.add(SysSchedule);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return SysScheduleList;
    }

    /**
     * 获取所有运行中的任务
     *
     * @return 结果集合
     */
    public List<SysSchedule> getAllRuningSysSchedule() {
        List<SysSchedule> SysScheduleList = null;
        try {
            List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
            SysScheduleList = new ArrayList<SysSchedule>(executingJobs.size());
            for (JobExecutionContext executingJob : executingJobs) {
                SysSchedule SysSchedule = new SysSchedule();
                JobDetail jobDetail = executingJob.getJobDetail();
                JobKey jobKey = jobDetail.getKey();
                Trigger trigger = executingJob.getTrigger();
                SysSchedule.setName(jobKey.getName());
                SysSchedule.setSchGroup(jobKey.getGroup());
                //SysSchedule.setDescription("触发器:" + trigger.getKey());
                Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                SysSchedule.setStatus(triggerState.name());
                //获取要执行的定时任务类名
                SysSchedule.setClassname(jobDetail.getJobClass().getName());
                //判断trigger
                if (trigger instanceof SimpleTrigger) {
                    SimpleTrigger simple = (SimpleTrigger) trigger;
                    SysSchedule.setExpression("重复次数:" + (simple.getRepeatCount() == -1 ?
                            "无限" : simple.getRepeatCount()) + ",重复间隔:" + (simple.getRepeatInterval() / 1000L));
                    SysSchedule.setDescription(simple.getDescription());
                }
                if (trigger instanceof CronTrigger) {
                    CronTrigger cron = (CronTrigger) trigger;
                    SysSchedule.setExpression(cron.getCronExpression());
                    SysSchedule.setDescription(cron.getDescription());
                }
                SysScheduleList.add(SysSchedule);
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return SysScheduleList;
    }

    /**
     * 获取所有的触发器
     *
     * @return 结果集合
     */
    public List<SysSchedule> getTriggersInfo() {
        try {
            GroupMatcher<TriggerKey> matcher = GroupMatcher.anyTriggerGroup();
            Set<TriggerKey> Keys = scheduler.getTriggerKeys(matcher);
            List<SysSchedule> triggers = new ArrayList<SysSchedule>();

            for (TriggerKey key : Keys) {
                Trigger trigger = scheduler.getTrigger(key);
                SysSchedule SysSchedule = new SysSchedule();
                SysSchedule.setName(trigger.getJobKey().getName());
                SysSchedule.setSchGroup(trigger.getJobKey().getGroup());
                SysSchedule.setStatus(scheduler.getTriggerState(key) + "");
                if (trigger instanceof SimpleTrigger) {
                    SimpleTrigger simple = (SimpleTrigger) trigger;
                    SysSchedule.setExpression("重复次数:" + (simple.getRepeatCount() == -1 ?
                            "无限" : simple.getRepeatCount()) + ",重复间隔:" + (simple.getRepeatInterval() / 1000L));
                    SysSchedule.setDescription(simple.getDescription());
                }
                if (trigger instanceof CronTrigger) {
                    CronTrigger cron = (CronTrigger) trigger;
                    SysSchedule.setExpression(cron.getCronExpression());
                    SysSchedule.setDescription(cron.getDescription());
                }
                triggers.add(SysSchedule);
            }
            return triggers;
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return null;
    }

第六步:开发可视化界面,这个界面以及表单就是根据sys_schedule这个表生成的。具体的我把图贴出来供大家参考:

具体界面随开发进行变化,但是基本字段就这样,如果你还想加字段,也可以加。表单中的定时规则大家可以再往上找一个cron表达式的生成页面,让用户去选,然后生成表达式。在controller中,调用每一个任务时,参数都是SysSchedule这个实体类,把数据中的每个字段都让前端传到后端接收,后端只需要调用相应的任务方法即可。

当你测试或者保存的任务的时候,sys_schedule这个表就会多一条任务数据,相应的qrtz相关表里也会多出数据,测试是否集成成功了,你就测试新增成功后qrtz相关表里是否多了相应数据以及表达式的数据。

如果有什么问题,可以留言一起讨论

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是 SpringBoot 搭配 Quartz 实现动态定时任务的源码: 1. 首先,我们需要引入 QuartzSpringBoot 的依赖: ```xml <!-- Quartz相关依赖 --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz-jobs</artifactId> <version>2.3.2</version> </dependency> <!-- SpringBoot相关依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> ``` 2. 创建定时任务实体类,用于封装定时任务的信息,包括任务名称、任务组、任务类名、任务状态(是否启用)、任务表达式等: ```java @Entity @Table(name = "job_task") @Data public class JobTask implements Serializable { private static final long serialVersionUID = 1L; /** * ID */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * 任务名称 */ @NotBlank(message = "任务名称不能为空") private String name; /** * 任务分组 */ @NotBlank(message = "任务分组不能为空") private String group; /** * 任务类名 */ @NotBlank(message = "任务类名不能为空") private String className; /** * 任务状态,0:禁用,1:启用 */ @NotNull(message = "任务状态不能为空") private Integer status; /** * 任务表达式 */ @NotBlank(message = "任务表达式不能为空") private String cronExpression; /** * 创建时间 */ private LocalDateTime createTime; /** * 最后一次修改时间 */ private LocalDateTime updateTime; } ``` 3. 创建定时任务的服务类,用于管理定时任务的增删改查等操作,同时也需要实现 `InitializingBean` 接口,在启动应用时加载已存在的定时任务: ```java @Service @AllArgsConstructor public class JobTaskService implements InitializingBean { private final Scheduler scheduler; private final JobTaskRepository jobTaskRepository; /** * 添加任务 * @param jobTask * @return * @throws Exception */ public boolean addJobTask(JobTask jobTask) throws Exception { if (jobTask == null || StringUtils.isBlank(jobTask.getCronExpression())) { return false; } if (StringUtils.isBlank(jobTask.getName()) || StringUtils.isBlank(jobTask.getClassName())) { throw new Exception("任务名称或任务类名不能为空"); } // 判断任务是否已存在 JobKey jobKey = JobKey.jobKey(jobTask.getName(), jobTask.getGroup()); if (scheduler.checkExists(jobKey)) { return false; } // 构建任务实例 JobDetail jobDetail = JobBuilder.newJob(getClass(jobTask.getClassName()).getClass()) .withIdentity(jobTask.getName(), jobTask.getGroup()) .build(); // 构建任务触发器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobTask.getCronExpression()); CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity(jobTask.getName(), jobTask.getGroup()) .withSchedule(cronScheduleBuilder) .build(); // 注册任务和触发器 scheduler.scheduleJob(jobDetail, trigger); // 如果任务状态为启用,则立即启动任务 if (jobTask.getStatus() == 1) { scheduler.triggerJob(jobKey); } // 保存任务信息 jobTask.setCreateTime(LocalDateTime.now()); jobTask.setUpdateTime(LocalDateTime.now()); jobTaskRepository.save(jobTask); return true; } /** * 修改任务 * @param jobTask * @return * @throws Exception */ public boolean modifyJobTask(JobTask jobTask) throws Exception { if (jobTask == null || StringUtils.isBlank(jobTask.getCronExpression())) { return false; } if (StringUtils.isBlank(jobTask.getName()) || StringUtils.isBlank(jobTask.getClassName())) { throw new Exception("任务名称或任务类名不能为空"); } // 判断任务是否存在 JobKey jobKey = JobKey.jobKey(jobTask.getName(), jobTask.getGroup()); if (!scheduler.checkExists(jobKey)) { return false; } // 修改任务触发器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobTask.getCronExpression()); CronTrigger newTrigger = TriggerBuilder.newTrigger() .withIdentity(jobTask.getName(), jobTask.getGroup()) .withSchedule(cronScheduleBuilder) .build(); scheduler.rescheduleJob(TriggerKey.triggerKey(jobTask.getName(), jobTask.getGroup()), newTrigger); // 修改任务信息 JobTask oldJobTask = jobTaskRepository.findByNameAndGroup(jobTask.getName(), jobTask.getGroup()); oldJobTask.setClassName(jobTask.getClassName()); oldJobTask.setStatus(jobTask.getStatus()); oldJobTask.setCronExpression(jobTask.getCronExpression()); oldJobTask.setUpdateTime(LocalDateTime.now()); jobTaskRepository.save(oldJobTask); return true; } /** * 删除任务 * @param name * @param group * @return * @throws Exception */ public boolean deleteJobTask(String name, String group) throws Exception { JobKey jobKey = JobKey.jobKey(name, group); if (!scheduler.checkExists(jobKey)) { return false; } scheduler.deleteJob(jobKey); jobTaskRepository.deleteByNameAndGroup(name, group); return true; } /** * 获取所有任务 * @return */ public List<JobTask> getAllJobTask() { return jobTaskRepository.findAll(); } /** * 根据任务名称和分组获取任务信息 * @param name * @param group * @return */ public JobTask getJobTaskByNameAndGroup(String name, String group) { return jobTaskRepository.findByNameAndGroup(name, group); } /** * 获取任务类实例 * @param className * @return * @throws Exception */ private Object getClass(String className) throws Exception { Class<?> clazz = Class.forName(className); return clazz.newInstance(); } /** * 实现 InitializingBean 接口,在启动应用时加载已存在的定时任务 * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { List<JobTask> jobTaskList = jobTaskRepository.findAll(); for (JobTask jobTask : jobTaskList) { if (jobTask.getStatus() == 1) { addJobTask(jobTask); } } } } ``` 4. 创建定时任务的控制器类,用于处理新增、修改、删除等请求: ```java @RestController @AllArgsConstructor @RequestMapping("/job") public class JobTaskController { private final JobTaskService jobTaskService; /** * 添加任务 * @param jobTask * @return * @throws Exception */ @PostMapping public ResponseEntity addJobTask(@RequestBody JobTask jobTask) throws Exception { boolean result = jobTaskService.addJobTask(jobTask); return result ? ResponseEntity.ok("任务添加成功") : ResponseEntity.badRequest().body("任务添加失败"); } /** * 修改任务 * @param jobTask * @return * @throws Exception */ @PutMapping public ResponseEntity modifyJobTask(@RequestBody JobTask jobTask) throws Exception { boolean result = jobTaskService.modifyJobTask(jobTask); return result ? ResponseEntity.ok("任务修改成功") : ResponseEntity.badRequest().body("任务修改失败"); } /** * 删除任务 * @param name * @param group * @return * @throws Exception */ @DeleteMapping("/{name}/{group}") public ResponseEntity deleteJobTask(@PathVariable String name, @PathVariable String group) throws Exception { boolean result = jobTaskService.deleteJobTask(name, group); return result ? ResponseEntity.ok("任务删除成功") : ResponseEntity.badRequest().body("任务删除失败"); } /** * 获取所有任务 * @return */ @GetMapping public ResponseEntity getAllJobTask() { List<JobTask> jobTaskList = jobTaskService.getAllJobTask(); return ResponseEntity.ok(jobTaskList); } /** * 根据任务名称和分组获取任务信息 * @param name * @param group * @return */ @GetMapping("/{name}/{group}") public ResponseEntity getJobTaskByNameAndGroup(@PathVariable String name, @PathVariable String group) { JobTask jobTask = jobTaskService.getJobTaskByNameAndGroup(name, group); return ResponseEntity.ok(jobTask); } } ``` 5. 创建定时任务的启动类,用于启动 SpringBoot 应用: ```java @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } /** * 注册定时任务调度器 * @return * @throws SchedulerException */ @Bean public SchedulerFactoryBean schedulerFactoryBean() throws SchedulerException { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); Properties properties = new Properties(); properties.put("org.quartz.scheduler.instanceName", "ChitGPTScheduler"); properties.put("org.quartz.threadPool.threadCount", "10"); schedulerFactoryBean.setQuartzProperties(properties); schedulerFactoryBean.setStartupDelay(5); return schedulerFactoryBean; } /** * 注册定时任务实例 * @return */ @Bean public Scheduler scheduler() { return schedulerFactoryBean().getScheduler(); } } ``` 以上就是 SpringBoot 搭配 Quartz 实现动态定时任务的源码,希望能对您有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值