MyBatis-Plus数据权限拦截器实现原理与实战指南
1. 从业务需求到技术选型为什么需要数据权限拦截器在任何一个稍具规模的企业级应用中数据权限都是一个绕不开的核心议题。想象一下一个全国性的销售系统华南区的销售总监登录后理应只能看到华南区的销售数据和下属的业绩报表一个多租户的SaaS平台A公司的管理员绝对不能看到B公司的任何客户信息。这种基于用户身份、角色、部门等维度对数据行进行访问控制的需求就是典型的数据权限也称为行级权限。早期我们最朴素的做法是在每一个查询的SQL语句中手动拼接上WHERE条件比如WHERE dept_id #{currentUserDeptId}。这种做法在小型项目或简单场景下尚可应付但随着业务模块激增、权限规则复杂化例如某人可以看到本部门及所有下级部门的数据这种“散弹枪”式的代码会迅速演变成一场维护灾难。每个Mapper方法都要处理权限逻辑代码重复度高极易遗漏一旦权限规则变更就需要进行全局搜索和修改风险巨大。这时一个集中、统一、非侵入式的数据权限解决方案就显得尤为迫切。我们需要一个机制能在SQL执行前自动、智能地根据当前用户的上下文为查询语句注入相应的过滤条件。这就是DataPermissionInterceptor数据权限拦截器诞生的背景。在MyBatis-Plus简称MP的生态中它作为InnerInterceptor接口的一个实现为我们提供了实现这一愿景的优雅途径。它并非MP开箱即用的功能而是需要我们自己基于其拦截器体系进行扩展实现的核心组件其价值在于将业务逻辑数据权限规则与数据访问逻辑SQL执行解耦让开发者能更专注于业务本身。2. 核心架构解析DataPermissionInterceptor如何工作要理解如何实现必须先吃透它的工作原理。DataPermissionInterceptor的本质是MyBatis插件Plugin机制的一种应用更具体地说它是MP对MyBatis插件的一层高级封装。其核心工作流程可以概括为“拦截、分析、改写、放行”四个步骤。2.1 拦截器链与执行时机MyBatis允许我们在映射语句执行过程中的某一点进行拦截。MP的InnerInterceptor主要作用于Executor执行器层面。DataPermissionInterceptor通常会在Executor#query方法被执行前进行拦截。此时MP已经完成了SQL的初步解析生成了一个包含原始SQL语句、参数映射等信息的MappedStatement对象以及包含了分页参数等信息的BoundSql对象。我们的拦截器就挂载在这个执行链上。当一条查询请求到来时执行链会依次经过所有已注册的拦截器。DataPermissionInterceptor需要判断当前执行的SQL是否需要添加数据权限过滤。这个判断依据可能来自于方法上的注解如InterceptorIgnore、当前线程绑定的用户上下文、或是SQL语句本身例如某些特定的表或操作不需要权限控制。2.2 SQL解析与条件注入这是最核心的技术环节。拦截器获取到原始的、待执行的SQL字符串后不能简单地用字符串拼接WHERE条件那样极易造成SQL语法错误例如原SQL没有WHERE关键字或有GROUP BY、ORDER BY等子句。一个健壮的实现需要依赖SQL解析器。我们可以使用像jsqlparser这样的第三方库它能将SQL字符串解析成一棵抽象语法树AST。通过对AST的遍历和修改我们可以精准地在合适的位置通常是查询语句的WHERE子句插入新的条件表达式。例如原始SQL是SELECT id, name, amount FROM sales_order经过解析和权限规则计算假设当前用户只能查看dept_id5的数据拦截器需要将其改写为SELECT id, name, amount FROM sales_order WHERE dept_id 5如果原SQL已有WHERE条件如WHERE status ACTIVE则需要将其合并为SELECT id, name, amount FROM sales_order WHERE status ACTIVE AND dept_id 5这个过程必须处理好运算符优先级和括号确保生成的SQL语义正确。jsqlparser提供了CCJSqlParserUtil和StatementVisitor等工具让我们可以相对方便地实现AST的修改。2.3 权限规则引擎与上下文传递WHERE dept_id 5中的“5”这个值从何而来这依赖于一个独立的权限规则引擎或称为DataPermissionHandler。这个处理器是业务逻辑的核心它根据当前登录用户的身份信息从ThreadLocal或安全框架如Spring Security的SecurityContextHolder中获取计算出一组应用于当前查询的过滤规则。规则可能是简单的等值条件dept_id ?也可能是复杂的范围条件dept_id IN (?,?,?)或dept_path LIKE ?甚至是动态的多表关联子查询。DataPermissionHandler的接口设计应足够灵活能够返回一个或多个可被拼接到SQL中的表达式片段。上下文传递是关键。用户信息必须在请求进入时例如通过过滤器或拦截器被安全地绑定到当前线程确保在DataPermissionInterceptor执行时能够准确获取。同时也要考虑异步线程池场景下线程上下文丢失的问题。3. 手把手实现构建你的DataPermissionInterceptor理论清晰后我们进入实战环节。下面我将以一个经典的“基于部门的数据权限”场景为例分步骤实现一个完整的方案。3.1 环境准备与依赖引入首先确保你的项目已引入MyBatis-Plus依赖。这里以Spring Boot项目为例在pom.xml中需要dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version !-- 请使用最新稳定版 -- /dependency !-- SQL解析器 -- dependency groupIdcom.github.jsqlparser/groupId artifactIdjsqlparser/artifactId version4.5/version /dependency3.2 定义数据权限注解与忽略注解为了灵活控制我们需要定义两个注解。1. 数据权限注解 (DataPermission): 用于在Mapper方法或Mapper接口上声明此方法需要应用的数据权限规则。可以通过注解属性来指定规则类型或参数。Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataPermission { /** * 权限类型可用于区分不同的规则处理器 */ String type() default dept; /** * 关联的表字段名默认为dept_id */ String field() default dept_id; }2. 拦截器忽略注解 (InterceptorIgnore): MP本身提供了这个注解我们直接使用即可。它可以用于忽略MP的内置拦截器如分页、租户我们也可以约定用它来忽略我们自定义的数据权限拦截器。将其放在方法上表示该方法不进行数据权限过滤。// 直接使用MyBatis-Plus提供的注解 import com.baomidou.mybatisplus.annotation.InterceptorIgnore; Mapper public interface UserMapper extends BaseMapperUser { InterceptorIgnore(dataPermission true) // 假设我们约定这个属性用于忽略数据权限 ListUser selectAllWithoutPermission(); }3.3 实现权限规则处理器 (DataPermissionHandler)这是业务逻辑的承载者。我们定义一个接口和实现类。public interface IDataPermissionHandler { /** * 获取当前用户的数据权限SQL表达式片段 * param mappedStatementId 执行的Mapper方法全限定名 * param tableName 表名可能为空需解析 * return SQL WHERE条件片段例如 dept_id 5 或 dept_id IN (1,2,3)。返回null或空字符串表示无权限限制。 */ String getPermissionSql(String mappedStatementId, String tableName); } Component public class DeptDataPermissionHandler implements IDataPermissionHandler { Override public String getPermissionSql(String mappedStatementId, String tableName) { // 1. 获取当前用户上下文这里从ThreadLocal中获取实际项目可能结合Spring Security UserContext currentUser UserContextHolder.get(); if (currentUser null || currentUser.isAdmin()) { // 超级管理员或未登录情况返回null表示不加任何限制 return null; } // 2. 根据用户信息计算权限范围 // 例如用户所在部门ID是5并且可以查看下级部门部门路径为 1.2.5. Long deptId currentUser.getDeptId(); String deptPath currentUser.getDeptPath(); // 假设为 1.2.5. // 3. 构建SQL片段 // 场景A仅查看本部门 // return String.format(%s.dept_id %d, tableName, deptId); // 场景B查看本部门及所有下级部门假设有dept_path字段存储路径 // 需要确保表中有 dept_path 字段且索引设计合理 // return String.format(%s.dept_path LIKE %s%%, tableName, deptPath); // 场景C更复杂的通过关联表查询 // 例如权限存储在独立的 user_data_scope 表这里返回一个子查询 // return String.format(%s.dept_id IN (SELECT dept_id FROM user_data_scope WHERE user_id %d), tableName, currentUser.getId()); // 本例采用场景A // 注意tableName可能为空需要处理。一种做法是在拦截器中解析出表名再传入。 // 这里简单返回一个带占位符的字段条件实际字段名可能通过注解传递。 return dept_id deptId; } }注意这里的UserContextHolder是一个自定义的、基于ThreadLocal的工具类用于在同一个线程内传递用户信息。你必须在请求入口如HandlerInterceptor或Filter中设置用户信息。3.4 实现核心拦截器 (DataPermissionInterceptor)现在实现最核心的拦截器类。它需要实现MP的InnerInterceptor接口。Component Intercepts({Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class DataPermissionInterceptor implements InnerInterceptor { Autowired private IDataPermissionHandler dataPermissionHandler; Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 1. 判断是否需要忽略数据权限 if (shouldIgnorePermission(ms)) { return; } // 2. 获取原始SQL String originalSql boundSql.getSql(); if (StringUtils.isBlank(originalSql)) { return; } // 3. 获取当前方法需要的数据权限SQL片段 String permissionSql dataPermissionHandler.getPermissionSql(ms.getId(), getTableName(originalSql)); if (StringUtils.isBlank(permissionSql)) { return; // 无需添加权限条件 } // 4. 使用jsqlparser解析并改写SQL String finalSql rewriteSqlWithPermission(originalSql, permissionSql); if (finalSql ! null !finalSql.equals(originalSql)) { // 5. 关键步骤修改BoundSql中的SQL语句 // 这里需要利用反射修改BoundSql的私有字段sql MetaObject metaObject SystemMetaObject.forObject(boundSql); metaObject.setValue(sql, finalSql); } } /** * 判断是否忽略权限检查 */ private boolean shouldIgnorePermission(MappedStatement ms) { try { // 获取Mapper接口类和方法名 String id ms.getId(); String className id.substring(0, id.lastIndexOf(.)); String methodName id.substring(id.lastIndexOf(.) 1); Class? clazz Class.forName(className); Method method Arrays.stream(clazz.getMethods()) .filter(m - m.getName().equals(methodName)) .findFirst() .orElse(null); if (method ! null) { // 检查方法上是否有InterceptorIgnore注解并忽略数据权限 InterceptorIgnore ignoreAnnotation method.getAnnotation(InterceptorIgnore.class); if (ignoreAnnotation ! null true.equals(ignoreAnnotation.dataPermission())) { return true; } // 也可以检查方法或类上是否有我们自定义的DataPermission注解如果没有则默认忽略或应用全局规则根据业务定 } } catch (Exception e) { // 日志记录但不中断流程 log.warn(Check permission ignore annotation error, will apply permission filter., e); } return false; } /** * 简单从SQL中提取表名仅适用于简单查询复杂查询需完善 */ private String getTableName(String sql) { // 这是一个简化实现。生产环境应使用jsqlparser进行更准确的解析。 // 正则匹配第一个FROM后的单词忽略别名和复杂情况 Pattern pattern Pattern.compile(FROM\\s(\\w), Pattern.CASE_INSENSITIVE); Matcher matcher pattern.matcher(sql); if (matcher.find()) { return matcher.group(1); } return null; } /** * 使用jsqlparser重写SQL注入权限条件 */ private String rewriteSqlWithPermission(String originalSql, String permissionCondition) { try { Statement statement CCJSqlParserUtil.parse(originalSql); if (statement instanceof Select) { Select select (Select) statement; PlainSelect plainSelect (PlainSelect) select.getSelectBody(); Expression where plainSelect.getWhere(); // 将权限条件字符串解析为Expression对象 Expression permissionExpr CCJSqlParserUtil.parseCondExpression(permissionCondition); if (where null) { // 原SQL无WHERE直接设置权限条件 plainSelect.setWhere(permissionExpr); } else { // 原SQL有WHERE用AND连接原条件和权限条件 AndExpression andExpression new AndExpression(where, permissionExpr); plainSelect.setWhere(andExpression); } return select.toString(); } // 如果不是SELECT语句如UPDATE, DELETE可根据业务决定是否处理 } catch (JSQLParserException e) { log.error(Failed to parse or rewrite SQL with data permission., e); // 解析失败应抛出异常或降级处理如返回原SQL否则可能导致系统错误 // 抛出运行时异常让上层捕获并处理避免执行错误的SQL throw new RuntimeException(Data permission SQL rewrite failed, e); } return originalSql; } }3.5 注册拦截器与配置最后需要将我们自定义的拦截器注册到MyBatis-Plus的配置中。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor(DataPermissionInterceptor dataPermissionInterceptor) { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 注意拦截器的添加顺序数据权限拦截器通常应在分页拦截器之前添加 interceptor.addInnerInterceptor(dataPermissionInterceptor); // 如果还有其他拦截器如分页插件按需添加 // interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }4. 避坑指南与进阶优化实现一个能用于生产环境的数据权限拦截器远不止上述基础代码那么简单。下面是我在多个项目中趟过的坑和总结的优化点。4.1 多表关联查询的权限注入上面的简单实现getTableName方法在面对多表关联JOIN或子查询时会完全失效。例如SQLSELECT a.*, b.name FROM order a LEFT JOIN user b ON a.user_id b.id。权限条件应该加在a.dept_id还是b.dept_id或者都需要加解决方案在注解中显式指定在DataPermission注解中增加tableAlias或tables属性让开发者明确指定需要过滤的表及其别名。DataPermission(field dept_id, tableAlias a) ListOrderVO selectComplexOrders();在Handler中智能判断在IDataPermissionHandler.getPermissionSql方法中不仅传入tableName还可以传入解析后的SQL语句对象Select。处理器可以遍历Select中的FromItem和Joins根据表名、别名以及业务规则动态生成针对不同表的条件并用AND连接。// 伪代码在Handler中处理多表 public String getPermissionSql(Select selectBody) { ListExpression conditions new ArrayList(); // 解析主表 processFromItem(selectBody.getFromItem(), conditions); // 解析关联表 if (selectBody.getJoins() ! null) { for (Join join : selectBody.getJoins()) { processFromItem(join.getRightItem(), conditions); } } // 将多个条件用AND连接 return StringUtils.join(conditions, AND ); }这种方法更为强大和自动但实现复杂度高需要对jsqlparser的AST结构有深入理解。4.2 性能考量与SQL解析开销每一次查询都进行SQL解析和重写必然带来额外的性能损耗。对于高频、简单的查询这个损耗需要关注。优化策略缓存解析结果对于相同的MappedStatement ID和相同的权限SQL片段组合其最终生成的SQL是固定的。可以设计一个缓存如Guava Cache键为msId permissionSql值为重写后的SQL字符串。在beforeQuery中先查缓存命中则直接使用避免重复解析。减少不必要的解析在shouldIgnorePermission阶段就快速判断如果方法明确忽略或用户无需过滤则尽早返回。getTableName这种简单正则匹配也可以作为一个快速路径对于无法匹配的复杂SQL再走完整的jsqlparser解析。批量操作支持注意INSERT ... SELECT,UPDATE ... WHERE,DELETE ... WHERE这类语句。我们的拦截器目前只拦截了Executor.query对于基于WHERE的更新和删除同样需要数据权限控制否则会造成越权更新/删除。需要重写beforeUpdate等方法逻辑类似但务必小心避免破坏数据的完整性。4.3 与MP其他拦截器的协作与冲突MP的拦截器链是有顺序的。常见的冲突发生在与**分页插件PaginationInnerInterceptor和租户插件TenantLineInnerInterceptor**之间。与分页插件的顺序数据权限条件必须在分页计算之前注入。因为分页插件会先进行COUNT(1)查询获取总数这个总数必须是经过数据权限过滤后的结果。因此DataPermissionInterceptor必须添加到PaginationInnerInterceptor之前。与租户插件的协作如果你的系统同时有多租户和数据权限需求两者都是通过添加WHERE条件实现的。你需要决定它们的优先级和组合方式。通常租户隔离是更高维度的、必须的过滤tenant_id ?数据权限是在此基础上的进一步细化AND dept_id ?。确保两个拦截器都能正常工作且条件正确叠加。有时你可能需要将两者合并到一个更复杂的拦截器中统一处理。4.4 权限规则的动态性与复杂性前面的例子是简单的部门ID过滤。实际业务中规则可能极其复杂多重维度用户可能同时受部门、区域、产品线等多个维度限制。数据权限集用户拥有一个可访问的数据ID集合如id IN (1,2,3,4,5)这个集合可能来自角色配置并且是动态变化的。自定义规则如“可查看本人创建以及下属创建的数据”。应对方案设计强大的IDataPermissionHandler接口使其能够接收更丰富的上下文信息用户对象、注解属性、方法参数等并返回复杂的Expression对象而不仅仅是字符串。引入规则引擎对于特别复杂的、可配置的规则如基于部门树、角色、数据范围的组合可以考虑集成轻量级规则引擎如Drools、Easy Rules或自研的规则解析器。Handler负责调用规则引擎根据输入的事实用户信息、操作对象计算出最终的过滤条件。使用视图或SQL注释另一种思路是在数据库层解决。为不同权限角色创建不同的数据库视图或者在MapperXML中编写包含动态条件的SQL通过MyBatis的动态SQL功能或script标签来实现。这种方案将复杂度转移到了SQL层可能更直观但失去了MP拦截器的透明性和非侵入性优点。4.5 测试策略与边界情况数据权限功能事关系统安全必须进行充分测试。单元测试针对DataPermissionHandler和SQL重写逻辑rewriteSqlWithPermission编写详尽的单元测试。覆盖各种SQL场景无WHERE、有WHERE、有JOIN、有子查询、带有括号的复杂条件等。集成测试在Spring Boot Test中模拟不同用户登录调用各个Mapper方法断言查询返回的数据范围是否正确。特别要测试InterceptorIgnore注解是否生效。边界与异常测试超级管理员确保管理员账号不受限制getPermissionSql返回null。空结果权限如果用户没有任何数据权限Handler应该返回一个永假条件如10这样查询会返回空结果集而不是抛出异常或返回全部数据。SQL注入风险确保permissionCondition来自可信的Handler计算而不是外部传入的字符串。避免在Handler中直接拼接用户输入到SQL片段中。事务与连接池确保在事务上下文中用户上下文不会因为线程复用而错乱。UserContextHolder的清理工作必须在请求结束时如Interceptor的afterCompletion或使用ThreadLocal时配合try...finally进行。5. 总结与个人实践心得实现一个健壮的DataPermissionInterceptor是一个典型的“麻雀虽小五脏俱全”的系统工程它涉及MyBatis插件机制、SQL解析、业务规则计算、线程上下文管理等多个技术点。经过几个项目的迭代我个人的体会是首先明确边界至关重要。数据权限拦截器应该只做一件事根据当前用户上下文向查询的WHERE子句中注入过滤条件。它不应该承载具体的权限计算逻辑那属于DataPermissionHandler的职责也不应该处理复杂的多表别名映射这部分最好通过注解或配置显式声明。清晰的职责分离能让代码更易维护和测试。其次“配置优于编码”在复杂规则下是伪命题。初期我们总希望做一个高度可配置的通用系统通过页面配置就能实现所有权限规则。但现实是过于复杂的配置界面会让业务人员望而却步且很难覆盖所有边角情况。我的经验是为最常见的几种权限模式如按部门、按用户、按角色数据范围提供内置的、简单的Handler实现。对于真正复杂的、定制化的规则宁愿让开发者在Handler中写一小段Java代码来实现。代码的表达能力和灵活性远胜于配置。再者性能问题往往出现在意料之外的地方。我们曾遇到一个性能瓶颈不是SQL解析而是因为某个Handler的实现中每次调用都去数据库查询一次用户的权限数据范围。对于列表查询可能分页查10条数据来说这产生了N1问题。解决方案是使用缓存将用户-权限关系在登录时或变更时加载到内存如RedisHandler中直接读取内存数据。另一个坑是当权限条件IN子句中的ID集合非常大时例如上万条会导致SQL性能急剧下降。这时需要考虑拆分查询或改用EXISTS子查询、临时表关联等策略。最后监控与日志不可或缺。一定要为拦截器添加详细的DEBUG或TRACE级别日志记录原始SQL、注入的权限条件、最终SQL。这在线下排查“为什么这条数据查不到”或“为什么这条数据被意外过滤”时是唯一有效的线索。在生产环境可以通过开关控制日志输出量避免日志泛滥。数据权限是系统安全的基石之一它的实现需要细致和严谨。Mybatis-plus的拦截器机制为我们提供了一个非常强大的切入点但如何用好它构建出既安全可靠又灵活高效的数据权限体系仍需我们在深刻理解业务的基础上进行精心的设计和持续的打磨。希望这篇长文能为你提供一个坚实的起点和清晰的实现路径。

相关新闻

服务器、储能进入48V时代,电流采样如何保证精准?FP135/FP136电流检测放大器:面向48V系统的高精度采样方案

服务器、储能进入48V时代,电流采样如何保证精准?FP135/FP136电流检测放大器:面向48V系统的高精度采样方案

48V电源架构逐渐成为高功率设备的发展方向 随着 AI 算力、云计算以及数据中心业务持续增长,服务器系统对于供电效率、功率密度以及热管理能力提出了更高要求。传统12V供电架构在面对高功率负载时,电流较大,线缆损耗以及供电效率问题逐渐凸显。…

2026/7/31 14:46:53 阅读更多 →
5步掌握QRemeshify:Blender智能重拓扑插件终极指南

5步掌握QRemeshify:Blender智能重拓扑插件终极指南

5步掌握QRemeshify:Blender智能重拓扑插件终极指南 【免费下载链接】QRemeshify A Blender extension for an easy-to-use remesher that outputs good-quality quad topology 项目地址: https://gitcode.com/gh_mirrors/qr/QRemeshify 在Blender中进行3D建模…

2026/7/31 14:46:53 阅读更多 →
宝汁们,问卷填小技巧,学会啦记得免费留个小赞赞

宝汁们,问卷填小技巧,学会啦记得免费留个小赞赞

毕业论文调研救星来了!不用到处求人填问卷,高效收集答卷,数据分析一键导出,写论文再也不用为样本量头疼,就去找微信小程序球球问卷

2026/7/31 14:45:53 阅读更多 →

最新新闻

3步破解:Windows ADB Fastboot驱动安装的终极解决方案

3步破解:Windows ADB Fastboot驱动安装的终极解决方案

3步破解:Windows ADB Fastboot驱动安装的终极解决方案 【免费下载链接】Latest-adb-fastboot-installer-for-windows A Simple Android Driver installer tool for windows (Always installs the latest version) 项目地址: https://gitcode.com/gh_mirrors/la/La…

2026/7/31 15:27:04 阅读更多 →
猫抓浏览器扩展:5个核心功能揭秘,让你成为网页资源管理大师

猫抓浏览器扩展:5个核心功能揭秘,让你成为网页资源管理大师

猫抓浏览器扩展:5个核心功能揭秘,让你成为网页资源管理大师 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 你是否曾面对在…

2026/7/31 15:27:04 阅读更多 →
华沿控制柜系统重装教程

华沿控制柜系统重装教程

华沿控制柜系统重装教程 本文用于指导现场人员使用再生龙(Clonezilla)镜像恢复华沿控制柜系统。操作会覆盖控制柜本机硬盘数据,请确认镜像来源、目标设备和 U 盘后再执行。 一、准备工作 1. 硬件准备 需要准备两个 U 盘: 再生…

2026/7/31 15:27:04 阅读更多 →
WebShell应急响应实战:从发现到清理加固的全流程指南

WebShell应急响应实战:从发现到清理加固的全流程指南

1. 项目概述:当服务器响起警报那天下午,我正在处理一个常规的部署任务,监控系统突然弹出一条高优先级告警:某台Web服务器的某个目录下,出现了异常的文件创建行为,文件后缀是.php。我心里“咯噔”一下&#…

2026/7/31 15:27:04 阅读更多 →
免费B站视频下载器BilibiliDown:5分钟掌握跨平台下载技巧

免费B站视频下载器BilibiliDown:5分钟掌握跨平台下载技巧

免费B站视频下载器BilibiliDown:5分钟掌握跨平台下载技巧 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader 😳 项目地址: https://gitcode.com/gh_mirror…

2026/7/31 15:27:04 阅读更多 →
Unity光影技术演进:从静态烘焙到动态全局光照的实战指南

Unity光影技术演进:从静态烘焙到动态全局光照的实战指南

1. 项目概述:从静态烘焙到动态光影的十年跨越 如果你是从Unity 5.x甚至更早版本一路走来的开发者,提起LightMap,脑海里浮现的很可能是一段漫长而充满不确定性的等待——点击“烘焙”按钮,泡杯咖啡,祈祷不要出现漏光、接…

2026/7/31 15:26:04 阅读更多 →

日新闻

物理复制比逻辑复制好在哪?数据库复制原理详解

物理复制比逻辑复制好在哪?数据库复制原理详解

数据库复制是把主库数据同步到备库的机制,分为逻辑复制和物理复制两种。逻辑复制传输的是 SQL 语句或行变更事件,物理复制传输的是存储引擎底层的物理日志。阿里云 PolarDB(云原生数据库)采用物理复制,在同步延迟、数据…

2026/7/31 0:00:34 阅读更多 →
BilibiliDown:3分钟学会B站视频下载的终极指南

BilibiliDown:3分钟学会B站视频下载的终极指南

BilibiliDown:3分钟学会B站视频下载的终极指南 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader 😳 项目地址: https://gitcode.com/gh_mirrors/bi/Bilib…

2026/7/31 0:00:34 阅读更多 →
有哪些游戏数据AI平台?游戏行业Data+AI融合方案盘点

有哪些游戏数据AI平台?游戏行业Data+AI融合方案盘点

当前,游戏行业的“DataAI融合”已从概念验证进入价值落地阶段。根据IDC 2025年数据,中国AI游戏云市场规模已达18.6亿元;同时,游戏研发环节AI渗透率高达86%,生成式AI内容普及率超过50%。面对庞大的市场,游戏…

2026/7/31 0:00:34 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/31 1:03:03 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/29 14:34:28 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/31 4:19:39 阅读更多 →

月新闻