springboot系列十二:拦截器和文件上传

在这里插入图片描述

基本介绍

1.在Spring Boot项目中, 拦截器是开发中常用手段, 要来做登陆验证, 性能检查, 日志记录等.

2.基本步骤:
√ 编写一个拦截器实现HandlerInterceptor接口
√ 拦截器注册到配置类中(实现WebMvcConfigureraddInterceptors)
√ 指定拦截器规则
回顾SpringMVC中写到的Interceptor: SpringMVC系列十一: 文件上传与自定义拦截器

拦截器应用实例

需求分析

需求: 使用拦截器防止用户非法登录. 使用拦截器就不需要在每个方法验证了.

●浏览器输入 http://localhost:8080/manage.html, 如果用户没有登录, 则返回登录页面

在这里插入图片描述

1.修改AdminController.java

使用Lombok支持日志输出

@Controller
@Slf4j
public class AdminController {
    //响应用户的登录请求
    @PostMapping("/login")
    public String login(Admin admin, HttpSession session, Model model) {
    		//...
    }

    //处理用户请求 manage.html
    @GetMapping("/manage.html")
    public String mainPage(Model model,
                           @SessionAttribute(value = "loginAdmin", required = false) Admin admin) {
        //这里暂时在方法中验证, 后面我们统一使用拦截器

        log.info("进入mainPage()");

        //用集合模拟用户数据, 放入到request域中, 并显示
        List<User> users = new ArrayList<>();
        users.add(new User(1, "张三", "123456", 23, "张三@163.com"));
        users.add(new User(2, "李四", "123456", 24, "李四@163.com"));
        users.add(new User(3, "王五", "123456", 25, "王五@163.com"));
        users.add(new User(4, "赵六", "123456", 26, "赵六@163.com"));
        users.add(new User(5, "田七", "123456", 27, "田七@163.com"));

        //将数据放入到request域中
        model.addAttribute("users", users);
        return "manage";//这里是我们的视图解析器,到 templates/manage.html
    }
}

2.修改 templates/manage.html, 去掉 [[${session.loginAdmin.name}]], 大体代码如下.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>管理后台</title>
</head>
<body bgcolor="#CED3FE">
<img src="images/1.GIF"/>
<a href='#'>返回管理界面</a>  <a href='#' th:href="@{/}">安全退出</a>   欢迎您:
<hr/>
<div style="text-align: center">
    <h1>管理雇员~</h1>
    <table border="1px" cellspacing="0" bordercolor="green" style="width:800px;margin: auto">
        <tr bgcolor="pink">
            <td>id</td>
            <td>name</td>
            <td>pwd</td>
            <td>age</td>
            <td>email</td>
        </tr>
        <tr bgcolor="#ffc0cb" th:each="user:${users}">
            <td th:text="${user.id}">a</td>
            <td th:text="${user.name}">b</td>
            <td th:text="${user.password}">c</td>
            <td th:text="${user.age}">d</td>
            <td th:text="${user.email}">e</td>
        </tr>
    </table>
    <br/>
</div>
<hr/>
<img src="images/logo.png"/>
</body>
</html>

3.浏览器/Postman, 能够访问 http://localhost:8080/manage.html

在这里插入图片描述

代码实现

1.创建src/main/java/com/zzw/springboot/interceptor/LoginInterceptor.java

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //为了让你们看到访问的URI
        String requestURI = request.getRequestURI();
        log.info("preHandle拦截到的请求的URI={}", requestURI);

        //进行登录的验证
        HttpSession session = request.getSession();
        Object loginAdmin = session.getAttribute("loginAdmin");
        if (loginAdmin != null) {//说明该用户已经成功登录
            //放行
            return true;
        }
        //拦截, 重新返回登陆页面
        request.setAttribute("error", "你没有登录, 请重新登录~");
        request.getRequestDispatcher("/").forward(request, response);
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle() 被执行...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion() 被执行...");
    }
}

2.注册拦截器到配置类中(实现WebMvcConfigurer接口的addInterceptors方法)

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器 LoginInterceptor
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") //拦截所有的请求
                .excludePathPatterns("/", "/login","/images/**"); //指定要放行的, 后面可以根据业务需求, 添加放行的请求路径
    }
}

3.测试

在这里插入图片描述

注意事项和细节

1.将/images/**去掉会有什么后果.

1)首先, 浏览器禁用缓存, 重启(是为了去掉保存在session里的登录信息).

在这里插入图片描述

2)其次, 刷新页面 http://localhost:8080/, 图片请求被拦截.

在这里插入图片描述

3)因为static是类路径下, 所谓的静态资源访问, 所以可以写成images/**, 而不用写成static/images/**

4)在前端代码中, 图片url可以写成<img src="images/1.GIF">, <img src="images/logo.png">

2.URIURL的区别. HttpServletRequest常用方法
URI: Universal Resource Identifier
URL: Universal Resource Locator
Identifier: 标识符. Locator: 定位器, 从字面上来看, URI可以在站点内唯一标识一个资源, URL可以在全网内提供找到该资源的路径.

举例

String requestURI = request.getRequestURI();
String requestURL = request.getRequestURL().toString();
log.info("preHandle拦截到的请求的URI={}", requestURI);// /manage.html
log.info("preHandle拦截到的请求的URL={}", requestURL);// http://localhost:8080/manage.html

3.注册拦截器, 依然可以使用如下方式, 参考注册自定义转换器

@Configuration
public class WebConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        /*
        class WebConfig2$1 implements WebMvcConfigurer {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                //注册自定义拦截器 LoginInterceptor
                registry.addInterceptor(new LoginInterceptor())
                        .addPathPatterns("/**") //拦截所有的请求
                        .excludePathPatterns("/", "/login", "/images/**"); //指定要放行的, 后面可以根据业务需求, 添加放行的请求路径
            }
        }
        WebMvcConfigurer webMvcConfigurer = new WebConfig2$1();
        return webMvcConfigurer;
         */
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                System.out.println("~~~~~匿名内部类注册拦截器~~~~~");
                //注册自定义拦截器 LoginInterceptor
                registry.addInterceptor(new LoginInterceptor())
                        .addPathPatterns("/**") //拦截所有的请求
                        .excludePathPatterns("/", "/login", "/images/**"); //指定要放行的, 后面可以根据业务需求, 添加放行的请求路径
            }
        };
    }
}

文件上传

需求说明

需求: 演示在SpringBoot 中通过表单注册用户, 并支持上传图片.

回顾 SpringMVC文件上传

代码实现

1.创建templates/upload.html, 要求头像只能选择一个, 而宠物可以上传多张图片

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>
<body bgcolor="#CED3FE">
<img src="images/1.GIF">
<div style="text-align: center;">
    <h1>注册用户~</h1>
    <form action="#" th:action="@{/upload}" method="post" enctype="multipart/form-data">
        用户名:<input type="text" style="width: 150px" name="username"><br/><br/>
        邮 件:<input type="text" style="width: 150px" name="email"><br/><br/>
        年 龄:<input type="text" style="width: 150px" name="age"><br/><br/>
        职 位:<input type="text" style="width: 150px" name="job"><br/><br/>
        头 像:<input type="file" style="width: 150px" name="avatar"><br/><br/>
        宠 物:<input type="file" style="width: 150px" name="pets" multiple><br/><br/>
        <input type="submit" value="注册"/>
        <input type="reset" value="重新填写">
    </form>
</div>
<img src="images/logo.png">
</body>
</html>

2.指定拦截器放行upload.html. upload

@Configuration
public class WebConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                System.out.println("~~~~~匿名内部类注册拦截器~~~~~");
                //注册自定义拦截器 LoginInterceptor
                registry.addInterceptor(new LoginInterceptor())
                        .addPathPatterns("/**") //拦截所有的请求
                        .excludePathPatterns("/", "/login", "/images/**", "/upload.html", "/upload"); //指定要放行的, 后面可以根据业务需求, 添加放行的请求路径
            }
        };
    }
}

3.创建src/main/java/com/zzw/springboot/controller/UploadController.java

@Controller
public class UploadController {

    //处理转发到用户注册页面upload.html-可以完成文件上传页面
    @RequestMapping("/upload.html")
    public String uploadPage() {
        return "upload";//thymeleaf进行视图解析, 转发到templates/upload.html
    }
}

4.浏览器访问 http://localhost:8080/upload.html, 测试

在这里插入图片描述

5.修改src/main/java/com/zzw/springboot/controller/UploadController.java

@Controller
@Slf4j
public class UploadController {

    //处理转发到用户注册页面upload.html-可以完成文件上传页面
    @RequestMapping("/upload.html")
    public String uploadPage() {
        return "upload";//thymeleaf进行视图解析, 转发到templates/upload.html
    }

    //处理用户的注册请求-包括处理文件上传
    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam(value = "username") String name,
                         @RequestParam(value = "email") String email,
                         @RequestParam(value = "age") Integer age,
                         @RequestParam(value = "job") String job,
                         @RequestParam(value = "avatar") MultipartFile avatar,
                         @RequestParam(value = "pets") MultipartFile[] pets) {
        //输出获取到的信息
        log.info("上传的信息 name={}, email={}, age={}, job={}, avartar={}, pets={}",
                name, email, age, job, avatar.getSize(), pets.length);

        //如果信息都注册成功, 我们就将文件保存到指定的目录, 比如保存在d:\\temp_upload
        //1.我们先将文件保存到指定到指定的目录, 比如d:\\temp_upload
        //2.后面我们再演示把文件保存到动态创建的目录
        
        return "用户注册成功/文件上传成功";
    }
}

6.修改src/main/java/com/zzw/springboot/controller/UploadController.java
SpringMVC系列十一: 文件上传与自定义拦截器

@Controller
@Slf4j
public class UploadController {

    //处理转发到用户注册页面upload.html-可以完成文件上传页面
    @RequestMapping("/upload.html")
    public String uploadPage() {
        return "upload";//thymeleaf进行视图解析, 转发到templates/upload.html
    }

    //处理用户的注册请求-包括处理文件上传
    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam(value = "username") String name,
                         @RequestParam(value = "email") String email,
                         @RequestParam(value = "age") Integer age,
                         @RequestParam(value = "job") String job,
                         @RequestParam(value = "avatar") MultipartFile avatar,
                         @RequestParam(value = "pets") MultipartFile[] pets) throws IOException {
        //输出获取到的信息
        log.info("上传的信息 name={}, email={}, age={}, job={}, avartar={}, pets={}",
                name, email, age, job, avatar.getSize(), pets.length);

        //如果信息都注册成功, 我们就将文件保存到指定的目录, 比如保存在d:\\temp_upload
        //1.我们先将文件保存到指定到指定的目录, 比如d:\\temp_upload
        //2.后面我们再演示把文件保存到动态创建的目录

        if (!avatar.isEmpty()) {//处理头像上传
            String originalFilename = avatar.getOriginalFilename();
            log.info("你要上传的文件名={}", originalFilename);
            //你要把文件上传到哪个路径[全路径: 包括文件名]
            String fileFullPath = "d:\\temp_upload\\" + originalFilename;
            File file = new File(fileFullPath);
            avatar.transferTo(file);
        }
        if (pets.length > 0) {//处理宠物图片[多张]
            for (MultipartFile pet : pets) {//循环遍历
                if (!pet.isEmpty()) {
                    String originalFilename = pet.getOriginalFilename();//宠物图片名
                    log.info("你要上传的宠物名={}", originalFilename);
                    String fileFullPath = "d:\\temp_upload\\" + originalFilename;
                    File file = new File(fileFullPath);
                    pet.transferTo(file);
                }
            }
        }
        return "用户注册成功/文件上传成功";
    }
}

7.测试

在这里插入图片描述

8.修改src/main/java/com/zzw/springboot/controller/UploadController.java

@Controller
@Slf4j
public class UploadController {

    //处理转发到用户注册页面upload.html-可以完成文件上传页面
    @RequestMapping("/upload.html")
    public String uploadPage() {
        return "upload";//thymeleaf进行视图解析, 转发到templates/upload.html
    }

    //处理用户的注册请求-包括处理文件上传
    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam(value = "username") String name,
                         @RequestParam(value = "email") String email,
                         @RequestParam(value = "age") Integer age,
                         @RequestParam(value = "job") String job,
                         @RequestParam(value = "avatar") MultipartFile avatar,
                         @RequestParam(value = "pets") MultipartFile[] pets) throws IOException {
        //输出获取到的信息
        log.info("上传的信息 name={}, email={}, age={}, job={}, avartar={}, pets={}",
                name, email, age, job, avatar.getSize(), pets.length);

        //如果信息都注册成功, 我们就将文件保存到指定的目录, 比如保存在d:\\temp_upload
        //1.我们先将文件保存到指定到指定的目录, 比如d:\\temp_upload
        //2.后面我们再演示把文件保存到动态创建的目录
        //  比如E:\idea_project\zzw_springboot\springboot-usersys\target\classes\static\images\\upload\

        //得到类路径(运行的时候)
        String path = ResourceUtils.getURL("classpath:").getPath();
        //log.info("path={}", path);

        //动态创建指定的目录
        File parentFile = new File(path + "static/images/upload/");
        if (!parentFile.exists()) {//如果目录不存在, 就创建 java io
            parentFile.mkdirs();
        }

        if (!avatar.isEmpty()) {//处理头像上传
            String originalFilename = avatar.getOriginalFilename();
            log.info("你要上传的文件名={}", originalFilename);
            //你要把文件上传到哪个路径[全路径: 包括文件名]
            //String fileFullPath = "d:\\temp_upload\\" + originalFilename;
            //File file = new File(fileFullPath);
            //这里我们需要指定保存文件的绝对路径: E:\idea_project\zzw_springboot\springboot-usersys\target\classes\static\images\\upload\

            //log.info("保存文件的绝对路径={}", parentFile.getAbsolutePath());
            //保存到动态创建的目录
            avatar.transferTo(new File(parentFile, originalFilename));
        }
        if (pets.length > 0) {//处理宠物图片[多张]
            for (MultipartFile pet : pets) {//循环遍历
                if (!pet.isEmpty()) {
                    String originalFilename = pet.getOriginalFilename();//宠物图片名
                    log.info("你要上传的宠物名={}", originalFilename);
                    //String fileFullPath = "d:\\temp_upload\\" + originalFilename;
                    //File file = new File(fileFullPath);

                    //保存到动态创建的目录
                    pet.transferTo(new File(parentFile, originalFilename));
                }
            }
        }
        return "用户注册成功/文件上传成功";
    }
}

9.测试

在这里插入图片描述

注意事项和细节

1.根据项目需求修改文件上传的参数,否则文件上传会抛出异常。MultipartProperties.java

@ConfigurationProperties用法

properties自动配置

在这里插入图片描述

在这里插入图片描述

2.修改src/main/resources/application.yml

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB

3.上传超大文件 8MB

在这里插入图片描述

在这里插入图片描述

课后扩展

1.解决文件覆盖问题, 如果文件名相同, 会出现覆盖问题, 如何解决.

2.解决文件分目录存放问题, 如果将文件都上传到一个目录下, 当上传文件很多时, 会造成访问文件速度变慢. 因此, 可以将文件上传到不同目录, 比如 一天上传的文件, 统一以年/月/日的形式放到一个文件夹, 比如2024/8/5目录.

3.参考 项目实战系列三: 家居购项目 第六部分
JavaWeb系列二十三: web 应用常用功能(文件上传下载)

代码实现
1.新建src/main/java/com/zzw/springboot/util/WebUtils.java

public class WebUtils {
    //定义一个文件上传的路径
    public static String UPLOAD_FILE_DIRECTORY = "static/images/upload/";
    
    //编写方法, 生成一个目录-根据当前日期, 生成 年/月/日
    public static String getUploadFileDirectory() {
        LocalDate ld = LocalDate.now();
        return ld.getYear() + "/" + ld.getMonthValue() + "/" + ld.getDayOfMonth() + "/";
    }
}

2.修改src/main/java/com/zzw/springboot/controller/UploadController.java

@Controller
@Slf4j
public class UploadController {

    //处理转发到用户注册页面upload.html-可以完成文件上传页面
    @RequestMapping("/upload.html")
    public String uploadPage() {
        return "upload";//thymeleaf进行视图解析, 转发到templates/upload.html
    }

    //处理用户的注册请求-包括处理文件上传
    @PostMapping("/upload")
    @ResponseBody
    public String upload(@RequestParam(value = "username", required = false) String name,
                         @RequestParam(value = "email", required = false) String email,
                         @RequestParam(value = "age", required = false) Integer age,
                         @RequestParam(value = "job", required = false) String job,
                         @RequestParam(value = "avatar", required = false) MultipartFile avatar,
                         @RequestParam(value = "pets", required = false) MultipartFile[] pets) throws IOException {
        //输出获取到的信息
        log.info("上传的信息 name={}, email={}, age={}, job={}, avartar={}, pets={}",
                name, email, age, job, avatar.getSize(), pets.length);

        //如果信息都注册成功, 我们就将文件保存到指定的目录, 比如保存在d:\\temp_upload
        //1.我们先将文件保存到指定到指定的目录, 比如d:\\temp_upload
        //2.后面我们再演示把文件保存到动态创建的目录
        //  比如E:\idea_project\zzw_springboot\springboot-usersys\target\classes\static\images\\upload\

        //得到类路径(运行的时候)
        String path = ResourceUtils.getURL("classpath:").getPath();
        //log.info("path={}", path);

        //动态创建指定的目录
        File parentFile = new File(path + WebUtils.getUploadFileDirectory());
        if (!parentFile.exists()) {//如果目录不存在, 就创建 java io
            parentFile.mkdirs();
        }

        if (!avatar.isEmpty()) {//处理头像上传
            String originalFilename = avatar.getOriginalFilename();
            log.info("你要上传的文件名={}", originalFilename);

            //对上传的文件名进行处理, 增加一个前缀, 保证是唯一的, 防止文件名重复造成覆盖
            String fileName = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + originalFilename;

            //你要把文件上传到哪个路径[全路径: 包括文件名]
            //String fileFullPath = "d:\\temp_upload\\" + originalFilename;
            //File file = new File(fileFullPath);
            //这里我们需要指定保存文件的绝对路径: E:\idea_project\zzw_springboot\springboot-usersys\target\classes\static\images\\upload\

            //log.info("保存文件的绝对路径={}", parentFile.getAbsolutePath());
            //保存到动态创建的目录
            File destPath = new File(parentFile, fileName);
            if (!destPath.exists()) {
                destPath.mkdirs();
            }
            avatar.transferTo(new File(destPath, fileName));
        }
        if (pets.length > 0) {//处理宠物图片[多张]
            for (MultipartFile pet : pets) {//循环遍历
                if (!pet.isEmpty()) {
                    String originalFilename = pet.getOriginalFilename();//宠物图片名
                    log.info("你要上传的宠物名={}", originalFilename);

                    //对上传的文件名进行处理, 增加一个前缀, 保证是唯一的, 防止文件名重复造成覆盖
                    String fileName = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + originalFilename;

                    //String fileFullPath = "d:\\temp_upload\\" + originalFilename;
                    //File file = new File(fileFullPath);

                    //保存到动态创建的目录
                    File destPath = new File(parentFile, fileName);
                    if (!destPath.exists()) {
                        destPath.mkdirs();
                    }
                    avatar.transferTo(new File(destPath, fileName));
                }
            }
        }
        return "用户注册成功/文件上传成功";
    }
}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot 中,你可以通过使用拦截器来统一限制上传文件的类型和大小。拦截器可以拦截所有的上传请求,然后在上传之前进行校验。 首先,你需要创建一个拦截器类,并实现 HandlerInterceptor 接口。在拦截器中,你可以重写 preHandle 方法来实现文件上传的校验。以下是一个简单的例子: ``` public class FileUploadInterceptor implements HandlerInterceptor { private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB private static final List<String> ALLOWED_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/png"); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; // 检查方法是否有 @PostMapping 注解 PostMapping postMapping = handlerMethod.getMethodAnnotation(PostMapping.class); if (postMapping == null) { return true; } // 检查方法参数是否有 MultipartFile 类型 MethodParameter[] methodParameters = handlerMethod.getMethodParameters(); for (MethodParameter methodParameter : methodParameters) { if (methodParameter.getParameterType().equals(MultipartFile.class)) { MultipartFile file = (MultipartFile) request.getAttribute(methodParameter.getParameterName()); // 检查文件大小 if (file.getSize() > MAX_FILE_SIZE) { response.setStatus(HttpStatus.BAD_REQUEST.value()); response.getWriter().write("File size too large"); return false; } // 检查文件类型 String contentType = file.getContentType(); if (!ALLOWED_CONTENT_TYPES.contains(contentType)) { response.setStatus(HttpStatus.BAD_REQUEST.value()); response.getWriter().write("File type not allowed"); return false; } } } } return true; } } ``` 在上面的代码中,我们首先检查请求处理的方法是否有 @PostMapping 注解,并且是否有 MultipartFile 类型的参数。如果都符合条件,我们就可以从请求中获取到上传的文件,并进行校验。如果上传的文件不符合要求,我们就返回一个错误响应。否则,就放行请求,让请求继续被处理。 接下来,你需要在 Spring Boot 应用程序中注册这个拦截器。你可以创建一个配置类,并实现 WebMvcConfigurer 接口。在这个配置类中,你可以重写 addInterceptors 方法来注册你的拦截器。以下是一个简单的例子: ``` @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private FileUploadInterceptor fileUploadInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(fileUploadInterceptor) .addPathPatterns("/upload"); } } ``` 在上面的代码中,我们首先创建了一个 WebMvcConfigurer 类,并注入了我们之前创建的 FileUploadInterceptor。然后,我们重写了 addInterceptors 方法,并将我们的拦截器注册到了 /upload 路径下。这样,当用户上传文件时,我们的拦截器就会拦截请求,并对上传的文件进行校验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

~ 小团子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值