1 Commits

Author SHA1 Message Date
wkc
28088d43a8 迁移项目到 RuoYi-Vue springboot2 基线 2026-04-14 15:13:51 +08:00
544 changed files with 73860 additions and 70367 deletions

7
.gitignore vendored
View File

@@ -38,6 +38,7 @@ nbdist/
######################################################################
# Others
.DS_Store
*.log
*.xml.versionsBackup
*.swp
@@ -45,9 +46,3 @@ nbdist/
!*/build/*.java
!*/build/*.html
!*/build/*.xml
logs/
ruoyi-ui/dist.zip
.playwright-cli

View File

@@ -0,0 +1,18 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "上虞利率定价系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]
- img [ref=e18]
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -0,0 +1,20 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "上虞利率定价系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]: admin123admin
- img [ref=e11]
- generic [ref=e14]:
- generic [ref=e15]:
- textbox "密码" [ref=e16]
- img [ref=e18]
- generic [ref=e29]: 请输入您的密码
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [active] [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1,191 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- link "上虞利率定价系统" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- heading "上虞利率定价系统" [level=1] [ref=e8]
- menubar [ref=e12]:
- link "流程列表" [ref=e14] [cursor=pointer]:
- /url: /index
- menuitem "流程列表" [ref=e15]:
- img [ref=e16]
- text: 流程列表
- menuitem "系统管理 " [ref=e19]:
- generic [ref=e20] [cursor=pointer]:
- img [ref=e21]
- text: 系统管理
- generic [ref=e23]:
- text:
- generic [ref=e25]:
- generic [ref=e26]:
- generic [ref=e27]:
- img [ref=e29] [cursor=pointer]
- navigation "Breadcrumb" [ref=e31]:
- generic:
- generic [ref=e32]:
- link "首页" [ref=e33]
- text: /
- generic [ref=e379]:
- link "利率定价管理" [ref=e380]
- text: /
- link "流程列表" [ref=e382]
- generic [ref=e36]:
- img [ref=e38] [cursor=pointer]
- img [ref=e41] [cursor=pointer]
- button [ref=e44] [cursor=pointer]:
- img [ref=e45]
- button "若依" [ref=e48] [cursor=pointer]:
- img [ref=e49]
- text: 若依
- generic [ref=e50]:
- generic [ref=e53]:
- generic [ref=e54] [cursor=pointer]: 流程列表
- generic [ref=e383] [cursor=pointer]:
- text: 流程详情
- generic [ref=e384]:
- text:      
- generic [ref=e385]:
- generic [ref=e386]:
- heading "流程详情" [level=2] [ref=e387]
- button " 返回" [ref=e388] [cursor=pointer]:
- generic [ref=e389]:
- text: 返回
- generic [ref=e391]:
- generic [ref=e393]:
- generic [ref=e396]: 关键信息
- table [ref=e400]:
- rowgroup [ref=e401]:
- row "业务方流水号" [ref=e402]:
- columnheader "业务方流水号" [ref=e403]
- row "20260410150311114" [ref=e404]:
- cell "20260410150311114" [ref=e405]
- rowgroup [ref=e406]:
- row "客户名称" [ref=e407]:
- columnheader "客户名称" [ref=e408]
- row "t***" [ref=e409]:
- cell "t***" [ref=e410]
- rowgroup [ref=e411]:
- row "客户类型" [ref=e412]:
- columnheader "客户类型" [ref=e413]
- row "个人" [ref=e414]:
- cell "个人" [ref=e415]
- rowgroup [ref=e416]:
- row "申请金额" [ref=e417]:
- columnheader "申请金额" [ref=e418]
- row "1000 元" [ref=e419]:
- cell "1000 元" [ref=e420]
- rowgroup [ref=e421]:
- row "基准利率" [ref=e422]:
- columnheader "基准利率" [ref=e423]
- row "4.35 %" [ref=e424]:
- cell "4.35 %" [ref=e425]
- rowgroup [ref=e426]:
- row "浮动BP" [ref=e427]:
- columnheader "浮动BP" [ref=e428]
- row "350" [ref=e429]:
- cell "350" [ref=e430]
- rowgroup [ref=e431]:
- row "最终测算利率" [ref=e432]:
- columnheader "最终测算利率" [ref=e433]
- row "6.05 %" [ref=e434]:
- cell "6.05 %" [ref=e435]
- rowgroup [ref=e436]:
- row "执行利率" [ref=e437]:
- columnheader "执行利率" [ref=e438]
- row "% 确定" [ref=e439]:
- cell "% 确定" [ref=e440]:
- generic [ref=e441]:
- generic [ref=e442]:
- textbox "请输入执行利率" [ref=e443]
- generic [ref=e444]: "%"
- button "确定" [ref=e445] [cursor=pointer]
- generic [ref=e446]:
- generic [ref=e447]:
- generic [ref=e450]: 流程详情
- generic [ref=e451]:
- generic [ref=e452]:
- heading "基本信息" [level=4] [ref=e453]
- table [ref=e456]:
- rowgroup [ref=e457]:
- row "机构编码 892000 运行模式 1" [ref=e458]:
- rowheader "机构编码" [ref=e459]
- cell "892000" [ref=e460]
- rowheader "运行模式" [ref=e461]
- cell "1" [ref=e462]
- rowgroup [ref=e463]:
- row "客户内码 test 证件类型 身份证" [ref=e464]:
- rowheader "客户内码" [ref=e465]
- cell "test" [ref=e466]
- rowheader "证件类型" [ref=e467]
- cell "身份证" [ref=e468]
- rowgroup [ref=e469]:
- row "证件号码 ** 创建时间 2026-04-10 15:03:11" [ref=e470]:
- rowheader "证件号码" [ref=e471]
- cell "**" [ref=e472]
- rowheader "创建时间" [ref=e473]
- cell "2026-04-10 15:03:11" [ref=e474]
- rowgroup [ref=e475]:
- row "创建者 若依-admin" [ref=e476]:
- rowheader "创建者" [ref=e477]
- cell "若依-admin" [ref=e478]
- generic [ref=e479]:
- heading "业务信息" [level=4] [ref=e480]
- table [ref=e483]:
- rowgroup [ref=e484]:
- row "担保方式 信用 申请金额 1000 元" [ref=e485]:
- rowheader "担保方式" [ref=e486]
- cell "信用" [ref=e487]
- rowheader "申请金额" [ref=e488]
- cell "1000 元" [ref=e489]
- rowgroup [ref=e490]:
- row "贷款用途 消费 借款期限 1" [ref=e491]:
- rowheader "贷款用途" [ref=e492]
- cell "消费" [ref=e493]
- rowheader "借款期限" [ref=e494]
- cell "1" [ref=e495]
- rowgroup [ref=e496]:
- row "是否有经营佐证 否 循环功能 否" [ref=e497]:
- rowheader "是否有经营佐证" [ref=e498]
- cell "否" [ref=e499]
- rowheader "循环功能" [ref=e500]
- cell "否" [ref=e501]
- rowgroup [ref=e502]:
- row "抵质押类型 - 抵质押物是否三方所有 否" [ref=e503]:
- rowheader "抵质押类型" [ref=e504]
- cell "-" [ref=e505]
- rowheader "抵质押物是否三方所有" [ref=e506]
- cell "否" [ref=e507]
- generic [ref=e508]:
- generic [ref=e511]: 模型输出
- generic [ref=e513]:
- generic [ref=e515]:
- generic [ref=e517] [cursor=pointer]:
- generic [ref=e519] [cursor=pointer]:
- tablist [ref=e521]:
- tab "基本信息" [selected] [ref=e523]
- tab "忠诚度分析" [ref=e524]
- tab "贡献度分析" [ref=e525]
- tab "关联度分析" [ref=e526]
- tab "贷款特征" [ref=e527]
- tab "风险度分析" [ref=e528]
- tab "测算结果" [ref=e529]
- tabpanel "基本信息" [ref=e531]:
- table [ref=e534]:
- rowgroup [ref=e535]:
- row "客户内码 CUST20260121001 客户名称 张*" [ref=e536]:
- rowheader "客户内码" [ref=e537]
- cell "CUST20260121001" [ref=e538]
- rowheader "客户名称" [ref=e539]
- cell "张*" [ref=e540]
- rowgroup [ref=e541]:
- row "证件类型 身份证 证件号码 3301********1234" [ref=e542]:
- rowheader "证件类型" [ref=e543]
- cell "身份证" [ref=e544]
- rowheader "证件号码" [ref=e545]
- cell "3301********1234" [ref=e546]
- rowgroup [ref=e547]:
- row "基准利率 4.35 %" [ref=e548]:
- rowheader "基准利率" [ref=e549]
- cell "4.35 %" [ref=e550]
- text:

View File

@@ -0,0 +1,18 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "上虞利率定价系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]
- img [ref=e18]
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1 @@
- generic [ref=e2]:

View File

@@ -0,0 +1,28 @@
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "若依管理系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]: admin
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]: admin123
- img [ref=e18]
- generic [ref=e21]:
- generic [ref=e22]:
- textbox "验证码" [ref=e23]
- img [ref=e25]
- generic [ref=e27]:
- img
- generic [ref=e28] [cursor=pointer]:
- generic [ref=e29]:
- checkbox "记住密码"
- generic [ref=e31]: 记住密码
- button "登 录" [ref=e34] [cursor=pointer]:
- generic [ref=e35]: 登 录
- generic [ref=e36]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:
- alert [ref=e37]:
- generic [ref=e38]:
- paragraph [ref=e39]: 系统接口500异常

View File

@@ -0,0 +1,18 @@
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- heading "若依管理系统" [level=3] [ref=e5]
- generic [ref=e8]:
- textbox "账号" [ref=e9]: admin
- img [ref=e11]
- generic [ref=e15]:
- textbox "密码" [ref=e16]: admin123
- img [ref=e18]
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- checkbox "记住密码"
- generic [ref=e23]: 记住密码
- button "登 录" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: 登 录
- generic [ref=e28]: Copyright © 2018-2026 RuoYi. All Rights Reserved.
- text:

View File

@@ -1,11 +1,11 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.1</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.9.2</h1>
<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.1-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.9.2-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
@@ -13,16 +13,37 @@
若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
* 本仓库为RuoYi-Vue的Spring Boot 2 的版本,保持同步更新。
* 前端采用Vue、Element UI。
* 后端采用Spring Boot、Spring Security、Redis & Jwt。
* 权限认证使用Jwt支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
* 高效率开发,使用代码生成器可以一键生成前后端代码。
* 提供了技术栈([Vue3](https://v3.cn.vuejs.org) [Element Plus](https://element-plus.org/zh-CN) [Vite](https://cn.vitejs.dev))版本[RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3),保持同步更新。
* 提供了单应用版本[RuoYi-Vue-fast](https://gitcode.com/yangzongzhuan/RuoYi-Vue-fast)Oracle版本[RuoYi-Vue-Oracle](https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle),保持同步更新。
* 不分离版本,请移步[RuoYi](https://gitee.com/y_project/RuoYi),微服务版本,请移步[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
# 版本分支
RuoYi-Vue 后端项目提供 Spring Boot 2.x / 3.x / 4.x 多版本分支的并行维护。
| 名称 | 说明 | 地址 |
| :---------------- | :------------------------ | :------------------------------------------------------ |
| master 默认分支 | Spring Boot 4.x (JDK 17+) | https://gitee.com/y_project/RuoYi-Vue |
| springboot3 分支 | Spring Boot 3.x (JDK 17+) | https://gitee.com/y_project/RuoYi-Vue/tree/springboot3 |
| springboot2 分支 | Spring Boot 2.x (JDK 8+) | https://gitee.com/y_project/RuoYi-Vue/tree/springboot2 |
RuoYi-Vue 前端项目提供 Vue 2.x / 3.x / JavaScript TypeScript 版本均可混用搭配
| 项目名称 | **RuoYi-Vue** | **RuoYi-Vue3** | **RuoYi-Vue3-TypeScript** |
| :--- | :--- | :--- | :--- |
| **前端框架** | Vue 2 | Vue 3 | Vue 3 |
| **脚本语言** | JavaScript | JavaScript | TypeScript |
| **构建工具** | Vue CLI | Vite | Vite |
| **UI 组件库** | Element UI | Element Plus | Element Plus |
| **状态管理** | Vuex | Pinia | Pinia |
| **路由管理** | Vue Router 3 | Vue Router 4 | Vue Router 4 |
| **核心特点** | 1. 技术栈经典稳定<br>2. 社区资料丰富<br>3. 当前维护重心已转移 | 1. 现代前端技术栈<br>2. 开发体验与性能更优<br>3. 官方主推的活跃版本 | 1. 类型加持,减少沟通成本<br>2. 开发时有提示,效率更高<br>3. 多人协作企业级开发项目 |
| **仓库地址** | [RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) | [RuoYi-Vue3](https://gitcode.com/yangzongzhuan/RuoYi-Vue3) | [RuoYi-Vue3-TypeScript](https://gitcode.com/yangzongzhuan/RuoYi-Vue3/tree/typescript) |
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
@@ -92,4 +113,4 @@
## 若依前后端分离交流群
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) 点击按钮入群。
QQ群 [![加入QQ群](https://img.shields.io/badge/已满-937441-blue.svg)](https://jq.qq.com/?_wv=1027&k=5bVB1og) [![加入QQ群](https://img.shields.io/badge/已满-887144332-blue.svg)](https://jq.qq.com/?_wv=1027&k=5eiA4DH) [![加入QQ群](https://img.shields.io/badge/已满-180251782-blue.svg)](https://jq.qq.com/?_wv=1027&k=5AxMKlC) [![加入QQ群](https://img.shields.io/badge/已满-104180207-blue.svg)](https://jq.qq.com/?_wv=1027&k=51G72yr) [![加入QQ群](https://img.shields.io/badge/已满-186866453-blue.svg)](https://jq.qq.com/?_wv=1027&k=VvjN2nvu) [![加入QQ群](https://img.shields.io/badge/已满-201396349-blue.svg)](https://jq.qq.com/?_wv=1027&k=5vYAqA05) [![加入QQ群](https://img.shields.io/badge/已满-101456076-blue.svg)](https://jq.qq.com/?_wv=1027&k=kOIINEb5) [![加入QQ群](https://img.shields.io/badge/已满-101539465-blue.svg)](https://jq.qq.com/?_wv=1027&k=UKtX5jhs) [![加入QQ群](https://img.shields.io/badge/已满-264312783-blue.svg)](https://jq.qq.com/?_wv=1027&k=EI9an8lJ) [![加入QQ群](https://img.shields.io/badge/已满-167385320-blue.svg)](https://jq.qq.com/?_wv=1027&k=SWCtLnMz) [![加入QQ群](https://img.shields.io/badge/已满-104748341-blue.svg)](https://jq.qq.com/?_wv=1027&k=96Dkdq0k) [![加入QQ群](https://img.shields.io/badge/已满-160110482-blue.svg)](https://jq.qq.com/?_wv=1027&k=0fsNiYZt) [![加入QQ群](https://img.shields.io/badge/已满-170801498-blue.svg)](https://jq.qq.com/?_wv=1027&k=7xw4xUG1) [![加入QQ群](https://img.shields.io/badge/已满-108482800-blue.svg)](https://jq.qq.com/?_wv=1027&k=eCx8eyoJ) [![加入QQ群](https://img.shields.io/badge/已满-101046199-blue.svg)](https://jq.qq.com/?_wv=1027&k=SpyH2875) [![加入QQ群](https://img.shields.io/badge/已满-136919097-blue.svg)](https://jq.qq.com/?_wv=1027&k=tKEt51dz) [![加入QQ群](https://img.shields.io/badge/已满-143961921-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=0vBbSb0ztbBgVtn3kJS-Q4HUNYwip89G&authKey=8irq5PhutrZmWIvsUsklBxhj57l%2F1nOZqjzigkXZVoZE451GG4JHPOqW7AW6cf0T&noverify=0&group_code=143961921) [![加入QQ群](https://img.shields.io/badge/已满-174951577-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=ZFAPAbp09S2ltvwrJzp7wGlbopsc0rwi&authKey=HB2cxpxP2yspk%2Bo3WKTBfktRCccVkU26cgi5B16u0KcAYrVu7sBaE7XSEqmMdFQp&noverify=0&group_code=174951577) [![加入QQ群](https://img.shields.io/badge/已满-161281055-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Fn2aF5IHpwsy8j6VlalNJK6qbwFLFHat&authKey=uyIT%2B97x2AXj3odyXpsSpVaPMC%2Bidw0LxG5MAtEqlrcBcWJUA%2FeS43rsF1Tg7IRJ&noverify=0&group_code=161281055) [![加入QQ群](https://img.shields.io/badge/已满-138988063-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=XIzkm_mV2xTsUtFxo63bmicYoDBA6Ifm&authKey=dDW%2F4qsmw3x9govoZY9w%2FoWAoC4wbHqGal%2BbqLzoS6VBarU8EBptIgPKN%2FviyC8j&noverify=0&group_code=138988063) [![加入QQ群](https://img.shields.io/badge/已满-151450850-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=DkugnCg68PevlycJSKSwjhFqfIgrWWwR&authKey=pR1Pa5lPIeGF%2FFtIk6d%2FGB5qFi0EdvyErtpQXULzo03zbhopBHLWcuqdpwY241R%2F&noverify=0&group_code=151450850) [![加入QQ群](https://img.shields.io/badge/已满-224622315-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=F58bgRa-Dp-rsQJThiJqIYv8t4-lWfXh&authKey=UmUs4CVG5OPA1whvsa4uSespOvyd8%2FAr9olEGaWAfdLmfKQk%2FVBp2YU3u2xXXt76&noverify=0&group_code=224622315) [![加入QQ群](https://img.shields.io/badge/已满-287842588-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=Nxb2EQ5qozWa218Wbs7zgBnjLSNk_tVT&authKey=obBKXj6SBKgrFTJZx0AqQnIYbNOvBB2kmgwWvGhzxR67RoRr84%2Bus5OadzMcdJl5&noverify=0&group_code=287842588) [![加入QQ群](https://img.shields.io/badge/已满-187944233-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=numtK1M_I4eVd2Gvg8qtbuL8JgX42qNh&authKey=giV9XWMaFZTY%2FqPlmWbkB9g3fi0Ev5CwEtT9Tgei0oUlFFCQLDp4ozWRiVIzubIm&noverify=0&group_code=187944233) [![加入QQ群](https://img.shields.io/badge/已满-228578329-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=G6r5KGCaa3pqdbUSXNIgYloyb8e0_L0D&authKey=4w8tF1eGW7%2FedWn%2FHAypQksdrML%2BDHolQSx7094Agm7Luakj9EbfPnSTxSi2T1LQ&noverify=0&group_code=228578329) [![加入QQ群](https://img.shields.io/badge/已满-191164766-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=GsOo-OLz53J8y_9TPoO6XXSGNRTgbFxA&authKey=R7Uy%2Feq%2BZsoKNqHvRKhiXpypW7DAogoWapOawUGHokJSBIBIre2%2FoiAZeZBSLuBc&noverify=0&group_code=191164766) [![加入QQ群](https://img.shields.io/badge/已满-174569686-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=PmYavuzsOthVqfdAPbo4uAeIbu7Ttjgc&authKey=p52l8%2FXa4PS1JcEmS3VccKSwOPJUZ1ZfQ69MEKzbrooNUljRtlKjvsXf04bxNp3G&noverify=0&group_code=174569686) [![加入QQ群](https://img.shields.io/badge/127358632-blue.svg)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=M9y5NjAl44lAL_Vh2crmEehZU_PMU6KS&authKey=ZSDz8hEREWSaPuxQV3gEwqGIaGjfRNnkB4rJjf0IvXhrSUGSGwQFmBA%2Boe8HFxyl&noverify=0&group_code=127358632) 点击按钮入群。

BIN
bin/.DS_Store vendored

Binary file not shown.

0
bin/prod/restart_java_test.sh Normal file → Executable file
View File

0
bin/restart_java_backend_test.sh Normal file → Executable file
View File

View File

@@ -0,0 +1,43 @@
# 本地 Tomcat 与 TongWeb 打包并存后端实施计划
## 目标
- 恢复本地 `jar + 内嵌 Tomcat` 启动链路
- 保留服务器 `war + TongWeb` 部署链路
- 一次 `mvn package` 同时产出 `ruoyi-admin.jar``ruoyi-admin.war`
## 改动范围
- Maven 打包配置
- 后端启动与部署脚本
- 脚本测试
- 相关运行文档和实施记录
## 实施步骤
1. 先修改脚本测试,重新定义目标行为
- 本地测试脚本期望 `restart_java_backend.sh` 使用 `java -jar`
- 生产测试脚本继续期望 TongWeb 使用 `war`
2. 调整 Maven 打包配置
- `ruoyi-admin` 恢复主产物 `jar`
- 增加附加 `war` 产物
- 恢复本地运行所需的内嵌 Tomcat 依赖
3. 调整脚本
- 本地脚本改回管理 `ruoyi-admin.jar`
- 生产脚本继续管理 `ruoyi-admin.war`
4. 更新文档
- 更新运行说明
- 新增本次实施记录
5. 执行验证
## 验证要求
- `sh bin/restart_java_backend_test.sh`
- `sh bin/prod/restart_java_test.sh`
- `sh bin/prod/deploy_from_package_test.sh`
- `sh -n bin/restart_java_backend.sh`
- `sh -n bin/prod/restart_java.sh`
- `sh -n bin/prod/deploy_from_package.sh`
- `mvn -pl ruoyi-admin -am package -DskipTests`
- 确认 `ruoyi-admin/target/ruoyi-admin.jar`
- 确认 `ruoyi-admin/target/ruoyi-admin.war`

View File

@@ -0,0 +1,147 @@
# 本地 Tomcat 运行与 TongWeb 打包并存设计
## 背景
当前项目已经被调整为统一的 TongWeb `war` 交付模式,这会导致本地开发时也必须围绕 TongWeb 组织启动流程,不符合当前开发诉求。
本次目标是同时保留两条链路:
- 本地开发运行继续使用内嵌 Tomcat
- 打包交付继续支持服务器上的 TongWeb
并且要求一次 `mvn package` 同时产出本地运行所需的 `jar` 和服务器部署所需的 `war`
## 目标
- 保留 `IDEA``mvn spring-boot:run`、本地脚本直启后端的开发体验
- 保留面向 TongWeb 的 `war` 交付方式
- `mvn package` 后同时得到 `ruoyi-admin.jar``ruoyi-admin.war`
- 本地不强依赖安装 TongWeb
- 服务器部署脚本继续只消费 `war`
## 非目标
- 不新增第二个后端启动模块
- 不拆分额外的部署工程
- 不修改前端构建方式
- 不引入“兼容模式”“降级模式”之类额外分支逻辑
## 设计方案
### 1. 构建产物设计
`ruoyi-admin` 恢复为以 `jar` 为主产物的 Spring Boot 应用,用于本地开发运行。
在同一个 Maven 模块中补充 `war` 打包步骤,使一次 `mvn package` 后同时得到:
- `ruoyi-admin/target/ruoyi-admin.jar`
- `ruoyi-admin/target/ruoyi-admin.war`
这样本地和服务器都从同一套源码构建,但消费不同产物:
- 本地消费 `jar`
- 服务器消费 `war`
### 2. 依赖设计
为了保证本地可继续走内嵌 Tomcat
- 恢复 Web 模块中的内嵌 Tomcat 依赖链
- 保持 `spring-boot:run``java -jar` 均可正常工作
为了保证 TongWeb 外部容器部署:
- 打出的 `war` 不能把容器自身实现错误打包成部署冲突形式
- `Servlet API` 继续按外部容器提供的思路处理
本质上,本地运行和 TongWeb 部署共享同一套业务代码,但运行容器不同。
### 3. 启动脚本设计
#### 本地脚本
`bin/restart_java_backend.sh` 恢复为本地开发脚本:
- 执行 Maven 打包
- 使用 `ruoyi-admin.jar`
- 通过 `java -jar` 管理本地后端进程
这条链路不再依赖 `TONGWEB_HOME`
#### 生产脚本
以下脚本保持 TongWeb 交付模型:
- `bin/prod/restart_java.sh`
- `bin/prod/deploy_from_package.sh`
- `bin/prod/deploy_release.sh`
它们继续只处理 `ruoyi-admin.war`,不回退到 `jar`
### 4. 本地与服务器联调设计
本地开发时不要求本机安装 TongWeb。
如果需要验证 TongWeb 运行环境,只通过两种方式完成:
- 打包后部署到服务器 TongWeb 验证
- 本地系统直接调用服务器上已部署的 TongWeb 地址联调
这意味着:
- 本地开发链路只围绕 `jar + Tomcat`
- 服务器部署链路只围绕 `war + TongWeb`
## 验证方案
### 构建验证
执行:
```sh
mvn -pl ruoyi-admin -am package -DskipTests
```
确认同时存在:
- `ruoyi-admin/target/ruoyi-admin.jar`
- `ruoyi-admin/target/ruoyi-admin.war`
### 本地运行验证
执行:
```sh
sh bin/restart_java_backend.sh restart
```
确认本地以 `java -jar` 正常运行。
### TongWeb 脚本验证
执行:
```sh
sh bin/prod/restart_java_test.sh
sh bin/prod/deploy_from_package_test.sh
```
确认 TongWeb 侧仍围绕 `war` 工作。
## 影响范围
- `ruoyi-admin` Maven 打包配置
- Web 相关模块的容器依赖声明
- 本地后端脚本
- 生产 TongWeb 脚本
- 运行文档与实施记录
## 结论
本方案采用最短路径实现“双产物、双运行链路并存”:
- 本地运行继续走内嵌 Tomcat
- 服务器部署继续走 TongWeb
- 一次打包同时产出 `jar``war`
在不新增模块、不扩散复杂度的前提下,满足开发与部署两端的实际需要。

View File

@@ -0,0 +1,39 @@
# 东方通替换 Tomcat 后端实施计划
## 目标
- 将后端交付形态从内嵌 Tomcat 的 `jar` 调整为部署到东方通 TongWeb 的 `war`
- 清理当前发布链路中围绕 `java -jar` / `ruoyi-admin.jar` 的脚本约定
- 保持现有前端发布方式和 Nginx 入口不变,后端仍沿用 `63310` 作为反向代理目标端口
## 改动范围
- Maven 构建
- 调整 `ruoyi-admin` 打包类型为 `war`
- 去除模块链路中的嵌入式 Tomcat 打包依赖
- 明确 Servlet API 由外部容器提供
- 部署脚本
- 将生产部署脚本中的后端产物从 `ruoyi-admin.jar` 切换为 `ruoyi-admin.war`
- 将生产重启脚本从 Java 进程启停改为 TongWeb 容器启停与 `war` 发布
- 调整本地后端重启脚本,使其面向 TongWeb 进行构建和部署
- 运行文档
- 更新本地安装手册中的后端环境说明,改为 TongWeb
- 新增本次改动实施记录
## 实施步骤
1. 先修改现有脚本测试,明确新的 `war + TongWeb` 约束
2. 调整 Maven 配置,产出 `ruoyi-admin.war`
3. 修改生产部署脚本和本地重启脚本
4. 更新运行文档与实施记录
5. 执行脚本测试、语法校验和 Maven 打包验证
## 验证要求
- `mvn -pl ruoyi-admin -am clean package -DskipTests` 成功,且产物为 `ruoyi-admin.war`
- `sh bin/prod/restart_java_test.sh` 成功
- `sh bin/prod/deploy_from_package_test.sh` 成功
- `sh bin/restart_java_backend_test.sh` 成功
- `sh -n bin/prod/restart_java.sh`
- `sh -n bin/prod/deploy_from_package.sh`
- `sh -n bin/restart_java_backend.sh`

View File

@@ -0,0 +1,41 @@
# RuoYi-Vue springboot2 后端迁移实施计划
## 目标
以上游 `RuoYi-Vue/springboot2` 为后端框架基线,将当前项目的后端框架层整体回退并重对齐到 Spring Boot 2 / Java 8同时恢复 `ruoyi-loan-pricing` 业务模块和管理端业务接入配置。
## 范围
-`pom.xml`
- `ruoyi-admin`
- `ruoyi-common`
- `ruoyi-framework`
- `ruoyi-generator`
- `ruoyi-quartz`
- `ruoyi-system`
- `ruoyi-loan-pricing`
- `sql`
## 执行步骤
1. 备份当前后端业务模块与业务配置
2. 用上游 `springboot2` 覆盖根 POM 和基础后端模块
3. 恢复 `ruoyi-loan-pricing` 模块目录
4. 在根 POM 与 `ruoyi-admin/pom.xml` 中重新挂载 `ruoyi-loan-pricing`
5. 恢复 `ruoyi-admin/src/main/resources` 中的 `loan-pricing` 业务配置
6. 检查并修正 `ruoyi-loan-pricing` 中不兼容 Spring Boot 2 的依赖、注解和包引用
7. 校正 Mapper、资源文件和测试依赖保证模块能参与 Maven 聚合构建
8. 保留并整理业务 SQL 脚本
9. 在 Java 8 环境下执行后端编译与关键测试
## 验证要求
- `mvn -pl ruoyi-admin -am test` 至少能够完成依赖解析和关键模块测试
- `mvn -pl ruoyi-admin -am package -DskipTests` 能通过
- `ruoyi-loan-pricing` 模块可被 `ruoyi-admin` 正常引用
## 注意事项
- 不保留 Spring Boot 3 / Java 17 双配置
- 不引入兼容层或过渡层
- 若业务模块使用了 Boot 3 专属依赖,直接改为 Boot 2 可运行实现

View File

@@ -0,0 +1,37 @@
# RuoYi-Vue springboot2 前端迁移实施计划
## 目标
以上游 `RuoYi-Vue/springboot2` 为前端基线,将当前项目前端整体回退并重对齐到上游 `ruoyi-ui`,然后恢复 `loanPricing` 业务页面、接口调用、路由与相关依赖。
## 范围
- `ruoyi-ui/package.json`
- `ruoyi-ui/src`
- `ruoyi-ui/public`
- `ruoyi-ui/build`
- `ruoyi-ui/tests`
## 执行步骤
1. 备份当前 `loanPricing` 页面、接口文件、路由改动和业务测试脚本
2. 用上游 `springboot2``ruoyi-ui` 覆盖当前前端框架层
3. 恢复 `src/views/loanPricing` 页面目录
4. 恢复 `src/api/loanPricing` 接口文件
5. 将业务路由重新挂回 `src/router/index.js`
6. 恢复业务所需的前端依赖与测试脚本
7.`nvm` 切换到合适的 Node 版本后重新安装依赖
8. 执行前端构建与页面联调验证
## 验证要求
- `npm install` 成功
- `npm run build:prod` 成功
- `loanPricing` 页面路由可访问
- 页面基础交互和接口调用链路未丢失
## 注意事项
- 前端直接以 `springboot2` 上游为准,不保留当前非业务性的历史前端定制
- Node 版本必须通过 `nvm` 控制
- 测试完成后要关闭前端调试进程

View File

@@ -0,0 +1,120 @@
# RuoYi-Vue springboot2 基线迁移设计
## 1. 目标
本次迁移以上游 `https://gitee.com/y_project/RuoYi-Vue/tree/springboot2` 为唯一框架基线,先将当前仓库整体回退并重对齐到该基线,再迁回现有业务模块与业务页面,最终形成一个“框架层跟随上游、业务层保留现状”的项目结构。
本次迁移不采用兼容层、补丁层或双栈并存方案,不保留 Spring Boot 3 / Java 17 的框架实现。
## 2. 现状与目标差异
当前仓库已经是 RuoYi 多模块工程,但后端已升级到 `Spring Boot 3.5.x``Java 17`,并引入了以下业务定制:
- 后端业务模块:`ruoyi-loan-pricing`
- 前端业务页面:`ruoyi-ui/src/views/loanPricing`
- 前端业务接口:`ruoyi-ui/src/api/loanPricing`
- 管理端业务接入:`ruoyi-admin` 中的业务依赖与配置
- 业务 SQL`sql/loan_pricing_*``sql/model_*``sql/loan_pricing_menu.sql`
目标上游 `springboot2` 分支采用:
- `RuoYi-Vue 3.9.2`
- `Spring Boot 2.5.15`
- `Java 8`
- `Vue 2 + Element UI`
因此本次迁移的本质是:先将框架层彻底切回 Spring Boot 2 基线,再把利率定价业务重新挂载到新的基线上。
## 3. 迁移范围
### 3.1 框架层
以下内容以上游 `springboot2` 版本为准:
- 根目录框架文件与脚本
-`pom.xml`
- `ruoyi-admin`
- `ruoyi-common`
- `ruoyi-framework`
- `ruoyi-generator`
- `ruoyi-quartz`
- `ruoyi-system`
- `ruoyi-ui`
- 上游自带 `sql` 基础脚本
### 3.2 业务层
以下内容需要从当前仓库迁回到新基线:
- `ruoyi-loan-pricing` 全模块
- `ruoyi-admin` 中与 `loan-pricing` 相关的业务配置、业务依赖
- `ruoyi-ui/src/views/loanPricing` 页面
- `ruoyi-ui/src/api/loanPricing` 接口文件
- `ruoyi-ui/src/router/index.js` 中的业务路由
- 业务 SQL、部署脚本、项目文档
## 4. 实施策略
### 4.1 基线覆盖策略
采用“上游覆盖 + 业务回贴”模式:
1. 先备份当前业务目录与业务配置
2. 将上游 `springboot2` 内容覆盖到当前仓库
3. 恢复业务模块、业务页面、业务配置和业务 SQL
4. 处理 Spring Boot 2 下的编译和运行差异
### 4.2 后端回贴策略
后端只保留一套 Spring Boot 2 实现,不额外保留 Spring Boot 3 兼容写法。若 `ruoyi-loan-pricing` 内存在 Boot 3 / Jakarta / SpringDoc 相关依赖或 API则直接改回 Boot 2 可运行写法。
### 4.3 前端回贴策略
前端以 `springboot2` 上游 `ruoyi-ui` 为基础,回贴 `loanPricing` 页面、接口调用、路由和必要依赖;不保留与当前业务无关的历史定制。
## 5. 风险点与处理原则
### 5.1 框架依赖回退风险
风险:
- `springdoc``jakarta.*`、Boot 3 专用依赖不兼容 Boot 2
- 测试依赖、插件版本、JDK 版本需要一并回退
处理原则:
- 以 Boot 2 上游依赖为准
- 业务模块仅保留完成业务所必需的依赖
### 5.2 业务接入点遗漏风险
风险:
- `ruoyi-admin` 配置遗漏
- 前端路由、菜单、接口路径遗漏
- SQL 脚本与权限菜单脚本遗漏
处理原则:
- 逐类枚举迁移对象
- 迁移后通过编译、页面访问、接口调用进行闭环验证
## 6. 验证标准
迁移完成后至少满足以下条件:
- Maven 多模块在 Spring Boot 2 / Java 8 环境下可编译
- `ruoyi-admin` 可启动
- `ruoyi-ui` 在指定 Node 版本下可安装并构建
- `loanPricing` 页面路由可访问
- 利率定价相关接口类与 Mapper 可通过编译
- 业务 SQL 与菜单脚本仍保留在仓库内
## 7. 产出物
本次任务最终需产出:
- 本设计文档
- 后端实施计划文档
- 前端实施计划文档
- 迁移实施记录文档

View File

@@ -0,0 +1,34 @@
# 本地 Tomcat 与 TongWeb 双产物实施记录
## 本次改动
-`ruoyi-admin` 的主打包方式从 `war` 恢复为 `jar`
- 恢复 `spring-boot-maven-plugin``repackage`,保证本地可直接运行 `ruoyi-admin.jar`
-`ruoyi-admin` 中增加附加 `war` 打包步骤,使 `mvn package` 同时产出:
- `ruoyi-admin.jar`
- `ruoyi-admin.war`
-`war` 打包中排除内嵌 Tomcat 相关 jar避免 TongWeb 部署时容器冲突
-`bin/restart_java_backend.sh` 恢复为本地 `java -jar` 启动链路
- 保持 `bin/prod/restart_java.sh``bin/prod/deploy_from_package.sh` 继续消费 `ruoyi-admin.war`
- 更新 `bin/run.bat`,恢复为本地 `jar` 启动入口
- 新增设计文档 `doc/2026-04-13-local-tomcat-remote-tongweb-design.md`
- 新增实施计划 `doc/2026-04-13-local-tomcat-remote-tongweb-backend-plan.md`
## 验证结果
- 已执行 `sh bin/restart_java_backend_test.sh`
- 已执行 `sh bin/prod/restart_java_test.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 已执行 `sh -n bin/restart_java_backend.sh`
- 已执行 `sh -n bin/prod/restart_java.sh`
- 已执行 `sh -n bin/prod/deploy_from_package.sh`
- 已执行 `mvn -pl ruoyi-admin -am clean package -DskipTests`
- 已确认产物:
- `ruoyi-admin/target/ruoyi-admin.jar`
- `ruoyi-admin/target/ruoyi-admin.war`
## 结果说明
- 本地开发运行继续使用内嵌 Tomcat不要求本机安装 TongWeb
- 服务器部署继续使用 TongWeb只消费 `war`
- 一次打包即可同时得到本地运行产物和 TongWeb 部署产物

View File

@@ -0,0 +1,25 @@
# Tomcat 替换为东方通实施记录
## 本次改动
-`ruoyi-admin` 打包方式从 `jar` 调整为 `war`
-`ruoyi-admin` 中显式声明 `spring-boot-starter-tomcat``provided`
-`ruoyi-framework``ruoyi-loan-pricing` 中排除 `spring-boot-starter-web` 传递进来的嵌入式 Tomcat
-`ruoyi-common` 中的 `jakarta.servlet-api` 调整为 `provided`
- 删除 `application-dev.yml``application-uat.yml``application-pro.yml` 中仅对内嵌 Tomcat 生效的 `server.tomcat` 配置
-`bin/prod/restart_java.sh``java -jar` 启停改为 TongWeb 启停与 `war` 同步
-`bin/prod/deploy_from_package.sh``bin/prod/deploy_release.sh` 的后端交付物从 `ruoyi-admin.jar` 改为 `ruoyi-admin.war`
-`bin/restart_java_backend.sh` 改为本地构建 `war` 并发布到 TongWeb
- 更新 `deploy/2026-03-31-local-nginx-java-install-manual.md`,将后端运行环境说明改为 TongWeb
- 新增后端实施计划 `doc/2026-04-13-tongweb-replace-tomcat-backend-plan.md`
## 验证结果
- 已执行 `sh bin/prod/restart_java_test.sh`
- 已执行 `sh bin/prod/deploy_from_package_test.sh`
- 已执行 `sh bin/restart_java_backend_test.sh`
## 说明
- 本次替换按当前项目 `Spring Boot 3 + Jakarta Servlet` 路线落地,要求实际使用的东方通版本能够承载 Jakarta 体系应用
- Nginx 入口和反向代理端口保持不变,仍通过 `63310` 转发到后端容器

View File

@@ -0,0 +1,105 @@
# RuoYi-Vue springboot2 基线迁移实施记录
## 本次完成内容
-`RuoYi-Vue/springboot2` 为基线覆盖当前仓库的框架层:
-`pom.xml`
- `ruoyi-admin`
- `ruoyi-common`
- `ruoyi-framework`
- `ruoyi-generator`
- `ruoyi-quartz`
- `ruoyi-system`
- `ruoyi-ui`
- `sql` 基础脚本
- 恢复并接回业务模块与业务页面:
- `ruoyi-loan-pricing`
- `ruoyi-ui/src/views/loanPricing`
- `ruoyi-ui/src/api/loanPricing`
- `ruoyi-ui/src/router/index.js` 中的业务路由
- `ruoyi-admin` 中的业务配置
-`ruoyi-loan-pricing` 从 Boot 3 / Java 17 写法回退到 Boot 2 / Java 8 可编译形态:
- 移除 `springdoc` 注解和依赖
-`jakarta.*` 改回 `javax.*`
-`String.repeat` 改为 Java 8 兼容实现
- 将模型调用改为模块内 `RestTemplate + form-urlencoded`
- 补回当前项目的“无 Redis”本地缓存实现
- `InMemoryCacheEntry`
- `InMemoryCacheStats`
- `InMemoryCacheStore`
- 本地缓存版 `RedisCache`
- 本地缓存版 `CacheController`
- 调整前端业务依赖与脚本:
- 恢复 `crypto-js`
- 恢复 `splitpanes`
- 恢复 `ruoyi-ui/tests` 下 4 个业务测试脚本
- 恢复项目原有部署辅助脚本:
- `bin/prod/*.sh`
- `bin/restart_java_backend*.sh`
- 修正本地启动所需配置:
- `logback.xml` 日志目录改为项目内 `logs`
- `application.yml` 补齐 `swagger``redis``mybatis`
- 增加 `ruoyi-loan-pricing` 模块挂载和依赖声明
## 验证结果
### 后端编译验证
已通过:
- `export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home && export PATH="$JAVA_HOME/bin:$PATH" && mvn -pl ruoyi-admin -am -DskipTests package`
- `export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home && export PATH="$JAVA_HOME/bin:$PATH" && mvn -pl ruoyi-admin -am install -DskipTests`
### 前端构建验证
已通过:
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm install`
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`
### 前后端联调验证
已验证:
- 前端开发服务成功启动于 `http://localhost:1024`
- 浏览器打开 `http://localhost:1024/login`,页面标题显示为“若依管理系统”
- 浏览器点击登录后成功进入 `/index`
- 页面已实际渲染“流程列表”业务页面
- 通过前端代理访问 `http://localhost:1024/dev-api/captchaImage` 返回:
- `{"msg":"操作成功","code":200,"captchaEnabled":false}`
- 源码态后端在 `Java 8 + spring-boot:run` 模式下可启动成功
- 登录接口可成功返回 token
- `POST /login`
## 联调中发现的遗留问题
浏览器进入系统后,页面出现两条后端错误提示:
1. 数据库缺少 `sys_notice_read`
2. `GET /loanPricing/workflow/list` 仍返回 `TooManyResultsException`
其中第 1 项已确认当前仓库 SQL 中已有对应建表语句:
- `sql/ry_20260330.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
说明当前代码迁移已落地,但联调数据库尚未完全补齐到 `springboot2` 基线所需表结构。
第 2 项已经定位到当前运行态仍存在列表查询返回值与 MyBatis 映射结果不一致的问题,表现为:
- `GET /loanPricing/workflow/list?pageNum=1&pageSize=10`
- 返回:`{"msg":"nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 15","code":500}`
## 结论
本次代码迁移已经完成:
- 框架层已切回 `RuoYi-Vue springboot2`
- 业务模块和业务页面已接回
- Java 8 / Spring Boot 2 / Vue 2 的编译与构建链路已打通
- 浏览器已成功进入业务页面与登录链路
当前剩余问题集中在:
- 联调数据库尚未补齐 `sys_notice_read`
- `workflow/list` 运行态查询仍需继续收口

147
pom.xml
View File

@@ -6,54 +6,106 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.9.1</version>
<version>3.9.2</version>
<name>ruoyi</name>
<url>http://www.ruoyi.vip</url>
<description>若依管理系统</description>
<properties>
<ruoyi.version>3.9.1</ruoyi.version>
<ruoyi.version>3.9.2</ruoyi.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<mybatis-spring-boot.version>3.0.5</mybatis-spring-boot.version>
<druid.version>1.2.27</druid.version>
<spring-boot.version>2.5.15</spring-boot.version>
<druid.version>1.2.28</druid.version>
<yauaa.version>7.32.0</yauaa.version>
<swagger.version>3.0.0</swagger.version>
<kaptcha.version>2.3.3</kaptcha.version>
<pagehelper.boot.version>2.1.1</pagehelper.boot.version>
<fastjson.version>2.0.60</fastjson.version>
<oshi.version>6.9.1</oshi.version>
<pagehelper.boot.version>1.4.7</pagehelper.boot.version>
<fastjson.version>2.0.61</fastjson.version>
<oshi.version>6.10.0</oshi.version>
<commons.io.version>2.21.0</commons.io.version>
<poi.version>4.1.2</poi.version>
<velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version>
<quartz.version>2.5.2</quartz.version>
<mysql.version>8.2.0</mysql.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<jakarta.version>6.0.0</jakarta.version>
<springdoc.version>2.8.14</springdoc.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<jsqlparser.version>4.5</jsqlparser.version>
<!-- override dependency version -->
<tomcat.version>9.0.112</tomcat.version>
<logback.version>1.2.13</logback.version>
<spring-security.version>5.7.14</spring-security.version>
<spring-framework.version>5.3.39</spring-framework.version>
</properties>
<!-- 依赖声明 -->
<dependencyManagement>
<dependencies>
<!-- 覆盖SpringFramework的依赖配置-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring-framework.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 覆盖SpringSecurity的依赖配置-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>${spring-security.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.5.8</version>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 覆盖logback的依赖配置-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- 覆盖tomcat的依赖配置-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
@@ -71,30 +123,6 @@
<version>${pagehelper.boot.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.version}</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${jakarta.version}</version>
</dependency>
<!-- 获取系统信息 -->
<dependency>
<groupId>com.github.oshi</groupId>
@@ -102,11 +130,17 @@
<version>${oshi.version}</version>
</dependency>
<!-- spring-doc -->
<!-- Swagger3依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- io常用工具类 -->
@@ -130,13 +164,6 @@
<version>${velocity.version}</version>
</dependency>
<!-- 定时任务 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
@@ -200,6 +227,18 @@
<version>${ruoyi.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -219,19 +258,13 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.1</version>
<configuration>
<parameters>true</parameters>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>

BIN
ruoyi-admin/.DS_Store vendored

Binary file not shown.

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
<version>3.9.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
@@ -24,16 +24,23 @@
<optional>true</optional> <!-- 表示依赖不会传递 -->
</dependency>
<!-- spring-doc -->
<!-- swagger3-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误排除3.0.0中的引用手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.6.2</version>
</dependency>
<!-- Mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 核心模块-->
@@ -60,12 +67,6 @@
<artifactId>ruoyi-loan-pricing</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -73,9 +74,9 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.5.4</version>
<version>2.5.15</version>
<configuration>
<addResources>true</addResources>
<fork>true</fork> <!-- 如果没有该配置devtools不会生效 -->
</configuration>
<executions>
<execution>

View File

@@ -3,9 +3,9 @@ package com.ruoyi.web.controller.common;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import jakarta.annotation.Resource;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;

View File

@@ -2,8 +2,8 @@ package com.ruoyi.web.controller.common;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -7,6 +7,13 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysCache;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -15,33 +22,24 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 缓存监控
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/cache")
public class CacheController
{
private final RedisCache redisCache;
private final static List<SysCache> caches = new ArrayList<SysCache>();
private static final List<SysCache> CACHES = new ArrayList<SysCache>();
static
{
caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
CACHES.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
CACHES.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
CACHES.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
CACHES.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
CACHES.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
CACHES.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
CACHES.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
}
public CacheController(RedisCache redisCache)
@@ -55,23 +53,23 @@ public class CacheController
{
InMemoryCacheStats stats = redisCache.getCacheStats();
Map<String, Object> info = new LinkedHashMap<>();
info.put("cache_type", stats.cacheType());
info.put("cache_mode", stats.mode());
info.put("key_size", stats.keySize());
info.put("hit_count", stats.hitCount());
info.put("miss_count", stats.missCount());
info.put("expired_count", stats.expiredCount());
info.put("write_count", stats.writeCount());
info.put("cache_type", stats.getCacheType());
info.put("cache_mode", stats.getMode());
info.put("key_size", stats.getKeySize());
info.put("hit_count", stats.getHitCount());
info.put("miss_count", stats.getMissCount());
info.put("expired_count", stats.getExpiredCount());
info.put("write_count", stats.getWriteCount());
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", stats.keySize());
result.put("dbSize", stats.getKeySize());
List<Map<String, String>> pieList = new ArrayList<>();
pieList.add(statEntry("hit_count", stats.hitCount()));
pieList.add(statEntry("miss_count", stats.missCount()));
pieList.add(statEntry("expired_count", stats.expiredCount()));
pieList.add(statEntry("write_count", stats.writeCount()));
pieList.add(statEntry("hit_count", stats.getHitCount()));
pieList.add(statEntry("miss_count", stats.getMissCount()));
pieList.add(statEntry("expired_count", stats.getExpiredCount()));
pieList.add(statEntry("write_count", stats.getWriteCount()));
result.put("commandStats", pieList);
return AjaxResult.success(result);
}
@@ -80,7 +78,7 @@ public class CacheController
@GetMapping("/getNames")
public AjaxResult cache()
{
return AjaxResult.success(caches);
return AjaxResult.success(CACHES);
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@@ -139,9 +137,9 @@ public class CacheController
{
return StringUtils.EMPTY;
}
if (cacheValue instanceof String stringValue)
if (cacheValue instanceof String)
{
return stringValue;
return (String) cacheValue;
}
return JSON.toJSONString(cacheValue);
}

View File

@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;

View File

@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.monitor;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;

View File

@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;

View File

@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -110,6 +111,20 @@ public class SysDeptController extends BaseController
return toAjax(deptService.updateDept(dept));
}
/**
* 保存部门排序
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "保存部门排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] deptIds = params.get("deptIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
deptService.updateDeptSort(deptIds, orderNums);
return success();
}
/**
* 删除部门
*/

View File

@@ -2,7 +2,7 @@ package com.ruoyi.web.controller.system;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;

View File

@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;

View File

@@ -1,10 +1,17 @@
package com.ruoyi.web.controller.system;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
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.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
/**
* 首页
@@ -18,6 +25,9 @@ public class SysIndexController
@Autowired
private RuoYiConfig ruoyiConfig;
@Autowired
private ISysUserService userService;
/**
* 访问首页,提示语
*/
@@ -26,4 +36,29 @@ public class SysIndexController
{
return StringUtils.format("欢迎使用{}后台管理框架当前版本v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
}
/**
* 解锁屏幕
*/
@PostMapping("/unlockscreen")
public AjaxResult unlockScreen(@RequestBody Map<String, String> body)
{
String password = body.get("password");
if (StringUtils.isEmpty(password))
{
return AjaxResult.error("密码不能为空");
}
String username = SecurityUtils.getUsername();
SysUser user = userService.selectUserByUserName(username);
if (user == null)
{
return AjaxResult.error("服务器超时,请重新登录");
}
if (!SecurityUtils.matchesPassword(password, user.getPassword()))
{
return AjaxResult.error("密码错误,请重新输入");
}
return AjaxResult.success("解锁成功");
}
}

View File

@@ -18,7 +18,6 @@ import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.framework.web.service.TokenService;
@@ -48,9 +47,6 @@ public class SysLoginController
@Autowired
private ISysConfigService configService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 登录方法
*
@@ -61,7 +57,6 @@ public class SysLoginController
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
loginBody.setPassword(passwordTransferCryptoService.decrypt(loginBody.getPassword()));
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
@@ -69,22 +64,6 @@ public class SysLoginController
return ajax;
}
/**
* 登录方法
* 该方法处理用户登录请求,无需验证码
* @param loginBody 登录信息,包含用户名和密码
* @return 结果
*/
@PostMapping("/login/test")
public AjaxResult loginWithoutCaptcha (@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.loginWithoutCaptcha(loginBody.getUsername(), loginBody.getPassword());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*

View File

@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@@ -129,6 +130,20 @@ public class SysMenuController extends BaseController
return toAjax(menuService.updateMenu(menu));
}
/**
* 保存菜单排序
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@Log(title = "保存菜单排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] menuIds = params.get("menuIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
menuService.updateMenuSort(menuIds, orderNums);
return success();
}
/**
* 删除菜单
*/

View File

@@ -11,13 +11,16 @@ 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.ResponseBody;
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.core.text.Convert;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysNotice;
import com.ruoyi.system.service.ISysNoticeReadService;
import com.ruoyi.system.service.ISysNoticeService;
/**
@@ -32,6 +35,9 @@ public class SysNoticeController extends BaseController
@Autowired
private ISysNoticeService noticeService;
@Autowired
private ISysNoticeReadService noticeReadService;
/**
* 获取通知公告列表
*/
@@ -78,6 +84,46 @@ public class SysNoticeController extends BaseController
return toAjax(noticeService.updateNotice(notice));
}
/**
* 首页顶部公告列表返回全部正常公告带当前用户已读标记最多5条
*/
@GetMapping("/listTop")
@ResponseBody
public AjaxResult listTop()
{
Long userId = getUserId();
List<SysNotice> list = noticeReadService.selectNoticeListWithReadStatus(userId, 5);
long unreadCount = list.stream().filter(n -> !n.getIsRead()).count();
AjaxResult result = AjaxResult.success(list);
result.put("unreadCount", unreadCount);
return result;
}
/**
* 标记公告已读
*/
@PostMapping("/markRead")
@ResponseBody
public AjaxResult markRead(Long noticeId)
{
Long userId = getUserId();
noticeReadService.markRead(noticeId, userId);
return success();
}
/**
* 批量标记已读
*/
@PostMapping("/markReadAll")
@ResponseBody
public AjaxResult markReadAll(String ids)
{
Long userId = getUserId();
Long[] noticeIds = Convert.toLongArray(ids);
noticeReadService.markReadBatch(userId, noticeIds);
return success();
}
/**
* 删除通知公告
*/
@@ -86,6 +132,7 @@ public class SysNoticeController extends BaseController
@DeleteMapping("/{noticeIds}")
public AjaxResult remove(@PathVariable Long[] noticeIds)
{
noticeReadService.deleteByNoticeIds(noticeIds);
return toAjax(noticeService.deleteNoticeByIds(noticeIds));
}
}

View File

@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;

View File

@@ -23,7 +23,6 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
@@ -42,9 +41,6 @@ public class SysProfileController extends BaseController
@Autowired
private TokenService tokenService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 个人信息
*/
@@ -96,8 +92,8 @@ public class SysProfileController extends BaseController
@PutMapping("/updatePwd")
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
{
String oldPassword = passwordTransferCryptoService.decrypt(params.get("oldPassword"));
String newPassword = passwordTransferCryptoService.decrypt(params.get("newPassword"));
String oldPassword = params.get("oldPassword");
String newPassword = params.get("newPassword");
LoginUser loginUser = getLoginUser();
Long userId = loginUser.getUserId();
SysUser user = userService.selectUserById(userId);

View File

@@ -8,7 +8,6 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.RegisterBody;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysRegisterService;
import com.ruoyi.system.service.ISysConfigService;
@@ -26,9 +25,6 @@ public class SysRegisterController extends BaseController
@Autowired
private ISysConfigService configService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
@PostMapping("/register")
public AjaxResult register(@RequestBody RegisterBody user)
{
@@ -36,7 +32,6 @@ public class SysRegisterController extends BaseController
{
return error("当前系统没有开启注册功能!");
}
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
String msg = registerService.register(user);
return StringUtils.isEmpty(msg) ? success() : error(msg);
}

View File

@@ -1,7 +1,7 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;

View File

@@ -2,7 +2,7 @@ package com.ruoyi.web.controller.system;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -27,7 +27,6 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
@@ -54,9 +53,6 @@ public class SysUserController extends BaseController
@Autowired
private ISysPostService postService;
@Autowired
private PasswordTransferCryptoService passwordTransferCryptoService;
/**
* 获取用户列表
*/
@@ -143,7 +139,6 @@ public class SysUserController extends BaseController
return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setCreateBy(getUsername());
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
return toAjax(userService.insertUser(user));
}
@@ -201,7 +196,6 @@ public class SysUserController extends BaseController
{
userService.checkUserAllowed(user);
userService.checkUserDataScope(user.getUserId());
user.setPassword(passwordTransferCryptoService.decrypt(user.getPassword()));
user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
user.setUpdateBy(getUsername());
return toAjax(userService.resetPwd(user));

View File

@@ -15,16 +15,19 @@ import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
/**
* swagger 用户测试方法
*
* @author ruoyi
*/
@Tag(name = "用户信息管理")
@Api("用户信息管理")
@RestController
@RequestMapping("/test/user")
public class TestController extends BaseController
@@ -35,7 +38,7 @@ public class TestController extends BaseController
users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
}
@Operation(summary = "获取用户列表")
@ApiOperation("获取用户列表")
@GetMapping("/list")
public R<List<UserEntity>> userList()
{
@@ -43,10 +46,10 @@ public class TestController extends BaseController
return R.ok(userList);
}
@Operation(summary = "获取用户详细")
@ApiOperation("获取用户详细")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
@GetMapping("/{userId}")
public R<UserEntity> getUser(@PathVariable(name = "userId")
Integer userId)
public R<UserEntity> getUser(@PathVariable Integer userId)
{
if (!users.isEmpty() && users.containsKey(userId))
{
@@ -58,7 +61,13 @@ public class TestController extends BaseController
}
}
@Operation(summary = "新增用户")
@ApiOperation("新增用户")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
@ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
})
@PostMapping("/save")
public R<String> save(UserEntity user)
{
@@ -70,10 +79,9 @@ public class TestController extends BaseController
return R.ok();
}
@Operation(summary = "更新用户")
@ApiOperation("更新用户")
@PutMapping("/update")
public R<String> update(@RequestBody
UserEntity user)
public R<String> update(@RequestBody UserEntity user)
{
if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
{
@@ -88,10 +96,10 @@ public class TestController extends BaseController
return R.ok();
}
@Operation(summary = "删除用户信息")
@ApiOperation("删除用户信息")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
@DeleteMapping("/{userId}")
public R<String> delete(@PathVariable(name = "userId")
Integer userId)
public R<String> delete(@PathVariable Integer userId)
{
if (!users.isEmpty() && users.containsKey(userId))
{
@@ -105,19 +113,19 @@ public class TestController extends BaseController
}
}
@Schema(description = "用户实体")
@ApiModel(value = "UserEntity", description = "用户实体")
class UserEntity
{
@Schema(title = "用户ID")
@ApiModelProperty("用户ID")
private Integer userId;
@Schema(title = "用户名称")
@ApiModelProperty("用户名称")
private String username;
@Schema(title = "用户密码")
@ApiModelProperty("用户密码")
private String password;
@Schema(title = "用户手机")
@ApiModelProperty("用户手机")
private String mobile;
public UserEntity()

View File

@@ -1,15 +1,26 @@
package com.ruoyi.web.core.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.ruoyi.common.config.RuoYiConfig;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
/**
* Swagger2的接口配置
@@ -23,42 +34,92 @@ public class SwaggerConfig
@Autowired
private RuoYiConfig ruoyiConfig;
/** 是否开启swagger */
@Value("${swagger.enabled}")
private boolean enabled;
/** 设置请求的统一前缀 */
@Value("${swagger.pathMapping}")
private String pathMapping;
/**
* 自定义的 OpenAPI 对象
* 创建API
*/
@Bean
public OpenAPI customOpenApi()
public Docket createRestApi()
{
return new OpenAPI().components(new Components()
// 设置认证的请求头
.addSecuritySchemes("apikey", securityScheme()))
.addSecurityItem(new SecurityRequirement().addList("apikey"))
.info(getApiInfo());
return new Docket(DocumentationType.OAS_30)
// 是否启用Swagger
.enable(enabled)
// 用来创建该API的基本信息展示在文档的页面中自定义展示的信息
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 扫描指定包中的swagger注解
// .apis(RequestHandlerSelectors.basePackage("com.ruoyi.project.tool.swagger"))
// 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
/* 设置安全模式swagger可以设置访问token */
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping(pathMapping);
}
@Bean
public SecurityScheme securityScheme()
/**
* 安全模式这里指定token通过Authorization头请求头传递
*/
private List<SecurityScheme> securitySchemes()
{
return new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.name("Authorization")
.in(SecurityScheme.In.HEADER)
.scheme("Bearer");
List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
return apiKeyList;
}
/**
* 安全上下文
*/
private List<SecurityContext> securityContexts()
{
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(
SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(o -> o.requestMappingPattern().matches("/.*"))
.build());
return securityContexts;
}
/**
* 默认的安全上引用
*/
private List<SecurityReference> defaultAuth()
{
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
return securityReferences;
}
/**
* 添加摘要信息
*/
public Info getApiInfo()
private ApiInfo apiInfo()
{
return new Info()
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题若依管理系统_接口文档")
// 描述
.description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact().name(ruoyiConfig.getName()))
.contact(new Contact(ruoyiConfig.getName(), null, null))
// 版本
.version("版本号:" + ruoyiConfig.getVersion());
.version("版本号:" + ruoyiConfig.getVersion())
.build();
}
}

View File

@@ -0,0 +1,61 @@
# 数据源配置
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
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 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

View File

@@ -3,7 +3,7 @@ ruoyi:
# 名称
name: RuoYi
# 版本
version: 3.9.1
version: 3.9.2
# 版权年份
copyrightYear: 2026
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
@@ -49,6 +49,19 @@ spring:
restart:
# 热部署开关
enabled: true
# redis 配置
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 10s
lettuce:
pool:
min-idle: 0
max-idle: 8
max-active: 8
max-wait: -1ms
# token配置
@@ -60,8 +73,8 @@ token:
# 令牌有效期默认30分钟
expireTime: 30
# MyBatis Plus配置
mybatis-plus:
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.**.domain
# 配置mapper的扫描找到所有的mapper.xml映射文件
@@ -75,14 +88,12 @@ pagehelper:
supportMethodsArguments: true
params: count=countSql
# Springdoc配置
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
# Swagger配置
swagger:
# 是否开启swagger
enabled: true
path: /swagger-ui.html
tags-sorter: alpha
# 请求前缀
pathMapping: /dev-api
# 防盗链配置
referer:

View File

@@ -1,19 +0,0 @@
window.onload = function() {
window.ui = SwaggerUIBundle({
url: "/v3/api-docs/default",
configUrl: "/v3/api-docs/swagger-config",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
tagsSorter: "alpha",
validatorUrl: "",
persistAuthorization: true
});
};

View File

@@ -1,44 +0,0 @@
package com.ruoyi.web.controller.monitor;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
import com.ruoyi.common.core.redis.RedisCache;
class CacheControllerTest
{
@Test
void shouldReturnInMemoryCacheSummary() throws Exception
{
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CacheController(redisCache)).build();
mockMvc.perform(get("/monitor/cache"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.info.cache_type").value("IN_MEMORY"))
.andExpect(jsonPath("$.data.info.cache_mode").value("single-instance"));
}
@Test
void shouldClearCacheKeysByPrefix() throws Exception
{
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
redisCache.setCacheObject("login_tokens:a", "A");
redisCache.setCacheObject("login_tokens:b", "B");
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CacheController(redisCache)).build();
mockMvc.perform(delete("/monitor/cache/clearCacheName/login_tokens:"))
.andExpect(status().isOk());
mockMvc.perform(get("/monitor/cache/getKeys/login_tokens:"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").isEmpty());
}
}

View File

@@ -1,40 +0,0 @@
package com.ruoyi.web.controller.system;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysLoginService;
class SysLoginControllerPasswordTransferTest
{
@Test
void shouldDecryptPasswordBeforeCallingLoginService() throws Exception
{
SysLoginService loginService = mock(SysLoginService.class);
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("admin123");
when(loginService.login("admin", "admin123", "1", "u")).thenReturn("token");
SysLoginController controller = new SysLoginController();
ReflectionTestUtils.setField(controller, "loginService", loginService);
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(post("/login")
.contentType("application/json")
.content("{\"username\":\"admin\",\"password\":\"cipher\",\"code\":\"1\",\"uuid\":\"u\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("cipher");
verify(loginService).login("admin", "admin123", "1", "u");
}
}

View File

@@ -1,72 +0,0 @@
package com.ruoyi.web.controller.system;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysUserService;
class SysProfileControllerPasswordTransferTest
{
@AfterEach
void tearDown()
{
SecurityContextHolder.clearContext();
}
@Test
void shouldDecryptPasswordsBeforeCheckingOldPassword() throws Exception
{
ISysUserService userService = mock(ISysUserService.class);
TokenService tokenService = mock(TokenService.class);
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
when(passwordTransferCryptoService.decrypt("oldCipher")).thenReturn("oldPlain");
when(passwordTransferCryptoService.decrypt("newCipher")).thenReturn("newPlain");
when(userService.resetUserPwd(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString()))
.thenReturn(1);
SysUser storedUser = new SysUser();
storedUser.setUserId(2L);
storedUser.setPassword(SecurityUtils.encryptPassword("oldPlain"));
when(userService.selectUserById(2L)).thenReturn(storedUser);
SysUser currentUser = new SysUser();
currentUser.setUserId(2L);
currentUser.setUserName("admin");
LoginUser loginUser = new LoginUser(2L, 1L, currentUser, Collections.emptySet());
SecurityContextHolder.getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
SysProfileController controller = new SysProfileController();
ReflectionTestUtils.setField(controller, "userService", userService);
ReflectionTestUtils.setField(controller, "tokenService", tokenService);
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(put("/system/user/profile/updatePwd")
.contentType("application/json")
.content("{\"oldPassword\":\"oldCipher\",\"newPassword\":\"newCipher\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("oldCipher");
verify(passwordTransferCryptoService).decrypt("newCipher");
verify(userService).resetUserPwd(org.mockito.ArgumentMatchers.eq(2L), org.mockito.ArgumentMatchers.anyString());
verify(tokenService).setLoginUser(loginUser);
}
}

View File

@@ -1,50 +0,0 @@
package com.ruoyi.web.controller.system;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.ruoyi.common.core.domain.model.RegisterBody;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.framework.web.service.SysRegisterService;
import com.ruoyi.system.service.ISysConfigService;
class SysRegisterControllerPasswordTransferTest
{
@Test
void shouldDecryptPasswordBeforeCallingRegisterService() throws Exception
{
SysRegisterService registerService = mock(SysRegisterService.class);
ISysConfigService configService = mock(ISysConfigService.class);
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
when(configService.selectConfigByKey("sys.account.registerUser")).thenReturn("true");
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("admin123");
when(registerService.register(any(RegisterBody.class))).thenReturn("");
SysRegisterController controller = new SysRegisterController();
ReflectionTestUtils.setField(controller, "registerService", registerService);
ReflectionTestUtils.setField(controller, "configService", configService);
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(post("/register")
.contentType("application/json")
.content("{\"username\":\"u1\",\"password\":\"cipher\",\"code\":\"1\",\"uuid\":\"u\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("cipher");
ArgumentCaptor<RegisterBody> captor = ArgumentCaptor.forClass(RegisterBody.class);
verify(registerService).register(captor.capture());
assertEquals("admin123", captor.getValue().getPassword());
}
}

View File

@@ -1,113 +0,0 @@
package com.ruoyi.web.controller.system;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.PasswordTransferCryptoService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
import com.ruoyi.system.service.ISysUserService;
class SysUserControllerPasswordTransferTest
{
@AfterEach
void tearDown()
{
SecurityContextHolder.clearContext();
}
@Test
void shouldDecryptPasswordBeforeAddingUser() throws Exception
{
ISysUserService userService = mock(ISysUserService.class);
ISysRoleService roleService = mock(ISysRoleService.class);
ISysDeptService deptService = mock(ISysDeptService.class);
ISysPostService postService = mock(ISysPostService.class);
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("initPwd");
when(userService.checkUserNameUnique(org.mockito.ArgumentMatchers.any(SysUser.class))).thenReturn(true);
when(userService.insertUser(org.mockito.ArgumentMatchers.any(SysUser.class))).thenReturn(1);
setAuthentication();
SysUserController controller = new SysUserController();
ReflectionTestUtils.setField(controller, "userService", userService);
ReflectionTestUtils.setField(controller, "roleService", roleService);
ReflectionTestUtils.setField(controller, "deptService", deptService);
ReflectionTestUtils.setField(controller, "postService", postService);
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(post("/system/user")
.contentType("application/json")
.content("{\"userName\":\"u1\",\"nickName\":\"n1\",\"deptId\":1,\"password\":\"cipher\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("cipher");
ArgumentCaptor<SysUser> captor = ArgumentCaptor.forClass(SysUser.class);
verify(userService).insertUser(captor.capture());
assertTrue(SecurityUtils.matchesPassword("initPwd", captor.getValue().getPassword()));
}
@Test
void shouldDecryptPasswordBeforeResettingUserPassword() throws Exception
{
ISysUserService userService = mock(ISysUserService.class);
ISysRoleService roleService = mock(ISysRoleService.class);
ISysDeptService deptService = mock(ISysDeptService.class);
ISysPostService postService = mock(ISysPostService.class);
PasswordTransferCryptoService passwordTransferCryptoService = mock(PasswordTransferCryptoService.class);
when(passwordTransferCryptoService.decrypt("cipher")).thenReturn("resetPwd");
when(userService.resetPwd(org.mockito.ArgumentMatchers.any(SysUser.class))).thenReturn(1);
setAuthentication();
SysUserController controller = new SysUserController();
ReflectionTestUtils.setField(controller, "userService", userService);
ReflectionTestUtils.setField(controller, "roleService", roleService);
ReflectionTestUtils.setField(controller, "deptService", deptService);
ReflectionTestUtils.setField(controller, "postService", postService);
ReflectionTestUtils.setField(controller, "passwordTransferCryptoService", passwordTransferCryptoService);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(put("/system/user/resetPwd")
.contentType("application/json")
.content("{\"userId\":2,\"password\":\"cipher\"}"))
.andExpect(status().isOk());
verify(passwordTransferCryptoService).decrypt("cipher");
ArgumentCaptor<SysUser> captor = ArgumentCaptor.forClass(SysUser.class);
verify(userService).resetPwd(captor.capture());
assertTrue(SecurityUtils.matchesPassword("resetPwd", captor.getValue().getPassword()));
}
private void setAuthentication()
{
SysUser currentUser = new SysUser();
currentUser.setUserId(1L);
currentUser.setUserName("admin");
LoginUser loginUser = new LoginUser(1L, 1L, currentUser, Collections.emptySet());
SecurityContextHolder.getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
<version>3.9.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -89,6 +89,18 @@
<artifactId>jaxb-api</artifactId>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
@@ -97,32 +109,8 @@
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
<!-- ruoyi-springboot3 / mybatis-plus 配置 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>

View File

@@ -16,15 +16,25 @@ import java.lang.annotation.Target;
@Documented
public @interface DataScope
{
/**
* 用户表的别名
*/
public String userAlias() default "";
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别
* 用户字段
*/
public String userAlias() default "";
public String userField() default "user_id";
/**
* 部门字段名
*/
public String deptField() default "dept_id";
/**
* 权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取多个权限用逗号分隔开来

View File

@@ -56,7 +56,6 @@ public @interface Excel
/**
* BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
*/
@SuppressWarnings("deprecation")
public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
/**

View File

@@ -170,4 +170,35 @@ public class Constants
*/
public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
"org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" };
/**
* 部门相关常量
*/
public static class Dept
{
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
}
}

View File

@@ -31,6 +31,9 @@ public class GenConstants
/** 上级菜单名称字段 */
public static final String PARENT_MENU_NAME = "parentMenuName";
/** 生成详情页开关 */
public static final String GEN_VIEW = "genView";
/** 数据库字符串类型 */
public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" };

View File

@@ -1,8 +1,27 @@
package com.ruoyi.common.core.cache;
record InMemoryCacheEntry(Object value, Long expireAtMillis)
public class InMemoryCacheEntry
{
boolean isExpired(long now)
private final Object value;
private final Long expireAtMillis;
public InMemoryCacheEntry(Object value, Long expireAtMillis)
{
this.value = value;
this.expireAtMillis = expireAtMillis;
}
public Object getValue()
{
return value;
}
public Long getExpireAtMillis()
{
return expireAtMillis;
}
public boolean isExpired(long now)
{
return expireAtMillis != null && expireAtMillis <= now;
}

View File

@@ -1,12 +1,58 @@
package com.ruoyi.common.core.cache;
public record InMemoryCacheStats(
String cacheType,
String mode,
long keySize,
long hitCount,
long missCount,
long expiredCount,
long writeCount)
public class InMemoryCacheStats
{
private final String cacheType;
private final String mode;
private final long keySize;
private final long hitCount;
private final long missCount;
private final long expiredCount;
private final long writeCount;
public InMemoryCacheStats(String cacheType, String mode, long keySize, long hitCount, long missCount, long expiredCount, long writeCount)
{
this.cacheType = cacheType;
this.mode = mode;
this.keySize = keySize;
this.hitCount = hitCount;
this.missCount = missCount;
this.expiredCount = expiredCount;
this.writeCount = writeCount;
}
public String getCacheType()
{
return cacheType;
}
public String getMode()
{
return mode;
}
public long getKeySize()
{
return keySize;
}
public long getHitCount()
{
return hitCount;
}
public long getMissCount()
{
return missCount;
}
public long getExpiredCount()
{
return expiredCount;
}
public long getWriteCount()
{
return writeCount;
}
}

View File

@@ -1,8 +1,12 @@
package com.ruoyi.common.core.cache;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -10,7 +14,6 @@ import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Component;
@Component
public class InMemoryCacheStore
@@ -36,7 +39,7 @@ public class InMemoryCacheStore
public <T> T get(String key)
{
InMemoryCacheEntry entry = readEntry(key);
return entry == null ? null : (T) entry.value();
return entry == null ? null : (T) entry.getValue();
}
public boolean hasKey(String key)
@@ -63,12 +66,13 @@ public class InMemoryCacheStore
{
purgeExpiredEntries();
Set<String> matchedKeys = new TreeSet<>();
entries.forEach((key, value) -> {
if (matches(pattern, key))
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
{
matchedKeys.add(key);
if (matches(pattern, entry.getKey()))
{
matchedKeys.add(entry.getKey());
}
}
});
return matchedKeys;
}
@@ -76,9 +80,24 @@ public class InMemoryCacheStore
{
Objects.requireNonNull(unit, "TimeUnit must not be null");
long expireAtMillis = System.currentTimeMillis() + Math.max(0L, unit.toMillis(timeout));
return entries.computeIfPresent(key, (cacheKey, entry) -> entry.isExpired(System.currentTimeMillis())
? null
: new InMemoryCacheEntry(entry.value(), expireAtMillis)) != null;
while (true)
{
InMemoryCacheEntry currentEntry = entries.get(key);
if (currentEntry == null)
{
return false;
}
if (currentEntry.isExpired(System.currentTimeMillis()))
{
removeExpiredEntry(key, currentEntry);
return false;
}
InMemoryCacheEntry nextEntry = new InMemoryCacheEntry(currentEntry.getValue(), expireAtMillis);
if (entries.replace(key, currentEntry, nextEntry))
{
return true;
}
}
}
public long getExpire(String key)
@@ -93,11 +112,11 @@ public class InMemoryCacheStore
{
return -2L;
}
if (entry.expireAtMillis() == null)
if (entry.getExpireAtMillis() == null)
{
return -1L;
}
long remainingMillis = Math.max(0L, entry.expireAtMillis() - System.currentTimeMillis());
long remainingMillis = Math.max(0L, entry.getExpireAtMillis() - System.currentTimeMillis());
long unitMillis = Math.max(1L, unit.toMillis(1));
return (remainingMillis + unitMillis - 1) / unitMillis;
}
@@ -109,10 +128,10 @@ public class InMemoryCacheStore
entries.compute(key, (cacheKey, currentEntry) -> {
long now = System.currentTimeMillis();
boolean missingOrExpired = currentEntry == null || currentEntry.isExpired(now);
long nextValue = missingOrExpired ? 1L : toLong(currentEntry.value()) + 1L;
Long expireAtMillis = missingOrExpired || currentEntry.expireAtMillis() == null
long nextValue = missingOrExpired ? 1L : toLong(currentEntry.getValue()) + 1L;
Long expireAtMillis = (missingOrExpired || currentEntry.getExpireAtMillis() == null)
? now + Math.max(0L, unit.toMillis(timeout))
: currentEntry.expireAtMillis();
: currentEntry.getExpireAtMillis();
if (missingOrExpired && currentEntry != null)
{
expiredCount.incrementAndGet();
@@ -142,6 +161,7 @@ public class InMemoryCacheStore
writeCount.get());
}
@SuppressWarnings("unchecked")
public <T> Map<String, T> getMap(String key)
{
Map<String, T> value = get(key);
@@ -178,6 +198,7 @@ public class InMemoryCacheStore
return true;
}
@SuppressWarnings("unchecked")
public <T> Set<T> getSet(String key)
{
Set<T> value = get(key);
@@ -189,15 +210,28 @@ public class InMemoryCacheStore
set(key, new HashSet<>(dataSet));
}
public <T> java.util.List<T> getList(String key)
@SuppressWarnings("unchecked")
public <T> List<T> getList(String key)
{
java.util.List<T> value = get(key);
return value == null ? null : new java.util.ArrayList<>(value);
List<T> value = get(key);
return value == null ? null : new ArrayList<>(value);
}
public <T> void putList(String key, java.util.List<T> dataList)
public <T> void putList(String key, List<T> dataList)
{
set(key, new java.util.ArrayList<>(dataList));
set(key, new ArrayList<>(dataList));
}
private void setWithOptionalTtl(String key, Object value, long ttlMillis)
{
if (ttlMillis > 0)
{
set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
}
else
{
set(key, value);
}
}
private void putEntry(String key, InMemoryCacheEntry entry)
@@ -227,12 +261,13 @@ public class InMemoryCacheStore
private void purgeExpiredEntries()
{
long now = System.currentTimeMillis();
entries.forEach((key, entry) -> {
if (entry.isExpired(now))
for (Map.Entry<String, InMemoryCacheEntry> entry : entries.entrySet())
{
removeExpiredEntry(key, entry);
if (entry.getValue().isExpired(now))
{
removeExpiredEntry(entry.getKey(), entry.getValue());
}
}
});
}
private void removeExpiredEntry(String key, InMemoryCacheEntry expectedEntry)
@@ -258,20 +293,10 @@ public class InMemoryCacheStore
private long toLong(Object value)
{
if (value instanceof Number number)
if (value instanceof Number)
{
return number.longValue();
return ((Number) value).longValue();
}
return Long.parseLong(String.valueOf(value));
}
private void setWithOptionalTtl(String key, Object value, long ttlMillis)
{
if (ttlMillis > 0)
{
set(key, value, ttlMillis, TimeUnit.MILLISECONDS);
return;
}
set(key, value);
}
}

View File

@@ -2,10 +2,10 @@ package com.ruoyi.common.core.domain.entity;
import java.util.ArrayList;
import java.util.List;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.domain.BaseEntity;

View File

@@ -1,7 +1,7 @@
package com.ruoyi.common.core.domain.entity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;

View File

@@ -1,8 +1,8 @@
package com.ruoyi.common.core.domain.entity;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;

View File

@@ -2,9 +2,9 @@ package com.ruoyi.common.core.domain.entity;
import java.util.ArrayList;
import java.util.List;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.domain.BaseEntity;

View File

@@ -1,9 +1,9 @@
package com.ruoyi.common.core.domain.entity;
import java.util.Set;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;

View File

@@ -2,9 +2,10 @@ package com.ruoyi.common.core.domain.entity;
import java.util.Date;
import java.util.List;
import jakarta.validation.constraints.*;
import javax.validation.constraints.*;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.annotation.Excel.ColumnType;
@@ -69,6 +70,7 @@ public class SysUser extends BaseEntity
private String loginIp;
/** 最后登录时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
private Date loginDate;

View File

@@ -1,5 +1,9 @@
package com.ruoyi.common.core.redis;
import com.ruoyi.common.core.cache.InMemoryCacheStats;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -7,15 +11,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.cache.InMemoryCacheStats;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
/**
* 本地缓存门面,保留原有 RedisCache 业务入口。
*
* @author ruoyi
**/
@SuppressWarnings("unchecked")
@Component
public class RedisCache

View File

@@ -3,14 +3,14 @@ package com.ruoyi.common.filter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 防盗链过滤器

View File

@@ -1,13 +1,13 @@
package com.ruoyi.common.filter;
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import com.ruoyi.common.utils.StringUtils;

View File

@@ -4,11 +4,11 @@ import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import com.ruoyi.common.utils.http.HttpHelper;
import com.ruoyi.common.constant.Constants;

View File

@@ -3,14 +3,14 @@ package com.ruoyi.common.filter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.enums.HttpMethod;

View File

@@ -2,10 +2,10 @@ package com.ruoyi.common.filter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

View File

@@ -16,7 +16,6 @@ import org.apache.commons.lang3.time.DateFormatUtils;
*
* @author ruoyi
*/
@SuppressWarnings("deprecation")
public class DateUtils extends org.apache.commons.lang3.time.DateUtils
{
public static String YYYY = "yyyy";

View File

@@ -4,7 +4,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.redis.RedisCache;
@@ -41,14 +41,10 @@ public class DictUtils
*/
public static List<SysDictData> getDictCache(String key)
{
Object cacheObject = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
if (cacheObject instanceof List<?> listCache)
JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
if (StringUtils.isNotNull(arrayCache))
{
return (List<SysDictData>) listCache;
}
if (StringUtils.isNotNull(cacheObject))
{
return JSON.parseArray(JSON.toJSONString(cacheObject), SysDictData.class);
return arrayCache.toList(SysDictData.class);
}
return null;
}

View File

@@ -7,10 +7,10 @@ import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

View File

@@ -15,7 +15,6 @@ import com.ruoyi.common.core.text.StrFormatter;
*
* @author ruoyi
*/
@SuppressWarnings("deprecation")
public class StringUtils extends org.apache.commons.lang3.StringUtils
{
/** 空字符串 */

View File

@@ -1,9 +1,9 @@
package com.ruoyi.common.utils.bean;
import java.util.Set;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
/**
* bean对象属性验证

View File

@@ -9,8 +9,8 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;

View File

@@ -5,7 +5,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.ServletRequest;
import javax.servlet.ServletRequest;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -11,8 +11,6 @@ import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
@@ -23,10 +21,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.MediaType;
/**
* 通用http发送方法
@@ -295,79 +290,4 @@ public class HttpUtils
return true;
}
}
/**
* 发送 POST 请求application/x-www-form-urlencoded 格式)
* @param url 请求地址
* @param params 表单参数
* @param headers 请求头(可为 null
* @param responseType 响应类型
* @param <T> 泛型返回值
* @return 响应结果
*/
public static <T> T doPostFormUrlEncoded(String url, Map<String, String> params, HttpHeaders headers, Class<T> responseType) {
// 构建表单参数
MultiValueMap<String, String> formParams = new LinkedMultiValueMap<>();
if (params != null && !params.isEmpty()) {
formParams.setAll(params);
}
// 构建请求头(指定 Content-Type
HttpHeaders requestHeaders = headers == null ? new HttpHeaders() : headers;
requestHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
requestHeaders.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formParams, requestHeaders);
RestTemplate restTemplate = new RestTemplate();
// 发送请求
try {
ResponseEntity<T> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
responseType
);
log.info("---------------------->POST(form-urlencoded) 请求成功URL{},响应结果:{}", url, response.getBody());
return response.getBody();
} catch (Exception e) {
throw new RuntimeException("POST(form-urlencoded) 请求失败URL" + url + ",异常信息:" + e.getMessage(), e);
}
}
// ========== 新增JSON 格式 POST 请求 ==========
/**
* 发送 POST 请求application/json 格式)
* @param url 请求地址
* @param requestBody 请求体Java 对象,自动序列化为 JSON
* @param headers 请求头(可为 null默认已设置 Content-Type: application/json
* @param responseType 响应类型
* @param <T> 响应泛型类型
* @param <R> 请求体类型
* @return 响应结果
*/
public static <T, R> T doPostJson(String url, R requestBody, HttpHeaders headers, Class<T> responseType) {
// 构建请求头,默认设置 JSON 格式
HttpHeaders requestHeaders = headers == null ? new HttpHeaders() : headers;
// 确保 Content-Type 为 JSON避免外部传入覆盖
if (!requestHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
requestHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
}
// 构建请求实体(请求体 + 请求头)
HttpEntity<R> requestEntity = new HttpEntity<>(requestBody, requestHeaders);
RestTemplate restTemplate = new RestTemplate();
// 发送 JSON POST 请求
try {
ResponseEntity<T> response = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
responseType
);
log.info("---------------------->POST(JSON) 请求成功URL{},响应结果:{}", url, response.getBody());
return response.getBody();
} catch (Exception e) {
throw new RuntimeException("POST(JSON) 请求失败URL" + url + ",异常信息:" + e.getMessage(), e);
}
}
}

View File

@@ -19,7 +19,7 @@ public class AddressUtils
private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
// IP地址查询
public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp";
public static final String IP_URL = "https://whois.pconline.com.cn/ipJson.jsp";
// 未知地址
public static final String UNKNOWN = "XX XX";

View File

@@ -2,7 +2,7 @@ package com.ruoyi.common.utils.ip;
import java.net.InetAddress;
import java.net.UnknownHostException;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;

View File

@@ -0,0 +1,85 @@
package com.ruoyi.common.utils.poi;
import java.util.ArrayList;
import java.util.List;
/**
* 多 Sheet 导出时的数据信息
*
* 使用示例:
* <pre>
* List<ExcelSheet<?>> sheets = new ArrayList<>();
* sheets.add(new ExcelSheet<>("参数数据", configList, Config.class, "参数信息"));
* sheets.add(new ExcelSheet<>("岗位数据", postList, Post.class, "岗位信息"));
* return ExcelUtil.exportMultiSheet(sheets);
* </pre>
*
* @author ruoyi
*/
public class ExcelSheet<T>
{
/** Sheet 名称 */
private String sheetName;
/** 导出数据集合 */
private List<T> list;
/** 数据对应的实体 Class */
private Class<T> clazz;
/** Sheet 顶部大标题(可为空) */
private String title;
public ExcelSheet(String sheetName, List<T> list, Class<T> clazz)
{
this(sheetName, list, clazz, "");
}
public ExcelSheet(String sheetName, List<T> list, Class<T> clazz, String title)
{
this.sheetName = sheetName;
this.list = list != null ? list : new ArrayList<>();
this.clazz = clazz;
this.title = title != null ? title : "";
}
public String getSheetName()
{
return sheetName;
}
public List<T> getList()
{
return list;
}
public Class<T> getClazz()
{
return clazz;
}
public String getTitle()
{
return title;
}
public void setSheetName(String sheetName)
{
this.sheetName = sheetName;
}
public void setList(List<T> list)
{
this.list = list;
}
public void setClazz(Class<T> clazz)
{
this.clazz = clazz;
}
public void setTitle(String title)
{
this.title = title;
}
}

View File

@@ -23,7 +23,7 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -106,6 +106,11 @@ public class ExcelUtil<T>
*/
public Map<String, String> sysDictMap = new HashMap<String, String>();
/**
* 单元格样式缓存
*/
private Map<String, CellStyle> cellStyleCache = new HashMap<String, CellStyle>();
/**
* Excel sheet最大行数默认65536
*/
@@ -418,7 +423,7 @@ public class ExcelUtil<T>
Object val = this.getCellValue(row, entry.getKey());
// 如果不存在实例则新建.
entity = (entity == null ? clazz.getDeclaredConstructor().newInstance() : entity);
entity = (entity == null ? clazz.newInstance() : entity);
// 从map中得到对应列的field.
Field field = (Field) entry.getValue()[0];
Excel attr = (Excel) entry.getValue()[1];
@@ -580,6 +585,117 @@ public class ExcelUtil<T>
exportExcel(response);
}
/**
* 多 Sheet 导出 —— 将多个不同类型的数据集合写入同一 Excel直接输出到 HttpServletResponse
*
* @param response HTTP 响应
* @param sheets Sheet 描述列表
*/
public static void exportMultiSheet(HttpServletResponse response, List<ExcelSheet<?>> sheets)
{
if (sheets == null || sheets.isEmpty())
{
return;
}
SXSSFWorkbook wb = buildWorkbook(sheets);
try
{
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
wb.write(response.getOutputStream());
}
catch (Exception e)
{
log.error("多Sheet导出Excel异常{}", e.getMessage());
}
finally
{
IOUtils.closeQuietly(wb);
}
}
/**
* 多 Sheet 导出 —— 将多个不同类型的数据集合写入同一 Excel生成文件并返回下载地址
*
* @param sheets Sheet 描述列表
* @return AjaxResult含文件下载地址
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static AjaxResult exportMultiSheet(List<ExcelSheet<?>> sheets)
{
if (sheets == null || sheets.isEmpty())
{
return AjaxResult.error("导出数据不能为空");
}
SXSSFWorkbook wb = buildWorkbook(sheets);
OutputStream out = null;
try
{
ExcelUtil firstUtil = new ExcelUtil(sheets.get(0).getClazz());
String filename = firstUtil.encodingFilename(sheets.get(0).getSheetName());
out = new FileOutputStream(firstUtil.getAbsoluteFile(filename));
wb.write(out);
return AjaxResult.success(filename);
}
catch (Exception e)
{
log.error("多Sheet导出Excel异常{}", e.getMessage());
throw new UtilException("导出Excel失败请联系网站管理员");
}
finally
{
IOUtils.closeQuietly(wb);
IOUtils.closeQuietly(out);
}
}
/**
* 构建多 Sheet Workbook —— 创建 SXSSFWorkbook 并将所有 Sheet 数据写入
*
* @param sheets Sheet 描述列表
* @return 已写入所有 Sheet 数据的 SXSSFWorkbook
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private static SXSSFWorkbook buildWorkbook(List<ExcelSheet<?>> sheets)
{
SXSSFWorkbook wb = new SXSSFWorkbook(500);
for (ExcelSheet<?> excelSheet : sheets)
{
ExcelUtil util = new ExcelUtil(excelSheet.getClazz());
util.initWithWorkbook(wb, excelSheet.getList(), excelSheet.getSheetName(), excelSheet.getTitle());
util.writeSheet();
}
return wb;
}
/**
* 使用外部传入的 Workbook 初始化(多 Sheet 导出专用)
* 与 init() 的区别:不新建 Workbook而是在已有 wb 上追加新 Sheet
*
* @param wb 已有工作簿
* @param list 数据集合
* @param sheetName Sheet 名称
* @param title 大标题(可为空)
*/
public void initWithWorkbook(SXSSFWorkbook wb, List<T> list, String sheetName, String title)
{
if (list == null)
{
list = new ArrayList<T>();
}
this.list = list;
this.sheetName = sheetName;
this.title = title != null ? title : "";
this.type = Type.EXPORT;
this.rownum = 0;
this.wb = wb;
this.sheet = wb.createSheet(sheetName);
createExcelField();
this.styles = createStyles(wb);
createTitle();
createSubHead();
}
/**
* 对list数据源将其里面的数据导入到excel表单
*
@@ -1124,7 +1240,6 @@ public class ExcelUtil<T>
/**
* 添加单元格
*/
@SuppressWarnings("deprecation")
public Cell addCell(Excel attr, Row row, T vo, Field field, int column)
{
Cell cell = null;
@@ -1202,9 +1317,16 @@ public class ExcelUtil<T>
*/
private CellStyle createCellStyle(CellStyle cellStyle, String format)
{
String key = cellStyle.getIndex() + "|" + format;
CellStyle cached = cellStyleCache.get(key);
if (cached != null)
{
return cached;
}
CellStyle style = wb.createCellStyle();
style.cloneStyleFrom(cellStyle);
style.setDataFormat(wb.getCreationHelper().createDataFormat().getFormat(format));
cellStyleCache.put(key, style);
return style;
}
@@ -1426,7 +1548,7 @@ public class ExcelUtil<T>
{
try
{
Object instance = excel.handler().getDeclaredConstructor().newInstance();
Object instance = excel.handler().newInstance();
Method formatMethod = excel.handler().getMethod("format", new Class[] { Object.class, String[].class, Cell.class, Workbook.class });
value = formatMethod.invoke(instance, value, excel.args(), cell, this.wb);
}

View File

@@ -310,7 +310,6 @@ public class ReflectUtils
/**
* 改变private/protected的方法为public尽量不调用实际改动的语句避免JDK的SecurityManager抱怨。
*/
@SuppressWarnings("deprecation")
public static void makeAccessible(Method method)
{
if ((!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))
@@ -323,7 +322,6 @@ public class ReflectUtils
/**
* 改变private/protected的成员变量为public尽量不调用实际改动的语句避免JDK的SecurityManager抱怨。
*/
@SuppressWarnings("deprecation")
public static void makeAccessible(Field field)
{
if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())

View File

@@ -13,7 +13,7 @@ public class SqlUtil
/**
* 定义常用的 sql关键字
*/
public static String SQL_REGEX = "\u000B|and |extractvalue|updatexml|sleep|information_schema|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
public static String SQL_REGEX = "\u000B|%0A|and |extractvalue|updatexml|sleep|information_schema|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
/**
* 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)
@@ -58,12 +58,13 @@ public class SqlUtil
{
return;
}
String normalizedValue = value.replaceAll("\\p{Z}|\\s", "");
String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
for (String sqlKeyword : sqlKeywords)
{
if (StringUtils.indexOfIgnoreCase(value, sqlKeyword) > -1)
if (StringUtils.indexOfIgnoreCase(normalizedValue, sqlKeyword) > -1)
{
throw new UtilException("参数存在SQL注入风险");
throw new UtilException("请求参数包含敏感关键词'" + sqlKeyword + "',可能存在安全风险");
}
}
}

View File

@@ -1,7 +1,7 @@
package com.ruoyi.common.xss;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

View File

@@ -1,8 +1,8 @@
package com.ruoyi.common.xss;
import com.ruoyi.common.utils.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

View File

@@ -1,53 +0,0 @@
package com.ruoyi.common.core.cache;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
class InMemoryCacheStoreTest
{
@Test
void shouldExpireEntryAfterTtl() throws Exception
{
InMemoryCacheStore store = new InMemoryCacheStore();
store.set("captcha_codes:1", "1234", 20, TimeUnit.MILLISECONDS);
Thread.sleep(40);
assertNull(store.get("captcha_codes:1"));
}
@Test
void shouldReturnPrefixKeysInSortedOrder()
{
InMemoryCacheStore store = new InMemoryCacheStore();
store.set("login_tokens:a", "A");
store.set("login_tokens:b", "B");
store.set("sys_dict:x", "X");
assertEquals(Set.of("login_tokens:a", "login_tokens:b"), store.keys("login_tokens:*"));
}
@Test
void shouldTrackHitsMissesWritesAndExpiredEntries() throws Exception
{
InMemoryCacheStore store = new InMemoryCacheStore();
store.set("captcha_codes:2", "5678", 20, TimeUnit.MILLISECONDS);
assertTrue(store.hasKey("captcha_codes:2"));
assertEquals("5678", store.get("captcha_codes:2"));
Thread.sleep(40);
assertNull(store.get("captcha_codes:2"));
assertFalse(store.hasKey("captcha_codes:2"));
InMemoryCacheStats stats = store.snapshot();
assertEquals("IN_MEMORY", stats.cacheType());
assertEquals("single-instance", stats.mode());
assertEquals(0, stats.keySize());
assertEquals(2, stats.hitCount());
assertEquals(2, stats.missCount());
assertEquals(1, stats.expiredCount());
assertEquals(1, stats.writeCount());
}
}

View File

@@ -1,52 +0,0 @@
package com.ruoyi.common.core.redis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
class RedisCacheTest
{
@Test
void shouldSupportSetGetDeleteAndExpireThroughRedisCacheFacade()
{
RedisCache cache = new RedisCache(new InMemoryCacheStore());
cache.setCacheObject("login_tokens:abc", "payload", 1, TimeUnit.SECONDS);
assertEquals("payload", cache.getCacheObject("login_tokens:abc"));
assertTrue(cache.hasKey("login_tokens:abc"));
assertTrue(cache.deleteObject("login_tokens:abc"));
}
@Test
void shouldSupportKeysBulkDeleteAndRemainingTtl()
{
RedisCache cache = new RedisCache(new InMemoryCacheStore());
cache.setCacheObject("login_tokens:a", "A", 5, TimeUnit.SECONDS);
cache.setCacheObject("login_tokens:b", "B", 5, TimeUnit.SECONDS);
cache.setCacheObject("sys_dict:x", "X");
assertNotNull(cache.getExpire("login_tokens:a"));
assertTrue(cache.getExpire("login_tokens:a") > 0);
assertEquals(Set.of("login_tokens:a", "login_tokens:b"), Set.copyOf(cache.keys("login_tokens:*")));
assertTrue(cache.deleteObject(List.of("login_tokens:a", "login_tokens:b")));
assertFalse(cache.hasKey("login_tokens:a"));
}
@Test
void shouldIncrementCounterWithinTtlWindow() throws Exception
{
RedisCache cache = new RedisCache(new InMemoryCacheStore());
assertEquals(1L, cache.increment("rate_limit:test", 50, TimeUnit.MILLISECONDS));
assertEquals(2L, cache.increment("rate_limit:test", 50, TimeUnit.MILLISECONDS));
Thread.sleep(70);
assertNull(cache.getCacheObject("rate_limit:test"));
assertEquals(1L, cache.increment("rate_limit:test", 50, TimeUnit.MILLISECONDS));
}
}

View File

@@ -1,45 +0,0 @@
package com.ruoyi.common.utils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.test.util.ReflectionTestUtils;
import com.ruoyi.common.core.cache.InMemoryCacheStore;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.spring.SpringUtils;
class DictUtilsTest
{
@AfterEach
void clearSpringUtils()
{
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", null);
ReflectionTestUtils.setField(SpringUtils.class, "applicationContext", null);
}
@Test
void shouldReadDictListFromLocalCache()
{
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("redisCache", new RedisCache(new InMemoryCacheStore()));
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", beanFactory);
SysDictData dictData = new SysDictData();
dictData.setDictLabel("正常");
dictData.setDictValue("0");
DictUtils.setDictCache("sys_normal_disable", List.of(dictData));
List<SysDictData> cached = DictUtils.getDictCache("sys_normal_disable");
assertEquals(1, cached.size());
assertEquals("正常", cached.get(0).getDictLabel());
DictUtils.clearDictCache();
assertNull(DictUtils.getDictCache("sys_normal_disable"));
}
}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>ruoyi</artifactId>
<groupId>com.ruoyi</groupId>
<version>3.9.1</version>
<version>3.9.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -32,7 +32,7 @@
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- 验证码 -->
@@ -59,12 +59,6 @@
<artifactId>ruoyi-system</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -7,6 +7,7 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.SysRole;
@@ -26,31 +27,6 @@ import com.ruoyi.framework.security.context.PermissionContextHolder;
@Component
public class DataScopeAspect
{
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 数据权限过滤关键字
*/
@@ -74,7 +50,7 @@ public class DataScopeAspect
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
{
String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission);
dataScopeFilter(joinPoint, currentUser, controllerDataScope.userAlias(), controllerDataScope.deptAlias(), controllerDataScope.userField(), controllerDataScope.deptField(), permission);
}
}
}
@@ -88,13 +64,13 @@ public class DataScopeAspect
* @param userAlias 用户别名
* @param permission 权限字符
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String userAlias, String deptAlias, String userField, String deptField, String permission)
{
StringBuilder sqlString = new StringBuilder();
List<String> conditions = new ArrayList<String>();
List<String> scopeCustomIds = new ArrayList<String>();
user.getRoles().forEach(role -> {
if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && (StringUtils.isEmpty(permission) || StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))))
if (Constants.Dept.DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && (StringUtils.isEmpty(permission) || StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))))
{
scopeCustomIds.add(Convert.toStr(role.getRoleId()));
}
@@ -111,42 +87,42 @@ public class DataScopeAspect
{
continue;
}
if (DATA_SCOPE_ALL.equals(dataScope))
if (Constants.Dept.DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
conditions.add(dataScope);
break;
}
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
else if (Constants.Dept.DATA_SCOPE_CUSTOM.equals(dataScope))
{
if (scopeCustomIds.size() > 1)
{
// 多个自定数据权限使用in查询避免多次拼接。
sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds)));
sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, deptField, String.join(",", scopeCustomIds)));
}
else
{
sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, role.getRoleId()));
sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, deptField, role.getRoleId()));
}
}
else if (DATA_SCOPE_DEPT.equals(dataScope))
else if (Constants.Dept.DATA_SCOPE_DEPT.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
sqlString.append(StringUtils.format(" OR {}.{} = {} ", deptAlias, deptField, user.getDeptId()));
}
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
else if (Constants.Dept.DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, user.getDeptId(), user.getDeptId()));
sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, deptField, user.getDeptId(), user.getDeptId()));
}
else if (DATA_SCOPE_SELF.equals(dataScope))
else if (Constants.Dept.DATA_SCOPE_SELF.equals(dataScope))
{
if (StringUtils.isNotBlank(userAlias))
{
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
sqlString.append(StringUtils.format(" OR {}.{} = {} ", userAlias, userField, user.getUserId()));
}
else
{
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
sqlString.append(StringUtils.format(" OR {}.{} = 0 ", deptAlias, deptField));
}
}
conditions.add(dataScope);
@@ -155,7 +131,7 @@ public class DataScopeAspect
// 角色都不包含传递过来的权限字符这个时候sqlString也会为空所以要限制一下,不查询任何数据
if (StringUtils.isEmpty(conditions))
{
sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
sqlString.append(StringUtils.format(" OR {}.{} = 0 ", deptAlias, deptField));
}
if (StringUtils.isNotBlank(sqlString.toString()))

View File

@@ -2,8 +2,8 @@ package com.ruoyi.framework.aspectj;
import java.util.Collection;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;

View File

@@ -1,18 +1,22 @@
package com.ruoyi.framework.aspectj;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.Collections;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.enums.LimitType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
/**
@@ -26,11 +30,20 @@ public class RateLimiterAspect
{
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
private final RedisCache redisCache;
private RedisTemplate<Object, Object> redisTemplate;
public RateLimiterAspect(RedisCache redisCache)
private RedisScript<Long> limitScript;
@Autowired
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
{
this.redisCache = redisCache;
this.redisTemplate = redisTemplate;
}
@Autowired
public void setLimitScript(RedisScript<Long> limitScript)
{
this.limitScript = limitScript;
}
@Before("@annotation(rateLimiter)")
@@ -40,14 +53,15 @@ public class RateLimiterAspect
int count = rateLimiter.count();
String combineKey = getCombineKey(rateLimiter, point);
List<Object> keys = Collections.singletonList(combineKey);
try
{
long number = redisCache.increment(combineKey, time, TimeUnit.SECONDS);
if (number > count)
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (StringUtils.isNull(number) || number.intValue() > count)
{
throw new ServiceException("访问过于频繁,请稍候再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number, combineKey);
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
}
catch (ServiceException e)
{

View File

@@ -3,6 +3,11 @@ package com.ruoyi.framework.config;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -11,18 +16,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
/**
* druid 配置多数据源
@@ -96,7 +96,7 @@ public class DruidConfig
Filter filter = new Filter()
{
@Override
public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
{
}
@Override

View File

@@ -0,0 +1,52 @@
package com.ruoyi.framework.config;
import java.nio.charset.Charset;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import com.ruoyi.common.constant.Constants;
/**
* Redis使用FastJson序列化
*
* @author ruoyi
*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
private Class<T> clazz;
public FastJson2JsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
}
}

Some files were not shown because too many files have changed in this diff Show More