MyBatis-Plus 笔记
MyBatis-Plus学习(3.3.1.tmp版本教程)
1、快速开始
1.1、数据库脚本
DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) ); DELETE FROM user; INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com');
1.2、创建一个空的 Spring Boot 工程
1、pom依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <!-- 数据库连接-MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- MyBatis-Plus依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1.tmp</version> </dependency>
2、数据连接配置文件:application.properties
spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mysql 8 驱动包为com.mysql.cj.jdbc.Driver、需要增加时区的配置 serverTimezone=GMT%2B8
1.3、Service-Dao层代码
常规操作:pojo-dao-service-controller 要写很多的代码 在dao和service层 调来调去的
mybatis-plus: 不用自己动手,CRUD全部帮你生成,写代码越来越方便,人们也在变懒(其中不知道所以然的人),
码农----程序员----大佬======逐渐成长 --- 脚踏实地---- 一步一脚印 有多大能力 干多大事 拿多大的工资
实体类(没有变化)
@Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String name; private int age; private String email; }
Mapper(Dao)代码(就下面这样): 结束了,他帮我们完成了所有的CRUD 关于 BaseMapper
之前还要写一大批的增删改查在xml文件中 并且容易出错等还要一系列的配置 头大 。。。
所以说越来越轻量化了 -- 以后会不会被人工智能取代啊。。。那肯定是不可能的。。只能说生态越来越好了Java开发
他的底层发展到可以说是完美级别天花板之说。不用人们再去敲重复的代码。被封装成一个工具框架包直接拿来用,人们就可以把心思放在扩展业务方面了。。但是呢,该弄懂的底层还是要懂得。不然出了问题就会一脸懵逼,头只会更大喽!
@Mapper //或者@Repository+@MapperScan("com.mlf.mapper") public interface UserMapper extends BaseMapper<User> { }
1.4、测试
直接带过两行。。没啥差别和传统mybatis
@Autowired private UserMapper userMapper; @Test void contextLoads() { // 查询全部User信息 参数是一个Wrapper,条件构造器,这里我们没有条件 先为null List<User> userList = userMapper.selectList(null); userList.forEach(System.out::println); }
这就入门结束了偶。。感觉可以去和面试官对线之后上班了。。NONONO!这些都是小儿科 小学生都会 要想人前富贵,必现受苦受罪。看源码呗。。。
2、日志配置
用了Plus之后是看不见SQL语句的,配置日志来打印供我们查看一目了然
#配置日志MyBatis-Plus 默认:控制台输出 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
3、CRUD扩展
1、Insert
@Test void insertTest(){ User user = new User(); user.setName("马龙飞"); user.setAge(18); user.setEmail("2644844007@qq.com"); int result = userMapper.insert(user); System.out.println(result); System.out.println(user); }
注意:这里的ID类型设为Long不然类型转换不了就会生成0,第二次报错。 前提是设计表时没有没设置id自增
2、主键生成策略:
数据库插入的ID的默认值为:全局的唯一ID
//对应数据库中的主键(uuid、自增id、雪花算法、Redis、zookeeper!) @TableId(type = IdType.ASSIGN_ID) private Long id;
雪花算法:
snowflake是推特开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生4096个ID),最后还有一个符号位,永远是0.可以保证几乎全球唯一!
3、update
4、自动填充
数据库表中的gmt_modify、gmt_creat自动生成。自动化 gmt指全球时间
方式一:数据库级别操作
1、添加数据库字段名
gmt_modify、gmt_creat
gmt_modify datetime default current_timestamp null comment '更新时间', gmt_creat datetime default current_timestamp null comment '创建时间'
2、修改实体类
private Date gmtModify; private Date gmtCreat;
3、添加、修改数据测试
方式二:代码级别操作(去掉数据库默认生成)
1、修改实体类
public enum FieldFill { /** * 默认不处理 */ DEFAULT, /** * 插入填充字段 */ INSERT, /** * 更新填充字段 */ UPDATE, /** * 插入和更新填充字段 */ INSERT_UPDATE }
// 注意!这里需要标记为填充字段 @TableField(fill = FieldFill.INSERT_UPDATE) private Date gmtModify; @TableField(fill = FieldFill.INSERT) private Date gmtCreat;
2、自定义实现类 MyMetaObjectHandler
注意属性字段名
@Slf4j @Component //把方法丢给SpringIOC容器 public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("gmtCreat",new Date(),metaObject); this.setFieldValByName("gmtModify",new Date(),metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("gmtModify",new Date(),metaObject); } }
3、测试
5、乐观锁
乐观锁:乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现)
特点:乐观锁是一种并发类型的锁,其本身不对数据进行加锁通而是通过业务实现锁的功能,不对数据进行加锁就意味着允许多个请求同时访问数据,同时也省掉了对数据加锁和解锁的过程,这种方式因为节省了悲观锁加锁的操作,所以可以一定程度的的提高操作的性能,不过在并发非常高的情况下,会导致大量的请求冲突,冲突导致大部分操作无功而返而浪费资源,所以在高并发的场景下,乐观锁的性能却反而不如悲观锁
悲观锁:顾名思义,悲观锁是基于一种悲观的态度类来防止一切数据冲突,它是以一种预防的姿态在修改数据之前把数据锁住,然后再对数据进行读写,在它释放锁之前任何人都不能对其数据进行操作,直到前面一个人把锁释放后下一个人数据加锁才可对数据进行加锁,然后才可以对数据进行操作,**一般数据库本身锁的机制都是基于悲观锁的机制实现的;*
特点:可以完全保证数据的独占性和正确性,因为每次请求都会先对数据进行加锁, 然后进行数据操作,最后再解锁,而加锁释放锁的过程会造成消耗,所以性能不高
乐观锁实现方式:
取出记录时,获取当前version,更新时,带上这个version同时更新
若version对不上,就更新失败
1、增加数据库字段
version int(50) null comment '乐观锁',
2、更新Java实体类
//乐观锁字段 @Version private int version;
3、乐观锁组件注册
// Spring Boot 方式 @Configuration @MapperScan("com.mlf.mapper") public class MybatisPlusConfig { @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } }
4、测试
1、成功时
//乐观锁测试 正常情况下执行 成功时 @Test void updateLockTest1(){ User user = userMapper.selectById(1L); user.setName("马龙飞学JavaEE"); user.setAge(18); int result = userMapper.updateById(user); System.out.println(result); System.out.println(user); }
2、失败时
//乐观锁测试 异常情况下执行 失败时 @Test void updateLockTest2(){ //模拟第一个线程 User user1 = userMapper.selectById(1); user1.setName("马龙飞学JavaEE11111"); //模拟第二个线程更新 加塞进去的理论会执行成功! User user2 = userMapper.selectById(1); user2.setName("马龙飞学JavaEE22222"); int result1 = userMapper.updateById(user2); System.out.println("user2::===="+result1); //第一个线程updateById执行的时候version已经被更新 造成不匹配执行失败 int result2 = userMapper.updateById(user1); System.out.println("user1::===="+result2); }
6、select
1、分页插件注册
//分页插件配置 @Bean public PaginationInterceptor paginationInnerInterceptor(){ return new PaginationInterceptor(); }
2、测试
//批量查询测试 @Test void selectBatchIdsTest(){ List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3, 4, 5)); users.forEach(System.out::println); } //Map集合查询测试 @Test void selectByMapTest(){ HashMap<String, Object> map = new HashMap<>(); map.put("name","马龙飞"); map.put("age","18"); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); } //分页查询测试 @Test void pageHelperTest(){ Page<User> page = new Page<>(2,5); userMapper.selectPage(page, null); //获取用户记录 然后再输出 getRecords<User> page.getRecords().forEach(System.out::println); System.out.println(page.getTotal()); System.out.println(page.getSize()); }
7、delete
//删除操作 @Test void deleteTest(){ //通过ID删除 userMapper.deleteById(1518866041659854865L); //批量删除 userMapper.deleteBatchIds(Arrays.asList(1518866041659854863L,1518866041659854862L,1518866041659854864L)); } //通过Map删除 @Test void deleteByMapTest(){ HashMap<String, Object> map = new HashMap<>(); map.put("id","1518866041659854866"); map.put("name",""); userMapper.deleteByMap(map); }
8、逻辑删除
说明:
只对自动注入的 sql 起效:
- 插入: 不作限制
- 查找: 追加 where 条件过滤掉已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 更新: 追加 where 条件防止更新到已删除数据,且使用 wrapper.entity 生成的 where 条件会忽略该字段
- 删除: 转变为 更新
物理删除:从数据库中的数据删除
逻辑删除:从逻辑上删除,相当于增加一个字段标记是否删除
1、在数据库表中增加字段deleted 初始值为0
deleted int(1) default 0 null comment '逻辑删除',
2、修改pojo实体类
@TableLogic private int deleted; //逻辑删除
3、配置拦截器插件(新版本不用配置,直接加注解可)
注释千万别写配置代码行后边否则报错它会把value后的字符全部当做value赋值给字段而导致TypeException异常···
#配置逻辑删除 # 全局逻辑删除的实体字段名(since 3.3.0,配置后可不加注解 @TableLogic) mybatis-plus.global-config.db-config.logic-delete-field=deleted # 逻辑已删除值(默认为 1) mybatis-plus.global-config.db-config.logic-delete-value=1 # 逻辑未删除值(默认为 0) mybatis-plus.global-config.db-config.logic-not-delete-value=0
4、测试
//逻辑删除 @Test void logicDeletedTest(){ userMapper.deleteById(1518866041659854868L); }
4、p6spy
1、application.properties变化
调整数据源驱动、URL加上p6spy
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/mybatis_plus?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
2、spy.properties
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 #appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql appender=com.p6spy.engine.spy.appender.Slf4JLogger # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 #useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2
3、引入依赖
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>
5、条件构造器
说明:
QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类
用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件
注意: entity 生成的 where 条件与 使用各个 api 生成的 where 条件没有任何关联行为
测试:
1、allEq
allEq(Map<R, V> params) allEq(Map<R, V> params, boolean null2IsNull) allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
个别参数说明:
params
: key
为数据库字段名,value
为字段值
null2IsNull
: 为true
则在map
的value
为null
时调用 isNull 方法,为false
时则忽略value
为null
的
@SpringBootTest public class WrapperTest { @Autowired private UserMapper userMapper; //equals @Test void eqTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("name","Tom");//name=Tom userMapper.selectList(wrapper).forEach(System.out::println); } //not equals @Test void neTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.ne("name","Tom");//name!=Tom userMapper.selectList(wrapper).forEach(System.out::println); } //greater:大于 less:小于 equals:等于 // gt:大于,ge:大于等于,lt:小于,le:小于等于 @Test void gtTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.gt("age","24");//age>24 // Execute SQL:SELECT id,name,age,version,deleted,email,gmt_modify,gmt_creat FROM user WHERE deleted=0 AND (age > '24') userMapper.selectList(wrapper).forEach(System.out::println); } @Test void geTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.ge("age","24");//age>25 //Execute SQL:SELECT id,name,age,version,deleted,email,gmt_modify,gmt_creat FROM user WHERE deleted=0 AND (age >= '24') userMapper.selectList(wrapper).forEach(System.out::println); } @Test void ltTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.lt("age","24");//age<24 //Execute SQL:SELECT id,name,age,version,deleted,email,gmt_modify,gmt_creat FROM user WHERE deleted=0 AND (age < '24') userMapper.selectList(wrapper).forEach(System.out::println); } @Test void leTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.le("age","24");//age<24 //Execute SQL:SELECT id,name,age,version,deleted,email,gmt_modify,gmt_creat FROM user WHERE deleted=0 AND (age <= '24') userMapper.selectList(wrapper).forEach(System.out::println); } //between:范围 @Test void betweenTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.between("age",20,24); // Execute SQL:SELECT id,name,age,version,deleted,email,gmt_modify,gmt_creat FROM user WHERE deleted=0 AND (age BETWEEN 20 AND 24) userMapper.selectList(wrapper).forEach(System.out::println); } //notBetween:不在這個范围 @Test void notBetweenTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.notBetween("age",20,24); // Execute SQL:SELECT id,name,age,version,deleted,email,gmt_modify,gmt_creat FROM user WHERE deleted=0 AND (age NOT BETWEEN 20 AND 24) userMapper.selectList(wrapper).forEach(System.out::println); } //like:模糊查询 @Test void likeTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.like("name","马"); // Execute SQL: WHERE deleted=0 AND (name LIKE '%马%') userMapper.selectList(wrapper).forEach(System.out::println); } //likeLeft: 左模糊 即为:可查询以什么结尾的条件 @Test void likeLeftTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.likeLeft("email",".com"); // Execute SQL: WHERE deleted=0 AND (email LIKE '%.com') userMapper.selectList(wrapper).forEach(System.out::println); } //likeRight: 右模糊 即为:可查询以什么开头的数据 @Test void likeRightTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.likeRight("email","test"); // Execute SQL: WHERE deleted=0 AND (email LIKE 'test%') userMapper.selectList(wrapper).forEach(System.out::println); } //isNull: 字段为空 @Test void isNullTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.isNull("email"); // Execute SQL: WHERE deleted=0 AND (email IS NULL) userMapper.selectList(wrapper).forEach(System.out::println); } //isNotNull: 字段不为空 @Test void isNotNullTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.isNotNull("email"); // Execute SQL: WHERE deleted=0 AND (email IS NOT NULL) userMapper.selectList(wrapper).forEach(System.out::println); } //in: 查询该字段在value中的值 @Test void inTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.in("id",1,2,3,4); // Execute SQL: WHERE deleted=0 AND (id IN (1,2,3,4)) userMapper.selectList(wrapper).forEach(System.out::println); } //notIn: 查询该字段不在value中的值 @Test void notInTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.notIn("id",1,2,3,4); // Execute SQL: WHERE deleted=0 AND (id NOT IN (1,2,3,4)) userMapper.selectList(wrapper).forEach(System.out::println); } //inSql、notInSql: 按照SQL语句查询 in表示遵循SQL notIN表示不遵循 @Test void inSql() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.notInSql("id","select id from user where id>2"); // Execute SQL: WHERE deleted=0 AND (id NOT IN (1,2,3,4)) userMapper.selectList(wrapper).forEach(System.out::println); } //or:拼接 OR,AND 嵌套 //主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接) @Test void orTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper .like("name","马龙飞") .or() .like("name","T"); // Execute SQL: WHERE deleted=0 AND (name LIKE '%马龙飞%' OR name LIKE '%T%') //查询name 中包含"马龙飞" or "T"的信息 userMapper.selectList(wrapper).forEach(System.out::println); } //and 不使用and默认就是and连接 @Test void andTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper .eq("version","1") .and(i -> i.eq("name", "Tom").like("email", ".com")); // Execute SQL: WHERE deleted=0 AND (version = '1' AND (name = 'Tom' AND email LIKE '%.com%')) //查询name 中包含"马龙飞" or "T"的信息 userMapper.selectList(wrapper).forEach(System.out::println); } //exists:拼接 EXISTS ( sql语句 ),notExists:拼接 NOT EXISTS ( sql语句 ) @Test void existsTest() { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.exists("select * from user"); // Execute SQL: WHERE deleted=0 AND (exists("select * from user")) //查询name 中包含"马龙飞" or "T"的信息 userMapper.selectList(wrapper).forEach(System.out::println); } //orderByAsc:排序:ORDER BY 字段, … ASC,orderByDesc:排序:ORDER BY 字段, … DESC @Test void orderByTest(){ QueryWrapper<User> wrapper = new QueryWrapper<>(); //wrapper.orderByAsc("age");//升序 //wrapper.orderByDesc("age");//降序 userMapper.selectList(wrapper).forEach(System.out::println); } }
6、代码生成器
1、导入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.1</version> </dependency>
2、自动生成代码类
// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 public class AtuoGeneratorCode { public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 1、全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("mlf"); gc.setOpen(false); gc.setFileOverride(true);//是否覆盖 gc.setSwagger2(true); //实体属性 Swagger2 注解 gc.setServiceName("%sService"); gc.setIdType(IdType.ASSIGN_ID); gc.setDateType(DateType.ONLY_DATE); mpg.setGlobalConfig(gc); // 2、数据源配置 DataSourceConfig dsc =new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?useUnicode=true&useSSL=false&characterEncoding=utf8"); dsc.setSchemaName("user"); dsc.setDriverName("com.mysql.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); mpg.setDataSource(dsc); // 3、包配置 PackageConfig pc = new PackageConfig(); //pc.setModuleName(scanner("用户")); pc.setParent("com.mlf"); pc.setEntity("pojo"); pc.setMapper("mapper"); pc.setController("controller"); pc.setService("service"); mpg.setPackageInfo(pc); // 4、策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setInclude("user");//设置要映射的表明 strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); //自动lombok strategy.setRestControllerStyle(true); //rest风格 strategy.setLogicDeleteFieldName("deleted"); //逻辑删除生成 // 自动填充配置 TableFill gmt_creat = new TableFill("gmt_creat", FieldFill.INSERT); TableFill gmt_modify = new TableFill("gmt_creat", FieldFill.INSERT_UPDATE); ArrayList<TableFill> tableFills = new ArrayList<>(); tableFills.add(gmt_creat); tableFills.add(gmt_modify); strategy.setTableFillList(tableFills); //6、执行 mpg.execute(); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY