继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Mybatis-Plus使用全解

oKong
关注TA
已关注
手记 64
粉丝 200
获赞 409

前言

之前写了《SpringBoot | 第九章:Mybatis-plus的集成和使用》一文,只是简单的使用条件构造器列举了一些通用的CURD操作。本人也想写一篇通用的关于mybatis-plus的使用示例,一方面也让自己更加了解mybatismybatis-plus,另一方面,也因为很多新人刚入职公司时,对这块不是很熟悉,会有一些疑惑。所以,总的来说还是作为一份资产,可供人查阅,这样也能减少了很多的沟通成本。


所以本章节,就主要来讲解下关于Mybatis-plus的不同场景的用法,目前主要想到的是以下几个知识点,也是很常用的知识点了,后面有补充的会再启章节来记录的。另外,官网的文档已经很详尽了,大家可认真查阅下。

  1. 代码生成器
  2. 通用的CURD
  3. 条件构造器
  4. 自定义SQL语句
  5. 分页插件、性能分析插件
  6. 公共字段自动填充

工程准备

这里还是以**user表为例子,数据库为mysql**

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) DEFAULT NULL COMMENT '唯一标示',
  `code` varchar(20) DEFAULT NULL COMMENT '编码',
  `name` varchar(64) DEFAULT NULL COMMENT '名称',
  `status` char(1) DEFAULT '1' COMMENT '状态 1启用 0 停用',
  `gmt_create` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

关于SpringBootMybatis-plus的集成,这里就不阐述了,这个不是今天的重点。不熟悉的同学,可移步:http://blog.lqdev.cn/2018/07/21/springboot/chapter-nine/进行查看了解。

代码生成器

Mybatis-Plus已经提供了大量的自定义设置,生成的代码完全能够满足各类型的需求,基本覆盖了大部分的配置了。这里贴一个比较完整的代码生成器类,大家可根据实际情况进行修改。

MysqlGenerator.java:

//省略了import
public class MysqlGenerator {

    /**
     * 包名
     */
    private static final String PACKAGE_NAME = "cn.lqdev.learning.mybatisplus.samples";
    /**
     * 模块名称
     */
    private static final String MODULE_NAME = "biz";
    /**
     * 输出文件的路径
     */
    private static final String OUT_PATH = "D:\\develop\\code";
    /**
     * 代码生成者
     */
    private static final String AUTHOR = "oKong";

    /**
     * JDBC相关配置
     */
    private static final String DRIVER = "com.mysql.jdbc.Driver";
    private static final String URL = "jdbc:mysql://127.0.0.1:3306/learning?useUnicode=true&characterEncoding=UTF-8";
    private static final String USER_NAME = "root";
    private static final String PASSWORD = "bs";

    /**
     * <p>
     * MySQL 生成演示
     * </p>
     */
    public static void main(String[] args) {
        // 自定义需要填充的字段
        List<TableFill> tableFillList = new ArrayList<TableFill>();
        //如 每张表都有一个创建时间、修改时间
        //而且这基本上就是通用的了,新增时,创建时间和修改时间同时修改
        //修改时,修改时间会修改,
        //虽然像Mysql数据库有自动更新几只,但像ORACLE的数据库就没有了,
        //使用公共字段填充功能,就可以实现,自动按场景更新了。
        //如下是配置
        TableFill createField = new TableFill("gmt_create", FieldFill.INSERT); 
        TableFill modifiedField = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE); 
        tableFillList.add(createField);
        tableFillList.add(modifiedField);
        
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator().setGlobalConfig(
                // 全局配置
                new GlobalConfig().setOutputDir(OUT_PATH)// 输出目录
                        .setFileOverride(true)// 是否覆盖文件
                        .setActiveRecord(true)// 开启 activeRecord 模式
                        .setEnableCache(false)// XML 二级缓存
                        .setBaseResultMap(false)// XML ResultMap
                        .setBaseColumnList(true)// XML columList
                        .setAuthor(AUTHOR)
                        // 自定义文件命名,注意 %s 会自动填充表实体属性!
                        .setXmlName("%sMapper").setMapperName("%sDao")
        // .setServiceName("MP%sService")
        // .setServiceImplName("%sServiceDiy")
        // .setControllerName("%sAction")
        ).setDataSource(
                // 数据源配置
                new DataSourceConfig().setDbType(DbType.MYSQL)// 数据库类型
                        .setTypeConvert(new MySqlTypeConvert() {
                            // 自定义数据库表字段类型转换【可选】
                            @Override
                            public DbColumnType processTypeConvert(String fieldType) {
                                System.out.println("转换类型:" + fieldType);
                                // if ( fieldType.toLowerCase().contains( "tinyint" ) ) {
                                // return DbColumnType.BOOLEAN;
                                // }
                                return super.processTypeConvert(fieldType);
                            }
                        }).setDriverName(DRIVER).setUsername(USER_NAME).setPassword(PASSWORD).setUrl(URL))
                .setStrategy(
                        // 策略配置
                        new StrategyConfig()
                                // .setCapitalMode(true)// 全局大写命名
                                .setDbColumnUnderline(true)// 全局下划线命名
                                // .setTablePrefix(new String[]{"unionpay_"})// 此处可以修改为您的表前缀
                                .setNaming(NamingStrategy.underline_to_camel)// 表名生成策略
                                // .setInclude(new String[] {"user"}) // 需要生成的表
                                // .setExclude(new String[]{"test"}) // 排除生成的表
                                // 自定义实体,公共字段
                                // .setSuperEntityColumns(new String[]{"test_id"})
                                .setTableFillList(tableFillList)
                                // 自定义实体父类
                                // .setSuperEntityClass("com.baomidou.demo.base.BsBaseEntity")
                                // // 自定义 mapper 父类
                                // .setSuperMapperClass("com.baomidou.demo.base.BsBaseMapper")
                                // // 自定义 service 父类
                                // .setSuperServiceClass("com.baomidou.demo.base.BsBaseService")
                                // // 自定义 service 实现类父类
                                // .setSuperServiceImplClass("com.baomidou.demo.base.BsBaseServiceImpl")
                                // 自定义 controller 父类
                                // .setSuperControllerClass("com.baomidou.demo.TestController")
                                // 【实体】是否生成字段常量(默认 false)
                                // public static final String ID = "test_id";
                                .setEntityColumnConstant(true)
                                // 【实体】是否为构建者模型(默认 false)
                                // public User setName(String name) {this.name = name; return this;}
                                .setEntityBuilderModel(true)
                                // 【实体】是否为lombok模型(默认 false)<a href="https://projectlombok.org/">document</a>
                                .setEntityLombokModel(true)
                // Boolean类型字段是否移除is前缀处理
                // .setEntityBooleanColumnRemoveIsPrefix(true)
                // .setRestControllerStyle(true)
                // .setControllerMappingHyphenStyle(true)
                ).setPackageInfo(
                        // 包配置
                        new PackageConfig().setModuleName(MODULE_NAME).setParent(PACKAGE_NAME)// 自定义包路径
                                .setController("controller")// 这里是控制器包名,默认 web
                                .setXml("mapper").setMapper("dao")

                ).setCfg(
                        // 注入自定义配置,可以在 VM 中使用 cfg.abc 设置的值
                        new InjectionConfig() {
                            @Override
                            public void initMap() {
                                Map<String, Object> map = new HashMap<String, Object>();
                                map.put("abc", this.getConfig().getGlobalConfig().getAuthor() + "-mp");
                                this.setMap(map);
                            }
                        }.setFileOutConfigList(
                                Collections.<FileOutConfig>singletonList(new FileOutConfig("/templates/mapper.xml.vm") {
                                    // 自定义输出文件目录
                                    @Override
                                    public String outputFile(TableInfo tableInfo) {
                                        return OUT_PATH + "/xml/" + tableInfo.getEntityName() + "Mapper.xml";
                                    }
                                })))
                .setTemplate(
                        // 关闭默认 xml 生成,调整生成 至 根目录
                        new TemplateConfig().setXml(null)
        // 自定义模板配置,模板可以参考源码 /mybatis-plus/src/main/resources/template 使用 copy
        // 至您项目 src/main/resources/template 目录下,模板名称也可自定义如下配置:
        // .setController("...");
        // .setEntity("...");
        // .setMapper("...");
        // .setXml("...");
        // .setService("...");
        // .setServiceImpl("...");
        );

        // 执行生成
        mpg.execute();
    }

}

按以上代码生成器,生成的目录结构如下(依赖中需要加入velocity-engine-core包,是利用模版引擎来生成的)

工程结构

对应mapper.xml

对于需要自定义模版时,大家可查看官方的mybatis-plus-generate包,默认的官方模版都放在此包下。

官方模版

有了代码生成器,省了很多机械性的复制黏贴操作,还不会出错,写错了再执行一次就好了!

通用的CURD

MP提供了ActiveRecord的支持,所以实体类只需继承 Model 类即可实现基本 CRUD 操作。

这里以编写测试类的形式,进行通用CURD操作,代码类有相应的注释说明。

GeneralTest.java:

/**
 * 通用CURD示例
 * @author oKong
 *
 */
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public class GeneralTest {

    @Autowired
    IUserService userService;
    
    @Test
    public void testInsert() {
        User user = new User();
        user.setCode("001");
        user.setName("okong-insert");
        //默认的插入策略为:FieldStrategy.NOT_NULL,即:判断 null
        //对应在mapper.xml时写法为:<if test="field!=null">
        //这个可以修改的,设置字段的@TableField(strategy=FieldStrategy.NOT_EMPTY)
        //所以这个时候,为null的字段是不会更新的,也可以开启性能插件,查看sql语句就可以知道
        userService.insert(user);
        
        //新增所有字段,
        userService.insertAllColumn(user);
        log.info("新增结束");
    }
    
    @Test
    public void testUpdate() {
        
        User user = new User();
        user.setCode("101");
        user.setName("oKong-insert");
        //这就是ActiveRecord的功能
        user.insert();
        //也可以直接 userService.insert(user);

        //更新
        User updUser = new User();
        updUser.setId(user.getId());
        updUser.setName("okong-upd");
        
        updUser.updateById();
        log.info("更新结束");
    }
    
    @Test
    public void testDelete() {
        User user = new User();
        user.setCode("101");
        user.setName("oKong-delete");
        
        user.insert();
        
        //删除
        user.deleteById();
        log.info("删除结束");

    }
    
    @Test
    public void testSelect() {
        User user = new User();
        user.setCode("201");
        user.setName("oKong-selecdt");
        
        user.insert();
        
        log.info("查询:{}",user.selectById());
    }
}

以上就列举了常用的,官方提供了很多的通用方法:

通用方法

注意控制台的sql输出,对比下就知道各方法之间的区别了。


对于通用代码如何注入的,可查看com.baomidou.mybatisplus.mapper.AutoSqlInjector类,这个就是注入通用的CURD方法的类。

AutoSqlInjector类


条件构造器

在通用的CURD无法满足时,这个时候 强大的条件构造器就排上用场了。主要提供了实体包装器,用于处理 sql 拼接,排序,实体参数查询等!

这里需要注意:使用的是数据库字段,不是Java属性!,原来使用另一款通用mapper时记得使用的是JAVA属性。

sql条件拼接

这也是条件构造器最灵活的地方了。

ConditionTest.java

@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public class ConditionTest {
	
	@Autowired
	IUserService userService;
	
	@Test
	public void testOne() {
		User user =  new User();
		user.setCode("701");
		user.setName("okong-condition");
	    user.insert();
	    
		EntityWrapper<User> qryWrapper = new EntityWrapper<>();
		
		qryWrapper.eq(User.CODE, user.getCode());
		qryWrapper.eq(User.NAME, user.getName());
		
		//也可以直接 
//		qryWrapper.setEntity(user);
		
		//打印sql语句
		System.out.println(qryWrapper.getSqlSegment());
		
		//设置select 字段 即:select code,name from 
		qryWrapper.setSqlSelect(User.CODE,User.NAME);
		System.out.println(qryWrapper.getSqlSelect());
		
		//查询
	    User qryUser = userService.selectOne(qryWrapper);
	    System.out.println(qryUser);
	    log.info("拼接一结束");
	}
	
	@Test
	public void testTwo() {
		User user =  new User();
		user.setCode("702");
		user.setName("okong-condition");
	    user.insert();
	    
		EntityWrapper<User> qryWrapper = new EntityWrapper<>();
		qryWrapper.where("code = {0}", user.getCode())
		.and("name = {0}",user.getName())
		.andNew("status = 0");
		System.out.println(qryWrapper.getSqlSegment());
		//等等很复杂的。
		//复杂的建议直接写在xml里面了,要是非动态的话 比较xml一眼看得懂呀
		//查询
	    User qryUser = userService.selectOne(qryWrapper);
	    System.out.println(qryUser);
	    log.info("拼接二结束");
	}

}

com.baomidou.mybatisplus.mapper.Wrapper<T>类还有很多的方法,大家可以试试。
wrapper

条件参数说明

查询方式 说明
setSqlSelect 设置 SELECT 查询字段
where WHERE 语句,拼接 + WHERE 条件
and AND 语句,拼接 + AND 字段=值
andNew AND 语句,拼接 + AND (字段=值)
or OR 语句,拼接 + OR 字段=值
orNew OR 语句,拼接 + OR (字段=值)
eq 等于=
allEq 基于 map 内容等于=
ne 不等于<>
gt 大于>
ge 大于等于>=
lt 小于<
le 小于等于<=
like 模糊查询 LIKE
notLike 模糊查询 NOT LIKE
in IN 查询
notIn NOT IN 查询
isNull NULL 值查询
isNotNull IS NOT NULL
groupBy 分组 GROUP BY
having HAVING 关键词
orderBy 排序 ORDER BY
orderAsc ASC 排序 ORDER BY
orderDesc DESC 排序 ORDER BY
exists EXISTS 条件语句
notExists NOT EXISTS 条件语句
between BETWEEN 条件语句
notBetween NOT BETWEEN 条件语句
addFilter 自由拼接 SQL
last 拼接在最后,例如:last(“LIMIT 1”)

自定义SQL使用条件构造器

UserDao.java加入接口方法:

/**
	 * 
	 * @param rowBounds 分页对象 直接传入page即可
	 * @param wrapper 条件构造器
	 * @return
	 */
	List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);

UserMapper.xml加入对应的xml节点:

    <!-- 条件构造器形式 -->
	<select id="selectUserWrapper" resultType="user">
		SELECT
		<include refid="Base_Column_List" />
		FROM USER
		<where>
			${ew.sqlSegment}
		</where>
	</select>

测试类:

@Test
	public void testCustomSql() {
		User user = new User();
		user.setCode("703");
		user.setName("okong-condition");
	    user.insert();
	    
		EntityWrapper<User> qryWrapper = new EntityWrapper<>();
		qryWrapper.eq(User.CODE, user.getCode());
		
		Page<User> pageUser = new Page<>();
	    pageUser.setCurrent(1);
	    pageUser.setSize(10);
	    
	    List<User> userlist = userDao.selectUserWrapper(pageUser, qryWrapper);
	    System.out.println(userlist.get(0));
	    log.info("自定义sql结束");
	}

xml形式使用wrapper

UserDao.java

/**
	 * 
	 * @param rowBounds 分页对象 直接传入page即可
	 * @param wrapper 条件构造器
	 * @return
	 */
	List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);

对应的UserMapper.xml:


    <!-- 条件构造器形式 -->
	<select id="selectUserWrapper" resultType="user">
		SELECT
		<include refid="Base_Column_List" />
		FROM USER
		<where>
			${ew.sqlSegment}
		</where>
	</select>

自定义SQL语句

在一些需要多表关联时,条件构造器和通用CURD都无法满足时,还可以自行手写sql语句进行扩展。注意:这都是mybatis的用法。

以下两种方式都是改造UserDao接口。

注解形式

@Select("SELECT * FROM USER WHERE CODE = #{userCode}")
	List<User> selectUserCustomParamsByAnno(@Param("userCode")String userCode);

xml形式

List<User> selectUserCustomParamsByXml(@Param("userCode")String userCode);

同时,UserMapper.xml新增一个节点:

    <!-- 由于设置了别名:typeAliasesPackage=cn.lqdev.learning.mybatisplus.samples.biz.entity,所以resultType可以不写全路径了。 -->
    <select id="selectUserCustomParamsByXml" resultType="user">
        SELECT 
        <include refid="Base_Column_List"/> 
        FROM USER 
       WHERE CODE = #{userCode}
    </select>

测试类CustomSqlTest.java

@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public class CustomSqlTest {
	
	@Autowired
	UserDao userDao;
	
	@Test
	public void testCustomAnno() {
        User user = new User();
        user.setCode("901");
        user.setName("okong-sql");
        user.insert();
		List<User> userlist = userDao.selectUserCustomParamsByAnno(user.getCode());
		//由于新增的 肯定不为null 故不判断了。
		System.out.println(userlist.get(0).toString());
		log.info("注解形式结束------");
	}
	
	@Test
	public void testCustomXml() {
		User user = new User();
		user.setCode("902");
		user.setName("okong-sql");
		user.insert();
		List<User> userlist = userDao.selectUserCustomParamsByXml(user.getCode());
		//由于新增的 肯定不为null 故不判断了。
		System.out.println(userlist.get(0).toString());
		log.info("xml形式结束------");
	}

}

注意事项

在使用spring-boot-maven-plugin插件打包成springboot运行jar时,需要注意下,由于springboot的jar扫描路径方式问题,会导致别名的包未扫描到,所以这个只需要把mybatis默认的扫描设置为SpringbootVFS实现。

直接修改spring-mybatis.xml文件:

  <!--mybatis-->
    <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
		<!-- 自动扫描mapper.xml文件,支持通配符 -->
        <property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
		<!-- 配置文件,比如参数配置(是否启动驼峰等)、插件配置等 -->
        <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
		<!-- 启用别名,这样就无需写全路径类名了,具体可自行查阅资料 -->
        <property name="typeAliasesPackage" value="cn.lqdev.learning.mybatisplus.samples.biz.entity"/>
        <!-- MP 全局配置注入 -->
        <property name="globalConfig" ref="globalConfig"/>
        <!-- 设置vfs实现,避免路径扫描问题 -->
        <property name="vfs"  value="com.baomidou.mybatisplus.spring.boot.starter.SpringBootVFS"></property>
    </bean>

分页插件、性能分析插件

mybatis的插件机制使用起来是很简单的,只需要注册即可。

mybatis-config.xml

    <plugins>
      <!-- SQL 执行性能分析,开发环境使用,线上不推荐。 -->
      <plugin interceptor="com.baomidou.mybatisplus.plugins.PerformanceInterceptor"></plugin>
      <!-- 分页插件配置 -->
      <plugin interceptor="com.baomidou.mybatisplus.plugins.PaginationInterceptor"></plugin>
    </plugins>

分页测试类(性能分析,配置后可以输出sql及取数时间):

@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public class PluginTest {
	
	@Autowired
	IUserService userService;
	
	@Test
	public void testPagination() {
		Page<User> page = new Page<>();
		//每页数
		page.setSize(10);
		//当前页码
		page.setCurrent(1);
		
		//无条件时
		Page<User> pageList = userService.selectPage(page);
		System.out.println(pageList.getRecords().get(0));
		
		//新增数据 避免查询不到数据
		User user = new User();
		user.setCode("801");
		user.setName("okong-Pagination");
		user.insert();
		//加入条件构造器
		EntityWrapper<User> qryWapper = new EntityWrapper<>();
		//这里也能直接设置 entity 这是条件就是entity的非空字段值了
//	    qryWapper.setEntity(user);
		//这里建议直接用 常量 
	//	qryWapper.eq(User.CODE, user.getCode());
		pageList = userService.selectPage(page, qryWapper);
		System.out.println(pageList.getRecords().get(0));
		log.info("分页结束");
	}

}

性能插件体现,控制台输出:

 Time:4 ms - ID:cn.lqdev.learning.mybatisplus.samples.biz.dao.UserDao.selectPage
 Execute SQL: SELECT id AS id,code,`name`,`status`,gmt_create AS gmtCreate,gmt_modified AS gmtModified FROM user WHERE id=1026120705692434433 AND code='801' AND `name`='okong-Pagination' LIMIT 0,10

公共字段自动填充

通常,每个公司都有自己的表定义,在《阿里巴巴_Java_开发手册》中,就强制规定表必备三字段:id, gmt_create, gmt_modified。所以通常我们都会写个公共的拦截器去实现自动填充比如创建时间和更新时间的,无需开发人员手动设置。而在MP中就提供了这么一个公共字段自动填充功能

设置填充字段的填充类型

User.java

    /**
     * 创建时间
     */
    @TableField(fill=FieldFill.INSERT)
    private Date gmtCreate;
    /**
     * 修改时间
     */
    @TableField(fill=FieldFill.INSERT_UPDATE)
    private Date gmtModified;

注意这里是可以在代码生成器里面配置规则的,可自动配置,详见代码生成器类。

定义处理类

MybatisObjectHandler.java

public class MybatisObjectHandler extends MetaObjectHandler{

	@Override
	public void insertFill(MetaObject metaObject) {
		//新增时填充的字段
		setFieldValByName("gmtCreate", new Date(), metaObject);
		setFieldValByName("gmtModified", new Date(), metaObject);
		
	}

	@Override
	public void updateFill(MetaObject metaObject) {
		//更新时 需要填充字段
		setFieldValByName("gmtModified", new Date(), metaObject);
	}
}

同时修改springb-mybatis.xml文件,加入此配置:

    <bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
        <!--
            AUTO->`0`("数据库ID自增")QW
             INPUT->`1`(用户输入ID")
            ID_WORKER->`2`("全局唯一ID")
            UUID->`3`("全局唯一ID")
        -->
        <property name="idType" value="2" />
        <property name="metaObjectHandler" ref="mybatisObjectHandler"></property>
    </bean>
    
    <bean id="mybatisObjectHandler" class="cn.lqdev.learning.mybatisplus.samples.config.MybatisObjectHandler"/>

这个时候再新增或者修改,对应时间就会进行更新了。

 Time:31 ms - ID:cn.lqdev.learning.mybatisplus.samples.biz.dao.UserDao.insert
 Execute SQL: INSERT INTO user ( id, code, `name`, gmt_create,gmt_modified ) VALUES ( 1026135016838037506, '702', 'okong-condition', '2018-08-05 23:57:07.344','2018-08-05 23:57:07.344' )

相关资料

总结

本文主要列举了开发过程中常用的操作数据库的方法及相关配置。应该可以应付百分之八十以上的需求了吧。之后有时间,会进行补充的,比如自定义插件、大批量数据的写法等。

最后

若文中有错误或者遗漏之处,还望指出,共同进步!

老生常谈

  • 个人QQ:499452441
  • 公众号:lqdevOps

公众号

个人博客:http://blog.lqdev.cn

打开App,阅读手记
5人推荐
发表评论
随时随地看视频慕课网APP