quartz配合springboot使用的坑与记录

公司项目需要用到任务调度,所以用上了quartz。

目前是试过两种spring版本的集成,1.5.9跟2.0.3版本,有所区别。

1.5的spring并没有提供专门的start启动类,所以很多东西都要自己手动写,而2.0提供有start,方便很多。

quartz工作原理:由schedule来调度一个任务,而任务由trigger和jobdetail组成,jobdetail调用job来实现业务规则,这里借用一张图:

通常来讲,一个jobdetail和一个trigger是对应的,他们将设定同一个group名。trigger有简单trigger,也有cronTrigger,作用是指定触发规则(触发几次、间隔多少等)。而jobdetail用来指定一个job和部分调度规则,我们要把我们实现的业务规则写到job里面。jobdetail还有一个作用是给job传递一些参数(坑1:遗憾的是只有基础类型,你无法传递一个Object)。

坑2:很蛋疼的是,schedule的生成并不参与spring的bean生命周期管理,这意味着你无法在job里写业务规则的时候使用@autowire!为此,spring提供了专门的工厂类来解决这个问题。

除此之外,quartz还未trigger、job、schedule提供了响应的监听器,让你可以对他们的生命周期做出响应的处理,比如说一个任务触发前该做什么,触发后/结束后该做什么。

如果你将quartz设置为持久化到数据库中,那么以上的设定都是可以持久化的(坑3:低版本quartz的job分为静态非静态,静态job的jobdetail数据无法持久化到库中),除了监听器。

想让监听器在服务重启后一样有效的解决方法也很简单,配置一个全局的监听类就可以了,在监听类中可以用group来区别不同的调度类型,做出不同的处理。

坑4:cron类型的触发和简单触发似乎完全区别开,简单触发可以指定触发次数,而cron规则不能指定次数,要指定触发次数,似乎只能手动计数,然后在监听器中手动停掉。

 

首先是2.0的start集成方式:

yml配置

spring:
  quartz:
    #相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            instanceName: quartzScheduler
            instanceId: AUTO
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true

这个配置并没有实现持久化,也没有实现集群,是最简单的配置。

job需要集成QuartzJobBean

public class MyJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("任务启动");
    }
}

最后需要一个配置类QuartzConfiguration

@Configuration
public class QuartzConfiguration {
    @Bean
    public JobDetail myJobDetail() {
        return JobBuilder.newJob(MyJob.class).withIdentity("myJob").storeDurably().build();
    }

    // 把jobDetail注册到trigger上去
    @Bean
    public Trigger myJobTrigger() {
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(15).repeatForever();

        return TriggerBuilder.newTrigger()
                .forJob(myJobDetail())
                .withIdentity("myJobTrigger")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

非常简洁,你不需要手动去启动调度,只需要像这样,把job、trigger注入,就可以使用了。

 

 

1.5的使用就繁琐得多了:

org.quartz.scheduler.instanceName=DefaultQuartzScheduler
#调度名称,全局可以有多个scheduler对象
org.quartz.threadPool.
class=org.quartz.simpl.SimpleThreadPool #线程池实现类,可以自定义,org.quartz.simpl.SimpleThreadPool是quartz自带的。
org.quartz.threadPool.threadCount
=20 #可用于并发执行作业的线程数
org.quartz.threadPool.threadPriority
=5 #线程权限值,1-10,默认为5
org.quartz.jobStore.misfireThreshold
=60000 #用来设置调度引擎对触发器超时的忍耐时间,有可能任务触发时线程池已满或者调度挂了,但不超过这个时间就不算超时。
org.quartz.jobStore.
class= org.quartz.impl.jdbcjobstore.JobStoreTX #指定使用的JobStore,有org.quartz.simpl.RAMJobStore和org.quartz.impl.jdbcjobstore.JobStoreTX,前者代表数据存于内存中,后者代表持久化到数据库。
org.quartz.jobStore.useProperties
=false #若为true,则JobDataMaps中的所有值都将是String类型,false就可以使用所有基本类型。
org.quartz.jobStore.driverDelegateClass
= org.quartz.impl.jdbcjobstore.oracle.OracleDelegate #不同的数据库对应不同的DriverDelegate
org.quartz.jobStore.tablePrefix
=qrtz_ #表前缀,这将体现在数据库表名上
org.quartz.jobStore.dataSource
=qzDS #设置JobStore应该使用哪个DataSource,这对应着dataSource后面的那个属性名 org.quartz.dataSource.qzDS.driver= oracle.jdbc.OracleDriver org.quartz.dataSource.qzDS.URL= jdbc:oracle:thin:@192.168.1.1:1521/orclpdb org.quartz.dataSource.qzDS.user= admin org.quartz.dataSource.qzDS.password= admin

#设置数据库的各项信息
org.quartz.triggerListener.NAME.class = com.ly.cloud.datacollection.quartz.Listener.LoopTaskTriggerListener
#设置监听器,除此之外还有SchedulerListeners、JobListener可以配

这里要注意,这样配置依旧是未生效的,你必须手动加载配置。

一般来说,scheduler可以new,也可以使用quartz的全局scheduler。

Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); 

这里要使用配置生成,我们使用spring来进行配置读取和bean注入。可以在spring启动的时候干这个事,创建一个ApplicationStartQuartzJobListener,来监听spring生命周期,当启动的时候进行scheduler的注入。

@Configuration
@Slf4j
public class ApplicationStartQuartzJobListener implements ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private QuartzUtils quartzUtils;

    @Autowired
    private LoopTaskMapper loopTaskMapper;

    @Autowired
    private OrdinaryTaskMapper ordinaryTaskMapper;

    /**
     * 初始启动quartz
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        
    }

    /**
     * 初始注入scheduler
     *
     * @return
     * @throws SchedulerException
     */
    @Bean
    public Scheduler scheduler(SchedulerFactoryBean schedulerFactoryBean) throws SchedulerException {
        return schedulerFactoryBean.getScheduler();
    }

    @Bean
    public MyJobFactory jobFactory() {
        return new MyJobFactory();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(MyJobFactory jobFactory) throws IOException {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        schedulerFactoryBean.setJobFactory(jobFactory);
        schedulerFactoryBean.setQuartzProperties(quartzProperties());
        return schedulerFactoryBean;
    }

    @Bean
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        // 在quartz.properties中的属性被读取并注入后再初始化对象
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }
}

前面说过,job里是无法使用@autowire的,解决方法是使用spring提供的SpringBeanJobFactory类。所以我们还得新建这个。

public class MyJobFactory extends SpringBeanJobFactory {

    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //调用父类的方法
        Object jobInstance = super.createJobInstance(bundle);
        //进行注入
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance; } }

 

这样,我们在其他地方就可以用@autowire来取这个scheduler了。

    @Autowired
    private Scheduler scheduler;

 

调度的创建使用大体都是一样的,trigger、jobDetail、job、listener。

        String cronRuler = "* * 4 * ? *";
        Date taskStartDate = new Date();
        
        JobDetail jobDetail = JobBuilder
                .newJob(MyJob.class)
                .withIdentity("123456", "jobGrounp")
                .usingJobData("name", "张三")
                .build();
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronRuler);
        
        CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("123456", "jobGrounp")
            .withSchedule(cronScheduleBuilder)
            .startAt(taskStartDate)
            .build();    
        scheduler.scheduleJob(jobDetail, cronTrigger);
        scheduler.start();

usingJobData("name", "张三") 可以用于将必要的变量(基础类型)传递到job中使用。

JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap();

String name= dataMap.getString("name");

在job中取触发时间:

Date date = jobExecutionContext.getFireTime();

在job中再次存取变量:

            dataMap.put("fireTimes", fireTimes);
            jobExecutionContext.getJobDetail().getJobBuilder().setJobData(dataMap);

监听器的创建也差不多,继承实现就好了,但是此处有三个坑,监听器必须由一个getName属性返回一个名字,必须有一个无参构造函数,另外全局的listener一样无法通过@autowire注入,因为它不参与spring bean的管理!

想要注入只能用ApplicationContext了。

@Component
@Slf4j
public class MyTriggerListener implements TriggerListener {

    // 监听器的名字,必须有个名字
    public static final String LISTENER_NAME = "MyTriggerListener";
    
    public MyTriggerListener() {

    }

    @Override
    public String getName() {
        return LISTENER_NAME;
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {

    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        return false;
    }

    @Override
    @Transactional
    public void triggerMisfired(Trigger trigger) {
    
    }

    @Override
    @Transactional
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
            CompletedExecutionInstruction triggerInstructionCode) {
            
    }

}

推荐是写一个QuartzUtill来管理scheduler的启动、销毁等操作。cron表达式建议去https://www.pppet.net/撸一下,以确保准确性。

 

 

oracle建表语句,并不是所有的表都用到,看你使用的场景,可以择选。

DROP TABLE  QRTZ_FIRED_TRIGGERS;
DROP TABLE QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE QRTZ_SCHEDULER_STATE;
DROP TABLE QRTZ_LOCKS;
DROP TABLE QRTZ_SIMPLE_TRIGGERS;
DROP TABLE QRTZ_SIMPROP_TRIGGERS;
DROP TABLE QRTZ_CRON_TRIGGERS;
DROP TABLE QRTZ_BLOB_TRIGGERS;
DROP TABLE QRTZ_TRIGGERS;
DROP TABLE QRTZ_JOB_DETAILS;
DROP TABLE QRTZ_CALENDARS;
 
 
-- 存储每一个已配置的 Job 的详细信息
CREATE TABLE qrtz_job_details
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  JOB_NAME  VARCHAR2(200) NOT NULL,
  JOB_GROUP VARCHAR2(200) NOT NULL,
  DESCRIPTION VARCHAR2(250) NULL,
  JOB_CLASS_NAME   VARCHAR2(250) NOT NULL,
  IS_DURABLE VARCHAR2(1) NOT NULL,
  IS_NONCONCURRENT VARCHAR2(1) NOT NULL,
  IS_UPDATE_DATA VARCHAR2(1) NOT NULL,
  REQUESTS_RECOVERY VARCHAR2(1) NOT NULL,
  JOB_DATA BLOB NULL,
  CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
--  存储已配置的 Trigger 的信息
CREATE TABLE qrtz_triggers
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_NAME VARCHAR2(200) NOT NULL,
  TRIGGER_GROUP VARCHAR2(200) NOT NULL,
  JOB_NAME  VARCHAR2(200) NOT NULL,
  JOB_GROUP VARCHAR2(200) NOT NULL,
  DESCRIPTION VARCHAR2(250) NULL,
  NEXT_FIRE_TIME NUMBER(13) NULL,
  PREV_FIRE_TIME NUMBER(13) NULL,
  PRIORITY NUMBER(13) NULL,
  TRIGGER_STATE VARCHAR2(16) NOT NULL,
  TRIGGER_TYPE VARCHAR2(8) NOT NULL,
  START_TIME NUMBER(13) NOT NULL,
  END_TIME NUMBER(13) NULL,
  CALENDAR_NAME VARCHAR2(200) NULL,
  MISFIRE_INSTR NUMBER(2) NULL,
  JOB_DATA BLOB NULL,
  CONSTRAINT QRTZ_TRIGGERS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
  CONSTRAINT QRTZ_TRIGGER_TO_JOBS_FK FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
  REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);
-- 存储简单的 Trigger,包括重复次数,间隔,以及已触的次数
CREATE TABLE qrtz_simple_triggers
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_NAME VARCHAR2(200) NOT NULL,
  TRIGGER_GROUP VARCHAR2(200) NOT NULL,
  REPEAT_COUNT NUMBER(7) NOT NULL,
  REPEAT_INTERVAL NUMBER(12) NOT NULL,
  TIMES_TRIGGERED NUMBER(10) NOT NULL,
  CONSTRAINT QRTZ_SIMPLE_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
  CONSTRAINT QRTZ_SIMPLE_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
  REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
-- 存储 Cron Trigger,包括 Cron 表达式和时区信息
CREATE TABLE qrtz_cron_triggers
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_NAME VARCHAR2(200) NOT NULL,
  TRIGGER_GROUP VARCHAR2(200) NOT NULL,
  CRON_EXPRESSION VARCHAR2(120) NOT NULL,
  TIME_ZONE_ID VARCHAR2(80),
  CONSTRAINT QRTZ_CRON_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
  CONSTRAINT QRTZ_CRON_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
  REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_simprop_triggers
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_NAME VARCHAR2(200) NOT NULL,
  TRIGGER_GROUP VARCHAR2(200) NOT NULL,
  STR_PROP_1 VARCHAR2(512) NULL,
  STR_PROP_2 VARCHAR2(512) NULL,
  STR_PROP_3 VARCHAR2(512) NULL,
  INT_PROP_1 NUMBER(10) NULL,
  INT_PROP_2 NUMBER(10) NULL,
  LONG_PROP_1 NUMBER(13) NULL,
  LONG_PROP_2 NUMBER(13) NULL,
  DEC_PROP_1 NUMERIC(13,4) NULL,
  DEC_PROP_2 NUMERIC(13,4) NULL,
  BOOL_PROP_1 VARCHAR2(1) NULL,
  BOOL_PROP_2 VARCHAR2(1) NULL,
  CONSTRAINT QRTZ_SIMPROP_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
  CONSTRAINT QRTZ_SIMPROP_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
  REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
-- Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,<span style="color:#800080;">JobStore</span> 并不知道如何存储实例的时候)
CREATE TABLE qrtz_blob_triggers
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_NAME VARCHAR2(200) NOT NULL,
  TRIGGER_GROUP VARCHAR2(200) NOT NULL,
  BLOB_DATA BLOB NULL,
  CONSTRAINT QRTZ_BLOB_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
  CONSTRAINT QRTZ_BLOB_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
  REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
-- 以 Blob 类型存储 Quartz 的 Calendar 信息
CREATE TABLE qrtz_calendars
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  CALENDAR_NAME  VARCHAR2(200) NOT NULL,
  CALENDAR BLOB NOT NULL,
  CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
-- 存储已暂停的 Trigger 组的信息
CREATE TABLE qrtz_paused_trigger_grps
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  TRIGGER_GROUP  VARCHAR2(200) NOT NULL,
  CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
-- 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息
CREATE TABLE qrtz_fired_triggers
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  ENTRY_ID VARCHAR2(95) NOT NULL,
  TRIGGER_NAME VARCHAR2(200) NOT NULL,
  TRIGGER_GROUP VARCHAR2(200) NOT NULL,
  INSTANCE_NAME VARCHAR2(200) NOT NULL,
  FIRED_TIME NUMBER(13) NOT NULL,
  SCHED_TIME NUMBER(13) NOT NULL,
  PRIORITY NUMBER(13) NOT NULL,
  STATE VARCHAR2(16) NOT NULL,
  JOB_NAME VARCHAR2(200) NULL,
  JOB_GROUP VARCHAR2(200) NULL,
  IS_NONCONCURRENT VARCHAR2(1) NULL,
  REQUESTS_RECOVERY VARCHAR2(1) NULL,
  CONSTRAINT QRTZ_FIRED_TRIGGER_PK PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
-- 存储少量的有关 Scheduler 的状态信息,和别的 Scheduler 实例(假如是用于一个集群中)
CREATE TABLE qrtz_scheduler_state
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  INSTANCE_NAME VARCHAR2(200) NOT NULL,
  LAST_CHECKIN_TIME NUMBER(13) NOT NULL,
  CHECKIN_INTERVAL NUMBER(13) NOT NULL,
  CONSTRAINT QRTZ_SCHEDULER_STATE_PK PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
-- 存储程序的悲观锁的信息(假如使用了悲观锁)
CREATE TABLE qrtz_locks
(
  SCHED_NAME VARCHAR2(120) NOT NULL,
  LOCK_NAME  VARCHAR2(40) NOT NULL,
  CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
 
create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP);
 
create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME);
create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP);
create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE);
create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
 
create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
 
create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP);

 

 

引用:

Quartz官方文档

Quartz Scheduler misfireThreshold属性的意义与触发器超时后的处理策略

Quartz任务持久化和配置管理

任务调度框架quartz使用总结(异常处理,解决恢复后多次调度处理)

SpringBoot集成Quartz动态定时任务

 

转载于:https://www.cnblogs.com/chrisweiii/p/11380300.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值