Bladeren bron

no message

BF-202308171519\Administrator 2 jaren geleden
bovenliggende
commit
2c3f8cea92
100 gewijzigde bestanden met toevoegingen van 7057 en 14 verwijderingen
  1. 52 14
      .gitignore
  2. 202 0
      LICENSE
  3. 28 0
      kxs-admin/pom.xml
  4. 155 0
      kxs-admin/src/main/java/com/kxs/admin/controller/SysUserController.java
  5. 31 0
      kxs-app/pom.xml
  6. 56 0
      kxs-common/kxs-common-core/pom.xml
  7. 63 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RedisTemplateConfiguration.java
  8. 20 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RestTemplateConfiguration.java
  9. 55 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/TaskExecutorConfiguration.java
  10. 55 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/WebMvcConfiguration.java
  11. 52 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CacheConstants.java
  12. 83 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CommonConstants.java
  13. 120 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/SecurityConstants.java
  14. 38 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/DictTypeEnum.java
  15. 32 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/ErrorTypeEnum.java
  16. 38 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/LoginTypeEnum.java
  17. 43 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/MenuTypeEnum.java
  18. 37 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/CheckedException.java
  19. 25 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/ValidateCodeException.java
  20. 97 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/ClassUtils.java
  21. 81 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/R.java
  22. 640 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RedisUtils.java
  23. 272 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RetOps.java
  24. 87 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/SpringContextHolder.java
  25. 156 0
      kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/WebUtils.java
  26. 5 0
      kxs-common/kxs-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  27. 17 0
      kxs-common/kxs-common-core/src/main/resources/banner.txt
  28. 72 0
      kxs-common/kxs-common-core/src/main/resources/logback-spring.xml
  29. 53 0
      kxs-common/kxs-common-log/pom.xml
  30. 28 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/LogAutoConfiguration.java
  31. 29 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/annotation/SysLog.java
  32. 81 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/aspect/SysLogAspect.java
  33. 40 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/config/SkyLogProperties.java
  34. 19 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEvent.java
  35. 21 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEventSource.java
  36. 75 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogListener.java
  37. 35 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/init/ApplicationLoggerInitializer.java
  38. 54 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/jackson/SkyJavaTimeModule.java
  39. 38 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/LogTypeEnum.java
  40. 122 0
      kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/SysLogUtils.java
  41. 30 0
      kxs-common/kxs-common-log/src/main/resources/META-INF/spring-configuration-metadata.json
  42. 1 0
      kxs-common/kxs-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  43. 57 0
      kxs-common/kxs-common-mybatis/pom.xml
  44. 54 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/MybatisAutoConfiguration.java
  45. 51 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/base/BaseEntity.java
  46. 96 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/config/MybatisPlusMetaObjectHandler.java
  47. 57 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/handler/JsonLongArrayTypeHandler.java
  48. 57 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/handler/JsonStringArrayTypeHandler.java
  49. 66 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/plugins/SkyPaginationInnerInterceptor.java
  50. 91 0
      kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/resolver/SqlFilterArgumentResolver.java
  51. 1 0
      kxs-common/kxs-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  52. 52 0
      kxs-common/kxs-common-security/pom.xml
  53. 25 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/annotation/EnableSkyResourceServer.java
  54. 29 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/annotation/Inner.java
  55. 42 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/PermissionService.java
  56. 64 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/PermitAllUrlProperties.java
  57. 57 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/ResourceAuthExceptionEntryPoint.java
  58. 106 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyBearerTokenExtractor.java
  59. 40 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyClientCredentialsOAuth2AuthenticatedPrincipal.java
  60. 52 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyCustomOAuth2AccessTokenResponseHttpMessageConverter.java
  61. 83 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyCustomOpaqueTokenIntrospector.java
  62. 58 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyResourceServerAutoConfiguration.java
  63. 60 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyResourceServerConfiguration.java
  64. 50 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkySecurityInnerAspect.java
  65. 30 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkySecurityMessageSourceConfiguration.java
  66. 68 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/model/KxsUser.java
  67. 55 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/service/KxsRedisOAuth2AuthorizationConsentService.java
  68. 143 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/service/KxsRedisOAuth2AuthorizationService.java
  69. 43 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/service/KxsUserDetailsService.java
  70. 81 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/OAuth2EndpointUtils.java
  71. 43 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/OAuth2ErrorCodesExpand.java
  72. 29 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/OAuthClientException.java
  73. 29 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/ScopeException.java
  74. 72 0
      kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/SecurityUtils.java
  75. 8 0
      kxs-common/kxs-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  76. 50 0
      kxs-common/kxs-common-security/src/main/resources/i18n/errors/messages_zh_CN.properties
  77. 35 0
      kxs-common/pom.xml
  78. 43 0
      kxs-system/kxs-system-api/pom.xml
  79. 91 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/dto/SysLogDTO.java
  80. 42 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/dto/UserDTO.java
  81. 35 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/dto/UserInfo.java
  82. 91 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysDict.java
  83. 113 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysDictItem.java
  84. 98 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysFile.java
  85. 140 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysLog.java
  86. 138 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysMenu.java
  87. 137 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysOauthClientDetails.java
  88. 98 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysPost.java
  89. 110 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysPublicParam.java
  90. 78 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysRole.java
  91. 35 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysRoleMenu.java
  92. 159 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysUser.java
  93. 35 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysUserRole.java
  94. 98 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/util/DictResolver.java
  95. 60 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/util/ParamResolver.java
  96. 59 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/PreLogVO.java
  97. 65 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/RoleExcelVO.java
  98. 24 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/RoleVO.java
  99. 28 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/TokenVo.java
  100. 108 0
      kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/UserExcelVO.java

+ 52 - 14
.gitignore

@@ -1,23 +1,61 @@
-# Compiled class file
-*.class
+### gradle ###
+.gradle
+/build/
+!gradle/wrapper/gradle-wrapper.jar
 
-# Log file
-*.log
+### STS ###
+.settings/
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+bin/
+data/
+target/
 
-# BlueJ files
-*.ctxt
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+rebel.xml
 
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
+### NetBeans ###
+nbproject/private/
+build/
+nbbuild/
+nbdist/
+.nb-gradle/
 
-# Package Files #
-*.jar
+### maven ###
+target/
 *.war
-*.nar
 *.ear
 *.zip
+*.tar
 *.tar.gz
-*.rar
+*.versionsBackup
+
+### vscode ###
+.vscode
+
+### logs ###
+/logs/
+*.log
+
+### temp ignore ###
+*.cache
+*.diff
+*.patch
+*.tmp
+*.java~
+*.properties~
+*.xml~
 
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
+### system ignore ###
+.DS_Store
+Thumbs.db
+Servers
+.metadata

+ 202 - 0
LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   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.

+ 28 - 0
kxs-admin/pom.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-admin</artifactId>
+    <packaging>jar</packaging>
+
+    <description>用户权限管理系统业务处理模块</description>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-system-biz</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 155 - 0
kxs-admin/src/main/java/com/kxs/admin/controller/SysUserController.java

@@ -0,0 +1,155 @@
+package com.kxs.admin.controller;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.kxs.common.core.constant.CommonConstants;
+import com.kxs.common.core.util.R;
+import com.kxs.common.log.annotation.SysLog;
+import com.kxs.common.security.annotation.Inner;
+import com.kxs.common.security.util.SecurityUtils;
+import com.kxs.system.api.dto.UserDTO;
+import com.kxs.system.api.model.SysUser;
+import com.kxs.system.biz.service.SysUserService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.AllArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author lengleng
+ * @date 2018/12/16
+ */
+@RestController
+@AllArgsConstructor
+@RequestMapping("/user")
+@Tag(description = "user", name = "用户管理模块")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class SysUserController {
+
+	private final SysUserService userService;
+
+
+	/**
+	 * 获取当前用户全部信息
+	 * @return 用户信息
+	 */
+	@GetMapping(value = { "/info" })
+	public R info() {
+		String username = SecurityUtils.getUser().getUsername();
+		SysUser user = userService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername, username));
+		if (user == null) {
+			return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_QUERY_ERROR));
+		}
+		return R.ok(userService.findUserInfo(user));
+	}
+
+	/**
+	 * 通过ID查询用户信息
+	 * @param id ID
+	 * @return 用户信息
+	 */
+	@GetMapping("/details/{id}")
+	public R user(@PathVariable Long id) {
+		return R.ok(userService.selectUserVoById(id));
+	}
+
+	/**
+	 * 查询用户信息
+	 * @param query 查询条件
+	 * @return 不为空返回用户名
+	 */
+	@Inner(value = false)
+	@GetMapping("/details")
+	public R getDetails(@ParameterObject SysUser query) {
+		SysUser sysUser = userService.getOne(Wrappers.query(query), false);
+		return R.ok(sysUser == null ? null : CommonConstants.SUCCESS);
+	}
+
+	/**
+	 * 删除用户信息
+	 * @param ids ID
+	 * @return R
+	 */
+	@SysLog("删除用户信息")
+	@DeleteMapping
+	@PreAuthorize("@pms.hasPermission('sys_user_del')")
+	@Operation(summary = "删除用户", description = "根据ID删除用户")
+	public R userDel(@RequestBody Long[] ids) {
+		return R.ok(userService.deleteUserByIds(ids));
+	}
+
+	/**
+	 * 添加用户
+	 * @param userDto 用户信息
+	 * @return success/false
+	 */
+	@SysLog("添加用户")
+	@PostMapping
+	@PreAuthorize("@pms.hasPermission('sys_user_add')")
+	public R user(@RequestBody UserDTO userDto) {
+		return R.ok(userService.saveUser(userDto));
+	}
+
+	/**
+	 * 更新用户信息
+	 * @param userDto 用户信息
+	 * @return R
+	 */
+	@SysLog("更新用户信息")
+	@PutMapping
+	@PreAuthorize("@pms.hasPermission('sys_user_edit')")
+	public R updateUser(@Valid @RequestBody UserDTO userDto) {
+		return R.ok(userService.updateUser(userDto));
+	}
+
+	/**
+	 * 分页查询用户
+	 * @param page 参数集
+	 * @param userDTO 查询参数列表
+	 * @return 用户集合
+	 */
+	@GetMapping("/page")
+	public R getUserPage(@ParameterObject Page page, @ParameterObject UserDTO userDTO) {
+		return R.ok(userService.getUsersWithRolePage(page, userDTO));
+	}
+
+	/**
+	 * 修改个人信息
+	 * @param userDto userDto
+	 * @return success/false
+	 */
+	@SysLog("修改个人信息")
+	@PutMapping("/edit")
+	public R updateUserInfo(@Valid @RequestBody UserDTO userDto) {
+		return userService.updateUserInfo(userDto);
+	}
+
+	/**
+	 * 锁定指定用户
+	 * @param username 用户名
+	 * @return R
+	 */
+	@Inner
+	@PutMapping("/lock/{username}")
+	public R lockUser(@PathVariable String username) {
+		return userService.lockUser(username);
+	}
+
+	@PutMapping("/password")
+	public R password(@RequestBody UserDTO userDto) {
+		String username = SecurityUtils.getUser().getUsername();
+		userDto.setUsername(username);
+		return userService.changePassword(userDto);
+	}
+
+	@PostMapping("/check")
+	public R check(String password) {
+		return userService.checkPassword(password);
+	}
+
+}

+ 31 - 0
kxs-app/pom.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-app</artifactId>
+    <packaging>jar</packaging>
+
+    <description>kxs 客户端API模块</description>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 56 - 0
kxs-common/kxs-common-core/pom.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-core</artifactId>
+    <packaging>jar</packaging>
+
+    <description>kxs 公共工具类核心包</description>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--hutool-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+        <!--redis-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!--server-api-->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
+        <!--hibernate-validator-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <!--json模块-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-json</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 63 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RedisTemplateConfiguration.java

@@ -0,0 +1,63 @@
+
+package com.kxs.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.*;
+import org.springframework.data.redis.serializer.RedisSerializer;
+
+
+/**
+ * Redis 配置类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@EnableCaching
+@AutoConfiguration
+@AutoConfigureBefore(RedisAutoConfiguration.class)
+public class RedisTemplateConfiguration {
+
+	@Bean
+	@Primary
+	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+		redisTemplate.setKeySerializer(RedisSerializer.string());
+		redisTemplate.setHashKeySerializer(RedisSerializer.string());
+		redisTemplate.setValueSerializer(RedisSerializer.java());
+		redisTemplate.setHashValueSerializer(RedisSerializer.java());
+		redisTemplate.setConnectionFactory(factory);
+		return redisTemplate;
+	}
+
+	@Bean
+	public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForHash();
+	}
+
+	@Bean
+	public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
+		return redisTemplate.opsForValue();
+	}
+
+	@Bean
+	public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForList();
+	}
+
+	@Bean
+	public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForSet();
+	}
+
+	@Bean
+	public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
+		return redisTemplate.opsForZSet();
+	}
+
+}

+ 20 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/RestTemplateConfiguration.java

@@ -0,0 +1,20 @@
+
+package com.kxs.common.core.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author lengleng
+ * @date 2023/2/1 RestTemplate
+ */
+@AutoConfiguration
+public class RestTemplateConfiguration {
+
+	@Bean
+	public RestTemplate restTemplate() {
+		return new RestTemplate();
+	}
+
+}

+ 55 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/TaskExecutorConfiguration.java

@@ -0,0 +1,55 @@
+package com.kxs.common.core.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * @author lengleng
+ * @date 2022/5/20
+ */
+@AutoConfiguration
+public class TaskExecutorConfiguration implements AsyncConfigurer {
+
+	/**
+	 * 获取当前机器的核数, 不一定准确 请根据实际场景 CPU密集 || IO 密集
+	 */
+	public static final int cpuNum = Runtime.getRuntime().availableProcessors();
+
+	@Value("${thread.pool.corePoolSize:}")
+	private Optional<Integer> corePoolSize;
+
+	@Value("${thread.pool.maxPoolSize:}")
+	private Optional<Integer> maxPoolSize;
+
+	@Value("${thread.pool.queueCapacity:}")
+	private Optional<Integer> queueCapacity;
+
+	@Value("${thread.pool.awaitTerminationSeconds:}")
+	private Optional<Integer> awaitTerminationSeconds;
+
+	@Override
+	@Bean
+	public Executor getAsyncExecutor() {
+		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+		// 核心线程大小 默认区 CPU 数量
+		taskExecutor.setCorePoolSize(corePoolSize.orElse(cpuNum));
+		// 最大线程大小 默认区 CPU * 2 数量
+		taskExecutor.setMaxPoolSize(maxPoolSize.orElse(cpuNum * 2));
+		// 队列最大容量
+		taskExecutor.setQueueCapacity(queueCapacity.orElse(500));
+		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+		taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
+		taskExecutor.setAwaitTerminationSeconds(awaitTerminationSeconds.orElse(60));
+		taskExecutor.setThreadNamePrefix("PIG-Thread-");
+		taskExecutor.initialize();
+		return taskExecutor;
+	}
+
+}

+ 55 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/config/WebMvcConfiguration.java

@@ -0,0 +1,55 @@
+
+package com.kxs.common.core.config;
+
+import cn.hutool.core.date.DatePattern;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.format.FormatterRegistry;
+import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;
+
+/**
+ * @author lengleng
+ * @date 2023-06-24
+ * <p>
+ * 注入自自定义SQL 过滤
+ */
+@AutoConfiguration
+@ConditionalOnWebApplication(type = SERVLET)
+public class WebMvcConfiguration implements WebMvcConfigurer {
+
+	/**
+	 * 增加GET请求参数中时间类型转换 {@link com.kxs.common.core.jackson.SkyJavaTimeModule}
+	 * <ul>
+	 * <li>HH:mm:ss -> LocalTime</li>
+	 * <li>yyyy-MM-dd -> LocalDate</li>
+	 * <li>yyyy-MM-dd HH:mm:ss -> LocalDateTime</li>
+	 * </ul>
+	 * @param registry
+	 */
+	@Override
+	public void addFormatters(FormatterRegistry registry) {
+		DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
+		registrar.setTimeFormatter(DatePattern.NORM_TIME_FORMATTER);
+		registrar.setDateFormatter(DatePattern.NORM_DATE_FORMATTER);
+		registrar.setDateTimeFormatter(DatePattern.NORM_DATETIME_FORMATTER);
+		registrar.registerFormatters(registry);
+	}
+
+	/**
+	 * 系统国际化文件配置
+	 * @return MessageSource
+	 */
+	@Bean
+	public MessageSource messageSource() {
+		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
+		messageSource.setBasename("classpath:i18n/messages");
+		return messageSource;
+	}
+
+}

+ 52 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CacheConstants.java

@@ -0,0 +1,52 @@
+
+package com.kxs.common.core.constant;
+
+/**
+ * @author lengleng
+ * @date 2020年01月01日
+ * <p>
+ * 缓存的key 常量
+ */
+public interface CacheConstants {
+
+	/**
+	 * oauth 缓存前缀
+	 */
+	String PROJECT_OAUTH_ACCESS = "token::access_token";
+
+	/**
+	 * 验证码前缀
+	 */
+	String DEFAULT_CODE_KEY = "DEFAULT_CODE_KEY:";
+
+	/**
+	 * 菜单信息缓存
+	 */
+	String MENU_DETAILS = "menu_details";
+
+	/**
+	 * 用户信息缓存
+	 */
+	String USER_DETAILS = "user_details";
+
+	/**
+	 * 字典信息缓存
+	 */
+	String DICT_DETAILS = "dict_details";
+
+	/**
+	 * 角色信息缓存
+	 */
+	String ROLE_DETAILS = "role_details";
+
+	/**
+	 * oauth 客户端信息
+	 */
+	String CLIENT_DETAILS_KEY = "client:details";
+
+	/**
+	 * 参数缓存
+	 */
+	String PARAMS_DETAILS = "params_details";
+
+}

+ 83 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/CommonConstants.java

@@ -0,0 +1,83 @@
+
+package com.kxs.common.core.constant;
+
+
+/**
+ * 公共常量
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+public interface CommonConstants {
+
+	/**
+	 * 删除
+	 */
+	String STATUS_DEL = "1";
+
+	/**
+	 * 正常
+	 */
+	String STATUS_NORMAL = "0";
+
+	/**
+	 * 锁定
+	 */
+	String STATUS_LOCK = "9";
+
+	/**
+	 * 菜单树根节点
+	 */
+	Long MENU_TREE_ROOT_ID = -1L;
+
+	/**
+	 * 菜单
+	 */
+	String MENU = "0";
+
+	/**
+	 * 编码
+	 */
+	String UTF8 = "UTF-8";
+
+	/**
+	 * JSON 资源
+	 */
+	String CONTENT_TYPE = "application/json; charset=utf-8";
+
+	/**
+	 * 前端工程名
+	 */
+	String FRONT_END_PROJECT = "kxs-ui";
+
+	/**
+	 * 后端工程名
+	 */
+	String BACK_END_PROJECT = "kxs";
+
+	/**
+	 * 成功标记
+	 */
+	Integer SUCCESS = 0;
+
+	/**
+	 * 失败标记
+	 */
+	Integer FAIL = 1;
+
+	/**
+	 * 当前页
+	 */
+	String CURRENT = "current";
+
+	/**
+	 * size
+	 */
+	String SIZE = "size";
+
+	/**
+	 * 请求开始时间
+	 */
+	String REQUEST_START_TIME = "REQUEST-START-TIME";
+
+}

+ 120 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/SecurityConstants.java

@@ -0,0 +1,120 @@
+
+package com.kxs.common.core.constant;
+
+/**
+ * @author lengleng
+ * @date 2023/2/1
+ */
+public interface SecurityConstants {
+
+	/**
+	 * 角色前缀
+	 */
+	String ROLE = "ROLE_";
+
+	/**
+	 * 前缀
+	 */
+	String PROJECT_PREFIX = "kxs";
+
+	/**
+	 * 内部
+	 */
+	String FROM_IN = "Y";
+
+	/**
+	 * 标志
+	 */
+	String FROM = "from";
+
+	/**
+	 * 请求header
+	 */
+	String HEADER_FROM_IN = FROM + "=" + FROM_IN;
+
+	/**
+	 * 默认登录URL
+	 */
+	String OAUTH_TOKEN_URL = "/oauth2/token";
+
+	/**
+	 * grant_type
+	 */
+	String REFRESH_TOKEN = "refresh_token";
+
+	/**
+	 * 手机号登录
+	 */
+	String MOBILE = "mobile";
+
+	/**
+	 * 时空客户端
+	 */
+	String KXS = "kxs";
+
+	/**
+	 * {bcrypt} 加密的特征码
+	 */
+	String BCRYPT = "{bcrypt}";
+
+	/**
+	 * {noop} 加密的特征码
+	 */
+	String NOOP = "{noop}";
+
+	/***
+	 * 资源服务器默认bean名称
+	 */
+	String RESOURCE_SERVER_CONFIGURER = "resourceServerConfigurerAdapter";
+
+	/**
+	 * 用户名
+	 */
+	String USERNAME = "username";
+
+	/**
+	 * 用户信息
+	 */
+	String DETAILS_USER = "user_info";
+
+	/**
+	 * 用户ID
+	 */
+	String DETAILS_USER_ID = "user_id";
+
+	/**
+	 * 协议字段
+	 */
+	String DETAILS_LICENSE = "license";
+
+	/**
+	 * 验证码有效期,默认 60秒
+	 */
+	long CODE_TIME = 60;
+
+	/**
+	 * 验证码长度
+	 */
+	String CODE_SIZE = "6";
+
+	/**
+	 * 客户端模式
+	 */
+	String CLIENT_CREDENTIALS = "client_credentials";
+
+	/**
+	 * 客户端ID
+	 */
+	String CLIENT_ID = "clientId";
+
+	/**
+	 * 短信登录 参数名称
+	 */
+	String SMS_PARAMETER_NAME = "mobile";
+
+	/**
+	 * 授权码模式confirm
+	 */
+	String CUSTOM_CONSENT_PAGE_URI = "/token/confirm_access";
+
+}

+ 38 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/DictTypeEnum.java

@@ -0,0 +1,38 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+
+/**
+ * <p>
+ * 字典类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum DictTypeEnum {
+
+	/**
+	 * 字典类型-系统内置(不可修改)
+	 */
+	SYSTEM("1", "系统内置"),
+
+	/**
+	 * 字典类型-业务类型
+	 */
+	BIZ("0", "业务类");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 32 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/ErrorTypeEnum.java

@@ -0,0 +1,32 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 错误类型枚举
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+@Getter
+@RequiredArgsConstructor
+public enum ErrorTypeEnum {
+
+    TOKEN_EXPIRED(-1, "令牌已过期"),
+    CLIENT_NULL(-1, "客户端查询异常,请检查数据库链接"),
+
+
+
+    SYS_PARAM_DELETE_SYSTEM(-1, "系统内置参数不能删除"),
+    SYS_DICT_DELETE_SYSTEM(-1, "系统内置字典不允许删除"),
+    SYS_MENU_DELETE_EXISTING(-1, "菜单存在下级节点 删除失败"),
+    SYS_USER_USERNAME_EXISTING(-1, "用户名已存在"),
+    SYS_USER_UPDATE_PASSWORD_ERROR(-1, "用户原密码错误"),
+    SYS_DICT_UPDATE_SYSTEM(-1, "系统内置字典不能修改");
+
+
+    private final int code;
+
+    private final String description;
+}

+ 38 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/LoginTypeEnum.java

@@ -0,0 +1,38 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+
+/**
+ * <p>
+ * 社交登录类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum LoginTypeEnum {
+
+	/**
+	 * 账号密码登录
+	 */
+	PWD("PWD", "账号密码登录"),
+
+	/**
+	 * 验证码登录
+	 */
+	SMS("SMS", "验证码登录");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 43 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/constant/enums/MenuTypeEnum.java

@@ -0,0 +1,43 @@
+package com.kxs.common.core.constant.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+
+/**
+ * <p>
+ * 菜单类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Getter
+@RequiredArgsConstructor
+public enum MenuTypeEnum {
+
+	/**
+	 * 左侧菜单
+	 */
+	LEFT_MENU("0", "left"),
+
+	/**
+	 * 顶部菜单
+	 */
+	TOP_MENU("2", "top"),
+
+	/**
+	 * 按钮
+	 */
+	BUTTON("1", "button");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 37 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/CheckedException.java

@@ -0,0 +1,37 @@
+
+package com.kxs.common.core.exception;
+
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+
+
+/**
+ * 检查异常
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@NoArgsConstructor
+public class CheckedException extends RuntimeException {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	public CheckedException(String message) {
+		super(message);
+	}
+
+	public CheckedException(Throwable cause) {
+		super(cause);
+	}
+
+	public CheckedException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public CheckedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+		super(message, cause, enableSuppression, writableStackTrace);
+	}
+
+}

+ 25 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/exception/ValidateCodeException.java

@@ -0,0 +1,25 @@
+
+package com.kxs.common.core.exception;
+
+
+import java.io.Serial;
+
+/**
+ * 验证代码异常
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+public class ValidateCodeException extends RuntimeException {
+
+	@Serial
+	private static final long serialVersionUID = -7285211528095468156L;
+
+	public ValidateCodeException() {
+	}
+
+	public ValidateCodeException(String msg) {
+		super(msg);
+	}
+
+}

+ 97 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/ClassUtils.java

@@ -0,0 +1,97 @@
+
+package com.kxs.common.core.util;
+
+import lombok.experimental.UtilityClass;
+import org.springframework.core.BridgeMethodResolver;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
+import org.springframework.web.method.HandlerMethod;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+
+/**
+ * 类工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@UtilityClass
+public class ClassUtils extends org.springframework.util.ClassUtils {
+
+	private final ParameterNameDiscoverer PARAMETERNAMEDISCOVERER = new DefaultParameterNameDiscoverer();
+
+	/**
+	 * 获取方法参数信息
+	 * @param constructor 构造器
+	 * @param parameterIndex 参数序号
+	 * @return {MethodParameter}
+	 */
+	public MethodParameter getMethodParameter(Constructor<?> constructor, int parameterIndex) {
+		MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex);
+		methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
+		return methodParameter;
+	}
+
+	/**
+	 * 获取方法参数信息
+	 * @param method 方法
+	 * @param parameterIndex 参数序号
+	 * @return {MethodParameter}
+	 */
+	public MethodParameter getMethodParameter(Method method, int parameterIndex) {
+		MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex);
+		methodParameter.initParameterNameDiscovery(PARAMETERNAMEDISCOVERER);
+		return methodParameter;
+	}
+
+	/**
+	 * 获取Annotation
+	 * @param method Method
+	 * @param annotationType 注解类
+	 * @param <A> 泛型标记
+	 * @return {Annotation}
+	 */
+	public <A extends Annotation> A getAnnotation(Method method, Class<A> annotationType) {
+		Class<?> targetClass = method.getDeclaringClass();
+		// The method may be on an interface, but we need attributes from the target
+		// class.
+		// If the target class is null, the method will be unchanged.
+		Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
+		// If we are dealing with method with generic parameters, find the original
+		// method.
+		specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
+		// 先找方法,再找方法上的类
+		A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType);
+		;
+		if (null != annotation) {
+			return annotation;
+		}
+		// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
+		return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType);
+	}
+
+	/**
+	 * 获取Annotation
+	 * @param handlerMethod HandlerMethod
+	 * @param annotationType 注解类
+	 * @param <A> 泛型标记
+	 * @return {Annotation}
+	 */
+	public <A extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
+		// 先找方法,再找方法上的类
+		A annotation = handlerMethod.getMethodAnnotation(annotationType);
+		if (null != annotation) {
+			return annotation;
+		}
+		// 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类
+		Class<?> beanType = handlerMethod.getBeanType();
+		return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType);
+	}
+
+}

+ 81 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/R.java

@@ -0,0 +1,81 @@
+package com.kxs.common.core.util;
+
+import com.kxs.common.core.constant.CommonConstants;
+import com.kxs.common.core.constant.enums.ErrorTypeEnum;
+import lombok.*;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 响应信息主体
+ *
+ * @param <T>
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+@ToString
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class R<T> implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	@Getter
+	@Setter
+	private int code;
+
+	@Getter
+	@Setter
+	private String msg;
+
+	@Getter
+	@Setter
+	private T data;
+
+	public static <T> R<T> ok() {
+		return restResult(null, CommonConstants.SUCCESS, null);
+	}
+
+	public static <T> R<T> ok(T data) {
+		return restResult(data, CommonConstants.SUCCESS, null);
+	}
+
+	public static <T> R<T> ok(T data, String msg) {
+		return restResult(data, CommonConstants.SUCCESS, msg);
+	}
+
+	public static <T> R<T> failed() {
+		return restResult(null, CommonConstants.FAIL, null);
+	}
+
+	public static <T> R<T> failed(ErrorTypeEnum errorTypeEnum) {
+		return restResult(null, errorTypeEnum.getCode(), errorTypeEnum.getDescription());
+	}
+
+	public static <T> R<T> failed(String msg) {
+		return restResult(null, CommonConstants.FAIL, msg);
+	}
+
+	public static <T> R<T> failed(T data) {
+		return restResult(data, CommonConstants.FAIL, null);
+	}
+
+	public static <T> R<T> failed(T data, String msg) {
+		return restResult(data, CommonConstants.FAIL, msg);
+	}
+
+	public static <T> R<T> restResult(T data, int code, String msg) {
+		R<T> apiResult = new R<>();
+		apiResult.setCode(code);
+		apiResult.setData(data);
+		apiResult.setMsg(msg);
+		return apiResult;
+	}
+
+}

+ 640 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RedisUtils.java

@@ -0,0 +1,640 @@
+package com.kxs.common.core.util;
+
+import cn.hutool.core.convert.Convert;
+import lombok.experimental.UtilityClass;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.*;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * 缓存工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@UtilityClass
+public class RedisUtils {
+
+	private static final Long SUCCESS = 1L;
+
+	/**
+	 * 指定缓存失效时间
+	 * @param key 键
+	 * @param time 时间(秒)
+	 */
+	public boolean expire(String key, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate)
+			.filter(template -> time > 0)
+			.ifPresent(template -> template.expire(key, time, TimeUnit.SECONDS));
+		return true;
+	}
+
+	/**
+	 * 根据 key 获取过期时间
+	 * @param key 键 不能为null
+	 * @return 时间(秒) 返回0代表为永久有效
+	 */
+	public long getExpire(Object key) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate)
+			.map(template -> template.getExpire(key, TimeUnit.SECONDS))
+			.orElse(-1L);
+	}
+
+	/**
+	 * 查找匹配key
+	 * @param pattern key
+	 * @return /
+	 */
+	public List<String> scan(String pattern) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			RedisConnectionFactory factory = template.getConnectionFactory();
+			RedisConnection rc = Objects.requireNonNull(factory).getConnection();
+			Cursor<byte[]> cursor = rc.scan(options);
+			List<String> result = new ArrayList<>();
+			while (cursor.hasNext()) {
+				result.add(new String(cursor.next()));
+			}
+			RedisConnectionUtils.releaseConnection(rc, factory);
+			return result;
+		}).orElse(Collections.emptyList());
+	}
+
+	/**
+	 * 分页查询 key
+	 * @param patternKey key
+	 * @param page 页码
+	 * @param size 每页数目
+	 * @return /
+	 */
+	public List<String> findKeysForPage(String patternKey, int page, int size) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
+		RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
+		RedisConnection rc = Objects.requireNonNull(factory).getConnection();
+		Cursor<byte[]> cursor = rc.scan(options);
+		List<String> result = new ArrayList<>(size);
+		int tmpIndex = 0;
+		int fromIndex = page * size;
+		int toIndex = page * size + size;
+		while (cursor.hasNext()) {
+			if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
+				result.add(new String(cursor.next()));
+				tmpIndex++;
+				continue;
+			}
+			// 获取到满足条件的数据后,就可以退出了
+			if (tmpIndex >= toIndex) {
+				break;
+			}
+			tmpIndex++;
+			cursor.next();
+		}
+		RedisConnectionUtils.releaseConnection(rc, factory);
+		return result;
+	}
+
+	/**
+	 * 判断key是否存在
+	 * @param key 键
+	 * @return true 存在 false不存在
+	 */
+	public boolean hasKey(String key) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> template.hasKey(key)).orElse(false);
+	}
+
+	/**
+	 * 删除缓存
+	 * @param keys 可以传一个值 或多个
+	 */
+	public void del(String... keys) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(keys)
+			.map(Arrays::asList)
+			.filter(keysList -> !keysList.isEmpty())
+			.ifPresent(redisTemplate::delete);
+	}
+
+	/**
+	 * 获取锁
+	 * @param lockKey 锁key
+	 * @param value value
+	 * @param expireTime:单位-秒
+	 * @return boolean
+	 */
+	public boolean getLock(String lockKey, String value, int expireTime) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate)
+			.map(template -> template.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS))
+			.orElse(false);
+	}
+
+	/**
+	 * 释放锁
+	 * @param lockKey 锁key
+	 * @param value value
+	 * @return boolean
+	 */
+	public boolean releaseLock(String lockKey, String value) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
+		RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
+		return Optional.ofNullable(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value))
+			.map(Convert::toLong)
+			.filter(SUCCESS::equals)
+			.isPresent();
+	}
+
+	// ============================String=============================
+
+	/**
+	 * 普通缓存获取
+	 * @param key 键
+	 * @return 值
+	 */
+	public <T> T get(String key) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForValue().get(key);
+	}
+
+	/**
+	 * 批量获取
+	 * @param keys
+	 * @return
+	 */
+	public <T> List<T> multiGet(List<String> keys) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForValue().multiGet(keys);
+	}
+
+	/**
+	 * 普通缓存放入
+	 * @param key 键
+	 * @param value 值
+	 * @return true成功 false失败
+	 */
+	public boolean set(String key, Object value) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForValue().set(key, value);
+			return true;
+		});
+		return true;
+	}
+
+	/**
+	 * 普通缓存放入并设置时间
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
+	 * @return true成功 false 失败
+	 */
+	public boolean set(String key, Object value, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			if (time > 0) {
+				template.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+			}
+			else {
+				template.opsForValue().set(key, value);
+			}
+			return true;
+		}).orElse(false);
+	}
+
+	/**
+	 * 普通缓存放入并设置时间
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间
+	 * @param timeUnit 类型
+	 * @return true成功 false 失败
+	 */
+	public <T> boolean set(String key, T value, long time, TimeUnit timeUnit) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			if (time > 0) {
+				template.opsForValue().set(key, value, time, timeUnit);
+			}
+			else {
+				template.opsForValue().set(key, value);
+			}
+			return true;
+		});
+		return true;
+	}
+
+	// ================================Map=================================
+
+	/**
+	 * HashGet
+	 * @param key 键 不能为null
+	 * @param hashKey 项 不能为null
+	 * @return 值
+	 */
+	public <HK, HV> HV hget(String key, HK hashKey) {
+		RedisTemplate<String, HV> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.<HK, HV>opsForHash().get(key, hashKey);
+	}
+
+	/**
+	 * 获取hashKey对应的所有键值
+	 * @param key 键
+	 * @return 对应的多个键值
+	 */
+	public <HK, HV> Map<HK, HV> hmget(String key) {
+		RedisTemplate<String, HV> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.<HK, HV>opsForHash().entries(key);
+	}
+
+	/**
+	 * HashSet
+	 * @param key 键
+	 * @param map 对应多个键值
+	 * @return true 成功 false 失败
+	 */
+	public boolean hmset(String key, Map<String, Object> map) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().putAll(key, map);
+			return true;
+		});
+		return true;
+	}
+
+	/**
+	 * HashSet 并设置时间
+	 * @param key 键
+	 * @param map 对应多个键值
+	 * @param time 时间(秒)
+	 * @return true成功 false失败
+	 */
+	public boolean hmset(String key, Map<String, Object> map, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().putAll(key, map);
+			if (time > 0) {
+				template.expire(key, time, TimeUnit.SECONDS);
+			}
+			return true;
+		});
+		return true;
+	}
+
+	/**
+	 * 向一张hash表中放入数据,如果不存在将创建
+	 * @param key 键
+	 * @param item 项
+	 * @param value 值
+	 * @return true 成功 false失败
+	 */
+	public boolean hset(String key, String item, Object value) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().put(key, item, value);
+			return true;
+		}).orElse(false);
+	}
+
+	/**
+	 * 向一张hash表中放入数据,如果不存在将创建
+	 * @param key 键
+	 * @param item 项
+	 * @param value 值
+	 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
+	 * @return true 成功 false失败
+	 */
+	public boolean hset(String key, String item, Object value, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return Optional.ofNullable(redisTemplate).map(template -> {
+			template.opsForHash().put(key, item, value);
+			if (time > 0) {
+				template.expire(key, time, TimeUnit.SECONDS);
+			}
+			return true;
+		}).orElse(false);
+	}
+
+	/**
+	 * 删除hash表中的值
+	 * @param key 键 不能为null
+	 * @param item 项 可以使多个 不能为null
+	 */
+	public void hdel(String key, Object... item) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForHash().delete(key, item);
+	}
+
+	/**
+	 * 判断hash表中是否有该项的值
+	 * @param key 键 不能为null
+	 * @param item 项 不能为null
+	 * @return true 存在 false不存在
+	 */
+	public boolean hHasKey(String key, String item) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForHash().hasKey(key, item);
+	}
+
+	/**
+	 * hash递增 如果不存在,就会创建一个 并把新增后的值返回
+	 * @param key 键
+	 * @param item 项
+	 * @param by 要增加几(大于0)
+	 * @return
+	 */
+	public double hincr(String key, String item, double by) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForHash().increment(key, item, by);
+	}
+
+	/**
+	 * hash递减
+	 * @param key 键
+	 * @param item 项
+	 * @param by 要减少记(小于0)
+	 * @return
+	 */
+	public double hdecr(String key, String item, double by) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForHash().increment(key, item, -by);
+	}
+
+	// ============================set=============================
+
+	/**
+	 * 根据key获取Set中的所有值
+	 * @param key 键
+	 * @return
+	 */
+	public <T> Set<T> sGet(String key) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().members(key);
+	}
+
+	/**
+	 * 根据value从一个set中查询,是否存在
+	 * @param key 键
+	 * @param value 值
+	 * @return true 存在 false不存在
+	 */
+	public boolean sHasKey(String key, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().isMember(key, value);
+	}
+
+	/**
+	 * 将数据放入set缓存
+	 * @param key 键
+	 * @param values 值 可以是多个
+	 * @return 成功个数
+	 */
+	public long sSet(String key, Object... values) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().add(key, values);
+	}
+
+	/**
+	 * 将set数据放入缓存
+	 * @param key 键
+	 * @param time 时间(秒)
+	 * @param values 值 可以是多个
+	 * @return 成功个数
+	 */
+	public long sSetAndTime(String key, long time, Object... values) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Long count = redisTemplate.opsForSet().add(key, values);
+		if (time > 0) {
+			expire(key, time);
+		}
+		return count;
+	}
+
+	/**
+	 * 获取set缓存的长度
+	 * @param key 键
+	 * @return
+	 */
+	public long sGetSetSize(String key) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().size(key);
+	}
+
+	/**
+	 * 移除值为value的
+	 * @param key 键
+	 * @param values 值 可以是多个
+	 * @return 移除的个数
+	 */
+	public long setRemove(String key, Object... values) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Long count = redisTemplate.opsForSet().remove(key, values);
+		return count;
+	}
+
+	/**
+	 * 获集合key1和集合key2的差集元素
+	 * @param key 键
+	 * @return
+	 */
+	public <T> Set<T> sDifference(String key, String otherKey) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForSet().difference(key, otherKey);
+	}
+
+	// ===============================list=================================
+
+	/**
+	 * 获取list缓存的内容
+	 * @param key 键
+	 * @param start 开始
+	 * @param end 结束 0 到 -1代表所有值
+	 * @return
+	 */
+	public <T> List<T> lGet(String key, long start, long end) {
+		RedisTemplate<String, T> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().range(key, start, end);
+	}
+
+	/**
+	 * 获取list缓存的长度
+	 * @param key 键
+	 * @return
+	 */
+	public long lGetListSize(String key) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().size(key);
+	}
+
+	/**
+	 * 通过索引 获取list中的值
+	 * @param key 键
+	 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
+	 * @return
+	 */
+	public Object lGetIndex(String key, long index) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().index(key, index);
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @return
+	 */
+	public boolean lSet(String key, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPush(key, value);
+		return true;
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间(秒)
+	 * @return
+	 */
+	public boolean lSet(String key, Object value, long time) {
+		RedisTemplate<Object, Object> redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPush(key, value);
+		if (time > 0) {
+			Optional.ofNullable(redisTemplate).ifPresent(template -> template.expire(key, time, TimeUnit.SECONDS));
+		}
+		return true;
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @return
+	 */
+	public boolean lSet(String key, List<Object> value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPushAll(key, value);
+		return true;
+	}
+
+	/**
+	 * 将list放入缓存
+	 * @param key 键
+	 * @param value 值
+	 * @param time 时间(秒)
+	 * @return
+	 */
+	public boolean lSet(String key, List<Object> value, long time) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().rightPushAll(key, value);
+		if (time > 0) {
+			expire(key, time);
+		}
+		return true;
+	}
+
+	/**
+	 * 根据索引修改list中的某条数据
+	 * @param key 键
+	 * @param index 索引
+	 * @param value 值
+	 * @return /
+	 */
+	public boolean lUpdateIndex(String key, long index, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		redisTemplate.opsForList().set(key, index, value);
+		return true;
+	}
+
+	/**
+	 * 移除N个值为value
+	 * @param key 键
+	 * @param count 移除多少个
+	 * @param value 值
+	 * @return 移除的个数
+	 */
+	public long lRemove(String key, long count, Object value) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForList().remove(key, count, value);
+	}
+
+	/**
+	 * 将zSet数据放入缓存
+	 * @param key
+	 * @param time
+	 * @param tuples
+	 * @return
+	 */
+	public long zSetAndTime(String key, long time, Set<ZSetOperations.TypedTuple<Object>> tuples) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		Long count = redisTemplate.opsForZSet().add(key, tuples);
+		if (time > 0) {
+			expire(key, time);
+		}
+		return count;
+
+	}
+
+	/**
+	 * Sorted set:有序集合获取
+	 * @param key
+	 * @param min
+	 * @param max
+	 * @return
+	 */
+	public Set<Object> zRangeByScore(String key, double min, double max) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
+		return zset.rangeByScore(key, min, max);
+
+	}
+
+	/**
+	 * Sorted set:有序集合获取 正序
+	 * @param key
+	 * @param start
+	 * @param end
+	 * @return
+	 */
+	public Set<Object> zRange(String key, long start, long end) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
+		return zset.range(key, start, end);
+
+	}
+
+	/**
+	 * Sorted set:有序集合获取 倒叙
+	 * @param key
+	 * @param start
+	 * @param end
+	 * @return
+	 */
+	public Set<Object> zReverseRange(String key, long start, long end) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
+		return zset.reverseRange(key, start, end);
+
+	}
+
+	/**
+	 * 获取zSet缓存的长度
+	 * @param key 键
+	 * @return
+	 */
+	public long zGetSetSize(String key) {
+		RedisTemplate redisTemplate = SpringContextHolder.getBean(RedisTemplate.class);
+		return redisTemplate.opsForZSet().size(key);
+	}
+
+}

+ 272 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/RetOps.java

@@ -0,0 +1,272 @@
+
+package com.kxs.common.core.util;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.kxs.common.core.constant.CommonConstants;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+
+/**
+ *
+ * 简化{@code R<T>} 的访问操作,例子 <pre>
+ * R<Integer> result = R.ok(0);
+ * // 使用场景1: 链式操作: 断言然后消费
+ * RetOps.of(result)
+ * 		.assertCode(-1,r -> new RuntimeException("error "+r.getCode()))
+ * 		.assertDataNotEmpty(r -> new IllegalStateException("oops!"))
+ * 		.useData(System.out::println);
+ *
+ * // 使用场景2: 读取原始值(data),这里返回的是Optional
+ * RetOps.of(result).getData().orElse(null);
+ *
+ * // 使用场景3: 类型转换
+ * R<String> s = RetOps.of(result)
+ *        .assertDataNotNull(r -> new IllegalStateException("nani??"))
+ *        .map(i -> Integer.toHexString(i))
+ *        .peek();
+ * </pre>
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+public class RetOps<T> {
+
+	/** 状态码为成功 */
+	public static final Predicate<R<?>> CODE_SUCCESS = r -> CommonConstants.SUCCESS == r.getCode();
+
+	/** 数据有值 */
+	public static final Predicate<R<?>> HAS_DATA = r -> ObjectUtil.isNotEmpty(r.getData());
+
+	/** 数据有值,并且包含元素 */
+	public static final Predicate<R<?>> HAS_ELEMENT = r -> ObjectUtil.isNotEmpty(r.getData());
+
+	/** 状态码为成功并且有值 */
+	public static final Predicate<R<?>> DATA_AVAILABLE = CODE_SUCCESS.and(HAS_DATA);
+
+	private final R<T> original;
+
+	// ~ 初始化
+	// ===================================================================================================
+
+	RetOps(R<T> original) {
+		this.original = original;
+	}
+
+	public static <T> RetOps<T> of(R<T> original) {
+		return new RetOps<>(Objects.requireNonNull(original));
+	}
+
+	// ~ 杂项方法
+	// ===================================================================================================
+
+	/**
+	 * 观察原始值
+	 * @return R
+	 */
+	public R<T> peek() {
+		return original;
+	}
+
+	/**
+	 * 读取{@code code}的值
+	 * @return 返回code的值
+	 */
+	public int getCode() {
+		return original.getCode();
+	}
+
+	/**
+	 * 读取{@code data}的值
+	 * @return 返回 Optional 包装的data
+	 */
+	public Optional<T> getData() {
+		return Optional.ofNullable(original.getData());
+	}
+
+	/**
+	 * 有条件地读取{@code data}的值
+	 * @param predicate 断言函数
+	 * @return 返回 Optional 包装的data,如果断言失败返回empty
+	 */
+	public Optional<T> getDataIf(Predicate<? super R<?>> predicate) {
+		return predicate.test(original) ? getData() : Optional.empty();
+	}
+
+	/**
+	 * 读取{@code msg}的值
+	 * @return 返回Optional包装的 msg
+	 */
+	public Optional<String> getMsg() {
+		return Optional.of(original.getMsg());
+	}
+
+	/**
+	 * 对{@code code}的值进行相等性测试
+	 * @param value 基准值
+	 * @return 返回ture表示相等
+	 */
+	public boolean codeEquals(int value) {
+		return original.getCode() == value;
+	}
+
+	/**
+	 * 对{@code code}的值进行相等性测试
+	 * @param value 基准值
+	 * @return 返回ture表示不相等
+	 */
+	public boolean codeNotEquals(int value) {
+		return !codeEquals(value);
+	}
+
+	/**
+	 * 是否成功
+	 * @return 返回ture表示成功
+	 * @see CommonConstants#SUCCESS
+	 */
+	public boolean isSuccess() {
+		return codeEquals(CommonConstants.SUCCESS);
+	}
+
+	/**
+	 * 是否失败
+	 * @return 返回ture表示失败
+	 */
+	public boolean notSuccess() {
+		return !isSuccess();
+	}
+
+	// ~ 链式操作
+	// ===================================================================================================
+
+	/**
+	 * 断言{@code code}的值
+	 * @param expect 预期的值
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertCode(int expect, Function<? super R<T>, ? extends Ex> func)
+			throws Ex {
+		if (codeNotEquals(expect)) {
+			throw func.apply(original);
+		}
+		return this;
+	}
+
+	/**
+	 * 断言成功
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertSuccess(Function<? super R<T>, ? extends Ex> func) throws Ex {
+		return assertCode(CommonConstants.SUCCESS, func);
+	}
+
+	/**
+	 * 断言业务数据有值
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertDataNotNull(Function<? super R<T>, ? extends Ex> func) throws Ex {
+		if (Objects.isNull(original.getData())) {
+			throw func.apply(original);
+		}
+		return this;
+	}
+
+	/**
+	 * 断言业务数据有值,并且包含元素
+	 * @param func 用户函数,负责创建异常对象
+	 * @param <Ex> 异常类型
+	 * @return 返回实例,以便于继续进行链式操作
+	 * @throws Ex 断言失败时抛出
+	 */
+	public <Ex extends Exception> RetOps<T> assertDataNotEmpty(Function<? super R<T>, ? extends Ex> func) throws Ex {
+		if (ObjectUtil.isNotEmpty(original.getData())) {
+			throw func.apply(original);
+		}
+		return this;
+	}
+
+	/**
+	 * 对业务数据(data)转换
+	 * @param mapper 业务数据转换函数
+	 * @param <U> 数据类型
+	 * @return 返回新实例,以便于继续进行链式操作
+	 */
+	public <U> RetOps<U> map(Function<? super T, ? extends U> mapper) {
+		R<U> result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
+		return of(result);
+	}
+
+	/**
+	 * 对业务数据(data)转换
+	 * @param predicate 断言函数
+	 * @param mapper 业务数据转换函数
+	 * @param <U> 数据类型
+	 * @return 返回新实例,以便于继续进行链式操作
+	 * @see RetOps#CODE_SUCCESS
+	 * @see RetOps#HAS_DATA
+	 * @see RetOps#HAS_ELEMENT
+	 * @see RetOps#DATA_AVAILABLE
+	 */
+	public <U> RetOps<U> mapIf(Predicate<? super R<T>> predicate, Function<? super T, ? extends U> mapper) {
+		R<U> result = R.restResult(mapper.apply(original.getData()), original.getCode(), original.getMsg());
+		return of(result);
+	}
+
+	// ~ 数据消费
+	// ===================================================================================================
+
+	/**
+	 * 消费数据,注意此方法保证数据可用
+	 * @param consumer 消费函数
+	 */
+	public void useData(Consumer<? super T> consumer) {
+		consumer.accept(original.getData());
+	}
+
+	/**
+	 * 条件消费(错误代码匹配某个值)
+	 * @param consumer 消费函数
+	 * @param codes 错误代码集合,匹配任意一个则调用消费函数
+	 */
+	public void useDataOnCode(Consumer<? super T> consumer, int... codes) {
+		useDataIf(o -> Arrays.stream(codes).filter(c -> original.getCode() == c).findFirst().isPresent(), consumer);
+	}
+
+	/**
+	 * 条件消费(错误代码表示成功)
+	 * @param consumer 消费函数
+	 */
+	public void useDataIfSuccess(Consumer<? super T> consumer) {
+		useDataIf(CODE_SUCCESS, consumer);
+	}
+
+	/**
+	 * 条件消费
+	 * @param predicate 断言函数
+	 * @param consumer 消费函数,断言函数返回{@code true}时被调用
+	 * @see RetOps#CODE_SUCCESS
+	 * @see RetOps#HAS_DATA
+	 * @see RetOps#HAS_ELEMENT
+	 * @see RetOps#DATA_AVAILABLE
+	 */
+	public void useDataIf(Predicate<? super R<T>> predicate, Consumer<? super T> consumer) {
+		if (predicate.test(original)) {
+			consumer.accept(original.getData());
+		}
+	}
+
+}

+ 87 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/SpringContextHolder.java

@@ -0,0 +1,87 @@
+
+package com.kxs.common.core.util;
+
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+
+/**
+ * Spring 工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Slf4j
+@Service
+@Lazy(false)
+public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
+
+	private static ApplicationContext applicationContext = null;
+
+	/**
+	 * 取得存储在静态变量中的ApplicationContext.
+	 */
+	public static ApplicationContext getApplicationContext() {
+		return applicationContext;
+	}
+
+	/**
+	 * 实现ApplicationContextAware接口, 注入Context到静态变量中.
+	 */
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext) {
+		SpringContextHolder.applicationContext = applicationContext;
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T getBean(String name) {
+		return (T) applicationContext.getBean(name);
+	}
+
+	/**
+	 * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
+	 */
+	public static <T> T getBean(Class<T> requiredType) {
+		return applicationContext.getBean(requiredType);
+	}
+
+	/**
+	 * 清除SpringContextHolder中的ApplicationContext为Null.
+	 */
+	public static void clearHolder() {
+		if (log.isDebugEnabled()) {
+			log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
+		}
+		applicationContext = null;
+	}
+
+	/**
+	 * 发布事件
+	 * @param event 事件
+	 */
+	public static void publishEvent(ApplicationEvent event) {
+		if (applicationContext == null) {
+			return;
+		}
+		applicationContext.publishEvent(event);
+	}
+
+	/**
+	 * 实现DisposableBean接口, 在Context关闭时清理静态变量.
+	 */
+	@Override
+	@SneakyThrows
+	public void destroy() {
+		SpringContextHolder.clearHolder();
+	}
+
+}

+ 156 - 0
kxs-common/kxs-common-core/src/main/java/com/kxs/common/core/util/WebUtils.java

@@ -0,0 +1,156 @@
+
+package com.kxs.common.core.util;
+
+import cn.hutool.core.codec.Base64;
+import com.kxs.common.core.exception.CheckedException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotNull;
+import lombok.SneakyThrows;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.method.HandlerMethod;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.Optional;
+
+
+/**
+ * Miscellaneous utilities for web applications.
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/27
+ */
+@Slf4j
+@UtilityClass
+public class WebUtils extends org.springframework.web.util.WebUtils {
+
+	private final String BASIC_ = "Basic ";
+
+	private final String UNKNOWN = "unknown";
+
+	/**
+	 * 判断是否ajax请求 spring ajax 返回含有 ResponseBody 或者 RestController注解
+	 * @param handlerMethod HandlerMethod
+	 * @return 是否ajax请求
+	 */
+	public boolean isBody(HandlerMethod handlerMethod) {
+		ResponseBody responseBody = ClassUtils.getAnnotation(handlerMethod, ResponseBody.class);
+		return responseBody != null;
+	}
+
+	/**
+	 * 读取cookie
+	 * @param name cookie name
+	 * @return cookie value
+	 */
+	public String getCookieVal(String name) {
+		if (WebUtils.getRequest().isPresent()) {
+			return getCookieVal(WebUtils.getRequest().get(), name);
+		}
+		return null;
+	}
+
+	/**
+	 * 读取cookie
+	 * @param request HttpServletRequest
+	 * @param name cookie name
+	 * @return cookie value
+	 */
+	public String getCookieVal(HttpServletRequest request, String name) {
+		Cookie cookie = getCookie(request, name);
+		return cookie != null ? cookie.getValue() : null;
+	}
+
+	/**
+	 * 清除 某个指定的cookie
+	 * @param response HttpServletResponse
+	 * @param key cookie key
+	 */
+	public void removeCookie(HttpServletResponse response, String key) {
+		setCookie(response, key, null, 0);
+	}
+
+	/**
+	 * 设置cookie
+	 * @param response HttpServletResponse
+	 * @param name cookie name
+	 * @param value cookie value
+	 * @param maxAgeInSeconds maxage
+	 */
+	public void setCookie(HttpServletResponse response, String name, String value, int maxAgeInSeconds) {
+		Cookie cookie = new Cookie(name, value);
+		cookie.setPath("/");
+		cookie.setMaxAge(maxAgeInSeconds);
+		cookie.setHttpOnly(true);
+		response.addCookie(cookie);
+	}
+
+	/**
+	 * 获取 HttpServletRequest
+	 * @return {HttpServletRequest}
+	 */
+	public Optional<HttpServletRequest> getRequest() {
+		return Optional
+			.of(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest());
+	}
+
+	/**
+	 * 获取 HttpServletResponse
+	 * @return {HttpServletResponse}
+	 */
+	public HttpServletResponse getResponse() {
+		return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
+	}
+
+	/**
+	 * 从request 获取CLIENT_ID
+	 * @return
+	 */
+	@SneakyThrows
+	public String getClientId(ServerHttpRequest request) {
+		String header = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+		return splitClient(header)[0];
+	}
+
+	@SneakyThrows
+	public String getClientId() {
+		if (WebUtils.getRequest().isPresent()) {
+			String header = WebUtils.getRequest().get().getHeader(HttpHeaders.AUTHORIZATION);
+			return splitClient(header)[0];
+		}
+		return null;
+	}
+
+	@NotNull
+	private static String[] splitClient(String header) {
+		if (header == null || !header.startsWith(BASIC_)) {
+			throw new CheckedException("请求头中client信息为空");
+		}
+		byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
+		byte[] decoded;
+		try {
+			decoded = Base64.decode(base64Token);
+		}
+		catch (IllegalArgumentException e) {
+			throw new CheckedException("Failed to decode basic authentication token");
+		}
+
+		String token = new String(decoded, StandardCharsets.UTF_8);
+
+		int delim = token.indexOf(":");
+
+		if (delim == -1) {
+			throw new CheckedException("Invalid basic authentication token");
+		}
+		return new String[] { token.substring(0, delim), token.substring(delim + 1) };
+	}
+
+}

+ 5 - 0
kxs-common/kxs-common-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,5 @@
+com.kxs.common.core.config.TaskExecutorConfiguration
+com.kxs.common.core.config.RedisTemplateConfiguration
+com.kxs.common.core.config.RestTemplateConfiguration
+com.kxs.common.core.util.SpringContextHolder
+com.kxs.common.core.config.WebMvcConfiguration

+ 17 - 0
kxs-common/kxs-common-core/src/main/resources/banner.txt

@@ -0,0 +1,17 @@
+${AnsiColor.BRIGHT_YELLOW}
+
+                :::::::::       :::::::::::       ::::::::
+               :+:    :+:          :+:          :+:    :+:
+              +:+    +:+          +:+          +:+
+             +#++:++#+           +#+          :#:
+            +#+                 +#+          +#+   +#+#
+           #+#                 #+#          #+#    #+#
+          ###             ###########       ########
+
+                       www.cdshikongyizhan.com
+
+                  sky Microservice Architecture
+${AnsiColor.DEFAULT}
+
+
+

+ 72 - 0
kxs-common/kxs-common-core/src/main/resources/logback-spring.xml

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright (c) 2020 sky4cloud Authors. All Rights Reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ 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.
+  -->
+
+<configuration debug="false" scan="false">
+	<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>
+	<property name="log.path" value="logs/${spring.application.name}"/>
+	<!-- 彩色日志格式 -->
+	<property name="CONSOLE_LOG_PATTERN"
+			  value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+	<!-- 彩色日志依赖的渲染类 -->
+	<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
+	<conversionRule conversionWord="wex"
+					converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
+	<conversionRule conversionWord="wEx"
+					converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+	<!-- Console log output -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Log file debug output -->
+	<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/debug.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+			<maxFileSize>50MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Log file error output -->
+	<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/error.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+			<maxFileSize>50MB</maxFileSize>
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${CONSOLE_LOG_PATTERN}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<level>ERROR</level>
+		</filter>
+	</appender>
+
+	<!-- Level: FATAL 0  ERROR 3  WARN 4  INFO 6  DEBUG 7 -->
+	<root level="INFO">
+		<appender-ref ref="console"/>
+		<appender-ref ref="debug"/>
+		<appender-ref ref="error"/>
+	</root>
+</configuration>

+ 53 - 0
kxs-common/kxs-common-log/pom.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-log</artifactId>
+    <packaging>jar</packaging>
+
+    <description>kxs 日志服务</description>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--工具类核心包-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-http</artifactId>
+        </dependency>
+        <!--系統模块-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-system-api</artifactId>
+        </dependency>
+        <!--安全依赖获取上下文信息-->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-core</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 28 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/LogAutoConfiguration.java

@@ -0,0 +1,28 @@
+package com.kxs.common.log;
+
+import com.kxs.common.log.aspect.SysLogAspect;
+import com.kxs.common.log.config.SkyLogProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+
+/**
+ * 日志自动配置
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@EnableAsync
+@Configuration(proxyBeanMethods = false)
+@ConditionalOnProperty(value = "security.log.enabled", matchIfMissing = true)
+public class LogAutoConfiguration {
+
+	@Bean
+	public SysLogAspect sysLogAspect() {
+		return new SysLogAspect();
+	}
+
+}

+ 29 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/annotation/SysLog.java

@@ -0,0 +1,29 @@
+package com.kxs.common.log.annotation;
+
+import java.lang.annotation.*;
+
+
+/**
+ * 操作日志注解
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SysLog {
+
+	/**
+	 * 描述
+	 * @return {String}
+	 */
+	String value() default "";
+
+	/**
+	 * spel 表达式
+	 * @return 日志描述
+	 */
+	String expression() default "";
+
+}

+ 81 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/aspect/SysLogAspect.java

@@ -0,0 +1,81 @@
+
+package com.kxs.common.log.aspect;
+
+import cn.hutool.core.util.StrUtil;
+import com.kxs.common.core.util.SpringContextHolder;
+import com.kxs.common.log.event.SysLogEvent;
+import com.kxs.common.log.event.SysLogEventSource;
+import com.kxs.common.log.util.LogTypeEnum;
+import com.kxs.common.log.util.SysLogUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.expression.EvaluationContext;
+
+
+/**
+ * 操作日志使用spring event异步入库
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Aspect
+@Slf4j
+@RequiredArgsConstructor
+public class SysLogAspect {
+
+	@Around("@annotation(sysLog)")
+	@SneakyThrows
+	public Object around(ProceedingJoinPoint point, com.kxs.common.log.annotation.SysLog sysLog) {
+		String strClassName = point.getTarget().getClass().getName();
+		String strMethodName = point.getSignature().getName();
+		log.debug("[类名]:{},[方法]:{}", strClassName, strMethodName);
+
+		String value = sysLog.value();
+		String expression = sysLog.expression();
+		// 当前表达式存在 SPEL,会覆盖 value 的值
+		if (StrUtil.isNotBlank(expression)) {
+			// 解析SPEL
+			MethodSignature signature = (MethodSignature) point.getSignature();
+			EvaluationContext context = SysLogUtils.getContext(point.getArgs(), signature.getMethod());
+			try {
+				value = SysLogUtils.getValue(context, expression, String.class);
+			}
+			catch (Exception e) {
+				// SPEL 表达式异常,获取 value 的值
+				log.error("@SysLog 解析SPEL {} 异常", expression);
+			}
+		}
+
+		SysLogEventSource logVo = SysLogUtils.getSysLog();
+		logVo.setTitle(value);
+		// 获取请求body参数
+		if (StrUtil.isBlank(logVo.getParams())) {
+			logVo.setBody(point.getArgs());
+		}
+		// 发送异步日志事件
+		Long startTime = System.currentTimeMillis();
+		Object obj;
+
+		try {
+			obj = point.proceed();
+		}
+		catch (Exception e) {
+			logVo.setLogType(LogTypeEnum.ERROR.getType());
+			logVo.setException(e.getMessage());
+			throw e;
+		}
+		finally {
+			Long endTime = System.currentTimeMillis();
+			logVo.setTime(endTime - startTime);
+			SpringContextHolder.publishEvent(new SysLogEvent(logVo));
+		}
+
+		return obj;
+	}
+
+}

+ 40 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/config/SkyLogProperties.java

@@ -0,0 +1,40 @@
+package com.kxs.common.log.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+
+/**
+ * 日志配置类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Getter
+@Setter
+@ConfigurationProperties(SkyLogProperties.PREFIX)
+public class SkyLogProperties {
+
+	public static final String PREFIX = "security.log";
+
+	/**
+	 * 开启日志记录
+	 */
+	private boolean enabled = true;
+
+	/**
+	 * 放行字段,password,mobile,idcard,phone
+	 */
+	@Value("${security.log.exclude-fields:password,mobile,idcard,phone}")
+	private List<String> excludeFields;
+
+	/**
+	 * 请求报文最大存储长度
+	 */
+	private Integer maxLength = 2000;
+
+}

+ 19 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEvent.java

@@ -0,0 +1,19 @@
+package com.kxs.common.log.event;
+
+import com.kxs.system.api.model.SysLog;
+import org.springframework.context.ApplicationEvent;
+
+
+/**
+ * 系统日志事件
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class SysLogEvent extends ApplicationEvent {
+
+	public SysLogEvent(SysLog source) {
+		super(source);
+	}
+
+}

+ 21 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogEventSource.java

@@ -0,0 +1,21 @@
+package com.kxs.common.log.event;
+
+import com.kxs.system.api.model.SysLog;
+import lombok.Data;
+
+
+/**
+ * sys日志事件源
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Data
+public class SysLogEventSource extends SysLog {
+
+	/**
+	 * 参数重写成object
+	 */
+	private Object body;
+
+}

+ 75 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/event/SysLogListener.java

@@ -0,0 +1,75 @@
+//package com.kxs.common.log.event;
+//
+//import cn.hutool.core.util.StrUtil;
+//import com.fasterxml.jackson.annotation.JsonFilter;
+//import com.fasterxml.jackson.databind.ObjectMapper;
+//import com.fasterxml.jackson.databind.ser.FilterProvider;
+//import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
+//import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+//import com.kxs.common.log.jackson.SkyJavaTimeModule;
+//import com.kxs.system.api.model.SysLog;
+//import com.kxs.common.log.config.SkyLogProperties;
+//import lombok.RequiredArgsConstructor;
+//import lombok.SneakyThrows;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.beans.BeanUtils;
+//import org.springframework.beans.factory.InitializingBean;
+//import org.springframework.context.event.EventListener;
+//import org.springframework.core.annotation.Order;
+//import org.springframework.scheduling.annotation.Async;
+//
+//import java.util.Objects;
+//
+//
+///**
+// * 异步监听日志事件
+// *
+// * @author 没秃顶的码农
+// * @date 2023/10/28
+// */
+//@Slf4j
+//@RequiredArgsConstructor
+//public class SysLogListener implements InitializingBean {
+//
+//	// new 一个 避免日志脱敏策略影响全局ObjectMapper
+//	private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+//
+//	private final SysLogService sysLogService;
+//
+//	private final SkyLogProperties logProperties;
+//
+//	@SneakyThrows
+//	@Async
+//	@Order
+//	@EventListener(SysLogEvent.class)
+//	public void saveSysLog(SysLogEvent event) {
+//		SysLogEventSource source = (SysLogEventSource) event.getSource();
+//		SysLog sysLog = new SysLog();
+//		BeanUtils.copyProperties(source, sysLog);
+//
+//		// json 格式刷参数放在异步中处理,提升性能
+//		if (Objects.nonNull(source.getBody())) {
+//			String params = OBJECT_MAPPER.writeValueAsString(source.getBody());
+//			sysLog.setParams(StrUtil.subPre(params, logProperties.getMaxLength()));
+//		}
+//
+//		sysLogService.saveLog(sysLog);
+//	}
+//
+//	@Override
+//	public void afterPropertiesSet() {
+//		OBJECT_MAPPER.addMixIn(Object.class, PropertyFilterMixIn.class);
+//		String[] ignorableFieldNames = logProperties.getExcludeFields().toArray(new String[0]);
+//
+//		FilterProvider filters = new SimpleFilterProvider().addFilter("filter properties by name",
+//				SimpleBeanPropertyFilter.serializeAllExcept(ignorableFieldNames));
+//		OBJECT_MAPPER.setFilterProvider(filters);
+//		OBJECT_MAPPER.registerModule(new SkyJavaTimeModule());
+//	}
+//
+//	@JsonFilter("filter properties by name")
+//	class PropertyFilterMixIn {
+//
+//	}
+//
+//}

+ 35 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/init/ApplicationLoggerInitializer.java

@@ -0,0 +1,35 @@
+package com.kxs.common.log.init;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+
+
+/**
+ * <p>
+ * 通过环境变量的形式注入 logging.file 自动维护 Spring Boot Admin Logger Viewer
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class ApplicationLoggerInitializer implements EnvironmentPostProcessor, Ordered {
+
+	@Override
+	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+		String appName = environment.getProperty("spring.application.name");
+		String logBase = environment.getProperty("LOGGING_PATH", "logs");
+
+		// spring boot admin 直接加载日志
+		System.setProperty("logging.file.name", String.format("%s/%s/debug.log", logBase, appName));
+
+		// 避免 sentinel 1.8.4+ 心跳日志过大
+		System.setProperty("csp.sentinel.log.level", "OFF");
+	}
+
+	@Override
+	public int getOrder() {
+		return Ordered.LOWEST_PRECEDENCE;
+	}
+
+}

+ 54 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/jackson/SkyJavaTimeModule.java

@@ -0,0 +1,54 @@
+package com.kxs.common.log.jackson;
+
+import cn.hutool.core.date.DatePattern;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.PackageVersion;
+import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.InstantSerializer;
+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 java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * java 8 时间默认序列化
+ *
+ * @author L.cm
+ * @author lishanbu
+ */
+
+public class SkyJavaTimeModule extends SimpleModule {
+
+	public SkyJavaTimeModule() {
+		super(PackageVersion.VERSION);
+
+		// ======================= 时间序列化规则 ===============================
+		// yyyy-MM-dd HH:mm:ss
+		this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMATTER));
+		// yyyy-MM-dd
+		this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE));
+		// HH:mm:ss
+		this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME));
+		// Instant 类型序列化
+		this.addSerializer(Instant.class, InstantSerializer.INSTANCE);
+
+		// ======================= 时间反序列化规则 ==============================
+		// yyyy-MM-dd HH:mm:ss
+		this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMATTER));
+		// yyyy-MM-dd
+		this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE));
+		// HH:mm:ss
+		this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME));
+		// Instant 反序列化
+		this.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
+	}
+
+}

+ 38 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/LogTypeEnum.java

@@ -0,0 +1,38 @@
+package com.kxs.common.log.util;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+
+/**
+ * <p>
+ * 日志类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Getter
+@RequiredArgsConstructor
+public enum LogTypeEnum {
+
+	/**
+	 * 正常日志类型
+	 */
+	NORMAL("0", "正常日志"),
+
+	/**
+	 * 错误日志类型
+	 */
+	ERROR("9", "错误日志");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+}

+ 122 - 0
kxs-common/kxs-common-log/src/main/java/com/kxs/common/log/util/SysLogUtils.java

@@ -0,0 +1,122 @@
+package com.kxs.common.log.util;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.URLUtil;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import cn.hutool.http.HttpUtil;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.core.util.SpringContextHolder;
+import com.kxs.common.log.config.SkyLogProperties;
+import com.kxs.common.log.event.SysLogEventSource;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.experimental.UtilityClass;
+import org.springframework.core.StandardReflectionParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Objects;
+
+
+/**
+ * 系统日志工具类
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@UtilityClass
+public class SysLogUtils {
+
+	public SysLogEventSource getSysLog() {
+		HttpServletRequest request = ((ServletRequestAttributes) Objects
+			.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+		SysLogEventSource sysLog = new SysLogEventSource();
+		sysLog.setLogType(LogTypeEnum.NORMAL.getType());
+		sysLog.setRequestUri(URLUtil.getPath(request.getRequestURI()));
+		sysLog.setMethod(request.getMethod());
+		sysLog.setRemoteAddr(JakartaServletUtil.getClientIP(request));
+		sysLog.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT));
+		sysLog.setCreateBy(getUsername());
+		sysLog.setServiceId(getClientId());
+
+		// get 参数脱敏
+		SkyLogProperties logProperties = SpringContextHolder.getBean(SkyLogProperties.class);
+		Map<String, String[]> paramsMap = MapUtil.removeAny(request.getParameterMap(),
+				ArrayUtil.toArray(logProperties.getExcludeFields(), String.class));
+		sysLog.setParams(HttpUtil.toParams(paramsMap));
+		return sysLog;
+	}
+
+	/**
+	 * 获取用户名称
+	 * @return username
+	 */
+	private String getUsername() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication == null) {
+			return null;
+		}
+		return authentication.getName();
+	}
+
+	/**
+	 * 获取spel 定义的参数值
+	 * @param context 参数容器
+	 * @param key key
+	 * @param clazz 需要返回的类型
+	 * @param <T> 返回泛型
+	 * @return 参数值
+	 */
+	public <T> T getValue(EvaluationContext context, String key, Class<T> clazz) {
+		SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
+		Expression expression = spelExpressionParser.parseExpression(key);
+		return expression.getValue(context, clazz);
+	}
+
+	/**
+	 * 获取参数容器
+	 * @param arguments 方法的参数列表
+	 * @param signatureMethod 被执行的方法体
+	 * @return 装载参数的容器
+	 */
+	public EvaluationContext getContext(Object[] arguments, Method signatureMethod) {
+		String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(signatureMethod);
+		EvaluationContext context = new StandardEvaluationContext();
+		if (parameterNames == null) {
+			return context;
+		}
+		for (int i = 0; i < arguments.length; i++) {
+			context.setVariable(parameterNames[i], arguments[i]);
+		}
+		return context;
+	}
+
+	/**
+	 * 获取客户端
+	 * @return clientId
+	 */
+	private String getClientId() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication == null) {
+			return null;
+		}
+
+		Object principal = authentication.getPrincipal();
+		if (principal instanceof OAuth2AuthenticatedPrincipal) {
+			OAuth2AuthenticatedPrincipal auth2Authentication = (OAuth2AuthenticatedPrincipal) principal;
+			return MapUtil.getStr(auth2Authentication.getAttributes(), SecurityConstants.CLIENT_ID);
+		}
+		return null;
+	}
+
+}

+ 30 - 0
kxs-common/kxs-common-log/src/main/resources/META-INF/spring-configuration-metadata.json

@@ -0,0 +1,30 @@
+{
+  "groups": [
+    {
+      "name": "security.log",
+      "type": "com.kxs.common.log.config.SkyLogProperties",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    }
+  ],
+  "properties": [
+    {
+      "name": "security.log.enabled",
+      "type": "java.lang.Boolean",
+      "description": "开启日志记录",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    },
+    {
+      "name": "security.log.exclude-fields",
+      "type": "java.util.List<java.lang.String>",
+      "description": "放行字段,password,mobile,idcard,phone",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    },
+    {
+      "name": "security.log.max-length",
+      "type": "java.lang.Integer",
+      "description": "请求报文最大存储长度",
+      "sourceType": "com.kxs.common.log.config.SkyLogProperties"
+    }
+  ],
+  "hints": []
+}

+ 1 - 0
kxs-common/kxs-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.kxs.common.log.LogAutoConfiguration

+ 57 - 0
kxs-common/kxs-common-mybatis/pom.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-mybatis</artifactId>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--hutool-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+        <!--mybatis-->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-annotation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-extension</artifactId>
+        </dependency>
+        <!--server-api-->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <!--security 依赖-->
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-core</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 54 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/MybatisAutoConfiguration.java

@@ -0,0 +1,54 @@
+package com.kxs.common.mybatis;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.kxs.common.mybatis.config.MybatisPlusMetaObjectHandler;
+import com.kxs.common.mybatis.plugins.SkyPaginationInnerInterceptor;
+import com.kxs.common.mybatis.resolver.SqlFilterArgumentResolver;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+
+/**
+ * <p>
+ * mybatis plus 统一配置
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Configuration(proxyBeanMethods = false)
+public class MybatisAutoConfiguration implements WebMvcConfigurer {
+
+	/**
+	 * SQL 过滤器避免SQL 注入
+	 * @param argumentResolvers
+	 */
+	@Override
+	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
+		argumentResolvers.add(new SqlFilterArgumentResolver());
+	}
+
+	/**
+	 * 分页插件, 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型
+	 */
+	@Bean
+	public MybatisPlusInterceptor mybatisPlusInterceptor() {
+		MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+		interceptor.addInnerInterceptor(new SkyPaginationInnerInterceptor());
+		return interceptor;
+	}
+
+	/**
+	 * 审计字段自动填充
+	 * @return {@link MetaObjectHandler}
+	 */
+	@Bean
+	public MybatisPlusMetaObjectHandler mybatisPlusMetaObjectHandler() {
+		return new MybatisPlusMetaObjectHandler();
+	}
+
+}

+ 51 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/base/BaseEntity.java

@@ -0,0 +1,51 @@
+package com.kxs.common.mybatis.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 抽象实体
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Getter
+@Setter
+public class BaseEntity implements Serializable {
+
+	/**
+	 * 创建者
+	 */
+	@Schema(description = "创建人")
+	@TableField(fill = FieldFill.INSERT)
+	private String createBy;
+
+	/**
+	 * 创建时间
+	 */
+	@Schema(description = "创建时间")
+	@TableField(fill = FieldFill.INSERT)
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新者
+	 */
+	@Schema(description = "更新人")
+	@TableField(fill = FieldFill.INSERT_UPDATE)
+	private String updateBy;
+
+	/**
+	 * 更新时间
+	 */
+	@Schema(description = "更新时间")
+	@TableField(fill = FieldFill.INSERT_UPDATE)
+	private LocalDateTime updateTime;
+
+}

+ 96 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/config/MybatisPlusMetaObjectHandler.java

@@ -0,0 +1,96 @@
+package com.kxs.common.mybatis.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.kxs.common.core.constant.CommonConstants;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.ClassUtils;
+
+import java.nio.charset.Charset;
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+
+/**
+ * MybatisPlus 自动填充配置
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Slf4j
+public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
+
+	@Override
+	public void insertFill(MetaObject metaObject) {
+		log.debug("mybatis plus start insert fill ....");
+		LocalDateTime now = LocalDateTime.now();
+
+		fillValIfNullByName("createTime", now, metaObject, true);
+		fillValIfNullByName("updateTime", now, metaObject, true);
+		fillValIfNullByName("createBy", getUserName(), metaObject, true);
+		fillValIfNullByName("updateBy", getUserName(), metaObject, true);
+
+		// 删除标记自动填充
+		fillValIfNullByName("delFlag", CommonConstants.STATUS_NORMAL, metaObject, true);
+	}
+
+	@Override
+	public void updateFill(MetaObject metaObject) {
+		log.debug("mybatis plus start update fill ....");
+		fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
+		fillValIfNullByName("updateBy", getUserName(), metaObject, true);
+	}
+
+	/**
+	 * 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
+	 * @param fieldName 属性名
+	 * @param fieldVal 属性值
+	 * @param metaObject MetaObject
+	 * @param isCover 是否覆盖原有值,避免更新操作手动入参
+	 */
+	private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
+		// 0. 如果填充值为空
+		if (fieldVal == null) {
+			return;
+		}
+
+		// 1. 没有 set 方法
+		if (!metaObject.hasSetter(fieldName)) {
+			return;
+		}
+		// 2. 如果用户有手动设置的值
+		Object userSetValue = metaObject.getValue(fieldName);
+		String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset());
+		if (StrUtil.isNotBlank(setValueStr) && !isCover) {
+			return;
+		}
+		// 3. field 类型相同时设置
+		Class<?> getterType = metaObject.getGetterType(fieldName);
+		if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
+			metaObject.setValue(fieldName, fieldVal);
+		}
+	}
+
+	/**
+	 * 获取 spring security 当前的用户名
+	 * @return 当前用户名
+	 */
+	private String getUserName() {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		// 匿名接口直接返回
+		if (authentication instanceof AnonymousAuthenticationToken) {
+			return null;
+		}
+
+		if (Optional.ofNullable(authentication).isPresent()) {
+			return authentication.getName();
+		}
+
+		return null;
+	}
+
+}

+ 57 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/handler/JsonLongArrayTypeHandler.java

@@ -0,0 +1,57 @@
+package com.kxs.common.mybatis.handler;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.SneakyThrows;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+
+/**
+ * Mybatis数组,符串互转
+ * <p>
+ * MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@MappedTypes(value = { Long[].class })
+@MappedJdbcTypes(value = JdbcType.VARCHAR)
+public class JsonLongArrayTypeHandler extends BaseTypeHandler<Long[]> {
+
+	@Override
+	public void setNonNullParameter(PreparedStatement ps, int i, Long[] parameter, JdbcType jdbcType)
+			throws SQLException {
+		ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
+	}
+
+	@Override
+	@SneakyThrows
+	public Long[] getNullableResult(ResultSet rs, String columnName) {
+		String reString = rs.getString(columnName);
+		return Convert.toLongArray(reString);
+	}
+
+	@Override
+	@SneakyThrows
+	public Long[] getNullableResult(ResultSet rs, int columnIndex) {
+		String reString = rs.getString(columnIndex);
+		return Convert.toLongArray(reString);
+	}
+
+	@Override
+	@SneakyThrows
+	public Long[] getNullableResult(CallableStatement cs, int columnIndex) {
+		String reString = cs.getString(columnIndex);
+		return Convert.toLongArray(reString);
+	}
+
+}

+ 57 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/handler/JsonStringArrayTypeHandler.java

@@ -0,0 +1,57 @@
+package com.kxs.common.mybatis.handler;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.SneakyThrows;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+
+/**
+ * Mybatis数组,符串互转
+ * <p>
+ * MappedJdbcTypes 数据库中的数据类型 MappedTypes java中的的数据类型
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@MappedTypes(value = { String[].class })
+@MappedJdbcTypes(value = JdbcType.VARCHAR)
+public class JsonStringArrayTypeHandler extends BaseTypeHandler<String[]> {
+
+	@Override
+	public void setNonNullParameter(PreparedStatement ps, int i, String[] parameter, JdbcType jdbcType)
+			throws SQLException {
+		ps.setString(i, ArrayUtil.join(parameter, StrUtil.COMMA));
+	}
+
+	@Override
+	@SneakyThrows
+	public String[] getNullableResult(ResultSet rs, String columnName) {
+		String reString = rs.getString(columnName);
+		return Convert.toStrArray(reString);
+	}
+
+	@Override
+	@SneakyThrows
+	public String[] getNullableResult(ResultSet rs, int columnIndex) {
+		String reString = rs.getString(columnIndex);
+		return Convert.toStrArray(reString);
+	}
+
+	@Override
+	@SneakyThrows
+	public String[] getNullableResult(CallableStatement cs, int columnIndex) {
+		String reString = cs.getString(columnIndex);
+		return Convert.toStrArray(reString);
+	}
+
+}

+ 66 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/plugins/SkyPaginationInnerInterceptor.java

@@ -0,0 +1,66 @@
+package com.kxs.common.mybatis.plugins;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.ParameterUtils;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+
+import java.sql.SQLException;
+
+
+/**
+ * 分页拦截器
+ * <p>
+ * 重构分页插件, 当 size 小于 0 时, 直接设置为 0, 防止错误查询全表
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@NoArgsConstructor
+public class SkyPaginationInnerInterceptor extends PaginationInnerInterceptor {
+
+	/**
+	 * 数据库类型
+	 * <p>
+	 * 查看 {@link #findIDialect(Executor)} 逻辑
+	 */
+	private DbType dbType;
+
+	/**
+	 * 方言实现类
+	 * <p>
+	 * 查看 {@link #findIDialect(Executor)} 逻辑
+	 */
+	private IDialect dialect;
+
+	public SkyPaginationInnerInterceptor(DbType dbType) {
+		this.dbType = dbType;
+	}
+
+	public SkyPaginationInnerInterceptor(IDialect dialect) {
+		this.dialect = dialect;
+	}
+
+	@Override
+	public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
+			ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
+		IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
+		// size 小于 0 直接设置为 0 , 即不查询任何数据
+		if (null != page && page.getSize() < 0) {
+			page.setSize(0);
+		}
+		super.beforeQuery(executor, ms, page, rowBounds, resultHandler, boundSql);
+	}
+
+}

+ 91 - 0
kxs-common/kxs-common-mybatis/src/main/java/com/kxs/common/mybatis/resolver/SqlFilterArgumentResolver.java

@@ -0,0 +1,91 @@
+package com.kxs.common.mybatis.resolver;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.MethodParameter;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+
+/**
+ * <p>
+ * 解决Mybatis Plus Order By SQL注入问题
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Slf4j
+public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
+
+	private final static String[] KEYWORDS = { "master", "truncate", "insert", "select", "delete", "update", "declare",
+			"alter", "drop", "sleep", "extractvalue", "concat" };
+
+	/**
+	 * 判断Controller是否包含page 参数
+	 * @param parameter 参数
+	 * @return 是否过滤
+	 */
+	@Override
+	public boolean supportsParameter(MethodParameter parameter) {
+		return parameter.getParameterType().equals(Page.class);
+	}
+
+	/**
+	 * @param parameter 入参集合
+	 * @param mavContainer model 和 view
+	 * @param webRequest web相关
+	 * @param binderFactory 入参解析
+	 * @return 检查后新的page对象
+	 * <p>
+	 * page 只支持查询 GET .如需解析POST获取请求报文体处理
+	 */
+	@Override
+	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
+
+		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
+
+		String[] ascs = Objects.requireNonNull(request).getParameterValues("ascs");
+		String[] descs = request.getParameterValues("descs");
+		String current = request.getParameter("current");
+		String size = request.getParameter("size");
+
+		Page<?> page = new Page<>();
+		if (StrUtil.isNotBlank(current)) {
+			page.setCurrent(Long.parseLong(current));
+		}
+
+		if (StrUtil.isNotBlank(size)) {
+			page.setSize(Long.parseLong(size));
+		}
+
+		List<OrderItem> orderItemList = new ArrayList<>();
+		Optional.ofNullable(ascs)
+			.ifPresent(s -> orderItemList.addAll(
+					Arrays.stream(s).filter(sqlInjectPredicate()).map(OrderItem::asc).toList()));
+		Optional.ofNullable(descs)
+			.ifPresent(s -> orderItemList.addAll(
+					Arrays.stream(s).filter(sqlInjectPredicate()).map(OrderItem::desc).toList()));
+		page.addOrder(orderItemList);
+
+		return page;
+	}
+
+	/**
+	 * 判断用户输入里面有没有关键字
+	 * @return Predicate
+	 */
+	private Predicate<String> sqlInjectPredicate() {
+		return sql -> Arrays.stream(KEYWORDS).noneMatch(keyword -> StrUtil.containsIgnoreCase(sql, keyword));
+	}
+
+}

+ 1 - 0
kxs-common/kxs-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.kxs.common.mybatis.MybatisAutoConfiguration

+ 52 - 0
kxs-common/kxs-common-security/pom.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-common</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common-security</artifactId>
+    <packaging>jar</packaging>
+
+    <description>kxs 安全工具类</description>
+
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--工具类核心包-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-jose</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-oauth2-authorization-server</artifactId>
+            <version>${spring.authorization.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webmvc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 25 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/annotation/EnableSkyResourceServer.java

@@ -0,0 +1,25 @@
+package com.kxs.common.security.annotation;
+
+import com.kxs.common.security.component.SkyResourceServerAutoConfiguration;
+import com.kxs.common.security.component.SkyResourceServerConfiguration;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+
+/**
+ * <p>
+ * 资源服务注解
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Documented
+@Inherited
+@Target({ ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Import({ SkyResourceServerAutoConfiguration.class, SkyResourceServerConfiguration.class,
+		SkyFeignClientConfiguration.class })
+public @interface EnableSkyResourceServer {
+
+}

+ 29 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/annotation/Inner.java

@@ -0,0 +1,29 @@
+package com.kxs.common.security.annotation;
+
+import java.lang.annotation.*;
+
+
+/**
+ *  url请求不鉴权注解
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Target({ ElementType.METHOD, ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Inner {
+
+	/**
+	 * 是否AOP统一处理
+	 * @return false, true
+	 */
+	boolean value() default true;
+
+	/**
+	 * 需要特殊判空的字段(预留)
+	 * @return {}
+	 */
+	String[] field() default {};
+
+}

+ 42 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/PermissionService.java

@@ -0,0 +1,42 @@
+
+package com.kxs.common.security.component;
+
+import cn.hutool.core.util.ArrayUtil;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.PatternMatchUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.Collection;
+
+
+/**
+ * 接口权限判断工具
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class PermissionService {
+
+	/**
+	 * 判断接口是否有任意xxx,xxx权限
+	 * @param permissions 权限
+	 * @return {boolean}
+	 */
+	public boolean hasPermission(String... permissions) {
+		if (ArrayUtil.isEmpty(permissions)) {
+			return false;
+		}
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		if (authentication == null) {
+			return false;
+		}
+		Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
+		return authorities.stream()
+			.map(GrantedAuthority::getAuthority)
+			.filter(StringUtils::hasText)
+			.anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x));
+	}
+
+}

+ 64 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/PermitAllUrlProperties.java

@@ -0,0 +1,64 @@
+package com.kxs.common.security.component;
+
+import cn.hutool.core.util.ReUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import com.kxs.common.security.annotation.Inner;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+
+/**
+ * <p>
+ * 资源服务器对外直接暴露URL,如果设置contex-path 要特殊处理
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@Slf4j
+@ConfigurationProperties(prefix = "security.oauth2.ignore")
+public class PermitAllUrlProperties implements InitializingBean {
+
+	private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
+
+	private static final String[] DEFAULT_IGNORE_URLS = new String[] { "/actuator/**", "/error", "/v3/api-docs" };
+
+	@Getter
+	@Setter
+	private List<String> urls = new ArrayList<>();
+
+	@Override
+	public void afterPropertiesSet() {
+		urls.addAll(Arrays.asList(DEFAULT_IGNORE_URLS));
+		RequestMappingHandlerMapping mapping = SpringUtil.getBean("requestMappingHandlerMapping");
+		Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
+
+		map.keySet().forEach(info -> {
+			HandlerMethod handlerMethod = map.get(info);
+
+			// 获取方法上边的注解 替代path variable 为 *
+			Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
+			Optional.ofNullable(method)
+				.ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
+					.getPatternValues()
+					.forEach(url -> urls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
+
+			// 获取类上边的注解, 替代path variable 为 *
+			Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
+			Optional.ofNullable(controller)
+				.ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
+					.getPatternValues()
+					.forEach(url -> urls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
+		});
+	}
+
+}

+ 57 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/ResourceAuthExceptionEntryPoint.java

@@ -0,0 +1,57 @@
+package com.kxs.common.security.component;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kxs.common.core.constant.CommonConstants;
+import com.kxs.common.core.constant.enums.ErrorTypeEnum;
+import com.kxs.common.core.util.R;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import java.io.PrintWriter;
+
+
+/**
+ * 客户端异常处理 AuthenticationException 不同细化异常处理
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@RequiredArgsConstructor
+public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
+
+	private final ObjectMapper objectMapper;
+
+	@Override
+	@SneakyThrows
+	public void commence(HttpServletRequest request, HttpServletResponse response,
+			AuthenticationException authException) {
+		response.setCharacterEncoding(CommonConstants.UTF8);
+		response.setContentType(CommonConstants.CONTENT_TYPE);
+		R<String> result = new R<>();
+		result.setCode(CommonConstants.FAIL);
+		response.setStatus(HttpStatus.UNAUTHORIZED.value());
+		if (authException != null) {
+			result.setMsg("error");
+			result.setData(authException.getMessage());
+		}
+
+		// 针对令牌过期返回特殊的 424
+		if (authException instanceof InvalidBearerTokenException
+				|| authException instanceof InsufficientAuthenticationException) {
+			response.setStatus(HttpStatus.FAILED_DEPENDENCY.value());
+			result.setMsg(ErrorTypeEnum.TOKEN_EXPIRED.getDescription());
+		}
+		PrintWriter printWriter = response.getWriter();
+		printWriter.append(objectMapper.writeValueAsString(result));
+	}
+
+}

+ 106 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyBearerTokenExtractor.java

@@ -0,0 +1,106 @@
+package com.kxs.common.security.component;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.server.resource.BearerTokenError;
+import org.springframework.security.oauth2.server.resource.BearerTokenErrors;
+import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+import org.springframework.util.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class SkyBearerTokenExtractor implements BearerTokenResolver {
+
+	private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-:._~+/]+=*)$",
+			Pattern.CASE_INSENSITIVE);
+
+	private boolean allowFormEncodedBodyParameter = false;
+
+	private boolean allowUriQueryParameter = true;
+
+	private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION;
+
+	private final PathMatcher pathMatcher = new AntPathMatcher();
+
+	private final PermitAllUrlProperties urlProperties;
+
+	public SkyBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
+		this.urlProperties = urlProperties;
+	}
+
+	@Override
+	public String resolve(HttpServletRequest request) {
+		boolean match = urlProperties.getUrls()
+			.stream()
+			.anyMatch(url -> pathMatcher.match(url, request.getRequestURI()));
+
+		if (match) {
+			return null;
+		}
+
+		final String authorizationHeaderToken = resolveFromAuthorizationHeader(request);
+		final String parameterToken = isParameterTokenSupportedForRequest(request)
+				? resolveFromRequestParameters(request) : null;
+		if (authorizationHeaderToken != null) {
+			if (parameterToken != null) {
+				final BearerTokenError error = BearerTokenErrors
+					.invalidRequest("Found multiple bearer tokens in the request");
+				throw new OAuth2AuthenticationException(error);
+			}
+			return authorizationHeaderToken;
+		}
+		if (parameterToken != null && isParameterTokenEnabledForRequest(request)) {
+			return parameterToken;
+		}
+		return null;
+	}
+
+	private String resolveFromAuthorizationHeader(HttpServletRequest request) {
+		String authorization = request.getHeader(this.bearerTokenHeaderName);
+		if (!StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
+			return null;
+		}
+		Matcher matcher = authorizationPattern.matcher(authorization);
+		if (!matcher.matches()) {
+			BearerTokenError error = BearerTokenErrors.invalidToken("Bearer token is malformed");
+			throw new OAuth2AuthenticationException(error);
+		}
+		return matcher.group("token");
+	}
+
+	private static String resolveFromRequestParameters(HttpServletRequest request) {
+		String[] values = request.getParameterValues("access_token");
+		if (values == null || values.length == 0) {
+			return null;
+		}
+		if (values.length == 1) {
+			return values[0];
+		}
+		BearerTokenError error = BearerTokenErrors.invalidRequest("Found multiple bearer tokens in the request");
+		throw new OAuth2AuthenticationException(error);
+	}
+
+	private boolean isParameterTokenSupportedForRequest(final HttpServletRequest request) {
+		return (("POST".equals(request.getMethod())
+				&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
+				|| "GET".equals(request.getMethod()));
+	}
+
+	private boolean isParameterTokenEnabledForRequest(final HttpServletRequest request) {
+		return ((this.allowFormEncodedBodyParameter && "POST".equals(request.getMethod())
+				&& MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(request.getContentType()))
+				|| (this.allowUriQueryParameter && "GET".equals(request.getMethod())));
+	}
+
+}

+ 40 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyClientCredentialsOAuth2AuthenticatedPrincipal.java

@@ -0,0 +1,40 @@
+package com.kxs.common.security.component;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022/7/6
+ *
+ * credential 支持客户端模式的用户存储
+ */
+@RequiredArgsConstructor
+public class SkyClientCredentialsOAuth2AuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal {
+
+	private final Map<String, Object> attributes;
+
+	private final Collection<GrantedAuthority> authorities;
+
+	private final String name;
+
+	@Override
+	public Map<String, Object> getAttributes() {
+		return this.attributes;
+	}
+
+	@Override
+	public Collection<? extends GrantedAuthority> getAuthorities() {
+		return this.authorities;
+	}
+
+	@Override
+	public String getName() {
+		return this.name;
+	}
+
+}

+ 52 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyCustomOAuth2AccessTokenResponseHttpMessageConverter.java

@@ -0,0 +1,52 @@
+package com.kxs.common.security.component;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kxs.common.core.util.SpringContextHolder;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.security.oauth2.core.endpoint.DefaultOAuth2AccessTokenResponseMapConverter;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
+
+import java.util.Map;
+
+/**
+ * 扩展原生的实现,支持 Long2String
+ *
+ * @author 没秃顶的码农
+ * @date 2023/6/28
+ */
+public class SkyCustomOAuth2AccessTokenResponseHttpMessageConverter
+		extends OAuth2AccessTokenResponseHttpMessageConverter {
+
+	private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
+	};
+
+	private final Converter<OAuth2AccessTokenResponse, Map<String, Object>> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
+
+	@Override
+	protected void writeInternal(OAuth2AccessTokenResponse tokenResponse, HttpOutputMessage outputMessage)
+			throws HttpMessageNotWritableException {
+		try {
+			Map<String, Object> tokenResponseParameters = this.accessTokenResponseParametersConverter
+				.convert(tokenResponse);
+
+			ObjectMapper objectMapper = SpringContextHolder.getBean(ObjectMapper.class);
+			GenericHttpMessageConverter<Object> jsonMessageConverter = new MappingJackson2HttpMessageConverter(
+					objectMapper);
+			assert tokenResponseParameters != null;
+			jsonMessageConverter.write(tokenResponseParameters, STRING_OBJECT_MAP.getType(), MediaType.APPLICATION_JSON,
+					outputMessage);
+		}
+		catch (Exception ex) {
+			throw new HttpMessageNotWritableException(
+					"An error occurred writing the OAuth 2.0 Access Token Response: " + ex.getMessage(), ex);
+		}
+	}
+
+}

+ 83 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyCustomOpaqueTokenIntrospector.java

@@ -0,0 +1,83 @@
+package com.kxs.common.security.component;
+
+import cn.hutool.extra.spring.SpringUtil;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.security.model.KxsUser;
+import com.kxs.common.security.service.KxsUserDetailsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.Ordered;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+
+import java.security.Principal;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022/5/28
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class SkyCustomOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
+
+	private final OAuth2AuthorizationService authorizationService;
+
+	@Override
+	public OAuth2AuthenticatedPrincipal introspect(String token) {
+		OAuth2Authorization oldAuthorization = authorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
+		if (Objects.isNull(oldAuthorization)) {
+			throw new InvalidBearerTokenException(token);
+		}
+
+		// 客户端模式默认返回
+		if (AuthorizationGrantType.CLIENT_CREDENTIALS.equals(oldAuthorization.getAuthorizationGrantType())) {
+			return new SkyClientCredentialsOAuth2AuthenticatedPrincipal(oldAuthorization.getAttributes(),
+					AuthorityUtils.NO_AUTHORITIES, oldAuthorization.getPrincipalName());
+		}
+
+		Map<String, KxsUserDetailsService> userDetailsServiceMap = SpringUtil
+			.getBeansOfType(KxsUserDetailsService.class);
+
+		Optional<KxsUserDetailsService> optional = userDetailsServiceMap.values()
+			.stream()
+			.filter(service -> service.support(Objects.requireNonNull(oldAuthorization).getRegisteredClientId(),
+					oldAuthorization.getAuthorizationGrantType().getValue()))
+			.max(Comparator.comparingInt(Ordered::getOrder));
+
+		UserDetails userDetails = null;
+		try {
+			Object principal = Objects.requireNonNull(oldAuthorization).getAttributes().get(Principal.class.getName());
+			UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) principal;
+			Object tokenPrincipal = usernamePasswordAuthenticationToken.getPrincipal();
+			userDetails = optional.get().loadUserByUser((KxsUser) tokenPrincipal);
+		}
+		catch (UsernameNotFoundException notFoundException) {
+			log.warn("用户不不存在 {}", notFoundException.getLocalizedMessage());
+			throw notFoundException;
+		}
+		catch (Exception ex) {
+			log.error("资源服务器 introspect Token error {}", ex.getLocalizedMessage());
+		}
+
+		// 注入客户端信息,方便上下文中获取
+		KxsUser skyxUser = (KxsUser) userDetails;
+		Objects.requireNonNull(skyxUser)
+			.getAttributes()
+			.put(SecurityConstants.CLIENT_ID, oldAuthorization.getRegisteredClientId());
+		return skyxUser;
+	}
+
+}

+ 58 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyResourceServerAutoConfiguration.java

@@ -0,0 +1,58 @@
+package com.kxs.common.security.component;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022-06-02
+ */
+@RequiredArgsConstructor
+@EnableConfigurationProperties(PermitAllUrlProperties.class)
+public class SkyResourceServerAutoConfiguration {
+
+	/**
+	 * 鉴权具体的实现逻辑
+	 * @return (#pms.xxx)
+	 */
+	@Bean("pms")
+	public PermissionService permissionService() {
+		return new PermissionService();
+	}
+
+	/**
+	 * 请求令牌的抽取逻辑
+	 * @param urlProperties 对外暴露的接口列表
+	 * @return BearerTokenExtractor
+	 */
+	@Bean
+	public SkyBearerTokenExtractor skyBearerTokenExtractor(PermitAllUrlProperties urlProperties) {
+		return new SkyBearerTokenExtractor(urlProperties);
+	}
+
+	/**
+	 * 资源服务器异常处理
+	 * @param objectMapper jackson 输出对象
+	 * @return ResourceAuthExceptionEntryPoint
+	 */
+	@Bean
+	public ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint(ObjectMapper objectMapper) {
+		return new ResourceAuthExceptionEntryPoint(objectMapper);
+	}
+
+	/**
+	 * 资源服务器toke内省处理器
+	 * @param authorizationService token 存储实现
+	 * @return TokenIntrospector
+	 */
+	@Bean
+	public OpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2AuthorizationService authorizationService) {
+		return new SkyCustomOpaqueTokenIntrospector(authorizationService);
+	}
+
+}

+ 60 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkyResourceServerConfiguration.java

@@ -0,0 +1,60 @@
+package com.kxs.common.security.component;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022-06-04
+ *
+ * 资源服务器认证授权配置
+ */
+@Slf4j
+@EnableWebSecurity
+@EnableMethodSecurity
+@RequiredArgsConstructor
+public class SkyResourceServerConfiguration {
+
+	protected final ResourceAuthExceptionEntryPoint resourceAuthExceptionEntryPoint;
+
+	private final PermitAllUrlProperties permitAllUrl;
+
+	private final SkyBearerTokenExtractor skyBearerTokenExtractor;
+
+	private final OpaqueTokenIntrospector customOpaqueTokenIntrospector;
+
+	@Bean
+	@Order(Ordered.HIGHEST_PRECEDENCE)
+	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+		AntPathRequestMatcher[] requestMatchers = permitAllUrl.getUrls()
+			.stream()
+			.map(AntPathRequestMatcher::new)
+			.toList()
+			.toArray(new AntPathRequestMatcher[] {});
+
+		http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.requestMatchers(requestMatchers)
+			.permitAll()
+			.anyRequest()
+			.authenticated())
+			.oauth2ResourceServer(
+					oauth2 -> oauth2.opaqueToken(token -> token.introspector(customOpaqueTokenIntrospector))
+						.authenticationEntryPoint(resourceAuthExceptionEntryPoint)
+						.bearerTokenResolver(skyBearerTokenExtractor))
+			.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+			.csrf(AbstractHttpConfigurer::disable);
+
+		return http.build();
+	}
+
+}

+ 50 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkySecurityInnerAspect.java

@@ -0,0 +1,50 @@
+package com.kxs.common.security.component;
+
+import cn.hutool.core.util.StrUtil;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.security.annotation.Inner;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.security.access.AccessDeniedException;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022-06-04
+ *
+ * 服务间接口不鉴权处理逻辑
+ */
+@Slf4j
+@Aspect
+@RequiredArgsConstructor
+public class SkySecurityInnerAspect implements Ordered {
+
+	private final HttpServletRequest request;
+
+	@SneakyThrows
+	@Before("@within(inner) || @annotation(inner)")
+	public void around(JoinPoint point, Inner inner) {
+		// 实际注入的inner实体由表达式后一个注解决定,即是方法上的@Inner注解实体,若方法上无@Inner注解,则获取类上的
+		if (inner == null) {
+			Class<?> clazz = point.getTarget().getClass();
+			inner = AnnotationUtils.findAnnotation(clazz, Inner.class);
+		}
+		String header = request.getHeader(SecurityConstants.FROM);
+		if (inner.value() && !StrUtil.equals(SecurityConstants.FROM_IN, header)) {
+			log.warn("访问接口 {} 没有权限", point.getSignature().getName());
+			throw new AccessDeniedException("Access is denied");
+		}
+	}
+
+	@Override
+	public int getOrder() {
+		return Ordered.HIGHEST_PRECEDENCE + 1;
+	}
+
+}

+ 30 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/component/SkySecurityMessageSourceConfiguration.java

@@ -0,0 +1,30 @@
+package com.kxs.common.security.component;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.context.MessageSource;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.Locale;
+
+import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET;
+
+/**
+ * @author 没秃顶的码农
+ * @date 2022-06-04
+ * <p>
+ * 注入自定义错误处理,覆盖 org/springframework/security/messages 内置异常
+ */
+@ConditionalOnWebApplication(type = SERVLET)
+public class SkySecurityMessageSourceConfiguration implements WebMvcConfigurer {
+
+	@Bean
+	public MessageSource securityMessageSource() {
+		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
+		messageSource.addBasenames("classpath:i18n/errors/messages");
+		messageSource.setDefaultLocale(Locale.CHINA);
+		return messageSource;
+	}
+
+}

+ 68 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/model/KxsUser.java

@@ -0,0 +1,68 @@
+package com.kxs.common.security.model;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Getter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
+
+import java.io.Serial;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * kxs用户
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+public class KxsUser extends User implements OAuth2AuthenticatedPrincipal {
+
+	@Serial
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+
+	/**
+	 * 扩展属性,方便存放oauth 上下文相关信息
+	 */
+	private final Map<String, Object> attributes = new HashMap<>();
+
+	/**
+	 * 用户ID
+	 */
+	@Getter
+	@JsonSerialize(using = ToStringSerializer.class)
+	private final Long id;
+
+	/**
+	 * 手机号
+	 */
+	@Getter
+	private final String phone;
+
+	public KxsUser(Long id, String username, String password, String phone, boolean enabled,
+			boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked,
+			Collection<? extends GrantedAuthority> authorities) {
+		super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
+		this.id = id;
+		this.phone = phone;
+	}
+
+	/**
+	 * Get the OAuth 2.0 token attributes
+	 * @return the OAuth 2.0 token attributes
+	 */
+	@Override
+	public Map<String, Object> getAttributes() {
+		return this.attributes;
+	}
+
+	@Override
+	public String getName() {
+		return this.getUsername();
+	}
+
+}

+ 55 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/service/KxsRedisOAuth2AuthorizationConsentService.java

@@ -0,0 +1,55 @@
+package com.kxs.common.security.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.util.Assert;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * kxs-redis oauth2授权同意服务
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@RequiredArgsConstructor
+public class KxsRedisOAuth2AuthorizationConsentService implements OAuth2AuthorizationConsentService {
+
+	private final RedisTemplate<String, Object> redisTemplate;
+
+	private final static Long TIMEOUT = 10L;
+
+	@Override
+	public void save(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+
+		redisTemplate.opsForValue()
+			.set(buildKey(authorizationConsent), authorizationConsent, TIMEOUT, TimeUnit.MINUTES);
+
+	}
+
+	@Override
+	public void remove(OAuth2AuthorizationConsent authorizationConsent) {
+		Assert.notNull(authorizationConsent, "authorizationConsent cannot be null");
+		redisTemplate.delete(buildKey(authorizationConsent));
+	}
+
+	@Override
+	public OAuth2AuthorizationConsent findById(String registeredClientId, String principalName) {
+		Assert.hasText(registeredClientId, "registeredClientId cannot be empty");
+		Assert.hasText(principalName, "principalName cannot be empty");
+		return (OAuth2AuthorizationConsent) redisTemplate.opsForValue()
+			.get(buildKey(registeredClientId, principalName));
+	}
+
+	private static String buildKey(String registeredClientId, String principalName) {
+		return "token:consent:" + registeredClientId + ":" + principalName;
+	}
+
+	private static String buildKey(OAuth2AuthorizationConsent authorizationConsent) {
+		return buildKey(authorizationConsent.getRegisteredClientId(), authorizationConsent.getPrincipalName());
+	}
+
+}

+ 143 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/service/KxsRedisOAuth2AuthorizationService.java

@@ -0,0 +1,143 @@
+package com.kxs.common.security.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
+import org.springframework.util.Assert;
+
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author lengleng
+ * @date 2022/5/27
+ */
+@RequiredArgsConstructor
+public class KxsRedisOAuth2AuthorizationService implements OAuth2AuthorizationService {
+
+	private final static Long TIMEOUT = 10L;
+
+	private static final String AUTHORIZATION = "token";
+
+	private final RedisTemplate<String, Object> redisTemplate;
+
+	@Override
+	public void save(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+
+		if (isState(authorization)) {
+			String token = authorization.getAttribute("state");
+			redisTemplate.setValueSerializer(RedisSerializer.java());
+			redisTemplate.opsForValue()
+				.set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT, TimeUnit.MINUTES);
+		}
+
+		if (isCode(authorization)) {
+			OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
+				.getToken(OAuth2AuthorizationCode.class);
+			OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
+			long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
+					authorizationCodeToken.getExpiresAt());
+			redisTemplate.setValueSerializer(RedisSerializer.java());
+			redisTemplate.opsForValue()
+				.set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()), authorization,
+						between, TimeUnit.MINUTES);
+		}
+
+		if (isRefreshToken(authorization)) {
+			OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
+			long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
+			redisTemplate.setValueSerializer(RedisSerializer.java());
+			redisTemplate.opsForValue()
+				.set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()), authorization, between,
+						TimeUnit.SECONDS);
+		}
+
+		if (isAccessToken(authorization)) {
+			OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
+			long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
+			redisTemplate.setValueSerializer(RedisSerializer.java());
+			redisTemplate.opsForValue()
+				.set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()), authorization, between,
+						TimeUnit.SECONDS);
+		}
+	}
+
+	@Override
+	public void remove(OAuth2Authorization authorization) {
+		Assert.notNull(authorization, "authorization cannot be null");
+
+		List<String> keys = new ArrayList<>();
+		if (isState(authorization)) {
+			String token = authorization.getAttribute("state");
+			keys.add(buildKey(OAuth2ParameterNames.STATE, token));
+		}
+
+		if (isCode(authorization)) {
+			OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
+				.getToken(OAuth2AuthorizationCode.class);
+			OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
+			keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
+		}
+
+		if (isRefreshToken(authorization)) {
+			OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
+			keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
+		}
+
+		if (isAccessToken(authorization)) {
+			OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
+			keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
+		}
+		redisTemplate.delete(keys);
+	}
+
+	@Override
+	@Nullable
+	public OAuth2Authorization findById(String id) {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	@Nullable
+	public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
+		Assert.hasText(token, "token cannot be empty");
+		Assert.notNull(tokenType, "tokenType cannot be empty");
+		redisTemplate.setValueSerializer(RedisSerializer.java());
+		return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
+	}
+
+	private String buildKey(String type, String id) {
+		return String.format("%s::%s::%s", AUTHORIZATION, type, id);
+	}
+
+	private static boolean isState(OAuth2Authorization authorization) {
+		return Objects.nonNull(authorization.getAttribute("state"));
+	}
+
+	private static boolean isCode(OAuth2Authorization authorization) {
+		OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
+			.getToken(OAuth2AuthorizationCode.class);
+		return Objects.nonNull(authorizationCode);
+	}
+
+	private static boolean isRefreshToken(OAuth2Authorization authorization) {
+		return Objects.nonNull(authorization.getRefreshToken());
+	}
+
+	private static boolean isAccessToken(OAuth2Authorization authorization) {
+		return Objects.nonNull(authorization.getAccessToken());
+	}
+
+}

+ 43 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/service/KxsUserDetailsService.java

@@ -0,0 +1,43 @@
+package com.kxs.common.security.service;
+
+import com.kxs.common.security.model.KxsUser;
+import org.springframework.core.Ordered;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+/**
+ * @author lengleng
+ * @date 2021/12/21
+ */
+public interface KxsUserDetailsService extends UserDetailsService, Ordered {
+
+	/**
+	 * 是否支持此客户端校验
+	 * @param clientId 目标客户端
+	 * @return true/false
+	 */
+	default boolean support(String clientId, String grantType) {
+		return true;
+	}
+
+	/**
+	 * 排序值 默认取最大的
+	 * @return 排序值
+	 */
+	@Override
+	default int getOrder() {
+		return 0;
+	}
+
+
+	/**
+	 * 通过用户实体查询
+	 *
+	 * @param kxsUser user
+	 * @return 用户详细信息
+	 */
+	default UserDetails loadUserByUser(KxsUser kxsUser) {
+		return this.loadUserByUsername(kxsUser.getUsername());
+	}
+
+}

+ 81 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/OAuth2EndpointUtils.java

@@ -0,0 +1,81 @@
+package com.kxs.common.security.util;
+
+import cn.hutool.core.map.MapUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.experimental.UtilityClass;
+import org.springframework.security.oauth2.core.*;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import java.io.IOException;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+
+/**
+ * @author jumuning
+ * @description OAuth2 端点工具
+ */
+@UtilityClass
+public class OAuth2EndpointUtils {
+
+	public final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
+
+	public MultiValueMap<String, String> getParameters(HttpServletRequest request) {
+		Map<String, String[]> parameterMap = request.getParameterMap();
+		MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
+		parameterMap.forEach((key, values) -> {
+			if (values.length > 0) {
+				for (String value : values) {
+					parameters.add(key, value);
+				}
+			}
+		});
+		return parameters;
+	}
+
+	public boolean matchesPkceTokenRequest(HttpServletRequest request) {
+		return AuthorizationGrantType.AUTHORIZATION_CODE.getValue()
+			.equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE))
+				&& request.getParameter(OAuth2ParameterNames.CODE) != null
+				&& request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
+	}
+
+	public void throwError(String errorCode, String parameterName, String errorUri) {
+		OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
+		throw new OAuth2AuthenticationException(error);
+	}
+
+	/**
+	 * 格式化输出token 信息
+	 * @param authentication 用户认证信息
+	 * @param claims 扩展信息
+	 * @return
+	 * @throws IOException
+	 */
+	public OAuth2AccessTokenResponse sendAccessTokenResponse(OAuth2Authorization authentication,
+			Map<String, Object> claims) {
+
+		OAuth2AccessToken accessToken = authentication.getAccessToken().getToken();
+		OAuth2RefreshToken refreshToken = authentication.getRefreshToken().getToken();
+
+		OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
+			.tokenType(accessToken.getTokenType())
+			.scopes(accessToken.getScopes());
+		if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
+			builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
+		}
+		if (refreshToken != null) {
+			builder.refreshToken(refreshToken.getTokenValue());
+		}
+
+		if (MapUtil.isNotEmpty(claims)) {
+			builder.additionalParameters(claims);
+		}
+		return builder.build();
+	}
+
+}

+ 43 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/OAuth2ErrorCodesExpand.java

@@ -0,0 +1,43 @@
+package com.kxs.common.security.util;
+
+/**
+ * @author jumuning
+ * @description OAuth2 异常信息
+ */
+public interface OAuth2ErrorCodesExpand {
+
+	/** 用户名未找到 */
+	String USERNAME_NOT_FOUND = "username_not_found";
+
+	/** 错误凭证 */
+	String BAD_CREDENTIALS = "bad_credentials";
+
+	/** 用户被锁 */
+	String USER_LOCKED = "user_locked";
+
+	/** 用户禁用 */
+	String USER_DISABLE = "user_disable";
+
+	/** 用户过期 */
+	String USER_EXPIRED = "user_expired";
+
+	/** 证书过期 */
+	String CREDENTIALS_EXPIRED = "credentials_expired";
+
+	/** scope 为空异常 */
+	String SCOPE_IS_EMPTY = "scope_is_empty";
+
+	/**
+	 * 令牌不存在
+	 */
+	String TOKEN_MISSING = "token_missing";
+
+	/** 未知的登录异常 */
+	String UN_KNOW_LOGIN_ERROR = "un_know_login_error";
+
+	/**
+	 * 不合法的Token
+	 */
+	String INVALID_BEARER_TOKEN = "invalid_bearer_token";
+
+}

+ 29 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/OAuthClientException.java

@@ -0,0 +1,29 @@
+package com.kxs.common.security.util;
+
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+
+/**
+ * @author lengleng
+ * @description OAuthClientException 异常信息
+ */
+public class OAuthClientException extends OAuth2AuthenticationException {
+
+	/**
+	 * Constructs a <code>ScopeException</code> with the specified message.
+	 * @param msg the detail message.
+	 */
+	public OAuthClientException(String msg) {
+		super(new OAuth2Error(msg), msg);
+	}
+
+	/**
+	 * Constructs a {@code ScopeException} with the specified message and root cause.
+	 * @param msg the detail message.
+	 * @param cause root cause
+	 */
+	public OAuthClientException(String msg, Throwable cause) {
+		super(new OAuth2Error(msg), cause);
+	}
+
+}

+ 29 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/ScopeException.java

@@ -0,0 +1,29 @@
+package com.kxs.common.security.util;
+
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+
+/**
+ * @author jumuning
+ * @description ScopeException 异常信息
+ */
+public class ScopeException extends OAuth2AuthenticationException {
+
+	/**
+	 * Constructs a <code>ScopeException</code> with the specified message.
+	 * @param msg the detail message.
+	 */
+	public ScopeException(String msg) {
+		super(new OAuth2Error(msg), msg);
+	}
+
+	/**
+	 * Constructs a {@code ScopeException} with the specified message and root cause.
+	 * @param msg the detail message.
+	 * @param cause root cause
+	 */
+	public ScopeException(String msg, Throwable cause) {
+		super(new OAuth2Error(msg), cause);
+	}
+
+}

+ 72 - 0
kxs-common/kxs-common-security/src/main/java/com/kxs/common/security/util/SecurityUtils.java

@@ -0,0 +1,72 @@
+package com.kxs.common.security.util;
+
+import cn.hutool.core.util.StrUtil;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.security.model.KxsUser;
+import lombok.experimental.UtilityClass;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+
+/**
+ * 安全工具類
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/28
+ */
+@UtilityClass
+public class SecurityUtils {
+
+	/**
+	 * 获取Authentication
+	 */
+	public Authentication getAuthentication() {
+		return SecurityContextHolder.getContext().getAuthentication();
+	}
+
+	/**
+	 * 获取用户
+	 */
+	public KxsUser getUser(Authentication authentication) {
+		Object principal = authentication.getPrincipal();
+		if (principal instanceof KxsUser) {
+			return (KxsUser) principal;
+		}
+		return null;
+	}
+
+	/**
+	 * 获取用户
+	 */
+	public KxsUser getUser() {
+		Authentication authentication = getAuthentication();
+		if (authentication == null) {
+			return null;
+		}
+		return getUser(authentication);
+	}
+
+	/**
+	 * 获取用户角色信息
+	 * @return 角色集合
+	 */
+	public List<Long> getRoles() {
+		Authentication authentication = getAuthentication();
+		Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
+
+		List<Long> roleIds = new ArrayList<>();
+		authorities.stream()
+			.filter(granted -> StrUtil.startWith(granted.getAuthority(), SecurityConstants.ROLE))
+			.forEach(granted -> {
+				String id = StrUtil.removePrefix(granted.getAuthority(), SecurityConstants.ROLE);
+				roleIds.add(Long.parseLong(id));
+			});
+		return roleIds;
+	}
+
+}

+ 8 - 0
kxs-common/kxs-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,8 @@
+com.kxs.system.biz.security.KxsUserDetailsServiceImpl
+com.kxs.common.security.service.SkyAppUserDetailsServiceImpl
+com.kxs.common.security.service.SkyCustomUserDetailsServiceImpl
+com.kxs.common.security.service.KxsRedisOAuth2AuthorizationService
+com.kxs.common.security.service.KxsRedisOAuth2AuthorizationConsentService
+com.kxs.common.security.component.SkySecurityInnerAspect
+com.kxs.common.security.component.SkySecurityMessageSourceConfiguration
+com.kxs.system.biz.security.KxsRemoteRegisteredClientRepository

+ 50 - 0
kxs-common/kxs-common-security/src/main/resources/i18n/errors/messages_zh_CN.properties

@@ -0,0 +1,50 @@
+AbstractAccessDecisionManager.accessDenied=\u4E0D\u5141\u8BB8\u8BBF\u95EE
+AbstractLdapAuthenticationProvider.emptyPassword=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF
+AbstractSecurityInterceptor.authenticationNotFound=\u672A\u5728SecurityContext\u4E2D\u67E5\u627E\u5230\u8BA4\u8BC1\u5BF9\u8C61
+OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired=\u8BF7\u6C42\u4EE4\u724C\u5DF2\u8FC7\u671F
+AbstractUserDetailsAuthenticationProvider.smsBadCredentials=\u624B\u673A\u53F7\u4E0D\u5B58\u5728\u767B\u5F55\u5931\u8D25
+AbstractUserDetailsAuthenticationProvider.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF
+AbstractUserDetailsAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
+AbstractUserDetailsAuthenticationProvider.disabled=\u7528\u6237\u5DF2\u5931\u6548
+AbstractUserDetailsAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
+AbstractUserDetailsAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
+AbstractUserDetailsAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken
+AccountStatusUserDetailsChecker.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
+AccountStatusUserDetailsChecker.disabled=\u7528\u6237\u5DF2\u5931\u6548
+AccountStatusUserDetailsChecker.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
+AccountStatusUserDetailsChecker.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
+AclEntryAfterInvocationProvider.noPermission=\u7ED9\u5B9A\u7684Authentication\u5BF9\u8C61({0})\u6839\u672C\u65E0\u6743\u64CD\u63A7\u9886\u57DF\u5BF9\u8C61({1})
+AnonymousAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684AnonymousAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
+BindAuthenticator.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF
+BindAuthenticator.emptyPassword=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF
+CasAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684CasAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
+CasAuthenticationProvider.noServiceTicket=\u672A\u80FD\u591F\u6B63\u786E\u63D0\u4F9B\u5F85\u9A8C\u8BC1\u7684CAS\u670D\u52A1\u7968\u6839
+ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\u5DF2\u7ECF\u8D85\u8FC7\u4E86\u5F53\u524D\u4E3B\u4F53({0})\u88AB\u5141\u8BB8\u7684\u6700\u5927\u4F1A\u8BDD\u6570\u91CF
+DigestAuthenticationFilter.incorrectRealm=\u54CD\u5E94\u7ED3\u679C\u4E2D\u7684Realm\u540D\u5B57({0})\u540C\u7CFB\u7EDF\u6307\u5B9A\u7684Realm\u540D\u5B57({1})\u4E0D\u543B\u5408
+DigestAuthenticationFilter.incorrectResponse=\u9519\u8BEF\u7684\u54CD\u5E94\u7ED3\u679C
+DigestAuthenticationFilter.missingAuth=\u9057\u6F0F\u4E86\u9488\u5BF9'auth' QOP\u7684\u3001\u5FC5\u987B\u7ED9\u5B9A\u7684\u6458\u8981\u53D6\u503C; \u63A5\u6536\u5230\u7684\u5934\u4FE1\u606F\u4E3A{0}
+DigestAuthenticationFilter.missingMandatory=\u9057\u6F0F\u4E86\u5FC5\u987B\u7ED9\u5B9A\u7684\u6458\u8981\u53D6\u503C; \u63A5\u6536\u5230\u7684\u5934\u4FE1\u606F\u4E3A{0}
+DigestAuthenticationFilter.nonceCompromised=Nonce\u4EE4\u724C\u5DF2\u7ECF\u5B58\u5728\u95EE\u9898\u4E86\uFF0C{0}
+DigestAuthenticationFilter.nonceEncoding=Nonce\u672A\u7ECF\u8FC7Base64\u7F16\u7801; \u76F8\u5E94\u7684nonce\u53D6\u503C\u4E3A {0}
+DigestAuthenticationFilter.nonceExpired=Nonce\u5DF2\u7ECF\u8FC7\u671F/\u8D85\u65F6
+DigestAuthenticationFilter.nonceNotNumeric=Nonce\u4EE4\u724C\u7684\u7B2C1\u90E8\u5206\u5E94\u8BE5\u662F\u6570\u5B57\uFF0C\u4F46\u7ED3\u679C\u5374\u662F{0}
+DigestAuthenticationFilter.nonceNotTwoTokens=Nonce\u5E94\u8BE5\u7531\u4E24\u90E8\u5206\u53D6\u503C\u6784\u6210\uFF0C\u4F46\u7ED3\u679C\u5374\u662F{0}
+DigestAuthenticationFilter.usernameNotFound=\u7528\u6237\u540D{0}\u672A\u627E\u5230
+ExceptionTranslationFilter.insufficientAuthentication=\u8BBF\u95EE\u6B64\u8D44\u6E90\u9700\u8981\u5B8C\u5168\u8EAB\u4EFD\u9A8C\u8BC1
+JdbcDaoImpl.noAuthority=\u6CA1\u6709\u4E3A\u7528\u6237{0}\u6307\u5B9A\u89D2\u8272
+JdbcDaoImpl.notFound=\u672A\u627E\u5230\u7528\u6237{0}
+LdapAuthenticationProvider.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF
+LdapAuthenticationProvider.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
+LdapAuthenticationProvider.disabled=\u7528\u6237\u5DF2\u5931\u6548
+LdapAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
+LdapAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
+LdapAuthenticationProvider.emptyUsername=\u7528\u6237\u540D\u4E0D\u5141\u8BB8\u4E3A\u7A7A
+LdapAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken
+PasswordComparisonAuthenticator.badCredentials=\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF
+#PersistentTokenBasedRememberMeServices.cookieStolen=Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
+ProviderManager.providerNotFound=\u672A\u67E5\u627E\u5230\u9488\u5BF9{0}\u7684AuthenticationProvider
+RememberMeAuthenticationProvider.incorrectKey=\u5C55\u793ARememberMeAuthenticationToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
+RunAsImplAuthenticationProvider.incorrectKey=\u5C55\u793A\u7684RunAsUserToken\u4E0D\u542B\u6709\u9884\u671F\u7684key
+SubjectDnX509PrincipalExtractor.noMatching=\u672A\u5728subjectDN\: {0}\u4E2D\u627E\u5230\u5339\u914D\u7684\u6A21\u5F0F
+SwitchUserFilter.noCurrentUser=\u4E0D\u5B58\u5728\u5F53\u524D\u7528\u6237
+SwitchUserFilter.noOriginalAuthentication=\u4E0D\u80FD\u591F\u67E5\u627E\u5230\u539F\u5148\u7684\u5DF2\u8BA4\u8BC1\u5BF9\u8C61

+ 35 - 0
kxs-common/pom.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-common</artifactId>
+    <packaging>pom</packaging>
+
+    <description>kxs 公共聚合模块</description>
+
+
+    <modules>
+        <module>kxs-common-core</module>
+        <module>kxs-common-security</module>
+        <module>kxs-common-log</module>
+        <module>kxs-common-mybatis</module>
+    </modules>
+    <dependencyManagement>
+        <dependencies>
+            <!--sky 公共版本定义-->
+            <dependency>
+                <groupId>com.kxs</groupId>
+                <artifactId>kxs-common-core</artifactId>
+                <version>${kxs.common.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>

+ 43 - 0
kxs-system/kxs-system-api/pom.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.kxs</groupId>
+        <artifactId>kxs-system</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>kxs-system-api</artifactId>
+    <packaging>jar</packaging>
+
+    <properties>
+        <maven.compiler.source>17</maven.compiler.source>
+        <maven.compiler.target>17</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!--core 工具类-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-core</artifactId>
+        </dependency>
+        <!-- excel 导入导出 https://github.com/sky-mesh/excel-spring-boot-starter -->
+        <dependency>
+            <groupId>com.pig4cloud.excel</groupId>
+            <artifactId>excel-spring-boot-starter</artifactId>
+        </dependency>
+        <!--mybatis 依赖-->
+        <dependency>
+            <groupId>com.kxs</groupId>
+            <artifactId>kxs-common-mybatis</artifactId>
+        </dependency>
+        <!--swagger 注解-->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 91 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/dto/SysLogDTO.java

@@ -0,0 +1,91 @@
+package com.kxs.system.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author lengleng
+ * @date 2020/10/9
+ * <p>
+ * 日志查询传输对象
+ */
+@Data
+@Schema(description = "日志查询对象")
+public class SysLogDTO {
+
+	/**
+	 * 编号
+	 */
+	private Long id;
+
+	/**
+	 * 日志类型
+	 */
+	@NotBlank(message = "日志类型不能为空")
+	private String logType;
+
+	/**
+	 * 日志标题
+	 */
+	@NotBlank(message = "日志标题不能为空")
+	private String title;
+
+	/**
+	 * 创建者
+	 */
+	private String createBy;
+
+	/**
+	 * 更新时间
+	 */
+	private LocalDateTime updateTime;
+
+	/**
+	 * 操作IP地址
+	 */
+	private String remoteAddr;
+
+	/**
+	 * 用户代理
+	 */
+	private String userAgent;
+
+	/**
+	 * 请求URI
+	 */
+	private String requestUri;
+
+	/**
+	 * 操作方式
+	 */
+	private String method;
+
+	/**
+	 * 操作提交的数据
+	 */
+	private String params;
+
+	/**
+	 * 执行时间
+	 */
+	private Long time;
+
+	/**
+	 * 异常信息
+	 */
+	private String exception;
+
+	/**
+	 * 服务ID
+	 */
+	private String serviceId;
+
+	/**
+	 * 创建时间区间 [开始时间,结束时间]
+	 */
+	private LocalDateTime[] createTime;
+
+}

+ 42 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/dto/UserDTO.java

@@ -0,0 +1,42 @@
+package com.kxs.system.api.dto;
+
+import com.kxs.system.api.model.SysUser;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/**
+ * @author lengleng
+ * @date 2023/11/5
+ */
+@Data
+@Schema(description = "系统用户传输对象")
+@EqualsAndHashCode(callSuper = true)
+public class UserDTO extends SysUser {
+
+	/**
+	 * 角色ID
+	 */
+	@Schema(description = "角色id集合")
+	private List<Long> role;
+
+	/**
+	 * 部门id
+	 */
+	@Schema(description = "部门id")
+	private Long deptId;
+
+	/**
+	 * 岗位ID
+	 */
+	private List<Long> post;
+
+	/**
+	 * 新密码
+	 */
+	@Schema(description = "新密码")
+	private String newpassword1;
+
+}

+ 35 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/dto/UserInfo.java

@@ -0,0 +1,35 @@
+package com.kxs.system.api.dto;
+
+import com.kxs.system.api.model.SysUser;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lengleng
+ * @date 2023/11/11
+ */
+@Data
+@Schema(description = "用户信息")
+public class UserInfo implements Serializable {
+
+	/**
+	 * 用户基本信息
+	 */
+	@Schema(description = "用户基本信息")
+	private SysUser sysUser;
+
+	/**
+	 * 权限标识集合
+	 */
+	@Schema(description = "权限标识集合")
+	private String[] permissions;
+
+	/**
+	 * 角色集合
+	 */
+	@Schema(description = "角色标识集合")
+	private Long[] roles;
+
+}

+ 91 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysDict.java

@@ -0,0 +1,91 @@
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 字典表
+ *
+ * @author lengleng
+ * @date 2023/03/19
+ */
+@Data
+@Schema(description = "字典类型")
+@EqualsAndHashCode(callSuper = true)
+public class SysDict extends Model<SysDict> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 编号
+	 */
+	@TableId(type = IdType.ASSIGN_ID)
+	@Schema(description = "字典编号")
+	private Long id;
+
+	/**
+	 * 类型
+	 */
+	@Schema(description = "字典类型")
+	private String dictType;
+
+	/**
+	 * 描述
+	 */
+	@Schema(description = "字典描述")
+	private String description;
+
+	/**
+	 * 创建时间
+	 */
+	@Schema(description = "创建时间")
+	@TableField(fill = FieldFill.INSERT)
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@Schema(description = "更新时间")
+	@TableField(fill = FieldFill.UPDATE)
+	private LocalDateTime updateTime;
+
+	/**
+	 * 是否是系统内置
+	 */
+	@Schema(description = "是否系统内置")
+	private String systemFlag;
+
+	/**
+	 * 备注信息
+	 */
+	@Schema(description = "备注信息")
+	private String remarks;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+}

+ 113 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysDictItem.java

@@ -0,0 +1,113 @@
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * 字典项
+ *
+ * @author lengleng
+ * @date 2023/03/19
+ */
+@Data
+@Schema(description = "字典项")
+@EqualsAndHashCode(callSuper = true)
+public class SysDictItem extends Model<SysDictItem> {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 编号
+	 */
+	@TableId(type = IdType.ASSIGN_ID)
+	@Schema(description = "字典项id")
+	private Long id;
+
+	/**
+	 * 所属字典类id
+	 */
+	@Schema(description = "所属字典类id")
+	private Long dictId;
+
+	/**
+	 * 数据值
+	 */
+	@Schema(description = "数据值")
+	@JsonProperty(value = "value")
+	private String itemValue;
+
+	/**
+	 * 标签名
+	 */
+	@Schema(description = "标签名")
+	private String label;
+
+	/**
+	 * 类型
+	 */
+	@Schema(description = "类型")
+	private String dictType;
+
+	/**
+	 * 描述
+	 */
+	@Schema(description = "描述")
+	private String description;
+
+	/**
+	 * 排序(升序)
+	 */
+	@Schema(description = "排序值,默认升序")
+	private Integer sortOrder;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+
+	/**
+	 * 备注信息
+	 */
+	@Schema(description = "备注信息")
+	private String remarks;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+}

+ 98 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysFile.java

@@ -0,0 +1,98 @@
+
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 文件管理
+ *
+ * @author Luckly
+ * @date 2023-06-18 17:18:42
+ */
+@Data
+@Schema(description = "文件")
+@EqualsAndHashCode(callSuper = true)
+public class SysFile extends Model<SysFile> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 编号
+	 */
+	@TableId(type = IdType.ASSIGN_ID)
+	@Schema(description = "文件编号")
+	private Long id;
+
+	/**
+	 * 文件名
+	 */
+	@Schema(description = "文件名")
+	private String fileName;
+
+	/**
+	 * 原文件名
+	 */
+	@Schema(description = "原始文件名")
+	private String original;
+
+	/**
+	 * 容器名称
+	 */
+	@Schema(description = "存储桶名称")
+	private String bucketName;
+
+	/**
+	 * 文件类型
+	 */
+	@Schema(description = "文件类型")
+	private String type;
+
+	/**
+	 * 文件大小
+	 */
+	@Schema(description = "文件大小")
+	private Long fileSize;
+
+	/**
+	 * 上传人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建者")
+	private String createBy;
+
+	/**
+	 * 上传时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "更新者")
+	private String updateBy;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+
+	/**
+	 * 删除标识:1-删除,0-正常
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+}

+ 140 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysLog.java

@@ -0,0 +1,140 @@
+
+package com.kxs.system.api.model;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.baomidou.mybatisplus.annotation.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 日志表
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-11-20
+ */
+@Data
+@Schema(description = "日志")
+public class SysLog implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 编号
+	 */
+	@TableId(type = IdType.ASSIGN_ID)
+	@ExcelProperty("日志编号")
+	@Schema(description = "日志编号")
+	private Long id;
+
+	/**
+	 * 日志类型
+	 */
+	@NotBlank(message = "日志类型不能为空")
+	@ExcelProperty("日志类型(0-正常 9-错误)")
+	@Schema(description = "日志类型")
+	private String logType;
+
+	/**
+	 * 日志标题
+	 */
+	@NotBlank(message = "日志标题不能为空")
+	@ExcelProperty("日志标题")
+	@Schema(description = "日志标题")
+	private String title;
+
+	/**
+	 * 创建者
+	 */
+	@ExcelProperty("创建人")
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 创建时间
+	 */
+	@ExcelProperty("创建时间")
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@ExcelIgnore
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+
+	/**
+	 * 操作IP地址
+	 */
+	@ExcelProperty("操作ip地址")
+	@Schema(description = "操作ip地址")
+	private String remoteAddr;
+
+	/**
+	 * 用户代理
+	 */
+	@Schema(description = "用户代理")
+	private String userAgent;
+
+	/**
+	 * 请求URI
+	 */
+	@ExcelProperty("浏览器")
+	@Schema(description = "请求uri")
+	private String requestUri;
+
+	/**
+	 * 操作方式
+	 */
+	@ExcelProperty("操作方式")
+	@Schema(description = "操作方式")
+	private String method;
+
+	/**
+	 * 操作提交的数据
+	 */
+	@ExcelProperty("提交数据")
+	@Schema(description = "提交数据")
+	private String params;
+
+	/**
+	 * 执行时间
+	 */
+	@ExcelProperty("执行时间")
+	@Schema(description = "方法执行时间")
+	private Long time;
+
+	/**
+	 * 异常信息
+	 */
+	@ExcelProperty("异常信息")
+	@Schema(description = "异常信息")
+	private String exception;
+
+	/**
+	 * 服务ID
+	 */
+	@ExcelProperty("应用标识")
+	@Schema(description = "应用标识")
+	private String serviceId;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@ExcelIgnore
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+}

+ 138 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysMenu.java

@@ -0,0 +1,138 @@
+
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 菜单权限表
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-11-08
+ */
+@Data
+@Schema(description = "菜单")
+@EqualsAndHashCode(callSuper = true)
+public class SysMenu extends Model<SysMenu> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 菜单ID
+	 */
+	@TableId(value = "menu_id", type = IdType.ASSIGN_ID)
+	@Schema(description = "菜单id")
+	private Long menuId;
+
+	/**
+	 * 菜单名称
+	 */
+	@NotBlank(message = "菜单名称不能为空")
+	@Schema(description = "菜单名称")
+	private String name;
+
+	/**
+	 * 菜单名称
+	 */
+	@Schema(description = "菜单名称")
+	private String enName;
+
+	/**
+	 * 菜单权限标识
+	 */
+	@Schema(description = "菜单权限标识")
+	private String permission;
+
+	/**
+	 * 父菜单ID
+	 */
+	@NotNull(message = "菜单父ID不能为空")
+	@Schema(description = "菜单父id")
+	private Long parentId;
+
+	/**
+	 * 图标
+	 */
+	@Schema(description = "菜单图标")
+	private String icon;
+
+	/**
+	 * 前端路由标识路径,默认和 comment 保持一致 过期
+	 */
+	@Schema(description = "前端路由标识路径")
+	private String path;
+
+	/**
+	 * 菜单显示隐藏控制
+	 */
+	@Schema(description = "菜单是否显示")
+	private String visible;
+
+	/**
+	 * 排序值
+	 */
+	@Schema(description = "排序值")
+	private Integer sortOrder;
+
+	/**
+	 * 菜单类型 (0菜单 1按钮)
+	 */
+	@NotNull(message = "菜单类型不能为空")
+	@Schema(description = "菜单类型,0:菜单 1:按钮")
+	private String menuType;
+
+	/**
+	 * 路由缓冲
+	 */
+	@Schema(description = "路由缓冲")
+	private String keepAlive;
+
+	@Schema(description = "菜单是否内嵌")
+	private String embedded;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+
+	/**
+	 * 0--正常 1--删除
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+}

+ 137 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysOauthClientDetails.java

@@ -0,0 +1,137 @@
+
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 客户端信息
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-05-15
+ */
+@Data
+@Schema(description = "客户端信息")
+@EqualsAndHashCode(callSuper = true)
+public class SysOauthClientDetails extends Model<SysOauthClientDetails> {
+
+	private static final long serialVersionUID = 1L;
+
+	@TableId(value = "id", type = IdType.ASSIGN_ID)
+	@Schema(description = "id")
+	private Long id;
+
+	/**
+	 * 客户端ID
+	 */
+	@NotBlank(message = "client_id 不能为空")
+	@Schema(description = "客户端id")
+	private String clientId;
+
+	/**
+	 * 客户端密钥
+	 */
+	@NotBlank(message = "client_secret 不能为空")
+	@Schema(description = "客户端密钥")
+	private String clientSecret;
+
+	/**
+	 * 资源ID
+	 */
+	@Schema(description = "资源id列表")
+	private String resourceIds;
+
+	/**
+	 * 作用域
+	 */
+	@NotBlank(message = "scope 不能为空")
+	@Schema(description = "作用域")
+	private String scope;
+
+	/**
+	 * 授权方式[A,B,C]
+	 */
+	@Schema(description = "授权方式")
+	private String[] authorizedGrantTypes;
+
+	/**
+	 * 回调地址
+	 */
+	@Schema(description = "回调地址")
+	private String webServerRedirectUri;
+
+	/**
+	 * 权限
+	 */
+	@Schema(description = "权限列表")
+	private String authorities;
+
+	/**
+	 * 请求令牌有效时间
+	 */
+	@Schema(description = "请求令牌有效时间")
+	private Integer accessTokenValidity;
+
+	/**
+	 * 刷新令牌有效时间
+	 */
+	@Schema(description = "刷新令牌有效时间")
+	private Integer refreshTokenValidity;
+
+	/**
+	 * 扩展信息
+	 */
+	@Schema(description = "扩展信息")
+	private String additionalInformation;
+
+	/**
+	 * 是否自动放行
+	 */
+	@Schema(description = "是否自动放行")
+	private String autoapprove;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+
+}

+ 98 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysPost.java

@@ -0,0 +1,98 @@
+
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 岗位信息表
+ *
+ * @author fxz
+ * @date 2022-03-26 12:50:43
+ */
+@Data
+@TableName("sys_post")
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "岗位信息表")
+public class SysPost extends Model<SysPost> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 岗位ID
+	 */
+	@TableId(value = "post_id", type = IdType.ASSIGN_ID)
+	@Schema(description = "岗位ID")
+	private Long postId;
+
+	/**
+	 * 岗位编码
+	 */
+	@NotBlank(message = "岗位编码不能为空")
+	@Schema(description = "岗位编码")
+	private String postCode;
+
+	/**
+	 * 岗位名称
+	 */
+	@NotBlank(message = "岗位名称不能为空")
+	@Schema(description = "岗位名称")
+	private String postName;
+
+	/**
+	 * 岗位排序
+	 */
+	@NotNull(message = "排序值不能为空")
+	@Schema(description = "岗位排序")
+	private Integer postSort;
+
+	/**
+	 * 岗位描述
+	 */
+	@Schema(description = "岗位描述")
+	private String remark;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 是否删除 -1:已删除 0:正常
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "是否删除  -1:已删除  0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建时间
+	 */
+	@Schema(description = "创建时间")
+	@TableField(fill = FieldFill.INSERT)
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@Schema(description = "更新时间")
+	@TableField(fill = FieldFill.UPDATE)
+	private LocalDateTime updateTime;
+
+}

+ 110 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysPublicParam.java

@@ -0,0 +1,110 @@
+
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 公共参数配置
+ *
+ * @author Lucky
+ * @date 2023-04-29
+ */
+@Data
+@Schema(description = "公共参数")
+@EqualsAndHashCode(callSuper = true)
+public class SysPublicParam extends Model<SysPublicParam> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 编号
+	 */
+	@TableId(type = IdType.ASSIGN_ID)
+	@Schema(description = "公共参数编号")
+	private Long publicId;
+
+	/**
+	 * 公共参数名称
+	 */
+	@Schema(description = "公共参数名称", required = true, example = "公共参数名称")
+	private String publicName;
+
+	/**
+	 * 公共参数地址值,英文大写+下划线
+	 */
+	@Schema(description = "键[英文大写+下划线]", required = true, example = "PIGX_PUBLIC_KEY")
+	private String publicKey;
+
+	/**
+	 * 值
+	 */
+	@Schema(description = "值", required = true, example = "999")
+	private String publicValue;
+
+	/**
+	 * 状态(1有效;2无效;)
+	 */
+	@Schema(description = "标识[1有效;2无效]", example = "1")
+	private String status;
+
+	/**
+	 * 公共参数编码
+	 */
+	@Schema(description = "编码", example = "^(PIG|PIGX)$")
+	private String validateCode;
+
+	/**
+	 * 是否是系统内置
+	 */
+	@Schema(description = "是否是系统内置")
+	private String systemFlag;
+
+	/**
+	 * 配置类型:0-默认;1-检索;2-原文;3-报表;4-安全;5-文档;6-消息;9-其他
+	 */
+	@Schema(description = "类型[1-检索;2-原文...]", example = "1")
+	private String publicType;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 删除标记
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 更新时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "更新时间")
+	private LocalDateTime updateTime;
+
+}

+ 78 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysRole.java

@@ -0,0 +1,78 @@
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 角色表
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-10-29
+ */
+@Data
+@Schema(description = "角色")
+@EqualsAndHashCode(callSuper = true)
+public class SysRole extends Model<SysRole> {
+
+	private static final long serialVersionUID = 1L;
+
+	@TableId(value = "role_id", type = IdType.ASSIGN_ID)
+	@Schema(description = "角色编号")
+	private Long roleId;
+
+	@NotBlank(message = "角色名称不能为空")
+	@Schema(description = "角色名称")
+	private String roleName;
+
+	@NotBlank(message = "角色标识不能为空")
+	@Schema(description = "角色标识")
+	private String roleCode;
+
+	@Schema(description = "角色描述")
+	private String roleDesc;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@Schema(description = "创建时间")
+	@TableField(fill = FieldFill.INSERT)
+	private LocalDateTime createTime;
+
+	/**
+	 * 修改时间
+	 */
+	@Schema(description = "修改时间")
+	@TableField(fill = FieldFill.UPDATE)
+	private LocalDateTime updateTime;
+
+	/**
+	 * 删除标识(0-正常,1-删除)
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+}

+ 35 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysRoleMenu.java

@@ -0,0 +1,35 @@
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 角色菜单表
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-10-29
+ */
+@Data
+@Schema(description = "角色菜单")
+@EqualsAndHashCode(callSuper = true)
+public class SysRoleMenu extends Model<SysRoleMenu> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 角色ID
+	 */
+	@Schema(description = "角色id")
+	private Long roleId;
+
+	/**
+	 * 菜单ID
+	 */
+	@Schema(description = "菜单id")
+	private Long menuId;
+
+}

+ 159 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysUser.java

@@ -0,0 +1,159 @@
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 用户表
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-10-29
+ */
+@Data
+@Schema(description = "用户")
+public class SysUser implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 主键ID
+	 */
+	@TableId(value = "user_id", type = IdType.ASSIGN_ID)
+	@Schema(description = "主键id")
+	private Long userId;
+
+	/**
+	 * 用户名
+	 */
+	@Schema(description = "用户名")
+	private String username;
+
+	/**
+	 * 密码
+	 */
+	@Schema(description = "密码")
+	private String password;
+
+	/**
+	 * 随机盐
+	 */
+	@JsonIgnore
+	@Schema(description = "随机盐")
+	private String salt;
+
+	/**
+	 * 创建人
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建人")
+	private String createBy;
+
+	/**
+	 * 修改人
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改人")
+	private String updateBy;
+
+	/**
+	 * 创建时间
+	 */
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "创建时间")
+	private LocalDateTime createTime;
+
+	/**
+	 * 修改时间
+	 */
+	@TableField(fill = FieldFill.UPDATE)
+	@Schema(description = "修改时间")
+	private LocalDateTime updateTime;
+
+	/**
+	 * 0-正常,1-删除
+	 */
+	@TableLogic
+	@TableField(fill = FieldFill.INSERT)
+	@Schema(description = "删除标记,1:已删除,0:正常")
+	private String delFlag;
+
+	/**
+	 * 锁定标记
+	 */
+	@Schema(description = "锁定标记")
+	private String lockFlag;
+
+	/**
+	 * 手机号
+	 */
+	@Schema(description = "手机号")
+	private String phone;
+
+	/**
+	 * 头像
+	 */
+	@Schema(description = "头像地址")
+	private String avatar;
+
+	/**
+	 * 部门ID
+	 */
+	@Schema(description = "用户所属部门id")
+	private Long deptId;
+
+	/**
+	 * 微信openid
+	 */
+	@Schema(description = "微信openid")
+	private String wxOpenid;
+
+	/**
+	 * 微信小程序openId
+	 */
+	@Schema(description = "微信小程序openid")
+	private String miniOpenid;
+
+	/**
+	 * QQ openid
+	 */
+	@Schema(description = "QQ openid")
+	private String qqOpenid;
+
+	/**
+	 * 码云唯一标识
+	 */
+	@Schema(description = "码云唯一标识")
+	private String giteeLogin;
+
+	/**
+	 * 开源中国唯一标识
+	 */
+	@Schema(description = "开源中国唯一标识")
+	private String oscId;
+
+	/**
+	 * 昵称
+	 */
+	@Schema(description = "昵称")
+	private String nickname;
+
+	/**
+	 * 姓名
+	 */
+	@Schema(description = "姓名")
+	private String name;
+
+	/**
+	 * 邮箱
+	 */
+	@Schema(description = "邮箱")
+	private String email;
+
+}

+ 35 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/model/SysUserRole.java

@@ -0,0 +1,35 @@
+package com.kxs.system.api.model;
+
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 用户角色表
+ * </p>
+ *
+ * @author lengleng
+ * @since 2023-10-29
+ */
+@Data
+@Schema(description = "用户角色")
+@EqualsAndHashCode(callSuper = true)
+public class SysUserRole extends Model<SysUserRole> {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 用户ID
+	 */
+	@Schema(description = "用户id")
+	private Long userId;
+
+	/**
+	 * 角色ID
+	 */
+	@Schema(description = "角色id")
+	private Long roleId;
+
+}

+ 98 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/util/DictResolver.java

@@ -0,0 +1,98 @@
+package com.kxs.system.api.util;
+
+import cn.hutool.core.lang.Assert;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.kxs.common.core.util.SpringContextHolder;
+import com.kxs.system.api.model.SysDictItem;
+import lombok.experimental.UtilityClass;
+
+import java.util.List;
+
+/**
+ * @author fxz
+ * @date 2022/3/24 字典解析器
+ */
+@UtilityClass
+public class DictResolver {
+
+	/**
+	 * 根据字典类型获取所有字典项
+	 * @param type 字典类型
+	 * @return 字典数据项集合
+	 */
+	public List<SysDictItem> getDictItemsByType(String type) {
+		Assert.isTrue(StringUtils.isNotBlank(type), "参数不合法");
+
+		RemoteDictService remoteDictService = SpringContextHolder.getBean(RemoteDictService.class);
+
+		return remoteDictService.getDictByType(type).getData();
+	}
+
+	/**
+	 * 根据字典类型以及字典项字典值获取字典标签
+	 * @param type 字典类型
+	 * @param itemValue 字典项字典值
+	 * @return 字典项标签值
+	 */
+	public String getDictItemLabel(String type, String itemValue) {
+		Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemValue), "参数不合法");
+
+		SysDictItem sysDictItem = getDictItemByItemValue(type, itemValue);
+
+		return ObjectUtils.isNotEmpty(sysDictItem) ? sysDictItem.getLabel() : StringPool.EMPTY;
+	}
+
+	/**
+	 * 根据字典类型以及字典标签获取字典值
+	 * @param type 字典类型
+	 * @param itemLabel 字典数据标签
+	 * @return 字典数据项值
+	 */
+	public String getDictItemValue(String type, String itemLabel) {
+		Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemLabel), "参数不合法");
+
+		SysDictItem sysDictItem = getDictItemByItemLabel(type, itemLabel);
+
+		return ObjectUtils.isNotEmpty(sysDictItem) ? sysDictItem.getItemValue() : StringPool.EMPTY;
+	}
+
+	/**
+	 * 根据字典类型以及字典值获取字典项
+	 * @param type 字典类型
+	 * @param itemValue 字典数据值
+	 * @return 字典数据项
+	 */
+	public SysDictItem getDictItemByItemValue(String type, String itemValue) {
+		Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemValue), "参数不合法");
+
+		List<SysDictItem> dictItemList = getDictItemsByType(type);
+
+		if (CollectionUtils.isNotEmpty(dictItemList)) {
+			return dictItemList.stream().filter(item -> itemValue.equals(item.getItemValue())).findFirst().orElse(null);
+		}
+
+		return null;
+	}
+
+	/**
+	 * 根据字典类型以及字典标签获取字典项
+	 * @param type 字典类型
+	 * @param itemLabel 字典数据项标签
+	 * @return 字典数据项
+	 */
+	public SysDictItem getDictItemByItemLabel(String type, String itemLabel) {
+		Assert.isTrue(StringUtils.isNotBlank(type) && StringUtils.isNotBlank(itemLabel), "参数不合法");
+
+		List<SysDictItem> dictItemList = getDictItemsByType(type);
+
+		if (CollectionUtils.isNotEmpty(dictItemList)) {
+			return dictItemList.stream().filter(item -> itemLabel.equals(item.getLabel())).findFirst().orElse(null);
+		}
+
+		return null;
+	}
+
+}

+ 60 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/util/ParamResolver.java

@@ -0,0 +1,60 @@
+package com.kxs.system.api.util;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.StrUtil;
+import com.kxs.common.core.constant.SecurityConstants;
+import com.kxs.common.core.util.SpringContextHolder;
+import com.kxs.system.api.feign.RemoteParamService;
+import lombok.experimental.UtilityClass;
+
+/**
+ * @author lengleng
+ * @date 2020/5/12
+ * <p>
+ * 系统参数配置解析器
+ */
+@UtilityClass
+public class ParamResolver {
+
+	/**
+	 * 根据key 查询value 配置
+	 * @param key key
+	 * @param defaultVal 默认值
+	 * @return value
+	 */
+	public Long getLong(String key, Long... defaultVal) {
+		return checkAndGet(key, Long.class, defaultVal);
+	}
+
+	/**
+	 * 根据key 查询value 配置
+	 * @param key key
+	 * @param defaultVal 默认值
+	 * @return value
+	 */
+	public String getStr(String key, String... defaultVal) {
+		return checkAndGet(key, String.class, defaultVal);
+	}
+
+	private <T> T checkAndGet(String key, Class<T> clazz, T... defaultVal) {
+		// 校验入参是否合法
+		if (StrUtil.isBlank(key) || defaultVal.length > 1) {
+			throw new IllegalArgumentException("参数不合法");
+		}
+
+		RemoteParamService remoteParamService = SpringContextHolder.getBean(RemoteParamService.class);
+
+		String result = remoteParamService.getByKey(key, SecurityConstants.FROM_IN).getData();
+
+		if (StrUtil.isNotBlank(result)) {
+			return Convert.convert(clazz, result);
+		}
+
+		if (defaultVal.length == 1) {
+			return Convert.convert(clazz, defaultVal.clone()[0]);
+
+		}
+		return null;
+	}
+
+}

+ 59 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/PreLogVO.java

@@ -0,0 +1,59 @@
+package com.kxs.system.api.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+
+/**
+ * 前端日志vo
+ *
+ * @author 没秃顶的码农
+ * @date 2023/10/29
+ */
+@Data
+@Schema(description = "前端日志展示对象")
+public class PreLogVO {
+
+	/**
+	 * 请求url
+	 */
+	@Schema(description = "请求url")
+	private String url;
+
+	/**
+	 * 请求耗时
+	 */
+	@Schema(description = "请求耗时")
+	private String time;
+
+	/**
+	 * 请求用户
+	 */
+	@Schema(description = "请求用户")
+	private String user;
+
+	/**
+	 * 请求结果
+	 */
+	@Schema(description = "请求结果0:成功9:失败")
+	private String type;
+
+	/**
+	 * 请求传递参数
+	 */
+	@Schema(description = "请求传递参数")
+	private String message;
+
+	/**
+	 * 异常信息
+	 */
+	@Schema(description = "异常信息")
+	private String stack;
+
+	/**
+	 * 日志标题
+	 */
+	@Schema(description = "日志标题")
+	private String info;
+
+}

+ 65 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/RoleExcelVO.java

@@ -0,0 +1,65 @@
+package com.kxs.system.api.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.pig4cloud.plugin.excel.annotation.ExcelLine;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 角色excel 对应的实体
+ *
+ * @author fxz
+ * @date 2022/3/21
+ */
+@Data
+@ColumnWidth(30)
+public class RoleExcelVO implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 导入时候回显行号
+	 */
+	@ExcelLine
+	@ExcelIgnore
+	private Long lineNum;
+
+	/**
+	 * 主键ID
+	 */
+	@ExcelProperty("角色编号")
+	private Long roleId;
+
+	/**
+	 * 角色名称
+	 */
+	@NotBlank(message = "角色名称不能为空")
+	@ExcelProperty("角色名称")
+	private String roleName;
+
+	/**
+	 * 角色标识
+	 */
+	@NotBlank(message = "角色标识不能为空")
+	@ExcelProperty("角色标识")
+	private String roleCode;
+
+	/**
+	 * 角色描述
+	 */
+	@NotBlank(message = "角色描述不能为空")
+	@ExcelProperty("角色描述")
+	private String roleDesc;
+
+	/**
+	 * 创建时间
+	 */
+	@ExcelProperty(value = "创建时间")
+	private LocalDateTime createTime;
+
+}

+ 24 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/RoleVO.java

@@ -0,0 +1,24 @@
+package com.kxs.system.api.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * @author lengleng
+ * @date 2020/2/10
+ */
+@Data
+@Schema(description = "前端角色展示对象")
+public class RoleVO {
+
+	/**
+	 * 角色id
+	 */
+	private Long roleId;
+
+	/**
+	 * 菜单列表
+	 */
+	private String menuIds;
+
+}

+ 28 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/TokenVo.java

@@ -0,0 +1,28 @@
+package com.kxs.system.api.vo;
+
+import lombok.Data;
+
+/**
+ * 前端展示令牌管理
+ *
+ * @author lengleng
+ * @date 2022/6/2
+ */
+@Data
+public class TokenVo {
+
+	private String id;
+
+	private Long userId;
+
+	private String clientId;
+
+	private String username;
+
+	private String accessToken;
+
+	private String issuedAt;
+
+	private String expiresAt;
+
+}

+ 108 - 0
kxs-system/kxs-system-api/src/main/java/com/kxs/system/api/vo/UserExcelVO.java

@@ -0,0 +1,108 @@
+package com.kxs.system.api.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import com.pig4cloud.plugin.excel.annotation.ExcelLine;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 用户excel 对应的实体
+ *
+ * @author lengleng
+ * @date 2021/8/4
+ */
+@Data
+@ColumnWidth(30)
+public class UserExcelVO implements Serializable {
+
+	@Serial
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 导入时候回显行号
+	 */
+	@ExcelLine
+	@ExcelIgnore
+	private Long lineNum;
+
+	/**
+	 * 主键ID
+	 */
+	@ExcelProperty("用户编号")
+	private Long userId;
+
+	/**
+	 * 用户名
+	 */
+	@NotBlank(message = "用户名不能为空")
+	@ExcelProperty("用户名")
+	private String username;
+
+	/**
+	 * 手机号
+	 */
+	@NotBlank(message = "手机号不能为空")
+	@ExcelProperty("手机号")
+	private String phone;
+
+	/**
+	 * 手机号
+	 */
+	@NotBlank(message = "昵称不能为空")
+	@ExcelProperty("昵称")
+	private String nickname;
+
+	/**
+	 * 手机号
+	 */
+	@NotBlank(message = "姓名不能为空")
+	@ExcelProperty("姓名")
+	private String name;
+
+	/**
+	 * 手机号
+	 */
+	@NotBlank(message = "邮箱不能为空")
+	@ExcelProperty("邮箱")
+	private String email;
+
+	/**
+	 * 部门名称
+	 */
+	@NotBlank(message = "部门名称不能为空")
+	@ExcelProperty("部门名称")
+	private String deptName;
+
+	/**
+	 * 角色列表
+	 */
+	@NotBlank(message = "角色不能为空")
+	@ExcelProperty("角色")
+	private String roleNameList;
+
+	/**
+	 * 角色列表
+	 */
+	@NotBlank(message = "岗位不能为空")
+	@ExcelProperty("岗位名称")
+	private String postNameList;
+
+	/**
+	 * 锁定标记
+	 */
+	@ExcelProperty("锁定标记,0:正常,9:已锁定")
+	private String lockFlag;
+
+	/**
+	 * 创建时间
+	 */
+	@ExcelProperty(value = "创建时间")
+	private LocalDateTime createTime;
+
+}

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