mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-08 17:12:28 +08:00
Compare commits
719 Commits
v3.7.1Spri
...
main-jeecg
| Author | SHA1 | Date | |
|---|---|---|---|
| 17d609cf34 | |||
| bfcd2797f1 | |||
| e112f4eea3 | |||
| 3b36f1909a | |||
| 5e5e11f952 | |||
| d6051addeb | |||
| 281c3ff3c8 | |||
| 38d44c2487 | |||
| 8c88f8adf5 | |||
| 526734c5a5 | |||
| 44b48ad916 | |||
| 1a3ae4f61c | |||
| 859c509f08 | |||
| 0704f187af | |||
| 199d2b439e | |||
| 5f898ed034 | |||
| 5a9cb05c86 | |||
| 98936680d5 | |||
| fc043fd5f3 | |||
| 54531002a7 | |||
| 728a95c00d | |||
| 13c9951c1f | |||
| 8dfaa3c3e1 | |||
| 050c478dce | |||
| 2688e8b6e2 | |||
| a9f30f0ca5 | |||
| 1bf4a0595a | |||
| 74cd57fd99 | |||
| c9ac4c9945 | |||
| 3b3371ee1a | |||
| 1d3bde9fe7 | |||
| 90b50a51a7 | |||
| 668ac59a5c | |||
| 5f01bdd29b | |||
| 82bfcc7b14 | |||
| ef210a2242 | |||
| 435445cb4e | |||
| fdf7cd1f6b | |||
| 92a38f41b0 | |||
| 7ed3bcc912 | |||
| 2706c0a519 | |||
| f4b80365a9 | |||
| 1108aa5288 | |||
| 9919ae2bc5 | |||
| 1f73837b7d | |||
| 9571e0b169 | |||
| 1a923596db | |||
| 62549e0a1c | |||
| 2740a2f419 | |||
| 899264250c | |||
| 0be7d00eb2 | |||
| 7152ae9e49 | |||
| 58b41db786 | |||
| d715c7a0ac | |||
| aca407e1ce | |||
| cfea79a187 | |||
| 7848d1fb33 | |||
| 91fa645878 | |||
| c9fc948658 | |||
| b97d041e7f | |||
| 6492f2c99a | |||
| bf32385a06 | |||
| 6ef637c46f | |||
| bc6f336745 | |||
| 0d86df8e9e | |||
| 3db673b67d | |||
| 3ba5395d33 | |||
| e7eed37470 | |||
| 30ac3f7c72 | |||
| 03e6c97d80 | |||
| b9f6f6dc53 | |||
| 107e13c8af | |||
| 0512b41b2b | |||
| d6d880f887 | |||
| b0e974a418 | |||
| 388fa9b8c2 | |||
| bc04bd1433 | |||
| 35aba0784d | |||
| c3822ab702 | |||
| d4487356f0 | |||
| ae4363dc72 | |||
| 3e6c7651ee | |||
| c0ffd14b7a | |||
| 914875d6a1 | |||
| 2298ee3eed | |||
| 2a8853b353 | |||
| b920c5b794 | |||
| d3fa38a9e6 | |||
| b0df78b06c | |||
| 80749098bd | |||
| 19b7f2cb29 | |||
| 39f5c3a5be | |||
| 9ee3a36fbb | |||
| 8c5cf3a0d9 | |||
| 053552c123 | |||
| fc44deca83 | |||
| 5d5d9fc53d | |||
| 002bfe25f8 | |||
| 7cb2dc4fde | |||
| 9c244bd266 | |||
| ab151879b3 | |||
| 000ae1db30 | |||
| 63e066180d | |||
| 44c1079f87 | |||
| e825e0f912 | |||
| 132e89b0e1 | |||
| 881a637285 | |||
| 02e9f8984f | |||
| a4343fc2cb | |||
| 152e8c7aaa | |||
| d7dc81455d | |||
| aefdcd6315 | |||
| 1cf4054e76 | |||
| 7829cf18d7 | |||
| 69c3a9da9a | |||
| 4d34150479 | |||
| 29687c8908 | |||
| 2e93a92dde | |||
| d383f7458d | |||
| 700318e1c1 | |||
| d728d6b090 | |||
| 7abc2e4c9c | |||
| da2b0cc354 | |||
| a6751c22be | |||
| f087525a75 | |||
| 4f46213df6 | |||
| d76842ae07 | |||
| 8c64db46e5 | |||
| 81fb2ac3b2 | |||
| fa98817aeb | |||
| 1158977826 | |||
| 8b6def0ee3 | |||
| 39c0d5b3f5 | |||
| adeebee840 | |||
| 862aaa8632 | |||
| 6a11ff8a64 | |||
| 73059b8a53 | |||
| e377bf6990 | |||
| 434b42e9ed | |||
| f1ceb08e16 | |||
| d2eedacc85 | |||
| 8791384791 | |||
| fd60e49f5b | |||
| 5f1dc06067 | |||
| b67770ff14 | |||
| 1dae808cf1 | |||
| 70d8353219 | |||
| 208d9990ae | |||
| 3e208de18e | |||
| 4f3c71af5b | |||
| 70bd639206 | |||
| d245ef3037 | |||
| 7af8346b79 | |||
| 1b8a31f0d3 | |||
| 294ad5a6c9 | |||
| 065b255d90 | |||
| 56976e68b4 | |||
| db1ff0268b | |||
| b7519d7199 | |||
| 81ba07c853 | |||
| 92ed296e63 | |||
| c2aff84914 | |||
| e002cd3bf3 | |||
| 1de07ff3ff | |||
| 35852d41f1 | |||
| a2cb1d9f25 | |||
| 2002af54d0 | |||
| 89747403a2 | |||
| 3db0995c3f | |||
| 950621dd88 | |||
| 033cf51d69 | |||
| fb9f367517 | |||
| b2da45d803 | |||
| 2840f0d325 | |||
| 6ace7eae8a | |||
| 3d88147c59 | |||
| 08f245bdf9 | |||
| 8cc033b86f | |||
| 6b7542620b | |||
| ba0052d452 | |||
| 69fca254f0 | |||
| b3de596199 | |||
| f46273d15e | |||
| 0fe258dbc2 | |||
| de7f23c555 | |||
| 67d9865861 | |||
| d97e56b2f0 | |||
| c868496b78 | |||
| c5150baa69 | |||
| 3d9f59c69b | |||
| 420d6db3fb | |||
| 473a626039 | |||
| 0308b0597c | |||
| cd809a6573 | |||
| 2191f5d48c | |||
| 1158b0b6e7 | |||
| ead2cef1f4 | |||
| 83bb0a0a6a | |||
| b474e9e5a5 | |||
| 422373e300 | |||
| 1cf11a4c2a | |||
| ac446691c4 | |||
| 925f163784 | |||
| 0feb307e8d | |||
| 781d61e96e | |||
| d01c1d7d47 | |||
| 3576b54945 | |||
| a760f94b94 | |||
| e795e03365 | |||
| 342bdd2e38 | |||
| 59ece16059 | |||
| 91208a4968 | |||
| 419e2bea0b | |||
| 443abc3ede | |||
| 358e46559f | |||
| 128c2c97f6 | |||
| 424dc33bba | |||
| 1cb48b4f0c | |||
| ea59454f51 | |||
| 615a1bc4ff | |||
| d976f12c8f | |||
| 3783765161 | |||
| 8cc6810fdd | |||
| a902d9af19 | |||
| 37a116f2fb | |||
| 1564831f7e | |||
| 18c1cd00c1 | |||
| a988b05e72 | |||
| a9a6fd529d | |||
| 6586d3a880 | |||
| af354f9f5e | |||
| 79b182819b | |||
| a638a93b65 | |||
| 621781d336 | |||
| f1cad333da | |||
| e70844ce61 | |||
| e00ffa2670 | |||
| 676fffa2c8 | |||
| 23cc569a47 | |||
| dafacf153b | |||
| 7a9f357510 | |||
| 6c15b45a8c | |||
| f30a8c658a | |||
| e84d7726d2 | |||
| 0f39802698 | |||
| a014a3ed0e | |||
| 5720d1a01e | |||
| 5eed6ac6d2 | |||
| 0cfa1e223a | |||
| 219869f4c0 | |||
| e6edde963a | |||
| c44b66128e | |||
| 9356b04741 | |||
| d0a094f9a3 | |||
| 73eb625737 | |||
| 74880705b8 | |||
| f67cfa1bfb | |||
| 8d91caa4e6 | |||
| 0d9f9a04cc | |||
| 90565fcf79 | |||
| 118775cf79 | |||
| 7b80ae3e68 | |||
| cf4d888839 | |||
| 336e7851aa | |||
| 56b9131675 | |||
| 3c0cc49f0c | |||
| 69b2e97935 | |||
| 1c2a49d371 | |||
| 967197d224 | |||
| 396718bc5e | |||
| 37c62c3962 | |||
| f510578cb7 | |||
| 96b378bb7a | |||
| 2d7c51eadc | |||
| 9d440a4261 | |||
| 4870c43f39 | |||
| 9c21f621c0 | |||
| 8d382b76ad | |||
| 410f2539e1 | |||
| baee00921f | |||
| 216d4b9a1f | |||
| e00b25af42 | |||
| 44d6b37873 | |||
| 271712b050 | |||
| 3482b3a0db | |||
| 10bf5cf127 | |||
| 6cc74699aa | |||
| 0e8fdc2c5a | |||
| bd6051d972 | |||
| 9cfb9dd2b7 | |||
| 87b41ab36f | |||
| 378f42bcb7 | |||
| 6838888f5f | |||
| bb8281107e | |||
| e9d44bfc80 | |||
| 8057172f8c | |||
| c6dd83a262 | |||
| 7469474124 | |||
| dc9f36905d | |||
| 30486c5358 | |||
| 5b7a90c8bc | |||
| ac40596ad6 | |||
| 6e0edc093b | |||
| 2831996dd4 | |||
| 130793fffe | |||
| 5cabfe1655 | |||
| e99e29a523 | |||
| d64e8b0adb | |||
| 4108ba54c3 | |||
| e8ad887096 | |||
| 6c24b3cf30 | |||
| 208861f808 | |||
| f7fe1dfc58 | |||
| e2805050cf | |||
| b34bb84fc9 | |||
| 11beca16da | |||
| b0c9a2fd9e | |||
| 6b4e8695ae | |||
| e8ca74f2ab | |||
| eabf98533a | |||
| 7848881dcb | |||
| 553bee57f1 | |||
| b751570789 | |||
| e45d29c4ff | |||
| 8c8a448a71 | |||
| 0148f45979 | |||
| 3d414aaec8 | |||
| 7a4c66af57 | |||
| 444c7140f6 | |||
| 0fda30b5d2 | |||
| c8e33d43cb | |||
| 6c6af870ae | |||
| 79107599a6 | |||
| b6e8b37aee | |||
| 195ddd0421 | |||
| 9ddad931ff | |||
| fc05fe1aff | |||
| 7e325f68ca | |||
| f74da23d06 | |||
| e279915ba2 | |||
| bb6f077a95 | |||
| 7a031e6135 | |||
| 5972c74b43 | |||
| 157877f9a6 | |||
| 25a71fa66c | |||
| fdc02fa68a | |||
| ec3c34969a | |||
| 5a215525d5 | |||
| 450b93d916 | |||
| 2d62bad2a9 | |||
| 0b10096f1c | |||
| d69cb121fc | |||
| 431ddb8fcb | |||
| ddf0f61ae5 | |||
| 10a9edd10b | |||
| 4042579167 | |||
| c71ff3fbcc | |||
| bd5fda5968 | |||
| fdbd9c30ac | |||
| b8b4d3f29d | |||
| 78212aa7c0 | |||
| 08612d5bfa | |||
| 6dc3c6af2a | |||
| 2ecce8f02d | |||
| 62937f14fb | |||
| d6ccc4a326 | |||
| 1893108136 | |||
| 7980915bdc | |||
| b7a6812140 | |||
| 550997268b | |||
| 9e7d40a080 | |||
| 7efc51e30e | |||
| 2c38db456b | |||
| bd83b994bc | |||
| e52538d304 | |||
| e91cbd5cd8 | |||
| 8fb81f331c | |||
| fb188a83a1 | |||
| 70cec8b5c6 | |||
| d2365088ce | |||
| a679571a5a | |||
| b9c74e549f | |||
| 81c1724016 | |||
| 56d59eb589 | |||
| a00fcae3a3 | |||
| 9aea5de668 | |||
| 8979dd7ae9 | |||
| a56bd05389 | |||
| 9cf3328ea4 | |||
| 286d10a50f | |||
| 37c593e1d4 | |||
| 68f36cb1e5 | |||
| 78454d3434 | |||
| 56fbc2ed8f | |||
| 197d7adaaf | |||
| 49ba40e98a | |||
| e952518d71 | |||
| 0e184eaa64 | |||
| 86a3ed9dae | |||
| 1e259c805e | |||
| 8a82141c95 | |||
| 94bff11eb1 | |||
| 590d73dfe3 | |||
| fe9630d15c | |||
| 77ae25b86a | |||
| 3b34276cf8 | |||
| cffba084fc | |||
| d1589acc41 | |||
| 888a032266 | |||
| 0d18e536f0 | |||
| 21392c44f8 | |||
| 2730d8e06f | |||
| 0002606d41 | |||
| 8bd19484ee | |||
| 5d2db92613 | |||
| c1b39d21dd | |||
| e83c9b8190 | |||
| 309c76d268 | |||
| 9bd03f467d | |||
| f78eabfc66 | |||
| e032591366 | |||
| de767e07b4 | |||
| b77d3e36ab | |||
| 7885aaed3b | |||
| 6f4c2eb77c | |||
| 3f0597a0f6 | |||
| 04a3764f00 | |||
| 68464109de | |||
| 79866c5823 | |||
| d64b8ecaef | |||
| ec9f2b146a | |||
| 69fd2888a1 | |||
| 748331d649 | |||
| cb1d8e3527 | |||
| a89b299a4b | |||
| 7a15bfc161 | |||
| 65f7eb9542 | |||
| 92808f9164 | |||
| d3ed3f49d3 | |||
| da3d39c59c | |||
| f332758179 | |||
| 61a8904e52 | |||
| b70e709e53 | |||
| b032a415aa | |||
| 81821eeddc | |||
| 5288b1fe73 | |||
| 408f192b37 | |||
| 6a9f188282 | |||
| 168f15e1c2 | |||
| ae814a7e8b | |||
| 2ba17648c4 | |||
| 7160ea32cb | |||
| 19fc610ef5 | |||
| 44146f073e | |||
| 36caab37e2 | |||
| 8abce5ad9c | |||
| 6e721e4120 | |||
| 447e439612 | |||
| 088c79238e | |||
| ad72c807f6 | |||
| db3d95e1a7 | |||
| 1a16b5550f | |||
| c6fe809013 | |||
| 677b57ae09 | |||
| 02f21de8d5 | |||
| f7ca26fff0 | |||
| 6c6aa964e8 | |||
| b0bab050dd | |||
| 87197be8f7 | |||
| 75aa1fe5a0 | |||
| 3e434ce6b4 | |||
| e07508d29f | |||
| e18e980892 | |||
| a2f18fd0d9 | |||
| 9ad7ef5ab4 | |||
| 27a7046465 | |||
| 47a2a6fbac | |||
| 83b1c8692e | |||
| 73a5f64d7e | |||
| 1d18a54b8a | |||
| e877929a42 | |||
| 5f5970d7d5 | |||
| e05c9ddc08 | |||
| b77e9e25a3 | |||
| a8bf090352 | |||
| b6c9f9db68 | |||
| 8c406dcdea | |||
| 215b263d1a | |||
| 4247ffd082 | |||
| ec87621858 | |||
| bc6cf0e7b4 | |||
| 353ad058d1 | |||
| cf63b056d5 | |||
| 60eeef14c7 | |||
| 25b30153a0 | |||
| beff2a271e | |||
| f743622655 | |||
| 70a1f0b7db | |||
| e166d916da | |||
| b79ba97614 | |||
| 4f371672d0 | |||
| 80ae183b58 | |||
| b4cac11368 | |||
| 4936e140e9 | |||
| 9fd4c3b3d2 | |||
| 4f2f1d6265 | |||
| b878f6b6be | |||
| 507289ff6c | |||
| bb4cdf93b1 | |||
| 7d7fde63ec | |||
| c22fd21233 | |||
| d4d0c884f0 | |||
| e6fe3459e4 | |||
| 9ebc3c49ec | |||
| a17b403675 | |||
| 45e9b03e2d | |||
| 696b65240f | |||
| f4339677e7 | |||
| 0aaf0ad3ad | |||
| 0e5cd5bb3e | |||
| 24406a4cac | |||
| 1a1c840a3b | |||
| 2e257343a4 | |||
| 7871c465c8 | |||
| 714d9dc244 | |||
| f23b176cf4 | |||
| 494f5e21f5 | |||
| dea260cae3 | |||
| 0fcc08c204 | |||
| 632fd72d79 | |||
| 608d2e1318 | |||
| 7e436f2efd | |||
| 15fc262675 | |||
| c18fb68e81 | |||
| 299f63c6e9 | |||
| 20037ae02b | |||
| dcdbb5ac7a | |||
| cd9ba3bac9 | |||
| 060bf2282c | |||
| d88f2f81e9 | |||
| 18cc746fcf | |||
| fcc7842e89 | |||
| e91af8cfd3 | |||
| a57cc1fa3d | |||
| 80d37f9a03 | |||
| 502ef2f65d | |||
| 62daec9c16 | |||
| 9191a8b620 | |||
| 4fb53637aa | |||
| 178a2368cb | |||
| 0813dee0d4 | |||
| 9579a60abd | |||
| 5cd5bf7a0b | |||
| c0265981d1 | |||
| 2ec13165d3 | |||
| ac5ee60364 | |||
| 5ff1b56fe2 | |||
| ea914be3a6 | |||
| cb65e1796c | |||
| ff815f1ae6 | |||
| c8ebdc162f | |||
| 6768d65e1e | |||
| 410ab7bcc3 | |||
| 76df4f8600 | |||
| a27b94c4c6 | |||
| 5ad06274cb | |||
| d38fa1901e | |||
| 174f1ae432 | |||
| eef2f7e269 | |||
| 6a0ec66d3d | |||
| b4aab76057 | |||
| d87ffc11e7 | |||
| a67b3409b1 | |||
| e11d831114 | |||
| ec410456b2 | |||
| e9326cab8c | |||
| 61d2857ca7 | |||
| cba4847413 | |||
| bc56384325 | |||
| d7dc1b8dc7 | |||
| 11d9f2b4e4 | |||
| 163b0b531f | |||
| 64fcf7fcfc | |||
| e6ddeef18c | |||
| d1af49a33f | |||
| 03265691e6 | |||
| 47a074d9cb | |||
| c32f0407e8 | |||
| 22bbc23069 | |||
| b2078d502a | |||
| ed165464c8 | |||
| 9cf66e7026 | |||
| 11f8038196 | |||
| d2105dc095 | |||
| 32287d98ac | |||
| de9cc2f30d | |||
| 31cf94ac7b | |||
| 26887959cd | |||
| 0d28c92c83 | |||
| 1cc5cee92d | |||
| 26246eda7a | |||
| 10dd2a2dd0 | |||
| 5abc745496 | |||
| 353e8e2fb6 | |||
| 56661815ec | |||
| 03b6d20fcb | |||
| 7e15e81218 | |||
| 2388c8c46d | |||
| 45f905ddac | |||
| 55696efcaa | |||
| 3671b81f01 | |||
| 74ad7e6f92 | |||
| 49d55a79f4 | |||
| 8395d106a2 | |||
| d716111ded | |||
| 81b4cbbe35 | |||
| 86cfc18fe8 | |||
| 98ba72ae4b | |||
| 8b0e0367c7 | |||
| 334f7dbb62 | |||
| 29248561d6 | |||
| ad74b78134 | |||
| bd716417e8 | |||
| e9ddd21286 | |||
| 1acb773adf | |||
| 5753f3d1d9 | |||
| 46fb1cce91 | |||
| d0487e7a9d | |||
| b8c096eee5 | |||
| c84f62e418 | |||
| c2107ee74a | |||
| 48a9ba6253 | |||
| 1a4abfc89c | |||
| 9550c15d56 | |||
| 229256738a | |||
| 781432c678 | |||
| a4fba72da5 | |||
| 65f90ea84a | |||
| b8ef121fe7 | |||
| d6c6a1c01f | |||
| 7257dfe5ba | |||
| 18166a3c69 | |||
| 54abb67303 | |||
| 84f46c2142 | |||
| 63d60092dc | |||
| dd9ec47cdb | |||
| 49ce9e0763 | |||
| a7089b7b16 | |||
| 53f8c0c076 | |||
| 6ad8c3d824 | |||
| 6947f47bd3 | |||
| 973804189e | |||
| 1942980154 | |||
| 64b852e1c6 | |||
| 0277a968f2 | |||
| 79ca5ebf96 | |||
| 84a7ce9b7a | |||
| ab2469ff98 | |||
| 8fc27bb261 | |||
| d9737c891f | |||
| 17dc1916aa | |||
| a00e4684b5 | |||
| a2a3ff6709 | |||
| 266f49e72f | |||
| 6be0f94ddc | |||
| 7159df8d18 | |||
| b4cd341abc | |||
| 327f034640 | |||
| 00b1250901 | |||
| 964e9b3851 | |||
| 9c5e71476f | |||
| 80a54746b1 | |||
| 9861e1a867 | |||
| 5b2ac82e94 | |||
| c5909cae34 | |||
| b0880c373b | |||
| 2b0bedd263 | |||
| ac30ed67bc | |||
| 458526075e | |||
| a1b55f0d40 | |||
| ee382c0a5b | |||
| f7f6d0ff88 | |||
| 4d058e1dce | |||
| 5c9c2dbebe | |||
| 44f7075316 | |||
| c88bfcf35a | |||
| 11d13c8305 | |||
| 0582436b9c | |||
| 00252f392f | |||
| ef16814216 | |||
| a626b6a4d0 | |||
| fca5d0e54e | |||
| 1c3b9a10f1 | |||
| b0c4194602 | |||
| 64b29f47e0 | |||
| 6198a3702f | |||
| 453acb9b4e | |||
| 565753e370 | |||
| e2aaf0f978 | |||
| 32c8370ef2 | |||
| a79004b924 | |||
| eb1612f8dd | |||
| 2f0a3bcd87 | |||
| a35555619c | |||
| b3e3951064 | |||
| 44ec26574e | |||
| 55a25caafd | |||
| 62f7b0d489 | |||
| b16fdef8dc | |||
| b5b667058b | |||
| 6c0c259742 | |||
| ca56c54aa0 | |||
| 30d3a9f17b | |||
| 74297af987 | |||
| fedb6b84b9 | |||
| fdc713339e | |||
| 03739f2837 | |||
| f28a2dbbeb | |||
| b81435aaca |
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -3,3 +3,4 @@
|
||||
*.ts linguist-language=vue
|
||||
*.html linguist-language=vue
|
||||
*.sql linguist-language=Java
|
||||
*.ftl linguist-language=Java
|
||||
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: 提交 Bug
|
||||
about: 请告诉我框架存在的问题,我会协助你解决此问题!
|
||||
labels: bug
|
||||
assignees: getActivity
|
||||
|
||||
---
|
||||
|
||||
|
||||
##### 版本号:
|
||||
|
||||
|
||||
##### 分支:
|
||||
|
||||
|
||||
##### 问题描述:
|
||||
|
||||
|
||||
|
||||
##### 错误截图:
|
||||
|
||||
|
||||
|
||||
|
||||
#### 友情提示:
|
||||
- 未按格式要求发帖、描述过于简单的,会被直接删掉;
|
||||
- 描述问题请图文并茂,方便我们理解并快速定位问题;
|
||||
- 如果使用的不是master,请说明你使用的分支;
|
||||
@ -1,6 +1,17 @@
|
||||
---
|
||||
name: 提交建议
|
||||
about: 请告诉我框架的不足之处,让我做得更好!
|
||||
labels: help wanted
|
||||
assignees: getActivity
|
||||
|
||||
---
|
||||
|
||||
##### 版本号:
|
||||
|
||||
|
||||
##### 分支:
|
||||
|
||||
|
||||
##### 问题描述:
|
||||
|
||||
|
||||
@ -13,4 +24,4 @@
|
||||
#### 友情提示:
|
||||
- 未按格式要求发帖、描述过于简单的,会被直接删掉;
|
||||
- 描述问题请图文并茂,方便我们理解并快速定位问题;
|
||||
- 如果使用的不是master,请说明你使用的分支;
|
||||
- 如果使用的不是master,请说明你使用的分支;
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,3 +13,5 @@ os_del.cmd
|
||||
os_del_doc.cmd
|
||||
.svn
|
||||
derby.log
|
||||
.cursor
|
||||
.history
|
||||
12
LICENSE
12
LICENSE
@ -200,14 +200,4 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
|
||||
JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京,地址:中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱:jeecgos@163.com
|
||||
本软件受适用的国家软件著作权法(包括国际条约)和开源协议 双重保护许可。
|
||||
|
||||
开源协议中文释意如下:
|
||||
1.JeecgBoot开源版本无任何限制,在遵循本开源协议条款下,允许商用使用,不会造成侵权行为。
|
||||
2.允许基于本平台软件开展业务系统开发。
|
||||
3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。
|
||||
|
||||
最终解释权归:http://www.jeecg.com
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
395
README-EN.md
395
README-EN.md
@ -1,395 +0,0 @@
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
JEECG BOOT Low Code Development Platform
|
||||
===============
|
||||
|
||||
Current version: 3.7.1 (Release date: 2024-09-12)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://www.jeecg.com)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
|
||||
|
||||
Project introduction
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
|
||||
|
||||
JeecgBoot is a `low code development platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
|
||||
|
||||
JeecgBoot provides a series of low code modules to make Online development truly zero code: Online form development, online reports, report configuration capabilities, online chart design, large screen design, mobile configuration capabilities, form designer, online design flow, process automation configuration, plug-in capabilities (pluggable) and more!
|
||||
|
||||
|
||||
The purpose of JEECG is: simple functions are implemented by OnlineCoding configuration, so that zero code development; Complex functions are generated by code generator and manually Merge to achieve low code development, which ensures both intelligence and flexibility. The implementation of low code development and support flexible coding at the same time, to solve the current low code products are generally not flexible drawbacks!
|
||||
|
||||
JEECG Business process: Using workflow to implement and extend the task interface for developing and writing business logic, forms provides a variety of solutions: form designer, online configuration form, and coding form. At the same time, the separation design of process and form (loose coupling) is realized, and the flexible configuration of task nodes is supported, which not only ensures the confidentiality of the company's process, but also reduces the workload of developers.
|
||||
|
||||
|
||||
Technical support
|
||||
-----------------------------------
|
||||
|
||||
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
|
||||
|
||||
|
||||
##### Project description
|
||||
|
||||
| Project | description |
|
||||
|--------------------|------------------------|
|
||||
| `jeecg-boot` | SpringBoot background source code (support microservices) |
|
||||
| `jeecgboot-vue3` | Vue3+TS new front-end source code|
|
||||
| `jeecg-uniapp` | [APP development framework, a code multi terminal adaptation, and support APP, small program, H5](https://github.com/jeecgboot/jeecg-uniapp) |
|
||||
|
||||
|
||||
|
||||
Download other source code
|
||||
-----------------------------------
|
||||
- APP SourceCode:https://github.com/jeecgboot/jeecg-uniapp
|
||||
|
||||
|
||||
|
||||
For the project
|
||||
-----------------------------------
|
||||
Jeecg-Boot low code development platform can be applied in the development of any J2EE project, especially for SAAS projects, enterprise information management system (MIS), internal office system (OA), enterprise resource planning system (ERP), customer relationship management system (CRM), etc. Its semi-intelligent manual Merge development method, Can significantly improve the development efficiency of more than 70%, greatly reduce the development cost.
|
||||
|
||||
|
||||
Starts the project
|
||||
-----------------------------------
|
||||
|
||||
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup.html)
|
||||
- [Docker Quick start](https://help.jeecg.com/java/docker/quick.html)
|
||||
|
||||
|
||||
|
||||
Technical documentation
|
||||
-----------------------------------
|
||||
|
||||
- Website: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- Demo : [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
|
||||
- Doc: [http://help.jeecg.com](http://help.jeecg.com)
|
||||
- Newbie guide: [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
|
||||
- QQ group : ⑨808791225、⑧825232878、⑦791696430、⑥730954414(full)、683903138(full)、⑤860162132(full)、④774126647(full)、③816531124(full)、②769925425(full)、①284271917(full)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Star charts
|
||||
-----------------------------------
|
||||
|
||||
[](https://star-history.com/#jeecgboot/jeecg-boot)
|
||||
|
||||
|
||||
|
||||
|
||||
Background directory Structure
|
||||
-----------------------------------
|
||||
```
|
||||
project structure
|
||||
├─jeecg-boot-parent
|
||||
│ ├─jeecg-boot-base-core
|
||||
│ ├─jeecg-module-demo
|
||||
│ ├─jeecg-module-system
|
||||
│ │ ├─jeecg-system-biz
|
||||
│ │ ├─jeecg-system-start system (8080)
|
||||
│ │ ├─jeecg-system-api
|
||||
│ │ │ ├─jeecg-system-cloud-api
|
||||
│ │ │ ├─jeecg-system-local-api
|
||||
│ ├─jeecg-server-cloud
|
||||
├─jeecg-cloud-gateway (9999)
|
||||
├─jeecg-cloud-nacos --Nacos(8848)
|
||||
├─jeecg-system-cloud-start --System(7001)
|
||||
├─jeecg-demo-cloud-start --Demo(7002)
|
||||
├─jeecg-visual
|
||||
├─jeecg-cloud-monitor -- (9111)
|
||||
├─jeecg-cloud-xxljob -- (9080)
|
||||
├─jeecg-cloud-sentinel --sentinel (9000)
|
||||
├─jeecg-cloud-test
|
||||
├─jeecg-cloud-test-more
|
||||
├─jeecg-cloud-test-rabbitmq
|
||||
├─jeecg-cloud-test-seata
|
||||
├─jeecg-cloud-test-shardingsphere
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
Why JeecgBoot?
|
||||
-----------------------------------
|
||||
* Adopt the latest mainstream front and back separation framework (Springboot+Mybatis+antd), easy to use; Code generator has low dependency, flexible expansion ability, and can quickly realize secondary development;
|
||||
* Support microservices SpringCloud Alibaba(Nacos, Gateway, Sentinel, Skywalking), and provide switching mechanism to support free switching between single and microservices
|
||||
* High development efficiency, using code generator, single table, tree list, one-to-many, one-to-one and other data models, add, delete, change and search function one-key generation, menu configuration directly use;
|
||||
* Code generator provides powerful template mechanism, support custom template, currently provide four sets of style template (single table two sets, tree model one set, one to many three sets)
|
||||
* Code generator is very intelligent, online business modeling, online configuration, WYSIWYG support 23 kinds of controls, a key to generate front and back end code, greatly improve the development efficiency, no longer worry about repeated work.
|
||||
* Low code ability: Online online form (without coding, through online configuration of the form, to achieve the addition, deletion, change and check of the form, support single table, tree, one-to-many, one-to-one model, to achieve everyone can code)
|
||||
* Low code ability: Online online report (without coding, through online configuration, to achieve data report, can quickly extract data, reduce development pressure, to achieve everyone can code)
|
||||
* Low code ability: Online online chart (without coding, through online configuration, to achieve graphs, bar graphs, data reports, etc., support custom layout, to achieve everyone can code)
|
||||
* Complete encapsulation of user, role, menu, organization, data dictionary, online scheduled tasks and other basic functions, support access authorization, button permission, data permission and other functions
|
||||
* Commonly used common package, various tools (scheduled task, SMS interface, email sending,Excel import and export, etc.), basically meeting 80% of project requirements
|
||||
* Easy Excel import and export, support single table export and one-to-many table mode export, generated code with import and export function
|
||||
* Integrated simple report tools, image report and data export is very convenient, can be extremely convenient to generate graphical reports, pdf, excel, word and other reports;
|
||||
* Before and after the separation technology, the page UI style is exquisite, for the commonly used components to do the encapsulation: time, row table control, interception display control, report component, editor and so on
|
||||
* Query filter: query function automatically generated, the background dynamic spell SQL additional query conditions; Supports multiple matching modes (full matching, fuzzy query, included query, and unmatched query).
|
||||
* Data permission (fine data permission control, control to row level, list level, form field level, realize different people see different data, different people operate different fields on the same page
|
||||
* Page verification automatically generated (must be input, digital verification, amount verification, time and space, etc.);
|
||||
* Support SAAS service model and provide SaaS multi-tenant architecture solution.
|
||||
* Distributed file service, integration of minio, Ali OSS and other excellent third parties, to provide convenient file upload and management, but also support local storage.
|
||||
* Mainstream database compatibility, a set of code is fully compatible with Mysql, Postgresql, Oracle, Sqlserver, MariaDB, dream and other mainstream databases.
|
||||
* Integrate workflow flowable and realize only the configuration of flow direction in the page, which can greatly simplify the development of bpm workflow; Using bpm's process designer to draw the flow direction, a workflow is basically complete with a small amount of java code;
|
||||
* Low code ability: online process design, using open source Activiti process engine, to achieve online drawing process, custom form, form attachment, business flow
|
||||
* Multi-data source: its simple way of use, online configuration of data source configuration, convenient to grab data from other data;
|
||||
* Provide single sign-on CAS integration solution, and complete docking code has been provided in the project
|
||||
* Low code ability: form designer, support user custom form layout, support single table, one to many forms, support select, radio, checkbox, textarea, date, popup, list, macro and other controls
|
||||
* Professional interface docking mechanism, unified using restful interface, integrated swagger-ui online interface documentation, Jwt token security verification, convenient client docking
|
||||
* Interface security mechanism, can be refined control interface authorization, very simple to realize different clients only see their own data control
|
||||
* Advanced combination query function, online configuration support primary and sub-table associated query, can save the query history
|
||||
* Provide a variety of system monitoring, real-time tracking system running conditions (monitoring Redis, Tomcat, jvm, server information, request tracking, SQL monitoring)
|
||||
* Message center (support SMS, email, wechat push, etc.)
|
||||
* Integrate Websocket message notification mechanism
|
||||
* Excellent mobile adaptive effect, providing APP release scheme:
|
||||
* Support multiple languages and provide internationalization solutions;
|
||||
* Data change record log, can record each change of data content, through the version comparison function to view historical changes
|
||||
* The platform UI is powerful and mobile adaptation is implemented
|
||||
* Platform home page style, provide a variety of combination mode, support custom style
|
||||
* Provide easy to use print plug-in, support Google, Firefox, IE11+ and other browsers
|
||||
* Rich sample code, provide a lot of learning case reference
|
||||
* Using maven module development method
|
||||
* Support dynamic menu routing
|
||||
* RBAC (Role-Based Access Control) is used for permission control.
|
||||
* Provide new row edit table JVXETable, easily meet a variety of complex ERP layout, with higher performance, more flexible extension, more powerful functions
|
||||
|
||||
|
||||
|
||||
|
||||
Technical Architecture:
|
||||
-----------------------------------
|
||||
|
||||
#### Development Environment
|
||||
|
||||
- Language: Java 8+ (17)
|
||||
|
||||
- IDE(JAVA) : IDEA (lombok plug-in must be installed)
|
||||
|
||||
- IDE(front-end) : Vscode, WebStorm, IDEA
|
||||
|
||||
- Dependency management: Maven
|
||||
|
||||
- Cache: Redis
|
||||
|
||||
- Database: MySQL5.7 + [More Databases](https://my.oschina.net/jeecg/blog/4905722)
|
||||
|
||||
|
||||
#### backend
|
||||
|
||||
- Basic framework: Spring Boot 2.7.18
|
||||
|
||||
- Microservice framework: Spring Cloud Alibaba 2021.0.1.0
|
||||
|
||||
- Persistence layer framework: MybatisPlus 3.5.3.2
|
||||
|
||||
- Report tool: JimuReport 1.8.1
|
||||
|
||||
- Security framework: Apache Shiro 1.12.0, Jwt 3.11.0
|
||||
|
||||
- Microservice technology stack: Spring Cloud Alibaba, Nacos, Gateway, Sentinel, Skywalking
|
||||
|
||||
- Database connection pool: Alibaba Druid 1.1.22
|
||||
|
||||
- Log printing: logback
|
||||
|
||||
- Others: autopoi, fastjson, poi, Swagger-ui, quartz, lombok (simplified code), etc.
|
||||
|
||||
|
||||
#### The front end
|
||||
|
||||
- TechnologyStack:`Vue3.0+TypeScript+Vite+AntDesignVue+pinia+echarts`
|
||||
|
||||
#### Front-end environment requirements
|
||||
|
||||
* `Node.js 、npm 、pnpm`
|
||||
* Node.js Version suggestion: `v20.15.0`
|
||||
` ( Since Vite5 no longer supports EOL Node.js 14/16/17/19, Node.js 18/20 + is now required )`
|
||||
|
||||
#### Support library
|
||||
|
||||
| database | support |
|
||||
| --- | --- |
|
||||
| MySQL | √ |
|
||||
| Oracle11g | √ |
|
||||
| Sqlserver2017 | √ |
|
||||
| PostgreSQL | √ |
|
||||
| MariaDB | √ |
|
||||
| 达梦 | √ |
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||
|
||||
|
||||
## Microservice solutions
|
||||
|
||||
- 1. Service registration and discovery Nacos √
|
||||
- 2. Nacos √
|
||||
- 3. Route gateway gateway(Three loading modes) √
|
||||
- 4. Distributed http feign √
|
||||
- 5. fuse degrade current limiting Sentinel √
|
||||
- 6. Distributed files Minio and Alioss √
|
||||
- 7. Unified permission control
|
||||
- 8. Service monitoring SpringBootAdmin√
|
||||
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking.html)
|
||||
- 10. Messaging middleware RabbitMQ √
|
||||
- 11. Distributed task xxl-job √
|
||||
- 12. Distributed Transaction Seata
|
||||
- 13. Distributed log Loki+grafana
|
||||
- 14. Support docker-compose, k8s, jenkins
|
||||
- 15. CAS SSO √
|
||||
- 16. Route traffic limiting √
|
||||
|
||||
|
||||
#### Microservice architecture diagram
|
||||

|
||||
|
||||
### Jeecg Boot product functionality blueprint
|
||||

|
||||
|
||||
### quick start
|
||||
- Microservice Development: [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
|
||||
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud.html)
|
||||
|
||||
|
||||
### Effect of system
|
||||
|
||||
##### ChatGPT AI Dialog
|
||||
> Go to the JeecgBoot background home page and click "AI Assistant" in the middle of the right side of the home page. The AI Assistant dialog screen is displayed.
|
||||

|
||||
|
||||
|
||||
##### PC
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### interactive
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### process Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### min process
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### dashboard Designer
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
##### report Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### form Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### bigscreen Designer
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### uniapp
|
||||

|
||||
|
||||

|
||||
|
||||
##### low app
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### app
|
||||

|
||||

|
||||
|
||||
##### PAD
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
##### chart
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
##### swagger
|
||||

|
||||

|
||||
|
||||
|
||||
## donation
|
||||
|
||||
If so, buy the author a cup of coffee ☺
|
||||
|
||||

|
||||
573
README.md
573
README.md
@ -1,478 +1,219 @@
|
||||
|
||||
JeecgBoot 低代码开发平台
|
||||
Jeecg AI应用平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.7.1(发布日期:2024-09-12)
|
||||
当前最新版本: 1.0.0(发布日期:2025-12-05)
|
||||
|
||||
|
||||
[](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
|
||||
[](http://guojusoft.com)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://github.com/jeecgboot/JeecgBoot)
|
||||
[](https://jeecg.com)
|
||||
[](https://jeecg.blog.csdn.net)
|
||||
[](https://github.com/jeecgboot/jeecg-ai)
|
||||
[](https://github.com/jeecgboot/jeecg-ai)
|
||||
[](https://github.com/jeecgboot/jeecg-ai)
|
||||
|
||||
|
||||
|
||||
项目介绍
|
||||
-----------------------------------
|
||||
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
|
||||
|
||||
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
|
||||
|
||||
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务。强大的代码生成器让前后端代码一键生成,实现低代码开发! JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
|
||||
|
||||
JeecgBoot 提供了一系列`低代码模块`,实现在线开发`真正的零代码`:Online表单开发、Online报表、报表配置能力、在线图表设计、仪表盘设计、大屏设计、移动配置能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)等等!
|
||||
|
||||
|
||||
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现,做到`零代码开发`;复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
|
||||
|
||||
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。
|
||||
|
||||
|
||||
适用项目
|
||||
-----------------------------------
|
||||
Jeecg-Boot低代码开发平台,可以应用在任何J2EE项目的开发中,支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。
|
||||
JeecgBoot平台的AIGC功能模块,是一套类似`Dify`的`AIGC应用开发平台`+`知识库问答`,是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
|
||||
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等,让您可以快速从原型到生产,拥有AI服务能力。
|
||||
|
||||
|
||||
|
||||
#### 项目说明
|
||||
### AI视频介绍
|
||||
|
||||
| 项目名 | 说明 |
|
||||
|--------------------|------------------------|
|
||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite5+ts最新技术栈) |
|
||||
| `jeecg-uniapp` | [配套APP框架](https://github.com/jeecgboot/jeecg-uniapp) 适配多个终端,支持APP、小程序、H5 |
|
||||
[](https://www.bilibili.com/video/BV1zmd7YFE4w)
|
||||
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
##### 功能大模块
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
||||
- 开发文档: [https://help.jeecg.com](https://help.jeecg.com)
|
||||
- 反馈问题: [在Github上提Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video)
|
||||
- QQ交流群 : ⑨808791225、其他(满)
|
||||
- AI应用开发平台
|
||||
- AI知识库系统
|
||||
- AI大模型管理
|
||||
- AI流程编排
|
||||
- AI对话支持图片
|
||||
- AI对话助手(智能问答)
|
||||
- AI建表(Online表单)
|
||||
- AI写文章(CMS)
|
||||
- AI表单字段建议(表单设计器)
|
||||
|
||||
|
||||
|
||||
#### Dify `VS` JEECG AI
|
||||
|
||||
> JEECG AI与Dify相比,在多个方面展现出显著的优势,特别是在文档处理、格式和图片保持方面。以下是一些具体的优点:
|
||||
> - Markdown文档库导入:
|
||||
> JEECG AI允许用户直接导入整个Markdown文档库,这不仅保留markdown格式,还支持图片的导入,确保文档内容的完整性和可视化效果。
|
||||
> - 对话回复格式美观:
|
||||
> 在对话过程中,JEECG AI能够保持回复内容的原格式,也不丢失图片,使得输出的文章更加美观,不会出现格式错乱的情况,还支持图片的渲染。
|
||||
> - PDF文档导入与格式转换:
|
||||
> JEECG AI在处理PDF文档时,能够更好地保持原始格式和图片,确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳,而JEECG AI在这方面做出了显著的优化
|
||||
|
||||
|
||||
| 功能 | Dify | Jeecg AI |
|
||||
|------------|------------------|-----------------------------------------|
|
||||
| AI工作流 | 有 | 有 |
|
||||
| RAG 管道向量搜索 | 有 | 有 |
|
||||
| AI模型管理 | 有 | 有 |
|
||||
| AI应用管理 | 有 | 有 |
|
||||
| AI知识库 | 有 | 有 |
|
||||
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
|
||||
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
|
||||
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎,用户可以通过AI流程配置各种业务流和AI流程 |
|
||||
| 实现语言 | python + react | JAVA + vue3 |
|
||||
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
|
||||
| AI对话支持发图和展示图片 | 支持 | 支持 |
|
||||
|
||||
|
||||
|
||||
### 技术文档
|
||||
|
||||
- [AIGC开发文档](https://help.jeecg.com/aigc)
|
||||
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
|
||||
|
||||
|
||||
|
||||
## 功能特点
|
||||
|
||||
- AI流程: 提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||
- AI流程即服务: 通过AI流程编排你需要的智能体,结合AI+自定义开发节点 实现功能性 API,让你瞬间拥有各种智能体API。
|
||||
- AI助手对话功能: 集成 ChatGPT、Deepseek、智普、私有大模型 等 AI 模型,提供智能对话和生成式 AI 功能,深度与知识库结合提供更精准的知识。
|
||||
- RAG 功能: 涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本,支持检索增强生成(RAG),将未训练数据与 AI 模型集成,提升智能交互能力。
|
||||
- AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
|
||||
- 模型管理:支持对接各种大模型,包括本地私有大模型(Deepseek/ Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等)和国外公共大模型(OpenAI / Claude / Gemini 等);
|
||||
- 无缝嵌入:Iframe一键嵌入,支持将AI聊天助手快速嵌入到第三方系统,让系统快速拥有智能问答能力,提高用户满意度。
|
||||
|
||||
|
||||
|
||||
|
||||
启动项目
|
||||
-----------------------------------
|
||||
#### 在线体验
|
||||
|
||||
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
|
||||
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
|
||||
- JeecgBoot演示: https://boot3.jeecg.com
|
||||
- 敲敲云在线搭建AI知识库:https://app.qiaoqiaoyun.com
|
||||
|
||||
|
||||
## 技术交流
|
||||
|
||||
- 开发文档:https://help.jeecg.com/aigc
|
||||
- QQ群:964611995、716488839(满)
|
||||
|
||||
|
||||
## 功能列表
|
||||
|
||||
- AI应用管理(普通应用、高级流程应用)
|
||||
- AI模型管理
|
||||
- AI知识库
|
||||
- AI应用平台(普通、对接AI流程)
|
||||
- AI流程编排
|
||||
- AI聊天支持嵌入第三方
|
||||
- AI向量库对接
|
||||
|
||||
|
||||
|
||||
## 支持AI模型
|
||||
|
||||
| AI大模型 | 支持 |
|
||||
|---------------| --- |
|
||||
| DeepSeek | √ |
|
||||
| ChatGTP | √ |
|
||||
| Qwq | √ |
|
||||
| 智库 | √ |
|
||||
| Ollama本地搭建大模型 | √ |
|
||||
| 等等。。 | √ |
|
||||
|
||||
|
||||
|
||||
|
||||
## AIGC能做什么
|
||||
|
||||
AIGC模块是一个基于AI的自动化流程编排工具和聊天应用搭建平台,它可以帮助用户快速生成AI流程接口和聊天应用,提高效率。
|
||||
以下是一些具体的应用场景和示例:
|
||||
|
||||
- 你可能需要一个翻译接口,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个接口转换工具,可以通过AI流程编排搭建出来。(比如:jimureport所需要接口返回格式与你的系统不同,你通过AI接口实现自动转换)
|
||||
- 你可能需要一个聊天机器人,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个自动化流程,可以通过AI流程编排搭建出来。
|
||||
- 你可能需要一个自动化处理文件的流程,可以通过AI流程结合python脚本实现操作电脑,文件等。
|
||||
|
||||
|
||||
技术架构:
|
||||
-----------------------------------
|
||||
|
||||
#### 前端
|
||||
|
||||
- 前端环境要求:Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
|
||||
|
||||
` ( Vite 不再支持已结束生命周期(EOL)的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
|
||||
|
||||
- 依赖管理:node、npm、pnpm
|
||||
- 前端IDE建议:IDEA、WebStorm、Vscode
|
||||
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||
- 最新技术栈:Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||
|
||||
#### 后端
|
||||
|
||||
- IDE建议: IDEA (必须安装lombok插件 )
|
||||
- 语言:Java 8+ (支持17)
|
||||
- 语言:Java 默认jdk17(jdk21、jdk24)
|
||||
- 依赖管理:Maven
|
||||
- 基础框架:Spring Boot 2.7.18
|
||||
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
||||
- 持久层框架:MybatisPlus 3.5.3.2
|
||||
- 报表工具: JimuReport 1.8.1
|
||||
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
||||
- 基础框架:Spring Boot 3.5.5
|
||||
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
|
||||
- 持久层框架:MybatisPlus 3.5.12
|
||||
- 报表工具: JimuReport 2.1.3
|
||||
- 安全框架:Apache Shiro 2.0.4,Jwt 4.5.0
|
||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||
- 数据库连接池:阿里巴巴Druid 1.1.22
|
||||
- 数据库连接池:阿里巴巴Druid 1.2.24
|
||||
- AI大模型:支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
|
||||
- 日志打印:logback
|
||||
- 缓存:Redis
|
||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||
- 默认数据库脚本:MySQL5.7+
|
||||
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
||||
- 默认提供MySQL5.7+数据库脚本
|
||||
|
||||
|
||||
#### 前端
|
||||
## AI应用平台功能展示
|
||||
|
||||
- 前端IDE建议:WebStorm、Vscode
|
||||
- 采用 Vue3.0+TypeScript+Vite5+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||
- 最新技术栈:Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||
- 依赖管理:node、npm、pnpm
|
||||
AI模型列表
|
||||
|
||||

|
||||
|
||||
#### 前端环境要求
|
||||
选择AI模型,配置你的参数
|
||||
|
||||
* 本地环境安装 `Node.js 、npm 、pnpm`
|
||||
* Node.js 版本建议`v20.15.0`,要求`Node 20+` 版本以上
|
||||

|
||||
|
||||
` ( 因为Vite5 不再支持已 EOL 的 Node.js 14 / 16 / 17 / 19,现在需要 Node.js 18 / 20+ )`
|
||||
|
||||
AI知识库支持手工录入文本,导入pdf\\word\\excel等文档,支持问答对训练
|
||||
|
||||
#### 支持库
|
||||

|
||||
|
||||
| 数据库 | 支持 |
|
||||
| --- | --- |
|
||||
| MySQL | √ |
|
||||
| Oracle11g | √ |
|
||||
| Sqlserver2017 | √ |
|
||||
| PostgreSQL | √ |
|
||||
| MariaDB | √ |
|
||||
| MariaDB | √ |
|
||||
| 达梦 | √ |
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||

|
||||
|
||||
|
||||
## 微服务解决方案
|
||||
|
||||
|
||||
- 1、服务注册和发现 Nacos √
|
||||
- 2、统一配置中心 Nacos √
|
||||
- 3、路由网关 gateway(三种加载方式) √
|
||||
- 4、分布式 http feign √
|
||||
- 5、熔断降级限流 Sentinel √
|
||||
- 6、分布式文件 Minio、阿里OSS √
|
||||
- 7、统一权限控制 JWT + Shiro √
|
||||
- 8、服务监控 SpringBootAdmin√
|
||||
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
|
||||
- 10、消息中间件 RabbitMQ √
|
||||
- 11、分布式任务 xxl-job √
|
||||
- 12、分布式事务 Seata
|
||||
- 13、轻量分布式日志 Loki+grafana套件
|
||||
- 14、支持 docker-compose、k8s、jenkins
|
||||
- 15、CAS 单点登录 √
|
||||
- 16、路由限流 √
|
||||
AI流程,提供强大的AI流程设计器引擎,支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
|
||||
|
||||

|
||||
|
||||
#### 微服务方式启动
|
||||
|
||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
|
||||
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud.html)
|
||||
目前支持的节点有:开始、结束、AI知识库节点、AI节点、分类节点、分支节点、JAVA节点、脚本节点、子流程节点、http请求节点、直接回复节点等节点
|
||||
|
||||

|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
节点项配置
|
||||
|
||||

|
||||
|
||||
在线运行看结果
|
||||
|
||||
为什么选择JeecgBoot?
|
||||
-----------------------------------
|
||||
* 1.采用最新主流前后分离框架(Springboot+Mybatis+antd+vue3),容易上手; 代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发;
|
||||
* 2.支持微服务SpringCloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供切换机制支持单体和微服务自由切换
|
||||
* 3.开发效率高,采用代码生成器,单表、树列表、一对多、一对一等数据模型,增删改查功能一键生成,菜单配置直接使用;
|
||||
* 4.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)
|
||||
* 5.代码生成器非常智能,在线业务建模、在线配置、所见即所得支持23种类控件,一键生成前后端代码,大幅度提升开发效率,不再为重复工作发愁。
|
||||
* 6.低代码能力:Online在线表单(无需编码,通过在线配置表单,实现表单的增删改查,支持单表、树、一对多、一对一等模型,实现人人皆可编码)
|
||||
* 7.低代码能力:Online在线报表(无需编码,通过在线配置方式,实现数据报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)
|
||||
* 8.低代码能力:Online在线图表(无需编码,通过在线配置方式,实现曲线图,柱状图,数据报表等,支持自定义排版布局,实现人人皆可编码)
|
||||
* 9.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
|
||||
* 10.常用共通封装,各种工具类(定时任务,短信接口,邮件发送,Excel导入导出等),基本满足80%项目需求
|
||||
* 11.简易Excel导入导出,支持单表导出和一对多表模式导出,生成的代码自带导入导出功能
|
||||
* 12.集成简易报表工具,图像报表和数据导出非常方便,可极其方便的生成图形报表、pdf、excel、word等报表;
|
||||
* 13.采用前后分离技术,页面UI风格精美,针对常用组件做了封装:时间、行表格控件、截取显示控件、报表组件,编辑器等等
|
||||
* 14.查询过滤器:查询功能自动生成,后台动态拼SQL追加查询条件;支持多种匹配方式(全匹配/模糊查询/包含查询/不匹配查询);
|
||||
* 15.数据权限(精细化数据权限控制,控制到行级,列表级,表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段
|
||||
* 16.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等);
|
||||
* 17.支持SAAS服务模式,提供SaaS多租户架构方案。
|
||||
* 18.分布式文件服务,集成minio、阿里OSS等优秀的第三方,提供便捷的文件上传与管理,同时也支持本地存储。
|
||||
* 19.主流数据库兼容,一套代码完全兼容Mysql、Postgresql、Oracle、Sqlserver、MariaDB、达梦、人大金仓等主流数据库。
|
||||
* 20.集成工作流flowable,并实现了只需在页面配置流程转向,可极大的简化bpm工作流的开发;用bpm的流程设计器画出了流程走向,一个工作流基本就完成了,只需写很少量的java代码;
|
||||
* 21.低代码能力:在线流程设计,采用开源flowable流程引擎,实现在线画流程,自定义表单,表单挂靠,业务流转
|
||||
* 22.多数据源:及其简易的使用方式,在线配置数据源配置,便捷的从其他数据抓取数据;
|
||||
* 23.提供单点登录CAS集成方案,项目中已经提供完善的对接代码
|
||||
* 24.低代码能力:表单设计器,支持用户自定义表单布局,支持单表,一对多表单、支持select、radio、checkbox、textarea、date、popup、列表、宏等控件
|
||||
* 25.专业接口对接机制,统一采用restful接口方式,集成swagger-ui在线接口文档,Jwt token安全验证,方便客户端对接
|
||||
* 26.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制
|
||||
* 27.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史
|
||||
* 28.提供各种系统监控,实时跟踪系统运行情况(监控 Redis、Tomcat、jvm、服务器信息、请求追踪、SQL监控)
|
||||
* 29.消息中心(支持短信、邮件、微信推送等等)
|
||||
* 30.集成Websocket消息通知机制
|
||||
* 31.移动自适应效果优秀,提供APP发布方案:
|
||||
* 32.支持多语言,提供国际化方案;
|
||||
* 33.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化
|
||||
* 34.平台UI强大,实现了移动自适应
|
||||
* 35.平台首页风格,提供多种组合模式,支持自定义风格
|
||||
* 36.提供简单易用的打印插件,支持谷歌、火狐、IE11+ 等各种浏览器
|
||||
* 37.示例代码丰富,提供很多学习案例参考
|
||||
* 38.采用maven分模块开发方式
|
||||
* 39.支持菜单动态路由
|
||||
* 40.权限控制采用 RBAC(Role-Based Access Control,基于角色的访问控制)
|
||||
* 41.提供新行编辑表格JVXETable,轻松满足各种复杂ERP布局,拥有更高的性能、更灵活的扩展、更强大的功能
|
||||
* 42.提供仪表盘设计器,类大屏设计支持移动端,免费的数据可视化设计工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作图表和门户设计;目前支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等等;
|
||||

|
||||
|
||||
|
||||
### Jeecg Boot 产品功能蓝图
|
||||

|
||||
AI应用配置,支持AI流程配置和简单的AI配置
|
||||
|
||||

|
||||
|
||||
可以关联多个知识库,右侧是AI智能回复,你可以搭建自己的智能体,比如搭建一个 “诗词达人” “翻译助手”
|
||||
|
||||

|
||||
|
||||
### 分支说明
|
||||
可以将创建的聊天应用,集成到第三方系统中
|
||||
|
||||
> 主干master更稳定,如果你对最新技术栈无要求,建议采用主干
|
||||
|
||||
#### springboot3分支
|
||||
- 源码地址:https://github.com/jeecgboot/JeecgBoot/tree/springboot3
|
||||
- 架构说明:升级Spring Boot3 & JDK 17 + Undertow + springdoc + fastjson2
|
||||
|
||||
#### springboot3_sas分支
|
||||
- 源码地址:https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas
|
||||
- 架构说明:在springboot3分支基础上,采用SpringAuthorizationServer替换Shiro
|
||||
|
||||
|
||||
|
||||
### 功能模块
|
||||
```
|
||||
├─Online在线开发(低代码)
|
||||
│ ├─Online在线表单
|
||||
│ ├─Online代码生成器
|
||||
│ ├─Online在线报表
|
||||
│ ├─仪表盘设计器
|
||||
│ ├─AI助手
|
||||
│ ├─系统编码规则
|
||||
│ ├─系统校验规则
|
||||
├─积木报表设计器
|
||||
│ ├─打印设计器
|
||||
│ ├─数据报表设计
|
||||
│ ├─图形报表设计(支持echart)
|
||||
├─系统管理
|
||||
│ ├─用户管理
|
||||
│ ├─角色管理
|
||||
│ ├─菜单管理
|
||||
│ ├─权限设置(支持按钮权限、数据权限)
|
||||
│ ├─表单权限(控制字段禁用、隐藏)
|
||||
│ ├─部门管理
|
||||
│ ├─我的部门(二级管理员)
|
||||
│ └─字典管理
|
||||
│ └─分类字典
|
||||
│ └─系统公告
|
||||
│ └─职务管理
|
||||
│ └─通讯录
|
||||
│ ├─多数据源管理
|
||||
│ └─多租户管理(租户管理、租户角色、我的租户)
|
||||
├─消息中心
|
||||
│ ├─消息管理
|
||||
│ ├─模板管理
|
||||
├─代码生成器(低代码)
|
||||
│ ├─代码生成器功能(一键生成前后端代码,生成后无需修改直接用,绝对是后端开发福音)
|
||||
│ ├─代码生成器模板(提供4套模板,分别支持单表和一对多模型,不同风格选择)
|
||||
│ ├─代码生成器模板(生成代码,自带excel导入导出)
|
||||
│ ├─查询过滤器(查询逻辑无需编码,系统根据页面配置自动生成)
|
||||
│ ├─高级查询器(弹窗自动组合查询条件)
|
||||
│ ├─Excel导入导出工具集成(支持单表,一对多 导入导出)
|
||||
│ ├─平台移动自适应支持
|
||||
├─系统监控
|
||||
│ ├─Gateway路由网关
|
||||
│ ├─性能扫描监控
|
||||
│ │ ├─监控 Redis
|
||||
│ │ ├─Tomcat
|
||||
│ │ ├─jvm
|
||||
│ │ ├─服务器信息
|
||||
│ │ ├─请求追踪
|
||||
│ │ ├─磁盘监控
|
||||
│ ├─定时任务
|
||||
│ ├─系统日志
|
||||
│ ├─消息中心(支持短信、邮件、微信推送等等)
|
||||
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
|
||||
│ ├─系统通知
|
||||
│ ├─SQL监控
|
||||
│ ├─swagger-ui(在线接口文档)
|
||||
│─报表示例
|
||||
│ ├─曲线图
|
||||
│ └─饼状图
|
||||
│ └─柱状图
|
||||
│ └─折线图
|
||||
│ └─面积图
|
||||
│ └─雷达图
|
||||
│ └─仪表图
|
||||
│ └─进度条
|
||||
│ └─排名列表
|
||||
│ └─等等
|
||||
│─大屏模板
|
||||
│ ├─作战指挥中心大屏
|
||||
│ └─物流服务中心大屏
|
||||
│─常用示例
|
||||
│ ├─自定义组件
|
||||
│ ├─对象存储(对接阿里云)
|
||||
│ ├─JVXETable示例(各种复杂ERP布局示例)
|
||||
│ ├─单表模型例子
|
||||
│ └─一对多模型例子
|
||||
│ └─打印例子
|
||||
│ └─一对多TAB例子
|
||||
│ └─内嵌table例子
|
||||
│ └─常用选择组件
|
||||
│ └─异步树table
|
||||
│ └─接口模拟测试
|
||||
│ └─表格合计示例
|
||||
│ └─异步树列表示例
|
||||
│ └─一对多JEditable
|
||||
│ └─JEditable组件示例
|
||||
│ └─图片拖拽排序
|
||||
│ └─图片翻页
|
||||
│ └─图片预览
|
||||
│ └─PDF预览
|
||||
│ └─分屏功能
|
||||
│─封装通用组件
|
||||
│ ├─行编辑表格JEditableTable
|
||||
│ └─省略显示组件
|
||||
│ └─时间控件
|
||||
│ └─高级查询
|
||||
│ └─用户选择组件
|
||||
│ └─报表组件封装
|
||||
│ └─字典组件
|
||||
│ └─下拉多选组件
|
||||
│ └─选人组件
|
||||
│ └─选部门组件
|
||||
│ └─通过部门选人组件
|
||||
│ └─封装曲线、柱状图、饼状图、折线图等等报表的组件(经过封装,使用简单)
|
||||
│ └─在线code编辑器
|
||||
│ └─上传文件组件
|
||||
│ └─验证码组件
|
||||
│ └─树列表组件
|
||||
│ └─表单禁用组件
|
||||
│ └─等等
|
||||
│─更多页面模板
|
||||
│ ├─各种高级表单
|
||||
│ ├─各种列表效果
|
||||
│ └─结果页面
|
||||
│ └─异常页面
|
||||
│ └─个人页面
|
||||
├─高级功能
|
||||
│ ├─提供单点登录CAS集成方案
|
||||
│ ├─提供APP发布方案
|
||||
│ ├─集成Websocket消息通知机制
|
||||
│─更多商业功能
|
||||
│ ├─流程设计器
|
||||
│ ├─表单设计器
|
||||
│ ├─大屏设计器
|
||||
│ └─我的任务
|
||||
│ └─历史流程
|
||||
│ └─历史流程
|
||||
│ └─流程实例管理
|
||||
│ └─流程监听管理
|
||||
│ └─流程表达式
|
||||
│ └─我发起的流程
|
||||
│ └─我的抄送
|
||||
│ └─流程委派、抄送、跳转
|
||||
│ └─OA办公组件
|
||||
│ └─。。。
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 系统效果
|
||||
|
||||
##### PC端
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
##### 系统交互
|
||||

|
||||
|
||||

|
||||
|
||||
##### AI助手
|
||||

|
||||
|
||||
|
||||
##### 仪表盘设计器
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 报表设计器
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 手机端
|
||||

|
||||

|
||||
|
||||
##### PAD端
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
##### 图表示例
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
##### 在线接口文档
|
||||

|
||||

|
||||
|
||||
|
||||
##### UNIAPP效果
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 大屏设计器
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
##### 流程设计
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
##### 表单设计器
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果觉得还不错,请作者喝杯咖啡吧 ☺
|
||||
|
||||

|
||||

|
||||
216
check_jeecgenv.py
Normal file
216
check_jeecgenv.py
Normal file
@ -0,0 +1,216 @@
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
from typing import Tuple, Optional
|
||||
|
||||
def run_command(cmd: str) -> Tuple[int, str]:
|
||||
"""执行命令并返回退出码和输出"""
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, check=False,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True)
|
||||
return result.returncode, result.stdout.strip()
|
||||
except Exception as e:
|
||||
return -1, str(e)
|
||||
|
||||
def check_java() -> bool:
|
||||
"""检查JDK 17+是否安装"""
|
||||
print("\n检查JDK 17+...")
|
||||
rc, output = run_command("java -version 2>&1")
|
||||
if rc != 0:
|
||||
print("❌ 未检测到Java,请安装JDK 17+")
|
||||
return False
|
||||
|
||||
version_pattern = r'"(\d+)(?:\.\d+)*(?:_\d+)?'
|
||||
match = re.search(version_pattern, output)
|
||||
if not match:
|
||||
print("❌ 无法解析Java版本")
|
||||
return False
|
||||
|
||||
version = int(match.group(1))
|
||||
if version >= 17:
|
||||
print(f"✅ JDK版本 {version} (满足17+要求)")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ JDK版本 {version} (需要17+)")
|
||||
return False
|
||||
|
||||
def check_maven() -> bool:
|
||||
"""检查Maven是否安装"""
|
||||
print("\n检查Maven...")
|
||||
rc, output = run_command("mvn -v")
|
||||
if rc == 0:
|
||||
print("✅ Maven已安装")
|
||||
return True
|
||||
else:
|
||||
print("❌ Maven未安装")
|
||||
return False
|
||||
|
||||
def check_node() -> bool:
|
||||
"""检查Node.js 20+是否安装"""
|
||||
print("\n检查Node.js 20+...")
|
||||
rc, output = run_command("node -v")
|
||||
if rc != 0:
|
||||
print("❌ Node.js未安装")
|
||||
return False
|
||||
|
||||
version_pattern = r'v(\d+)\.\d+\.\d+'
|
||||
match = re.search(version_pattern, output)
|
||||
if not match:
|
||||
print("❌ 无法解析Node.js版本")
|
||||
return False
|
||||
|
||||
version = int(match.group(1))
|
||||
if version >= 20:
|
||||
print(f"✅ Node.js版本 {version} (满足20+要求)")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Node.js版本 {version} (需要20+)")
|
||||
return False
|
||||
|
||||
def check_pnpm() -> bool:
|
||||
"""检查PNPM 9+是否安装"""
|
||||
print("\n检查PNPM 9+...")
|
||||
rc, output = run_command("pnpm -v")
|
||||
if rc != 0:
|
||||
print("❌ PNPM未安装")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 处理可能的版本号格式:v9.0.0 或 9.0.0 或 9
|
||||
version_str = output.strip().lstrip('v').split('.')[0]
|
||||
version = int(version_str)
|
||||
|
||||
if version >= 9:
|
||||
print(f"✅ PNPM版本 {output.strip()} (满足9+要求)")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ PNPM版本 {output.strip()} (需要9+)")
|
||||
return False
|
||||
except (ValueError, IndexError):
|
||||
print(f"❌ 无法解析PNPM版本: {output.strip()}")
|
||||
return False
|
||||
|
||||
def check_redis_connection() -> bool:
|
||||
"""检查Redis连接"""
|
||||
print("\n检查Redis连接...")
|
||||
print("⚠️ 请确保已配置Redis连接信息并在jeecg-boot项目中正确配置")
|
||||
print("⚠️ 此检查需要根据实际项目配置进行验证")
|
||||
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
|
||||
return True
|
||||
|
||||
def check_mysql_connection() -> bool:
|
||||
"""检查MySQL连接"""
|
||||
print("\n检查MySQL连接...")
|
||||
print("⚠️ 请确保已配置MySQL连接信息并在jeecg-boot项目中正确配置")
|
||||
print("⚠️ 此检查需要根据实际项目配置进行验证")
|
||||
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
|
||||
return True
|
||||
|
||||
def print_mysql_config():
|
||||
"""打印MySQL配置并提示需要修改的位置"""
|
||||
print("\nMySQL配置参考 (请检查以下配置是否正确):")
|
||||
print("""
|
||||
spring.datasource.dynamic.datasource:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
||||
username: root # ← 可能需要修改
|
||||
password: root # ← 可能需要修改
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
""")
|
||||
|
||||
def check_ai_vector_db() -> bool:
|
||||
"""检查AI向量库(pgvector)配置"""
|
||||
print("\n检查AI知识库向量库配置...")
|
||||
print("⚠️ 如果需要使用AI知识库功能,请配置pgvector向量库")
|
||||
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
|
||||
print("\n配置参考:")
|
||||
print("""
|
||||
jeecg.ai-rag:
|
||||
embed-store:
|
||||
host: 127.0.0.1 # ← 可能需要修改
|
||||
port: 5432 # ← 可能需要修改
|
||||
database: postgres # ← 可能需要修改
|
||||
user: postgres # ← 可能需要修改
|
||||
password: postgres # ← 可能需要修改
|
||||
table: embeddings # ← 可能需要修改
|
||||
""")
|
||||
print("⚠️ 注意: 请确保已安装PostgreSQL并添加pgvector扩展!docker安装参考:https://help.jeecg.com/aigc/config")
|
||||
return True
|
||||
|
||||
def check_ai_config() -> bool:
|
||||
"""检查AI账号配置"""
|
||||
print("\n检查AI功能配置...")
|
||||
print("⚠️ 如果需要使用AI聊天功能,请配置AI账号信息")
|
||||
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
|
||||
print("\n配置参考:")
|
||||
print("""
|
||||
jeecg:
|
||||
# AI集成
|
||||
ai-chat:
|
||||
enabled: true # ← 启用AI功能
|
||||
model: deepseek-chat # ← 模型名称
|
||||
apiKey: ?? # ← 必须修改为您的API Key
|
||||
apiHost: https://api.deepseek.com/v1 # ← API地址
|
||||
timeout: 60 # ← 超时时间(秒)
|
||||
""")
|
||||
print("⚠️ 注意: 请确保已获取有效的API Key并正确配置!AI账号注册获取参考: https://help.jeecg.com/java/deepSeekSupport")
|
||||
return True
|
||||
|
||||
|
||||
def print_redis_config():
|
||||
"""打印Redis配置并提示需要修改的位置"""
|
||||
print("\nRedis配置参考 (请检查以下配置是否正确):")
|
||||
print("""
|
||||
spring.redis:
|
||||
database: 0
|
||||
host: 127.0.0.1 # ← 可能需要修改
|
||||
port: 6379 # ← 可能需要修改
|
||||
password: '' # ← 如果需要密码请修改
|
||||
""")
|
||||
|
||||
def main():
|
||||
print("="*50)
|
||||
print("JeecgBoot 运行环境检查脚本")
|
||||
print("="*50)
|
||||
|
||||
all_checks_passed = True
|
||||
|
||||
# 检查各项依赖
|
||||
if not check_java():
|
||||
all_checks_passed = False
|
||||
|
||||
if not check_maven():
|
||||
all_checks_passed = False
|
||||
|
||||
if not check_node():
|
||||
all_checks_passed = False
|
||||
|
||||
if not check_pnpm():
|
||||
all_checks_passed = False
|
||||
|
||||
# 数据库提示
|
||||
print("="*50)
|
||||
check_redis_connection()
|
||||
print_redis_config()
|
||||
print("="*50)
|
||||
check_mysql_connection()
|
||||
print_mysql_config()
|
||||
print("="*50)
|
||||
check_ai_config()
|
||||
print("="*50)
|
||||
check_ai_vector_db()
|
||||
|
||||
print("\n" + "="*50)
|
||||
if all_checks_passed:
|
||||
print("✅ 所有基础环境检查通过")
|
||||
print("⚠️ 注意: 请确保Redis和MySQL、AI账号、向量库pgvector 已正确配置并连接成功")
|
||||
else:
|
||||
print("❌ 部分环境检查未通过,请根据上述提示解决问题")
|
||||
|
||||
print("="*50)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
input("\n按回车键退出...") # 等待用户输入
|
||||
@ -1,135 +0,0 @@
|
||||
version: '2'
|
||||
services:
|
||||
jeecg-boot-mysql:
|
||||
build:
|
||||
context: ./jeecg-boot/db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
TZ: Asia/Shanghai
|
||||
restart: always
|
||||
container_name: jeecg-boot-mysql
|
||||
image: jeecg-boot-mysql
|
||||
command:
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_general_ci
|
||||
--explicit_defaults_for_timestamp=true
|
||||
--lower_case_table_names=1
|
||||
--max_allowed_packet=128M
|
||||
--default-authentication-plugin=caching_sha2_password
|
||||
ports:
|
||||
- 3306:3306
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-redis:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
|
||||
ports:
|
||||
- 6379:6379
|
||||
restart: always
|
||||
hostname: jeecg-boot-redis
|
||||
container_name: jeecg-boot-redis
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-nacos:
|
||||
restart: always
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-cloud-nacos
|
||||
ports:
|
||||
- 8848:8848
|
||||
container_name: jeecg-boot-nacos
|
||||
depends_on:
|
||||
- jeecg-boot-mysql
|
||||
hostname: jeecg-boot-nacos
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-system:
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-system-cloud-start
|
||||
container_name: jeecg-system-start
|
||||
hostname: jeecg-boot-system
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-demo:
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-demo-cloud-start
|
||||
container_name: jeecg-demo-start
|
||||
hostname: jeecg-boot-demo
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-gateway:
|
||||
restart: on-failure
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-server-cloud/jeecg-cloud-gateway
|
||||
ports:
|
||||
- 9999:9999
|
||||
depends_on:
|
||||
- jeecg-boot-nacos
|
||||
- jeecg-boot-system
|
||||
container_name: jeecg-boot-gateway
|
||||
hostname: jeecg-boot-gateway
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
# jeecg-boot-rabbitmq:
|
||||
# image: rabbitmq:3.7.7-management
|
||||
# ports:
|
||||
# - 5672:5672
|
||||
# - 15672:15672
|
||||
# restart: always
|
||||
# container_name: jeecg-boot-rabbitmq
|
||||
# hostname: jeecg-boot-rabbitmq
|
||||
# environment:
|
||||
# RABBITMQ_DEFAULT_USER: guest
|
||||
# RABBITMQ_DEFAULT_PASS: guest
|
||||
# jeecg-boot-sentinel:
|
||||
# restart: on-failure
|
||||
# build:
|
||||
# context: ./jeecg-visual/jeecg-cloud-sentinel
|
||||
# ports:
|
||||
# - 9000:9000
|
||||
# depends_on:
|
||||
# - jeecg-boot-nacos
|
||||
# - jeecg-boot-demo
|
||||
# - jeecg-boot-system
|
||||
# - jeecg-boot-gateway
|
||||
# container_name: jeecg-boot-sentinel
|
||||
# hostname: jeecg-boot-sentinel
|
||||
#
|
||||
# jeecg-boot-xxljob:
|
||||
# build:
|
||||
# context: ./jeecg-visual/jeecg-cloud-xxljob
|
||||
# ports:
|
||||
# - 9080:9080
|
||||
# container_name: jeecg-boot-xxljob
|
||||
# hostname: jeecg-boot-xxljob
|
||||
|
||||
jeecg-vue:
|
||||
build:
|
||||
context: ./jeecgboot-vue3
|
||||
container_name: jeecgboot-vue3-nginx
|
||||
image: jeecgboot-vue3
|
||||
depends_on:
|
||||
- jeecg-boot-system
|
||||
networks:
|
||||
- jeecg-boot
|
||||
ports:
|
||||
- 80:80
|
||||
|
||||
networks:
|
||||
jeecg-boot:
|
||||
name: jeecg_boot
|
||||
@ -18,20 +18,33 @@ services:
|
||||
--max_allowed_packet=128M
|
||||
--default-authentication-plugin=caching_sha2_password
|
||||
ports:
|
||||
- 3306:3306
|
||||
- 13306:3306
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-redis:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
|
||||
ports:
|
||||
- 6379:6379
|
||||
# ports:
|
||||
# - 3792:6379
|
||||
restart: always
|
||||
hostname: jeecg-boot-redis
|
||||
container_name: jeecg-boot-redis
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-pgvector:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
|
||||
container_name: jeecg-boot-pgvector
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: vector_db
|
||||
ports:
|
||||
- 5432:5432
|
||||
restart: always
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-system:
|
||||
build:
|
||||
context: ./jeecg-boot/jeecg-module-system/jeecg-system-start
|
||||
|
||||
@ -198,16 +198,4 @@
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
|
||||
|
||||
JeecgBoot 是由 北京国炬信息技术有限公司 发行的软件。 总部位于北京,地址:中国·北京·朝阳区科荟前街1号院奥林佳泰大厦。邮箱:jeecgos@163.com
|
||||
本软件受适用的国家软件著作权法(包括国际条约)和开源协议 双重保护许可。
|
||||
|
||||
开源协议中文释意如下:
|
||||
1.JeecgBoot开源版本无任何限制,在遵循本开源协议条款下,允许商用使用,不会造成侵权行为。
|
||||
2.允许基于本平台软件开展业务系统开发。
|
||||
3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。
|
||||
|
||||
最终解释权归:http://www.jeecg.com
|
||||
limitations under the License.
|
||||
@ -1,165 +0,0 @@
|
||||
|
||||
JeecgBoot 低代码开发平台
|
||||
===============
|
||||
|
||||
当前最新版本: 3.7.1(发布日期:2024-09-12)
|
||||
|
||||
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
|
||||
[](http://jeecg.com/aboutusIndex)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
[](https://github.com/zhangdaiscott/jeecg-boot)
|
||||
|
||||
|
||||
|
||||
项目介绍
|
||||
-----------------------------------
|
||||
|
||||
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
|
||||
|
||||
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.x,SpringCloud,Ant Design Vue3,Mybatis-plus,Shiro,JWT,支持微服务。强大的代码生成器让前后端代码一键生成,实现低代码开发! JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
|
||||
|
||||
|
||||
#### 项目说明
|
||||
|
||||
| 项目名 | 说明 |
|
||||
|--------------------|------------------------|
|
||||
| `jeecg-boot` | 后端源码JAVA(SpringBoot微服务架构) |
|
||||
| `jeecgboot-vue3` | 前端源码VUE3(vue3+vite5+ts最新技术栈) |
|
||||
|
||||
|
||||
|
||||
技术文档
|
||||
-----------------------------------
|
||||
|
||||
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
|
||||
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
|
||||
- QQ交流群 : ⑨808791225、其他(满)
|
||||
- 在线演示 : [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
|
||||
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
|
||||
|
||||
|
||||
|
||||
启动项目
|
||||
-----------------------------------
|
||||
|
||||
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
|
||||
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
|
||||
|
||||
|
||||
微服务启动
|
||||
-----------------------------------
|
||||
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
|
||||
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud.html)
|
||||
|
||||
|
||||
|
||||
技术架构:
|
||||
-----------------------------------
|
||||
|
||||
#### 后端
|
||||
|
||||
- IDE建议: IDEA (必须安装lombok插件 )
|
||||
- 语言:Java 8+ (支持17)
|
||||
- 依赖管理:Maven
|
||||
- 基础框架:Spring Boot 2.7.18
|
||||
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
|
||||
- 持久层框架:MybatisPlus 3.5.3.2
|
||||
- 报表工具: JimuReport 1.8.1
|
||||
- 安全框架:Apache Shiro 1.12.0,Jwt 3.11.0
|
||||
- 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
|
||||
- 数据库连接池:阿里巴巴Druid 1.1.22
|
||||
- 日志打印:logback
|
||||
- 缓存:Redis
|
||||
- 其他:autopoi, fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。
|
||||
- 默认数据库脚本:MySQL5.7+
|
||||
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
|
||||
|
||||
|
||||
#### 前端
|
||||
|
||||
- 前端IDE建议:WebStorm、Vscode
|
||||
- 采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案,包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
|
||||
- 最新技术栈:Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
|
||||
- 依赖管理:node、npm、pnpm
|
||||
|
||||
|
||||
|
||||
#### 支持库
|
||||
|
||||
| 数据库 | 支持 |
|
||||
| --- | --- |
|
||||
| MySQL | √ |
|
||||
| Oracle11g | √ |
|
||||
| Sqlserver2017 | √ |
|
||||
| PostgreSQL | √ |
|
||||
| MariaDB | √ |
|
||||
| 达梦 | √ |
|
||||
| 人大金仓 | √ |
|
||||
| TiDB | √ |
|
||||
|
||||
|
||||
|
||||
|
||||
## 微服务解决方案
|
||||
|
||||
|
||||
- 1、服务注册和发现 Nacos √
|
||||
- 2、统一配置中心 Nacos √
|
||||
- 3、路由网关 gateway(三种加载方式) √
|
||||
- 4、分布式 http feign √
|
||||
- 5、熔断降级限流 Sentinel √
|
||||
- 6、分布式文件 Minio、阿里OSS √
|
||||
- 7、统一权限控制 JWT + Shiro √
|
||||
- 8、服务监控 SpringBootAdmin√
|
||||
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
|
||||
- 10、消息中间件 RabbitMQ √
|
||||
- 11、分布式任务 xxl-job √
|
||||
- 12、分布式事务 Seata
|
||||
- 13、轻量分布式日志 Loki+grafana套件
|
||||
- 14、支持 docker-compose、k8s、jenkins
|
||||
- 15、CAS 单点登录 √
|
||||
- 16、路由限流 √
|
||||
|
||||
|
||||
|
||||
后台目录结构
|
||||
-----------------------------------
|
||||
```
|
||||
项目结构
|
||||
├─jeecg-boot-parent(父POM: 项目依赖、modules组织)
|
||||
│ ├─jeecg-boot-base-core(共通模块: 工具类、config、权限、查询过滤器、注解等)
|
||||
│ ├─jeecg-module-demo 示例代码
|
||||
│ ├─jeecg-module-system System系统管理目录
|
||||
│ │ ├─jeecg-system-biz System系统管理权限等功能
|
||||
│ │ ├─jeecg-system-start System单体启动项目(8080)
|
||||
│ │ ├─jeecg-system-api System系统管理模块对外api
|
||||
│ │ │ ├─jeecg-system-cloud-api System模块对外提供的微服务接口
|
||||
│ │ │ ├─jeecg-system-local-api System模块对外提供的单体接口
|
||||
│ ├─jeecg-server-cloud --微服务模块
|
||||
├─jeecg-cloud-gateway --微服务网关模块(9999)
|
||||
├─jeecg-cloud-nacos --Nacos服务模块(8848)
|
||||
├─jeecg-system-cloud-start --System微服务启动项目(7001)
|
||||
├─jeecg-demo-cloud-start --Demo微服务启动项目(7002)
|
||||
├─jeecg-visual
|
||||
├─jeecg-cloud-monitor --微服务监控模块 (9111)
|
||||
├─jeecg-cloud-xxljob --微服务xxljob定时任务服务端 (9080)
|
||||
├─jeecg-cloud-sentinel --sentinel服务端 (9000)
|
||||
├─jeecg-cloud-test -- 微服务测试示例(各种例子)
|
||||
├─jeecg-cloud-test-more -- 微服务测试示例(feign、熔断降级、xxljob、分布式锁)
|
||||
├─jeecg-cloud-test-rabbitmq -- 微服务测试示例(rabbitmq)
|
||||
├─jeecg-cloud-test-seata -- 微服务测试示例(seata分布式事务)
|
||||
├─jeecg-cloud-test-shardingsphere -- 微服务测试示例(分库分表)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### 微服务架构图
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -8,6 +8,6 @@ RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
COPY ./tables_nacos.sql /docker-entrypoint-initdb.d
|
||||
|
||||
COPY ./jeecgboot-mysql-5.7.sql /docker-entrypoint-initdb.d
|
||||
COPY ./jeecgai-mysql-5.7.sql /docker-entrypoint-initdb.d
|
||||
|
||||
COPY ./tables_xxl_job.sql /docker-entrypoint-initdb.d
|
||||
9053
jeecg-boot/db/jeecgai-mysql-5.7.sql
Normal file
9053
jeecg-boot/db/jeecgai-mysql-5.7.sql
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,119 +0,0 @@
|
||||
#
|
||||
# XXL-JOB v2.2.0
|
||||
# Copyright (c) 2015-present, xuxueli.
|
||||
|
||||
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;
|
||||
use `xxl_job`;
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_info` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
|
||||
`job_cron` varchar(128) NOT NULL COMMENT '任务执行CRON',
|
||||
`job_desc` varchar(255) NOT NULL,
|
||||
`add_time` datetime DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
`author` varchar(64) DEFAULT NULL COMMENT '作者',
|
||||
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
|
||||
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
|
||||
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
|
||||
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
|
||||
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
|
||||
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
|
||||
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
|
||||
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
|
||||
`glue_source` mediumtext COMMENT 'GLUE源代码',
|
||||
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
|
||||
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
|
||||
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
|
||||
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
|
||||
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
|
||||
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_log` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
|
||||
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
|
||||
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
|
||||
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
|
||||
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
|
||||
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
|
||||
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
|
||||
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
|
||||
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
|
||||
`trigger_msg` text COMMENT '调度-日志',
|
||||
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
|
||||
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
|
||||
`handle_msg` text COMMENT '执行-日志',
|
||||
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `I_trigger_time` (`trigger_time`),
|
||||
KEY `I_handle_code` (`handle_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_log_report` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
|
||||
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
|
||||
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
|
||||
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_logglue` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
|
||||
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
|
||||
`glue_source` mediumtext COMMENT 'GLUE源代码',
|
||||
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
|
||||
`add_time` datetime DEFAULT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_registry` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`registry_group` varchar(50) NOT NULL,
|
||||
`registry_key` varchar(255) NOT NULL,
|
||||
`registry_value` varchar(255) NOT NULL,
|
||||
`update_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_group` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
|
||||
`title` varchar(12) NOT NULL COMMENT '执行器名称',
|
||||
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
|
||||
`address_list` varchar(512) DEFAULT NULL COMMENT '执行器地址列表,多地址逗号分隔',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_user` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL COMMENT '账号',
|
||||
`password` varchar(50) NOT NULL COMMENT '密码',
|
||||
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
|
||||
`permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `i_username` (`username`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `xxl_job_lock` (
|
||||
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
|
||||
PRIMARY KEY (`lock_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL);
|
||||
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
|
||||
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
|
||||
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
|
||||
|
||||
commit;
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
# 版本升级方法
|
||||
|
||||
> JeecgBoot属于平台级产品,每次升级改动较大,目前做不到平滑升级。
|
||||
|
||||
### 增量升级方案
|
||||
#### 1.代码合并
|
||||
本地通过svn或git做好主干,在分支上做业务开发,jeecg每次版本发布,可以手工覆盖主干的代码,对比合并代码;
|
||||
|
||||
#### 2.数据库升级
|
||||
- 从3.6.2+版本增加flyway自动升级数据库机制,支持 mysql5.7、mysql8;
|
||||
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
|
||||
> 注意: 升级sql只提供mysql版本;如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
|
||||
|
||||
#### 3.兼容问题
|
||||
每次发版,会针对不兼容地方重点说明。
|
||||
@ -18,24 +18,52 @@ services:
|
||||
--max_allowed_packet=128M
|
||||
--default-authentication-plugin=caching_sha2_password
|
||||
ports:
|
||||
- 3306:3306
|
||||
- 13306:3306
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-redis:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
|
||||
ports:
|
||||
- 6379:6379
|
||||
# ports:
|
||||
# - 6379:6379
|
||||
restart: always
|
||||
hostname: jeecg-boot-redis
|
||||
container_name: jeecg-boot-redis
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
jeecg-boot-pgvector:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
|
||||
container_name: jeecg-boot-pgvector
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: vector_db
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
restart: always
|
||||
networks:
|
||||
- jeecg-boot
|
||||
|
||||
# jeecg-boot-rabbitmq:
|
||||
# image: rabbitmq:3.7.7-management
|
||||
## ports:
|
||||
## - 5672:5672
|
||||
## - 15672:15672
|
||||
# restart: always
|
||||
# container_name: jeecg-boot-rabbitmq
|
||||
# hostname: jeecg-boot-rabbitmq
|
||||
# environment:
|
||||
# RABBITMQ_DEFAULT_USER: guest
|
||||
# RABBITMQ_DEFAULT_PASS: guest
|
||||
# networks:
|
||||
# - jeecg-boot
|
||||
|
||||
jeecg-boot-system:
|
||||
build:
|
||||
context: ./jeecg-module-system/jeecg-system-start
|
||||
restart: on-failure
|
||||
mac_address: 02:42:ac:11:00:02
|
||||
depends_on:
|
||||
- jeecg-boot-mysql
|
||||
- jeecg-boot-redis
|
||||
@ -46,6 +74,8 @@ services:
|
||||
- 8080:8080
|
||||
networks:
|
||||
- jeecg-boot
|
||||
volumes:
|
||||
- ./config:/jeecg-boot/config
|
||||
|
||||
networks:
|
||||
jeecg-boot:
|
||||
|
||||
@ -2,17 +2,13 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<version>3.9.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
|
||||
<properties>
|
||||
<spring-boot.version>3.1.5</spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>aliyun</id>
|
||||
@ -46,23 +42,19 @@
|
||||
<dependencies>
|
||||
<!--jeecg-tools-->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-common3</artifactId>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-common</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!--集成springmvc框架并实现自动配置 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
<!-- websocket -->
|
||||
<dependency>
|
||||
@ -112,9 +104,19 @@
|
||||
<!-- mybatis-plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<!-- minidao -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- druid -->
|
||||
<dependency>
|
||||
@ -141,7 +143,7 @@
|
||||
<!-- sqlserver-->
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>sqljdbc4</artifactId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<version>${sqljdbc4.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
@ -163,13 +165,13 @@
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
<artifactId>kingbase8</artifactId>
|
||||
<version>9.0.0</version>
|
||||
<version>${kingbase8.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT -->
|
||||
<dependency>
|
||||
<groupId>com.dameng</groupId>
|
||||
<artifactId>Dm8JdbcDriver18</artifactId>
|
||||
<artifactId>DmJdbcDriver18</artifactId>
|
||||
<version>${dm8.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -177,8 +179,6 @@
|
||||
<artifactId>DmDialect-for-hibernate5.0</artifactId>
|
||||
<version>${dm8.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Quartz定时任务 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -196,6 +196,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring-boot-starter</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
@ -204,34 +205,6 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- shiro-redis -->
|
||||
<dependency>
|
||||
<groupId>org.crazycake</groupId>
|
||||
<artifactId>shiro-redis</artifactId>
|
||||
<version>${shiro-redis.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
</exclusion>
|
||||
<!-- TODO shiro 无法使用 spring boot 3.X 自带的jedis,降版本处理 -->
|
||||
<exclusion>
|
||||
<artifactId>jedis</artifactId>
|
||||
<groupId>redis.clients</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- TODO shiro 无法使用 spring boot 3.X 自带的jedis,降版本处理 -->
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>${jedis.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-spring</artifactId>
|
||||
@ -255,6 +228,12 @@
|
||||
<artifactId>shiro-core</artifactId>
|
||||
<classifier>jakarta</classifier>
|
||||
<version>${shiro.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
@ -268,13 +247,33 @@
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!-- shiro-redis -->
|
||||
<dependency>
|
||||
<groupId>org.crazycake</groupId>
|
||||
<artifactId>shiro-redis</artifactId>
|
||||
<version>${shiro-redis.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.shiro</groupId>
|
||||
<artifactId>shiro-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- knife4j -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<artifactId>knife4j-openapi3-ui</artifactId>
|
||||
<version>${knife4j-spring-boot-starter.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 代码生成器 -->
|
||||
<!-- 如下载失败,请参考此文档 https://help.jeecg.com/java/setup/maven.html -->
|
||||
@ -282,23 +281,22 @@
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>codegenerate</artifactId>
|
||||
<version>${codegenerate.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<groupId>commons-io</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<groupId>mysql</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- AutoPoi Excel工具类-->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>autopoi-web</artifactId>
|
||||
<version>${autopoi-web.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>xercesImpl</artifactId>
|
||||
<groupId>xerces</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<groupId>org.jeecgframework</groupId>
|
||||
<artifactId>autopoi-spring-boot-3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>xerces</groupId>
|
||||
@ -311,6 +309,20 @@
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>checker-qual</artifactId>
|
||||
<groupId>org.checkerframework</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.google.errorprone</groupId>
|
||||
<artifactId>error_prone_annotations</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云短信 -->
|
||||
@ -362,11 +374,26 @@
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- chatgpt -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot</groupId>
|
||||
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
|
||||
</dependency>
|
||||
<!-- 腾讯云 -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||
<version>${tencentcloud-sdk-java-sms.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.common.api;
|
||||
|
||||
import org.jeecg.common.api.dto.AiragFlowDTO;
|
||||
import org.jeecg.common.system.vo.*;
|
||||
|
||||
import java.util.List;
|
||||
@ -131,7 +132,6 @@ public interface CommonAPI {
|
||||
*/
|
||||
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
|
||||
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
/**
|
||||
* 15 字典表的 翻译,可批量
|
||||
* @param table
|
||||
@ -142,6 +142,16 @@ public interface CommonAPI {
|
||||
* @return
|
||||
*/
|
||||
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
|
||||
/**
|
||||
* 16 运行AIRag流程
|
||||
* for [QQYUN-13634]在baseapi里面封装方法,方便其他模块调用
|
||||
*
|
||||
* @param airagFlowDTO
|
||||
* @return 流程执行结果,可能是String或者Map
|
||||
* @author chenrui
|
||||
* @date 2025/9/2 11:43
|
||||
*/
|
||||
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
package org.jeecg.common.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 调用AI流程入参
|
||||
* for [QQYUN-13634]在baseapi里面封装方法,方便其他模块调用
|
||||
* @author chenrui
|
||||
* @date 2025/9/2 14:11
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class AiragFlowDTO implements Serializable {
|
||||
|
||||
|
||||
private static final long serialVersionUID = 7431775881170684867L;
|
||||
|
||||
/**
|
||||
* 流程id
|
||||
*/
|
||||
private String flowId;
|
||||
|
||||
|
||||
/**
|
||||
* 输入参数
|
||||
*/
|
||||
private Map<String, Object> inputParams;
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package org.jeecg.common.api.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 流程审批意见DTO
|
||||
* @author scott
|
||||
* @date 2025-01-29
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalCommentDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 任务ID
|
||||
*/
|
||||
private String taskId;
|
||||
|
||||
/**
|
||||
* 任务名称
|
||||
*/
|
||||
private String taskName;
|
||||
|
||||
/**
|
||||
* 审批人ID
|
||||
*/
|
||||
private String approverId;
|
||||
|
||||
/**
|
||||
* 审批人姓名
|
||||
*/
|
||||
private String approverName;
|
||||
|
||||
/**
|
||||
* 审批意见
|
||||
*/
|
||||
private String approvalComment;
|
||||
|
||||
/**
|
||||
* 审批时间
|
||||
*/
|
||||
private Date approvalTime;
|
||||
}
|
||||
|
||||
@ -30,12 +30,10 @@ public class OnlineAuthDTO implements Serializable {
|
||||
*/
|
||||
private String onlineFormUrl;
|
||||
|
||||
//update-begin---author:chenrui ---date:20240123 for:[QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------
|
||||
/**
|
||||
* online工单的地址
|
||||
*/
|
||||
private String onlineWorkOrderUrl;
|
||||
//update-end---author:chenrui ---date:20240123 for:[QQYUN-7992]【online】工单申请下的online表单,未配置online表单开发菜单,操作报错无权限------------
|
||||
|
||||
public OnlineAuthDTO(){
|
||||
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
package org.jeecg.common.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 移动端消息推送
|
||||
* @author liusq
|
||||
* @date 2025/11/12 14:11
|
||||
*/
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class PushMessageDTO implements Serializable {
|
||||
|
||||
|
||||
private static final long serialVersionUID = 7431775881170684867L;
|
||||
|
||||
/**
|
||||
* 消息标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 推送形式:all:全推送 single:单用户推送
|
||||
*/
|
||||
private String pushType;
|
||||
|
||||
/**
|
||||
* 用户名usernameList
|
||||
*/
|
||||
List<String> usernames;
|
||||
|
||||
/**
|
||||
* 用户名idList
|
||||
*/
|
||||
List<String> userIds;
|
||||
|
||||
/**
|
||||
* 消息附加参数
|
||||
*/
|
||||
Map<String,Object> payload;
|
||||
}
|
||||
@ -84,7 +84,19 @@ public class MessageDTO implements Serializable {
|
||||
* 邮件抄送地址
|
||||
*/
|
||||
protected Set<String> ccEmailList;
|
||||
|
||||
/**
|
||||
* 是否为定时任务推送email
|
||||
*/
|
||||
private Boolean isTimeJob = false;
|
||||
|
||||
//---【邮件相关参数】-------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 枚举:org.jeecg.common.constant.enums.NoticeTypeEnum
|
||||
* 通知类型(system:系统消息、file:知识库、flow:流程、plan:日程计划、meeting:会议)
|
||||
*/
|
||||
private String noticeType;
|
||||
|
||||
public MessageDTO(){
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.jeecg.common.api.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
|
||||
@ -20,7 +20,6 @@ import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.IpUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.BindingResult;
|
||||
@ -122,9 +121,8 @@ public class AutoLogAspect {
|
||||
if (operateType > 0) {
|
||||
return operateType;
|
||||
}
|
||||
//update-begin---author:wangshuai ---date:20220331 for:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
||||
// 代码逻辑说明: 阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
||||
return OperateTypeEnum.getTypeByMethodName(methodName);
|
||||
//update-end---author:wangshuai ---date:20220331 for:阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
|
||||
}
|
||||
|
||||
/**
|
||||
@ -144,14 +142,15 @@ public class AutoLogAspect {
|
||||
// https://my.oschina.net/mengzhang6/blog/2395893
|
||||
Object[] arguments = new Object[paramsArray.length];
|
||||
for (int i = 0; i < paramsArray.length; i++) {
|
||||
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
|
||||
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || paramsArray[i] instanceof MultipartFile[]) {
|
||||
//ServletRequest不能序列化,从入参里排除,否则报异常:java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
|
||||
//ServletResponse不能序列化 从入参里排除,否则报异常:java.lang.IllegalStateException: getOutputStream() has already been called for this response
|
||||
//MultipartFile和MultipartFile[]不能序列化,从入参里排除
|
||||
continue;
|
||||
}
|
||||
arguments[i] = paramsArray[i];
|
||||
}
|
||||
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
|
||||
// 代码逻辑说明: 日志数据太长的直接过滤掉
|
||||
PropertyFilter profilter = new PropertyFilter() {
|
||||
@Override
|
||||
public boolean apply(Object o, String name, Object value) {
|
||||
@ -166,14 +165,13 @@ public class AutoLogAspect {
|
||||
}
|
||||
};
|
||||
params = JSONObject.toJSONString(arguments, profilter);
|
||||
//update-end-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
|
||||
} else {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
// 请求的方法参数值
|
||||
Object[] args = joinPoint.getArgs();
|
||||
// 请求的方法参数名称
|
||||
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
|
||||
StandardReflectionParameterNameDiscoverer u= new StandardReflectionParameterNameDiscoverer();
|
||||
String[] paramNames = u.getParameterNames(method);
|
||||
if (args != null && paramNames != null) {
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
|
||||
@ -105,29 +105,24 @@ public class DictAspect {
|
||||
Map<String, List<String>> dataListMap = new HashMap<>(5);
|
||||
//取出结果集
|
||||
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
|
||||
//update-begin--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
||||
// 代码逻辑说明: 【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
||||
Boolean hasDict= checkHasDict(records);
|
||||
if(!hasDict){
|
||||
return result;
|
||||
}
|
||||
|
||||
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
|
||||
//update-end--Author:zyf -- Date:20220606 ----for:【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
|
||||
for (Object record : records) {
|
||||
String json="{}";
|
||||
try {
|
||||
//update-begin--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
||||
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
|
||||
json = objectMapper.writeValueAsString(record);
|
||||
//update-end--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("json解析失败"+e.getMessage(),e);
|
||||
}
|
||||
//update-begin--Author:scott -- Date:20211223 ----for:【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
||||
// 代码逻辑说明: 【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
||||
JSONObject item = JSONObject.parseObject(json, Feature.OrderedField);
|
||||
//update-end--Author:scott -- Date:20211223 ----for:【issues/3303】restcontroller返回json数据后key顺序错乱 -----
|
||||
|
||||
//update-begin--Author:scott -- Date:20190603 ----for:解决继承实体字段无法翻译问题------
|
||||
//for (Field field : record.getClass().getDeclaredFields()) {
|
||||
// 遍历所有字段,把字典Code取出来,放到 map 里
|
||||
for (Field field : oConvertUtils.getAllFields(record)) {
|
||||
@ -135,7 +130,6 @@ public class DictAspect {
|
||||
if (oConvertUtils.isEmpty(value)) {
|
||||
continue;
|
||||
}
|
||||
//update-end--Author:scott -- Date:20190603 ----for:解决继承实体字段无法翻译问题------
|
||||
if (field.getAnnotation(Dict.class) != null) {
|
||||
if (!dictFieldList.contains(field)) {
|
||||
dictFieldList.add(field);
|
||||
@ -143,26 +137,22 @@ public class DictAspect {
|
||||
String code = field.getAnnotation(Dict.class).dicCode();
|
||||
String text = field.getAnnotation(Dict.class).dicText();
|
||||
String table = field.getAnnotation(Dict.class).dictTable();
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
List<String> dataList;
|
||||
String dictCode = code;
|
||||
if (!StringUtils.isEmpty(table)) {
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
}
|
||||
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
|
||||
}
|
||||
//date类型默认转换string格式化日期
|
||||
//update-begin--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
||||
//if (JAVA_UTIL_DATE.equals(field.getType().getName())&&field.getAnnotation(JsonFormat.class)==null&&item.get(field.getName())!=null){
|
||||
//SimpleDateFormat aDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
// item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
|
||||
//}
|
||||
//update-end--Author:zyf -- Date:20220531 ----for:【issues/#3629】 DictAspect Jackson序列化报错-----
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
@ -176,15 +166,12 @@ public class DictAspect {
|
||||
String code = field.getAnnotation(Dict.class).dicCode();
|
||||
String text = field.getAnnotation(Dict.class).dicText();
|
||||
String table = field.getAnnotation(Dict.class).dictTable();
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 自定义的字典表数据源
|
||||
String dataSource = field.getAnnotation(Dict.class).ds();
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
String fieldDictCode = code;
|
||||
if (!StringUtils.isEmpty(table)) {
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
}
|
||||
|
||||
String value = record.getString(field.getName());
|
||||
@ -286,25 +273,20 @@ public class DictAspect {
|
||||
String[] arr = dictCode.split(",");
|
||||
String table = arr[0], text = arr[1], code = arr[2];
|
||||
String values = String.join(",", needTranslDataTable);
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
// 自定义的数据源
|
||||
String dataSource = null;
|
||||
if (arr.length > 3) {
|
||||
dataSource = arr[3];
|
||||
}
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
|
||||
log.debug("translateDictFromTableByKeys.values:" + values);
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
|
||||
//update-begin---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
||||
// 代码逻辑说明: 微服务下为空报错没有参数需要传递空字符串---
|
||||
if(null == dataSource){
|
||||
dataSource = "";
|
||||
}
|
||||
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
|
||||
|
||||
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
log.debug("translateDictFromTableByKeys.result:" + texts);
|
||||
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
|
||||
list.addAll(texts);
|
||||
@ -313,10 +295,8 @@ public class DictAspect {
|
||||
for (DictModel dict : texts) {
|
||||
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
|
||||
try {
|
||||
// update-begin-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
|
||||
// 保留5分钟
|
||||
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
|
||||
// update-end-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage(), e);
|
||||
}
|
||||
@ -400,7 +380,7 @@ public class DictAspect {
|
||||
if (k.trim().length() == 0) {
|
||||
continue; //跳过循环
|
||||
}
|
||||
//update-begin--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
||||
// 代码逻辑说明: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
||||
if (!StringUtils.isEmpty(table)){
|
||||
log.debug("--DictAspect------dicTable="+ table+" ,dicText= "+text+" ,dicCode="+code);
|
||||
String keyString = String.format("sys:cache:dictTable::SimpleKey [%s,%s,%s,%s]",table,text,code,k.trim());
|
||||
@ -425,7 +405,6 @@ public class DictAspect {
|
||||
tmpValue = commonApi.translateDict(code, k.trim());
|
||||
}
|
||||
}
|
||||
//update-end--Author:scott -- Date:20210531 ----for: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
|
||||
|
||||
if (tmpValue != null) {
|
||||
if (!"".equals(textValue.toString())) {
|
||||
|
||||
@ -57,7 +57,6 @@ public class PermissionDataAspect {
|
||||
String requestMethod = request.getMethod();
|
||||
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
|
||||
requestPath = filterUrl(requestPath);
|
||||
//update-begin-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
|
||||
//先判断是否online报表请求
|
||||
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
|
||||
// 获取地址栏参数
|
||||
@ -66,7 +65,6 @@ public class PermissionDataAspect {
|
||||
requestPath+="?"+urlParamString;
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
|
||||
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
|
||||
String username = JwtUtil.getUserNameByToken(request);
|
||||
//查询数据权限信息
|
||||
|
||||
@ -41,7 +41,6 @@ public @interface Dict {
|
||||
String dictTable() default "";
|
||||
|
||||
|
||||
//update-begin---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
/**
|
||||
* 方法描述: 数据字典表所在数据源名称
|
||||
* 作 者: chenrui
|
||||
@ -50,5 +49,4 @@ public @interface Dict {
|
||||
* @return 返回类型: String
|
||||
*/
|
||||
String ds() default "";
|
||||
//update-end---author:chenrui ---date:20231221 for:[issues/#5643]解决分布式下表字典跨库无法查询问题------------
|
||||
}
|
||||
|
||||
@ -91,6 +91,23 @@ public interface CommonConstant {
|
||||
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
|
||||
/** 登录用户Token令牌缓存KEY前缀 */
|
||||
String PREFIX_USER_TOKEN = "prefix_user_token:";
|
||||
/** 登录用户Token令牌作废提示信息,比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
|
||||
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
|
||||
|
||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||
/** 客户端类型:PC端 */
|
||||
String CLIENT_TYPE_PC = "PC";
|
||||
/** 客户端类型:APP端 */
|
||||
String CLIENT_TYPE_APP = "APP";
|
||||
/** 客户端类型:手机号登录 */
|
||||
String CLIENT_TYPE_PHONE = "PHONE";
|
||||
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
|
||||
/** 单点登录:用户在APP端的Token缓存KEY前缀 (username -> token) */
|
||||
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
|
||||
/** 单点登录:用户在手机号登录的Token缓存KEY前缀 (username -> token) */
|
||||
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
|
||||
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
|
||||
|
||||
// /** Token缓存时间:3600秒即一小时 */
|
||||
// int TOKEN_EXPIRE_TIME = 3600;
|
||||
|
||||
@ -303,6 +320,15 @@ public interface CommonConstant {
|
||||
*/
|
||||
String SYS_USER_ID_MAPPING_CACHE = "sys:cache:user:id_mapping";
|
||||
|
||||
/**
|
||||
* 系统角色管理员编码
|
||||
*/
|
||||
String SYS_ROLE_ADMIN = "admin";
|
||||
|
||||
/**
|
||||
* 考勤补卡业务状态 (0:处理中)
|
||||
*/
|
||||
String SIGN_PATCH_BIZ_STATUS_0 = "0";
|
||||
/**
|
||||
* 考勤补卡业务状态 (1:同意 2:不同意)
|
||||
*/
|
||||
@ -428,6 +454,11 @@ public interface CommonConstant {
|
||||
*/
|
||||
String NOTICE_MSG_BUS_TYPE = "NOTICE_MSG_BUS_TYPE";
|
||||
|
||||
/**
|
||||
* 通知类型,用于区分来源 file 知识 flow 流程 plan 日程 system 系统消息
|
||||
*/
|
||||
String NOTICE_TYPE = "noticeType";
|
||||
|
||||
/**
|
||||
* 邮箱消息中地址登录时地址后携带的token,需要替换成真实的token值
|
||||
*/
|
||||
@ -581,7 +612,6 @@ public interface CommonConstant {
|
||||
String ORDER_TYPE_DESC = "DESC";
|
||||
|
||||
|
||||
//update-begin---author:scott ---date:2023-09-10 for:积木报表常量----
|
||||
/**
|
||||
* 报表允许设计开发的角色
|
||||
*/
|
||||
@ -596,14 +626,17 @@ public interface CommonConstant {
|
||||
* 数据隔离模式: 按照租户隔离
|
||||
*/
|
||||
public static final String SAAS_MODE_TENANT = "tenant";
|
||||
//update-end---author:scott ---date::2023-09-10 for:积木报表常量----
|
||||
|
||||
//update-begin---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
||||
/**
|
||||
* 修改手机号短信验证码redis-key的前缀
|
||||
*/
|
||||
String CHANGE_PHONE_REDIS_KEY_PRE = "sys:cache:phone:change_phone_msg:";
|
||||
|
||||
/**
|
||||
* 手机号短信验证码redis-key的前缀
|
||||
*/
|
||||
String LOG_OFF_PHONE_REDIS_KEY_PRE = "sys:cache:phone:qqy_log_off_user_msg:";
|
||||
|
||||
/**
|
||||
* 缓存用户最后一次收到消息通知的时间 KEY
|
||||
*/
|
||||
@ -618,10 +651,94 @@ public interface CommonConstant {
|
||||
* 修改手机号
|
||||
*/
|
||||
String UPDATE_PHONE = "updatePhone";
|
||||
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
|
||||
|
||||
/**
|
||||
* 修改手机号验证码请求次数超出
|
||||
*/
|
||||
Integer PHONE_SMS_FAIL_CODE = 40002;
|
||||
|
||||
|
||||
/**
|
||||
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户 DEFAULT:默认首页)
|
||||
*
|
||||
*/
|
||||
String HOME_RELATION_ROLE = "ROLE";
|
||||
String HOME_RELATION_USER = "USER";
|
||||
String HOME_RELATION_DEFAULT = "DEFAULT";
|
||||
|
||||
/**
|
||||
* 是否置顶(0否 1是)
|
||||
*/
|
||||
Integer IZ_TOP_1 = 1;
|
||||
Integer IZ_TOP_0 = 0;
|
||||
|
||||
|
||||
//关注流程缓存前缀
|
||||
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
|
||||
//任务缓办时间缓存前缀
|
||||
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:";
|
||||
/**
|
||||
* 用户代理类型:离职:quit 代理:agent
|
||||
*/
|
||||
String USER_AGENT_TYPE_QUIT = "quit";
|
||||
String USER_AGENT_TYPE_AGENT = "agent";
|
||||
/**
|
||||
* 督办流程首节点任务taskKey
|
||||
*/
|
||||
String SUPERVISE_FIRST_TASK_KEY = "Task_1bhxpt0";
|
||||
|
||||
/**
|
||||
* wps模板预览数据缓存前缀
|
||||
*/
|
||||
String EOA_WPS_TEMPLATE_VIEW_DATA ="eoa:wps:templateViewData:";
|
||||
|
||||
/**
|
||||
* wps模板预览版本号缓存前缀
|
||||
*/
|
||||
String EOA_WPS_TEMPLATE_VIEW_VERSION ="eoa:wps:templateViewVersion:";
|
||||
/**
|
||||
* 表单设计器oa新增字段
|
||||
* x_oa_timeout_date:逾期时间
|
||||
* x_oa_archive_status:归档状态
|
||||
*/
|
||||
String X_OA_TIMEOUT_DATE ="x_oa_timeout_date";
|
||||
String X_OA_ARCHIVE_STATUS ="x_oa_archive_status";
|
||||
/**
|
||||
* 流程状态
|
||||
* 待提交: 1
|
||||
* 处理中: 2
|
||||
* 已完成: 3
|
||||
* 已作废: 4
|
||||
* 已挂起: 5
|
||||
*/
|
||||
String BPM_STATUS_1 ="1";
|
||||
String BPM_STATUS_2 ="2";
|
||||
String BPM_STATUS_3 ="3";
|
||||
String BPM_STATUS_4 ="4";
|
||||
String BPM_STATUS_5 ="5";
|
||||
|
||||
/**
|
||||
* 默认租户产品包
|
||||
*/
|
||||
String TENANT_PACK_DEFAULT = "default";
|
||||
|
||||
/**
|
||||
* 部门名称redisKey(全路径)
|
||||
*/
|
||||
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
|
||||
|
||||
/**
|
||||
* 默认用户排序值
|
||||
*/
|
||||
Integer DEFAULT_USER_SORT = 1000;
|
||||
|
||||
/**
|
||||
* 发送短信方式:腾讯
|
||||
*/
|
||||
String SMS_SEND_TYPE_TENCENT = "tencent";
|
||||
|
||||
/**
|
||||
* 发送短信方式:阿里云
|
||||
*/
|
||||
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
|
||||
}
|
||||
|
||||
@ -4,6 +4,20 @@ package org.jeecg.common.constant;
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
public interface DataBaseConstant {
|
||||
|
||||
/**
|
||||
* 内置的系统变量键列表
|
||||
*/
|
||||
public static final String[] SYSTEM_KEYS = {
|
||||
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
|
||||
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
|
||||
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
|
||||
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
|
||||
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
|
||||
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
|
||||
DataBaseConstant.SYS_BASE_PATH
|
||||
};
|
||||
|
||||
//*********数据库类型****************************************
|
||||
|
||||
/**MYSQL数据库*/
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package org.jeecg.common.constant;
|
||||
|
||||
/**
|
||||
* @Description: 密码常量类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/8/27 20:10
|
||||
*/
|
||||
public interface PasswordConstant {
|
||||
|
||||
/**
|
||||
* 导入用户默认密码
|
||||
*/
|
||||
String DEFAULT_PASSWORD = "123456";
|
||||
}
|
||||
@ -39,20 +39,18 @@ public class ProvinceCityArea {
|
||||
this.initAreaList();
|
||||
if(areaList!=null && areaList.size()>0){
|
||||
for(int i=areaList.size()-1;i>=0;i--){
|
||||
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||
String areaText = areaList.get(i).getText();
|
||||
String cityText = areaList.get(i).getAheadText();
|
||||
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
|
||||
return areaList.get(i).getId();
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// update-begin-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
|
||||
/**
|
||||
* 获取省市区code,精准匹配
|
||||
* @param texts 文本数组,省,市,区
|
||||
@ -117,11 +115,10 @@ public class ProvinceCityArea {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// update-end-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
|
||||
|
||||
public void getAreaByCode(String code,List<String> ls){
|
||||
for(Area area: areaList){
|
||||
if(area.getId().equals(code)){
|
||||
if(null != area && area.getId().equals(code)){
|
||||
String pid = area.getPid();
|
||||
ls.add(0,area.getText());
|
||||
getAreaByCode(pid,ls);
|
||||
@ -154,9 +151,8 @@ public class ProvinceCityArea {
|
||||
for(String areaKey:areaJson.keySet()){
|
||||
//System.out.println("········"+areaKey);
|
||||
Area area = new Area(areaKey,areaJson.getString(areaKey),cityKey);
|
||||
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||
area.setAheadText(cityJson.getString(cityKey));
|
||||
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
|
||||
this.areaList.add(area);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
package org.jeecg.common.constant.enums;
|
||||
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
/**
|
||||
* @Description: 部门类型枚举类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/8/19 21:37
|
||||
*/
|
||||
public enum DepartCategoryEnum {
|
||||
|
||||
DEPART_CATEGORY_COMPANY("部门类型:公司","公司","1"),
|
||||
DEPART_CATEGORY_DEPART("部门类型:部门","部门","2"),
|
||||
DEPART_CATEGORY_POST("部门类型:岗位","岗位","3"),
|
||||
DEPART_CATEGORY_SUB_COMPANY("部门类型:子公司","子公司","4");
|
||||
|
||||
DepartCategoryEnum(String described, String name, String value) {
|
||||
this.value = value;
|
||||
this.name = name;
|
||||
this.described = described;
|
||||
}
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String described;
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
public String getDescribed() {
|
||||
return described;
|
||||
}
|
||||
|
||||
public void setDescribed(String described) {
|
||||
this.described = described;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值获取名称
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static String getNameByValue(String value){
|
||||
if (oConvertUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
for (DepartCategoryEnum val : values()) {
|
||||
if (val.getValue().equals(value)) {
|
||||
return val.getName();
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取值
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public static String getValueByName(String name){
|
||||
if (oConvertUtils.isEmpty(name)) {
|
||||
return null;
|
||||
}
|
||||
for (DepartCategoryEnum val : values()) {
|
||||
if (val.getName().equals(name)) {
|
||||
return val.getValue();
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@ -9,22 +9,14 @@ import org.apache.commons.lang3.StringUtils;
|
||||
public enum DySmsEnum {
|
||||
|
||||
/**登录短信模板编码*/
|
||||
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
/**忘记密码短信模板编码*/
|
||||
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
/**修改密码短信模板编码*/
|
||||
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
|
||||
/**注册账号短信模板编码*/
|
||||
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code"),
|
||||
/**会议通知*/
|
||||
MEET_NOTICE_TEMPLATE_CODE("SMS_201480469","JEECG","username,title,minute,time"),
|
||||
/**我的计划通知*/
|
||||
PLAN_NOTICE_TEMPLATE_CODE("SMS_201470515","JEECG","username,title,time"),
|
||||
/**支付成功短信通知*/
|
||||
PAY_SUCCESS_NOTICE_CODE("SMS_461735163","敲敲云","realname,money,endTime"),
|
||||
/**会员到期通知提醒*/
|
||||
VIP_EXPIRE_NOTICE_CODE("SMS_461885023","敲敲云","realname,endTime");
|
||||
|
||||
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
|
||||
/**修改密码短信模板编码*/
|
||||
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
|
||||
/**注册账号短信模板编码*/
|
||||
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
|
||||
|
||||
/**
|
||||
* 短信模板编码
|
||||
*/
|
||||
|
||||
@ -13,6 +13,10 @@ public enum EmailTemplateEnum {
|
||||
* 流程催办
|
||||
*/
|
||||
BPM_CUIBAN_EMAIL("bpm_cuiban_email", "/templates/email/bpm_cuiban_email.ftl"),
|
||||
/**
|
||||
* 流程抄送
|
||||
*/
|
||||
BPM_CC_EMAIL("bpm_cc_email", "/templates/email/bpm_cc_email.ftl"),
|
||||
/**
|
||||
* 流程新任务
|
||||
*/
|
||||
|
||||
@ -8,21 +8,30 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
@EnumDict("messageType")
|
||||
public enum MessageTypeEnum {
|
||||
|
||||
/** 系统消息 */
|
||||
XT("system", "系统消息"),
|
||||
/** 邮件消息 */
|
||||
YJ("email", "邮件消息"),
|
||||
/** 钉钉消息 */
|
||||
/**
|
||||
* 系统消息
|
||||
*/
|
||||
XT("system", "系统消息"),
|
||||
/**
|
||||
* 邮件消息
|
||||
*/
|
||||
YJ("email", "邮件消息"),
|
||||
/**
|
||||
* 钉钉消息
|
||||
*/
|
||||
DD("dingtalk", "钉钉消息"),
|
||||
/** 企业微信 */
|
||||
/**
|
||||
* 企业微信
|
||||
*/
|
||||
QYWX("wechat_enterprise", "企业微信");
|
||||
|
||||
MessageTypeEnum(String type, String note){
|
||||
MessageTypeEnum(String type, String note) {
|
||||
this.type = type;
|
||||
this.note = note;
|
||||
}
|
||||
@ -56,12 +65,13 @@ public enum MessageTypeEnum {
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static List<DictModel> getDictList(){
|
||||
public static List<DictModel> getDictList() {
|
||||
List<DictModel> list = new ArrayList<>();
|
||||
DictModel dictModel = null;
|
||||
for(MessageTypeEnum e: MessageTypeEnum.values()){
|
||||
for (MessageTypeEnum e : MessageTypeEnum.values()) {
|
||||
dictModel = new DictModel();
|
||||
dictModel.setValue(e.getType());
|
||||
dictModel.setText(e.getNote());
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
package org.jeecg.common.constant.enums;
|
||||
|
||||
/**
|
||||
* @Description: 文件类型枚举类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/6/26 17:29
|
||||
*/
|
||||
public enum NoticeTypeEnum {
|
||||
|
||||
//VUE3专用
|
||||
NOTICE_TYPE_FILE("知识库消息","file"),
|
||||
NOTICE_TYPE_FLOW("工作流消息","flow"),
|
||||
NOTICE_TYPE_PLAN("日程消息","plan"),
|
||||
//暂时没用到
|
||||
NOTICE_TYPE_MEETING("会议消息","meeting"),
|
||||
NOTICE_TYPE_SYSTEM("系统消息","system"),
|
||||
/**
|
||||
* 协同工作
|
||||
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
|
||||
*/
|
||||
NOTICE_TYPE_COLLABORATION("协同工作", "collab"),
|
||||
/**
|
||||
* 督办
|
||||
*/
|
||||
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
|
||||
/**
|
||||
* 考勤
|
||||
*/
|
||||
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
|
||||
|
||||
/**
|
||||
* 文件类型名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 文件类型值
|
||||
*/
|
||||
private String value;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
NoticeTypeEnum(String name, String value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天通知类型
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static String getChatNoticeType(String value){
|
||||
return value + "Notice";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知名称
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static String getNoticeNameByValue(String value){
|
||||
value = value.replace("Notice","");
|
||||
for (NoticeTypeEnum e : NoticeTypeEnum.values()) {
|
||||
if (e.getValue().equals(value)) {
|
||||
return e.getName();
|
||||
}
|
||||
}
|
||||
return "系统消息";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
package org.jeecg.common.constant.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 职级枚举类
|
||||
*
|
||||
* 注意:此枚举仅适用于天津临港控股OA项目,职级的名称和等级均为写死(需要与数据库配置一致)
|
||||
* @date 2025-08-26
|
||||
* @author scott
|
||||
*/
|
||||
public enum PositionLevelEnum {
|
||||
|
||||
// 领导层级(等级1-3)
|
||||
CHAIRMAN("董事长", 1, PositionType.LEADER),
|
||||
GENERAL_MANAGER("总经理", 2, PositionType.LEADER),
|
||||
VICE_GENERAL_MANAGER("副总经理", 3, PositionType.LEADER),
|
||||
|
||||
// 职员层级(等级4-6)
|
||||
MINISTER("部长", 4, PositionType.STAFF),
|
||||
VICE_MINISTER("副部长", 5, PositionType.STAFF),
|
||||
STAFF("职员", 6, PositionType.STAFF);
|
||||
|
||||
private final String name;
|
||||
private final int level;
|
||||
private final PositionType type;
|
||||
|
||||
PositionLevelEnum(String name, int level, PositionType type) {
|
||||
this.name = name;
|
||||
this.level = level;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public PositionType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 职级类型枚举
|
||||
*/
|
||||
public enum PositionType {
|
||||
STAFF("职员层级"),
|
||||
LEADER("领导层级");
|
||||
|
||||
private final String desc;
|
||||
|
||||
PositionType(String desc) {
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据职级名称获取枚举
|
||||
* @param name 职级名称
|
||||
* @return 职级枚举
|
||||
*/
|
||||
public static PositionLevelEnum getByName(String name) {
|
||||
for (PositionLevelEnum position : values()) {
|
||||
if (position.getName().equals(name)) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据职级等级获取枚举
|
||||
* @param level 职级等级
|
||||
* @return 职级枚举
|
||||
*/
|
||||
public static PositionLevelEnum getByLevel(int level) {
|
||||
for (PositionLevelEnum position : values()) {
|
||||
if (position.getLevel() == level) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据职级名称判断是否为职员层级
|
||||
* @param name 职级名称
|
||||
* @return true-职员层级,false-非职员层级
|
||||
*/
|
||||
public static boolean isStaffLevel(String name) {
|
||||
PositionLevelEnum position = getByName(name);
|
||||
return position != null && position.getType() == PositionType.STAFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据职级名称判断是否为领导层级
|
||||
* @param name 职级名称
|
||||
* @return true-领导层级,false-非领导层级
|
||||
*/
|
||||
public static boolean isLeaderLevel(String name) {
|
||||
PositionLevelEnum position = getByName(name);
|
||||
return position != null && position.getType() == PositionType.LEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个职级的等级高低
|
||||
* @param name1 职级名称1
|
||||
* @param name2 职级名称2
|
||||
* @return 正数表示name1等级更高,负数表示name2等级更高,0表示等级相同
|
||||
*/
|
||||
public static int compareLevel(String name1, String name2) {
|
||||
PositionLevelEnum pos1 = getByName(name1);
|
||||
PositionLevelEnum pos2 = getByName(name2);
|
||||
|
||||
if (pos1 == null || pos2 == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 等级数字越小代表职级越高
|
||||
return pos2.getLevel() - pos1.getLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为更高等级
|
||||
* @param currentName 当前职级名称
|
||||
* @param targetName 目标职级名称
|
||||
* @return true-目标职级更高,false-目标职级不高于当前职级
|
||||
*/
|
||||
public static boolean isHigherLevel(String currentName, String targetName) {
|
||||
return compareLevel(targetName, currentName) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有职员层级名称
|
||||
* @return 职员层级名称列表
|
||||
*/
|
||||
public static List<String> getStaffLevelNames() {
|
||||
return Arrays.asList(MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有领导层级名称
|
||||
* @return 领导层级名称列表
|
||||
*/
|
||||
public static List<String> getLeaderLevelNames() {
|
||||
return Arrays.asList(CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有职级名称(按等级排序)
|
||||
* @return 所有职级名称列表
|
||||
*/
|
||||
public static List<String> getAllPositionNames() {
|
||||
return Arrays.asList(
|
||||
CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName(),
|
||||
MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定等级范围的职级
|
||||
* @param minLevel 最小等级
|
||||
* @param maxLevel 最大等级
|
||||
* @return 职级名称列表
|
||||
*/
|
||||
public static List<String> getPositionsByLevelRange(int minLevel, int maxLevel) {
|
||||
return Arrays.stream(values())
|
||||
.filter(p -> p.getLevel() >= minLevel && p.getLevel() <= maxLevel)
|
||||
.map(PositionLevelEnum::getName)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
}
|
||||
@ -23,7 +23,25 @@ public enum SysAnnmentTypeEnum {
|
||||
/**
|
||||
* 邀请用户跳转到个人设置
|
||||
*/
|
||||
TENANT_INVITE("tenant_invite", "url", "/system/usersetting");
|
||||
TENANT_INVITE("tenant_invite", "url", "/system/usersetting"),
|
||||
/**
|
||||
* 协同工作-待办通知
|
||||
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
|
||||
*/
|
||||
EOA_CO_NOTIFY("eoa_co_notify", "url", "/collaboration/pending"),
|
||||
/**
|
||||
* 协同工作-催办通知
|
||||
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
|
||||
*/
|
||||
EOA_CO_REMIND("eoa_co_remind", "url", "/collaboration/pending"),
|
||||
/**
|
||||
* 督办管理-催办
|
||||
*/
|
||||
EOA_SUP_REMIND("eoa_sup_remind", "url", "/superivse/list"),
|
||||
/**
|
||||
* 督办管理-通知
|
||||
*/
|
||||
EOA_SUP_NOTIFY("eoa_sup_notify", "url", "/superivse/list");
|
||||
|
||||
/**
|
||||
* 业务类型(email:邮件 bpm:流程)
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
package org.jeecg.common.constant.enums;
|
||||
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
/**
|
||||
* UniPush 消息推送枚举
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
public enum UniPushTypeEnum {
|
||||
/**
|
||||
* 聊天
|
||||
*/
|
||||
CHAT("chat", "聊天消息", "收到%s发来的聊天消息"),
|
||||
/**
|
||||
* 流程跳转到我的任务
|
||||
*/
|
||||
BPM("bpm_task", "待办任务", "收到%s待办任务"),
|
||||
|
||||
/**
|
||||
* 流程抄送任务
|
||||
*/
|
||||
BPM_VIEW("bpm_cc", "知会任务", "收到%s知会任务"),
|
||||
/**
|
||||
* 系统消息
|
||||
*/
|
||||
SYS_MSG("system", "系统消息", "收到一条系统通告");
|
||||
|
||||
/**
|
||||
* 业务类型(chat:聊天 bpm_task:流程 bpm_cc:流程抄送)
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 消息标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
UniPushTypeEnum(String type, String title, String content) {
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title ;
|
||||
}
|
||||
|
||||
public void setTitle(String openType) {
|
||||
this.title = openType;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public static UniPushTypeEnum getByType(String type) {
|
||||
if (oConvertUtils.isEmpty(type)) {
|
||||
return null;
|
||||
}
|
||||
for (UniPushTypeEnum val : values()) {
|
||||
if (val.getType().equals(type)) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -73,7 +73,7 @@ public class SensitiveDataAspect {
|
||||
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
|
||||
}
|
||||
long endTime=System.currentTimeMillis();
|
||||
log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
||||
log.debug((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时:" + (endTime - startTime) + "ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -387,7 +387,7 @@ public class JeecgElasticsearchTemplate {
|
||||
data.remove("id");
|
||||
bodySb.append(data.toJSONString()).append("\n");
|
||||
}
|
||||
System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
|
||||
//System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
|
||||
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
|
||||
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
|
||||
return true;
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.common.exception;
|
||||
|
||||
/**
|
||||
* jeecgboot断言异常
|
||||
* for [QQYUN-10990]AIRAG
|
||||
* @author chenrui
|
||||
* @date 2025/2/14 14:31
|
||||
*/
|
||||
public class JeecgBootAssertException extends JeecgBootException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
public JeecgBootAssertException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JeecgBootAssertException(String message, int errCode) {
|
||||
super(message, errCode);
|
||||
}
|
||||
|
||||
}
|
||||
@ -25,14 +25,18 @@ import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.data.redis.connection.PoolException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.multipart.MultipartException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 异常处理器
|
||||
@ -47,6 +51,13 @@ public class JeecgBootExceptionHandler {
|
||||
@Resource
|
||||
BaseCommonService baseCommonService;
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
addSysLog(e);
|
||||
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义异常
|
||||
*/
|
||||
@ -110,13 +121,12 @@ public class JeecgBootExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<?> handleException(Exception e){
|
||||
log.error(e.getMessage(), e);
|
||||
//update-begin---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
||||
// 代码逻辑说明: 处理Sentinel限流自定义异常
|
||||
Throwable throwable = e.getCause();
|
||||
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
|
||||
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
|
||||
return Result.error(errorInfoEnum.getError());
|
||||
}
|
||||
//update-end---author:zyf ---date:20220411 for:处理Sentinel限流自定义异常
|
||||
addSysLog(e);
|
||||
return Result.error("操作失败,"+e.getMessage());
|
||||
}
|
||||
@ -156,6 +166,27 @@ public class JeecgBootExceptionHandler {
|
||||
return Result.error("文件大小超出10MB限制, 请压缩或降低文件质量! ");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件过大异常.
|
||||
* jdk17中的MultipartException异常类已经被拆分成了MultipartException和MaxUploadSizeExceededException
|
||||
* for [QQYUN-11716]上传大图片失败没有精确提示
|
||||
* @param e
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/8 16:13
|
||||
*/
|
||||
@ExceptionHandler(MultipartException.class)
|
||||
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IllegalStateException) {
|
||||
log.error("文件大小超出限制: {}", cause.getMessage(), e);
|
||||
addSysLog(e);
|
||||
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
|
||||
} else {
|
||||
return handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
@ -192,7 +223,6 @@ public class JeecgBootExceptionHandler {
|
||||
return Result.error("校验失败,存在SQL注入风险!" + msg);
|
||||
}
|
||||
|
||||
//update-begin---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
||||
/**
|
||||
* 添加异常新系统日志
|
||||
* @param e 异常
|
||||
@ -212,9 +242,12 @@ public class JeecgBootExceptionHandler {
|
||||
}
|
||||
if (null != request) {
|
||||
//请求的参数
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if(!CollectionUtils.isEmpty(parameterMap)){
|
||||
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
||||
if (!isTooBigException(e)) {
|
||||
// 文件上传过大异常时不能获取参数,否则会报错
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if(!CollectionUtils.isEmpty(parameterMap)) {
|
||||
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
|
||||
}
|
||||
}
|
||||
// 请求地址
|
||||
log.setRequestUrl(request.getRequestURI());
|
||||
@ -239,6 +272,27 @@ public class JeecgBootExceptionHandler {
|
||||
|
||||
baseCommonService.addLog(log);
|
||||
}
|
||||
//update-end---author:chenrui ---date:20240423 for:[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
|
||||
|
||||
/**
|
||||
* 是否文件过大异常
|
||||
* for [QQYUN-11716]上传大图片失败没有精确提示
|
||||
* @param e
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/8 20:21
|
||||
*/
|
||||
private static boolean isTooBigException(Throwable e) {
|
||||
boolean isTooBigException = false;
|
||||
if(e instanceof MultipartException){
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IllegalStateException){
|
||||
isTooBigException = true;
|
||||
}
|
||||
}
|
||||
if(e instanceof MaxUploadSizeExceededException){
|
||||
isTooBigException = true;
|
||||
}
|
||||
return isTooBigException;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,11 @@ import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 将枚举类转化成字典数据
|
||||
*
|
||||
* <<使用说明>>
|
||||
* 1. 枚举类需以 `Enum` 结尾,并且在类上添加 `@EnumDict` 注解。
|
||||
* 2. 需要手动将枚举类所在包路径** 添加到 `org.jeecg.common.system.util.ResourceUtil.BASE_SCAN_PACKAGES` 配置数组中。
|
||||
*
|
||||
* @Author taoYan
|
||||
* @Date 2022/7/8 10:34
|
||||
**/
|
||||
|
||||
@ -18,6 +18,7 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
@ -67,12 +68,16 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
//此处设置的filename无效 ,前端会重更新设置一下
|
||||
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
||||
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
||||
//update-begin--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置--------------------
|
||||
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
|
||||
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
|
||||
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
|
||||
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
|
||||
//update-end--Author:liusq Date:20210126 for:图片导出报错,ImageBasePath未设置----------------------
|
||||
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
|
||||
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
|
||||
// 代码逻辑说明: 【issues/9052】BasicTable列表页导出excel可以指定列---
|
||||
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
|
||||
if(oConvertUtils.isNotEmpty(exportFields)){
|
||||
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
|
||||
}
|
||||
return mv;
|
||||
}
|
||||
/**
|
||||
@ -93,14 +98,12 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
// Step.2 计算分页sheet数据
|
||||
double total = service.count();
|
||||
int count = (int)Math.ceil(total/pageNum);
|
||||
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
||||
// Step.3 过滤选中数据
|
||||
String selections = request.getParameter("selections");
|
||||
if (oConvertUtils.isNotEmpty(selections)) {
|
||||
List<String> selectionList = Arrays.asList(selections.split(","));
|
||||
queryWrapper.in("id",selectionList);
|
||||
}
|
||||
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
|
||||
// Step.4 多sheet处理
|
||||
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
|
||||
for (int i = 1; i <=count ; i++) {
|
||||
@ -127,6 +130,53 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
return mv;
|
||||
}
|
||||
|
||||
/**
|
||||
* 大数据导出
|
||||
* @param request
|
||||
* @param object
|
||||
* @param clazz
|
||||
* @param title
|
||||
* @param pageSize 每次查询的数据量
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/8/11 16:11
|
||||
*/
|
||||
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
|
||||
// 组装查询条件
|
||||
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
|
||||
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
// 计算分页数
|
||||
double total = service.count();
|
||||
int count = (int) Math.ceil(total / pageSize);
|
||||
// 过滤选中数据
|
||||
String selections = request.getParameter("selections");
|
||||
if (oConvertUtils.isNotEmpty(selections)) {
|
||||
List<String> selectionList = Arrays.asList(selections.split(","));
|
||||
queryWrapper.in("id", selectionList);
|
||||
}
|
||||
|
||||
// 定义IExcelExportServer
|
||||
IExcelExportServer excelExportServer = (queryParams, pageNum) -> {
|
||||
if (pageNum > count) {
|
||||
return null;
|
||||
}
|
||||
Page<T> page = new Page<T>(pageNum, pageSize);
|
||||
IPage<T> pageList = service.page(page, (QueryWrapper<T>) queryParams);
|
||||
return new ArrayList<>(pageList.getRecords());
|
||||
};
|
||||
|
||||
// AutoPoi 导出Excel
|
||||
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
|
||||
//此处设置的filename无效 ,前端会重更新设置一下
|
||||
mv.addObject(NormalExcelConstants.FILE_NAME, title);
|
||||
mv.addObject(NormalExcelConstants.CLASS, clazz);
|
||||
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, jeecgBaseConfig.getPath().getUpload());
|
||||
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
|
||||
mv.addObject(NormalExcelConstants.EXPORT_SERVER, excelExportServer);
|
||||
mv.addObject(NormalExcelConstants.QUERY_PARAMS, queryWrapper);
|
||||
return mv;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据权限导出excel,传入导出字段参数
|
||||
@ -172,16 +222,15 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
params.setNeedSave(true);
|
||||
try {
|
||||
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
|
||||
//update-begin-author:taoyan date:20190528 for:批量插入数据
|
||||
// 代码逻辑说明: 批量插入数据
|
||||
long start = System.currentTimeMillis();
|
||||
service.saveBatch(list);
|
||||
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
|
||||
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
|
||||
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
|
||||
//update-end-author:taoyan date:20190528 for:批量插入数据
|
||||
return Result.ok("文件导入成功!数据行数:" + list.size());
|
||||
} catch (Exception e) {
|
||||
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
|
||||
// 代码逻辑说明: 导入数据重复增加提示
|
||||
String msg = e.getMessage();
|
||||
log.error(msg, e);
|
||||
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
|
||||
@ -189,7 +238,6 @@ public class JeecgController<T, S extends IService<T>> {
|
||||
}else{
|
||||
return Result.error("文件导入失败:" + e.getMessage());
|
||||
}
|
||||
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
|
||||
} finally {
|
||||
try {
|
||||
file.getInputStream().close();
|
||||
|
||||
@ -9,10 +9,10 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* @Description: Entity基类
|
||||
|
||||
@ -11,6 +11,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import org.apache.commons.beanutils.PropertyUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
@ -96,7 +97,6 @@ public class QueryGenerator {
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||
/**
|
||||
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
|
||||
* @param searchObj 查询实体
|
||||
@ -111,7 +111,6 @@ public class QueryGenerator {
|
||||
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
|
||||
return queryWrapper;
|
||||
}
|
||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||
|
||||
/**
|
||||
* 组装Mybatis Plus 查询条件
|
||||
@ -141,7 +140,6 @@ public class QueryGenerator {
|
||||
}
|
||||
|
||||
String name, type, column;
|
||||
// update-begin--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
||||
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
|
||||
Map<String,String> fieldColumnMap = new HashMap<>(5);
|
||||
for (int i = 0; i < origDescriptors.length; i++) {
|
||||
@ -187,7 +185,7 @@ public class QueryGenerator {
|
||||
queryWrapper.and(j -> j.like(field,vals[0]));
|
||||
}
|
||||
}else {
|
||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||
// 代码逻辑说明: [TV360X-378]增加自定义字段查询规则功能------------
|
||||
QueryRuleEnum rule;
|
||||
if(null != customRuleMap && customRuleMap.containsKey(name)) {
|
||||
// 有自定义规则,使用自定义规则.
|
||||
@ -196,7 +194,6 @@ public class QueryGenerator {
|
||||
//根据参数值带什么关键字符串判断走什么类型的查询
|
||||
rule = convert2Rule(value);
|
||||
}
|
||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]增加自定义字段查询规则功能------------
|
||||
value = replaceValue(rule,value);
|
||||
// add -begin 添加判断为字符串时设为全模糊查询
|
||||
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
|
||||
@ -216,7 +213,6 @@ public class QueryGenerator {
|
||||
|
||||
//高级查询
|
||||
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
|
||||
// update-end--Author:taoyan Date:20200923 for:issues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
|
||||
|
||||
}
|
||||
|
||||
@ -257,15 +253,74 @@ public class QueryGenerator {
|
||||
if(parameterMap!=null&& parameterMap.containsKey(ORDER_TYPE)) {
|
||||
order = parameterMap.get(ORDER_TYPE)[0];
|
||||
}
|
||||
log.debug("排序规则>>列:" + column + ",排序方式:" + order);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(column)){
|
||||
log.debug("单字段排序规则>> column:" + column + ",排序方式:" + order);
|
||||
}
|
||||
|
||||
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
|
||||
// 1. 列表多字段排序优先
|
||||
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
|
||||
// 多字段排序
|
||||
String sortInfoString = parameterMap.get("sortInfoString")[0];
|
||||
log.debug("多字段排序规则>> sortInfoString:" + sortInfoString);
|
||||
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
||||
log.debug(orderItemList.toString());
|
||||
if (orderItemList != null && !orderItemList.isEmpty()) {
|
||||
for (OrderItem item : orderItemList) {
|
||||
// 一、获取排序数据库字段
|
||||
String columnName = item.getColumn();
|
||||
// 1.字典字段,去掉字典翻译文本后缀
|
||||
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
|
||||
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
||||
}
|
||||
// 2.实体驼峰字段转为数据库字段
|
||||
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
|
||||
|
||||
// 二、设置字段排序规则
|
||||
if (item.isAsc()) {
|
||||
queryWrapper.orderByAsc(columnName);
|
||||
} else {
|
||||
queryWrapper.orderByDesc(columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 列表单字段默认排序
|
||||
if(oConvertUtils.isEmpty(column) && parameterMap!=null&& parameterMap.containsKey("defSortString")) {
|
||||
// 多字段排序
|
||||
String sortInfoString = parameterMap.get("defSortString")[0];
|
||||
log.info("默认多字段排序规则>> defSortString:" + sortInfoString);
|
||||
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
|
||||
log.info(orderItemList.toString());
|
||||
if (orderItemList != null && !orderItemList.isEmpty()) {
|
||||
for (OrderItem item : orderItemList) {
|
||||
// 一、获取排序数据库字段
|
||||
String columnName = item.getColumn();
|
||||
// 1.字典字段,去掉字典翻译文本后缀
|
||||
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
|
||||
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
||||
}
|
||||
// 2.实体驼峰字段转为数据库字段
|
||||
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
|
||||
|
||||
// 二、设置字段排序规则
|
||||
if (item.isAsc()) {
|
||||
queryWrapper.orderByAsc(columnName);
|
||||
} else {
|
||||
queryWrapper.orderByDesc(columnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
|
||||
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
|
||||
column = "id";
|
||||
log.warn("检测到实体里没有字段createTime,改成采用ID排序!");
|
||||
}
|
||||
//update-end-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
|
||||
|
||||
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
|
||||
//字典字段,去掉字典翻译文本后缀
|
||||
@ -273,15 +328,12 @@ public class QueryGenerator {
|
||||
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
||||
//判断column是不是当前实体的
|
||||
log.debug("当前字段有:"+ allFields);
|
||||
if (!allColumnExist(column, allFields)) {
|
||||
throw new JeecgBootException("请注意,将要排序的列字段不存在:" + column);
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
||||
|
||||
//update-begin-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
|
||||
//多字段排序方法没有读取 MybatisPlus 注解 @TableField 里 value 的值
|
||||
if (column.contains(",")) {
|
||||
List<String> columnList = Arrays.asList(column.split(","));
|
||||
@ -292,12 +344,10 @@ public class QueryGenerator {
|
||||
}else{
|
||||
column = fieldColumnMap.get(column);
|
||||
}
|
||||
//update-end-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
|
||||
|
||||
//SQL注入check
|
||||
SqlInjectionUtil.filterContentMulti(column);
|
||||
|
||||
//update-begin--Author:scott Date:20210531 for:36 多条件排序无效问题修正-------
|
||||
// 排序规则修改
|
||||
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
|
||||
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
|
||||
@ -306,11 +356,9 @@ public class QueryGenerator {
|
||||
} else {
|
||||
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
|
||||
}
|
||||
//update-end--Author:scott Date:20210531 for:36 多条件排序无效问题修正-------
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
||||
/**
|
||||
* 多字段排序 判断所传字段是否存在
|
||||
* @return
|
||||
@ -330,7 +378,6 @@ public class QueryGenerator {
|
||||
}
|
||||
return exist;
|
||||
}
|
||||
//update-end-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时,使用SQL注入生效
|
||||
|
||||
/**
|
||||
* 高级查询
|
||||
@ -343,42 +390,46 @@ public class QueryGenerator {
|
||||
String superQueryParams = parameterMap.get(SUPER_QUERY_PARAMS)[0];
|
||||
String superQueryMatchType = parameterMap.get(SUPER_QUERY_MATCH_TYPE) != null ? parameterMap.get(SUPER_QUERY_MATCH_TYPE)[0] : MatchTypeEnum.AND.getValue();
|
||||
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
|
||||
// update-begin--Author:sunjianlei Date:20200325 for:高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
||||
// 代码逻辑说明: 高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
||||
try {
|
||||
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
|
||||
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
|
||||
if (conditions == null || conditions.size() == 0) {
|
||||
return;
|
||||
}
|
||||
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
||||
// 代码逻辑说明: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
||||
List<QueryCondition> filterConditions = conditions.stream().filter(
|
||||
rule -> oConvertUtils.isNotEmpty(rule.getField())
|
||||
&& oConvertUtils.isNotEmpty(rule.getRule())
|
||||
&& oConvertUtils.isNotEmpty(rule.getVal())
|
||||
rule -> (oConvertUtils.isNotEmpty(rule.getField())
|
||||
&& oConvertUtils.isNotEmpty(rule.getRule())
|
||||
&& oConvertUtils.isNotEmpty(rule.getVal())
|
||||
)
|
||||
|| "empty".equals(rule.getRule())
|
||||
).collect(Collectors.toList());
|
||||
if (filterConditions.size() == 0) {
|
||||
return;
|
||||
}
|
||||
// update-end-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
|
||||
log.debug("---高级查询参数-->" + filterConditions);
|
||||
|
||||
queryWrapper.and(andWrapper -> {
|
||||
for (int i = 0; i < filterConditions.size(); i++) {
|
||||
QueryCondition rule = filterConditions.get(i);
|
||||
if (oConvertUtils.isNotEmpty(rule.getField())
|
||||
&& oConvertUtils.isNotEmpty(rule.getRule())
|
||||
&& oConvertUtils.isNotEmpty(rule.getVal())) {
|
||||
if (
|
||||
(
|
||||
oConvertUtils.isNotEmpty(rule.getField()) && oConvertUtils.isNotEmpty(rule.getRule()) && oConvertUtils.isNotEmpty(rule.getVal())
|
||||
)
|
||||
|| "empty".equals(rule.getRule())
|
||||
) {
|
||||
|
||||
log.debug("SuperQuery ==> " + rule.toString());
|
||||
|
||||
//update-begin-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
||||
// 代码逻辑说明: 【高级查询】 oracle 日期等于查询报错
|
||||
Object queryValue = rule.getVal();
|
||||
if("date".equals(rule.getType())){
|
||||
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
|
||||
}else if("datetime".equals(rule.getType())){
|
||||
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
|
||||
}
|
||||
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||
String dbType = rule.getDbType();
|
||||
if (oConvertUtils.isNotEmpty(dbType)) {
|
||||
try {
|
||||
@ -411,9 +462,8 @@ public class QueryGenerator {
|
||||
log.error("高级查询值转换失败:", e);
|
||||
}
|
||||
}
|
||||
// update-begin--author:sunjianlei date:20210702 for:【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换,查询参数都是字符串类型 ----
|
||||
addEasyQuery(andWrapper, fieldColumnMap.get(rule.getField()), QueryRuleEnum.getByValue(rule.getRule()), queryValue);
|
||||
//update-end-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
|
||||
|
||||
// 如果拼接方式是OR,就拼接OR
|
||||
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
|
||||
@ -429,7 +479,6 @@ public class QueryGenerator {
|
||||
log.error("--高级查询拼接失败:" + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
// update-end--Author:sunjianlei Date:20200325 for:高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
|
||||
}
|
||||
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
|
||||
}
|
||||
@ -441,7 +490,7 @@ public class QueryGenerator {
|
||||
*/
|
||||
public static QueryRuleEnum convert2Rule(Object value) {
|
||||
// 避免空数据
|
||||
// update-begin-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||
// 代码逻辑说明: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||
if (value == null) {
|
||||
return QueryRuleEnum.EQ;
|
||||
}
|
||||
@ -449,10 +498,8 @@ public class QueryGenerator {
|
||||
if (val.length() == 0) {
|
||||
return QueryRuleEnum.EQ;
|
||||
}
|
||||
// update-end-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
|
||||
QueryRuleEnum rule =null;
|
||||
|
||||
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||
//TODO 此处规则,只适用于 le lt ge gt
|
||||
// step 2 .>= =<
|
||||
int length2 = 2;
|
||||
@ -468,14 +515,12 @@ public class QueryGenerator {
|
||||
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
|
||||
}
|
||||
}
|
||||
//update-end--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
||||
|
||||
// step 3 like
|
||||
//update-begin-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
||||
// 代码逻辑说明: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
||||
if(rule == null && val.equals(STAR)){
|
||||
rule = QueryRuleEnum.EQ;
|
||||
}
|
||||
//update-end-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
|
||||
if (rule == null && val.contains(STAR)) {
|
||||
if (val.startsWith(STAR) && val.endsWith(STAR)) {
|
||||
rule = QueryRuleEnum.LIKE;
|
||||
@ -500,12 +545,10 @@ public class QueryGenerator {
|
||||
rule = QueryRuleEnum.EQ_WITH_ADD;
|
||||
}
|
||||
|
||||
//update-begin--Author:taoyan Date:20201229 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
||||
//特殊处理:Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号,会被识别为in查询,转为等于查询
|
||||
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
|
||||
rule = QueryRuleEnum.EQ;
|
||||
}
|
||||
//update-end--Author:taoyan Date:20201229 for:initQueryWrapper组装sql查询条件错误 #284---------------------
|
||||
|
||||
return rule != null ? rule : QueryRuleEnum.EQ;
|
||||
}
|
||||
@ -525,11 +568,10 @@ public class QueryGenerator {
|
||||
return value;
|
||||
}
|
||||
String val = (value + "").toString().trim();
|
||||
//update-begin-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
||||
// 代码逻辑说明: 查询条件的值为等号(=)bug #3443
|
||||
if(QueryRuleEnum.EQ.getValue().equals(val)){
|
||||
return val;
|
||||
}
|
||||
//update-end-author:taoyan date:20220302 for: 查询条件的值为等号(=)bug #3443
|
||||
if (rule == QueryRuleEnum.LIKE) {
|
||||
value = val.substring(1, val.length() - 1);
|
||||
//mysql 模糊查询之特殊字符下划线 (_、\)
|
||||
@ -547,21 +589,19 @@ public class QueryGenerator {
|
||||
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
|
||||
value = val.replaceAll("\\+\\+", COMMA);
|
||||
}else {
|
||||
//update-begin--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||
// 代码逻辑说明: initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||
if(val.startsWith(rule.getValue())){
|
||||
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
|
||||
value = val.replaceFirst(rule.getValue(),"");
|
||||
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
|
||||
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
|
||||
}
|
||||
//update-end--Author:scott Date:20190724 for:initQueryWrapper组装sql查询条件错误 #284-------------------
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
|
||||
if(oConvertUtils.isNotEmpty(value)) {
|
||||
//update-begin--Author:sunjianlei Date:20220104 for:【JTC-409】修复逗号分割情况下没有转换类型,导致类型严格的数据库查询报错 -------------------
|
||||
// 针对数字类型字段,多值查询
|
||||
if(value.contains(COMMA)){
|
||||
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
|
||||
@ -577,7 +617,6 @@ public class QueryGenerator {
|
||||
}
|
||||
Object temp = QueryGenerator.parseByType(value, type, rule);
|
||||
addEasyQuery(queryWrapper, name, rule, temp);
|
||||
//update-end--Author:sunjianlei Date:20220104 for:【JTC-409】修复逗号分割情况下没有转换类型,导致类型严格的数据库查询报错 -------------------
|
||||
}
|
||||
}
|
||||
|
||||
@ -654,7 +693,11 @@ public class QueryGenerator {
|
||||
* @param value 查询条件值
|
||||
*/
|
||||
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
|
||||
if (name==null || value == null || rule == null || oConvertUtils.isEmpty(value)) {
|
||||
if (
|
||||
(
|
||||
name==null || value == null || rule == null || oConvertUtils.isEmpty(value)
|
||||
)
|
||||
&& !QueryRuleEnum.EMPTY.equals(rule)) {
|
||||
return;
|
||||
}
|
||||
name = oConvertUtils.camelToUnderline(name);
|
||||
@ -666,6 +709,9 @@ public class QueryGenerator {
|
||||
case GE:
|
||||
queryWrapper.ge(name, value);
|
||||
break;
|
||||
case EMPTY:
|
||||
queryWrapper.isNull(name);
|
||||
break;
|
||||
case LT:
|
||||
queryWrapper.lt(name, value);
|
||||
break;
|
||||
@ -685,13 +731,12 @@ public class QueryGenerator {
|
||||
}else if(value instanceof String[]) {
|
||||
queryWrapper.in(name, (Object[]) value);
|
||||
}
|
||||
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
||||
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
|
||||
else if(value.getClass().isArray()) {
|
||||
queryWrapper.in(name, (Object[])value);
|
||||
}else {
|
||||
queryWrapper.in(name, value);
|
||||
}
|
||||
//update-end-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
||||
break;
|
||||
case LIKE:
|
||||
queryWrapper.like(name, value);
|
||||
@ -708,7 +753,7 @@ public class QueryGenerator {
|
||||
case NOT_RIGHT_LIKE:
|
||||
queryWrapper.notLikeRight(name, value);
|
||||
break;
|
||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||
// 代码逻辑说明: [TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||
case LIKE_WITH_OR:
|
||||
final String nameFinal = name;
|
||||
Object[] vals;
|
||||
@ -717,7 +762,7 @@ public class QueryGenerator {
|
||||
} else if (value instanceof String[]) {
|
||||
vals = (Object[]) value;
|
||||
}
|
||||
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
|
||||
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
|
||||
else if (value.getClass().isArray()) {
|
||||
vals = (Object[]) value;
|
||||
} else {
|
||||
@ -732,7 +777,6 @@ public class QueryGenerator {
|
||||
}
|
||||
});
|
||||
break;
|
||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
|
||||
default:
|
||||
log.info("--查询规则未匹配到---");
|
||||
break;
|
||||
@ -746,10 +790,8 @@ public class QueryGenerator {
|
||||
private static boolean judgedIsUselessField(String name) {
|
||||
return "class".equals(name) || "ids".equals(name)
|
||||
|| "page".equals(name) || "rows".equals(name)
|
||||
//// update-begin--author:sunjianlei date:20240808 for:【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
|
||||
//// https://github.com/jeecgboot/JeecgBoot/issues/6937
|
||||
// || "sort".equals(name) || "order".equals(name)
|
||||
//// update-end----author:sunjianlei date:20240808 for:【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
|
||||
;
|
||||
}
|
||||
|
||||
@ -762,13 +804,12 @@ public class QueryGenerator {
|
||||
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
|
||||
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
|
||||
List<SysPermissionDataRuleModel> list = null;
|
||||
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
||||
// 代码逻辑说明: QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
||||
try {
|
||||
list = JeecgDataAutorUtils.loadDataSearchConditon();
|
||||
}catch (Exception e){
|
||||
log.error("根据request对象获取权限数据失败,可能是定时任务中执行的。", e);
|
||||
}
|
||||
//update-end-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
|
||||
if(list != null&&list.size()>0){
|
||||
if(list.get(0)==null){
|
||||
return ruleMap;
|
||||
@ -805,7 +846,8 @@ public class QueryGenerator {
|
||||
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
|
||||
}
|
||||
}else {
|
||||
addEasyQuery(queryWrapper, name, rule, NumberUtils.parseNumber(dataRule.getRuleValue(), propertyType));
|
||||
// 代码逻辑说明: [issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
|
||||
addEasyQuery(queryWrapper, name, rule, NumberUtils.parseNumber(converRuleValue(dataRule.getRuleValue()), propertyType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -859,7 +901,8 @@ public class QueryGenerator {
|
||||
return null;
|
||||
}
|
||||
Set<String> varParams = new HashSet<String>();
|
||||
String regex = "\\#\\{\\w+\\}";
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
String regex = "#\\{\\[*\\w+]*}";
|
||||
|
||||
Pattern p = Pattern.compile(regex);
|
||||
Matcher m = p.matcher(sql);
|
||||
@ -915,9 +958,8 @@ public class QueryGenerator {
|
||||
Class propType = origDescriptors[i].getPropertyType();
|
||||
boolean isString = propType.equals(String.class);
|
||||
Object value;
|
||||
//update-begin---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
||||
// 代码逻辑说明: [TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
||||
if(isString || Date.class.equals(propType)) {
|
||||
//update-end---author:chenrui ---date:20240527 for:[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
|
||||
value = converRuleValue(dataRule.getRuleValue());
|
||||
}else {
|
||||
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);
|
||||
|
||||
@ -54,8 +54,10 @@ public enum QueryRuleEnum {
|
||||
NOT_EMPTY("NOT_EMPTY","not_empty","值不为空"),
|
||||
/**查询规则 不包含*/
|
||||
NOT_IN("NOT_IN","not_in","不包含"),
|
||||
/**查询规则 多词匹配*/
|
||||
/**查询规则 多词精确匹配*/
|
||||
ELE_MATCH("ELE_MATCH","elemMatch","多词匹配"),
|
||||
/**查询规则 多词精确不匹配*/
|
||||
ELE_NOT_MATCH("ELE_NOT_MATCH","elemNotMatch","多词精确不匹配"),
|
||||
/**查询规则 范围查询*/
|
||||
RANGE("RANGE","range","范围查询"),
|
||||
/**查询规则 不在范围内查询*/
|
||||
|
||||
@ -11,6 +11,9 @@ import com.google.common.base.Joiner;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@ -38,8 +41,10 @@ import org.jeecg.common.util.oConvertUtils;
|
||||
@Slf4j
|
||||
public class JwtUtil {
|
||||
|
||||
/**Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
|
||||
/**PC端,Token有效期为7天(Token在reids中缓存时间为两倍)*/
|
||||
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
|
||||
/**APP端,Token有效期为30天(Token在reids中缓存时间为两倍)*/
|
||||
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
|
||||
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
|
||||
|
||||
/**
|
||||
@ -48,24 +53,24 @@ public class JwtUtil {
|
||||
* @param code
|
||||
* @param errorMsg
|
||||
*/
|
||||
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
// issues/I4YH95浏览器显示乱码问题
|
||||
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
|
||||
Result jsonResult = new Result(code, errorMsg);
|
||||
jsonResult.setSuccess(false);
|
||||
OutputStream os = null;
|
||||
try {
|
||||
os = httpServletResponse.getOutputStream();
|
||||
httpServletResponse.setCharacterEncoding("UTF-8");
|
||||
httpServletResponse.setStatus(code);
|
||||
os.write(new ObjectMapper().writeValueAsString(jsonResult).getBytes("UTF-8"));
|
||||
os.flush();
|
||||
os.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
|
||||
try {
|
||||
Result jsonResult = new Result(code, errorMsg);
|
||||
jsonResult.setSuccess(false);
|
||||
|
||||
// 设置响应头和内容类型
|
||||
response.setStatus(code);
|
||||
response.setHeader("Content-type", "text/html;charset=UTF-8");
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
// 使用 ObjectMapper 序列化为 JSON 字符串
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String json = objectMapper.writeValueAsString(jsonResult);
|
||||
response.getWriter().write(json);
|
||||
response.getWriter().flush();
|
||||
} catch (IOException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验token是否正确
|
||||
@ -82,7 +87,8 @@ public class JwtUtil {
|
||||
// 效验TOKEN
|
||||
DecodedJWT jwt = verifier.verify(token);
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
} catch (Exception e) {
|
||||
log.warn("Token验证失败:" + e.getMessage(),e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -97,6 +103,7 @@ public class JwtUtil {
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
return jwt.getClaim("username").asString();
|
||||
} catch (JWTDecodeException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -107,7 +114,9 @@ public class JwtUtil {
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @return 加密的token
|
||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||
*/
|
||||
@Deprecated
|
||||
public static String sign(String username, String secret) {
|
||||
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
@ -116,6 +125,68 @@ public class JwtUtil {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成签名,5min后过期
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @param expireTime 过期时间
|
||||
* @return 加密的token
|
||||
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
|
||||
*/
|
||||
@Deprecated
|
||||
public static String sign(String username, String secret, Long expireTime) {
|
||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
// 附带username信息
|
||||
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名,根据客户端类型自动选择过期时间
|
||||
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param secret 用户的密码
|
||||
* @param clientType 客户端类型(PC或APP)
|
||||
* @return 加密的token
|
||||
*/
|
||||
public static String sign(String username, String secret, String clientType) {
|
||||
// 根据客户端类型选择对应的过期时间
|
||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||
? APP_EXPIRE_TIME
|
||||
: EXPIRE_TIME;
|
||||
Date date = new Date(System.currentTimeMillis() + expireTime);
|
||||
Algorithm algorithm = Algorithm.HMAC256(secret);
|
||||
// 附带username和clientType信息
|
||||
return JWT.create()
|
||||
.withClaim("username", username)
|
||||
.withClaim("clientType", clientType)
|
||||
.withExpiresAt(date)
|
||||
.sign(algorithm);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取客户端类型
|
||||
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 客户端类型,如果不存在则返回PC(兼容旧token)
|
||||
*/
|
||||
public static String getClientType(String token) {
|
||||
try {
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
String clientType = jwt.getClaim("clientType").asString();
|
||||
// 如果clientType为空,返回默认值PC(兼容旧token)
|
||||
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
|
||||
} catch (JWTDecodeException e) {
|
||||
log.warn("解析token中的clientType失败,使用默认值PC:" + e.getMessage());
|
||||
return CommonConstant.CLIENT_TYPE_PC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据request中的token获取用户账号
|
||||
*
|
||||
@ -195,6 +266,14 @@ public class JwtUtil {
|
||||
} else {
|
||||
key = key;
|
||||
}
|
||||
// 是否存在字符串标志
|
||||
boolean multiStr;
|
||||
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
|
||||
key = key.substring(1,key.length()-1);
|
||||
multiStr = true;
|
||||
} else {
|
||||
multiStr = false;
|
||||
}
|
||||
//替换为当前系统时间(年月日)
|
||||
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
|
||||
returnValue = DateUtils.formatDate();
|
||||
@ -263,11 +342,25 @@ public class JwtUtil {
|
||||
if(user==null){
|
||||
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
|
||||
returnValue = sysUser.getOrgCode();
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||
}else{
|
||||
if(user.isOneDepart()) {
|
||||
returnValue = user.getSysMultiOrgCode().get(0);
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
|
||||
}else {
|
||||
returnValue = Joiner.on(",").join(user.getSysMultiOrgCode());
|
||||
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
|
||||
returnValue = user.getSysMultiOrgCode().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(orgCode -> {
|
||||
if (multiStr) {
|
||||
return "'" + orgCode + "'";
|
||||
} else {
|
||||
return orgCode;
|
||||
}
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -281,7 +374,7 @@ public class JwtUtil {
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin-author:taoyan date:20210330 for:多租户ID作为系统变量
|
||||
// 代码逻辑说明: 多租户ID作为系统变量
|
||||
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
|
||||
try {
|
||||
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
|
||||
@ -289,7 +382,6 @@ public class JwtUtil {
|
||||
log.warn("获取系统租户异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
|
||||
if(returnValue!=null){returnValue = returnValue + moshi;}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@ -13,31 +13,33 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 资源加载工具类
|
||||
* 枚举字典数据 资源加载工具类
|
||||
*
|
||||
* @Author taoYan
|
||||
* @Date 2022/7/8 10:40
|
||||
**/
|
||||
@Slf4j
|
||||
public class ResourceUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 多个包扫描根路径
|
||||
*
|
||||
* 之所以让用户手工配置扫描路径,是为了避免不必要的类加载开销,提升启动性能。
|
||||
* 请务必将所有枚举类所在包路径添加到此配置中。
|
||||
*/
|
||||
private final static String[] BASE_SCAN_PACKAGES = {
|
||||
"org.jeecg.common.constant.enums",
|
||||
"org.jeecg.modules.message.enums"
|
||||
};
|
||||
|
||||
/**
|
||||
* 枚举字典数据
|
||||
*/
|
||||
private final static Map<String, List<DictModel>> enumDictData = new HashMap<>(5);
|
||||
|
||||
/**
|
||||
* 所有java类
|
||||
*/
|
||||
private final static String CLASS_PATTERN="/**/*.class";
|
||||
|
||||
/**
|
||||
* 所有枚举java类
|
||||
*/
|
||||
@ -45,9 +47,9 @@ public class ResourceUtil {
|
||||
private final static String CLASS_ENUM_PATTERN="/**/*Enum.class";
|
||||
|
||||
/**
|
||||
* 包路径 org.jeecg
|
||||
* 初始化状态标识
|
||||
*/
|
||||
private final static String BASE_PACKAGE = "org.jeecg";
|
||||
private static volatile boolean initialized = false;
|
||||
|
||||
/**
|
||||
* 枚举类中获取字典数据的方法名
|
||||
@ -55,59 +57,135 @@ public class ResourceUtil {
|
||||
private final static String METHOD_NAME = "getDictList";
|
||||
|
||||
/**
|
||||
* 获取枚举字典数据
|
||||
* 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems()
|
||||
* @return
|
||||
*
|
||||
* @return 枚举字典数据
|
||||
*/
|
||||
public static Map<String, List<DictModel>> getEnumDictData(){
|
||||
if(enumDictData.keySet().size()>0){
|
||||
return enumDictData;
|
||||
}
|
||||
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
|
||||
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(BASE_PACKAGE) + CLASS_ENUM_PATTERN;
|
||||
try {
|
||||
Resource[] resources = resourcePatternResolver.getResources(pattern);
|
||||
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
||||
for (Resource resource : resources) {
|
||||
MetadataReader reader = readerFactory.getMetadataReader(resource);
|
||||
String classname = reader.getClassMetadata().getClassName();
|
||||
Class<?> clazz = Class.forName(classname);
|
||||
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
|
||||
if (enumDict != null) {
|
||||
EnumDict annotation = clazz.getAnnotation(EnumDict.class);
|
||||
String key = annotation.value();
|
||||
if(oConvertUtils.isNotEmpty(key)){
|
||||
List<DictModel> list = (List<DictModel>) clazz.getDeclaredMethod(METHOD_NAME).invoke(null);
|
||||
enumDictData.put(key, list);
|
||||
}
|
||||
public static Map<String, List<DictModel>> getEnumDictData() {
|
||||
if (!initialized) {
|
||||
synchronized (ResourceUtil.class) {
|
||||
if (!initialized) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
|
||||
|
||||
initEnumDictData();
|
||||
initialized = true;
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
log.debug("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
|
||||
}
|
||||
}
|
||||
}catch (Exception e){
|
||||
log.error("获取枚举类字典数据异常", e.getMessage());
|
||||
// e.printStackTrace();
|
||||
}
|
||||
return enumDictData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
|
||||
* @param dictCodeList
|
||||
* @param keys
|
||||
* @return
|
||||
* 使用多包路径扫描方式初始化枚举字典数据
|
||||
*/
|
||||
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys){
|
||||
if(enumDictData.keySet().size()==0){
|
||||
getEnumDictData();
|
||||
private static void initEnumDictData() {
|
||||
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
|
||||
|
||||
long scanStartTime = System.currentTimeMillis();
|
||||
List<Resource> allResources = new ArrayList<>();
|
||||
|
||||
// 扫描多个包路径
|
||||
for (String basePackage : BASE_SCAN_PACKAGES) {
|
||||
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(basePackage) + CLASS_ENUM_PATTERN;
|
||||
|
||||
try {
|
||||
Resource[] resources = resourcePatternResolver.getResources(pattern);
|
||||
allResources.addAll(Arrays.asList(resources));
|
||||
log.debug("【枚举字典加载】扫描包 {} 找到 {} 个枚举类文件", basePackage, resources.length);
|
||||
} catch (Exception e) {
|
||||
log.warn("【枚举字典加载】扫描包 {} 时出现异常: {}", basePackage, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
long scanEndTime = System.currentTimeMillis();
|
||||
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
|
||||
|
||||
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
|
||||
|
||||
long processStartTime = System.currentTimeMillis();
|
||||
int processedCount = 0;
|
||||
|
||||
for (Resource resource : allResources) {
|
||||
try {
|
||||
MetadataReader reader = readerFactory.getMetadataReader(resource);
|
||||
String classname = reader.getClassMetadata().getClassName();
|
||||
|
||||
// 提前检查是否有@EnumDict注解,避免不必要的Class.forName
|
||||
if (hasEnumDictAnnotation(reader)) {
|
||||
processEnumClass(classname);
|
||||
processedCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("处理资源异常: {} - {}", resource.getFilename(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
long processEndTime = System.currentTimeMillis();
|
||||
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查类是否有EnumDict注解(通过元数据,避免类加载)
|
||||
*/
|
||||
private static boolean hasEnumDictAnnotation(MetadataReader reader) {
|
||||
try {
|
||||
return reader.getAnnotationMetadata().hasAnnotation(EnumDict.class.getName());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个枚举类
|
||||
*/
|
||||
private static void processEnumClass(String classname) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(classname);
|
||||
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
|
||||
|
||||
if (enumDict != null) {
|
||||
String key = enumDict.value();
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
Method method = clazz.getDeclaredMethod(METHOD_NAME);
|
||||
List<DictModel> list = (List<DictModel>) method.invoke(null);
|
||||
enumDictData.put(key, list);
|
||||
log.debug("成功加载枚举字典: {} -> {}", key, classname);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("处理枚举类异常: {} - {}", classname, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
|
||||
*
|
||||
* @param dictCodeList 字典编码列表
|
||||
* @param keys 键值列表
|
||||
* @return 字典数据映射
|
||||
*/
|
||||
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys) {
|
||||
Map<String, List<DictModel>> enumDict = getEnumDictData();
|
||||
Map<String, List<DictModel>> map = new HashMap<>();
|
||||
for (String code : enumDictData.keySet()) {
|
||||
if(dictCodeList.indexOf(code)>=0){
|
||||
List<DictModel> dictItemList = enumDictData.get(code);
|
||||
for(DictModel dm: dictItemList){
|
||||
|
||||
// 使用更高效的查找方式
|
||||
Set<String> dictCodeSet = new HashSet<>(dictCodeList);
|
||||
Set<String> keySet = new HashSet<>(keys);
|
||||
|
||||
for (String code : enumDict.keySet()) {
|
||||
if (dictCodeSet.contains(code)) {
|
||||
List<DictModel> dictItemList = enumDict.get(code);
|
||||
for (DictModel dm : dictItemList) {
|
||||
String value = dm.getValue();
|
||||
if(keys.indexOf(value)>=0){
|
||||
if (keySet.contains(value)) {
|
||||
List<DictModel> list = new ArrayList<>();
|
||||
list.add(new DictModel(value, dm.getText()));
|
||||
map.put(code,list);
|
||||
map.put(code, list);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -115,22 +193,5 @@ public class ResourceUtil {
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实现类
|
||||
*
|
||||
* @param classPath
|
||||
*/
|
||||
public static Object getImplementationClass(String classPath){
|
||||
try {
|
||||
Class<?> aClass = Class.forName(classPath);
|
||||
return SpringContextUtils.getBean(aClass);
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error("类没有找到",e);
|
||||
return null;
|
||||
} catch (NoSuchBeanDefinitionException e){
|
||||
log.error(classPath + "没有实现",e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,13 +1,22 @@
|
||||
package org.jeecg.common.system.util;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.query.QueryRuleEnum;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@ -141,7 +150,7 @@ public class SqlConcatUtil {
|
||||
}
|
||||
|
||||
private static String getInConditionValue(Object value,boolean isString) {
|
||||
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
||||
// 代码逻辑说明: 查询条件如果输入,导致sql报错
|
||||
String[] temp = value.toString().split(",");
|
||||
if(temp.length==0){
|
||||
return "('')";
|
||||
@ -159,7 +168,6 @@ public class SqlConcatUtil {
|
||||
}else {
|
||||
return "("+value.toString()+")";
|
||||
}
|
||||
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,7 +214,6 @@ public class SqlConcatUtil {
|
||||
}
|
||||
}else {
|
||||
|
||||
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
||||
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
|
||||
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
|
||||
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
|
||||
@ -227,7 +234,6 @@ public class SqlConcatUtil {
|
||||
return "'%" + str + "%'";
|
||||
}
|
||||
}
|
||||
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
|
||||
|
||||
}
|
||||
}
|
||||
@ -239,5 +245,47 @@ public class SqlConcatUtil {
|
||||
private static String getDbType() {
|
||||
return CommonUtils.getDatabaseType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前端传过来的 "多字段排序信息: sortInfoString"
|
||||
* @return
|
||||
*/
|
||||
public static List<OrderItem> getQueryConditionOrders(String column, String order, String queryInfoString){
|
||||
List<OrderItem> list = new ArrayList<>();
|
||||
if(oConvertUtils.isEmpty(queryInfoString)){
|
||||
//默认以创建时间倒序查询
|
||||
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(order)){
|
||||
list.add(OrderItem.desc(column));
|
||||
}else{
|
||||
list.add(OrderItem.asc(column));
|
||||
}
|
||||
}else{
|
||||
// 【TV360X-967】URL解码(微服务下需要)
|
||||
if (queryInfoString.contains("%22column%22")) {
|
||||
log.info("queryInfoString 原生 = {}", queryInfoString);
|
||||
try {
|
||||
queryInfoString = URLDecoder.decode(queryInfoString, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new JeecgBootException(e);
|
||||
}
|
||||
log.info("queryInfoString 解码 = {}", queryInfoString);
|
||||
}
|
||||
JSONArray array = JSONArray.parseArray(queryInfoString);
|
||||
Iterator it = array.iterator();
|
||||
while(it.hasNext()){
|
||||
JSONObject json = (JSONObject)it.next();
|
||||
String tempColumn = json.getString("column");
|
||||
if(oConvertUtils.isNotEmpty(tempColumn)){
|
||||
String tempOrder = json.getString("order");
|
||||
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(tempOrder)){
|
||||
list.add(OrderItem.desc(tempColumn));
|
||||
}else{
|
||||
list.add(OrderItem.asc(tempColumn));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -68,6 +68,12 @@ public class LoginUser {
|
||||
@SensitiveField
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 工号
|
||||
*/
|
||||
@SensitiveField
|
||||
private String workNo;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
@ -138,4 +144,8 @@ public class LoginUser {
|
||||
/**设备id uniapp推送用*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 主岗位
|
||||
*/
|
||||
private String mainDepPostId;
|
||||
}
|
||||
|
||||
@ -0,0 +1,239 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
|
||||
import org.jeecg.common.exception.JeecgBootAssertException;
|
||||
|
||||
/**
|
||||
* 断言检查工具
|
||||
* for for [QQYUN-10990]AIRAG
|
||||
* @author chenrui
|
||||
* @date 2017-06-22 10:05:56
|
||||
*/
|
||||
public class AssertUtils {
|
||||
|
||||
/**
|
||||
* 确保对象为空,如果不为空抛出异常
|
||||
*
|
||||
* @param msg
|
||||
* @param obj
|
||||
* @throws JeecgBootAssertException
|
||||
* @author chenrui
|
||||
* @date 2017-06-22 10:05:56
|
||||
*/
|
||||
public static void assertEmpty(String msg, Object obj) {
|
||||
if (oConvertUtils.isObjectNotEmpty(obj)) {
|
||||
throw new JeecgBootAssertException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 确保对象不为空,如果为空抛出异常
|
||||
*
|
||||
* @param msg
|
||||
* @param obj
|
||||
* @throws JeecgBootAssertException
|
||||
* @author chenrui
|
||||
* @date 2017-06-22 10:05:56
|
||||
*/
|
||||
public static void assertNotEmpty(String msg, Object obj) {
|
||||
if (oConvertUtils.isObjectEmpty(obj)) {
|
||||
throw new JeecgBootAssertException(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证对象是否相同
|
||||
*
|
||||
* @param message
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @author chenrui
|
||||
* @date 2018/9/12 15:45
|
||||
*/
|
||||
public static void assertEquals(String message, Object expected,
|
||||
Object actual) {
|
||||
if (oConvertUtils.isEqual(expected, actual)) {
|
||||
return;
|
||||
}
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证不相同
|
||||
*
|
||||
* @param message
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @author chenrui
|
||||
* @date 2018/9/12 15:45
|
||||
*/
|
||||
public static void assertNotEquals(String message, Object expected,
|
||||
Object actual) {
|
||||
if (oConvertUtils.isEqual(expected, actual)) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否相等
|
||||
*
|
||||
* @param message
|
||||
* @param expected
|
||||
* @param actual
|
||||
* @author chenrui
|
||||
* @date 2018/9/12 15:45
|
||||
*/
|
||||
public static void assertSame(String message, Object expected,
|
||||
Object actual) {
|
||||
if (expected == actual) {
|
||||
return;
|
||||
}
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证不相等
|
||||
*
|
||||
* @param message
|
||||
* @param unexpected
|
||||
* @param actual
|
||||
* @author chenrui
|
||||
* @date 2018/9/12 15:45
|
||||
*/
|
||||
public static void assertNotSame(String message, Object unexpected,
|
||||
Object actual) {
|
||||
if (unexpected == actual) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否为真
|
||||
*
|
||||
* @param message
|
||||
* @param condition
|
||||
*/
|
||||
public static void assertTrue(String message, boolean condition) {
|
||||
if (!condition) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 condition是否为false
|
||||
*
|
||||
* @param message
|
||||
* @param condition
|
||||
*/
|
||||
public static void assertFalse(String message, boolean condition) {
|
||||
assertTrue(message, !condition);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证是否存在
|
||||
*
|
||||
* @param message
|
||||
* @param obj
|
||||
* @param objs
|
||||
* @param <T>
|
||||
* @throws JeecgBootAssertException
|
||||
* @author chenrui
|
||||
* @date 2018/1/31 22:14
|
||||
*/
|
||||
public static <T> void assertIn(String message, T obj, T... objs) {
|
||||
assertNotEmpty(message, obj);
|
||||
assertNotEmpty(message, objs);
|
||||
if (!oConvertUtils.isIn(obj, objs)) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证是否不存在
|
||||
*
|
||||
* @param message
|
||||
* @param obj
|
||||
* @param objs
|
||||
* @param <T>
|
||||
* @throws JeecgBootAssertException
|
||||
* @author chenrui
|
||||
* @date 2018/1/31 22:14
|
||||
*/
|
||||
|
||||
public static <T> void assertNotIn(String message, T obj, T... objs) {
|
||||
assertNotEmpty(message, obj);
|
||||
assertNotEmpty(message, objs);
|
||||
if (oConvertUtils.isIn(obj, objs)) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 确保src大于des
|
||||
*
|
||||
* @param message
|
||||
* @param src
|
||||
* @param des
|
||||
* @author chenrui
|
||||
* @date 2018/9/19 15:30
|
||||
*/
|
||||
public static void assertGt(String message, Number src, Number des) {
|
||||
if (oConvertUtils.isGt(src, des)) {
|
||||
return;
|
||||
}
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保src大于等于des
|
||||
*
|
||||
* @param message
|
||||
* @param src
|
||||
* @param des
|
||||
* @author chenrui
|
||||
* @date 2018/9/19 15:30
|
||||
*/
|
||||
public static void assertGe(String message, Number src, Number des) {
|
||||
if (oConvertUtils.isGe(src, des)) {
|
||||
return;
|
||||
}
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 确保src小于des
|
||||
*
|
||||
* @param message
|
||||
* @param src
|
||||
* @param des
|
||||
* @author chenrui
|
||||
* @date 2018/9/19 15:30
|
||||
*/
|
||||
public static void assertLt(String message, Number src, Number des) {
|
||||
if (oConvertUtils.isGe(src, des)) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保src小于等于des
|
||||
*
|
||||
* @param message
|
||||
* @param src
|
||||
* @param des
|
||||
* @author chenrui
|
||||
* @date 2018/9/19 15:30
|
||||
*/
|
||||
public static void assertLe(String message, Number src, Number des) {
|
||||
if (oConvertUtils.isGt(src, des)) {
|
||||
throw new JeecgBootAssertException(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -25,6 +25,7 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.SQLException;
|
||||
@ -152,9 +153,9 @@ public class CommonUtils {
|
||||
*/
|
||||
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
|
||||
try {
|
||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
||||
SsrfFileTypeFilter.checkUploadFileType(mf);
|
||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
||||
// 文件安全校验,防止上传漏洞文件
|
||||
SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
|
||||
|
||||
String fileName = null;
|
||||
File file = new File(uploadpath + File.separator + bizPath + File.separator );
|
||||
if (!file.exists()) {
|
||||
@ -163,6 +164,10 @@ public class CommonUtils {
|
||||
}
|
||||
// 获取文件名
|
||||
String orgName = mf.getOriginalFilename();
|
||||
// 无中文情况下进行转码
|
||||
if (orgName != null && !CommonUtils.ifContainChinese(orgName)) {
|
||||
orgName = new String(orgName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
|
||||
}
|
||||
orgName = CommonUtils.getFileName(orgName);
|
||||
if(orgName.indexOf(SymbolConstant.SPOT)!=-1){
|
||||
fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."));
|
||||
@ -242,6 +247,10 @@ public class CommonUtils {
|
||||
try {
|
||||
DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class);
|
||||
dbTypeEnum = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
|
||||
//【采用SQL_SERVER2005引擎】QQYUN-13298 解决升级mybatisPlus后SqlServer分页使用OFFSET,无排序字段报错问题
|
||||
if (dbTypeEnum == DbType.SQL_SERVER) {
|
||||
dbTypeEnum = DbType.SQL_SERVER2005;
|
||||
}
|
||||
return dbTypeEnum;
|
||||
} catch (SQLException e) {
|
||||
log.warn(e.getMessage(), e);
|
||||
|
||||
@ -13,6 +13,8 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
@ -814,4 +816,44 @@ public class DateUtils extends PropertyEditorSupport {
|
||||
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个日期之间的所有日期列表,包含开始和结束日期
|
||||
*
|
||||
* @param begin
|
||||
* @param end
|
||||
* @return
|
||||
*/
|
||||
public static List<Date> getDateRangeList(Date begin, Date end) {
|
||||
List<Date> dateList = new ArrayList<>();
|
||||
if (begin == null || end == null) {
|
||||
return dateList;
|
||||
}
|
||||
|
||||
// 清除时间部分,只比较日期
|
||||
Calendar beginCal = Calendar.getInstance();
|
||||
beginCal.setTime(begin);
|
||||
beginCal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
beginCal.set(Calendar.MINUTE, 0);
|
||||
beginCal.set(Calendar.SECOND, 0);
|
||||
beginCal.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
Calendar endCal = Calendar.getInstance();
|
||||
endCal.setTime(end);
|
||||
endCal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
endCal.set(Calendar.MINUTE, 0);
|
||||
endCal.set(Calendar.SECOND, 0);
|
||||
endCal.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
if (endCal.before(beginCal)) {
|
||||
return dateList;
|
||||
}
|
||||
|
||||
dateList.add(beginCal.getTime());
|
||||
while (beginCal.before(endCal)) {
|
||||
beginCal.add(Calendar.DAY_OF_YEAR, 1);
|
||||
dateList.add(beginCal.getTime());
|
||||
}
|
||||
return dateList;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
@ -8,11 +9,17 @@ import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.aliyuncs.profile.IClientProfile;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.enums.DySmsEnum;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.JeecgSmsTemplateConfig;
|
||||
import org.jeecg.config.StaticConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Created on 17/6/7.
|
||||
* 短信API产品的DEMO程序,工程中包含了一个SmsDemo类,直接通过
|
||||
@ -56,17 +63,21 @@ public class DySmsHelper {
|
||||
|
||||
|
||||
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
|
||||
//可自助调整超时时间
|
||||
JeecgBaseConfig config = SpringContextUtils.getBean(JeecgBaseConfig.class);
|
||||
String smsSendType = config.getSmsSendType();
|
||||
if(oConvertUtils.isNotEmpty(smsSendType) && CommonConstant.SMS_SEND_TYPE_TENCENT.equals(smsSendType)){
|
||||
return TencentSms.sendTencentSms(phone, templateParamJson, config.getTencent(), dySmsEnum);
|
||||
}
|
||||
//可自助调整超时时间
|
||||
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
|
||||
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
|
||||
|
||||
//update-begin-author:taoyan date:20200811 for:配置类数据获取
|
||||
// 代码逻辑说明: 配置类数据获取
|
||||
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
|
||||
//logger.info("阿里大鱼短信秘钥 accessKeyId:" + staticConfig.getAccessKeyId());
|
||||
//logger.info("阿里大鱼短信秘钥 accessKeySecret:"+ staticConfig.getAccessKeySecret());
|
||||
setAccessKeyId(staticConfig.getAccessKeyId());
|
||||
setAccessKeySecret(staticConfig.getAccessKeySecret());
|
||||
//update-end-author:taoyan date:20200811 for:配置类数据获取
|
||||
|
||||
//初始化acsClient,暂不支持region化
|
||||
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
|
||||
@ -75,15 +86,32 @@ public class DySmsHelper {
|
||||
|
||||
//验证json参数
|
||||
validateParam(templateParamJson,dySmsEnum);
|
||||
|
||||
// 代码逻辑说明: 【QQYUN-9422】短信模板管理,阿里云---
|
||||
String templateCode = dySmsEnum.getTemplateCode();
|
||||
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
|
||||
Map<String, String> smsTemplate = baseConfig.getTemplateCode();
|
||||
if(smsTemplate.containsKey(templateCode) && StringUtils.isNotEmpty(smsTemplate.get(templateCode))){
|
||||
templateCode = smsTemplate.get(templateCode);
|
||||
logger.info("yml中读取短信code{}",templateCode);
|
||||
}
|
||||
}
|
||||
//签名名称
|
||||
String signName = dySmsEnum.getSignName();
|
||||
if(baseConfig != null && StringUtils.isNotEmpty(baseConfig.getSignature())){
|
||||
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
|
||||
signName = baseConfig.getSignature();
|
||||
}
|
||||
|
||||
//组装请求对象-具体描述见控制台-文档部分内容
|
||||
SendSmsRequest request = new SendSmsRequest();
|
||||
//必填:待发送手机号
|
||||
request.setPhoneNumbers(phone);
|
||||
//必填:短信签名-可在短信控制台中找到
|
||||
request.setSignName(dySmsEnum.getSignName());
|
||||
request.setSignName(signName);
|
||||
//必填:短信模板-可在短信控制台中找到
|
||||
request.setTemplateCode(dySmsEnum.getTemplateCode());
|
||||
request.setTemplateCode(templateCode);
|
||||
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
|
||||
request.setTemplateParam(templateParamJson.toJSONString());
|
||||
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
@ -203,4 +211,150 @@ public class FileDownloadUtils {
|
||||
dir.mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下载单个文件到ZIP流
|
||||
* 核心功能:获取文件流,写入ZIP条目
|
||||
* @param fileUrl 文件URL(可以是HTTP URL或本地路径)
|
||||
* @param fileName ZIP内的文件名
|
||||
* @param zous ZIP输出流
|
||||
*/
|
||||
public static void downLoadSingleFile(String fileUrl, String fileName, String uploadUrl,ZipArchiveOutputStream zous) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
// 创建ZIP条目:每个文件在ZIP中都是一个独立条目
|
||||
ZipArchiveEntry entry = new ZipArchiveEntry(fileName);
|
||||
zous.putArchiveEntry(entry);
|
||||
|
||||
// 获取文件输入流:区分普通文件和快捷方式
|
||||
if (fileUrl.endsWith(".url")) {
|
||||
// 处理快捷方式:生成.url文件内容
|
||||
inputStream = FileDownloadUtils.createInternetShortcut(fileName, fileUrl, "");
|
||||
} else {
|
||||
// 普通文件下载:从URL或本地路径获取流
|
||||
inputStream = getDownInputStream(fileUrl,uploadUrl);
|
||||
}
|
||||
|
||||
if (inputStream != null) {
|
||||
// 将文件流写入ZIP
|
||||
IOUtils.copy(inputStream, zous);
|
||||
}
|
||||
// 关闭当前ZIP条目
|
||||
zous.closeArchiveEntry();
|
||||
} catch (IOException e) {
|
||||
log.error("文件下载失败: {}", e);
|
||||
} finally {
|
||||
// 确保输入流关闭
|
||||
IoUtil.close(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载文件输入流
|
||||
* 功能:根据URL类型(HTTP或本地)获取文件流
|
||||
* @param fileUrl 文件URL(支持HTTP和本地路径)
|
||||
* @return 文件输入流,失败返回null
|
||||
*/
|
||||
public static InputStream getDownInputStream(String fileUrl, String uploadUrl) {
|
||||
try {
|
||||
// 处理HTTP URL:通过网络下载
|
||||
if (oConvertUtils.isNotEmpty(fileUrl) && fileUrl.startsWith(CommonConstant.STR_HTTP)) {
|
||||
URL url = new URL(fileUrl);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(5000); // 连接超时5秒
|
||||
connection.setReadTimeout(30000); // 读取超时30秒
|
||||
return connection.getInputStream();
|
||||
} else {
|
||||
// 处理本地文件:直接读取文件系统
|
||||
String downloadFilePath = uploadUrl + File.separator + fileUrl;
|
||||
// 安全检查:防止下载危险文件类型
|
||||
SsrfFileTypeFilter.checkDownloadFileType(downloadFilePath);
|
||||
return new BufferedInputStream(new FileInputStream(downloadFilePath));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 异常时返回null,上层会处理空流情况
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
* 功能:从文件名中提取扩展名
|
||||
* @param fileName 文件名
|
||||
* @return 文件扩展名(不含点),如"txt"、"png"
|
||||
*/
|
||||
public static String getFileExtension(String fileName) {
|
||||
int dotIndex = fileName.lastIndexOf('.');
|
||||
return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建快捷方式(.url文件内容)
|
||||
* 功能:生成Internet快捷方式文件内容
|
||||
* @param name 快捷方式名称
|
||||
* @param url 目标URL地址
|
||||
* @param icon 图标路径(可选)
|
||||
* @return 包含.url文件内容的输入流
|
||||
*/
|
||||
public static InputStream createInternetShortcut(String name, String url, String icon) {
|
||||
StringWriter sw = new StringWriter();
|
||||
try {
|
||||
// 按照Windows快捷方式格式写入内容
|
||||
sw.write("[InternetShortcut]\n");
|
||||
sw.write("URL=" + url + "\n");
|
||||
if (oConvertUtils.isNotEmpty(icon)) {
|
||||
sw.write("IconFile=" + icon + "\n");
|
||||
}
|
||||
// 将字符串内容转换为输入流
|
||||
return new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8));
|
||||
} finally {
|
||||
IoUtil.close(sw);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 从URL中提取文件名
|
||||
* 功能:从HTTP URL或本地路径中提取纯文件名
|
||||
* @param fileUrl 文件URL
|
||||
* @return 文件名(不含路径)
|
||||
*/
|
||||
public static String getFileNameFromUrl(String fileUrl) {
|
||||
try {
|
||||
// 处理HTTP URL:从路径部分提取文件名
|
||||
if (fileUrl.startsWith(CommonConstant.STR_HTTP)) {
|
||||
URL url = new URL(fileUrl);
|
||||
String path = url.getPath();
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
// 处理本地文件路径:从文件路径提取文件名
|
||||
return fileUrl.substring(fileUrl.lastIndexOf(File.separator) + 1);
|
||||
} catch (Exception e) {
|
||||
// 如果解析失败,使用时间戳作为文件名
|
||||
return "file_" + System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 生成ZIP中的文件名
|
||||
* 功能:避免文件名冲突,为多个文件添加序号
|
||||
* @param fileUrl 文件URL(用于提取原始文件名)
|
||||
* @param index 文件序号(从0开始)
|
||||
* @param total 文件总数
|
||||
* @return 处理后的文件名(带序号)
|
||||
*/
|
||||
public static String generateFileName(String fileUrl, int index, int total) {
|
||||
// 从URL中提取原始文件名
|
||||
String originalFileName = getFileNameFromUrl(fileUrl);
|
||||
|
||||
// 如果只有一个文件,直接使用原始文件名
|
||||
if (total == 1) {
|
||||
return originalFileName;
|
||||
}
|
||||
|
||||
// 多个文件时,使用序号+原始文件名
|
||||
String extension = getFileExtension(originalFileName);
|
||||
String nameWithoutExtension = originalFileName.replace("." + extension, "");
|
||||
|
||||
return String.format("%s_%d.%s", nameWithoutExtension, index + 1, extension);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,9 +4,13 @@ import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.handler.IFillRuleHandler;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@ -42,6 +46,30 @@ public class FillRuleUtil {
|
||||
if (params == null) {
|
||||
params = new JSONObject();
|
||||
}
|
||||
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
|
||||
// 解析 params 中的变量
|
||||
// 优先级:queryString > 系统变量 > 默认值
|
||||
for (String key : params.keySet()) {
|
||||
// 1. 判断 queryString 中是否有该参数,如果有就优先取值
|
||||
//noinspection ConstantValue
|
||||
if (request != null) {
|
||||
String parameter = request.getParameter(key);
|
||||
if (oConvertUtils.isNotEmpty(parameter)) {
|
||||
params.put(key, parameter);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
String value = params.getString(key);
|
||||
// 2. 用于替换 系统变量的值 #{sys_user_code}
|
||||
if (value != null && value.contains(SymbolConstant.SYS_VAR_PREFIX)) {
|
||||
value = QueryGenerator.getSqlRuleValue(value);
|
||||
params.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (formData == null) {
|
||||
formData = new JSONObject();
|
||||
}
|
||||
|
||||
@ -38,14 +38,12 @@ public class HTMLUtils {
|
||||
* @return
|
||||
*/
|
||||
public static String parseMarkdown(String markdownContent) {
|
||||
//update-begin---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
|
||||
/*PegDownProcessor pdp = new PegDownProcessor();
|
||||
return pdp.markdownToHtml(markdownContent);*/
|
||||
Parser parser = Parser.builder().build();
|
||||
Node document = parser.parse(markdownContent);
|
||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
return renderer.render(document);
|
||||
//update-end---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -55,13 +55,11 @@ public class MinioUtil {
|
||||
*/
|
||||
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
|
||||
String fileUrl = "";
|
||||
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
||||
// 业务路径过滤,防止攻击
|
||||
bizPath = StrAttackFilter.filter(bizPath);
|
||||
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
||||
|
||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
||||
SsrfFileTypeFilter.checkUploadFileType(file);
|
||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
||||
// 文件安全校验,防止上传漏洞文件
|
||||
SsrfFileTypeFilter.checkUploadFileType(file, bizPath);
|
||||
|
||||
String newBucket = bucketName;
|
||||
if(oConvertUtils.isNotEmpty(customBucket)){
|
||||
@ -163,11 +161,10 @@ public class MinioUtil {
|
||||
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
|
||||
initMinio(minioUrl, minioName,minioPass);
|
||||
try{
|
||||
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
||||
// 代码逻辑说明: 获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
||||
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
|
||||
.bucket(bucketName)
|
||||
.expiry(expires).method(Method.GET).build();
|
||||
//update-begin---author:liusq Date:20220121 for:获取文件外链报错提示method不能为空,导致文件下载和预览失败----
|
||||
String url = minioClient.getPresignedObjectUrl(objectArgs);
|
||||
return URLDecoder.decode(url,"UTF-8");
|
||||
}catch (Exception e){
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import org.apache.commons.fileupload.FileItem;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* @date 2025-09-04
|
||||
* @author scott
|
||||
*
|
||||
* 升级springboot3 无法使用 CommonsMultipartFile
|
||||
* 自定义 MultipartFile 实现类,支持从 FileItem 构造
|
||||
*/
|
||||
public class MyCommonsMultipartFile implements MultipartFile {
|
||||
|
||||
private final byte[] fileContent;
|
||||
private final String fileName;
|
||||
private final String contentType;
|
||||
|
||||
// 新增构造方法,支持 FileItem 参数
|
||||
public MyCommonsMultipartFile(FileItem fileItem) throws IOException {
|
||||
this.fileName = fileItem.getName();
|
||||
this.contentType = fileItem.getContentType();
|
||||
try (InputStream inputStream = fileItem.getInputStream()) {
|
||||
this.fileContent = inputStream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
// 现有构造方法
|
||||
public MyCommonsMultipartFile(InputStream inputStream, String fileName, String contentType) throws IOException {
|
||||
this.fileName = fileName;
|
||||
this.contentType = contentType;
|
||||
this.fileContent = inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOriginalFilename() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return fileContent.length == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return fileContent.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() {
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(fileContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferTo(File dest) throws IOException {
|
||||
try (OutputStream os = new FileOutputStream(dest)) {
|
||||
os.write(fileContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,9 +99,8 @@ public class PasswordUtil {
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
|
||||
//update-begin-author:sccott date:20180815 for:中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
||||
// 代码逻辑说明: 中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
||||
encipheredData = cipher.doFinal(plaintext.getBytes("utf-8"));
|
||||
//update-end-author:sccott date:20180815 for:中文作为用户名时,加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return bytesToHexString(encipheredData);
|
||||
|
||||
@ -2,6 +2,7 @@ package org.jeecg.common.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
@ -16,6 +17,7 @@ import java.util.List;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Lazy(false)
|
||||
public class PmsUtil {
|
||||
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@ -56,12 +56,19 @@ public class RestUtil {
|
||||
private final static RestTemplate RT;
|
||||
|
||||
static {
|
||||
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(3000);
|
||||
requestFactory.setReadTimeout(3000);
|
||||
// 解决[issues/8859]online表单java增强失效------------
|
||||
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(30000);
|
||||
requestFactory.setReadTimeout(30000);
|
||||
RT = new RestTemplate(requestFactory);
|
||||
// 解决乱码问题
|
||||
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
||||
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
|
||||
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||
RT.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static RestTemplate getRestTemplate() {
|
||||
@ -216,11 +223,94 @@ public class RestUtil {
|
||||
if (variables != null && !variables.isEmpty()) {
|
||||
url += ("?" + asUrlVariables(variables));
|
||||
}
|
||||
// 解决[issues/8951]从jeecgboot 3.8.2 升级到 3.8.3 在线表单java增强功能报错------------
|
||||
// Content-Length 强制设置(解决可能出现的截断问题)
|
||||
if (StringUtils.isNotEmpty(body)) {
|
||||
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
|
||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||
headers.setContentLength(contentLength);
|
||||
log.info(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
}
|
||||
}
|
||||
// 发送请求
|
||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||
return RT.exchange(url, method, request, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求(支持自定义超时时间)
|
||||
*
|
||||
* @param url 请求地址
|
||||
* @param method 请求方式
|
||||
* @param headers 请求头 可空
|
||||
* @param variables 请求url参数 可空
|
||||
* @param params 请求body参数 可空
|
||||
* @param responseType 返回类型
|
||||
* @param timeout 超时时间(毫秒),如果为0或负数则使用默认超时
|
||||
* @return ResponseEntity<responseType>
|
||||
*/
|
||||
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
|
||||
JSONObject variables, Object params, Class<T> responseType, int timeout) {
|
||||
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
|
||||
|
||||
if (StringUtils.isEmpty(url)) {
|
||||
throw new RuntimeException("url 不能为空");
|
||||
}
|
||||
if (method == null) {
|
||||
throw new RuntimeException("method 不能为空");
|
||||
}
|
||||
if (headers == null) {
|
||||
headers = new HttpHeaders();
|
||||
}
|
||||
|
||||
// 创建自定义RestTemplate(如果需要设置超时)
|
||||
RestTemplate restTemplate = RT;
|
||||
if (timeout > 0) {
|
||||
// 代码逻辑说明: [issues/8859]online表单java增强失效------------
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
|
||||
requestFactory.setConnectTimeout(timeout);
|
||||
requestFactory.setReadTimeout(timeout);
|
||||
restTemplate = new RestTemplate(requestFactory);
|
||||
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8)
|
||||
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
|
||||
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
|
||||
restTemplate.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 请求体
|
||||
String body = "";
|
||||
if (params != null) {
|
||||
if (params instanceof JSONObject) {
|
||||
body = ((JSONObject) params).toJSONString();
|
||||
} else {
|
||||
body = params.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// 拼接 url 参数
|
||||
if (variables != null && !variables.isEmpty()) {
|
||||
url += ("?" + asUrlVariables(variables));
|
||||
}
|
||||
|
||||
// Content-Length 强制设置(解决可能出现的截断问题)
|
||||
if (StringUtils.isNotEmpty(body) && !headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
|
||||
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
|
||||
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
|
||||
if (current == null || !current.equals(String.valueOf(contentLength))) {
|
||||
headers.setContentLength(contentLength);
|
||||
log.info(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
HttpEntity<String> request = new HttpEntity<>(body, headers);
|
||||
return restTemplate.exchange(url, method, request, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JSON请求头
|
||||
*/
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* @date 2025-09-04
|
||||
* @author scott
|
||||
*
|
||||
* @Description: 支持shiro的API,获取当前登录人方法的线程池
|
||||
*/
|
||||
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
|
||||
|
||||
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
|
||||
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
SecurityManager securityManager = SecurityUtils.getSecurityManager();
|
||||
super.execute(() -> {
|
||||
try {
|
||||
ThreadContext.bind(securityManager);
|
||||
ThreadContext.bind(subject);
|
||||
command.run();
|
||||
} finally {
|
||||
ThreadContext.unbindSubject();
|
||||
ThreadContext.unbindSecurityManager();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import org.jeecg.common.constant.ServiceNameConstants;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@ -16,6 +17,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
* @Description: spring上下文工具类
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
@Lazy(false)
|
||||
@Component
|
||||
public class SpringContextUtils implements ApplicationContextAware {
|
||||
|
||||
|
||||
@ -16,7 +16,13 @@ import java.util.regex.Pattern;
|
||||
* @author zhoujf
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlInjectionUtil {
|
||||
public class SqlInjectionUtil {
|
||||
|
||||
/**
|
||||
* sql注入黑名单数据库名
|
||||
*/
|
||||
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
|
||||
|
||||
/**
|
||||
* 默认—sql注入关键词
|
||||
*/
|
||||
@ -167,7 +173,28 @@ public class SqlInjectionUtil {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断是否存在SQL注入关键词字符串
|
||||
*
|
||||
* @param keyword
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("AlibabaUndefineMagicConstant")
|
||||
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
|
||||
// 需要匹配的,sql注入关键词
|
||||
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
|
||||
for (String matchingText : matchingTexts) {
|
||||
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
|
||||
for (String checkText : checkTexts) {
|
||||
if (sql.contains(checkText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* sql注入过滤处理,遇到注入关键字抛异常
|
||||
*
|
||||
@ -208,6 +235,14 @@ public class SqlInjectionUtil {
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
|
||||
for (String xssTableStr : xssTableArr) {
|
||||
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
|
||||
// 三、SQL注入检测存在绕过风险 (正则校验)
|
||||
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
|
||||
@ -244,6 +279,14 @@ public class SqlInjectionUtil {
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
|
||||
for (String xssTableStr : xssTableArr) {
|
||||
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
|
||||
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
|
||||
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
|
||||
}
|
||||
}
|
||||
|
||||
// 三、SQL注入检测存在绕过风险 (正则校验)
|
||||
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
|
||||
@ -292,13 +335,12 @@ public class SqlInjectionUtil {
|
||||
return table;
|
||||
}
|
||||
|
||||
//update-begin---author:scott ---date:2024-05-28 for:表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
||||
// 代码逻辑说明: 表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
||||
int index = table.toLowerCase().indexOf(" where ");
|
||||
if (index != -1) {
|
||||
table = table.substring(0, index);
|
||||
log.info("截掉where之后的新表名:" + table);
|
||||
}
|
||||
//update-end---author:scott ---date::2024-05-28 for:表单设计器列表翻译存在表名带条件,导致翻译出问题----
|
||||
|
||||
table = table.trim();
|
||||
/**
|
||||
|
||||
@ -0,0 +1,184 @@
|
||||
package org.jeecg.common.util;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.enums.DySmsEnum;
|
||||
import org.jeecg.config.JeecgSmsTemplateConfig;
|
||||
import org.jeecg.config.tencent.JeecgTencent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 腾讯发送短信
|
||||
* @author: wangshuai
|
||||
* @date: 2025/11/4 19:27
|
||||
*/
|
||||
public class TencentSms {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(TencentSms.class);
|
||||
|
||||
/**
|
||||
* 发送腾讯短信
|
||||
*
|
||||
* @param phone
|
||||
* @param templateParamJson
|
||||
* @param tencent
|
||||
* @param dySmsEnum
|
||||
* @return
|
||||
*/
|
||||
public static boolean sendTencentSms(String phone, JSONObject templateParamJson, JeecgTencent tencent, DySmsEnum dySmsEnum) {
|
||||
//获取客户端链接
|
||||
SmsClient client = getSmsClient(tencent);
|
||||
//构建腾讯云短信发送请求
|
||||
SendSmsRequest req = buildSendSmsRequest(phone, templateParamJson, dySmsEnum, tencent);
|
||||
try {
|
||||
//发送短信
|
||||
SendSmsResponse resp = client.SendSms(req);
|
||||
// 处理响应
|
||||
SendStatus[] statusSet = resp.getSendStatusSet();
|
||||
if (statusSet != null && statusSet.length > 0) {
|
||||
SendStatus status = statusSet[0];
|
||||
if ("Ok".equals(status.getCode())) {
|
||||
logger.info("短信发送成功,手机号:{}", phone);
|
||||
return true;
|
||||
} else {
|
||||
logger.error("短信发送失败,手机号:{},错误码:{},错误信息:{}",
|
||||
phone, status.getCode(), status.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (TencentCloudSDKException e) {
|
||||
logger.error("短信发送失败{}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取sms客户端
|
||||
*
|
||||
* @param tencent 腾讯云配置
|
||||
* @return SmsClient对象
|
||||
*/
|
||||
private static SmsClient getSmsClient(JeecgTencent tencent) {
|
||||
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
|
||||
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
//指定接入地域域名*/
|
||||
httpProfile.setEndpoint(tencent.getEndpoint());
|
||||
//实例化一个客户端配置对象
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
//实例化要请求产品的client对象,第二个参数是地域信息
|
||||
return new SmsClient(cred, tencent.getRegion(), clientProfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建腾讯云短信发送请求
|
||||
*
|
||||
* @param phone 手机号码
|
||||
* @param templateParamJson 模板参数JSON对象
|
||||
* @param dySmsEnum 短信枚举配置
|
||||
* @param tencent 腾讯云配置
|
||||
* @return 构建好的SendSmsRequest对象
|
||||
*/
|
||||
private static SendSmsRequest buildSendSmsRequest(
|
||||
String phone,
|
||||
JSONObject templateParamJson,
|
||||
DySmsEnum dySmsEnum,
|
||||
JeecgTencent tencent) {
|
||||
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
|
||||
// 1. 设置短信应用ID
|
||||
String sdkAppId = tencent.getSdkAppId();
|
||||
req.setSmsSdkAppId(sdkAppId);
|
||||
// 2. 设置短信签名
|
||||
String signName = getSmsSignName(dySmsEnum);
|
||||
req.setSignName(signName);
|
||||
// 3. 设置模板ID
|
||||
String templateId = getSmsTemplateId(dySmsEnum);
|
||||
req.setTemplateId(templateId);
|
||||
// 4. 设置模板参数
|
||||
String[] templateParams = extractTemplateParams(templateParamJson);
|
||||
req.setTemplateParamSet(templateParams);
|
||||
// 5. 设置手机号码
|
||||
String[] phoneNumberSet = { phone };
|
||||
req.setPhoneNumberSet(phoneNumberSet);
|
||||
|
||||
logger.debug("构建短信请求完成 - 应用ID: {}, 签名: {}, 模板ID: {}, 手机号: {}",
|
||||
sdkAppId, signName, templateId, phone);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信签名名称
|
||||
*
|
||||
* @param dySmsEnum 腾讯云对象
|
||||
*/
|
||||
private static String getSmsSignName(DySmsEnum dySmsEnum) {
|
||||
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||
String signName = dySmsEnum.getSignName();
|
||||
|
||||
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
|
||||
logger.debug("yml中读取签名名称: {}", baseConfig.getSignature());
|
||||
signName = baseConfig.getSignature();
|
||||
}
|
||||
|
||||
return signName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信模板ID
|
||||
*
|
||||
* @param dySmsEnum 腾讯云对象
|
||||
*/
|
||||
private static String getSmsTemplateId(DySmsEnum dySmsEnum) {
|
||||
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
|
||||
String templateCode = dySmsEnum.getTemplateCode();
|
||||
|
||||
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
|
||||
Map<String, String> smsTemplate = baseConfig.getTemplateCode();
|
||||
if (smsTemplate.containsKey(templateCode) &&
|
||||
StringUtils.isNotEmpty(smsTemplate.get(templateCode))) {
|
||||
templateCode = smsTemplate.get(templateCode);
|
||||
logger.debug("yml中读取短信模板ID: {}", templateCode);
|
||||
}
|
||||
}
|
||||
return templateCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSONObject中提取模板参数(按原始顺序)
|
||||
*
|
||||
* @param templateParamJson 模板参数
|
||||
*/
|
||||
private static String[] extractTemplateParams(JSONObject templateParamJson) {
|
||||
if (templateParamJson == null || templateParamJson.isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
List<String> params = new ArrayList<>();
|
||||
for (String key : templateParamJson.keySet()) {
|
||||
Object value = templateParamJson.get(key);
|
||||
if (value != null) {
|
||||
params.add(value.toString());
|
||||
} else {
|
||||
// 处理null值
|
||||
params.add("");
|
||||
}
|
||||
}
|
||||
logger.debug("提取模板参数: {}", params);
|
||||
return params.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
@ -65,6 +65,10 @@ public class TokenUtils {
|
||||
if (tenantId == null) {
|
||||
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
|
||||
}
|
||||
|
||||
if (oConvertUtils.isNotEmpty(tenantId) && "undefined".equals(tenantId)) {
|
||||
return null;
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
@ -117,7 +121,9 @@ public class TokenUtils {
|
||||
}
|
||||
// 校验token是否超时失效 & 或者账号密码是否错误
|
||||
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
|
||||
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
|
||||
// 用户登录Token过期提示信息
|
||||
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
|
||||
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -135,10 +141,15 @@ public class TokenUtils {
|
||||
if (oConvertUtils.isNotEmpty(cacheToken)) {
|
||||
// 校验token有效性
|
||||
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
|
||||
String newAuthorization = JwtUtil.sign(userName, passWord);
|
||||
// 设置Toekn缓存有效时间
|
||||
// 从token中解析客户端类型,保持续期时使用相同的客户端类型
|
||||
String clientType = JwtUtil.getClientType(token);
|
||||
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
|
||||
// 根据客户端类型设置对应的缓存有效时间
|
||||
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
|
||||
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
|
||||
: JwtUtil.EXPIRE_TIME * 2 / 1000;
|
||||
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
|
||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
|
||||
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -68,6 +68,13 @@ public class DbTypeUtils {
|
||||
return dbTypeIf(dbType, DbType.ORACLE, DbType.ORACLE_12C, DbType.DM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是达梦
|
||||
*/
|
||||
public static boolean dbTypeIsDm(DbType dbType) {
|
||||
return dbTypeIf(dbType, DbType.DM);
|
||||
}
|
||||
|
||||
public static boolean dbTypeIsSqlServer(DbType dbType) {
|
||||
return dbTypeIf(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
|
||||
}
|
||||
|
||||
@ -84,6 +84,10 @@ public class DynamicDBUtil {
|
||||
} else {
|
||||
DruidDataSource dataSource = getJdbcDataSource(dbSource);
|
||||
if(dataSource!=null && dataSource.isEnable()){
|
||||
|
||||
// 【TV360X-2060】设置超时时间 6秒
|
||||
dataSource.setMaxWait(6000);
|
||||
|
||||
DataSourceCachePool.putCacheBasicDataSource(dbKey, dataSource);
|
||||
}else{
|
||||
throw new JeecgBootException("动态数据源连接失败,dbKey:"+dbKey);
|
||||
@ -106,9 +110,10 @@ public class DynamicDBUtil {
|
||||
dataSource.getConnection().commit();
|
||||
dataSource.getConnection().close();
|
||||
dataSource.close();
|
||||
DataSourceCachePool.removeCache(dbKey);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
log.warn(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -54,11 +54,10 @@ public class FreemarkerParseFactory {
|
||||
//classic_compatible设置,解决报空指针错误
|
||||
SQL_CONFIG.setClassicCompatible(true);
|
||||
|
||||
//update-begin-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||
// 解决freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||
//https://ackcent.com/in-depth-freemarker-template-injection/
|
||||
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||
//update-end-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,13 +72,12 @@ public class FreemarkerParseFactory {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//update-begin--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
||||
// 代码逻辑说明: 解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误-----
|
||||
if (e instanceof ParseException) {
|
||||
log.error(e.getMessage(), e.fillInStackTrace());
|
||||
throw new Exception(e);
|
||||
}
|
||||
log.debug("----isExistTemplate----" + e.toString());
|
||||
//update-end--Author:scott Date:20180320 for:解决问题 - 错误提示sql文件不存在,实际问题是sql freemarker用法错误------
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -1,124 +1,107 @@
|
||||
package org.jeecg.common.util.encryption;
|
||||
|
||||
import org.apache.shiro.codec.Base64;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.lang.codec.Base64;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @Description: AES 加密
|
||||
* @author: jeecg-boot
|
||||
* @date: 2022/3/30 11:48
|
||||
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
|
||||
*/
|
||||
@Slf4j
|
||||
public class AesEncryptUtil {
|
||||
|
||||
/**
|
||||
* 使用AES-128-CBC加密模式 key和iv可以相同
|
||||
*/
|
||||
private static String KEY = EncryptedString.key;
|
||||
private static String IV = EncryptedString.iv;
|
||||
private static final String KEY = EncryptedString.key;
|
||||
private static final String IV = EncryptedString.iv;
|
||||
|
||||
/**
|
||||
* 加密方法
|
||||
* @param data 要加密的数据
|
||||
* @param key 加密key
|
||||
* @param iv 加密iv
|
||||
* @return 加密的结果
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String encrypt(String data, String key, String iv) throws Exception {
|
||||
try {
|
||||
/* -------- 新版:CBC + PKCS5Padding (与前端 CryptoJS Pkcs7 兼容) -------- */
|
||||
private static String decryptPkcs5(String cipherBase64) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
|
||||
return new String(plain, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
//"算法/模式/补码方式"NoPadding PkcsPadding
|
||||
/* -------- 旧版:CBC + NoPadding (手工补 0) -------- */
|
||||
private static String decryptLegacyNoPadding(String cipherBase64) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
|
||||
byte[] data = cipher.doFinal(Base64.decode(cipherBase64));
|
||||
return new String(data, StandardCharsets.UTF_8)
|
||||
.replace("\u0000",""); // 旧填充 0
|
||||
}
|
||||
|
||||
/* -------- 兼容入口:登录使用 -------- */
|
||||
public static String resolvePassword(String input){
|
||||
if(oConvertUtils.isEmpty(input)){
|
||||
return input;
|
||||
}
|
||||
// 1. 先尝试新版
|
||||
try{
|
||||
String p = decryptPkcs5(input);
|
||||
return clean(p);
|
||||
}catch(Exception ignore){
|
||||
//log.debug("【AES解密】Password not AES PKCS5 cipher, try legacy.");
|
||||
}
|
||||
|
||||
// 2. 回退旧版
|
||||
try{
|
||||
String legacy = decryptLegacyNoPadding(input);
|
||||
return clean(legacy);
|
||||
}catch(Exception e){
|
||||
log.debug("【AES解密】Password not AES cipher, raw used.");
|
||||
}
|
||||
|
||||
// 3. 视为明文
|
||||
return input;
|
||||
}
|
||||
|
||||
/* -------- 可选:统一清理尾部不可见控制字符 -------- */
|
||||
private static String clean(String s){
|
||||
if(s==null) return null;
|
||||
// 去除结尾控制符/空白(不影响中间合法空格)
|
||||
return s.replaceAll("[\\p{Cntrl}]+","").trim();
|
||||
}
|
||||
|
||||
/* -------- 若仍需要旧接口,可保留 (不建议再用于新前端) -------- */
|
||||
@Deprecated
|
||||
public static String desEncrypt(String data) throws Exception {
|
||||
return decryptLegacyNoPadding(data);
|
||||
}
|
||||
|
||||
/* 加密(若前端不再使用,可忽略;保留旧实现避免影响历史) */
|
||||
@Deprecated
|
||||
public static String encrypt(String data) throws Exception {
|
||||
try{
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
int blockSize = cipher.getBlockSize();
|
||||
|
||||
byte[] dataBytes = data.getBytes();
|
||||
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
|
||||
int plaintextLength = dataBytes.length;
|
||||
if (plaintextLength % blockSize != 0) {
|
||||
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
|
||||
plaintextLength += (blockSize - (plaintextLength % blockSize));
|
||||
}
|
||||
|
||||
byte[] plaintext = new byte[plaintextLength];
|
||||
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
|
||||
|
||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
||||
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
|
||||
|
||||
SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
|
||||
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
|
||||
byte[] encrypted = cipher.doFinal(plaintext);
|
||||
|
||||
return Base64.encodeToString(encrypted);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}catch(Exception e){
|
||||
throw new IllegalStateException("legacy encrypt error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密方法
|
||||
* @param data 要解密的数据
|
||||
* @param key 解密key
|
||||
* @param iv 解密iv
|
||||
* @return 解密的结果
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String desEncrypt(String data, String key, String iv) throws Exception {
|
||||
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
||||
byte[] encrypted1 = Base64.decode(data);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
|
||||
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
|
||||
|
||||
byte[] original = cipher.doFinal(encrypted1);
|
||||
String originalString = new String(original);
|
||||
//加密解码后的字符串会出现\u0000
|
||||
return originalString.replaceAll("\\u0000", "");
|
||||
//update-end-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的key和iv加密
|
||||
* @param data
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String encrypt(String data) throws Exception {
|
||||
return encrypt(data, KEY, IV);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认的key和iv解密
|
||||
* @param data
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public static String desEncrypt(String data) throws Exception {
|
||||
return desEncrypt(data, KEY, IV);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// /**
|
||||
// * 测试
|
||||
// */
|
||||
// public static void main(String args[]) throws Exception {
|
||||
// String test1 = "sa";
|
||||
// String test =new String(test1.getBytes(),"UTF-8");
|
||||
// String data = null;
|
||||
// String key = KEY;
|
||||
// String iv = IV;
|
||||
// // /g2wzfqvMOeazgtsUVbq1kmJawROa6mcRAzwG1/GeJ4=
|
||||
// data = encrypt(test, key, iv);
|
||||
// System.out.println("数据:"+test);
|
||||
// System.out.println("加密:"+data);
|
||||
// String jiemi =desEncrypt(data, key, iv).trim();
|
||||
// System.out.println("解密:"+jiemi);
|
||||
// public static void main(String[] args) throws Exception {
|
||||
// // 前端 CBC/Pkcs7 密文测试
|
||||
// String frontCipher = encrypt("sa"); // 仅验证管道是否可用(旧方式)
|
||||
// System.out.println(resolvePassword(frontCipher));
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ package org.jeecg.common.util.filter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -34,6 +35,7 @@ public class SsrfFileTypeFilter {
|
||||
FILE_TYPE_WHITE_LIST.add("bmp");
|
||||
FILE_TYPE_WHITE_LIST.add("svg");
|
||||
FILE_TYPE_WHITE_LIST.add("ico");
|
||||
FILE_TYPE_WHITE_LIST.add("heic");
|
||||
|
||||
//文本文件
|
||||
FILE_TYPE_WHITE_LIST.add("txt");
|
||||
@ -42,6 +44,7 @@ public class SsrfFileTypeFilter {
|
||||
FILE_TYPE_WHITE_LIST.add("pdf");
|
||||
FILE_TYPE_WHITE_LIST.add("csv");
|
||||
// FILE_TYPE_WHITE_LIST.add("xml");
|
||||
FILE_TYPE_WHITE_LIST.add("md");
|
||||
|
||||
//音视频文件
|
||||
FILE_TYPE_WHITE_LIST.add("mp4");
|
||||
@ -65,6 +68,10 @@ public class SsrfFileTypeFilter {
|
||||
FILE_TYPE_WHITE_LIST.add("apk");
|
||||
FILE_TYPE_WHITE_LIST.add("wgt");
|
||||
|
||||
//幻灯片文件后缀
|
||||
FILE_TYPE_WHITE_LIST.add("ppt");
|
||||
FILE_TYPE_WHITE_LIST.add("pptx");
|
||||
|
||||
//设置禁止文件的头部标记
|
||||
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
|
||||
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");
|
||||
@ -143,29 +150,38 @@ public class SsrfFileTypeFilter {
|
||||
public static void checkDownloadFileType(String filePath) throws IOException {
|
||||
//文件后缀
|
||||
String suffix = getFileTypeBySuffix(filePath);
|
||||
log.info("suffix:{}", suffix);
|
||||
log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
|
||||
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
||||
//是否允许下载的文件
|
||||
if (!isAllowExtension) {
|
||||
throw new IOException("下载失败,存在非法文件类型:" + suffix);
|
||||
throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件类型过滤
|
||||
*
|
||||
* @param file
|
||||
*/
|
||||
public static void checkUploadFileType(MultipartFile file) throws Exception {
|
||||
//获取文件真是后缀
|
||||
String suffix = getFileType(file);
|
||||
|
||||
log.info("suffix:{}", suffix);
|
||||
checkUploadFileType(file, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件类型过滤
|
||||
*
|
||||
* @param file
|
||||
*/
|
||||
public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception {
|
||||
//1. 路径安全校验
|
||||
validatePathSecurity(customPath);
|
||||
//2. 校验文件后缀和头
|
||||
String suffix = getFileType(file, customPath);
|
||||
log.info("【文件上传校验】文件后缀 suffix: {},customPath:{}", suffix, customPath);
|
||||
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
|
||||
//是否允许下载的文件
|
||||
if (!isAllowExtension) {
|
||||
throw new Exception("上传失败,存在非法文件类型:" + suffix);
|
||||
throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,8 +193,8 @@ public class SsrfFileTypeFilter {
|
||||
* @throws Exception
|
||||
*/
|
||||
|
||||
private static String getFileType(MultipartFile file) throws Exception {
|
||||
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||
private static String getFileType(MultipartFile file, String customPath) throws Exception {
|
||||
// 代码逻辑说明: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||
String fileExtendName = null;
|
||||
InputStream is = null;
|
||||
try {
|
||||
@ -197,7 +213,7 @@ public class SsrfFileTypeFilter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
log.info("-----获取到的指定文件类型------"+fileExtendName);
|
||||
log.debug("-----获取到的指定文件类型------"+fileExtendName);
|
||||
// 如果不是上述类型,则判断扩展名
|
||||
if (StringUtils.isBlank(fileExtendName)) {
|
||||
String fileName = file.getOriginalFilename();
|
||||
@ -208,7 +224,6 @@ public class SsrfFileTypeFilter {
|
||||
// 如果有扩展名,则返回扩展名
|
||||
return getFileTypeBySuffix(fileName);
|
||||
}
|
||||
log.info("-----最終的文件类型------"+fileExtendName);
|
||||
is.close();
|
||||
return fileExtendName;
|
||||
} catch (Exception e) {
|
||||
@ -219,7 +234,6 @@ public class SsrfFileTypeFilter {
|
||||
is.close();
|
||||
}
|
||||
}
|
||||
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用,注释掉此方法tomcat就能自动清理掉临时文件
|
||||
}
|
||||
|
||||
/**
|
||||
@ -243,4 +257,34 @@ public class SsrfFileTypeFilter {
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径安全校验
|
||||
*/
|
||||
private static void validatePathSecurity(String customPath) throws JeecgBootException {
|
||||
if (customPath == null || customPath.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 统一分隔符为 /
|
||||
String normalized = customPath.replace("\\", "/");
|
||||
|
||||
// 1. 防止路径遍历攻击
|
||||
if (normalized.contains("..") || normalized.contains("~")) {
|
||||
throw new JeecgBootException("上传业务路径包含非法字符!");
|
||||
}
|
||||
|
||||
// 2. 限制路径深度
|
||||
int depth = normalized.split("/").length;
|
||||
if (depth > 5) {
|
||||
throw new JeecgBootException("上传业务路径深度超出限制!");
|
||||
}
|
||||
|
||||
// 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠)
|
||||
if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) {
|
||||
throw new JeecgBootException("上传业务路径包含非法字符!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -7,12 +7,18 @@ import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
@ -21,6 +27,7 @@ import java.sql.Date;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -463,7 +470,7 @@ public class oConvertUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] childs = (String[]) childArray.toArray();
|
||||
List<String> childs = childArray.toJavaList(String.class);
|
||||
for (String v : childs) {
|
||||
if (!isIn(v, all)) {
|
||||
return false;
|
||||
@ -472,6 +479,23 @@ public class oConvertUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为JSON格式
|
||||
* @param str
|
||||
* @return
|
||||
*/
|
||||
public static boolean isJson(String str) {
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
com.alibaba.fastjson.JSON.parse(str);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Map对象
|
||||
*/
|
||||
@ -544,10 +568,8 @@ public class oConvertUtils {
|
||||
return "";
|
||||
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
|
||||
// 不含下划线,仅将首字母小写
|
||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||
// 代码逻辑说明: TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
|
||||
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||
}
|
||||
// 用下划线将原始字符串分割
|
||||
String[] camels = name.split("_");
|
||||
@ -592,7 +614,6 @@ public class oConvertUtils {
|
||||
return result.substring(0, result.length() - 1);
|
||||
}
|
||||
|
||||
//update-begin--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||
/**
|
||||
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
|
||||
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
|
||||
@ -625,7 +646,6 @@ public class oConvertUtils {
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
//update-end--Author:zhoujf Date:20180503 for:TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
|
||||
|
||||
/**
|
||||
* 将驼峰命名转化成下划线
|
||||
@ -963,17 +983,18 @@ public class oConvertUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 判断 list1中的元素是否在list2中出现
|
||||
* 判断 sourceList中的元素是否在targetList中出现
|
||||
*
|
||||
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
|
||||
* @param list1
|
||||
* @param list2
|
||||
* @return
|
||||
* @param sourceList 源列表,要检查的元素列表
|
||||
* @param targetList 目标列表,用于匹配的列表
|
||||
* @return 如果sourceList中有任何元素在targetList中存在则返回true,否则返回false
|
||||
*/
|
||||
public static boolean isInList(List<String> list1, List<String> list2){
|
||||
for(String str1: list1){
|
||||
public static boolean isInList(List<String> sourceList, List<String> targetList){
|
||||
for(String sourceItem: sourceList){
|
||||
boolean flag = false;
|
||||
for(String str2: list2){
|
||||
if(str1.equals(str2)){
|
||||
for(String targetItem: targetList){
|
||||
if(sourceItem.equals(targetItem)){
|
||||
flag = true;
|
||||
break;
|
||||
}
|
||||
@ -985,6 +1006,35 @@ public class oConvertUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 sourceList中的所有元素是否都在targetList中存在
|
||||
* @param sourceList 源列表,要检查的元素列表
|
||||
* @param targetList 目标列表,用于匹配的列表
|
||||
* @return 如果sourceList中的所有元素都在targetList中存在则返回true,否则返回false
|
||||
*/
|
||||
public static boolean isAllInList(List<String> sourceList, List<String> targetList){
|
||||
if(sourceList == null || sourceList.isEmpty()){
|
||||
return true; // 空列表视为所有元素都存在
|
||||
}
|
||||
if(targetList == null || targetList.isEmpty()){
|
||||
return false; // 目标列表为空,源列表非空时返回false
|
||||
}
|
||||
|
||||
for(String sourceItem: sourceList){
|
||||
boolean found = false;
|
||||
for(String targetItem: targetList){
|
||||
if(sourceItem.equals(targetItem)){
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found){
|
||||
return false; // 有任何一个元素不在目标列表中,返回false
|
||||
}
|
||||
}
|
||||
return true; // 所有元素都找到了
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文件大小转成MB
|
||||
* @param uploadCount
|
||||
@ -1028,5 +1078,158 @@ public class oConvertUtils {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象是否为空 <br/>
|
||||
* 支持各种类型的对象
|
||||
* for for [QQYUN-10990]AIRAG
|
||||
* @param obj
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/13 18:34
|
||||
*/
|
||||
public static boolean isObjectEmpty(Object obj) {
|
||||
if (null == obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj instanceof CharSequence) {
|
||||
return isEmpty(obj);
|
||||
} else if (obj instanceof Map) {
|
||||
return ((Map<?, ?>) obj).isEmpty();
|
||||
} else if (obj instanceof Iterable) {
|
||||
return isObjectEmpty(((Iterable<?>) obj).iterator());
|
||||
} else if (obj instanceof Iterator) {
|
||||
return !((Iterator<?>) obj).hasNext();
|
||||
} else if (isArray(obj)) {
|
||||
return 0 == Array.getLength(obj);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* iterator 是否为空
|
||||
* for for [QQYUN-10990]AIRAG
|
||||
* @param iterator Iterator对象
|
||||
* @return 是否为空
|
||||
*/
|
||||
public static boolean isEmptyIterator(Iterator<?> iterator) {
|
||||
return null == iterator || false == iterator.hasNext();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断对象是否不为空
|
||||
* for for [QQYUN-10990]AIRAG
|
||||
* @param object
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/13 18:35
|
||||
*/
|
||||
public static boolean isObjectNotEmpty(Object object) {
|
||||
return !isObjectEmpty(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果src大于des返回true
|
||||
* for [QQYUN-10990]AIRAG
|
||||
* @param src
|
||||
* @param des
|
||||
* @return
|
||||
* @author: chenrui
|
||||
* @date: 2018/9/19 15:30
|
||||
*/
|
||||
public static boolean isGt(Number src, Number des) {
|
||||
if (null == src || null == des) {
|
||||
throw new IllegalArgumentException("参数不能为空");
|
||||
}
|
||||
if (src.doubleValue() > des.doubleValue()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果src大于等于des返回true
|
||||
* for [QQYUN-10990]AIRAG
|
||||
* @param src
|
||||
* @param des
|
||||
* @return
|
||||
* @author: chenrui
|
||||
* @date: 2018/9/19 15:30
|
||||
*/
|
||||
public static boolean isGe(Number src, Number des) {
|
||||
if (null == src || null == des) {
|
||||
throw new IllegalArgumentException("参数不能为空");
|
||||
}
|
||||
if (src.doubleValue() < des.doubleValue()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断是否存在
|
||||
* for [QQYUN-10990]AIRAG
|
||||
* @param obj
|
||||
* @param objs
|
||||
* @param <T>
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2020/9/12 15:50
|
||||
*/
|
||||
public static <T> boolean isIn(T obj, T... objs) {
|
||||
if (isEmpty(objs)) {
|
||||
return false;
|
||||
}
|
||||
for (T obj1 : objs) {
|
||||
if (isEqual(obj, obj1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断租户ID是否有效
|
||||
* @param tenantId
|
||||
* @return
|
||||
*/
|
||||
public static boolean isEffectiveTenant(String tenantId) {
|
||||
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制源对象的非空属性到目标对象(同名属性)
|
||||
*
|
||||
* @param source 源对象(页面)
|
||||
* @param target 目标对象(数据库实体)
|
||||
*/
|
||||
public static void copyNonNullFields(Object source, Object target) {
|
||||
if (source == null || target == null) {
|
||||
return;
|
||||
}
|
||||
// 获取源对象的非空属性名数组
|
||||
String[] nullPropertyNames = getNullPropertyNames(source);
|
||||
// 复制:忽略源对象的空属性,仅覆盖目标对象的对应非空属性
|
||||
BeanUtils.copyProperties(source, target, nullPropertyNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取源对象中值为 null 的属性名数组
|
||||
*
|
||||
* @param source
|
||||
*/
|
||||
private static String[] getNullPropertyNames(Object source) {
|
||||
BeanWrapper beanWrapper = new BeanWrapperImpl(source);
|
||||
//获取类的属性
|
||||
PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
|
||||
// 过滤出值为 null 的属性名
|
||||
return Stream.of(propertyDescriptors)
|
||||
.map(PropertyDescriptor::getName)
|
||||
.filter(name -> beanWrapper.getPropertyValue(name) == null)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -97,9 +97,8 @@ public class OssBootUtil {
|
||||
* @return oss 中的相对文件路径
|
||||
*/
|
||||
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
|
||||
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
|
||||
// 文件安全校验,防止上传漏洞文件
|
||||
SsrfFileTypeFilter.checkUploadFileType(file);
|
||||
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
|
||||
|
||||
String filePath = null;
|
||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||
@ -125,9 +124,8 @@ public class OssBootUtil {
|
||||
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
|
||||
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
|
||||
}
|
||||
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
||||
// 代码逻辑说明: 过滤上传文件夹名特殊字符,防止攻击
|
||||
fileDir=StrAttackFilter.filter(fileDir);
|
||||
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
|
||||
fileUrl = fileUrl.append(fileDir + fileName);
|
||||
|
||||
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
|
||||
@ -264,9 +262,8 @@ public class OssBootUtil {
|
||||
newBucket = bucket;
|
||||
}
|
||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||
// 代码逻辑说明: 替换objectName前缀,防止key不一致导致获取不到文件----
|
||||
objectName = OssBootUtil.replacePrefix(objectName,bucket);
|
||||
//update-end---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
|
||||
inputStream = new BufferedInputStream(ossObject.getObjectContent());
|
||||
}catch (Exception e){
|
||||
@ -294,9 +291,8 @@ public class OssBootUtil {
|
||||
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
|
||||
initOss(endPoint, accessKeyId, accessKeySecret);
|
||||
try{
|
||||
//update-begin---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||
// 代码逻辑说明: 替换objectName前缀,防止key不一致导致获取不到文件----
|
||||
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
|
||||
//update-end---author:liusq Date:20220120 for:替换objectName前缀,防止key不一致导致获取不到文件----
|
||||
if(ossClient.doesObjectExist(bucketName,objectName)){
|
||||
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
|
||||
//log.info("原始url : {}", url.toString());
|
||||
|
||||
@ -3,6 +3,8 @@ package org.jeecg.common.util.security;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import org.jeecg.common.util.SqlInjectionUtil;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
@ -61,11 +63,13 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
if(list==null){
|
||||
return true;
|
||||
}
|
||||
log.info(" 获取sql信息 :{} ", list.toString());
|
||||
log.debug(" 获取sql信息 :{} ", list.toString());
|
||||
boolean flag = checkTableAndFieldsName(list);
|
||||
if(flag == false){
|
||||
return false;
|
||||
}
|
||||
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
|
||||
|
||||
for (QueryTable table : list) {
|
||||
String name = table.getName();
|
||||
String fieldRule = ruleMap.get(name);
|
||||
@ -81,6 +85,16 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
}
|
||||
|
||||
}
|
||||
// 判断是否调用了黑名单数据库
|
||||
String dbName = table.getDbName();
|
||||
if (oConvertUtils.isNotEmpty(dbName)) {
|
||||
dbName = dbName.toLowerCase().trim();
|
||||
if (xssTableSet.contains(dbName)) {
|
||||
flag = false;
|
||||
log.warn("sql黑名单校验,数据库【" + dbName + "】禁止查询");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 返回黑名单校验结果(不合法直接抛出异常)
|
||||
@ -135,6 +149,8 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
* 查询的表的信息
|
||||
*/
|
||||
protected class QueryTable {
|
||||
//数据库名
|
||||
private String dbName;
|
||||
//表名
|
||||
private String name;
|
||||
//表的别名
|
||||
@ -158,6 +174,14 @@ public abstract class AbstractQueryBlackListHandler {
|
||||
this.fields.add(field);
|
||||
}
|
||||
|
||||
public String getDbName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
public void setDbName(String dbName) {
|
||||
this.dbName = dbName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
package org.jeecg.common.util.sqlInjection;
|
||||
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
|
||||
import net.sf.jsqlparser.parser.SimpleNode;
|
||||
import net.sf.jsqlparser.statement.select.UnionOp;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
|
||||
/**
|
||||
* 基于抽象语法树(AST)的注入攻击分析实现
|
||||
*
|
||||
* @author guyadong
|
||||
*/
|
||||
public class InjectionAstNodeVisitor extends CCJSqlParserDefaultVisitor {
|
||||
public InjectionAstNodeVisitor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理禁止联合查询
|
||||
*
|
||||
* @param node
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Object visit(SimpleNode node, Object data) {
|
||||
Object value = node.jjtGetValue();
|
||||
if (value instanceof UnionOp) {
|
||||
throw new JeecgSqlInjectionException("DISABLE UNION");
|
||||
}
|
||||
return super.visit(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,172 +0,0 @@
|
||||
package org.jeecg.common.util.sqlInjection;
|
||||
|
||||
|
||||
import net.sf.jsqlparser.expression.BinaryExpression;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.Function;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.statement.select.Join;
|
||||
import net.sf.jsqlparser.statement.select.OrderByElement;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.SelectItem;
|
||||
import net.sf.jsqlparser.statement.select.SubSelect;
|
||||
import net.sf.jsqlparser.statement.select.WithItem;
|
||||
import net.sf.jsqlparser.util.TablesNamesFinder;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import org.jeecg.common.util.sqlInjection.parse.ConstAnalyzer;
|
||||
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
|
||||
|
||||
/**
|
||||
* 基于SQL语法对象的SQL注入攻击分析实现
|
||||
*
|
||||
* @author guyadong
|
||||
*/
|
||||
public class InjectionSyntaxObjectAnalyzer extends TablesNamesFinder {
|
||||
/**
|
||||
* 危险函数名
|
||||
*/
|
||||
private static final String DANGROUS_FUNCTIONS = "(sleep|benchmark|extractvalue|updatexml|ST_LatFromGeoHash|ST_LongFromGeoHash|GTID_SUBSET|GTID_SUBTRACT|floor|ST_Pointfromgeohash"
|
||||
+ "|geometrycollection|multipoint|polygon|multipolygon|linestring|multilinestring)";
|
||||
|
||||
private static ThreadLocal<Boolean> disableSubselect = new ThreadLocal<Boolean>() {
|
||||
@Override
|
||||
protected Boolean initialValue() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
private ConstAnalyzer constAnalyzer = new ConstAnalyzer();
|
||||
|
||||
public InjectionSyntaxObjectAnalyzer() {
|
||||
super();
|
||||
init(true);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitBinaryExpression(BinaryExpression binaryExpression) {
|
||||
if (binaryExpression instanceof ComparisonOperator) {
|
||||
if (isConst(binaryExpression.getLeftExpression()) && isConst(binaryExpression.getRightExpression())) {
|
||||
/** 禁用恒等式 */
|
||||
throw new JeecgSqlInjectionException("DISABLE IDENTICAL EQUATION " + binaryExpression);
|
||||
}
|
||||
}
|
||||
super.visitBinaryExpression(binaryExpression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AndExpression andExpression) {
|
||||
super.visit(andExpression);
|
||||
checkConstExpress(andExpression.getLeftExpression());
|
||||
checkConstExpress(andExpression.getRightExpression());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OrExpression orExpression) {
|
||||
super.visit(orExpression);
|
||||
checkConstExpress(orExpression.getLeftExpression());
|
||||
checkConstExpress(orExpression.getRightExpression());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Function function) {
|
||||
if (function.getName().matches(DANGROUS_FUNCTIONS)) {
|
||||
/** 禁用危险函数 */
|
||||
throw new JeecgSqlInjectionException("DANGROUS FUNCTION: " + function.getName());
|
||||
}
|
||||
super.visit(function);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(WithItem withItem) {
|
||||
try {
|
||||
/** 允许 WITH 语句中的子查询 */
|
||||
disableSubselect.set(false);
|
||||
super.visit(withItem);
|
||||
} finally {
|
||||
disableSubselect.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SubSelect subSelect) {
|
||||
try {
|
||||
/** 允许语句中的子查询 */
|
||||
disableSubselect.set(false);
|
||||
super.visit(subSelect);
|
||||
} finally {
|
||||
disableSubselect.set(true);
|
||||
}
|
||||
// if (disableSubselect.get()) {
|
||||
// // 禁用子查询
|
||||
// throw new JeecgSqlInjectionException("DISABLE subselect " + subSelect);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Column tableColumn) {
|
||||
if (ParserSupport.isBoolean(tableColumn)) {
|
||||
throw new JeecgSqlInjectionException("DISABLE CONST BOOL " + tableColumn);
|
||||
}
|
||||
super.visit(tableColumn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(PlainSelect plainSelect) {
|
||||
if (plainSelect.getSelectItems() != null) {
|
||||
for (SelectItem item : plainSelect.getSelectItems()) {
|
||||
item.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (plainSelect.getFromItem() != null) {
|
||||
plainSelect.getFromItem().accept(this);
|
||||
}
|
||||
|
||||
if (plainSelect.getJoins() != null) {
|
||||
for (Join join : plainSelect.getJoins()) {
|
||||
join.getRightItem().accept(this);
|
||||
for (Expression e : join.getOnExpressions()) {
|
||||
e.accept(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (plainSelect.getWhere() != null) {
|
||||
plainSelect.getWhere().accept(this);
|
||||
checkConstExpress(plainSelect.getWhere());
|
||||
}
|
||||
|
||||
if (plainSelect.getHaving() != null) {
|
||||
plainSelect.getHaving().accept(this);
|
||||
}
|
||||
|
||||
if (plainSelect.getOracleHierarchical() != null) {
|
||||
plainSelect.getOracleHierarchical().accept(this);
|
||||
}
|
||||
if (plainSelect.getOrderByElements() != null) {
|
||||
for (OrderByElement orderByElement : plainSelect.getOrderByElements()) {
|
||||
orderByElement.getExpression().accept(this);
|
||||
}
|
||||
}
|
||||
if (plainSelect.getGroupBy() != null) {
|
||||
for (Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions()) {
|
||||
expression.accept(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConst(Expression expression) {
|
||||
return constAnalyzer.isConstExpression(expression);
|
||||
}
|
||||
|
||||
private void checkConstExpress(Expression expression) {
|
||||
if (constAnalyzer.isConstExpression(expression)) {
|
||||
/** 禁用常量表达式 */
|
||||
throw new JeecgSqlInjectionException("DISABLE CONST EXPRESSION " + expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
package org.jeecg.common.util.sqlInjection;
|
||||
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
import org.jeecg.common.util.sqlInjection.parse.ParserSupport;
|
||||
;
|
||||
|
||||
/**
|
||||
* SQL注入攻击分析器
|
||||
*
|
||||
* @author guyadong
|
||||
* 参考:
|
||||
* https://blog.csdn.net/10km/article/details/127767358
|
||||
* https://gitee.com/l0km/sql2java/tree/dev/sql2java-manager/src/main/java/gu/sql2java/parser
|
||||
*/
|
||||
public class SqlInjectionAnalyzer {
|
||||
|
||||
//启用/关闭注入攻击检查
|
||||
private boolean injectCheckEnable = true;
|
||||
//防止SQL注入攻击分析实现
|
||||
private final InjectionSyntaxObjectAnalyzer injectionChecker;
|
||||
private final InjectionAstNodeVisitor injectionVisitor;
|
||||
|
||||
public SqlInjectionAnalyzer() {
|
||||
this.injectionChecker = new InjectionSyntaxObjectAnalyzer();
|
||||
this.injectionVisitor = new InjectionAstNodeVisitor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/关闭注入攻击检查,默认启动
|
||||
*
|
||||
* @param enable
|
||||
* @return
|
||||
*/
|
||||
public SqlInjectionAnalyzer injectCheckEnable(boolean enable) {
|
||||
injectCheckEnable = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对解析后的SQL对象执行注入攻击分析,有注入攻击的危险则抛出异常{@link JeecgSqlInjectionException}
|
||||
*
|
||||
* @param sqlParserInfo
|
||||
* @throws JeecgSqlInjectionException
|
||||
*/
|
||||
public ParserSupport.SqlParserInfo injectAnalyse(ParserSupport.SqlParserInfo sqlParserInfo) throws JeecgSqlInjectionException {
|
||||
if (null != sqlParserInfo && injectCheckEnable) {
|
||||
/** SQL注入攻击检查 */
|
||||
sqlParserInfo.statement.accept(injectionChecker);
|
||||
sqlParserInfo.simpleNode.jjtAccept(injectionVisitor, null);
|
||||
}
|
||||
return sqlParserInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* sql校验
|
||||
*/
|
||||
public static void checkSql(String sql,boolean check){
|
||||
SqlInjectionAnalyzer sqlInjectionAnalyzer = new SqlInjectionAnalyzer();
|
||||
sqlInjectionAnalyzer.injectCheckEnable(check);
|
||||
ParserSupport.SqlParserInfo sqlParserInfo = ParserSupport.parse0(sql, null,null);
|
||||
sqlInjectionAnalyzer.injectAnalyse(sqlParserInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,569 +0,0 @@
|
||||
package org.jeecg.common.util.sqlInjection.parse;
|
||||
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseOr;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
|
||||
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.Between;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.FullTextSearch;
|
||||
import net.sf.jsqlparser.expression.operators.relational.GeometryDistance;
|
||||
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
|
||||
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor;
|
||||
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
|
||||
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.Matches;
|
||||
import net.sf.jsqlparser.expression.operators.relational.MinorThan;
|
||||
import net.sf.jsqlparser.expression.operators.relational.MinorThanEquals;
|
||||
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.RegExpMatchOperator;
|
||||
import net.sf.jsqlparser.expression.operators.relational.RegExpMySQLOperator;
|
||||
import net.sf.jsqlparser.expression.operators.relational.SimilarToExpression;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.statement.select.AllColumns;
|
||||
import net.sf.jsqlparser.statement.select.AllTableColumns;
|
||||
import net.sf.jsqlparser.statement.select.OrderByElement;
|
||||
import net.sf.jsqlparser.statement.select.SubSelect;
|
||||
|
||||
/**
|
||||
* 判断表达是否为常量的分析器
|
||||
*
|
||||
* @author guyadong
|
||||
*/
|
||||
public class ConstAnalyzer implements ExpressionVisitor, ItemsListVisitor {
|
||||
|
||||
private static ThreadLocal<Boolean> constFlag = new ThreadLocal<Boolean>() {
|
||||
@Override
|
||||
protected Boolean initialValue() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void visit(NullValue value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Function function) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SignedExpression expr) {
|
||||
expr.getExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(JdbcParameter parameter) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(JdbcNamedParameter parameter) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(DoubleValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(LongValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(DateValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(TimeValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(TimestampValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Parenthesis parenthesis) {
|
||||
parenthesis.getExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(StringValue value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Addition expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Division expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IntegerDivision expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Multiplication expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Subtraction expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AndExpression expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OrExpression expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(XorExpression expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Between expr) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
expr.getBetweenExpressionStart().accept(this);
|
||||
expr.getBetweenExpressionEnd().accept(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于处理 OverlapsCondition 类型的表达式
|
||||
* @param overlapsCondition
|
||||
*/
|
||||
@Override
|
||||
public void visit(OverlapsCondition overlapsCondition) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
/**
|
||||
* 用于处理 SafeCastExpression 类型的表达式。
|
||||
* @param safeCastExpression
|
||||
*/
|
||||
@Override
|
||||
public void visit(SafeCastExpression safeCastExpression) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(EqualsTo expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(GreaterThan expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(GreaterThanEquals expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(InExpression expr) {
|
||||
if (expr.getLeftExpression() != null) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IsNullExpression expr) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(FullTextSearch expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IsBooleanExpression expr) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(LikeExpression expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MinorThan expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MinorThanEquals expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(NotEqualsTo expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Column column) {
|
||||
if (!ParserSupport.isBoolean(column)) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SubSelect subSelect) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CaseExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(WhenClause expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ExistsExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AnyComparisonExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Concat expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Matches expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BitwiseAnd expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BitwiseOr expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BitwiseXor expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CastExpression expr) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(TryCastExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Modulo expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AnalyticExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ExtractExpression expr) {
|
||||
expr.getExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IntervalExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OracleHierarchicalExpression expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegExpMatchOperator expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ExpressionList expressionList) {
|
||||
for (Expression expr : expressionList.getExpressions()) {
|
||||
expr.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(NamedExpressionList namedExpressionList) {
|
||||
for (Expression expr : namedExpressionList.getExpressions()) {
|
||||
expr.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MultiExpressionList multiExprList) {
|
||||
for (ExpressionList list : multiExprList.getExpressionLists()) {
|
||||
visit(list);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(NotExpression notExpr) {
|
||||
notExpr.getExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BitwiseRightShift expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(BitwiseLeftShift expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
protected void visitBinaryExpression(BinaryExpression expr) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
expr.getRightExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(JsonExpression jsonExpr) {
|
||||
jsonExpr.getExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(JsonOperator expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RegExpMySQLOperator expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(UserVariable var) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(NumericBind bind) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(KeepExpression expr) {
|
||||
for (OrderByElement element : expr.getOrderByElements()) {
|
||||
element.getExpression().accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MySQLGroupConcat groupConcat) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ValueListExpression valueListExpression) {
|
||||
for (Expression expr : valueListExpression.getExpressionList().getExpressions()) {
|
||||
expr.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AllColumns allColumns) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AllTableColumns allTableColumns) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AllValue allValue) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IsDistinctExpression isDistinctExpression) {
|
||||
visitBinaryExpression(isDistinctExpression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RowGetExpression rowGetExpression) {
|
||||
rowGetExpression.getExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(HexValue hexValue) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OracleHint hint) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(TimeKeyExpression timeKeyExpression) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(DateTimeLiteralExpression literal) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(NextValExpression nextVal) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CollateExpression col) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(SimilarToExpression expr) {
|
||||
visitBinaryExpression(expr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ArrayExpression array) {
|
||||
array.getObjExpression().accept(this);
|
||||
if (array.getIndexExpression() != null) {
|
||||
array.getIndexExpression().accept(this);
|
||||
}
|
||||
if (array.getStartIndexExpression() != null) {
|
||||
array.getStartIndexExpression().accept(this);
|
||||
}
|
||||
if (array.getStopIndexExpression() != null) {
|
||||
array.getStopIndexExpression().accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ArrayConstructor aThis) {
|
||||
for (Expression expression : aThis.getExpressions()) {
|
||||
expression.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(VariableAssignment var) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(XMLSerializeExpr expr) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(TimezoneExpression expr) {
|
||||
expr.getLeftExpression().accept(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(JsonAggregateFunction expression) {
|
||||
Expression expr = expression.getExpression();
|
||||
if (expr != null) {
|
||||
expr.accept(this);
|
||||
}
|
||||
|
||||
expr = expression.getFilterExpression();
|
||||
if (expr != null) {
|
||||
expr.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(JsonFunction expression) {
|
||||
for (JsonFunctionExpression expr : expression.getExpressions()) {
|
||||
expr.getExpression().accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(ConnectByRootOperator connectByRootOperator) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(GeometryDistance geometryDistance) {
|
||||
visitBinaryExpression(geometryDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(RowConstructor rowConstructor) {
|
||||
constFlag.set(false);
|
||||
}
|
||||
|
||||
public boolean isConstExpression(Expression expression) {
|
||||
if (null != expression) {
|
||||
constFlag.set(true);
|
||||
expression.accept(this);
|
||||
return constFlag.get();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,177 +0,0 @@
|
||||
package org.jeecg.common.util.sqlInjection.parse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.parser.*;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.PlainSelect;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import net.sf.jsqlparser.statement.select.SelectBody;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.exception.JeecgSqlInjectionException;
|
||||
|
||||
/**
|
||||
* 解析sql支持
|
||||
*/
|
||||
@Slf4j
|
||||
public class ParserSupport {
|
||||
/**
|
||||
* 解析SELECT SQL语句,解析失败或非SELECT语句则抛出异常
|
||||
*
|
||||
* @param sql
|
||||
* @return
|
||||
*/
|
||||
public static Select parseSelect(String sql) {
|
||||
Statement stmt;
|
||||
try {
|
||||
stmt = CCJSqlParserUtil.parse(checkNotNull(sql, "sql is null"));
|
||||
} catch (JSQLParserException e) {
|
||||
throw new JeecgBootException(e);
|
||||
}
|
||||
checkArgument(stmt instanceof Select, "%s is not SELECT statment", sql);
|
||||
Select select = (Select) stmt;
|
||||
SelectBody selectBody = select.getSelectBody();
|
||||
// 暂时只支持简单的SELECT xxxx FROM ....语句不支持复杂语句如WITH
|
||||
checkArgument(selectBody instanceof PlainSelect, "ONLY SUPPORT plain select statement %s", sql);
|
||||
return (Select) stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析SELECT SQL语句,解析失败或非SELECT语句则
|
||||
*
|
||||
* @param sql
|
||||
* @return
|
||||
*/
|
||||
public static Select parseSelectUnchecked(String sql) {
|
||||
try {
|
||||
return parseSelect(sql);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现SQL语句解析,解析成功则返回解析后的{@link Statement},
|
||||
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
|
||||
* @param sqlSyntaxNormalizer SQL语句分析转换器,为{@code null}忽略
|
||||
* @throws JSQLParserException 输入的SQL语句有语法错误
|
||||
* @see #parse0(String, CCJSqlParserVisitor, SqlSyntaxNormalizer)
|
||||
*/
|
||||
public static Statement parse(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxNormalizer) throws JSQLParserException {
|
||||
return parse0(sql, visitor, sqlSyntaxNormalizer).statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参照{@link CCJSqlParserUtil#parseAST(String)}和{@link CCJSqlParserUtil#parse(String)}实现SQL语句解析,
|
||||
* 解析成功则返回解析后的{@link SqlParserInfo}对象,
|
||||
* 并通过{@code visitor}参数提供基于AST(抽象语法树)的遍历所有节点的能力。
|
||||
*
|
||||
* @param sql SQL语句
|
||||
* @param visitor 遍历所有节点的{@link SimpleNodeVisitor}接口实例,为{@code null}忽略
|
||||
* @param sqlSyntaxAnalyzer SQL语句分析转换器,为{@code null}忽略
|
||||
* @throws JSQLParserException 输入的SQL语句有语法错误
|
||||
* @see net.sf.jsqlparser.parser.Node#jjtAccept(SimpleNodeVisitor, Object)
|
||||
*/
|
||||
public static SqlParserInfo parse0(String sql, CCJSqlParserVisitor visitor, SqlSyntaxNormalizer sqlSyntaxAnalyzer) throws JeecgSqlInjectionException {
|
||||
|
||||
//检查是否非select开头,暂不支持
|
||||
if(!sql.toLowerCase().trim().startsWith("select ")) {
|
||||
log.warn("传入sql 非select开头,不支持非select开头的语句解析!");
|
||||
return null;
|
||||
}
|
||||
|
||||
//检查是否存储过程,暂不支持
|
||||
if(sql.toLowerCase().trim().startsWith("call ")){
|
||||
log.warn("传入call 开头存储过程,不支持存储过程解析!");
|
||||
return null;
|
||||
}
|
||||
|
||||
//检查特殊语义的特殊字符,目前检查冒号、$、#三种特殊语义字符
|
||||
String specialCharacters = "[:$#]";
|
||||
Pattern pattern = Pattern.compile(specialCharacters);
|
||||
Matcher matcher = pattern.matcher(sql);
|
||||
if (matcher.find()) {
|
||||
sql = sql.replaceAll("[:$#]", "@");
|
||||
}
|
||||
|
||||
checkArgument(null != sql, "sql is null");
|
||||
boolean allowComplexParsing = CCJSqlParserUtil.getNestingDepth(sql) <= CCJSqlParserUtil.ALLOWED_NESTING_DEPTH;
|
||||
|
||||
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql).withAllowComplexParsing(allowComplexParsing);
|
||||
Statement stmt;
|
||||
try {
|
||||
stmt = parser.Statement();
|
||||
} catch (Exception ex) {
|
||||
log.error("请注意,SQL语法可能存在问题---> {}", ex.getMessage());
|
||||
throw new JeecgSqlInjectionException("请注意,SQL语法可能存在问题:"+sql);
|
||||
}
|
||||
if (null != visitor) {
|
||||
parser.getASTRoot().jjtAccept(visitor, null);
|
||||
}
|
||||
if (null != sqlSyntaxAnalyzer) {
|
||||
stmt.accept(sqlSyntaxAnalyzer.resetChanged());
|
||||
}
|
||||
return new SqlParserInfo(stmt.toString(), stmt, (SimpleNode) parser.getASTRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用{@link CCJSqlParser}解析SQL语句部件返回解析生成的对象,如{@code 'ORDER BY id DESC'}
|
||||
*
|
||||
* @param <T>
|
||||
* @param input
|
||||
* @param method 指定调用的{@link CCJSqlParser}解析方法
|
||||
* @param targetType 返回的解析对象类型
|
||||
* @return
|
||||
* @since 3.18.3
|
||||
*/
|
||||
public static <T> T parseComponent(String input, String method, Class<T> targetType) {
|
||||
try {
|
||||
CCJSqlParser parser = new CCJSqlParser(new StringProvider(input));
|
||||
try {
|
||||
return checkNotNull(targetType, "targetType is null").cast(parser.getClass().getMethod(method).invoke(parser));
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwables.throwIfUnchecked(e.getTargetException());
|
||||
throw new RuntimeException(e.getTargetException());
|
||||
}
|
||||
} catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
|
||||
Throwables.throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果{@link Column}没有定义table,且字段名为true/false(不区分大小写)则视为布尔常量
|
||||
*
|
||||
* @param column
|
||||
*/
|
||||
public static boolean isBoolean(Column column) {
|
||||
return null != column && null == column.getTable() &&
|
||||
Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE).matcher(column.getColumnName()).matches();
|
||||
}
|
||||
|
||||
public static class SqlParserInfo {
|
||||
public String nativeSql;
|
||||
public Statement statement;
|
||||
public SimpleNode simpleNode;
|
||||
|
||||
SqlParserInfo(String nativeSql, Statement statement, SimpleNode simpleNode) {
|
||||
this.nativeSql = nativeSql;
|
||||
this.statement = statement;
|
||||
this.simpleNode = simpleNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
package org.jeecg.common.util.sqlInjection.parse;
|
||||
|
||||
import net.sf.jsqlparser.util.TablesNamesFinder;
|
||||
|
||||
/**
|
||||
* SQL语句分析转换器基类<br>
|
||||
* 基于SQL语法对象实现对SQL的修改
|
||||
* (暂时用不到)
|
||||
*
|
||||
* @author guyadong
|
||||
* @since 3.17.0
|
||||
*/
|
||||
public class SqlSyntaxNormalizer extends TablesNamesFinder {
|
||||
protected static final ThreadLocal<Boolean> changed = new ThreadLocal<>();
|
||||
|
||||
public SqlSyntaxNormalizer() {
|
||||
super();
|
||||
init(true);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 语句改变返回{@code true},否则返回{@code false}
|
||||
*/
|
||||
public boolean changed() {
|
||||
return Boolean.TRUE.equals(changed.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 复位线程局部变量{@link #changed}状态
|
||||
*/
|
||||
public SqlSyntaxNormalizer resetChanged() {
|
||||
changed.remove();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,255 +1,255 @@
|
||||
package org.jeecg.common.util.sqlparse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.*;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 解析所有表名和字段的类
|
||||
*/
|
||||
@Slf4j
|
||||
public class JSqlParserAllTableManager {
|
||||
|
||||
private final String sql;
|
||||
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
||||
/**
|
||||
* 别名对应实际表名
|
||||
*/
|
||||
private final Map<String, String> tableAliasMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 解析后的sql
|
||||
*/
|
||||
private String parsedSql = null;
|
||||
|
||||
JSqlParserAllTableManager(String selectSql) {
|
||||
this.sql = selectSql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始解析
|
||||
*
|
||||
* @return
|
||||
* @throws JSQLParserException
|
||||
*/
|
||||
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
||||
// 1. 创建解析器
|
||||
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// 2. 使用解析器解析sql生成具有层次结构的java类
|
||||
Statement stmt = mgr.parse(new StringReader(this.sql));
|
||||
if (stmt instanceof Select) {
|
||||
Select selectStatement = (Select) stmt;
|
||||
SelectBody selectBody = selectStatement.getSelectBody();
|
||||
this.parsedSql = selectBody.toString();
|
||||
// 3. 解析select查询sql的信息
|
||||
if (selectBody instanceof PlainSelect) {
|
||||
PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
// 4. 合并 fromItems
|
||||
List<FromItem> fromItems = new ArrayList<>();
|
||||
fromItems.add(plainSelect.getFromItem());
|
||||
// 4.1 处理join的表
|
||||
List<Join> joins = plainSelect.getJoins();
|
||||
if (joins != null) {
|
||||
joins.forEach(join -> fromItems.add(join.getRightItem()));
|
||||
}
|
||||
// 5. 处理 fromItems
|
||||
for (FromItem fromItem : fromItems) {
|
||||
// 5.1 通过表名的方式from
|
||||
if (fromItem instanceof Table) {
|
||||
this.addSqlInfoByTable((Table) fromItem);
|
||||
}
|
||||
// 5.2 通过子查询的方式from
|
||||
else if (fromItem instanceof SubSelect) {
|
||||
this.handleSubSelect((SubSelect) fromItem);
|
||||
}
|
||||
}
|
||||
// 6. 解析 selectFields
|
||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
for (SelectItem selectItem : selectItems) {
|
||||
// 6.1 查询的是全部字段
|
||||
if (selectItem instanceof AllColumns) {
|
||||
// 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
||||
String tableName = plainSelect.getFromItem(Table.class).getName();
|
||||
// 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
||||
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
assert sqlInfo != null;
|
||||
// 设置为查询全部字段
|
||||
sqlInfo.setSelectAll(true);
|
||||
sqlInfo.setSelectFields(null);
|
||||
sqlInfo.setRealSelectFields(null);
|
||||
}
|
||||
// 6.2 查询的是带表别名( u.* )的全部字段
|
||||
else if (selectItem instanceof AllTableColumns) {
|
||||
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
||||
String aliasName = allTableColumns.getTable().getName();
|
||||
// 通过别名获取表名
|
||||
String tableName = this.tableAliasMap.get(aliasName);
|
||||
if (tableName == null) {
|
||||
tableName = aliasName;
|
||||
}
|
||||
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
// 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
||||
if (sqlInfo != null) {
|
||||
// 设置为查询全部字段
|
||||
sqlInfo.setSelectAll(true);
|
||||
sqlInfo.setSelectFields(null);
|
||||
sqlInfo.setRealSelectFields(null);
|
||||
}
|
||||
}
|
||||
// 6.3 各种字段表达式处理
|
||||
else if (selectItem instanceof SelectExpressionItem) {
|
||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
Expression expression = selectExpressionItem.getExpression();
|
||||
Alias alias = selectExpressionItem.getAlias();
|
||||
this.handleExpression(expression, alias, plainSelect.getFromItem());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
}
|
||||
} else {
|
||||
// 非 select 查询sql,不做处理
|
||||
throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
}
|
||||
return this.allTableMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理子查询
|
||||
*
|
||||
* @param subSelect
|
||||
*/
|
||||
private void handleSubSelect(SubSelect subSelect) {
|
||||
try {
|
||||
String subSelectSql = subSelect.getSelectBody().toString();
|
||||
// 递归调用解析
|
||||
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
||||
if (map != null) {
|
||||
this.assignMap(map);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析子查询出错", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询字段表达式
|
||||
*
|
||||
* @param expression
|
||||
*/
|
||||
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
||||
// 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
if (expression instanceof Function) {
|
||||
Function functionExp = (Function) expression;
|
||||
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
for (Expression expItem : expressions) {
|
||||
this.handleExpression(expItem, null, fromItem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 处理字段上的子查询
|
||||
if (expression instanceof SubSelect) {
|
||||
this.handleSubSelect((SubSelect) expression);
|
||||
return;
|
||||
}
|
||||
// 不处理字面量
|
||||
if (expression instanceof StringValue ||
|
||||
expression instanceof NullValue ||
|
||||
expression instanceof LongValue ||
|
||||
expression instanceof DoubleValue ||
|
||||
expression instanceof HexValue ||
|
||||
expression instanceof DateValue ||
|
||||
expression instanceof TimestampValue ||
|
||||
expression instanceof TimeValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理字段
|
||||
if (expression instanceof Column) {
|
||||
Column column = (Column) expression;
|
||||
// 查询字段名
|
||||
String fieldName = column.getColumnName();
|
||||
String aliasName = fieldName;
|
||||
if (alias != null) {
|
||||
aliasName = alias.getName();
|
||||
}
|
||||
String tableName;
|
||||
if (column.getTable() != null) {
|
||||
// 通过列的表名获取 sqlInfo
|
||||
// 例如 user.name,这里的 tableName 就是 user
|
||||
tableName = column.getTable().getName();
|
||||
// 有可能是别名,需要转换为真实表名
|
||||
if (this.tableAliasMap.get(tableName) != null) {
|
||||
tableName = this.tableAliasMap.get(tableName);
|
||||
}
|
||||
} else {
|
||||
// 当column的table为空时,说明是 fromItem 中的字段
|
||||
tableName = ((Table) fromItem).getName();
|
||||
}
|
||||
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
||||
if ($sqlInfo != null) {
|
||||
$sqlInfo.addSelectField(aliasName, fieldName);
|
||||
} else {
|
||||
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表名添加sqlInfo
|
||||
*
|
||||
* @param table
|
||||
*/
|
||||
private void addSqlInfoByTable(Table table) {
|
||||
String tableName = table.getName();
|
||||
// 解析 aliasName
|
||||
if (table.getAlias() != null) {
|
||||
this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
||||
}
|
||||
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
||||
sqlInfo.setFromTableName(table.getName());
|
||||
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并map
|
||||
*
|
||||
* @param source
|
||||
*/
|
||||
private void assignMap(Map<String, SelectSqlInfo> source) {
|
||||
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
||||
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
||||
if (sqlInfo == null) {
|
||||
this.allTableMap.put(entry.getKey(), entry.getValue());
|
||||
} else {
|
||||
// 合并
|
||||
if (sqlInfo.getSelectFields() == null) {
|
||||
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
||||
} else {
|
||||
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
||||
}
|
||||
if (sqlInfo.getRealSelectFields() == null) {
|
||||
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
||||
} else {
|
||||
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//package org.jeecg.common.util.sqlparse;
|
||||
//
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import net.sf.jsqlparser.JSQLParserException;
|
||||
//import net.sf.jsqlparser.expression.*;
|
||||
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
//import net.sf.jsqlparser.schema.Column;
|
||||
//import net.sf.jsqlparser.schema.Table;
|
||||
//import net.sf.jsqlparser.statement.Statement;
|
||||
//import net.sf.jsqlparser.statement.select.*;
|
||||
//import org.jeecg.common.exception.JeecgBootException;
|
||||
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
//
|
||||
//import java.io.StringReader;
|
||||
//import java.util.ArrayList;
|
||||
//import java.util.HashMap;
|
||||
//import java.util.List;
|
||||
//import java.util.Map;
|
||||
//
|
||||
///**
|
||||
// * 解析所有表名和字段的类
|
||||
// */
|
||||
//@Slf4j
|
||||
//public class JSqlParserAllTableManager {
|
||||
//
|
||||
// private final String sql;
|
||||
// private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
|
||||
// /**
|
||||
// * 别名对应实际表名
|
||||
// */
|
||||
// private final Map<String, String> tableAliasMap = new HashMap<>();
|
||||
//
|
||||
// /**
|
||||
// * 解析后的sql
|
||||
// */
|
||||
// private String parsedSql = null;
|
||||
//
|
||||
// JSqlParserAllTableManager(String selectSql) {
|
||||
// this.sql = selectSql;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 开始解析
|
||||
// *
|
||||
// * @return
|
||||
// * @throws JSQLParserException
|
||||
// */
|
||||
// public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
|
||||
// // 1. 创建解析器
|
||||
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// // 2. 使用解析器解析sql生成具有层次结构的java类
|
||||
// Statement stmt = mgr.parse(new StringReader(this.sql));
|
||||
// if (stmt instanceof Select) {
|
||||
// Select selectStatement = (Select) stmt;
|
||||
// SelectBody selectBody = selectStatement.getSelectBody();
|
||||
// this.parsedSql = selectBody.toString();
|
||||
// // 3. 解析select查询sql的信息
|
||||
// if (selectBody instanceof PlainSelect) {
|
||||
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
// // 4. 合并 fromItems
|
||||
// List<FromItem> fromItems = new ArrayList<>();
|
||||
// fromItems.add(plainSelect.getFromItem());
|
||||
// // 4.1 处理join的表
|
||||
// List<Join> joins = plainSelect.getJoins();
|
||||
// if (joins != null) {
|
||||
// joins.forEach(join -> fromItems.add(join.getRightItem()));
|
||||
// }
|
||||
// // 5. 处理 fromItems
|
||||
// for (FromItem fromItem : fromItems) {
|
||||
// // 5.1 通过表名的方式from
|
||||
// if (fromItem instanceof Table) {
|
||||
// this.addSqlInfoByTable((Table) fromItem);
|
||||
// }
|
||||
// // 5.2 通过子查询的方式from
|
||||
// else if (fromItem instanceof SubSelect) {
|
||||
// this.handleSubSelect((SubSelect) fromItem);
|
||||
// }
|
||||
// }
|
||||
// // 6. 解析 selectFields
|
||||
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
// for (SelectItem selectItem : selectItems) {
|
||||
// // 6.1 查询的是全部字段
|
||||
// if (selectItem instanceof AllColumns) {
|
||||
// // 当 selectItem 为 AllColumns 时,fromItem 必定为 Table
|
||||
// String tableName = plainSelect.getFromItem(Table.class).getName();
|
||||
// // 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
|
||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
// assert sqlInfo != null;
|
||||
// // 设置为查询全部字段
|
||||
// sqlInfo.setSelectAll(true);
|
||||
// sqlInfo.setSelectFields(null);
|
||||
// sqlInfo.setRealSelectFields(null);
|
||||
// }
|
||||
// // 6.2 查询的是带表别名( u.* )的全部字段
|
||||
// else if (selectItem instanceof AllTableColumns) {
|
||||
// AllTableColumns allTableColumns = (AllTableColumns) selectItem;
|
||||
// String aliasName = allTableColumns.getTable().getName();
|
||||
// // 通过别名获取表名
|
||||
// String tableName = this.tableAliasMap.get(aliasName);
|
||||
// if (tableName == null) {
|
||||
// tableName = aliasName;
|
||||
// }
|
||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
|
||||
// // 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
|
||||
// if (sqlInfo != null) {
|
||||
// // 设置为查询全部字段
|
||||
// sqlInfo.setSelectAll(true);
|
||||
// sqlInfo.setSelectFields(null);
|
||||
// sqlInfo.setRealSelectFields(null);
|
||||
// }
|
||||
// }
|
||||
// // 6.3 各种字段表达式处理
|
||||
// else if (selectItem instanceof SelectExpressionItem) {
|
||||
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
// Expression expression = selectExpressionItem.getExpression();
|
||||
// Alias alias = selectExpressionItem.getAlias();
|
||||
// this.handleExpression(expression, alias, plainSelect.getFromItem());
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
// }
|
||||
// } else {
|
||||
// // 非 select 查询sql,不做处理
|
||||
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
// }
|
||||
// return this.allTableMap;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理子查询
|
||||
// *
|
||||
// * @param subSelect
|
||||
// */
|
||||
// private void handleSubSelect(SubSelect subSelect) {
|
||||
// try {
|
||||
// String subSelectSql = subSelect.getSelectBody().toString();
|
||||
// // 递归调用解析
|
||||
// Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
|
||||
// if (map != null) {
|
||||
// this.assignMap(map);
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// log.error("解析子查询出错", e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理查询字段表达式
|
||||
// *
|
||||
// * @param expression
|
||||
// */
|
||||
// private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
|
||||
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
// if (expression instanceof Function) {
|
||||
// Function functionExp = (Function) expression;
|
||||
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
// for (Expression expItem : expressions) {
|
||||
// this.handleExpression(expItem, null, fromItem);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// // 处理字段上的子查询
|
||||
// if (expression instanceof SubSelect) {
|
||||
// this.handleSubSelect((SubSelect) expression);
|
||||
// return;
|
||||
// }
|
||||
// // 不处理字面量
|
||||
// if (expression instanceof StringValue ||
|
||||
// expression instanceof NullValue ||
|
||||
// expression instanceof LongValue ||
|
||||
// expression instanceof DoubleValue ||
|
||||
// expression instanceof HexValue ||
|
||||
// expression instanceof DateValue ||
|
||||
// expression instanceof TimestampValue ||
|
||||
// expression instanceof TimeValue
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 处理字段
|
||||
// if (expression instanceof Column) {
|
||||
// Column column = (Column) expression;
|
||||
// // 查询字段名
|
||||
// String fieldName = column.getColumnName();
|
||||
// String aliasName = fieldName;
|
||||
// if (alias != null) {
|
||||
// aliasName = alias.getName();
|
||||
// }
|
||||
// String tableName;
|
||||
// if (column.getTable() != null) {
|
||||
// // 通过列的表名获取 sqlInfo
|
||||
// // 例如 user.name,这里的 tableName 就是 user
|
||||
// tableName = column.getTable().getName();
|
||||
// // 有可能是别名,需要转换为真实表名
|
||||
// if (this.tableAliasMap.get(tableName) != null) {
|
||||
// tableName = this.tableAliasMap.get(tableName);
|
||||
// }
|
||||
// } else {
|
||||
// // 当column的table为空时,说明是 fromItem 中的字段
|
||||
// tableName = ((Table) fromItem).getName();
|
||||
// }
|
||||
// SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
|
||||
// if ($sqlInfo != null) {
|
||||
// $sqlInfo.addSelectField(aliasName, fieldName);
|
||||
// } else {
|
||||
// log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 根据表名添加sqlInfo
|
||||
// *
|
||||
// * @param table
|
||||
// */
|
||||
// private void addSqlInfoByTable(Table table) {
|
||||
// String tableName = table.getName();
|
||||
// // 解析 aliasName
|
||||
// if (table.getAlias() != null) {
|
||||
// this.tableAliasMap.put(table.getAlias().getName(), tableName);
|
||||
// }
|
||||
// SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
|
||||
// sqlInfo.setFromTableName(table.getName());
|
||||
// this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 合并map
|
||||
// *
|
||||
// * @param source
|
||||
// */
|
||||
// private void assignMap(Map<String, SelectSqlInfo> source) {
|
||||
// for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
|
||||
// SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
|
||||
// if (sqlInfo == null) {
|
||||
// this.allTableMap.put(entry.getKey(), entry.getValue());
|
||||
// } else {
|
||||
// // 合并
|
||||
// if (sqlInfo.getSelectFields() == null) {
|
||||
// sqlInfo.setSelectFields(entry.getValue().getSelectFields());
|
||||
// } else {
|
||||
// sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
|
||||
// }
|
||||
// if (sqlInfo.getRealSelectFields() == null) {
|
||||
// sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
|
||||
// } else {
|
||||
// sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@ -1,184 +1,190 @@
|
||||
package org.jeecg.common.util.sqlparse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.*;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class JSqlParserUtils {
|
||||
|
||||
/**
|
||||
* 解析 查询(select)sql的信息,
|
||||
* 此方法会展开所有子查询到一个map里,
|
||||
* key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
||||
* value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
||||
* <p>
|
||||
* 例如:SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
|
||||
* 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
||||
*
|
||||
* @param selectSql
|
||||
* @return
|
||||
*/
|
||||
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
||||
if (oConvertUtils.isEmpty(selectSql)) {
|
||||
return null;
|
||||
}
|
||||
// log.info("解析查询Sql:{}", selectSql);
|
||||
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
||||
return allTableManager.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 查询(select)sql的信息,子查询嵌套
|
||||
*
|
||||
* @param selectSql
|
||||
* @return
|
||||
*/
|
||||
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
||||
if (oConvertUtils.isEmpty(selectSql)) {
|
||||
return null;
|
||||
}
|
||||
// log.info("解析查询Sql:{}", selectSql);
|
||||
// 使用 JSqlParer 解析sql
|
||||
// 1、创建解析器
|
||||
CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// 2、使用解析器解析sql生成具有层次结构的java类
|
||||
Statement stmt = mgr.parse(new StringReader(selectSql));
|
||||
if (stmt instanceof Select) {
|
||||
Select selectStatement = (Select) stmt;
|
||||
// 3、解析select查询sql的信息
|
||||
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
||||
} else {
|
||||
// 非 select 查询sql,不做处理
|
||||
throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 select 查询sql的信息
|
||||
*
|
||||
* @param selectBody
|
||||
* @return
|
||||
*/
|
||||
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
||||
// 简单的select查询
|
||||
if (selectBody instanceof PlainSelect) {
|
||||
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
||||
PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
FromItem fromItem = plainSelect.getFromItem();
|
||||
// 解析 aliasName
|
||||
if (fromItem.getAlias() != null) {
|
||||
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
||||
}
|
||||
// 解析 表名
|
||||
if (fromItem instanceof Table) {
|
||||
// 通过表名的方式from
|
||||
Table fromTable = (Table) fromItem;
|
||||
sqlInfo.setFromTableName(fromTable.getName());
|
||||
} else if (fromItem instanceof SubSelect) {
|
||||
// 通过子查询的方式from
|
||||
SubSelect fromSubSelect = (SubSelect) fromItem;
|
||||
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
||||
sqlInfo.setFromSubSelect(subSqlInfo);
|
||||
}
|
||||
// 解析 selectFields
|
||||
List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
for (SelectItem selectItem : selectItems) {
|
||||
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
||||
// 全部字段
|
||||
sqlInfo.setSelectAll(true);
|
||||
sqlInfo.setSelectFields(null);
|
||||
sqlInfo.setRealSelectFields(null);
|
||||
break;
|
||||
} else if (selectItem instanceof SelectExpressionItem) {
|
||||
// 获取单个查询字段名
|
||||
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
Expression expression = selectExpressionItem.getExpression();
|
||||
Alias alias = selectExpressionItem.getAlias();
|
||||
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
||||
}
|
||||
}
|
||||
return sqlInfo;
|
||||
} else {
|
||||
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询字段表达式
|
||||
*
|
||||
* @param sqlInfo
|
||||
* @param expression
|
||||
* @param alias 是否有别名,无传null
|
||||
*/
|
||||
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
||||
// 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
if (expression instanceof Function) {
|
||||
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
||||
return;
|
||||
}
|
||||
// 处理字段上的子查询
|
||||
if (expression instanceof SubSelect) {
|
||||
SubSelect subSelect = (SubSelect) expression;
|
||||
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
||||
// 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
||||
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
||||
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
||||
return;
|
||||
}
|
||||
// 不处理字面量
|
||||
if (expression instanceof StringValue ||
|
||||
expression instanceof NullValue ||
|
||||
expression instanceof LongValue ||
|
||||
expression instanceof DoubleValue ||
|
||||
expression instanceof HexValue ||
|
||||
expression instanceof DateValue ||
|
||||
expression instanceof TimestampValue ||
|
||||
expression instanceof TimeValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询字段名
|
||||
String selectField = expression.toString();
|
||||
// 实际查询字段名
|
||||
String realSelectField = selectField;
|
||||
// 判断是否有别名
|
||||
if (alias != null) {
|
||||
selectField = alias.getName();
|
||||
}
|
||||
// 获取真实字段名
|
||||
if (expression instanceof Column) {
|
||||
Column column = (Column) expression;
|
||||
realSelectField = column.getColumnName();
|
||||
}
|
||||
sqlInfo.addSelectField(selectField, realSelectField);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理函数式字段
|
||||
*
|
||||
* @param functionExp
|
||||
* @param sqlInfo
|
||||
*/
|
||||
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
||||
List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
for (Expression expression : expressions) {
|
||||
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
//package org.jeecg.common.util.sqlparse;
|
||||
//
|
||||
//import lombok.extern.slf4j.Slf4j;
|
||||
//import net.sf.jsqlparser.JSQLParserException;
|
||||
//import net.sf.jsqlparser.expression.*;
|
||||
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
|
||||
//import net.sf.jsqlparser.schema.Column;
|
||||
//import net.sf.jsqlparser.schema.Table;
|
||||
//import net.sf.jsqlparser.statement.Statement;
|
||||
//import net.sf.jsqlparser.statement.select.*;
|
||||
//import org.jeecg.common.exception.JeecgBootException;
|
||||
//import org.jeecg.common.util.oConvertUtils;
|
||||
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
|
||||
//
|
||||
//import java.io.StringReader;
|
||||
//import java.util.List;
|
||||
//import java.util.Map;
|
||||
//
|
||||
//@Slf4j
|
||||
//public class JSqlParserUtils {
|
||||
//
|
||||
// /**
|
||||
// * 解析 查询(select)sql的信息,
|
||||
// * 此方法会展开所有子查询到一个map里,
|
||||
// * key只存真实的表名,如果查询的没有真实的表名,则会被忽略。
|
||||
// * value只存真实的字段名,如果查询的没有真实的字段名,则会被忽略。
|
||||
// * <p>
|
||||
// * 例如:SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
|
||||
// * 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
|
||||
// *
|
||||
// * @param selectSql
|
||||
// * @return
|
||||
// */
|
||||
// public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
|
||||
// if (oConvertUtils.isEmpty(selectSql)) {
|
||||
// return null;
|
||||
// }
|
||||
// // log.info("解析查询Sql:{}", selectSql);
|
||||
// JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
|
||||
// return allTableManager.parse();
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 解析 查询(select)sql的信息,子查询嵌套
|
||||
// *
|
||||
// * @param selectSql
|
||||
// * @return
|
||||
// */
|
||||
// public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
|
||||
// if (oConvertUtils.isEmpty(selectSql)) {
|
||||
// return null;
|
||||
// }
|
||||
// // log.info("解析查询Sql:{}", selectSql);
|
||||
// // 使用 JSqlParer 解析sql
|
||||
// // 1、创建解析器
|
||||
// CCJSqlParserManager mgr = new CCJSqlParserManager();
|
||||
// // 2、使用解析器解析sql生成具有层次结构的java类
|
||||
// Statement stmt = mgr.parse(new StringReader(selectSql));
|
||||
// if (stmt instanceof Select) {
|
||||
// Select selectStatement = (Select) stmt;
|
||||
// // 3、解析select查询sql的信息
|
||||
// return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
|
||||
// } else {
|
||||
// // 非 select 查询sql,不做处理
|
||||
// throw new JeecgBootException("非 select 查询sql,不做处理");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 解析 select 查询sql的信息
|
||||
// *
|
||||
// * @param selectBody
|
||||
// * @return
|
||||
// */
|
||||
// private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
|
||||
// // 判断是否使用了union等操作
|
||||
// if (selectBody instanceof SetOperationList) {
|
||||
// // 如果使用了union等操作,则只解析第一个查询
|
||||
// List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
|
||||
// return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
|
||||
// }
|
||||
// // 简单的select查询
|
||||
// if (selectBody instanceof PlainSelect) {
|
||||
// SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
|
||||
// PlainSelect plainSelect = (PlainSelect) selectBody;
|
||||
// FromItem fromItem = plainSelect.getFromItem();
|
||||
// // 解析 aliasName
|
||||
// if (fromItem.getAlias() != null) {
|
||||
// sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
|
||||
// }
|
||||
// // 解析 表名
|
||||
// if (fromItem instanceof Table) {
|
||||
// // 通过表名的方式from
|
||||
// Table fromTable = (Table) fromItem;
|
||||
// sqlInfo.setFromTableName(fromTable.getName());
|
||||
// } else if (fromItem instanceof SubSelect) {
|
||||
// // 通过子查询的方式from
|
||||
// SubSelect fromSubSelect = (SubSelect) fromItem;
|
||||
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
|
||||
// sqlInfo.setFromSubSelect(subSqlInfo);
|
||||
// }
|
||||
// // 解析 selectFields
|
||||
// List<SelectItem> selectItems = plainSelect.getSelectItems();
|
||||
// for (SelectItem selectItem : selectItems) {
|
||||
// if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
|
||||
// // 全部字段
|
||||
// sqlInfo.setSelectAll(true);
|
||||
// sqlInfo.setSelectFields(null);
|
||||
// sqlInfo.setRealSelectFields(null);
|
||||
// break;
|
||||
// } else if (selectItem instanceof SelectExpressionItem) {
|
||||
// // 获取单个查询字段名
|
||||
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
|
||||
// Expression expression = selectExpressionItem.getExpression();
|
||||
// Alias alias = selectExpressionItem.getAlias();
|
||||
// JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
|
||||
// }
|
||||
// }
|
||||
// return sqlInfo;
|
||||
// } else {
|
||||
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
|
||||
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理查询字段表达式
|
||||
// *
|
||||
// * @param sqlInfo
|
||||
// * @param expression
|
||||
// * @param alias 是否有别名,无传null
|
||||
// */
|
||||
// private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
|
||||
// // 处理函数式字段 CONCAT(name,'(',age,')')
|
||||
// if (expression instanceof Function) {
|
||||
// JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
|
||||
// return;
|
||||
// }
|
||||
// // 处理字段上的子查询
|
||||
// if (expression instanceof SubSelect) {
|
||||
// SubSelect subSelect = (SubSelect) expression;
|
||||
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
|
||||
// // 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
|
||||
// sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
|
||||
// sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
|
||||
// return;
|
||||
// }
|
||||
// // 不处理字面量
|
||||
// if (expression instanceof StringValue ||
|
||||
// expression instanceof NullValue ||
|
||||
// expression instanceof LongValue ||
|
||||
// expression instanceof DoubleValue ||
|
||||
// expression instanceof HexValue ||
|
||||
// expression instanceof DateValue ||
|
||||
// expression instanceof TimestampValue ||
|
||||
// expression instanceof TimeValue
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 查询字段名
|
||||
// String selectField = expression.toString();
|
||||
// // 实际查询字段名
|
||||
// String realSelectField = selectField;
|
||||
// // 判断是否有别名
|
||||
// if (alias != null) {
|
||||
// selectField = alias.getName();
|
||||
// }
|
||||
// // 获取真实字段名
|
||||
// if (expression instanceof Column) {
|
||||
// Column column = (Column) expression;
|
||||
// realSelectField = column.getColumnName();
|
||||
// }
|
||||
// sqlInfo.addSelectField(selectField, realSelectField);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 处理函数式字段
|
||||
// *
|
||||
// * @param functionExp
|
||||
// * @param sqlInfo
|
||||
// */
|
||||
// private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
|
||||
// List<Expression> expressions = functionExp.getParameters().getExpressions();
|
||||
// for (Expression expression : expressions) {
|
||||
// JSqlParserUtils.handleExpression(sqlInfo, expression, null);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@ -1,101 +1,101 @@
|
||||
package org.jeecg.common.util.sqlparse.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import net.sf.jsqlparser.statement.select.SelectBody;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* select 查询 sql 的信息
|
||||
*/
|
||||
@Data
|
||||
public class SelectSqlInfo {
|
||||
|
||||
/**
|
||||
* 查询的表名,如果是子查询,则此处为null
|
||||
*/
|
||||
private String fromTableName;
|
||||
/**
|
||||
* 表别名
|
||||
*/
|
||||
private String fromTableAliasName;
|
||||
/**
|
||||
* 通过子查询获取的表信息,例如:select name from (select * from user) u
|
||||
* 如果不是子查询,则为null
|
||||
*/
|
||||
private SelectSqlInfo fromSubSelect;
|
||||
/**
|
||||
* 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
||||
*/
|
||||
private Set<String> selectFields;
|
||||
/**
|
||||
* 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
||||
*/
|
||||
private Set<String> realSelectFields;
|
||||
/**
|
||||
* 是否是查询所有字段
|
||||
*/
|
||||
private boolean selectAll;
|
||||
|
||||
/**
|
||||
* 解析之后的 SQL (关键字都是大写)
|
||||
*/
|
||||
private final String parsedSql;
|
||||
|
||||
public SelectSqlInfo(String parsedSql) {
|
||||
this.parsedSql = parsedSql;
|
||||
}
|
||||
|
||||
public SelectSqlInfo(SelectBody selectBody) {
|
||||
this.parsedSql = selectBody.toString();
|
||||
}
|
||||
|
||||
public void addSelectField(String selectField, String realSelectField) {
|
||||
if (this.selectFields == null) {
|
||||
this.selectFields = new HashSet<>();
|
||||
}
|
||||
if (this.realSelectFields == null) {
|
||||
this.realSelectFields = new HashSet<>();
|
||||
}
|
||||
this.selectFields.add(selectField);
|
||||
this.realSelectFields.add(realSelectField);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字段,包括子查询里的。
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Set<String> getAllRealSelectFields() {
|
||||
Set<String> fields = new HashSet<>();
|
||||
// 递归获取所有字段,起个直观的方法名为:
|
||||
this.recursiveGetAllFields(this, fields);
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取所有字段
|
||||
*/
|
||||
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
||||
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
||||
fields.addAll(sqlInfo.getRealSelectFields());
|
||||
}
|
||||
if (sqlInfo.getFromSubSelect() != null) {
|
||||
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SelectSqlInfo{" +
|
||||
"fromTableName='" + fromTableName + '\'' +
|
||||
", fromSubSelect=" + fromSubSelect +
|
||||
", aliasName='" + fromTableAliasName + '\'' +
|
||||
", selectFields=" + selectFields +
|
||||
", realSelectFields=" + realSelectFields +
|
||||
", selectAll=" + selectAll +
|
||||
"}";
|
||||
}
|
||||
|
||||
}
|
||||
//package org.jeecg.common.util.sqlparse.vo;
|
||||
//
|
||||
//import lombok.Data;
|
||||
//import net.sf.jsqlparser.statement.select.SelectBody;
|
||||
//
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Set;
|
||||
//
|
||||
///**
|
||||
// * select 查询 sql 的信息
|
||||
// */
|
||||
//@Data
|
||||
//public class SelectSqlInfo {
|
||||
//
|
||||
// /**
|
||||
// * 查询的表名,如果是子查询,则此处为null
|
||||
// */
|
||||
// private String fromTableName;
|
||||
// /**
|
||||
// * 表别名
|
||||
// */
|
||||
// private String fromTableAliasName;
|
||||
// /**
|
||||
// * 通过子查询获取的表信息,例如:select name from (select * from user) u
|
||||
// * 如果不是子查询,则为null
|
||||
// */
|
||||
// private SelectSqlInfo fromSubSelect;
|
||||
// /**
|
||||
// * 查询的字段集合,如果是 * 则为null,如果设了别名则为别名
|
||||
// */
|
||||
// private Set<String> selectFields;
|
||||
// /**
|
||||
// * 真实的查询字段集合,如果是 * 则为null,如果设了别名则为原始字段名
|
||||
// */
|
||||
// private Set<String> realSelectFields;
|
||||
// /**
|
||||
// * 是否是查询所有字段
|
||||
// */
|
||||
// private boolean selectAll;
|
||||
//
|
||||
// /**
|
||||
// * 解析之后的 SQL (关键字都是大写)
|
||||
// */
|
||||
// private final String parsedSql;
|
||||
//
|
||||
// public SelectSqlInfo(String parsedSql) {
|
||||
// this.parsedSql = parsedSql;
|
||||
// }
|
||||
//
|
||||
// public SelectSqlInfo(SelectBody selectBody) {
|
||||
// this.parsedSql = selectBody.toString();
|
||||
// }
|
||||
//
|
||||
// public void addSelectField(String selectField, String realSelectField) {
|
||||
// if (this.selectFields == null) {
|
||||
// this.selectFields = new HashSet<>();
|
||||
// }
|
||||
// if (this.realSelectFields == null) {
|
||||
// this.realSelectFields = new HashSet<>();
|
||||
// }
|
||||
// this.selectFields.add(selectField);
|
||||
// this.realSelectFields.add(realSelectField);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 获取所有字段,包括子查询里的。
|
||||
// *
|
||||
// * @return
|
||||
// */
|
||||
// public Set<String> getAllRealSelectFields() {
|
||||
// Set<String> fields = new HashSet<>();
|
||||
// // 递归获取所有字段,起个直观的方法名为:
|
||||
// this.recursiveGetAllFields(this, fields);
|
||||
// return fields;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 递归获取所有字段
|
||||
// */
|
||||
// private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
|
||||
// if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
|
||||
// fields.addAll(sqlInfo.getRealSelectFields());
|
||||
// }
|
||||
// if (sqlInfo.getFromSubSelect() != null) {
|
||||
// recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public String toString() {
|
||||
// return "SelectSqlInfo{" +
|
||||
// "fromTableName='" + fromTableName + '\'' +
|
||||
// ", fromSubSelect=" + fromSubSelect +
|
||||
// ", aliasName='" + fromTableAliasName + '\'' +
|
||||
// ", selectFields=" + selectFields +
|
||||
// ", realSelectFields=" + realSelectFields +
|
||||
// ", selectAll=" + selectAll +
|
||||
// "}";
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@ -3,13 +3,14 @@ package org.jeecg.config;
|
||||
import org.jeecgframework.core.util.ApplicationContextUtil;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
/**
|
||||
* @Author: Scott
|
||||
* @Date: 2018/2/7
|
||||
* @description: autopoi 配置类
|
||||
*/
|
||||
|
||||
@Lazy(false)
|
||||
@Configuration
|
||||
public class AutoPoiConfig {
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
* @Version:1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Lazy(false)
|
||||
@Service
|
||||
public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
||||
final static String EXCEL_SPLIT_TAG = "_";
|
||||
@ -59,17 +60,15 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
|
||||
|
||||
|
||||
for (DictModel t : dictList) {
|
||||
//update-begin---author:liusq Date:20230517 for:[issues/4917]excel 导出异常---
|
||||
// 代码逻辑说明: [issues/4917]excel 导出异常---
|
||||
if(t!=null && t.getText()!=null && t.getValue()!=null){
|
||||
//update-end---author:liusq Date:20230517 for:[issues/4917]excel 导出异常---
|
||||
//update-begin---author:scott Date:20211220 for:[issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
||||
// 代码逻辑说明: [issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
||||
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
|
||||
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
|
||||
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
|
||||
}else{
|
||||
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
|
||||
}
|
||||
//update-end---author:20211220 Date:20211220 for:[issues/I4MBB3]@Excel dicText字段的值有下划线时,导入功能不能正确解析---
|
||||
}
|
||||
}
|
||||
if (dictReplaces != null && dictReplaces.size() != 0) {
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.jeecg.config.tencent.JeecgTencent;
|
||||
import org.jeecg.config.vo.*;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
@ -11,12 +16,17 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component("jeecgBaseConfig")
|
||||
@ConfigurationProperties(prefix = "jeecg")
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public class JeecgBaseConfig {
|
||||
/**
|
||||
* 签名密钥串(字典等敏感接口)
|
||||
* @TODO 降低使用成本加的默认值,实际以 yml配置 为准
|
||||
*/
|
||||
private String signatureSecret = "dd05f1c54d63749eda95f9fa6d49v442a";
|
||||
/**
|
||||
* 自定义后台资源前缀,解决表单设计器无法通过前端nginx转发访问
|
||||
*/
|
||||
private String customResourcePrefixPath;
|
||||
/**
|
||||
* 需要加强校验的接口清单
|
||||
*/
|
||||
@ -68,6 +78,42 @@ public class JeecgBaseConfig {
|
||||
*/
|
||||
private BaiduApi baiduApi;
|
||||
|
||||
/**
|
||||
* minio配置
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private JeecgMinio minio;
|
||||
|
||||
/**
|
||||
* oss配置
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private JeecgOSS oss;
|
||||
|
||||
/**
|
||||
* 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private String smsSendType = "aliyun";
|
||||
|
||||
/**
|
||||
* 腾讯配置
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
private JeecgTencent tencent;
|
||||
|
||||
public String getCustomResourcePrefixPath() {
|
||||
return customResourcePrefixPath;
|
||||
}
|
||||
|
||||
public void setCustomResourcePrefixPath(String customResourcePrefixPath) {
|
||||
this.customResourcePrefixPath = customResourcePrefixPath;
|
||||
}
|
||||
|
||||
public Elasticsearch getElasticsearch() {
|
||||
return elasticsearch;
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import org.jeecg.config.vo.GaoDeApi;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
/**
|
||||
* 高德账号配置
|
||||
*/
|
||||
@Lazy(false)
|
||||
@Configuration("jeecgGaodeBaseConfig")
|
||||
@ConfigurationProperties(prefix = "jeecg.jmreport")
|
||||
public class JeecgGaodeBaseConfig {
|
||||
|
||||
/**
|
||||
* 高德开放API配置
|
||||
*/
|
||||
private GaoDeApi gaoDeApi;
|
||||
|
||||
public GaoDeApi getGaoDeApi() {
|
||||
return gaoDeApi;
|
||||
}
|
||||
|
||||
public void setGaoDeApi(GaoDeApi gaoDeApi) {
|
||||
this.gaoDeApi = gaoDeApi;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 短信模板
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2024/11/5 下午3:44
|
||||
*/
|
||||
@Data
|
||||
@Component("jeecgSmsTemplateConfig")
|
||||
@ConfigurationProperties(prefix = "jeecg.oss.sms-template")
|
||||
public class JeecgSmsTemplateConfig {
|
||||
|
||||
/**
|
||||
* 短信签名
|
||||
*/
|
||||
private String signature;
|
||||
|
||||
|
||||
/**
|
||||
* 短信模板code
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Map<String,String> templateCode;
|
||||
|
||||
}
|
||||
@ -2,12 +2,14 @@ package org.jeecg.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 设置静态参数初始化
|
||||
* @author: jeecg-boot
|
||||
*/
|
||||
@Lazy(false)
|
||||
@Component
|
||||
@Data
|
||||
public class StaticConfig {
|
||||
@ -21,6 +23,12 @@ public class StaticConfig {
|
||||
@Value(value = "${spring.mail.username:}")
|
||||
private String emailFrom;
|
||||
|
||||
/**
|
||||
* 是否开启定时发送
|
||||
*/
|
||||
@Value(value = "${spring.mail.timeJobSend:false}")
|
||||
private Boolean timeJobSend;
|
||||
|
||||
// /**
|
||||
// * 签名密钥串
|
||||
// */
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//package org.jeecg.config;
|
||||
//
|
||||
//
|
||||
//import io.swagger.annotations.ApiOperation;
|
||||
//import io.swagger.v3.oas.annotations.Operation;
|
||||
//import org.jeecg.common.constant.CommonConstant;
|
||||
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
//import org.springframework.beans.BeansException;
|
||||
@ -95,13 +95,11 @@
|
||||
// List<Parameter> pars = new ArrayList<>();
|
||||
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||
// pars.add(tokenPar.build());
|
||||
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
|
||||
// ParameterBuilder tenantPar = new ParameterBuilder();
|
||||
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
|
||||
// pars.add(tenantPar.build());
|
||||
// }
|
||||
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时,全局参数增加租户id
|
||||
//
|
||||
// return pars;
|
||||
// }
|
||||
|
||||
@ -3,22 +3,68 @@ package org.jeecg.config;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Paths;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springdoc.core.customizers.OperationCustomizer;
|
||||
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @author eightmonth
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "knife4j", name = "production", havingValue = "false", matchIfMissing = true)
|
||||
@PropertySource("classpath:config/default-spring-doc.properties")
|
||||
public class Swagger3Config implements WebMvcConfigurer {
|
||||
/**
|
||||
|
||||
// 路径匹配结果缓存,避免重复计算
|
||||
private static final Map<String, Boolean> EXCLUDED_PATHS_CACHE = new ConcurrentHashMap<>();
|
||||
// 定义不需要注入安全要求的路径集合
|
||||
private static final Set<String> excludedPaths = new HashSet<>(Arrays.asList(
|
||||
"/sys/randomImage/**",
|
||||
"/sys/login",
|
||||
"/sys/phoneLogin",
|
||||
"/sys/mLogin",
|
||||
"/sys/sms",
|
||||
"/sys/cas/client/validateLogin",
|
||||
"/test/jeecgDemo/demo3",
|
||||
"/sys/thirdLogin/**",
|
||||
"/sys/user/register"
|
||||
));
|
||||
// 预处理通配符模式,提高匹配效率
|
||||
private static final Set<String> wildcardPatterns = new HashSet<>();
|
||||
private static final Set<String> exactPatterns = new HashSet<>();
|
||||
static {
|
||||
// 初始化时分离精确匹配和通配符匹配
|
||||
for (String pattern : excludedPaths) {
|
||||
if (pattern.endsWith("/**")) {
|
||||
wildcardPatterns.add(pattern.substring(0, pattern.length() - 3));
|
||||
} else {
|
||||
exactPatterns.add(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 显示swagger-ui.html文档展示页,还必须注入swagger资源:
|
||||
*
|
||||
@ -32,28 +78,75 @@ public class Swagger3Config implements WebMvcConfigurer {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi swaggerOpenApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("default")
|
||||
.packagesToScan("org.jeecg")
|
||||
// 剔除以下几个包路径的接口生成文档
|
||||
.packagesToExclude("org.jeecg.modules.drag", "org.jeecg.modules.online", "org.jeecg.modules.jmreport")
|
||||
// 加了Operation注解的方法,才生成接口文档
|
||||
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class))
|
||||
.build();
|
||||
public GlobalOpenApiMethodFilter globalOpenApiMethodFilter() {
|
||||
return method -> method.isAnnotationPresent(Operation.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OperationCustomizer operationCustomizer() {
|
||||
return (operation, handlerMethod) -> {
|
||||
String path = getFullPath(handlerMethod);
|
||||
if (!isExcludedPath(path)) {
|
||||
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
|
||||
}else{
|
||||
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
|
||||
}
|
||||
return operation;
|
||||
};
|
||||
}
|
||||
|
||||
private String getFullPath(HandlerMethod handlerMethod) {
|
||||
StringBuilder fullPath = new StringBuilder();
|
||||
|
||||
// 获取类级别的路径
|
||||
RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
|
||||
if (classMapping != null && classMapping.value().length > 0) {
|
||||
fullPath.append(classMapping.value()[0]);
|
||||
}
|
||||
|
||||
// 获取方法级别的路径
|
||||
RequestMapping methodMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
|
||||
if (methodMapping != null && methodMapping.value().length > 0) {
|
||||
String methodPath = methodMapping.value()[0];
|
||||
// 确保路径正确拼接,处理斜杠
|
||||
if (!fullPath.toString().endsWith("/") && !methodPath.startsWith("/")) {
|
||||
fullPath.append("/");
|
||||
}
|
||||
fullPath.append(methodPath);
|
||||
}
|
||||
|
||||
return fullPath.toString();
|
||||
}
|
||||
|
||||
|
||||
private boolean isExcludedPath(String path) {
|
||||
// 使用缓存避免重复计算
|
||||
return EXCLUDED_PATHS_CACHE.computeIfAbsent(path, p -> {
|
||||
// 精确匹配
|
||||
if (exactPatterns.contains(p)) {
|
||||
return true;
|
||||
}
|
||||
// 通配符匹配
|
||||
return wildcardPatterns.stream().anyMatch(p::startsWith);
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("JeecgBoot 后台服务API接口文档")
|
||||
.version("1.0")
|
||||
.version("3.9.0")
|
||||
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
|
||||
.description( "后台API接口")
|
||||
.description("后台API接口")
|
||||
.termsOfService("NO terms of service")
|
||||
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html"))
|
||||
);
|
||||
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
|
||||
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
|
||||
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
|
||||
new SecurityScheme()
|
||||
.name(CommonConstant.X_ACCESS_TOKEN)
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package org.jeecg.config;
|
||||
|
||||
import io.undertow.server.DefaultByteBufferPool;
|
||||
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
|
||||
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
|
||||
@Override
|
||||
public void customize(UndertowServletWebServerFactory factory) {
|
||||
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
|
||||
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
|
||||
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
|
||||
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -10,21 +10,20 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import io.micrometer.prometheus.PrometheusMeterRegistry;
|
||||
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
@ -40,6 +39,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Spring Boot 2.0 解决跨域问题
|
||||
@ -47,6 +47,7 @@ import java.util.List;
|
||||
* @Author qinfeng
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@ -58,6 +59,14 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
@Autowired(required = false)
|
||||
private PrometheusMeterRegistry prometheusMeterRegistry;
|
||||
|
||||
/**
|
||||
* meterRegistryPostProcessor
|
||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
@Qualifier("meterRegistryPostProcessor")
|
||||
private BeanPostProcessor meterRegistryPostProcessor;
|
||||
|
||||
/**
|
||||
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
|
||||
*/
|
||||
@ -70,6 +79,8 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
|
||||
}
|
||||
resourceHandlerRegistration.addResourceLocations(staticLocations.split(","));
|
||||
// 设置缓存控制标头 Cache-Control有效期为30天
|
||||
resourceHandlerRegistration.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,7 +89,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
*/
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/").setViewName("doc.html");
|
||||
registry.addViewController("/").setViewName("redirect:/doc.html");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -131,7 +142,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
//update-begin---author:chenrui ---date:20240514 for:[QQYUN-9247]系统监控功能优化------------
|
||||
// /**
|
||||
// * SpringBootAdmin的Httptrace不见了
|
||||
// * https://blog.csdn.net/u013810234/article/details/110097201
|
||||
@ -140,15 +150,21 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
|
||||
// return new InMemoryHttpTraceRepository();
|
||||
// }
|
||||
//update-end---author:chenrui ---date:20240514 for:[QQYUN-9247]系统监控功能优化------------
|
||||
|
||||
|
||||
/**
|
||||
* 解决metrics端点不显示jvm信息的问题(zyf)
|
||||
* 在Bean初始化完成后立即配置PrometheusMeterRegistry,避免在Meter注册后才配置MeterFilter
|
||||
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使,接口404
|
||||
* @author chenrui
|
||||
* @date 2025/5/26 16:46
|
||||
*/
|
||||
@Bean
|
||||
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
|
||||
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
|
||||
@PostConstruct
|
||||
public void initPrometheusMeterRegistry() {
|
||||
// 确保在应用启动早期就配置MeterFilter,避免警告
|
||||
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
|
||||
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
|
||||
log.info("PrometheusMeterRegistry 配置完成");
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user