mirror of
				https://github.com/dromara/RuoYi-Vue-Plus.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	Compare commits
	
		
			295 Commits
		
	
	
		
			79ec850eca
			...
			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