SpringBoot2概览-开发实用篇

知识点


热部署
  • 概念:项目刚启动时需要完成重启+重载,而热部署只要重启即可(即仅加载当前开发者自定义开发的资源,不加载jar资源)

    • 重启:加载自定义开发代码,包含类、页面、配置文件等,加载位置在restart类加载器
    • 重载:加载jar包(后续不需要再重复加载),加载位置在base类加载器
  • 手动启动热部署:

    • 添加依赖包:
    <dependency>
    	<groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
    
    • 激活热部署:每次修改代码后,点击build project(快捷键为Ctrl + F9)
  • 自动启动热部署:每次修改代码后并且编辑器失去焦点后便会自动进行热部署

    • 设置自动构建项目:

    在这里插入图片描述

    • 设置自动构建项目:打开全局搜索(Ctrl+N),在Actions中搜索Registry

    在这里插入图片描述

  • 自定义不参与重启(热部署)的文件/文件夹:不想每次修改某文件的代码后去就自动重启进行热部署,可以进行相应配置

# 写在application.yml中即可
devtools:
  restart:
    exclude: public/**,static/**
  • 关闭热部署:热部署只需要在开发环境中有效,所以需要能随时关闭热部署。热部署的关闭可以在配置文件中进行,但该操作并不是优先级最高的方式,因此有可能会被其他方式的配置覆盖导致热部署又开启,所以需要进行最高优先级的配置
public static void main(String[] args) {
    System.setProperty("spring.devtools.restart.enabled", "false");
    SpringApplication.run(SSMPApplication.class);
}

配置高级
  • 第三方bean绑定属性:假设引入了DruidDataSource后需要设置数据库连接信息。此时使用@ConfigurationProperties接口,该注解除了可以为自定义的属性进行属性绑定(可参考01-基础篇/获取yml配置文件中的属性值),还可以进行第三方bean的属性绑定
// 不使用@ConfigurationProperties
@Bean
public DruidDataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    ds.setDriverClassName("test1");
    return ds;
}
// 使用@ConfigurationProperties
@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource dataSource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
}

// 测试代码
public static void main(String[] agrs){
    ConfigurableApplicationContext ctx = SpringApplication.run(
        SpringbootLearnApplication.class, args
    );
    DruidDataSource ds = ctx.getBean(DruidDataSource.class);
    System.out.println(ds.getDriverClassName());
}
datasource:
  driverClassName: test2
  • 宽松绑定:

    • @ConfigurationProperties绑定属性支持属性名宽松绑定,比如下面配置均能匹配上属性ipAddress
    servers:
      ipAddress: 192.168.1.1
      # 下面三种均可
      # ip_address: 192.168.1.2
      # IP_ADDRESS: 192.168.1.4
      # ip-address: 192.168.1.3  # 主流模式
      port: 2345
      timeout: -1
    
    @Data
    @ConfigurationProperties(prefix="servers")
    public class ServerConfig {
        private String ipAddress;
        private int port;
        private long timeout;
    }
    // 测试代码
    public static void main(String[] agrs){
        ConfigurableApplicationContext ctx = SpringApplication.run(
            SpringbootLearnApplication.class, args
        );
        ServerConfig bean = ctx.getBean(ServerConfig.class);
        System.out.println(bean);
    }
    
    • 不支持注解@Value引用单个属性的方式:即在yml配置文件中使用的名称必须和类中属性名一致
    @Value("${servers.ipAddress}") 
    private String msg;
    
    // 测试代码
    @Test
    void contextLoads(){
        // 如果在配置文件中不是以下写法则会报错
        // servers:
        //   ipAddress: 192.168.1.1
        System.out.println(msg);
    }
    
    • @ConfigurationProperties绑定仅能使用纯小写字母、数字、下划线作为合法的字符
    @Bean
    // "datasource"必须全部为小写,即使在配置文件中写的是dataSource:xxx
    @ConfigurationProperties(prefix = "datasource")
    public DruidDataSource datasource(){
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }
    
  • 数据校验:在yml配置文件中定义的值可能与类中属性类型不匹配,需要先进行校验

    • 添加JSR303规范坐标与Hibernate校验框架对应坐标:
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
    </dependency>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
    </dependency>
    
    • Bean开启校验功能并设置具体规则:
    @Component
    @Data
    @ConfigurationProperties(prefix = "servers")
    @Validated  // 开启校验
    public class ServerConfig {
        @Max(value = 400,message = "最大值不能超过400")  // 设置具体规则
        private int port;
    }
    

测试

  • 加载测试专用属性:该属性仅对当前测试类有效,缩小作用范围,降低冲突

    • 在启动测试环境时可以通过properties参数设置测试环境专用的属性
    // 在yml配置文件中不配置test.prop属性也会生效,如果都配置了则该处配置的优先级大于配置文件中的
    @SpringBootTest(properties = {"test.prop=testValue1"})
    public class PropertiesAndArgsTest {
        @Value("${test.prop}")
        private String msg;
    
        @Test
        void testProperties(){
            System.out.println(msg);  // 输出testValue1
        }
    }
    
    • 在启动测试环境时可以通过args参数设置测试环境专用的命令行参数
    // 相当于之前在命令行中添加属性(所以是--xxx.xxx的形式),如果在yml配置文件和properties中都配置了相同属性,该处的优先级最高(命令行的设置优先级高)
    @SpringBootTest(args = {"--test.arg=testValue2"})
    public class PropertiesAndArgsTest {
        @Value("${test.arg}")
        private String msg;
    
        @Test
        void testArgs(){
            System.out.println(msg);  // 输出testValue2
        }
    }
    
    • 使用@Import注解加载当前测试类专用的配置
    // 因为该类只需要在测试环境生效,所以不能定义在src目录下,应该定义在test目录下
    @Configuration
    public class MsgConfig {
        @Bean
        public String msg(){
            return "bean msg";
        }
    }
    
    @SpringBootTest
    @Import(MsgConfig.class)
    public class ConfigurationTest {
        @Autowired
        private String msg;
    
        @Test
        void testConfiguration(){
            System.out.println(msg);  // 输出bean msg
        }
    }
    
  • Web环境模拟测试:之前在测试类对数据层和业务层进行了测试,并未涉及到表现层的测试,测试表现层使用的是Postman

// 以下测试通过后没有提示信息,只有未通过才会有信息
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)  // 模拟端口
@AutoConfigureMockMvc  //开启虚拟MVC调用
public class WebTest {
    @Test
    void testGetById(@Autowired MockMvc mvc) throws Exception {
        // 1.创建虚拟请求,访问/books
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        // 执行对应的请求
        ResultActions action = mvc.perform(builder);

        // 2.测试状态是否匹配
        // 定义本次调用的预期值(下面同理)
        StatusResultMatchers status = MockMvcResultMatchers.status();
        ResultMatcher ok = status.isOk();
        // 添加预计值到本次调用过程中进行匹配(下面同理)
        action.andExpect(ok);

        // 3.测试响应头是否匹配
        HeaderResultMatchers header = MockMvcResultMatchers.header();
        ResultMatcher contentType = header.string("Content-Type", "application/json");
        action.andExpect(contentType);

        // 4.测试响应体(json格式)是否匹配
        ContentResultMatchers content = MockMvcResultMatchers.content();
        ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot\",\"type\":\"springboot\",\"description\":\"springboot\"}");
        action.andExpect(result);
    }
}
  • 业务层测试事务回滚:每次执行测试只是想测试功能的运行情况,并不想在数据库中留下测试的数据
@SpringBootTest
@Transactional  // 为测试用例添加事务,执行了测试代码后数据库不会存在测试数据,但是id会加1,下次插入数据时id比上一条数据的大2
// @Rollback(false)  // 如果想留下测试数据,则在使用了@Transactional的前提下添加上该注解并设置为false
public class DaoTest {
    @Autowired
    private BookService bookService;
    
    // 具体操作数据库的代码
    ...
}
  • 测试用例数据设定:测试用例数据通常采用随机值进行测试,使用SpringBoot提供的随机数为其赋值
# 其中()可以是任意字符,例如[],!!均可
# 测试的设定代码写在测试环境的配置文件中
testcast:
  book:
    id: ${random.int}           # 随机整数
    id2: ${random.int(10)}      # 10以内随机数
    type: ${random.int(10,20)}  # 10到20随机数
    uuid: ${random.uuid}        # 随机uuid
    name: ${random.value}       # 随机字符串,MD5字符串,32位
    publishTime: ${random.long} # 随机整数(long范围)

数据层解决方案-SQL

现有数据层解决方案技术选型:数据源DruidDataSource + 持久化技术:MyBatis-Plus/MyBatis + 数据库MySQL

  • 数据源配置:假设不在配置文件中声明使用Druid数据源,并且不导入Druid的依赖包,则会使用SpringBoot内置数据源
    • HikariCP:默认内置数据源对象
    • Tomcat提供DataSourceHikariCP不可用的情况下,且在web环境中,将使用tomcat服务器配置的数据源对象
    • Commons DBCPHikari不可用,tomcat数据源也不可用,将使用dbcp数据源
spring:
  datasource:
    druid:  # 把该行删除,使用内置数据源
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/xxxx?serverTimezone=UTC
      username: xxxx
      password: xxxx

# 通用配置无法设置具体的数据源配置信息,仅提供基本的连接相关配置,如需配置,在下一级配置中设置具体设定
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/xxxx
    username: xxxx
    password: xxxx
    hikari:
      maximum-pool-size: 50
  • 内置持久化解决方案-JdbcTemplate:现在不使用MyBatis的依赖,并且导入spring-boot-starter-jdbc的依赖
@SpringBootTest
class SpringBootApplicationTest{
    @Autowired
	private JdbcTemplate jdbcTemplate;

    @Test
    void testJdbc(){
        String sql "select from tbl_book where id 1";
        List<Book>query jdbcTemplate.query(sql,new RowMapper<Book>(){
            @Override
            public Book mapRow(ResultSet rs,int rowNum) throws SQLException{
                Book temp new Book();
                temp.setId(rs.getInt("id"));
                temp.setName(rs.getstring("name"));
                temp.setType(rs.getstring("type"));
                temp.setDescription(rs.getstring("description"));
                return temp;
        	}
        });
        System.out.println(query);
    }
}
  • 内嵌数据库:SpringBoot提供了三种内嵌数据库(导入对应的依赖包就并且配置连接信息就能使用)供开发者选择,都在内存运行,运行速度快,提高开发测试效率
    • H2
    • HSQL
    • Derby

tips:

  • 即使不在配置文件声明使用DruidSpringBoot的自动装配也会找到依赖的包并且使用对应的数据源,就和导入web依赖包就能注解使用内置的Tomcat服务器一个道理
  • H2数据库控制台仅用于开发阶段,线上项目请务必关闭控制台功能,要不然会存在安全隐患
spring:
  h2:
  console:
    path: /h2
    enabled: false
  • SpringBoot可以根据url地址自动识别数据库种类,所以在保障驱动类存在的情况下可以省略配置(即省略driver-class-name)

数据层解决方案-NoSQL

上述的数据层解决方案是基于SQL的,现在介绍三种基于NoSQL

  • Redis

    • 概念:Redis是一款key-value存储结构的内存级NoSQL数据库

      • 支持多种数据存储格式
      • 支持持久化
      • 支持集群
    • 下载与安装(以Windows为例):

      • 下载(https://github.com/tporadowski/redis/releases)后在Windows解压安装或一键式安装
      • 服务端启动命令:redis-server.exe redis.windows.conf
      • 客户端启动命令:redis-cli.exe
    • SpringBoot整合Redis

      • 导入SpringBoot整合Redis的依赖包:
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
      
      • 配置Redis
      spring:
        redis:
          host: localhost # 127.0.0.1
          port: 6379
      
      • 使用RedisTemplate/StringRedisTemplate操作Redis
        • RedisTemplate以对象作为keyvalue,内部对数据进行序列化
        • **StringRedisTemplate**以字符串作为keyvalue,与Redis客户端操作等效(常用)
      @SpringBootTest
      class NosqlApplicationTests{
          @Autowired
          StringRedisTemplate redisTemplate;
      
          @Test
          void set(){
              ValueOperations ops = redisTemplate.opsForValue();
              ops.set("testKey","testvalue");
          }
      }
      
    • 客户端选择:可以选择使用lettcus(默认)与jedis。如果要切换为jedis则需要先导入jedis的依赖包,然后在配置文件中声明client-type: lettuce

      • jedis连接Redis服务器是直连模式,当多线程模式下使用jedis会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,但这样整体性能受影响
      • lettcus是基于Netty框架进行与Redis服务器连接,底层设计中采用StatefulRedisConnection,它自身是线程安全的,可以保障并发访问安全问题,所以一个连接可以被多线程复用。lettcus也支持多连接实例一起工作
  • Mongo

    • 概念:MongoDB是一个开源、高性能、无模式的文档型数据库,是最像关系型数据库的非关系型数据库,它可以存储游戏装备数据、直播数据、物联网数据等临时存储且修改频度高的数据

    • 下载与安装(以Windows为例):

      • 下载(https://www.mongodb.com/try/download)后在Windows解压并设置数据目录
      • 服务端启动命令:mongod --dbpath=..\data\db
      • 客户端启动命令:mongo --host=127.0.0.1 --port=27017
    • SpringBoot整合Mongo

      • 导入Mongodb驱动:
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-mongodb</artifactId>
      </dependency>
      
      • 配置客户端:
      spring:
        data:
          mongodb:
          uri: mongodb://localhost/xxxx
      
      • 客户端读写Mongodb
      @Test
      void testSave(@Autowired MongoTemplate mongoTemplate){
          Book book = new Book();
          book.setId(1);
          book.setType("springboot");
          book.setName("springboot");
          book.setDescription("springboot");
          mongoTemplate.save(book);
      }
      
  • ES

    • 概念:Elasticsearch是一个分布式全文搜索引擎

    • 下载与安装(以Windows为例):

      • 下载(https://www.elastic.co/cn/downloads/elasticsearch)后在Windows解压
      • 运行bin/elasticsearch.bat
    • SpringBoot整合ES

      • 导入坐标:SpringBoot平台并没有跟随ES的更新速度进行同步更新,所以不使用spring-boot-starter-data-elasticsearch
      <dependency>
          <groupId>org.elasticsearch.client</groupId>
          <artifactId>elasticsearch-rest-high-level-client</artifactId>
      </dependency>
      
      • 客户端代码:
      @Test
      void test() throws IOException {
          HttpHost host = HttpHost.create("http://localhost:9200");
          RestClientBuilder builder = RestClient.builder(host);
          RestHighLevelClient client = new RestHighLevelClient(builder);
          // 客户端操作
          CreateIndexRequest request = new CreateIndexRequest("books");
          // 获取操作索引的客户端对象,调用创建索引操作
          client.indices().create(request, RequestOptions.DEFAULT);
          // 关闭客户端
          client.close();
      }
      

tips:

  • 如果启动不了Redis的服务端,则需要先启动客户端,然后执行shutdown退出客户端,再去启动服务端即可
  • 在安装Mongo时可能会报找不到某个dll文件,则需要:
    • 下载对应的dll文件
    • 拷贝到windows安装路径下的system32目录中
    • 执行命令注册对应dll文件:regsvr32 xxxx.dll

整合第三方技术
  • 缓存:

    • 概念:

      • 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质
      • 使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘IO),提高系统性能
      • 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间(即存储代码运行过程中生成的数据)
    • 使用:以默认的缓存方案为例(还有RedisEhcachememcached等其他缓存技术,但是接口是统一的)

      • 导入缓存技术对应的starter
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-cache</artifactId>
      </dependency>
      
      • 启用缓存:
      @SpringBootApplication
      @EnableCaching  // 开启缓存
      public class CacheApplication {
          public static void main(String[] args) {
              SpringApplication.run(CacheApplication.class, args);
          }
      }
      
      • 设置当前操作的结果数据进入缓存:
      // value指定缓存放在哪里,key表示下次用什么去找该缓存好的数据
      @Cacheable(value="cacheSpace", key="#id")  
      public Book getById(Integer id) {  // 初次执行会去数据库中查,后面不会,直接从缓存中取
          return bookDao.selectById(id);
      }
      
  • 任务:

    • 概念:

      • 定时任务是企业级应用中的常见操作,比如制作年度报表、缓存统计报告等

      • 市面上流行的定时任务技术有QuartzSpring Task

      • 相关词汇:

        • 工作(Job):用于定义具体执行的工作
        • 工作明细(JobDetail):用于描述定时工作相关的信息
        • 触发器(Trigger):用于描述触发工作的规则,通常使用cron表达式定义调度规则
        • 调度器(Scheduler):描述了工作明细与触发器的对应关系
    • 使用:

      • SpringBoot整合Quartz

        • 导入SpringBoot整合quartz的坐标:
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        
        • 定义具体要执行的任务,继承QuartzJobBean
        // 具体的工作,只需要声明为普通的类,不需要给Spring管理
        public class QuartzTaskBean extends QuartzJobBean {
            @Override
            protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
                System.out.println(“quartz job run... ");
            }
        }
        
        • 定义工作明细与触发器,并绑定对应关系:
        @Configuration
        public class QuartzConfig {
            @Bean
            public JobDetail printJobDetail(){
                // 绑定具体的工作
                return JobBuilder.newJob(QuartzTaskBean.class).storeDurably().build();
            }
        
            @Bean
            public Trigger printJobTrigger() {
                CronScheduleBuilder cronScheduleBuilder = 
                    CronScheduleBuilder.cronSchedule("0/3 * * * * ?");
                return TriggerBuilder
                    .newTrigger()
                    .forJob(printJobDetail())
                    .withSchedule(cronScheduleBuilder).build();
            }
        }
        
      • Spring Task

        • 开启定时任务功能:
        @SpringBootApplication
        @EnableScheduling
        public class TaskApplication {
            public static void main(String[] args) {
                SpringApplication.run(TaskApplication.class, args);
            }
        }
        
        • 设置定时执行的任务,并设定执行周期:
        @Component
        public class ScheduledBean {
            @Scheduled(cron = "0/5 * * * * ?")
            public void printLog(){
                System.out.println(Thread.currentThread().getName()+":run...");
            }
        }
        
        • 定时任务相关配置:
        spring:
          task:
            scheduling:
            #任务凋度线程池大小,默认为1
            pool:
              size: 1
            #调度线程名称前缀,默认为scheduling-
            thread-name-prefix: xxx_
            shutdown:
              # 线程池关闭时等待所有任务完成
              await-termination: false
              # 调度线程关闭前最大等待时间,确保最后一定关闭
              await-termination-period: 10s
        
  • 消息:

    • 概念:

      • 处理消息的双方:分为消息发送方(生产者)和消息接收方(消费者)
      • 消息分类:异步(不需要对方进行确认后也能接着发)和同步(需要对方进行确认后才能接着发)消息
      • 消息队列:单个业务系统处理所有请求的压力太大,所以将每个请求转换为一个个的消息并存储在消息队列(MQ)中,而子业务系统则从中获取消息去处理。其中所有MQ技术均需要遵循JMSAMQPMQTT中的规范或协议

      在这里插入图片描述

    • JMS(Java Message Service):一个规范,等同于JDBC规范,提供了与消息服务相关的API接口

      • JMS消息模型:
        • peer-2-peer:点对点模型,消息发送到一个队列中后队列保存消息。队列的消息只能被一个消费者消费,或超时
        • publish-subscribe:发布订阅模型,消息可以被多个消费者消费,生产者和消费者完全独立,不需要感知对方的存在
      • JMS消息种类:
        • extMessage
        • MapMessage
        • BytesMessage
        • StreamMessage
        • ObjectMessage
        • Message (只有消息头和属性)
      • JMS实现:ActiveMQRedisHornetMQRabbitMQRocketMQ(没有完全遵守JMS规范)
    • AMQP(advanced message queuing protocol):一种协议,规范了网络交换的数据格式,兼容JMS具有跨平台性,服务器供应商、生产者和消费者可以使用不同的语言来实现

      • AMQP消息模型:
        • direct exchange
        • fanout exchange
        • topic exchange
        • headers exchange
        • system exchange
      • AMQP消息种类:byte[]
      • AMQP实现:RabbitMQStormMQRocketMQ
    • MQTT(Message Queueing Telemetry Transport):消息队列遥测传输,专为小设备设计,是物联网生态系统中主要成分

  • 监控:

    • 意义:
      • 监控服务状态是否宕机
      • 监控服务运行指标(内存、虚拟机、线程、请求等)
      • 监控日志
      • 管理服务(服务下线)
    • 实施方式:
      • 显示监控信息的服务器:用于获取服务信息(可以主动拉取,也可以被动获取),并显示对应的信息
      • 运行的服务:启动时主动上报,告知监控服务器自己需要受到监控(告知服务器自己是否需要被监控,哪些部分被监控)
    • 原理:
      • Actuator提供了SpringBoot生产就绪功能,通过端点(其实就是请求,每个请求可以获取到不同的监控信息)的配置与访问,获取端点信息
      • 端点描述了一组监控信息,SpringBoot提供了多个内置端点,也可以根据需要自定义端点信息
      • 访问当前应用所有端点信息:/actuator;访问端点详细信息:/actuator/端点名称(比如访问/actuator/health获取到应用程序的健康信息)

注意事项


  • @EnableConfigurationProperties可将使用@ConfigurationProperties对应的类加入Spring容器,所以@EnableConfigurationProperties@Component不能同时使用
// 之所以将@Component注释是因为在主程序中用@EnableConfigurationProperties将ServerConfig类完成了注入,再加会报错
// @Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {...}

// 主程序
@SpringBootApplication
@EnableConfigurationProperties(ServerConfig.class)
public class DemoApplication {}
  • IDEA有时会报识别不了@ConfigurationProperties注解,可添加下面依赖接触警告:
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
  • yml文件中对于数字的定义支持进制书写格式,如需使用字符串请使用引号明确标注,要不然会出现以下情况:
# 配置文件
dataSource:
password: 0127  # 需要写为"0127"
@Value("${dataSource.password}") 
private String password;

// 测试代码
@Test
void contextLoads(){
  System.out.println(password);  // 输出87: 因为0127会被识别为二进制,然后转换为十进制输出
}

资料

视频参考黑马程序员SpringBoot2全套视频教程 P70-P142


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值