MyBatis-Plus必知必会:告别低效CRUD,高效开发持久层

本文同步更新于微信公众号:

mp.weixin.qq.com/s/j8__DcSTy...


theme: awesome-green

前言

日常Java后台开发中,绝大多数业务都是单表增删改查。原生MyBatis虽然灵活,但存在大量重复劳动:每张表都要手写insert、update、select、delete的XML语句,动态条件拼接需要手动处理字符串判空,分页、时间戳、软删除等通用能力需要自己封装工具类,开发效率低且重复代码难以维护。

本文带你从零入门MyBatis-Plus,理清核心能力与基础用法,适合新手快速上手。

一、背景:MyBatis-Plus 解决了什么问题

1. MyBatis 的痛点

  1. 单表CRUD必须手动编写XML/注解SQL,实体越多,重复代码越多;

  2. 动态查询条件需要手动拼接字符串,极易出现SQL注入、括号逻辑错乱;

  3. 分页、逻辑删除、创建更新时间、并发乐观锁、分布式主键等通用功能无内置实现,每个项目重复封装;

  4. 实体与数据库字段映射、多条件查询样板代码冗余。

2. MyBatis-Plus(简称MP)定位

MP 是**「MyBatis 的增强工具,只增强、不修改」**,完全兼容原生MyBatis所有能力,底层依旧依赖MyBatis执行SQL。

核心目标:「消除单表CRUD冗余代码,内置企业级通用持久层能力,大幅提升后台开发效率」

3. 核心优势

  • 无侵入:原有MyBatis XML、自定义SQL完全保留,可平滑迁移;

  • 开箱即用:CRUD、分页、条件构造器无需手写SQL;

  • 内置通用语法糖:逻辑删除、自动填充、乐观锁、分布式主键;

  • Lambda语法构造条件,编译校验字段,杜绝字符串硬编码错误。

适用场景:管理后台、CRUD为主业务、中小型服务;复杂多表联查、统计报表依旧可以手写原生MyBatis XML。

二、MyBatis-Plus 核心增强功能

2.1 强大CRUD封装:BaseMapper + IService 双层持久层

MP 提供两套分层CRUD封装,分别对应DAO层与Service层,职责清晰。

1)DAO层:BaseMapper(Mapper接口继承)

所有Mapper接口继承BaseMapper<T>,无需编写任何方法,自动拥有单表基础操作能力,直接操作数据库。

java 复制代码
// Mapper接口,无需自定义CRUD方法
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

常用内置方法:

  • insert(T entity):新增单条数据;

  • selectById(id)deleteById(id)updateById(entity):根据主键查询/删除/更新;

  • selectList(Wrapper)delete(Wrapper):按条件批量查询、批量删除;

  • selectPage(Page, Wrapper):分页查询。

  • 「自定义SQL方法」:开发者可在Mapper接口中自定义任意抽象方法,配合XML/注解编写专属SQL,和BaseMapper自带方法共存使用。

2)Service层:IService + ServiceImpl(业务层封装)

业务层做二次封装,在BaseMapper基础上新增批量操作、链式查询、语义化方法,规范业务代码分层。

  • 接口:继承IService<T>

  • 实现类:继承ServiceImpl<M,T>(内置注入Mapper,封装通用逻辑)。

java 复制代码
// Service接口
public interface UserService extends IService<User> {
}

// Service实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

IService独有增强能力:

  1. 批量操作:saveBatch批量新增、updateBatchById批量更新;

  2. 链式Lambda查询:lambdaQuery().eq().list()极简写法;

  3. 语义化方法:getByIdlistpage,区分DAO层底层方法,业务可读性更强。

  4. 「主键批量操作(高频实用)」:IService 内置开箱即用批量主键操作方法,无需手动拼接 IN 条件,底层优化批量SQL性能:

  • listByIds(Collection<ID> idList):根据主键批量查询多条数据
  • removeByIds(Collection<ID> idList):根据主键批量删除数据
  • updateBatchById(Collection<T> entityList):批量更新多条数据
  • saveBatch(Collection<T> entityList):批量新增数据
ini 复制代码
// 批量查询
List<Long> idList = Arrays.asList(1L,2L,3L);
List<User> userList = userService.listByIds(idList);

// 批量删除
userService.removeByIds(idList);

两者区别总结

层级 核心特点 使用场景
BaseMapper Mapper/DAO 底层原生数据库操作,无批量封装 简单单条操作、自定义Mapper内部调用
IService Service业务层 封装批量、链式查询、事务友好 项目标准分层开发,Controller直接调用

2.2 条件构造器 Wrapper:动态拼接Where条件(重点完整版)

Wrapper 是 MyBatis-Plus 最核心的语法糖,「彻底解决原生MyBatis动态SQL繁琐拼接问题」。无需写XML、无需拼接字符串、无需判空,链式调用自动生成安全的 WHERE 条件,同时杜绝 SQL 注入风险。

1)四大构造器介绍(项目固定选型)

Wrapper 分为查询、更新两大体系,「开发一律优先 Lambda 版本」,编译校验不报错、重构不翻车。

  • 「QueryWrapper」:查询/删除,字段写字符串(硬编码,不推荐)
  • 「LambdaQueryWrapper」:查询/删除,Lambda方法引用(企业首选)
  • 「UpdateWrapper」:更新条件,字符串字段(不推荐)
  • 「LambdaUpdateWrapper」:更新条件,Lambda安全写法(企业首选)
ini 复制代码
// 快速创建方式(推荐)
LambdaQueryWrapper<User> lqw = Wrappers.lambdaQuery();
LambdaUpdateWrapper<User> luw = Wrappers.lambdaUpdate();

2)常用条件语法全覆盖

下面是开发 「99% 场景」 会用到的条件,整理成可直接复制的模板。

① 等值条件(eq / ne)
  • eq:等于 =

  • ne:不等于 <>

perl 复制代码
// name = "张三" AND age != 18
lqw.eq(User::getName, "张三")
   .ne(User::getAge, 18);
② 区间大小条件(gt / ge / lt / le / between)
  • gt:大于 >

  • ge:大于等于 >=

  • lt:小于 <

  • le:小于等于 <=

  • between:区间范围

less 复制代码
// age > 18 AND age <= 60
lqw.gt(User::getAge, 18)
   .le(User::getAge, 60);

// age BETWEEN 18 AND 30
lqw.between(User::getAge, 18, 30);
③ 模糊查询(like 系列)
  • like:前后模糊 %值%

  • likeRight:右模糊 值%

  • likeLeft:左模糊 %值

less 复制代码
// 姓名包含"李"
lqw.like(User::getName, "李");
// 手机号以 138 开头
lqw.likeRight(User::getPhone, "138");
④ 空值判断(isNull / isNotNull)
less 复制代码
// 邮箱为 NULL
lqw.isNull(User::getEmail);
// 手机号不为空
lqw.isNotNull(User::getPhone);
⑤ 包含集合(in / notIn)
less 复制代码
// 部门ID在 1、2、3 中
lqw.in(User::getDeptId, Arrays.asList(1L,2L,3L));

3)逻辑关系:AND / OR 与括号嵌套(重点难点)

「默认规则」 :链式连续调用默认是AND 关系。

「普通OR」:直接拼接 or()

perl 复制代码
// name=张三 OR age>18
lqw.eq(User::getName, "张三")
   .or()
   .gt(User::getAge, 18);

「带括号复杂逻辑(必须用lambda子表达式)」

场景:name = 张三 AND (age > 18 OR deptId = 2)

perl 复制代码
lqw.eq(User::getName, "张三")
   .and(w -> w.gt(User::getAge, 18).or().eq(User::getDeptId, 2));

场景:name = 张三 OR (age > 18 AND deptId = 2)

perl 复制代码
lqw.eq(User::getName, "张三")
   .or(w -> w.gt(User::getAge, 18).eq(User::getDeptId, 2));

💡 核心要点:「只要需要括号,必须使用 and(λ) / or(λ) 嵌套」,否则SQL优先级错乱。

4)condition 动态条件(开发最常用、消除大量if)

Wrapper 所有条件方法**「第一个参数为 boolean condition」**:

  • 「condition = true」:拼接该SQL条件

  • 「condition = false」:直接忽略该条件,不拼接

专门用于:前端传参、非必填查询条件,「彻底消灭 if 判断嵌套」

ini 复制代码
// 前端参数
String name = "";
Integer minAge = 18;

// 有姓名才模糊查,有最小年龄才判断
lqw.like(StringUtils.hasText(name), User::getName, name)
   .ge(minAge != null, User::getAge, minAge);

等价传统写法,但代码精简 80%:

scss 复制代码
if (StringUtils.hasText(name)) {
    lqw.like(User::getName, name);
}
if (minAge != null) {
    lqw.ge(User::getAge, minAge);
}

5、字段查询过滤(指定返回字段)

Wrapper 支持 select() 方法自定义查询字段,只返回需要的列,避免查询全字段、查询大文本字段,优化SQL性能、减少返回数据体积,是日常优化高频用法。只支持 Lambda 安全写法,杜绝字段硬编码。

ini 复制代码
// 只查询 id、姓名、手机号,忽略其他所有字段
LambdaQueryWrapper<User> lqw = Wrappers.lambdaQuery();
lqw.select(User::getId, User::getName, User::getPhone)
   .eq(User::getDeleted, 0);

List<User> userList = userMapper.selectList(lqw);

适用场景:列表页展示、分页查询、接口返回精简数据,过滤冗余大字段(头像、备注、大文本)。

6)更新构造器与核心注意事项

更新构造器(LambdaUpdateWrapper)用于构建动态更新条件,实现安全的局部字段更新,是业务开发中替代实体全量更新的主流写法。

1、基础使用示例

通过链式set指定需要更新的字段,搭配where条件精准更新数据,仅修改声明的字段,其余数据库字段保留原值。

ini 复制代码
// 将部门ID=1的所有用户,年龄统一更新为25
LambdaUpdateWrapper<User> luw = Wrappers.lambdaUpdate();
// 仅设置需要更新的字段
luw.set(User::getAge, 25)
   .eq(User::getDeptId, 1L);
// 第一个参数传null:表示不使用实体对象赋值,全部由Wrapper控制更新逻辑
userMapper.update(null, luw);
2、开发核心注意事项(两大高频坑点)

「注意事项1:普通set() 存在空值覆盖数据风险」

原生无参set()无自动判空逻辑,若传入null、空字符串,会强制覆盖数据库原有字段值。前端局部编辑、动态传参场景极易造成数据清空。解决方案:使用**「带条件set(condition)**「自定义非空判断,或直接使用」**setIfPresent」**自动忽略null值,规避空值覆盖问题。

「注意事项2:updateById 实体更新存在全量覆盖坑」

updateById属于**「全量字段覆盖更新」,原理是根据实体主键匹配数据,实体中所有null字段会直接清空数据库对应原值。仅适合完整实体保存、全字段修改场景,「绝对不适合局部编辑」**。

「核心选型总结」:日常局部动态更新,优先使用 Wrapper 链式更新(增量更新、安全不丢数据);避免直接使用 updateById 处理部分字段修改。

2.3 分页功能:开箱即用物理分页

MP提供内置分页拦截器,无需手写limit,自动适配MySQL、Oracle等多数据库,纯物理分页,性能稳定。

1. 前置配置(必须注册分页插件)

java 复制代码
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mpInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // MySQL分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

2. Mapper层分页使用

ini 复制代码
// 参数1:当前页,参数2:每页条数
Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery().eq(User::getDeleted,0);
Page<User> pageResult = userMapper.selectPage(page, wrapper);

// 分页结果获取
List<User> dataList = pageResult.getRecords(); // 当前页数据
long total = pageResult.getTotal(); // 总条数
long pages = pageResult.getPages(); // 总页数

3. Service层极简分页(推荐业务使用)

perl 复制代码
// 链式lambda分页
Page<User> pageData = userService.lambdaQuery()
        .eq(User::getAge, 18)
        .page(new Page<>(1,10));

4. 自定义SQL分页(多表联查/复杂统计 必备)

上述分页方式仅适用于 MP 链式单表查询,实际开发中,「多表联查、子查询、复杂统计、自定义条件SQL」 无法使用 Wrapper,需要手写 XML 自定义SQL。

MP 分页插件完美支持**「自定义SQL分页」**,无需手动写 count 总数、无需拼接 limit,只需遵循固定格式,插件自动 「拦截SQL、自动分页、自动统计总条数」

核心规则:「Mapper方法第一个参数必须是 Page 对象,返回值必须是 Page 对象」

第一步:Mapper 接口定义方法
less 复制代码
// 自定义多表分页查询
Page<UserVO> selectUserPage(@Param("page") Page<UserVO> page,
                            @Param("name") String name);
第二步:XML 手写自定义SQL(无需写limit、无需写count)
sql 复制代码
<select id="selectUserPage" resultType="com.xxx.vo.UserVO">
    SELECT u.id, u.name, u.phone, d.dept_name
    FROM sys_user u
    LEFT JOIN sys_dept d ON u.dept_id = d.id
    <where>
        <if test="name != null and name != ''">
            AND u.name LIKE CONCAT('%',#{name},'%')
        </if>
        AND u.deleted = 0
    </where>
</select>
第三步:业务层调用
ini 复制代码
// 构建分页参数
Page<UserVO> page = new Page<>(1,10);
// 调用自定义分页方法
Page<UserVO> resultPage = userMapper.selectUserPage(page, "张三");

// 获取分页数据
List<UserVO> records = resultPage.getRecords();
long total = resultPage.getTotal();
关键说明
  • 「无需手动分页」:MP插件自动拦截SQL,拼接 LIMIT 分页语句
  • 「无需手动COUNT」:插件自动生成 count 查询统计总条数
  • 「支持返回VO」:自定义分页可以返回自定义视图对象,不限于实体类
  • 「适配所有复杂SQL」:多表联查、分组统计、子查询全部支持

5. 分页扩展特性(性能&容错优化)

  1. 「关闭Count统计(大数据分页性能优化)」 :对于海量数据列表、无需展示总条数/总页数的导出、滚动加载场景,可手动关闭count总数查询,避免全表统计耗时,大幅提升分页性能。构造Page对象时传入第三个参数 false 即可:new Page<>(1,10,false)

  2. 「分页溢出容错」 :开启插件溢出适配,当请求页码超过最大页数时,自动跳转到最后一页,避免返回空数据,配置:setOverflow(true)

  3. 「兼容自定义复杂SQL分页」:手写XML多表联查、子查询、分组统计等复杂SQL,可直接传入Page对象,MP分页插件自动拦截SQL、自动处理分页Limit与总数统计,无需手动编写分页逻辑。

2.4 其他实用语法糖(企业开发标配 + 完整配置)

1)逻辑删除(软删除)

不物理删除数据,通过deleted字段标记删除状态,适配数据溯源、恢复场景。

  1. yml全局配置
arduino 复制代码
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-not-delete-value: 0 # 未删除
      logic-delete-value: 1     # 已删除
  1. 实体增加标记字段
java 复制代码
@TableLogic
private Integer deleted;

生效原理:无需业务代码干预,调用 Mapper 层 delete 删除方法时,底层自动将 DELETE 语句转为 UPDATE 更新语句;所有查询、分页、列表查询会自动拼接 WHERE deleted = 0,自动过滤已删除数据,无需手动处理。

2)自动填充 创建/更新时间

无需业务代码手动set时间,新增、修改时自动赋值create_timeupdate_time

  1. 实体字段注解
ini 复制代码
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
  1. 注册全局自动填充处理器(核心配置,必须配置)

新建自定义填充类,实现 MetaObjectHandler,交给Spring管理,统一自动填充创建时间、更新时间、创建人、更新人等字段。

kotlin 复制代码
@Component
public class MyMetaHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        // 新增时自动填充创建时间、更新时间
        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 修改时只更新更新时间
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
    }
}

生效原理:新增/修改数据时,MP 自动触发填充逻辑,无需业务代码手动 set 时间字段,全局统一规范。

3)乐观锁(并发更新防覆盖)

高并发场景防止多人同时更新覆盖数据,依靠version版本号实现无锁并发控制。

  1. 实体添加版本字段
java 复制代码
@Version
private Integer version;
  1. 注册乐观锁插件(核心配置,必须配置)

在 MP 全局配置类中添加乐观锁拦截器,否则版本号不会自动递增、并发控制失效。

java 复制代码
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mpInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 乐观锁插件(新增)
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
  1. 更新执行逻辑:更新数据时自动携带 where 版本条件,更新成功后版本号自动+1;若并发导致版本不匹配,更新行数为0,更新失败,避免数据覆盖。

  2. 更新逻辑:更新时携带原版本号,版本匹配才更新,版本不匹配更新失败。

4)主键策略 @TableId

统一管理主键生成规则,适配单机自增、分布式唯一ID:

kotlin 复制代码
@TableId(type = IdType.AUTO)
private Long id;

可通过 YAML 全局统一配置,无需每个实体单独注解(可选):

python 复制代码
mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id # 全局默认雪花算法

常用策略说明:

  • AUTO:数据库自增(MySQL单机);

  • ASSIGN_ID:雪花算法,分布式Long唯一ID(MP默认);

  • ASSIGN_UUID:字符串UUID主键。

MyBatis-Plus 除基础CRUD与语法糖外,内置大量企业级高阶插件与防护能力,适配生产环境安全、性能、SaaS多租户、数据脱敏等场景,全部开箱即用。

2.5 高阶增强功能(企业级进阶能力)

1)SQL 日志打印(开发调试必备)

全局开启SQL日志,完整打印执行SQL、参数、执行耗时,方便排查BUG、优化SQL。

yml配置:

yaml 复制代码
mybatis-plus:
  configuration:
    # 开启SQL日志输出
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

效果:控制台完整输出最终执行的SQL语句、查询参数、结果行数,可直接复制到数据库执行调试。

2)SQL 性能分析插件(慢SQL监控)

用于开发、测试环境监控慢SQL,拦截超时SQL,避免劣质SQL上线导致数据库卡顿。可自定义超时阈值,超时自动打印警告日志。

ini 复制代码
@Bean
public MybatisPlusInterceptor mpInterceptor(){
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 慢SQL监控:超时200ms告警
    PerformanceInnerInterceptor performanceInterceptor = new PerformanceInnerInterceptor();
    performanceInterceptor.setMaxTime(200);
    interceptor.addInnerInterceptor(performanceInterceptor);
    return interceptor;
}

生产环境建议关闭,仅用于开发调优。

3)防全表更新/删除(生产安全核心)

MP 内置**「防误操作安全机制」**,默认禁止无条件全表更新、全表删除,彻底杜绝开发人员误写空条件导致的数据清空事故,生产刚需。

原理:当 Wrapper 无任何查询条件、直接执行 update、delete 时,直接抛出异常阻断SQL执行。

如需临时关闭(超级管理员、后台批量操作场景),可手动配置放行,禁止业务代码随意关闭。

4)数据脱敏插件(隐私数据防护)

内置字段自动脱敏能力,无需手动代码处理,查询返回自动脱敏,适配手机号、身份证、邮箱、地址等隐私字段。

支持注解配置脱敏规则:掩码、中间隐藏、首尾展示等,新增/修改数据库存储原始明文,「仅查询返回脱敏」,不影响数据库真实数据。

5)多租户插件(SaaS系统必备)

MP 官方内置多租户数据隔离插件,适配 SaaS、多商户、多团队共用一套数据库场景,实现**「代码零侵入全局数据隔离」**。

核心原理:拦截所有CRUD SQL,自动追加租户条件,无需业务代码手动拼接租户ID。

核心细节:

  • 默认租户字段:tenant_id(可自定义任意字段名,如 merchant_id、company_id)
  • 租户ID由业务拦截器解析Token后存入 ThreadLocal,插件仅读取不生成
  • 支持自定义忽略表(字典表、公共配置表、租户表本身不隔离)
  • 支持超级管理员关闭租户隔离,查询全量数据

核心配置示例:

less 复制代码
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
    // 获取当前登录租户ID
    @Override
    public Long getTenantId() {
        return UserContext.getTenantId();
    }
    // 自定义租户字段名(可不写,默认tenant_id)
    @Override
    public String getTenantField() {
        return "tenant_id";
    }
    // 忽略隔离的公共表
    @Override
    public boolean ignoreTable(String tableName) {
        return Arrays.asList("sys_dict","sys_tenant").contains(tableName);
    }
}));

6)游标分页(大数据量高性能分页)

区别于普通 offset 分页,「游标分页适配百万级大数据列表、数据导出场景」,解决传统分页越往后越慢的性能痛点。

优势:

  • 无需 count 统计总数,性能极高
  • 基于主键游标滚动查询,避免深分页偏移性能衰减
  • 适合批量数据同步、文件导出、数据清洗场景

三、入门快速上手步骤(极简流程)

  1. SpringBoot项目引入mybatis-plus-boot-starter依赖,移除原生MyBatis启动器;

  2. application.yml配置数据库、开启下划线转驼峰、打印SQL日志;

  3. 实体类使用@TableName@TableId@TableField映射数据库字段;

  4. Mapper接口继承BaseMapper,启动类添加@MapperScan扫描Mapper;

  5. Service层实现IService+ServiceImpl分层;

  6. 需要分页、乐观锁则注册对应插件;

  7. 使用LambdaWrapper拼接动态条件,调用内置CRUD方法完成业务。

四、总结

  1. MP核心价值:封装单表通用操作,把开发人员从重复SQL中解放;

  2. 分层CRUD:BaseMapper负责底层DAO,IService适配业务层批量、链式操作;

  3. Wrapper条件构造器优先使用Lambda写法,配合condition动态条件,彻底消灭冗余if,支持复杂and/or括号逻辑;

  4. 分页、逻辑删除、自动填充、乐观锁、主键策略均为内置语法糖(2.4章节),开箱即用;高阶能力(2.5章节)支持SQL监控、数据脱敏、多租户隔离、大数据游标分页、生产防误删防护;

  5. MP不替代MyBatis,复杂多表、统计查询依旧支持自定义XML,两者可共存。

参考文献

1 MyBatis-Plus 官方文档. baomidou.com/

2 MyBatis 官方使用文档. mybatis.org/mybatis-3/

(注:部分内容可能由 AI 生成)

本文使用 markdown.com.cn 排版

相关推荐
用户4603812919411 小时前
终于知道AQS是啥东西了
后端
神奇小汤圆12 小时前
SpringBoot 相关的 Skills 全景指南
后端
神奇小汤圆13 小时前
高性能进程内队列 BufferQueue 1.0 版本发布
后端
爸爸61913 小时前
空数据状态与引导:多维度 UI 提示设计
后端
Reart14 小时前
Leetcode 674.最长连续递增序列 (719)
后端·算法
芒鸽14 小时前
HarmonyOS ArkUI Search 搜索框:联想词、历史记录与热门标签
后端
Reart14 小时前
Leetcode 714.买卖股票的最佳时机含手续费(719)
后端·算法
芒鸽14 小时前
HarmonyOS ArkUI RichEditor 富文本编辑器:内联样式、Span 管理与格式工具栏
后端
花开彼岸天~14 小时前
Router 路由管理:pushUrl、replaceUrl、back 与参数传递
后端