quartz定时任务,两张表实现数据持久化

描述
  • 系统需要使用到定时任务去完成一些数据处理的工作。
  • 希望有一个页面可以控制任务的执行计划,并且记录每次执行的结果,如有异常则记录日志到数据库中。
  • quartz官网给出的需要11张表来协同完成,实际中并不需要这么多功能(可能你的项目需要)。
  • 我认为两种表就可以完成日常需要的工作,schedule_job(定时任务表),schedule_log(任务执行记录表)。
我的思路
  • 第一步 创建两种表用于存储定时任务信息以及执行记录信息。
  • 第二步 需要记录每条执行的状态结果,执行中,执行失败,执行成功等,不可能在每个Job类中写,本来是想用aop切面,但是一直不生效。后来发现可以通过JobListener监听器的方式实现。这里需要注意就是,在执行开始新增记录,在执行结束或异常更新记录,为保证取得记录的logId,在执行开始时把logId放在ThreadLocal中,执行结束在从ThreadLocal取的logId。
  • 第三步 JobListener监听器需要监听那些任务,需要在JobSchedule初始化的时候指定。第二步中的static {}静态代码块中
  • 第四步 为了确保项目启动时,数据库中启动状态的项目正常启动,所以需要在项目初始化时启动数据库中的任务。
  • 通过页面控制的话,我需要提供一些接口如:新增一个定时任务,修改执行计划/启动或关闭,查询某个任务的执行记录等。
  • 对定时任务的操作状态需要同步更新到数据库中,所以我在service中调用JobSchedule工具类控制任务执行,同时跟新数据的任务状态等信息。
加载依赖
  • 配置的话我这里使用默认的。(不写就是默认了)
<!--quartz依赖-->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.3</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.2.3</version>
</dependency>
第一步:创建数据库
  • schedule_job:用于记录每个定时任务的信息
CREATE TABLE `schedule_job` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(100) NOT NULL COMMENT '任务名称',
  `clazz` varchar(100) NOT NULL COMMENT '任务执行的类',
  `cron` varchar(50) NOT NULL COMMENT 'cron表达式',
  `status` int(11) NOT NULL DEFAULT '2' COMMENT '1 开启 2 关闭 ',
  `job_group_name` varchar(50) NOT NULL DEFAULT 'DEFAULT_JOB_GROUP',
  `trigger_group_name` varchar(50) NOT NULL DEFAULT 'DEFAULT_TRIGGER_GROUP',
  `des` varchar(200) DEFAULT NULL COMMENT '描述',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • schedule_log:用于记录定时任务的执行详情
CREATE TABLE `schedule_log` (
  `id` varchar(32) NOT NULL COMMENT '主键',
  `job_id` varchar(100) NOT NULL COMMENT '定时任务的id',
  `job_name` varchar(100) DEFAULT NULL,
  `job_clazz` varchar(100) DEFAULT NULL COMMENT '任务执行的类',
  `status` int(11) NOT NULL DEFAULT '1' COMMENT '1 成功 2 异常 ',
  `log_info` text COMMENT '描述',
  `create_time` bigint(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第二步:创建一个定时任务的工具类
  • JobSchedule 工具类提供对任务的 添加,修改,删除操作。
  • 提供对定时任务的控制页面的话,就是通过Controller对应的service中调用此工具类,来实现对定时任务的控制。(后面会有Controller - service)的代码
/**
 * @calssName JobSchedule
 * @Description 任务调度的工具类
 * @Author xxx
 * @DATE 2020/5/28 11:20
 */
public class JobSchedule {
    private static final Logger LOGGER = LoggerFactory.getLogger(JobSchedule.class);
    private static SchedulerFactory gSchedulerFactory = new StdSchedulerFactory();
    private static String JOB_GROUP_NAME = "DEFAULT_JOB_GROUP";
    private static String TRIGGER_GROUP_NAME = "DEFAULT_TRIGGER_GROUP";

    // 默认将 MyJobListener 绑定到 JOB_GROUP_NAME,在MyJobListener通过监听的方式记录 记录定时任务的执行记录
    static {
        try {
            Scheduler sched = gSchedulerFactory.getScheduler();
            sched.getListenerManager().addJobListener(new MyJobListener(),jobGroupEquals(JOB_GROUP_NAME));
        }catch (Exception e){
            LOGGER.error(">>>>>>>>>>>>>>>>>>>>>> 定时任务addJobListener失败 >>>>>>>>>>>>>>>>>>>>>>");
        }
    }

    private JobSchedule() {
    }

    /**
     * @Description: 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
     * @param jobName 任务名
     * @param cls 任务
     * @param cron 时间设置,参考quartz说明文档
     * @throws SchedulerException
     *
     */
    public static void addJob(String jobName, Class cls, String cron) throws SchedulerException {

        Scheduler sched = gSchedulerFactory.getScheduler();
        // 用于描叙Job实现类及其他的一些静态信息,构建一个作业实例
        JobDetail jobDetail = JobBuilder.newJob(cls).withIdentity(jobName, JOB_GROUP_NAME).build();
        // 构建一个触发器,规定触发的规则
        Trigger trigger = TriggerBuilder.newTrigger()// 创建一个新的TriggerBuilder来规范一个触发器
                .withIdentity(jobName, TRIGGER_GROUP_NAME)// 给触发器起一个名字和组名
                .startNow()// 立即执行
                .withSchedule(CronScheduleBuilder.cronSchedule(cron)) // 触发器的执行时间
                .build();// 产生触发器
        sched.scheduleJob(jobDetail, trigger);
        LOGGER.info("添加任务:{},{},{}",jobName,cls,cron);
        // 启动
        if (!sched.isShutdown()) {
            sched.start();
        }
    }

    /**
     * @Description: 添加一个定时任务
     *
     * @param jobName 任务名
     * @param jobGroupName 任务组名
     * @param triggerName 触发器名
     * @param triggerGroupName 触发器组名
     * @param cls 任务
     * @param cron 时间设置,参考quartz说明文档
     */
    public static void addJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName, Class cls, String cron) throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        // 用于描叙Job实现类及其他的一些静态信息,构建一个作业实例
        JobDetail jobDetail = JobBuilder.newJob(cls).withIdentity(jobName, jobGroupName).build();
        // 构建一个触发器,规定触发的规则
        Trigger trigger = TriggerBuilder.newTrigger()// 创建一个新的TriggerBuilder来规范一个触发器
                .withIdentity(jobName, triggerGroupName)// 给触发器起一个名字和组名
                .startNow()// 立即执行
                .withSchedule(CronScheduleBuilder.cronSchedule(cron)) // 触发器的执行时间
                .build();// 产生触发器
        sched.scheduleJob(jobDetail, trigger);
        LOGGER.info("添加任务:{},{},{},{},{},{}",jobName,jobGroupName,triggerName,triggerGroupName,cls,cron);
        // 启动
        if (!sched.isShutdown()) {
            sched.start();
        }

    }

    /**
     * @Description: 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名)
     *
     * @param jobName
     * @param cron
     * @throws SchedulerException
     */
    public static void modifyJobTime(String jobName, String cron) throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        TriggerKey triggerKey = new TriggerKey(jobName, TRIGGER_GROUP_NAME);
        CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerKey);
        if (trigger == null) {
            return;
        }
        String oldTime = trigger.getCronExpression();
        if (!oldTime.equalsIgnoreCase(cron)) {
            JobDetail jobDetail = sched.getJobDetail(new JobKey(jobName, JOB_GROUP_NAME));
            Class objJobClass = jobDetail.getJobClass();
            removeJob(jobName);
            addJob(jobName, objJobClass, cron);
            LOGGER.info("修改任务:{},{}",jobName,cron);
        }
    }

    /**
     * @Description: 修改或添加一个任务,jobName 存在时修改任务,不存在时则新增任务
     */
    public static void modifyOrAddJobTime(String jobName, String cron, Class cls) throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        TriggerKey triggerKey = new TriggerKey(jobName, TRIGGER_GROUP_NAME);
        CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerKey);
        if (trigger == null) { // 此任务不存在
            addJob(jobName, cls, cron);
            LOGGER.info("修改任务:{},{}",jobName,cron);
        }else{
            String oldTime = trigger.getCronExpression();
            if (!oldTime.equalsIgnoreCase(cron)) {
                JobDetail jobDetail = sched.getJobDetail(new JobKey(jobName, JOB_GROUP_NAME));
                Class objJobClass = jobDetail.getJobClass();
                removeJob(jobName);
                addJob(jobName, objJobClass, cron);
                LOGGER.info("修改任务:{},{}",jobName,cron);
            }
        }
    }

    /**
     * @Description: 移除一个任务(使用默认的任务组名,触发器名,触发器组名)
     * @param jobName
     * @throws SchedulerException
     */
    public static void removeJob(String jobName) throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        JobKey jobKey = new JobKey(jobName, TRIGGER_GROUP_NAME);
        // 停止触发器
        sched.pauseJob(jobKey);
        sched.unscheduleJob(new TriggerKey(jobName, TRIGGER_GROUP_NAME));// 移除触发器
        sched.deleteJob(jobKey);// 删除任务
        LOGGER.info("移除任务:{}",jobName);
    }

    /**
     * 移除任务
     *
     * @param jobName
     * @param jobGroupName
     * @param triggerName
     * @param triggerGroupName
     * @throws SchedulerException
     */
    public static void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        JobKey jobKey = new JobKey(jobName, jobGroupName);
        // 停止触发器
        sched.pauseJob(jobKey);
        sched.unscheduleJob(new TriggerKey(jobName, triggerGroupName));// 移除触发器
        sched.deleteJob(jobKey);// 删除任务
        LOGGER.info("移除任务:{},{},{},{},{},{}",jobName,jobGroupName,triggerName,triggerGroupName);
    }

    /**
     * 启动所有任务
     *
     * @throws SchedulerException
     */
    public static void startJobs() throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        sched.start();
        LOGGER.info("启动所有任务");
    }

    /**
     * 关闭所有定时任务
     * @throws SchedulerException
     */
    public static void shutdownJobs() throws SchedulerException {
        Scheduler sched = gSchedulerFactory.getScheduler();
        if (!sched.isShutdown()) {
            sched.shutdown();
            LOGGER.info("关闭所有任务");
        }
    }
}
第三步:定时任务监听器MyJobListener
  • 定时任务监听器:通过监听的方式记录 记录定时任务的执行记录。
  • 创建日志的时候回把logId放在ThreadLocal中,在执行结果从ThreadLocal取logId来更新日志状态。
/**
 * @calssName MyJobListener
 * @Description 定时任务监听器:通过监听的方式记录 记录定时任务的执行记录
 * @Author xxx
 * @DATE 2020/5/29 14:18
 */
@Component
public class MyJobListener implements JobListener{
    private JobService jobService;

    // 用于保存 logId
    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    @Override
    public String getName() {
        return "myJobListener";
    }
    
    // 执行开始新增一条执行日志,状态为执行中
    @Override
    public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
        if(jobService == null){
            jobService = SpringUtils.getBean(JobService.class);
        }
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        String name = jobDetail.getKey().getName();
        String logId = jobService.saveScheduleLog(name, JobLogStatusEnum.RUNNING.getCode(), "");
        threadLocal.set(logId); // 把执行记录放到 threadLocal,提供给执行结束后取此结果
    }
    
    // 任务拒绝执行时,修改日志状态为:执行失败
    @Override
    public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
        String logId = threadLocal.get(); // 执行记录logId
        jobService.updateScheduleLog(logId, JobLogStatusEnum.ERROR.getCode(), "执行失败");
    }
    
    // 任务执行时结果,判断是否有异常,修改对应状态
    @Override
    public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
        String logId = threadLocal.get(); // 执行记录logId
        if(e == null){ // 没有异常修改记录为成功
            jobService.updateScheduleLog(logId, JobLogStatusEnum.SUCCESS.getCode(), "执行成功");
        }else{ // 存在异常修改记录失败,并且把异常信息保存到数据库中
            jobService.updateScheduleLog(logId, JobLogStatusEnum.ERROR.getCode(), e.toString());
        }
    }
}
第四步:重启项目的时候确保定时任务启动
  • 需要在项目初始化的时候,把数据中启动的任务添加到数据库中
  • 该类会在项目启动后执行: xxx implements ServletContextAware
@Configuration
public class ScheduleJobInitConfig implements ServletContextAware {
    private static final Logger logger = LoggerFactory.getLogger(ScheduleJobInitConfig.class);
    @Autowired
    private JobService jobService;
    @Override
    public void setServletContext(ServletContext servletContext) {
        jobService.initScheduleJob();
        logger.info("初始化定时任务成功 ...");
    }
}
第五步:创建一个定时任务
/**
 * @calssName JobA
 * @Description 每写一个定时任务需要在数据库中schedule_job中配置一条数据,game-admin项目初始化时会加载数据库中的已启动的定时任务
 * @Author xxx
 * @DATE 2020/5/28 11:14
 */
@Component
public class JobA implements Job {

    private static final Logger logger = LoggerFactory.getLogger(JobA.class);
    private JobService jobService;

    @Override
    public void execute(JobExecutionContext context) {
        System.out.println("=================>JobA ");
        // 这里写需要执行的业务逻辑
    }
}
控制任务的接口方法,对任务的增删该查 都在 下面的service类中
  • 完成以上这些,定时任务模块基本完成。接下来的就是为了控制界面提供的接口。我们这边前后的分离于是我直接使用postman调用接口控制。
  • controller
/**
 * @calssName ScheduleController
 * @Description TODO
 * @Author xxx
 * @DATE 2020/5/28 12:26
 */
@RestController
@RequestMapping("/v1/schedule/job")
public class JobController {

    @Autowired
    private JobService jobService;

    /**
     * 查看所有任务列表
     */
    @GetMapping("/list")
    public R getJobList(){
        try{
            Map<String, Object> result = jobService.getAllJob();
            return R.data(result);
        }catch (Exception e){
            return R.error(e);
        }
    }

    /**
     * 查看某个任务的执行详情
     */
    @GetMapping("/schedule/detail")
    public R getJobScheduleDetail(@RequestParam("jobId") int jobId){
        try{
            Map<String, Object> result = jobService.getJobScheduleDetail(jobId);
            return R.data(result);
        }catch (Exception e){
            return R.error(e);
        }
    }

    /**
     * 查看某个任务的详情
     */
    @GetMapping("/detail")
    public R getJobDetail(@RequestParam("jobId") int jobId){
        try{
            Map<String, Object> result = jobService.getJobDetail(jobId);
            return R.data(result);
        }catch (Exception e){
            return R.error(e);
        }
    }

    /**
     * 修改任务
     */
    @PostMapping("/update")
    public R updateJob(ScheduleJob job){
        try{
            Map<String, Object> result = jobService.updateJob(job);
            return R.data(result);
        }catch (Exception e){
            return R.error(e);
        }
    }
}
  • service
/**
 * @calssName JobService
 * @Description
 * @Author xxx
 * @DATE 2020/5/28 15:59
 */
@Service
public class JobService {

    private static final Logger logger = LoggerFactory.getLogger(JobService.class);

    @Autowired
    private ScheduleJobRepoImpl scheduleJobRepo;
    @Autowired
    private ScheduleLogRepoImpl scheduleLogRepo;

    /**
     * 查看所有任务列表
     */
    public Map<String,Object> getAllJob(){
        List<ScheduleJob> list = scheduleJobRepo.getList();
        Map<String,Object> result = new HashMap<>();
        result.put("list", list);
        return result;
    }


    /**
     * 查看某个任务的执行详情
     */
    public Map<String,Object> getJobScheduleDetail(int jobId){
        List<ScheduleLog> list = scheduleLogRepo.getListByJobId(jobId);
        Map<String,Object> result = new HashMap<>();
        result.put("list", list);
        return result;
    }

    /**
     * 查看某个任务的详情
     */
    public Map<String,Object> getJobDetail(int jobId){
        ScheduleJob scheduleJob = scheduleJobRepo.getScheduleJobById(jobId);
        Map<String,Object> result = new HashMap<>();
        result.put("scheduleJob", scheduleJob);
        return result;
    }

    /**
     * 修改任务
     */
    public Map<String,Object> updateJob(ScheduleJob scheduleJob){
        Map<String,Object> result = new HashMap<>();

        Class<?> clszz = null;
        try {
            clszz = Class.forName(scheduleJob.getClazz());
        }catch (ClassNotFoundException e){
            logger.error("修改定时任务异常,无法找到指定的任务类");
            throw new AppException(ErrorCode.SYS_PARAMS_ERROR.code(), "无法找到指定的任务类");
        }

        try {
            Integer status = scheduleJob.getStatus();
            if(status == 1){ // 修改或者添加任务
                JobSchedule.modifyOrAddJobTime(scheduleJob.getName(), scheduleJob.getCron(),clszz);
            }else if(status == 2){ // 停止任务
                JobSchedule.removeJob(scheduleJob.getName());
            }else{
                return result;
            }
        }catch (SchedulerException e){
            logger.error("修改定时任务异常:{}", e);
            throw new AppException(ErrorCode.SYS_ERROR);
        }

        scheduleJobRepo.updateScheduleJob(scheduleJob);
        return result;
    }

    /**
     * 启动数据库中配置的定时任务
     */
    public void initScheduleJob(){
        List<ScheduleJob> list = scheduleJobRepo.getEnableList();

        for (ScheduleJob scheduleJob: list){
            Class<?> clszz = null;
            try {
                clszz = Class.forName(scheduleJob.getClazz());
                JobSchedule.addJob(scheduleJob.getName(), clszz, scheduleJob.getCron());
            } catch (SchedulerException e){
                logger.error("初始化启动定时任务异常:{}", e);
                continue;
            } catch (ClassNotFoundException e){
                logger.error("初始化启动定时任务异常,无法找到指定的任务类");
                continue;
            }
        }
    }

    /**
     * 新增一条执行日志
     */
    public String saveScheduleLog(String name, int status, String logInfo){
        ScheduleJob scheduleJob = scheduleJobRepo.getScheduleJobByName(name);
        ScheduleLog scheduleLog = new ScheduleLog(scheduleJob,status,logInfo);
        scheduleLogRepo.saveScheduleLog(scheduleLog);
        return scheduleLog.getId();
    }

    /**
     * 更新一条执行日志
     */
    public void updateScheduleLog(String id, int status, String logInfo){
        scheduleLogRepo.updateScheduleLog(id,status,logInfo);
    }
}
最后
  • 如果在这里获得过启发和思考,希望点赞支持!对于内容有不同的看法欢迎来信交流。
  • 技术栈 >> java
  • 邮箱 >> 15673219519@163.com
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 C# 中,可以使用 Quartz.NET 来实现定时任务,同时支持数据持久化Quartz.NET 是一个功能强大且灵活的开源作业调度库,可以用于创建定时任务和计划任务,支持数据持久化和集群部署等高级功能。 要使用 Quartz.NET 实现数据持久化,需要先创建一个用于存储调度程序数据数据,可以使用 Quartz.NET 提供的 SQL 脚本来创建。然后在应用程序中配置 Quartz.NET,指定数据库类型和连接字符串等信息。示例如下: ```csharp using Quartz; using Quartz.Impl; using Quartz.Impl.AdoJobStore; using Quartz.Spi; using System; using System.Collections.Specialized; class Program { static void Main() { Console.WriteLine("Starting scheduler..."); // 创建一个调度程序实例 ISchedulerFactory schedulerFactory = new StdSchedulerFactory(GetSchedulerProperties()); IScheduler scheduler = schedulerFactory.GetScheduler().Result; // 启动调度程序 scheduler.Start(); Console.WriteLine("Scheduler started."); // 创建一个作业实例 IJobDetail job = JobBuilder.Create<HelloJob>() .WithIdentity("job1", "group1") .Build(); // 创建一个触发器实例,每秒钟触发一次 ITrigger trigger = TriggerBuilder.Create() .WithIdentity("trigger1", "group1") .StartNow() .WithSimpleSchedule(x => x .WithIntervalInSeconds(1) .RepeatForever()) .Build(); // 将作业和触发器添加到调度程序中 scheduler.ScheduleJob(job, trigger); // 等待用户按下 Enter 键退出 Console.ReadLine(); // 关闭调度程序 scheduler.Shutdown(); Console.WriteLine("Scheduler stopped."); } static NameValueCollection GetSchedulerProperties() { // 配置调度程序属性,指定数据持久化和相关参数 NameValueCollection properties = new NameValueCollection(); properties["quartz.scheduler.instanceName"] = "MyScheduler"; properties["quartz.scheduler.instanceId"] = "AUTO"; properties["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz"; properties["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz"; properties["quartz.jobStore.dataSource"] = "default"; properties["quartz.dataSource.default.provider"] = "SqlServer-20"; properties["quartz.dataSource.default.connectionString"] = "Server=(local);Database=Quartz;Trusted_Connection=True;"; return properties; } } public class HelloJob : IJob { public void Execute(IJobExecutionContext context) { Console.WriteLine("Hello, Quartz.NET!"); } } ``` 这个示例会创建一个调度程序实例,然后创建一个作业实例和触发器实例,并将它们添加到调度程序中。作业类 HelloJob 实现了 IJob 接口,用于定义作业执行的逻辑。在 GetSchedulerProperties 方法中,配置了调度程序属性,指定了数据库类型和连接字符串等信息。在这个示例中,使用的是 SQL Server 数据库。 需要注意的是,在使用 Quartz.NET 进行数据持久化时,需要保证数据库连接可靠和高效,同时需要考虑并发执行的问题。可以适当地调整作业和触发器的参数,以达到最优的性能和可靠性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

放码过来_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值