JavaWeb基础(3)-会话技术(cookie和session),过滤器(Filter),监听器(Listener)

27 篇文章 0 订阅
11 篇文章 0 订阅

JavaWeb基础(3)-会话技术(cookie和session),过滤器(Filter),监听器(Listener)

8 会话技术(Cookie、Session)

8.1 会话与会话跟踪技术

8.1.1 会话

会话: 用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。

  • 在一次会话中可以包含多次请求和响应
  • 浏览器发出请求到服务端响应数据给前端之后,一次会话(在浏览器和服务器之间)就被建立了
  • 会话被建立后,如果浏览器或服务端都没有被关闭,则会话就会持续建立着
  • 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为会话

用实际场景来理解下会话,比如在我们访问京东的时候,当打开浏览器进入京东首页后,浏览器和京东的服务器之间就建立了一次会话,后面的搜索商品,查看商品的详情,加入购物车等都是在这一次会话中完成。

思考:下图中总共建立了几个会话?

image-20240102141637364

每个浏览器都会与服务端建立了一个会话,加起来总共是3个会话。

8.1.2 会话跟踪

引入:

  • HTTP协议是无状态的,靠HTTP协议是无法实现会话跟踪
  • 想要实现会话跟踪,就需要用到Cookie和Session

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据

  • 服务器需要用来识别请求是否来自同一个浏览器
  • 服务器用来识别浏览器的过程,这个过程就是会话跟踪;
  • 服务器识别浏览器后就可以在同一个会话中多次请求之间来共享数据

为什么现在浏览器和服务器不支持数据共享

  • 浏览器和服务器之间使用的是HTTP请求来进行数据传输
  • HTTP协议是无状态的,(之前学的状态是响应状态码,在请求数据时不起作用)每次浏览器向服务器请求时,服务器都会将该请求视为新的请求
  • HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
  • 请求与请求之间独立后,就无法实现多次请求之间的数据共享

一个会话中的多次请求需要共享数据的情况:

  • 加入购物车去购物车结算是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享

  • 页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息(登陆后在不同页面跳转任可以看到自己的信息)

  • 网站登录页面的【记住我】功能:通常是用于控制是否在用户的浏览器中设置一个长期有效的 Cookie

    注意:网页自动登录的背后原理中,会话技术中的cookie或其他相关技术并没有直接存储账号和密码,Cookie本身并不存储账号和密码,而是包含一些用于识别用户身份的信息的小型文本文件。

    在网站上实现**自动登录**的过程通常如下:

    1. 登录过程: 用户在登录页面输入用户名和密码,服务器验证这些信息,如果验证成功,会为用户创建一个会话。
    2. 会话管理: 服务器会将一个唯一的标识符(通常是一个会话 ID)发送到用户的浏览器,该标识符用于标识用户的会话。
    3. Cookie: 服务器通常使用 Cookie 将会话标识符存储在用户的浏览器中。Cookie 是存储在用户计算机上的小型文本文件,它包含有关用户的信息,例如会话标识符、过期时间等。
    4. 下次访问: 当用户再次访问网站时,浏览器会将存储的 Cookie 发送回服务器,服务器通过解析 Cookie 中的会话标识符来识别用户。如果会话仍然有效(未过期),用户就被视为已经登录。
    5. 自动登录: 如果用户的会话仍然有效,服务器可以自动将用户登录,而无需再次输入用户名和密码。

    当用户修改其账号对应的密码时,服务器会采取相应的措施来保障安全性,而与之相关的会话可能会被标记为无效或过期

  • 登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册

实现会话跟踪技术的两种方式:

(1) 客户端会话跟踪技术:Cookie

(2) 服务端会话跟踪技术:Session

这两个技术都可以实现会话跟踪,它们之间最大的区别:Cookie是存储在浏览器端而Session是存储在服务器

8.2 Cookie

概念:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问

image-20240102155936268

  • 服务端提供了两个Servlet,分别是ServletA和ServletB
  • 浏览器发送HTTP请求1给服务端,服务端ServletA接收请求并进行业务处理
  • **服务端ServletA在处理的过程中可以创建一个Cookie对象**并将name=zs的数据存入Cookie
  • 服务端ServletA在响应数据的时候,会把Cookie对象响应给浏览器
  • 浏览器接收到响应数据,会把Cookie对象中的数据存储在浏览器内存中,此时浏览器和服务端就建立了一次会话
  • 在同一次会话中浏览器再次发送HTTP请求2给服务端ServletB,浏览器会携带Cookie对象中的所有数据
  • ServletB接收到请求和数据后,就可以获取到存储在Cookie对象中的数据,这样同一个会话中的多次请求之间就实现了数据共享
8.2.1 Cookie的基本使用
8.2.1.1 服务端发送Cookie
  • 创建Cookie对象,并设置数据
Cookie cookie = new Cookie("key","value");
  • 发送Cookie到客户端:使用response对象
response.addCookie(cookie);
  • 查看浏览器中的cookie

    image-20240102202013577

    image-20240102202101298

    image-20240102202227209

    在这里可以看到不同网站下的cookie

    image-20240102202330475

    另一种查看方式:使用开发者工具

    image-20240102202602214

8.2.1.2 服务端获取Cookie
  • 获取客户端携带的所有Cookie,使用request对象
Cookie[] cookies = request.getCookies();
  • 遍历数组,获取每一个Cookie对象:for
  • 使用Cookie对象方法获取数据(这两个方法不能传入参数
cookie.getName();
cookie.getValue();
  • Spring框架中注解:@CookieValue

    @CookieValue 注解用于从HTTP请求中获取指定名称的 Cookie 的值。

    import org.springframework.web.bind.annotation.CookieValue;
    import org.springframework.web.bind.annotation.GetMapping;
    
    // ...
    
    @GetMapping("/example")
    public String exampleMethod(@CookieValue(name = "yourCookieName", required = false) String yourCookieValue) {
        // 在这里你可以使用yourCookieValue来处理Cookie的值
        System.out.println("Your Cookie Value: " + yourCookieValue);
    
        // 其他业务逻辑...
    
        return "yourView";
    }
    

    @CookieValue注解允许你通过方法参数直接获取指定name的Cookie值。请注意,required = false表示如果找不到指定的Cookie也不会抛出异常,而是将yourCookieValue设置为null

8.2.2 cookie案例
  • 编写Servlet类,名称为AServlet

  • 在AServlet中创建Cookie对象,存入数据,发送给前端

  • 启动测试,在浏览器查看Cookie对象中的值

  • 编写一个新Servlet类,名称为BServlet

  • 在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值

  • 启动测试,在控制台打印出获取的值

代码:

@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //发送Cookie
        //1. 创建Cookie对象
        Cookie cookie = new Cookie("username","zs");
        //2. 发送Cookie,response
        response.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取Cookie
        //1. 获取Cookie数组
        Cookie[] cookies = request.getCookies();
        //2. 遍历数组
        for (Cookie cookie : cookies) {
            //3. 获取数据
            String name = cookie.getName();
            if("username".equals(name)){
                String value = cookie.getValue();
                System.out.println(name+":"+value);
                break;
            }
        }

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
    }
}
8.2.3 cookie使用细节
  • 默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁

  • 设置Cookie存活时间setMaxAge(int seconds)

    1.正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除

    2.负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁

    3.零:删除对应Cookie

    //发送Cookie
    //1. 创建Cookie对象
    Cookie cookie = new Cookie("username","zs");
    //设置存活时间   ,1周 7天
    cookie.setMaxAge(60*60*24*7); //易阅读,需程序计算
    //cookie.setMaxAge(604800); //不易阅读(可以使用注解弥补),程序少进行一次计算
    //2. 发送Cookie,response
    response.addCookie(cookie);
    
  • Cookie不能直接存储中文(无论是name还是value)

    解决方法:在Servlet中对中文进行URL编码,采用URLEncoder.encode(),将编码后的值存入Cookie中

    //发送Cookie
    String value = "张三";
    //对中文进行URL编码
    value = URLEncoder.encode(value, "UTF-8");
    //将编码后的值存入Cookie中
    Cookie cookie = new Cookie("username",value);
    //设置存活时间   ,1周 7天
    cookie.setMaxAge(60*60*24*7);
    //2. 发送Cookie,response
    response.addCookie(cookie);
    

8.3 Session

Session:服务端会话跟踪技术:将数据保存到服务端。

  • Session是存储在服务端;而Cookie是存储在客户端
  • 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
  • 存储在服务端的数据相比于客户端来说就更安全

image-20240102205323844

  • 在服务端的AServlet获取一个Session对象,把数据存入其中
  • 在服务端的BServlet获取到相同的Session对象,从中取出数据
  • 就可以实现一次会话中多次请求之间的数据共享
8.3.1 Session的基本使用
  • HttpSession session = request.getSession(); 获取Session对象,使用的是request对象

  • Session对象提供的功能:

    • void setAttribute(String name, Object o);存储数据到 session 域中;Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。

    • Object getAttribute(String name);根据 key,获取值

    • void removeAttribute(String name);根据 key,删除该键值对

8.3.2 Session原理

Session是如何保证在一次会话中获取的Session对象是同一个呢?

Session是基于Cookie实现的

image-20240102210604619

请求1、2的session对象是同一个;但是请求3的session对象并不是

Session背后的原理:Session是基于Cookie实现的:

(1)demo1在第一次获取session对象的时候,session对象会有一个唯一的标识,假如是id:10

(2)demo1在session中存入其他数据并处理完成所有业务后,需要通过Tomcat服务器响应结果给浏览器

(3)Tomcat服务器发现业务处理中使用了session对象,就会把session的唯一标识id:10当做一个cookie,添加Set-Cookie:JESSIONID=10到响应头中,并响应给浏览器

(4)浏览器接收到响应结果后,会把响应头中的coookie数据存储到浏览器的内存中

(5)浏览器在同一会话中访问demo2的时候,会把cookie中的数据按照cookie: JESSIONID=10的格式添加到请求头中并发送给服务器Tomcat

(6)demo2获取到请求后,从请求头中就读取cookie中的JSESSIONID值为10,然后就会到服务器内存中寻找id:10的session对象,如果找到了,就直接返回该对象,如果没有则新创建一个session对象

(7)关闭打开浏览器后,因为浏览器的cookie已被销毁,所以就没有JESSIONID的数据,服务端获取到的session就是一个全新的session对象

image-20240102211049037
8.3.3 Session使用细节
8.3.3.1 Session钝化与活化

服务器重启后,Session中的数据是否还在?

只要服务器是正常关闭和启动,session中的数据是可以被保存下来的,但session对象也是一个新的对象

钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中

钝化的数据路径为:项目目录\target\tomcat\work\Tomcat\localhost\项目名称\SESSIONS.ser

活化:再次启动服务器后,从文件中加载数据到Session中

数据加载到Session中后,路径中的SESSIONS.ser文件会被删除掉

要实现 Session 数据的共享,需要确保用户的浏览器在一段时间内保持开启状态。

8.3.3.2 Session销毁

session的销毁会有两种方式:

  • 默认情况下,无操作,30分钟自动销毁

    对于这个失效时间,是可以通过配置进行修改的

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">
    
        <session-config>
            <session-timeout>100</session-timeout>
        </session-config>
    </web-app>
    

    如果没有配置,默认是30分钟,默认值是在Tomcat的web.xml配置文件中写死的

    image-20240102213221768

  • 调用Session对象的invalidate()进行销毁

    // 销毁
    session.invalidate();
    

8.4 总结

Cookie 和 Session 都是来完成一次会话内多次请求间数据共享的。

所需两个对象放在一块,就需要思考:

Cookie和Session的区别是什么?

Cookie和Session的应用场景分别是什么?

  • 区别:
    • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
    • 安全性:Cookie不安全,Session安全
    • 数据大小:Cookie最大3KB,Session无大小限制
    • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
    • 服务器性能:Cookie不占服务器资源,Session占用服务器资源
  • 应用场景:
    • 购物车:使用Cookie来存储
    • 以登录用户的名称展示:使用Session来存储(登录之后多个页面之间都可以看到自己的信息,不用反复登录)
    • **记住我功能:**使用Cookie来存储
    • 验证码:使用session来存储
  • 结论
    • Cookie是用来保证用户在**未登录情况下的身份识别**
    • Session是用来保存用户**登录后的数据**

介绍完Cookie和Session以后,具体用哪个还是需要根据具体的业务进行具体分析。

9 Filter(过滤器)、Listener(监听器)

Filter 表示过滤器,Listener 表示监听器,他们属于 JavaWeb 三大组件(Servlet、Filter、Listener)

9.1 Filter过滤器

过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。

如下图所示,浏览器可以访问服务器上的所有的资源(servlet、jsp、html等)

而在访问到这些资源之前可以使过滤器拦截来下,也就是说在访问资源之前会先经过 Filter,如下图

image-20210823184657328

拦截器拦截到后可以做什么功能呢?

过滤器一般完成一些通用的操作。

比如每个资源都要写一些代码完成某个功能,我们总不能在每个资源中写这样的代码吧,而此时我们可以将这些代码写在过滤器中,因为**请求每一个资源都要经过过滤器**。

权限控制:在数据访问时,希望实现的效果是用户如果登陆过了就跳转到品牌数据展示的页面;如果没有登陆就跳转到登陆页面让用户进行登陆

细粒度权限控制:…

统一编码处理:…

敏感字符处理:…

9.1.2 Filter快速入门

进行 Filter 开发分成以下三步实现

  • 定义类,实现 Filter接口,并重写其所有方法

    我们将 filter 创建在 com.baidu.web.filter 包下

    image-20240103162719660

    import javax.servlet.*;
    import java.io.IOException;
    
    public class FilterDemo implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            
        }
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
        }
        @Override
        public void destroy() {
    
        }
    }
    
  • 配置Filter拦截资源的路径:在类上定义 @WebFilter 注解。而注解的 value 属性值 /* 表示拦截所有的资源

    image-20240103214805645

  • 在doFilter方法中输出一句话,并放行

    @WebFilter("/*")
    public class FilterDemo implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("this filter is running...");
            filterChain.doFilter(servletRequest,servletResponse);
        }
        @Override
        public void destroy() {
        }
    }
    

    运行结果:

    image-20240103214920673

    上述代码中的 filterChain.doFilter(request,response); 就是放行,也就是让其访问本该访问的资源。

9.1.3 Filter执行流程

image-20240103215836606

Filter的执行流程如下:

image-20240103215905964

执行流程测试:

后端:

image-20240103215941671

前端:

image-20240103215954182

(实验中这里JSP输出代码有误,不能在控制台输出)

输出结果:

image-20240103221600333

以后我们可以将对请求进行处理的代码放在放行之前进行处理,而如果请求完资源后还要对响应的数据进行处理时可以在放行后进行逻辑处理。

9.1.4 Filter拦截路径配置

Filter 会对请求的哪些资源进行拦截

使用 @WebFilter 注解进行配置。如:@WebFilter("拦截路径")

拦截路径有如下四种配置方式

  • 拦截具体的资源:/index.jsp:只有访问index.jsp时才会被拦截
  • 目录拦截:/user/*:访问/user下的所有资源,都会被拦截
  • 后缀名拦截:*.jsp:访问后缀名为jsp的资源,都会被拦截
  • 拦截所有:/*:访问所有资源,都会被拦截

通过上面拦截路径的学习,大家会发现拦截路径的配置方式和 Servlet 的请求资源路径配置方式一样,但是表示的含义不同。

9.1.5 过滤器链

过滤器链是指在一个Web应用,可以配置多个过滤器这多个过滤器称为过滤器链

image-20240103223841201

上图中的过滤器链执行是按照以下流程执行:(放行代码≠后逻辑代码)

  1. 执行 Filter1 的放行前逻辑代码
  2. 执行 Filter1放行代码
  3. 执行 Filter2 的放行前逻辑代码
  4. 执行 Filter2放行代码
  5. 访问到资源
  6. 执行 Filter2 的放行后逻辑代码
  7. 执行 Filter1 的放行后逻辑代码

以上流程串起来就像一条链子,故称之为过滤器链。

多个过滤器如何定义他的执行顺序呢?

我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。

比如有如下两个名称的过滤器 : BFilterDemoAFilterDemo 。那一定是 AFilterDemo 过滤器先执行。

9.1.6 放行

常见的情况:如果没有登录,就要跳转到登录页面,但是一些资源却被拦截了下来,如css渲染文件;同样的,如果没有账号需要注册,也要跳转到相关的注册页面,那么这个注册页面是需要被放行的。

我们要在判断用户是否登录之前(即相关session代码之前),对一些资源放行:

//判断访问资源路径是否和登录注册相关
//1,在数组中存储登陆和注册相关的资源路径
String[] urls = {"/login.jsp","/imgs/","/css/","/loginServlet","/register.jsp","/registerServlet","/checkCodeServlet"};
//2,获取当前访问的资源路径
String url = req.getRequestURL().toString(); 

//3,遍历数组,获取到每一个需要放行的资源路径
for (String u : urls) {
    //4,判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
    /*
    	比如当前访问的资源路径是  /brand-demo/login.jsp
    	而字符串 /brand-demo/login.jsp 包含了  字符串 /login.jsp ,所以这个字符串就需要放行
    */
    if(url.contains(u)){
        //找到了,放行
        chain.doFilter(request, response);
        //break;
        return;
    }
}

那么这样过滤器的完整代码为:

@WebFilter("/*")
public class LoginFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req = (HttpServletRequest) request;
        
        //判断访问资源路径是否和登录注册相关
        //1,在数组中存储登陆和注册相关的资源路径
        String[] urls = {"/login.jsp","/imgs/","/css/","/loginServlet","/register.jsp","/registerServlet","/checkCodeServlet"};
        //2,获取当前访问的资源路径
        String url = req.getRequestURL().toString(); 

        //3,遍历数组,获取到每一个需要放行的资源路径
        for (String u : urls) {
            //4,判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
            /*
                比如当前访问的资源路径是  /brand-demo/login.jsp
                而字符串 /brand-demo/login.jsp 包含了  字符串 /login.jsp ,所以这个字符串就需要放行
            */
            if(url.contains(u)){
                //找到了,放行
                chain.doFilter(request, response);
                //break;
                return;
            }
        }
   
        //1. 判断session中是否有user
        HttpSession session = req.getSession();
        Object user = session.getAttribute("user");

        //2. 判断user是否为null
        if(user != null){
            // 登录过了
            //放行
            chain.doFilter(request, response);
        }else {
            // 没有登陆,存储提示信息,跳转到登录页面

            req.setAttribute("login_msg","您尚未登陆!");
            req.getRequestDispatcher("/login.jsp").forward(req,response);
        }
    }

    public void init(FilterConfig config) throws ServletException {
    }

    public void destroy() {
    }
}

9.2 Listener监听器

  • Listener 表示监听器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一
  • 监听器可以监听就是在 application,session,request 三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件
  • applicationServletContext 类型的对象
  • ServletContext 代表整个web应用在服务器启动的时候,tomcat会自动创建该对象在服务器关闭时会自动销毁该对象
9.2.1 监听器分类

JavaWeb 提供了8个监听器:

image-20240103225131071

常用: ServletContextListener

ServletContextListener 是用来监听 ServletContext 对象的创建和销毁

ServletContextListener 接口中有以下两个方法:

  • void contextInitialized(ServletContextEvent sce)ServletContext 对象**被创建**了会自动执行的方法
  • void contextDestroyed(ServletContextEvent sce)ServletContext 对象**被销毁**时会自动执行的方法
9.2.2 监听器运用

ServletContextListener 监听器

  • 定义一个类,实现ServletContextListener 接口
  • 重写所有的抽象方法
  • 使用 @WebListener 进行配置

代码如下:

@WebListener
public class ContextLoaderListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        //加载资源
        System.out.println("ContextLoaderListener...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        //释放资源
    }
}

启动服务器,就可以在启动的日志信息中看到 contextInitialized() 方法输出的内容,同时也说明了 ServletContext 对象在服务器启动的时候被创建了。

运行结果:

image-20240103225721047

9.2.2 监听器运用

ServletContextListener 监听器

  • 定义一个类,实现ServletContextListener 接口
  • 重写所有的抽象方法
  • 使用 @WebListener 进行配置

代码如下:

@WebListener
public class ContextLoaderListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        //加载资源
        System.out.println("ContextLoaderListener...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        //释放资源
    }
}

启动服务器,就可以在启动的日志信息中看到 contextInitialized() 方法输出的内容,同时也说明了 ServletContext 对象在服务器启动的时候被创建了。

运行结果:

image-20240103225721047

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值