SpringBoot初探!

SpringBoot


心态:如何学习新东西,如何持续学习,如何关注这个行业。

面试中的软实力!

聊天+举止+谈吐+见解。

学习底层虽然效果来得慢,但它是谈资!

很多人说不学spring直接学springboot也是可以的,但也有人说学习要渐进。我选择了后者,在花了一个学期的时间学习前置知识,比如数据库sql,spring思想等,最后用SSM的知识做了一个小项目。在学习springboot的时候,我越发觉得前面那些知识的重要,通过对比学习更体会到springboot对spring的简化。


一、基本概念
  1. 简化的原理:约定大于配置

  2. SpringBoot不是新的框架,它默认配置了很多框架的使用方式,就像Maven整合了所有的jar包,SpringBoot整合了所有的框架

  3. 优点:

    • 更快入门
    • 开箱即用,提供各种默认配置来简化项目配置
    • 内嵌式窗口简化web项目
    • 没有冗余代码生成和xml配置的要求
  4. 微服务:

    从all in one(一个war包完整复制到各个服务器,分流),到把各个模块放到拆分到各个服务器,各个模块互不影响,高内聚,低偶合。和iso七层模型有异曲同工之妙。

  5. 微服务文章:

    http://martinfowler.com/articles/microservices.html

    https://www.cnblogs.com/liuning8023/p/4493156.html

二、第一个SpringBoot程序
1、创建工程方式
  1. 在官网中下载zip包,然后用idea打开。https://start.spring.io/(记得勾选web依赖)
  2. 在idea快速创建。实际上,idea是集成了官网,是idea帮你在官网创建了。
2、小玩意
  1. 改变端口号:server.port=8081
  2. 改变初始图标:在resources根目录下,创建banner.txt文件,在https://www.bootschool.net/ascii-art网站得到自己喜欢的ascii图样,复制到txt文件内,就可以直接改变。
三、原理集合
1、自动装配原理

(更准确地说是@SpringBootApplication注解做了什么,其中包含了自动装配的原理)

  • 根据注解层次来看就行了。
  • 第一级,分三个部分,自动扫描,说明组件,及最重要的是它是一个自动配置。
  • 在自动配置分支下,@import导入了一个最重要的自动配置选择器。
  • 选择器中,有一个得到候选配置的方法,会在spring-boot-autoconfigure-2.4.2.jar的META-IF中的spring.factories文件中找到所有的自动配置类
  • 自动配置类不一定生效,有@ConditionalOnxxx注解限制生效。这里也是starter的原理由来
  • 每个xxxAutoConfiguration类必定会搭配一个xxxProperties类,而xxxProperties类中的默认配置可以通过yaml主配置文件更改。
注解层次关系
SpringBootApplication
ComponentScan
自动扫描主入口下同级及子级的组件
SpringBootConfiguration
Configuration
Component说明是一个组件
EnableAutoConfiguration
AutoConfigurationPackage
扫描到的自动配置类在这里注册
Import AutoConfigurationImportSelector.class
getCandidateConfigurations方法得到候选配置
META-INF/spring.factories
xxxAutoConfiguration所有默认自动配置类都在这个文件中声明
xxxProperties类 所有都有一个对应
ConditionalOnxxx候选条件成立自动配置才生效
2、run主程序启动原理

开启了一个服务,不是简单运行一个方法!

  1. 推断应用的类型是普通的项目还是web项目
  2. 查找并加载所有可用的初始化器,设置到initializers属性中
  3. 找出所有的应用程序监听器,设置到listeners属性中
  4. 推断并设置main方法的定义类,找到运行的这一类
3、配置文件原理

这也是springboot自动装配原理的一部分。

三者层次关系如下:

  • 一般在构造方法中或者@EnableConfigurationProperties注解中可以找到对应的xxxProperties类
  • 在springboot装配xxxAutoConfiguration类的时候,会先找到xxxProperties类中的默认配置
  • 如果在配置文件中有改变默认配置,则加载配置文件中的配置
  • 在xxxProperties类中,肯定有一个这样的类似注解:@ConfigurationProperties(prefix = “server”),作用是识别配置文件中的前缀"server", 把这个类绑定到配置文件中相关前缀。(具体看yaml赋值应用) 如server.port = 8081, 则是设置了xxxProperties类中的port属性。(server是自定义的前缀),里面调用了setPort()方法。
xxxAutoConfituration
xxxProperties
配置文件
4、MVC扩展原理
  • 组件配置法:其实,如果用户没有配置bean(组件),那么会使用默认的bean配置;如果用户配置了,就用用户配置的;如果允许存在多个bean,那么会组合起来用。
  • 重写接口法:重写WebMvcConfigurer接口方法,可以扩展
  • 不能加@EnableWebMvc。原因如下
    • @EnableWebMvc上一级注解中,有@Import({DelegatingWebMvcConfiguration.class}),即导入了DelegatingWebMvcConfiguration类
    • 而WebMvcAutoConfiguration中有@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),即WebMvcConfigurationSupport类不存在bean中,自动配置类才生效。而DelegatingWebMvcConfiguration是WebMvcConfigurationSupport的子类
    • 结论:加了@EnableWebMvc会使mvc自动配置全面失效
  • 公司自研starter,思路之一就是利用@ConditionalOnxxx注解
  • 在springboot中,有非常多的xxxConfiguration(如MyMvcConfiguration),会帮助我们进行扩展配置,只要看到这个东西,就要注意他改变了默认的什么东西
//配置类,并实现了mvc的配置接口
@Configuration
public class MyMvcConfiguration implements WebMvcConfigurer {

    //把特定的功能类添加到bean即可
    @Bean
    public ViewResolver viewResolver() {
        return new MyViewResolver();
    }

    //要扩展,只需写一个类,实现了某个特定的接口
    public static class MyViewResolver implements ViewResolver {

        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }

    //alt+insert
    //方法二:实现接口
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/hello").setViewName("index");
    }
}
四、yaml基础
1、基本语法

基本语法:对空格的要求极其严格

key:空格value

对象:

#写法一
student:
 name: hao
 age: 3
 #写法二
 student: {name: hao,age: 3}
 #propetiest,只能保存键值对
 student.name=hao
 student.age=3

数组

pets:
 - cat
 - dog
 - pig
 
pets: [cat,dog,pig]
2、赋值应用
  1. 应用一:把配置写在yaml中,然后注入到配置类
  2. 应用二:把yaml中的值写入实体类中
  3. 总的来说,都是把类和配置文件绑定在一起
<!--导入这个依赖后,写配置会有提示-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

yaml文件:

dog:
  name: nick
  age: 3
person:
  name: tony
  age: 30
  birthday: 2020/03/01
  list:
    - love
    - lucky
    - apple
  #这里注意一下map的绑定方式
  map: {k1: v1,k2: v2,k3: v3}
  dog:
    name: nickkk
    age: 3

Person:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private int age;
    private Date birthday;
    private List<String> list;
    private Map<String, Object> map;
    private Dog dog;
}

Dog:

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "dog")
public class Dog {
    private String name;
    private int age;
}

通过configurationProperties注解和prefix值来指定注入,然后用Autowried来绑定。

而原先spring是采用@value("")来实现的

@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Component
public class Dog {
    @Value("${dog.name}")//这里用特定值的绑定
    private String name;
    private int age;
}
3、其它注解
  1. @Value(“spring的EL表达式”),从配置文件中拿值
  2. @PropertySource(value = “classpath:文件名”),加载指定的配置文件。配合@value来使用
五、校验
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在实体类上@Validated,在要校验的值中加上自己要校验的注解。具体的在导入的依赖包中的javax-validation-constraints中 自己查看

六、配置文件优先级及多环境

四个位置可以放配置文件:(优先级从大到小)

  1. 项目根目录/config/application.yaml
  2. 项目根目录下/application.yaml
  3. resources(类路径)/config/applicaiton.yaml
  4. resources(类路径)/application.yaml

得出结论:springboot默认给我们的是最低级的配置文件!这样可以在不同的生产环境中覆盖!

在不同环境中:

可以有三个properties配置文件:

  1. application.properties
  2. application-dev.properties
  3. application-test.properties

多环境配置:

在默认配置文件中:

spring.profiles.active=dev

还可以用yaml的多模块性:

server:
 port: 8081
spring:
 profiles:
  active: dev #在这里选择哪个环境,不写就默认
---
server:
 port: 8082
spring:
 profiles: dev #取个名字
---
server:
 port: 8083
 profiles: test
七、实战
1、静态资源位置

在WebMvcAutoConfiguration类中获得以下信息:

  • webJars
  • resources/public, static, resources, /**
  • 优先级:resources>static(默认)>public
  • 自定义:spring.mvc.static-path-pattern=classpath:/路径

补充:src目录(编译后生成的clases目录)和resources目录(会被直接放在classes目录下)都是classpath路径

2、首页问题
  1. 首页名为index.html要放在public,resources,static
  2. templates目录要通过controller才能访问到
3、thymeleaf语法
<div th:text="${message}"></div>
<div th:utext="${message}"></div>
<div th:each="user:${users}">
    [[${user}]]
</div>
<div th:each="user:${users}" th:text="${user}">
    
</div>

八、真正的实战
1、国际化
  1. 在resources目录下创建i18n,下面创建login.properties和login_zh_CN.properties和login_en_US.properties
  2. 在主配置文件中配置spring.mvc.message.base=i18n.login
  3. 自己实现一个LocateResover,并注入到bean中
  4. 用thymeleaf的#语法,直接读(因为在主配置已经配置过了)
2、自己配置controller

在@Configuration类即java配置类中,实现WebMvcConfigurer, 然后重写addViewControllers,

里面registry.addViewController("/hello").setViewName(“index”)

3、自定义404

在templates文件夹下,创建一个error文件夹,然后创建404.html, 报404错误后,直接跳到这里。如果是500错误,则创建一个500.html文件

九、数据源
1、jdbc

启动器

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

dataSource接口

@SpringBootTest
class Boot1ApplicationTests {

    //默认数据源为com.zaxxer.hikari.pool.HikariProxyConnection
    //得到这个接口,那么所有的操作就和普通的jdcb一样了
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        Connection connection = dataSource.getConnection();
        System.out.println("数据源为:" + connection.getClass());
        Statement statement = connection.createStatement();
        String sql = "select * from music_shop.song";
        ResultSet resultSet = statement.executeQuery(sql);
        while (resultSet.next()) {
            System.out.println(resultSet.getString(2));
        }
    }
}

Spring模板

在之前的Javaweb学习中,学习了手动封装JdbcTemplate,其好处是通过(sql语句+参数)模板化了编程。而真正的JdbcTemplate类,是Spring框架为我们写好的。它是 Spring 框架中提供的一个对象,是对原始 Jdbc API 对象的简单封装。除了JdbcTemplate,spring 框架还为我们提供了很多的操作模板类。

@SpringBootTest
class Boot1ApplicationTests {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    void contextLoads() throws SQLException {
        String sql = "select * from music_shop.song";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        for (Map<String, Object> map : maps) {
            System.out.println(map);
        }
    }
}
2、Druid初探

利用DataSource接口

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.2</version>
</dependency>
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/music_shop?useSSL=true&useUnicode=true&charactEncoding=true
    driver-class-name: com.mysql.jdbc.Driver
    #把数据源类型更改一下就行,其他所有操作不变
    type: com.alibaba.druid.pool.DruidDataSource
@SpringBootTest
class Boot1ApplicationTests {

    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        Connection connection = dataSource.getConnection();
        System.out.println(connection.getClass());
        //class com.alibaba.druid.pool.DruidPooledConnection

    }
}

自定义配置

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/music_shop?useSSL=true&useUnicode=true&charactEncoding=true
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    # druid 配置
    dbType: mysql   # 指定数据库类型 mysql
    initialSize: 5  # 启动初始化连接数量
    minIdle: 5      # 最小空闲连接
    maxActive: 20   # 最大连接数量(包含使用中的和空闲的)
    maxWait: 60000  # 最大连接等待时间 ,超出时间报错
    timeBetweenEvictionRunsMillis: 60000  # 设置执行一次连接回收器的时间
    minEvictableIdleTimeMillis: 300000   # 设置时间: 该时间内没有任何操作的空闲连接会被回收
    validationQuery: select 'x'         # 验证连接有效性的sql
    testWhileIdle: true             # 空闲时校验
    testOnBorrow: false  # 使用中是否校验有效性
    testOnReturn: false  # 归还连接池时是否校验
    poolPreparedStatements: false  # mysql 不推荐打开预处理连接池
    filters: stat,wall,logback  #设置过滤器 stat用于接收状态,wall防止sql注入,logback说明使用logback进行日志输出
    userGlobalataSourceStat: true  # 统计所有数据源状态
    connectionProperties: druid.stat.mergSql=true;druid.stat.slowSqlMillis=500  # sql合并统计 设置慢sql时间为500,超过500 会有记录提示
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){

        return  new DruidDataSource();
    }


    // 注册后台监控界面
    @Bean
    public ServletRegistrationBean servletRegistrationBean(){
        // 绑定后台监控界面的路径  为localhost/druid
        ServletRegistrationBean bean=new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
        Map&lt;String,String&gt;map=new HashMap&lt;&gt;();
        // 设置后台界面的用户名
        map.put("loginUsername","guoxw");
        //设置后台界面密码
        map.put("loginPassword","123456");
        // 设置那些ip允许访问," " 为所有
        map.put("allow","");
        // 不允许该ip访问
        map.put("deny","33.32.43.123");
        bean.setInitParameters(map);
        return bean;

    }

    // 监听获取应用的数据,filter用于收集数据,servlet用于数据展示

    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean bean=new FilterRegistrationBean();
        // 设置过滤器
        bean.setFilter(new WebStatFilter());
        // 对所有请求进行过滤捕捉监听
        bean.addUrlPatterns("/*");
        Map&lt;String,String&gt;map=new HashMap&lt;&gt;();
        // 排除 . png  .js 的监听  用于排除一些静态资源访问
        map.put("exclusions","*.png,*.js");
        bean.setInitParameters(map);
        return bean;

    }
3、Mybatis

重要配置

#mybatis配置
mybatis:
  type-aliases-package: com.hao.pojo #给类起别名
  mapper-locations: classpath:mybatis/mapper/*.xml #说明mapper.xml文件位置

mapper接口

@Mapper//说明是一个mybatis mapper类
@Repository//交给spring管理
public interface SongMapper {
    List<Song> getAllSongs();
}

xml文件

resources目录下的mybatis下的mapper目录下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hao.mapper.SongMapper">
    <select id="getAllSongs" resultType="Song">
        select * from song;
    </select>
</mapper>
十、安全
1、SrpingSecurity

Spring Security是针对Spring项目的安全框架,也是SpringBoot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。

  • WebSecurityConfigurerAdapter: 自定义Security策略
  • AuthenticationManagerBuilder: 自定义认证策略

Authentication认证 Authorization授权

用户认证与授权

//一定不能忘了加这个注解,否则配置无效
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //添加请求认证
        http.authorizeRequests()
                .antMatchers("/", "/index", "/index.html").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");
        
        //定制登录功能
        http.formLogin()//没认证,则跳到登录页
            .loginPage("/toLogin")//自定义首页
            .usernameParameter("user")//首页中接收用户名的参数名即name属性
            .passwordParameter("password")//首页中接收密码的参数名即name属性
            .loginProcessingUrl("login");//登录时,表单要发送的请求地址
        //注销,注销成功后跳到首页
        http.logout().logoutSuccessUrl("/");
        //记住我。如果自定义了首页,则自定义的首页中可以加入一个checkbox, 并把name属性改为和下面指定的一样就可以了。
        //如果没有自定义首页,则不用加.remeberMeParameter方法
        http.rememberMe().rememberMeParameter("rememberMe");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //从内存中取账号密码
        //密码在新版本中要使用编码,否则报错
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhong").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
                .and()
                .withUser("wen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
                .and()
                .withUser("hao").password(new BCryptPasswordEncoder().encode("123456")).roles("vip3");
    }
}
2、Shiro
十一、Swagger

优点:

  • 可以给接口或者实体类中标注注释
  • 接口文档可以实时更新
  • 可以在线测试
  • 可以分组,后端人员职责明确。前端人员定位错误更快。
1、初始

maven:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.1</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

加入配置类:

@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
2、配置主界面信息
@Configuration
@EnableSwagger2
public class SwaggerConfig {

    //配置了Swagger的Docket的bean实例
    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        Contact contact = new Contact("wenhao", "", "");
        return new ApiInfo(
                "API文档",
                "你介绍",
                "v1.0",
                "http://xxx",
                contact,
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE",
                new ArrayList<>()
        );
    }
}
3、获得生产环境
Profiles profiles = Profiles.of("dev", "test");
boolen flag = environment.accptsProfiles(profiles);
4、扫描接口
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket() {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            //select下面的是用来扫描接口用的
            .select()
            //RequestHandlerSelectors有多种方法可以选择扫描方式
            .apis(RequestHandlerSelectors.basePackage("com.hao.controller"))
            //过滤接口路径
            .paths(PathSelectors.ant("/hello"))
            .build();
}
5、标注注解

实体类标注:

注意:控制器接口方法中,必须要用到实体类才能被扫描到。

原因:swagger本身就是为接口服务的,接口中没有实体类,那还扫描干嘛?

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("歌曲实体类")
public class Song {

    @ApiModelProperty("歌曲实体类的名字")
    private String name;
}

标注接口:

@ApiOperation("欢迎控制器接口方法")
@RequestMapping("/")
//如果参数是标注在实体类上,swagger会把实体类解析成实体类的字段
//比如,song有一个name字段,那么,会显示需要name参数
public String hello(@ApiParam("歌曲对象")Song song, Model model) {
    model.addAttribute("message", "hellowo");
    return "index";
}
6、分组

创建多个docket实例对象即可

@Bean
public Docket docketA() {
    return new Docket(DocumentationType.SWAGGER_2)
            .groupName("A");
}
@Bean
public Docket docketB() {
    return new Docket(DocumentationType.SWAGGER_2)
            .groupName("B");
}
十二、任务
1、异步任务
@Async //标注在要开启异步的方法上
@EnableAsync //标注在主入口上,开启异步注解功能
2、邮件任务

yaml:

spring:
  mail:
    username: xxx@qq.com
    password: xxxx
    host: smtp.qq.com
    properties: #QQ邮箱特有的配置属性
      mail:
        smtp:
          ssl:
            enable: true

简单邮件:

@Test
void contextLoads() throws SQLException {
    SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
    simpleMailMessage.setSubject("这是主题:xxx");
    simpleMailMessage.setText("这是内容:xxx");
    simpleMailMessage.setTo("xx@qq.com");
    simpleMailMessage.setFrom("xxx@qq.com");
    javaMailSender.send(simpleMailMessage);
}

复杂邮件:

@Test
void contextLoads() throws SQLException, MessagingException {
    MimeMessage mimeMessage = javaMailSender.createMimeMessage();
    //true代表能发送复杂文件
    MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);

    mimeMessageHelper.setSubject("这是主题");
    mimeMessageHelper.setText("<h style='color:purple'> 能解析html </h>", true);

    mimeMessageHelper.addAttachment("这是附件的名称.jpg", new File("C:\\Users\\aj\\Desktop\\123.JPG"));

    mimeMessageHelper.setTo("xxx@qq.com");
    mimeMessageHelper.setFrom("xxx@qq.com");

    javaMailSender.send(mimeMessage);
}
3、定时任务
两个接口:
    TaskScheduler 任务调度者
    TaskExecutor  任务执行者
两个注解:
    @EnableScheduling //标注在程度主入口,开启定时功能
    @Scheduled(cron = "crom表达式")//表示什么时候执行
十三、分布式

四个核心问题:

  1. 这么多服务,客户端该如何去访问
  2. 这么多服务,服务之间如何进行通信
  3. 这么多服务,如何治理
  4. 服务挂了,怎么办

**问题本质:**网络不可靠

解决的方法:

  1. API网关,服务路由
  2. HTTP,RPC框架,异步调用
  3. 服务注册与发现,高可用
  4. 熔断机制,服务降级
1、rpc与分布式
2、dubbo与zookeeper

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Meow_Sir

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

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

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

打赏作者

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

抵扣说明:

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

余额充值