SpringBoot与Quartz整合练习Demo

SpringBoot与Quartz整合,通过API动态配置定时任务,持久化到数据库。

SpringBoot2.0的spring-boot-starter-quartz包已为我们做好自动化配置,将Quartz托管给spring,不再像之前我们得使用QuartzConfiguration类来自己完成Quartz需要的一系列配置,如:JobFactory、SchedulerFactoryBean等。

<!-- springboot2.0之前的quartz相关依赖 -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>${quartz.version}</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>${quartz.version}</version>
</dependency>

<!-- springboot2.0之后的quartz依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

自动化配置源码:

我们找到Idea的External Libraries并且展开spring-boot-autoconfigure-2.0.0.RELEASE.jar,找到org.springframework.boot.autoconfigure.quartz,该目录就是SpringBoot为我们提供的Quartz自动化配置源码实现,在该目录下有如下所示几个类:

  • AutowireCapableBeanJobFactory

该类替代了我们之前在QuartzConfiguration配置类的AutowiringSpringBeanJobFactory内部类实现,主要作用是将我们自定义的QuartzJobBean子类托管给Spring IOC,可以在定时任务类内注入任意被Spring IOC托管的类。

  • JobStoreType

该类是一个枚举类型,定义了对应application.yml、application.properties文件内spring.quartz.job-store-type配置,其目的是配置quartz任务的数据存储方式,分别为:MEMORY(内存方式:默认)、JDBC(数据库方式)。

  • QuartzAutoConfiguration

该类是自动配置的主类,内部配置了SchedulerFactoryBean以及JdbcStoreTypeConfiguration,使用QuartzProperties作为属性自动化配置条件。

  • QuartzDataSourceInitializer

该类主要用于数据源初始化后的一些操作,根据不同平台类型的数据库进行选择不同的数据库脚本。

  • QuartzProperties

该类对应了spring.quartz在application.yml、application.properties文件内开头的相关配置。

  • SchedulerFactoryBeanCustomizer

这是一个接口,我们实现该接口后并且将实现类使用Spring IOC托管,可以完成SchedulerFactoryBean的个性化设置,这里的设置完全可以对SchedulerFactoryBean做出全部的设置变更。

 

------ 开始 ------

pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.0.M1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>quartz</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>quartz</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-quartz</artifactId>
		</dependency>
		<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>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.0.0</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.6</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

	<repositories>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</pluginRepository>
		<pluginRepository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
		</pluginRepository>
	</pluginRepositories>

</project>

application.yml文件如下:

spring:
  datasource:
      url: jdbc:mysql://localhost:3306/lesson?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
      driverClassName: com.mysql.cj.jdbc.Driver
      username: root
      password: root
      type: com.alibaba.druid.pool.DruidDataSource
  jpa:
    properties:
      hibernate:
        show_sql: true
        format_sql: true
  quartz:
    properties:
      org:
        quartz:
          scheduler:
            instanceName: DefaultQuartzScheduler
            #如果使用集群,instanceId必须唯一,设置成AUTO
            instanceId: AUTO
          jobStore:
            #存储方式使用JobStoreTX,也就是数据库
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            #默认表前缀
            tablePrefix: QRTZ_
            #是否使用集群(如果项目只部署到一台服务器,就不用了)
            isClustered: false
            clusterCheckinInterval: 10000
            useProperties: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 5
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
    #quartz任务的数据持久化方式,默认是MEMORY内存方式,配置JDBC以使用数据库方式持久化任务
    job-store-type: JDBC
    #自动将quartz需要的数据表通过配置方式进行初始化
#   jdbc:
#     initialize-schema: never

使用Quartz需要的11张表: 

CREATE TABLE QRTZ_JOB_DETAILS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    JOB_CLASS_NAME   VARCHAR(250) NOT NULL,
    IS_DURABLE VARCHAR(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);

CREATE TABLE QRTZ_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    JOB_NAME  VARCHAR(200) NOT NULL,
    JOB_GROUP VARCHAR(200) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    NEXT_FIRE_TIME BIGINT(13) NULL,
    PREV_FIRE_TIME BIGINT(13) NULL,
    PRIORITY INTEGER NULL,
    TRIGGER_STATE VARCHAR(16) NOT NULL,
    TRIGGER_TYPE VARCHAR(8) NOT NULL,
    START_TIME BIGINT(13) NOT NULL,
    END_TIME BIGINT(13) NULL,
    CALENDAR_NAME VARCHAR(200) NULL,
    MISFIRE_INSTR SMALLINT(2) NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
        REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);

CREATE TABLE QRTZ_SIMPLE_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    REPEAT_COUNT BIGINT(7) NOT NULL,
    REPEAT_INTERVAL BIGINT(12) NOT NULL,
    TIMES_TRIGGERED BIGINT(10) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE QRTZ_CRON_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    CRON_EXPRESSION VARCHAR(200) NOT NULL,
    TIME_ZONE_ID VARCHAR(80),
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE QRTZ_SIMPROP_TRIGGERS
  (          
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    STR_PROP_1 VARCHAR(512) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR(1) NULL,
    BOOL_PROP_2 VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE QRTZ_BLOB_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    BLOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);

CREATE TABLE QRTZ_CALENDARS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    CALENDAR_NAME  VARCHAR(200) NOT NULL,
    CALENDAR BLOB NOT NULL,
    PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);

CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR(200) NOT NULL, 
    PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);

CREATE TABLE QRTZ_FIRED_TRIGGERS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    ENTRY_ID VARCHAR(95) NOT NULL,
    TRIGGER_NAME VARCHAR(200) NOT NULL,
    TRIGGER_GROUP VARCHAR(200) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    FIRED_TIME BIGINT(13) NOT NULL,
    SCHED_TIME BIGINT(13) NOT NULL,
    PRIORITY INTEGER NOT NULL,
    STATE VARCHAR(16) NOT NULL,
    JOB_NAME VARCHAR(200) NULL,
    JOB_GROUP VARCHAR(200) NULL,
    IS_NONCONCURRENT VARCHAR(1) NULL,
    REQUESTS_RECOVERY VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);

CREATE TABLE QRTZ_SCHEDULER_STATE
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(200) NOT NULL,
    LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
    CHECKIN_INTERVAL BIGINT(13) NOT NULL,
    PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);

CREATE TABLE QRTZ_LOCKS
  (
    SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME  VARCHAR(40) NOT NULL, 
    PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

JobController.java

package com.example.quartz;

import com.example.quartz.entity.JobDetailsEntity;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping(value="/job")
public class JobController {

    @Resource
    private JobService jobService;

    @PostMapping(value="/add")
    public String addJob(@RequestParam(value="jobClassName") String jobClassName,
                         @RequestParam(value="jobGroupName") String jobGroupName,
                         @RequestParam(value="cronExpression") String cronExpression) throws Exception {
        return jobService.jobAdd(jobClassName, jobGroupName, cronExpression);
    }

    @PostMapping(value="/pause")
    public String pauseJob(@RequestParam(value="jobClassName") String jobClassName, @RequestParam(value="jobGroupName") String jobGroupName) throws Exception  {
        return jobService.jobPause(jobClassName, jobGroupName);
    }


    @PostMapping(value="/resume")
    public String resumeJob(@RequestParam(value="jobClassName") String jobClassName, @RequestParam(value="jobGroupName")String jobGroupName) throws Exception {
        return jobService.jobResume(jobClassName, jobGroupName);
    }

    @PostMapping(value="/reschedule")
    public String rescheduleJob(@RequestParam(value="jobClassName") String jobClassName,
                              @RequestParam(value="jobGroupName") String jobGroupName,
                              @RequestParam(value="cronExpression") String cronExpression) throws Exception {
        return jobService.jobReschedule(jobClassName, jobGroupName, cronExpression);
    }

    @PostMapping(value="/delete")
    public String deleteJob(@RequestParam(value="jobClassName") String jobClassName, @RequestParam(value="jobGroupName") String jobGroupName) throws Exception {
        return jobService.jobDelete(jobClassName, jobGroupName);
    }

    @GetMapping("/query")
    public List<JobDetailsEntity> queryJob() {
        return jobService.queryJob();
    }
}

JobServiceImpl.java(此处未贴出JobService接口)
直接注入org.quartz包中的定时任务调度器Scheduler,对进行定时任务的一系列操作。

package com.example.quartz;

import com.example.quartz.entity.JobDetailsEntity;
import com.example.quartz.jpa.JobDetailsJPA;
import org.quartz.*;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class JobServiceImpl implements JobService {

    @Resource
    private Scheduler scheduler;

    @Resource
    private JobDetailsJPA jobDetailsJPA;

    @Override
    public String jobAdd(String jobClassName, String jobGroupName, String cronExpression) throws Exception {
        //启动调度器
        scheduler.start();
        //创建任务
        JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobClassName, jobGroupName).build();
        //表达式调度构建器
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
        //按cronExpression构建一个新的触发器,定义任务调度的时间规则
        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName)
                .withSchedule(scheduleBuilder).build();
        try {
            //将任务与触发器绑定到调度器内
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            System.out.println("创建定时任务失败" + e);
            throw new Exception("创建定时任务失败");
        }
        return "创建定时任务:" + jobClassName;
    }

    private static Job getClass(String classname) throws Exception {
        return (Job) Class.forName(classname).newInstance();
    }

    @Override
    public String jobPause(String jobClassName, String jobGroupName) throws Exception {
        scheduler.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));
        return "暂停定时任务:" + jobClassName;
    }

    @Override
    public String jobResume(String jobClassName, String jobGroupName) throws Exception {
        scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));
        return "重启定时任务:" + jobClassName;
    }

    @Override
    public String jobReschedule(String jobClassName, String jobGroupName, String cronExpression) throws Exception {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
            //表达式调度构建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            //按新的cronExpression重新构建触发器
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
            //按新的trigger重新绑定到调度器
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            System.out.println("更新定时任务失败" + e);
            throw new Exception("更新定时任务失败");
        }
        return "更新定时任务:" + jobClassName;
    }

    @Override
    public String jobDelete(String jobClassName, String jobGroupName) throws Exception {
        scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));
        scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));
        scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));
        return "删除定时任务:" + jobClassName;
    }

    @Override
    public List<JobDetailsEntity> queryJob() {
        return jobDetailsJPA.findAll();
    }
}

测试用定时任务 TestJob.java 

package com.example.quartz;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.stereotype.Service;

import java.util.Date;

@Service
public class TestJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(new Date() + " 测试定时任务....");
    }
}

注意调用add接口时,任务类名需要全路径com.example.quartz.TestJob

 

数据库相关代码: JobDetailsJPA接口

package com.example.quartz.jpa;

import com.example.quartz.entity.JobDetailsEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

import java.io.Serializable;

public interface JobDetailsJPA extends
        JpaRepository<JobDetailsEntity,Long>,
        JpaSpecificationExecutor<JobDetailsEntity>,
        Serializable {
}

任务详情实体类JobDetailsEntity.java 

package com.example.quartz.entity;

import javax.persistence.*;
import java.io.Serializable;
import java.sql.Blob;

@Entity
@Table(name="qrtz_job_details")
public class JobDetailsEntity implements Serializable {

    @EmbeddedId //复合主键
    private JobDetailsKey jobDetailsKey;

    @Column(name="DESCRIPTION")
    private String description;

    @Column(name="JOB_CLASS_NAME")
    private String jobClassName;

    @Column(name="IS_DURABLE")
    private String isDurable;

    @Column(name="IS_NONCONCURRENT")
    private String isNonconcurrent;

    @Column(name="IS_UPDATE_DATA")
    private String isUpdateData;

    @Column(name="REQUESTS_RECOVERY")
    private String requestRecovery;

//方便起见,就不读取blob类型的属性了
//    @Column(name="JOB_DATA")
//    private Blob jobData;

    public JobDetailsKey getJobDetailsKey() {
        return jobDetailsKey;
    }

    public void setJobDetailsKey(JobDetailsKey jobDetailsKey) {
        this.jobDetailsKey = jobDetailsKey;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getJobClassName() {
        return jobClassName;
    }

    public void setJobClassName(String jobClassName) {
        this.jobClassName = jobClassName;
    }

    public String getIsDurable() {
        return isDurable;
    }

    public void setIsDurable(String isDurable) {
        this.isDurable = isDurable;
    }

    public String getIsNonconcurrent() {
        return isNonconcurrent;
    }

    public void setIsNonconcurrent(String isNonconcurrent) {
        this.isNonconcurrent = isNonconcurrent;
    }

    public String getIsUpdateData() {
        return isUpdateData;
    }

    public void setIsUpdateData(String isUpdateData) {
        this.isUpdateData = isUpdateData;
    }

    public String getRequestRecovery() {
        return requestRecovery;
    }

    public void setRequestRecovery(String requestRecovery) {
        this.requestRecovery = requestRecovery;
    }
}

由于qrtz_job_details表是复合主键PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),所以需要创建复合主键类 JobDetailsKey.java 

package com.example.quartz.entity;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;

@Embeddable
public class JobDetailsKey implements Serializable {

    @Column(name="SCHED_NAME")
    private String schedName;

    @Column(name="JOB_NAME")
    private String jobName;

    @Column(name="JOB_GROUP")
    private String jobGroup;

    public String getSchedName() {
        return schedName;
    }

    public void setSchedName(String schedName) {
        this.schedName = schedName;
    }

    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public String getJobGroup() {
        return jobGroup;
    }

    public void setJobGroup(String jobGroup) {
        this.jobGroup = jobGroup;
    }
}

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值