当前位置:

首页 > 编程开发 > Mybatis-Plus多租户插件&属性自动赋值方式

Mybatis-Plus多租户插件&属性自动赋值方式

Mybatis-Plus多租户插件TenantLineInnerInterceptor通过拦截器自动拼接租户字段条件实现数据隔离。其核心TenantLineHandler接口需实现获取租户ID、指定租户字段名(默认tenant_id)及忽略表逻辑。自定义处理器从ThreadLocalUtil获取租户ID,设置字段为archive_company_id,并通过

本文目录

    1、Mybatis-Plus 多租户插件

    TenantLineInnerInterceptor 是 MyBatis-Plus 提供的一个插件,专门用来做多租户的数据隔离。说白了,它就是一道防线——确保每个租户只能看到自己的数据,不会越界。从本质上讲,这其实就是一个拦截器,在执行的 增删改查 语句上自动追加租户字段的条件。

    1.1、属性介绍

    这个拦截器的核心是 tenantLineHandler,一个 TenantLineHandler 接口的实例,它决定了租户逻辑怎么走。

    属性名类型默认值描述
    tenantLineHandlerTenantLineHandler租户处理器( TenantId 行级 )

    TenantLineHandler 接口里定义了几个关键方法:

    public interface TenantLineHandler {
    
        /**
         * 获取租户 ID 值表达式,只支持单个 ID 值
         *
         * @return 租户 ID 值表达式
         */
        Expression getTenantId();
    
        /**
         * 获取租户字段名
         * 默认字段名叫: tenant_id
         *
         * @return 租户字段名
         */
        default String getTenantIdColumn() {
            return "tenant_id";//默认
        }
    
        /**
         * 根据表名判断是否忽略拼接多租户条件
         * 默认都要进行解析并拼接多租户条件
         *
         * @param tableName 表名
         * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
         */
        default boolean ignoreTable(String tableName) {
            return false;
        }
    
        /**
         * 忽略插入租户字段逻辑
         *
         * @param columns        插入字段
         * @param tenantIdColumn 租户 ID 字段
         * @return
         */
        default boolean ignoreInsert(List columns, String tenantIdColumn) {
            return columns.stream().map(Column::getColumnName).anyMatch(i -> i.equalsIgnoreCase(tenantIdColumn));
        }
    }
    

    1.2、使用多租户插件

    比如我们有一张表 biz_archive_common,结构如下:

    -- security_manager.biz_archive_common definition
    CREATE TABLE `biz_archive_common` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '数据主键ID',
      `title_name` varchar(255) DEFAULT NULL COMMENT '题名',
      `secrecy_level_id` bigint(20) DEFAULT NULL COMMENT '密级id',
      `archive_num` varchar(510) DEFAULT NULL COMMENT '档号(照片号)',
      `roll_num` varchar(31) DEFAULT NULL COMMENT '案卷号(册号/带号)',
      `abandon` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否废弃 默认 0false/1true',
      `del` varchar(200) NOT NULL DEFAULT '0' COMMENT '是否删除 默认 0false/1true',
      `create_user` varchar(31) DEFAULT NULL COMMENT '创建者账户',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_user` varchar(31) DEFAULT NULL COMMENT '更新者账户',
      `update_time` datetime DEFAULT NULL COMMENT '更新时间',
      `archive_company_id` bigint(20) DEFAULT NULL COMMENT '全宗单位ID',
      PRIMARY KEY (`id`) USING BTREE,
      KEY `indexArchiveCompanyId` (`archive_company_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='档案共有信息表';

    Mybatis-Plus多租户插件&属性自动赋值方式

    先加上 ma ven 依赖和 yml 配置,这些是基础,不细说了。

    ma ven

    
            
                org.springframework.boot
                spring-boot-starter-web
            
            
            
                com.baomidou
                mybatis-plus-spring-boot3-starter
                3.5.6
            
            
                com.mysql
                mysql-connector-j
                runtime
            
            
                com.alibaba
                druid-spring-boot-starter
                1.2.8
            
            
                org.springframework.boot
                spring-boot-starter-test
                test
            
            
            
                org.projectlombok
                lombok
            
        

    yml

    server:
      port: 8001
      #address: 127.0.0.1
    #spring数据源配置
    spring:
      application:
        name: token #项目名
      # 数据源
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/security_manager?serverTimezone=GMT%2B8&useUnicode=true&useSSL=false&characterEncoding=utf-8
        username: root
        password: root
        druid:
          initial-size: 20
          min-idle: 20
          max-active: 100
          max-wait: 10000
          time-between-eviction-0runs-millis: 60000
          min-evictable-idle-time-millis: 30000
          validation-query: SELECT 1 FROM DUAL
          test-while-idle: true
          test-on-borrow: true
          test-on-return: true
    # mybatis-plus配置
    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
      configuration:
        map-underscore-to-camel-case: true # 数据库下划线自动转驼峰标示关闭
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志配置
      mapper-locations: classpath*:/mapper/**/*.xml

    然后需要一个线程级别的工具类来传递租户 ID,这里用 ThreadLocalUtil。

    package cn.js.util;
    
    import ja va.util.HashMap;
    import ja va.util.Map;
    
    /**
     * Description:
     *
     * @Author Js
     * @Create 2024-11-17 14:12
     * @Version 1.0
     */
    public class ThreadLocalUtil {
    
    
        //1 初始化TreadLocal
        private static ThreadLocal> RES = new ThreadLocal>() {
    
            /**
             * 和继承ThreadLocal 类一样,也是一个方法的复写
             */
            protected Map initialValue() {
    
    
                return new HashMap();
            }
    
            ;
        };
    
        /*
         * 给线程里面设置一个值
         */
        public static void set(String name, Object object) {
    
            Map map = RES.get(); // 取出来的map 集合位null
            map.put(name, object);
        }
    
        /**
         * 从线程里面取值
         */
        public static Object get(String name) {
    
            Map map = RES.get();
            if (!map.containsKey(name)) {
    
    
                return null;
            }
            return map.get(name);
        }
    
        /**
         * 清空线程的值
         */
        public static void clear() {
            Map map = RES.get();
            map.clear();
            map = null; // jvm 自动回收
    
        }
    
    
    
    }
    
    

    关键部分来了:实现租户处理器并注入到拦截器里。注意这里的 getTenantId() 是从 ThreadLocalUtil 中取的租户ID,然后通过 LongValue 封装。同时指定了租户字段名为 archive_company_id,并且通过 ignoreTable 方法让某些表跳过租户条件。

    package cn.js.config;
    
    import cn.js.util.ThreadLocalUtil;
    import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
    import net.sf.jsqlparser.expression.Expression;
    import net.sf.jsqlparser.expression.LongValue;
    import net.sf.jsqlparser.schema.Column;
    import org.springframework.boot.autoconfigure.AutoConfigureBefore;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import ja va.util.ArrayList;
    import ja va.util.List;
    
    /**
     * @Author Js
     * @Description
     * @Date 2024-11-15 21:34
     * @Version 1.0
     **/
    @Configuration
    @AutoConfigureBefore(MybatisPlusAutoConfiguration.class)
    public class PaginationInterceptorConfig {
        @Bean
        public MybatisPlusInterceptor addMybatisPlusInterceptor(){
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler()));
            return interceptor;
        }
        
    
        private class TenantLineHandler implements com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler{
    
            /**
             * 获取当前租户 ID。
             */
            @Override
            public Expression getTenantId() {
                Object id = ThreadLocalUtil.get("id");
                Long tenantId=Long.valueOf(String.valueOf(id));
                // 返回租户ID的表达式,LongValue 是 JSQLParser 中表示 bigint 类型的 class
                return new LongValue(tenantId);
            }
    
    
            /**
             * 表结构中那个字段用于拼接多租户条件
             */
            @Override
            public String getTenantIdColumn() {
                return "archive_company_id";
            }
    
    
            /**
             * 默认返回false:表示所有表都需要拼接多租户条件
             * tableName:表名称
             */
            @Override
            public boolean ignoreTable(String tableName) {
                //如果那些表不需要拼接多租户条件,
                List tableList = new ArrayList<>();
                tableList.add("sql_version");
                tableList.add("User");
                tableList.add("Kf");
                if(tableList.contains(tableName)){
                    //如果不需要添加的表名称在list中,就返回false,不用拼接租户条件
                    return true;
    
                }
                return false;
            }
    
    
            /**
             * 获取租户 ID 字段名。
             */
            @Override
            public boolean ignoreInsert(List columns, String tenantIdColumn) {
                return com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler.super.ignoreInsert(columns, tenantIdColumn);
            }
        }
    
    }
    
    

    写个简单的 Controller 测试一下,设置 ThreadLocalUtil.set("id",1645),然后查询全部数据:

    package cn.js.controller;
    
    import cn.js.domain.BizArchiveCommon;
    import cn.js.service.BizArchiveCommonService;
    import cn.js.util.ThreadLocalUtil;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import ja vax.annotation.Resource;
    import ja va.util.List;
    
    /**
     * @Author Js
     * @Description
     * @Date 2024-11-15 21:26
     * @Version 1.0
     **/
    @RequestMapping("/common")
    @RestController
    public class BizArchiveCommonController {
        @Resource
        private BizArchiveCommonService bizArchiveCommonService;
    
        @GetMapping("/getAll")
        public List getAll() {
    
            ThreadLocalUtil.set("id",1645);
            List archiveCommons = bizArchiveCommonService.list();
            return archiveCommons;
        }
    
    }
    
    

    对应的 domain、service、mapper 也比较常规:

    domain
    package cn.js.domain;
    
    import com.baomidou.mybatisplus.annotation.FieldFill;
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableLogic;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    
    import ja va.time.LocalDateTime;
    
    /**
     * @Author Js
     * @Description
     * @Date 2024-11-15 21:22
     * @Version 1.0
     **/
    @Data
    @TableName(value="biz_archive_common")
    public class BizArchiveCommon {
    
        private Long id;
        private String titleName;
        private Long secrecyLevelId;
        private String archiveNum;
        private String rollNum;
        @TableLogic(value = "false", delval = "true")
        private Boolean abandon;
        /**
         * 创建人
         */
        @TableField(fill = FieldFill.INSERT)
        private String createUser;
        /**
         * 创建时间
         */
        @TableField(fill = FieldFill.INSERT)
        private LocalDateTime createTime;
        /**
         * 修改人
         */
        @TableField(fill = FieldFill.UPDATE)
        private String updateUser;
        /**
         * 修改时间
         */
        @TableField(fill = FieldFill.UPDATE)
        private LocalDateTime updateTime;
        /**
         * 是否逻辑删除,true:删除 false:未删除
         */
        @TableLogic(value = "false", delval = "true")
        private Boolean del;
        /**
         * 全宗单位id
         */
        private Long archiveCompanyId;
    
    }
    
    
    service & ServiceImpl
    package cn.js.service;
    
    import cn.js.domain.BizArchiveCommon;
    import com.baomidou.mybatisplus.extension.service.IService;
    
    public interface BizArchiveCommonService extends IService {
    }
    
    
    
    package cn.js.service.impl;
    
    import cn.js.domain.BizArchiveCommon;
    import cn.js.mapper.BizArchiveCommonMapper;
    import cn.js.service.BizArchiveCommonService;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import org.springframework.stereotype.Service;
    
    /**
     * @Author Js
     * @Description
     * @Date 2024-11-15 21:27
     * @Version 1.0
     **/
    @Service
    public class BizArchiveCommonServiceImpl extends ServiceImpl implements  BizArchiveCommonService {
    }
    
    
    
    
    mapper
    package cn.js.mapper;
    
    import cn.js.domain.BizArchiveCommon;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import org.apache.ibatis.annotations.Mapper;
    
    
    
    @Mapper
    public interface BizArchiveCommonMapper extends BaseMapper {
    }
    
    

    运行日志可以清楚地看到,SELECT 语句自动加上了 WHERE archive_company_id = 1645:

    JDBC Connection [HikariProxyConnection@639980080 wrapping com.mysql.cj.jdbc.ConnectionImpl@7c12090] will not be managed by Spring
    ==>  Preparing: SELECT id, title_name, secrecy_level_id, archive_num, roll_num, abandon, del, create_user, create_time, update_user, update_time, archive_company_id FROM biz_archive_common WHERE archive_company_id = 1645
    ==> Parameters: 
    <==      Total: 0
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38e12bd4]
    

    Mybatis-Plus多租户插件&属性自动赋值方式

    那如果用自定义的 mapper.xml 呢?也完全没问题——只要表名没有被 ignoreTable 排除,插件照样会追加租户条件。

    
    
    
        
    
    @RequestMapping("/common")
    @RestController
    public class BizArchiveCommonController {
        @Resource
        private BizArchiveCommonService bizArchiveCommonService;
    
       
        @GetMapping("/getOne")
        public BizArchiveCommon getOnes() {
    
            ThreadLocalUtil.set("id",1645);
            BizArchiveCommon archiveCommon = bizArchiveCommonService.getones();
            return archiveCommon;
        }
    
    }
    
    

    日志:

    JDBC Connection [HikariProxyConnection@1523711906 wrapping com.mysql.cj.jdbc.ConnectionImpl@6db5719b] will not be managed by Spring
    ==>  Preparing: SELECT * FROM biz_archive_common WHERE id = 1 AND archive_company_id = 1645
    ==> Parameters: 
    <==      Total: 0
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@70ef05c0]
    

    Mybatis-Plus多租户插件&属性自动赋值方式

    1.3、不使用多租户插件

    有时候某个 SQL 确实不需要租户条件,比如跨租户统计。这时可以在对应的 Mapper 方法上加注解 @InterceptorIgnore(tenantLine = "true"),让插件对该方法放行。

    package cn.js.mapper;
    
    import cn.js.domain.BizArchiveCommon;
    import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import org.apache.ibatis.annotations.Mapper;
    
    
    
    @Mapper
    public interface BizArchiveCommonMapper extends BaseMapper {
        @InterceptorIgnore(tenantLine = "true")
        BizArchiveCommon ones();
    }
    
    

    Mybatis-Plus多租户插件&属性自动赋值方式

    2、实体对象的属性自动赋值

    Mybatis-Plus多租户插件&属性自动赋值方式

    很多表都有 create_time、create_user、update_time、update_user 这类公共字段。如果能自动填充,就不需要在每次新增或修改时手动赋值了。MyBatis-Plus 的自动填充功能正好解决这个问题。

    使用

    1. 定义实体类

    在实体类的字段上添加 @TableField 注解,通过 fill 属性指定填充策略:

    public class User {
        @TableField(fill = FieldFill.INSERT)
        private String createTime;
    
        @TableField(fill = FieldFill.UPDATE)
        private String updateTime;
    
        // 其他字段...
    }
    

    2. 实现 MetaObjectHandler

    创建一个类实现 MetaObjectHandler 接口,重写 insertFill 和 updateFill 方法,在里面设置具体值:

    package cn.js.config;
    
    import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.reflection.MetaObject;
    import org.springframework.stereotype.Component;
    
    import ja va.time.LocalDateTime;
    
    /**
     * Description:
     *
     * @Author Js
     * @Create 2024-11-17 15:39
     * @Version 1.0
     */
    @Component
    @Slf4j
    public class MetaObjectHandlerConfig implements MetaObjectHandler {
        @Override
        public void insertFill(MetaObject metaObject) {
           log.info("开始插入填充...");
           this.strictInsertFill(metaObject,"createTime", LocalDateTime.class,LocalDateTime.now());
           this.strictInsertFill(metaObject,"createUser", String.class,"张三");
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("开始更新填充...");
            this.strictUpdateFill(metaObject,"updateTime", LocalDateTime.class,LocalDateTime.now());
            this.strictUpdateFill(metaObject,"updateUser", String.class,"王五");
    
        }
    }
    
    

    3. 配置自动填充处理器

    确保这个实现类被 Spring 管理(加了 @Component 就行了),MyBatis-Plus 会自动发现它。

    注意事项

    • 自动填充是直接给实体类的属性设置值,如果属性本身已经有值,默认填充策略不会覆盖。
    • 如果没填,入库时就是 null,所以最好搭配数据库的默认值或非空约束。
    • 字段必须声明 @TableField(fill = ...) 注解,否则填充器不会处理。
    • strictInsertFill 和 strictUpdateFill 会根据字段名、类型区分,推荐使用;如果不需区分,可以用 fillStrategy 统一处理。
    • 在 update(T entity, Wrapper updateWrapper) 时,entity 不能为空,否则填充会失效;而 update(Wrapper updateWrapper) 根本不会触发自动填充,需要手动处理。

    4. 测试

    package cn.js.controller;
    
    import cn.js.domain.BizArchiveCommon;
    import cn.js.service.BizArchiveCommonService;
    import cn.js.util.ThreadLocalUtil;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import ja vax.annotation.Resource;
    import ja va.util.List;
    
    /**
     * @Author Js
     * @Description
     * @Date 2024-11-15 21:26
     * @Version 1.0
     **/
    @RequestMapping("/common")
    @RestController
    public class BizArchiveCommonController {
        @Resource
        private BizArchiveCommonService bizArchiveCommonService;
    
      
    
        @GetMapping("/sa ve")
        public Boolean sa ve() {
    
            ThreadLocalUtil.set("id",1645);
            BizArchiveCommon bizArchiveCommon = new BizArchiveCommon();
            bizArchiveCommon.setTitleName("这是新增的");
            bizArchiveCommon.setSecrecyLevelId(123L);
            bizArchiveCommon.setArchiveNum("8080-25201-38245");
            bizArchiveCommon.setRollNum("25201");
            bizArchiveCommon.setAbandon(false);
            boolean archiveCommon = bizArchiveCommonService.sa ve(bizArchiveCommon);
            return archiveCommon;
        }
    
    
    
        @GetMapping("/update")
        public Boolean update() {
    
            ThreadLocalUtil.set("id",1645);
            BizArchiveCommon bizArchiveCommon = new BizArchiveCommon();
            bizArchiveCommon.setId(1L);
            bizArchiveCommon.setTitleName("这是新增的,进行修改!");
            bizArchiveCommon.setSecrecyLevelId(123L);
            bizArchiveCommon.setArchiveNum("8080-25201-38245");
            bizArchiveCommon.setRollNum("25201");
            bizArchiveCommon.setAbandon(false);
            boolean b = bizArchiveCommonService.updateById(bizArchiveCommon);
            return b;
        }
    
    }
    
    

    新增时的日志:

    JDBC Connection [HikariProxyConnection@1186756471 wrapping com.mysql.cj.jdbc.ConnectionImpl@22ef7a2e] will not be managed by Spring
    ==>  Preparing: INSERT INTO biz_archive_common (id, title_name, secrecy_level_id, archive_num, roll_num, abandon, create_user, create_time, archive_company_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1645)
    ==> Parameters: 1858059911531872257(Long), 这是新增的(String), 123(Long), 8080-25201-38245(String), 25201(String), false(Boolean), 张三(String), 2024-11-17T16:09:38.650358300(LocalDateTime)
    <==    Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b266740]
    

    Mybatis-Plus多租户插件&属性自动赋值方式

    修改时的日志:

    JDBC Connection [HikariProxyConnection@724111921 wrapping com.mysql.cj.jdbc.ConnectionImpl@5db6c17a] will not be managed by Spring
    ==>  Preparing: UPDATE biz_archive_common SET title_name = ?, secrecy_level_id = ?, archive_num = ?, roll_num = ?, abandon = ?, update_user = ?, update_time = ? WHERE id = ? AND del = false AND archive_company_id = 1645
    ==> Parameters: 这是新增的,进行修改!(String), 123(Long), 8080-25201-38245(String), 25201(String), false(Boolean), 王五(String), 2024-11-17T16:22:59.641094300(LocalDateTime), 1(Long)
    <==    Updates: 1
    Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5390448d]
    

    Mybatis-Plus多租户插件&属性自动赋值方式

    总结

    从上面的示例可以看出,多租户插件和自动填充功能组合使用,能让数据隔离和公共字段处理变得非常优雅。核心思路就是:先通过拦截器自动拼上租户条件,再通过填充器自动注入创建/修改信息,业务代码里只需关注业务逻辑本身。当然,实际项目里还可以结合 ThreadLocal 或请求上下文来动态传递租户 ID,这里的实现只是一个简化版本,供大家参考。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发
    相关文章 更多
    联想 Tab Plus Gen 2 平板海外开售:12.1 英寸屏幕、JBL 九单元扬声器
    联想 Tab Plus Gen 2 平板海外开售:12.1 英寸屏幕、JBL 九单元扬声器

    联想TabPlusGen2海外上市,12.1英寸2.5K120HzIPS屏,天玑7400芯片,后盖集成JBL九单元扬声器及杜比全景声,圆形支架可自立,内置10200mAh电池+45W快充,预装Android16,8+128GB约3367元。

    酷态科6月24日推出神秘新品,有望为“CP模块风扇Plus”底座
    酷态科6月24日推出神秘新品,有望为“CP模块风扇Plus”底座

    酷态科6月24日将推出CP模块风扇Plus磁吸底座,通过Power-Pin接口使风扇支持充放电。风扇搭载30000RPM无刷电机,风速11m/s,内置双5000mAh电池,续航37小时,支持33W快充。

    酷态科官宣 6 月 24 日推出神秘新品,有望为“CP 模块风扇 Plus”桌面底座
    酷态科官宣 6 月 24 日推出神秘新品,有望为“CP 模块风扇 Plus”桌面底座

    酷态科6月24日将推出神秘新品,有望为CP模块风扇Plus的桌面底座。该风扇采用30000RPM电机,风速达11m/s,内置两块5000mAh电池,续航37小时,支持充放电及主流快充协议。新品或使风扇兼具吹风与充电功能。

    首创“证据锚定”技术,百川智能发布 M3 Plus
    首创“证据锚定”技术,百川智能发布 M3 Plus

    百川智能发布医疗大模型Baichuan-M3Plus,事实性幻觉率降至2.6%,创下全球最低纪录。首创“证据锚定”技术,实现医学结论可验证、可追溯。多项国际评测中超越GPT-5.2等领先模型,API调用成本较前代下降70%,性能表现卓越,堪称行业里程碑。

    联想Tab Plus 2代发布:9扬声器+10200mAh大电池
    联想Tab Plus 2代发布:9扬声器+10200mAh大电池

    联想TabPlus2代在韩国发布,搭载联发科天玑7400芯片,12.1英寸120Hz600尼特屏幕,9个JBL扬声器支持杜比全景声和蓝牙音箱模式。内置10200mAh电池,预装安卓16,承诺升级至安卓18。配备联想AI实时转写、智能阅读和AI笔记等功能。

    戴尔推出2026款灵越Plus 16英寸笔记本,7004元起
    戴尔推出2026款灵越Plus 16英寸笔记本,7004元起

    全新戴尔2026款灵越Plus16英寸笔记本正式发布,其搭载酷睿Ultra5-225U处理器,性能强劲,起售价为7004元,可享受国家补贴后最低仅需5698元。机身重1.94千克,配备1920×1200分辨率60HzIPS屏幕,接口含雷电4、HDMI2.1和RJ45网口,且扩展性强,非常适合办公与学习。

    券后184.3元,酷态科CP模块风扇Plus曜石黑配色开售
    券后184.3元,酷态科CP模块风扇Plus曜石黑配色开售

    酷态科今天正式开售了CP模块风扇Plus的「曜石黑」版本,定价199元。电商平台上的券后价格大概在184.3元,有兴趣的可以留意一下。 这款新品最大的亮点就是模块化可扩展设计,内部搭载一颗30000 RPM的新一代高速三相无刷电机,技术原理和高速吹风机用的电机类似。实测风速最高能到11m/s,配合T

    券后 184.3 元,酷态科 CP 模块风扇 Plus 曜石黑配色开售
    券后 184.3 元,酷态科 CP 模块风扇 Plus 曜石黑配色开售

    酷态科CP模块风扇Plus曜石黑配色正式开售,到手价仅184.3元。搭载30000RPM高速三相无刷电机与涡流增压技术,最大风速可达11m/s。内置两块5000mAh电池,续航时间长达37小时,支持33W输出及多种快充协议,还可当作移动电源使用。

    中兴畅行70 Plus手机发售:展锐T9100 + 8G + 256G售2099元
    中兴畅行70 Plus手机发售:展锐T9100 + 8G + 256G售2099元

    中兴畅行70Plus已正式发售,定价为两千零九十九元,搭载了展锐T9100处理器、8GB运存与256GB存储。配备了6.75英寸120Hz高刷新率LCD打孔屏,内置了6050mAh大容量电池并支持45W快充,后置了一千三百万像素主摄,还提供了AI键、红外遥控及无网通信功能。

    如何使用SQLPlus进行数据库管理
    如何使用SQLPlus进行数据库管理

    SQLPlus 是 Oracle 数据库的一个命令行工具,用于执行 SQL 查询、PL/SQL 程序以及管理数据库。以下是一些基本的 SQLPlus 命令和用法,用于数据库管理:开启SQLPlus:打开命令行界面(在Windows系统上为CMD或PowerShell,在Unix/Linux系统上为T

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。