当前位置:

首页 > 编程开发 > 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_timecreate_userupdate_timeupdate_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 接口,重写 insertFillupdateFill 方法,在里面设置具体值:

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 = ...) 注解,否则填充器不会处理。
  • strictInsertFillstrictUpdateFill 会根据字段名、类型区分,推荐使用;如果不需区分,可以用 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,这里的实现只是一个简化版本,供大家参考。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
联想 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电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。