mirror of
				https://github.com/dromara/RuoYi-Vue-Plus.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	Compare commits
	
		
			343 Commits
		
	
	
		
			1a12aecd49
			...
			5.X
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b58085fde1 | ||
| 
						 | 
					5ea8d8c950 | ||
| 
						 | 
					3318109044 | ||
| 
						 | 
					aa1f89e253 | ||
| 
						 | 
					35c77403d6 | ||
| 
						 | 
					603fb7b92d | ||
| 
						 | 
					6cf0c79433 | ||
| 
						 | 
					3934e119d6 | ||
| 
						 | 
					33a6a21fdf | ||
| 
						 | 
					7800b1259f | ||
| 
						 | 
					3623fc33d9 | ||
| 
						 | 
					f8612eb52e | ||
| 
						 | 
					8d32b0311a | ||
| 
						 | 
					60bcd2d6e9 | ||
| 
						 | 
					5ccb511064 | ||
| 
						 | 
					78baf6497a | ||
| 
						 | 
					0719e53f01 | ||
| 
						 | 
					5f2c4205a5 | ||
| 
						 | 
					2fe4c96706 | ||
| 
						 | 
					5c634940c2 | ||
| 
						 | 
					6036f8750b | ||
| 
						 | 
					dbcd8f58eb | ||
| 
						 | 
					8905e232e5 | ||
| 
						 | 
					4f15158486 | ||
| 
						 | 
					d2413abd5c | ||
| 
						 | 
					f7ffadeaff | ||
| 
						 | 
					f9eec856e7 | ||
| 
						 | 
					62562650fe | ||
| 
						 | 
					df171097c3 | ||
| 
						 | 
					1977aabc9a | ||
| 
						 | 
					483c4e6d0a | ||
| 
						 | 
					b30ffa952f | ||
| 
						 | 
					f616c6931c | ||
| 
						 | 
					26e10293f5 | ||
| 
						 | 
					60e578f763 | ||
| 
						 | 
					5cd4d8ca11 | ||
| 
						 | 
					41a6230b6e | ||
| 
						 | 
					effda4f6e8 | ||
| 
						 | 
					af4c38e439 | ||
| 
						 | 
					fafa8cd573 | ||
| 
						 | 
					8909b8a7d4 | ||
| 
						 | 
					8ae9bde731 | ||
| 
						 | 
					a703cb2ad1 | ||
| 
						 | 
					a918b880d6 | ||
| 
						 | 
					e795e315eb | ||
| 
						 | 
					81869cfeb3 | ||
| 
						 | 
					fc6f61bc95 | ||
| 
						 | 
					d44e45ad3b | ||
| 
						 | 
					00ed9ddd10 | ||
| 
						 | 
					341fc144a1 | ||
| 
						 | 
					b6b1b2de18 | ||
| 
						 | 
					c19f2b9e4e | ||
| 
						 | 
					3a11f18656 | ||
| 
						 | 
					5a43212ccc | ||
| 
						 | 
					f4cfd1c913 | ||
| 
						 | 
					26ce8f30c9 | ||
| 
						 | 
					2258962770 | ||
| 
						 | 
					655e84012c | ||
| 
						 | 
					f683ef00b8 | ||
| 
						 | 
					424b2ea164 | ||
| 
						 | 
					7bb4838132 | ||
| 
						 | 
					20516758ea | ||
| 
						 | 
					2d5f84ebc2 | ||
| 
						 | 
					6bc28e41de | ||
| 
						 | 
					a4fb3fadaf | ||
| 
						 | 
					cfa67fcd8c | ||
| 
						 | 
					e5e8d305d2 | ||
| 
						 | 
					9d0084409e | ||
| 
						 | 
					ee02f46dfd | ||
| 
						 | 
					25de0b3530 | ||
| 
						 | 
					aa76859a05 | ||
| 
						 | 
					71b70a59fe | ||
| 
						 | 
					05c9528549 | ||
| 
						 | 
					1feb2a3861 | ||
| 
						 | 
					237e78e80c | ||
| 
						 | 
					ffc3dcaec9 | ||
| 
						 | 
					a94e474069 | ||
| 
						 | 
					40a0e57870 | ||
| 
						 | 
					c01ed34602 | ||
| 
						 | 
					26a99003d2 | ||
| 
						 | 
					93c886d3ed | ||
| 
						 | 
					9e1027690b | ||
| 
						 | 
					cc120c06fd | ||
| 
						 | 
					3827da078a | ||
| 
						 | 
					70d3505b94 | ||
| 
						 | 
					a39a69cac5 | ||
| 
						 | 
					1dbce3ab7c | ||
| 
						 | 
					9742b1b596 | ||
| 
						 | 
					d98d11ae2d | ||
| 
						 | 
					6742dcb33e | ||
| 
						 | 
					09a51478a5 | ||
| 
						 | 
					f02601ab2c | ||
| 
						 | 
					ac56ca0e81 | ||
| 
						 | 
					0fcf77e2ed | ||
| 
						 | 
					0f0a3a181e | ||
| 
						 | 
					e24e2c51e4 | ||
| 
						 | 
					2b0dd82d3d | ||
| 
						 | 
					b97f711eb4 | ||
| 
						 | 
					0250ca4eb8 | ||
| 
						 | 
					23338995d7 | ||
| 
						 | 
					84fd02e7d8 | ||
| 
						 | 
					ae5bec994d | ||
| 
						 | 
					8f3a1b589e | ||
| 
						 | 
					ad6b3d4b3f | ||
| 
						 | 
					e2801037cf | ||
| 
						 | 
					65061f17fe | ||
| 
						 | 
					d0f4d93615 | ||
| 
						 | 
					5d69832423 | ||
| 
						 | 
					0c1e39ea14 | ||
| 
						 | 
					a39bc870d1 | ||
| 
						 | 
					7357912681 | ||
| 
						 | 
					901992674e | ||
| 
						 | 
					7ceb85ffa0 | ||
| 
						 | 
					4672d7de4d | ||
| 
						 | 
					87ab6e1744 | ||
| 
						 | 
					ae0a03728b | ||
| 
						 | 
					6fc82a59f1 | ||
| 
						 | 
					6c33fa48ec | ||
| 
						 | 
					343d5d21d8 | ||
| 
						 | 
					0ba909c52e | ||
| 
						 | 
					808ce9c25a | ||
| 
						 | 
					4351fc5239 | ||
| 
						 | 
					a545f7fc44 | ||
| 
						 | 
					acfcdf4d9a | ||
| 
						 | 
					9683252783 | ||
| 
						 | 
					49c00e162b | ||
| 
						 | 
					076a0a44fa | ||
| 
						 | 
					0d93589d99 | ||
| 
						 | 
					54a8189e27 | ||
| 
						 | 
					84f17011ad | ||
| 
						 | 
					f47bd39644 | ||
| 
						 | 
					89f9617ccb | ||
| 
						 | 
					bf10a13088 | ||
| 
						 | 
					f2e0361fb6 | ||
| 
						 | 
					554152635d | ||
| 
						 | 
					b379574637 | ||
| 
						 | 
					6a556cc6ff | ||
| 
						 | 
					a6950275ad | ||
| 
						 | 
					58b1bf5c33 | ||
| 
						 | 
					c85f693ca6 | ||
| 
						 | 
					5f466fd0c4 | ||
| 
						 | 
					127eaf936c | ||
| 
						 | 
					fcd8556076 | ||
| 
						 | 
					0512781513 | ||
| 
						 | 
					2472359adb | ||
| 
						 | 
					29d4bb4e59 | ||
| 
						 | 
					cce95424ce | ||
| 
						 | 
					8d7358e663 | ||
| 
						 | 
					240f10ab45 | ||
| 
						 | 
					48213bc9c9 | ||
| 
						 | 
					3995d9699d | ||
| 
						 | 
					ecd4e3eaf0 | ||
| 
						 | 
					2e3a42c669 | ||
| 
						 | 
					82997fc6cd | ||
| 
						 | 
					0dce571270 | ||
| 
						 | 
					9375578925 | ||
| 
						 | 
					e19ccf5064 | ||
| 
						 | 
					1cea7b72d7 | ||
| 
						 | 
					5da9ddf5e3 | ||
| 
						 | 
					93ee01c6b9 | ||
| 
						 | 
					acd30fda3c | ||
| 
						 | 
					3f62a76cc8 | ||
| 
						 | 
					b0b4e573f6 | ||
| 
						 | 
					de61899eed | ||
| 
						 | 
					3a9bdb36f1 | ||
| 
						 | 
					b815b8e574 | ||
| 
						 | 
					45edee4e63 | ||
| 
						 | 
					868bc492a2 | ||
| 
						 | 
					90fef1bb17 | ||
| 
						 | 
					d79b48ea99 | ||
| 
						 | 
					f6993a1491 | ||
| 
						 | 
					6b0b7382a6 | ||
| 
						 | 
					c41add355f | ||
| 
						 | 
					74e3d232f5 | ||
| 
						 | 
					03fca40c7d | ||
| 
						 | 
					b2ad257bd8 | ||
| 
						 | 
					7e4f0d73f4 | ||
| 
						 | 
					2ec802f17f | ||
| 
						 | 
					e0ce662c28 | ||
| 
						 | 
					3d9ed1b92f | ||
| 
						 | 
					d4e6e70c43 | ||
| 
						 | 
					2095a96e67 | ||
| 
						 | 
					328b61b252 | ||
| 
						 | 
					781463417c | ||
| 
						 | 
					446a14b928 | ||
| 
						 | 
					6c2518640b | ||
| 
						 | 
					d8d138092f | ||
| 
						 | 
					ec31b736c7 | ||
| 
						 | 
					e7467b2c5c | ||
| 
						 | 
					8050e2f1b1 | ||
| 
						 | 
					7de4559b4a | ||
| 
						 | 
					fc9c0d7657 | ||
| 
						 | 
					ab3037dc4f | ||
| 
						 | 
					8281b838b9 | ||
| 
						 | 
					2bc7171abd | ||
| 
						 | 
					4f99487d24 | ||
| 
						 | 
					a7cddc8d40 | ||
| 
						 | 
					f3c4c02d73 | ||
| 
						 | 
					eb631360f4 | ||
| 
						 | 
					a62bf04428 | ||
| 
						 | 
					7147f81b42 | ||
| 
						 | 
					c9098563ca | ||
| 
						 | 
					d4a8c25eab | ||
| 
						 | 
					0ddba506bf | ||
| 
						 | 
					d02bea85cb | ||
| 
						 | 
					d27c58bfe8 | ||
| 
						 | 
					34bb51f5c0 | ||
| 
						 | 
					64c37aaec6 | ||
| 
						 | 
					3de036adde | ||
| 
						 | 
					e0df8c15d8 | ||
| 
						 | 
					fd5d028e95 | ||
| 
						 | 
					176793e15b | ||
| 
						 | 
					589ec1fdbc | ||
| 
						 | 
					b421c8d017 | ||
| 
						 | 
					e2200bac71 | ||
| 
						 | 
					f8950d1e20 | ||
| 
						 | 
					9dfe9f610d | ||
| 
						 | 
					17610e8721 | ||
| 
						 | 
					f29b787767 | ||
| 
						 | 
					9775283a24 | ||
| 
						 | 
					debc73d7d4 | ||
| 
						 | 
					64100cf1ff | ||
| 
						 | 
					d501e82541 | ||
| 
						 | 
					3002585e63 | ||
| 
						 | 
					60aca2eef3 | ||
| 
						 | 
					5baf342478 | ||
| 
						 | 
					3f9919fbee | ||
| 
						 | 
					682d8b0099 | ||
| 
						 | 
					bbabffe191 | ||
| 
						 | 
					6722f2eeed | ||
| 
						 | 
					5a9728c868 | ||
| 
						 | 
					eea96e87d9 | ||
| 
						 | 
					e659740cb8 | ||
| 
						 | 
					314909a536 | ||
| 
						 | 
					8f5d60f543 | ||
| 
						 | 
					1ad0d5387b | ||
| 
						 | 
					770c3bd03e | ||
| 
						 | 
					4b04a4bf09 | ||
| 
						 | 
					1598447f6b | ||
| 
						 | 
					a8a1db4463 | ||
| 
						 | 
					4b47053dcf | ||
| 
						 | 
					03054fc1e8 | ||
| 
						 | 
					9dce540a09 | ||
| 
						 | 
					534182deff | ||
| 
						 | 
					4577c45110 | ||
| 
						 | 
					0796791ec9 | ||
| 
						 | 
					84baac0a4f | ||
| 
						 | 
					74d257a610 | ||
| 
						 | 
					cb8fa6ff9a | ||
| 
						 | 
					c157012807 | ||
| 
						 | 
					ffa01bdb3a | ||
| 
						 | 
					3fa572f0a8 | ||
| 
						 | 
					f868de1b7b | ||
| 
						 | 
					fb785dc17f | ||
| 
						 | 
					f3d475438f | ||
| 
						 | 
					d7af327248 | ||
| 
						 | 
					bd88e27c82 | ||
| 
						 | 
					83b6addbba | ||
| 
						 | 
					d51f3b9f4e | ||
| 
						 | 
					9256432532 | ||
| 
						 | 
					97d3a31aba | ||
| 
						 | 
					d9cc85187a | ||
| 
						 | 
					2ff2d89b2d | ||
| 
						 | 
					be2e5059fd | ||
| 
						 | 
					fad91f01ff | ||
| 
						 | 
					8f95374cef | ||
| 
						 | 
					7471fa7ee0 | ||
| 
						 | 
					529f1e5dbb | ||
| 
						 | 
					eff131a1ed | ||
| 
						 | 
					60b0faa3c6 | ||
| 
						 | 
					b2d694b90b | ||
| 
						 | 
					fecc564099 | ||
| 
						 | 
					297e920179 | ||
| 
						 | 
					ea9379a52f | ||
| 
						 | 
					0b0f2ee8ea | ||
| 
						 | 
					6d2f104a43 | ||
| 
						 | 
					7e7d857ba5 | ||
| 
						 | 
					2e50e30778 | ||
| 
						 | 
					daf79683b3 | ||
| 
						 | 
					5849ddc160 | ||
| 
						 | 
					d22b2a10df | ||
| 
						 | 
					957a4d1fcd | ||
| 
						 | 
					c88367939c | ||
| 
						 | 
					a748d0d62c | ||
| 
						 | 
					49ef8378fe | ||
| 
						 | 
					cd531f1d39 | ||
| 
						 | 
					92f73a4a72 | ||
| 
						 | 
					a4e3f7ea5e | ||
| 
						 | 
					26b4561a71 | ||
| 
						 | 
					dbe276a33b | ||
| 
						 | 
					4ab4e1685c | ||
| 
						 | 
					aab87d322c | ||
| 
						 | 
					79ee168293 | ||
| 
						 | 
					10e4b0618c | ||
| 
						 | 
					7c3316e116 | ||
| 
						 | 
					8460316632 | ||
| 
						 | 
					5d356aa6c4 | ||
| 
						 | 
					a776d28294 | ||
| 
						 | 
					a002a4e7a1 | ||
| 
						 | 
					79ec850eca | ||
| 
						 | 
					d1889c42a3 | ||
| 
						 | 
					a7ea096319 | ||
| 
						 | 
					4e3fc7002d | ||
| 
						 | 
					1752695751 | ||
| 
						 | 
					2b89c3f8d0 | ||
| 
						 | 
					6b387b2456 | ||
| 
						 | 
					ffc971cf92 | ||
| 
						 | 
					887d5e85d0 | ||
| 
						 | 
					8c603ff8d7 | ||
| 
						 | 
					a0831dda45 | ||
| 
						 | 
					336b2e8cc3 | ||
| 
						 | 
					cea4855f57 | ||
| 
						 | 
					9fc043b105 | ||
| 
						 | 
					d729c8ecde | ||
| 
						 | 
					b726a91cdb | ||
| 
						 | 
					05d5d9be2c | ||
| 
						 | 
					c40a8b2f0b | ||
| 
						 | 
					8232908b3f | ||
| 
						 | 
					1db0bc83b2 | ||
| 
						 | 
					74a0ec1ec3 | ||
| 
						 | 
					1228e8f3ea | ||
| 
						 | 
					737838d92f | ||
| 
						 | 
					c054029cfc | ||
| 
						 | 
					62bbd78033 | ||
| 
						 | 
					82a5ed632f | ||
| 
						 | 
					52ddccba3e | ||
| 
						 | 
					57dd6831d3 | ||
| 
						 | 
					8aa60abb1f | ||
| 
						 | 
					7a9f51fc7a | ||
| 
						 | 
					159e30c982 | ||
| 
						 | 
					7334d91d6b | ||
| 
						 | 
					95c01301f6 | ||
| 
						 | 
					296466fa13 | ||
| 
						 | 
					3c8d864b5f | ||
| 
						 | 
					ea50a57602 | ||
| 
						 | 
					7e14b98676 | ||
| 
						 | 
					015b406001 | ||
| 
						 | 
					098d3347a0 | ||
| 
						 | 
					08d4493994 | ||
| 
						 | 
					367d739e2d | ||
| 
						 | 
					d6688a367d | ||
| 
						 | 
					0b331796e2 | ||
| 
						 | 
					456620b638 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -38,6 +38,7 @@ nbdist/
 | 
			
		||||
######################################################################
 | 
			
		||||
# Others
 | 
			
		||||
*.log
 | 
			
		||||
*.log.gz
 | 
			
		||||
*.xml.versionsBackup
 | 
			
		||||
*.swp
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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:5.3.1" />
 | 
			
		||||
        <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.1" />
 | 
			
		||||
        <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:5.3.1" />
 | 
			
		||||
        <option name="imageTag" value="ruoyi/ruoyi-server:5.5.1" />
 | 
			
		||||
        <option name="buildOnly" value="true" />
 | 
			
		||||
        <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
 | 
			
		||||
      </settings>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  <configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
 | 
			
		||||
    <deployment type="dockerfile">
 | 
			
		||||
      <settings>
 | 
			
		||||
        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.3.1" />
 | 
			
		||||
        <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.1" />
 | 
			
		||||
        <option name="buildOnly" value="true" />
 | 
			
		||||
        <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
 | 
			
		||||
      </settings>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							@@ -10,7 +10,7 @@
 | 
			
		||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
 | 
			
		||||
[](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
 | 
			
		||||
<br>
 | 
			
		||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
 | 
			
		||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
 | 
			
		||||
[]()
 | 
			
		||||
[]()
 | 
			
		||||
[]()
 | 
			
		||||
@@ -23,10 +23,11 @@
 | 
			
		||||
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
 | 
			
		||||
 | 
			
		||||
> 官方前端项目地址: [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/JavaLionLi/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
 | 
			
		||||
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
 | 
			
		||||
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
 | 
			
		||||
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)<br>
 | 
			
		||||
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
 | 
			
		||||
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
 | 
			
		||||
 | 
			
		||||
> 文档地址: [plus-doc](https://plus-doc.dromara.org) 文档在华为云上如果打不开大概率是DNS问题 可以尝试切换网络等方式(或者科学上网)
 | 
			
		||||
> 文档地址: [plus-doc](https://plus-doc.dromara.org) 国内加速: [plus-doc.top](https://plus-doc.top)
 | 
			
		||||
 | 
			
		||||
## 赞助商
 | 
			
		||||
 | 
			
		||||
@@ -36,7 +37,10 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
 | 
			
		||||
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
 | 
			
		||||
<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
 | 
			
		||||
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
 | 
			
		||||
[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
 | 
			
		||||
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
 | 
			
		||||
Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
 | 
			
		||||
 | 
			
		||||
[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group)
 | 
			
		||||
 | 
			
		||||
# 本框架与RuoYi的功能差异
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										77
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								pom.xml
									
									
									
									
									
								
							@@ -13,49 +13,48 @@
 | 
			
		||||
    <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <revision>5.3.1</revision>
 | 
			
		||||
        <spring-boot.version>3.4.5</spring-boot.version>
 | 
			
		||||
        <revision>5.5.1</revision>
 | 
			
		||||
        <spring-boot.version>3.5.7</spring-boot.version>
 | 
			
		||||
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
			
		||||
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 | 
			
		||||
        <java.version>17</java.version>
 | 
			
		||||
        <mybatis.version>3.5.16</mybatis.version>
 | 
			
		||||
        <springdoc.version>2.8.5</springdoc.version>
 | 
			
		||||
        <springdoc.version>2.8.13</springdoc.version>
 | 
			
		||||
        <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
 | 
			
		||||
        <fastexcel.version>1.2.0</fastexcel.version>
 | 
			
		||||
        <fastexcel.version>1.3.0</fastexcel.version>
 | 
			
		||||
        <velocity.version>2.3</velocity.version>
 | 
			
		||||
        <satoken.version>1.42.0</satoken.version>
 | 
			
		||||
        <mybatis-plus.version>3.5.11</mybatis-plus.version>
 | 
			
		||||
        <satoken.version>1.44.0</satoken.version>
 | 
			
		||||
        <mybatis-plus.version>3.5.14</mybatis-plus.version>
 | 
			
		||||
        <p6spy.version>3.9.1</p6spy.version>
 | 
			
		||||
        <hutool.version>5.8.35</hutool.version>
 | 
			
		||||
        <spring-boot-admin.version>3.4.5</spring-boot-admin.version>
 | 
			
		||||
        <redisson.version>3.45.1</redisson.version>
 | 
			
		||||
        <hutool.version>5.8.40</hutool.version>
 | 
			
		||||
        <spring-boot-admin.version>3.5.5</spring-boot-admin.version>
 | 
			
		||||
        <redisson.version>3.51.0</redisson.version>
 | 
			
		||||
        <lock4j.version>2.2.7</lock4j.version>
 | 
			
		||||
        <dynamic-ds.version>4.3.1</dynamic-ds.version>
 | 
			
		||||
        <snailjob.version>1.4.0</snailjob.version>
 | 
			
		||||
        <mapstruct-plus.version>1.4.6</mapstruct-plus.version>
 | 
			
		||||
        <snailjob.version>1.8.0</snailjob.version>
 | 
			
		||||
        <mapstruct-plus.version>1.5.0</mapstruct-plus.version>
 | 
			
		||||
        <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
 | 
			
		||||
        <lombok.version>1.18.36</lombok.version>
 | 
			
		||||
        <lombok.version>1.18.40</lombok.version>
 | 
			
		||||
        <bouncycastle.version>1.80</bouncycastle.version>
 | 
			
		||||
        <justauth.version>1.16.7</justauth.version>
 | 
			
		||||
        <!-- 离线IP地址定位库 -->
 | 
			
		||||
        <ip2region.version>2.7.0</ip2region.version>
 | 
			
		||||
 | 
			
		||||
        <!-- OSS 配置 -->
 | 
			
		||||
        <aws.sdk.version>2.28.22</aws.sdk.version>
 | 
			
		||||
        <!-- SMS 配置 -->
 | 
			
		||||
        <sms4j.version>3.3.4</sms4j.version>
 | 
			
		||||
        <sms4j.version>3.3.5</sms4j.version>
 | 
			
		||||
        <!-- 限制框架中的fastjson版本 -->
 | 
			
		||||
        <fastjson.version>1.2.83</fastjson.version>
 | 
			
		||||
        <!-- 面向运行时的D-ORM依赖 -->
 | 
			
		||||
        <anyline.version>8.7.2-20250101</anyline.version>
 | 
			
		||||
        <anyline.version>8.7.2-20250603</anyline.version>
 | 
			
		||||
        <!-- 工作流配置 -->
 | 
			
		||||
        <warm-flow.version>1.7.2</warm-flow.version>
 | 
			
		||||
        <warm-flow.version>1.8.2</warm-flow.version>
 | 
			
		||||
 | 
			
		||||
        <!-- 插件版本 -->
 | 
			
		||||
        <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
 | 
			
		||||
        <maven-war-plugin.version>3.2.2</maven-war-plugin.version>
 | 
			
		||||
        <maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
 | 
			
		||||
        <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
 | 
			
		||||
        <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
 | 
			
		||||
        <maven-war-plugin.version>3.4.0</maven-war-plugin.version>
 | 
			
		||||
        <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
 | 
			
		||||
        <maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
 | 
			
		||||
        <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
 | 
			
		||||
        <!-- 打包默认跳过测试 -->
 | 
			
		||||
        <skipTests>true</skipTests>
 | 
			
		||||
@@ -119,25 +118,6 @@
 | 
			
		||||
                <scope>import</scope>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.dromara.warm</groupId>
 | 
			
		||||
                <artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
 | 
			
		||||
                <version>${warm-flow.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.dromara.warm</groupId>
 | 
			
		||||
                <artifactId>warm-flow-plugin-ui-sb-web</artifactId>
 | 
			
		||||
                <version>${warm-flow.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- JustAuth 的依赖配置-->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>me.zhyd.oauth</groupId>
 | 
			
		||||
                <artifactId>JustAuth</artifactId>
 | 
			
		||||
                <version>${justauth.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- common 的依赖配置-->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.dromara</groupId>
 | 
			
		||||
@@ -314,6 +294,25 @@
 | 
			
		||||
                <version>${mapstruct-plus.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- Warm-Flow国产工作流引擎, 在线文档:http://warm-flow.cn/ -->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.dromara.warm</groupId>
 | 
			
		||||
                <artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
 | 
			
		||||
                <version>${warm-flow.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.dromara.warm</groupId>
 | 
			
		||||
                <artifactId>warm-flow-plugin-ui-sb-web</artifactId>
 | 
			
		||||
                <version>${warm-flow.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- JustAuth 的依赖配置-->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>me.zhyd.oauth</groupId>
 | 
			
		||||
                <artifactId>JustAuth</artifactId>
 | 
			
		||||
                <version>${justauth.version}</version>
 | 
			
		||||
            </dependency>
 | 
			
		||||
 | 
			
		||||
            <!-- 离线IP地址定位库 ip2region -->
 | 
			
		||||
            <dependency>
 | 
			
		||||
                <groupId>org.lionsoul</groupId>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
 | 
			
		||||
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
 | 
			
		||||
#FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
 | 
			
		||||
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
 | 
			
		||||
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
 | 
			
		||||
#FROM findepi/graalvm:java17-native
 | 
			
		||||
 | 
			
		||||
LABEL maintainer="Lion Li"
 | 
			
		||||
@@ -18,8 +18,6 @@ EXPOSE ${SERVER_PORT}
 | 
			
		||||
EXPOSE ${SNAIL_PORT}
 | 
			
		||||
 | 
			
		||||
ADD ./target/ruoyi-admin.jar ./app.jar
 | 
			
		||||
# 工作流字体文件
 | 
			
		||||
ADD ./zhFonts/ /usr/share/fonts/zhFonts/
 | 
			
		||||
 | 
			
		||||
SHELL ["/bin/bash", "-c"]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,8 @@ import org.dromara.common.core.domain.model.SocialLoginBody;
 | 
			
		||||
import org.dromara.common.core.utils.*;
 | 
			
		||||
import org.dromara.common.encrypt.annotation.ApiEncrypt;
 | 
			
		||||
import org.dromara.common.json.utils.JsonUtils;
 | 
			
		||||
import org.dromara.common.ratelimiter.annotation.RateLimiter;
 | 
			
		||||
import org.dromara.common.ratelimiter.enums.LimitType;
 | 
			
		||||
import org.dromara.common.satoken.utils.LoginHelper;
 | 
			
		||||
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
 | 
			
		||||
import org.dromara.common.social.config.properties.SocialProperties;
 | 
			
		||||
@@ -198,6 +200,7 @@ public class AuthController {
 | 
			
		||||
     *
 | 
			
		||||
     * @return 租户列表
 | 
			
		||||
     */
 | 
			
		||||
    @RateLimiter(time = 60, count = 20, limitType = LimitType.IP)
 | 
			
		||||
    @GetMapping("/tenant/list")
 | 
			
		||||
    public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
 | 
			
		||||
        // 返回对象
 | 
			
		||||
 
 | 
			
		||||
@@ -131,15 +131,18 @@ public class CaptchaController {
 | 
			
		||||
        String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
 | 
			
		||||
        // 生成验证码
 | 
			
		||||
        CaptchaType captchaType = captchaProperties.getType();
 | 
			
		||||
        boolean isMath = CaptchaType.MATH == captchaType;
 | 
			
		||||
        Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
 | 
			
		||||
        CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
 | 
			
		||||
        CodeGenerator codeGenerator;
 | 
			
		||||
        if (CaptchaType.MATH == captchaType) {
 | 
			
		||||
            codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false);
 | 
			
		||||
        } else {
 | 
			
		||||
            codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength());
 | 
			
		||||
        }
 | 
			
		||||
        AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
 | 
			
		||||
        captcha.setGenerator(codeGenerator);
 | 
			
		||||
        captcha.createCode();
 | 
			
		||||
        // 如果是数学验证码,使用SpEL表达式处理验证码结果
 | 
			
		||||
        String code = captcha.getCode();
 | 
			
		||||
        if (isMath) {
 | 
			
		||||
        if (CaptchaType.MATH == captchaType) {
 | 
			
		||||
            ExpressionParser parser = new SpelExpressionParser();
 | 
			
		||||
            Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
 | 
			
		||||
            code = exp.getValue(String.class);
 | 
			
		||||
 
 | 
			
		||||
@@ -158,6 +158,6 @@ public class UserActionListener implements SaTokenListener {
 | 
			
		||||
     * 每次Token续期时触发
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
 | 
			
		||||
    public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ public class SysRegisterService {
 | 
			
		||||
            recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
 | 
			
		||||
            throw new CaptchaExpireException();
 | 
			
		||||
        }
 | 
			
		||||
        if (!code.equalsIgnoreCase(captcha)) {
 | 
			
		||||
        if (!StringUtils.equalsIgnoreCase(code, captcha)) {
 | 
			
		||||
            recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
 | 
			
		||||
            throw new CaptchaException();
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
 | 
			
		||||
            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
 | 
			
		||||
            throw new CaptchaExpireException();
 | 
			
		||||
        }
 | 
			
		||||
        if (!code.equalsIgnoreCase(captcha)) {
 | 
			
		||||
        if (!StringUtils.equalsIgnoreCase(code, captcha)) {
 | 
			
		||||
            loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
 | 
			
		||||
            throw new CaptchaException();
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,7 @@ package org.dromara.web.service.impl;
 | 
			
		||||
import cn.dev33.satoken.stp.StpUtil;
 | 
			
		||||
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.map.MapUtil;
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import cn.hutool.http.HttpUtil;
 | 
			
		||||
import cn.hutool.http.Method;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import me.zhyd.oauth.model.AuthResponse;
 | 
			
		||||
@@ -68,15 +65,6 @@ public class SocialAuthStrategy implements IAuthStrategy {
 | 
			
		||||
            throw new ServiceException(response.getMsg());
 | 
			
		||||
        }
 | 
			
		||||
        AuthUser authUserData = response.getData();
 | 
			
		||||
        if ("GITEE".equals(authUserData.getSource())) {
 | 
			
		||||
            // 如用户使用 gitee 登录顺手 star 给作者一点支持 拒绝白嫖
 | 
			
		||||
            HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
 | 
			
		||||
                    .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
 | 
			
		||||
                    .executeAsync();
 | 
			
		||||
            HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Cloud-Plus")
 | 
			
		||||
                    .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
 | 
			
		||||
                    .executeAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
 | 
			
		||||
        if (CollUtil.isEmpty(list)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,6 @@ snail-job:
 | 
			
		||||
  port: 2${server.port}
 | 
			
		||||
  # 客户端ip指定
 | 
			
		||||
  host:
 | 
			
		||||
  # RPC类型: netty, grpc
 | 
			
		||||
  rpc-type: grpc
 | 
			
		||||
 | 
			
		||||
--- # 数据源配置
 | 
			
		||||
spring:
 | 
			
		||||
 
 | 
			
		||||
@@ -30,8 +30,6 @@ snail-job:
 | 
			
		||||
  port: 2${server.port}
 | 
			
		||||
  # 客户端ip指定
 | 
			
		||||
  host:
 | 
			
		||||
  # RPC类型: netty, grpc
 | 
			
		||||
  rpc-type: grpc
 | 
			
		||||
 | 
			
		||||
--- # 数据源配置
 | 
			
		||||
spring:
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,13 @@ spring:
 | 
			
		||||
    # 开启虚拟线程 仅jdk21可用
 | 
			
		||||
    virtual:
 | 
			
		||||
      enabled: false
 | 
			
		||||
  task:
 | 
			
		||||
    execution:
 | 
			
		||||
      # 从 springboot 3.5 开始 spring自带线程池
 | 
			
		||||
      # 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
 | 
			
		||||
      thread-name-prefix: async-
 | 
			
		||||
      # 由spring自己初始化线程池
 | 
			
		||||
      mode: force
 | 
			
		||||
  # 资源信息
 | 
			
		||||
  messages:
 | 
			
		||||
    # 国际化资源文件路径
 | 
			
		||||
@@ -110,7 +117,7 @@ security:
 | 
			
		||||
    - /error
 | 
			
		||||
    - /*/api-docs
 | 
			
		||||
    - /*/api-docs/**
 | 
			
		||||
    - /warm-flow-ui/token-name
 | 
			
		||||
    - /warm-flow-ui/config
 | 
			
		||||
 | 
			
		||||
# 多租户配置
 | 
			
		||||
tenant:
 | 
			
		||||
@@ -127,6 +134,7 @@ tenant:
 | 
			
		||||
    - sys_user_role
 | 
			
		||||
    - sys_client
 | 
			
		||||
    - sys_oss_config
 | 
			
		||||
    - flow_spel
 | 
			
		||||
 | 
			
		||||
# MyBatisPlus配置
 | 
			
		||||
# https://baomidou.com/config/
 | 
			
		||||
@@ -183,19 +191,12 @@ springdoc:
 | 
			
		||||
    # 描述
 | 
			
		||||
    description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
 | 
			
		||||
    # 版本
 | 
			
		||||
    version: '版本号: ${ruoyi.version}'
 | 
			
		||||
    version: '版本号: ${project.version}'
 | 
			
		||||
    # 作者信息
 | 
			
		||||
    contact:
 | 
			
		||||
      name: Lion Li
 | 
			
		||||
      email: crazylionli@163.com
 | 
			
		||||
      url: https://gitee.com/dromara/RuoYi-Vue-Plus
 | 
			
		||||
  components:
 | 
			
		||||
    # 鉴权方式配置
 | 
			
		||||
    security-schemes:
 | 
			
		||||
      apiKey:
 | 
			
		||||
        type: APIKEY
 | 
			
		||||
        in: HEADER
 | 
			
		||||
        name: ${sa-token.token-name}
 | 
			
		||||
  #这里定义了两个分组,可定义多个,也可以不定义
 | 
			
		||||
  group-configs:
 | 
			
		||||
    - group: 1.演示模块
 | 
			
		||||
@@ -213,20 +214,10 @@ springdoc:
 | 
			
		||||
xss:
 | 
			
		||||
  # 过滤开关
 | 
			
		||||
  enabled: true
 | 
			
		||||
  # 排除链接(多个用逗号分隔)
 | 
			
		||||
  # 排除链接
 | 
			
		||||
  excludeUrls:
 | 
			
		||||
    - /system/notice
 | 
			
		||||
 | 
			
		||||
# 全局线程池相关配置
 | 
			
		||||
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
 | 
			
		||||
thread-pool:
 | 
			
		||||
  # 是否开启线程池
 | 
			
		||||
  enabled: false
 | 
			
		||||
  # 队列最大长度
 | 
			
		||||
  queueCapacity: 128
 | 
			
		||||
  # 线程池维护线程所允许的空闲时间
 | 
			
		||||
  keepAliveSeconds: 300
 | 
			
		||||
 | 
			
		||||
--- # 分布式锁 lock4j 全局配置
 | 
			
		||||
lock4j:
 | 
			
		||||
  # 获取分布式锁超时时间,默认为 3000 毫秒
 | 
			
		||||
@@ -266,13 +257,9 @@ warm-flow:
 | 
			
		||||
  enabled: true
 | 
			
		||||
  # 是否开启设计器ui
 | 
			
		||||
  ui: true
 | 
			
		||||
  # 是否显示流程图顶部文字
 | 
			
		||||
  top-text-show: true
 | 
			
		||||
  # 是否渲染节点悬浮提示,默认true
 | 
			
		||||
  node-tooltip: true
 | 
			
		||||
  # 默认Authorization,如果有多个token,用逗号分隔
 | 
			
		||||
  token-name: ${sa-token.token-name},clientid
 | 
			
		||||
  # 流程状态对应的三元色
 | 
			
		||||
  chart-status-color:
 | 
			
		||||
    ## 未办理
 | 
			
		||||
    - 62,62,62
 | 
			
		||||
    ## 待办理
 | 
			
		||||
    - 255,205,23
 | 
			
		||||
    ## 已办理
 | 
			
		||||
    - 157,255,0
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
 | 
			
		||||
user.password.not.blank=用户密码不能为空
 | 
			
		||||
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 | 
			
		||||
user.password.not.valid=* 5-50个字符
 | 
			
		||||
user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
 | 
			
		||||
user.email.not.valid=邮箱格式错误
 | 
			
		||||
user.email.not.blank=邮箱不能为空
 | 
			
		||||
user.phonenumber.not.blank=用户手机号不能为空
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ user.username.length.valid=Account length must be between {min} and {max} charac
 | 
			
		||||
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.password.format.valid=Password must contain uppercase, lowercase, digit, and special character
 | 
			
		||||
user.email.not.valid=Mailbox format error
 | 
			
		||||
user.email.not.blank=Mailbox cannot be blank
 | 
			
		||||
user.phonenumber.not.blank=Phone number cannot be blank
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
 | 
			
		||||
user.password.not.blank=用户密码不能为空
 | 
			
		||||
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
 | 
			
		||||
user.password.not.valid=* 5-50个字符
 | 
			
		||||
user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
 | 
			
		||||
user.email.not.valid=邮箱格式错误
 | 
			
		||||
user.email.not.blank=邮箱不能为空
 | 
			
		||||
user.phonenumber.not.blank=用户手机号不能为空
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -38,7 +38,7 @@
 | 
			
		||||
        <!-- 循环政策:基于时间创建日志文件 -->
 | 
			
		||||
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
 | 
			
		||||
            <!-- 日志文件名格式 -->
 | 
			
		||||
            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
 | 
			
		||||
            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
 | 
			
		||||
            <!-- 日志最大的历史 60天 -->
 | 
			
		||||
            <maxHistory>60</maxHistory>
 | 
			
		||||
        </rollingPolicy>
 | 
			
		||||
@@ -60,7 +60,7 @@
 | 
			
		||||
        <!-- 循环政策:基于时间创建日志文件 -->
 | 
			
		||||
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
 | 
			
		||||
            <!-- 日志文件名格式 -->
 | 
			
		||||
            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
 | 
			
		||||
            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
 | 
			
		||||
            <!-- 日志最大的历史 60天 -->
 | 
			
		||||
            <maxHistory>60</maxHistory>
 | 
			
		||||
        </rollingPolicy>
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
3f2ee348-0303-40ca-bf03-03f48d2d2141
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -1,4 +0,0 @@
 | 
			
		||||
3
 | 
			
		||||
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
 | 
			
		||||
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
 | 
			
		||||
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
3
 | 
			
		||||
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
 | 
			
		||||
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
 | 
			
		||||
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
    </description>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <revision>5.3.1</revision>
 | 
			
		||||
        <revision>5.5.1</revision>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <dependencyManagement>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
package org.dromara.common.core.config;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.ArrayUtil;
 | 
			
		||||
import org.dromara.common.core.exception.ServiceException;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
 | 
			
		||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
 | 
			
		||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
 | 
			
		||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
 | 
			
		||||
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 异步配置
 | 
			
		||||
 * <p>
 | 
			
		||||
 * 如果未使用虚拟线程则生效
 | 
			
		||||
 *
 | 
			
		||||
 * @author Lion Li
 | 
			
		||||
 */
 | 
			
		||||
@AutoConfiguration
 | 
			
		||||
public class AsyncConfig implements AsyncConfigurer {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义 @Async 注解使用系统线程池
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public Executor getAsyncExecutor() {
 | 
			
		||||
        if(SpringUtils.isVirtual()) {
 | 
			
		||||
            return new VirtualThreadTaskExecutor("async-");
 | 
			
		||||
        }
 | 
			
		||||
        return SpringUtils.getBean("scheduledExecutorService");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 异步执行异常处理
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
 | 
			
		||||
        return (throwable, method, objects) -> {
 | 
			
		||||
            throwable.printStackTrace();
 | 
			
		||||
            StringBuilder sb = new StringBuilder();
 | 
			
		||||
            sb.append("Exception message - ").append(throwable.getMessage())
 | 
			
		||||
                .append(", Method name - ").append(method.getName());
 | 
			
		||||
            if (ArrayUtil.isNotEmpty(objects)) {
 | 
			
		||||
                sb.append(", Parameter value - ").append(Arrays.toString(objects));
 | 
			
		||||
            }
 | 
			
		||||
            throw new ServiceException(sb.toString());
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -5,17 +5,12 @@ import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
 | 
			
		||||
import org.dromara.common.core.config.properties.ThreadPoolProperties;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.dromara.common.core.utils.Threads;
 | 
			
		||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
 | 
			
		||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.ScheduledExecutorService;
 | 
			
		||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
 | 
			
		||||
import java.util.concurrent.ThreadPoolExecutor;
 | 
			
		||||
import java.util.concurrent.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 线程池配置
 | 
			
		||||
@@ -34,18 +29,6 @@ public class ThreadPoolConfig {
 | 
			
		||||
 | 
			
		||||
    private ScheduledExecutorService scheduledExecutorService;
 | 
			
		||||
 | 
			
		||||
    @Bean(name = "threadPoolTaskExecutor")
 | 
			
		||||
    @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
 | 
			
		||||
    public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
 | 
			
		||||
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
 | 
			
		||||
        executor.setCorePoolSize(core);
 | 
			
		||||
        executor.setMaxPoolSize(core * 2);
 | 
			
		||||
        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
 | 
			
		||||
        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
 | 
			
		||||
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
 | 
			
		||||
        return executor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 执行周期性或定时任务
 | 
			
		||||
     */
 | 
			
		||||
@@ -64,7 +47,7 @@ public class ThreadPoolConfig {
 | 
			
		||||
            @Override
 | 
			
		||||
            protected void afterExecute(Runnable r, Throwable t) {
 | 
			
		||||
                super.afterExecute(r, t);
 | 
			
		||||
                Threads.printException(r, t);
 | 
			
		||||
                printException(r, t);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        this.scheduledExecutorService = scheduledThreadPoolExecutor;
 | 
			
		||||
@@ -73,15 +56,57 @@ public class ThreadPoolConfig {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 销毁事件
 | 
			
		||||
     * 停止线程池
 | 
			
		||||
     * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
 | 
			
		||||
     * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
 | 
			
		||||
     * 如果仍然超時,則強制退出.
 | 
			
		||||
     * 另对在shutdown时线程本身被调用中断做了处理.
 | 
			
		||||
     */
 | 
			
		||||
    @PreDestroy
 | 
			
		||||
    public void destroy() {
 | 
			
		||||
        try {
 | 
			
		||||
            log.info("====关闭后台任务任务线程池====");
 | 
			
		||||
            Threads.shutdownAndAwaitTermination(scheduledExecutorService);
 | 
			
		||||
            ScheduledExecutorService pool = scheduledExecutorService;
 | 
			
		||||
            if (pool != null && !pool.isShutdown()) {
 | 
			
		||||
                pool.shutdown();
 | 
			
		||||
                try {
 | 
			
		||||
                    if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
 | 
			
		||||
                        pool.shutdownNow();
 | 
			
		||||
                        if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
 | 
			
		||||
                            log.info("Pool did not terminate");
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (InterruptedException ie) {
 | 
			
		||||
                    pool.shutdownNow();
 | 
			
		||||
                    Thread.currentThread().interrupt();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.error(e.getMessage(), e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 打印线程异常信息
 | 
			
		||||
     */
 | 
			
		||||
    public static void printException(Runnable r, Throwable t) {
 | 
			
		||||
        if (t == null && r instanceof Future<?>) {
 | 
			
		||||
            try {
 | 
			
		||||
                Future<?> future = (Future<?>) r;
 | 
			
		||||
                if (future.isDone()) {
 | 
			
		||||
                    future.get();
 | 
			
		||||
                }
 | 
			
		||||
            } catch (CancellationException ce) {
 | 
			
		||||
                t = ce;
 | 
			
		||||
            } catch (ExecutionException ee) {
 | 
			
		||||
                t = ee.getCause();
 | 
			
		||||
            } catch (InterruptedException ie) {
 | 
			
		||||
                Thread.currentThread().interrupt();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (t != null) {
 | 
			
		||||
            log.error(t.getMessage(), t);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -72,5 +72,10 @@ public interface Constants {
 | 
			
		||||
     */
 | 
			
		||||
    Long TOP_PARENT_ID = 0L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 加密头
 | 
			
		||||
     */
 | 
			
		||||
    String ENCRYPT_HEADER = "ENC_";
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -77,4 +77,9 @@ public interface SystemConstants {
 | 
			
		||||
     */
 | 
			
		||||
    String ROOT_DEPT_ANCESTORS = "0";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认部门 ID
 | 
			
		||||
     */
 | 
			
		||||
    Long DEFAULT_DEPT_ID = 100L;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,11 @@ public class CompleteTaskDTO implements Serializable {
 | 
			
		||||
     */
 | 
			
		||||
    private String notice;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 办理人(可不填 用于覆盖当前节点办理人)
 | 
			
		||||
     */
 | 
			
		||||
    private String handler;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程变量
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
package org.dromara.common.core.domain.dto;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 流程实例业务扩展对象
 | 
			
		||||
 *
 | 
			
		||||
 * @author may
 | 
			
		||||
 * @date 2025-08-05
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class FlowInstanceBizExtDTO implements Serializable {
 | 
			
		||||
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = 1L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 主键
 | 
			
		||||
     */
 | 
			
		||||
    private Long id;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程实例ID
 | 
			
		||||
     */
 | 
			
		||||
    private Long instanceId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 业务ID
 | 
			
		||||
     */
 | 
			
		||||
    private String businessId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 业务编码
 | 
			
		||||
     */
 | 
			
		||||
    private String businessCode;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 业务标题
 | 
			
		||||
     */
 | 
			
		||||
    private String businessTitle;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package org.dromara.common.core.domain.dto;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
@@ -30,11 +31,21 @@ public class StartProcessDTO implements Serializable {
 | 
			
		||||
     */
 | 
			
		||||
    private String flowCode;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 办理人(可不填 用于覆盖当前节点办理人)
 | 
			
		||||
     */
 | 
			
		||||
    private String handler;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
 | 
			
		||||
     */
 | 
			
		||||
    private Map<String, Object> variables;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程业务扩展信息
 | 
			
		||||
     */
 | 
			
		||||
    private FlowInstanceBizExtDTO bizExt;
 | 
			
		||||
 | 
			
		||||
    public Map<String, Object> getVariables() {
 | 
			
		||||
        if (variables == null) {
 | 
			
		||||
            return new HashMap<>(16);
 | 
			
		||||
@@ -42,4 +53,11 @@ public class StartProcessDTO implements Serializable {
 | 
			
		||||
        variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
 | 
			
		||||
        return variables;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FlowInstanceBizExtDTO getBizExt() {
 | 
			
		||||
        if (ObjectUtil.isNull(bizExt)) {
 | 
			
		||||
            bizExt = new FlowInstanceBizExtDTO();
 | 
			
		||||
        }
 | 
			
		||||
        return bizExt;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,17 +52,17 @@ public class TaskAssigneeDTO implements Serializable {
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> List<TaskHandler> convertToHandlerList(
 | 
			
		||||
        List<T> sourceList,
 | 
			
		||||
        Function<T, Long> storageId,
 | 
			
		||||
        Function<T, String> storageId,
 | 
			
		||||
        Function<T, String> handlerCode,
 | 
			
		||||
        Function<T, String> handlerName,
 | 
			
		||||
        Function<T, Long> groupName,
 | 
			
		||||
        Function<T, String> groupName,
 | 
			
		||||
        Function<T, Date> createTimeMapper) {
 | 
			
		||||
        return sourceList.stream()
 | 
			
		||||
            .map(item -> new TaskHandler(
 | 
			
		||||
                String.valueOf(storageId.apply(item)),
 | 
			
		||||
                storageId.apply(item),
 | 
			
		||||
                handlerCode.apply(item),
 | 
			
		||||
                handlerName.apply(item),
 | 
			
		||||
                groupName != null ? String.valueOf(groupName.apply(item)) : null,
 | 
			
		||||
                groupName.apply(item),
 | 
			
		||||
                createTimeMapper.apply(item)
 | 
			
		||||
            )).collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,11 @@ public class ProcessEvent implements Serializable {
 | 
			
		||||
     */
 | 
			
		||||
    private String flowCode;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 实例id
 | 
			
		||||
     */
 | 
			
		||||
    private Long instanceId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 业务id
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,15 @@ import lombok.Data;
 | 
			
		||||
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
import java.io.Serializable;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 流程创建任务监听
 | 
			
		||||
 * 流程任务监听
 | 
			
		||||
 *
 | 
			
		||||
 * @author may
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
public class ProcessCreateTaskEvent implements Serializable {
 | 
			
		||||
public class ProcessTaskEvent implements Serializable {
 | 
			
		||||
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = 1L;
 | 
			
		||||
@@ -46,9 +47,24 @@ public class ProcessCreateTaskEvent implements Serializable {
 | 
			
		||||
     */
 | 
			
		||||
    private Long taskId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 实例id
 | 
			
		||||
     */
 | 
			
		||||
    private Long instanceId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 业务id
 | 
			
		||||
     */
 | 
			
		||||
    private String businessId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 流程状态
 | 
			
		||||
     */
 | 
			
		||||
    private String status;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 办理参数
 | 
			
		||||
     */
 | 
			
		||||
    private Map<String, Object> params;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -26,6 +26,7 @@ public class PasswordLoginBody extends LoginBody {
 | 
			
		||||
     */
 | 
			
		||||
    @NotBlank(message = "{user.password.not.blank}")
 | 
			
		||||
    @Length(min = 5, max = 30, message = "{user.password.length.valid}")
 | 
			
		||||
//    @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
 | 
			
		||||
    private String password;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,8 +26,12 @@ public class RegisterBody extends LoginBody {
 | 
			
		||||
     */
 | 
			
		||||
    @NotBlank(message = "{user.password.not.blank}")
 | 
			
		||||
    @Length(min = 5, max = 30, message = "{user.password.length.valid}")
 | 
			
		||||
//    @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
 | 
			
		||||
    private String password;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户类型
 | 
			
		||||
     */
 | 
			
		||||
    private String userType;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,15 @@
 | 
			
		||||
package org.dromara.common.core.exception;
 | 
			
		||||
 | 
			
		||||
import lombok.*;
 | 
			
		||||
import cn.hutool.core.text.StrFormatter;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.EqualsAndHashCode;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 业务异常
 | 
			
		||||
 * 业务异常(支持占位符 {} )
 | 
			
		||||
 *
 | 
			
		||||
 * @author ruoyi
 | 
			
		||||
 */
 | 
			
		||||
@@ -42,6 +46,10 @@ public final class ServiceException extends RuntimeException {
 | 
			
		||||
        this.code = code;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ServiceException(String message, Object... args) {
 | 
			
		||||
        this.message = StrFormatter.format(message, args);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getMessage() {
 | 
			
		||||
        return message;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package org.dromara.common.core.service;
 | 
			
		||||
import org.dromara.common.core.domain.dto.DeptDTO;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 通用 部门服务
 | 
			
		||||
@@ -34,4 +35,12 @@ public interface DeptService {
 | 
			
		||||
     */
 | 
			
		||||
    List<DeptDTO> selectDeptsByList();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据部门 ID 列表查询部门名称映射关系
 | 
			
		||||
     *
 | 
			
		||||
     * @param deptIds 部门 ID 列表
 | 
			
		||||
     * @return Map,其中 key 为部门 ID,value 为对应的部门名称
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
package org.dromara.common.core.service;
 | 
			
		||||
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 用户权限处理
 | 
			
		||||
 *
 | 
			
		||||
 * @author Lion Li
 | 
			
		||||
 */
 | 
			
		||||
public interface PermissionService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取角色数据权限
 | 
			
		||||
     *
 | 
			
		||||
     * @param userId  用户id
 | 
			
		||||
     * @return 角色权限信息
 | 
			
		||||
     */
 | 
			
		||||
    Set<String> getRolePermission(Long userId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取菜单数据权限
 | 
			
		||||
     *
 | 
			
		||||
     * @param userId  用户id
 | 
			
		||||
     * @return 菜单权限信息
 | 
			
		||||
     */
 | 
			
		||||
    Set<String> getMenuPermission(Long userId);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
package org.dromara.common.core.service;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 通用 岗位服务
 | 
			
		||||
 *
 | 
			
		||||
@@ -7,4 +10,12 @@ package org.dromara.common.core.service;
 | 
			
		||||
 */
 | 
			
		||||
public interface PostService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据岗位 ID 列表查询岗位名称映射关系
 | 
			
		||||
     *
 | 
			
		||||
     * @param postIds 岗位 ID 列表
 | 
			
		||||
     * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectPostNamesByIds(List<Long> postIds);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
package org.dromara.common.core.service;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 通用 角色服务
 | 
			
		||||
 *
 | 
			
		||||
@@ -7,4 +10,12 @@ package org.dromara.common.core.service;
 | 
			
		||||
 */
 | 
			
		||||
public interface RoleService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据角色 ID 列表查询角色名称映射关系
 | 
			
		||||
     *
 | 
			
		||||
     * @param roleIds 角色 ID 列表
 | 
			
		||||
     * @return Map,其中 key 为角色 ID,value 为对应的角色名称
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -100,28 +100,4 @@ public interface UserService {
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectUserNamesByIds(List<Long> userIds);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据角色 ID 列表查询角色名称映射关系
 | 
			
		||||
     *
 | 
			
		||||
     * @param roleIds 角色 ID 列表
 | 
			
		||||
     * @return Map,其中 key 为角色 ID,value 为对应的角色名称
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据部门 ID 列表查询部门名称映射关系
 | 
			
		||||
     *
 | 
			
		||||
     * @param deptIds 部门 ID 列表
 | 
			
		||||
     * @return Map,其中 key 为部门 ID,value 为对应的部门名称
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据岗位 ID 列表查询岗位名称映射关系
 | 
			
		||||
     *
 | 
			
		||||
     * @param postIds 岗位 ID 列表
 | 
			
		||||
     * @return Map,其中 key 为岗位 ID,value 为对应的岗位名称
 | 
			
		||||
     */
 | 
			
		||||
    Map<Long, String> selectPostNamesByIds(List<Long> postIds);
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,7 @@ public interface WorkflowService {
 | 
			
		||||
     * completeTask.getVariables().put("ignore", true);
 | 
			
		||||
     *
 | 
			
		||||
     * @param completeTask 参数
 | 
			
		||||
     * @return 结果
 | 
			
		||||
     */
 | 
			
		||||
    boolean completeTask(CompleteTaskDTO completeTask);
 | 
			
		||||
 | 
			
		||||
@@ -90,6 +91,15 @@ public interface WorkflowService {
 | 
			
		||||
     *
 | 
			
		||||
     * @param taskId  任务ID
 | 
			
		||||
     * @param message 办理意见
 | 
			
		||||
     * @return 结果
 | 
			
		||||
     */
 | 
			
		||||
    boolean completeTask(Long taskId, String message);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 启动流程并办理第一个任务
 | 
			
		||||
     *
 | 
			
		||||
     * @param startProcess 参数
 | 
			
		||||
     * @return 结果
 | 
			
		||||
     */
 | 
			
		||||
    boolean startCompleteTask(StartProcessDTO startProcess);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -293,7 +293,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
 | 
			
		||||
 | 
			
		||||
        // 校验时间跨度不超过最大限制
 | 
			
		||||
        if (diff > maxValue) {
 | 
			
		||||
            throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
 | 
			
		||||
            throw new ServiceException("最大时间跨度为 {} {}", maxValue, unit.toString().toLowerCase());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -115,7 +115,7 @@ public class ServletUtils extends JakartaServletUtil {
 | 
			
		||||
    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(), StringUtils.SEPARATOR));
 | 
			
		||||
            params.put(entry.getKey(), StringUtils.joinComma(entry.getValue()));
 | 
			
		||||
        }
 | 
			
		||||
        return params;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import lombok.NoArgsConstructor;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.function.BiFunction;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
@@ -31,8 +30,10 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return CollUtil.newArrayList();
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(function)
 | 
			
		||||
            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
 | 
			
		||||
        return collection.stream().filter(function).collect(Collectors.toList());
 | 
			
		||||
            .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -40,13 +41,26 @@ public class StreamUtils {
 | 
			
		||||
     *
 | 
			
		||||
     * @param collection 需要查询的集合
 | 
			
		||||
     * @param function   过滤方法
 | 
			
		||||
     * @return 找到符合条件的第一个元素,没有则返回null
 | 
			
		||||
     * @return 找到符合条件的第一个元素,没有则返回 Optional.empty()
 | 
			
		||||
     */
 | 
			
		||||
    public static <E> E findFirst(Collection<E> collection, Predicate<E> function) {
 | 
			
		||||
    public static <E> Optional<E> findFirst(Collection<E> collection, Predicate<E> function) {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return null;
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream().filter(function).findFirst().orElse(null);
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(function)
 | 
			
		||||
            .findFirst();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 找到流中满足条件的第一个元素值
 | 
			
		||||
     *
 | 
			
		||||
     * @param collection 需要查询的集合
 | 
			
		||||
     * @param function   过滤方法
 | 
			
		||||
     * @return 找到符合条件的第一个元素,没有则返回 null
 | 
			
		||||
     */
 | 
			
		||||
    public static <E> E findFirstValue(Collection<E> collection, Predicate<E> function) {
 | 
			
		||||
        return findFirst(collection,function).orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -54,13 +68,26 @@ public class StreamUtils {
 | 
			
		||||
     *
 | 
			
		||||
     * @param collection 需要查询的集合
 | 
			
		||||
     * @param function   过滤方法
 | 
			
		||||
     * @return 找到符合条件的任意一个元素,没有则返回null
 | 
			
		||||
     * @return 找到符合条件的任意一个元素,没有则返回 Optional.empty()
 | 
			
		||||
     */
 | 
			
		||||
    public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream().filter(function).findAny();
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(function)
 | 
			
		||||
            .findAny();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 找到流中任意一个满足条件的元素值
 | 
			
		||||
     *
 | 
			
		||||
     * @param collection 需要查询的集合
 | 
			
		||||
     * @param function   过滤方法
 | 
			
		||||
     * @return 找到符合条件的任意一个元素,没有则返回null
 | 
			
		||||
     */
 | 
			
		||||
    public static <E> E findAnyValue(Collection<E> collection, Predicate<E> function) {
 | 
			
		||||
        return findAny(collection,function).orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -86,7 +113,10 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return StringUtils.EMPTY;
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .map(function)
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.joining(delimiter));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -100,8 +130,11 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return CollUtil.newArrayList();
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .sorted(comparing)
 | 
			
		||||
            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
 | 
			
		||||
        return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
 | 
			
		||||
            .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -118,7 +151,9 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -137,7 +172,25 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        }
 | 
			
		||||
        return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.toMap(key, value, (l, r) -> l));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 map 中的数据作为新 Map 的 value ,key 不变
 | 
			
		||||
     * @param map 需要处理的map
 | 
			
		||||
     * @param take 取值函数
 | 
			
		||||
     * @param <K> map中的key类型
 | 
			
		||||
     * @param <E> map中的value类型
 | 
			
		||||
     * @param <V> 新map中的value类型
 | 
			
		||||
     * @return 新的map
 | 
			
		||||
     */
 | 
			
		||||
    public static <K, E, V> Map<K, V> toMap(Map<K, E> map, BiFunction<K, E, V> take) {
 | 
			
		||||
        if (CollUtil.isEmpty(map)) {
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        }
 | 
			
		||||
        return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -154,8 +207,8 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        }
 | 
			
		||||
        return collection
 | 
			
		||||
            .stream().filter(Objects::nonNull)
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -175,8 +228,8 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        }
 | 
			
		||||
        return collection
 | 
			
		||||
            .stream().filter(Objects::nonNull)
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -193,11 +246,11 @@ public class StreamUtils {
 | 
			
		||||
     * @return 分类后的map
 | 
			
		||||
     */
 | 
			
		||||
    public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
 | 
			
		||||
        if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        }
 | 
			
		||||
        return collection
 | 
			
		||||
            .stream().filter(Objects::nonNull)
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -215,8 +268,7 @@ public class StreamUtils {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return CollUtil.newArrayList();
 | 
			
		||||
        }
 | 
			
		||||
        return collection
 | 
			
		||||
            .stream()
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .map(function)
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
 | 
			
		||||
@@ -234,11 +286,10 @@ public class StreamUtils {
 | 
			
		||||
     * @return 转化后的Set
 | 
			
		||||
     */
 | 
			
		||||
    public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
 | 
			
		||||
        if (CollUtil.isEmpty(collection) || function == null) {
 | 
			
		||||
        if (CollUtil.isEmpty(collection)) {
 | 
			
		||||
            return CollUtil.newHashSet();
 | 
			
		||||
        }
 | 
			
		||||
        return collection
 | 
			
		||||
            .stream()
 | 
			
		||||
        return collection.stream()
 | 
			
		||||
            .map(function)
 | 
			
		||||
            .filter(Objects::nonNull)
 | 
			
		||||
            .collect(Collectors.toSet());
 | 
			
		||||
@@ -258,26 +309,20 @@ public class StreamUtils {
 | 
			
		||||
     * @return 合并后的map
 | 
			
		||||
     */
 | 
			
		||||
    public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
 | 
			
		||||
        if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
 | 
			
		||||
        if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) {
 | 
			
		||||
            // 如果两个 map 都为空,则直接返回空的 map
 | 
			
		||||
            return MapUtil.newHashMap();
 | 
			
		||||
        } else if (MapUtil.isEmpty(map1)) {
 | 
			
		||||
            map1 = MapUtil.newHashMap();
 | 
			
		||||
        } else if (MapUtil.isEmpty(map2)) {
 | 
			
		||||
            map2 = MapUtil.newHashMap();
 | 
			
		||||
        } else if (CollUtil.isEmpty(map1)) {
 | 
			
		||||
            // 如果 map1 为空,则直接处理返回 map2
 | 
			
		||||
            return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue()));
 | 
			
		||||
        } else if (CollUtil.isEmpty(map2)) {
 | 
			
		||||
            // 如果 map2 为空,则直接处理返回 map1
 | 
			
		||||
            return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null));
 | 
			
		||||
        }
 | 
			
		||||
        Set<K> key = new HashSet<>();
 | 
			
		||||
        key.addAll(map1.keySet());
 | 
			
		||||
        key.addAll(map2.keySet());
 | 
			
		||||
        Map<K, V> map = new HashMap<>();
 | 
			
		||||
        for (K t : key) {
 | 
			
		||||
            X x = map1.get(t);
 | 
			
		||||
            Y y = map2.get(t);
 | 
			
		||||
            V z = merge.apply(x, y);
 | 
			
		||||
            if (z != null) {
 | 
			
		||||
                map.put(t, z);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return map;
 | 
			
		||||
        Set<K> keySet = new HashSet<>();
 | 
			
		||||
        keySet.addAll(map1.keySet());
 | 
			
		||||
        keySet.addAll(map2.keySet());
 | 
			
		||||
        return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -260,13 +260,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
 | 
			
		||||
        if (s != null) {
 | 
			
		||||
            final int len = s.length();
 | 
			
		||||
            if (s.length() <= size) {
 | 
			
		||||
                sb.append(String.valueOf(c).repeat(size - len));
 | 
			
		||||
                sb.append(Convert.toStr(c).repeat(size - len));
 | 
			
		||||
                sb.append(s);
 | 
			
		||||
            } else {
 | 
			
		||||
                return s.substring(len - size, len);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            sb.append(String.valueOf(c).repeat(Math.max(0, size)));
 | 
			
		||||
            sb.append(Convert.toStr(c).repeat(Math.max(0, size)));
 | 
			
		||||
        }
 | 
			
		||||
        return sb.toString();
 | 
			
		||||
    }
 | 
			
		||||
@@ -361,5 +361,24 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
 | 
			
		||||
            return input;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * 将可迭代对象中的元素使用逗号拼接成字符串
 | 
			
		||||
     *
 | 
			
		||||
     * @param iterable 可迭代对象,如 List、Set 等
 | 
			
		||||
     * @return 拼接后的字符串
 | 
			
		||||
     */
 | 
			
		||||
    public static String joinComma(Iterable<?> iterable) {
 | 
			
		||||
        return StringUtils.join(iterable, SEPARATOR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 将数组中的元素使用逗号拼接成字符串
 | 
			
		||||
     *
 | 
			
		||||
     * @param array 任意类型的数组
 | 
			
		||||
     * @return 拼接后的字符串
 | 
			
		||||
     */
 | 
			
		||||
    public static String joinComma(Object[] array) {
 | 
			
		||||
        return StringUtils.join(array, SEPARATOR);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
package org.dromara.common.core.utils;
 | 
			
		||||
 | 
			
		||||
import lombok.AccessLevel;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 线程相关工具类.
 | 
			
		||||
 *
 | 
			
		||||
 * @author ruoyi
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
 | 
			
		||||
public class Threads {
 | 
			
		||||
    /**
 | 
			
		||||
     * 停止线程池
 | 
			
		||||
     * 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
 | 
			
		||||
     * 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
 | 
			
		||||
     * 如果仍然超時,則強制退出.
 | 
			
		||||
     * 另对在shutdown时线程本身被调用中断做了处理.
 | 
			
		||||
     */
 | 
			
		||||
    public static void shutdownAndAwaitTermination(ExecutorService pool) {
 | 
			
		||||
        if (pool != null && !pool.isShutdown()) {
 | 
			
		||||
            pool.shutdown();
 | 
			
		||||
            try {
 | 
			
		||||
                if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
 | 
			
		||||
                    pool.shutdownNow();
 | 
			
		||||
                    if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
 | 
			
		||||
                        log.info("Pool did not terminate");
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } catch (InterruptedException ie) {
 | 
			
		||||
                pool.shutdownNow();
 | 
			
		||||
                Thread.currentThread().interrupt();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 打印线程异常信息
 | 
			
		||||
     */
 | 
			
		||||
    public static void printException(Runnable r, Throwable t) {
 | 
			
		||||
        if (t == null && r instanceof Future<?>) {
 | 
			
		||||
            try {
 | 
			
		||||
                Future<?> future = (Future<?>) r;
 | 
			
		||||
                if (future.isDone()) {
 | 
			
		||||
                    future.get();
 | 
			
		||||
                }
 | 
			
		||||
            } catch (CancellationException ce) {
 | 
			
		||||
                t = ce;
 | 
			
		||||
            } catch (ExecutionException ee) {
 | 
			
		||||
                t = ee.getCause();
 | 
			
		||||
            } catch (InterruptedException ie) {
 | 
			
		||||
                Thread.currentThread().interrupt();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (t != null) {
 | 
			
		||||
            log.error(t.getMessage(), t);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
 | 
			
		||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
@@ -60,6 +62,31 @@ public class TreeBuildUtils extends TreeUtil {
 | 
			
		||||
        return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构建多根节点的树结构(支持多个顶级节点)
 | 
			
		||||
     *
 | 
			
		||||
     * @param list        原始数据列表
 | 
			
		||||
     * @param getId       获取节点 ID 的方法引用,例如:node -> node.getId()
 | 
			
		||||
     * @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId()
 | 
			
		||||
     * @param parser      树节点属性映射器,用于将原始节点 T 转为 Tree 节点
 | 
			
		||||
     * @param <T>         原始数据类型(如实体类、DTO 等)
 | 
			
		||||
     * @param <K>         节点 ID 类型(如 Long、String)
 | 
			
		||||
     * @return 构建完成的树形结构(可能包含多个顶级根节点)
 | 
			
		||||
     */
 | 
			
		||||
    public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list, Function<T, K> getId, Function<T, K> getParentId, NodeParser<T, K> parser) {
 | 
			
		||||
        if (CollUtil.isEmpty(list)) {
 | 
			
		||||
            return CollUtil.newArrayList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Set<K> rootParentIds = StreamUtils.toSet(list, getParentId);
 | 
			
		||||
        rootParentIds.removeAll(StreamUtils.toSet(list, getId));
 | 
			
		||||
 | 
			
		||||
        // 构建每一个根 parentId 下的树,并合并成最终结果列表
 | 
			
		||||
        return rootParentIds.stream()
 | 
			
		||||
            .flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
 | 
			
		||||
            .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取节点列表中所有节点的叶子节点
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -16,28 +16,55 @@ import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
 | 
			
		||||
public class AddressUtils {
 | 
			
		||||
 | 
			
		||||
    // 未知IP
 | 
			
		||||
    public static final String UNKNOWN_IP = "XX XX";
 | 
			
		||||
    // 内网地址
 | 
			
		||||
    public static final String LOCAL_ADDRESS = "内网IP";
 | 
			
		||||
    // 未知地址
 | 
			
		||||
    public static final String UNKNOWN = "XX XX";
 | 
			
		||||
    public static final String UNKNOWN_ADDRESS = "未知";
 | 
			
		||||
 | 
			
		||||
    public static String getRealAddressByIP(String ip) {
 | 
			
		||||
        // 处理空串并过滤HTML标签
 | 
			
		||||
        ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip,""));
 | 
			
		||||
        boolean isIPv6 = NetUtils.isIPv6(ip);
 | 
			
		||||
        // 判断是否为IPv4或IPv6,如果不是则返回未知地址
 | 
			
		||||
        if (!NetUtils.isIPv4(ip) && !isIPv6) {
 | 
			
		||||
            return UNKNOWN;
 | 
			
		||||
        // 判断是否为IPv4
 | 
			
		||||
        if (NetUtils.isIPv4(ip)) {
 | 
			
		||||
            return resolverIPv4Region(ip);
 | 
			
		||||
        }
 | 
			
		||||
        // 判断是否为IPv6
 | 
			
		||||
        if (NetUtils.isIPv6(ip)) {
 | 
			
		||||
            return resolverIPv6Region(ip);
 | 
			
		||||
        }
 | 
			
		||||
        // 如果不是IPv4或IPv6,则返回未知IP
 | 
			
		||||
        return UNKNOWN_IP;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据IPv4地址查询IP归属行政区域
 | 
			
		||||
     * @param ip ipv4地址
 | 
			
		||||
     * @return 归属行政区域
 | 
			
		||||
     */
 | 
			
		||||
    private static String resolverIPv4Region(String ip){
 | 
			
		||||
        // 内网不查询
 | 
			
		||||
        if (NetUtils.isInnerIPv6(ip) || NetUtils.isInnerIP(ip)) {
 | 
			
		||||
            return "内网IP";
 | 
			
		||||
        }
 | 
			
		||||
        // 不支持IPv6,不再进行没有必要的IP地址信息的解析,直接返回
 | 
			
		||||
        if (isIPv6) {
 | 
			
		||||
            log.warn("ip2region不支持IPV6地址解析:{}", ip);
 | 
			
		||||
            // 如有需要,可自行实现IPv6地址信息解析逻辑,并在这里返回
 | 
			
		||||
            return "未知";
 | 
			
		||||
        if (NetUtils.isInnerIP(ip)) {
 | 
			
		||||
            return LOCAL_ADDRESS;
 | 
			
		||||
        }
 | 
			
		||||
        return RegionUtils.getCityInfo(ip);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据IPv6地址查询IP归属行政区域
 | 
			
		||||
     * @param ip ipv6地址
 | 
			
		||||
     * @return 归属行政区域
 | 
			
		||||
     */
 | 
			
		||||
    private static String resolverIPv6Region(String ip){
 | 
			
		||||
        // 内网不查询
 | 
			
		||||
        if (NetUtils.isInnerIPv6(ip)) {
 | 
			
		||||
            return LOCAL_ADDRESS;
 | 
			
		||||
        }
 | 
			
		||||
        log.warn("ip2region不支持IPV6地址解析:{}", ip);
 | 
			
		||||
        // 不支持IPv6,不再进行没有必要的IP地址信息的解析,直接返回
 | 
			
		||||
        // 如有需要,可自行实现IPv6地址信息解析逻辑,并在这里返回
 | 
			
		||||
        return UNKNOWN_ADDRESS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
package org.dromara.common.core.validate.dicts;
 | 
			
		||||
 | 
			
		||||
import jakarta.validation.Constraint;
 | 
			
		||||
import jakarta.validation.Payload;
 | 
			
		||||
 | 
			
		||||
import java.lang.annotation.ElementType;
 | 
			
		||||
import java.lang.annotation.Retention;
 | 
			
		||||
import java.lang.annotation.RetentionPolicy;
 | 
			
		||||
import java.lang.annotation.Target;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 字典项校验注解
 | 
			
		||||
 *
 | 
			
		||||
 * @author AprilWind
 | 
			
		||||
 */
 | 
			
		||||
@Constraint(validatedBy = DictPatternValidator.class)
 | 
			
		||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
public @interface DictPattern {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 字典类型,如 "sys_user_sex"
 | 
			
		||||
     */
 | 
			
		||||
    String dictType();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 分隔符
 | 
			
		||||
     */
 | 
			
		||||
    String separator();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认校验失败提示信息
 | 
			
		||||
     */
 | 
			
		||||
    String message() default "字典值无效";
 | 
			
		||||
 | 
			
		||||
    Class<?>[] groups() default {};
 | 
			
		||||
 | 
			
		||||
    Class<? extends Payload>[] payload() default {};
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
package org.dromara.common.core.validate.dicts;
 | 
			
		||||
 | 
			
		||||
import jakarta.validation.ConstraintValidator;
 | 
			
		||||
import jakarta.validation.ConstraintValidatorContext;
 | 
			
		||||
import org.dromara.common.core.service.DictService;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 自定义字典值校验器
 | 
			
		||||
 *
 | 
			
		||||
 * @author AprilWind
 | 
			
		||||
 */
 | 
			
		||||
public class DictPatternValidator implements ConstraintValidator<DictPattern, String> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 字典类型
 | 
			
		||||
     */
 | 
			
		||||
    private String dictType;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 分隔符
 | 
			
		||||
     */
 | 
			
		||||
    private String separator = ",";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化校验器,提取注解上的字典类型
 | 
			
		||||
     *
 | 
			
		||||
     * @param annotation 注解实例
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void initialize(DictPattern annotation) {
 | 
			
		||||
        this.dictType = annotation.dictType();
 | 
			
		||||
        if (StringUtils.isNotBlank(annotation.separator())) {
 | 
			
		||||
            this.separator = annotation.separator();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验字段值是否为指定字典类型中的合法值
 | 
			
		||||
     *
 | 
			
		||||
     * @param value   被校验的字段值
 | 
			
		||||
     * @param context 校验上下文(可用于构建错误信息)
 | 
			
		||||
     * @return true 表示校验通过(合法字典值),false 表示不通过
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isValid(String value, ConstraintValidatorContext context) {
 | 
			
		||||
        if (StringUtils.isBlank(dictType) || StringUtils.isBlank(value)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        String dictLabel = SpringUtils.getBean(DictService.class).getDictLabel(dictType, value, separator);
 | 
			
		||||
        return StringUtils.isNotBlank(dictLabel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,7 @@ import org.dromara.common.core.utils.reflect.ReflectUtils;
 | 
			
		||||
 */
 | 
			
		||||
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
 | 
			
		||||
 | 
			
		||||
    private EnumPattern annotation;;
 | 
			
		||||
    private EnumPattern annotation;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void initialize(EnumPattern annotation) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
org.dromara.common.core.config.ApplicationConfig
 | 
			
		||||
org.dromara.common.core.config.AsyncConfig
 | 
			
		||||
org.dromara.common.core.config.ThreadPoolConfig
 | 
			
		||||
org.dromara.common.core.config.ValidatorConfig
 | 
			
		||||
org.dromara.common.core.utils.SpringUtils
 | 
			
		||||
 
 | 
			
		||||
@@ -54,6 +54,7 @@ public class SpringDocConfig {
 | 
			
		||||
        openApi.externalDocs(properties.getExternalDocs());
 | 
			
		||||
        openApi.tags(properties.getTags());
 | 
			
		||||
        openApi.paths(properties.getPaths());
 | 
			
		||||
        if (properties.getComponents() != null) {
 | 
			
		||||
            openApi.components(properties.getComponents());
 | 
			
		||||
            Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
 | 
			
		||||
            List<SecurityRequirement> list = new ArrayList<>();
 | 
			
		||||
@@ -61,7 +62,7 @@ public class SpringDocConfig {
 | 
			
		||||
            keySet.forEach(securityRequirement::addList);
 | 
			
		||||
            list.add(securityRequirement);
 | 
			
		||||
            openApi.security(list);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        return openApi;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import org.dromara.common.encrypt.properties.ApiDecryptProperties;
 | 
			
		||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
 | 
			
		||||
import org.springframework.boot.web.servlet.FilterRegistration;
 | 
			
		||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
 | 
			
		||||
@@ -20,13 +21,14 @@ import org.springframework.context.annotation.Bean;
 | 
			
		||||
public class ApiDecryptAutoConfiguration {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
 | 
			
		||||
        FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
 | 
			
		||||
        registration.setDispatcherTypes(DispatcherType.REQUEST);
 | 
			
		||||
        registration.setFilter(new CryptoFilter(properties));
 | 
			
		||||
        registration.addUrlPatterns("/*");
 | 
			
		||||
        registration.setName("cryptoFilter");
 | 
			
		||||
        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
 | 
			
		||||
        return registration;
 | 
			
		||||
    @FilterRegistration(
 | 
			
		||||
        name = "cryptoFilter",
 | 
			
		||||
        urlPatterns = "/*",
 | 
			
		||||
        order = FilterRegistrationBean.HIGHEST_PRECEDENCE,
 | 
			
		||||
        dispatcherTypes = DispatcherType.REQUEST
 | 
			
		||||
    )
 | 
			
		||||
    public CryptoFilter cryptoFilter(ApiDecryptProperties properties) {
 | 
			
		||||
        return new CryptoFilter(properties);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.ReflectUtil;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.ibatis.io.Resources;
 | 
			
		||||
import org.dromara.common.core.constant.Constants;
 | 
			
		||||
import org.dromara.common.core.utils.ObjectUtils;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
import org.dromara.common.encrypt.annotation.EncryptField;
 | 
			
		||||
@@ -92,8 +93,12 @@ public class EncryptorManager {
 | 
			
		||||
     * @param encryptContext 加密相关的配置信息
 | 
			
		||||
     */
 | 
			
		||||
    public String encrypt(String value, EncryptContext encryptContext) {
 | 
			
		||||
        if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
        IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
 | 
			
		||||
        return encryptor.encrypt(value, encryptContext.getEncode());
 | 
			
		||||
        String encrypt = encryptor.encrypt(value, encryptContext.getEncode());
 | 
			
		||||
        return Constants.ENCRYPT_HEADER + encrypt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -103,8 +108,12 @@ public class EncryptorManager {
 | 
			
		||||
     * @param encryptContext 加密相关的配置信息
 | 
			
		||||
     */
 | 
			
		||||
    public String decrypt(String value, EncryptContext encryptContext) {
 | 
			
		||||
        if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
 | 
			
		||||
            return value;
 | 
			
		||||
        }
 | 
			
		||||
        IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
 | 
			
		||||
        return encryptor.decrypt(value);
 | 
			
		||||
        String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER);
 | 
			
		||||
        return encryptor.decrypt(str);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import cn.hutool.core.convert.Convert;
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.ibatis.executor.parameter.ParameterHandler;
 | 
			
		||||
import org.apache.ibatis.executor.resultset.ResultSetHandler;
 | 
			
		||||
import org.apache.ibatis.plugin.*;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
@@ -39,12 +40,23 @@ public class MybatisDecryptInterceptor implements Interceptor {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object intercept(Invocation invocation) throws Throwable {
 | 
			
		||||
        // 开始进行参数解密
 | 
			
		||||
        ResultSetHandler resultSetHandler = (ResultSetHandler) invocation.getTarget();
 | 
			
		||||
        Field parameterHandlerField = resultSetHandler.getClass().getDeclaredField("parameterHandler");
 | 
			
		||||
        parameterHandlerField.setAccessible(true);
 | 
			
		||||
        Object target = parameterHandlerField.get(resultSetHandler);
 | 
			
		||||
        if (target instanceof ParameterHandler parameterHandler) {
 | 
			
		||||
            Object parameterObject = parameterHandler.getParameterObject();
 | 
			
		||||
            if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
 | 
			
		||||
                this.decryptHandler(parameterObject);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // 获取执行mysql执行结果
 | 
			
		||||
        Object result = invocation.proceed();
 | 
			
		||||
        if (result == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        decryptHandler(result);
 | 
			
		||||
        this.decryptHandler(result);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,17 +6,13 @@ import java.lang.annotation.RetentionPolicy;
 | 
			
		||||
import java.lang.annotation.Target;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 批注
 | 
			
		||||
 * 批注 此注解仅用于单表头 不支持多层级表头
 | 
			
		||||
 * @author guzhouyanyu
 | 
			
		||||
 */
 | 
			
		||||
@Target({ElementType.FIELD})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
public @interface ExcelNotation {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * col index
 | 
			
		||||
     */
 | 
			
		||||
    int index() default -1;
 | 
			
		||||
    /**
 | 
			
		||||
     * 批注内容
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -8,17 +8,13 @@ import java.lang.annotation.RetentionPolicy;
 | 
			
		||||
import java.lang.annotation.Target;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 是否必填
 | 
			
		||||
 * 是否必填 此注解仅用于单表头 不支持多层级表头
 | 
			
		||||
 * @author guzhouyanyu
 | 
			
		||||
 */
 | 
			
		||||
@Target({ElementType.FIELD})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
public @interface ExcelRequired {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * col index
 | 
			
		||||
     */
 | 
			
		||||
    int index() default -1;
 | 
			
		||||
    /**
 | 
			
		||||
     * 字体颜色
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ public class ExcelBigNumberConvert implements Converter<Long> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public CellDataTypeEnum supportExcelTypeKey() {
 | 
			
		||||
        return CellDataTypeEnum.STRING;
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,200 @@
 | 
			
		||||
package org.dromara.common.excel.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.util.ReflectUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.idev.excel.annotation.ExcelIgnore;
 | 
			
		||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
 | 
			
		||||
import cn.idev.excel.annotation.ExcelProperty;
 | 
			
		||||
import lombok.SneakyThrows;
 | 
			
		||||
import org.apache.poi.ss.util.CellRangeAddress;
 | 
			
		||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
 | 
			
		||||
import org.dromara.common.excel.annotation.CellMerge;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Field;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 单元格合并处理器
 | 
			
		||||
 *
 | 
			
		||||
 * @author Lion Li
 | 
			
		||||
 */
 | 
			
		||||
public class CellMergeHandler {
 | 
			
		||||
 | 
			
		||||
    private final boolean hasTitle;
 | 
			
		||||
    private int rowIndex;
 | 
			
		||||
 | 
			
		||||
    private CellMergeHandler(final boolean hasTitle) {
 | 
			
		||||
        this.hasTitle = hasTitle;
 | 
			
		||||
        // 行合并开始下标
 | 
			
		||||
        this.rowIndex = hasTitle ? 1 : 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    public List<CellRangeAddress> handle(List<?> rows) {
 | 
			
		||||
        // 如果入参为空集合则返回空集
 | 
			
		||||
        if (CollUtil.isEmpty(rows)) {
 | 
			
		||||
            return Collections.emptyList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 获取有合并注解的字段
 | 
			
		||||
        Map<Field, FieldColumnIndex> mergeFields = getFieldColumnIndexMap(rows.get(0).getClass());
 | 
			
		||||
        // 如果没有需要合并的字段则返回空集
 | 
			
		||||
        if (CollUtil.isEmpty(mergeFields)) {
 | 
			
		||||
            return Collections.emptyList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 结果集
 | 
			
		||||
        List<CellRangeAddress> result = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
        // 生成两两合并单元格
 | 
			
		||||
        Map<Field, RepeatCell> rowRepeatCellMap = new HashMap<>();
 | 
			
		||||
        for (Map.Entry<Field, FieldColumnIndex> item : mergeFields.entrySet()) {
 | 
			
		||||
            Field field = item.getKey();
 | 
			
		||||
            FieldColumnIndex itemValue = item.getValue();
 | 
			
		||||
            int colNum = itemValue.colIndex();
 | 
			
		||||
            CellMerge cellMerge = itemValue.cellMerge();
 | 
			
		||||
 | 
			
		||||
            for (int i = 0; i < rows.size(); i++) {
 | 
			
		||||
                // 当前行数据
 | 
			
		||||
                Object currentRowObj = rows.get(i);
 | 
			
		||||
                // 当前行数据字段值
 | 
			
		||||
                Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName());
 | 
			
		||||
 | 
			
		||||
                // 空值跳过不处理
 | 
			
		||||
                if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 单元格合并Map是否存在数据,如果不存在则添加当前行的字段值
 | 
			
		||||
                if (!rowRepeatCellMap.containsKey(field)) {
 | 
			
		||||
                    rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 获取 单元格合并Map 中字段值
 | 
			
		||||
                RepeatCell repeatCell = rowRepeatCellMap.get(field);
 | 
			
		||||
                Object cellValue = repeatCell.value();
 | 
			
		||||
                int current = repeatCell.current();
 | 
			
		||||
 | 
			
		||||
                // 检查是否满足合并条件
 | 
			
		||||
                // currentRowObj 当前行数据
 | 
			
		||||
                // rows.get(i - 1) 上一行数据 注:由于 if (!rowRepeatCellMap.containsKey(field)) 条件的存在,所以该 i 必不可能小于1
 | 
			
		||||
                // cellMerge 当前行字段合并注解
 | 
			
		||||
                boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge);
 | 
			
		||||
 | 
			
		||||
                // 是否添加到结果集
 | 
			
		||||
                boolean isAddResult = false;
 | 
			
		||||
                // 最新行
 | 
			
		||||
                int lastRow = i + rowIndex - 1;
 | 
			
		||||
 | 
			
		||||
                // 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换
 | 
			
		||||
                if (!currentRowObjFieldVal.equals(cellValue) || !merge) {
 | 
			
		||||
                    rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
 | 
			
		||||
                    isAddResult = true;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // 如果最后一行不能合并,检查之前的数据是否需要合并;如果最后一行可以合并,则直接合并到最后
 | 
			
		||||
                if (i == rows.size() - 1) {
 | 
			
		||||
                    isAddResult = true;
 | 
			
		||||
                    if (i > current) {
 | 
			
		||||
                        lastRow = i + rowIndex;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (isAddResult && i > current) {
 | 
			
		||||
                    result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取带有合并注解的字段列索引和合并注解信息Map集
 | 
			
		||||
     */
 | 
			
		||||
    private Map<Field, FieldColumnIndex> getFieldColumnIndexMap(Class<?> clazz) {
 | 
			
		||||
        boolean annotationPresent = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
 | 
			
		||||
        Field[] fields = ReflectUtils.getFields(clazz, field -> {
 | 
			
		||||
            if ("serialVersionUID".equals(field.getName())) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            if (field.isAnnotationPresent(ExcelIgnore.class)) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return !annotationPresent || field.isAnnotationPresent(ExcelProperty.class);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 有注解的字段
 | 
			
		||||
        Map<Field, FieldColumnIndex> mergeFields = new HashMap<>();
 | 
			
		||||
        for (int i = 0; i < fields.length; i++) {
 | 
			
		||||
            Field field = fields[i];
 | 
			
		||||
            if (!field.isAnnotationPresent(CellMerge.class)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            CellMerge cm = field.getAnnotation(CellMerge.class);
 | 
			
		||||
            int index = cm.index() == -1 ? i : cm.index();
 | 
			
		||||
            mergeFields.put(field, FieldColumnIndex.of(index, cm));
 | 
			
		||||
 | 
			
		||||
            if (hasTitle) {
 | 
			
		||||
                ExcelProperty property = field.getAnnotation(ExcelProperty.class);
 | 
			
		||||
                rowIndex = Math.max(rowIndex, property.value().length);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return mergeFields;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
 | 
			
		||||
        final String[] mergeBy = cellMerge.mergeBy();
 | 
			
		||||
        if (StrUtil.isAllNotBlank(mergeBy)) {
 | 
			
		||||
            //比对当前行和上一行的各个属性值一一比对 如果全为真 则为真
 | 
			
		||||
            for (String fieldName : mergeBy) {
 | 
			
		||||
                final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName);
 | 
			
		||||
                final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName);
 | 
			
		||||
                if (!Objects.equals(valPre, valCurrent)) {
 | 
			
		||||
                    //依赖字段如有任一不等值,则标记为不可合并
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 单元格合并
 | 
			
		||||
     */
 | 
			
		||||
    record RepeatCell(Object value, int current) {
 | 
			
		||||
        static RepeatCell of(Object value, int current) {
 | 
			
		||||
            return new RepeatCell(value, current);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 字段列索引和合并注解信息
 | 
			
		||||
     */
 | 
			
		||||
    record FieldColumnIndex(int colIndex, CellMerge cellMerge) {
 | 
			
		||||
        static FieldColumnIndex of(int colIndex, CellMerge cellMerge) {
 | 
			
		||||
            return new FieldColumnIndex(colIndex, cellMerge);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建一个单元格合并处理器实例
 | 
			
		||||
     *
 | 
			
		||||
     * @param hasTitle 是否合并标题
 | 
			
		||||
     * @return 单元格合并处理器
 | 
			
		||||
     */
 | 
			
		||||
    public static CellMergeHandler of(final boolean hasTitle) {
 | 
			
		||||
        return new CellMergeHandler(hasTitle);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建一个单元格合并处理器实例(默认不合并标题)
 | 
			
		||||
     *
 | 
			
		||||
     * @return 单元格合并处理器
 | 
			
		||||
     */
 | 
			
		||||
    public static CellMergeHandler of() {
 | 
			
		||||
        return new CellMergeHandler(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +1,15 @@
 | 
			
		||||
package org.dromara.common.excel.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.util.ReflectUtil;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import cn.idev.excel.annotation.ExcelProperty;
 | 
			
		||||
import cn.idev.excel.metadata.Head;
 | 
			
		||||
import cn.idev.excel.write.handler.WorkbookWriteHandler;
 | 
			
		||||
import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext;
 | 
			
		||||
import cn.idev.excel.write.merge.AbstractMergeStrategy;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import lombok.SneakyThrows;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.apache.poi.ss.usermodel.Cell;
 | 
			
		||||
import org.apache.poi.ss.usermodel.Sheet;
 | 
			
		||||
import org.apache.poi.ss.util.CellRangeAddress;
 | 
			
		||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
 | 
			
		||||
import org.dromara.common.excel.annotation.CellMerge;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Field;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -30,21 +21,22 @@ import java.util.*;
 | 
			
		||||
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
 | 
			
		||||
 | 
			
		||||
    private final List<CellRangeAddress> cellList;
 | 
			
		||||
    private final boolean hasTitle;
 | 
			
		||||
    private int rowIndex;
 | 
			
		||||
 | 
			
		||||
    public CellMergeStrategy(List<CellRangeAddress> cellList) {
 | 
			
		||||
        this.cellList = cellList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CellMergeStrategy(List<?> list, boolean hasTitle) {
 | 
			
		||||
        this.hasTitle = hasTitle;
 | 
			
		||||
        // 行合并开始下标
 | 
			
		||||
        this.rowIndex = hasTitle ? 1 : 0;
 | 
			
		||||
        this.cellList = handle(list, hasTitle);
 | 
			
		||||
        this.cellList = CellMergeHandler.of(hasTitle).handle(list);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
 | 
			
		||||
        if (CollUtil.isEmpty(cellList)){
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
 | 
			
		||||
        final int rowIndex = cell.getRowIndex();
 | 
			
		||||
        if (CollUtil.isNotEmpty(cellList)){
 | 
			
		||||
        for (CellRangeAddress cellAddresses : cellList) {
 | 
			
		||||
            final int firstRow = cellAddresses.getFirstRow();
 | 
			
		||||
            if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
 | 
			
		||||
@@ -52,112 +44,16 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
 | 
			
		||||
        if (CollUtil.isEmpty(cellList)){
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        //当前表格写完后,统一写入
 | 
			
		||||
        if (CollUtil.isNotEmpty(cellList)){
 | 
			
		||||
        for (CellRangeAddress item : cellList) {
 | 
			
		||||
            context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SneakyThrows
 | 
			
		||||
    private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
 | 
			
		||||
        List<CellRangeAddress> cellList = new ArrayList<>();
 | 
			
		||||
        if (CollUtil.isEmpty(list)) {
 | 
			
		||||
            return cellList;
 | 
			
		||||
        }
 | 
			
		||||
        Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
 | 
			
		||||
 | 
			
		||||
        // 有注解的字段
 | 
			
		||||
        List<Field> mergeFields = new ArrayList<>();
 | 
			
		||||
        List<Integer> mergeFieldsIndex = new ArrayList<>();
 | 
			
		||||
        for (int i = 0; i < fields.length; i++) {
 | 
			
		||||
            Field field = fields[i];
 | 
			
		||||
            if (field.isAnnotationPresent(CellMerge.class)) {
 | 
			
		||||
                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);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
                Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
 | 
			
		||||
 | 
			
		||||
                int colNum = mergeFieldsIndex.get(j);
 | 
			
		||||
                if (!map.containsKey(field)) {
 | 
			
		||||
                    map.put(field, new RepeatCell(val, i));
 | 
			
		||||
                } else {
 | 
			
		||||
                    RepeatCell repeatCell = map.get(field);
 | 
			
		||||
                    Object cellValue = repeatCell.getValue();
 | 
			
		||||
                    if (cellValue == null || "".equals(cellValue)) {
 | 
			
		||||
                        // 空值跳过不合并
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!cellValue.equals(val)) {
 | 
			
		||||
                        if ((i - repeatCell.getCurrent() > 1)) {
 | 
			
		||||
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
 | 
			
		||||
                        }
 | 
			
		||||
                        map.put(field, new RepeatCell(val, i));
 | 
			
		||||
                    } else if (i == list.size() - 1) {
 | 
			
		||||
                        if (!isMerge(list, i, field)) {
 | 
			
		||||
                            // 如果最后一行不能合并,检查之前的数据是否需要合并
 | 
			
		||||
                            if (i - repeatCell.getCurrent() > 1) {
 | 
			
		||||
                                cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (i > repeatCell.getCurrent()) {
 | 
			
		||||
                            // 如果最后一行可以合并,则直接合并到最后
 | 
			
		||||
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (!isMerge(list, i, field)) {
 | 
			
		||||
                        if ((i - repeatCell.getCurrent() > 1)) {
 | 
			
		||||
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
 | 
			
		||||
                        }
 | 
			
		||||
                        map.put(field, new RepeatCell(val, i));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return cellList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isMerge(List<?> list, int i, Field field) {
 | 
			
		||||
        boolean isMerge = true;
 | 
			
		||||
        CellMerge cm = field.getAnnotation(CellMerge.class);
 | 
			
		||||
        final String[] mergeBy = cm.mergeBy();
 | 
			
		||||
        if (StrUtil.isAllNotBlank(mergeBy)) {
 | 
			
		||||
            //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
 | 
			
		||||
            for (String fieldName : mergeBy) {
 | 
			
		||||
                final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
 | 
			
		||||
                final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
 | 
			
		||||
                if (!Objects.equals(valPre, valCurrent)) {
 | 
			
		||||
                    //依赖字段如有任一不等值,则标记为不可合并
 | 
			
		||||
                    isMerge = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return isMerge;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    @AllArgsConstructor
 | 
			
		||||
    static class RepeatCell {
 | 
			
		||||
 | 
			
		||||
        private Object value;
 | 
			
		||||
 | 
			
		||||
        private int current;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package org.dromara.common.excel.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.convert.Convert;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
@@ -65,7 +66,7 @@ public class DropDownOptions {
 | 
			
		||||
        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]));
 | 
			
		||||
            String var = StrUtil.trimToEmpty(Convert.toStr(vars[i]));
 | 
			
		||||
            if (!var.matches(regex)) {
 | 
			
		||||
                throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package org.dromara.common.excel.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.convert.Convert;
 | 
			
		||||
import cn.hutool.core.util.ArrayUtil;
 | 
			
		||||
import cn.hutool.core.util.EnumUtil;
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
@@ -103,7 +104,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
 | 
			
		||||
                if (StringUtils.isNotBlank(dictType)) {
 | 
			
		||||
                    // 如果传递了字典名,则依据字典建立下拉
 | 
			
		||||
                    Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
 | 
			
		||||
                        .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
 | 
			
		||||
                        .orElseThrow(() -> new ServiceException("字典 {} 不存在", dictType))
 | 
			
		||||
                        .values();
 | 
			
		||||
                    options = new ArrayList<>(values);
 | 
			
		||||
                } else if (StringUtils.isNotBlank(converterExp)) {
 | 
			
		||||
@@ -115,7 +116,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
 | 
			
		||||
                // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
 | 
			
		||||
                ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
 | 
			
		||||
                List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
 | 
			
		||||
                options = StreamUtils.toList(values, String::valueOf);
 | 
			
		||||
                options = StreamUtils.toList(values, Convert::toStr);
 | 
			
		||||
            }
 | 
			
		||||
            if (ObjectUtil.isNotEmpty(options)) {
 | 
			
		||||
                // 仅当下拉可选项不为空时执行
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package org.dromara.common.excel.handler;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.idev.excel.annotation.ExcelProperty;
 | 
			
		||||
import cn.idev.excel.metadata.data.DataFormatData;
 | 
			
		||||
import cn.idev.excel.metadata.data.WriteCellData;
 | 
			
		||||
import cn.idev.excel.util.StyleUtil;
 | 
			
		||||
@@ -13,7 +14,6 @@ import cn.idev.excel.write.metadata.style.WriteFont;
 | 
			
		||||
import org.apache.poi.ss.usermodel.*;
 | 
			
		||||
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
 | 
			
		||||
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
 | 
			
		||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
 | 
			
		||||
import org.dromara.common.excel.annotation.ExcelNotation;
 | 
			
		||||
import org.dromara.common.excel.annotation.ExcelRequired;
 | 
			
		||||
 | 
			
		||||
@@ -31,12 +31,12 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * 批注
 | 
			
		||||
     */
 | 
			
		||||
    private final Map<Integer, String> notationMap;
 | 
			
		||||
    private final Map<String, String> notationMap;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 头列字体颜色
 | 
			
		||||
     */
 | 
			
		||||
    private final Map<Integer, Short> headColumnMap;
 | 
			
		||||
    private final Map<String, Short> headColumnMap;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public DataWriteHandler(Class<?> clazz) {
 | 
			
		||||
@@ -49,15 +49,16 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
 | 
			
		||||
        if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // 第一行
 | 
			
		||||
        WriteCellData<?> cellData = context.getFirstCellData();
 | 
			
		||||
        // 第一个格子
 | 
			
		||||
        WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
 | 
			
		||||
 | 
			
		||||
        if (context.getHead()) {
 | 
			
		||||
            DataFormatData dataFormatData = new DataFormatData();
 | 
			
		||||
            // 单元格设置为文本格式
 | 
			
		||||
            dataFormatData.setIndex((short) 49);
 | 
			
		||||
            writeCellStyle.setDataFormatData(dataFormatData);
 | 
			
		||||
 | 
			
		||||
        if (context.getHead()) {
 | 
			
		||||
            Cell cell = context.getCell();
 | 
			
		||||
            WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder();
 | 
			
		||||
            Sheet sheet = writeSheetHolder.getSheet();
 | 
			
		||||
@@ -67,17 +68,17 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
 | 
			
		||||
            WriteFont headWriteFont = new WriteFont();
 | 
			
		||||
            // 加粗
 | 
			
		||||
            headWriteFont.setBold(true);
 | 
			
		||||
            if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getColumnIndex())) {
 | 
			
		||||
            if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getStringCellValue())) {
 | 
			
		||||
                // 设置字体颜色
 | 
			
		||||
                headWriteFont.setColor(headColumnMap.get(cell.getColumnIndex()));
 | 
			
		||||
                headWriteFont.setColor(headColumnMap.get(cell.getStringCellValue()));
 | 
			
		||||
            }
 | 
			
		||||
            writeCellStyle.setWriteFont(headWriteFont);
 | 
			
		||||
            CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, writeCellStyle);
 | 
			
		||||
            cell.setCellStyle(cellStyle);
 | 
			
		||||
 | 
			
		||||
            if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getColumnIndex())) {
 | 
			
		||||
            if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getStringCellValue())) {
 | 
			
		||||
                // 批注内容
 | 
			
		||||
                String notationContext = notationMap.get(cell.getColumnIndex());
 | 
			
		||||
                String notationContext = notationMap.get(cell.getStringCellValue());
 | 
			
		||||
                // 创建绘图对象
 | 
			
		||||
                Comment comment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5));
 | 
			
		||||
                comment.setString(new XSSFRichTextString(notationContext));
 | 
			
		||||
@@ -89,23 +90,16 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取必填列
 | 
			
		||||
     */
 | 
			
		||||
    private static Map<Integer, Short> getRequiredMap(Class<?> clazz) {
 | 
			
		||||
        Map<Integer, Short> requiredMap = new HashMap<>();
 | 
			
		||||
    private static Map<String, Short> getRequiredMap(Class<?> clazz) {
 | 
			
		||||
        Map<String, Short> requiredMap = new HashMap<>();
 | 
			
		||||
        Field[] fields = clazz.getDeclaredFields();
 | 
			
		||||
        // 检查 fields 数组是否为空
 | 
			
		||||
        if (fields.length == 0) {
 | 
			
		||||
            return requiredMap;
 | 
			
		||||
        }
 | 
			
		||||
        Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < filteredFields.length; i++) {
 | 
			
		||||
            Field field = filteredFields[i];
 | 
			
		||||
        for (Field field : fields) {
 | 
			
		||||
            if (!field.isAnnotationPresent(ExcelRequired.class)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            ExcelRequired excelRequired = field.getAnnotation(ExcelRequired.class);
 | 
			
		||||
            int columnIndex =  excelRequired.index() == -1 ? i : excelRequired.index();
 | 
			
		||||
            requiredMap.put(columnIndex, excelRequired.fontColor().getIndex());
 | 
			
		||||
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
 | 
			
		||||
            requiredMap.put(excelProperty.value()[0], excelRequired.fontColor().getIndex());
 | 
			
		||||
        }
 | 
			
		||||
        return requiredMap;
 | 
			
		||||
    }
 | 
			
		||||
@@ -113,22 +107,16 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取批注
 | 
			
		||||
     */
 | 
			
		||||
    private static Map<Integer, String> getNotationMap(Class<?> clazz) {
 | 
			
		||||
        Map<Integer, String> notationMap = new HashMap<>();
 | 
			
		||||
    private static Map<String, String> getNotationMap(Class<?> clazz) {
 | 
			
		||||
        Map<String, String> notationMap = new HashMap<>();
 | 
			
		||||
        Field[] fields = clazz.getDeclaredFields();
 | 
			
		||||
        // 检查 fields 数组是否为空
 | 
			
		||||
        if (fields.length == 0) {
 | 
			
		||||
            return notationMap;
 | 
			
		||||
        }
 | 
			
		||||
        Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
 | 
			
		||||
        for (int i = 0; i < filteredFields.length; i++) {
 | 
			
		||||
            Field field = filteredFields[i];
 | 
			
		||||
        for (Field field : fields) {
 | 
			
		||||
            if (!field.isAnnotationPresent(ExcelNotation.class)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            ExcelNotation excelNotation = field.getAnnotation(ExcelNotation.class);
 | 
			
		||||
            int columnIndex =  excelNotation.index() == -1 ? i : excelNotation.index();
 | 
			
		||||
            notationMap.put(columnIndex, excelNotation.value());
 | 
			
		||||
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
 | 
			
		||||
            notationMap.put(excelProperty.value()[0], excelNotation.value());
 | 
			
		||||
        }
 | 
			
		||||
        return notationMap;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Excel相关处理
 | 
			
		||||
@@ -203,6 +204,44 @@ public class ExcelUtil {
 | 
			
		||||
        builder.doWrite(list);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 导出excel
 | 
			
		||||
     *
 | 
			
		||||
     * @param headType 带Excel注解的类型
 | 
			
		||||
     * @param os       输出流
 | 
			
		||||
     * @param options  Excel下拉可选项
 | 
			
		||||
     * @param consumer 导出助手消费函数
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> void exportExcel(Class<T> headType, OutputStream os, List<DropDownOptions> options, Consumer<ExcelWriterWrapper<T>> consumer) {
 | 
			
		||||
        try (ExcelWriter writer = FastExcel.write(os, headType)
 | 
			
		||||
            .autoCloseStream(false)
 | 
			
		||||
            // 自动适配
 | 
			
		||||
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
 | 
			
		||||
            // 大数值自动转换 防止失真
 | 
			
		||||
            .registerConverter(new ExcelBigNumberConvert())
 | 
			
		||||
            // 批注必填项处理
 | 
			
		||||
            .registerWriteHandler(new DataWriteHandler(headType))
 | 
			
		||||
            // 添加下拉框操作
 | 
			
		||||
            .registerWriteHandler(new ExcelDownHandler(options))
 | 
			
		||||
            .build()) {
 | 
			
		||||
            // 执行消费函数
 | 
			
		||||
            consumer.accept(ExcelWriterWrapper.of(writer));
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            throw new RuntimeException(e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 导出excel
 | 
			
		||||
     *
 | 
			
		||||
     * @param headType 带Excel注解的类型
 | 
			
		||||
     * @param os       输出流
 | 
			
		||||
     * @param consumer 导出助手消费函数
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> void exportExcel(Class<T> headType, OutputStream os, Consumer<ExcelWriterWrapper<T>> consumer) {
 | 
			
		||||
        exportExcel(headType, os, null, consumer);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 单表多数据模板导出 模板格式为 {.属性}
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,127 @@
 | 
			
		||||
package org.dromara.common.excel.utils;
 | 
			
		||||
 | 
			
		||||
import cn.idev.excel.ExcelWriter;
 | 
			
		||||
import cn.idev.excel.FastExcel;
 | 
			
		||||
import cn.idev.excel.context.WriteContext;
 | 
			
		||||
import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
 | 
			
		||||
import cn.idev.excel.write.builder.ExcelWriterTableBuilder;
 | 
			
		||||
import cn.idev.excel.write.metadata.WriteSheet;
 | 
			
		||||
import cn.idev.excel.write.metadata.WriteTable;
 | 
			
		||||
import cn.idev.excel.write.metadata.fill.FillConfig;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ExcelWriterWrapper Excel写出包装器
 | 
			
		||||
 * <br>
 | 
			
		||||
 * 提供了一组与 ExcelWriter 一一对应的写出方法,避免直接提供 ExcelWriter 而导致的一些不可控问题(比如提前关闭了IO流等)
 | 
			
		||||
 *
 | 
			
		||||
 * @author 秋辞未寒
 | 
			
		||||
 * @see ExcelWriter
 | 
			
		||||
 */
 | 
			
		||||
public record ExcelWriterWrapper<T>(ExcelWriter excelWriter) {
 | 
			
		||||
 | 
			
		||||
    public void write(Collection<T> data, WriteSheet writeSheet) {
 | 
			
		||||
        excelWriter.write(data, writeSheet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet) {
 | 
			
		||||
        excelWriter.write(supplier.get(), writeSheet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void write(Collection<T> data, WriteSheet writeSheet, WriteTable writeTable) {
 | 
			
		||||
        excelWriter.write(data, writeSheet, writeTable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet, WriteTable writeTable) {
 | 
			
		||||
        excelWriter.write(supplier.get(), writeSheet, writeTable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void fill(Object data, WriteSheet writeSheet) {
 | 
			
		||||
        excelWriter.fill(data, writeSheet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) {
 | 
			
		||||
        excelWriter.fill(data, fillConfig, writeSheet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void fill(Supplier<Object> supplier, WriteSheet writeSheet) {
 | 
			
		||||
        excelWriter.fill(supplier, writeSheet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void fill(Supplier<Object> supplier, FillConfig fillConfig, WriteSheet writeSheet) {
 | 
			
		||||
        excelWriter.fill(supplier, fillConfig, writeSheet);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public WriteContext writeContext() {
 | 
			
		||||
        return excelWriter.writeContext();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建一个 ExcelWriterWrapper
 | 
			
		||||
     *
 | 
			
		||||
     * @param excelWriter ExcelWriter
 | 
			
		||||
     * @return ExcelWriterWrapper
 | 
			
		||||
     */
 | 
			
		||||
    public static  <T> ExcelWriterWrapper<T> of(ExcelWriter excelWriter) {
 | 
			
		||||
        return new ExcelWriterWrapper<>(excelWriter);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------------------------- sheet start
 | 
			
		||||
 | 
			
		||||
    public static WriteSheet buildSheet(Integer sheetNo, String sheetName) {
 | 
			
		||||
        return sheetBuilder(sheetNo, sheetName).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static WriteSheet buildSheet(Integer sheetNo) {
 | 
			
		||||
        return sheetBuilder(sheetNo).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static WriteSheet buildSheet(String sheetName) {
 | 
			
		||||
        return sheetBuilder(sheetName).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static WriteSheet buildSheet() {
 | 
			
		||||
        return sheetBuilder().build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo, String sheetName) {
 | 
			
		||||
        return FastExcel.writerSheet(sheetNo, sheetName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo) {
 | 
			
		||||
        return FastExcel.writerSheet(sheetNo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ExcelWriterSheetBuilder sheetBuilder(String sheetName) {
 | 
			
		||||
        return FastExcel.writerSheet(sheetName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ExcelWriterSheetBuilder sheetBuilder() {
 | 
			
		||||
        return FastExcel.writerSheet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------------------------- sheet end
 | 
			
		||||
 | 
			
		||||
    // -------------------------------- table start
 | 
			
		||||
 | 
			
		||||
    public static WriteTable buildTable(Integer tableNo) {
 | 
			
		||||
        return tableBuilder(tableNo).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static WriteTable buildTable() {
 | 
			
		||||
        return tableBuilder().build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ExcelWriterTableBuilder tableBuilder(Integer tableNo) {
 | 
			
		||||
        return FastExcel.writerTable(tableNo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static ExcelWriterTableBuilder tableBuilder() {
 | 
			
		||||
        return FastExcel.writerTable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------------------------- table end
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package org.dromara.common.json.config;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.Module;
 | 
			
		||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 | 
			
		||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 | 
			
		||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
 | 
			
		||||
@@ -29,8 +30,7 @@ import java.util.TimeZone;
 | 
			
		||||
public class JacksonConfig {
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
 | 
			
		||||
        return builder -> {
 | 
			
		||||
    public Module registerJavaTimeModule() {
 | 
			
		||||
        // 全局配置序列化返回 JSON 处理
 | 
			
		||||
        JavaTimeModule javaTimeModule = new JavaTimeModule();
 | 
			
		||||
        javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
 | 
			
		||||
@@ -41,7 +41,12 @@ public class JacksonConfig {
 | 
			
		||||
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
 | 
			
		||||
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
 | 
			
		||||
        javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
 | 
			
		||||
            builder.modules(javaTimeModule);
 | 
			
		||||
        return javaTimeModule;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
 | 
			
		||||
        return builder -> {
 | 
			
		||||
            builder.timeZone(TimeZone.getDefault());
 | 
			
		||||
            log.info("初始化 jackson 配置");
 | 
			
		||||
        };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
package org.dromara.common.json.handler;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.date.DateTime;
 | 
			
		||||
import cn.hutool.core.date.DateUtil;
 | 
			
		||||
import com.fasterxml.jackson.core.JsonParser;
 | 
			
		||||
import com.fasterxml.jackson.databind.DeserializationContext;
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonDeserializer;
 | 
			
		||||
import org.dromara.common.core.utils.ObjectUtils;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.Date;
 | 
			
		||||
@@ -25,7 +27,11 @@ public class CustomDateDeserializer extends JsonDeserializer<Date> {
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
 | 
			
		||||
        return DateUtil.parse(p.getText());
 | 
			
		||||
        DateTime parse = DateUtil.parse(p.getText());
 | 
			
		||||
        if (ObjectUtils.isNull(parse)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return parse.toJdkDate();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.ArrayUtil;
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import com.fasterxml.jackson.core.JsonProcessingException;
 | 
			
		||||
import com.fasterxml.jackson.core.type.TypeReference;
 | 
			
		||||
import com.fasterxml.jackson.databind.JsonNode;
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper;
 | 
			
		||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
 | 
			
		||||
import lombok.AccessLevel;
 | 
			
		||||
@@ -167,4 +168,58 @@ public class JsonUtils {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断字符串是否为合法 JSON(对象或数组)
 | 
			
		||||
     *
 | 
			
		||||
     * @param str 待校验字符串
 | 
			
		||||
     * @return true = 合法 JSON,false = 非法或空
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isJson(String str) {
 | 
			
		||||
        if (StringUtils.isBlank(str)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            OBJECT_MAPPER.readTree(str);
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断字符串是否为 JSON 对象({})
 | 
			
		||||
     *
 | 
			
		||||
     * @param str 待校验字符串
 | 
			
		||||
     * @return true = JSON 对象
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isJsonObject(String str) {
 | 
			
		||||
        if (StringUtils.isBlank(str)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            JsonNode node = OBJECT_MAPPER.readTree(str);
 | 
			
		||||
            return node.isObject();
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断字符串是否为 JSON 数组([])
 | 
			
		||||
     *
 | 
			
		||||
     * @param str 待校验字符串
 | 
			
		||||
     * @return true = JSON 数组
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isJsonArray(String str) {
 | 
			
		||||
        if (StringUtils.isBlank(str)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            JsonNode node = OBJECT_MAPPER.readTree(str);
 | 
			
		||||
            return node.isArray();
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
package org.dromara.common.json.validate;
 | 
			
		||||
 | 
			
		||||
import jakarta.validation.Constraint;
 | 
			
		||||
import jakarta.validation.Payload;
 | 
			
		||||
 | 
			
		||||
import java.lang.annotation.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * JSON 格式校验注解
 | 
			
		||||
 *
 | 
			
		||||
 * @author AprilWind
 | 
			
		||||
 */
 | 
			
		||||
@Documented
 | 
			
		||||
@Target({ElementType.METHOD, ElementType.FIELD})
 | 
			
		||||
@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
@Constraint(validatedBy = JsonPatternValidator.class)
 | 
			
		||||
public @interface JsonPattern {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 限制 JSON 类型,默认为 {@link JsonType#ANY},即对象或数组都允许
 | 
			
		||||
     */
 | 
			
		||||
    JsonType type() default JsonType.ANY;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验失败时的提示消息
 | 
			
		||||
     */
 | 
			
		||||
    String message() default "不是有效的 JSON 格式";
 | 
			
		||||
 | 
			
		||||
    Class<?>[] groups() default {};
 | 
			
		||||
 | 
			
		||||
    Class<? extends Payload>[] payload() default {};
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
package org.dromara.common.json.validate;
 | 
			
		||||
 | 
			
		||||
import jakarta.validation.ConstraintValidator;
 | 
			
		||||
import jakarta.validation.ConstraintValidatorContext;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
import org.dromara.common.json.utils.JsonUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * JSON 格式校验器
 | 
			
		||||
 *
 | 
			
		||||
 * @author AprilWind
 | 
			
		||||
 */
 | 
			
		||||
public class JsonPatternValidator implements ConstraintValidator<JsonPattern, String> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 注解中指定的 JSON 类型枚举
 | 
			
		||||
     */
 | 
			
		||||
    private JsonType jsonType;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化校验器,从注解中提取 JSON 类型
 | 
			
		||||
     *
 | 
			
		||||
     * @param annotation 注解实例
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void initialize(JsonPattern annotation) {
 | 
			
		||||
        this.jsonType = annotation.type();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验字符串是否为合法 JSON
 | 
			
		||||
     *
 | 
			
		||||
     * @param value   待校验字符串
 | 
			
		||||
     * @param context 校验上下文,可用于自定义错误信息
 | 
			
		||||
     * @return true = 合法 JSON 或为空,false = 非法 JSON
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isValid(String value, ConstraintValidatorContext context) {
 | 
			
		||||
        if (StringUtils.isBlank(value)) {
 | 
			
		||||
            // 交给 @NotBlank 或 @NotNull 控制是否允许为空
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        // 根据 JSON 类型进行不同的校验
 | 
			
		||||
        return switch (jsonType) {
 | 
			
		||||
            case ANY -> JsonUtils.isJson(value);
 | 
			
		||||
            case OBJECT -> JsonUtils.isJsonObject(value);
 | 
			
		||||
            case ARRAY -> JsonUtils.isJsonArray(value);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package org.dromara.common.json.validate;
 | 
			
		||||
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * JSON 类型枚举
 | 
			
		||||
 *
 | 
			
		||||
 * @author AprilWind
 | 
			
		||||
 */
 | 
			
		||||
@Getter
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public enum JsonType {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * JSON 对象,例如 {"a":1}
 | 
			
		||||
     */
 | 
			
		||||
    OBJECT,
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * JSON 数组,例如 [1,2,3]
 | 
			
		||||
     */
 | 
			
		||||
    ARRAY,
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 任意 JSON 类型,对象或数组都可以
 | 
			
		||||
     */
 | 
			
		||||
    ANY
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -27,9 +27,7 @@ import org.springframework.http.HttpMethod;
 | 
			
		||||
import org.springframework.validation.BindingResult;
 | 
			
		||||
import org.springframework.web.multipart.MultipartFile;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.StringJoiner;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 操作日志记录处理
 | 
			
		||||
@@ -176,15 +174,29 @@ public class LogAspect {
 | 
			
		||||
        if (ArrayUtil.isEmpty(paramsArray)) {
 | 
			
		||||
            return params.toString();
 | 
			
		||||
        }
 | 
			
		||||
        String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES);
 | 
			
		||||
        for (Object o : paramsArray) {
 | 
			
		||||
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
 | 
			
		||||
                String str = JsonUtils.toJsonString(o);
 | 
			
		||||
                String str = "";
 | 
			
		||||
                if (o instanceof List<?> list) {
 | 
			
		||||
                    List<Dict> list1 = new ArrayList<>();
 | 
			
		||||
                    for (Object obj : list) {
 | 
			
		||||
                        String str1 = JsonUtils.toJsonString(obj);
 | 
			
		||||
                        Dict dict = JsonUtils.parseMap(str1);
 | 
			
		||||
                        if (MapUtil.isNotEmpty(dict)) {
 | 
			
		||||
                            MapUtil.removeAny(dict, exclude);
 | 
			
		||||
                            list1.add(dict);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    str = JsonUtils.toJsonString(list1);
 | 
			
		||||
                } else {
 | 
			
		||||
                    str = JsonUtils.toJsonString(o);
 | 
			
		||||
                    Dict dict = JsonUtils.parseMap(str);
 | 
			
		||||
                    if (MapUtil.isNotEmpty(dict)) {
 | 
			
		||||
                    MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
 | 
			
		||||
                    MapUtil.removeAny(dict, excludeParamNames);
 | 
			
		||||
                        MapUtil.removeAny(dict, exclude);
 | 
			
		||||
                        str = JsonUtils.toJsonString(dict);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                params.add(str);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
package org.dromara.common.mybatis.aspect;
 | 
			
		||||
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.aopalliance.intercept.MethodInterceptor;
 | 
			
		||||
import org.aopalliance.intercept.MethodInvocation;
 | 
			
		||||
import org.dromara.common.mybatis.annotation.DataPermission;
 | 
			
		||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.lang.reflect.Proxy;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 数据权限注解Advice
 | 
			
		||||
 *
 | 
			
		||||
 * @author 秋辞未寒
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class DataPermissionAdvice implements MethodInterceptor {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object invoke(MethodInvocation invocation) throws Throwable {
 | 
			
		||||
        Object target = invocation.getThis();
 | 
			
		||||
        Method method = invocation.getMethod();
 | 
			
		||||
        Object[] args = invocation.getArguments();
 | 
			
		||||
        // 设置权限注解
 | 
			
		||||
        DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method, args));
 | 
			
		||||
        try {
 | 
			
		||||
            // 执行代理方法
 | 
			
		||||
            return invocation.proceed();
 | 
			
		||||
        } finally {
 | 
			
		||||
            // 清除权限注解
 | 
			
		||||
            DataPermissionHelper.removePermission();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取数据权限注解
 | 
			
		||||
     */
 | 
			
		||||
    private DataPermission getDataPermissionAnnotation(Object target, Method method,Object[] args){
 | 
			
		||||
        DataPermission dataPermission = method.getAnnotation(DataPermission.class);
 | 
			
		||||
        // 优先获取方法上的注解
 | 
			
		||||
        if (dataPermission != null) {
 | 
			
		||||
            return dataPermission;
 | 
			
		||||
        }
 | 
			
		||||
        // 方法上没有注解,则获取类上的注解
 | 
			
		||||
        Class<?> targetClass = target.getClass();
 | 
			
		||||
        // 如果是 JDK 动态代理,则获取真实的Class实例
 | 
			
		||||
        if (Proxy.isProxyClass(targetClass)) {
 | 
			
		||||
            targetClass = targetClass.getInterfaces()[0];
 | 
			
		||||
        }
 | 
			
		||||
        dataPermission = targetClass.getAnnotation(DataPermission.class);
 | 
			
		||||
        return dataPermission;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,50 +0,0 @@
 | 
			
		||||
package org.dromara.common.mybatis.aspect;
 | 
			
		||||
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.aspectj.lang.JoinPoint;
 | 
			
		||||
import org.aspectj.lang.annotation.AfterReturning;
 | 
			
		||||
import org.aspectj.lang.annotation.AfterThrowing;
 | 
			
		||||
import org.aspectj.lang.annotation.Aspect;
 | 
			
		||||
import org.aspectj.lang.annotation.Before;
 | 
			
		||||
import org.dromara.common.mybatis.annotation.DataPermission;
 | 
			
		||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 数据权限处理
 | 
			
		||||
 *
 | 
			
		||||
 * @author Lion Li
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Aspect
 | 
			
		||||
public class DataPermissionAspect {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 处理请求前执行
 | 
			
		||||
     */
 | 
			
		||||
    @Before(value = "@annotation(dataPermission)")
 | 
			
		||||
    public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) {
 | 
			
		||||
        DataPermissionHelper.setPermission(dataPermission);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 处理完请求后执行
 | 
			
		||||
     *
 | 
			
		||||
     * @param joinPoint 切点
 | 
			
		||||
     */
 | 
			
		||||
    @AfterReturning(pointcut = "@annotation(dataPermission)")
 | 
			
		||||
    public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) {
 | 
			
		||||
        DataPermissionHelper.removePermission();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 拦截异常操作
 | 
			
		||||
     *
 | 
			
		||||
     * @param joinPoint 切点
 | 
			
		||||
     * @param e         异常
 | 
			
		||||
     */
 | 
			
		||||
    @AfterThrowing(value = "@annotation(dataPermission)", throwing = "e")
 | 
			
		||||
    public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) {
 | 
			
		||||
        DataPermissionHelper.removePermission();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
package org.dromara.common.mybatis.aspect;
 | 
			
		||||
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.dromara.common.mybatis.annotation.DataPermission;
 | 
			
		||||
import org.springframework.aop.support.StaticMethodMatcherPointcut;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.lang.reflect.Proxy;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 数据权限匹配切点
 | 
			
		||||
 *
 | 
			
		||||
 * @author 秋辞未寒
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@SuppressWarnings("all")
 | 
			
		||||
public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean matches(Method method, Class<?> targetClass) {
 | 
			
		||||
        // 优先匹配方法
 | 
			
		||||
        // 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
 | 
			
		||||
        if (method.isAnnotationPresent(DataPermission.class)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // MyBatis 的 Mapper 就是通过 JDK 动态代理实现的,所以这里需要检查是否匹配 JDK 的动态代理
 | 
			
		||||
        Class<?> targetClassRef = targetClass;
 | 
			
		||||
        if (Proxy.isProxyClass(targetClassRef)) {
 | 
			
		||||
            // 数据权限注解不对继承生效,但由于 SpringIOC 容器拿到的实际上是 MyBatis 代理过后的 Mapper,而 targetClass.isAnnotationPresent 实际匹配的是 Proxy 类的注解,不会查找代理类。
 | 
			
		||||
            // 所以这里不能用 targetClass.isAnnotationPresent,只能用 AnnotatedElementUtils.hasAnnotation 或 targetClass.getInterfaces()[0].isAnnotationPresent 去做匹配,以检查被代理的 MapperClass 是否具有注解
 | 
			
		||||
            // 原理:JDK 动态代理本质上就是对接口进行实现然后对具体的接口实现做代理,所以直接通过接口可以拿到实际的 MapperClass
 | 
			
		||||
            targetClassRef = targetClass.getInterfaces()[0];
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        return targetClassRef.isAnnotationPresent(DataPermission.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
package org.dromara.common.mybatis.aspect;
 | 
			
		||||
 | 
			
		||||
import org.aopalliance.aop.Advice;
 | 
			
		||||
import org.springframework.aop.Pointcut;
 | 
			
		||||
import org.springframework.aop.support.AbstractPointcutAdvisor;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 数据权限注解切面定义
 | 
			
		||||
 *
 | 
			
		||||
 * @author 秋辞未寒
 | 
			
		||||
 */
 | 
			
		||||
@SuppressWarnings("all")
 | 
			
		||||
public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor {
 | 
			
		||||
 | 
			
		||||
    private final Advice advice;
 | 
			
		||||
    private final Pointcut pointcut;
 | 
			
		||||
 | 
			
		||||
    public DataPermissionPointcutAdvisor() {
 | 
			
		||||
        this.advice = new DataPermissionAdvice();
 | 
			
		||||
        this.pointcut =  new DataPermissionPointcut();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Pointcut getPointcut() {
 | 
			
		||||
        return this.pointcut;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Advice getAdvice() {
 | 
			
		||||
        return this.advice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -11,15 +11,17 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
 | 
			
		||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
 | 
			
		||||
import org.dromara.common.core.factory.YmlPropertySourceFactory;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.dromara.common.mybatis.aspect.DataPermissionAspect;
 | 
			
		||||
import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor;
 | 
			
		||||
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
 | 
			
		||||
import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
 | 
			
		||||
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
 | 
			
		||||
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
 | 
			
		||||
import org.mybatis.spring.annotation.MapperScan;
 | 
			
		||||
import org.springframework.beans.BeansException;
 | 
			
		||||
import org.springframework.beans.factory.config.BeanDefinition;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.PropertySource;
 | 
			
		||||
import org.springframework.context.annotation.Role;
 | 
			
		||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -27,6 +29,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 | 
			
		||||
 *
 | 
			
		||||
 * @author Lion Li
 | 
			
		||||
 */
 | 
			
		||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | 
			
		||||
@EnableTransactionManagement(proxyTargetClass = true)
 | 
			
		||||
@MapperScan("${mybatis-plus.mapperPackage}")
 | 
			
		||||
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
 | 
			
		||||
@@ -54,15 +57,16 @@ public class MybatisPlusConfig {
 | 
			
		||||
     * 数据权限拦截器
 | 
			
		||||
     */
 | 
			
		||||
    public PlusDataPermissionInterceptor dataPermissionInterceptor() {
 | 
			
		||||
        return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
 | 
			
		||||
        return new PlusDataPermissionInterceptor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 数据权限切面处理器
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    public DataPermissionAspect dataPermissionAspect() {
 | 
			
		||||
        return new DataPermissionAspect();
 | 
			
		||||
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
 | 
			
		||||
    public DataPermissionPointcutAdvisor dataPermissionPointcutAdvisor() {
 | 
			
		||||
        return new DataPermissionPointcutAdvisor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -42,17 +42,46 @@ public enum DataBaseType {
 | 
			
		||||
     * 根据数据库产品名称查找对应的数据库类型
 | 
			
		||||
     *
 | 
			
		||||
     * @param databaseProductName 数据库产品名称
 | 
			
		||||
     * @return 对应的数据库类型枚举值,如果未找到则返回 null
 | 
			
		||||
     * @return 对应的数据库类型枚举值
 | 
			
		||||
     */
 | 
			
		||||
    public static DataBaseType find(String databaseProductName) {
 | 
			
		||||
        if (StringUtils.isBlank(databaseProductName)) {
 | 
			
		||||
            return null;
 | 
			
		||||
            return MY_SQL;
 | 
			
		||||
        }
 | 
			
		||||
        for (DataBaseType type : values()) {
 | 
			
		||||
            if (type.getType().equals(databaseProductName)) {
 | 
			
		||||
                return type;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
        return MY_SQL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否为 MySQL 类型
 | 
			
		||||
     */
 | 
			
		||||
    public boolean isMySql() {
 | 
			
		||||
        return this == MY_SQL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否为 Oracle 类型
 | 
			
		||||
     */
 | 
			
		||||
    public boolean isOracle() {
 | 
			
		||||
        return this == ORACLE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否为 PostgreSQL 类型
 | 
			
		||||
     */
 | 
			
		||||
    public boolean isPostgreSql() {
 | 
			
		||||
        return this == POSTGRE_SQL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 判断是否为 SQL Server 类型
 | 
			
		||||
     */
 | 
			
		||||
    public boolean isSqlServer() {
 | 
			
		||||
        return this == SQL_SERVER;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,11 @@ import java.util.Date;
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class InjectionMetaObjectHandler implements MetaObjectHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 如果用户不存在默认注入-1代表无用户
 | 
			
		||||
     */
 | 
			
		||||
    private static final Long DEFAULT_USER_ID = -1L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 插入填充方法,用于在插入数据时自动填充实体对象中的创建时间、更新时间、创建人、更新人等信息
 | 
			
		||||
     *
 | 
			
		||||
@@ -45,6 +50,11 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
 | 
			
		||||
                        baseEntity.setCreateBy(userId);
 | 
			
		||||
                        baseEntity.setUpdateBy(userId);
 | 
			
		||||
                        baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // 填充创建人、更新人和创建部门信息
 | 
			
		||||
                        baseEntity.setCreateBy(DEFAULT_USER_ID);
 | 
			
		||||
                        baseEntity.setUpdateBy(DEFAULT_USER_ID);
 | 
			
		||||
                        baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), DEFAULT_USER_ID));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -74,6 +84,8 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
 | 
			
		||||
                Long userId = LoginHelper.getUserId();
 | 
			
		||||
                if (ObjectUtil.isNotNull(userId)) {
 | 
			
		||||
                    baseEntity.setUpdateBy(userId);
 | 
			
		||||
                } else {
 | 
			
		||||
                    baseEntity.setUpdateBy(DEFAULT_USER_ID);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
 | 
			
		||||
@@ -93,7 +105,6 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
 | 
			
		||||
        try {
 | 
			
		||||
            loginUser = LoginHelper.getLoginUser();
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.warn("自动注入警告 => 用户未登录");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return loginUser;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
package org.dromara.common.mybatis.handler;
 | 
			
		||||
 | 
			
		||||
import cn.dev33.satoken.exception.NotLoginException;
 | 
			
		||||
import cn.hutool.http.HttpStatus;
 | 
			
		||||
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
 | 
			
		||||
import jakarta.servlet.http.HttpServletRequest;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.dromara.common.core.domain.R;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
import org.mybatis.spring.MyBatisSystemException;
 | 
			
		||||
import org.springframework.dao.DuplicateKeyException;
 | 
			
		||||
import org.springframework.web.bind.annotation.ExceptionHandler;
 | 
			
		||||
@@ -35,13 +36,54 @@ public class MybatisExceptionHandler {
 | 
			
		||||
    @ExceptionHandler(MyBatisSystemException.class)
 | 
			
		||||
    public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
 | 
			
		||||
        String requestURI = request.getRequestURI();
 | 
			
		||||
        String message = e.getMessage();
 | 
			
		||||
        if (StringUtils.contains(message, "CannotFindDataSourceException")) {
 | 
			
		||||
        Throwable root = getRootCause(e);
 | 
			
		||||
        if (root instanceof NotLoginException) {
 | 
			
		||||
            log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage());
 | 
			
		||||
            return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
 | 
			
		||||
        }
 | 
			
		||||
        if (root instanceof CannotFindDataSourceException) {
 | 
			
		||||
            log.error("请求地址'{}', 未找到数据源", requestURI);
 | 
			
		||||
            return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
 | 
			
		||||
        }
 | 
			
		||||
        log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
 | 
			
		||||
        return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message);
 | 
			
		||||
        return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取异常的根因(递归查找)
 | 
			
		||||
     *
 | 
			
		||||
     * @param e 当前异常
 | 
			
		||||
     * @return 根因异常(最底层的 cause)
 | 
			
		||||
     * <p>
 | 
			
		||||
     * 逻辑说明:
 | 
			
		||||
     * 1. 如果 e 没有 cause,说明 e 本身就是根因,直接返回
 | 
			
		||||
     * 2. 如果 e 的 cause 和自身相同(防止循环引用),也返回 e
 | 
			
		||||
     * 3. 否则递归调用,继续向下寻找最底层的 cause
 | 
			
		||||
     */
 | 
			
		||||
    public static Throwable getRootCause(Throwable e) {
 | 
			
		||||
        Throwable cause = e.getCause();
 | 
			
		||||
        if (cause == null || cause == e) {
 | 
			
		||||
            return e;
 | 
			
		||||
        }
 | 
			
		||||
        return getRootCause(cause);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 在异常链中查找指定类型的异常
 | 
			
		||||
     *
 | 
			
		||||
     * @param e     当前异常
 | 
			
		||||
     * @param clazz 目标异常类
 | 
			
		||||
     * @return 找到的指定类型异常,如果没有找到返回 null
 | 
			
		||||
     */
 | 
			
		||||
    public static Throwable findCause(Throwable e, Class<? extends Throwable> clazz) {
 | 
			
		||||
        Throwable t = e;
 | 
			
		||||
        while (t != null && t != t.getCause()) {
 | 
			
		||||
            if (clazz.isInstance(t)) {
 | 
			
		||||
                return t;
 | 
			
		||||
            }
 | 
			
		||||
            t = t.getCause();
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package org.dromara.common.mybatis.handler;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.annotation.AnnotationUtil;
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
@@ -10,7 +9,6 @@ import net.sf.jsqlparser.expression.Expression;
 | 
			
		||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
 | 
			
		||||
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
 | 
			
		||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
 | 
			
		||||
import org.apache.ibatis.io.Resources;
 | 
			
		||||
import org.dromara.common.core.domain.dto.RoleDTO;
 | 
			
		||||
import org.dromara.common.core.domain.model.LoginUser;
 | 
			
		||||
import org.dromara.common.core.exception.ServiceException;
 | 
			
		||||
@@ -22,22 +20,13 @@ import org.dromara.common.mybatis.annotation.DataPermission;
 | 
			
		||||
import org.dromara.common.mybatis.enums.DataScopeType;
 | 
			
		||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
 | 
			
		||||
import org.dromara.common.satoken.utils.LoginHelper;
 | 
			
		||||
import org.springframework.context.ConfigurableApplicationContext;
 | 
			
		||||
import org.springframework.context.expression.BeanFactoryResolver;
 | 
			
		||||
import org.springframework.core.io.Resource;
 | 
			
		||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 | 
			
		||||
import org.springframework.core.io.support.ResourcePatternResolver;
 | 
			
		||||
import org.springframework.core.type.ClassMetadata;
 | 
			
		||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
 | 
			
		||||
import org.springframework.expression.*;
 | 
			
		||||
import org.springframework.expression.common.TemplateParserContext;
 | 
			
		||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
 | 
			
		||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
 | 
			
		||||
import org.springframework.util.ClassUtils;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -49,11 +38,6 @@ import java.util.function.Function;
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class PlusDataPermissionHandler {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
 | 
			
		||||
     */
 | 
			
		||||
    private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * spel 解析器
 | 
			
		||||
     */
 | 
			
		||||
@@ -64,27 +48,17 @@ public class PlusDataPermissionHandler {
 | 
			
		||||
     */
 | 
			
		||||
    private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构造方法,扫描指定包下的 Mapper 类并初始化缓存
 | 
			
		||||
     *
 | 
			
		||||
     * @param mapperPackage Mapper 类所在的包路径
 | 
			
		||||
     */
 | 
			
		||||
    public PlusDataPermissionHandler(String mapperPackage) {
 | 
			
		||||
        scanMapperClasses(mapperPackage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取数据过滤条件的 SQL 片段
 | 
			
		||||
     *
 | 
			
		||||
     * @param where             原始的查询条件表达式
 | 
			
		||||
     * @param mappedStatementId Mapper 方法的 ID
 | 
			
		||||
     * @param isSelect          是否为查询语句
 | 
			
		||||
     * @return 数据过滤条件的 SQL 片段
 | 
			
		||||
     */
 | 
			
		||||
    public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
 | 
			
		||||
    public Expression getSqlSegment(Expression where, boolean isSelect) {
 | 
			
		||||
        try {
 | 
			
		||||
            // 获取数据权限配置
 | 
			
		||||
            DataPermission dataPermission = getDataPermission(mappedStatementId);
 | 
			
		||||
            DataPermission dataPermission = getDataPermission();
 | 
			
		||||
            // 获取当前登录用户信息
 | 
			
		||||
            LoginUser currentUser = DataPermissionHelper.getVariable("user");
 | 
			
		||||
            if (ObjectUtil.isNull(currentUser)) {
 | 
			
		||||
@@ -206,92 +180,22 @@ public class PlusDataPermissionHandler {
 | 
			
		||||
        return StringUtils.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类
 | 
			
		||||
     *
 | 
			
		||||
     * @param mapperPackage Mapper 类所在的包路径
 | 
			
		||||
     */
 | 
			
		||||
    private void scanMapperClasses(String mapperPackage) {
 | 
			
		||||
        // 创建资源解析器和元数据读取工厂
 | 
			
		||||
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
 | 
			
		||||
        CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
 | 
			
		||||
        // 将 Mapper 包路径按分隔符拆分为数组
 | 
			
		||||
        String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
 | 
			
		||||
        String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
 | 
			
		||||
        try {
 | 
			
		||||
            for (String packagePattern : packagePatternArray) {
 | 
			
		||||
                // 将包路径转换为资源路径
 | 
			
		||||
                String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
 | 
			
		||||
                // 获取指定路径下的所有 .class 文件资源
 | 
			
		||||
                Resource[] resources = resolver.getResources(classpath + path + "/*.class");
 | 
			
		||||
                for (Resource resource : resources) {
 | 
			
		||||
                    // 获取资源的类元数据
 | 
			
		||||
                    ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
 | 
			
		||||
                    // 获取资源对应的类对象
 | 
			
		||||
                    Class<?> clazz = Resources.classForName(classMetadata.getClassName());
 | 
			
		||||
                    // 查找类中的特定注解
 | 
			
		||||
                    findAnnotation(clazz);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            log.error("初始化数据安全缓存时出错:{}", e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 在指定的类中查找特定的注解 DataPermission,并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中
 | 
			
		||||
     *
 | 
			
		||||
     * @param clazz 要查找的类
 | 
			
		||||
     */
 | 
			
		||||
    private void findAnnotation(Class<?> clazz) {
 | 
			
		||||
        DataPermission dataPermission;
 | 
			
		||||
        for (Method method : clazz.getMethods()) {
 | 
			
		||||
            if (method.isDefault() || method.isVarArgs()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            String mappedStatementId = clazz.getName() + "." + method.getName();
 | 
			
		||||
            if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
 | 
			
		||||
                dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
 | 
			
		||||
                dataPermissionCacheMap.put(mappedStatementId, dataPermission);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
 | 
			
		||||
            dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
 | 
			
		||||
            dataPermissionCacheMap.put(clazz.getName(), dataPermission);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
 | 
			
		||||
     *
 | 
			
		||||
     * @param mapperId 映射语句 ID
 | 
			
		||||
     * @return DataPermission 注解对象,如果不存在则返回 null
 | 
			
		||||
     */
 | 
			
		||||
    public DataPermission getDataPermission(String mapperId) {
 | 
			
		||||
        // 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
 | 
			
		||||
        if (DataPermissionHelper.getPermission() != null) {
 | 
			
		||||
    public DataPermission getDataPermission() {
 | 
			
		||||
        return DataPermissionHelper.getPermission();
 | 
			
		||||
    }
 | 
			
		||||
        // 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象
 | 
			
		||||
        if (dataPermissionCacheMap.containsKey(mapperId)) {
 | 
			
		||||
            return dataPermissionCacheMap.get(mapperId);
 | 
			
		||||
        }
 | 
			
		||||
        // 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找
 | 
			
		||||
        String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
 | 
			
		||||
        if (dataPermissionCacheMap.containsKey(clazzName)) {
 | 
			
		||||
            return dataPermissionCacheMap.get(clazzName);
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
 | 
			
		||||
     *
 | 
			
		||||
     * @param mapperId 映射语句 ID
 | 
			
		||||
     * @return 如果找到对应的 DataPermission 注解对象,则返回 false;否则返回 true
 | 
			
		||||
     */
 | 
			
		||||
    public boolean invalid(String mapperId) {
 | 
			
		||||
        return getDataPermission(mapperId) == null;
 | 
			
		||||
    public boolean invalid() {
 | 
			
		||||
        return getDataPermission() == null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,14 @@ public class DataBaseHelper {
 | 
			
		||||
    private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取当前数据库类型
 | 
			
		||||
     * 获取当前数据源对应的数据库类型
 | 
			
		||||
     * <p>
 | 
			
		||||
     * 通过 DynamicRoutingDataSource 获取当前线程绑定的数据源,
 | 
			
		||||
     * 然后从数据源获取数据库连接,利用连接的元数据获取数据库产品名称,
 | 
			
		||||
     * 最后调用 DataBaseType.find 方法将数据库名称转换为对应的枚举类型
 | 
			
		||||
     *
 | 
			
		||||
     * @return 当前数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL
 | 
			
		||||
     * @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常
 | 
			
		||||
     */
 | 
			
		||||
    public static DataBaseType getDataBaseType() {
 | 
			
		||||
        DataSource dataSource = DS.determineDataSource();
 | 
			
		||||
@@ -39,37 +46,31 @@ public class DataBaseHelper {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isMySql() {
 | 
			
		||||
        return DataBaseType.MY_SQL == getDataBaseType();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isOracle() {
 | 
			
		||||
        return DataBaseType.ORACLE == getDataBaseType();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isPostgerSql() {
 | 
			
		||||
        return DataBaseType.POSTGRE_SQL == getDataBaseType();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static boolean isSqlServer() {
 | 
			
		||||
        return DataBaseType.SQL_SERVER == getDataBaseType();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段
 | 
			
		||||
     * <p>
 | 
			
		||||
     * 用于判断指定值是否存在于逗号分隔的字符串列中,SQL写法根据不同数据库方言自动切换:
 | 
			
		||||
     * - Oracle 使用 instr 函数
 | 
			
		||||
     * - PostgreSQL 使用 strpos 函数
 | 
			
		||||
     * - SQL Server 使用 charindex 函数
 | 
			
		||||
     * - 其他默认使用 MySQL 的 find_in_set 函数
 | 
			
		||||
     *
 | 
			
		||||
     * @param var1 要查找的值(支持任意类型,内部会转换成字符串)
 | 
			
		||||
     * @param var2 存储逗号分隔值的数据库列名
 | 
			
		||||
     * @return 适用于当前数据库的 SQL 条件字符串,通常用于 where 或 apply 中拼接
 | 
			
		||||
     */
 | 
			
		||||
    public static String findInSet(Object var1, String var2) {
 | 
			
		||||
        DataBaseType dataBasyType = getDataBaseType();
 | 
			
		||||
        String var = Convert.toStr(var1);
 | 
			
		||||
        if (dataBasyType == DataBaseType.SQL_SERVER) {
 | 
			
		||||
            // charindex(',100,' , ',0,100,101,') <> 0
 | 
			
		||||
            return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
 | 
			
		||||
        } else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
 | 
			
		||||
            // (select strpos(',0,100,101,' , ',100,')) <> 0
 | 
			
		||||
            return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
 | 
			
		||||
        } else if (dataBasyType == DataBaseType.ORACLE) {
 | 
			
		||||
        return switch (getDataBaseType()) {
 | 
			
		||||
            // instr(',0,100,101,' , ',100,') <> 0
 | 
			
		||||
            return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
 | 
			
		||||
        }
 | 
			
		||||
            case ORACLE -> "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
 | 
			
		||||
            // (select strpos(',0,100,101,' , ',100,')) <> 0
 | 
			
		||||
            case POSTGRE_SQL -> "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
 | 
			
		||||
            // charindex(',100,' , ',0,100,101,') <> 0
 | 
			
		||||
            case SQL_SERVER -> "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
 | 
			
		||||
            // find_in_set(100 , '0,100,101')
 | 
			
		||||
        return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
 | 
			
		||||
            default -> "find_in_set('%s' , %s) <> 0".formatted(var, var2);
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -35,16 +35,7 @@ import java.util.List;
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
 | 
			
		||||
 | 
			
		||||
    private final PlusDataPermissionHandler dataPermissionHandler;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 构造函数,初始化 PlusDataPermissionHandler 实例
 | 
			
		||||
     *
 | 
			
		||||
     * @param mapperPackage 扫描的映射器包
 | 
			
		||||
     */
 | 
			
		||||
    public PlusDataPermissionInterceptor(String mapperPackage) {
 | 
			
		||||
        this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage);
 | 
			
		||||
    }
 | 
			
		||||
    private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 在执行查询之前,检查并处理数据权限相关逻辑
 | 
			
		||||
@@ -64,7 +55,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // 检查是否缺少有效的数据权限注解
 | 
			
		||||
        if (dataPermissionHandler.invalid(ms.getId())) {
 | 
			
		||||
        if (dataPermissionHandler.invalid()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // 解析 sql 分配对应方法
 | 
			
		||||
@@ -92,7 +83,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            // 检查是否缺少有效的数据权限注解
 | 
			
		||||
            if (dataPermissionHandler.invalid(ms.getId())) {
 | 
			
		||||
            if (dataPermissionHandler.invalid()) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
 | 
			
		||||
@@ -128,7 +119,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void processUpdate(Update update, int index, String sql, Object obj) {
 | 
			
		||||
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
 | 
			
		||||
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), false);
 | 
			
		||||
        if (null != sqlSegment) {
 | 
			
		||||
            update.setWhere(sqlSegment);
 | 
			
		||||
        }
 | 
			
		||||
@@ -144,7 +135,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
 | 
			
		||||
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
 | 
			
		||||
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), false);
 | 
			
		||||
        if (null != sqlSegment) {
 | 
			
		||||
            delete.setWhere(sqlSegment);
 | 
			
		||||
        }
 | 
			
		||||
@@ -157,7 +148,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
 | 
			
		||||
     * @param mappedStatementId 映射语句的 ID
 | 
			
		||||
     */
 | 
			
		||||
    protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
 | 
			
		||||
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
 | 
			
		||||
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), true);
 | 
			
		||||
        if (null != sqlSegment) {
 | 
			
		||||
            plainSelect.setWhere(sqlSegment);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ package org.dromara.common.oss.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.io.IoUtil;
 | 
			
		||||
import cn.hutool.core.util.IdUtil;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.dromara.common.core.constant.Constants;
 | 
			
		||||
import org.dromara.common.core.utils.DateUtils;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
@@ -13,9 +14,7 @@ import org.dromara.common.oss.exception.OssException;
 | 
			
		||||
import org.dromara.common.oss.properties.OssProperties;
 | 
			
		||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
 | 
			
		||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
 | 
			
		||||
import software.amazon.awssdk.core.ResponseInputStream;
 | 
			
		||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
 | 
			
		||||
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
 | 
			
		||||
import software.amazon.awssdk.core.async.*;
 | 
			
		||||
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
 | 
			
		||||
import software.amazon.awssdk.regions.Region;
 | 
			
		||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
 | 
			
		||||
@@ -29,9 +28,12 @@ import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
 | 
			
		||||
import java.io.*;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URL;
 | 
			
		||||
import java.nio.channels.Channels;
 | 
			
		||||
import java.nio.channels.WritableByteChannel;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -40,6 +42,7 @@ import java.util.function.Consumer;
 | 
			
		||||
 *
 | 
			
		||||
 * @author AprilWind
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class OssClient {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -177,12 +180,12 @@ public class OssClient {
 | 
			
		||||
            // 创建异步请求体(length如果为空会报错)
 | 
			
		||||
            BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
 | 
			
		||||
                .contentLength(length)
 | 
			
		||||
                .subscribeTimeout(Duration.ofSeconds(30))
 | 
			
		||||
                .subscribeTimeout(Duration.ofSeconds(120))
 | 
			
		||||
                .build();
 | 
			
		||||
 | 
			
		||||
            // 使用 transferManager 进行上传
 | 
			
		||||
            Upload upload = transferManager.upload(
 | 
			
		||||
                x -> x.requestBody(body)
 | 
			
		||||
                x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create())
 | 
			
		||||
                    .putObjectRequest(
 | 
			
		||||
                        y -> y.bucket(properties.getBucketName())
 | 
			
		||||
                            .key(key)
 | 
			
		||||
@@ -237,30 +240,61 @@ public class OssClient {
 | 
			
		||||
     * @param key 文件在 Amazon S3 中的对象键
 | 
			
		||||
     * @param out 输出流
 | 
			
		||||
     * @param consumer 自定义处理逻辑
 | 
			
		||||
     * @return 输出流中写入的字节数(长度)
 | 
			
		||||
     * @throws OssException 如果下载失败,抛出自定义异常
 | 
			
		||||
     */
 | 
			
		||||
    public void download(String key, OutputStream out, Consumer<Long> consumer) {
 | 
			
		||||
        try {
 | 
			
		||||
            this.download(key, consumer).writeTo(out);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 下载文件从 Amazon S3 到 输出流
 | 
			
		||||
     *
 | 
			
		||||
     * @param key 文件在 Amazon S3 中的对象键
 | 
			
		||||
     * @param contentLengthConsumer 文件大小消费者函数
 | 
			
		||||
     * @return 写出订阅器
 | 
			
		||||
     * @throws OssException 如果下载失败,抛出自定义异常
 | 
			
		||||
     */
 | 
			
		||||
    public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) {
 | 
			
		||||
        try {
 | 
			
		||||
            // 构建下载请求
 | 
			
		||||
            DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
 | 
			
		||||
            DownloadRequest<ResponsePublisher<GetObjectResponse>> publisherDownloadRequest = DownloadRequest.builder()
 | 
			
		||||
                // 文件对象
 | 
			
		||||
                .getObjectRequest(y -> y.bucket(properties.getBucketName())
 | 
			
		||||
                    .key(key)
 | 
			
		||||
                    .build())
 | 
			
		||||
                .addTransferListener(LoggingTransferListener.create())
 | 
			
		||||
                // 使用订阅转换器
 | 
			
		||||
                .responseTransformer(AsyncResponseTransformer.toBlockingInputStream())
 | 
			
		||||
                // 使用发布订阅转换器
 | 
			
		||||
                .responseTransformer(AsyncResponseTransformer.toPublisher())
 | 
			
		||||
                .build();
 | 
			
		||||
 | 
			
		||||
            // 使用 S3TransferManager 下载文件
 | 
			
		||||
            Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest);
 | 
			
		||||
            // 输出到流中
 | 
			
		||||
            try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream
 | 
			
		||||
                if (consumer != null) {
 | 
			
		||||
                    consumer.accept(responseStream.response().contentLength());
 | 
			
		||||
            Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(publisherDownloadRequest);
 | 
			
		||||
            // 获取下载发布订阅转换器
 | 
			
		||||
            ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result();
 | 
			
		||||
            // 执行文件大小消费者函数
 | 
			
		||||
            Optional.ofNullable(contentLengthConsumer)
 | 
			
		||||
                .ifPresent(lengthConsumer -> lengthConsumer.accept(publisher.response().contentLength()));
 | 
			
		||||
 | 
			
		||||
            // 构建写出订阅器对象
 | 
			
		||||
            return out -> {
 | 
			
		||||
                // 创建可写入的字节通道
 | 
			
		||||
                try(WritableByteChannel channel = Channels.newChannel(out)){
 | 
			
		||||
                    // 订阅数据
 | 
			
		||||
                    publisher.subscribe(byteBuffer -> {
 | 
			
		||||
                        while (byteBuffer.hasRemaining()) {
 | 
			
		||||
                            try {
 | 
			
		||||
                                channel.write(byteBuffer);
 | 
			
		||||
                            } catch (IOException e) {
 | 
			
		||||
                                throw new RuntimeException(e);
 | 
			
		||||
                            }
 | 
			
		||||
                responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread
 | 
			
		||||
                        }
 | 
			
		||||
                    }).join();
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
package org.dromara.common.oss.core;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 写出订阅器
 | 
			
		||||
 *
 | 
			
		||||
 * @author 秋辞未寒
 | 
			
		||||
 */
 | 
			
		||||
@FunctionalInterface
 | 
			
		||||
public interface WriteOutSubscriber<T> {
 | 
			
		||||
 | 
			
		||||
    void writeTo(T out) throws IOException;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,9 @@ import java.util.function.Function;
 | 
			
		||||
 *
 | 
			
		||||
 * @author Lion Li
 | 
			
		||||
 * @version 3.6.0 新增
 | 
			
		||||
 * @deprecated redisson 新版本已经将队列功能标记删除 一些技术问题无法解决 建议搭建MQ使用
 | 
			
		||||
 */
 | 
			
		||||
@Deprecated
 | 
			
		||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
 | 
			
		||||
public class QueueUtils {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -129,9 +129,9 @@ public class RedisUtils {
 | 
			
		||||
            } catch (Exception e) {
 | 
			
		||||
                long timeToLive = bucket.remainTimeToLive();
 | 
			
		||||
                if (timeToLive == -1) {
 | 
			
		||||
                    setCacheObject(key, value);
 | 
			
		||||
                    bucket.set(value);
 | 
			
		||||
                } else {
 | 
			
		||||
                    setCacheObject(key, value, Duration.ofMillis(timeToLive));
 | 
			
		||||
                    bucket.set(value, Duration.ofMillis(timeToLive));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -147,11 +147,8 @@ public class RedisUtils {
 | 
			
		||||
     * @param duration 时间
 | 
			
		||||
     */
 | 
			
		||||
    public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
 | 
			
		||||
        RBatch batch = CLIENT.createBatch();
 | 
			
		||||
        RBucketAsync<T> bucket = batch.getBucket(key);
 | 
			
		||||
        bucket.setAsync(value);
 | 
			
		||||
        bucket.expireAsync(duration);
 | 
			
		||||
        batch.execute();
 | 
			
		||||
        RBucket<T> bucket = CLIENT.getBucket(key);
 | 
			
		||||
        bucket.set(value, duration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,19 @@
 | 
			
		||||
package org.dromara.common.redis.utils;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.convert.Convert;
 | 
			
		||||
import cn.hutool.core.date.DatePattern;
 | 
			
		||||
import cn.hutool.core.date.DateUtil;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
import lombok.AccessLevel;
 | 
			
		||||
import lombok.NoArgsConstructor;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
import org.redisson.api.RIdGenerator;
 | 
			
		||||
import org.redisson.api.RedissonClient;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
import java.time.LocalDate;
 | 
			
		||||
import java.time.LocalDateTime;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
import java.time.temporal.TemporalAccessor;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 发号器工具类
 | 
			
		||||
@@ -23,20 +27,28 @@ public class SequenceUtils {
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认初始值
 | 
			
		||||
     */
 | 
			
		||||
    public static final Long DEFAULT_INIT_VALUE = 1L;
 | 
			
		||||
    public static final long DEFAULT_INIT_VALUE = 1L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认步长
 | 
			
		||||
     */
 | 
			
		||||
    public static final Long DEFAULT_STEP_VALUE = 1L;
 | 
			
		||||
    public static final long DEFAULT_STEP_VALUE = 1L;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认过期时间-天
 | 
			
		||||
     */
 | 
			
		||||
    public static final Duration DEFAULT_EXPIRE_TIME_DAY = Duration.ofDays(1);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认过期时间-分钟
 | 
			
		||||
     */
 | 
			
		||||
    public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认最小ID容量位数 - 6位数(即至少可以生成的ID为999999个)
 | 
			
		||||
     */
 | 
			
		||||
    public static final int DEFAULT_MIN_ID_CAPACITY_BITS = 6;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取Redisson客户端实例
 | 
			
		||||
     */
 | 
			
		||||
@@ -51,14 +63,11 @@ public class SequenceUtils {
 | 
			
		||||
     * @param stepValue  ID步长
 | 
			
		||||
     * @return ID生成器
 | 
			
		||||
     */
 | 
			
		||||
    private static RIdGenerator getIdGenerator(String key, Duration expireTime, Long initValue, Long stepValue) {
 | 
			
		||||
        if (initValue == null || initValue <= 0) {
 | 
			
		||||
            initValue = DEFAULT_INIT_VALUE;
 | 
			
		||||
        }
 | 
			
		||||
        if (stepValue == null || stepValue <= 0) {
 | 
			
		||||
            stepValue = DEFAULT_STEP_VALUE;
 | 
			
		||||
        }
 | 
			
		||||
    public static RIdGenerator getIdGenerator(String key, Duration expireTime, long initValue, long stepValue) {
 | 
			
		||||
        RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
 | 
			
		||||
        // 初始值和步长不能小于等于0
 | 
			
		||||
        initValue = initValue <= 0 ? DEFAULT_INIT_VALUE : initValue;
 | 
			
		||||
        stepValue = stepValue <= 0 ? DEFAULT_STEP_VALUE : stepValue;
 | 
			
		||||
        // 设置初始值和步长
 | 
			
		||||
        idGenerator.tryInit(initValue, stepValue);
 | 
			
		||||
        // 设置过期时间
 | 
			
		||||
@@ -66,6 +75,17 @@ public class SequenceUtils {
 | 
			
		||||
        return idGenerator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取ID生成器
 | 
			
		||||
     *
 | 
			
		||||
     * @param key        业务key
 | 
			
		||||
     * @param expireTime 过期时间
 | 
			
		||||
     * @return ID生成器
 | 
			
		||||
     */
 | 
			
		||||
    public static RIdGenerator getIdGenerator(String key, Duration expireTime) {
 | 
			
		||||
        return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取指定业务key的唯一id
 | 
			
		||||
     *
 | 
			
		||||
@@ -75,10 +95,21 @@ public class SequenceUtils {
 | 
			
		||||
     * @param stepValue  ID步长
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) {
 | 
			
		||||
    public static long getNextId(String key, Duration expireTime, long initValue, long stepValue) {
 | 
			
		||||
        return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
 | 
			
		||||
     *
 | 
			
		||||
     * @param key        业务key
 | 
			
		||||
     * @param expireTime 过期时间
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static long getNextId(String key, Duration expireTime) {
 | 
			
		||||
        return getIdGenerator(key, expireTime).nextId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取指定业务key的唯一id字符串
 | 
			
		||||
     *
 | 
			
		||||
@@ -88,19 +119,8 @@ public class SequenceUtils {
 | 
			
		||||
     * @param stepValue  ID步长
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) {
 | 
			
		||||
        return String.valueOf(nextId(key, expireTime, initValue, stepValue));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
 | 
			
		||||
     *
 | 
			
		||||
     * @param key        业务key
 | 
			
		||||
     * @param expireTime 过期时间
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static long nextId(String key, Duration expireTime) {
 | 
			
		||||
        return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
 | 
			
		||||
    public static String getNextIdString(String key, Duration expireTime, long initValue, long stepValue) {
 | 
			
		||||
        return Convert.toStr(getNextId(key, expireTime, initValue, stepValue));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -110,56 +130,222 @@ public class SequenceUtils {
 | 
			
		||||
     * @param expireTime 过期时间
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String nextIdStr(String key, Duration expireTime) {
 | 
			
		||||
        return String.valueOf(nextId(key, expireTime));
 | 
			
		||||
    public static String getNextIdString(String key, Duration expireTime) {
 | 
			
		||||
        return Convert.toStr(getNextId(key, expireTime));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 yyyyMMdd 开头的唯一id
 | 
			
		||||
     * 获取指定业务key的唯一id字符串 (ID初始值=1,ID步长=1),不足位数自动补零
 | 
			
		||||
     *
 | 
			
		||||
     * @param key        业务key
 | 
			
		||||
     * @param expireTime 过期时间
 | 
			
		||||
     * @param width      位数,不足左补0
 | 
			
		||||
     * @return 补零后的唯一id字符串
 | 
			
		||||
     */
 | 
			
		||||
    public static String getPaddedNextIdString(String key, Duration expireTime, Integer width) {
 | 
			
		||||
        return StringUtils.leftPad(getNextIdString(key, expireTime), width, '0');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 yyyyMMdd 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     * @deprecated 请使用 {@link #getDateId(String)} 或 {@link #getDateId(String, boolean)}、{@link #getDateId(String, boolean, int)},确保不同业务的ID连续性
 | 
			
		||||
     */
 | 
			
		||||
    public static String nextIdDate() {
 | 
			
		||||
        return nextIdDate("");
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public static String getDateId() {
 | 
			
		||||
        return getDateId("");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 开头的唯一id
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix 业务前缀
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String nextIdDate(String prefix) {
 | 
			
		||||
        // 前缀+日期 构建 prefixKey
 | 
			
		||||
        String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMATTER));
 | 
			
		||||
        // 获取下一个id
 | 
			
		||||
        long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_DAY, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
 | 
			
		||||
        // 返回完整id
 | 
			
		||||
        return StringUtils.format("{}{}", prefixKey, nextId);
 | 
			
		||||
    public static String getDateId(String prefix) {
 | 
			
		||||
        return getDateId(prefix, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 yyyyMMddHHmmss 开头的唯一id
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix       业务前缀
 | 
			
		||||
     * @param isWithPrefix id是否携带业务前缀
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String nextIdDateTime() {
 | 
			
		||||
        return nextIdDateTime("");
 | 
			
		||||
    public static String getDateId(String prefix, boolean isWithPrefix) {
 | 
			
		||||
        return getDateId(prefix, isWithPrefix, -1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 开头的唯一id
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})})
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix       业务前缀
 | 
			
		||||
     * @param isWithPrefix id是否携带业务前缀
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getPaddedDateId(String prefix, boolean isWithPrefix) {
 | 
			
		||||
        return getDateId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
 | 
			
		||||
        return getDateId(prefix, isWithPrefix, minIdCapacityBits, LocalDate.now());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @param time              时间
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time) {
 | 
			
		||||
        return getDateId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMdd 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @param time              时间
 | 
			
		||||
     * @param initValue         ID初始值
 | 
			
		||||
     * @param stepValue         ID步长
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time, long initValue, long stepValue) {
 | 
			
		||||
        return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATE_FORMATTER, DEFAULT_EXPIRE_TIME_DAY, initValue, stepValue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 yyyyMMddHHmmss 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     * @deprecated 请使用 {@link #getDateTimeId(String)} 或 {@link #getDateTimeId(String, boolean)}、{@link #getDateTimeId(String, boolean, int)},确保不同业务的ID连续性
 | 
			
		||||
     */
 | 
			
		||||
    @Deprecated
 | 
			
		||||
    public static String getDateTimeId() {
 | 
			
		||||
        return getDateTimeId("", false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix 业务前缀
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String nextIdDateTime(String prefix) {
 | 
			
		||||
        // 前缀+日期时间 构建 prefixKey
 | 
			
		||||
        String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMATTER));
 | 
			
		||||
        // 获取下一个id
 | 
			
		||||
        long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_MINUTE, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
 | 
			
		||||
        // 返回完整id
 | 
			
		||||
        return StringUtils.format("{}{}", prefixKey, nextId);
 | 
			
		||||
    public static String getDateTimeId(String prefix) {
 | 
			
		||||
        return getDateTimeId(prefix, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix       业务前缀
 | 
			
		||||
     * @param isWithPrefix id是否携带业务前缀
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateTimeId(String prefix, boolean isWithPrefix) {
 | 
			
		||||
        return getDateTimeId(prefix, isWithPrefix, -1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})})
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix       业务前缀
 | 
			
		||||
     * @param isWithPrefix id是否携带业务前缀
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getPaddedDateTimeId(String prefix, boolean isWithPrefix) {
 | 
			
		||||
        return getDateTimeId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
 | 
			
		||||
        return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, LocalDateTime.now());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @param time              时间
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time) {
 | 
			
		||||
        return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取 prefix + yyyyMMddHHmmss 格式的唯一id
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @param initValue         ID初始值
 | 
			
		||||
     * @param stepValue         ID步长
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time, long initValue, long stepValue) {
 | 
			
		||||
        return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATETIME_FORMATTER, DEFAULT_EXPIRE_TIME_MINUTE, initValue, stepValue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取指定业务key的指定时间格式的ID
 | 
			
		||||
     *
 | 
			
		||||
     * @param prefix            业务前缀
 | 
			
		||||
     * @param isWithPrefix      id是否携带业务前缀
 | 
			
		||||
     * @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
 | 
			
		||||
     * @param temporalAccessor  时间访问器
 | 
			
		||||
     * @param timeFormatter     时间格式
 | 
			
		||||
     * @param expireTime        过期时间
 | 
			
		||||
     * @param initValue         ID初始值
 | 
			
		||||
     * @param stepValue         ID步长
 | 
			
		||||
     * @return 唯一id
 | 
			
		||||
     */
 | 
			
		||||
    private static String getDatePatternId(String prefix, boolean isWithPrefix, int minIdCapacityBits, TemporalAccessor temporalAccessor, DateTimeFormatter timeFormatter, Duration expireTime, long initValue, long stepValue) {
 | 
			
		||||
        // 时间前缀
 | 
			
		||||
        String timePrefix = timeFormatter.format(temporalAccessor);
 | 
			
		||||
        // 业务前缀 + 时间前缀 构建 prefixKey
 | 
			
		||||
        String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), timePrefix);
 | 
			
		||||
 | 
			
		||||
        // 获取id,例 -> 1
 | 
			
		||||
        String nextId = getNextIdString(prefixKey, expireTime, initValue, stepValue);
 | 
			
		||||
 | 
			
		||||
        // minIdCapacityBits 大于0,且 nextId 的长度小于 minIdCapacityBits,则左补0
 | 
			
		||||
        if (minIdCapacityBits > 0 && nextId.length() < minIdCapacityBits) {
 | 
			
		||||
            nextId = StringUtils.leftPad(nextId, minIdCapacityBits, '0');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 是否携带业务前缀
 | 
			
		||||
        if (isWithPrefix) {
 | 
			
		||||
            // 例 -> P202507031
 | 
			
		||||
            // 其中 P 为业务前缀,202507031 为 yyyyMMdd 格式时间, 1 为nextId
 | 
			
		||||
            return StringUtils.format("{}{}", prefixKey, nextId);
 | 
			
		||||
        }
 | 
			
		||||
        // 例 -> 202507031
 | 
			
		||||
        // 其中 202507031 为 yyyyMMdd 格式时间, 1 为nextId
 | 
			
		||||
        return StringUtils.format("{}{}", timePrefix, nextId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,13 +52,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
 | 
			
		||||
        // 判断是否为永不过期
 | 
			
		||||
        if (timeout == NEVER_EXPIRE) {
 | 
			
		||||
            RedisUtils.setCacheObject(key, value);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (RedisUtils.hasKey(key)) {
 | 
			
		||||
                RedisUtils.setCacheObject(key, value, true);
 | 
			
		||||
        } else {
 | 
			
		||||
            RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
        CAFFEINE.invalidate(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -78,7 +74,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void delete(String key) {
 | 
			
		||||
        RedisUtils.deleteObject(key);
 | 
			
		||||
        if (RedisUtils.deleteObject(key)) {
 | 
			
		||||
            CAFFEINE.invalidate(key);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -133,13 +131,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
 | 
			
		||||
        // 判断是否为永不过期
 | 
			
		||||
        if (timeout == NEVER_EXPIRE) {
 | 
			
		||||
            RedisUtils.setCacheObject(key, object);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (RedisUtils.hasKey(key)) {
 | 
			
		||||
                RedisUtils.setCacheObject(key, object, true);
 | 
			
		||||
        } else {
 | 
			
		||||
            RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
        CAFFEINE.invalidate(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -159,7 +153,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    public void deleteObject(String key) {
 | 
			
		||||
        RedisUtils.deleteObject(key);
 | 
			
		||||
        if (RedisUtils.deleteObject(key)) {
 | 
			
		||||
            CAFFEINE.invalidate(key);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
package org.dromara.common.satoken.core.service;
 | 
			
		||||
 | 
			
		||||
import cn.dev33.satoken.stp.StpInterface;
 | 
			
		||||
import cn.hutool.core.collection.CollUtil;
 | 
			
		||||
import cn.hutool.core.util.ObjectUtil;
 | 
			
		||||
import org.dromara.common.core.domain.model.LoginUser;
 | 
			
		||||
import org.dromara.common.core.enums.UserType;
 | 
			
		||||
import org.dromara.common.core.exception.ServiceException;
 | 
			
		||||
import org.dromara.common.core.service.PermissionService;
 | 
			
		||||
import org.dromara.common.core.utils.SpringUtils;
 | 
			
		||||
import org.dromara.common.core.utils.StringUtils;
 | 
			
		||||
import org.dromara.common.satoken.utils.LoginHelper;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
@@ -21,14 +27,26 @@ public class SaPermissionImpl implements StpInterface {
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<String> getPermissionList(Object loginId, String loginType) {
 | 
			
		||||
        LoginUser loginUser = LoginHelper.getLoginUser();
 | 
			
		||||
        if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
 | 
			
		||||
            PermissionService permissionService = getPermissionService();
 | 
			
		||||
            if (ObjectUtil.isNotNull(permissionService)) {
 | 
			
		||||
                List<String> list = StringUtils.splitList(loginId.toString(), ":");
 | 
			
		||||
                return new ArrayList<>(permissionService.getMenuPermission(Long.parseLong(list.get(1))));
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new ServiceException("PermissionService 实现类不存在");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        UserType userType = UserType.getUserType(loginUser.getUserType());
 | 
			
		||||
        if (userType == UserType.SYS_USER) {
 | 
			
		||||
            return new ArrayList<>(loginUser.getMenuPermission());
 | 
			
		||||
        } else if (userType == UserType.APP_USER) {
 | 
			
		||||
        if (userType == UserType.APP_USER) {
 | 
			
		||||
            // 其他端 自行根据业务编写
 | 
			
		||||
        }
 | 
			
		||||
        if (CollUtil.isNotEmpty(loginUser.getMenuPermission())) {
 | 
			
		||||
            // SYS_USER 默认返回权限
 | 
			
		||||
            return new ArrayList<>(loginUser.getMenuPermission());
 | 
			
		||||
        } else {
 | 
			
		||||
            return new ArrayList<>();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取角色权限列表
 | 
			
		||||
@@ -36,12 +54,33 @@ public class SaPermissionImpl implements StpInterface {
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<String> getRoleList(Object loginId, String loginType) {
 | 
			
		||||
        LoginUser loginUser = LoginHelper.getLoginUser();
 | 
			
		||||
        if (ObjectUtil.isNull(loginUser) || !loginUser.getLoginId().equals(loginId)) {
 | 
			
		||||
            PermissionService permissionService = getPermissionService();
 | 
			
		||||
            if (ObjectUtil.isNotNull(permissionService)) {
 | 
			
		||||
                List<String> list = StringUtils.splitList(loginId.toString(), ":");
 | 
			
		||||
                return new ArrayList<>(permissionService.getRolePermission(Long.parseLong(list.get(1))));
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new ServiceException("PermissionService 实现类不存在");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        UserType userType = UserType.getUserType(loginUser.getUserType());
 | 
			
		||||
        if (userType == UserType.SYS_USER) {
 | 
			
		||||
            return new ArrayList<>(loginUser.getRolePermission());
 | 
			
		||||
        } else if (userType == UserType.APP_USER) {
 | 
			
		||||
        if (userType == UserType.APP_USER) {
 | 
			
		||||
            // 其他端 自行根据业务编写
 | 
			
		||||
        }
 | 
			
		||||
        if (CollUtil.isNotEmpty(loginUser.getRolePermission())) {
 | 
			
		||||
            // SYS_USER 默认返回权限
 | 
			
		||||
            return new ArrayList<>(loginUser.getRolePermission());
 | 
			
		||||
        } else {
 | 
			
		||||
            return new ArrayList<>();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PermissionService getPermissionService() {
 | 
			
		||||
        try {
 | 
			
		||||
            return SpringUtils.getBean(PermissionService.class);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -207,7 +207,8 @@ public class LoginHelper {
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isLogin() {
 | 
			
		||||
        try {
 | 
			
		||||
            return getLoginUser() != null;
 | 
			
		||||
            StpUtil.checkLogin();
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ import cn.dev33.satoken.interceptor.SaInterceptor;
 | 
			
		||||
import cn.dev33.satoken.router.SaRouter;
 | 
			
		||||
import cn.dev33.satoken.stp.StpUtil;
 | 
			
		||||
import cn.dev33.satoken.util.SaResult;
 | 
			
		||||
import cn.dev33.satoken.util.SaTokenConsts;
 | 
			
		||||
import jakarta.servlet.http.HttpServletRequest;
 | 
			
		||||
import jakarta.servlet.http.HttpServletResponse;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.dromara.common.core.constant.HttpStatus;
 | 
			
		||||
@@ -55,6 +57,8 @@ public class SecurityConfig implements WebMvcConfigurer {
 | 
			
		||||
                    // 对未排除的路径进行检查
 | 
			
		||||
                    .check(() -> {
 | 
			
		||||
                        HttpServletRequest request = ServletUtils.getRequest();
 | 
			
		||||
                        HttpServletResponse response = ServletUtils.getResponse();
 | 
			
		||||
                        response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
 | 
			
		||||
                        // 检查是否登录 是否有token
 | 
			
		||||
                        StpUtil.checkLogin();
 | 
			
		||||
 | 
			
		||||
@@ -94,7 +98,11 @@ public class SecurityConfig implements WebMvcConfigurer {
 | 
			
		||||
            .setAuth(obj -> {
 | 
			
		||||
                SaHttpBasicUtil.check(username + ":" + password);
 | 
			
		||||
            })
 | 
			
		||||
            .setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
 | 
			
		||||
            .setError(e -> {
 | 
			
		||||
                HttpServletResponse response = ServletUtils.getResponse();
 | 
			
		||||
                response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
 | 
			
		||||
                return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
package org.dromara.common.sensitive.core;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.convert.Convert;
 | 
			
		||||
import cn.hutool.core.util.DesensitizedUtil;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +53,7 @@ public enum SensitiveStrategy {
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户ID
 | 
			
		||||
     */
 | 
			
		||||
    USER_ID(s -> String.valueOf(DesensitizedUtil.userId())),
 | 
			
		||||
    USER_ID(s -> Convert.toStr(DesensitizedUtil.userId())),
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 密码
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,154 @@
 | 
			
		||||
package me.zhyd.oauth.request;
 | 
			
		||||
 | 
			
		||||
import com.alibaba.fastjson.JSONObject;
 | 
			
		||||
import me.zhyd.oauth.cache.AuthStateCache;
 | 
			
		||||
import me.zhyd.oauth.config.AuthConfig;
 | 
			
		||||
import me.zhyd.oauth.config.AuthSource;
 | 
			
		||||
import me.zhyd.oauth.enums.AuthResponseStatus;
 | 
			
		||||
import me.zhyd.oauth.enums.AuthUserGender;
 | 
			
		||||
import me.zhyd.oauth.exception.AuthException;
 | 
			
		||||
import me.zhyd.oauth.model.AuthCallback;
 | 
			
		||||
import me.zhyd.oauth.model.AuthToken;
 | 
			
		||||
import me.zhyd.oauth.model.AuthUser;
 | 
			
		||||
import me.zhyd.oauth.utils.HttpUtils;
 | 
			
		||||
import me.zhyd.oauth.utils.StringUtils;
 | 
			
		||||
import me.zhyd.oauth.utils.UrlBuilder;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * <p>
 | 
			
		||||
 * 企业微信登录父类
 | 
			
		||||
 * </p>
 | 
			
		||||
 *
 | 
			
		||||
 * @author liguanhua (347826496(a)qq.com)
 | 
			
		||||
 * @since 1.15.9
 | 
			
		||||
 */
 | 
			
		||||
public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultRequest {
 | 
			
		||||
 | 
			
		||||
    public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source) {
 | 
			
		||||
        super(config,source);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) {
 | 
			
		||||
        super(config, source, authStateCache);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AuthToken getAccessToken(AuthCallback authCallback) {
 | 
			
		||||
        String response = doGetAuthorizationCode(accessTokenUrl(null));
 | 
			
		||||
 | 
			
		||||
        JSONObject object = this.checkResponse(response);
 | 
			
		||||
 | 
			
		||||
        return AuthToken.builder()
 | 
			
		||||
            .accessToken(object.getString("access_token"))
 | 
			
		||||
            .expireIn(object.getIntValue("expires_in"))
 | 
			
		||||
            .code(authCallback.getCode())
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AuthUser getUserInfo(AuthToken authToken) {
 | 
			
		||||
        String response = doGetUserInfo(authToken);
 | 
			
		||||
        JSONObject object = this.checkResponse(response);
 | 
			
		||||
 | 
			
		||||
        // 返回 OpenId 或其他,均代表非当前企业用户,不支持
 | 
			
		||||
        // https://github.com/justauth/JustAuth/issues/227 修复bug
 | 
			
		||||
        if (!object.containsKey("userid")) {
 | 
			
		||||
            throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
 | 
			
		||||
        }
 | 
			
		||||
        String userId = object.getString("userid");
 | 
			
		||||
        String userTicket = object.getString("user_ticket");
 | 
			
		||||
        JSONObject userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
 | 
			
		||||
 | 
			
		||||
        return AuthUser.builder()
 | 
			
		||||
            .rawUserInfo(userDetail)
 | 
			
		||||
            .username(userDetail.getString("name"))
 | 
			
		||||
            .nickname(userDetail.getString("alias"))
 | 
			
		||||
            .avatar(userDetail.getString("avatar"))
 | 
			
		||||
            .location(userDetail.getString("address"))
 | 
			
		||||
            .email(userDetail.getString("email"))
 | 
			
		||||
            .uuid(userId)
 | 
			
		||||
            .gender(AuthUserGender.getWechatRealGender(userDetail.getString("gender")))
 | 
			
		||||
            .token(authToken)
 | 
			
		||||
            .source(source.toString())
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 校验请求结果
 | 
			
		||||
     *
 | 
			
		||||
     * @param response 请求结果
 | 
			
		||||
     * @return 如果请求结果正常,则返回JSONObject
 | 
			
		||||
     */
 | 
			
		||||
    private JSONObject checkResponse(String response) {
 | 
			
		||||
        JSONObject object = JSONObject.parseObject(response);
 | 
			
		||||
 | 
			
		||||
        if (object.containsKey("errcode") && object.getIntValue("errcode") != 0) {
 | 
			
		||||
            throw new AuthException(object.getString("errmsg"), source);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return object;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回获取accessToken的url
 | 
			
		||||
     *
 | 
			
		||||
     * @param code 授权码
 | 
			
		||||
     * @return 返回获取accessToken的url
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String accessTokenUrl(String code) {
 | 
			
		||||
        return UrlBuilder.fromBaseUrl(source.accessToken())
 | 
			
		||||
            .queryParam("corpid", config.getClientId())
 | 
			
		||||
            .queryParam("corpsecret", config.getClientSecret())
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回获取userInfo的url
 | 
			
		||||
     *
 | 
			
		||||
     * @param authToken 用户授权后的token
 | 
			
		||||
     * @return 返回获取userInfo的url
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String userInfoUrl(AuthToken authToken) {
 | 
			
		||||
        return UrlBuilder.fromBaseUrl(source.userInfo())
 | 
			
		||||
            .queryParam("access_token", authToken.getAccessToken())
 | 
			
		||||
            .queryParam("code", authToken.getCode())
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 用户详情
 | 
			
		||||
     *
 | 
			
		||||
     * @param accessToken accessToken
 | 
			
		||||
     * @param userId      企业内用户id
 | 
			
		||||
     * @param userTicket  成员票据,用于获取用户信息或敏感信息
 | 
			
		||||
     * @return 用户详情
 | 
			
		||||
     */
 | 
			
		||||
    private JSONObject getUserDetail(String accessToken, String userId, String userTicket) {
 | 
			
		||||
        // 用户基础信息
 | 
			
		||||
        String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
 | 
			
		||||
            .queryParam("access_token", accessToken)
 | 
			
		||||
            .queryParam("userid", userId)
 | 
			
		||||
            .build();
 | 
			
		||||
        String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
 | 
			
		||||
        JSONObject userInfo = checkResponse(userInfoResponse);
 | 
			
		||||
 | 
			
		||||
        // 用户敏感信息
 | 
			
		||||
        if (StringUtils.isNotEmpty(userTicket)) {
 | 
			
		||||
            String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
 | 
			
		||||
                .queryParam("access_token", accessToken)
 | 
			
		||||
                .build();
 | 
			
		||||
            JSONObject param = new JSONObject();
 | 
			
		||||
            param.put("user_ticket", userTicket);
 | 
			
		||||
            String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, param.toJSONString()).getBody();
 | 
			
		||||
            JSONObject userDetail = checkResponse(userDetailResponse);
 | 
			
		||||
 | 
			
		||||
            userInfo.putAll(userDetail);
 | 
			
		||||
        }
 | 
			
		||||
        return userInfo;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,109 @@
 | 
			
		||||
package me.zhyd.oauth.request;
 | 
			
		||||
 | 
			
		||||
import com.alibaba.fastjson.JSONObject;
 | 
			
		||||
import com.xkcoding.http.support.HttpHeader;
 | 
			
		||||
import me.zhyd.oauth.cache.AuthStateCache;
 | 
			
		||||
import me.zhyd.oauth.config.AuthConfig;
 | 
			
		||||
import me.zhyd.oauth.config.AuthDefaultSource;
 | 
			
		||||
import me.zhyd.oauth.enums.scope.AuthDingTalkScope;
 | 
			
		||||
import me.zhyd.oauth.exception.AuthException;
 | 
			
		||||
import me.zhyd.oauth.model.AuthCallback;
 | 
			
		||||
import me.zhyd.oauth.model.AuthToken;
 | 
			
		||||
import me.zhyd.oauth.model.AuthUser;
 | 
			
		||||
import me.zhyd.oauth.utils.AuthScopeUtils;
 | 
			
		||||
import me.zhyd.oauth.utils.GlobalAuthUtils;
 | 
			
		||||
import me.zhyd.oauth.utils.HttpUtils;
 | 
			
		||||
import me.zhyd.oauth.utils.UrlBuilder;
 | 
			
		||||
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 新版钉钉二维码登录
 | 
			
		||||
 *
 | 
			
		||||
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 | 
			
		||||
 * @since 1.16.7
 | 
			
		||||
 */
 | 
			
		||||
public class AuthDingTalkV2Request extends AuthDefaultRequest {
 | 
			
		||||
 | 
			
		||||
    public AuthDingTalkV2Request(AuthConfig config) {
 | 
			
		||||
        super(config, AuthDefaultSource.DINGTALK_V2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
 | 
			
		||||
        super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String authorize(String state) {
 | 
			
		||||
        return UrlBuilder.fromBaseUrl(source.authorize())
 | 
			
		||||
            .queryParam("response_type", "code")
 | 
			
		||||
            .queryParam("client_id", config.getClientId())
 | 
			
		||||
            .queryParam("scope", this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
 | 
			
		||||
            .queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
 | 
			
		||||
            .queryParam("prompt", "consent")
 | 
			
		||||
            .queryParam("org_type", config.getDingTalkOrgType())
 | 
			
		||||
            .queryParam("corpId", config.getDingTalkCorpId())
 | 
			
		||||
            .queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
 | 
			
		||||
            .queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
 | 
			
		||||
            .queryParam("state", getRealState(state))
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AuthToken getAccessToken(AuthCallback authCallback) {
 | 
			
		||||
        Map<String, String> params = new HashMap<>();
 | 
			
		||||
        params.put("grantType", "authorization_code");
 | 
			
		||||
        params.put("clientId", config.getClientId());
 | 
			
		||||
        params.put("clientSecret", config.getClientSecret());
 | 
			
		||||
        params.put("code", authCallback.getCode());
 | 
			
		||||
        String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
 | 
			
		||||
        JSONObject accessTokenObject = JSONObject.parseObject(response);
 | 
			
		||||
        if (!accessTokenObject.containsKey("accessToken")) {
 | 
			
		||||
            throw new AuthException(JSONObject.toJSONString(response), source);
 | 
			
		||||
        }
 | 
			
		||||
        return AuthToken.builder()
 | 
			
		||||
            .accessToken(accessTokenObject.getString("accessToken"))
 | 
			
		||||
            .refreshToken(accessTokenObject.getString("refreshToken"))
 | 
			
		||||
            .expireIn(accessTokenObject.getIntValue("expireIn"))
 | 
			
		||||
            .corpId(accessTokenObject.getString("corpId"))
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AuthUser getUserInfo(AuthToken authToken) {
 | 
			
		||||
        HttpHeader header = new HttpHeader();
 | 
			
		||||
        header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
 | 
			
		||||
 | 
			
		||||
        String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
 | 
			
		||||
        JSONObject object = JSONObject.parseObject(response);
 | 
			
		||||
 | 
			
		||||
        authToken.setOpenId(object.getString("openId"));
 | 
			
		||||
        authToken.setUnionId(object.getString("unionId"));
 | 
			
		||||
        return AuthUser.builder()
 | 
			
		||||
            .rawUserInfo(object)
 | 
			
		||||
            .uuid(object.getString("unionId"))
 | 
			
		||||
            .username(object.getString("nick"))
 | 
			
		||||
            .nickname(object.getString("nick"))
 | 
			
		||||
            .avatar(object.getString("avatarUrl"))
 | 
			
		||||
            .snapshotUser(object.getBooleanValue("visitor"))
 | 
			
		||||
            .token(authToken)
 | 
			
		||||
            .source(source.toString())
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回获取accessToken的url
 | 
			
		||||
     *
 | 
			
		||||
     * @param code 授权码
 | 
			
		||||
     * @return 返回获取accessToken的url
 | 
			
		||||
     */
 | 
			
		||||
    protected String accessTokenUrl(String code) {
 | 
			
		||||
        return UrlBuilder.fromBaseUrl(source.accessToken())
 | 
			
		||||
            .queryParam("code", code)
 | 
			
		||||
            .queryParam("clientId", config.getClientId())
 | 
			
		||||
            .queryParam("clientSecret", config.getClientSecret())
 | 
			
		||||
            .queryParam("grantType", "authorization_code")
 | 
			
		||||
            .build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user