SpringMVC学习笔记 - 02

SpringMVC系列文章


1. 响应数据和结果视图

对于控制器(Controller)中方法的返回值,可以为以下其中一种:

  • String 字符串类型
  • void 无返回值
  • ModelAndView SpringMVC 为我们提供的一种可以包含数据和视图的对象
  • JavaBean 对象 (通过 json 数据响应)

1.1 返回值为 String

  • 当控制器返回值为 String 类型的时候,默认情况是返回逻辑视图名,然后被视图解析器解析为物理视图地址,最终底层通过请求转发将请求转发到对应页面。见代码
  • jsp代码
<a href="${pageContext.request.contextPath}/user/testString">testString</a><hr/>
  • 控制器代码
/**
* 返回值为 String ,结合 model 对象保存数据
* @param model
* @return
*/
@RequestMapping("/testString")
public String testString(Model model) {
    System.out.println("testString方法执行了...");
    // 模拟从数据库查数据
    User user = new User();
    user.setUsername("鱼开饭");
    user.setPassword("123456");
    user.setAge(22);

    // 添加到request域
    model.addAttribute("user", user);
    
    // 逻辑视图名 success 会被视图解析器解析为 /WEB-INF/pages/success.jsp
    // 可以在 springmvc 配置文件中修改视图解析器的解析
    return "success";
}

1.2 返回值为 void

  • 当使用 void 作为控制器方法的返回值时,方法执行结束后默认转发的路径为当前方法绑定的路径,也就是@RequestMapping 中的值。但是实际上这个返回值也会被视图解析器解析,所以一般都会报 404 错误。譬如,此时有一个控制器方法为:
@RequestMapping("/testVoid")
public void testVoid() {
	// ...
}
  • 当方法执行结束后,默认会返回 /testVoid ,经过视图解析器解析后则为 /WEB-INF/pages/testVoid.jsp,如图
    void默认返回值
  • 如果既想使用 void 返回值,同时又想对请求做出响应,譬如请求转发或者重定向,那么我们就可以利用 Servlet 提供的原生 API 作为控制器方法的参数
  • 请求转发
@RequestMapping("/testVoid")
public void testVoid(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
    System.out.println("testVoid方法执行了...");

    // 转发是一次请求,无需加项目虚拟目录,但是因为直接通过原生api进行转发,不会经过视图解析器,所以应该写具体路径
    request.getRequestDispatcher("/WEB-INF/pages/success.jsp").forward(request, response);
    // 转发或重定向后,如果还有代码,会继续执行,此时可以使用 return; 结束
    return;
}
  • 重定向
@RequestMapping("/testVoid")
public void testVoid(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
    System.out.println("testVoid方法执行了...");

    // 重定向,需要项目虚拟目录
    response.sendRedirect(request.getContextPath() + "/index.jsp");
    // 转发或重定向后,如果还有代码,会继续执行,此时可以使用 return; 结束
    return;
}
  • 直接像客户端生成响应,譬如响应 json 数据
@RequestMapping("/testVoid")
public void testVoid(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
    System.out.println("testVoid方法执行了...");

    // 直接响应,先设置编码
    response.setContentType("text/html;charset=utf-8");
    response.getWriter().println("假设这是 Json 数据");
    // 转发或重定向后,如果还有代码,会继续执行,此时可以使用 return; 结束
    return;
}

当方法的参数中含有 HttpServletResponse 时,即使方法体中不做任何处理,方法结束后也不会跳转到别的页面。关于这点,查阅官方文档,是因为此时 SpringMVC 会认为对请求已经作出了响应,所以不会再帮我们跳转到别的页面。
在这里插入图片描述

1.3 返回值为 ModelAndView

  • 当控制器方法返回值为 ModelAndView 的时候,其实就相当于第一种方式的返回值为 String 加上参数为 Model,因为 ModelAndView 也可以传入数据(在页面上直接使用 EL 表达式获取:${attributeName}),同时还可以设置将要跳转的逻辑视图名称。见代码
  • jsp 代码
<!-- response.jsp -->
<a href="${pageContext.request.contextPath}/user/testModelAndView">testModelAndView</a>

<!-- success.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>成功</title>
</head>
<body>
<h3>执行成功</h3>
    ${requestScope.user}
</body>
</html>
  • 控制器方法
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
    System.out.println("testModelAndView方法执行了...");
    // 创建 ModelAndView 对象
    ModelAndView mv = new ModelAndView();

    // 模拟查询数据
    User user = new User();
    user.setUsername("用户1");
    user.setPassword("654321");
    user.setAge(10);

    // 将 user 对象存入到 ModelAndView 对象中,最终存入 request 域
    mv.addObject("user", user);

    // 指定要跳转的逻辑视图名,会使用视图解析器
    mv.setViewName("success");

    return mv;
}

1.4 请求转发和重定向

  • 当控制器方法的返回值为 String 的时候,默认就是请求转发,我们也可以像上面的例子一样,使用 Servlet 原生的 API 来完成请求转发或者重定向。不过,SpringMVC 还为我们提供了另一种方式,那就是使用 forward:redirect: 关键字。见代码
@RequestMapping("/testForward")
public String testForward() {
    System.out.println("testForward方法执行了...");

    // forward: 关键字转发,不会使用视图解析器
    return "forward:/WEB-INF/pages/success.jsp";
}
@RequestMapping("/testRedirect")
public String testRedirect() {
    System.out.println("testRedirect方法执行了...");

    // redirect: 关键字重定向,不用加项目路径
    return "redirect:/index.jsp";
}
  • 对于forward: 关键字,根据官方文档,底层也是使用 Servlet 原生 API 进行转发,也就是 RequestDispatcher.forward() ,所以不会经过视图解析器,因此转发路径需要具体路径。
    在这里插入图片描述
  • 对于 response: 关键字,根据官方文档,SpringMVC 会根据当前 Servlet 的上下文进行重定向,因此不需要写项目路径。
    在这里插入图片描述

1.5 使用 @RequestBody 和 @ResponseBody 进行 json 交互

  • 在开发中,很多时候都是客户端提交 json 数据,服务器接收 json 数据并解析,然后生成 json 响应给客户端。这个时候我们就可以使用 @RequestBody@ResponseBody 来完成 json 数据的交互。见代码
  • jsp代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Json 数据交互</title>
    <script src="js/jquery-1.8.2.min.js"></script>
    <script type="text/javascript">
        // 页面加载
        $(function () {
            // 绑定单击事件
            $("#btn").click(function () {
                // 发送ajax请求
                $.ajax({
                    url: "${pageContext.request.contextPath}/user/testAjax",
                    // 发送数据的 MIME 类型
                    contentType: "application/json;charset=utf-8",
                    // 发送数据为一个 json 串
                    data: '{"username":"鱼开饭","password":"123456","age":22}',
                    // 返回的数据类型
                    dataType: "json",
                    // 请求方式
                    type: "post",
                    // 成功回调函数
                    success: function (data) {
                        console.log(data);
                        console.log(data.username);
                        console.log(data.password);
                        console.log(data.age);
                    }
                });
            });
        });
    </script>
</head>
<body>
	<button id="btn">测试 ajax 请求 json 和响应 json</button>
</body>
</html>

因为我们配置了前端控制器 DispatcherServlet ,因此对于所有的请求都会进行拦截,包括静态资源。为了使引入的 jquery.js 不被拦截,我们需要在配置文件 springmvc.xml 中配置不拦截静态资源

<!-- 设置静态资源不过滤 -->
<mvc:resources location="/css/" mapping="/css/**"/> <!-- 样式 -->
<mvc:resources location="/images/" mapping="/images/**"/> <!-- 图片 -->
<mvc:resources location="/js/" mapping="/js/**"/> <!-- javascript -->
  • location 属性 : 指定 webapp 目录下的包
  • mapping 属性 : 表示以 /static 开头的所有请求路径
  • 控制器代码
/**
 * responsebody 加在方法上或者加在返回值前面都可以
 *
 * @param user 
 */
@RequestMapping("/testAjax")
public @ResponseBody User testAjax(@RequestBody User user) {
    System.out.println("testAjax方法执行了...");
    System.out.println(user);

    // 模拟查询出新的用户
    user.setPassword("654321");
    user.setAge(10);

    return user;
}
  • 客户端发送了一个 json 串,服务器使用 @RequestBody 获取请求体内容,并使用 jackson 将 json 串封装到 User 对象中;服务器使用 @ResponseBody 生成响应,通过 jackson 将 User 对象转成 json 串。
  • SpringMVC 默认使用 MappingJacksonHttpMessageConverter 进行转换,因此需要导入 jackson 的依赖
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>
  • 使用 jackson 进行 json 和 JavaBean 对象的自动转换时,需要保证 json 串的键与 JavaBean 的属性名称一一对应

2. SpringMVC 文件上传

2.1 传统方式文件上传

  • 在讲文件上传之前,我们需要了解以下几点:
    • 用于提交文件的表单的 enctype 属性必须为 multipart/form-data(默认值为 application/x-www-form-urlencoded),该属性是指定表单请求正文的类型
    • 表单的 method 属性必须为 post
    • 表单中需要提供一个文件选择域 <input type="file" />
  • 使用传统方式进行文件上传时,我们需要依赖 Commons-fileupload jar 包,所以需要先导入依赖
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>
  • 编写 jsp 代码
<h3>传统方式文件上传</h3>
<form action="${pageContext.request.contextPath}/fileupload/test1" method="post" enctype="multipart/form-data">
    选择文件:<input type="file" name="upload"/><br/>
    <input type="submit" value="上传文件">
</form>
  • 编写控制器代码
@Controller
@RequestMapping("/fileupload")
public class FileUploadController {
    /**
     * 使用 fileupload jar包实现传统文件上传,上传的文件和访问的应用存在于同一台服务器上
     *
     * @param request
     * @return
     */
    @RequestMapping("/test1")
    public String test1(HttpServletRequest request) throws Exception {
        System.out.println("传统方式文件上传...");

        // 获取要保存的真实目录路径
        String path = request.getSession().getServletContext().getRealPath("/uploads");
        // 判断文件夹是否存在,不存在则创建
        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        // 创建磁盘文件工厂
        DiskFileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        // 解析 request 对象,获取上传文件项
        List<FileItem> fileItems = upload.parseRequest(request);
        for (FileItem fileItem : fileItems) {
            if (fileItem.isFormField()) {
                // 普通表单项,不做处理
            } else {
                // 上传文件项,获取文件名
                String filename = fileItem.getName();
                // 加时间戳防止重名
                filename = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "_" + filename;
                // 上传文件
                fileItem.write(new File(path, filename));
                // 删除临时文件
                fileItem.delete();
            }
        }

        return "success";
    }
}

2.2 SpringMVC 方式文件上传

  • 从上面的例子可以看出,如果使用传统方式进行文件上传,我们需要自己解析 request,分辨每一项究竟是普通表单项还是文件项,特别繁琐。而如果使用 SpringMVC,我们只需要配置一个文件解析器,由文件解析器来替我们解析,然后我们在控制器使用 MultipartFile 对象接收即可(该对象表示要上传的文件)
  • 控制器代码
/**
 * SpringMVC 方式上传,同服务器
 *
 * @param request
 * @param upload  参数名称必须与表单中 file 组件的名称保持一致
 * @return
 */
@RequestMapping("/test2")
public String test2(HttpServletRequest request, MultipartFile upload) throws Exception {
    System.out.println("SpringMVC方式文件上传...");

    // 获取要保存的真实目录路径
    String path = request.getSession().getServletContext().getRealPath("/uploads");
    // 判断文件夹是否存在,不存在则创建
    File file = new File(path);
    if (!file.exists()) {
        file.mkdirs();
    }
    // 获取文件原始名
    String filename = upload.getOriginalFilename();
    // 加时间戳
    filename = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "_" + filename;
    // 上传文件
    upload.transferTo(new File(path, filename));

    return "success";
}
  • 使用该方式上传文件时,要确保 MultipartFile 对象名称与表单中 file 组件的名称保持一致。如果不一致,则需要使用 @RequestParam 进行参数的绑定。譬如表单为:<input type="file" name="file"/>,则控制器方法应该为 test2(HttpServletRequest request, @RequestParam("file") MultipartFile upload)
  • 获取上传文件的文件名时,应该使用 getOriginalFilename() ,不能使用 getName()getName() 是获取表单中 file 组件的名称。
  • 如果要想实现多文件上传,那么需要在 file 组件加上属性 multiple="multiple"(HTML5 的新属性),然后控制器用 List<MultipartFile> 接收即可。
  • 在 SpringMVC 配置文件中配置文件解析器
<!-- 配置文件解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 配置上传文件最大大小,字节(Byte)为单位。这里设置 10MB=10*1024KB=10*1024*1024B -->
    <property name="maxUploadSize" value="10485760"/>
    <!-- 设置编码,防止中文乱码 -->
    <property name="defaultEncoding" value="utf-8"/>
</bean>
  • 文件解析器的 id 必须为 multipartResolvermaxUploadSize 属性以 Byte 为单位。

2.3 SpringMVC 跨服务器方式文件上传

  • 在实际开发中,我们可能会有好几台服务器,有的服务器用于部署应用,而有的服务器用于存储用户上传的文件。那么,此时我们如果需要跨服务器上传文件,就需要借助 jersey-client 的 jar 包。
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-client</artifactId>
    <version>1.18.1</version>
</dependency>
  • 控制器代码
/**
 * SpringMVC 跨服务器方式的文件上传
 *
 * @param upload
 * @return
 */
@RequestMapping("/test3")
public String test3(MultipartFile upload) throws IOException {
    System.out.println("SpringMVC 跨服务器文件上传...");

    // 定义文件服务器存储路径,注意这里末尾的 /
    String path = "http://localhost:8888/uploadserver/uploads/";
    // 获取上传文件名
    String filename = upload.getOriginalFilename();
    filename = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + "_" + filename;

    // 创建客户端对象
    Client client = Client.create();
    // 和文件服务器进行连接,指定上传文件的地址,该地址是 web 路径
    WebResource resource = client.resource(path + filename);
    // 上传文件,使用字节数组传输
    resource.put(upload.getBytes());

    return "success";
}
  • 想要测试跨服务器文件上传,需要部署两个 Tomcat 服务器,一个服务器部署应用,一个服务器用于接收文件。这里将存放文件的路径指定为接收文件项目的根目录下的 uploads 文件夹。
  • 如果程序报 405 错误,那么应该在文件服务器的 Tomcat 的配置文件 conf\web.xml 中加入以下配置,表示允许服务器进行读写操作(默认只读)
    在这里插入图片描述
<servlet>
      <servlet-name>default</servlet-name>
      <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
      <init-param>
          <param-name>readonly</param-name>
          <param-value>false</param-value>
      </init-param>
</servlet>
  • 如果程序报 409 错误,那么应该在文件服务器的 target 目录下创建 uploads 文件夹
    在这里插入图片描述
    在这里插入图片描述

3. SpringMVC 中的异常处理

  • 在开发中,一般我们都是 Controller 调用 ServiceService 调用 Dao ,在这个过程中,如果有某个地方出现了异常,那么异常都会被往上一层一层 throws Exception (抛出)。Dao 抛给 Service , Service 抛给 DispatcherServlet ,最后由 DispatcherServlet 将异常交给 HandlerExceptionResolver (异常处理器)进行处理。

3.1 操作步骤

  • 编写自定义异常类,记得继承 Exception
public class SysException extends Exception {
    private String message;

    public SysException(String message) {
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
  • 编写错误页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>错误页面</title>
</head>
<body>
${errorMsg}
</body>
</html>
  • 编写自定义异常处理器,需要实现 HandlerExceptionResolver 接口
public class SysExceptionResolver implements HandlerExceptionResolver {
    /**
     * 异常处理
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @param handler             当前异常处理器对象
     * @param e                   捕获的异常对象
     * @return
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler, Exception e) {
        // 打印异常信息
        e.printStackTrace();

        SysException sysException;
        // 如果抛出的是自定义异常,那么就直接转换
        if (e instanceof SysException) {
            sysException = (SysException) e;
        } else {
            // 如果抛出的不是自定义异常,那么就创建一个新的自定义异常
            sysException = new SysException("系统正在维护...");
        }
        // 创建 ModelAndView 对象
        ModelAndView mv = new ModelAndView();
        mv.addObject("errorMsg",sysException.getMessage());
        mv.setViewName("error");

        return mv;
    }
}
  • 在 SpringMVC 配置文件中配置异常处理器
<!-- 配置异常处理器 -->
<bean id="sysExceptionResolver" class="cn.ykf.exception.SysExceptionResolver"></bean>
  • 测试代码
@Controller
@RequestMapping("/exception")
public class ExceptionController {

    @RequestMapping("/testException")
    public String testException() throws SysException {
        System.out.println("testException方法执行了...");
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            // 抛出自定义异常,这里必须在方法声明上 throws
            throw new SysException("抛出了一个自定义异常...");
        }

        return "success";
    }

4. SpringMVC 中的拦截器

4.1 拦截器的说明

  • SpringMVC 中的 Interceptor(拦截器) 类似于 Servlet 中的 Filter(过滤器),拦截器是用于对 Controller(处理器)进行预处理和后处理的。和过滤器类似,过滤器可以定义过滤链,拦截器也可以定义拦截器链(Interceptor Chain),也就是将拦截器按一定的顺序联结成一条链,在访问被拦截的方法之前,拦截器链中的拦截器就会按之前定义的顺序进行拦截
  • 过滤器和拦截器虽然比较类似,但是还是有区别的:
    • 过滤器是 Servlet 规范中的,也就是说,只要是 JavaWeb 工程就可以使用;而拦截器是 SpringMVC 框架的,只有使用了 SpringMVC 框架的工程才可以使用
    • 过滤器可以拦截所有的资源,也就是不仅可以拦截 Controller ,其它的所有资源(像 jsp、js 这些静态资源)它都可以拦截;而拦截器是针对 Controller 的,它只能拦截控制器
    • 拦截器是 AOP 思想的一种实现方式
  • 如果想自定义拦截器,那么就需要实现 HandlerInterceptor 接口,该接口中有 3 个 default 方法:
    • boolean preHandle(..) : 该方法在 Controller 方法执行前调用。如果返回 true 就代表放行,执行下一个拦截器,如果没有拦截器,那就执行 Controller 的方法;如果返回 false 就代表不放行,此时可以使用转发或者重定向跳转到指定的页面。该方法是按拦截器定义顺序进行调用的。
    • void postHandle(..) : 该方法在 Controller 方法执行后(但是在视图渲染之前)调用。此时我们可以通过 ModelAndView (模型和视图对象)来对模型数据或视图进行处理,也可以通过 HttpServletRequestHttpServletResponse 进行转发或者重定向。该方法是按拦截器定义逆序进行调用的。
    • void afterCompletion(..) : 该方法在 整个请求处理完毕后调用(也就是在视图渲染完毕后)。 可以在该方法中进行一些资源清理的操作(不能在该方法中进行页面的跳转了),相当于 try...catch...finally 中的 finally。该方法是按拦截器定义逆序进行调用的,而且只有 preHandle() 返回 true 才调用。

4.2 操作步骤

  • 创建自定义拦截器,实现 Interceptor 接口并重写其中的方法
public class MyInterceptor1 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("第 1 个拦截器中的preHandle方法执行了...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("第 1 个拦截器中的postHandle方法执行了...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("第 1 个拦截器中的afterCompletion方法执行了...");
    }
}
  • 在 SpringMVC 配置文件中配置拦截器
<!-- 配置拦截器链 -->
<mvc:interceptors>
    <!-- 配置第一个拦截器 -->
    <mvc:interceptor>
        <!-- 要拦截的路径 -->
        <mvc:mapping path="/user/*"/>
        <!-- 不拦截的路径 -->
        <!--<mvc:exclude-mapping path=""/>-->
        <!-- 注册拦截器对象 -->
        <bean class="cn.ykf.interceptor.MyInterceptor1"/>
    </mvc:interceptor>
</mvc:interceptors>

<mvc:mapping> 标签用于指定要拦截的路径,而 <mvc:exclude-mapping> 标签用于指定不拦截的路径,两者选其一即可。

  • 编写控制器代码
@Controller
@RequestMapping("/user")
public class UserController {
    /**
     * 测试拦截器
     * @return
     */
    @RequestMapping("/testInterceptor")
    public String testInterceptor() {
        System.out.println("testInterceptor方法执行了...");
        return "success";
    }
}
  • 编写jsp代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>成功</title>
</head>
<body>
<h3>执行成功</h3>
<%
    System.out.println("success.jsp 执行了");
%>
</body>
</html>
  • 运行结果
    在这里插入图片描述

4.3 特殊情况

  • 如果有多个拦截器,此时拦截器 1 的 preHandle 方法返回 true,但是拦截器 2 的 preHandle 方法返回 false,那么拦截器 1 的 afterCompletion 的方法是否执行?
    • 因为我们上面说过,afterCompletion 方法只有当 preHandle 返回 true 的时候执行。所以,拦截器 1 的 afterCompletion 方法会执行,而拦截器 2 的 afterCompletion 方法不会执行。
      一个放行一个不放行
  • 如果某个拦截器在 postHandle 方法中使用 requestresponse 进行转发,那么最终的结果视图(这里是 success.jsp)还会不会执行?
    • 我们上面说过,postHandle 方法是在控制器执行后,视图渲染前被调用。所以,当我们转发到某个页面时,浏览器会把这个页面展示出来,但是 postHandle 方法还是会继续执行,也就是说 success.jsp 还是会被执行到,只不过不会再被渲染展示了
      在这里插入图片描述
  • 如果某个拦截器在 postHandle 方法中不通过 request 进行转发,而是使用 ModelAndView.setViewName("error"),那么最终的结果视图(这里是 success.jsp)还会不会执行?
    • 因为 ModelAndView 中存储的是结果视图,也就是控制器方法的返回值 success,此时我们直接调用 setViewName() 方法,就相当于修改了控制器方法的返回值,所以最终只会跳转到 error.jsp 而不是 success.jsp
      修改结果视图
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值