第4章 Spring Boot的Web应用开发入门

Spring 框架不断在Web开发领域发展,由于Spring兼容了各种常用的(无论过时与不过时)Web组件,并且这些组件使用时需要自己配置,导致Spring Web开发越来越复杂,学习曲线越来越陡峭。而Spring Boot将传统Web开发中的mvc、validation、tomcat等框架汇总在一起整合,形成了Spring Boot的Web组件即 spring-boot-starter-web。spring-boot-starter-web组件内嵌了tomcat以及Spring MVC的依赖,使得开发人员可以非常简单的完成web开发环境的配置。

Spring Boot搭建Web应用开发环境

使用Spring Boot框架搭建Web应用开发环境很简单,一般有以下种方法

方法1:在项目向导中选择Web应用开发

在使用IDEA创建项目向导中,在选择依赖的界面选择Web然后选择Spring Web

这样创建的项目中就会自动添加spring-boot-starter-web依赖组件,从而为Web应用开发提供支持

方法2:https://start.spring.io/创建项目时添加Web应用

点击右侧的添加依赖按钮,然后选择Web组件

方法3:手动在pom.xml文件中添加依赖组件

在pom.xml添加以下依赖,待相关组件导入之后,项目便可以进行Web开发了

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

Web项目目录结构

Spring Boot的Web应用开发目录和其他应用开发目录相比在resource目录下多了2个目录。

一个是static目录用于存放静态资源,比如图片、视频等。

另一个是templates页面模板目录,用于存放页面模板。

最简单的Web请求实现

其实我们在前面的章节中已经接触到了Spring Boot如何实现最简单的Web请求,就是在Web页面上输出“helloworld”。

@RestController
public class HelloworldController {
 @RequestMapping("/helloworld")
 public String helloworld() {
  return "helloworld";
}
}
 

然后我们通过“http://localhost:8080/helloworld”的URL访问页面可以看到helloworld的内容,说明应用程序已经响应了web请求。

这里可能有人已经注意到了,Spring Boot不像其他Web框架那样要求必须继承某个Web类才能处理http请求。对于Spring Boot来说只要在类上声明@Controller 注解,这就告诉框架这个类是一个控制器类,然后在该类具体提供服务的方法上使用@RequestMapping 注解将路径和响应该路径请求的方法关联上即可。

Spring Boot和Web相关的注解

从前面例子可以看出Spring Boot对Web应用开发主要通过注解实现。接下来对Spring Boot中与Web有关的注解进行介绍,Spring Boot中与Web有关的注解主要有

  • @Controller

  • @ResponseBody

  • @RestController

  • @RequestMapping

  • @PathVariable

  • @RequestParam

@Controller

Controller是SpringBoot里最基本的注解,它的作用是通知框架这是一个控制器能把用户请求通过对URL的匹配,分配给不同的接收器,再进行处理,然后向用户返回结果。

@Controller
public class IndexController {
  @RequestMapping("/index")
  @ResponseBody
  public String index() {
    return "index";
  }
}

上面的例子用于请求/index地址,返回包含“index”字符串的页面。

@ResponseBody

一般是使用在方法上,需要哪个方法返回json数据格式,就在哪个方法上使用。如果一个@Controller 类中,如果只要返回数据到前台页面,则需要使用@ResponseBody 注解。

@Controller
@RequestMapping("/person")
public class PersonController {
  @RequestMapping("/xiaoming")
  @ResponseBody
   public String person() {
      return "person, name xiaoming, age 18";
   }
}
 

@RestController

@RestController作用是,实现数据请求同时返回JSON格式的数据,可以看作是@Controller 和@ResponseBody的结合体。

//@Controller
@RestController
@RequestMapping("/person")
public class PersonController {

    @RequestMapping("/getPerson")
    public String getUser() {
        Person person = new Person();
        person.setName("xiaoqiang");
        person.setAge(10);
        return person.toString();
    }
}
 

​@RequestMapping

该注解的主要任务是对访问URL进行路由映射。@RequestMapping可以添加在Controller类或者方法上,如果添加在Controller类上,则这个Controller中所有访问URL都要先加上该路径规则。比如

@controller 
@RequestMapping("/person")
public class PersonController {
...
 

则访问PersonController下的方法都需要加上person前缀, "/person/fun1", "/person/fun2"。

  • RequestMapping注解有很多属性参数来定义http的请求映射规则:
  • value: 映射请求URL的路径, 支持URL模板、通配符、正则表达式
  • method: 指定HTTP请求的方法
  • consumes: 允许的媒体类型,如consumes="application/json"为HTTP的Content-Type
  • produces: 相应的媒体类型, 如consumes="application/json"为HTTP的Accept字段
  • params:请求参数
  • headers:请求的值

URL路径匹配

RequestMapping中的Value主要用于URL匹配,value支持表达式

完全匹配

假设有如下代码,用户需要用 /getPersonAll 路径访问

@RequestMapping("/getPersonAll")
public String getPersonAll() {
       return "getPersonAll";
}
另外value支持表达式,此时一般会和@PathVariable 一起使用
@RequestMapping("/getPersonByNo/{id}")
public String getPersonByNo(@PathVariable("id") Long id) {
   return "getPersonByNo:" + id;
} 
 

PathVariable注解用在方法参数中时,用于表示参数的值是从URL路径字符串中获取的。例如上面就表示id的值来自于URL中{id}匹配到的部分。另外如果URL中的参数名称与方法中的参数名称一致,则可以简化为

public String getPersonByNo(@PathVariable Long id)

通配符匹配

@RequestMapping(value = "/{name:([a-z][0-9a-z-]{3,31}}/**")
public void getPerson(@PathVariable String name){
 
}
 

表示name满足4--32位以字母开头的字母与数字字符

RequestMapping 中value参数支持各种正则表达,包括常见的通配符

"*"一个星号匹配人员字符, "**"两个星号匹配任意路径,"?"一个问号匹配单个字符。严格匹配高优先级高于通配匹配,有通配符的优先级第一没有通配符的, 比如 /getPerson/xiaoming 比/getPerson/ 优先匹配.

Method参数

HTTP请求Method有Get、POST、PUT、DELETE、PATCH等方式

GET请求:是指从指定的资源请求数据。完整请求一个资源,用于获取数据,GET方法查询的字符串以“名称/值”对形式存放在在GET请求的URL中发送的,如:/test/test.html?para1=value1&parm2=value2。

POST请求:是向指定的资源提交要被处理的数据(提交表单)(新建资源、更新资源),注意post查询字符串(名称/值对)是在post请求的http消息主体中发送的:

POST /test/test.html HTTP/1.1

Host: ...

para1=value1&para2=value2

PUT请求: 用来更新资源put的侧重点在于对于数据的修改操作,PUT请求是向服务器端发送数据的,从而改变信息,该请求就像数据库的update操作一样,用来修改数据的内容,但是不会增加数据的种类等,也就是说无论进行多少次PUT操作,其结果并没有不同。

DELETE请求:用于请求服务器删除所请求URI所标识的资源。DELETE请求后指定资源会被删除。

PATCH请求:与PUT请求类似,同样用于资源的更新。二者有以下两点不同:但PATCH一般用于资源的部分更新,而PUT一般用于资源的整体更新,当资源不存在时,PATCH会创建一个新的资源,而PUT只会对已在资源进行更新。

@RequestMapping注解提供了method参数指定请求的Method类型,如RequestMethod.GET,RequestMethod.POST,RequestMethod.DELETE, RequestMethod.PUT等值

@RequestMapping(value="/getdata", method=RequestMethod.GET)
public String getData() {
   return "GET Request";
}

@RequestMapping(value="/getdata", method=RequestMethod.POST)
public String postDate() {
   return "POST Request";
}   
 

当使用GET方式请求/getdata路径接口时,应用程序返回"GET Request",当使用POST方式请求/getdata接口时,则返回“POST Request”,这样就通过method参数完成了不同的映射从而实现差异化的服务。

consumes,produces参数

该参数表示请求的HTTP头的Content-Type媒体类型与consumes的值匹配才会匹配调用该方法。表示HTTP请求中的Accept字段只有匹配成功才可以调用。

@RequestMapping(value="/test", method=RequestMethod.POST, consumes="application/json")
public String Consumes(@RequestBody Map param) {
   return "consumes POST application/json";
}
 

params,headers参数

params: 指定request中必须包含某些参数值时,才让该方法处理。

headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求

@RequestMapping(value = "testParamsAndHeaders", params = { "username","age!=10" })
    public String testParamsAndHeaders() {
        return "testParamsAndHeaders";
    }
 

设定必须包含username 和age两个参数,且age参数不为10 时才允许调用,testParamsAndHeaders处理

@RequestMapping(value = "testParamsAndHeaders", params = {"username","age!=10"}, headers = { "Host=localhost" })
    public String testParamsAndHeaders() {
        return "testParamsAndHeaders";
    }
 

这要求HTTP的header头中有Host=localhost参数才可以执行testParamsAndHeaders处理。

@PathVariable

带占位符的 URL 是 Spring3.0 新增的功能,通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中。

@RequestMapping("/getPersonByName/{name}")
public String getPersonByName(@PathVariable("name") String name) {
   return "getPersonByName:" + name;
} 

{xxx}占位符相当于定义了一个名为xxx的变量,当URL中有相应的填充字符时相当于为这个变量赋值了,而通过PathVariable可以从URL中取出这个变量,就像上面的代码中,如果URL为/getPersonByName/xiaoming那PathVariable取出的变量就是name=xiaoming,最后getPersonByName返回的值就是“getPersonByName:xiaoming”

前面的例子是URL传递单个变量,同样RequestMapping也支持多个变量传递

@RequestMapping("/getPersonByName/{name}/age/{d}")
public String getPersonByName(@PathVariable String name, @PathVariable int d) {
   return "getPersonByName:" + name + "and age is:" + d;
} 
 

注意这里RequestMapping中传递了两个参数{name}和{d},同时由于getPersonByName函数参数中的变量名和URL中定义的变量名相同,则PathVariable注解可以简写成上面的的形式,并且PathVariable注解的参数支持基本数据类型的转换,如string,int,long等

@RequestParam

@RequestParam 也是获取请求参数的,它和 @PathVariable 的主要区别是

@PathValiable 是从 URL 模板中获取参数值:http://localhost:8080/person/{id};

而 @RequestParam 是从 Request 里获取参数值:http://localhost:8080/person?id=1。

RequestParam有三个参数

  1. value / name:请求参数中的名称 (必写参数)

  2. required:请求参数中是否必须提供此参数,默认值是true,true为必须提供

  3. defaultValue:默认值

@RequestMapping("/person")
public String testRequestParam(@RequestParam(value = "id", required = false) Integer id) {
       return "person id is" + id;
}
 

RequestParam注解还可以用于 POST 请求,如果前端提交过了的参数不多的话,可以用RequestParam接收前端表单提交的参数,假如前端通过表单提交 username 和 password 两个参数,那我们可以使用 @RequestParam 来接收

@PostMapping("/login")
public String testForm(@RequestParam String username, @RequestParam String password) {
    System.out.println("username:" + username);
    System.out.println("password:" + password);
    return "SUCCESS";
}
 

前面说了如果参数不多的话,在通过RequestParam从URL中一一获取参数比较方便,但如果表单一次提交数据太多,则用RequestParam比较繁琐,这时可以封装一个实体类(Java Bean)来存储数据

public class User{
    private String username;
    private String password;
    // set get省略
}

使用实体接收的话,我们不必参数在前面加一一加上 @RequestParam 注解,直接使用即可,Spring Boot会帮我们处理细节。

@PostMapping("/login")
public String testForm(User user) {
    System.out.println("username:" + username);
    System.out.println("password:" + password);
    return "SUCCESS";
}
 

拦截器与过滤器

在Web开发中经常需要对某些行为进行拦截,其目的一般是拦截用户某个行为之后进行处理,比如拦截用户的请求,判断用户是否有权限访问请求的资源,限制某些IP地址的访问请求这些拦截需求Spring Boot中通过拦截器实现。

拦截器

HandlerInterceptor接口

Spring Boot通过其定义的HandlerInterceptor接口来实现用户自定义拦截器的功能。HandlerInterceptor接口定义了三个方法preHandle, postHandle,afterCompletion, 用户通过重写这三种方法实现请求前、请求后等操作

boolean preHandle(HttpServletRequest request, 
                  HttpServletResponse response, 
                  Object handler)
			  throws Exception;

void postHandle(HttpServletRequest request, 
                HttpServletResponse response, 
                Object handler, 
                ModelAndView modelAndView)
			throws Exception;

void afterCompletion(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler, Exception ex)
			throws Exception;

preHandle方法 在真正业务被处理之前回调,可以实现预处理功能,一般用于调用之前修改编码方式、登录检查、安全控制、权限校验等处理。注意和其他两个方法不同,preHandle有一个boolean型返回值。返回true表示继续流程,返回false则表示流程中断即预处理失败,不会继续调用后续程序,此时会通过response产生响应。

postHandle方法 在业务处理器处理请求执行完成后,生成视图之前执行。后处理调用了Service并返回ModelAndView,但未进行页面渲染,有机会修改ModelAndView。一般来说业务处理器调用控制层处理完用户请求后,会把结果数据存储在该类的model属性中,把要返回的视图信息存储在该类的view属性中,然后让该ModelAndView返回到前台。简单的说就是ModelAndView类包含了业务处理结果的数据和页面内容,前台根据返回的ModelAndView返回的内容进行渲染。

afterCompletion:完全处理完请求后被调用,已经渲染了页面,可用于清理资源等。

由于如果实现HandlerInterceptor接口,则根据Java语法必须实现preHandle, postHandle, afterCompletion三个方法,但是很多时候我们实际只需要实现这3个方法之一即可完成业务需求,针对这种情况Spring 提供了一个名为HandlerIntercetorAdapter适配器,这个适配器可以让我们只实现我们需要的三个方法之一。

HandlerInterceptor实际应用

在大多数实际应用的系统中不同的账号可以访问不同的资源,比如一些影视网站需要VIP才能观看一些节目,而非VIP不得观看,因此当用户请求访问这些资源时需要对其访问请求进行拦截,检查用户是否有访问权限。对于这种需求可以通过HandlerInterceptor实现。

首先我们先定义一个访问拦截器

public class VisitInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
        Object user = request.getSession().getAttribute("vipuser");
        if (user == null) {
            request.setAttribute("msg", "充值VIP即可访问");
            request.getRequestDispatcher("/").forward(request, response);
            return false;
        }
        return  true;
    }
}
 

上面的代码实现了一个名为VisitInterceptor的拦截器,该拦截器实现了preHandle接口,从session中获取当前用户是否为vip用户,如果不是则返回根目录,如果是vip用户才可以继续访问。现在定义了拦截器,接下来就是要如何使用它。要使用这个拦截器需要将它注入到系统配置中,在Spring Boot 2.0 之后官方推荐直接实现WebMvcConfigurer接口或者WebMVCConfigurationSupport类,来注入拦截器。所以我们先定义一个配置类VisitMvcConfig

 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class VisitMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new VisitInterceptor())
                .addPathPatterns("/**").excludePathPatterns("/", "/user/login");
    }
}

调用addInterceptor方法新建注入VisitInterceptor拦截器,同时用addPathPatterns指明需要拦截的路径是“/**”,使用excludePathPatterns排除某些地址不拦截,比如一般来说登录地址不需要拦截。

过滤器

有时候除了对http请求需要拦截之外,我们还会需要对去Request请求和Response返回进行检查和过滤,比如过滤一些敏感词和排除一些有危险的字符等等。为方便开发人员使用Spring Boot中内置了很多过滤器,比如OrderedCharacterEncodingFilter可以对字符编码进行过滤。如果我们需要自己实现过滤器可以使用FilterRegistrationBean进行过滤。

如果同时使用过滤器和拦截器其生效流程为先执行过滤器然后执行拦截器:业务处理前过滤->业务处理前拦截->处理业务->业务处理后拦截->业务处理后过滤。

FilterRegistrationBean实现过滤器

我们可以使用Spring Boot提供的FilterRegistrationBean实现过滤器。在注册过滤器之前需要一个实现Filter接口的Filter类

@Component
public class WorkedTimeFilter implements Filter {
    @Override
    public void init(FilterConfig para) throws ServletException {

    }

    @Override
    public void destroy() {
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        System.out.println("work begin");
        long start = new Date().getTime();
        chain.doFilter(request, response);
        long end = new Date().getTime();
        System.out.println("WorkedTimerFilter end, use total time: " + (end - start));

    }


}
完成了过滤器定义之后,下一步就需要注入到Spring Boot项目中去,这是通过系统配置注入的,我们需要定义一个配置类,然后将过滤器添加进去
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WorkedTimeWebConfig {
    @Bean
    public FilterRegistrationBean commonWorkerFilterRegistration() {
        FilterRegistrationBean<WorkedTimeFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new WorkedTimeFilter());
        registration.addUrlPatterns("/*");
        registration.setName("WorkedTimeFilter");
        registration.setOrder(1);
        return registration;
    }
}

注意这里的setOrder()方法可以控制多个过滤器直接的顺序,数字越小优先级越高。

Spring Boot应用的打包与部署

由于Spring Boot应用使用的是嵌入式的Servlet容器,所以它选择的默认打包形式是Jar包打包。这和普通的Web程序选择War包打包不一样。不过如果因为应用需求需求使用War打包也是支持的。

Jar包形式打包部署

Spring Boot打包需要在pom.xml文件中引入打包插件,为了方便开发人员Spring Boot直接为项目打包提供了整合好的Maven打包插件spring-boot-maven-plugin,可以直接在pom.xml文件中添加

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

maven中默认只能读取resources文件夹下的资源,如果要读取其他路径下的资源文件,那么就需要用到maven-resources-plugin这一插件,不然打包时容易出现问题,建议也把它添加到pom.xml上

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.1.0</version>
            </plugin>

完成pom.xml修改后,我们可以使用IDEA进行打包,点击IDEA左侧下发的小图标

在弹出的菜单中选择Maven,则可以在IDEA编辑窗口右侧看到Maven的菜单

点击工程下面的Lifecycle,会展开菜单,需要打包Jar包的话双击击package即可

打包好的jar包在target目录下

可以将其拷贝到需要的目录,然后执行

java -jar XX.jar 

则该Jar包将会运行。为什么Spring Boot打包的jar包可以独立运行呢?我们可以看一下Jar包的目录

Jar包中的核心目录是BOOT-INF目录,里面有两个目录classes和lib。classes中存放的是将项目打包编译后的所有文件。

另外一个目录是lib目录,里面存放了项目自动引入的各种医疗Jar文件。同时这里面包含了tomcat-embed文件,这就是Spring Boot框架内嵌的tomcat,所以Spring Boot打包出的Jar文件可以独立部署

​War包形式打包部署

有的时候需要将Spring Boot以War包形式打包以便放置在tomcat中与其他War包一起部署。需要将项目打包为war包,需要在pom.xml中进行配置,使用<packaging>标签告诉打包工具需要打包成的文件格式。

    <name>chapter04</name>
    <description>Demo project for Spring Boot</description>
    <packaging>war</packaging>
    <properties>
        <java.version>1.8</java.version>
    </properties>
 

由于Spring Boot默认使用内嵌的tomcat,为类让项目可以以war包的形式打包部署,还需要声明使用外部的tomcat服务器。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
 

使用scope标签将tomcat服务声明为外部已提供provided,这样在项目打包部署是,可以使用外部配置的tomcat以war包形式部署,同时还可以使用内嵌tomcat以Jar包形式部署

修改完tomcat声明之后还需要对Spring Boot项目内容进行修改,使得允许应用程序在Servlet容器在启动时可以进行配置。打开项目的主程序启动类,这里是Chapter04Application,对其进行修改

@SpringBootApplication
public class Chapter04Application extends SpringBootServletInitializer {

    //重写configure方法使得程序可以配置
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Chapter04Application.class);
    }
    
    public static void main(String[] args) {
        SpringApplication.run(Chapter04Application.class, args);
    }

}
 

注意这里让主程序继承了SpringBootServletInitializer类,并重写了其configure。

完成上面的步骤之后,则可以实现War包形式打包了。点击package,一切正常的话可以在target中看到

之后将War包放置到tomcat的webapps目录下,然后启动tomcat(执行bin目录下的startup.sh或startup.bat)即可启动项目。然后注意以War包部署时访问项目的具体地址中要带上War包项目的全名,以便tomcat访问正确的目录地址 如 http://localhost:8080/chapter04-0.0.1-SNAPSHOT/login

可以访问:GitHub - qwdzq/springboot: spring boot 入门

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
当前课程中博客项目的实战源码是我在 GitHub上开源项目 My-Blog,目前已有 3000 多个 star:本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 大部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 个人博客项目功能的讲解,通过本课程的学习,不仅仅让你掌握基本的 Spring Boot 开发能力以及 Spring Boot 项目的大部分开发使用场景,同时帮你提前甄别和处理掉将要遇到的技术难点,认真学完这个课程后,你将会对 Spring Boot 有更加深入而全面的了解,同时你也会得到一个大家都在使用的博客系统源码,你可以根据自己的需求和想法进行改造,也可以直接使用它来作为自己的个人网站,这个课程一定会给你带来巨大的收获。作者寄语本课程录制于 2020 年,代码基于 Spring Boot 2.x 版本。到目前为止,Spring Boot 技术栈也有一些版本升级,比如 Spring Boot 2.7 发版、Spring Boot 3.x 版本发布正式版本。对于这些情况,笔者会在本课程实战项目的开源仓库中创建不同的代码分支,保持实战项目的源码更新,保证读者朋友们不会学习过气的知识点。课程特色 课程内容紧贴 Spring Boot 技术栈,涵盖大部分 Spring Boot 使用场景。开发教程详细完整、文档资源齐全、实验过程循序渐进简单明了。实践项目页面美观且实用,交互效果完美。包含从零搭建项目、以及完整的后台管理系统和博客展示系统两个系统的功能开发流程。技术栈新颖且知识点丰富,学习后可以提升大家对于知识的理解和掌握,对于提升你的市场竞争力有一定的帮助。实战项目预览    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈小房

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

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

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

打赏作者

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

抵扣说明:

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

余额充值