mirror of
				https://github.com/dromara/RuoYi-Vue-Plus.git
				synced 2025-10-31 14:23:45 +08:00 
			
		
		
		
	Compare commits
	
		
			295 Commits
		
	
	
		
			c054029cfc
			...
			v4.8.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ed43f0a889 | ||
|  | a7daed30d8 | ||
|  | f2598e7961 | ||
|  | 4c848cf4cb | ||
|  | 4d1bf7c25c | ||
|  | 88bd127b67 | ||
|  | 1186b86f73 | ||
|  | eb946e029d | ||
|  | 9b68b27168 | ||
|  | 5dbb255323 | ||
|  | cc326e36cc | ||
|  | bacdd8d685 | ||
|  | 8ce96690fc | ||
|  | 9d0d52a48c | ||
|  | 8d0c06b5bf | ||
|  | 245ade0d79 | ||
|  | 8de02e285e | ||
|  | 0005710f45 | ||
|  | 88e09f2189 | ||
|  | c7e79d2681 | ||
|  | 86223d5a6b | ||
|  | ce04052da3 | ||
|  | f91f225ba6 | ||
|  | 2cfdb9bcda | ||
|  | a06f3255d0 | ||
|  | 2affe91837 | ||
|  | 7544ae318c | ||
|  | e59f04bfee | ||
|  | e35df6d1d2 | ||
|  | 81dcd8b512 | ||
|  | 8ca601dc66 | ||
|  | 5ff7203929 | ||
|  | a6ef8bb938 | ||
|  | 79fd63599a | ||
|  | 3c89a83d6b | ||
|  | d45ae7ffc5 | ||
|  | 34babcbfa5 | ||
|  | e5661a2cf4 | ||
|  | cc40f9c4ac | ||
|  | 6e38a9e412 | ||
|  | 481170922e | ||
|  | f46b8d0758 | ||
|  | 1d43448423 | ||
|  | 2de97fe45c | ||
|  | c41688831c | ||
|  | 2a36ce4302 | ||
|  | f2200df15a | ||
|  | 3b498c4939 | ||
|  | 2cead2c5fd | ||
|  | 23bf840684 | ||
|  | f8d551e226 | ||
|  | 2e3b068341 | ||
|  | 0d9b1c2e82 | ||
|  | 148c7c588e | ||
|  | 3a82fb82e8 | ||
|  | c319579c0b | ||
|  | acfa39369e | ||
|  | f71b8110a9 | ||
|  | 8311454342 | ||
|  | ed4df16201 | ||
|  | 2f3dbbfe32 | ||
|  | dc752ade25 | ||
|  | ebef89be62 | ||
|  | 7d209aeb41 | ||
|  | e82a031dd0 | ||
|  | fea9fdaa5f | ||
|  | 952cd0b4d3 | ||
|  | ec6704181f | ||
|  | 3389a11933 | ||
|  | db0c882f1b | ||
|  | 13d468d8cc | ||
|  | b6daa886e0 | ||
|  | 1d10611172 | ||
|  | 8a5cc7f76c | ||
|  | d4ab8a8e22 | ||
|  | 8a86c5bdc5 | ||
|  | e9cc7c3058 | ||
|  | f7dd7d1e42 | ||
|  | 71317201b8 | ||
|  | 19ba2b4207 | ||
|  | cb54a9576e | ||
|  | 7f73bfc478 | ||
|  | ed8d6b651a | ||
|  | fd2a1cb9b9 | ||
|  | 775bd5925a | ||
|  | fc0a33e512 | ||
|  | e093b7b2ec | ||
|  | 350e62c6cb | ||
|  | 2c3d2e4ea3 | ||
|  | 57bddf259a | ||
|  | c40b45e7bb | ||
|  | 7b924e2482 | ||
|  | 9699d1fedb | ||
|  | 2004a0cc64 | ||
|  | 11485c2538 | ||
|  | 36b5051cbd | ||
|  | 8b3310cd00 | ||
|  | 788e370cc0 | ||
|  | c230eedbc0 | ||
|  | 39d4efee6a | ||
|  | 1bd9688533 | ||
|  | 2e50b52ae8 | ||
|  | 73e14d33fe | ||
|  | 9dab37060b | ||
|  | 1e69726d77 | ||
|  | 45eec24b7f | ||
|  | 618ba481b8 | ||
|  | 81f488e24a | ||
|  | 7cdeb3d3d7 | ||
|  | 0ff533e6b5 | ||
|  | d785a01973 | ||
|  | bbbfeb59c8 | ||
|  | 361e210583 | ||
|  | c7f14a8b21 | ||
|  | 040ce14acd | ||
|  | cf66216f3e | ||
|  | adc286d25a | ||
|  | ad7731a0e9 | ||
|  | bbda70c2a0 | ||
|  | cbd0aca8eb | ||
|  | d2e18fd571 | ||
|  | df544f32c9 | ||
|  | cad98d8384 | ||
|  | 88377e2e05 | ||
|  | c6324e80af | ||
|  | 04eb841cde | ||
|  | 2ad0299493 | ||
|  | 0f5603aed4 | ||
|  | 72882374be | ||
|  | 21570cbd33 | ||
|  | 049da896f8 | ||
|  | 0affb8ab54 | ||
|  | a412b20118 | ||
|  | 2dcfb8e3ce | ||
|  | c631a084f3 | ||
|  | 022d33bc41 | ||
|  | 47379dd702 | ||
|  | 8c8b53e266 | ||
|  | 9e8a58af43 | ||
|  | 4409b96c74 | ||
|  | 9253186c27 | ||
|  | b9eee8f399 | ||
|  | 230d5e7d56 | ||
|  | 68404f3f6e | ||
|  | a312794423 | ||
|  | 98e9c11378 | ||
|  | d9b8dd9cc2 | ||
|  | f366457aa1 | ||
|  | 311df68fa6 | ||
|  | c5d071dbaf | ||
|  | b55409bf37 | ||
|  | 11f548b53b | ||
|  | d8b264a13d | ||
|  | 77407899e8 | ||
|  | 6296a50259 | ||
|  | 754b138abe | ||
|  | 811177f530 | ||
|  | 54a45d0900 | ||
|  | b55d8faa67 | ||
|  | 1ad5a526e7 | ||
|  | ecdb629438 | ||
|  | df5eb800cd | ||
|  | f3fa1386e7 | ||
|  | a31d4a92d6 | ||
|  | 0804854175 | ||
|  | bb265274c5 | ||
|  | 9895517097 | ||
|  | 93dda236dc | ||
|  | 8619d97749 | ||
|  | 6762ab739a | ||
|  | 5b43d0e29f | ||
|  | 0662756e8c | ||
|  | 5cd6dcff4f | ||
|  | 0b0439b72e | ||
|  | 0abc2e9624 | ||
|  | 7343cdbe70 | ||
|  | ded82af207 | ||
|  | 16d58bacbc | ||
|  | a5abbbf988 | ||
|  | 5c040dc5cb | ||
|  | 6c41fd1cf3 | ||
|  | b4cf1dbb6d | ||
|  | eba4c88833 | ||
|  | 773b497761 | ||
|  | 3832dc933d | ||
|  | 7fd262fd8b | ||
|  | 5e44843854 | ||
|  | 09d72dceef | ||
|  | 684b84a16d | ||
|  | 35c289ab92 | ||
|  | b91f02f5b7 | ||
|  | ab7f6957b9 | ||
|  | b13c10ccd6 | ||
|  | eb23098a4d | ||
|  | b02ad6a0cc | ||
|  | 2fd44be9db | ||
|  | fda19131ae | ||
|  | 24d942f036 | ||
|  | 7484b9b6a9 | ||
|  | 9ae3e9902a | ||
|  | cbd50e6b3d | ||
|  | 9a9507ce5a | ||
|  | 46e126dff7 | ||
|  | 56d7023e41 | ||
|  | 0e0370f532 | ||
|  | 025f3a9bcf | ||
|  | 4dc8af274b | ||
|  | 7f0661c007 | ||
|  | a847a1dbcc | ||
|  | a5ee33b559 | ||
|  | 8f87da19ea | ||
|  | a2d8ff9286 | ||
|  | 8387ec6134 | ||
|  | 9e0fca3b54 | ||
|  | f8c079c651 | ||
|  | 9b7adfdaf4 | ||
|  | 91a61b6927 | ||
|  | d38fa0ec05 | ||
|  | d7d398763f | ||
|  | f6d4e23bf6 | ||
|  | 4b07faf89a | ||
|  | 31194414eb | ||
|  | d6c49b915f | ||
|  | b465972e7c | ||
|  | 7a5abcd0f8 | ||
|  | 5464dfb830 | ||
|  | 7a97377c28 | ||
|  | 453b8fcbb2 | ||
|  | 2a46cdbd88 | ||
|  | 995578c561 | ||
|  | f88c93b335 | ||
|  | edcd7c99ba | ||
|  | dadf05c25c | ||
|  | a2c43aceb1 | ||
|  | 4bd2691422 | ||
|  | 088002bd62 | ||
|  | 5722ba3f6c | ||
|  | 40aeeeced1 | ||
|  | 6f209cad99 | ||
|  | db6796e740 | ||
|  | 4382cf2217 | ||
|  | d93307151a | ||
|  | f3d800d598 | ||
|  | d2baaaaf7b | ||
|  | c203d0af46 | ||
|  | 1f507f2d22 | ||
|  | 5cf3287064 | ||
|  | 57bcb5dbf6 | ||
|  | e803388cad | ||
|  | b9b76539ac | ||
|  | bc06550918 | ||
|  | 61db843576 | ||
|  | 6c6d92a776 | ||
|  | f8ac8c085e | ||
|  | 7c05920e73 | ||
|  | 2d7535012e | ||
|  | 654944b5c7 | ||
|  | d2675744f4 | ||
|  | e20dacbfd9 | ||
|  | 540afd839d | ||
|  | b34c2fd6ee | ||
|  | 1ae44c8124 | ||
|  | 6fb34a7717 | ||
|  | 43982c5a36 | ||
|  | d8062087c7 | ||
|  | cedb174ffd | ||
|  | 1a70cf658c | ||
|  | 1496220a74 | ||
|  | d6894c89ed | ||
|  | ebf780617a | ||
|  | 0208fa14af | ||
|  | 8f34644692 | ||
|  | d2774d3706 | ||
|  | 0788bcfcb0 | ||
|  | e4fff795b8 | ||
|  | fd7dc204de | ||
|  | 4e807455e0 | ||
|  | 67e20711d4 | ||
|  | 138842e9ba | ||
|  | 1a8e2b3e63 | ||
|  | 6f83ed3285 | ||
|  | 369e4be31c | ||
|  | 5c9fe22780 | ||
|  | b2a927cc5e | ||
|  | 23cbed3aac | ||
|  | 8acce4a7fc | ||
|  | bf362b9aba | ||
|  | 9b57c67d3e | ||
|  | 0700c60636 | ||
|  | 95a2c74462 | ||
|  | ca085b9aaa | ||
|  | 329c070e05 | ||
|  | f3a4104fd0 | ||
|  | 82e8ab2385 | ||
|  | c9260c4966 | 
							
								
								
									
										50
									
								
								.gitee/ISSUE_TEMPLATE/bug.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								.gitee/ISSUE_TEMPLATE/bug.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| name: Bug 反馈 | ||||
| description: 当你中发现了一个 Bug,导致应用崩溃或抛出异常,或者有一个组件存在问题,或者某些地方看起来不对劲。 | ||||
| title: "[Bug]: " | ||||
| labels: ["bug"] | ||||
| body: | ||||
|   - type: textarea | ||||
|     id: version | ||||
|     attributes: | ||||
|       label: 版本 | ||||
|       description: 你当前正在使用我们软件的哪个版本(pom文件内的版本号)? | ||||
|       value: | | ||||
|         jdk版本(带上尾号): 例如 1.8.0 | ||||
|         框架版本(项目启动时输出的版本号): 例如 4.4.0 | ||||
|         其他依赖版本(你觉得有必要的): | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: 功能不好用不会用是否已经看过项目文档? | ||||
|       options: | ||||
|         - label: https://plus-doc.dromara.org | ||||
|           required: true | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: 这个问题是否已经存在? | ||||
|       options: | ||||
|         - label: 我已经搜索过现有的问题 (https://gitee.com/dromara/RuoYi-Vue-Plus/issues) | ||||
|           required: true | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: 希望结果 | ||||
|       description: 想知道你觉得怎么样是正常或者合理的。 | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       label: 如何复现 | ||||
|       description: 请详细告诉我们如何复现你遇到的问题 | ||||
|       value: | | ||||
|         如涉及代码 可提供一个最小代码示例 并使用```附上它 或者截图均可 越详细越好<br> | ||||
|         大多数问题都是 代码编写错误问题 逻辑问题 或者用法错误等问题 | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: 相关代码与报错信息(请勿发混乱格式) | ||||
|       description: 如果可以的话,上传任何关于 bug 的截图。 | ||||
|       value: | | ||||
|         [在这里上传图片] | ||||
|  | ||||
							
								
								
									
										5
									
								
								.gitee/ISSUE_TEMPLATE/config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.gitee/ISSUE_TEMPLATE/config.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: RuoYi-Vue-Plus 文档中心 | ||||
|     url: https://plus-doc.dromara.org | ||||
|     about: 提供 RuoYi-Vue-Plus 搭建使用指南、平台基本开发使用方式、介绍、基础知识和常见问题解答 | ||||
							
								
								
									
										43
									
								
								.gitee/ISSUE_TEMPLATE/feature.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								.gitee/ISSUE_TEMPLATE/feature.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| name: 功能建议 | ||||
| description: 对本项目提出一个功能建议 | ||||
| title: "[功能建议]: " | ||||
| labels: ["enhancement"] | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         感谢提出功能建议,我们将仔细考虑!请持续关注该issues,在加入计划后我们会有贡献者设置为负责人,同时状态成为进行中。 | ||||
|   - type: textarea | ||||
|     id: related-problem | ||||
|     attributes: | ||||
|       label: 你的功能建议是否和某个问题相关? | ||||
|       description: 清晰并简洁地描述问题是什么,例如,当我...时,我总是感到困扰。 | ||||
|     validations: | ||||
|       required: false | ||||
|   - type: textarea | ||||
|     id: desired-solution | ||||
|     attributes: | ||||
|       label: 你希望看到什么解决方案? | ||||
|       description: 清晰并简洁地描述你希望发生的事情。 | ||||
|     validations: | ||||
|       required: true | ||||
|   - type: textarea | ||||
|     id: alternatives | ||||
|     attributes: | ||||
|       label: 你考虑过哪些替代方案? | ||||
|       description: 清晰并简洁地描述你考虑过的任何替代解决方案或功能。 | ||||
|     validations: | ||||
|       required: false | ||||
|   - type: textarea | ||||
|     id: additional-context | ||||
|     attributes: | ||||
|       label: 你有其他上下文或截图吗? | ||||
|       description: 在此处添加有关功能请求的任何其他上下文或截图。 | ||||
|     validations: | ||||
|       required: false | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: 意向参与贡献 | ||||
|       options: | ||||
|         - label: 我有意向参与具体功能的开发实现并将代码贡献回到上游社区 | ||||
|           required: false | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> | ||||
|     <deployment type="dockerfile"> | ||||
|       <settings> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.5.0" /> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.8.2" /> | ||||
|         <option name="buildOnly" value="true" /> | ||||
|         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" /> | ||||
|       </settings> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> | ||||
|     <deployment type="dockerfile"> | ||||
|       <settings> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-server:4.5.0" /> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-server:4.8.2" /> | ||||
|         <option name="buildOnly" value="true" /> | ||||
|         <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" /> | ||||
|       </settings> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <configuration default="false" name="ruoyi-xxl-job-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> | ||||
|     <deployment type="dockerfile"> | ||||
|       <settings> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.5.0" /> | ||||
|         <option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.8.2" /> | ||||
|         <option name="buildOnly" value="true" /> | ||||
|         <option name="sourceFilePath" value="ruoyi-extend/ruoyi-xxl-job-admin/Dockerfile" /> | ||||
|       </settings> | ||||
|   | ||||
							
								
								
									
										244
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,10 +1,24 @@ | ||||
| <img src="https://foruda.gitee.com/images/1679673773341074847/178e8451_1766278.png" width="50%" height="50%"> | ||||
| <div style="height: 10px; clear: both;"></div> | ||||
|  | ||||
| - - - | ||||
|  | ||||
| ## 版本状态说明 | ||||
|  | ||||
| 由于 springboot 2.X 与 vue 2.X 官方均宣布停止维护, 故而 框架 4.X 版本 进入维护状态(只处理问题不更新功能) | ||||
|  | ||||
| 停止维护时间预计: 2024年6-10月具体根据使用人数动态决定, 此版本已经相当稳定 即便不更新功能也不影响使用 | ||||
|  | ||||
| 如果依旧选择使用 jdk8 或者 jdk11 可以放心使用此版本, 如果希望使用 jdk17 或者 jdk21 可以选择使用 5.X 分支 | ||||
|  | ||||
| ## 平台简介 | ||||
| [](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus) | ||||
| [](https://github.com/JavaLionLi/RuoYi-Vue-Plus) | ||||
| [](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/blob/master/LICENSE) | ||||
|  | ||||
| [](https://gitee.com/dromara/RuoYi-Vue-Plus) | ||||
| [](https://github.com/dromara/RuoYi-Vue-Plus) | ||||
| [](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE) | ||||
| [](https://www.jetbrains.com/?from=RuoYi-Vue-Plus) | ||||
| <br> | ||||
| [](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus) | ||||
| [](https://gitee.com/dromara/RuoYi-Vue-Plus) | ||||
| []() | ||||
| []() | ||||
| []() | ||||
| @@ -14,62 +28,95 @@ | ||||
| > 项目代码、文档 均开源免费可商用 遵循开源协议在项目中保留开源协议文件即可<br> | ||||
| 活到老写到老 为兴趣而开源 为学习而开源 为让大家真正可以学到技术而开源 | ||||
|  | ||||
| > 系统演示: [传送门](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4836388&doc_id=1469725) | ||||
| > 系统演示: [传送门](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4836388&doc_id=1469725) | ||||
|  | ||||
| | 功能介绍     | 使用技术                | 文档地址                                                                                              | 特性注意事项                     | | ||||
| |----------|---------------------|---------------------------------------------------------------------------------------------------|----------------------------| | ||||
| | 当前框架     | RuoYi-Vue-Plus      | [RuoYi-Vue-Plus文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages)                       | 重写RuoYi-Vue全方位升级(不兼容原框架)   | | ||||
| | 微服务分支    | RuoYi-Cloud-Plus    | [微服务分支地址](https://gitee.com/JavaLionLi/RuoYi-Cloud-Plus)                                          | 重写RuoYi-Cloud全方位升级(不兼容原框架) | | ||||
| | 单体分支     | RuoYi-Vue-Plus-fast | [fast分支地址](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/tree/fast/)                                | 单体应用结构                     | | ||||
| | Vue3分支   | RuoYi-Vue-Plus-UI   | [UI地址](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI)                                            | 由于组件还未完善 仅供学习              | | ||||
| | 原框架      | RuoYi-Vue           | [RuoYi-Vue官网](http://ruoyi.vip/)                                                                  | 定期同步需要的功能                  | | ||||
| | 前端开发框架   | Vue、Element UI      | [Element UI官网](https://element.eleme.cn/#/zh-CN)                                                  |                            | | ||||
| | 后端开发框架   | SpringBoot          | [SpringBoot官网](https://spring.io/projects/spring-boot/#learn)                                     |                            | | ||||
| | 容器框架     | Undertow            | [Undertow官网](https://undertow.io/)                                                                | 基于 XNIO 的高性能容器             | | ||||
| | 权限认证框架   | Sa-Token、Jwt        | [Sa-Token官网](https://sa-token.dev33.cn/)                                                          | 强解耦、强扩展                    | | ||||
| | 关系数据库    | MySQL               | [MySQL官网](https://dev.mysql.com/)                                                                 | 适配 8.X 最低 5.7              | | ||||
| | 关系数据库    | Oracle              | [Oracle官网](https://www.oracle.com/cn/database/)                                                   | 适配 11g 12c                 | | ||||
| | 关系数据库    | PostgreSQL          | [PostgreSQL官网](https://www.postgresql.org/)                                                       | 适配 13 14                   | | ||||
| | 关系数据库    | SQLServer           | [SQLServer官网](https://docs.microsoft.com/zh-cn/sql/sql-server)                                    | 适配 2017 2019               | | ||||
| | 缓存数据库    | Redis               | [Redis官网](https://redis.io/)                                                                      | 适配 6.X 最低 4.X              | | ||||
| | 数据库框架    | Mybatis-Plus        | [Mybatis-Plus文档](https://baomidou.com/guide/)                                                     | 快速 CRUD 增加开发效率             | | ||||
| | 数据库框架    | p6spy               | [p6spy官网](https://p6spy.readthedocs.io/)                                                          | 更强劲的 SQL 分析                | | ||||
| | 多数据源框架   | dynamic-datasource  | [dynamic-ds文档](https://www.kancloud.cn/tracy5546/dynamic-datasource/content)                      | 支持主从与多种类数据库异构              | | ||||
| | 序列化框架    | Jackson             | [Jackson官网](https://github.com/FasterXML/jackson)                                                 | 统一使用 jackson 高效可靠          | | ||||
| | Redis客户端 | Redisson            | [Redisson文档](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95)                        | 支持单机、集群配置                  | | ||||
| | 分布式限流    | Redisson            | [Redisson文档](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95)                        | 全局、请求IP、集群ID 多种限流          | | ||||
| | 分布式队列    | Redisson            | [Redisson文档](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95)                        | 普通队列、延迟队列、优先队列 等           | | ||||
| | 分布式锁     | Lock4j              | [Lock4j官网](https://gitee.com/baomidou/lock4j)                                                     | 注解锁、工具锁 多种多样               | | ||||
| | 分布式幂等    | Redisson            | [Lock4j文档](https://gitee.com/baomidou/lock4j)                                                     | 拦截重复提交                     | | ||||
| | 分布式链路追踪  | Apache SkyWalking   | [Apache SkyWalking文档](https://skywalking.apache.org/docs/)                                        | 链路追踪、网格分析、度量聚合、可视化         | | ||||
| | 分布式任务调度  | Xxl-Job             | [Xxl-Job官网](https://www.xuxueli.com/xxl-job/)                                                     | 高性能 高可靠 易扩展                | | ||||
| | 文件存储     | Minio               | [Minio文档](https://docs.min.io/)                                                                   | 本地存储                       | | ||||
| | 文件存储     | 七牛、阿里、腾讯            | [OSS使用文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4359146&doc_id=1469725) | 云存储                        | | ||||
| | 短信模块     | 阿里、腾讯               | [短信使用文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5578491&doc_id=1469725)  | 短信发送                       | | ||||
| | 监控框架     | SpringBoot-Admin    | [SpringBoot-Admin文档](https://codecentric.github.io/spring-boot-admin/current/)                    | 全方位服务监控                    | | ||||
| | 校验框架     | Validation          | [Validation文档](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/)    | 增强接口安全性、严谨性 支持国际化          | | ||||
| | Excel框架  | Alibaba EasyExcel   | [EasyExcel文档](https://www.yuque.com/easyexcel/doc/easyexcel)                                      | 性能优异 扩展性强                  | | ||||
| | 文档框架     | SpringDoc、javadoc   | [接口文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5805266&doc_id=1469725)    | 无注解零入侵基于java注释             | | ||||
| | 工具类框架    | Hutool、Lombok       | [Hutool文档](https://www.hutool.cn/docs/)                                                           | 减少代码冗余 增加安全性               | | ||||
| | 代码生成器    | 适配MP、SpringDoc规范化代码 | [代码生成文档](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5522329&doc_id=1469725)  | 一键生成前后端代码                  | | ||||
| | 部署方式     | Docker              | [Docker文档](https://docs.docker.com/)                                                              | 容器编排 一键部署业务集群              | | ||||
| | 国际化      | SpringMessage       | [SpringMVC文档](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc)   | Spring标准国际化方案              | | ||||
| # 本框架与RuoYi的功能差异 | ||||
|  | ||||
| | 功能          | 本框架                                                                                                               | RuoYi                                                                              | | ||||
| |-------------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | ||||
| | 前端项目        | 基于vue3-element-admin开源项目重写<br/>Vue3 + TS + ElementPlus                                                            | 基于Vue2/Vue3 + JS                                                                   |  | ||||
| | 后端项目结构      | 采用插件化 + 扩展包形式 结构解耦 易于扩展                                                                                           | 模块相互注入耦合严重难以扩展                                                                     |  | ||||
| | 后端代码风格      | 严格遵守Alibaba规范与项目统一配置的代码格式化                                                                                        | 代码书写与常规结构不同阅读障碍大                                                                   | | ||||
| | Web容器       | 采用 Undertow 基于 XNIO 的高性能容器                                                                                        | 采用 Tomcat                                                                          | | ||||
| | 权限认证        | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展                                                                                  | Spring Security 配置繁琐扩展性极差                                                          | | ||||
| | 权限注解        | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR` 或 `权限 OR 角色` 等复杂表达式        | 只支持是否存在匹配                                                                          | | ||||
| | 关系数据库支持     | 原生支持 MySQL、Oracle、PostgreSQL、SQLServer<br/>可同时使用异构切换                                                              | 支持 Mysql、Oracle 不支持同时使用、不支持异构切换                                                    | | ||||
| | 缓存数据库       | 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列                                                                             | Redis 简单 get set 支持                                                                | | ||||
| | Redis客户端    | 采用 Redisson Redis官方推荐 基于Netty的客户端工具<br/>支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan<br/>支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐<br/>连接池采用 common-pool Bug多经常性出问题              | | ||||
| | 缓存注解        | 采用 Spring-Cache 注解 对其扩展了实现支持了更多功能<br/>例如 过期时间 最大空闲时间 组最大长度等 只需一个注解即可完成数据自动缓存                                      | 需手动编写Redis代码逻辑                                                                     | | ||||
| | ORM框架       | 采用 Mybatis-Plus 基于对象几乎不用写SQL全java操作 功能强大插件众多<br/>例如多租户插件 分页插件 乐观锁插件等等                                             | 采用 Mybatis 基于XML需要手写SQL                                                            | | ||||
| | SQL监控       | 采用 p6spy 可输出完整SQL与执行时间监控                                                                                          | log输出 需手动拼接sql与参数无法快速查看调试问题                                                        | | ||||
| | 数据分页        | 采用 Mybatis-Plus 分页插件<br/>框架对其进行了扩展 对象化分页对象 支持多种方式传参 支持前端多排序 复杂排序                                                  | 采用 PageHelper 仅支持单查询分页 参数只能从param传 只能单排序 功能扩展性差 体验不好                               | | ||||
| | 数据权限        | 采用 Mybatis-Plus 插件 自行分析拼接SQL 无感式过滤<br/>只需为Mapper设置好注解条件 支持多种自定义 不限于部门角色                                           | 采用 注解+aop 实现 基于部门角色 生成的sql兼容性差 不支持其他业务扩展<br/>生成sql后需手动拼接到具体业务sql上 对于多个Mapper查询不起作用 | | ||||
| | 数据脱敏        | 采用 注解 + jackson 序列化期间脱敏 支持不同模块不同的脱敏条件<br/>支持多种策略 如身份证、手机号、地址、邮箱、银行卡等 可自行扩展                                        | 无                                                                                  | | ||||
| | 数据加解密       | 采用 注解 + mybatis 拦截器 对存取数据期间自动加解密<br/>支持多种策略 如BASE64、AES、RSA、SM2、SM4等                                              | 无                                                                                  | | ||||
| | 数据翻译        | 采用 注解 + jackson 序列化期间动态修改数据 数据进行翻译<br/>支持多种模式: `映射翻译` `直接翻译` `其他扩展条件翻译` 接口化两步即可完成自定义扩展 内置多种翻译实现                   | 无                                                                                  | | ||||
| | 多数据源框架      | 采用 dynamic-datasource 支持世面大部分数据库<br/>通过yml配置即可动态管理异构不同种类的数据库 也可通过前端页面添加数据源<br/>支持spel表达式从请求头参数等条件切换数据源            | 基于 druid 手动编写代码配置数据源 配置繁琐 支持性差                                                     | | ||||
| | 多数据源事务      | 采用 dynamic-datasource 支持多数据源不同种类的数据库事务回滚                                                                          | 不支持                                                                                | | ||||
| | 数据库连接池      | 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下                                                                        | 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般                                               | | ||||
| | 数据库主键       | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁                                                                  | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一                                                     | | ||||
| | WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物                                                         | 无                                                                                  | | ||||
| | 序列化         | 采用 Jackson Spring官方内置序列化 靠谱!!!                                                                                    | 采用 fastjson bugjson 远近闻名                                                           |  | ||||
| | 分布式幂等       | 参考美团GTIS防重系统简化实现(细节可看文档)                                                                                          | 手动编写注解基于aop实现                                                                      | | ||||
| | 分布式任务调度     | 采用 Xxl-Job 天生支持分布式 统一的管理中心                                                                                        | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造                                                   |  | ||||
| | 文件存储        | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储                                                     | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应                                                    | | ||||
| | 云存储         | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家                                                                          | 不支持                                                                                | | ||||
| | 短信          | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用                                                                 | 不支持                                                                                | | ||||
| | 邮件          | 采用 mail-api 通用协议支持大部分邮件厂商                                                                                         | 不支持                                                                                | | ||||
| | 接口文档        | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了                                                     | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成                                                |  | ||||
| | 校验框架        | 采用 Validation 支持注解与工具类校验 注解支持国际化                                                                                  | 仅支持注解 且注解不支持国际化                                                                    | | ||||
| | Excel框架     | 采用 Alibaba EasyExcel 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等                                               | 基于 POI 手写实现 功能有限 复杂 扩展性差                                                           | | ||||
| | 工具类框架       | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码                                                       | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等                                            |  | ||||
| | 监控框架        | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控                                    | 无                                                                                  |  | ||||
| | 链路追踪        | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点                                            | 无                                                                                  | | ||||
| | 代码生成器       | 只需设计好表结构 一键生成所有crud代码与页面<br/>降低80%的开发量 把精力都投入到业务设计上<br/>框架为其适配MP、SpringDoc规范化代码 同时支持动态多数据源代码生成                    | 代码生成原生结构 只支持单数据源生成                                                                 | | ||||
| | 部署方式        | 支持 Docker 编排 一键搭建所有环境 让开发人员从此不再为搭建环境而烦恼                                                                           | 原生jar部署 其他环境需手动下载安装 自行搭建                                                           |  | ||||
| | 项目路径修改      | 提供详细的修改方案文档 并为其做了一些改动 非常简单即可修改成自己想要的                                                                              | 需要做很多改造 文档说明有限                                                                     | | ||||
| | 国际化         | 基于请求头动态返回不同语种的文本内容 开发难度低 有对应的工具类 支持大部分注解内容国际化                                                                     | 只提供基础功能 其他需自行编写扩展                                                                  | | ||||
| | 代码单例测试      | 提供单例测试 使用方式编写方法与maven多环境单测插件                                                                                      | 只提供基础功能 其他需自行编写扩展                                                                  | | ||||
| | Demo案例      | 提供框架功能的实际使用案例 单独一个模块提供了很多很全                                                                                       | 无                                                                                  | | ||||
|  | ||||
|  | ||||
| ## 本框架与RuoYi的业务差异 | ||||
|  | ||||
| | 业务     | 功能说明                                    | 本框架 | RuoYi            | | ||||
| |--------|-----------------------------------------|-----|------------------| | ||||
| | 用户管理   | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等          | 支持  | 支持               | | ||||
| | 部门管理   | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限          | 支持  | 支持               | | ||||
| | 岗位管理   | 配置系统用户所属担任职务                            | 支持  | 支持               | | ||||
| | 菜单管理   | 配置系统菜单、操作权限、按钮权限标识等                     | 支持  | 支持               | | ||||
| | 角色管理   | 角色菜单权限分配、设置角色按机构进行数据范围权限划分              | 支持  | 支持               | | ||||
| | 字典管理   | 对系统中经常使用的一些较为固定的数据进行维护                  | 支持  | 支持               | | ||||
| | 参数管理   | 对系统动态配置常用参数                             | 支持  | 支持               | | ||||
| | 通知公告   | 系统通知公告信息发布维护                            | 支持  | 支持               | | ||||
| | 操作日志   | 系统正常操作日志记录和查询 系统异常信息日志记录和查询             | 支持  | 支持               | | ||||
| | 登录日志   | 系统登录日志记录查询包含登录异常                        | 支持  | 支持               | | ||||
| | 文件管理   | 系统文件展示、上传、下载、删除等管理                      | 支持  | 无                | | ||||
| | 文件配置管理 | 系统文件上传、下载所需要的配置信息动态添加、修改、删除等管理          | 支持  | 无                | | ||||
| | 在线用户管理 | 已登录系统的在线用户信息监控与强制踢出操作                   | 支持  | 支持               | | ||||
| | 定时任务   | 运行报表、任务管理(添加、修改、删除)、日志管理、执行器管理等         | 支持  | 仅支持任务与日志管理       | | ||||
| | 代码生成   | 多数据源前后端代码的生成(java、html、xml、sql)支持CRUD下载 | 支持  | 仅支持单数据源          | | ||||
| | 系统接口   | 根据业务代码自动生成相关的api接口文档                    | 支持  | 支持               | | ||||
| | 服务监控   | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等     | 支持  | 仅支持单机CPU、内存、磁盘监控 | | ||||
| | 缓存监控   | 对系统的缓存信息查询,命令统计等。                       | 支持  | 支持               | | ||||
| | 在线构建器  | 拖动表单元素生成相应的HTML代码。                      | 支持  | 支持               | | ||||
| | 使用案例   | 系统的一些功能案例                               | 支持  | 不支持              | | ||||
|  | ||||
| ## 参考文档 | ||||
|  | ||||
| 使用框架前请仔细阅读文档重点注意事项 | ||||
| <br> | ||||
| >[初始化项目 必看](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4164117&doc_id=1469725) | ||||
| >>[https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4164117&doc_id=1469725](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4164117&doc_id=1469725) | ||||
| >[初始化项目 必看](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4164117&doc_id=1469725) | ||||
| >>[https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4164117&doc_id=1469725](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4164117&doc_id=1469725) | ||||
| > | ||||
| >[专栏与视频 入门必看](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5473272&doc_id=1469725) | ||||
| >>[https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5473272&doc_id=1469725](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=5473272&doc_id=1469725) | ||||
| >[专栏与视频 入门必看](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=5473272&doc_id=1469725) | ||||
| >>[https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=5473272&doc_id=1469725](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=5473272&doc_id=1469725) | ||||
| > | ||||
| >[部署项目 必看](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4219382&doc_id=1469725) | ||||
| >>[https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4219382&doc_id=1469725](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4219382&doc_id=1469725) | ||||
| >[部署项目 必看](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4219382&doc_id=1469725) | ||||
| >>[https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4219382&doc_id=1469725](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4219382&doc_id=1469725) | ||||
| >  | ||||
| >[参考文档 Wiki](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages) | ||||
| >>[https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages) | ||||
| >[参考文档 Wiki](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages) | ||||
| >>[https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages) | ||||
|  | ||||
| ## 软件架构图 | ||||
|  | ||||
| @@ -83,81 +130,42 @@ | ||||
| ### 其他 | ||||
|  | ||||
| * 同步升级 RuoYi-Vue | ||||
| * GitHub 地址 [RuoYi-Vue-Plus-github](https://github.com/JavaLionLi/RuoYi-Vue-Plus) | ||||
| * 单模块 分支 [RuoYi-Vue-Plus-fast](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/tree/fast/) | ||||
| * GitHub 地址 [RuoYi-Vue-Plus-github](https://github.com/dromara/RuoYi-Vue-Plus) | ||||
| * 单模块 分支 [RuoYi-Vue-Plus-fast](https://gitee.com/dromara/RuoYi-Vue-Plus/tree/fast/) | ||||
| * 微服务 分支 [RuoYi-Cloud-Plus](https://gitee.com/JavaLionLi/RuoYi-Cloud-Plus) | ||||
| * 用户扩展项目 [扩展项目列表](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages?sort_id=4478302&doc_id=1469725) | ||||
| * 用户扩展项目 [扩展项目列表](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4478302&doc_id=1469725) | ||||
|  | ||||
| ## 加群与捐献 | ||||
| >[加群与捐献](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/加群与捐献?sort_id=4104598) | ||||
| >>[https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/加群与捐献?sort_id=4104598](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/加群与捐献?sort_id=4104598) | ||||
| >[加群与捐献](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/加群与捐献?sort_id=4104598) | ||||
| >>[https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/加群与捐献?sort_id=4104598](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/加群与捐献?sort_id=4104598) | ||||
|  | ||||
| ## 捐献作者 | ||||
| 作者为兼职做开源,平时还需要工作,如果帮到了您可以请作者吃个盒饭   | ||||
| <img src="https://images.gitee.com/uploads/images/2022/0218/213734_b1b8197f_1766278.jpeg" width="300px" height="450px" /> | ||||
| <img src="https://images.gitee.com/uploads/images/2021/0525/101713_3d18b119_1766278.jpeg" width="300px" height="450px" /> | ||||
|  | ||||
| ## 业务功能 | ||||
|  | ||||
| | 功能 | 介绍 | | ||||
| |---|---| | ||||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置。 | | ||||
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。 | | ||||
| | 岗位管理 | 配置系统用户所属担任职务。 | | ||||
| | 菜单管理 | 配置系统菜单,操作权限,按钮权限标识等。 | | ||||
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分。 | | ||||
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护。 | | ||||
| | 参数管理 | 对系统动态配置常用参数。 | | ||||
| | 通知公告 | 系统通知公告信息发布维护。 | | ||||
| | 操作日志 | 系统正常操作日志记录和查询;系统异常信息日志记录和查询。 | | ||||
| | 登录日志 | 系统登录日志记录查询包含登录异常。 | | ||||
| | 文件管理 | 系统文件上传、下载等管理。 | | ||||
| | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志。 | | ||||
| | 代码生成 | 前后端代码的生成(java、html、xml、sql)支持CRUD下载 。 | | ||||
| | 系统接口 | 根据业务代码自动生成相关的api接口文档。 | | ||||
| | 服务监控 | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等。 | | ||||
| | 缓存监控 | 对系统的缓存信息查询,命令统计等。 | | ||||
| | 在线构建器 | 拖动表单元素生成相应的HTML代码。 | | ||||
| | 连接池监视 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。 | | ||||
| | 使用案例 | 系统的一些功能案例 | | ||||
|  | ||||
| ## 演示图例 | ||||
|  | ||||
| <table border="1" cellpadding="1" cellspacing="1" style="width:500px"> | ||||
|     <tbody> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-972235bcbe3518dedd351ff0e2ee7d1031c.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-5e0097702fa91e2e36391de8127676a7fa1.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td> | ||||
|             <p><img src="https://oscimg.oschina.net/oscnet/up-e56e3828f48cd9886d88731766f06d5f3c1.png" width="1920" /></p> | ||||
|             </td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-0715990ea1a9f254ec2138fcd063c1f556a.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-eaf5417ccf921bb64abb959e3d8e290467f.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-fc285cf33095ebf8318de6999af0f473861.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-60c83fd8bd61c29df6dbf47c88355e9c272.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-7f731948c8b73c7d90f67f9e1c7a534d5c3.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-e4de89b5e2d20c52d3c3a47f9eb88eb8526.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-8791d823a508eb90e67c604f36f57491a67.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-4589afd99982ead331785299b894174feb6.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-8ea177cdacaea20995daf2f596b15232561.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-32d1d04c55c11f74c9129fbbc58399728c4.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-04fa118f7631b7ae6fd72299ca0a1430a63.png" width="1920" /></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-fe7e85b65827802bfaadf3acd42568b58c7.png" width="1920" /></td> | ||||
|             <td><img src="https://oscimg.oschina.net/oscnet/up-eff2b02a54f8188022d8498cfe6af6fcc06.png" width="1920" /></td> | ||||
|         </tr> | ||||
|     </tbody> | ||||
| </table> | ||||
| |                                                                                            |                                                                                            | | ||||
| |--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
| |  |  | | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										77
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -6,45 +6,44 @@ | ||||
|  | ||||
|     <groupId>com.ruoyi</groupId> | ||||
|     <artifactId>ruoyi-vue-plus</artifactId> | ||||
|     <version>4.5.0</version> | ||||
|     <version>4.8.2</version> | ||||
|  | ||||
|     <name>RuoYi-Vue-Plus</name> | ||||
|     <url>https://gitee.com/JavaLionLi/RuoYi-Vue-Plus</url> | ||||
|     <url>https://gitee.com/dromara/RuoYi-Vue-Plus</url> | ||||
|     <description>RuoYi-Vue-Plus后台管理系统</description> | ||||
|  | ||||
|     <properties> | ||||
|         <ruoyi-vue-plus.version>4.5.0</ruoyi-vue-plus.version> | ||||
|         <spring-boot.version>2.7.7</spring-boot.version> | ||||
|         <ruoyi-vue-plus.version>4.8.2</ruoyi-vue-plus.version> | ||||
|         <spring-boot.version>2.7.18</spring-boot.version> | ||||
|         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||||
|         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | ||||
|         <java.version>1.8</java.version> | ||||
|         <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version> | ||||
|         <spring-boot.mybatis>2.2.2</spring-boot.mybatis> | ||||
|         <springdoc.version>1.6.14</springdoc.version> | ||||
|         <springdoc.version>1.6.15</springdoc.version> | ||||
|         <poi.version>5.2.3</poi.version> | ||||
|         <easyexcel.version>3.1.5</easyexcel.version> | ||||
|         <easyexcel.version>3.3.2</easyexcel.version> | ||||
|         <velocity.version>2.3</velocity.version> | ||||
|         <satoken.version>1.34.0</satoken.version> | ||||
|         <mybatis-plus.version>3.5.3.1</mybatis-plus.version> | ||||
|         <satoken.version>1.37.0</satoken.version> | ||||
|         <mybatis-plus.version>3.5.4</mybatis-plus.version> | ||||
|         <p6spy.version>3.9.1</p6spy.version> | ||||
|         <hutool.version>5.8.11</hutool.version> | ||||
|         <hutool.version>5.8.22</hutool.version> | ||||
|         <okhttp.version>4.10.0</okhttp.version> | ||||
|         <spring-boot-admin.version>2.7.10</spring-boot-admin.version> | ||||
|         <redisson.version>3.19.1</redisson.version> | ||||
|         <spring-boot-admin.version>2.7.11</spring-boot-admin.version> | ||||
|         <redisson.version>3.20.1</redisson.version> | ||||
|         <lock4j.version>2.2.3</lock4j.version> | ||||
|         <dynamic-ds.version>3.5.2</dynamic-ds.version> | ||||
|         <alibaba-ttl.version>2.14.2</alibaba-ttl.version> | ||||
|         <xxl-job.version>2.3.1</xxl-job.version> | ||||
|         <lombok.version>1.18.24</lombok.version> | ||||
|  | ||||
|         <!-- 临时修复 snakeyaml 漏洞 --> | ||||
|         <snakeyaml.version>1.33</snakeyaml.version> | ||||
|         <xxl-job.version>2.4.0</xxl-job.version> | ||||
|         <lombok.version>1.18.30</lombok.version> | ||||
|         <bouncycastle.version>1.72</bouncycastle.version> | ||||
|         <!-- 离线IP地址定位库 --> | ||||
|         <ip2region.version>2.7.0</ip2region.version> | ||||
|  | ||||
|         <!-- OSS 配置 --> | ||||
|         <aws-java-sdk-s3.version>1.12.373</aws-java-sdk-s3.version> | ||||
|         <aws-java-sdk-s3.version>1.12.540</aws-java-sdk-s3.version> | ||||
|         <!-- SMS 配置 --> | ||||
|         <aliyun.sms.version>2.0.23</aliyun.sms.version> | ||||
|         <tencent.sms.version>3.1.660</tencent.sms.version> | ||||
|         <sms4j.version>2.2.0</sms4j.version> | ||||
|     </properties> | ||||
|  | ||||
|     <profiles> | ||||
| @@ -53,7 +52,7 @@ | ||||
|             <properties> | ||||
|                 <!-- 环境标识,需要与配置文件的名称相对应 --> | ||||
|                 <profiles.active>local</profiles.active> | ||||
|                 <logging.level>debug</logging.level> | ||||
|                 <logging.level>info</logging.level> | ||||
|             </properties> | ||||
|         </profile> | ||||
|         <profile> | ||||
| @@ -61,7 +60,7 @@ | ||||
|             <properties> | ||||
|                 <!-- 环境标识,需要与配置文件的名称相对应 --> | ||||
|                 <profiles.active>dev</profiles.active> | ||||
|                 <logging.level>debug</logging.level> | ||||
|                 <logging.level>info</logging.level> | ||||
|             </properties> | ||||
|             <activation> | ||||
|                 <!-- 默认环境 --> | ||||
| @@ -197,16 +196,11 @@ | ||||
|                 <version>${aws-java-sdk-s3.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!--短信sms4j--> | ||||
|             <dependency> | ||||
|                 <groupId>com.aliyun</groupId> | ||||
|                 <artifactId>dysmsapi20170525</artifactId> | ||||
|                 <version>${aliyun.sms.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <dependency> | ||||
|                 <groupId>com.tencentcloudapi</groupId> | ||||
|                 <artifactId>tencentcloud-sdk-java-sms</artifactId> | ||||
|                 <version>${tencent.sms.version}</version> | ||||
|                 <groupId>org.dromara.sms4j</groupId> | ||||
|                 <artifactId>sms4j-spring-boot-starter</artifactId> | ||||
|                 <version>${sms4j.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <dependency> | ||||
| @@ -257,11 +251,18 @@ | ||||
|                 <version>${alibaba-ttl.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- 临时修复 snakeyaml 漏洞 --> | ||||
|             <!-- 离线IP地址定位库 ip2region --> | ||||
|             <dependency> | ||||
|                 <groupId>org.yaml</groupId> | ||||
|                 <artifactId>snakeyaml</artifactId> | ||||
|                 <version>${snakeyaml.version}</version> | ||||
|                 <groupId>org.lionsoul</groupId> | ||||
|                 <artifactId>ip2region</artifactId> | ||||
|                 <version>${ip2region.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- 加密包引入 --> | ||||
|             <dependency> | ||||
|                 <groupId>org.bouncycastle</groupId> | ||||
|                 <artifactId>bcprov-jdk15to18</artifactId> | ||||
|                 <version>${bouncycastle.version}</version> | ||||
|             </dependency> | ||||
|  | ||||
|             <!-- 定时任务 --> | ||||
| @@ -403,8 +404,8 @@ | ||||
|     <repositories> | ||||
|         <repository> | ||||
|             <id>public</id> | ||||
|             <name>aliyun nexus</name> | ||||
|             <url>https://maven.aliyun.com/repository/public/</url> | ||||
|             <name>huawei nexus</name> | ||||
|             <url>https://mirrors.huaweicloud.com/repository/maven/</url> | ||||
|             <releases> | ||||
|                 <enabled>true</enabled> | ||||
|             </releases> | ||||
| @@ -414,8 +415,8 @@ | ||||
|     <pluginRepositories> | ||||
|         <pluginRepository> | ||||
|             <id>public</id> | ||||
|             <name>aliyun nexus</name> | ||||
|             <url>https://maven.aliyun.com/repository/public/</url> | ||||
|             <name>huawei nexus</name> | ||||
|             <url>https://mirrors.huaweicloud.com/repository/maven/</url> | ||||
|             <releases> | ||||
|                 <enabled>true</enabled> | ||||
|             </releases> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|     <parent> | ||||
|         <artifactId>ruoyi-vue-plus</artifactId> | ||||
|         <groupId>com.ruoyi</groupId> | ||||
|         <version>4.5.0</version> | ||||
|         <version>4.8.2</version> | ||||
|     </parent> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
|     <packaging>jar</packaging> | ||||
|   | ||||
| @@ -10,16 +10,19 @@ import com.ruoyi.common.constant.Constants; | ||||
| import com.ruoyi.common.core.domain.R; | ||||
| import com.ruoyi.common.enums.CaptchaType; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import com.ruoyi.common.utils.email.MailUtils; | ||||
| import com.ruoyi.common.utils.redis.RedisUtils; | ||||
| import com.ruoyi.common.utils.reflect.ReflectUtils; | ||||
| import com.ruoyi.common.utils.spring.SpringUtils; | ||||
| import com.ruoyi.framework.config.properties.CaptchaProperties; | ||||
| import com.ruoyi.sms.config.properties.SmsProperties; | ||||
| import com.ruoyi.sms.core.SmsTemplate; | ||||
| import com.ruoyi.sms.entity.SmsResult; | ||||
| import com.ruoyi.framework.config.properties.MailProperties; | ||||
| import com.ruoyi.system.service.ISysConfigService; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.sms4j.api.SmsBlend; | ||||
| import org.dromara.sms4j.api.entity.SmsResponse; | ||||
| import org.dromara.sms4j.core.factory.SmsFactory; | ||||
| import org.dromara.sms4j.provider.enumerate.SupplierType; | ||||
| import org.springframework.expression.Expression; | ||||
| import org.springframework.expression.ExpressionParser; | ||||
| import org.springframework.expression.spel.standard.SpelExpressionParser; | ||||
| @@ -30,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController; | ||||
| import javax.validation.constraints.NotBlank; | ||||
| import java.time.Duration; | ||||
| import java.util.HashMap; | ||||
| import java.util.LinkedHashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
| @@ -45,8 +49,8 @@ import java.util.Map; | ||||
| public class CaptchaController { | ||||
|  | ||||
|     private final CaptchaProperties captchaProperties; | ||||
|     private final SmsProperties smsProperties; | ||||
|     private final ISysConfigService configService; | ||||
|     private final MailProperties mailProperties; | ||||
|  | ||||
|     /** | ||||
|      * 短信验证码 | ||||
| @@ -54,23 +58,41 @@ public class CaptchaController { | ||||
|      * @param phonenumber 用户手机号 | ||||
|      */ | ||||
|     @GetMapping("/captchaSms") | ||||
|     public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") | ||||
|                               String phonenumber) { | ||||
|         if (!smsProperties.getEnabled()) { | ||||
|             return R.fail("当前系统没有开启短信功能!"); | ||||
|         } | ||||
|     public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) { | ||||
|         String key = CacheConstants.CAPTCHA_CODE_KEY + phonenumber; | ||||
|         String code = RandomUtil.randomNumbers(4); | ||||
|         RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); | ||||
|         // 验证码模板id 自行处理 (查数据库或写死均可) | ||||
|         String templateId = ""; | ||||
|         Map<String, String> map = new HashMap<>(1); | ||||
|         LinkedHashMap<String, String> map = new LinkedHashMap<>(1); | ||||
|         map.put("code", code); | ||||
|         SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class); | ||||
|         SmsResult result = smsTemplate.send(phonenumber, templateId, map); | ||||
|         if (!result.isSuccess()) { | ||||
|             log.error("验证码短信发送异常 => {}", result); | ||||
|             return R.fail(result.getMessage()); | ||||
|         SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA); | ||||
|         SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map); | ||||
|         if (!"OK".equals(smsResponse.getCode())) { | ||||
|             log.error("验证码短信发送异常 => {}", smsResponse); | ||||
|             return R.fail(smsResponse.getMessage()); | ||||
|         } | ||||
|         return R.ok(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 邮箱验证码 | ||||
|      * | ||||
|      * @param email 邮箱 | ||||
|      */ | ||||
|     @GetMapping("/captchaEmail") | ||||
|     public R<Void> emailCode(@NotBlank(message = "{user.email.not.blank}") String email) { | ||||
|         if (!mailProperties.getEnabled()) { | ||||
|             return R.fail("当前系统没有开启邮箱功能!"); | ||||
|         } | ||||
|         String key = CacheConstants.CAPTCHA_CODE_KEY + email; | ||||
|         String code = RandomUtil.randomNumbers(4); | ||||
|         RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); | ||||
|         try { | ||||
|             MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。"); | ||||
|         } catch (Exception e) { | ||||
|             log.error("验证码短信发送异常 => {}", e.getMessage()); | ||||
|             return R.fail(e.getMessage()); | ||||
|         } | ||||
|         return R.ok(); | ||||
|     } | ||||
|   | ||||
| @@ -33,7 +33,6 @@ public class CacheController { | ||||
|     private final static List<SysCache> CACHES = new ArrayList<>(); | ||||
|  | ||||
|     static { | ||||
|         CACHES.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息")); | ||||
|         CACHES.add(new SysCache(CacheConstants.ONLINE_TOKEN_KEY, "在线用户")); | ||||
|         CACHES.add(new SysCache(CacheNames.SYS_CONFIG, "配置信息")); | ||||
|         CACHES.add(new SysCache(CacheNames.SYS_DICT, "数据字典")); | ||||
|   | ||||
| @@ -45,9 +45,9 @@ public class SysUserOnlineController extends BaseController { | ||||
|         List<String> keys = StpUtil.searchTokenValue("", 0, -1, false); | ||||
|         List<UserOnlineDTO> userOnlineDTOList = new ArrayList<>(); | ||||
|         for (String key : keys) { | ||||
|             String token = key.replace(CacheConstants.LOGIN_TOKEN_KEY, ""); | ||||
|             String token = StringUtils.substringAfterLast(key, ":"); | ||||
|             // 如果已经过期则跳过 | ||||
|             if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) { | ||||
|             if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) { | ||||
|                 continue; | ||||
|             } | ||||
|             userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token)); | ||||
|   | ||||
| @@ -79,7 +79,7 @@ public class SysConfigController extends BaseController { | ||||
|     @Log(title = "参数管理", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysConfig config) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) { | ||||
|         if (!configService.checkConfigKeyUnique(config)) { | ||||
|             return R.fail("新增参数'" + config.getConfigName() + "'失败,参数键名已存在"); | ||||
|         } | ||||
|         configService.insertConfig(config); | ||||
| @@ -93,7 +93,7 @@ public class SysConfigController extends BaseController { | ||||
|     @Log(title = "参数管理", businessType = BusinessType.UPDATE) | ||||
|     @PutMapping | ||||
|     public R<Void> edit(@Validated @RequestBody SysConfig config) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(configService.checkConfigKeyUnique(config))) { | ||||
|         if (!configService.checkConfigKeyUnique(config)) { | ||||
|             return R.fail("修改参数'" + config.getConfigName() + "'失败,参数键名已存在"); | ||||
|         } | ||||
|         configService.updateConfig(config); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package com.ruoyi.web.controller.system; | ||||
|  | ||||
| import cn.dev33.satoken.annotation.SaCheckPermission; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import com.ruoyi.common.annotation.Log; | ||||
| import com.ruoyi.common.constant.UserConstants; | ||||
| import com.ruoyi.common.core.controller.BaseController; | ||||
| @@ -49,7 +49,7 @@ public class SysDeptController extends BaseController { | ||||
|     public R<List<SysDept>> excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) { | ||||
|         List<SysDept> depts = deptService.selectDeptList(new SysDept()); | ||||
|         depts.removeIf(d -> d.getDeptId().equals(deptId) | ||||
|             || ArrayUtil.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")); | ||||
|             || StringUtils.splitList(d.getAncestors()).contains(Convert.toStr(deptId))); | ||||
|         return R.ok(depts); | ||||
|     } | ||||
|  | ||||
| @@ -72,7 +72,7 @@ public class SysDeptController extends BaseController { | ||||
|     @Log(title = "部门管理", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysDept dept) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { | ||||
|         if (!deptService.checkDeptNameUnique(dept)) { | ||||
|             return R.fail("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在"); | ||||
|         } | ||||
|         return toAjax(deptService.insertDept(dept)); | ||||
| @@ -87,13 +87,16 @@ public class SysDeptController extends BaseController { | ||||
|     public R<Void> edit(@Validated @RequestBody SysDept dept) { | ||||
|         Long deptId = dept.getDeptId(); | ||||
|         deptService.checkDeptDataScope(deptId); | ||||
|         if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) { | ||||
|         if (!deptService.checkDeptNameUnique(dept)) { | ||||
|             return R.fail("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在"); | ||||
|         } else if (dept.getParentId().equals(deptId)) { | ||||
|             return R.fail("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己"); | ||||
|         } else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) | ||||
|             && deptService.selectNormalChildrenDeptById(deptId) > 0) { | ||||
|             return R.fail("该部门包含未停用的子部门!"); | ||||
|         } else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus())) { | ||||
|             if (deptService.selectNormalChildrenDeptById(deptId) > 0) { | ||||
|                 return R.fail("该部门包含未停用的子部门!"); | ||||
|             } else if (deptService.checkDeptExistUser(deptId)) { | ||||
|                 return R.fail("该部门下存在已分配用户,不能禁用!"); | ||||
|             } | ||||
|         } | ||||
|         return toAjax(deptService.updateDept(dept)); | ||||
|     } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ public class SysDictTypeController extends BaseController { | ||||
|     @Log(title = "字典类型", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysDictType dict) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) { | ||||
|         if (!dictTypeService.checkDictTypeUnique(dict)) { | ||||
|             return R.fail("新增字典'" + dict.getDictName() + "'失败,字典类型已存在"); | ||||
|         } | ||||
|         dictTypeService.insertDictType(dict); | ||||
| @@ -83,7 +83,7 @@ public class SysDictTypeController extends BaseController { | ||||
|     @Log(title = "字典类型", businessType = BusinessType.UPDATE) | ||||
|     @PutMapping | ||||
|     public R<Void> edit(@Validated @RequestBody SysDictType dict) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(dictTypeService.checkDictTypeUnique(dict))) { | ||||
|         if (!dictTypeService.checkDictTypeUnique(dict)) { | ||||
|             return R.fail("修改字典'" + dict.getDictName() + "'失败,字典类型已存在"); | ||||
|         } | ||||
|         dictTypeService.updateDictType(dict); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import com.ruoyi.common.constant.Constants; | ||||
| import com.ruoyi.common.core.domain.R; | ||||
| import com.ruoyi.common.core.domain.entity.SysMenu; | ||||
| import com.ruoyi.common.core.domain.entity.SysUser; | ||||
| import com.ruoyi.common.core.domain.model.EmailLoginBody; | ||||
| import com.ruoyi.common.core.domain.model.LoginBody; | ||||
| import com.ruoyi.common.core.domain.model.LoginUser; | ||||
| import com.ruoyi.common.core.domain.model.SmsLoginBody; | ||||
| @@ -57,7 +58,7 @@ public class SysLoginController { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 短信登录(示例) | ||||
|      * 短信登录 | ||||
|      * | ||||
|      * @param smsLoginBody 登录信息 | ||||
|      * @return 结果 | ||||
| @@ -72,6 +73,21 @@ public class SysLoginController { | ||||
|         return R.ok(ajax); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 邮件登录 | ||||
|      * | ||||
|      * @param body 登录信息 | ||||
|      * @return 结果 | ||||
|      */ | ||||
|     @PostMapping("/emailLogin") | ||||
|     public R<Map<String, Object>> emailLogin(@Validated @RequestBody EmailLoginBody body) { | ||||
|         Map<String, Object> ajax = new HashMap<>(); | ||||
|         // 生成令牌 | ||||
|         String token = loginService.emailLogin(body.getEmail(), body.getEmailCode()); | ||||
|         ajax.put(Constants.TOKEN, token); | ||||
|         return R.ok(ajax); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 小程序登录(示例) | ||||
|      * | ||||
|   | ||||
| @@ -82,7 +82,7 @@ public class SysMenuController extends BaseController { | ||||
|     @Log(title = "菜单管理", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysMenu menu) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { | ||||
|         if (!menuService.checkMenuNameUnique(menu)) { | ||||
|             return R.fail("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); | ||||
|         } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) { | ||||
|             return R.fail("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); | ||||
| @@ -97,7 +97,7 @@ public class SysMenuController extends BaseController { | ||||
|     @Log(title = "菜单管理", businessType = BusinessType.UPDATE) | ||||
|     @PutMapping | ||||
|     public R<Void> edit(@Validated @RequestBody SysMenu menu) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(menuService.checkMenuNameUnique(menu))) { | ||||
|         if (!menuService.checkMenuNameUnique(menu)) { | ||||
|             return R.fail("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在"); | ||||
|         } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) { | ||||
|             return R.fail("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头"); | ||||
|   | ||||
| @@ -2,10 +2,7 @@ package com.ruoyi.web.controller.system; | ||||
|  | ||||
|  | ||||
| import cn.dev33.satoken.annotation.SaCheckPermission; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.http.HttpException; | ||||
| import cn.hutool.http.HttpUtil; | ||||
| import com.ruoyi.common.annotation.Log; | ||||
| import com.ruoyi.common.core.controller.BaseController; | ||||
| import com.ruoyi.common.core.domain.PageQuery; | ||||
| @@ -13,11 +10,6 @@ import com.ruoyi.common.core.domain.R; | ||||
| import com.ruoyi.common.core.page.TableDataInfo; | ||||
| import com.ruoyi.common.core.validate.QueryGroup; | ||||
| import com.ruoyi.common.enums.BusinessType; | ||||
| import com.ruoyi.common.exception.ServiceException; | ||||
| import com.ruoyi.common.utils.file.FileUtils; | ||||
| import com.ruoyi.oss.core.OssClient; | ||||
| import com.ruoyi.oss.factory.OssFactory; | ||||
| import com.ruoyi.system.domain.SysOss; | ||||
| import com.ruoyi.system.domain.bo.SysOssBo; | ||||
| import com.ruoyi.system.domain.vo.SysOssVo; | ||||
| import com.ruoyi.system.service.ISysOssService; | ||||
| @@ -80,7 +72,7 @@ public class SysOssController extends BaseController { | ||||
|     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | ||||
|     public R<Map<String, String>> upload(@RequestPart("file") MultipartFile file) { | ||||
|         if (ObjectUtil.isNull(file)) { | ||||
|             throw new ServiceException("上传文件不能为空"); | ||||
|             return R.fail("上传文件不能为空"); | ||||
|         } | ||||
|         SysOssVo oss = iSysOssService.upload(file); | ||||
|         Map<String, String> map = new HashMap<>(2); | ||||
|   | ||||
| @@ -69,9 +69,9 @@ public class SysPostController extends BaseController { | ||||
|     @Log(title = "岗位管理", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysPost post) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) { | ||||
|         if (!postService.checkPostNameUnique(post)) { | ||||
|             return R.fail("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在"); | ||||
|         } else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) { | ||||
|         } else if (!postService.checkPostCodeUnique(post)) { | ||||
|             return R.fail("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在"); | ||||
|         } | ||||
|         return toAjax(postService.insertPost(post)); | ||||
| @@ -84,10 +84,13 @@ public class SysPostController extends BaseController { | ||||
|     @Log(title = "岗位管理", businessType = BusinessType.UPDATE) | ||||
|     @PutMapping | ||||
|     public R<Void> edit(@Validated @RequestBody SysPost post) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(postService.checkPostNameUnique(post))) { | ||||
|         if (!postService.checkPostNameUnique(post)) { | ||||
|             return R.fail("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在"); | ||||
|         } else if (UserConstants.NOT_UNIQUE.equals(postService.checkPostCodeUnique(post))) { | ||||
|         } else if (!postService.checkPostCodeUnique(post)) { | ||||
|             return R.fail("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在"); | ||||
|         } else if (UserConstants.POST_DISABLE.equals(post.getStatus()) | ||||
|             && postService.countUserPostById(post.getPostId()) > 0) { | ||||
|             return R.fail("该岗位下存在已分配用户,不能禁用!"); | ||||
|         } | ||||
|         return toAjax(postService.updatePost(post)); | ||||
|     } | ||||
| @@ -109,7 +112,9 @@ public class SysPostController extends BaseController { | ||||
|      */ | ||||
|     @GetMapping("/optionselect") | ||||
|     public R<List<SysPost>> optionselect() { | ||||
|         List<SysPost> posts = postService.selectPostAll(); | ||||
|         SysPost post = new SysPost(); | ||||
|         post.setStatus(UserConstants.POST_NORMAL); | ||||
|         List<SysPost> posts = postService.selectPostList(post); | ||||
|         return R.ok(posts); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -58,12 +58,10 @@ public class SysProfileController extends BaseController { | ||||
|     @Log(title = "个人信息", businessType = BusinessType.UPDATE) | ||||
|     @PutMapping | ||||
|     public R<Void> updateProfile(@RequestBody SysUser user) { | ||||
|         if (StringUtils.isNotEmpty(user.getPhonenumber()) | ||||
|             && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { | ||||
|         if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) { | ||||
|             return R.fail("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); | ||||
|         } | ||||
|         if (StringUtils.isNotEmpty(user.getEmail()) | ||||
|             && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { | ||||
|         if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) { | ||||
|             return R.fail("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); | ||||
|         } | ||||
|         user.setUserId(getUserId()); | ||||
| @@ -80,8 +78,8 @@ public class SysProfileController extends BaseController { | ||||
|     /** | ||||
|      * 重置密码 | ||||
|      * | ||||
|      * @param newPassword 旧密码 | ||||
|      * @param oldPassword 新密码 | ||||
|      * @param newPassword 新密码 | ||||
|      * @param oldPassword 旧密码 | ||||
|      */ | ||||
|     @Log(title = "个人信息", businessType = BusinessType.UPDATE) | ||||
|     @PutMapping("/updatePwd") | ||||
|   | ||||
| @@ -1,19 +1,15 @@ | ||||
| package com.ruoyi.web.controller.system; | ||||
|  | ||||
| import cn.dev33.satoken.annotation.SaCheckPermission; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.ruoyi.common.annotation.Log; | ||||
| import com.ruoyi.common.constant.UserConstants; | ||||
| import com.ruoyi.common.core.controller.BaseController; | ||||
| import com.ruoyi.common.core.domain.PageQuery; | ||||
| import com.ruoyi.common.core.domain.R; | ||||
| import com.ruoyi.common.core.domain.entity.SysDept; | ||||
| import com.ruoyi.common.core.domain.entity.SysRole; | ||||
| import com.ruoyi.common.core.domain.entity.SysUser; | ||||
| import com.ruoyi.common.core.domain.model.LoginUser; | ||||
| import com.ruoyi.common.core.page.TableDataInfo; | ||||
| import com.ruoyi.common.enums.BusinessType; | ||||
| import com.ruoyi.common.helper.LoginHelper; | ||||
| import com.ruoyi.common.utils.poi.ExcelUtil; | ||||
| import com.ruoyi.system.domain.SysUserRole; | ||||
| import com.ruoyi.system.service.ISysDeptService; | ||||
| @@ -84,9 +80,10 @@ public class SysRoleController extends BaseController { | ||||
|     @Log(title = "角色管理", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysRole role) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) { | ||||
|         roleService.checkRoleAllowed(role); | ||||
|         if (!roleService.checkRoleNameUnique(role)) { | ||||
|             return R.fail("新增角色'" + role.getRoleName() + "'失败,角色名称已存在"); | ||||
|         } else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) { | ||||
|         } else if (!roleService.checkRoleKeyUnique(role)) { | ||||
|             return R.fail("新增角色'" + role.getRoleName() + "'失败,角色权限已存在"); | ||||
|         } | ||||
|         return toAjax(roleService.insertRole(role)); | ||||
| @@ -102,20 +99,14 @@ public class SysRoleController extends BaseController { | ||||
|     public R<Void> edit(@Validated @RequestBody SysRole role) { | ||||
|         roleService.checkRoleAllowed(role); | ||||
|         roleService.checkRoleDataScope(role.getRoleId()); | ||||
|         if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleNameUnique(role))) { | ||||
|         if (!roleService.checkRoleNameUnique(role)) { | ||||
|             return R.fail("修改角色'" + role.getRoleName() + "'失败,角色名称已存在"); | ||||
|         } else if (UserConstants.NOT_UNIQUE.equals(roleService.checkRoleKeyUnique(role))) { | ||||
|         } else if (!roleService.checkRoleKeyUnique(role)) { | ||||
|             return R.fail("修改角色'" + role.getRoleName() + "'失败,角色权限已存在"); | ||||
|         } | ||||
|  | ||||
|         if (roleService.updateRole(role) > 0) { | ||||
|             // 更新缓存用户权限 | ||||
|             LoginUser loginUser = getLoginUser(); | ||||
|             SysUser sysUser = userService.selectUserById(loginUser.getUserId()); | ||||
|             if (ObjectUtil.isNotNull(sysUser) && !sysUser.isAdmin()) { | ||||
|                 loginUser.setMenuPermission(permissionService.getMenuPermission(sysUser)); | ||||
|                 LoginHelper.setLoginUser(loginUser); | ||||
|             } | ||||
|             roleService.cleanOnlineUserByRole(role.getRoleId()); | ||||
|             return R.ok(); | ||||
|         } | ||||
|         return R.fail("修改角色'" + role.getRoleName() + "'失败,请联系管理员"); | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import com.ruoyi.common.helper.LoginHelper; | ||||
| import com.ruoyi.common.utils.StreamUtils; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import com.ruoyi.common.utils.poi.ExcelUtil; | ||||
| import com.ruoyi.system.domain.SysPost; | ||||
| import com.ruoyi.system.domain.vo.SysUserExportVo; | ||||
| import com.ruoyi.system.domain.vo.SysUserImportVo; | ||||
| import com.ruoyi.system.listener.SysUserImportListener; | ||||
| @@ -117,9 +118,13 @@ public class SysUserController extends BaseController { | ||||
|     public R<Map<String, Object>> getInfo(@PathVariable(value = "userId", required = false) Long userId) { | ||||
|         userService.checkUserDataScope(userId); | ||||
|         Map<String, Object> ajax = new HashMap<>(); | ||||
|         List<SysRole> roles = roleService.selectRoleAll(); | ||||
|         SysRole role = new SysRole(); | ||||
|         role.setStatus(UserConstants.ROLE_NORMAL); | ||||
|         SysPost post = new SysPost(); | ||||
|         post.setStatus(UserConstants.POST_NORMAL); | ||||
|         List<SysRole> roles = roleService.selectRoleList(role); | ||||
|         ajax.put("roles", LoginHelper.isAdmin(userId) ? roles : StreamUtils.filter(roles, r -> !r.isAdmin())); | ||||
|         ajax.put("posts", postService.selectPostAll()); | ||||
|         ajax.put("posts", postService.selectPostList(post)); | ||||
|         if (ObjectUtil.isNotNull(userId)) { | ||||
|             SysUser sysUser = userService.selectUserById(userId); | ||||
|             ajax.put("user", sysUser); | ||||
| @@ -136,13 +141,12 @@ public class SysUserController extends BaseController { | ||||
|     @Log(title = "用户管理", businessType = BusinessType.INSERT) | ||||
|     @PostMapping | ||||
|     public R<Void> add(@Validated @RequestBody SysUser user) { | ||||
|         if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user))) { | ||||
|         deptService.checkDeptDataScope(user.getDeptId()); | ||||
|         if (!userService.checkUserNameUnique(user)) { | ||||
|             return R.fail("新增用户'" + user.getUserName() + "'失败,登录账号已存在"); | ||||
|         } else if (StringUtils.isNotEmpty(user.getPhonenumber()) | ||||
|             && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { | ||||
|         } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) { | ||||
|             return R.fail("新增用户'" + user.getUserName() + "'失败,手机号码已存在"); | ||||
|         } else if (StringUtils.isNotEmpty(user.getEmail()) | ||||
|             && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { | ||||
|         } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) { | ||||
|             return R.fail("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在"); | ||||
|         } | ||||
|         user.setPassword(BCrypt.hashpw(user.getPassword())); | ||||
| @@ -158,13 +162,12 @@ public class SysUserController extends BaseController { | ||||
|     public R<Void> edit(@Validated @RequestBody SysUser user) { | ||||
|         userService.checkUserAllowed(user); | ||||
|         userService.checkUserDataScope(user.getUserId()); | ||||
|         if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user))) { | ||||
|         deptService.checkDeptDataScope(user.getDeptId()); | ||||
|         if (!userService.checkUserNameUnique(user)) { | ||||
|             return R.fail("修改用户'" + user.getUserName() + "'失败,登录账号已存在"); | ||||
|         } else if (StringUtils.isNotEmpty(user.getPhonenumber()) | ||||
|             && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) { | ||||
|         } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) { | ||||
|             return R.fail("修改用户'" + user.getUserName() + "'失败,手机号码已存在"); | ||||
|         } else if (StringUtils.isNotEmpty(user.getEmail()) | ||||
|             && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) { | ||||
|         } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) { | ||||
|             return R.fail("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在"); | ||||
|         } | ||||
|         return toAjax(userService.updateUser(user)); | ||||
|   | ||||
| @@ -19,8 +19,8 @@ xxl.job: | ||||
|   executor: | ||||
|     # 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册 | ||||
|     appname: xxl-job-executor | ||||
|     # 执行器端口号 执行器从9101开始往后写 | ||||
|     port: 9101 | ||||
|     # 28080 端口 随着主应用端口飘逸 避免集群冲突 | ||||
|     port: 2${server.port} | ||||
|     # 执行器注册:默认IP:PORT | ||||
|     address: | ||||
|     # 执行器IP:默认自动获取IP | ||||
| @@ -86,15 +86,17 @@ spring: | ||||
|         # 最小空闲线程数量 | ||||
|         minIdle: 10 | ||||
|         # 配置获取连接等待超时的时间 | ||||
|         connectionTimeout: 10000 | ||||
|         connectionTimeout: 30000 | ||||
|         # 校验超时时间 | ||||
|         validationTimeout: 5000 | ||||
|         # 空闲连接存活最大时间,默认10分钟 | ||||
|         idleTimeout: 60000 | ||||
|         idleTimeout: 600000 | ||||
|         # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟 | ||||
|         maxLifetime: 900000 | ||||
|         maxLifetime: 1800000 | ||||
|         # 连接测试query(配置检测连接是否有效) | ||||
|         connectionTestQuery: SELECT 1 | ||||
|         # 多久检查一次连接的活性 | ||||
|         keepaliveTime: 30000 | ||||
|  | ||||
| --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) | ||||
| spring: | ||||
| @@ -156,14 +158,29 @@ mail: | ||||
|   # Socket连接超时值,单位毫秒,缺省值不超时 | ||||
|   connectionTimeout: 0 | ||||
|  | ||||
| --- # sms 短信 | ||||
| --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商 | ||||
| # https://wind.kim/doc/start 文档地址 各个厂商可同时使用 | ||||
| sms: | ||||
|   enabled: false | ||||
|   # 阿里云 dysmsapi.aliyuncs.com | ||||
|   # 腾讯云 sms.tencentcloudapi.com | ||||
|   endpoint: "dysmsapi.aliyuncs.com" | ||||
|   alibaba: | ||||
|     #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置 | ||||
|     requestUrl: dysmsapi.aliyuncs.com | ||||
|     #阿里云的accessKey | ||||
|     accessKeyId: xxxxxxx | ||||
|   accessKeySecret: xxxxxx | ||||
|   signName: 测试 | ||||
|   # 腾讯专用 | ||||
|   sdkAppId: | ||||
|     #阿里云的accessKeySecret | ||||
|     accessKeySecret: xxxxxxx | ||||
|     #短信签名 | ||||
|     signature: 测试 | ||||
|   tencent: | ||||
|     #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置 | ||||
|     requestUrl: sms.tencentcloudapi.com | ||||
|     #腾讯云的accessKey | ||||
|     accessKeyId: xxxxxxx | ||||
|     #腾讯云的accessKeySecret | ||||
|     accessKeySecret: xxxxxxx | ||||
|     #短信签名 | ||||
|     signature: 测试 | ||||
|     #短信sdkAppId | ||||
|     sdkAppId: appid | ||||
|     #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置 | ||||
|     territory: ap-guangzhou | ||||
|   | ||||
| @@ -22,8 +22,8 @@ xxl.job: | ||||
|   executor: | ||||
|     # 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册 | ||||
|     appname: xxl-job-executor | ||||
|     # 执行器端口号 执行器从9101开始往后写 | ||||
|     port: 9101 | ||||
|     # 28080 端口 随着主应用端口飘逸 避免集群冲突 | ||||
|     port: 2${server.port} | ||||
|     # 执行器注册:默认IP:PORT | ||||
|     address: | ||||
|     # 执行器IP:默认自动获取IP | ||||
| @@ -89,15 +89,17 @@ spring: | ||||
|         # 最小空闲线程数量 | ||||
|         minIdle: 10 | ||||
|         # 配置获取连接等待超时的时间 | ||||
|         connectionTimeout: 10000 | ||||
|         connectionTimeout: 30000 | ||||
|         # 校验超时时间 | ||||
|         validationTimeout: 5000 | ||||
|         # 空闲连接存活最大时间,默认10分钟 | ||||
|         idleTimeout: 60000 | ||||
|         idleTimeout: 600000 | ||||
|         # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认30分钟 | ||||
|         maxLifetime: 900000 | ||||
|         maxLifetime: 1800000 | ||||
|         # 连接测试query(配置检测连接是否有效) | ||||
|         connectionTestQuery: SELECT 1 | ||||
|         # 多久检查一次连接的活性 | ||||
|         keepaliveTime: 30000 | ||||
|  | ||||
| --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) | ||||
| spring: | ||||
| @@ -159,14 +161,29 @@ mail: | ||||
|   # Socket连接超时值,单位毫秒,缺省值不超时 | ||||
|   connectionTimeout: 0 | ||||
|  | ||||
| --- # sms 短信 | ||||
| --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商 | ||||
| # https://wind.kim/doc/start 文档地址 各个厂商可同时使用 | ||||
| sms: | ||||
|   enabled: false | ||||
|   # 阿里云 dysmsapi.aliyuncs.com | ||||
|   # 腾讯云 sms.tencentcloudapi.com | ||||
|   endpoint: "dysmsapi.aliyuncs.com" | ||||
|   alibaba: | ||||
|     #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置 | ||||
|     requestUrl: dysmsapi.aliyuncs.com | ||||
|     #阿里云的accessKey | ||||
|     accessKeyId: xxxxxxx | ||||
|   accessKeySecret: xxxxxx | ||||
|   signName: 测试 | ||||
|   # 腾讯专用 | ||||
|   sdkAppId: | ||||
|     #阿里云的accessKeySecret | ||||
|     accessKeySecret: xxxxxxx | ||||
|     #短信签名 | ||||
|     signature: 测试 | ||||
|   tencent: | ||||
|     #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置 | ||||
|     requestUrl: sms.tencentcloudapi.com | ||||
|     #腾讯云的accessKey | ||||
|     accessKeyId: xxxxxxx | ||||
|     #腾讯云的accessKeySecret | ||||
|     accessKeySecret: xxxxxxx | ||||
|     #短信签名 | ||||
|     signature: 测试 | ||||
|     #短信sdkAppId | ||||
|     sdkAppId: appid | ||||
|     #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置 | ||||
|     territory: ap-guangzhou | ||||
|   | ||||
| @@ -5,11 +5,7 @@ ruoyi: | ||||
|   # 版本 | ||||
|   version: ${ruoyi-vue-plus.version} | ||||
|   # 版权年份 | ||||
|   copyrightYear: 2022 | ||||
|   # 实例演示开关 | ||||
|   demoEnabled: true | ||||
|   # 获取ip地址开关 | ||||
|   addressEnabled: true | ||||
|   copyrightYear: 2023 | ||||
|   # 缓存懒加载 | ||||
|   cacheLazy: false | ||||
|  | ||||
| @@ -51,7 +47,7 @@ logging: | ||||
|   level: | ||||
|     com.ruoyi: @logging.level@ | ||||
|     org.springframework: warn | ||||
|   config: classpath:logback.xml | ||||
|   config: classpath:logback-plus.xml | ||||
|  | ||||
| # 用户配置 | ||||
| user: | ||||
| @@ -104,8 +100,11 @@ sa-token: | ||||
|   token-name: Authorization | ||||
|   # token有效期 设为一天 (必定过期) 单位: 秒 | ||||
|   timeout: 86400 | ||||
|   # token临时有效期 (指定时间无操作就过期) 单位: 秒 | ||||
|   activity-timeout: 1800 | ||||
|   # 多端不同 token 有效期 可查看 LoginHelper.loginByDevice 方法自定义 | ||||
|   # token最低活跃时间 (指定时间无操作就过期) 单位: 秒 | ||||
|   active-timeout: 1800 | ||||
|   # 允许动态设置 token 有效期 | ||||
|   dynamic-active-timeout: true | ||||
|   # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) | ||||
|   is-concurrent: true | ||||
|   # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) | ||||
| @@ -128,8 +127,10 @@ security: | ||||
|     - /**/*.html | ||||
|     - /**/*.css | ||||
|     - /**/*.js | ||||
|     # swagger 文档配置 | ||||
|     # 公共路径 | ||||
|     - /favicon.ico | ||||
|     - /error | ||||
|     # swagger 文档配置 | ||||
|     - /*/api-docs | ||||
|     - /*/api-docs/** | ||||
|     # actuator 监控配置 | ||||
| @@ -180,10 +181,27 @@ mybatis-plus: | ||||
|       # 字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件 | ||||
|       where-strategy: NOT_NULL | ||||
|  | ||||
| # Swagger配置 | ||||
| swagger: | ||||
|   # 是否开启swagger | ||||
| # 数据加密 | ||||
| mybatis-encryptor: | ||||
|   # 是否开启加密 | ||||
|   enable: false | ||||
|   # 默认加密算法 | ||||
|   algorithm: BASE64 | ||||
|   # 编码方式 BASE64/HEX。默认BASE64 | ||||
|   encode: BASE64 | ||||
|   # 安全秘钥 对称算法的秘钥 如:AES,SM4 | ||||
|   password: | ||||
|   # 公私钥 非对称算法的公私钥 如:SM2,RSA | ||||
|   publicKey: | ||||
|   privateKey: | ||||
|  | ||||
| springdoc: | ||||
|   api-docs: | ||||
|     # 是否开启接口文档 | ||||
|     enabled: true | ||||
| #  swagger-ui: | ||||
| #    # 持久化认证数据 | ||||
| #    persistAuthorization: true | ||||
|   info: | ||||
|     # 标题 | ||||
|     title: '标题:${ruoyi.name}后台管理系统_接口文档' | ||||
| @@ -195,7 +213,7 @@ swagger: | ||||
|     contact: | ||||
|       name: Lion Li | ||||
|       email: crazylionli@163.com | ||||
|       url: https://gitee.com/JavaLionLi/RuoYi-Vue-Plus | ||||
|       url: https://gitee.com/dromara/RuoYi-Vue-Plus | ||||
|   components: | ||||
|     # 鉴权方式配置 | ||||
|     security-schemes: | ||||
| @@ -203,11 +221,6 @@ swagger: | ||||
|         type: APIKEY | ||||
|         in: HEADER | ||||
|         name: ${sa-token.token-name} | ||||
|  | ||||
| springdoc: | ||||
|   swagger-ui: | ||||
|     # 持久化认证数据 | ||||
|     persistAuthorization: true | ||||
|   #这里定义了两个分组,可定义多个,也可以不定义 | ||||
|   group-configs: | ||||
|     - group: 1.演示模块 | ||||
|   | ||||
| @@ -18,6 +18,7 @@ user.password.not.blank=用户密码不能为空 | ||||
| user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 | ||||
| user.password.not.valid=* 5-50个字符 | ||||
| user.email.not.valid=邮箱格式错误 | ||||
| user.email.not.blank=邮箱不能为空 | ||||
| user.phonenumber.not.blank=用户手机号不能为空 | ||||
| user.mobile.phone.number.not.valid=手机号格式错误 | ||||
| user.login.success=登录成功 | ||||
| @@ -42,4 +43,7 @@ rate.limiter.message=访问过于频繁,请稍候再试 | ||||
| sms.code.not.blank=短信验证码不能为空 | ||||
| sms.code.retry.limit.count=短信验证码输入错误{0}次 | ||||
| sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 | ||||
| email.code.not.blank=邮箱验证码不能为空 | ||||
| email.code.retry.limit.count=邮箱验证码输入错误{0}次 | ||||
| email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟 | ||||
| xcx.code.not.blank=小程序code不能为空 | ||||
|   | ||||
| @@ -18,6 +18,7 @@ user.password.not.blank=Password cannot be empty | ||||
| user.password.length.valid=Password length must be between {min} and {max} characters | ||||
| user.password.not.valid=* 5-50 characters | ||||
| user.email.not.valid=Mailbox format error | ||||
| user.email.not.blank=Mailbox cannot be blank | ||||
| user.phonenumber.not.blank=Phone number cannot be blank | ||||
| user.mobile.phone.number.not.valid=Phone number format error | ||||
| user.login.success=Login successful | ||||
| @@ -42,4 +43,7 @@ rate.limiter.message=Visit too frequently, please try again later | ||||
| sms.code.not.blank=Sms code cannot be blank | ||||
| sms.code.retry.limit.count=Sms code input error {0} times | ||||
| sms.code.retry.limit.exceed=Sms code input error {0} times, account locked for {1} minutes | ||||
| email.code.not.blank=Email code cannot be blank | ||||
| email.code.retry.limit.count=Email code input error {0} times | ||||
| email.code.retry.limit.exceed=Email code input error {0} times, account locked for {1} minutes | ||||
| xcx.code.not.blank=Mini program code cannot be blank | ||||
|   | ||||
| @@ -18,6 +18,7 @@ user.password.not.blank=用户密码不能为空 | ||||
| user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 | ||||
| user.password.not.valid=* 5-50个字符 | ||||
| user.email.not.valid=邮箱格式错误 | ||||
| user.email.not.blank=邮箱不能为空 | ||||
| user.phonenumber.not.blank=用户手机号不能为空 | ||||
| user.mobile.phone.number.not.valid=手机号格式错误 | ||||
| user.login.success=登录成功 | ||||
| @@ -42,4 +43,7 @@ rate.limiter.message=访问过于频繁,请稍候再试 | ||||
| sms.code.not.blank=短信验证码不能为空 | ||||
| sms.code.retry.limit.count=短信验证码输入错误{0}次 | ||||
| sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟 | ||||
| email.code.not.blank=邮箱验证码不能为空 | ||||
| email.code.retry.limit.count=邮箱验证码输入错误{0}次 | ||||
| email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟 | ||||
| xcx.code.not.blank=小程序code不能为空 | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								ruoyi-admin/src/main/resources/ip2region.xdb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								ruoyi-admin/src/main/resources/ip2region.xdb
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -5,7 +5,7 @@ | ||||
|     <parent> | ||||
|         <artifactId>ruoyi-vue-plus</artifactId> | ||||
|         <groupId>com.ruoyi</groupId> | ||||
|         <version>4.5.0</version> | ||||
|         <version>4.8.2</version> | ||||
|     </parent> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
|  | ||||
| @@ -153,6 +153,18 @@ | ||||
|             <artifactId>lock4j-redisson-spring-boot-starter</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 加密包引入 --> | ||||
|         <dependency> | ||||
|             <groupId>org.bouncycastle</groupId> | ||||
|             <artifactId>bcprov-jdk15to18</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 离线IP地址定位库 --> | ||||
|         <dependency> | ||||
|             <groupId>org.lionsoul</groupId> | ||||
|             <artifactId>ip2region</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|     </dependencies> | ||||
|  | ||||
| </project> | ||||
|   | ||||
| @@ -13,7 +13,9 @@ import java.lang.annotation.Target; | ||||
|  * 字典数据映射注解 | ||||
|  * | ||||
|  * @author itino | ||||
|  * @deprecated 建议使用通用翻译注解 | ||||
|  */ | ||||
| @Deprecated | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Target({ElementType.FIELD, ElementType.METHOD}) | ||||
| @JacksonAnnotationsInside | ||||
|   | ||||
| @@ -0,0 +1,44 @@ | ||||
| package com.ruoyi.common.annotation; | ||||
|  | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * 字段加密注解 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  */ | ||||
| @Documented | ||||
| @Inherited | ||||
| @Target({ElementType.FIELD}) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface EncryptField { | ||||
|  | ||||
|     /** | ||||
|      * 加密算法 | ||||
|      */ | ||||
|     AlgorithmType algorithm() default AlgorithmType.DEFAULT; | ||||
|  | ||||
|     /** | ||||
|      * 秘钥。AES、SM4需要 | ||||
|      */ | ||||
|     String password() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 公钥。RSA、SM2需要 | ||||
|      */ | ||||
|     String publicKey() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 私钥。RSA、SM2需要 | ||||
|      */ | ||||
|     String privateKey() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 编码方式。对加密算法为BASE64的不起作用 | ||||
|      */ | ||||
|     EncodeType encode() default EncodeType.DEFAULT; | ||||
|  | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| package com.ruoyi.common.annotation; | ||||
|  | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
| @@ -25,6 +27,6 @@ public @interface ExcelDictFormat { | ||||
|     /** | ||||
|      * 分隔符,读取字符串组内容 | ||||
|      */ | ||||
|     String separator() default ","; | ||||
|     String separator() default StringUtils.SEPARATOR; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,30 @@ | ||||
| package com.ruoyi.common.annotation; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * 枚举格式化 | ||||
|  * | ||||
|  * @author Liang | ||||
|  */ | ||||
| @Target({ElementType.FIELD}) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Inherited | ||||
| public @interface ExcelEnumFormat { | ||||
|  | ||||
|     /** | ||||
|      * 字典枚举类型 | ||||
|      */ | ||||
|     Class<? extends Enum<?>> enumClass(); | ||||
|  | ||||
|     /** | ||||
|      * 字典枚举类中对应的code属性名称,默认为code | ||||
|      */ | ||||
|     String codeField() default "code"; | ||||
|  | ||||
|     /** | ||||
|      * 字典枚举类中对应的text属性名称,默认为text | ||||
|      */ | ||||
|     String textField() default "text"; | ||||
|  | ||||
| } | ||||
| @@ -38,4 +38,10 @@ public @interface Log { | ||||
|      * 是否保存响应的参数 | ||||
|      */ | ||||
|     boolean isSaveResponseData() default true; | ||||
|  | ||||
|     /** | ||||
|      * 排除指定的请求参数 | ||||
|      */ | ||||
|     String[] excludeParamNames() default {}; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package com.ruoyi.common.annotation; | ||||
|  | ||||
| import com.ruoyi.common.constant.CacheConstants; | ||||
| import com.ruoyi.common.enums.LimitType; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
| @@ -15,9 +14,10 @@ import java.lang.annotation.*; | ||||
| @Documented | ||||
| public @interface RateLimiter { | ||||
|     /** | ||||
|      * 限流key | ||||
|      * 限流key,支持使用Spring el表达式来动态获取方法上的参数值 | ||||
|      * 格式类似于  #code.id #{#code} | ||||
|      */ | ||||
|     String key() default CacheConstants.RATE_LIMIT_KEY; | ||||
|     String key() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 限流时间,单位秒 | ||||
| @@ -33,4 +33,9 @@ public @interface RateLimiter { | ||||
|      * 限流类型 | ||||
|      */ | ||||
|     LimitType limitType() default LimitType.DEFAULT; | ||||
|  | ||||
|     /** | ||||
|      * 提示消息 支持国际化 格式为 {code} | ||||
|      */ | ||||
|     String message() default "{rate.limiter.message}"; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,39 @@ | ||||
| package com.ruoyi.common.annotation; | ||||
|  | ||||
| import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; | ||||
| import com.fasterxml.jackson.databind.annotation.JsonSerialize; | ||||
| import com.ruoyi.common.translation.handler.TranslationHandler; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * 通用翻译注解 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Inherited | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Target({ElementType.FIELD, ElementType.METHOD}) | ||||
| @Documented | ||||
| @JacksonAnnotationsInside | ||||
| @JsonSerialize(using = TranslationHandler.class) | ||||
| public @interface Translation { | ||||
|  | ||||
|     /** | ||||
|      * 类型 (需与实现类上的 {@link com.ruoyi.common.annotation.TranslationType} 注解type对应) | ||||
|      * <p> | ||||
|      * 默认取当前字段的值 如果设置了 @{@link Translation#mapper()} 则取映射字段的值 | ||||
|      */ | ||||
|     String type(); | ||||
|  | ||||
|     /** | ||||
|      * 映射字段 (如果不为空则取此字段的值) | ||||
|      */ | ||||
|     String mapper() default ""; | ||||
|  | ||||
|     /** | ||||
|      * 其他条件 例如: 字典type(sys_user_sex) | ||||
|      */ | ||||
|     String other() default ""; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| package com.ruoyi.common.annotation; | ||||
|  | ||||
| import java.lang.annotation.*; | ||||
|  | ||||
| /** | ||||
|  * 翻译类型注解 (标注到{@link com.ruoyi.common.translation.TranslationInterface} 的实现类) | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Inherited | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Target({ElementType.TYPE}) | ||||
| @Documented | ||||
| public @interface TranslationType { | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     String type(); | ||||
|  | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| package com.ruoyi.common.config; | ||||
|  | ||||
| import lombok.Data; | ||||
| import lombok.Getter; | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| @@ -31,24 +30,9 @@ public class RuoYiConfig { | ||||
|      */ | ||||
|     private String copyrightYear; | ||||
|  | ||||
|     /** | ||||
|      * 实例演示开关 | ||||
|      */ | ||||
|     private boolean demoEnabled; | ||||
|  | ||||
|     /** | ||||
|      * 缓存懒加载 | ||||
|      */ | ||||
|     private boolean cacheLazy; | ||||
|  | ||||
|     /** | ||||
|      * 获取地址开关 | ||||
|      */ | ||||
|     @Getter | ||||
|     private static boolean addressEnabled; | ||||
|  | ||||
|     public void setAddressEnabled(boolean addressEnabled) { | ||||
|         RuoYiConfig.addressEnabled = addressEnabled; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -7,11 +7,6 @@ package com.ruoyi.common.constant; | ||||
|  */ | ||||
| public interface CacheConstants { | ||||
|  | ||||
|     /** | ||||
|      * 登录用户 redis key | ||||
|      */ | ||||
|     String LOGIN_TOKEN_KEY = "Authorization:login:token:"; | ||||
|  | ||||
|     /** | ||||
|      * 在线用户 redis key | ||||
|      */ | ||||
|   | ||||
| @@ -30,6 +30,16 @@ public interface CacheNames { | ||||
|      */ | ||||
|     String SYS_DICT = "sys_dict"; | ||||
|  | ||||
|     /** | ||||
|      * 用户账户 | ||||
|      */ | ||||
|     String SYS_USER_NAME = "sys_user_name#30d"; | ||||
|  | ||||
|     /** | ||||
|      * 部门 | ||||
|      */ | ||||
|     String SYS_DEPT = "sys_dept#30d"; | ||||
|  | ||||
|     /** | ||||
|      * OSS内容 | ||||
|      */ | ||||
|   | ||||
| @@ -0,0 +1,30 @@ | ||||
| package com.ruoyi.common.constant; | ||||
|  | ||||
| /** | ||||
|  * 翻译常量 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public interface TransConstant { | ||||
|  | ||||
|     /** | ||||
|      * 用户id转账号 | ||||
|      */ | ||||
|     String USER_ID_TO_NAME = "user_id_to_name"; | ||||
|  | ||||
|     /** | ||||
|      * 部门id转名称 | ||||
|      */ | ||||
|     String DEPT_ID_TO_NAME = "dept_id_to_name"; | ||||
|  | ||||
|     /** | ||||
|      * 字典type转label | ||||
|      */ | ||||
|     String DICT_TYPE_TO_LABEL = "dict_type_to_label"; | ||||
|  | ||||
|     /** | ||||
|      * ossId转url | ||||
|      */ | ||||
|     String OSS_ID_TO_URL = "oss_id_to_url"; | ||||
|  | ||||
| } | ||||
| @@ -52,6 +52,16 @@ public interface UserConstants { | ||||
|      */ | ||||
|     String DEPT_DISABLE = "1"; | ||||
|  | ||||
|     /** | ||||
|      * 岗位正常状态 | ||||
|      */ | ||||
|     String POST_NORMAL = "0"; | ||||
|  | ||||
|     /** | ||||
|      * 岗位停用状态 | ||||
|      */ | ||||
|     String POST_DISABLE = "1"; | ||||
|  | ||||
|     /** | ||||
|      * 字典正常状态 | ||||
|      */ | ||||
| @@ -112,12 +122,6 @@ public interface UserConstants { | ||||
|      */ | ||||
|     String INNER_LINK = "InnerLink"; | ||||
|  | ||||
|     /** | ||||
|      * 校验返回结果码 | ||||
|      */ | ||||
|     String UNIQUE = "0"; | ||||
|     String NOT_UNIQUE = "1"; | ||||
|  | ||||
|     /** | ||||
|      * 用户名长度限制 | ||||
|      */ | ||||
| @@ -135,4 +139,9 @@ public interface UserConstants { | ||||
|      */ | ||||
|     Long ADMIN_ID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 管理员角色key | ||||
|      */ | ||||
|     String ADMIN_ROLE_KEY = "admin"; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| package com.ruoyi.common.convert; | ||||
|  | ||||
| import cn.hutool.core.annotation.AnnotationUtil; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.alibaba.excel.converters.Converter; | ||||
| import com.alibaba.excel.enums.CellDataTypeEnum; | ||||
| import com.alibaba.excel.metadata.GlobalConfiguration; | ||||
| import com.alibaba.excel.metadata.data.ReadCellData; | ||||
| import com.alibaba.excel.metadata.data.WriteCellData; | ||||
| import com.alibaba.excel.metadata.property.ExcelContentProperty; | ||||
| import com.ruoyi.common.annotation.ExcelEnumFormat; | ||||
| import com.ruoyi.common.utils.reflect.ReflectUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 枚举格式化转换处理 | ||||
|  * | ||||
|  * @author Liang | ||||
|  */ | ||||
| @Slf4j | ||||
| public class ExcelEnumConvert implements Converter<Object> { | ||||
|  | ||||
|     @Override | ||||
|     public Class<Object> supportJavaTypeKey() { | ||||
|         return Object.class; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public CellDataTypeEnum supportExcelTypeKey() { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { | ||||
|         cellData.checkEmpty(); | ||||
|         // Excel中填入的是枚举中指定的描述 | ||||
|         Object textValue = null; | ||||
|         switch (cellData.getType()) { | ||||
|             case STRING: | ||||
|             case DIRECT_STRING: | ||||
|             case RICH_TEXT_STRING: | ||||
|                 textValue = cellData.getStringValue(); | ||||
|                 break; | ||||
|             case NUMBER: | ||||
|                 textValue = cellData.getNumberValue(); | ||||
|                 break; | ||||
|             case BOOLEAN: | ||||
|                 textValue = cellData.getBooleanValue(); | ||||
|                 break; | ||||
|             default: | ||||
|                 throw new IllegalArgumentException("单元格类型异常!"); | ||||
|         } | ||||
|         // 如果是空值 | ||||
|         if (ObjectUtil.isNull(textValue)) { | ||||
|             return null; | ||||
|         } | ||||
|         Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty); | ||||
|         // 从Java输出至Excel是code转text | ||||
|         // 因此从Excel转Java应该将text与code对调 | ||||
|         Map<Object, Object> enumTextToCodeMap = new HashMap<>(); | ||||
|         enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key)); | ||||
|         // 应该从text -> code中查找 | ||||
|         Object codeValue = enumTextToCodeMap.get(textValue); | ||||
|         return Convert.convert(contentProperty.getField().getType(), codeValue); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { | ||||
|         if (ObjectUtil.isNull(object)) { | ||||
|             return new WriteCellData<>(""); | ||||
|         } | ||||
|         Map<Object, String> enumValueMap = beforeConvert(contentProperty); | ||||
|         String value = Convert.toStr(enumValueMap.get(object), ""); | ||||
|         return new WriteCellData<>(value); | ||||
|     } | ||||
|  | ||||
|     private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) { | ||||
|         ExcelEnumFormat anno = getAnnotation(contentProperty.getField()); | ||||
|         Map<Object, String> enumValueMap = new HashMap<>(); | ||||
|         Enum<?>[] enumConstants = anno.enumClass().getEnumConstants(); | ||||
|         for (Enum<?> enumConstant : enumConstants) { | ||||
|             Object codeValue = ReflectUtils.invokeGetter(enumConstant, anno.codeField()); | ||||
|             String textValue = ReflectUtils.invokeGetter(enumConstant, anno.textField()); | ||||
|             enumValueMap.put(codeValue, textValue); | ||||
|         } | ||||
|         return enumValueMap; | ||||
|     } | ||||
|  | ||||
|     private ExcelEnumFormat getAnnotation(Field field) { | ||||
|         return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class); | ||||
|     } | ||||
| } | ||||
| @@ -87,8 +87,8 @@ public class PageQuery implements Serializable { | ||||
|         // 兼容前端排序类型 | ||||
|         isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"}); | ||||
|  | ||||
|         String[] orderByArr = orderBy.split(","); | ||||
|         String[] isAscArr = isAsc.split(","); | ||||
|         String[] orderByArr = orderBy.split(StringUtils.SEPARATOR); | ||||
|         String[] isAscArr = isAsc.split(StringUtils.SEPARATOR); | ||||
|         if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) { | ||||
|             throw new ServiceException("排序参数有误"); | ||||
|         } | ||||
|   | ||||
| @@ -34,7 +34,7 @@ public class SysDept extends TreeEntity<SysDept> { | ||||
|      * 部门名称 | ||||
|      */ | ||||
|     @NotBlank(message = "部门名称不能为空") | ||||
|     @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符") | ||||
|     @Size(min = 0, max = 30, message = "部门名称长度不能超过{max}个字符") | ||||
|     private String deptName; | ||||
|  | ||||
|     /** | ||||
| @@ -51,14 +51,14 @@ public class SysDept extends TreeEntity<SysDept> { | ||||
|     /** | ||||
|      * 联系电话 | ||||
|      */ | ||||
|     @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符") | ||||
|     @Size(min = 0, max = 11, message = "联系电话长度不能超过{max}个字符") | ||||
|     private String phone; | ||||
|  | ||||
|     /** | ||||
|      * 邮箱 | ||||
|      */ | ||||
|     @Email(message = "邮箱格式不正确") | ||||
|     @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") | ||||
|     @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符") | ||||
|     private String email; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -44,7 +44,7 @@ public class SysDictData extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "字典标签") | ||||
|     @NotBlank(message = "字典标签不能为空") | ||||
|     @Size(min = 0, max = 100, message = "字典标签长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "字典标签长度不能超过{max}个字符") | ||||
|     private String dictLabel; | ||||
|  | ||||
|     /** | ||||
| @@ -52,7 +52,7 @@ public class SysDictData extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "字典键值") | ||||
|     @NotBlank(message = "字典键值不能为空") | ||||
|     @Size(min = 0, max = 100, message = "字典键值长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "字典键值长度不能超过{max}个字符") | ||||
|     private String dictValue; | ||||
|  | ||||
|     /** | ||||
| @@ -60,13 +60,13 @@ public class SysDictData extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "字典类型") | ||||
|     @NotBlank(message = "字典类型不能为空") | ||||
|     @Size(min = 0, max = 100, message = "字典类型长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "字典类型长度不能超过{max}个字符") | ||||
|     private String dictType; | ||||
|  | ||||
|     /** | ||||
|      * 样式属性(其他样式扩展) | ||||
|      */ | ||||
|     @Size(min = 0, max = 100, message = "样式属性长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "样式属性长度不能超过{max}个字符") | ||||
|     private String cssClass; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -38,7 +38,7 @@ public class SysDictType extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "字典名称") | ||||
|     @NotBlank(message = "字典名称不能为空") | ||||
|     @Size(min = 0, max = 100, message = "字典类型名称长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "字典类型名称长度不能超过{max}个字符") | ||||
|     private String dictName; | ||||
|  | ||||
|     /** | ||||
| @@ -46,7 +46,7 @@ public class SysDictType extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "字典类型") | ||||
|     @NotBlank(message = "字典类型不能为空") | ||||
|     @Size(min = 0, max = 100, message = "字典类型类型长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "字典类型类型长度不能超过{max}个字符") | ||||
|     @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)") | ||||
|     private String dictType; | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ public class SysMenu extends TreeEntity<SysMenu> { | ||||
|      * 菜单名称 | ||||
|      */ | ||||
|     @NotBlank(message = "菜单名称不能为空") | ||||
|     @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符") | ||||
|     @Size(min = 0, max = 50, message = "菜单名称长度不能超过{max}个字符") | ||||
|     private String menuName; | ||||
|  | ||||
|     /** | ||||
| @@ -44,13 +44,13 @@ public class SysMenu extends TreeEntity<SysMenu> { | ||||
|     /** | ||||
|      * 路由地址 | ||||
|      */ | ||||
|     @Size(min = 0, max = 200, message = "路由地址不能超过200个字符") | ||||
|     @Size(min = 0, max = 200, message = "路由地址不能超过{max}个字符") | ||||
|     private String path; | ||||
|  | ||||
|     /** | ||||
|      * 组件路径 | ||||
|      */ | ||||
|     @Size(min = 0, max = 200, message = "组件路径不能超过255个字符") | ||||
|     @Size(min = 0, max = 200, message = "组件路径不能超过{max}个字符") | ||||
|     private String component; | ||||
|  | ||||
|     /** | ||||
| @@ -88,7 +88,7 @@ public class SysMenu extends TreeEntity<SysMenu> { | ||||
|      * 权限字符串 | ||||
|      */ | ||||
|     @JsonInclude(JsonInclude.Include.NON_NULL) | ||||
|     @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "权限标识长度不能超过{max}个字符") | ||||
|     private String perms; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -17,7 +17,6 @@ import lombok.NoArgsConstructor; | ||||
| import javax.validation.constraints.NotBlank; | ||||
| import javax.validation.constraints.NotNull; | ||||
| import javax.validation.constraints.Size; | ||||
| import java.util.Set; | ||||
|  | ||||
| /** | ||||
|  * 角色表 sys_role | ||||
| @@ -44,7 +43,7 @@ public class SysRole extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "角色名称") | ||||
|     @NotBlank(message = "角色名称不能为空") | ||||
|     @Size(min = 0, max = 30, message = "角色名称长度不能超过30个字符") | ||||
|     @Size(min = 0, max = 30, message = "角色名称长度不能超过{max}个字符") | ||||
|     private String roleName; | ||||
|  | ||||
|     /** | ||||
| @@ -52,7 +51,7 @@ public class SysRole extends BaseEntity { | ||||
|      */ | ||||
|     @ExcelProperty(value = "角色权限") | ||||
|     @NotBlank(message = "权限字符不能为空") | ||||
|     @Size(min = 0, max = 100, message = "权限字符长度不能超过100个字符") | ||||
|     @Size(min = 0, max = 100, message = "权限字符长度不能超过{max}个字符") | ||||
|     private String roleKey; | ||||
|  | ||||
|     /** | ||||
| @@ -115,12 +114,6 @@ public class SysRole extends BaseEntity { | ||||
|     @TableField(exist = false) | ||||
|     private Long[] deptIds; | ||||
|  | ||||
|     /** | ||||
|      * 角色菜单权限 | ||||
|      */ | ||||
|     @TableField(exist = false) | ||||
|     private Set<String> permissions; | ||||
|  | ||||
|     public SysRole(Long roleId) { | ||||
|         this.roleId = roleId; | ||||
|     } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| package com.ruoyi.common.core.domain.entity; | ||||
|  | ||||
| import com.baomidou.mybatisplus.annotation.*; | ||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||||
| import com.ruoyi.common.annotation.Sensitive; | ||||
| import com.ruoyi.common.constant.UserConstants; | ||||
| import com.ruoyi.common.core.domain.BaseEntity; | ||||
| @@ -44,14 +46,15 @@ public class SysUser extends BaseEntity { | ||||
|      */ | ||||
|     @Xss(message = "用户账号不能包含脚本字符") | ||||
|     @NotBlank(message = "用户账号不能为空") | ||||
|     @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符") | ||||
|     @Size(min = 0, max = 30, message = "用户账号长度不能超过{max}个字符") | ||||
|     private String userName; | ||||
|  | ||||
|     /** | ||||
|      * 用户昵称 | ||||
|      */ | ||||
|     @Xss(message = "用户昵称不能包含脚本字符") | ||||
|     @Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符") | ||||
|     @NotBlank(message = "用户昵称不能为空") | ||||
|     @Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符") | ||||
|     private String nickName; | ||||
|  | ||||
|     /** | ||||
| @@ -64,7 +67,7 @@ public class SysUser extends BaseEntity { | ||||
|      */ | ||||
|     @Sensitive(strategy = SensitiveStrategy.EMAIL) | ||||
|     @Email(message = "邮箱格式不正确") | ||||
|     @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符") | ||||
|     @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符") | ||||
|     private String email; | ||||
|  | ||||
|     /** | ||||
| @@ -93,6 +96,12 @@ public class SysUser extends BaseEntity { | ||||
|     ) | ||||
|     private String password; | ||||
|  | ||||
|     @JsonIgnore | ||||
|     @JsonProperty | ||||
|     public String getPassword() { | ||||
|         return password; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 帐号状态(0正常 1停用) | ||||
|      */ | ||||
|   | ||||
| @@ -0,0 +1,30 @@ | ||||
| package com.ruoyi.common.core.domain.model; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import javax.validation.constraints.Email; | ||||
| import javax.validation.constraints.NotBlank; | ||||
|  | ||||
| /** | ||||
|  * 邮箱登录对象 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
|  | ||||
| @Data | ||||
| public class EmailLoginBody { | ||||
|  | ||||
|     /** | ||||
|      * 邮箱 | ||||
|      */ | ||||
|     @NotBlank(message = "{user.email.not.blank}") | ||||
|     @Email(message = "{user.email.not.valid}") | ||||
|     private String email; | ||||
|  | ||||
|     /** | ||||
|      * 邮箱code | ||||
|      */ | ||||
|     @NotBlank(message = "{email.code.not.blank}") | ||||
|     private String emailCode; | ||||
|  | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| package com.ruoyi.common.core.domain.model; | ||||
|  | ||||
| import com.ruoyi.common.core.domain.dto.RoleDTO; | ||||
| import com.ruoyi.common.helper.LoginHelper; | ||||
| import lombok.Data; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| @@ -111,7 +110,7 @@ public class LoginUser implements Serializable { | ||||
|         if (userId == null) { | ||||
|             throw new IllegalArgumentException("用户ID不能为空"); | ||||
|         } | ||||
|         return userType + LoginHelper.JOIN_CODE + userId; | ||||
|         return userType + ":" + userId; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -14,13 +14,13 @@ import javax.validation.constraints.NotBlank; | ||||
| public class SmsLoginBody { | ||||
|  | ||||
|     /** | ||||
|      * 用户名 | ||||
|      * 手机号 | ||||
|      */ | ||||
|     @NotBlank(message = "{user.phonenumber.not.blank}") | ||||
|     private String phonenumber; | ||||
|  | ||||
|     /** | ||||
|      * 用户密码 | ||||
|      * 短信code | ||||
|      */ | ||||
|     @NotBlank(message = "{sms.code.not.blank}") | ||||
|     private String smsCode; | ||||
|   | ||||
| @@ -180,12 +180,12 @@ public interface BaseMapperPlus<M, T, V> extends BaseMapper<T> { | ||||
|      * 分页查询VO | ||||
|      */ | ||||
|     default <C, P extends IPage<C>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper, Class<C> voClass) { | ||||
|         IPage<T> pageData = this.selectPage(page, wrapper); | ||||
|         IPage<C> voPage = new Page<>(pageData.getCurrent(), pageData.getSize(), pageData.getTotal()); | ||||
|         if (CollUtil.isEmpty(pageData.getRecords())) { | ||||
|         List<T> list = this.selectList(page, wrapper); | ||||
|         IPage<C> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); | ||||
|         if (CollUtil.isEmpty(list)) { | ||||
|             return (P) voPage; | ||||
|         } | ||||
|         voPage.setRecords(BeanCopyUtils.copyList(pageData.getRecords(), voClass)); | ||||
|         voPage.setRecords(BeanCopyUtils.copyList(list, voClass)); | ||||
|         return (P) voPage; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| package com.ruoyi.common.core.service; | ||||
|  | ||||
| /** | ||||
|  * 通用 部门服务 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public interface DeptService { | ||||
|  | ||||
|     /** | ||||
|      * 通过部门ID查询部门名称 | ||||
|      * | ||||
|      * @param deptIds 部门ID串逗号分隔 | ||||
|      * @return 部门名称串逗号分隔 | ||||
|      */ | ||||
|     String selectDeptNameByIds(String deptIds); | ||||
|  | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| package com.ruoyi.common.core.service; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 通用 字典服务 | ||||
|  * | ||||
| @@ -54,4 +56,11 @@ public interface DictService { | ||||
|      */ | ||||
|     String getDictValue(String dictType, String dictLabel, String separator); | ||||
|  | ||||
|     /** | ||||
|      * 获取字典下所有的字典值与标签 | ||||
|      * | ||||
|      * @param dictType 字典类型 | ||||
|      * @return dictValue为key,dictLabel为值组成的Map | ||||
|      */ | ||||
|     Map<String, String> getAllDictByDictType(String dictType); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,18 @@ | ||||
| package com.ruoyi.common.core.service; | ||||
|  | ||||
| /** | ||||
|  * 通用 OSS服务 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public interface OssService { | ||||
|  | ||||
|     /** | ||||
|      * 通过ossId查询对应的url | ||||
|      * | ||||
|      * @param ossIds ossId串逗号分隔 | ||||
|      * @return url串逗号分隔 | ||||
|      */ | ||||
|     String selectUrlByIds(String ossIds); | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| package com.ruoyi.common.core.service; | ||||
|  | ||||
| /** | ||||
|  * 通用 用户服务 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public interface UserService { | ||||
|  | ||||
|     /** | ||||
|      * 通过用户ID查询用户账户 | ||||
|      * | ||||
|      * @param userId 用户ID | ||||
|      * @return 用户账户 | ||||
|      */ | ||||
|     String selectUserNameById(Long userId); | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| package com.ruoyi.common.encrypt; | ||||
|  | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
| import lombok.Data; | ||||
|  | ||||
| /** | ||||
|  * 加密上下文 用于encryptor传递必要的参数。 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| @Data | ||||
| public class EncryptContext { | ||||
|  | ||||
|     /** | ||||
|      * 默认算法 | ||||
|      */ | ||||
|     private AlgorithmType algorithm; | ||||
|  | ||||
|     /** | ||||
|      * 安全秘钥 | ||||
|      */ | ||||
|     private String password; | ||||
|  | ||||
|     /** | ||||
|      * 公钥 | ||||
|      */ | ||||
|     private String publicKey; | ||||
|  | ||||
|     /** | ||||
|      * 私钥 | ||||
|      */ | ||||
|     private String privateKey; | ||||
|  | ||||
|     /** | ||||
|      * 编码方式,base64/hex | ||||
|      */ | ||||
|     private EncodeType encode; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| package com.ruoyi.common.encrypt; | ||||
|  | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
|  | ||||
| /** | ||||
|  * 加解者 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public interface IEncryptor { | ||||
|  | ||||
|     /** | ||||
|      * 获得当前算法 | ||||
|      */ | ||||
|     AlgorithmType algorithm(); | ||||
|  | ||||
|     /** | ||||
|      * 加密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @param encodeType 加密后的编码格式 | ||||
|      * @return 加密后的字符串 | ||||
|      */ | ||||
|     String encrypt(String value, EncodeType encodeType); | ||||
|  | ||||
|     /** | ||||
|      * 解密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @return 解密后的字符串 | ||||
|      */ | ||||
|     String decrypt(String value); | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| package com.ruoyi.common.encrypt.encryptor; | ||||
|  | ||||
| import com.ruoyi.common.encrypt.EncryptContext; | ||||
| import com.ruoyi.common.encrypt.IEncryptor; | ||||
|  | ||||
| /** | ||||
|  * 所有加密执行者的基类 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public abstract class AbstractEncryptor implements IEncryptor { | ||||
|  | ||||
|     public AbstractEncryptor(EncryptContext context) { | ||||
|         // 用户配置校验与配置注入 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,55 @@ | ||||
| package com.ruoyi.common.encrypt.encryptor; | ||||
|  | ||||
| import com.ruoyi.common.encrypt.EncryptContext; | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
| import com.ruoyi.common.utils.EncryptUtils; | ||||
|  | ||||
| /** | ||||
|  * AES算法实现 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public class AesEncryptor extends AbstractEncryptor { | ||||
|  | ||||
|     private final EncryptContext context; | ||||
|  | ||||
|     public AesEncryptor(EncryptContext context) { | ||||
|         super(context); | ||||
|         this.context = context; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得当前算法 | ||||
|      */ | ||||
|     @Override | ||||
|     public AlgorithmType algorithm() { | ||||
|         return AlgorithmType.AES; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @param encodeType 加密后的编码格式 | ||||
|      */ | ||||
|     @Override | ||||
|     public String encrypt(String value, EncodeType encodeType) { | ||||
|         if (encodeType == EncodeType.HEX) { | ||||
|             return EncryptUtils.encryptByAesHex(value, context.getPassword()); | ||||
|         } else { | ||||
|             return EncryptUtils.encryptByAes(value, context.getPassword()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      */ | ||||
|     @Override | ||||
|     public String decrypt(String value) { | ||||
|         return EncryptUtils.decryptByAes(value, context.getPassword()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package com.ruoyi.common.encrypt.encryptor; | ||||
|  | ||||
| import com.ruoyi.common.encrypt.EncryptContext; | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
| import com.ruoyi.common.utils.EncryptUtils; | ||||
|  | ||||
| /** | ||||
|  * Base64算法实现 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public class Base64Encryptor extends AbstractEncryptor { | ||||
|  | ||||
|     public Base64Encryptor(EncryptContext context) { | ||||
|         super(context); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得当前算法 | ||||
|      */ | ||||
|     @Override | ||||
|     public AlgorithmType algorithm() { | ||||
|         return AlgorithmType.BASE64; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @param encodeType 加密后的编码格式 | ||||
|      */ | ||||
|     @Override | ||||
|     public String encrypt(String value, EncodeType encodeType) { | ||||
|         return EncryptUtils.encryptByBase64(value); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      */ | ||||
|     @Override | ||||
|     public String decrypt(String value) { | ||||
|         return EncryptUtils.decryptByBase64(value); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| package com.ruoyi.common.encrypt.encryptor; | ||||
|  | ||||
| import com.ruoyi.common.encrypt.EncryptContext; | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
| import com.ruoyi.common.utils.EncryptUtils; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * RSA算法实现 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public class RsaEncryptor extends AbstractEncryptor { | ||||
|  | ||||
|     private final EncryptContext context; | ||||
|  | ||||
|     public RsaEncryptor(EncryptContext context) { | ||||
|         super(context); | ||||
|         String privateKey = context.getPrivateKey(); | ||||
|         String publicKey = context.getPublicKey(); | ||||
|         if (StringUtils.isAnyEmpty(privateKey, publicKey)) { | ||||
|             throw new IllegalArgumentException("RSA公私钥均需要提供,公钥加密,私钥解密。"); | ||||
|         } | ||||
|         this.context = context; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得当前算法 | ||||
|      */ | ||||
|     @Override | ||||
|     public AlgorithmType algorithm() { | ||||
|         return AlgorithmType.RSA; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @param encodeType 加密后的编码格式 | ||||
|      */ | ||||
|     @Override | ||||
|     public String encrypt(String value, EncodeType encodeType) { | ||||
|         if (encodeType == EncodeType.HEX) { | ||||
|             return EncryptUtils.encryptByRsaHex(value, context.getPublicKey()); | ||||
|         } else { | ||||
|             return EncryptUtils.encryptByRsa(value, context.getPublicKey()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      */ | ||||
|     @Override | ||||
|     public String decrypt(String value) { | ||||
|         return EncryptUtils.decryptByRsa(value, context.getPrivateKey()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,62 @@ | ||||
| package com.ruoyi.common.encrypt.encryptor; | ||||
|  | ||||
|  | ||||
| import com.ruoyi.common.encrypt.EncryptContext; | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
| import com.ruoyi.common.utils.EncryptUtils; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
|  | ||||
| /** | ||||
|  * sm2算法实现 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public class Sm2Encryptor extends AbstractEncryptor { | ||||
|  | ||||
|     private final EncryptContext context; | ||||
|  | ||||
|     public Sm2Encryptor(EncryptContext context) { | ||||
|         super(context); | ||||
|         String privateKey = context.getPrivateKey(); | ||||
|         String publicKey = context.getPublicKey(); | ||||
|         if (StringUtils.isAnyEmpty(privateKey, publicKey)) { | ||||
|             throw new IllegalArgumentException("SM2公私钥均需要提供,公钥加密,私钥解密。"); | ||||
|         } | ||||
|         this.context = context; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得当前算法 | ||||
|      */ | ||||
|     @Override | ||||
|     public AlgorithmType algorithm() { | ||||
|         return AlgorithmType.SM2; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @param encodeType 加密后的编码格式 | ||||
|      */ | ||||
|     @Override | ||||
|     public String encrypt(String value, EncodeType encodeType) { | ||||
|         if (encodeType == EncodeType.HEX) { | ||||
|             return EncryptUtils.encryptBySm2Hex(value, context.getPublicKey()); | ||||
|         } else { | ||||
|             return EncryptUtils.encryptBySm2(value, context.getPublicKey()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      */ | ||||
|     @Override | ||||
|     public String decrypt(String value) { | ||||
|         return EncryptUtils.decryptBySm2(value, context.getPrivateKey()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,55 @@ | ||||
| package com.ruoyi.common.encrypt.encryptor; | ||||
|  | ||||
| import com.ruoyi.common.encrypt.EncryptContext; | ||||
| import com.ruoyi.common.enums.AlgorithmType; | ||||
| import com.ruoyi.common.enums.EncodeType; | ||||
| import com.ruoyi.common.utils.EncryptUtils; | ||||
|  | ||||
| /** | ||||
|  * sm4算法实现 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public class Sm4Encryptor extends AbstractEncryptor { | ||||
|  | ||||
|     private final EncryptContext context; | ||||
|  | ||||
|     public Sm4Encryptor(EncryptContext context) { | ||||
|         super(context); | ||||
|         this.context = context; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获得当前算法 | ||||
|      */ | ||||
|     @Override | ||||
|     public AlgorithmType algorithm() { | ||||
|         return AlgorithmType.SM4; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 加密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      * @param encodeType 加密后的编码格式 | ||||
|      */ | ||||
|     @Override | ||||
|     public String encrypt(String value, EncodeType encodeType) { | ||||
|         if (encodeType == EncodeType.HEX) { | ||||
|             return EncryptUtils.encryptBySm4Hex(value, context.getPassword()); | ||||
|         } else { | ||||
|             return EncryptUtils.encryptBySm4(value, context.getPassword()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密 | ||||
|      * | ||||
|      * @param value      待加密字符串 | ||||
|      */ | ||||
|     @Override | ||||
|     public String decrypt(String value) { | ||||
|         return EncryptUtils.decryptBySm4(value, context.getPassword()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,48 @@ | ||||
| package com.ruoyi.common.enums; | ||||
|  | ||||
| import com.ruoyi.common.encrypt.encryptor.*; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
|  | ||||
| /** | ||||
|  * 算法名称 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| @Getter | ||||
| @AllArgsConstructor | ||||
| public enum AlgorithmType { | ||||
|  | ||||
|     /** | ||||
|      * 默认走yml配置 | ||||
|      */ | ||||
|     DEFAULT(null), | ||||
|  | ||||
|     /** | ||||
|      * base64 | ||||
|      */ | ||||
|     BASE64(Base64Encryptor.class), | ||||
|  | ||||
|     /** | ||||
|      * aes | ||||
|      */ | ||||
|     AES(AesEncryptor.class), | ||||
|  | ||||
|     /** | ||||
|      * rsa | ||||
|      */ | ||||
|     RSA(RsaEncryptor.class), | ||||
|  | ||||
|     /** | ||||
|      * sm2 | ||||
|      */ | ||||
|     SM2(Sm2Encryptor.class), | ||||
|  | ||||
|     /** | ||||
|      * sm4 | ||||
|      */ | ||||
|     SM4(Sm4Encryptor.class); | ||||
|  | ||||
|     private final Class<? extends AbstractEncryptor> clazz; | ||||
| } | ||||
| @@ -29,17 +29,17 @@ public enum DataScopeType { | ||||
|     /** | ||||
|      * 自定数据权限 | ||||
|      */ | ||||
|     CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""), | ||||
|     CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "), | ||||
|  | ||||
|     /** | ||||
|      * 部门数据权限 | ||||
|      */ | ||||
|     DEPT("3", " #{#deptName} = #{#user.deptId} ", ""), | ||||
|     DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "), | ||||
|  | ||||
|     /** | ||||
|      * 部门及以下数据权限 | ||||
|      */ | ||||
|     DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""), | ||||
|     DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "), | ||||
|  | ||||
|     /** | ||||
|      * 仅本人数据权限 | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| package com.ruoyi.common.enums; | ||||
|  | ||||
| /** | ||||
|  * 编码类型 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  * @version 4.6.0 | ||||
|  */ | ||||
| public enum EncodeType { | ||||
|  | ||||
|     /** | ||||
|      * 默认使用yml配置 | ||||
|      */ | ||||
|     DEFAULT, | ||||
|  | ||||
|     /** | ||||
|      * base64编码 | ||||
|      */ | ||||
|     BASE64, | ||||
|  | ||||
|     /** | ||||
|      * 16进制编码 | ||||
|      */ | ||||
|     HEX; | ||||
|  | ||||
| } | ||||
| @@ -22,6 +22,11 @@ public enum LoginType { | ||||
|      */ | ||||
|     SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"), | ||||
|  | ||||
|     /** | ||||
|      * 邮箱登录 | ||||
|      */ | ||||
|     EMAIL("email.code.retry.limit.exceed", "email.code.retry.limit.count"), | ||||
|  | ||||
|     /** | ||||
|      * 小程序登录 | ||||
|      */ | ||||
|   | ||||
| @@ -1,19 +1,20 @@ | ||||
| package com.ruoyi.common.excel; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import com.alibaba.excel.annotation.ExcelProperty; | ||||
| import com.alibaba.excel.metadata.Head; | ||||
| import com.alibaba.excel.write.merge.AbstractMergeStrategy; | ||||
| import com.ruoyi.common.annotation.CellMerge; | ||||
| import com.ruoyi.common.utils.reflect.ReflectUtils; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
| import lombok.SneakyThrows; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.collections4.CollectionUtils; | ||||
| import org.apache.poi.ss.usermodel.Cell; | ||||
| import org.apache.poi.ss.usermodel.Sheet; | ||||
| import org.apache.poi.ss.util.CellRangeAddress; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| @@ -24,20 +25,26 @@ import java.util.Map; | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @AllArgsConstructor | ||||
| @Slf4j | ||||
| public class CellMergeStrategy extends AbstractMergeStrategy { | ||||
|  | ||||
| 	private List<?> list; | ||||
| 	private boolean hasTitle; | ||||
|     private final List<CellRangeAddress> cellList; | ||||
|     private final boolean hasTitle; | ||||
|     private int rowIndex; | ||||
|  | ||||
|     public CellMergeStrategy(List<?> list, boolean hasTitle) { | ||||
|         this.hasTitle = hasTitle; | ||||
|         // 行合并开始下标 | ||||
|         this.rowIndex = hasTitle ? 1 : 0; | ||||
|         this.cellList = handle(list, hasTitle); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { | ||||
| 		List<CellRangeAddress> cellList = handle(list, hasTitle); | ||||
|         // judge the list is not null | ||||
| 		if (CollectionUtils.isNotEmpty(cellList)) { | ||||
|         if (CollUtil.isNotEmpty(cellList)) { | ||||
|             // the judge is necessary | ||||
| 			if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) { | ||||
|             if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) { | ||||
|                 for (CellRangeAddress item : cellList) { | ||||
|                     sheet.addMergedRegion(item); | ||||
|                 } | ||||
| @@ -46,13 +53,13 @@ public class CellMergeStrategy extends AbstractMergeStrategy { | ||||
|     } | ||||
|  | ||||
|     @SneakyThrows | ||||
| 	private static List<CellRangeAddress> handle(List<?> list, boolean hasTitle) { | ||||
|     private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) { | ||||
|         List<CellRangeAddress> cellList = new ArrayList<>(); | ||||
| 		if (CollectionUtils.isEmpty(list)) { | ||||
|         if (CollUtil.isEmpty(list)) { | ||||
|             return cellList; | ||||
|         } | ||||
| 		Class<?> clazz = list.get(0).getClass(); | ||||
| 		Field[] fields = clazz.getDeclaredFields(); | ||||
|         Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName())); | ||||
|  | ||||
|         // 有注解的字段 | ||||
|         List<Field> mergeFields = new ArrayList<>(); | ||||
|         List<Integer> mergeFieldsIndex = new ArrayList<>(); | ||||
| @@ -62,19 +69,19 @@ public class CellMergeStrategy extends AbstractMergeStrategy { | ||||
|                 CellMerge cm = field.getAnnotation(CellMerge.class); | ||||
|                 mergeFields.add(field); | ||||
|                 mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index()); | ||||
|                 if (hasTitle) { | ||||
|                     ExcelProperty property = field.getAnnotation(ExcelProperty.class); | ||||
|                     rowIndex = Math.max(rowIndex, property.value().length); | ||||
|                 } | ||||
|             } | ||||
| 		// 行合并开始下标 | ||||
| 		int rowIndex = hasTitle ? 1 : 0; | ||||
|         } | ||||
|  | ||||
|         Map<Field, RepeatCell> map = new HashMap<>(); | ||||
|         // 生成两两合并单元格 | ||||
|         for (int i = 0; i < list.size(); i++) { | ||||
|             for (int j = 0; j < mergeFields.size(); j++) { | ||||
|                 Field field = mergeFields.get(j); | ||||
| 				String name = field.getName(); | ||||
| 				String methodName = "get" + name.substring(0, 1).toUpperCase() + name.substring(1); | ||||
| 				Method readMethod = clazz.getMethod(methodName); | ||||
| 				Object val = readMethod.invoke(list.get(i)); | ||||
|                 Object val = ReflectUtils.invokeGetter(list.get(i), field.getName()); | ||||
|  | ||||
|                 int colNum = mergeFieldsIndex.get(j); | ||||
|                 if (!map.containsKey(field)) { | ||||
|   | ||||
| @@ -42,7 +42,7 @@ public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements | ||||
|     private ExcelResult<T> excelResult; | ||||
|  | ||||
|     public DefaultExcelListener(boolean isValidate) { | ||||
|         this.excelResult = new DefautExcelResult<>(); | ||||
|         this.excelResult = new DefaultExcelResult<>(); | ||||
|         this.isValidate = isValidate; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import java.util.List; | ||||
|  * @author Yjoioooo | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public class DefautExcelResult<T> implements ExcelResult<T> { | ||||
| public class DefaultExcelResult<T> implements ExcelResult<T> { | ||||
| 
 | ||||
|     /** | ||||
|      * 数据对象list | ||||
| @@ -26,17 +26,17 @@ public class DefautExcelResult<T> implements ExcelResult<T> { | ||||
|     @Setter | ||||
|     private List<String> errorList; | ||||
| 
 | ||||
|     public DefautExcelResult() { | ||||
|     public DefaultExcelResult() { | ||||
|         this.list = new ArrayList<>(); | ||||
|         this.errorList = new ArrayList<>(); | ||||
|     } | ||||
| 
 | ||||
|     public DefautExcelResult(List<T> list, List<String> errorList) { | ||||
|     public DefaultExcelResult(List<T> list, List<String> errorList) { | ||||
|         this.list = list; | ||||
|         this.errorList = errorList; | ||||
|     } | ||||
| 
 | ||||
|     public DefautExcelResult(ExcelResult<T> excelResult) { | ||||
|     public DefaultExcelResult(ExcelResult<T> excelResult) { | ||||
|         this.list = excelResult.getList(); | ||||
|         this.errorList = excelResult.getErrorList(); | ||||
|     } | ||||
| @@ -0,0 +1,149 @@ | ||||
| package com.ruoyi.common.excel; | ||||
|  | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import com.ruoyi.common.exception.ServiceException; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.function.Function; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * <h1>Excel下拉可选项</h1> | ||||
|  * 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接 | ||||
|  * | ||||
|  * @author Emil.Zhang | ||||
|  */ | ||||
| @Data | ||||
| @AllArgsConstructor | ||||
| @NoArgsConstructor | ||||
| @SuppressWarnings("unused") | ||||
| public class DropDownOptions { | ||||
|     /** | ||||
|      * 一级下拉所在列index,从0开始算 | ||||
|      */ | ||||
|     private int index = 0; | ||||
|     /** | ||||
|      * 二级下拉所在的index,从0开始算,不能与一级相同 | ||||
|      */ | ||||
|     private int nextIndex = 0; | ||||
|     /** | ||||
|      * 一级下拉所包含的数据 | ||||
|      */ | ||||
|     private List<String> options = new ArrayList<>(); | ||||
|     /** | ||||
|      * 二级下拉所包含的数据Map | ||||
|      * <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p> | ||||
|      */ | ||||
|     private Map<String, List<String>> nextOptions = new HashMap<>(); | ||||
|     /** | ||||
|      * 分隔符 | ||||
|      */ | ||||
|     private static final String DELIMITER = "_"; | ||||
|  | ||||
|     /** | ||||
|      * 创建只有一级的下拉选 | ||||
|      */ | ||||
|     public DropDownOptions(int index, List<String> options) { | ||||
|         this.index = index; | ||||
|         this.options = options; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * <h2>创建每个选项可选值</h2> | ||||
|      * <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p> | ||||
|      * | ||||
|      * @param vars 可选值内包含的参数 | ||||
|      * @return 合规的可选值 | ||||
|      */ | ||||
|     public static String createOptionValue(Object... vars) { | ||||
|         StringBuilder stringBuffer = new StringBuilder(); | ||||
|         String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$"; | ||||
|         for (int i = 0; i < vars.length; i++) { | ||||
|             String var = StrUtil.trimToEmpty(String.valueOf(vars[i])); | ||||
|             if (!var.matches(regex)) { | ||||
|                 throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字"); | ||||
|             } | ||||
|             stringBuffer.append(var); | ||||
|             if (i < vars.length - 1) { | ||||
|                 // 直至最后一个前,都以_作为切割线 | ||||
|                 stringBuffer.append(DELIMITER); | ||||
|             } | ||||
|         } | ||||
|         if (stringBuffer.toString().matches("^\\d_*$")) { | ||||
|             throw new ServiceException("禁止以数字开头"); | ||||
|         } | ||||
|         return stringBuffer.toString(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 将处理后合理的可选值解析为原始的参数 | ||||
|      * | ||||
|      * @param option 经过处理后的合理的可选项 | ||||
|      * @return 原始的参数 | ||||
|      */ | ||||
|     public static List<String> analyzeOptionValue(String option) { | ||||
|         return StrUtil.split(option, DELIMITER, true, true); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 创建级联下拉选项 | ||||
|      * | ||||
|      * @param parentList                  父实体可选项原始数据 | ||||
|      * @param parentIndex                 父下拉选位置 | ||||
|      * @param sonList                     子实体可选项原始数据 | ||||
|      * @param sonIndex                    子下拉选位置 | ||||
|      * @param parentHowToGetIdFunction    父类如何获取唯一标识 | ||||
|      * @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识 | ||||
|      * @param howToBuildEveryOption       如何生成下拉选内容 | ||||
|      * @return 级联下拉选项 | ||||
|      */ | ||||
|     public static <T> DropDownOptions buildLinkedOptions(List<T> parentList, | ||||
|                                                          int parentIndex, | ||||
|                                                          List<T> sonList, | ||||
|                                                          int sonIndex, | ||||
|                                                          Function<T, Number> parentHowToGetIdFunction, | ||||
|                                                          Function<T, Number> sonHowToGetParentIdFunction, | ||||
|                                                          Function<T, String> howToBuildEveryOption) { | ||||
|         DropDownOptions parentLinkSonOptions = new DropDownOptions(); | ||||
|         // 先创建父类的下拉 | ||||
|         parentLinkSonOptions.setIndex(parentIndex); | ||||
|         parentLinkSonOptions.setOptions( | ||||
|             parentList.stream() | ||||
|                 .map(howToBuildEveryOption) | ||||
|                 .collect(Collectors.toList()) | ||||
|         ); | ||||
|         // 提取父-子级联下拉 | ||||
|         Map<String, List<String>> sonOptions = new HashMap<>(); | ||||
|         // 父级依据自己的ID分组 | ||||
|         Map<Number, List<T>> parentGroupByIdMap = | ||||
|             parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction)); | ||||
|         // 遍历每个子集,提取到Map中 | ||||
|         sonList.forEach(everySon -> { | ||||
|             if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) { | ||||
|                 // 找到对应的上级 | ||||
|                 T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0); | ||||
|                 // 提取名称和ID作为Key | ||||
|                 String key = howToBuildEveryOption.apply(parentObj); | ||||
|                 // Key对应的Value | ||||
|                 List<String> thisParentSonOptionList; | ||||
|                 if (sonOptions.containsKey(key)) { | ||||
|                     thisParentSonOptionList = sonOptions.get(key); | ||||
|                 } else { | ||||
|                     thisParentSonOptionList = new ArrayList<>(); | ||||
|                     sonOptions.put(key, thisParentSonOptionList); | ||||
|                 } | ||||
|                 // 往Value中添加当前子集选项 | ||||
|                 thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon)); | ||||
|             } | ||||
|         }); | ||||
|         parentLinkSonOptions.setNextIndex(sonIndex); | ||||
|         parentLinkSonOptions.setNextOptions(sonOptions); | ||||
|         return parentLinkSonOptions; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,371 @@ | ||||
| package com.ruoyi.common.excel; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.util.EnumUtil; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import com.alibaba.excel.metadata.FieldCache; | ||||
| import com.alibaba.excel.metadata.FieldWrapper; | ||||
| import com.alibaba.excel.util.ClassUtils; | ||||
| import com.alibaba.excel.write.handler.SheetWriteHandler; | ||||
| import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; | ||||
| import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; | ||||
| import com.ruoyi.common.annotation.ExcelDictFormat; | ||||
| import com.ruoyi.common.annotation.ExcelEnumFormat; | ||||
| import com.ruoyi.common.core.service.DictService; | ||||
| import com.ruoyi.common.exception.ServiceException; | ||||
| import com.ruoyi.common.utils.StreamUtils; | ||||
| import com.ruoyi.common.utils.spring.SpringUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.poi.ss.usermodel.*; | ||||
| import org.apache.poi.ss.util.CellRangeAddressList; | ||||
| import org.apache.poi.ss.util.WorkbookUtil; | ||||
| import org.apache.poi.xssf.usermodel.XSSFDataValidation; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.*; | ||||
|  | ||||
| /** | ||||
|  * <h1>Excel表格下拉选操作</h1> | ||||
|  * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行 | ||||
|  * <p> | ||||
|  * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出 | ||||
|  * | ||||
|  * @author Emil.Zhang | ||||
|  */ | ||||
| @Slf4j | ||||
| public class ExcelDownHandler implements SheetWriteHandler { | ||||
|  | ||||
|     /** | ||||
|      * Excel表格中的列名英文 | ||||
|      * 仅为了解析列英文,禁止修改 | ||||
|      */ | ||||
|     private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | ||||
|     /** | ||||
|      * 单选数据Sheet名 | ||||
|      */ | ||||
|     private static final String OPTIONS_SHEET_NAME = "options"; | ||||
|     /** | ||||
|      * 联动选择数据Sheet名的头 | ||||
|      */ | ||||
|     private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions"; | ||||
|     /** | ||||
|      * 下拉可选项 | ||||
|      */ | ||||
|     private final List<DropDownOptions> dropDownOptions; | ||||
|     /** | ||||
|      * 当前单选进度 | ||||
|      */ | ||||
|     private int currentOptionsColumnIndex; | ||||
|     /** | ||||
|      * 当前联动选择进度 | ||||
|      */ | ||||
|     private int currentLinkedOptionsSheetIndex; | ||||
|     private final DictService dictService; | ||||
|  | ||||
|     public ExcelDownHandler(List<DropDownOptions> options) { | ||||
|         this.dropDownOptions = options; | ||||
|         this.currentOptionsColumnIndex = 0; | ||||
|         this.currentLinkedOptionsSheetIndex = 0; | ||||
|         this.dictService = SpringUtils.getBean(DictService.class); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * <h2>开始创建下拉数据</h2> | ||||
|      * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项 | ||||
|      * 如果有且设置了value值,则将其直接置为下拉可选项 | ||||
|      * <p> | ||||
|      * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉 | ||||
|      * <p> | ||||
|      * 3.二者并存,注意调用方式 | ||||
|      */ | ||||
|     @Override | ||||
|     public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { | ||||
|         Sheet sheet = writeSheetHolder.getSheet(); | ||||
|         // 开始设置下拉框 HSSFWorkbook | ||||
|         DataValidationHelper helper = sheet.getDataValidationHelper(); | ||||
|         Workbook workbook = writeWorkbookHolder.getWorkbook(); | ||||
|         FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder); | ||||
|         for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) { | ||||
|             Integer index = entry.getKey(); | ||||
|             FieldWrapper wrapper = entry.getValue(); | ||||
|             Field field = wrapper.getField(); | ||||
|             // 循环实体中的每个属性 | ||||
|             // 可选的下拉值 | ||||
|             List<String> options = new ArrayList<>(); | ||||
|             if (field.isAnnotationPresent(ExcelDictFormat.class)) { | ||||
|                 // 如果指定了@ExcelDictFormat,则使用字典的逻辑 | ||||
|                 ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class); | ||||
|                 String dictType = format.dictType(); | ||||
|                 String converterExp = format.readConverterExp(); | ||||
|                 if (StrUtil.isNotBlank(dictType)) { | ||||
|                     // 如果传递了字典名,则依据字典建立下拉 | ||||
|                     Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType)) | ||||
|                         .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType))) | ||||
|                         .values(); | ||||
|                     options = new ArrayList<>(values); | ||||
|                 } else if (StrUtil.isNotBlank(converterExp)) { | ||||
|                     // 如果指定了确切的值,则直接解析确切的值 | ||||
|                     options = StrUtil.split(converterExp, format.separator(), true, true); | ||||
|                 } | ||||
|             } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) { | ||||
|                 // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑 | ||||
|                 ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class); | ||||
|                 List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField()); | ||||
|                 options = StreamUtils.toList(values, String::valueOf); | ||||
|             } | ||||
|             if (ObjectUtil.isNotEmpty(options)) { | ||||
|                 // 仅当下拉可选项不为空时执行 | ||||
|                 if (options.size() > 20) { | ||||
|                     // 这里限制如果可选项大于20,则使用额外表形式 | ||||
|                     dropDownWithSheet(helper, workbook, sheet, index, options); | ||||
|                 } else { | ||||
|                     // 否则使用固定值形式 | ||||
|                     dropDownWithSimple(helper, sheet, index, options); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if (CollUtil.isEmpty(dropDownOptions)) { | ||||
|             return; | ||||
|         } | ||||
|         dropDownOptions.forEach(everyOptions -> { | ||||
|             // 如果传递了下拉框选择器参数 | ||||
|             if (!everyOptions.getNextOptions().isEmpty()) { | ||||
|                 // 当二级选项不为空时,使用额外关联表的形式 | ||||
|                 dropDownLinkedOptions(helper, workbook, sheet, everyOptions); | ||||
|             } else if (everyOptions.getOptions().size() > 10) { | ||||
|                 // 当一级选项参数个数大于10,使用额外表的形式 | ||||
|                 dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions()); | ||||
|             } else if (everyOptions.getOptions().size() != 0) { | ||||
|                 // 当一级选项个数不为空,使用默认形式 | ||||
|                 dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * <h2>简单下拉框</h2> | ||||
|      * 直接将可选项拼接为指定列的数据校验值 | ||||
|      * | ||||
|      * @param celIndex 列index | ||||
|      * @param value    下拉选可选值 | ||||
|      */ | ||||
|     private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) { | ||||
|         if (ObjectUtil.isEmpty(value)) { | ||||
|             return; | ||||
|         } | ||||
|         this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * <h2>额外表格形式的级联下拉框</h2> | ||||
|      * | ||||
|      * @param options 额外表格形式存储的下拉可选项 | ||||
|      */ | ||||
|     private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) { | ||||
|         String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex); | ||||
|         // 创建联动下拉数据表 | ||||
|         Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName)); | ||||
|         // 将下拉表隐藏 | ||||
|         workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true); | ||||
|         // 完善横向的一级选项数据表 | ||||
|         List<String> firstOptions = options.getOptions(); | ||||
|         Map<String, List<String>> secoundOptionsMap = options.getNextOptions(); | ||||
|  | ||||
|         // 创建名称管理器 | ||||
|         Name name = workbook.createName(); | ||||
|         // 设置名称管理器的别名 | ||||
|         name.setNameName(linkedOptionsSheetName); | ||||
|         // 以横向第一行创建一级下拉拼接引用位置 | ||||
|         String firstOptionsFunction = String.format("%s!$%s$1:$%s$1", | ||||
|             linkedOptionsSheetName, | ||||
|             getExcelColumnName(0), | ||||
|             getExcelColumnName(firstOptions.size()) | ||||
|         ); | ||||
|         // 设置名称管理器的引用位置 | ||||
|         name.setRefersToFormula(firstOptionsFunction); | ||||
|         // 设置数据校验为序列模式,引用的是名称管理器中的别名 | ||||
|         this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName)); | ||||
|  | ||||
|         for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) { | ||||
|             // 先提取主表中一级下拉的列名 | ||||
|             String firstOptionsColumnName = getExcelColumnName(columIndex); | ||||
|             // 一次循环是每一个一级选项 | ||||
|             int finalI = columIndex; | ||||
|             // 本次循环的一级选项值 | ||||
|             String thisFirstOptionsValue = firstOptions.get(columIndex); | ||||
|             // 创建第一行的数据 | ||||
|             Optional.ofNullable(linkedOptionsDataSheet.getRow(0)) | ||||
|                 // 如果不存在则创建第一行 | ||||
|                 .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI)) | ||||
|                 // 第一行当前列 | ||||
|                 .createCell(columIndex) | ||||
|                 // 设置值为当前一级选项值 | ||||
|                 .setCellValue(thisFirstOptionsValue); | ||||
|  | ||||
|             // 第二行开始,设置第二级别选项参数 | ||||
|             List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue); | ||||
|             if (CollUtil.isEmpty(secondOptions)) { | ||||
|                 // 必须保证至少有一个关联选项,否则将导致Excel解析错误 | ||||
|                 secondOptions = Collections.singletonList("暂无_0"); | ||||
|             } | ||||
|  | ||||
|             // 以该一级选项值创建子名称管理器 | ||||
|             Name sonName = workbook.createName(); | ||||
|             // 设置名称管理器的别名 | ||||
|             sonName.setNameName(thisFirstOptionsValue); | ||||
|             // 以第二行该列数据拼接引用位置 | ||||
|             String sonFunction = String.format("%s!$%s$2:$%s$%d", | ||||
|                 linkedOptionsSheetName, | ||||
|                 firstOptionsColumnName, | ||||
|                 firstOptionsColumnName, | ||||
|                 secondOptions.size() + 1 | ||||
|             ); | ||||
|             // 设置名称管理器的引用位置 | ||||
|             sonName.setRefersToFormula(sonFunction); | ||||
|             // 数据验证为序列模式,引用到每一个主表中的二级选项位置 | ||||
|             // 创建子项的名称管理器,只是为了使得Excel可以识别到数据 | ||||
|             String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex()); | ||||
|             for (int i = 0; i < 100; i++) { | ||||
|                 // 以一级选项对应的主体所在位置创建二级下拉 | ||||
|                 String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1); | ||||
|                 // 二级只能主表每一行的每一列添加二级校验 | ||||
|                 markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction)); | ||||
|             } | ||||
|  | ||||
|             for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) { | ||||
|                 // 从第二行开始填充二级选项 | ||||
|                 int finalRowIndex = rowIndex + 1; | ||||
|                 int finalColumIndex = columIndex; | ||||
|  | ||||
|                 Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex)) | ||||
|                     // 没有则创建 | ||||
|                     .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex)); | ||||
|                 Optional | ||||
|                     // 在本级一级选项所在的列 | ||||
|                     .ofNullable(row.getCell(finalColumIndex)) | ||||
|                     // 不存在则创建 | ||||
|                     .orElseGet(() -> row.createCell(finalColumIndex)) | ||||
|                     // 设置二级选项值 | ||||
|                     .setCellValue(secondOptions.get(rowIndex)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         currentLinkedOptionsSheetIndex++; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * <h2>额外表格形式的普通下拉框</h2> | ||||
|      * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉 | ||||
|      * | ||||
|      * @param celIndex 下拉选 | ||||
|      * @param value    下拉选可选值 | ||||
|      */ | ||||
|     private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) { | ||||
|         // 创建下拉数据表 | ||||
|         Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME))) | ||||
|             .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME))); | ||||
|         // 将下拉表隐藏 | ||||
|         workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true); | ||||
|         // 完善纵向的一级选项数据表 | ||||
|         for (int i = 0; i < value.size(); i++) { | ||||
|             int finalI = i; | ||||
|             // 获取每一选项行,如果没有则创建 | ||||
|             Row row = Optional.ofNullable(simpleDataSheet.getRow(i)) | ||||
|                 .orElseGet(() -> simpleDataSheet.createRow(finalI)); | ||||
|             // 获取本级选项对应的选项列,如果没有则创建 | ||||
|             Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex)) | ||||
|                 .orElseGet(() -> row.createCell(currentOptionsColumnIndex)); | ||||
|             // 设置值 | ||||
|             cell.setCellValue(value.get(i)); | ||||
|         } | ||||
|  | ||||
|         // 创建名称管理器 | ||||
|         Name name = workbook.createName(); | ||||
|         // 设置名称管理器的别名 | ||||
|         String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex); | ||||
|         name.setNameName(nameName); | ||||
|         // 以纵向第一列创建一级下拉拼接引用位置 | ||||
|         String function = String.format("%s!$%s$1:$%s$%d", | ||||
|             OPTIONS_SHEET_NAME, | ||||
|             getExcelColumnName(currentOptionsColumnIndex), | ||||
|             getExcelColumnName(currentOptionsColumnIndex), | ||||
|             value.size()); | ||||
|         // 设置名称管理器的引用位置 | ||||
|         name.setRefersToFormula(function); | ||||
|         // 设置数据校验为序列模式,引用的是名称管理器中的别名 | ||||
|         this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName)); | ||||
|         currentOptionsColumnIndex++; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 挂载下拉的列,仅限一级选项 | ||||
|      */ | ||||
|     private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex, | ||||
|                                     DataValidationConstraint constraint) { | ||||
|         // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 | ||||
|         CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex); | ||||
|         markDataValidationToSheet(helper, sheet, constraint, addressList); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 挂载下拉的列,仅限二级选项 | ||||
|      */ | ||||
|     private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex, | ||||
|                                           Integer celIndex, DataValidationConstraint constraint) { | ||||
|         // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 | ||||
|         CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex); | ||||
|         markDataValidationToSheet(helper, sheet, constraint, addressList); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 应用数据校验 | ||||
|      */ | ||||
|     private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet, | ||||
|                                            DataValidationConstraint constraint, CellRangeAddressList addressList) { | ||||
|         // 数据有效性对象 | ||||
|         DataValidation dataValidation = helper.createValidation(constraint, addressList); | ||||
|         // 处理Excel兼容性问题 | ||||
|         if (dataValidation instanceof XSSFDataValidation) { | ||||
|             //数据校验 | ||||
|             dataValidation.setSuppressDropDownArrow(true); | ||||
|             //错误提示 | ||||
|             dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP); | ||||
|             dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致"); | ||||
|             dataValidation.setShowErrorBox(true); | ||||
|             //选定提示 | ||||
|             dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败"); | ||||
|             dataValidation.setShowPromptBox(true); | ||||
|             sheet.addValidationData(dataValidation); | ||||
|         } else { | ||||
|             dataValidation.setSuppressDropDownArrow(false); | ||||
|         } | ||||
|         sheet.addValidationData(dataValidation); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * <h2>依据列index获取列名英文</h2> | ||||
|      * 依据列index转换为Excel中的列名英文 | ||||
|      * <p>例如第1列,index为0,解析出来为A列</p> | ||||
|      * 第27列,index为26,解析为AA列 | ||||
|      * <p>第28列,index为27,解析为AB列</p> | ||||
|      * | ||||
|      * @param columnIndex 列index | ||||
|      * @return 列index所在得英文名 | ||||
|      */ | ||||
|     private String getExcelColumnName(int columnIndex) { | ||||
|         // 26一循环的次数 | ||||
|         int columnCircleCount = columnIndex / 26; | ||||
|         // 26一循环内的位置 | ||||
|         int thisCircleColumnIndex = columnIndex % 26; | ||||
|         // 26一循环的次数大于0,则视为栏名至少两位 | ||||
|         String columnPrefix = columnCircleCount == 0 | ||||
|             ? StrUtil.EMPTY | ||||
|             : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1); | ||||
|         // 从26一循环内取对应的栏位名 | ||||
|         String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1); | ||||
|         // 将二者拼接即为最终的栏位名 | ||||
|         return columnPrefix + columnNext; | ||||
|     } | ||||
| } | ||||
| @@ -25,7 +25,7 @@ public class XssFilter implements Filter { | ||||
|     public void init(FilterConfig filterConfig) throws ServletException { | ||||
|         String tempExcludes = filterConfig.getInitParameter("excludes"); | ||||
|         if (StringUtils.isNotEmpty(tempExcludes)) { | ||||
|             String[] url = tempExcludes.split(","); | ||||
|             String[] url = tempExcludes.split(StringUtils.SEPARATOR); | ||||
|             for (int i = 0; url != null && i < url.length; i++) { | ||||
|                 excludes.add(url[i]); | ||||
|             } | ||||
|   | ||||
| @@ -66,7 +66,7 @@ public class DataBaseHelper { | ||||
|             // instr(',0,100,101,' , ',100,') <> 0 | ||||
|             return "instr(','||" + var2 + "||',' , '," + var + ",') <> 0"; | ||||
|         } | ||||
|         // find_in_set(100 , '0,100,101') | ||||
|         return "find_in_set(" + var + " , " + var2 + ") <> 0"; | ||||
|         // find_in_set('100' , '0,100,101') | ||||
|         return "find_in_set('" + var + "' , " + var2 + ") <> 0"; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,11 +3,14 @@ package com.ruoyi.common.helper; | ||||
| import cn.dev33.satoken.context.SaHolder; | ||||
| import cn.dev33.satoken.context.model.SaStorage; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy; | ||||
| import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.function.Supplier; | ||||
|  | ||||
| /** | ||||
|  * 数据权限助手 | ||||
| @@ -44,4 +47,47 @@ public class DataPermissionHelper { | ||||
|         } | ||||
|         throw new NullPointerException("data permission context type exception"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭) | ||||
|      */ | ||||
|     public static void enableIgnore() { | ||||
|         InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 关闭忽略数据权限 | ||||
|      */ | ||||
|     public static void disableIgnore() { | ||||
|         InterceptorIgnoreHelper.clearIgnoreStrategy(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 在忽略数据权限中执行 | ||||
|      * | ||||
|      * @param handle 处理执行方法 | ||||
|      */ | ||||
|     public static void ignore(Runnable handle) { | ||||
|         enableIgnore(); | ||||
|         try { | ||||
|             handle.run(); | ||||
|         } finally { | ||||
|             disableIgnore(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 在忽略数据权限中执行 | ||||
|      * | ||||
|      * @param handle 处理执行方法 | ||||
|      */ | ||||
|     public static <T> T ignore(Supplier<T> handle) { | ||||
|         enableIgnore(); | ||||
|         try { | ||||
|             return handle.get(); | ||||
|         } finally { | ||||
|             disableIgnore(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,25 +1,26 @@ | ||||
| package com.ruoyi.common.helper; | ||||
|  | ||||
| import cn.dev33.satoken.context.SaHolder; | ||||
| import cn.dev33.satoken.context.model.SaStorage; | ||||
| import cn.dev33.satoken.session.SaSession; | ||||
| import cn.dev33.satoken.stp.SaLoginModel; | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.ruoyi.common.constant.UserConstants; | ||||
| import com.ruoyi.common.core.domain.model.LoginUser; | ||||
| import com.ruoyi.common.enums.DeviceType; | ||||
| import com.ruoyi.common.enums.UserType; | ||||
| import com.ruoyi.common.exception.UtilException; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| /** | ||||
|  * 登录鉴权助手 | ||||
|  * | ||||
|  * <p> | ||||
|  * user_type 为 用户类型 同一个用户表 可以有多种用户类型 例如 pc,app | ||||
|  * deivce 为 设备类型 同一个用户类型 可以有 多种设备类型 例如 web,ios | ||||
|  * 可以组成 用户类型与设备类型多对多的 权限灵活控制 | ||||
|  * | ||||
|  * <p> | ||||
|  * 多用户体系 针对 多种用户类型 但权限控制不一致 | ||||
|  * 可以组成 多用户类型表与多设备类型 分别控制权限 | ||||
|  * | ||||
| @@ -28,8 +29,8 @@ import lombok.NoArgsConstructor; | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class LoginHelper { | ||||
|  | ||||
|     public static final String JOIN_CODE = ":"; | ||||
|     public static final String LOGIN_USER_KEY = "loginUser"; | ||||
|     public static final String USER_KEY = "userId"; | ||||
|  | ||||
|     /** | ||||
|      * 登录系统 | ||||
| @@ -37,9 +38,7 @@ public class LoginHelper { | ||||
|      * @param loginUser 登录用户信息 | ||||
|      */ | ||||
|     public static void login(LoginUser loginUser) { | ||||
|         SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser); | ||||
|         StpUtil.login(loginUser.getLoginId()); | ||||
|         setLoginUser(loginUser); | ||||
|         loginByDevice(loginUser, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -49,15 +48,22 @@ public class LoginHelper { | ||||
|      * @param loginUser 登录用户信息 | ||||
|      */ | ||||
|     public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) { | ||||
|         SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser); | ||||
|         StpUtil.login(loginUser.getLoginId(), deviceType.getDevice()); | ||||
|         setLoginUser(loginUser); | ||||
|         SaStorage storage = SaHolder.getStorage(); | ||||
|         storage.set(LOGIN_USER_KEY, loginUser); | ||||
|         storage.set(USER_KEY, loginUser.getUserId()); | ||||
|         SaLoginModel model = new SaLoginModel(); | ||||
|         if (ObjectUtil.isNotNull(deviceType)) { | ||||
|             model.setDevice(deviceType.getDevice()); | ||||
|         } | ||||
|  | ||||
|     /** | ||||
|      * 设置用户数据(多级缓存) | ||||
|      */ | ||||
|     public static void setLoginUser(LoginUser loginUser) { | ||||
|         // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 | ||||
|         // 例如: 后台用户30分钟过期 app用户1天过期 | ||||
| //        UserType userType = UserType.getUserType(loginUser.getUserType()); | ||||
| //        if (userType == UserType.SYS_USER) { | ||||
| //            model.setTimeout(86400).setActiveTimeout(1800); | ||||
| //        } else if (userType == UserType.APP_USER) { | ||||
| //            model.setTimeout(86400).setActiveTimeout(1800); | ||||
| //        } | ||||
|         StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId())); | ||||
|         StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser); | ||||
|     } | ||||
|  | ||||
| @@ -69,32 +75,41 @@ public class LoginHelper { | ||||
|         if (loginUser != null) { | ||||
|             return loginUser; | ||||
|         } | ||||
|         loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY); | ||||
|         SaSession session = StpUtil.getTokenSession(); | ||||
|         if (ObjectUtil.isNull(session)) { | ||||
|             return null; | ||||
|         } | ||||
|         loginUser = (LoginUser) session.get(LOGIN_USER_KEY); | ||||
|         SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser); | ||||
|         return loginUser; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取用户基于token | ||||
|      */ | ||||
|     public static LoginUser getLoginUser(String token) { | ||||
|         SaSession session = StpUtil.getTokenSessionByToken(token); | ||||
|         if (ObjectUtil.isNull(session)) { | ||||
|             return null; | ||||
|         } | ||||
|         return (LoginUser) session.get(LOGIN_USER_KEY); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取用户id | ||||
|      */ | ||||
|     public static Long getUserId() { | ||||
|         LoginUser loginUser = getLoginUser(); | ||||
|         if (ObjectUtil.isNull(loginUser)) { | ||||
|             String loginId = StpUtil.getLoginIdAsString(); | ||||
|             String userId = null; | ||||
|             for (UserType value : UserType.values()) { | ||||
|                 if (StringUtils.contains(loginId, value.getUserType())) { | ||||
|                     String[] strs = StringUtils.split(loginId, JOIN_CODE); | ||||
|                     // 用户id在总是在最后 | ||||
|                     userId = strs[strs.length - 1]; | ||||
|         Long userId; | ||||
|         try { | ||||
|             userId = Convert.toLong(SaHolder.getStorage().get(USER_KEY)); | ||||
|             if (ObjectUtil.isNull(userId)) { | ||||
|                 userId = Convert.toLong(StpUtil.getExtra(USER_KEY)); | ||||
|                 SaHolder.getStorage().set(USER_KEY, userId); | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             return null; | ||||
|         } | ||||
|             if (StringUtils.isBlank(userId)) { | ||||
|                 throw new UtilException("登录用户: LoginId异常 => " + loginId); | ||||
|             } | ||||
|             return Long.parseLong(userId); | ||||
|         } | ||||
|         return loginUser.getUserId(); | ||||
|         return userId; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -115,8 +130,8 @@ public class LoginHelper { | ||||
|      * 获取用户类型 | ||||
|      */ | ||||
|     public static UserType getUserType() { | ||||
|         String loginId = StpUtil.getLoginIdAsString(); | ||||
|         return UserType.getUserType(loginId); | ||||
|         String loginType = StpUtil.getLoginIdAsString(); | ||||
|         return UserType.getUserType(loginType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -22,7 +22,9 @@ import java.util.Objects; | ||||
|  * 字典数据json序列化工具 | ||||
|  * | ||||
|  * @author itino | ||||
|  * @deprecated 建议使用通用翻译注解 | ||||
|  */ | ||||
| @Deprecated | ||||
| @Slf4j | ||||
| public class DictDataJsonSerializer extends JsonSerializer<String> implements ContextualSerializer { | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| package com.ruoyi.common.translation; | ||||
|  | ||||
| /** | ||||
|  * 翻译接口 (实现类需标注 {@link com.ruoyi.common.annotation.TranslationType} 注解标明翻译类型) | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public interface TranslationInterface<T> { | ||||
|  | ||||
|     /** | ||||
|      * 翻译 | ||||
|      * | ||||
|      * @param key 需要被翻译的键(不为空) | ||||
|      * @return 返回键对应的值 | ||||
|      */ | ||||
|     T translation(Object key, String other); | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| package com.ruoyi.common.translation.handler; | ||||
|  | ||||
| import com.fasterxml.jackson.databind.BeanDescription; | ||||
| import com.fasterxml.jackson.databind.SerializationConfig; | ||||
| import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; | ||||
| import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Bean 序列化修改器 解决 Null 被单独处理问题 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| public class TranslationBeanSerializerModifier extends BeanSerializerModifier { | ||||
|  | ||||
|     @Override | ||||
|     public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, | ||||
|                                                      List<BeanPropertyWriter> beanProperties) { | ||||
|         for (BeanPropertyWriter writer : beanProperties) { | ||||
|             // 如果序列化器为 TranslationHandler 的话 将 Null 值也交给他处理 | ||||
|             if (writer.getSerializer() instanceof TranslationHandler) { | ||||
|                 writer.assignNullSerializer(writer.getSerializer()); | ||||
|             } | ||||
|         } | ||||
|         return beanProperties; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,65 @@ | ||||
| package com.ruoyi.common.translation.handler; | ||||
|  | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.fasterxml.jackson.core.JsonGenerator; | ||||
| import com.fasterxml.jackson.databind.BeanProperty; | ||||
| import com.fasterxml.jackson.databind.JsonMappingException; | ||||
| import com.fasterxml.jackson.databind.JsonSerializer; | ||||
| import com.fasterxml.jackson.databind.SerializerProvider; | ||||
| import com.fasterxml.jackson.databind.ser.ContextualSerializer; | ||||
| import com.ruoyi.common.annotation.Translation; | ||||
| import com.ruoyi.common.translation.TranslationInterface; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import com.ruoyi.common.utils.reflect.ReflectUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
|  | ||||
| /** | ||||
|  * 翻译处理器 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Slf4j | ||||
| public class TranslationHandler extends JsonSerializer<Object> implements ContextualSerializer { | ||||
|  | ||||
|     /** | ||||
|      * 全局翻译实现类映射器 | ||||
|      */ | ||||
|     public static final Map<String, TranslationInterface<?>> TRANSLATION_MAPPER = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     private Translation translation; | ||||
|  | ||||
|     @Override | ||||
|     public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { | ||||
|         TranslationInterface<?> trans = TRANSLATION_MAPPER.get(translation.type()); | ||||
|         if (ObjectUtil.isNotNull(trans)) { | ||||
|             // 如果映射字段不为空 则取映射字段的值 | ||||
|             if (StringUtils.isNotBlank(translation.mapper())) { | ||||
|                 value = ReflectUtils.invokeGetter(gen.getCurrentValue(), translation.mapper()); | ||||
|             } | ||||
|             // 如果为 null 直接写出 | ||||
|             if (ObjectUtil.isNull(value)) { | ||||
|                 gen.writeNull(); | ||||
|                 return; | ||||
|             } | ||||
|             Object result = trans.translation(value, translation.other()); | ||||
|             gen.writeObject(result); | ||||
|         } else { | ||||
|             gen.writeObject(value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { | ||||
|         Translation translation = property.getAnnotation(Translation.class); | ||||
|         if (Objects.nonNull(translation)) { | ||||
|             this.translation = translation; | ||||
|             return this; | ||||
|         } | ||||
|         return prov.findValueSerializer(property.getType(), property); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package com.ruoyi.common.translation.impl; | ||||
|  | ||||
| import com.ruoyi.common.annotation.TranslationType; | ||||
| import com.ruoyi.common.constant.TransConstant; | ||||
| import com.ruoyi.common.core.service.DeptService; | ||||
| import com.ruoyi.common.translation.TranslationInterface; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| /** | ||||
|  * 部门翻译实现 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Component | ||||
| @AllArgsConstructor | ||||
| @TranslationType(type = TransConstant.DEPT_ID_TO_NAME) | ||||
| public class DeptNameTranslationImpl implements TranslationInterface<String> { | ||||
|  | ||||
|     private final DeptService deptService; | ||||
|      | ||||
|     @Override | ||||
|     public String translation(Object key, String other) { | ||||
|         return deptService.selectDeptNameByIds(key.toString()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| package com.ruoyi.common.translation.impl; | ||||
|  | ||||
| import com.ruoyi.common.annotation.TranslationType; | ||||
| import com.ruoyi.common.constant.TransConstant; | ||||
| import com.ruoyi.common.core.service.DictService; | ||||
| import com.ruoyi.common.translation.TranslationInterface; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| /** | ||||
|  * 字典翻译实现 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Component | ||||
| @AllArgsConstructor | ||||
| @TranslationType(type = TransConstant.DICT_TYPE_TO_LABEL) | ||||
| public class DictTypeTranslationImpl implements TranslationInterface<String> { | ||||
|  | ||||
|     private final DictService dictService; | ||||
|  | ||||
|     @Override | ||||
|     public String translation(Object key, String other) { | ||||
|         if (key instanceof String && StringUtils.isNotBlank(other)) { | ||||
|             return dictService.getDictLabel(other, key.toString()); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package com.ruoyi.common.translation.impl; | ||||
|  | ||||
| import com.ruoyi.common.annotation.TranslationType; | ||||
| import com.ruoyi.common.constant.TransConstant; | ||||
| import com.ruoyi.common.core.service.OssService; | ||||
| import com.ruoyi.common.translation.TranslationInterface; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| /** | ||||
|  * OSS翻译实现 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Component | ||||
| @AllArgsConstructor | ||||
| @TranslationType(type = TransConstant.OSS_ID_TO_URL) | ||||
| public class OssUrlTranslationImpl implements TranslationInterface<String> { | ||||
|  | ||||
|     private final OssService ossService; | ||||
|  | ||||
|     @Override | ||||
|     public String translation(Object key, String other) { | ||||
|         return ossService.selectUrlByIds(key.toString()); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| package com.ruoyi.common.translation.impl; | ||||
|  | ||||
| import com.ruoyi.common.annotation.TranslationType; | ||||
| import com.ruoyi.common.constant.TransConstant; | ||||
| import com.ruoyi.common.core.service.UserService; | ||||
| import com.ruoyi.common.translation.TranslationInterface; | ||||
| import lombok.AllArgsConstructor; | ||||
| import org.springframework.stereotype.Component; | ||||
|  | ||||
| /** | ||||
|  * 用户名翻译实现 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
| @Component | ||||
| @AllArgsConstructor | ||||
| @TranslationType(type = TransConstant.USER_ID_TO_NAME) | ||||
| public class UserNameTranslationImpl implements TranslationInterface<String> { | ||||
|  | ||||
|     private final UserService userService; | ||||
|  | ||||
|     @Override | ||||
|     public String translation(Object key, String other) { | ||||
|         if (key instanceof Long) { | ||||
|             return userService.selectUserNameById((Long) key); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
| @@ -17,11 +17,10 @@ import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * bean深拷贝工具(基于 cglib 性能优异) | ||||
|  * bean拷贝工具(基于 cglib 性能优异) | ||||
|  * <p> | ||||
|  * 重点 cglib 不支持 拷贝到链式对象 | ||||
|  * 例如: 源对象 拷贝到 目标(链式对象) | ||||
|  * 请区分好`浅拷贝`和`深拷贝`再做使用 | ||||
|  * | ||||
|  * @author Lion Li | ||||
|  */ | ||||
|   | ||||
| @@ -0,0 +1,311 @@ | ||||
| package com.ruoyi.common.utils; | ||||
|  | ||||
| import cn.hutool.core.codec.Base64; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.hutool.crypto.SecureUtil; | ||||
| import cn.hutool.crypto.SmUtil; | ||||
| import cn.hutool.crypto.asymmetric.KeyType; | ||||
| import cn.hutool.crypto.asymmetric.RSA; | ||||
| import cn.hutool.crypto.asymmetric.SM2; | ||||
|  | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 安全相关工具类 | ||||
|  * | ||||
|  * @author 老马 | ||||
|  */ | ||||
| public class EncryptUtils { | ||||
|     /** | ||||
|      * 公钥 | ||||
|      */ | ||||
|     public static final String PUBLIC_KEY = "publicKey"; | ||||
|     /** | ||||
|      * 私钥 | ||||
|      */ | ||||
|     public static final String PRIVATE_KEY = "privateKey"; | ||||
|  | ||||
|     /** | ||||
|      * Base64加密 | ||||
|      * | ||||
|      * @param data 待加密数据 | ||||
|      * @return 加密后字符串 | ||||
|      */ | ||||
|     public static String encryptByBase64(String data) { | ||||
|         return Base64.encode(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Base64解密 | ||||
|      * | ||||
|      * @param data 待解密数据 | ||||
|      * @return 解密后字符串 | ||||
|      */ | ||||
|     public static String decryptByBase64(String data) { | ||||
|         return Base64.decodeStr(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * AES加密 | ||||
|      * | ||||
|      * @param data     待解密数据 | ||||
|      * @param password 秘钥字符串 | ||||
|      * @return 加密后字符串, 采用Base64编码 | ||||
|      */ | ||||
|     public static String encryptByAes(String data, String password) { | ||||
|         if (StrUtil.isBlank(password)) { | ||||
|             throw new IllegalArgumentException("AES需要传入秘钥信息"); | ||||
|         } | ||||
|         // aes算法的秘钥要求是16位、24位、32位 | ||||
|         int[] array = {16, 24, 32}; | ||||
|         if (!ArrayUtil.contains(array, password.length())) { | ||||
|             throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位"); | ||||
|         } | ||||
|         return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * AES加密 | ||||
|      * | ||||
|      * @param data     待解密数据 | ||||
|      * @param password 秘钥字符串 | ||||
|      * @return 加密后字符串, 采用Hex编码 | ||||
|      */ | ||||
|     public static String encryptByAesHex(String data, String password) { | ||||
|         if (StrUtil.isBlank(password)) { | ||||
|             throw new IllegalArgumentException("AES需要传入秘钥信息"); | ||||
|         } | ||||
|         // aes算法的秘钥要求是16位、24位、32位 | ||||
|         int[] array = {16, 24, 32}; | ||||
|         if (!ArrayUtil.contains(array, password.length())) { | ||||
|             throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位"); | ||||
|         } | ||||
|         return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * AES解密 | ||||
|      * | ||||
|      * @param data     待解密数据 | ||||
|      * @param password 秘钥字符串 | ||||
|      * @return 解密后字符串 | ||||
|      */ | ||||
|     public static String decryptByAes(String data, String password) { | ||||
|         if (StrUtil.isBlank(password)) { | ||||
|             throw new IllegalArgumentException("AES需要传入秘钥信息"); | ||||
|         } | ||||
|         // aes算法的秘钥要求是16位、24位、32位 | ||||
|         int[] array = {16, 24, 32}; | ||||
|         if (!ArrayUtil.contains(array, password.length())) { | ||||
|             throw new IllegalArgumentException("AES秘钥长度要求为16位、24位、32位"); | ||||
|         } | ||||
|         return SecureUtil.aes(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm4加密 | ||||
|      * | ||||
|      * @param data     待加密数据 | ||||
|      * @param password 秘钥字符串 | ||||
|      * @return 加密后字符串, 采用Base64编码 | ||||
|      */ | ||||
|     public static String encryptBySm4(String data, String password) { | ||||
|         if (StrUtil.isBlank(password)) { | ||||
|             throw new IllegalArgumentException("SM4需要传入秘钥信息"); | ||||
|         } | ||||
|         // sm4算法的秘钥要求是16位长度 | ||||
|         int sm4PasswordLength = 16; | ||||
|         if (sm4PasswordLength != password.length()) { | ||||
|             throw new IllegalArgumentException("SM4秘钥长度要求为16位"); | ||||
|         } | ||||
|         return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptBase64(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm4加密 | ||||
|      * | ||||
|      * @param data     待加密数据 | ||||
|      * @param password 秘钥字符串 | ||||
|      * @return 加密后字符串, 采用Base64编码 | ||||
|      */ | ||||
|     public static String encryptBySm4Hex(String data, String password) { | ||||
|         if (StrUtil.isBlank(password)) { | ||||
|             throw new IllegalArgumentException("SM4需要传入秘钥信息"); | ||||
|         } | ||||
|         // sm4算法的秘钥要求是16位长度 | ||||
|         int sm4PasswordLength = 16; | ||||
|         if (sm4PasswordLength != password.length()) { | ||||
|             throw new IllegalArgumentException("SM4秘钥长度要求为16位"); | ||||
|         } | ||||
|         return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).encryptHex(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm4解密 | ||||
|      * | ||||
|      * @param data     待解密数据 | ||||
|      * @param password 秘钥字符串 | ||||
|      * @return 解密后字符串 | ||||
|      */ | ||||
|     public static String decryptBySm4(String data, String password) { | ||||
|         if (StrUtil.isBlank(password)) { | ||||
|             throw new IllegalArgumentException("SM4需要传入秘钥信息"); | ||||
|         } | ||||
|         // sm4算法的秘钥要求是16位长度 | ||||
|         int sm4PasswordLength = 16; | ||||
|         if (sm4PasswordLength != password.length()) { | ||||
|             throw new IllegalArgumentException("SM4秘钥长度要求为16位"); | ||||
|         } | ||||
|         return SmUtil.sm4(password.getBytes(StandardCharsets.UTF_8)).decryptStr(data, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 产生sm2加解密需要的公钥和私钥 | ||||
|      * | ||||
|      * @return 公私钥Map | ||||
|      */ | ||||
|     public static Map<String, String> generateSm2Key() { | ||||
|         Map<String, String> keyMap = new HashMap<>(2); | ||||
|         SM2 sm2 = SmUtil.sm2(); | ||||
|         keyMap.put(PRIVATE_KEY, sm2.getPrivateKeyBase64()); | ||||
|         keyMap.put(PUBLIC_KEY, sm2.getPublicKeyBase64()); | ||||
|         return keyMap; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm2公钥加密 | ||||
|      * | ||||
|      * @param data      待加密数据 | ||||
|      * @param publicKey 公钥 | ||||
|      * @return 加密后字符串, 采用Base64编码 | ||||
|      */ | ||||
|     public static String encryptBySm2(String data, String publicKey) { | ||||
|         if (StrUtil.isBlank(publicKey)) { | ||||
|             throw new IllegalArgumentException("SM2需要传入公钥进行加密"); | ||||
|         } | ||||
|         SM2 sm2 = SmUtil.sm2(null, publicKey); | ||||
|         return sm2.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm2公钥加密 | ||||
|      * | ||||
|      * @param data      待加密数据 | ||||
|      * @param publicKey 公钥 | ||||
|      * @return 加密后字符串, 采用Hex编码 | ||||
|      */ | ||||
|     public static String encryptBySm2Hex(String data, String publicKey) { | ||||
|         if (StrUtil.isBlank(publicKey)) { | ||||
|             throw new IllegalArgumentException("SM2需要传入公钥进行加密"); | ||||
|         } | ||||
|         SM2 sm2 = SmUtil.sm2(null, publicKey); | ||||
|         return sm2.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm2私钥解密 | ||||
|      * | ||||
|      * @param data       待加密数据 | ||||
|      * @param privateKey 私钥 | ||||
|      * @return 解密后字符串 | ||||
|      */ | ||||
|     public static String decryptBySm2(String data, String privateKey) { | ||||
|         if (StrUtil.isBlank(privateKey)) { | ||||
|             throw new IllegalArgumentException("SM2需要传入私钥进行解密"); | ||||
|         } | ||||
|         SM2 sm2 = SmUtil.sm2(privateKey, null); | ||||
|         return sm2.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 产生RSA加解密需要的公钥和私钥 | ||||
|      * | ||||
|      * @return 公私钥Map | ||||
|      */ | ||||
|     public static Map<String, String> generateRsaKey() { | ||||
|         Map<String, String> keyMap = new HashMap<>(2); | ||||
|         RSA rsa = SecureUtil.rsa(); | ||||
|         keyMap.put(PRIVATE_KEY, rsa.getPrivateKeyBase64()); | ||||
|         keyMap.put(PUBLIC_KEY, rsa.getPublicKeyBase64()); | ||||
|         return keyMap; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * rsa公钥加密 | ||||
|      * | ||||
|      * @param data      待加密数据 | ||||
|      * @param publicKey 公钥 | ||||
|      * @return 加密后字符串, 采用Base64编码 | ||||
|      */ | ||||
|     public static String encryptByRsa(String data, String publicKey) { | ||||
|         if (StrUtil.isBlank(publicKey)) { | ||||
|             throw new IllegalArgumentException("RSA需要传入公钥进行加密"); | ||||
|         } | ||||
|         RSA rsa = SecureUtil.rsa(null, publicKey); | ||||
|         return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * rsa公钥加密 | ||||
|      * | ||||
|      * @param data      待加密数据 | ||||
|      * @param publicKey 公钥 | ||||
|      * @return 加密后字符串, 采用Hex编码 | ||||
|      */ | ||||
|     public static String encryptByRsaHex(String data, String publicKey) { | ||||
|         if (StrUtil.isBlank(publicKey)) { | ||||
|             throw new IllegalArgumentException("RSA需要传入公钥进行加密"); | ||||
|         } | ||||
|         RSA rsa = SecureUtil.rsa(null, publicKey); | ||||
|         return rsa.encryptHex(data, StandardCharsets.UTF_8, KeyType.PublicKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * rsa私钥解密 | ||||
|      * | ||||
|      * @param data       待加密数据 | ||||
|      * @param privateKey 私钥 | ||||
|      * @return 解密后字符串 | ||||
|      */ | ||||
|     public static String decryptByRsa(String data, String privateKey) { | ||||
|         if (StrUtil.isBlank(privateKey)) { | ||||
|             throw new IllegalArgumentException("RSA需要传入私钥进行解密"); | ||||
|         } | ||||
|         RSA rsa = SecureUtil.rsa(privateKey, null); | ||||
|         return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * md5加密 | ||||
|      * | ||||
|      * @param data 待加密数据 | ||||
|      * @return 加密后字符串, 采用Hex编码 | ||||
|      */ | ||||
|     public static String encryptByMd5(String data) { | ||||
|         return SecureUtil.md5(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sha256加密 | ||||
|      * | ||||
|      * @param data 待加密数据 | ||||
|      * @return 加密后字符串, 采用Hex编码 | ||||
|      */ | ||||
|     public static String encryptBySha256(String data) { | ||||
|         return SecureUtil.sha256(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * sm3加密 | ||||
|      * | ||||
|      * @param data 待加密数据 | ||||
|      * @return 加密后字符串, 采用Hex编码 | ||||
|      */ | ||||
|     public static String encryptBySm3(String data) { | ||||
|         return SmUtil.sm3(data); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -94,7 +94,7 @@ public class ServletUtils extends ServletUtil { | ||||
|     public static Map<String, String> getParamMap(ServletRequest request) { | ||||
|         Map<String, String> params = new HashMap<>(); | ||||
|         for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) { | ||||
|             params.put(entry.getKey(), StringUtils.join(entry.getValue(), ",")); | ||||
|             params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR)); | ||||
|         } | ||||
|         return params; | ||||
|     } | ||||
|   | ||||
| @@ -41,7 +41,7 @@ public class StreamUtils { | ||||
|      * @return 拼接后的list | ||||
|      */ | ||||
|     public static <E> String join(Collection<E> collection, Function<E, String> function) { | ||||
|         return join(collection, function, ","); | ||||
|         return join(collection, function, StringUtils.SEPARATOR); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -70,7 +70,7 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return CollUtil.newArrayList(); | ||||
|         } | ||||
|         return collection.stream().sorted(comparing).collect(Collectors.toList()); | ||||
|         return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -87,7 +87,7 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l)); | ||||
|         return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -106,7 +106,7 @@ public class StreamUtils { | ||||
|         if (CollUtil.isEmpty(collection)) { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l)); | ||||
|         return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -124,7 +124,7 @@ public class StreamUtils { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream() | ||||
|             .stream().filter(Objects::nonNull) | ||||
|             .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList())); | ||||
|     } | ||||
|  | ||||
| @@ -145,7 +145,7 @@ public class StreamUtils { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream() | ||||
|             .stream().filter(Objects::nonNull) | ||||
|             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList()))); | ||||
|     } | ||||
|  | ||||
| @@ -166,7 +166,7 @@ public class StreamUtils { | ||||
|             return MapUtil.newHashMap(); | ||||
|         } | ||||
|         return collection | ||||
|             .stream() | ||||
|             .stream().filter(Objects::nonNull) | ||||
|             .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l))); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,16 +1,16 @@ | ||||
| package com.ruoyi.common.utils; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.convert.Convert; | ||||
| import cn.hutool.core.lang.Validator; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
| import org.springframework.util.AntPathMatcher; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import java.util.*; | ||||
| import java.util.function.Function; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * 字符串工具类 | ||||
| @@ -20,6 +20,8 @@ import java.util.Set; | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|  | ||||
|     public static final String SEPARATOR = ","; | ||||
|  | ||||
|     /** | ||||
|      * 获取参数不为空值 | ||||
|      * | ||||
| @@ -224,7 +226,6 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|      * | ||||
|      * @param pattern 匹配规则 | ||||
|      * @param url     需要匹配的url | ||||
|      * @return | ||||
|      */ | ||||
|     public static boolean isMatch(String pattern, String url) { | ||||
|         AntPathMatcher matcher = new AntPathMatcher(); | ||||
| @@ -238,7 +239,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|      * @param size 字符串指定长度 | ||||
|      * @return 返回数字的字符串格式,该字符串为指定长度。 | ||||
|      */ | ||||
|     public static final String padl(final Number num, final int size) { | ||||
|     public static String padl(final Number num, final int size) { | ||||
|         return padl(num.toString(), size, '0'); | ||||
|     } | ||||
|  | ||||
| @@ -250,7 +251,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|      * @param c    用于补齐的字符 | ||||
|      * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 | ||||
|      */ | ||||
|     public static final String padl(final String s, final int size, final char c) { | ||||
|     public static String padl(final String s, final int size, final char c) { | ||||
|         final StringBuilder sb = new StringBuilder(size); | ||||
|         if (s != null) { | ||||
|             final int len = s.length(); | ||||
| @@ -270,4 +271,55 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { | ||||
|         return sb.toString(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 切分字符串(分隔符默认逗号) | ||||
|      * | ||||
|      * @param str 被切分的字符串 | ||||
|      * @return 分割后的数据列表 | ||||
|      */ | ||||
|     public static List<String> splitList(String str) { | ||||
|         return splitTo(str, Convert::toStr); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 切分字符串 | ||||
|      * | ||||
|      * @param str       被切分的字符串 | ||||
|      * @param separator 分隔符 | ||||
|      * @return 分割后的数据列表 | ||||
|      */ | ||||
|     public static List<String> splitList(String str, String separator) { | ||||
|         return splitTo(str, separator, Convert::toStr); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 切分字符串自定义转换(分隔符默认逗号) | ||||
|      * | ||||
|      * @param str    被切分的字符串 | ||||
|      * @param mapper 自定义转换 | ||||
|      * @return 分割后的数据列表 | ||||
|      */ | ||||
|     public static <T> List<T> splitTo(String str, Function<? super Object, T> mapper) { | ||||
|         return splitTo(str, SEPARATOR, mapper); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 切分字符串自定义转换 | ||||
|      * | ||||
|      * @param str       被切分的字符串 | ||||
|      * @param separator 分隔符 | ||||
|      * @param mapper    自定义转换 | ||||
|      * @return 分割后的数据列表 | ||||
|      */ | ||||
|     public static <T> List<T> splitTo(String str, String separator, Function<? super Object, T> mapper) { | ||||
|         if (isBlank(str)) { | ||||
|             return new ArrayList<>(0); | ||||
|         } | ||||
|         return StrUtil.split(str, separator) | ||||
|             .stream() | ||||
|             .filter(Objects::nonNull) | ||||
|             .map(mapper) | ||||
|             .collect(Collectors.toList()); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,7 @@ | ||||
| package com.ruoyi.common.utils.ip; | ||||
|  | ||||
| import cn.hutool.core.lang.Dict; | ||||
| import cn.hutool.core.net.NetUtil; | ||||
| import cn.hutool.http.HtmlUtil; | ||||
| import cn.hutool.http.HttpUtil; | ||||
| import com.ruoyi.common.config.RuoYiConfig; | ||||
| import com.ruoyi.common.constant.Constants; | ||||
| import com.ruoyi.common.utils.JsonUtils; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import lombok.AccessLevel; | ||||
| import lombok.NoArgsConstructor; | ||||
| @@ -21,40 +16,18 @@ import lombok.extern.slf4j.Slf4j; | ||||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||||
| public class AddressUtils { | ||||
|  | ||||
|     // IP地址查询 | ||||
|     public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp"; | ||||
|  | ||||
|     // 未知地址 | ||||
|     public static final String UNKNOWN = "XX XX"; | ||||
|  | ||||
|     public static String getRealAddressByIP(String ip) { | ||||
|         String address = UNKNOWN; | ||||
|         if (StringUtils.isBlank(ip)) { | ||||
|             return address; | ||||
|             return UNKNOWN; | ||||
|         } | ||||
|         // 内网不查询 | ||||
|         ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip); | ||||
|         ip = StringUtils.contains(ip, "0:0:0:0:0:0:0:1") ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip); | ||||
|         if (NetUtil.isInnerIP(ip)) { | ||||
|             return "内网IP"; | ||||
|         } | ||||
|         if (RuoYiConfig.isAddressEnabled()) { | ||||
|             try { | ||||
|                 String rspStr = HttpUtil.createGet(IP_URL) | ||||
|                     .body("ip=" + ip + "&json=true", Constants.GBK) | ||||
|                     .execute() | ||||
|                     .body(); | ||||
|                 if (StringUtils.isEmpty(rspStr)) { | ||||
|                     log.error("获取地理位置异常 {}", ip); | ||||
|                     return UNKNOWN; | ||||
|                 } | ||||
|                 Dict obj = JsonUtils.parseMap(rspStr); | ||||
|                 String region = obj.getStr("pro"); | ||||
|                 String city = obj.getStr("city"); | ||||
|                 return String.format("%s %s", region, city); | ||||
|             } catch (Exception e) { | ||||
|                 log.error("获取地理位置异常 {}", ip); | ||||
|             } | ||||
|         } | ||||
|         return UNKNOWN; | ||||
|         return RegionUtils.getCityInfo(ip); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,66 @@ | ||||
| package com.ruoyi.common.utils.ip; | ||||
|  | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| import cn.hutool.core.io.resource.ClassPathResource; | ||||
| import cn.hutool.core.util.ObjectUtil; | ||||
| import com.ruoyi.common.exception.ServiceException; | ||||
| import com.ruoyi.common.utils.file.FileUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.lionsoul.ip2region.xdb.Searcher; | ||||
|  | ||||
| import java.io.File; | ||||
|  | ||||
| /** | ||||
|  * 根据ip地址定位工具类,离线方式 | ||||
|  * 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a> | ||||
|  * | ||||
|  * @author lishuyan | ||||
|  */ | ||||
| @Slf4j | ||||
| public class RegionUtils { | ||||
|  | ||||
|     private static final Searcher SEARCHER; | ||||
|  | ||||
|     static { | ||||
|         String fileName = "/ip2region.xdb"; | ||||
|         File existFile = FileUtils.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName); | ||||
|         if (!FileUtils.exist(existFile)) { | ||||
|             ClassPathResource fileStream = new ClassPathResource(fileName); | ||||
|             if (ObjectUtil.isEmpty(fileStream.getStream())) { | ||||
|                 throw new ServiceException("RegionUtils初始化失败,原因:IP地址库数据不存在!"); | ||||
|             } | ||||
|             FileUtils.writeFromStream(fileStream.getStream(), existFile); | ||||
|         } | ||||
|  | ||||
|         String dbPath = existFile.getPath(); | ||||
|  | ||||
|         // 1、从 dbPath 加载整个 xdb 到内存。 | ||||
|         byte[] cBuff; | ||||
|         try { | ||||
|             cBuff = Searcher.loadContentFromFile(dbPath); | ||||
|         } catch (Exception e) { | ||||
|             throw new ServiceException("RegionUtils初始化失败,原因:从ip2region.xdb文件加载内容失败!" + e.getMessage()); | ||||
|         } | ||||
|         // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。 | ||||
|         try { | ||||
|             SEARCHER = Searcher.newWithBuffer(cBuff); | ||||
|         } catch (Exception e) { | ||||
|             throw new ServiceException("RegionUtils初始化失败,原因:" + e.getMessage()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 根据IP地址离线获取城市 | ||||
|      */ | ||||
|     public static String getCityInfo(String ip) { | ||||
|         try { | ||||
|             ip = ip.trim(); | ||||
|             // 3、执行查询 | ||||
|             String region = SEARCHER.search(ip); | ||||
|             return region.replace("0|", "").replace("|0", ""); | ||||
|         } catch (Exception e) { | ||||
|             log.error("IP地址离线获取城市异常 {}", ip); | ||||
|             return "未知"; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig; | ||||
| import com.alibaba.excel.write.metadata.fill.FillWrapper; | ||||
| import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; | ||||
| import com.ruoyi.common.convert.ExcelBigNumberConvert; | ||||
| import com.ruoyi.common.excel.CellMergeStrategy; | ||||
| import com.ruoyi.common.excel.DefaultExcelListener; | ||||
| import com.ruoyi.common.excel.ExcelListener; | ||||
| import com.ruoyi.common.excel.ExcelResult; | ||||
| import com.ruoyi.common.excel.*; | ||||
| import com.ruoyi.common.utils.StringUtils; | ||||
| import com.ruoyi.common.utils.file.FileUtils; | ||||
| import lombok.AccessLevel; | ||||
| @@ -88,7 +85,26 @@ public class ExcelUtil { | ||||
|         try { | ||||
|             resetResponse(sheetName, response); | ||||
|             ServletOutputStream os = response.getOutputStream(); | ||||
|             exportExcel(list, sheetName, clazz, false, os); | ||||
|             exportExcel(list, sheetName, clazz, false, os, null); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("导出Excel异常"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 导出excel | ||||
|      * | ||||
|      * @param list      导出数据集合 | ||||
|      * @param sheetName 工作表的名称 | ||||
|      * @param clazz     实体类 | ||||
|      * @param response  响应体 | ||||
|      * @param options   级联下拉选 | ||||
|      */ | ||||
|     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) { | ||||
|         try { | ||||
|             resetResponse(sheetName, response); | ||||
|             ServletOutputStream os = response.getOutputStream(); | ||||
|             exportExcel(list, sheetName, clazz, false, os, options); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("导出Excel异常"); | ||||
|         } | ||||
| @@ -107,7 +123,27 @@ public class ExcelUtil { | ||||
|         try { | ||||
|             resetResponse(sheetName, response); | ||||
|             ServletOutputStream os = response.getOutputStream(); | ||||
|             exportExcel(list, sheetName, clazz, merge, os); | ||||
|             exportExcel(list, sheetName, clazz, merge, os, null); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("导出Excel异常"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 导出excel | ||||
|      * | ||||
|      * @param list      导出数据集合 | ||||
|      * @param sheetName 工作表的名称 | ||||
|      * @param clazz     实体类 | ||||
|      * @param merge     是否合并单元格 | ||||
|      * @param response  响应体 | ||||
|      * @param options   级联下拉选 | ||||
|      */ | ||||
|     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) { | ||||
|         try { | ||||
|             resetResponse(sheetName, response); | ||||
|             ServletOutputStream os = response.getOutputStream(); | ||||
|             exportExcel(list, sheetName, clazz, merge, os, options); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("导出Excel异常"); | ||||
|         } | ||||
| @@ -122,7 +158,20 @@ public class ExcelUtil { | ||||
|      * @param os        输出流 | ||||
|      */ | ||||
|     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) { | ||||
|         exportExcel(list, sheetName, clazz, false, os); | ||||
|         exportExcel(list, sheetName, clazz, false, os, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 导出excel | ||||
|      * | ||||
|      * @param list      导出数据集合 | ||||
|      * @param sheetName 工作表的名称 | ||||
|      * @param clazz     实体类 | ||||
|      * @param os        输出流 | ||||
|      * @param options   级联下拉选内容 | ||||
|      */ | ||||
|     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) { | ||||
|         exportExcel(list, sheetName, clazz, false, os, options); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -134,7 +183,8 @@ public class ExcelUtil { | ||||
|      * @param merge     是否合并单元格 | ||||
|      * @param os        输出流 | ||||
|      */ | ||||
|     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) { | ||||
|     public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, | ||||
|                                        OutputStream os, List<DropDownOptions> options) { | ||||
|         ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz) | ||||
|             .autoCloseStream(false) | ||||
|             // 自动适配 | ||||
| @@ -146,6 +196,8 @@ public class ExcelUtil { | ||||
|             // 合并处理器 | ||||
|             builder.registerWriteHandler(new CellMergeStrategy(list, true)); | ||||
|         } | ||||
|         // 添加下拉框操作 | ||||
|         builder.registerWriteHandler(new ExcelDownHandler(options)); | ||||
|         builder.doWrite(list); | ||||
|     } | ||||
|  | ||||
| @@ -270,7 +322,7 @@ public class ExcelUtil { | ||||
|      */ | ||||
|     public static String convertByExp(String propertyValue, String converterExp, String separator) { | ||||
|         StringBuilder propertyString = new StringBuilder(); | ||||
|         String[] convertSource = converterExp.split(","); | ||||
|         String[] convertSource = converterExp.split(StringUtils.SEPARATOR); | ||||
|         for (String item : convertSource) { | ||||
|             String[] itemArray = item.split("="); | ||||
|             if (StringUtils.containsAny(propertyValue, separator)) { | ||||
| @@ -299,7 +351,7 @@ public class ExcelUtil { | ||||
|      */ | ||||
|     public static String reverseByExp(String propertyValue, String converterExp, String separator) { | ||||
|         StringBuilder propertyString = new StringBuilder(); | ||||
|         String[] convertSource = converterExp.split(","); | ||||
|         String[] convertSource = converterExp.split(StringUtils.SEPARATOR); | ||||
|         for (String item : convertSource) { | ||||
|             String[] itemArray = item.split("="); | ||||
|             if (StringUtils.containsAny(propertyValue, separator)) { | ||||
|   | ||||
| @@ -131,6 +131,32 @@ public class QueueUtils { | ||||
|         return priorityBlockingQueue.offer(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 优先队列获取一个队列数据 没有数据返回 null(不支持延迟队列) | ||||
|      * | ||||
|      * @param queueName 队列名 | ||||
|      */ | ||||
|     public static <T> T getPriorityQueueObject(String queueName) { | ||||
|         RPriorityBlockingQueue<T> queue = CLIENT.getPriorityBlockingQueue(queueName); | ||||
|         return queue.poll(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 优先队列删除队列数据(不支持延迟队列) | ||||
|      */ | ||||
|     public static <T> boolean removePriorityQueueObject(String queueName, T data) { | ||||
|         RPriorityBlockingQueue<T> queue = CLIENT.getPriorityBlockingQueue(queueName); | ||||
|         return queue.remove(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 优先队列销毁队列 所有阻塞监听 报错(不支持延迟队列) | ||||
|      */ | ||||
|     public static <T> boolean destroyPriorityQueue(String queueName) { | ||||
|         RPriorityBlockingQueue<T> queue = CLIENT.getPriorityBlockingQueue(queueName); | ||||
|         return queue.delete(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 尝试设置 有界队列 容量 用于限制数量 | ||||
|      * | ||||
| @@ -169,11 +195,41 @@ public class QueueUtils { | ||||
|         return boundedBlockingQueue.offer(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 有界队列获取一个队列数据 没有数据返回 null(不支持延迟队列) | ||||
|      * | ||||
|      * @param queueName 队列名 | ||||
|      */ | ||||
|     public static <T> T getBoundedQueueObject(String queueName) { | ||||
|         RBoundedBlockingQueue<T> queue = CLIENT.getBoundedBlockingQueue(queueName); | ||||
|         return queue.poll(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 有界队列删除队列数据(不支持延迟队列) | ||||
|      */ | ||||
|     public static <T> boolean removeBoundedQueueObject(String queueName, T data) { | ||||
|         RBoundedBlockingQueue<T> queue = CLIENT.getBoundedBlockingQueue(queueName); | ||||
|         return queue.remove(data); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 有界队列销毁队列 所有阻塞监听 报错(不支持延迟队列) | ||||
|      */ | ||||
|     public static <T> boolean destroyBoundedQueue(String queueName) { | ||||
|         RBoundedBlockingQueue<T> queue = CLIENT.getBoundedBlockingQueue(queueName); | ||||
|         return queue.delete(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 订阅阻塞队列(可订阅所有实现类 例如: 延迟 优先 有界 等) | ||||
|      */ | ||||
|     public static <T> void subscribeBlockingQueue(String queueName, Consumer<T> consumer) { | ||||
|     public static <T> void subscribeBlockingQueue(String queueName, Consumer<T> consumer, boolean isDelayed) { | ||||
|         RBlockingQueue<T> queue = CLIENT.getBlockingQueue(queueName); | ||||
|         if (isDelayed) { | ||||
|             // 订阅延迟队列 | ||||
|             CLIENT.getDelayedQueue(queue); | ||||
|         } | ||||
|         queue.subscribeOnElements(consumer); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -129,6 +129,18 @@ public class RedisUtils { | ||||
|         batch.execute(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 如果不存在则设置 并返回 true 如果存在则返回 false | ||||
|      * | ||||
|      * @param key   缓存的键值 | ||||
|      * @param value 缓存的值 | ||||
|      * @return set成功或失败 | ||||
|      */ | ||||
|     public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) { | ||||
|         RBucket<T> bucket = CLIENT.getBucket(key); | ||||
|         return bucket.setIfAbsent(value, duration); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 注册对象监听器 | ||||
|      * <p> | ||||
| @@ -374,6 +386,21 @@ public class RedisUtils { | ||||
|         return rMap.remove(hKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 删除Hash中的数据 | ||||
|      * | ||||
|      * @param key   Redis键 | ||||
|      * @param hKeys Hash键 | ||||
|      */ | ||||
|     public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) { | ||||
|         RBatch batch = CLIENT.createBatch(); | ||||
|         RMapAsync<String, T> rMap = batch.getMap(key); | ||||
|         for (String hKey : hKeys) { | ||||
|             rMap.removeAsync(hKey); | ||||
|         } | ||||
|         batch.execute(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取多个Hash中的数据 | ||||
|      * | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|     <parent> | ||||
|         <artifactId>ruoyi-vue-plus</artifactId> | ||||
|         <groupId>com.ruoyi</groupId> | ||||
|         <version>4.5.0</version> | ||||
|         <version>4.8.2</version> | ||||
|     </parent> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
|  | ||||
| @@ -28,17 +28,6 @@ | ||||
|             <artifactId>ruoyi-sms</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 短信 用哪个导入哪个依赖 --> | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>com.aliyun</groupId>--> | ||||
| <!--            <artifactId>dysmsapi20170525</artifactId>--> | ||||
| <!--        </dependency>--> | ||||
|  | ||||
| <!--        <dependency>--> | ||||
| <!--            <groupId>com.tencentcloudapi</groupId>--> | ||||
| <!--            <artifactId>tencentcloud-sdk-java-sms</artifactId>--> | ||||
| <!--        </dependency>--> | ||||
|  | ||||
|     </dependencies> | ||||
|  | ||||
| </project> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user