你知道MyBaits-Plus有哪些plus高级功能”玩法“吗?

简介: MyBaits-Plus提供的一些高级扩展功能,在日常开发中比较实用,都有相应的场景去使用,提高代码高效性,防止重复编码。

今天接着之前总结的入门教程分析:MyBatis-Plus最详细的入门教程,首先还是同样地需要准备一张表tb_user:

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_no` varchar(255) NOT NULL COMMENT '编号',
  `nickname` varchar(255) DEFAULT NULL COMMENT '昵称',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(255) NOT NULL COMMENT '手机号',
  `gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别  0:男生   1:女生',
  `birthday` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '出生日期',
  `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标志 0:否  1:是',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `address` varchar(1024) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

在项目服务中对应的实体类User:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tb_user")
public class User {
   
    @TableId(type = IdType.AUTO)
    private Long id;
    private String userNo;
    private String nickname;
    private String email;
    private String phone;
    private Integer gender;
    private Date birthday;
    private Integer isDelete;
    private Date createTime;
    private Date updateTime;
}

下面高级功能的示例都是基于上面的表进行展开的。

1.批量插入

我们在日常开发中知道大批量插入数据可能造成性能瓶颈,所以需要格外关注。在之前的入门教程讲过mp(MyBatis-Plus简称,下文都用简称)对数据库的CRUD操作提供了service层和mapper层的接口方法封装,两者的一大区别就是service CRUD接口提供了批量保存的操作,下面就分别来看看批量保存1000条user数据,然后对执行时间进行统计对比和性能评估,这里我连接的数据库是4核8G10M轻量服务器部署的,不同的数据库服务环境配置,执行效率是不一样的,所以下面的执行时间仅供对比参考。

mapper层的CRUD接口

    /**
     * mapper层的crud接口方法批量插入
     */
    @Test
    public void testMapperBatchAdd() {
   
        List<User> users = new ArrayList<>();
        for(long i = 1; i <= 1000; i++) {
   
            User user = User.builder()
                    .id(i)
                    .userNo("No-" + i)
                    .nickname("哈哈")
                    .phone("12345678901")
                    .email("shepherd_123@qq.com")
                    .birthday(new Date())
                    .gender(0)
                    .isDelete(0)
                    .build();
            users.add(user);
        }
        long start = System.currentTimeMillis();
        users.forEach(user -> {
   
            userDAO.insert(user);
        });
        long end = System.currentTimeMillis();
        System.out.println("执行时长:" + (end-start) + "毫秒");
    }

控制台输出如下:

执行时长:42516毫秒

service层CRUD接口

    /**
     * service层的crud接口方法批量插入
     */
    @Test
    public void testServiceBatchAdd() {
   
        List<User> users = new ArrayList<>();
        for(long i = 1; i <= 1000; i++) {
   
            User user = User.builder()
                    .id(i)
                    .userNo("No-" + i)
                    .nickname("哈哈")
                    .phone("12345678901")
                    .email("shepherd_123@qq.com")
                    .birthday(new Date())
                    .gender(0)
                    .isDelete(0)
                    .build();
            users.add(user);
        }
        long start = System.currentTimeMillis();
        userService.saveBatch(users);
        long end = System.currentTimeMillis();
        System.out.println("执行时长:" + (end-start) + "毫秒");
    }

执行结果:

执行时长:19385毫秒

可以看出,使用service层提供的批量保存接口userService.saveBatch(users)虽然快了很多,却仍需要19s,耗时比起mapper层的一条一条地插入快了23s,但是对于服务接口响应来说还是不可接受的。那为什么批量插入保存还是比较慢呢?

MySQL 的 JDBC 连接的 url 中要加 rewriteBatchedStatements 参数,并保证 5.1.13 以上版本的驱动,才能实现高性能的批量插入。

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://ip:3306/db_test?&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true

再次执行上面的service层crud接口批量插入方法,执行结果如下:

执行时长:1364毫秒

可以看到 jdbcurl 添加了 rewriteBatchedStatements=true 参数后,批量操作的执行耗时已经只有 1364 毫秒快的飞起,具体缘由分析请看:https://juejin.cn/post/7295688187752562751

2.逻辑删除

现今互联网系统数据安全越发重要,逻辑删表是指在删除表中数据时,并不是直接将数据从表中删除,而是将数据的状态标记为已删除。这种方式被称为逻辑删除,与之相对的是物理删除。逻辑删除可以保留数据的完整性,同时也方便数据恢复。

添加配置如下:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDelete # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

测试样例:

    /**
     * 逻辑删除
     */
    @Test
    public void testLogicDelete() {
   
        userDAO.deleteById(1L);
    }

控制台输出如下:

2023-11-16 17:01:47.656 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : ==>  Preparing: UPDATE tb_user SET is_delete=1 WHERE id=? AND is_delete=0
2023-11-16 17:01:47.702 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : ==> Parameters: 1(Long)
2023-11-16 17:01:47.744 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : <==    Updates: 1

从日志可以看出做了更新,这就是逻辑删除,接下来我们看看查询:

   @Test
    public void testQuery() {
        User user = userDAO.selectById(1L);
    }
2023-11-16 17:06:19.802 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,create_time,update_time FROM tb_user WHERE id=? AND is_delete=0
2023-11-16 17:06:19.851 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==> Parameters: 1(Long)
2023-11-16 17:06:19.896 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : <==      Total: 0

可以知道加上了未删除标志位这个条件is_delete=0,注意逻辑删除会造成唯一索引冲突

建立唯一索引时,需要额外增加 delete_time 字段,添加到唯一索引字段中,避免唯一索引冲突。例如说,tb_user 使用 user_no 作为唯一索引:

  • 未添加前:先逻辑删除了一条 user_no = 001 的记录,然后又插入了一条 user_no = 001 的记录时,会报索引冲突的异常。
  • 已添加后:先逻辑删除了一条 user_no = 001 的记录并更新 delete_time 为当前时间,然后又插入一条 user_no = 001 并且 delete_time 为 0 的记录,不会导致唯一索引冲突。

3.默认字段填充

按照一般设计规范,数据库表都有这几个字段创建时间(create_time)、更新时间(update_time)、创建人(create_by)、更新人(update_by),所以我抽取一个基础的DO类:BaseDO

/**
 * 数据库表字段公共属性抽象类
 */
@Data
public class BaseDO implements Serializable {
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    /**
     * 最后更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
    /**
     * 创建者
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createBy;
    /**
     * 更新者
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateBy;
    /**
     * 是否删除
     */
//    @TableLogic
//    private Integer deleted;
}

定义一个类实现MetaObjectHandler接口完成自动填充逻辑:DefaultDBFieldHandler

/**
 * 公共字段属性值自动填充
 */
public class DefaultDBFieldHandler implements MetaObjectHandler {
   

    @Override
    public void insertFill(MetaObject metaObject) {
   
        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
   
            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

            Date current = new Date();
            // 创建时间为空,则以当前时间为插入时间
            if (Objects.isNull(baseDO.getCreateTime())) {
   
                baseDO.setCreateTime(current);
            }
            // 更新时间为空,则以当前时间为更新时间
            if (Objects.isNull(baseDO.getUpdateTime())) {
   
                baseDO.setUpdateTime(current);
            }

            baseDO.setCreateBy(1001L);
            baseDO.setUpdateBy(1002L);

//            // 根据登录上下文信息设置创建人和更新人
//            LoginUser currentUser = RequestUserHolder.getCurrentUser();
//            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
//            if (Objects.nonNull(currentUser) && Objects.isNull(baseDO.getCreator())) {
   
//                baseDO.setCreator(currentUser.getId());
//            }
//            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
//            if (Objects.nonNull(currentUser) && Objects.isNull(baseDO.getUpdater())) {
   
//                baseDO.setUpdater(currentUser.getId());
//            }
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
   
        // 更新时间为空,则以当前时间为更新时间
        Object modifyTime = getFieldValByName("updateTime", metaObject);
        if (Objects.isNull(modifyTime)) {
   
            setFieldValByName("updateTime", new Date(), metaObject);
        }
        Object modifier = getFieldValByName("updateBy", metaObject);
        if (Objects.isNull(modifier)) {
   
            setFieldValByName("updateBy", 1002L, metaObject);
        }

//        LoginUser currentUser = RequestUserHolder.getCurrentUser();
//        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
//        if (Objects.nonNull(currentUser) && Objects.isNull(modifier)) {
   
//            setFieldValByName("updater", currentUser.getId(), metaObject);
//        }

    }
}

注入容器:

@Bean
public MetaObjectHandler defaultMetaObjectHandler(){
   
    return new DefaultDBFieldHandler();
}

测试示例:让上面的User类继承BaseDO

  @Test
  public void testAutoFieldFill() {
   
      User user = User.builder()
              .id(1001L)
              .userNo("No-001")
              .nickname("哈哈")
              .phone("12345678901")
              .email("shepherd_123@qq.com")
              .birthday(new Date())
              .gender(0)
              .isDelete(0)
              .build();
      userDAO.insert(user);
  }

上面我们并没有对创建时间,创建人等字段设置值,执行控制台输出如下:

2023-11-16 18:16:41.225 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==>  Preparing: INSERT INTO tb_user ( id, user_no, nickname, email, phone, gender, birthday, is_delete, create_time, update_time, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2023-11-16 18:16:41.267 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==> Parameters: 1001(Long), No-001(String), 哈哈(String), shepherd_123@qq.com(String), 12345678901(String), 0(Integer), 2023-11-16 18:16:40.968(Timestamp), 0(Integer), 2023-11-16 18:16:41.018(Timestamp), 2023-11-16 18:16:41.018(Timestamp), 1001(Long), 1002(Long)
2023-11-16 18:16:41.272 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : <==    Updates: 1

可以看到四个基础字段都进行了值填充。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

4.字段类型处理

我们平时会碰到有些表字段是一个"复杂"字段类型,比如说是个JSON或者数组,这时候需要手动处理之后保存,查询之后也需要自己处理转换,虽然不难但是繁杂,所以mp针对这一情况也做了对应功能,比如每个人都包含一个户籍地址,如果地址对象类是Address,那个user就包含一个address属性:

首先数据库tb_user表添加添加一个地址字段address,存的是JSON字符串,所以长度大一些varchar(1024),接着定义地址类Address

@Data
public class Address {
   
    private Long id;
    private String province;
    private String city;
    private String region;
    private String address;
}

实体类中添加地址address属性:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tb_user", autoResultMap = true)
public class User extends BaseDO {
   

    @TableId(type = IdType.AUTO)
    private Long id;

    private String userNo;

    private String nickname;

    private String email;

    private String phone;

    private Integer gender;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date birthday;

    private Integer isDelete;

    @TableField(typeHandler = JacksonTypeHandler.class)
    private Address address;

}

测试示例:

  @Test
  public void testTypeHandler() {
   
      Address address = new Address();
      address.setProvince("浙江省");
      address.setCity("杭州市");
      address.setRegion("余杭区");
      address.setAddress("城北万象城");
      address.setId(1L);
      User user = User.builder()
              .id(100L)
              .userNo("No-001")
              .nickname("哈哈")
              .phone("12345678901")
              .email("shepherd_123@qq.com")
              .birthday(new Date())
              .gender(0)
              .isDelete(0)
              .address(address)
              .build();
      userDAO.insert(user);
  }
2023-11-16 18:37:58.251 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==>  Preparing: INSERT INTO tb_user ( id, user_no, nickname, email, phone, gender, birthday, is_delete, address, create_time, update_time, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2023-11-16 18:37:58.333 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==> Parameters: 100(Long), No-001(String), 哈哈(String), shepherd_123@qq.com(String), 12345678901(String), 0(Integer), 2023-11-16 18:37:57.997(Timestamp), 0(Integer), {"id":1,"province":"浙江省","city":"杭州市","region":"余杭区","address":"城北万象城"}(String), 2023-11-16 18:37:58.039(Timestamp), 2023-11-16 18:37:58.039(Timestamp), 1001(Long), 1002(Long)
2023-11-16 18:37:58.336 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : <==    Updates: 1

查询示例:

    @Test
    public void testQuery() {
   
        User user = userDAO.selectById(100L);
        System.out.println(user);
    }
2023-11-16 18:42:47.023 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,address,create_time,update_time,create_by,update_by FROM tb_user WHERE id=?
2023-11-16 18:42:47.051 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==> Parameters: 100(Long)
2023-11-16 18:42:47.164 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : <==      Total: 1
User(id=100, userNo=No-001, nickname=哈哈, email=shepherd_123@qq.com, phone=12345678901, gender=0, birthday=Thu Nov 16 18:37:58 CST 2023, isDelete=0, address=Address(id=1, province=浙江省, city=杭州市, region=余杭区, address=城北万象城))

可以看出复杂类型对象字段都自动转换了,注意在实体类上一定要指定@TableName(autoResultMap = true)

之前我们分析过的字段加密存储,也是通过类型转换器实现,详见:Spring Boot如何优雅实现数据加密存储

5.动态表名

业务系统随着使用时间的推移数据量越来越大,所以我们会对数据量大的表进行归档处理,比如说通过日期进行了水平分表,需要通过日期参数,动态的查询数据,Mybatis-plus可以通过此插件解析替换设定表名为处理器的返回表名

插件配置

    @Bean
    public MybatisPlusInterceptor paginationInterceptor() 
    {
   
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //动态表名
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor());
   }

    @Bean
    public DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor() 
    {
   
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor =new DynamicTableNameInnerInterceptor();
        //
        TableNameHandler tableNameHandler=new MyTableNameHandler();
        dynamicTableNameInnerInterceptor.setTableNameHandler(tableNameHandler);
        return dynamicTableNameInnerInterceptor;
    }

动态表名规则处理类

public class MyTableNameHandler implements TableNameHandler
{
   
    private Logger logger = LoggerFactory.getLogger(MyTableNameHandler.class);
    @Override
    public String dynamicTableName(String sql, String tableName)
    {
   
        logger.info("dynamicTableName sql:{},tableName:{}",sql,tableName);

        //如果参数为空,则以不要动态动态查询表名
        Map<String, Object> paramMap = RequestDataHelper.getRequestData();
        if(paramMap==null || paramMap.isEmpty())
        {
   
            logger.info("dynamicTableName paramMap is null");
            return tableName;
        }

        // 获取参数方法
        paramMap.forEach((k, v) -> logger.info(k + "----" + v));

        int random = (int) paramMap.get("tableNo");
        String tableNo = "_1";
        if (random % 2 == 1) 
        {
   
            tableNo = "_2";
        }
        String queryTableName=tableName + tableNo;
        logger.info("---------> queryTableName:{}",queryTableName);
        return queryTableName;
    }
}

参数上下文传递

public class RequestDataHelper
{
   
    /**
     * 请求参数存取
     */
    private static final ThreadLocal<Map<String, Object>> REQUEST_DATA = new ThreadLocal<>();

    /**
     * 设置请求参数
     *
     * @param requestData 请求参数 MAP 对象
     */
    public static void setRequestData(Map<String, Object> requestData) {
   
        REQUEST_DATA.set(requestData);
    }


    /**
     * 获取请求参数
     *
     * @return 请求参数 MAP 对象
     */
    public static Map<String, Object> getRequestData() 
    {
   
        return REQUEST_DATA.get();
    }
}

6.多租户插件

关于多租户的使用场景和功能实现,插件使用,之前我们已经总结分析过了,传送门:浅析SaaS多租户系统数据隔离实现方案

7.总结

以上全部就是mp提供的一些高级扩展功能,在日常开发中比较实用,都有相应的场景去使用,提高代码高效性,防止重复编码。

目录
相关文章
|
1月前
|
数据可视化 大数据 API
低代码可视化开发-uniapp新闻跑马灯组件-代码生成器
低代码可视化开发-uniapp新闻跑马灯组件-代码生成器
82 2
|
3月前
|
小程序 前端开发 API
微信小程序全栈开发中的多端适配与响应式布局是一种高效的开发模式。
探讨小程序全栈开发中的多端适配与响应式布局,旨在实现统一的用户体验。多端适配包括平台和设备适配,确保小程序能在不同环境稳定运行。响应式布局利用媒体查询和弹性布局技术,使界面适应各种屏幕尺寸。实践中需考虑兼容性、性能优化及用户体验,借助跨平台框架如Taro或uni-app可简化开发流程,提升效率。
61 1
|
3月前
|
图形学 C# 开发者
全面掌握Unity游戏开发核心技术:C#脚本编程从入门到精通——详解生命周期方法、事件处理与面向对象设计,助你打造高效稳定的互动娱乐体验
【8月更文挑战第31天】Unity 是一款强大的游戏开发平台,支持多种编程语言,其中 C# 最为常用。本文介绍 C# 在 Unity 中的应用,涵盖脚本生命周期、常用函数、事件处理及面向对象编程等核心概念。通过具体示例,展示如何编写有效的 C# 脚本,包括 Start、Update 和 LateUpdate 等生命周期方法,以及碰撞检测和类继承等高级技巧,帮助开发者掌握 Unity 脚本编程基础,提升游戏开发效率。
79 0
|
3月前
|
SQL Java 关系型数据库
MyBatis-Plus 分页魅力绽放!紧跟技术热点,带你领略数据分页的高效与便捷
【8月更文挑战第29天】在 Java 开发中,数据处理至关重要,尤其在大量数据查询与展示时,分页功能尤为重要。MyBatis-Plus 作为一款强大的持久层框架,提供了便捷高效的分页解决方案。通过封装数据库分页查询语句,开发者能轻松实现分页功能。在实际应用中,只需创建 `Page` 对象并设置页码和每页条数,再通过 `QueryWrapper` 构建查询条件,调用 `selectPage` 方法即可完成分页查询。MyBatis-Plus 不仅生成分页 SQL 语句,还自动处理参数合法性检查,并支持条件查询和排序等功能,极大地提升了系统性能和稳定性。
55 0
|
3月前
|
移动开发 前端开发 JavaScript
UniApp H5项目大揭秘:高效生成与扫描二维码的终极策略,让你的应用脱颖而出!
【8月更文挑战第3天】UniApp让开发者能以Vue.js构建跨平台应用。在H5项目中,通过第三方库如qrcodejs2可轻松生成二维码,代码简洁易集成;或用Canvas API获得更高灵活性。扫描方面,H5+ API适合App环境,而纯H5项目则需前端库加后端服务配合。不同方法各有优势,应按需选择以优化体验。
287 0
|
5月前
|
数据采集 存储 中间件
Scrapy,作为一款强大的Python网络爬虫框架,凭借其高效、灵活、易扩展的特性,深受开发者的喜爱
【6月更文挑战第10天】Scrapy是Python的高效爬虫框架,以其异步处理、多线程及中间件机制提升爬取效率。它提供丰富组件和API,支持灵活的数据抓取、清洗、存储,可扩展到各种数据库。通过自定义组件,Scrapy能适应动态网页和应对反爬策略,同时与数据分析库集成进行复杂分析。但需注意遵守法律法规和道德规范,以合法合规的方式进行爬虫开发。随着技术发展,Scrapy在数据收集领域将持续发挥关键作用。
106 4
|
6月前
|
存储 设计模式
阿里P9大佬分享:如何让代码更加灵活
阿里P9大佬分享:如何让代码更加灵活
54 0
|
6月前
|
JSON 开发者 数据格式
开发者的瑞士军刀:DevToys带你探索更简单、更便捷的开发方式
开发者的瑞士军刀:DevToys带你探索更简单、更便捷的开发方式
97 0
|
6月前
|
数据处理 语音技术
(保姆教程及高级玩法-自定义数据处理)微信同声传译插件-语音识别
(保姆教程及高级玩法-自定义数据处理)微信同声传译插件-语音识别
81 0
|
6月前
|
监控 安全 数据挖掘
分享5款简洁的小工具,助你轻松日常
生活中的小工具,如同隐秘的宝藏,有时能为我们的日常增添一丝轻松和趣味。以下是五款简洁而实用的工具,或许它们能为你的生活带来一些小惊喜。
74 0