# 基于Spring Boot和layui的RBAC权限管理系统 **Repository Path**: reap_chen_hao_admin/spring_boot_layui_rbac_frame ## Basic Information - **Project Name**: 基于Spring Boot和layui的RBAC权限管理系统 - **Description**: 基于Spring boot的RBAC权限管理框架,基于Maven多模块搭建,使用 PostgreSQL开源数据库,依赖Redis和Nginx和LayuiAdmin。!!!!仅供学习使用!不得商业用途!!!!!!layuiAdmin 不遵循任何开源许可证!!! - **Primary Language**: Java - **License**: MulanPSL-1.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 1 - **Created**: 2020-05-15 - **Last Updated**: 2021-06-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 操作文档 [TOC] # 部署文档 ## 依赖环境 * `IDE编译器`:IDEA 2020.1 * `Redis`: 随意版本 * `Maven`: 随意版本 * `PostgreSQL`:PostgreSQL 12(随意版本也行) * `Nginx`: 随意版本 * `JDK`: 1.8 ## 修改配置文件 * 只需修改`web`模块下的`application.yml`配置文件 ```java spring: datasource: ## 连接地址 url: jdbc:postgresql://localhost:5432/postgres?currentSchema=public ## 数据库用户名 username: postgres ## 数据库密码 password: postgres redis: database: 0 ## Redis默认无密码,都是默认配置 port: 6379 host: localhost server: ## 访问端口 port: 8080 servlet: ## url访问前缀 context-path: /mrc ## 日志 logging: ## 修改日志存储路径,本机运行可以删掉 file: path: C:/Users/Administrator/Desktop/Git/mrc-log/my.log level: root: INFO ``` ## Redis缓存 * 缓存@Cacheable 的 Value集中放在`util`模块下的`com.mrc.SysCacheValue` * 使用interface进行分类 * interface下的成员变量是==全局变量==,以下代码 ```java public interface DICT { // 字典类型 String TYPE = "dice_type"; // 字典值 String VALUE = "dict_value"; } ``` 等同于,原理请百度去 ```java public interface DICT { // 字典类型 public static final String TYPE = "dice_type"; // 字典值 public static final String VALUE = "dict_value"; } ``` * `@CacheEvict`简单讲解,`allEntries = true`意思是属于这个value的缓存全部删除 * `beforeInvocation = true`是在执行下面的代码前就执行删除缓存 ```java @CacheEvict(value = '', allEntries = true, beforeInvocation = true) ``` ## Sql数据还原 1. 确保正确安装`PostgreSQL 12` 2. 打开`PostgresSQL`客户端`pgAdmin4` 3. 默认使用`public`架构 4. 右键`public` ![捕获3](.\pic\捕获3.JPG) 5. 选择备份文件 ![捕获4](.\pic\捕获4.JPG) 6. 选择文件 ![捕获5](.\pic\捕获5.JPG) 7. 点击还原,大多数都能成功还原。 ## Nginx配置 1. 安装好`Nginx`并测试,自行百度去 2. 安装完成后,打开`Nginx`安装文件夹,打开`conf`,用`记事本`打开`nginx.conf`,修改以下文件 ``` #user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; underscores_in_headers on; # 开启请求头下划线支持 client_max_body_size 30m; # 上传文件最大限制 proxy_connect_timeout 600; # 设置超时时间 proxy_read_timeout 600; proxy_send_timeout 600; sendfile on; keepalive_timeout 65; server { listen 8081; # 本地Nginx端口 server_name localhost; location / { root C:/Users/Administrator/Desktop/Git/work/html; # 前端页面路径 index index.html index.htm; } location ^~ /mrc/ { # 服务端访问前缀 proxy_pass http://localhost:8080; # 后端访问地址 proxy_redirect off; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Cookie $http_cookie; } } } ``` 3. 修改完成后重启或开启`nginx`,重启命令 `nginx.exe -s reload` 4. 测试是否能打开目标页面,不行自行==百度==去 # 服务端规范 ## 主要构成 `Spring boot 2.X`、 ` MyBatis` 、`PostgreSQL 12`、`Redis`、`maven`、`Nginx` ## Maven多模块介绍 ![捕获](.\pic\捕获.JPG) * 本系统基于Maven多模块搭建,各模块直接的依赖如上图,也可查看对应模块的pom.xml文件查看依赖关系,其中work-sheet是根模块,一切模块都依赖它。 * `web`负责controller,`service`负责业务,`dao`负责与数据库打交道,`domain`是实体类,`rbac`是系统鉴权中心,`conf`是配置中心,`util`是各种工具类 ## MyBatis-Plus * [MyBatis-Plus](https://github.com/baomidou/mybatis-plus)[ ](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis](http://www.mybatis.org/mybatis-3/)[ ](http://www.mybatis.org/mybatis-3/) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 * [官方文档地址]: https://mp.baomidou.com/ ### 代码生成器 * [代码生成器文档]: https://mp.baomidou.com/guide/generator.html * 代码生成器在项目中的位置:`dao`模块中的 `com.mrc.CodeGenerator.java` #### 使用方法 * 在`CodeGenerator.java`中找到以下代码段,修改参数,支持多表生产 ```java /** * 填入需要生成的表的表名,支持 String...,填入多个表名 */ strategy.setInclude(""); ``` * 填好后运行`main`程序,自动生成entity、dao、service、serviceImpl、controller * ==**注意:**==再生成代码完成后,手动将`dao`模块下的`com.mrc.mapper.xml`中的xml文件==剪切==到`dao`模块中的`resources`中的`xml`文件夹中,**不然不能运行自定义的sql语句** * 单表操作可以不用自定义SQL语句,直接在对应的controller编写业务逻辑,各种业务场景可参考`web`模块的`com.mrc.controller.TestController` ## 实体类(domain)规则 * 可参考`domain`模块中的`com.mrc.entity.Test` * ==代码生成器==生成的model,需在`class`文件上加上`@JsonInclude(JsonInclude.Include.NON_NULL)`,作用是值为`Null`字段不会是被JSON序列号 * 在标记为`ID`的属性上加上`@JsonSerialize(using = ToStringSerializer.class)`,==只适用在`ID`字段为`Long`属性的字段==,加上后JSON序列化返回前端时会变成String类型 * 在标记为`ID`的属性上再加上`@NotNull(message = "ID不能为空",groups = Update.class)` * 在==**新增**==时属性不能为空或者Null的属性加上`@NotEmpty(message = "",groups = Insert.class)`,message是自定义信息,不会返回给用户看,只会打印在日志中,方便到时排查问题。 * 在==修改==时不能为空的属性属性加上`@NotEmpty(message = "",groups = Insert.class)`或`@NotNull(message = "",groups = Insert.class)`,这些==注解==都是==Hibernate Validator==的注解,详情百度了解`Hibernate Validator`; * 在==代码生成器==生成的`createDate`、`modifyDate`、`isDelete`字段上的`@TableField`注解中,加上该属性`select = false`,代表在使用MyBatis-Plus查询构造器时不查询该字段。 * 在自定义的各种`dto`,`vo`中,必须实现序列号接口并自定义序列化UID,必要时需按照第二,第三 ```java implements Serializable private static final long serialVersionUID=1L; ``` * 由于使用了`lombok`,IDEA需安装插件,`File`—》`Settings`—》`Plugins`—》搜索`Lombok`,并安装 * `IDEA`插件推荐`Rainbow Brackets`、`MyBatisX`、`Alibaba Java Coding Guidelines`、`Translastion` * `Lombok`相关注解请自行百度 * `Accessors(chain = true)`有此注解的实体类变为链式对象,实际应用如下 ```java Test test = new Test() .setId(123L) .setName("名字") .setIsDelete(0) .setCreateDate(LocalDateTime.now()) .setModifyDate(LocalDateTime.of(2020, Month.JANUARY,20,12,20)); ``` ## 业务规则 * 在==业务类==或者==控制器类=上加上`@Slf4j`注解 * 所有需要`@Autowired`的统一采用构造器注入,例如下面这样子,不用在引用对象上加`@Autowired` ```java TestService service; public TestController(TestService service) { this.service = service; } ``` * 在有业务错误时,例如==插入失败==,==更新失败==等业务上的错误,需在错误时打印错误日志,使用`log.info()`,方便错误回溯。错误时统一返回`CommonEnum.INTERNAL_SERVER_ERROR`错误信息,例子 ```java @DeleteMapping("/{id}") ResultBody remove(@PathVariable Long id) { if (!service.removeById(id)) { log.info("Test删除失败,失败id:{}",id); return ResultBody.error(CommonEnum.INTERNAL_SERVER_ERROR); } return ResultBody.success(); } ``` * 统一RESTful接口风格,获取用GET,新增用POST,更新用PUT,删除用DELETE ## 命名规则 ### dao层命名规则 1. 获取单个对象: selectXXXByXXX,例子:selectTestById(Long testId); 2. 获取多个对象:selectXXXListByXXX,例子:selectTestListByName(String name); 3. 分页获取多个对象:selectXXXPage(long page, long limit); 4. 统计值:countXXX,例子:countTest(); 5. 插入新增: insert(Test test), batchInsert(List testList) 6. 修改更新:updateByXXX或update,例子:updateById(Test test),update(Test test) 7. 删除:deleteByXXX,例子:deleteById(Long id),deleteByIds(List id); ### service接口命名规则 1. 获取单个对象: getXXXByXXX,例子:getTestById(Long testId); 2. 获取多个对象:getXXXListByXXX,例子:getTestListByName(String name); 3. 分页获取多个对象:getXXXPage(long page, long limit); 4. 统计值:countXXX,例子:countTest(); 5. 插入新增: save(Test test), batchSave(List testList) 6. 修改更新:modifyByXXX或update,例子:modifyById(Test test),update(Test test) 7. 删除:removeByXXX,例子:removeById(Long id),removeByIds(List id); ### 领域模型(domain)命名规则 1. 基本对象:XXXX,例子:Test 2. 数据传输对象(一般用于接收用户数据):xxxxDto,例子:TestDto 3. 展示对象(一般用于分页数据表格):xxxVo,例子:TestVo ## RBAC模块使用方法 * 在前端HTML新建好页面后,需要操作数据库的三个表,`menu`,`interface`,`menu_interface`来关联菜单与接口的关联。 * ![捕获2](.\pic\捕获2.JPG) ```sql ---------------------------------------新增菜单-------------------------------------------- insert into public.menu( id, parent_id, title, check_arr, "name", icon, jump, "order" ) values( 1260459079697305600, 1259738910347890688, '测试', 0, '', '', '', 0 ); ---------------------------------------新增接口------------------------------------------- insert into public.interface( id, title, interface_alias ) values( 1260459079718277120, '', '' ), ( 1260459079743442944, '', '' ), ( 1260459079764414464, '鏇存柊', 'dict::update' ), ( 1260459079789580288, '', '' ); ------------------------------------关联菜单与接口关系------------------------------------ insert into public.menu_interface( id, menu_id, interface_id, create_date ) values( 1260459079814746112, 1260459079697305600, 1260459079718277120, now() ), ( 1260459079839911936, 1260459079697305600, 1260459079743442944, now() ), ( 1260459079865077760, 1260459079697305600, 1260459079764414464, now() ), ( 1260459079890243584, 1260459079697305600, 1260459079789580288, now() ); ``` ### 以上ID为自己手动填入,自己手动一一对应 `util`模块包里的`com.mrc.IdFactory`有个`main`方法可以生产10个ID, ### Menu菜单表 `parent_id`为`-1`的是一级菜单,必须设置`title`,`icon` `parent_id`为父菜单ID时,它们就产生了关联,必须填写`jump`,`title` `order`为排列顺序,最好从主页是`1`开始,到最后的设置,每一级菜单均需设置或者只设置一级菜单 ### Interface接口表 #### title命名规则 1. 新增类型接口:【新增】 2. 修改类型接口:【修改】 3. 读取类型接口:【读取】 4. 删除类型接口:【删除】 #### interface_alias命名规则 1. 新增类型接口:【xxx::add】,例子 role::add 2. 修改类型接口:【xxx::update】,例子 role::update 3. 读取类型接口:【xxx::get】,例子 role::get 4. 删除类型接口:【xxx::delete】,例子 role::delete 5. 其他类型的自定义:【操作类::操作名称】,例子 user::resetPwd ### 权限别名常量类 在==interface表==新增了相关接口权限后,需在`util`模块下的`com.mrc.Constant`的`Authorization`下添加相关接口描述,参照已有例子新建; ```java /** * 示例权限 */ DEMO_ADD("demo::add"), DEMO_GET("demo::get"), DEMO_DELETE("demo::delete"), DEMO_UPDATE("demo::update"), ``` ### Controller接口鉴权 * 在每个需要鉴权的接口加上`@Authorization()`注解 例子: ```java // 根据ID获取对象 @Authorization(Constant.Authorization.DEMO_GET) @GetMapping("/{id}") ResultBody getOne(@PathVariable Long id) { return ResultBody.success(service.getById(id)); } ``` * 注解参数填写刚刚在`com.mrc.Constant`里面添加的对应常量 ## ==Excel导入==数据工具类使用教程 * 在`util`模块下`com.mrc.excel`是Excel导入工具类 * 适用于用户适用Excel文件批量导入数据 * 支持 String,Short,Integer,Long,Date,暂时不支持 Float,Double ### 使用教程 1. 新建一个Excel模板,并输入数据`()`这个括号是提示信息,必须是中文的括号,读取时会忽略 ![excel](.\pic\excel.JPG) 2. 新建一个实体类,这里偷懒用到了`lombok`,`@ExcelId`是标识数据库为主键的列,`@Excel`是对应Excel文件的列标题,不用加`()`,`@ExcelId`支持`String`和`Long`,默认使用雪花ID算法。 ![excel2](.\pic\excel2.JPG) 3. 单元测试 ```java @SpringBootTest(classes = WebApplication.class) public class ExcelTest { @Test void ExcelRead() { try { File file = new File("C:\\Users\\Administrator\\Desktop\\Test.xlsx"); List list = ExcelRead.read(file, T.class); list.forEach(System.out::println); } catch (MyExcelException e) { e.printStackTrace(); } } } ``` 4. 打印结果 ![excel5](C:\Users\Administrator\Desktop\excel5.JPG) 5. ExcelRead.read(File file, Class model)是静态方法,直接用 6. 单元测试类放在`web`模块 —》`test `—》`ExcelTest` 7. ==重点:==由于是提供给用户在Web页面上传,上传的文件是**先保存**到服务器后,**再**读取服务器上的Excel文件,读取完后默认==**删除文件**==。所有单元测试后,文件会被删除!修改逻辑请去 `com.mrc.excel.ExcelRead`中修改逻辑,里面的注释很完整的啦。 8. ==感谢==,读取全靠它,我只负责装配工作 [HuToll]: https://www.hutool.cn/docs/#/ ## ==Redis_Token==简介 * 在没开发Redis_Token这个基于Token的用户登录验证之前,用的是JWT(JSON WEB TOKEN),但在使用时发现有点问题 1. 登录时间不能续期,过期了只能重新登录获取,也有很多解决方案,但都是要用户端更换Token。 2. 在Token未过期时,用户登出后,Token依然有效。解决方案是加入黑名单。 3. 复制Token,去JWT官网,可以解密,查看JWT存储的信息 * 所以基于JWT的特性,再结合Redis的特性,开发出一套小而精的登录鉴权 * 大致原理如下,对象信息是用的Redis的map数据类型存储。 * 有重复登录处理,逼退旧的,只给登录权限给最新登录的。 ![tok](.\pic\tok.JPG) # 前端规范 ## ID唯一性 * 因为开启了==标签页功能==,请==**务必**==注意 ID 的冲突,尤其是在你自己**绑定事件**的情况。ID 的命令可以遵循以下规则来规避冲突: * ``` LAY-路由-任意名 ``` * 以*消息中心*页面为例,假设它的路由为:`/app/message/`,那么 ID 应该命名为: * ``` ``` * ID唯一性同样适用于`lay-filter` ## 数据表格页面规范 * ==最上面必须有== ```html 操作日志 ``` * 其他可参考`src\views\demo\testDemo\list.html` ## JavaScript引用规范 * 统一采用以下这种方式引用JavaScript文件 * ```javascript ``` * JavaScript文件必须这样定义 ```javascript layui.define(['form', 'upload', 'table', 'aut'], function (exports) { var $ = layui.$, layer = layui.layer, setter = layui.setter, view = layui.view, admin = layui.admin, form = layui.form, table = layui.table, aut = layui.aut; var ok = setter.response.statusCode.ok; // 200 var fail = setter.response.statusCode.fail; // 400 var error = setter.response.statusCode.error; // 500 var forbidden = setter.response.statusCode.forbidden;// 403 // 下面的demo对应HTML页面的layui.use('demo', layui.factory('demo')); exports('demo', {}); }) ``` * ==不得采用传统方式导入JavaScript文件== ```javascript ``` ## 数据表格JavaScript规范 ### 数据表格代码 ```javascript var myTable = table.render({ // 生成一个引用对象,方便表格重载 elem: '#LAY-demo-testDemo-table', // 表格唯一ID url: '/mrc/test', // 获取数据的URL,默认采用GET方式访问 page: true, // 开启分页 loading: true, // 开启分页加载层 title:"示例", // 定义 table 的大标题(在文件导出等地方会用到) toolbar: true, // 开启工具条,可导出Excel autoSort: false, // 关闭前端的排序,启动服务端的排序 headers: { // 加上Token请求头 access_token: layui.data('layuiAdmin').access_token }, cols: [ [{ checkbox: true // 开启多选框,由于权限操作需要,关闭行操作按钮 }, { field: 'id', // 对应属性名 title: 'ID' // 表头 }, { field: 'name', title: '用户名' }, { field: 'create_date', // 当需要开启服务端排序时,该字段填写数据库字段名 title: '时间', align: 'center', templet: function(d) { // 并采用templet返回该行数据 return util.toDateString(d.createDate) }, sort: true } ] ] }); ``` ### 表格重载方法(封装成函数) ```javascript function table_reload() { myTable.reload({ // 这里的myTable对象是上面数据表格生成的引用对象 url: '/mrc/test' // 重载地址,不用指定重载页数,默认当前页面数 }) } ``` ### 表格搜索 ```javascript // LAY-demo-testDemo-table-search-btn为搜索按钮的lay-filter值 form.on('submit(LAY-demo-testDemo-table-search-btn)', function (data) { var field = data.field; myTable.reload({ // 这里的myTable对象是上面数据表格生成的引用对象 url: '/mrc/test', // 重载地址,不用指定重载页数,默认当前页面数 where: field // 搜索对象 }); return false; // 当搜索的form为
则不需要加 // 默认为方式,因为有reset按钮重置搜索框 }); ``` ### 操作表格按钮事件命名规则 ```html ``` * 其中的`data-type`属性也必须唯一,命名规则为`模块名_事件`,例如,用户模块的添加事件,`user_add` ## 权限按钮控制==(重点)== 封装了一个基于**LayUi模块**的**按钮权限控制模块** * 代码如下 ```javascript layui.define(function(exports) { var $ = layui.$, admin = layui.admin; var aut = { data: function(data){ if (data.length > 0 && Object.prototype.toString.call(data)== '[object Array]' && data[0].hasOwnProperty("alias") && data[0].hasOwnProperty("id")) { data.forEach((item)=>{ var b = false; admin.req({ url: '/mrc/rbac/aut?alias=' + item.alias, type: 'get', success: function (res) { b = res.data; if (!b) { $('#' + item.id + '').attr("disabled", "disabled"); $('#' + item.id + '').addClass("layui-btn-disabled"); } } }); }) } else{ throw new Error("非法数据") } } } exports('aut', aut); }) ``` * 使用方法如下 ```javascript aut.data([{ alias: "demo::add", // 对应Interface表和Constant.Authorization存储的值 id: "LAY-demo-testDemo-table-add" // 对应操作按钮的ID,必须全局唯一 }, { alias: "demo::update", id: "LAY-demo-testDemo-table-edit" }, { alias: "demo::delete", id: "LAY-demo-testDemo-table-batchdel" }, { alias: "demo::get", id: "LAY-demo-testDemo-table-detail" }, { alias: "demo::get", id: "LAY-demo-testDemo-table-search-btn" } ]) ``` * 数据格式必须是==**数组格式**==,即使只有一个按钮也必须是==**数组**==,`alias`和`id`无别名 ### 操作表格按钮函数 * 按钮例子 ```html ``` * 事件绑定必须写上的JavaScript代码,一般放在JavaScript文件最下面 ```javascript $('.layui-btn.layuiadmin-btn-admin').on('click', function () { var type = $(this).data('type'); active[type] ? active[type].call(this) : ''; }); ``` * 按钮事件 ```javascript add: function () { // add对应按钮上的data-type="add" admin.popup({ title: '新增', // 弹出层标题 area: ['30%', '40%'], // 弹出层宽高:[宽,高] id: 'LAY-popup-demo-testDemo-add', // 弹出层ID,必须全局唯一,必有 success: function (layero, index) { // view.render("弹出层在view中的路径"),在done函数可以写弹出层中的业务逻辑 view(this.id).render('demo/testDemo/demoForm').done(function () { // 更新表单状态,第二个参数对应弹出层的form表单lay-filter值,必须全局唯一 form.render(null, 'LAY-demo-testDemo-form'); // 监听表单提交,submit里填入弹出层按钮的lay-filter值,必须全局唯一 form.on('submit(LAY-demo-testDemo-form-submit)', function (data) { // 加载层 var load = layer.load(); // 提交的字段和值 var field = data.field; // ajax提交 admin.req({ type: "post", url: "/mrc/test", // 后端使用@RequestBody接收的必须JSON.stringify() data: JSON.stringify(field), // 后端使用@RequestBody接收必须加 dataType: 'json', // 后端使用@RequestBody接收必须加 contentType: "application/json", success: function (res) { // 关闭加载层 layer.close(load); // 此处的ok定义再最上面,配置在config.js文件中 if (res.code == ok) { layer.msg("新值成功", { icon: 1 }) } else if (res.code == fail) { layer.msg("新增失败!请重试!", { icon: 1 }) } else if (res.code == forbidden) { layer.msg(res.msg, { icon: 2 }) } else { layer.msg("系统错误,请联系开发商!", { icon: 2 }) } }, error: function () { layer.msg("系统错误!请联系开发商!", { icon: 0 }) layer.close(load); } }); // 关闭弹出层 layer.close(index); //执行关闭 }); }); }, // 关闭弹出层后的执行的函数 end: function () { // 重载表格 table_reload(); } }); }, ``` ## 数据表格服务端排序 * 数据表格加载时必须设置`autoSort: false`,取消前端排序 * 需要服务端排序的字段的`field`属性必须为数据库字段(也可自定义,关键是后端怎么处理) * 新增表格排序监听 ```javascript //注:sort 是工具条事件名,LAY-user-sysLog-table 是 table 原始容器的属性 lay-filter="对应的值" table.on('sort(LAY-user-sysLog-table)', function (obj) { console.log(obj.field); //当前排序的字段名 console.log(obj.type); //当前排序类型:desc(降序)、asc(升序)、null(空对象,默认排序) //尽管我们的 table 自带排序功能,但并没有请求服务端。 //有些时候,你可能需要根据当前排序的字段,重新向服务端发送请求,从而实现服务端排序,如: table.reload('LAY-user-sysLog-table', { initSort: obj , // 下面两个参数名可自定义 where: { sortField: obj.field, //排序字段 sortType: obj.type //排序方式 } }); }); ``` * 服务端处理代码(操作日志为例子) ```java @Authorization(Constant.Authorization.LOG_GET) @GetMapping ResultBody getList(SysLog sysLog, Long page, Long limit, String sortField,String sortType) { Page sysLogPage = new Page<>(page, limit); QueryWrapper wrapper = new QueryWrapper<>(); // 默认排序 wrapper.orderByAsc("create_date"); if (StringUtils.hasText(sysLog.getOperation())) { wrapper.likeRight("operation", sysLog.getOperation()); } if (StringUtils.hasText(sysLog.getIp())) { wrapper.likeRight("ip", sysLog.getIp()); } if (StringUtils.hasText(sysLog.getUserName())) { wrapper.likeRight("user_name", sysLog.getUserName()); } // 排序条件 if (StringUtils.hasText(sortField) && StringUtils.hasText(sortType)) { if (sortType.equals(Constant.SortType.ASC.getType())) { wrapper.orderBy(true,true, sortField); } else if (sortType.equals(Constant.SortType.DESC.getType())) { wrapper.orderBy(true,false, sortField); } } return ResultBody.successIPage(service.page(sysLogPage, wrapper)); } ``` # SQL优化(简略) * 查询列建索引 * 主键列不用建索引,默认生成主键索引 * `like`搜索的能`likeRight`就`likeRight`,就是这样子`name like '陈%'`,这样子才能走索引,`%陈%`只能全表检索 * `MySql`支持`instr`模糊查询,自行百度去 * 多条件查询的建立组合索引,遵循最左匹配原则,不在sql语句上做赋值、操作值操作 * 外表连接关联列建立索引 * 单表索引不宜超过5个