Spring MVC详解

1. 什么是Spring Web MVC  

        Spring Web MVC 是 Spring 框架的一个模块,专门用于构建基于 Web 的应用程序。它基于经典的 Model-View-Controller (MVC) 设计模式,帮助开发者组织 Web 应用的结构,并简化处理 Web 请求、响应、视图渲染等任务。

Spring Web MVC 是 Spring Framework 中的核心模块之一,通常用于构建 Web 应用和 REST API。通过将应用程序分为 Model、View、Controller 三个部分,Spring MVC 实现了关注点分离,提升了代码的可维护性和扩展性。

1.1 MVC定义

        MVC是Model View Controller的缩写,它是软件工程中的一种软件架构设计模式,它把软件系统分为模型,视图,控制器三个基本部分。

View (视图)指在应用程序中专门用来与浏览器进行交互,展示数据的资源。

Model( 模型) 是应用程序的主体部分,用来处理程序中数据逻辑的部分。

Controller (控制器) 可以理解为一个分发器,用来决定对于视图发来的请求,需要用哪一个模型来处理,以及处理完后需要跳回到哪一个视图。即用来连接视图和模型。 是用户与程序之间交互的桥梁。

MVC通过将业务逻辑与用户界面分离,使得代码更加模块化和易于维护。

例如:去饭店吃饭,客户进店之后,服务员来接待客户点餐,客户点完餐,把菜单交给前厅,前厅根据客户菜单给后厨下达命令,后厨负责做饭,做完之后再根据菜单告诉服务员,这是几号餐桌客人的饭。

服务员:就相当于View(视图),服务接待客户,帮助客户点餐,以及给顾客端饭。

前厅: 相当于Controller(控制器),根据用户的点餐情况,来选择给哪个后厨下达命令。

后厨: 相当于Model(模型),根据前厅的要求来完成客户的用餐需求。 

2. 学习Spring MVC

        既然是Web框架,那么当用户在浏览器中输入流url之后,我们的Spring MVC项目就可以感知到用户的请求,并给予响应。学习Spring MVC也就是学习如何通过浏览器和用户程序之间交互。        

2.1 建立连接

        将用户(浏览器)和Java程序连接起来,也就是访问一个地址能够调用到我们的Spring程序。

        在Spring MVC中使用@RequestMapping来实现URL路由映射,也就是浏览器连接程序的作用。用@RestController注解将类声明为Spring MVC控制器。告诉Spring容器该类将处理来自客户代表的HTTP请求,并且它应该被纳入Spring MVC的请求处理流程中。

package com.adviser.demotest.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    @PostMapping(value = "/sayHi")
    public String sayHi() {
        return "hello Spring MVC";
    }

}

接下来访问http://127.0.0.1:8080/sayHi,就能看到程序返回的结果。

2.1.1@RequestMapping注解介绍 

@RequestMapping是Spring Web MVC程序中最常用到的注解,它是用来注册接口的路由映射的。

表示服务器收到请求时,路径为。/sayHi的请求就会调用sayHi这个方法代码。

路由映射:当用户访问一个URL时,将用户的请求对应到程序中某个类的某个方法的过程就叫路由映射。

2.1.2@RequestMapping使用

@RequestMapping即可修饰类,也可就是方法,当修饰类和方法时,访问地址是类路径 + 方法路径。

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping(value = "/sayHi")
    public String sayHi() {
        return "hello Spring MVC";
    }

}

 2.1.3@RequestMapping是GET还是POST

@RequestMapping即支持Get请求也支持Post请求。

可以通过@PostMapping和@GetMapping指定只接受GET请求和POST请求

2.2 请求

传参介绍

1.普通传参也就是通过查询字符串来传参,我们通过URL来访问互联网上的某个资源URL格式如下:

其中,查询字符串就是请求的参数。

 

2.form-data(完整表示为:multipart/form-data)

表单提交的数据,在form标签中加上enctyped=“multipart/form-data”,通常用于提交图片/文件。对应Content-Type:multipart/form-data

3.x-www-form-urlencoded

form表单,对应Content-Type:application/x-www-form-urlencoded

上述使用的工具是PostMan:https://www.postman.com/downloads/

传递单个参数

package com.adviser.springtest.controller;


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/param")
@RestController
public class ParamController {

    @RequestMapping("/m1")
    public String method1(String name) {
        return "接收到参数name:" + name;
    }
}

 

 可以看到,后端程序正确拿到了name参数的值.Spring MVC会根据方法的参数名,找到对应的参数,赋值给方法.

参数不一致,是获取不到参数的

注意事项 :

使用基本类型来接收参数时,参数必须传(除boolean类型),否则会报500错误

类型不匹配时,会报400错误。

正常情况:

不传递age参数

后端日志

 就是说int类型是基本类型,不能传化成空值,考虑将它声明成包装类型。

传递参数类型错误:

 

对于包装类型,如果不传对应参数,Spring接收到的数据则为null,对于参数可能为空的数据 ,建议使用包装类型。

传递多个参数

@RequestMapping("/m2")
    public String method2(String name, String password) {
        return "接收到参数name:" + name + ", password: " + password;
    }

 

当有多个参数时,前后端进行参数匹配时,是以参数的名称进行匹配的,因此参数的位置是不影响后端获取参数的结果的. 

传递对象

当参数比较多时,方法声明就需要有很多形参, 并且后续每次新增一个参数,也需要修改方法声明,我们不妨把这些参数封装为一个对象,Spring MVC也可以自动实现对象参数的赋值,比如Person对象:

public class Person {
    private int id;
    private String name;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

传递对象代码实现

@RequestMapping("/m3")
    public String method3(Person person) {
        return person.toString();
    }

 可以看到,后端程序正确拿到了Person对象里面的各个属性值

Spring会根据参数名称自动绑定到对象的各个属性上,如果某个属性未传递,则赋值为null(基本类型则为默认初始值,比如int类型的属性,就会被赋值为0) 

后端参数重命名 (后端参数映射)

在某些特殊情况下,前端传递的参数key和我们后端接收的key不一致的情况下,可以使用@RequestParam来重命名前后端的参数        

@RequestMapping("/m4")
    public String method4(@RequestParam("time") String createTime) {
        return "接收到参数createTime:" + createTime;
    }

 

此时如果用createTime传参会发生什么?

 控制台打印日志显示参数time不存在

所以得出结论:

1. 使用@RequestParam进行参数重命名时,请求参数只能和@RequestParam声明的名称一致,才能进行参数绑定和赋值.

2. 使用@RequestParam进行参数重命名时, 参数就变成了必传参数.

非必传参数设置

如果我们实际业务的参数是一个非必传的参数,针对上述问题,如何解决?

我们先看@RequestParam注解实现细节

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {

	@AliasFor("name")
	String value() default "";

	@AliasFor("value")
	String name() default "";

	boolean required() default true;

	String defaultValue() default ValueConstants.DEFAULT_NONE;

}

可以看到required的默认值为true, 表示含义就是: 该注解修饰的参数默认为必传,既然这样我们就设置为false就行了

@RequestMapping("/m4")
    public String method4(@RequestParam(value = "time", required = false) String createTime) {
        return "接收到参数createTime:" + createTime;
    }

传递数组

@RequestMapping("/m5")
    public String method5(String[] arrayParam) {
        return Arrays.toString(arrayParam);
    }

http://127.0.0.1:8080/param/m5?arrayParam=zhangsan,lisi,wangwu 

http://127.0.0.1:8080/param/m5?arrayParam=zhangsan&arrayParam=lisi&arrayParam=wangwu 

 

传递集合

集合参数: 和数组类似,同一个请求参数名有多个,且需要使用@RequestParam绑定参数关系.

默认情况下,请求中参数名相同的多个值,是封装到数组.如果要封装到集合,要使用@RequestParam绑定参数关系.

@RequestMapping("/m6")
    public String method6(@RequestParam List<String> listParam) {
        return "size:" + listParam.size() + ", listParam:" + listParam;
    }

      

传递JSON数据

JSON语法

先看一段JSON数据

{
 "squadName": "Super hero squad",
 "homeTown": "Metro City",
 "formed": 2016,
 "secretBase": "Super tower",
 "active": true,
 "members": [{
 "name": "Molecule Man",
 "age": 29,
 "secretIdentity": "Dan Jukes",
 "powers": ["Radiation resistance", "Turning tiny", "Radiation 
blast"]
 }, {
 "name": "Madame Uppercut",
 "age": 39,
 "secretIdentity": "Jane Wilson",
 "powers": ["Million tonne punch", "Damage resistance", "Superhuman 
reflexes"]
 }, {
 "name": "Eternal Flame",
 "age": 1000000,
 "secretIdentity": "Unknown",
 "powers": ["Immortality", "Heat Immunity", "Inferno", 
"Teleportation", "Interdimensional travel"]
 }]
}

1. 数据在键值对(key/value)中

2. 数据由 , 分隔

3. 对象用 { } 表示

4.  数组用[ ] 表示

5. 值可以为对象, 也可以为数组, 数组中可以包含多个对象

JSON的两种结构

1. 对象: 大括号{ }保存的对象是一个无序的键值对集合. 一个对象以左括号 { 开始 , 右括号 } 结束. 每个 “键” 后跟一个冒号:,键值使用逗号,分隔。

2. 数组中括号[ ] 保存的数组的值(value)的有序集合,一个数组以左中括号 [ 开始,右中括号 ] 结束,值之间使用逗号 ,分隔。

JSON字符串和Java对象互转

JSON本质上是一个字符串,通过文本来存储和描述数据

Spring MVC框架也集成了JSON的转化工具,我们可以直接使用,来完成JSON和Java对象的互转  本质上是jackson-databind提供的功能。

JSON优点

1. 简单易用:语法简单,易于理解和编写,可以快速的进行数据交换

2.跨平台支持:JSON可以被多种编程语言解析和生成,可以在不同平台和语言之间进行数据交换和传输。

3.轻量级:相较于XML格式,JSON数据格式更加轻量级,传输占用带宽较小,可以提高数据传输速度。

4.易于扩展:JOSN的数据结构灵活,支持嵌套数组和对象等复杂数据结构,便于扩展和使用

5.安全性:JSON数据格式是一种纯文本格式,不包含可执行代码,不会执行恶意代码,因此具有较高的安全性        

传递JSON对象

接收JSON对象,需要使用@RequestBody

RequestBody:请求正文,意思是这个注解作用在请求正文的数据绑定,请求参数必须写在请求正文中

@RequestMapping("/m7")
    public String method7(@RequestBody Person person) {
        return person.toString();
    }

 通过fiddler观察一下

        

获取URL中的参数@PathVariable

@RequestMapping("/m8/{id}/{name}")
    public String method8(@PathVariable Integer id, @PathVariable("name") String userName) {
        return "解析参数id:"+id+",name" + userName;
    }

参数名和URL中变量名一致时,可以简写,不用给@PathVariable的属性赋值,如上述例子中的id,不一致时就需要赋值,如userName。 

上传文件@RequestPart

@RequestMapping("/m9")
    public String get(@RequestPart("file")MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();
        file.transferTo(new File("D:/temp/" + file.getOriginalFilename()));
        return "接收到文件名称为: " + fileName;
    }

 

获取Cookie和Session

Cookie

HTTP协议自生是 "无状态" 协议。

无状态的含义是指:默认情况下HTTP协议的客户端和服务器之间的这次通信,和下次通信之间没有直接的联系。

但是实际开发中,我们很多时候是需要知道请求之间的关联关系的。

例如登入网站成功之后,第二次访问的时候服务器就能知道该请求是否是已经登入过了。

 上述图中的 "令牌" 通常就存储在Cookie字段中。

比如去医院挂号

1.看病之前先挂号,挂号的时候需要提供身份证号,同时得到一张“就诊卡”,这个就诊卡就相当于患者的“令牌”。

2.后续去各个科室进行检查,诊断,开药等操作,都不必在出示身份证,只要凭就诊卡即可识别出当前患者的身份。

3.看完病了之后,不想要就诊卡,就可以注销这个卡。此时患者的身份和就诊卡的关联就销毁了。(类似于网站的注销操作)

4.又来看病,可以补一张新的就诊卡,此时就得到了一个新的“令牌”。

此时在服务器这边就需要记录“令牌”信息,以及令牌对应的用户信息,这个就是Session机制所作的工作。

Session

Session会话,对话的意思

在计算机领域,会话是一个客户与服务器之间的不中断的请求响应。对客户的每个请求,服务器能够识别出请求来自同一个客户,当一个未知客户向Web语言程序发送第一个请求时就开始了一个会话。当客户明确结束会话或服务器在一个时限内没有接收到客户的任何请求时,会话就结束了。

服务器在同一时间收到的请求是很多的。服务器需要清楚的区分每个请求是属于哪个用户,也就是属于哪个会话,就需要在服务器这边记录每个会话以及与用户的信息的对应关系。

Session就是服务器为了保存用户信息而创建的一个特殊对象。

Session的本质就是一个“哈希表”,存储了一些键值对结构。Key就是SessionID,Value就是用户信息(用户信息可以根据需求灵活设计)。

 SessionID是由服务器生成的一个“唯一性字符串”,从Session机制的角度来看,这个唯一性字符串称为“SessionId”。但是站在整个登入流程中看待,也可以把这个唯一性字符串称为"token".

上述例子中的令牌ID,就是可以看做是SessionId,只不过令牌除了ID之外,还会带有一些其他信息,比如时间,签名等。

 1.当用户登入的时候,服务器在Session中新增一个新的记录,并把sessionId返回给客户端。(通过HTTP响应中的Set-Cookie字段返回)。

2.客户端后续再给服务器发送请求的时候,需要在请求中带上sessionId。(通过HTTP请求中的Cookie字段带上)。

3.服务器收到请求之后,根据请求中的SessionId在session信息中获取到的对应的用户信息,在进行后续操作,找不到则重新创建Session,并把SessionID返回

Session默认是存储在内存中的,服务器重启Session信息就会丢失。

Cookie和Session的区别 

Cookie是客户端保存用户信息的一种机制。Session是服务器保存用户信息的一种机制。

Cookie和Session之间通过SessionId关联起来,SessionId是Cookie和Session之间的桥梁。

Cookie和Session经常会在一起配合使用,但不是必须配合。

        完全可以用Cookie来保存一些数据在客户端,这些数据不一定是用户身份信息,也不一定是SessionId

        Session中的SessionId也不需要非得通过Cookie/Set-Cookie传递,比如URL传递。       

传统获取Cookie
@RequestMapping("/m10")
    public String method10(HttpServletRequest request, HttpServletResponse response){
        Cookie[] cookies = request.getCookies();
        StringBuilder sb = new StringBuilder();
        if (cookies != null) {
            for (Cookie ck : cookies) {
                sb.append(ck);
            }
        }
        return "Cookie信息: " + sb;
    }

 先设置Cooike的值

 

 

简洁获取Cookie
@RequestMapping("/getCookie")
public String cookie(@CookieValue("name") String name) {
    return "name: " + name;
}

 

获取Session
Session的存储和获取

Session是服务器端的机制,需要先存储,才能获取

Session也是基于HttpServletRequest来存储获取的。

Session存储
@RequestMapping("/setSession")
    public String setSession(HttpServletRequest request) {
        HttpSession session = request.getSession();
        if (session != null) {
            session.setAttribute("userName", "java");
        }
        return "session 存储成功";
    }

这段代码看不到SessionId这样的概念。getSession操作内部提取到请求中的Cookie里的SessionId,然后根据SessionId获取到对应的Session对象,Session对象HttpSession来描述。 

获取Session的方式有两种

HttpSession getSession(boolean create);


HttpSession getSession();

 HttpSession getSession(boolean create); 如果参数为true,则当不存在会话时新建会话,参数如果为false,则当不存在会话时返回null。

HttpSession getSession(); 和 getSession(true);一样。默认值为true。

session.setAttribute(String name,Object value); 使用指定的名称绑定一个对象到该session会话。

Session读取
@RequestMapping("/getSession")
    public String getSession(HttpServletRequest request) {
        //如果session不存在,不会自动创建
        HttpSession session = request.getSession(false);
        String userName = null;
        if (session != null && session.getAttribute("userName") != null) {
            userName = (String)session.getAttribute("userName");
        }
        return "userName: " + userName;
    }

 

简洁获取Session
@RequestMapping("/getSession2")
    public String getSession2(@SessionAttribute(value = "userName", required = false) String userName) {
        return "userName: " + userName;
    }

 

 

    @RequestMapping("/getSession3")
    public String getSession3(HttpSession session) {
        String userName = (String)session.getAttribute("userName");
        return "userName: " + userName;
    }

 

获取Header

传统获取Header
    @RequestMapping("/getHeader")
    public String getHeader(HttpServletRequest request, HttpServletResponse response) {
        String userAgent = request.getHeader("User-Agent");
        return "userAgent: " + userAgent;
    }

 通过fiddler抓包

简洁获取Header
    @RequestMapping("/getHeader2")
    public String getHeader2(@RequestHeader("User-Agent") String userAgent) {
        return "userAgent: " + userAgent;
    }

2.3 响应

返回静态页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index⻚⾯</title>
</head>
<body>
Hello,Spring MVC,我是Index⻚⾯.
</body>
</html>

后端代码

@RestController
public class IndexController {
    @RequestMapping("/index")
    public Object index() {
        return "/index.html";
    }
}

 

访问url,发现结果未正确返回,http响应把“/index.html"当作了http响应正文的数据

那Spring MVC 如何才能识别出来 index.html是一个静态页面?

我们需要把@RestController改为@Controller

@Controller
public class IndexController {
    @RequestMapping("/index")
    public Object index() {
        return "/index.html";
    }
}

       

@RestController和@Controller的关联 

前面我们讲了MVC模式,后端会返回视图,这是早期的概念

随着互联网的发展,目前项目开发流行”前后端分离“模式,Java主要是用来做后端开发,所以也就不再处理前端相关的内容了

MVC的概念也逐渐发生了变化,View不在返回视图,而是返回显示视图时所需要的数据。

前面使用的@RestController其实就是返回数据

@RestController = @Controller + @ResponseBody

@Controller: 定义一个控制器,Spring框架启动时加载,把这个对象交给Spring管理。

@ResponseBody:定义返回的数据格式为非视图,返回一个text/html信息

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
 @AliasFor(
 annotation = Controller.class
 )
 String value() default "";
}

 上述为@RestController源码,想要返回视图只要把@ResponseBody去掉就行了,也就是@Controller

返回数据@ResponseBody

@ResponseBody既是类注解也是方法注解

如果作用在类上,表示该类的所有方法,返回的都是数据,如果作用在方法上,表示该方法返回的是数据。

同样,如果类上有@ResyController注解时,表示所有方法上添加了@ResponseBody注解,也就是当前类下所有方法返回值作为响应数据。

返回HTML代码片段

后端返回数据,如果数据中有HTML代码,也会被浏览器解析

@RequestMapping("/returnHtml")
@ResponseBody
public String returnHtml() {
 return "<h1>Hello,HTML~</h1>";
}

 

 

返回JSON对象

@RequestMapping("/returnJson")
@ResponseBody
public HashMap<String, String> returnJson() {
 HashMap<String, String> map = new HashMap<>();
 map.put("Java", "Java Value");
 map.put("MySQL", "MySQL Value");
 map.put("Redis", "Redis Value");
 return map;
}

设置状态码

Spring MVC会根据我们方法的返回结果自动设置响应状态码,程序员也可以手动指定状态码

通过Spring MVC的内置对象HttpServletResponse提供的方法来进行设置

    @RequestMapping("/setStatus")
    public String setStatus(HttpServletResponse response) {
        response.setStatus(401);
        return "设置状态码成功";
    }

 

设置Header

http响应报头也会向客户端传递一些附加信息,比如服务程序名称,请求的资源已移动到新地址等,如Content-Type,Local等。

这些信息通过@RequestMapping注解的属性来实现

先看@RequestMapping源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
 String name() default "";
 @AliasFor("path")
 String[] value() default {};
 @AliasFor("value")
 String[] path() default {};
 RequestMethod[] method() default {};
 String[] params() default {};
 String[] headers() default {};
 String[] consumes() default {};
 String[] produces() default {};
}

 1.value:指定映射的URL

2.Method:指定请求的method类型,如GET,POST等

3.consumes:指处理请求(request)的提交内容类型(Content-Type),例如application/json,text-html

4.produces:指返回的内容类型,仅当request请求投中的(Accept)类型中包含该指定类型才返回

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

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

设置Content-Type

响应中的Content-Type常见的取值有以下几种:

text/html:body数据格式是HTML

text/css:body数据格式是CSS

application/Javascript:body数据格式是JavaScript

application/json:body数据格式是JSON

我们可以通过produces属性的值,设置响应的报头Content-Type

@RequestMapping(value = "/returnJson", produces = "application/json")
    public String returnJson() {
        return "{\"success\":true}";
    }

设置其他Header

设置其他Header的话,需要使用Spring MVC的内置对象HttpServletResponse提供的方法来进行设置

    @RequestMapping(value = "/setHeader")
    public String setHeader(HttpServletResponse response) {
        response.setHeader("MyHeader","MyHeaderValue");
        return "设置Header成功";
    }

 

Spring MVC 是一种基于 Java 的开发框架,用于构建 Web 应用程序。它是 Spring 框架的一部分,提供了一种模型-视图-控制器(MVC)的架构模式,帮助开发人员将应用程序的不同方面进行解耦。 在 Spring MVC 中,应用程序的请求由 DispatcherServlet 接收并将其路由到适当的处理程序(也称为控制器)。控制器处理请求并生成模型数据,然后选择适当的视图来呈现这些模型数据给用户。 以下是 Spring MVC 的一些重要组件和概念: 1. DispatcherServlet:是整个 Spring MVC 的中央调度器,负责接收请求并将其分派给相应的处理程序。 2. 控制器(Controller):处理请求的组件,根据请求的类型和内容执行逻辑处理,并生成模型数据。 3. 模型(Model):表示应用程序的数据和状态。控制器可以通过模型对象来设置和获取数据,并将其传递给视图进行呈现。 4. 视图(View):负责将模型数据呈现给用户。可以是 JSP、Thymeleaf 或其他模板引擎。 5. 处理器映射器(Handler Mapping):将请求映射到相应的处理程序(控制器)。它根据配置文件或注解来确定请求与处理程序之间的映射关系。 6. 视图解析器(View Resolver):根据视图名称解析出实际的视图对象,它将逻辑视图名转换为物理视图。 7. 拦截器(Interceptor):在请求处理的过程中,可以对请求进行预处理和后处理。可以用于身份验证、日志记录等功能。 8. 数据绑定(Data Binding):自动将请求参数绑定到控制器方法的参数或模型对象的属性上。 9. 校验器(Validator):用于验证模型对象的数据的有效性。 Spring MVC 提供了灵活且强大的功能,使开发人员能够轻松构建可扩展和可维护的 Web 应用程序。它还支持 RESTful Web 服务和国际化等功能。通过良好的设计和组织,Spring MVC 可以实现松耦合、可测试和可扩展的应用程序架构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值