一、基本概念

1、简介

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,
在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

在这里插入图片描述

2、特性

1.无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑

2.损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作

3.强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分CRUD 操作,更有强大的条件构造器,满足各类使用需求

4.支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错

5.支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由
配置,完美解决主键问题

6.支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强
大的 CRUD 操作

7.支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )

8.内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用

9.内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询

10.分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库

11.内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询

12.内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防 误操作

二、快速使用

1. 引入依赖

      <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

2. application.yml 配置文件

mybatis-plus:
  configuration:
    #开启sql日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 
  global-config:
    # 是否打印 Logo banner
    banner: true
    # 是否初始化 SqlRunner
    #不打开,使用SqlRunner会报错:Cause: java.lang.IllegalArgumentException: Mapped
    # Statements collection does not contain value 			 
    #for com.baomidou.mybatisplus.core.mapper.SqlRunner.SelectList
    enableSqlRunner: true

3. 引导类中添加 @MapperScan 注解,扫描 Mapper 文件夹

@SpringBootApplication
@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

4.业务逻辑层接口继承IService接口

public interface UserService extends IService<UserEntity> {

    /**
     * 注册
     * @param vo 用户请求Vo
     * @return
     */
    JsonResult postUser(UserRequestVo vo);
    
}

5.业务逻辑层接口继承 ServiceImpl 类
在业务逻辑层实现类中调用IService或者Mapper封装的CRUD方法

@Slf4j
@Service
@SuppressWarnings({"rawtypes","unchecked"})
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {

   /**
     * 注册
     * @param vo 用户请求实体
     * @return
     */
    @Override
    public JsonResult postUser(UserRequestVo vo) {
        boolean phoneExistence = this.checkPhoneExistence(vo.getPhoneNumber());
        log.info("用户是否已经存在:{}",phoneExistence);
        if(phoneExistence){
            return JsonResult.fail("该手机号已注册");
        }

        String password = vo.getPassword();
        boolean isChinese = this.isChinese(password);
        if(isChinese){
            return JsonResult.fail("密码包含中文或者中文符号");
        }

        //对密码进行加密
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String encryptByKey = AesFront.aesEncryptByKey(password, uuid);
        log.info("加密后:encryptByKey{}",encryptByKey);

        UserEntity entity = UserEntity.builder().build();
        BeanUtils.copyProperties(vo,entity);
        entity.setPassword(encryptByKey);
        entity.setSalt(uuid);
        entity.setOpenId(UUID.randomUUID().toString().replace("-", ""));

        boolean flag = this.save(entity);
        return flag ? JsonResult.ok("注册成功") : JsonResult.fail("注册失败");
    }

}

6.Mapper继承 BaseMapper接口

@Mapper
public interface UserMapper extends BaseMapper<UserEntity>{

}

7.entity继承 自动填充 的BaseEntity

@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("user")
public class UserEntity extends BaseEntity implements Serializable {
    private static final long serialVersionUID = -7690860956874940192L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 姓名
     */
    private String userName;

    /**
     * 手机号
     */
    private String phoneNumber;

    /**
     * 密码
     */
    private String password;

    /**
     * 性别:1:男  2:女
     */
    private String gender;

    /**
     * 学历
     */
    private String education;

    /**
     * 家庭地址
     */
    private String address;

    /**
     * 盐
     */
    private String salt;

    /**
     * 用户编号
     */
    private String openId;

    /**
     * 用户状态:1:已入职,2,实习,3:已转正,4:已离职,5:已转岗
     */
    private String userState;

}

8.自动填充 BaseEntity

@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {
    private static final long serialVersionUID = 953878410015883109L;

    @TableField(fill = FieldFill.INSERT)
    private String createBy;

    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateBy;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    /**
     * 逻辑删除
     * value = "" 默认的原值
     * delval = "" 删除后的值
     * :@TableLogic(value="默认的原值",delval="删除后的值")
     *
     *  删:逻辑删除
     *  改:sql默认添加在where条件字段中 and is_del = 0
     *  查:sql自动加上了条件未删除条件:SELECT * from xxxtable  where is_del =0
     */
    @TableLogic(value = "0",delval = "1")
    private Integer isDel;

}

9.自定义填充器

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    private static final String UPDATE = "updateBy";

    private static final String UPDATE_TIME = "updateTime";

    /**
     * 新增 给公共的字段赋值
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {

        LoginUser loginUser = UserThreadLocal.getLoginUser();
        log.info("MyMetaObjectHandler=insertFill=loginUser:{}",loginUser);

        String userName = ObjectUtils.isNotNull(loginUser) ? loginUser.getUsername() : null;

        this.setFieldValByName("createBy",userName,metaObject);

        this.setFieldValByName("createTime",new Date(),metaObject);

        this.setFieldValByName(UPDATE,userName,metaObject);

        this.setFieldValByName(UPDATE_TIME,new Date(),metaObject);

    }

    /**
     * 修改 给公共的字段赋值
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {

        LoginUser loginUser = UserThreadLocal.getLoginUser();
        log.info("MyMetaObjectHandler=updateFill=loginUser:{}",loginUser);

        this.setFieldValByName(UPDATE,ObjectUtils.isNotNull(loginUser) ? loginUser.getUsername() : null,metaObject);

        this.setFieldValByName(UPDATE_TIME,new Date(),metaObject);

    }

}

mybatis-plus日志打印内容:

在这里插入图片描述

三、常用注解


@TableName("user")							表名注解,标识实体类对应的表

@TableId(value = "id", type = IdType.AUTO)  主键注解

@TableField("name")							字段注解(非主键)
											属性与字段名不相同需要加@TableField("name")	
											相同则可以不加@TableField(属性命名规则:数
											据库字段去掉下划线,下个单词首字母大写)
updateStrategy 属性
FieldStrategy.IGNORED:在更新操作期间将忽略该字段,如果该字段为null 将在数据库设置为null
FieldStrategy.NOT_NULL:该字段将插入或更新为非空。
FieldStrategy.NULL:该字段将插入或更新为空。
FieldStrategy.DEFAULT:当值不为 null 时将更新该字段												

@TableLogic(value = "0",delval = "1")		逻辑删除
											value = "" 	默认的原值
											delval = "" 删除后的值

@OrderBy									内置 SQL 默认指定排序,优先级低于 wrapper 条								
											件查询	
											isDesc:是否倒序查询
											sort:	数字越小越靠前

@TableField(fill = FieldFill.INSERT)		自动填充的字段
											DEFAULT:         默认不处理
											INSERT:          插⼊时填充字段
											UPDATE:          更新时填充字段
											INSERT_UPDATE:   插⼊和更新时填充字段

@TableField(exist = false)					表示该属性不为数据库表字段但又是必须使用的。


四、IService CRUD 接口

一、项目中的使用:

1.业务逻辑层接口继承IService

	public interface UserService extends IService<UserEntity> {}

2.业务逻辑层接口实现类继承ServiceImpl

	@Slf4j
	@Service
	@SuppressWarnings({"rawtypes","unchecked"})
	@RequiredArgsConstructor
	public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements  
	UserService {}

3.调用

1.新增

	boolean flag = this.save(entity);

2.查询

LambdaQueryWrapper<UserEntity> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserEntity::getPhoneNumber,phoneNumber);
    queryWrapper.ne(UserEntity::getUserState, UserStatusEnum.RESIGNED.getStateCode());
    return this.getOne(queryWrapper);

3.修改

	LambdaUpdateWrapper<UserEntity> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.set(UserEntity::getUserName,vo.getUserName());
    updateWrapper.eq(UserEntity::getId,vo.getId());

   //调用 boolean update(Wrapper<T> updateWrapper)方法,自动填充会失效
    boolean flag = this.update(UserEntity.builder().build(),updateWrapper);

4.分页查询

	    LambdaQueryWrapper<UserEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(vo.getUserName()),UserEntity::getUserName,vo.getUserName());
        queryWrapper.like(StringUtils.isNotBlank(vo.getPhoneNumber()),UserEntity::getPhoneNumber,vo.getPhoneNumber());
        queryWrapper.eq(StringUtils.isNotBlank(vo.getEducation()),UserEntity::getEducation,vo.getEducation());
        queryWrapper.orderByDesc(UserEntity::getCreateTime);
        Page<UserEntity> entities = this.baseMapper.selectPage(page, queryWrapper);
        
        return JsonResult.ok(new EuDataGridResult<>(entities.getTotal(),entities.getRecords()));

5.查询总数

       LambdaQueryWrapper<CustomizedOrder> queryWrapper = new LambdaQueryWrapper<>();
       queryWrapper.eq(CustomizedOrder::getUserOpenId,vo.getUserOpenId());
       //下单结束时间
       queryWrapper.lt(CustomizedOrder::getCreateAt,vo.getEndPlaceOrder());
       //下单开始时间
       queryWrapper.ge(CustomizedOrder::getCreateAt,vo.getBeginPlaceOrder());

       return this.baseMapper.selectCount(queryWrapper);

五、Mapper CRUD 接口

一、项目中的使用:

1.接口实现BaseMapper

@Mapper
public interface UserMapper extends BaseMapper<UserEntity>{

}

2.调用

int insert = this.baseMapper.insert(entity);

六、逻辑删除

只对自动注入的 sql 起效: 执行原生sql:是否删除条件不回自动添加

插入: 不作限制
查找: 追加 where 条件过滤掉已删除数据,如果使用 wrapper.entity 生成的 where 条件也会自动
	  追加该字段,追加未删除的条件
更新: 追加 where 条件防止更新到已删除数据,如果使用 wrapper.entity 生成的 where 条件也会
	  自动追加该字段,追加未删除的条件
删除: 转变为 更新(逻辑删除),更改是否删除字段的值

一、方法一 配置文件配置

步骤 1 application.yml

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag 	# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不
      						  	# 配置步骤2)
      logic-delete-value: 1 	# 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

步骤 2: 实体类字段上加上@TableLogic注解

		@TableLogic
		private Integer deleted;

二、方法二 直接在Entity写@TableLogic,并指定value和delval 的属性值

   /**
     * 逻辑删除
     * value = "" 默认的原值
     * delval = "" 删除后的值
     * :@TableLogic(value="默认的原值",delval="删除后的值")
     *
     *  删:逻辑删除
     *  改:sql默认添加在where条件字段中 and is_del = 0
     *  查:sql自动加上了条件未删除条件:SELECT * from xxxtable  where is_del =0
     */
    @TableLogic(value = "0",delval = "1")
    private Integer isDel;
    

七、自动填充功能

执行原生sql:不会自动填充,是否删除条件不回自动添加

一、填充原理:直接给entity的属性设置值若无值则入库会是null

二、自动填充失效

	使用boolean update(Wrapper updateWrapper)这个方法,自动填充会失效
	boolean update(Wrapper updateWrapper)的底层实现为:

	default boolean update(Wrapper<T> updateWrapper) {
        return this.update((Object)null, updateWrapper);
 	}
 	
	属性自动填充需要从第一个参数获取Object实体类,自动填充的核心方法:populateKeys中会判断
	tableInfo就是获取的实体类对象,所以导致属性自动填充失效.
 	
	if (null == tableInfo) {
     	 /* 不处理 */
   		 return parameterObject;
 	}

	使用带实体的修改方法
	boolean update(T entity, Wrapper<T> updateWrapper)
	例:new一个空的entity
	this.update(new ServiceItemIcon(),
                Wrappers.<ServiceItemIcon>lambdaUpdate()
                        .set(!ObjectUtils.isEmpty(status), ServiceItemIcon::getStatus, status)
                        .eq(ServiceItemIcon::getUid, uid)
     );
	

三、配置步骤

步骤 1: 自定义填充器

package com.alib.scenery.config.obturator;

import com.alib.scenery.common.threadLocal.UserThreadLocal;
import com.alib.scenery.vo.user.request.LoginUser;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @Description: 填充器 执行原生sql不会自动填充(如:SqlRunner)
 */
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    private static final String UPDATE = "updateBy";

    private static final String UPDATE_TIME = "updateTime";

    /**
     * 新增 给公共的字段赋值
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {

        LoginUser loginUser = UserThreadLocal.getLoginUser();
        log.info("MyMetaObjectHandler=insertFill=loginUser:{}",loginUser);

        String userName = ObjectUtils.isNotNull(loginUser) ? loginUser.getUsername() : null;

        this.setFieldValByName("createBy",userName,metaObject);

        this.setFieldValByName("createTime",new Date(),metaObject);

        this.setFieldValByName(UPDATE,userName,metaObject);

        this.setFieldValByName(UPDATE_TIME,new Date(),metaObject);

    }

    /**
     * 修改 给公共的字段赋值
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {

        LoginUser loginUser = UserThreadLocal.getLoginUser();
        log.info("MyMetaObjectHandler=updateFill=loginUser:{}",loginUser);

        this.setFieldValByName(UPDATE,ObjectUtils.isNotNull(loginUser) ? loginUser.getUsername() : null,metaObject);

        this.setFieldValByName(UPDATE_TIME,new Date(),metaObject);

    }

}

步骤 2: 定义BaseEntity,每个Entity 需继承BaseEntity

package com.alib.scenery.common.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.io.Serializable;
import java.util.Date;

/**
 * @Description: 统一自动填充实体(抽取每个表公共的字段) 需要配合填充器才能生效
 *
 * DEFAULT:         默认不处理
 * INSERT:          插⼊时填充字段
 * UPDATE:          更新时填充字段
 *INSERT_UPDATE:    插⼊和更新时填充字段
 */
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity implements Serializable {
    private static final long serialVersionUID = 953878410015883109L;

    @TableField(fill = FieldFill.INSERT)
    private String createBy;

    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateBy;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    /**
     * 逻辑删除
     * value = "" 默认的原值
     * delval = "" 删除后的值
     * :@TableLogic(value="默认的原值",delval="删除后的值")
     *
     *  删:逻辑删除
     *  改:sql默认添加在where条件字段中 and is_del = 0
     *  查:sql自动加上了条件未删除条件:SELECT * from xxxtable  where is_del =0
     */
    @TableLogic(value = "0",delval = "1")
    private Integer isDel;

}

八、分页拦截器配置

若不配置分页拦截器:mybatis-plus分页 ,records有记录,total却始终为0


/**
 * @Description: mybatis-plus 分页的拦截器配置
 */
@Configuration
public class MyBatisPlusConfiguration {

    /**
     * 解决:mybatis-plus分页 records有记录,total却始终为0
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

九、执行原生sql (使用SqlRunner方式)

注:
执行原生sql不会自动填充(SqlRunner),不初始化SqlRunner会报错:IllegalArgumentException


1.使用SqlRunner需要在application.yml配置文件中配置:
	# 是否初始化 SqlRunner
    #不打开使用SqlRunner报错:Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not 	  
    #contain value for com.baomidou.mybatisplus.core.mapper.SqlRunner.SelectList
    enableSqlRunner: true


代码

一、查询


   /**
     * 查询用户信息(原生sql) 执行原生sql不会自动填充(SqlRunner)
     * @param vo
     * @return
     */
    public JsonResult sqlRunnerFindUserByCondition(UserRequestVo vo){
        List<UserRequestVo> list = new ArrayList<>();
        List<Map<String, Object>> maps = SqlRunner.db().selectList("SELECT * FROM `user` WHERE 1 = 1 AND is_del = '0' ORDER BY create_time DESC");
        if(ObjectUtils.isNotNull(maps) && !maps.isEmpty()){
            UserRequestVo dataVo = null;
            for (Map<String, Object> map : maps) {
                dataVo = new UserRequestVo();
                Object id = map.get("id");
                dataVo.setId(ObjectUtils.isNull(id) ? null : Integer.parseInt(String.valueOf(id)));

                Object userName = map.get("user_name");
                dataVo.setUserName(ObjectUtils.isNull(userName) ? "" : String.valueOf(userName));

                Object phoneNumber = map.get("phone_number");
                dataVo.setPhoneNumber(ObjectUtils.isNull(phoneNumber) ? "" : String.valueOf(phoneNumber));

                list.add(dataVo);
            }
        }
        return JsonResult.ok(list);
    }

二、新增

    /**
     * 新增用户 (原生sql) 执行原生sql不会自动填充(SqlRunner)
     * @param vo
     * @return
     * INSERT INTO `user` (user_name,phone_number,gender,open_id) VALUES('哈哈' , '17890000000' , '1', '7f5b07c0a2914a7a94a23b84d468fe8f')
     */
    @Override
    public JsonResult sqlRunnerSaveUser(UserRequestVo vo) {
        boolean insertFlag = SqlRunner.db().insert("INSERT INTO `user` (user_name,phone_number,gender,open_id) VALUES({0} , {1} , {2}, {3})",
                vo.getUserName(),vo.getPhoneNumber(),vo.getGender(),UUID.randomUUID().toString().replace("-",""));

        return insertFlag ? JsonResult.ok("新增成功") : JsonResult.fail("新增失败");
    }
    

三、修改

    /**
     * 修改用户信息 (原生sql) 执行原生sql不会自动填充(SqlRunner)
     * @param vo
     * @return
     * UPDATE `user` SET user_name = '奥巴驴',gender = '1' WHERE id = 3
     */
    @Override
    public JsonResult sqlRunnerUpdateUser(UserRequestVo vo) {
        boolean updateFlag = SqlRunner.db().update("UPDATE `user` SET user_name = {0},gender = {1} WHERE id = {2}",
                vo.getUserName(), vo.getGender(), vo.getId());
        return updateFlag ? JsonResult.ok("修改成功") : JsonResult.fail("修改失败");
    }
    

十、新增后返回id

一、keyProperty=“id” useGeneratedKeys=“true”

<insert id="insert" keyProperty="id" useGeneratedKeys="true">
        insert into staff_order(tenant_id, institution_id, s_order_id, p_order_id, staff_id, staff_name,
                                staff_type, order_model,
                                staff_phone, provider_id,
                                order_status,
                                order_start_time, depart_start_time, service_start_time, service_stop_time, start_img,
                                start_lat,
                                start_longitude, start_address, stop_img, stop_lat, stop_longitude, stop_address,
                                description, status,order_no)
        values (#{tenantId}, #{institutionId}, #{sOrderId}, #{pOrderId}, #{staffId}, #{staffName}, #{staffType},
                #{orderModel}, #{staffPhone},
                #{providerId}, #{orderStatus},
                #{orderStartTime}, #{departStartTime}, #{serviceStartTime}, #{serviceStopTime}, #{startImg},
                #{startLat},
                #{startLongitude}, #{startAddress}, #{stopImg}, #{stopLat}, #{stopLongitude}, #{stopAddress},
                #{description},
                #{status},#{orderNo})
    </insert>

二、selectKey 标签

    <insert id="insertStaffOrder" parameterType="com.baidu.mine.common.entity.po.StaffOrder">
        <selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
            select last_insert_id();
        </selectKey>
        insert into staff_order(tenant_id, institution_id, s_order_id, p_order_id, staff_id, staff_name,
        staff_type, order_model,
        staff_phone, provider_id,
        order_status,
        order_start_time, depart_start_time, service_start_time, service_stop_time, start_img,
        start_lat,
        start_longitude, start_address, stop_img, stop_lat, stop_longitude, stop_address,
        description, status)
        values (#{tenantId}, #{institutionId}, #{sOrderId}, #{pOrderId}, #{staffId}, #{staffName}, #{staffType},
        #{orderModel}, #{staffPhone},
        #{providerId}, #{orderStatus},
        #{orderStartTime}, #{departStartTime}, #{serviceStartTime}, #{serviceStopTime}, #{startImg},
        #{startLat},
        #{startLongitude}, #{startAddress}, #{stopImg}, #{stopLat}, #{stopLongitude}, #{stopAddress},
        #{description},
        #{status})
    </insert>
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐