SpringBoot简单介绍及整合Mybatis

大家好,今天给大家分享一下SpringBoot框架以及如何整合Mybatis并且进行项目的分页查询。

 

一、背景介绍

随着项目的逐渐增大,会有许多的xml文件和各种复杂的bean依赖关系,所以就想要摆脱xml的束缚,以“约定优先配置”(convension over configuration)的思想来摆脱spring框架中的各种复杂配置。

在这个背景下,产生了springboot框架。它本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。

同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box),大部分的Spring Boot应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。

我们一开始学习都是使用的SSM框架,虽然SpringBoot比SSM简单方便,但这不代表我们一开始就应该学习SpringBoot,如果说SSM框架是自行车的话,那springboot就相当于摩托车。我们必须先学习SSM,了解那些bean的建立过程,了解xml中那些相关东西的配置,这样才能游刃有余的去使用SpringBoot。

至于为啥我们说SpringBoot好用呢?主要是基于以下几个优点:

1.Spring Boot Starter:依赖整合,开箱即用

2.自动配置:SpringBoot利用了Spring4对条件化配置的支持,合理推测应用所需的bean并自动化配置它们

3.摒弃繁琐的xml配置,使用application.properties自由组合,配置需要的属性。甚至web项目都不需要WEBAPP目录

4.提供了一系列大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等

5.打包方式自由,可以打jar包直接使用java命令启动,也可以打war包放在tomcat里启动

 

二、知识剖析

1.依赖

1)parent依赖

所谓parent依赖是指下面这个依赖,它是springboot所有依赖的parent,其他依赖会依赖此依赖,并且其他依赖可以省略version,版本会和parent的版本统一。

<!--项目中其他依赖会自动从parent中继承依赖版本-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.1.RELEASE</version>
    </parent>

点进spring-boot-starter-parent源码可以看到里面有很多默认的定义项,这就是springboot的默认配置以及可以开箱即用的原因,因为几乎所有东西都给你配置好了,除非你想修改。

2)依赖的传递

springboot的依赖并不是一个单纯的依赖,而是多个依赖整合起来的依赖包,相比我们自己添加依赖,其更方便快捷,而且可以避免版本冲突。

其中,它们使用了maven的依赖传递方案,Starter在自己的pom.xml文件中声明了多个依赖,当我们将某一个依赖添加到maven中时,starter的依赖将会自动地传递性解析。这些依赖本身可能也会有其他的依赖,一个Starter可能会传递性的引入几十个依赖,具体可以参考依赖源码。

2.项目结构

1)各个层级分配

首先上一张我自己的项目结构图

可以看到其他的都没什么,主要是有两处不一样,一个是多了一个配置类的文件夹,里面放的是一些配置类,相当于我们将xml文件的形式改为使用注解和java代码来实现。

第二个就是多了一个启动类,这个启动类的放置位置得在主文件夹之下,和其他文件夹平行,这样才能扫描到其他文件夹中的文件,并加载配置文件,这点需要额外注意。

2)application.properties文件

然后注意到我的resources里面有三个文件,我们首先来说第一个,也就是aplication.properties,

我们在介绍优点时说到了自动配置,开箱即用,这是因为所有依赖引入之后都有一个默认配置,

但是如果我们不想使用默认配置怎么办呢?最简单的方法就是在这个文件里面修改,当然,你也可以使用applicatiom.yml文件修改,道理大同小异。

#配置tomcat
#端口号,默认为8080
server.port=8080
#访问路径,默认为/
#server.context-path=/
server.tomcat.uri-encoding=utf-8
#输出日志文件,默认不输出
#logging.file=/log.txt
#修改日志级别,默认为INFO
#logging.level.root=DEBUG

#MySQL
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=4096
#hikari连接池最大连接数
spring.datasource.hikari.maximumPoolSize=100

#Spring Data JPA
spring.jpa.database=MYSQL
#设置为true可以在执行程序后在控制台看见sql语句
spring.jpa.show-sql=true

spring.jpa.hibernate.ddl-auto=update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the com.task.entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

#视图层控制
#定位thymeleaf模板目录
#spring.mvc.view.prefix=classpath:/templates/
#如果没有加thymeleaf依赖,则定位到static,加了就定位到/templates
spring.mvc.view.prefix=classpath:/
#给返回的模板添加后缀
spring.mvc.view.suffix=.html
#规定url上想直接访问静态资源必须带上/static,不能直接跳过此文件夹,且其他文件夹中的静态资源无法被访问
spring.mvc.static-path-pattern=/static/**

#将连接池切换为Hikari
spring.datasource.type=com.zaxxer.hikari.HikariDataSource

#将数据库中的NN_NN转换为驼峰命名法
mybatis.configuration.mapUnderscoreToCamelCase=true

#打印mybatis的SQL日志到控制台
logging.level.com.task.dao.UserDao=debug

#开启手动重启
spring.devtools.restart.trigger-file=trigger.txt

#设置所有端点不敏感
endpoints.sensitive=false

#设置redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.timeout=1000

几乎你所有需要修改的东西都可以在里面修改,方法就是使用包的前缀加上需要修改的属性,后面是你需要的值,至于哪些可以修改,一个是参考源码,一个是使用Actuator工具查看,这个今天先不聊。

3.注解

1)注解功能

传统的Spring做法是使用.xml文件来对bean进行注入或者是配置aop、事物,这么做有两个缺点:

①如果所有的内容都配置在.xml文件中,那么.xml文件将会十分庞大;如果按需求分开.xml文件,那么.xml文件又会非常多。总之这将导致配置文件的可读性与可维护性变得很低

②在开发中在.java文件和.xml文件之间不断切换,是一件麻烦的事,同时这种思维上的不连贯也会降低开发的效率

为了解决这两个问题,Spring引入了注解,通过"@XXX"的方式,让注解与Java Bean紧密结合,既大大减少了配置文件的体积,又增加了Java Bean的可读性与内聚性。
2)SpringBoot常用注解介绍

结合实际项目来看吧

首先是实体类,一个很简单的实体类,需要注意的就是@Entity注解,另外几个是JPA需要用到的注解

/声明实体类
@Entity
//绑定数据库的表格
@Table(name = "tbl_user")
public class User implements Serializable{
    //指定表的主键
    @Id
    //注释定义了标识字段生成方式,后面的代表使用数据库的IDENTITY列来保证唯一
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private String password;

    public long getId() {
        return id;
    }

    public void setId(long 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 "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

然后是dao层,这里我没有使用一般的mapper.xml,而是使用直接写SQL语句在接口上方的方式,主要的注解就是@Mapper,然后是增删查改四个方法的注解@Insert @Delete @Select @Update,下面的那几个是动态查询,先不用深究

//标记dao层的mapper类
@Mapper
public interface UserDao {
//    按照id查找
    @Select("select * from tbl_user where id=#{id}")
     List<User> findById(User parm);
//查询全部正序
    @Select("select * from tbl_user")
    List<User>  findAll();
//查询全部按照id逆序
    @Select("select * from tbl_user order by id desc")
    List<User> fineAllDesc();
//插入数据
    @Insert("insert into tbl_user (name,password) values(#{name},#{password})")
    //插入之后返回id
    @Options(useGeneratedKeys=true,keyProperty="id")
    int add(User parm);
    //动态查询
//    @Select("<script>select * from tbl_user " +
//            "<trim prefix=\"WHERE\" prefixOverrides=\"AND\">" +
//            "<if test=\"name!=null and name.length()>0\">AND name=#{name}</if>" +
//            "<if test=\"id!=0 \">AND id=#{id}</if>" +
//            "<if test=\"password!=null and password.length()>0\">AND password=#{password}</if>" +
//            "</trim></script>" )
//    List<User> findByUser(User parm);

    @SelectProvider(type = UserDaoProvider.class,method = "findByUser")
    List<User> findByUser(User parm);

    class UserDaoProvider{
        public String findByUser(User user){
            return new SQL(){{
                SELECT("*");
                FROM("tbl_user");
                if (user.getId()!=0)
                    WHERE("id=#{id}");
                if(user.getName()!=null)
                    WHERE("name=#{name}");
                if(user.getPassword()!=null)
                    WHERE("password=#{password}");
            }}.toString();
        }

//        public String findByUser(User user){
//            String sql="Select * from tbl_user ";
//            if(user.getId()!=0){
//                sql+="where id =#{id}";
//            }
//            if (user.getName()!=null){
//                sql+="where name =#{name}";
//            }
//            if (user.getPassword()!=null){
//                sql+="where password =#{password}";
//            }
//            return sql;
//        }

        public String updateByUser(User user){
            return new SQL(){{
                UPDATE("tbl_user");
                if(user.getName()!=null)
                    SET("name=#{name}");
                if(user.getPassword()!=null)
                    SET("password=#{password}");
                WHERE("id=#{id}");
            }}.toString();
        }
    }

    //动态修改
//    @Update("<script>UPDATE tbl_user " +
//            "<trim prefix=\"SET\" suffixOverrides=\",\" suffix=\"WHERE id = #{id}\" > " +
//            "<if test=\"name != null and name != '' \">name = #{name}, </if> " +
//            "<if test=\"password != null and password != '' \">password=#{password}, </if> " +
//            "</trim></script>")
//    boolean UpdateByUser(User user);

   @UpdateProvider(type = UserDaoProvider.class,method = "updateByUser")
    boolean UpdateByUser(User user);
}

service层,简单的不说,另外几个缓存,事务,异步也不是必须的

/标记service层
@Service
//开启事务,标记在类上则类中全部方法都使用事务功能
@Transactional
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Resource
    private UserDao userDao;

    public User findUserByName(String name){
        User user=null;
        try{
            user=userRepository.findByName(name);
        }catch (Exception e){
          e.printStackTrace();
        }
        return user;
    }
    @Cacheable(cacheNames = "findUserById",key = "#id")
    public List<User> findUserById(long id){
        System.out.println("没有用缓存,进入数据库查询");
        User user=new User();
        user.setId(id);
        return userDao.findById(user);
    }
}

controller层,这里可以说的比较多,比如@RestController等于@Controller+@ResponceBody,@GetMapping也是简写

/标记controller层
@Controller
//@RequestMapping(value = "/user")
public class UserController {
    @Autowired
    private UserService userService;
    private static final Logger log=LoggerFactory.getLogger(UserController.class);
//相当于@RequestMapping(method=request.get)
    @GetMapping(value = "/index")
    public String index() {
        System.out.println("123");
        int i=1/0;
        return "/index";
    }

    @GetMapping(value = "/html")
    public String html() {
        System.out.println("123");
        return "redirect:/user/index";
    }

    @PostMapping( value = "/show")
    //以json类型返回数据
    @ResponseBody
    public Map show(@RequestParam(value = "name") String name) {
        System.out.println("123");
        Map map=new HashMap();
        User user = userService.findUserByName(name);
        if (null != user)
            map.put("data",user);
            return map;
    }

配置类

//以最高优先级加载
@Order(Ordered.HIGHEST_PRECEDENCE)
//定义配置类,可以替换xml文件,相当于xml中的beans
@Configuration
//开启事务管理
@EnableTransactionManagement(proxyTargetClass = true)
//扫描jpa的repository
@EnableJpaRepositories(basePackages = "com.task.repository")
//扫描实体类
@EntityScan(basePackages = "com/task/entity")
//扫描dao层
@MapperScan(basePackages = "com/task/dao")
//配置异步
@EnableAsync
public class JpaConfiguration {
    //注册bean对象,相当于xml中的bean标签
    @Bean
    PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor(){
        return new PersistenceAnnotationBeanPostProcessor();
    }
}

启动类

/@Configuration、@EnableAutoConfiguration、@ComponentScan的集合,自动配置,自动整合
@SpringBootApplication
//扫描整个包
@ComponentScan("com.task")
//开启缓存功能
@EnableCaching
public class Entry {
    public static void main(String[] args) {
        SpringApplication.run(Entry.class,args);
    }
}

其实配置类和启动类很多的注解可以省略,因为我的启动类放置在外面了,如果启动类放在配置类的文件夹里,就要保证这些扫描的注解都有。

 

三、常见问题

1.怎么整合Mybatis

2.怎么使用分页插件实现分页查询

四、解决方案

1.要整合Mybatis,首先得加入相关依赖,第一个是连接mysql的依赖,第二个是mybaitis依赖,第三个mybaitis的分页插件

 <!-- 连接mysql所需依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.39</version>
        </dependency>
 <!-- 引入mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <!-- mybatis分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>

然后在配置文件中将mysql的相关信息填写进去,这个和SSM一样

#MySQL
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=4096

编写实体类,dao,service,controller,具体代码参考上面,启动就可以了

2.怎么实现分页:

首先,在第一个功能实现的基础上,我们将查询所有的service方法修改如下。

将findAll修改为分页查询,我这里形参设置了一个对象,其具体见下面,

就是哪一页开始查,一页几条数据,顺序还是逆序,具体数据在controller层传进来,因为是一个对象,所以前端传进来的可以是任意的属性,其他的没有传的就使用默认值,你也可以只使用页数这一个属性,其他改为不可修改直接写死。

这里就直接使用分页插件中的方法来帮助实现,首先是设置页数和每页的条数,由传进来的对象决定,

然后后面就紧跟查询语句,

其执行原理如下:

PageHelper.startPage会拦截下一个sql,也就是userDao.findAll()的SQL。

并且根据当前数据库的语法,把这个SQL改造成一个高性能的分页SQL,同时还会查询该表的总行数,具体可以看SQL日志。

PageHelper.startPage和userDao.findAll()最好紧跟在一起,中间不要有别的逻辑,否则可能出BUG。

Page<User> page:相当于一个list集合,findAll()方法查询完成后,会给page对象的相关参数赋值。

所以看起来好像findAll没有东西来接,实际上已经把数据存进page对象了。

public class PageModel {
    //创建三个查询条件,直接对其进行初始化,如果接收到的是null就自动启用初始条件
    private Integer pageNum=1;
    private Integer pageSize=10;
    private String order="asc";

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public String getOrder() {
        return order;
    }

    public void setOrder(String order) {
        this.order = order;
    }
}

 

 public Page<User> find(PageModel pageModel){
        int pageNum=pageModel.getPageNum();
        int pageSize=pageModel.getPageSize();
        String order=pageModel.getOrder();
        //pageNum=起始页,pageSize=每页数目
        Page<User> page = PageHelper.startPage(pageNum,pageSize);
        //拦截下一条语句,也就是下面这条findAll(),这条语句可以设置正序还是逆序排列,按照什么排列
        if (order.equalsIgnoreCase("asc"))
        userDao.findAll();
        else
            userDao.fineAllDesc();
        //数据总条数,不能设置count为false,否则无法计算
        page.getTotal();
        System.out.println(page.getTotal());
        //每页条数
        page.size();
        System.out.println(page.size());
        //获得指定下标的第几条数据,0基
        page.get(0);
        System.out.println(page.get(0));
        return page;
    }

然后在controller中就可以接收到传过来的对象了

 @GetMapping(value ="/list" )
    @ResponseBody
    public Map list(@ModelAttribute(value = "pageModel")PageModel pageModel){
        Map map=new HashMap();
       Page<User> userPage=null;
       //分页查询
        userPage=userService.find(pageModel);
        //数据总条数
       int total=(int)userPage.getTotal();
       //数据每页条数
       int pageSize=userPage.getPageSize();
       //总页数
       int pageCount=0;
       //是否有下一页
       boolean hasNextPage=false;
       if(total%pageSize==0)
           pageCount=total/pageSize;
       else
           pageCount=total/pageSize+1;
       //判断需要查询的页数是否小于总页数,是就返回还有下一页
       if(pageModel.getPageNum()<pageCount)
           hasNextPage=true;
        map.put("hasNextPage",hasNextPage);
        map.put("data",userPage);
        return map;
    }

五、更多讨论

1.如何启动日志功能

logback是springboot的默认日志组件,出自log4j的作者,相比log4j其功能更强大。

之前在配置properties文件的时候设置过简单的日志输出,但是如果想设置更复杂的,则需要创建xml文件

2.官方建议日志文件命名为logback-spring.xml,现创建一个具体的日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- appender是配置输出终端,ConsoleAppender是控制台,name是自定义名 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 配置日志格式,这是一个比较通用的格式 -->
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 输出终端是滚动文件 -->
    <appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 基于时间滚动,就是每天的日志输出到不同的文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 输出日志的目录文件名,window中默认分区为当前程序的硬盘分区,%d{yyyy-MM-dd}是当前日期 -->
            <fileNamePattern>/logs/warn/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 最大保存99个文件,超出的历史文件会被删除 -->
            <maxHistory>99</maxHistory>
        </rollingPolicy>
        <!-- 按照日志级别进行过滤 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 只收集WARN级别的日志,其他高级别和低级别的日志都放弃 -->
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 除了filter,其他 配置和上面一样, 只是name和文件路径不同-->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/logs/error/error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>99</maxHistory>
        </rollingPolicy>
        <!-- 阈值过滤器 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 收集ERROR及ERROR以上级别的日志 -->
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- root是根日志打印器,只有一个,负责整个系统的日志输出 -->
    <root level="INFO">
        <!-- 将上面三个输出终端配置到根打印器,将对整个系统 生效。 -->
        <appender-ref ref="STDOUT" />
        <appender-ref ref="WARN" />
        <appender-ref ref="ERROR" />
    </root>

    <!-- logger是root的子打印器,可以有多个,输出name配置的包中的日志。 -->
    <!-- com.task.repository是我的mybatis映射dao的包名,设置为debug可以打印mybatis的sql语句 -->
    <logger name="com.task.repository" level="DEBUG" />
</configuration>

然后就会在相应磁盘位置创建log文件了

2.怎么实现热部署

我们一般在修改完项目重新启动时,会重新加载Jar包并重载代码,这在平时开发中是比较浪费时间的,所以我们此时可以添加一个开发者工具,它可以帮我们只重新加载代码而不去加载依赖。
首先导入开发者工具

 <!--开发者工具,热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

注意:

1)修改pom.xml的依赖,也算是修改代码,也会重启,但不会重载你修改的依赖jar

2)修改前端代码不会触发重启

如果不想写一点就被重启,可以将启动重启改为手动触发重启

#开启手动重启
spring.devtools.restart.trigger-file=trigger.txt

此时我修改了resource中的此文件才会触发重启,而且必须要修改过代码才会触发,这样就可以实现手动触发了。

我们在打成jar包时,开发者工具是不会打进去的。

3.怎么进行单元测试?

Spring Boot的测试,和普通项目的测试类同,可以继续使用我们熟悉的测试工具。当然,这里的测试,主要还是后台代码的测试。

其实我们主要测试的还是dao层和service层,controller层的测试还不如直接启动用postman来测比较方便。
首先还是依赖的导入

 

 <!--测试相关依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

 

在test文件夹建立测试类,

 

@RunWith(SpringJUnit4ClassRunner.class)
//classes后面是启动类.class
@SpringBootTest(classes = Entry.class)
//@WebAppConfiguration
public class HelloTest {
@Autowired
    UserService userService;
    @Resource
    RedisCacheManager redisCacheManager;
@Test
    public void test1(){
    redisCacheManager.hset("user","name","wang");
    System.out.println(redisCacheManager.hget("user","name"));
    User user=new User();
    user.setId(122);
    user.setPassword("123");
    user.setName("wangshsh");
    redisCacheManager.hset("user","1",user);
    System.out.println(redisCacheManager.hget("user","1"));
}

注意这里几个注解的使用。

 

六、参考文献

https://blog.csdn.net/column/details/15339.html?&page=2

本文只是对springboot的一个简单介绍,具体可参考上面的这个博客,有很详细的介绍。

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值