噼里啪啦

来吧,整起,又一新功能,通用数据权限,注意是通用,通用的东西,反正挺烦的,(每天喝点茶,刷点文章,发表下博文,改下bug,写点crud多舒服的,啊哈哈,噗)
我还是第一次搞这玩意儿,因为之前做细节的数据权限都是直接写在代码里面的

好,开整,这篇文章我会写得详细一点,并且提供开源源码,全靠我自己设计,编码,一步步的敲出来的,很少的地方借鉴到了别人的东西,切看切珍惜,动动你的小手点个赞,点个收藏吧,写文章还真的挺累的。


一、啥子是数据权限?

嗯,数据权限?有些朋友可能会问了,“嗯,数据还有权限?”
没错,简单来讲:数据权限无非就是某人只能看到某些数据。
举个例子:张三登录了A系统,那么根据系统查询出来的张三所拥有的权限,比如张三有一个A部门的数据权限,
那么,在A系统中,张三只能看到A部门相关的数据。

二、做这个功能的思路是啷个一个样子的?

那好了,啷个才能实现这个功能呢?
别慌,我们先回忆一下我们在不做通用的情况下是怎么做数据权限的呢?

1.没有做通用数据权限

比如张三有A部门的权限:

String deptId = servie.getDeptByUser("张三");
//在xml里
select * from test_table
where dept_id = #{deptId}

如上面的代码所示,我们一般是通过直接写sql带条件的方式实现的,写起来非常的方便,但是代码多了就求了,万一有1W个mapper都需要这样做,那写到吐,好吧,给你搞个通用的。

2.实现的原理和思路

好,上面已经说到了痛点,那么我们只需要拦截住我们需要的sql,能根据获取到的用户数据权限,动态的拼接出我们想要的sql,再给他装回去,那这个问题就能解决了。
ok,按照这个步骤:用户登录→登录验证通过后获取到用户的所有权限信息→放入到redis中→做数据查询时拦截对应的sql→详细封装处理→执行新sql返回权限后数据


二、开整!

1.新建一个项目并添加依赖和工具类包


初始化

选择spring初始化,下一步下一步就可以了,名字随便取,先不用选依赖。
建完了在详细下面找到pom.xml,添加如下依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.kbplus.demo.data</groupId><artifactId>permission</artifactId><version>0.0.1-SNAPSHOT</version><name>permission</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.12</version></dependency><!-- 数据库--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.28</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.24</version></dependency><!-- mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version></dependency><!--mybaits-plus生成代码的依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.4.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency><!--      工具包  --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.16</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.8</version><scope>provided</scope></dependency><!--   swagger     --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.68</version></dependency><!--   redis     --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><!-- Token生成与解析--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

建库建表

添加数据库,并导入提供好的sql

名字随便取,字符集和排序规则选择我选择的就ok,比较通用的utf规则和排序规则,
简单介绍一下,utf8mb4是mysql的一种拓展字符集,可以存储一些特殊字符,utf8mb4_general_ci是兼容大多主流语言并同时比较高效的排序规则,如果你的项目有使用到少见的语言,比如俄语,可以使用utf8mb4_unicode_ci来提高精准性

导入完sql后,大概是这么一个结构。

添加公共配置,工具类

接着添加项目所需类包(源码里会提供),
大致结构如下:

2.书写简单的登录实现登录验证并保存用户信息

核心代码如下:

    /*** 登录验证** @param username 用户名* @param password 密码* @return 结果*/@Overridepublic String login(String username, String password){User user1 = baseMapper.selectOne(new QueryWrapper<User>().eq("username", username));if(user1==null){throw new CustomException("用户不存在");}if(!password.equals(user1.getPassword())){throw new CustomException("密码不正确");}//这里使用mybaitis特性collection之类的好像会更快,各位有兴趣可以尝试Set<Role> allRoleByUserId = roleMapper.getAllRoleByUserId(user1.getId());UserDept userDept = userDeptMapper.selectOne(new QueryWrapper<UserDept>().eq("user_id", user1.getId()));Set<String> allPositionByUserId = positionMapper.getAllPositionByUserId(user1.getId());List<String> deptChildren = departmentService.getDeptChildren(userDept.getDeptId());Set<String> allDataPermissionByUserId = dataPermissionMapper.getAllDataPermissionByUserId(user1.getId());LoginUser loginUser = new LoginUser();loginUser.setUser(user1);loginUser.setTenantId(user1.getTenantId());loginUser.setRoles(allRoleByUserId);loginUser.setUserId(user1.getId());loginUser.setDeptId(userDept.getDeptId());loginUser.setPostIds(allPositionByUserId);loginUser.setDeptChildren(deptChildren);loginUser.setDataPermissions(allDataPermissionByUserId);String token = UUID.randomUUID().toString();loginUser.setToken(token);//存入redistokenService.setLoginUser(loginUser);// 生成tokenreturn tokenService.createToken(loginUser);}

在创建token的时候有这一步,以便可以通过token直接拿到想要的信息,不用去redis再查。

    /*** 创建令牌* * @param loginUser 用户信息* @return 令牌*/public String createToken(LoginUser loginUser){Map<String, Object> claims = new HashMap<>();claims.put(Constants.LOGIN_USER_KEY, loginUser.getToken());claims.put("tenantId",loginUser.getTenantId());claims.put("id",loginUser.getUserId());claims.put("name",loginUser.getUser().getName());claims.put("postIds",loginUser.getPostIds());claims.put("organizationId",loginUser.getDeptId());return createToken(claims);}

3.添加基本数据并测试效果

1.添加用户数据,角色数据,权限数据等并关联:
大家可以使用sql一键导入,详情大家可以参考我的权限认证文章:待完善

我们使用user1账号登录后创建2条测试数据,再用user2登录创建2条测试数据,步骤如下:
2.使用user1模拟登录:

3.拿到返回的token去生成测试数据:


使用user2登录做同样的操作,让后可以看到表里有6条数据,null值的是我之前添加的不用在意~~,主要可以看到有4条是部门1的,2条是部门2的 我设置的权限分别是user1是部门1,user2是部门2,且部门2是部门1的子部门

4.测试效果:
使用user1成功返回6条数据,因为我现在给他的数据权限是拥有部门及子部门,所有能查到所有

使用user2登录,user2和user1拥有一样的角色,即拥有一样的权限,请求接口,发现只返回了2条数据,且都是dept2的,因为部门2是部门1的子集

目前系统支持以下类型的数据权限

    ALL("1","拥有所有数据权限"),NONE("2","未拥有数据权限"),DEPT("3","拥有部门权限"),DEPT_CHILDREN("4","拥有部门权限及子权限"),POST("5","拥有职位权限"),
//    POST_CHILDREN("6","拥有职位权限及子权限"),OWN("7","拥有自身权限"),

大家可以根据自己的需要进行测试

4.核心类解析

新建拦截机继承 InnerInterceptor (mybatis-plus的一个拦截器)

public class DataPermissionInterceptor implements InnerInterceptor

把拦截器注入进配置,这里注意添加拦截器的编写顺序,会影响到拦截器执行的先后顺序

package com.kbplus.demo.data.permission.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisPlusConfig {@Beanpublic DataPermissionInterceptor dataPermissionInterceptor() {return new DataPermissionInterceptor();}@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();paginationInnerInterceptor.setDbType(DbType.MYSQL);interceptor.addInnerInterceptor(dataPermissionInterceptor());interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}

核心拦截类:大致原理是先做有效性判断,包括是否属于管理员等,这里只拦截需要分页的查询,然后根据权限匹配,生成相对应的代码

package com.kbplus.demo.data.permission.config;import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.kbplus.demo.data.permission.entity.LoginUser;
import com.kbplus.demo.data.permission.entity.Role;
import com.kbplus.demo.data.permission.mapper.CommonMapper;
import com.kbplus.demo.data.permission.utils.SpringUtils;
import com.kbplus.demo.data.permission.utils.TokenService;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;/*** @author kbplus* @version v1.0* @date 2022-05-13 17:49:18*/
public class DataPermissionInterceptor implements InnerInterceptor {@Autowiredprivate TokenService tokenService;@Autowiredprivate HttpServletRequest httpServletRequest;@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql){String firstSql = boundSql.getSql();Field field = null;try {Select statement = (Select) CCJSqlParserUtil.parse(boundSql.getSql());if(!ifPage(parameter)){return;}LoginUser loginUser = tokenService.getLoginUser(httpServletRequest);if(loginUser==null){return;}Set<String> dataPermissions = loginUser.getDataPermissions();Set<Role> roles = loginUser.getRoles();for (Role role : roles) {if("admin".equals(role.getCode())){return;}}List<String> mainTables = getMainTable(statement);field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);if(dataPermissions==null || dataPermissions.contains(DataPermissionEnum.NONE.getCode())){String newSql = addWhereCondition(boundSql.getSql(), "1=2",DataPermissionEnum.NONE);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);return;}//获取公共mapper取字段是否存在CommonMapper commonMapper = SpringUtils.getBean(CommonMapper.class);String tenantTable = getFirstTableOnField("tenant_id", mainTables,commonMapper);if(tenantTable!=null) {//初始默认匹配租户id查询String tenantSql = addWhereCondition(boundSql.getSql(), tenantTable + ".tenant_id=" + loginUser.getTenantId(), DataPermissionEnum.NONE);//通过反射修改sql语句field.set(boundSql, tenantSql);System.out.println(tenantSql);}String organizationTable=null;if(dataPermissions.contains(DataPermissionEnum.DEPT.getCode())){organizationTable = getFirstTableOnField("create_user_organization_id", mainTables,commonMapper);if(organizationTable!=null) {String newSql = addWhereCondition(boundSql.getSql(), organizationTable + ".create_user_organization_id=" + loginUser.getDeptId(), DataPermissionEnum.DEPT);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}if(dataPermissions.contains(DataPermissionEnum.DEPT_CHILDREN.getCode())){if(organizationTable==null){organizationTable = getFirstTableOnField("create_user_organization_id", mainTables,commonMapper);}if(organizationTable!=null) {String newSql = addWhereCondition(boundSql.getSql(), getCondition(organizationTable, "create_user_organization_id", loginUser.getDeptChildren()), DataPermissionEnum.DEPT_CHILDREN);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}if(dataPermissions.contains(DataPermissionEnum.POST.getCode())){String postTable = getFirstTableOnField("tenant_id", mainTables,commonMapper);if(postTable!=null) {String sql = boundSql.getSql();if (sql.contains("where") || sql.contains("WHERE")) {sql = sql + " and ";} else {sql = sql + " where ";}String newSql = sql + getPositionCondition(postTable, "create_user_post_id", loginUser.getPostIds());//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}if(dataPermissions.contains(DataPermissionEnum.OWN.getCode())){String userTable = getFirstTableOnField("create_user", mainTables,commonMapper);if(userTable!=null) {String newSql = addWhereCondition(boundSql.getSql(), userTable + ".create_user=" + loginUser.getUserId(), DataPermissionEnum.OWN);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}} catch (JSQLParserException | NoSuchFieldException | IllegalAccessException e) {if(StringUtils.isNotEmpty(firstSql)&& ObjectUtils.isNotEmpty(field)){try {field.set(boundSql, firstSql);} catch (IllegalAccessException illegalAccessException) {illegalAccessException.printStackTrace();}}e.printStackTrace();}}/*** 获取拥有字段的指定第一张表** @param field* @return*/private String getFirstTableOnField(String field,List<String> tables,CommonMapper commonMapper) {if(CollectionUtil.isEmpty(tables))return null;for (String table : tables) {if(commonMapper.getFieldExists(table,field)>0)return table;}return null;}/*** 获取查询字段** @param selectBody* @return*/private List<SelectItem> getSelectItems(SelectBody selectBody) {if (selectBody instanceof PlainSelect) {return ((PlainSelect) selectBody).getSelectItems();}return null;}/*** 特殊处理 创建职业sql** @param tableName 表名* @param fieldName 字段名* @param ids       值* @return*/private String getPositionCondition(String tableName, String fieldName, Collection<String> ids) {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("(");for (String id : ids) {stringBuilder.append(tableName).append(".").append(fieldName).append(" like '%").append(id).append("%' or ");}stringBuilder.delete(stringBuilder.length()-3,stringBuilder.length()).append(")");return stringBuilder.toString();}/*** 生成where 条件字符串** @param tableName 表名* @param fieldName 字段名* @param ids       值* @return*/private String getCondition(String tableName, String fieldName, Collection<String> ids) {return tableName + "." + fieldName + " in (" + StringUtils.join(ids, ",") + ")";}/*** 获取tables的表名** @param statement* @return*/private List<String> getMainTable(Select statement) {TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();return tablesNamesFinder.getTableList(statement);}/*** 判断是否分页** @param selectBody* @return*/private Limit ifPage(SelectBody selectBody) {if (selectBody instanceof PlainSelect) {return ((PlainSelect) selectBody).getLimit();}return null;}/*** 判断是否分页** @param parameter* @return*/private  boolean ifPage(Object parameter) {if(parameter instanceof String) return false;JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(parameter));return jsonObject.containsKey("page") ||jsonObject.containsKey("size") ||jsonObject.containsKey("current");}/*** 在原有的sql中增加新的where条件** @param sql       原sql* @param condition 新的and条件* @return 新的sql*/private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, List<String> detps,Set<String> posts) {try {Select select = (Select) CCJSqlParserUtil.parse(sql);PlainSelect plainSelect = (PlainSelect) select.getSelectBody();final Expression expression = plainSelect.getWhere();final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);if (Objects.isNull(expression)) {plainSelect.setWhere(envCondition);} else {if(DataPermissionEnum.NONE==dataPermissionEnum){AndExpression andExpression = new AndExpression(expression, envCondition);plainSelect.setWhere(andExpression);}else {OrExpression orExpression = new OrExpression(expression, envCondition);plainSelect.setWhere(orExpression);}}return plainSelect.toString();} catch (JSQLParserException e) {throw new RuntimeException(e);}}private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum) {return addWhereCondition(sql,condition,dataPermissionEnum,null,null);}private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, List<String> detps) {return addWhereCondition(sql,condition,dataPermissionEnum,detps,null);}private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, Set<String> posts) {return addWhereCondition(sql,condition,dataPermissionEnum,null,posts);}}

5.开源源码

这里给大家提供开源源码:java-springboot-mybatis-数据权限详细实现
编写不易,点个赞再走!

java(springboot) mybatis 数据权限详细实现(图文)相关推荐

  1. 基于javaweb的在线点餐系统(java+springboot+mybatis+vue+mysql+redis)

    基于javaweb的在线点餐系统(java+springboot+mybatis+vue+mysql+redis) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:e ...

  2. Java程序员周末时间搞锭银行信息管理系统毕业设计(java+springboot+mybatis+mysql+vue+elementui)等实现 免费源码+论文答辩资料获取

    Java程序员周末时间搞锭银行信息管理系统毕业设计(java+springboot+mybatis+mysql+vue+elementui)等实现 前言介绍: 在社会快速发展的影响下,银行继续发展,大 ...

  3. 基于javaweb+mysql的酒店管理系统(java+springboot+mybatis+beetl+layui)

    基于javaweb+mysql的酒店管理系统(java+springboot+mybatis+beetl+layui) 运行环境 Java≥8.MySQL≥5.7 开发工具 eclipse/idea/ ...

  4. 基于java(springboot+mybatis)汽车信息管理系统设计和实现以及文档

    java毕业设计项目<100套>推荐 主要实现技术:Java.springmvc.springboot.mysql.mybaits.jQuery.js.css等.使用eclipse/ide ...

  5. 基于javaweb的水果生鲜商城系统(java+springboot+mybatis+vue+mysql)

    基于javaweb的水果生鲜商城系统(java+springboot+mybatis+vue+mysql) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:eclip ...

  6. Java SpringBoot+Mybatis Layui+JQuery+html微信公众号后台管理系统

    Java SpringBoot+Mybatis Layui++html微信公众号后台管理系统 hddhln 玛雅资源 技术框架 开发语言:JAVA 数据库:MYSQL JAVA开发框架:SpringB ...

  7. 基于javaweb的精品养老院管理系统(java+springboot+mybatis+vue+mysql)

    基于javaweb的精品养老院管理系统(java+springboot+mybatis+vue+mysql) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:ecli ...

  8. Java+Springboot+Mybatis+Mysql+Bootstrap+Maven实现网上商城系统

    网上商城系统 一.系统介绍 1.软件环境 2.功能模块图 3.系统功能 4.数据库表 5.SQL语句 6.工程截图 二.系统展示 1.用户-浏览商品 2.用户-注册 3.用户-登录 4.用户-购物车管 ...

  9. 基于javaweb的平行志愿管理系统(java+springboot+mybatis+vue+mysql)

    基于javaweb的平行志愿管理系统(java+springboot+mybatis+vue+mysql) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:eclip ...

最新文章

  1. linux awk 用一个或多个空格做分隔符
  2. 深夜,你的手机为谁开?
  3. (摘要)100个伟大的商业理念:理念34:企业社会责任
  4. boost::basic_thread_pool相关的测试程序
  5. iOS开源项目周报0323
  6. Matlab sumsqr函数
  7. c语言加减乘除计算程序,求一个计算加减乘除的C语言程序
  8. mysql ---- 多表设计
  9. tshark/wireshark抓包小结
  10. 使用路由器配置DHCP
  11. oracle查询小时差,ORACLE小时段 Connect By的查询,感觉还是有点难度的。
  12. Advanced Javascript outlining插件说明
  13. matlab画贝塞尔曲线给出图题,matlab练习程序(贝塞尔曲线)
  14. 用两个小样例来解释单例模式中的“双重锁定”
  15. facebook广告后台设置
  16. 蓝牙耳机蓝牙音箱出口加拿大亚马逊ICID认证周期费用
  17. 20190422每周精品之认知
  18. 一元二次求解matlab程序,规范MATLAB编程实例——求解一元二次方程
  19. 卷积神经网络膨胀卷积
  20. 基于retinex理论改进的低照度图像增强算法

热门文章

  1. 有包但import时pycharm提示No Modle name
  2. 分享 11 个常用的 Nginx 性能优化参数工作
  3. vue3中使用swiper完整版教程
  4. 2021年全球车辆到电网(V2G)收入大约33百万美元,预计2028年达到2008.7百万美元,2022至2028期间,年复合增长率CAGR为 79.9%
  5. Vue学习笔记1-什么是Vue
  6. 浏览器自己检测php代码,一个浏览器检查类_php
  7. 电脑技巧:分享五款办公文档密码解除小软件
  8. 如何给证件照换背景色并压缩到100K
  9. 微信中如何接入AI机器人才比较安全(不会收到警告或者f号)之第二步注入dll文件
  10. 基于AI深度学习的安全帽检测算法,如何应用在实际场景中?