Day76.文件上传、下载 | SpringMVC底层:运行原理、请求处理、启动过程、ContextLoaderListener

目录

文件上传、下载

一、文件上传

完善1:如果目录不存在就创建

完善2:通过uuid,避免同名文件覆盖

完善3:限制上传文件类型

完善4:限制上传文件大小

完善5:使用逻辑路径取代物理路径

最终代码:

二、文件下载

三、其他不重要的内容

1.springmvc配置文件默认位置和名称

2.@RequestMapping的更多参数

3.@ModelAttribute注解 (每个handler方法执行前执行)

SpringMVC 运行原理及其源码解读

一、SpringMVC运行原理

SpringMVC运行原理-SpringMVC核心API

二、请求处理过程

请求处理过程源码解读 (代码debug)

三、启动过程 (IoC容器初始化)

四、ContextLoaderListener原理源码

2、使用Listener创建IoC容器

2、出现的问题及其解决 (创建Bean重复,事务问题)


文件上传、下载

一、文件上传

底层基于commons-fileupload 组件,进行封装,简化操作。

上传文件的处理:
1、上传到服务器指定目录下
2、将文件路径写入到数据库表中,后续下载使用
注意:文件上传是将客户端的文件上传到服务器端,底层其实是Socket编程。进行了封装,不需要编写底层代码;目前客户端就是服务器端,在一台计算机。

添加依赖(pom.xml)

    <!--文件上传-->
    <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.1</version>
    </dependency>

配置解析器(springmvc.xml)

springmvc.xml
<!-- 配置文件上传解析器-->
<bean id="multipartResolver"
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
    <property name="defaultEncoding" value="UTF-8"/>
</bean>

准备页面

第一点:请求方式必须是 POST

第二点:请求体的编码方式必须是 multipart/form-data(通过 form 标签的 enctype 属性设置)

第三点:使用 input 标签、type 属性设置为 file 来生成文件上传框

<h1>文件上传</h1>
<form th:action="@{/user/regist2}" method="post" enctype="multipart/form-data">
    用户名:<input type="text" name="uid"> <br>
    姓名:<input type="text" name="uname"> <br>
    年龄:<input type="text" name="age"> <br>
    图像:<input type="file" name="photo1"> <br>
    <input type="submit" value="提交">
</form>

准备控制器

注意:因为User类有String photo属性,所以MultipartFile 的变量名不要重复

关键:MultipartFile,因为在xml中的配置

@Controller
@Slf4j
public class UserController {
    @RequestMapping("/user/regist")
    public String register(User user, MultipartFile photo1) throws IOException {
        //将照片的路径写入User对象
        user.setPhoto(photo1.getOriginalFilename());

        log.debug("user:"+user);
        log.debug("photo1"+photo1);

        log.debug("文件名:"+photo1.getOriginalFilename());
        log.debug("MIME类型:"+photo1.getContentType());
        log.debug("文件大小:"+photo1.getSize());
        log.debug("上传file表单项的name:"+photo1.getName());
        //准备上传文件夹
        File file = new File("e:/upload",photo1.getOriginalFilename());
        //执行转存
        photo1.transferTo(file);
        return "result";
    }


完善1:如果目录不存在就创建

    @RequestMapping("/user/regist2")
    public String register2(User user, MultipartFile photo1) throws IOException {
        //将照片的路径写入User对象
        log.debug("user:"+user);
        log.debug("photo1"+photo1);

        //完善1:如果文件夹不存在,就创建
        File dir = new File("e:/upload");
        if(!dir.exists()){
            dir.mkdirs();//可以创建多级
            //dir.mkdir();//只能创建一级
        }
        File file = new File(dir,photo1.getOriginalFilename());
        photo1.transferTo(file);
        return "result";
    }

完善2:通过uuid,避免同名文件覆盖

UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。

UUID只保证唯一,不保证有序,字符串类型UUID是一个128比特的数值,这个数值可以通过一定的算法计算出来。

UUID的唯一缺陷在于生成的结果串会比较长。关于UUID这个标准使用最普遍的是微软的GUID(Globals Unique Identifiers)。在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),可以从cflib 下载CreateGUID() UDF进行转换。

UUID由以下几部分的组合:

(1)UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。

(2)时钟序列

(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

        //完善2:避免同名文件覆盖,通过uuid
        String fileName = UUID.randomUUID().toString();

        //拼接文件扩展(后缀)名
        String extName = photo1.getOriginalFilename().substring(photo1.getOriginalFilename().lastIndexOf("."));
        fileName = fileName+extName;

完善3:限制上传文件类型

        //完善3:限制上传文件的类型
        //方法1 按照扩展名来限制 .jpg .gif .png
        //if(".extName".equals(extName.toLowerCase()) && !".gif".equals(extName.toLowerCase())){}        

        //方式2 通过MIME类型来限制 tomcat的web.xml中 有所有的MIME类型和扩展名的对应关系
        String contentType = photo1.getContentType().toLowerCase();
        //如果不同,错误信息
        if(!"image/jpeg".equals(contentType) && !"image/gif".equals(contentType)){
            model.addAttribute("error","上传类型只能是jpg和gif类型");
            return "portal";
        }

完善4:限制上传文件大小

在代码中限制,不采用

//完善4:限制上传文件大小
long size = photo1.getSize();
if(size>1024*100){ //100K
    model.addAttribute("error","上传大小不能超过100K");
    return "portal";
}

 在springmvc.xml中限制

    <!--配置文件上传解析器-->
    <bean id="multipartResolver"
          class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 由于上传文件的表单请求体编码方式是 multipart/form-data 格式,所以要在解析器中指定字符集 -->
        <property name="defaultEncoding" value="UTF-8"/>
        <!--限制多个上传文件的总大小-->
        <property name="maxUploadSize" value="1024000"></property>
        <!--限制单个上传文件的大小-->
        <property name="maxUploadSizePerFile" value="1024000"></property>
    </bean>

完善5:使用逻辑路径取代物理路径

原因:

  1. 在Linux中没有盘符的概念,在WINDOWS中盘符的概念。上传文件夹无法统一的指定,比如“d:/upload”
  2. 也可以让上传文件夹在项目中(不是开发时目录,是部署后目录),不同的服务器目录可能不同。因为Tomcat、idea的安装位置不同,导致真实目录不同。

解决:使用逻辑路径取代物理路径

        //完善5:根据逻辑路径获取物理路径
        String realPath = servletContext.getRealPath("/upload");
        System.out.println("tomcat中项目真实路径:"+realPath);
        //tomcat中项目真实路径:E:\tomcat-8.5.27\webapps\day13updownload\upload
        File dir = new File(realPath);

最终代码:


@Controller
@Slf4j
public class UserController {

    @Autowired
    private ServletContext servletContext;

    @RequestMapping("/user/regist2")
    public String register2(User user, MultipartFile photo1, Model model) throws IOException {
        //将照片的路径写入User对象
        log.debug("user:"+user);
        log.debug("photo1"+photo1);

        //完善5:根据逻辑路径获取物理路径
        String realPath = servletContext.getRealPath("/upload");
        System.out.println("tomcat中项目真实路径:"+realPath);
        File dir = new File(realPath);

        //完善1:如果文件夹不存在,就创建
        //File dir = new File("e:/upload");
        if(!dir.exists()){
            dir.mkdirs();//可以创建多级
            //dir.mkdir();//只能创建一级
        }
        //完善2:避免同名文件覆盖,通过uuid
        String fileName = UUID.randomUUID().toString();

        //拼接文件扩展(后缀)名
        String extName = photo1.getOriginalFilename().substring(photo1.getOriginalFilename().lastIndexOf("."));
        fileName = fileName+extName;


        //完善3:限制上传文件的类型
        //方法1 按照扩展名来限制 .jpg .gif .png
        //if(".extName".equals(extName.toLowerCase()) && !".gif".equals(extName.toLowerCase())){}        

        //方式2 通过MIME类型来限制 tomcat的web.xml中 有所有的MIME类型和扩展名的对应关系
        String contentType = photo1.getContentType().toLowerCase();
        //如果不同,错误信息
        if(!"image/jpeg".equals(contentType) && !"image/gif".equals(contentType)){
            model.addAttribute("error","上传类型只能是jpg和gif类型");
            return "portal";
        }

        //完善4:限制上传文件大小
//        long size = photo1.getSize();
//        if(size>1024*100){ //上传大小不能超过100k
//            model.addAttribute("error","上传大小不能超过100k");
//            return "portal";
//        }
        //在配置文件中设置


        File file = new File(dir,fileName);
        photo1.transferTo(file);
        return "result";
    }

二、文件下载

就是文件的复制,注意输入输出流分别是谁

<h1>文件下载</h1> <br>
<img th:src="@{/upload/3c361511-4b57-40ec-8e96-f190dd3ec42d.jpg}" width="700xp" height="450xp"> <br>
<a th:href="@{/upload/3c361511-4b57-40ec-8e96-f190dd3ec42d.jpg}">打开图片</a> <br>
<a th:href="@{/user/download(photoName=3c361511-4b57-40ec-8e96-f190dd3ec42d.jpg)}">下载图片,弹框下载</a> <br>
    @RequestMapping("/user/download")
    public void download(String photoName, HttpServletResponse response) throws IOException {
        //1.获取项目真实路径
        String realPath = servletContext.getRealPath("/upload");
        
        //2.创建一个输入流和输出流
        File file = new File(realPath, photoName);
        InputStream is = new FileInputStream(file);
        OutputStream os = response.getOutputStream();//写到客户端

        //3.指定响应的信息
        response.setHeader("Content-Disposition", "attachment;filename=Miku.jpg");
        //attachment 附件 下载 inline 在线打开
        //response.setContentLength((int)file.length());//long-->int//指定大小
        //response.setContentType("image/jpg");//根据具体图片类型来写,事先和文件名保存在数据库中

        //4.使用输入流和输出流完成下载操作
        IOUtils.copy(is, os);   //内部一边读一边写

        //关闭输入流和输出流
        is.close();
        os.close();
    }

如果文件不在项目目录下,而是在d:\upload目录下,该怎么办?

  • 不可以在超链接中直接指定路径
<a th:href="@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}">下载(直接打开)</a>
  • 不管是直接打开,还是附件下载,都要采用编程方式
File file = new File("d:/upload",photoName);
response.setHeader("Content-Disposition","attachment;filename=zwj.gif"); //attachment 附件
response.setHeader("Content-Disposition","inline"); //直接打开

三、其他不重要的内容

1.springmvc配置文件默认位置和名称

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

如果没有指定文件位置:默认在橙色路径寻找

 IOException parsing XML document from ServletContext resource [/WEB-INF/dispatcherServlet-servlet.xml]; nested exception is java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/dispatcherServlet-servlet.xml]

 

2.@RequestMapping的更多参数

需求映射方式
请求参数中必须包含userName@RequestMapping(value = "/xxx",
params="userName")
请求参数中不能包含userName@RequestMapping(value = "/xxx",
params="!userName")
请求参数中必须包含userName
且值必须为Tom2015
@RequestMapping(value = "/xxx",
params="userName=Tom2015")
请求参数中必须包含userName
但值不能为Tom2015
@RequestMapping(value = "/xxx",
params="userName=!Tom2015")
请求参数中必须包含userName
且值为Tom2015,
同时必须包含userPwd但值不限
@RequestMapping(value = "/xxx",
params={"userName=Tom2015","userPwd"} )

3.@ModelAttribute注解 (每个handler方法执行前执行)

效果1:在每个 handler 方法前执行

效果2:可以将某些数据提前存入请求域

@Controller
public class ModelAttrHandler {
 
    @ModelAttribute
    public void doSthBefore(Model model) {
        model.addAttribute("initAttr", "initValue");
}
}

SpringMVC 运行原理及其源码解读

一、SpringMVC运行原理

执行流程

  1. 首先浏览器发送请求——> DispatcherServlet(总控制器),前端控制器收到请求后自己不进行处理,而是委托给其他组件进行处理,作为统一访问点,进行全局的流程控制
  2. DispatcherServlet——> HandlerMapping(处理器映射器)处理器映射器将会把请求映射为HandlerExecutionChain(处理器的执行链) 对象( 包含一个Handler处理器对象多个HandlerInterceptor拦截器)对象
  3. DispatcherServlet——>HandlerAdapter(处理器配适器),处理器适配器将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器。
  4. HandlerAdapter——>调用处理器相应功能处理方法,并返回一个ModelAndView对象(Model部分是业务对象返回的模型数据,View部分为逻辑视图名)
  5. DispatcherServlet——> ViewResolver(视图解析器),视图解析器会将逻辑视图名解析为物理视图返回View 对象
  6. DispatcherServlet——>ViewView会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构
  7. DispatcherServlet——>响应,返回控制权给DispatcherServlet,由它返回响应给用户,到此一个流程结束

总结强调

  1. 总控制器不是撒手掌柜,每次调用其他组件都要求得到结果,并根据结果调用下一个组件
  2. 分控制器的返回值:ModelAndView。Model中是数据,View是要跳转到的视图。
  3. 如果要是Ajax请求,就没有ModelAndView了,而是之间返回数据片段,客户端通过回调函数来处理数据片段。
  4. SpringMVC流程固定,开发者的主要任务是:开发分控制器、视图文件,可能还有拦截器。

SpringMVC运行原理-SpringMVC核心API

SpringMVC的主要组件

  • DispatcherServlet(总控制器):总控制器
  • HandlerMapping(处理器映射器):建立了请求路径和分控制器方法之间的映射
  • HandlerExecutionChain(处理器执行链):总控制器调用HandlerMapping组件的返回值是一个执行链,不仅有要执行的分控制器方法,还有相应的多个拦截器,组成一个执行链
  • HandlerAdapter(处理器配适器):调用分控制器的方法,不是由总控制直接调用的,而是由HandlerAdapter来调用
  • ViewResolver(视图解析器):逻辑视图(result)----->物理视图
    /WEB-INF/templates/result.html

①中央控制器 DispatcherServlet

发现他的上级类中有HttpServlet,而Servlet的执行入口是service(),一会就从这个方法入手开始SpringMVC执行过程的讲解。

②处理器映射器 HandlerMapping

项目中会有多个@RequestMapping,每个RequestMapping对应一个类或者一个方法。用户给一个请求路径,需要通过HandlerMapping来获取该路径所对应的方法,返回的结果 (处理器执行链HandlerExecutionChain就是访问路径所对应的处理器和所需经过的拦截器。

③处理器执行链 HandlerExecutionChain

为什么请求Handler,要返回HandlerExecutionChain? 因为Handler的执行前后会有一个或者多个拦截器执行,并且拦截器是链式执行的

所有HandlerExecutionChain中就包含了要执行的一个处理器和多个拦截器的信息。

④处理器适配器 HandlerAdapter

为什么总控制器不直接调用? 因为会有XML方式、注解方式等处理器形式,具体执行会有不同,通过不同的HandlerAdapter来实现。这里用到了适配器设计模式。所以调用分控制器的方法,不是由总控制器之间调用的,而是由 HandlerAdapter来调用。

⑤视图解析器ViewResolver

ViewResolver实现逻辑视图到物理视图的解析

比如:对于如下视图解析器,”main”是逻辑视图,而添加了后缀前缀的“/WEB-INF/jsp/main.jsp”就是物理视图。

二、请求处理过程

整个请求处理过程都是 doDispatch() 方法在宏观上协调和调度,把握了这个方法就理解了 SpringMVC 总体上是如何处理请求的。

所在类:org.springframework.web.servlet.DispatcherServlet

所在方法:doDispatch()

 核心方法中的核心代码,作用是执行handler:

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

所在类所在方法断点行数作用
DispatcherServletdoDispatch()1037创建调用链对象
DispatcherServletdoDispatch()1044创建 HandlerAdapter 对象
DispatcherServletdoDispatch()1056调用拦截器 preHandle()方法
DispatcherServletdoDispatch()1061执行目标 handler 方法
DispatcherServletdoDispatch()1068调用拦截器 postHandle()方法
DispatcherServletdoDispatch()1078执行所有后续操作
AbstractHandlerMappinggetHandlerExecutionChain()592创建调用链对象
AbstractHandlerMappinggetHandlerExecutionChain()599在调用链中添加拦截器
HandlerExecutionChainapplyPreHandle()146调用拦截器 preHandle()方法
HandlerExecutionChainapplyPostHandle()163调用拦截器 postHandle()方法
HandlerExecutionChaintriggerAfterCompletion175调用拦截器 afterCompletion()方法
DataBinderdoBind()747执行数据绑定
RequestMappingHandlerAdapterinvokeHandlerMethod()868创建 ModelAndViewContainer 对象
RequestMappingHandlerAdapterinvokeHandlerMethod()893将ModelAndViewContainer 对象传入调用目标 handler 的方法
DispatcherServletprocessDispatchResult()1125处理异常
DispatcherServletprocessDispatchResult()1139渲染视图
DispatcherServletprocessDispatchResult()1157调用拦截器 afterCompletion()方法
WebEngineContext的内部类:
RequestAttributesVariablesMap
setVariable()783将模型数据存入请求域

请求处理过程源码解读 (代码debug)

调用HandlerMapping获取执行链

发现执行链中要执行的分控制器的方法EmployController的findAll方法,封装成一个
HandlerMethod。

发现其中有三个拦截器,其中两个是自定义拦截器,执行顺序和配置顺序一致。先2再1.

②获取HandlerAdapter

如果使用注解,调用的是 RequestMappingHandlerAdapter。

③正序执行拦截器的preHandler

④执行分控制器的方法,返回值是ModelAndView

 ⑤逆序执行拦截器的postHandler

⑥逆序执行拦截器的afterCompletion

三、启动过程 (IoC容器初始化)

ApplicationContext就是IoC容器的引用,如果不是web项目,之前我们使用的是
ClasspathApplicationContext。如果是Web项目,使用的是WebApplicationContext。

调试技巧:在调试页面中前进和后退:ctrl+alt+(<--   -->)

1、initWebApplicationContext 

2、createWebApplicationContext (wac)

3、定位获取springmvc配置文件路径名称的代码

在HttpServletBean的一个内部类中

4、定位读取读取springmvc配置文件的内容以及分控制器中@RequestMapping内容的代码

初始化了@RequestMapping

初始化了视图解析器

四、ContextLoaderListener原理源码

问题引入

SSM整合后,配置文件内容过多,可以分到两个配置文件中。这两个配置文件夹如何加载

方法1:DispatcherServlet加载所有的配置文件 (classpath:spring*.xml)

<!--配置SpringMVC总控制器,唯一的Servlet -->
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <!--指定SpringMVC配置文件的名称和位置,有默认位置 -->
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring*.xml</param-value>
    </init-param>
    <!-- 启动服务器时就加载总控制器-->
    <load-on-startup>1</load-on-startup>
</servlet>

只有一个Ioc容器,存放所有的Bean

方法2DispatcherServlet加载springmvc的配置文件使用ContextLoaderListener加载另外一个配置文件,以便维护。

会有两个Ioc容器

使用 ContextLoaderListener 加载另外一个配置文件创建的IoC容器是父容器

DispatcherServlet 加载springmvc的配置文件创建的IoC容器是子容器

注意:Servlet、Filter、Listener的加载顺序Listener、Filter、Servlet

2、使用Listener创建IoC容器

<!--配置全局上下文参数 -->
<context-param>
   <param-name>contextConfigLocation</param-name>
   <param-value>classpath:spring-persist.xml</param-value>
</context-param>
<!--指定监听器 -->
<listener>
   <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

ServletContextListener 监听项目上下文,只有两个方法。

public interface ServletContextListener extends EventListener {
//项目启动时执行该方法
    void contextInitialized(ServletContextEvent var1);
//项目销毁时执行该方法
    void contextDestroyed(ServletContextEvent var1);
}

两个容器是什么关系:父子关系

使用ContextLoaderListener加载另外一个配置文件创建的IoC容器是父容器。

DispatcherServlet加载springmvc的配置文件创建的IoC容器是子容器。

2、出现的问题及其解决 (创建Bean重复,事务问题)

如果<context:component-scan >的路径设置不合理,就会重复的创建Bean。比如两个配置文件的扫描路径都是com.atguigu,就都会扫描Controller和Service的两个Bean。

缺点:

  1. 重复的bean会多占用资源
  2. SpringMVC创建的Controller肯定是调用SpringMVC自己创建的Service和Dao,但是在SpringMVC的配置文件中并没有关于事务的设置,所以调用SpringMVC自己创建的Service和Dao,将无法使用到事务。这绝对不可以。

解决方案1:两个配置文件中扫描不同的包

结果:SpringMVC中创建了Controller,Listener中创建了Service并应用了事务。当SpringMVC在自己的IoC容器中找不到Service的时候,就会到父容器中去找Service。问题解决。

<!-- 配置注解扫描基准路径-->
<context:component-scan base-package="com.atguigu.service,com.atguigu.dao"></context:component-scan>

<!-- 配置注解扫描基准路径-->
<context:component-scan base-package="com.atguigu.controller"></context:component-scan>

解决方案二:使用 exclude-filter标签对重复包进行排除。

<!-- 配置注解扫描基准路径:
   use-default-filters="true" @Controller @Service  @Repository @Component
    use-default-filters="false" 只扫描基准包下被include-filter指定的注解
   -->
<context:component-scan base-package="com.atguigu" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>


<!-- 配置注解扫描基准路径:@Controller @Service  @Repository @Component-->
<context:component-scan base-package="com.atguigu">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值