zhouzeyu hace 3 semanas
commit
2b3585f361
Se han modificado 100 ficheros con 6188 adiciones y 0 borrados
  1. 38 0
      .dockerignore
  2. 75 0
      .gitignore
  3. 0 0
      .mvn
  4. 17 0
      Dockerfile
  5. 24 0
      README.md
  6. 10 0
      docker-compose.yml
  7. BIN
      logs/2025-12/hdis-2025-12-09-1.log.gz
  8. 118 0
      pom.xml
  9. 487 0
      tr-dependencies/pom.xml
  10. 159 0
      tr-framework/pom.xml
  11. 18 0
      tr-framework/src/main/java/cn/tr/core/annotation/TenantIgnore.java
  12. 56 0
      tr-framework/src/main/java/cn/tr/core/config/CreateAndUpdateMetaObjectHandler.java
  13. 26 0
      tr-framework/src/main/java/cn/tr/core/config/FaviconInterceptor.java
  14. 22 0
      tr-framework/src/main/java/cn/tr/core/config/RedisConfig.java
  15. 39 0
      tr-framework/src/main/java/cn/tr/core/constant/MybatisConstant.java
  16. 59 0
      tr-framework/src/main/java/cn/tr/core/context/BaseController.java
  17. 49 0
      tr-framework/src/main/java/cn/tr/core/context/RequestContextHolder.java
  18. 127 0
      tr-framework/src/main/java/cn/tr/core/context/SecurityContextHolder.java
  19. 15 0
      tr-framework/src/main/java/cn/tr/core/enums/CreateEnum.java
  20. 15 0
      tr-framework/src/main/java/cn/tr/core/enums/IEnum.java
  21. 7 0
      tr-framework/src/main/java/cn/tr/core/enums/LoginScopeType.java
  22. 17 0
      tr-framework/src/main/java/cn/tr/core/enums/UserTypeEnum.java
  23. 53 0
      tr-framework/src/main/java/cn/tr/core/enums/WebFilterOrderEnum.java
  24. 23 0
      tr-framework/src/main/java/cn/tr/core/exception/BaseCode.java
  25. 61 0
      tr-framework/src/main/java/cn/tr/core/exception/ServiceException.java
  26. 234 0
      tr-framework/src/main/java/cn/tr/core/exception/TRExcCode.java
  27. 42 0
      tr-framework/src/main/java/cn/tr/core/pojo/BaseDTO.java
  28. 55 0
      tr-framework/src/main/java/cn/tr/core/pojo/BasePO.java
  29. 113 0
      tr-framework/src/main/java/cn/tr/core/pojo/CommonResult.java
  30. 29 0
      tr-framework/src/main/java/cn/tr/core/pojo/KeyValue.java
  31. 28 0
      tr-framework/src/main/java/cn/tr/core/pojo/LoginResult.java
  32. 48 0
      tr-framework/src/main/java/cn/tr/core/pojo/PageDomain.java
  33. 48 0
      tr-framework/src/main/java/cn/tr/core/pojo/PageInfo.java
  34. 49 0
      tr-framework/src/main/java/cn/tr/core/pojo/TableDataInfo.java
  35. 21 0
      tr-framework/src/main/java/cn/tr/core/pojo/TenantPO.java
  36. 76 0
      tr-framework/src/main/java/cn/tr/core/strategy/DeptDataPermissionStrategy.java
  37. 37 0
      tr-framework/src/main/java/cn/tr/core/strategy/ExceptionStrategy.java
  38. 43 0
      tr-framework/src/main/java/cn/tr/core/strategy/LoginUserStrategy.java
  39. 72 0
      tr-framework/src/main/java/cn/tr/core/strategy/PageStrategy.java
  40. 16 0
      tr-framework/src/main/java/cn/tr/core/strategy/auth/ILoginUser.java
  41. 189 0
      tr-framework/src/main/java/cn/tr/core/tree/TreeBuilder.java
  42. 71 0
      tr-framework/src/main/java/cn/tr/core/tree/TreeNode.java
  43. 231 0
      tr-framework/src/main/java/cn/tr/core/utils/CommonNetWorkInfoUtil.java
  44. 241 0
      tr-framework/src/main/java/cn/tr/core/utils/IpUtil.java
  45. 287 0
      tr-framework/src/main/java/cn/tr/core/utils/JsonUtils.java
  46. 73 0
      tr-framework/src/main/java/cn/tr/core/utils/PswUtils.java
  47. 227 0
      tr-framework/src/main/java/cn/tr/core/utils/ServletUtils.java
  48. 53 0
      tr-framework/src/main/java/cn/tr/core/utils/UserUtil.java
  49. 145 0
      tr-framework/src/main/java/cn/tr/core/utils/ValidationUtils.java
  50. 19 0
      tr-framework/src/main/java/cn/tr/core/validation/Insert.java
  51. 19 0
      tr-framework/src/main/java/cn/tr/core/validation/Update.java
  52. 2 0
      tr-framework/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  53. BIN
      tr-framework/src/main/resources/ip2region.xdb
  54. 61 0
      tr-framework/src/test/java/cn/tr/core/utils/CommonNetWorkInfoUtilTest.java
  55. 77 0
      tr-framework/src/test/java/cn/tr/core/utils/IpUtilTest.java
  56. 99 0
      tr-framework/src/test/java/cn/tr/core/utils/JsonUtilsTest.java
  57. 149 0
      tr-framework/src/test/java/cn/tr/core/utils/ServletUtilsTest.java
  58. 127 0
      tr-framework/src/test/java/cn/tr/core/utils/TreeBuilderTest.java
  59. 95 0
      tr-framework/src/test/java/cn/tr/core/utils/ValidationUtilsTest.java
  60. 22 0
      tr-modules-api/pom.xml
  61. 41 0
      tr-modules-api/tr-module-export-api/pom.xml
  62. 53 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/api/ExcelApi.java
  63. 26 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/constant/ExcelConstant.java
  64. 26 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/annotation/ExcelCellMerge.java
  65. 33 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/annotation/ExcelPropertySupport.java
  66. 44 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/annotation/ExcelSelect.java
  67. 37 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/bean/ExportResult.java
  68. 27 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/converter/BooleanConverter.java
  69. 54 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/converter/DictCascadeSelectConverter.java
  70. 49 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/converter/SysUserCascadeSelectConverter.java
  71. 19 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/enums/ExportType.java
  72. 182 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/AbstractCascadeSelectConverter.java
  73. 65 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/AbstractDataHandler.java
  74. 20 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/DataHandler.java
  75. 27 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/NoneCascadeSelectConverter.java
  76. 21 0
      tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/register/ExportSampleRegister.java
  77. 14 0
      tr-modules-api/tr-module-quartz-api/pom.xml
  78. 22 0
      tr-modules-api/tr-module-quartz-api/src/main/java/cn/tr/module/api/quartz/JointJobQuartzApi.java
  79. 34 0
      tr-modules-api/tr-module-quartz-api/src/main/java/cn/tr/module/api/quartz/dto/JointJobQuartzDTO.java
  80. 21 0
      tr-modules-api/tr-module-system-api/pom.xml
  81. 41 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/dict/SysDictApi.java
  82. 45 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/log/annotation/OperateLog.java
  83. 64 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/log/bo/OperateLogBO.java
  84. 30 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/log/enums/LoginType.java
  85. 14 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/notice/SysNoticeApi.java
  86. 45 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/notice/SysNoticeBO.java
  87. 41 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/storage/StoragePojo.java
  88. 53 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/storage/SysStorageApi.java
  89. 13 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/tenant/SysTenantApi.java
  90. 36 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/tenant/SysTenantPojo.java
  91. 21 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/user/SysUserApi.java
  92. 81 0
      tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/user/dto/SysQuickMenuDTO.java
  93. 24 0
      tr-modules/pom.xml
  94. 66 0
      tr-modules/tr-module-ai/pom.xml
  95. 11 0
      tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/AiSpringBootApplication.java
  96. 26 0
      tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/controller/ChatController.java
  97. 16 0
      tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/service/Assistant.java
  98. 43 0
      tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/service/BookingTools.java
  99. 22 0
      tr-modules/tr-module-ai/src/main/resources/application.yml
  100. 68 0
      tr-modules/tr-module-export/pom.xml

+ 38 - 0
.dockerignore

@@ -0,0 +1,38 @@
+# IDE相关文件
+*.iml
+*.iws
+*.ipr
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Maven相关
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+
+# Git相关
+.git/
+.gitignore
+
+# 日志文件
+*.log
+
+# 临时文件
+/tmp/
+/temp/
+
+# Docker相关
+Dockerfile*
+docker-compose*.yml
+
+# 其他
+.DS_Store
+/logs/

+ 75 - 0
.gitignore

@@ -0,0 +1,75 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# IDE specific files
+.idea/
+*.iml
+*.iws
+*.ipr
+
+# Eclipse IDE
+.project
+.classpath
+.settings/
+
+# NetBeans IDE
+/nbproject/private/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+# VS Code
+.vscode/
+
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# Gradle
+.gradle/
+build/
+
+# SonarQube
+.sonar/
+
+# JetBrains
+*.orig
+
+# Spring Boot
+.m2/
+.springBeans
+
+# Temporary files
+*.tmp
+*.temp

+ 0 - 0
.mvn


+ 17 - 0
Dockerfile

@@ -0,0 +1,17 @@
+# 使用官方OpenJDK运行时作为基础镜像
+FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openjdk:21
+
+# 设置工作目录
+WORKDIR /app
+
+COPY tr-test.jar /app
+
+# 暴露端口
+EXPOSE 8083
+
+# 设置JVM配置参数的环境变量,默认值为-Xms512m -Xmx2g -XX:+UseG1GC
+ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC"
+
+# 运行应用
+# 通过环境变量配置JVM参数
+ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/tr-test.jar"]

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+|  框架   | 说明  |  版本 | 学习指南|
+|  ----  | ----  | ---- | ---- |
+| SpringBoot  | 基础手脚架 | 2.7.8 | [文档](https://spring.io/projects/spring-boot/) |
+| SaToken  | 权限框架 | 1.34.0 | [文档](https://sa-token.cc/index.html) |
+| MapStruct  | Bean转换器 | 1.5.3.Final | [文档](https://spring.io/projects/spring-boot/) |
+| Lombok  | 代码插件 | 1.18.20 | [文档](https://projectlombok.org/) |
+| Hutool  | 基础工具类 | 5.8.12 | [文档](https://hutool.mydoc.io/) |
+| Log4j2  | 日志框架 | 2.7.8 | [文档](https://logging.apache.org/log4j/2.x/) |
+|Druid| JDBC连接工具| 1.2.8 ||
+|Knife4j|Swagger 增强 UI 实现| 4.0.0|
+|Javassist|字节码增强| 3.25.0-GA|
+|Jackson| Json工具库|2.13.4 |
+| Mybatis-Plus  | MyBatis 增强工具包 | 3.5.3.1 | [文档](https://www.baomidou.com/pages/24112f/) |
+| PageHelper  | 分页插件 | 1.4.5 | [文档](https://pagehelper.github.io/) |
+| Mockito  | mock框架 | 4.11.0 | [文档](https://site.mockito.org/) |
+| JUnit  | 单元测试框架 | 4.11.0 | |
+| Redisson  | redis连接工具 | 3.17.7 | [文档](https://github.com/redisson/redisson) |
+| Tio  | 网络编程框架 | 3.6.0.v20200315-RELEASE | [文档](https://www.tiocloud.com/tio/) |
+|Quartz|任务调度框架| 2.3.2 |
+| SpringCloudStream  | 生产消费模型 | 3.2.6 |  |
+| SpringCloudBus  | 消息总线 | 3.1.2 |  |
+| RabbitMq  | 消息队列 | 3.12.0 | |
+| Mysql  | 基础手脚架 | 8.0.32 |  |
+| Redis  | 基础手脚架 | 7.0.11 | |

+ 10 - 0
docker-compose.yml

@@ -0,0 +1,10 @@
+version: '3.8'
+
+services:
+  jgliu:
+    image: springboot:21
+    container_name: jgliu
+    ports:
+      - 8082:8083
+    volumes:
+      - /home/software/jgliu-install/tr-test.jar:/app/tr-test.jar

BIN
logs/2025-12/hdis-2025-12-09-1.log.gz


+ 118 - 0
pom.xml

@@ -0,0 +1,118 @@
+<?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>
+
+    <groupId>cn.tr</groupId>
+    <artifactId>phototherapy</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+    <description>驼人通用型管理框架</description>
+
+    <modules>
+        <!--依赖包-->
+        <module>tr-dependencies</module>
+        <!--插件-->
+        <module>tr-plugins</module>
+        <!--一些框架公共服务-->
+        <module>tr-framework</module>
+        <!--系统、业务服务模块-->
+        <module>tr-modules</module>
+        <!--系统、业务服务对外提供api模块-->
+        <module>tr-modules-api</module>
+        <module>tr-test</module>
+    </modules>
+
+    <properties>
+        <revision>2.0.0</revision>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <maven.compiler.release>21</maven.compiler.release>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <versions-maven-plugin.version>2.7</versions-maven-plugin.version>
+        <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
+        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+        <skipTests>true</skipTests>
+        <lombok.version>1.18.32</lombok.version>
+        <mapstruct.version>1.5.3.Final</mapstruct.version>
+        <spring-boot.version>3.5.7</spring-boot.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-dependencies</artifactId>
+                <version>${revision}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!-- maven-surefire-plugin 插件,用于运行单元测试。 -->
+                <!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven-surefire-plugin.version}</version>
+                    <configuration>
+                        <argLine>-XX:+EnableDynamicAgentLoading</argLine>
+                    </configuration>
+                </plugin>
+                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>${maven-compiler-plugin.version}</version>
+                    <configuration>
+                        <source>21</source>
+                        <target>21</target>
+                        <release>21</release>
+                        <parameters>true</parameters>
+                        <compilerArgs>
+                            <arg>-parameters</arg>
+                        </compilerArgs>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring-boot.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.mapstruct</groupId>
+                                <artifactId>mapstruct-processor</artifactId>
+                                <version>${mapstruct.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+
+    <!-- 使用 aliyun 的 Maven 源,提升下载速度 -->
+    <repositories>
+        <repository>
+            <id>aliyunmaven</id>
+            <name>aliyun</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+        </repository>
+    </repositories>
+
+</project>

+ 487 - 0
tr-dependencies/pom.xml

@@ -0,0 +1,487 @@
+<?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>
+
+    <groupId>cn.tr</groupId>
+    <artifactId>tr-dependencies</artifactId>
+    <version>${revision}</version>
+    <packaging>pom</packaging>
+
+    <properties>
+        <skip>true</skip>
+        <revision>2.0.0</revision>
+
+        <spring-boot.version>3.5.7</spring-boot.version>
+        <hutool.version>5.8.41</hutool.version>
+        <mapstruct.version>1.6.3</mapstruct.version>
+        <ip2region.version>3.1.1</ip2region.version>
+        <!--数据库-->
+        <mybatis-plus.version>3.5.14</mybatis-plus.version>
+        <mybatis.version>3.5.19</mybatis.version>
+        <mybatis-plus-generator.version>3.5.14</mybatis-plus-generator.version>
+        <mysql-connector.version>8.0.33</mysql-connector.version>
+        <pagehelper.boot.version>2.1.0</pagehelper.boot.version>
+        <springdoc.version>2.8.13</springdoc.version>
+        <!--权限相关-->
+        <satoken.version>1.44.0</satoken.version>
+
+        <caffeine.version>2.6.2</caffeine.version>
+        <!--redission-->
+        <redisson.version>3.30.0</redisson.version>
+
+        <!--文件存储-->
+        <aliyun-oss.version>3.17.0</aliyun-oss.version>
+        <minio.version>8.6.0</minio.version>
+        <qiniu.version>7.9.5</qiniu.version>
+        <!--文件导入导出-->
+        <poi.version>5.2.2</poi.version>
+        <!--在线文档-->
+        <knife4j.verison>4.5.0</knife4j.verison>
+        <!--阿里云短信-->
+        <ali.sms.version>3.0.0</ali.sms.version>
+        <!--渲染模板引擎-->
+        <beetl.version>1.2.40.Beetl.RELEASE</beetl.version>
+        <!--硬件信息-->
+        <oshi.core.version>6.2.2</oshi.core.version>
+        <easy-captcha.version>1.6.2</easy-captcha.version>
+        <easy-excel.version>4.0.3</easy-excel.version>
+        <poi-tl.version>1.12.0</poi-tl.version>
+        <poi-ooxml-schemas.version>4.1.2</poi-ooxml-schemas.version>
+        <commons-cli.version>1.5.0</commons-cli.version>
+
+        <okhttp.version>4.12.0</okhttp.version>
+        <log4j2.version>2.24.3</log4j2.version>
+        <lang3.version>3.18.0</lang3.version>
+        <wx.version>4.7.0</wx.version>
+        <langchain4j.version>1.8.0-beta15</langchain4j.version>
+        <kuaishou.version>1.0.6</kuaishou.version>
+        <httpclient.version>4.5.13</httpclient.version>
+    </properties>
+
+
+    <dependencyManagement>
+        <dependencies>
+            <!--springboot pom文件-->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.apache.commons</groupId>
+                        <artifactId>commons-lang3</artifactId>
+                    </exclusion>
+                </exclusions>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- Hutool - 强大的工具类库 -->
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-bom</artifactId>
+                <version>${hutool.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>dev.langchain4j</groupId>
+                <artifactId>langchain4j-spring-boot-starter</artifactId>
+                <version>${langchain4j.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>dev.langchain4j</groupId>
+                <artifactId>langchain4j-reactor</artifactId>
+                <version>${langchain4j.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>dev.langchain4j</groupId>
+                <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
+                <version>${langchain4j.version}</version>
+            </dependency>
+
+            <!--redis客户端-->
+            <dependency>
+                <groupId>org.redisson</groupId>
+                <artifactId>redisson-spring-boot-starter</artifactId>
+                <version>${redisson.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.springframework.boot</groupId>
+                        <artifactId>spring-boot-starter-logging</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>weixin-java-miniapp</artifactId>
+                <version>${wx.version}</version>
+            </dependency>
+            <!--微信对应工具类-->
+            <dependency>
+                <groupId>com.github.binarywang</groupId>
+                <artifactId>weixin-java-mp</artifactId>
+                <version>${wx.version}</version>
+            </dependency>
+
+            <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-spring-boot3-starter</artifactId>
+                <version>${satoken.version}</version>
+            </dependency>
+
+            <!-- Sa-Token 整合 RedisTemplate -->
+            <dependency>
+                <groupId>cn.dev33</groupId>
+                <artifactId>sa-token-redis-template</artifactId>
+                <version>${satoken.version}</version>
+            </dependency>
+
+            <!--使用mapstruct转换-->
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+
+            <!-- ip地址查询 -->
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
+            <!--mybatis-plus-->
+            <dependency>
+                <groupId>org.mybatis</groupId>
+                <artifactId>mybatis</artifactId>
+                <version>${mybatis.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-jsqlparser</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
+                <version>${mybatis-plus-generator.version}</version>
+            </dependency>
+            <!--mybatis-plus-->
+
+            <!-- pagehelper 分页插件 -->
+            <dependency>
+                <groupId>com.github.pagehelper</groupId>
+                <artifactId>pagehelper-spring-boot-starter</artifactId>
+                <version>${pagehelper.boot.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>log4j-to-slf4j</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!--缓存-->
+            <dependency>
+                <groupId>com.github.ben-manes.caffeine</groupId>
+                <artifactId>caffeine</artifactId>
+                <version>${caffeine.version}</version>
+            </dependency>
+
+            <!--OSS模块-->
+            <!--ali OSS存储-->
+            <dependency>
+                <groupId>com.aliyun.oss</groupId>
+                <artifactId>aliyun-sdk-oss</artifactId>
+                <version>${aliyun-oss.version}</version>
+            </dependency>
+            <!-- minio -->
+            <dependency>
+                <groupId>io.minio</groupId>
+                <artifactId>minio</artifactId>
+                <version>${minio.version}</version>
+            </dependency>
+            <!--OSS模块-->
+
+            <!--系统硬件信息-->
+            <dependency>
+                <groupId>com.github.oshi</groupId>
+                <artifactId>oshi-core</artifactId>
+                <version>${oshi.core.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.deepoove</groupId>
+                <artifactId>poi-tl</artifactId>
+                <version>${poi-tl.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.apache.poi</groupId>
+                        <artifactId>poi-ooxml</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml</artifactId>
+                <version>${poi.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml-schemas</artifactId>
+                <version>${poi-ooxml-schemas.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.whvcse</groupId>
+                <artifactId>easy-captcha</artifactId>
+                <version>${easy-captcha.version}</version>
+            </dependency>
+
+            <!--在线文档-->
+            <dependency>
+                <groupId>org.springdoc</groupId>
+                <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+                <version>${springdoc.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.xiaoymin</groupId>
+                <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+                <version>${knife4j.verison}</version>
+            </dependency>
+
+            <!--业务组件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-framework</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-lock</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>mysql</groupId>
+                <artifactId>mysql-connector-java</artifactId>
+                <version>${mysql-connector.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--缓存插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-cache</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+
+            <!--文件插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-file</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--satoken插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-satoken</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--租户插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-biz-tenant</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--打印banner插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-banner</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-sse</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--敏感词插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-desensitize</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--web插件-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-web</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-quartz</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--api模块-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-system-api</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-system</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-jgliu</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-platform</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-ai</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-export</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--导出api模块-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-module-export-api</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--短信模块-->
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-sms</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!--阿里云短信-->
+            <dependency>
+                <groupId>com.aliyun</groupId>
+                <artifactId>dysmsapi20170525</artifactId>
+                <version>${ali.sms.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.ibeetl</groupId>
+                <artifactId>beetl-framework-starter</artifactId>
+                <version>${beetl.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.alibaba</groupId>
+                <artifactId>easyexcel</artifactId>
+                <version>${easy-excel.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>commons-cli</groupId>
+                <artifactId>commons-cli</artifactId>
+                <version>${commons-cli.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-lang3</artifactId>
+                <version>${lang3.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>${okhttp.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-log4j2</artifactId>
+                <version>${spring-boot.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>log4j-to-slf4j</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+
+            <dependency>
+                <groupId>cn.tr</groupId>
+                <artifactId>tr-spring-boot-starter-plugin-gen</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.httpcomponents</groupId>
+                <artifactId>httpclient</artifactId>
+                <version>${httpclient.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.github.kwaiopen</groupId>
+                <artifactId>kwai-open-sdk</artifactId>
+                <version>${kuaishou.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>

+ 159 - 0
tr-framework/pom.xml

@@ -0,0 +1,159 @@
+<?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">
+    <parent>
+        <artifactId>phototherapy</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-framework</artifactId>
+    <version>${revision}</version>
+
+    <description>系统内置的核心框架功能</description>
+
+    <dependencies>
+        <!-- 分页工具 -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Hutool工具库 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-db</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-crypto</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!-- 参数校验 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+            <exclusions>
+                <!-- 排除默认的日志实现,使用log4j2 -->
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <!-- 测试依赖 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <!-- 排除默认的日志实现 -->
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+                <!-- 排除logback实现 -->
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-classic</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- Log4j2日志实现 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-log4j2</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-to-slf4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- IP地址库 -->
+        <dependency>
+            <groupId>org.lionsoul</groupId>
+            <artifactId>ip2region</artifactId>
+        </dependency>
+
+        <!-- Web支持 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+            <exclusions>
+                <!-- 排除默认的日志实现 -->
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springdoc</groupId>
+                    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-redis</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 18 - 0
tr-framework/src/main/java/cn/tr/core/annotation/TenantIgnore.java

@@ -0,0 +1,18 @@
+package cn.tr.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 忽略租户,标记指定方法不进行租户的自动过滤
+ *
+ * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
+ * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
+ * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TenantIgnore {
+}

+ 56 - 0
tr-framework/src/main/java/cn/tr/core/config/CreateAndUpdateMetaObjectHandler.java

@@ -0,0 +1,56 @@
+package cn.tr.core.config;
+
+import cn.tr.core.strategy.LoginUserStrategy;
+import cn.tr.core.constant.MybatisConstant;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.reflection.MetaObject;
+import java.util.Date;
+
+/**
+ * MP注入处理器
+ *
+ * @author Kevin
+ */
+@Slf4j
+public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
+
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        try {
+            String localUserId = LoginUserStrategy.tr.getCurrentUserId();
+            if (metaObject.hasGetter(MybatisConstant.CREATE_TIME) && metaObject.getValue(MybatisConstant.CREATE_TIME) == null) {
+                this.strictInsertFill(metaObject,MybatisConstant. CREATE_TIME, Date.class, new Date());
+            }
+            if (metaObject.hasGetter(MybatisConstant.CREATE_BY) && metaObject.getValue(MybatisConstant.CREATE_BY) == null) {
+
+                this.strictInsertFill(metaObject, MybatisConstant.CREATE_BY, String.class,localUserId);
+            }
+            if (metaObject.hasGetter(MybatisConstant.DELETED) && metaObject.getValue(MybatisConstant.DELETED) == null) {
+                this.strictInsertFill(metaObject, MybatisConstant.DELETED, Integer.class,0);
+            }
+            updateFill(metaObject);
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.warn("CreateAndUpdateMetaObjectHandler insertFill confront a error,{}",e.getMessage());
+        }
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        try {
+            String localUserId = LoginUserStrategy.tr.getCurrentUserId();
+            if (metaObject.hasGetter(MybatisConstant.UPDATE_TIME) ) {
+                this.strictUpdateFill(metaObject, MybatisConstant.UPDATE_TIME, Date.class, new Date());
+            }
+            if (metaObject.hasGetter(MybatisConstant.UPDATE_BY) ) {
+                this.strictUpdateFill(metaObject,MybatisConstant. UPDATE_BY, String.class,localUserId);
+            }
+        } catch (Exception e) {
+            log.warn("CreateAndUpdateMetaObjectHandler updateFill confront a error,{}",e.getMessage());
+        }
+    }
+
+
+}

+ 26 - 0
tr-framework/src/main/java/cn/tr/core/config/FaviconInterceptor.java

@@ -0,0 +1,26 @@
+package cn.tr.core.config;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * Favicon拦截器
+ * 用于拦截/favicon.ico请求并返回空响应或默认favicon
+ */
+public class FaviconInterceptor implements HandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        String requestURI = request.getRequestURI();
+        // 检查是否是/favicon.ico请求
+        if ("/favicon.ico".equals(requestURI)) {
+            // 设置响应状态码为204 No Content
+            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+            return false; // 不继续执行后续处理
+        }
+        return true; // 继续执行后续处理
+    }
+}

+ 22 - 0
tr-framework/src/main/java/cn/tr/core/config/RedisConfig.java

@@ -0,0 +1,22 @@
+package cn.tr.core.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@ConditionalOnBean(RedisConnectionFactory.class)
+public class RedisConfig {
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
+        return template;
+    }
+}

+ 39 - 0
tr-framework/src/main/java/cn/tr/core/constant/MybatisConstant.java

@@ -0,0 +1,39 @@
+package cn.tr.core.constant;
+
+/**
+ * @ClassName : MybatisConstant
+ * @Description : mybatis常量
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public class MybatisConstant {
+    /**
+     * 创建时间字段名称
+     */
+    public static final String CREATE_TIME = "createTime";
+    /**
+     * 更新时间字段名称
+     */
+    public static final String UPDATE_TIME = "updateTime";
+    /**
+     * 创建人字段名称
+     */
+    public static final String CREATE_BY = "createBy";
+    /**
+     * 更新人字段名称
+     */
+    public static final String UPDATE_BY = "updateBy";
+
+    /**
+     * 租户id字段名称
+     */
+    public static final String TENANT_ID = "tenantId";
+
+    /**
+     * 分页信息缓存
+     */
+    public static final String PAGE_CACHE = "page_cache";
+
+    public static final String DELETED = "deleted";
+}

+ 59 - 0
tr-framework/src/main/java/cn/tr/core/context/BaseController.java

@@ -0,0 +1,59 @@
+package cn.tr.core.context;
+
+import cn.tr.core.exception.TRExcCode;
+import cn.tr.core.pojo.CommonResult;
+import cn.tr.core.pojo.PageInfo;
+import cn.tr.core.pojo.TableDataInfo;
+import cn.tr.core.strategy.PageStrategy;
+import com.github.pagehelper.Page;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Collection;
+
+/**
+ * web层通用数据处理
+ * 
+ * @author ruoyi
+ */
+@Slf4j
+public class BaseController {
+
+    /**
+     * 设置请求分页数据
+     */
+    protected void startPage() {
+        PageStrategy.tr.startPage.accept(PageStrategy.tr.createPage.get());
+    }
+
+    /**
+     * 响应请求分页数据
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    protected <T> TableDataInfo<T> getDataTable(Collection<T> list) {
+        Page<?> localPage = PageStrategy.tr.getPage.get();
+        PageInfo<T> page = PageInfo.of(localPage.getTotal(),localPage.getPageNum(), localPage.getPageSize(), list);
+        TableDataInfo<T> rspData = new TableDataInfo(page,TRExcCode.SUCCESS.getErrCode(),null);
+        return rspData;
+    }
+
+    /**
+     * 响应返回结果
+     * 
+     * @param rows 影响行数
+     * @return 操作结果
+     */
+    protected CommonResult<Integer> toResult(int rows) {
+        return rows > 0 ? CommonResult.success(rows) : CommonResult.success(0);
+    }
+
+    /**
+     * 响应返回结果
+     * 
+     * @param result 结果
+     * @return 操作结果
+     */
+    protected CommonResult<Boolean> toResult(boolean result) {
+        return result ? CommonResult.success(true) :CommonResult.success(false);
+    }
+
+}

+ 49 - 0
tr-framework/src/main/java/cn/tr/core/context/RequestContextHolder.java

@@ -0,0 +1,49 @@
+package cn.tr.core.context;
+
+/**
+ * 请求上下文持有者
+ *
+ * 用于在当前线程中存储和获取请求相关的上下文信息,
+ * 包括请求ID和请求体内容,便于在整个请求处理链路中追踪和使用。
+ *
+ * @author JR
+ * @since 2022年11月14日
+ */
+public class RequestContextHolder {
+
+    /**
+     * 设置当前请求的唯一标识符
+     *
+     * @param id 请求ID,用于链路追踪和日志记录
+     */
+    public static void setRequestId(String id) {
+        SecurityContextHolder.set("REQUEST_ID", id);
+    }
+
+    /**
+     * 获取当前请求的唯一标识符
+     *
+     * @return String 当前请求ID,如果未设置则返回null
+     */
+    public static String getRequestId() {
+        return SecurityContextHolder.getStr("REQUEST_ID");
+    }
+
+    /**
+     * 设置当前请求的请求体内容
+     *
+     * @param body 请求体字符串,用于日志记录或参数校验
+     */
+    public static void setRequestBody(String body) {
+        SecurityContextHolder.set("REQUEST_BODY", body);
+    }
+
+    /**
+     * 获取当前请求的请求体内容
+     *
+     * @return String 当前请求体内容,如果未设置则返回null
+     */
+    public static String getRequestBody() {
+        return SecurityContextHolder.getStr("REQUEST_BODY");
+    }
+}

+ 127 - 0
tr-framework/src/main/java/cn/tr/core/context/SecurityContextHolder.java

@@ -0,0 +1,127 @@
+package cn.tr.core.context;
+
+import cn.hutool.core.util.ObjectUtil;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 安全上下文持有者
+ * <p>
+ * 基于ThreadLocal实现的线程安全上下文容器,
+ * 用于存储和获取当前线程的安全相关信息(如用户ID、用户名、Token等)
+ * </p>
+ * <p>
+ * 注意:必须在网关通过请求头的方式传入安全信息,
+ * 同时在拦截器中设置值,否则无法获取到相关信息
+ * </p>
+ *
+ * @author lifang
+ */
+public class SecurityContextHolder {
+
+    /**
+     * 线程本地变量容器
+     * 使用ConcurrentHashMap确保线程安全和高并发性能
+     */
+    private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = new ThreadLocal<>();
+
+    /**
+     * 设置键值对到当前线程的上下文
+     * <p>
+     * 如果值为null,则不进行存储操作
+     * </p>
+     *
+     * @param key   键名
+     * @param value 键值,如果为null则不存储
+     */
+    public static void set(String key, Object value) {
+        if (ObjectUtil.isNull(value)) {
+            return;
+        }
+        Map<String, Object> map = getLocalMap();
+        map.put(key, value);
+    }
+
+    /**
+     * 从当前线程上下文中获取指定键的字符串值
+     *
+     * @param key 键名
+     * @return 字符串类型的值,如果不存在则返回null
+     */
+    public static String getStr(String key) {
+        return get(key, String.class);
+    }
+
+    /**
+     * 从当前线程上下文中获取指定键的值,并转换为指定类型
+     *
+     * @param key   键名
+     * @param clazz 期望的返回类型Class对象
+     * @param <T>   泛型类型
+     * @return 指定类型的值,如果不存在或类型不匹配则返回null
+     */
+    public static <T> T get(String key, Class<T> clazz) {
+        Map<String, Object> map = getLocalMap();
+        Object result = map.getOrDefault(key, null);
+        if (result == null) {
+            return null;
+        }
+        // 精确类型匹配,避免类型转换异常
+        if (result.getClass().equals(clazz)) {
+            return (T) result;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 从当前线程上下文中获取指定键的列表值
+     *
+     * @param key   键名
+     * @param clazz 列表元素类型Class对象
+     * @param <T>   泛型类型
+     * @return 指定类型的列表,如果不存在则返回null
+     */
+    public static <T> List<T> getList(String key, Class<T> clazz) {
+        return (List<T>) getLocalMap().getOrDefault(key, null);
+    }
+
+    /**
+     * 从当前线程上下文中获取指定键的Map值
+     *
+     * @param key 键名
+     * @return Map类型的值,如果不存在则返回null
+     */
+    public static Map getMap(String key) {
+        return (Map) getLocalMap().getOrDefault(key, null);
+    }
+
+    /**
+     * 获取当前线程的本地Map容器
+     * <p>
+     * 如果当前线程还没有创建Map容器,则创建一个新的ConcurrentHashMap并绑定到ThreadLocal
+     * </p>
+     *
+     * @return 当前线程的Map容器
+     */
+    private static Map<String, Object> getLocalMap() {
+        Map<String, Object> map = THREAD_LOCAL.get();
+        if (map == null) {
+            map = new ConcurrentHashMap<>();
+            THREAD_LOCAL.set(map);
+        }
+        return map;
+    }
+
+    /**
+     * 清除当前线程的上下文信息
+     * <p>
+     * 防止内存泄漏,应在请求处理完成后调用此方法
+     * </p>
+     */
+    public static void remove() {
+        THREAD_LOCAL.remove();
+    }
+}

+ 15 - 0
tr-framework/src/main/java/cn/tr/core/enums/CreateEnum.java

@@ -0,0 +1,15 @@
+package cn.tr.core.enums;
+
+/**
+ * @ClassName : CreateEnum
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年04月21日
+ */
+
+public enum  CreateEnum {
+    //系统类型
+    sys,
+    //自定义类型
+    custom
+}

+ 15 - 0
tr-framework/src/main/java/cn/tr/core/enums/IEnum.java

@@ -0,0 +1,15 @@
+package cn.tr.core.enums;
+
+/**
+ * @ClassName : IEnum
+ * @Description :
+ * @Author : JR
+ * @Date: 2022年11月12日
+ */
+
+public interface IEnum<T> {
+    T getValue();
+
+    String getLabel();
+
+}

+ 7 - 0
tr-framework/src/main/java/cn/tr/core/enums/LoginScopeType.java

@@ -0,0 +1,7 @@
+package cn.tr.core.enums;
+
+public interface LoginScopeType {
+    String LOGIN_SCOPE_TYPE_NORMAL = "login";
+
+    String LOGIN_SCOPE_TYPE_WX = "wxApplet";
+}

+ 17 - 0
tr-framework/src/main/java/cn/tr/core/enums/UserTypeEnum.java

@@ -0,0 +1,17 @@
+package cn.tr.core.enums;
+
+/**
+ * 全局用户类型枚举
+ */
+
+public interface UserTypeEnum {
+    /**
+     * 面向 b 端,管理后台
+     */
+    String ADMIN="admin";
+
+    /**
+     * 面向 c 端,普通用户
+     */
+    String MEMBER="member";
+}

+ 53 - 0
tr-framework/src/main/java/cn/tr/core/enums/WebFilterOrderEnum.java

@@ -0,0 +1,53 @@
+package cn.tr.core.enums;
+
+/**
+ * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
+ *
+ *  考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enum 包下
+ * order值越小越先执行
+ * @author 芋道源码
+ */
+public interface WebFilterOrderEnum {
+
+    int CORS_FILTER = Integer.MIN_VALUE;
+
+    int EXCEPTION_FILTER=CORS_FILTER+1;
+
+    int TRACE_FILTER = EXCEPTION_FILTER + 1;
+
+    int ENV_TAG_FILTER = TRACE_FILTER + 1;
+
+    int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500;
+
+    /**
+     * 执行租户过滤器
+     */
+    int TENANT_CONTEXT_FILTER = - 104;
+
+    /**
+     * 租户接口安全,在租户过滤器之后
+     */
+    int TENANT_SECURITY_FILTER = -99;
+
+    /**
+     * 请求记录过滤器
+     */
+    int API_ACCESS_LOG_FILTER = -103;
+
+    int XSS_FILTER = -102;  // 需要保证在 RequestBodyCacheFilter 后面
+
+    // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
+
+
+
+    int ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
+
+    /**
+     * 填充登录方式,放在最后
+     */
+    int FILL_LOGIN_TYPE=9999;
+    int DEMO_FILTER = Integer.MAX_VALUE;
+
+}

+ 23 - 0
tr-framework/src/main/java/cn/tr/core/exception/BaseCode.java

@@ -0,0 +1,23 @@
+package cn.tr.core.exception;
+
+/**
+ * @Enum : BaseCode
+ * @Description : 基础错误代码
+ * @Author : JR
+ * @Date: 2022年11月18日
+ */
+
+public interface BaseCode {
+
+    /**
+     * 错误码
+     * @return
+     */
+    String getErrCode();
+
+    /**
+     * 错误信息
+     * @return
+     */
+    String getErrMsg();
+}

+ 61 - 0
tr-framework/src/main/java/cn/tr/core/exception/ServiceException.java

@@ -0,0 +1,61 @@
+package cn.tr.core.exception;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 业务逻辑异常 Exception
+ * @author JR
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public final class ServiceException extends RuntimeException {
+
+    private static final long serialVersionUID = 7657422049507503668L;
+    /**
+     * 业务错误码
+     *
+     */
+    private TRExcCode code;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException() {
+    }
+
+    public ServiceException(TRExcCode errorCode) {
+        this.code = errorCode;
+        this.message = errorCode.getErrMsg();
+    }
+
+    public ServiceException(TRExcCode errCode, String message) {
+        this.code = errCode;
+        this.message = message;
+    }
+
+    public TRExcCode getCode() {
+        return code;
+    }
+
+    public ServiceException setCode(TRExcCode code) {
+        this.code = code;
+        return this;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+}

+ 234 - 0
tr-framework/src/main/java/cn/tr/core/exception/TRExcCode.java

@@ -0,0 +1,234 @@
+package cn.tr.core.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @ClassName : TRExcCode
+ * @Description : 根据阿里巴巴编码规约(泰山版)整理的错误码
+ * @Author : JR
+ * @Date: 2022年11月18日
+ */
+@AllArgsConstructor
+@Getter
+public enum TRExcCode implements BaseCode {
+        SUCCESS("00000", "成功"),
+    USER_ERROR_0001("A0001", "用户端错误"),
+    USER_ERROR_A0100("A0100", "用户注册错误"),
+    USER_ERROR_A0101("A0101", "用户未同意隐私协议"),
+    USER_ERROR_A0102("A0102", "注册国家或地区受限"),
+    USER_ERROR_A0110("A0110", "用户名校验失败"),
+    USER_ERROR_A0111("A0111", "用户名已存在"),
+    USER_ERROR_A0112("A0112", "用户名包含敏感词"),
+    USER_ERROR_A0113("A0113", "用户名包含特殊字符"),
+    USER_ERROR_A0120("A0120", "密码校验失败"),
+    USER_ERROR_A0121("A0121", "密码长度不够"),
+    USER_ERROR_A0122("A0122", "密码强度不够"),
+    USER_ERROR_A0130("A0130", "校验码输入错误"),
+    USER_ERROR_A0131("A0131", "短信校验码输入错误"),
+    USER_ERROR_A0132("A0132", "邮件校验码输入错误"),
+    USER_ERROR_A0133("A0133", "语音校验码输入错误"),
+    USER_ERROR_A0140("A0140", "用户证件异常"),
+    USER_ERROR_A0141("A0141", "用户证件类型未选择"),
+    USER_ERROR_A0142("A0142", "大陆身份证编号校验非法"),
+    USER_ERROR_A0143("A0143", "护照编号校验非法"),
+    USER_ERROR_A0144("A0144", "军官证编号校验非法"),
+    USER_ERROR_A0150("A0150", "用户基本信息校验失败"),
+    USER_ERROR_A0151("A0151", "手机格式校验失败"),
+    USER_ERROR_A0152("A0152", "地址格式校验失败"),
+    USER_ERROR_A0153("A0153", "邮箱格式校验失败"),
+
+    /**** 登录 **/
+    USER_ERROR_A0200("A0200", "用户登陆异常"),
+    USER_ERROR_A0201("A0201", "用户账户不存在"),
+    USER_ERROR_A0202("A0202", "用户账户被禁用"),
+    USER_ERROR_A0203("A0203", "用户账户已作废"),
+    USER_ERROR_A0210("A0210", "用户密码错误"),
+    USER_ERROR_A0211("A0211", "用户输入密码次数超限"),
+    USER_ERROR_A0220("A0220", "用户身份校验失败"),
+    USER_ERROR_A0221("A0221", "用户指纹识别失败"),
+    USER_ERROR_A0222("A0222", "用户面容识别失败"),
+    USER_ERROR_A0223("A0223", "用户未获得第三方登陆授权"),
+    USER_ERROR_A0230("A0230", "用户登陆已过期"),
+    USER_ERROR_A0240("A0240", "用户验证码错误"),
+    USER_ERROR_A0241("A0241", "用户验证码尝试次数超限"),
+    USER_ERROR_A0242("A0242", "用户未登录"),
+
+    USER_ERROR_A0243("A0243", "OAuth2 认证失败"),
+    USER_ERROR_A0204("A0244", "租户被禁用"),
+    USER_ERROR_A0205("A0245", "租户已被删除"),
+    /**** 权限 **/
+    USER_ERROR_A0300("A0300", "访问权限异常"),
+    USER_ERROR_A0301("A0301", "访问未授权"),
+    USER_ERROR_A0302("A0302", "正在授权中"),
+    USER_ERROR_A0303("A0303", "用户授权申请被拒绝"),
+    USER_ERROR_A0310("A0310", "因访问对象隐私设置被拦截"),
+    USER_ERROR_A0311("A0311", "授权已过期"),
+    USER_ERROR_A0312("A0312", "无权限使用API"),
+    USER_ERROR_A0320("A0320", "用户访问被拦截"),
+    USER_ERROR_A0321("A0321", "黑名单用户"),
+    USER_ERROR_A0322("A0322", "账号被禁用"),
+    USER_ERROR_A0323("A0323", "非法IP地址"),
+    USER_ERROR_A0324("A0324", "网关访问受限"),
+    USER_ERROR_A0325("A0325", "地域黑名单"),
+    USER_ERROR_A0330("A0330", "服务已欠费"),
+    USER_ERROR_A0340("A0340", "用户签名异常"),
+    USER_ERROR_A0341("A0341", "RSA签名错误"),
+
+
+    /**** 输入错误 **/
+    USER_ERROR_A0400("A0400", "用户请求参数错误"),
+    USER_ERROR_A0401("A0401", "包含非法恶意跳转链接"),
+    USER_ERROR_A0402("A0402", "无效的用户输入"),
+    USER_ERROR_A0403("A0403", "权限不足"),
+    USER_ERROR_A0404("A0404", "用户请求地址不存在"),
+    USER_ERROR_A0410("A0410", "请求必填参数为空"),
+    USER_ERROR_A0411("A0411", "用户订单号为空"),
+    USER_ERROR_A0412("A0412", "订购数量为空"),
+    USER_ERROR_A0413("A0413", "缺少时间戳参数"),
+    USER_ERROR_A0414("A0414", "非法的时间戳参数"),
+    USER_ERROR_A0420("A0420", "请求参数值超出允许的范围"),
+    USER_ERROR_A0421("A0421", "参数格式不匹配"),
+    USER_ERROR_A0422("A0422", "地址不在服务范围"),
+    USER_ERROR_A0423("A0423", "时间不在服务范围"),
+    USER_ERROR_A0424("A0424", "金额超出限制"),
+    USER_ERROR_A0425("A0425", "数量超出限制"),
+    USER_ERROR_A0426("A0426", "请求批量处理总个数超出限制"),
+    USER_ERROR_A0427("A0427", "请求JSON解析失败"),
+    USER_ERROR_A0430("A0430", "用户输入内容非法"),
+    USER_ERROR_A0431("A0431", "包含违禁敏感词"),
+    USER_ERROR_A0432("A0432", "图片包含违禁信息"),
+    USER_ERROR_A0433("A0433", "文件侵犯版权"),
+    USER_ERROR_A0440("A0440", "用户操作异常"),
+    USER_ERROR_A0441("A0441", "用户支付超时"),
+    USER_ERROR_A0442("A0442", "确认订单超时"),
+    USER_ERROR_A0443("A0443", "订单已关闭"),
+    USER_ERROR_A0500("A0500", "用户请求服务异常"),
+    USER_ERROR_A0501("A0501", "请求次数超出限制"),
+    USER_ERROR_A0502("A0502", "请求并发数超出限制"),
+    USER_ERROR_A0503("A0503", "用户操作请等待"),
+    USER_ERROR_A0504("A0504", "WebSocket连接异常"),
+    USER_ERROR_A0505("A0505", "WebSocket连接断开"),
+    USER_ERROR_A0506("A0506", "用户重复请求"),
+    USER_ERROR_A0600("A0600", "用户资源异常"),
+    USER_ERROR_A0601("A0601", "账户余额不足"),
+    USER_ERROR_A0602("A0602", "用户磁盘空间不足"),
+    USER_ERROR_A0603("A0603", "用户内存空间不足"),
+    USER_ERROR_A0604("A0604", "用户OSS容量不足"),
+    USER_ERROR_A0605("A0605", "用户配额已用光"),
+    USER_ERROR_A0700("A0700", "用户上传文件异常"),
+    USER_ERROR_A0701("A0701", "用户上传文件类型不匹配"),
+    USER_ERROR_A0702("A0702", "用户上传文件太大"),
+    USER_ERROR_A0703("A0703", "用户上传图片太大"),
+    USER_ERROR_A0704("A0704", "用户上传视频太大"),
+    USER_ERROR_A0705("A0705", "用户上传压缩文件太大"),
+    USER_ERROR_A0706("A0706", "不支持预览该类型文件预览"),
+    USER_ERROR_A0800("A0800", "用户当前版本异常"),
+    USER_ERROR_A0801("A0801", "用户安装版本与系统不匹配"),
+    USER_ERROR_A0802("A0802", "用户安装版本过低"),
+    USER_ERROR_A0803("A0803", "用户安装版本过高"),
+    USER_ERROR_A0804("A0804", "用户安装版本已过期"),
+    USER_ERROR_A0805("A0805", "用户API请求版本不匹配"),
+    USER_ERROR_A0806("A0806", "用户API请求版本过高"),
+    USER_ERROR_A0807("A0807", "用户API请求版本过低"),
+    USER_ERROR_A0900("A0900", "用户隐私未授权"),
+    USER_ERROR_A0901("A0901", "用户隐私未签署"),
+    USER_ERROR_A0902("A0902", "用户摄像头未授权"),
+    USER_ERROR_A0903("A0903", "用户相机未授权"),
+    USER_ERROR_A0904("A0904", "用户图片库未授权"),
+    USER_ERROR_A0905("A0905", "用户文件未授权"),
+    USER_ERROR_A0906("A0906", "用户位置信息未授权"),
+    USER_ERROR_A0907("A0907", "用户通讯录未授权"),
+    USER_ERROR_A1000("A1000", "用户设备异常"),
+    USER_ERROR_A1001("A1001", "用户相机异常"),
+    USER_ERROR_A1002("A1002", "用户麦克风异常"),
+    USER_ERROR_A1003("A1003", "用户听筒异常"),
+    USER_ERROR_A1004("A1004", "用户扬声器异常"),
+    USER_ERROR_A1005("A1005", "用户GPS定位异常"),
+
+    /**** 服务端错误 **/
+    SYSTEM_ERROR_B0001("B0001", "系统执行出错"),
+    SYSTEM_ERROR_B0100("B0100", "系统执行超时"),
+    SYSTEM_ERROR_B0101("B0101", "系统订单处理超时"),
+    SYSTEM_ERROR_B0200("B0200", "系统容灾功能被触发"),
+    SYSTEM_ERROR_B0210("B0210", "系统限流"),
+    SYSTEM_ERROR_B0220("B0220", "系统功能降级"),
+    SYSTEM_ERROR_B0300("B0300", "系统资源异常"),
+    SYSTEM_ERROR_B0310("B0310", "系统资源耗尽"),
+    SYSTEM_ERROR_B0311("B0311", "系统磁盘空间耗尽"),
+    SYSTEM_ERROR_B0312("B0312", "系统内存耗尽"),
+    SYSTEM_ERROR_B0313("B0313", "文件句柄耗尽"),
+    SYSTEM_ERROR_B0314("B0314", "系统连接池耗尽"),
+    SYSTEM_ERROR_B0315("B0315", "系统线程池耗尽"),
+    SYSTEM_ERROR_B0320("B0320", "系统资源访问异常"),
+    SYSTEM_ERROR_B0321("B0321", "系统读取磁盘文件失败"),
+    SYSTEM_ERROR_B0404("B0404", "资源未找到"),
+    SYSTEM_ERROR_B0401("B0401", "刷卡失败,并未找到今日班次的排班信息"),
+
+    /**** 第三方服务错误 **/
+    SERVICE_ERROR_C0001("C0001", "调用第三方服务出错"),
+    SERVICE_ERROR_C0100("C0100", "中间件服务出错"),
+    SERVICE_ERROR_C0110("C0110", "RPC服务出错"),
+    SERVICE_ERROR_C0111("C0111", "RPC服务未找到"),
+    SERVICE_ERROR_C0112("C0112", "RPC服务未注册"),
+    SERVICE_ERROR_C0113("C0113", "接口不存在"),
+    SERVICE_ERROR_C0120("C0120", "消息服务出错"),
+    SERVICE_ERROR_C0121("C0121", "消息投递出错"),
+    SERVICE_ERROR_C0122("C0122", "消息消费出错"),
+    SERVICE_ERROR_C0123("C0123", "消息订阅出错"),
+    SERVICE_ERROR_C0124("C0124", "消息分组未查到"),
+    SERVICE_ERROR_C0130("C0130", "缓存服务出错"),
+    SERVICE_ERROR_C0131("C0131", "key长度超过限制"),
+    SERVICE_ERROR_C0132("C0132", "value长度超过限制"),
+    SERVICE_ERROR_C0133("C0133", "存储容量已满"),
+    SERVICE_ERROR_C0134("C0134", "不支持的数据格式"),
+    SERVICE_ERROR_C0140("C0140", "配置服务出错"),
+    SERVICE_ERROR_C0150("C0150", "网络资源服务出错"),
+    SERVICE_ERROR_C0151("C0151", "VPN服务出错"),
+    SERVICE_ERROR_C0152("C0152", "CDN服务出错"),
+    SERVICE_ERROR_C0153("C0153", "域名解析服务出错"),
+    SERVICE_ERROR_C0154("C0154", "网关服务出错"),
+    SERVICE_ERROR_C0200("C0200", "第三方系统执行超时"),
+    SERVICE_ERROR_C0210("C0210", "RPC执行超时"),
+    SERVICE_ERROR_C0220("C0220", "消息投递超时"),
+    SERVICE_ERROR_C0230("C0230", "缓存服务超时"),
+    SERVICE_ERROR_C0240("C0240", "配置服务超时"),
+    SERVICE_ERROR_C0250("C0250", "数据库服务超时"),
+    SERVICE_ERROR_C0300("C0300", "数据库服务出错"),
+    SERVICE_ERROR_C0311("C0311", "表不存在"),
+    SERVICE_ERROR_C0312("C0312", "列不存在"),
+    SERVICE_ERROR_C0321("C0321", "多表关联中存在多个相同名称的列"),
+    SERVICE_ERROR_C0331("C0331", "数据库死锁"),
+    SERVICE_ERROR_C0341("C0341", "主键冲突"),
+    SERVICE_ERROR_C0400("C0400", "第三方容灾系统被触发"),
+    SERVICE_ERROR_C0401("C0401", "第三方系统限流"),
+    SERVICE_ERROR_C0402("C0402", "第三方功能降级"),
+    SERVICE_ERROR_C0500("C0500", "通知服务出错"),
+    SERVICE_ERROR_C0501("C0501", "短信提醒服务失败"),
+    SERVICE_ERROR_C0502("C0502", "语音提醒服务失败"),
+    SERVICE_ERROR_C0503("C0503", "邮件提醒服务失败"),
+
+    //DataX
+    CONFIG_ERROR("Common-00", "您提供的配置文件存在错误信息,请检查您的作业配置 ."),
+    CONVERT_NOT_SUPPORT("Common-01", "同步数据出现业务脏数据情况,数据类型转换错误 ."),
+    CONVERT_OVER_FLOW("Common-02", "同步数据出现业务脏数据情况,数据类型转换溢出 ."),
+    RETRY_FAIL("Common-10", "方法调用多次仍旧失败 ."),
+    RUNTIME_ERROR("Common-11", "运行时内部调用错误 ."),
+    HOOK_INTERNAL_ERROR("Common-12", "Hook运行错误 ."),
+    SHUT_DOWN_TASK("Common-20", "Task收到了shutdown指令,为failover做准备"),
+    WAIT_TIME_EXCEED("Common-21", "等待时间超出范围"),
+    TASK_HUNG_EXPIRED("Common-22", "任务hung住,Expired"),
+    
+    // SSE相关错误码
+    SSE_ERROR_D0001("D0001", "SSE服务异常"),
+    SSE_ERROR_D0002("D0002", "SSE客户端连接异常"),
+    SSE_ERROR_D0003("D0003", "SSE事件发布异常"),
+    SSE_ERROR_D0004("D0004", "SSE订阅异常"),
+    SSE_ERROR_D0005("D0005", "SSE客户端未找到"),
+    SSE_ERROR_D0006("D0006", "SSE主题未找到"),
+    ;
+    @Getter
+    private String errCode;
+    @Getter
+    private String errMsg;
+}

+ 42 - 0
tr-framework/src/main/java/cn/tr/core/pojo/BaseDTO.java

@@ -0,0 +1,42 @@
+package cn.tr.core.pojo;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @ClassName : BasePO
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月03日
+ */
+@Data
+public class BaseDTO implements Serializable {
+    private static final long serialVersionUID = -4315985598485514817L;
+
+    /**
+     * 创建时间
+     */
+    @JsonIgnoreProperties(allowGetters = true)
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    private Date createTime;
+
+    /**
+     * 最后更新时间
+     */
+    @JsonIgnoreProperties(allowGetters = true)
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    private Date updateTime;
+
+    @JsonIgnoreProperties
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    private String updateBy;
+
+    @JsonIgnoreProperties
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    private String createBy;
+}

+ 55 - 0
tr-framework/src/main/java/cn/tr/core/pojo/BasePO.java

@@ -0,0 +1,55 @@
+package cn.tr.core.pojo;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import org.apache.ibatis.type.JdbcType;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @ClassName : BasePO
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月03日
+ */
+@Data
+public class BasePO implements Serializable {
+    private static final long serialVersionUID = -4315985598485514817L;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 最后更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+    /**
+     * 创建者,目前使用 SysUser 的 id 编号
+     *
+     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
+     */
+    @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
+    private String createBy;
+    /**
+     * 更新者,目前使用 SysUser 的 id 编号
+     *
+     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
+    private String updateBy;
+
+//    /**
+//     * 是否删除
+//     */
+//    @Comment("删除标记")
+//    @ColumnDefaultValue("0")
+//    @TableLogic
+//    @TableField(jdbcType = JdbcType.TINYINT)
+//    private Boolean deleted;
+}

+ 113 - 0
tr-framework/src/main/java/cn/tr/core/pojo/CommonResult.java

@@ -0,0 +1,113 @@
+package cn.tr.core.pojo;
+
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.exception.BaseCode;
+import cn.tr.core.exception.ServiceException;
+import cn.tr.core.exception.TRExcCode;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.springframework.util.Assert;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * 通用返回
+ *
+ * @param <T> 数据泛型
+ */
+@Data
+public class CommonResult<T> implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -2619760277112039632L;
+
+    private String code;
+    private T data;
+    private String errorMsg;
+
+    // 构造函数
+    public CommonResult() {
+    }
+
+    public CommonResult(String code, T data, String errorMsg) {
+        this.code = code;
+        this.data = data;
+        this.errorMsg = errorMsg;
+    }
+
+    /**
+     * 创建错误结果
+     */
+    public static <T> CommonResult<T> error(BaseCode excCode, String detailErrorMsg) {
+        String errCode = excCode.getErrCode();
+        Assert.isTrue(!TRExcCode.SUCCESS.getErrCode().equals(errCode), "type 必须是错误的!");
+        String errorMsg = StrUtil.isNotBlank(detailErrorMsg) ? detailErrorMsg : excCode.getErrMsg();
+        return new CommonResult<>(errCode, null, errorMsg);
+    }
+
+    /**
+     * 创建错误结果(使用默认错误信息)
+     */
+    public static <T> CommonResult<T> error(BaseCode errorCode) {
+        return error(errorCode, errorCode.getErrMsg());
+    }
+
+    /**
+     * 创建成功结果(无数据)
+     */
+    public static <T> CommonResult<T> success() {
+        return success(null);
+    }
+
+    /**
+     * 创建成功结果(带数据)
+     */
+    public static <T> CommonResult<T> success(T data) {
+        return new CommonResult<>(TRExcCode.SUCCESS.getErrCode(), data, "");
+    }
+
+    /**
+     * 从 ServiceException 创建错误结果
+     */
+    public static <T> CommonResult<T> error(ServiceException serviceException) {
+        return error(TRExcCode.SYSTEM_ERROR_B0001, serviceException.getMessage());
+    }
+
+    /**
+     *  避免 jackson 序列化
+     * @return
+     */
+    @JsonIgnore
+    public boolean isSuccess() {
+        return isSuccess(code);
+    }
+
+    public static boolean isSuccess(String code) {
+        return Objects.equals(code, TRExcCode.SUCCESS.getErrCode());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        CommonResult<?> that = (CommonResult<?>) o;
+        return Objects.equals(code, that.code) &&
+                Objects.equals(data, that.data) &&
+                Objects.equals(errorMsg, that.errorMsg);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(code, data, errorMsg);
+    }
+
+    @Override
+    public String toString() {
+        return "CommonResult{" +
+                "code='" + code + '\'' +
+                ", data=" + data +
+                ", errorMsg='" + errorMsg + '\'' +
+                '}';
+    }
+}

+ 29 - 0
tr-framework/src/main/java/cn/tr/core/pojo/KeyValue.java

@@ -0,0 +1,29 @@
+package cn.tr.core.pojo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * Key Value 的键值对
+ *
+ * @author tr
+ */
+@Data
+@NoArgsConstructor
+public class KeyValue<K,V> implements Serializable {
+
+    private K key;
+    private V value;
+
+    public KeyValue(K key, V value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    public static <K,V> KeyValue<K,V> of(K key, V value) {
+        return new KeyValue<>(key, value);
+    }
+}

+ 28 - 0
tr-framework/src/main/java/cn/tr/core/pojo/LoginResult.java

@@ -0,0 +1,28 @@
+package cn.tr.core.pojo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @ClassName : LoginResult
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年03月02日
+ */
+@Data
+public class LoginResult implements Serializable {
+    private static final long serialVersionUID = 6736183242095354849L;
+
+    /**
+     * 登录token
+     */
+    private String token;
+
+    /**
+     * token过期时间
+     */
+    private long expireTime;
+
+
+}

+ 48 - 0
tr-framework/src/main/java/cn/tr/core/pojo/PageDomain.java

@@ -0,0 +1,48 @@
+package cn.tr.core.pojo;
+
+import cn.hutool.db.sql.Order;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页数据
+ *
+ * @author lifang
+ */
+// 假设你有一个 PageDomain 类,应类似如下结构
+@AllArgsConstructor
+@NoArgsConstructor
+public class PageDomain {
+    private Integer current;
+    private Integer size;
+    private List<Order> orders;
+
+    public Integer getCurrent() {
+        return current;
+    }
+
+    public void setCurrent(Integer current) {
+        this.current = current;
+    }
+
+    public Integer getSize() {
+        return size;
+    }
+
+    public void setSize(Integer size) {
+        this.size = size;
+    }
+
+    public List<Order> getOrders() {
+        return orders;
+    }
+
+    public void setOrders(List<Order> orders) {
+        this.orders = orders;
+    }
+}
+

+ 48 - 0
tr-framework/src/main/java/cn/tr/core/pojo/PageInfo.java

@@ -0,0 +1,48 @@
+package cn.tr.core.pojo;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * 分页信息
+ *
+ * @param <T> 数据泛型
+ */
+public record PageInfo<T>(
+        /**
+         * 总条数
+         */
+        Long total,
+
+        /**
+         * 当前记录起始索引
+         */
+        Integer pageNum,
+
+        /**
+         * 每页显示记录数
+         */
+        Integer pageSize,
+
+        /**
+         * 数据
+         */
+        Collection<T> data
+) implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+    /**
+     * 静态工厂方法创建 PageInfo 实例
+     */
+    public static <T> PageInfo<T> of(Long total, Integer pageNum, Integer pageSize, Collection<T> data) {
+        return new PageInfo<>(total, pageNum, pageSize, data);
+    }
+
+    /**
+     * 无参构造函数
+     */
+    public PageInfo() {
+        this(0L, 0, 0, null);
+    }
+}

+ 49 - 0
tr-framework/src/main/java/cn/tr/core/pojo/TableDataInfo.java

@@ -0,0 +1,49 @@
+package cn.tr.core.pojo;
+
+
+import cn.tr.core.exception.TRExcCode;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 表格分页数据对象
+ *
+ * @param <T> 数据泛型
+ */
+public record TableDataInfo<T>(
+        /** 列表数据 */
+        PageInfo<T> data,
+
+        /** 消息状态码 */
+        String code,
+
+        /** 消息内容 */
+        String errorMsg
+) implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 表格数据对象(无参构造函数)
+     */
+    public TableDataInfo() {
+        this(null, TRExcCode.SUCCESS.getErrCode(), "");
+    }
+
+    /**
+     * 分页
+     *
+     * @param page 分页数据
+     */
+    public TableDataInfo(PageInfo<T> page) {
+        this(page, TRExcCode.SUCCESS.getErrCode(), "");
+    }
+
+    /**
+     * 获取错误消息,如果为null则返回空字符串
+     */
+    public String errorMsg() {
+        return errorMsg == null ? "" : errorMsg;
+    }
+}

+ 21 - 0
tr-framework/src/main/java/cn/tr/core/pojo/TenantPO.java

@@ -0,0 +1,21 @@
+package cn.tr.core.pojo;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.ibatis.type.JdbcType;
+
+/**
+ * @ClassName : TenantPO
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年04月01日
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TenantPO extends BasePO {
+    @TableField(updateStrategy = FieldStrategy.NEVER,fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
+    private String tenantId;
+}

+ 76 - 0
tr-framework/src/main/java/cn/tr/core/strategy/DeptDataPermissionStrategy.java

@@ -0,0 +1,76 @@
+package cn.tr.core.strategy;
+
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * 部门数据权限策略类
+ * <p>
+ * 用于管理用户的数据权限配置,包括可访问的部门、是否可查看所有数据、是否可查看自身数据等策略。
+ * 采用单例模式设计,确保全局唯一实例。
+ * </p>
+ *
+ * @author LF
+ * @since 2023-03-03
+ */
+public class DeptDataPermissionStrategy {
+
+    /**
+     * 私有构造函数,防止外部实例化
+     * 符合单例模式设计原则
+     */
+    private DeptDataPermissionStrategy() {
+        // 私有构造函数,防止外部实例化
+    }
+
+    /**
+     * 部门数据权限策略的单例实例
+     * 使用 public static final 确保线程安全和不可变性
+     */
+    public static final DeptDataPermissionStrategy tr = new DeptDataPermissionStrategy();
+
+    /**
+     * 当前用户的部门ID集合供应器
+     * 通过函数式接口提供动态获取当前用户所属部门ID的能力
+     */
+    public volatile Supplier<Set<String>> currentUserDeptIdsSupplier = () -> null;
+
+    /**
+     * 是否允许查看所有数据的供应器
+     * 通过函数式接口提供动态判断是否可查看所有数据的能力
+     */
+    public volatile Supplier<Boolean> allowAllDataSupplier = () -> false;
+
+    /**
+     * 是否允许查看自己数据的供应器
+     * 通过函数式接口提供动态判断是否可查看自身数据的能力
+     */
+    public volatile Supplier<Boolean> allowSelfDataSupplier = () -> false;
+
+    /**
+     * 获取当前用户所属的部门ID集合
+     *
+     * @return 当前用户所属部门ID集合,可能为null
+     */
+    public Set<String> getCurrentUserDeptIds() {
+        return currentUserDeptIdsSupplier == null ? null : currentUserDeptIdsSupplier.get();
+    }
+
+    /**
+     * 判断是否允许查看所有数据
+     *
+     * @return true-允许查看所有数据,false-不允许,null-未设置
+     */
+    public Boolean getAll() {
+        return allowAllDataSupplier == null ? null : allowAllDataSupplier.get();
+    }
+
+    /**
+     * 判断是否允许查看用户自身的数据
+     *
+     * @return true-允许查看自身数据,false-不允许,null-未设置
+     */
+    public Boolean getSelf() {
+        return allowSelfDataSupplier == null ? null : allowSelfDataSupplier.get();
+    }
+}

+ 37 - 0
tr-framework/src/main/java/cn/tr/core/strategy/ExceptionStrategy.java

@@ -0,0 +1,37 @@
+package cn.tr.core.strategy;
+
+import cn.tr.core.exception.TRExcCode;
+import cn.tr.core.pojo.CommonResult;
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+/**
+ * @ClassName : ExceptionStrategy
+ * @Description : 异常处理策略
+ * @Author : LF
+ * @Date: 2023年02月28日
+ */
+
+public class ExceptionStrategy {
+    private ExceptionStrategy(){
+
+    }
+    private Map<Class<? extends Throwable>,BiFunction<HttpServletRequest, Throwable,CommonResult>> exceptionHandlerMap=new HashMap<>();
+
+    public static ExceptionStrategy tr =new ExceptionStrategy();
+
+
+    public <T extends Throwable> void  registerThrowableHandler(Class<T> aClass,BiFunction<HttpServletRequest,Throwable,CommonResult> exceptionSupplier){
+        exceptionHandlerMap.put(aClass,exceptionSupplier);
+    }
+
+    public CommonResult<?> exceptionHandle(HttpServletRequest request, Throwable ex){
+        //兜底处理
+        return exceptionHandlerMap
+                .getOrDefault(ex.getClass(), ((re, t) -> CommonResult.error(TRExcCode.SYSTEM_ERROR_B0001, t.getLocalizedMessage())))
+                .apply(request,ex);
+    }
+}

+ 43 - 0
tr-framework/src/main/java/cn/tr/core/strategy/LoginUserStrategy.java

@@ -0,0 +1,43 @@
+package cn.tr.core.strategy;
+
+
+import cn.tr.core.strategy.auth.ILoginUser;
+import java.util.function.Supplier;
+
+/**
+ * @ClassName : LoginUserStrategy
+ * @Description : 登录用户策略
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public class LoginUserStrategy {
+    private LoginUserStrategy(){
+
+    }
+
+    public static LoginUserStrategy tr =new LoginUserStrategy();
+
+    public Supplier<? extends ILoginUser> loginUserSupplier;
+
+    /**
+     * 当前地址是否允许匿名登录,默认不允许
+     */
+    public Supplier<Boolean> anonymousLoginSupplier=()->true;
+
+    public String getCurrentUserId(){
+        return loginUserSupplier==null?null:loginUserSupplier.get().getUserId();
+    }
+
+    public String getCurrentUsername(){
+        return loginUserSupplier.get().getUsername();
+    }
+
+    public String getTenantId(){
+        return loginUserSupplier.get().getTenantId();
+    }
+
+    public boolean isAnonymous(){
+        return anonymousLoginSupplier==null||Boolean.TRUE.equals(anonymousLoginSupplier.get());
+    }
+}

+ 72 - 0
tr-framework/src/main/java/cn/tr/core/strategy/PageStrategy.java

@@ -0,0 +1,72 @@
+package cn.tr.core.strategy;
+
+import cn.tr.core.pojo.PageDomain;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.PageHelper;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * 分页策略类
+ * <p>
+ * 提供统一的分页处理策略,支持自定义分页参数创建、分页执行和分页结果获取
+ * 使用策略模式允许外部自定义分页实现逻辑
+ * </p>
+ *
+ * @author LF
+ * @since 2023年02月21日
+ */
+public class PageStrategy {
+
+    /**
+     * 私有构造函数,防止外部实例化
+     * 使用单例模式确保全局只有一个分页策略实例
+     */
+    private PageStrategy() {
+    }
+
+    /**
+     * 分页策略单例实例
+     * 通过 PageStrategy.tr 访问分页策略功能
+     */
+    public static final PageStrategy tr = new PageStrategy();
+
+    /**
+     * 分页参数创建策略
+     * <p>
+     * 用于从请求中提取或创建分页参数,默认创建第1页,每页10条记录的分页参数
+     * 可通过外部设置自定义实现来替换默认行为
+     * </p>
+     */
+    public Supplier<PageDomain> createPage = () -> new PageDomain(1, 10, null);
+
+    /**
+     * 分页执行策略
+     * <p>
+     * 用于执行实际的分页操作,接收分页参数并执行分页逻辑
+     * 可通过外部设置自定义实现来替换默认行为
+     * </p>
+     */
+    public Consumer<PageDomain> startPage = page -> {};
+
+    /**
+     * 分页结果获取策略
+     * <p>
+     * 用于获取当前分页执行后的结果信息
+     * 可通过外部设置自定义实现来替换默认行为
+     * </p>
+     */
+    public Supplier<Page<?>> getPage = () -> new Page<>();
+
+    /**
+     * 清除分页上下文
+     * <p>
+     * 调用 PageHelper.clearPage() 清除当前线程的分页参数,
+     * 防止分页参数污染后续查询
+     * </p>
+     */
+    public void clear() {
+        PageHelper.clearPage();
+    }
+}

+ 16 - 0
tr-framework/src/main/java/cn/tr/core/strategy/auth/ILoginUser.java

@@ -0,0 +1,16 @@
+package cn.tr.core.strategy.auth;
+
+/**
+ * @ClassName : ILoginUser
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年02月21日
+ */
+
+public interface ILoginUser {
+    String getUserId();
+
+    String getUsername();
+
+    String getTenantId();
+}

+ 189 - 0
tr-framework/src/main/java/cn/tr/core/tree/TreeBuilder.java

@@ -0,0 +1,189 @@
+package cn.tr.core.tree;
+
+import cn.hutool.core.util.ObjectUtil;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 树结构构造工具类
+ *
+ * @author tuoren
+ */
+public class TreeBuilder {
+
+    /**
+     * 构建树结构
+     *
+     * @param nodes 所有节点列表
+     * @param <T> 节点类型
+     * @param <ID> 节点ID类型
+     * @return 树结构根节点列表
+     */
+    public static <T extends TreeNode<T, ID>, ID> List<T> buildTree(Collection<T> nodes) {
+        if (nodes == null || nodes.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 按父节点ID分组,处理null键的情况
+        Map<ID, List<T>> nodeMap = nodes.stream()
+                .collect(Collectors.groupingBy(
+                        TreeNode::getParentId,
+                        Collectors.toList()
+                ));
+
+        // 获取根节点(父ID为null的节点)
+        List<T> rootNodes = nodeMap.getOrDefault("0", new ArrayList<>());
+
+        // 递归构建子树
+        buildChildren(rootNodes, nodeMap);
+
+        return rootNodes;
+    }
+
+
+    /**
+     * 递归构建子节点
+     *
+     * @param nodes 当前层级节点
+     * @param nodeMap 节点映射表
+     * @param <T> 节点类型
+     * @param <ID> 节点ID类型
+     */
+    private static <T extends TreeNode<T, ID>, ID> void buildChildren(Collection<T> nodes, Map<ID, List<T>> nodeMap) {
+        for (T node : nodes) {
+            ID nodeId = node.getId();
+            List<T> children = nodeMap.getOrDefault(nodeId, new ArrayList<>());
+            node.setChildren(children);
+
+            // 递归构建子节点的子节点
+            if (!children.isEmpty()) {
+                buildChildren(children, nodeMap);
+            }
+        }
+    }
+
+    /**
+     * 根据指定根节点ID构建树结构
+     *
+     * @param nodes 所有节点列表
+     * @param rootParentId 根节点的父ID
+     * @param <T> 节点类型
+     * @param <ID> 节点ID类型
+     * @return 树结构根节点列表
+     */
+    public static <T extends TreeNode<T, ID>, ID> List<T> buildTreeWithRootParentId(List<T> nodes, ID rootParentId) {
+        if (nodes == null || nodes.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 按父节点ID分组
+        Map<ID, List<T>> nodeMap = nodes.stream()
+                .collect(Collectors.groupingBy(TreeNode::getParentId));
+
+        // 获取指定根节点
+        List<T> rootNodes = nodeMap.getOrDefault(rootParentId, new ArrayList<>());
+
+        // 递归构建子树
+        buildChildren(rootNodes, nodeMap);
+
+        return rootNodes;
+    }
+
+    /**
+     * 扁平化树结构为列表
+     *
+     * @param treeNodes 树节点列表
+     * @param <T> 节点类型
+     * @param <ID> 节点ID类型
+     * @return 扁平化的节点列表
+     */
+    public static <T extends TreeNode<T, ID>, ID> List<T> flattenTree(Collection<T> treeNodes) {
+        List<T> result = new ArrayList<>();
+        for (T node : treeNodes) {
+            result.add(node);
+            if (node.getChildren() != null && !node.getChildren().isEmpty()) {
+                result.addAll(flattenTree(node.getChildren()));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 验证节点的父子关系是否合法
+     *
+     * @param node 待验证的节点
+     * @param tree 完整的树节点集合
+     * @param <T> 节点类型
+     * @param <ID> 节点ID类型
+     * @throws UnsupportedOperationException 当父子关系不合法时抛出异常
+     */
+    public static <T extends TreeNode<T, ID>, ID> void validateNode(T node, Collection<T> tree) {
+        if (node == null || tree == null || tree.isEmpty()) {
+            return;
+        }
+
+        ID nodeId = node.getId();
+        ID parentId = node.getParentId();
+
+        // 检查节点ID和父ID是否相同(不能将自身作为上级节点)
+        if (nodeId != null && parentId != null && nodeId.equals(parentId)) {
+            throw new UnsupportedOperationException("不能将自身作为上级节点");
+        }
+
+        // 如果没有父节点或父节点为空,则验证通过
+        if (parentId == null) {
+            return;
+        }
+
+        // 构建节点ID到节点的映射,避免重复查找
+        Map<ID, T> nodeMap = tree.stream()
+                .collect(Collectors.toMap(TreeNode::getId, Function.identity(), (existing, replacement) -> existing));
+
+        // 检查父节点是否存在
+        if (!nodeMap.containsKey(parentId)&&!"0".equals(parentId)) {
+            throw new UnsupportedOperationException("父节点不存在");
+        }
+
+        // 检查是否将子节点作为上级节点(循环引用检查)
+        if (isChildNode(nodeId, parentId, nodeMap)) {
+            throw new UnsupportedOperationException("不能将子节点作为上级节点");
+        }
+    }
+
+    /**
+     * 递归检查目标节点是否是当前节点的子节点
+     *
+     * @param currentId 当前节点ID
+     * @param targetId 目标节点ID
+     * @param nodeMap 节点映射表
+     * @param <T> 节点类型
+     * @param <ID> 节点ID类型
+     * @return 如果目标节点是当前节点的子节点则返回true,否则返回false
+     */
+    private static <T extends TreeNode<T, ID>, ID> boolean isChildNode(ID currentId, ID targetId, Map<ID, T> nodeMap) {
+        if (currentId == null || targetId == null) {
+            return false;
+        }
+
+        T targetNode = nodeMap.get(targetId);
+        if (targetNode == null) {
+            return false;
+        }
+
+        ID targetParentId = targetNode.getParentId();
+        if (targetParentId == null) {
+            return false;
+        }
+
+        // 如果目标节点的父节点就是当前节点,则存在循环引用
+        if (currentId.equals(targetParentId)) {
+            return true;
+        }
+
+        // 递归向上查找
+        return isChildNode(currentId, targetParentId, nodeMap);
+    }
+
+}

+ 71 - 0
tr-framework/src/main/java/cn/tr/core/tree/TreeNode.java

@@ -0,0 +1,71 @@
+package cn.tr.core.tree;
+
+
+import lombok.Data;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 树节点基类
+ *
+ * @param <T> 节点类型
+ * @param <ID> 节点ID类型
+ */
+@Data
+public class TreeNode<T extends TreeNode<T, ID>, ID> implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -6426157171235936799L;
+    /**
+     * 节点ID
+     */
+    private ID id;
+
+    /**
+     * 父节点ID
+     */
+    private ID parentId;
+
+    /**
+     * 节点名称
+     */
+    private String name;
+
+    private Integer sort;
+    /**
+     * 子节点列表
+     */
+    private List<T> children = new ArrayList<>();
+
+    /**
+     * 添加子节点
+     *
+     * @param child 子节点
+     */
+    public void addChild(T child) {
+        if (this.children == null) {
+            this.children = new ArrayList<>();
+        }
+        this.children.add(child);
+    }
+
+    /**
+     * 判断是否为叶子节点
+     *
+     * @return true表示是叶子节点,false表示不是
+     */
+    public Boolean isLeaf() {
+        return this.children == null || this.children.isEmpty();
+    }
+
+    /**
+     * 判断是否为根节点
+     *
+     * @return true表示是根节点,false表示不是
+     */
+    public Boolean isRoot() {
+        return this.parentId == null;
+    }
+}

+ 231 - 0
tr-framework/src/main/java/cn/tr/core/utils/CommonNetWorkInfoUtil.java

@@ -0,0 +1,231 @@
+/*
+ * Copyright [2022] [https://www.xiaonuo.vip]
+ *
+ * Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
+ *
+ * 1.请不要删除和修改根目录下的LICENSE文件。
+ * 2.请不要删除和修改Snowy源码头部的版权声明。
+ * 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
+ * 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
+ * 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
+ * 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
+ */
+package cn.tr.core.utils;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.NumberUtil;
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.*;
+/**
+ * 通用获取当前网速工具类
+ *
+ * @author xuyuxiang
+ * @date 2022/9/1 23:45
+ */
+@Slf4j
+@UtilityClass
+public class CommonNetWorkInfoUtil {
+
+    /**
+     * 网速测速时间2s
+     */
+    private static final int SLEEP_SECONDS = 2;
+
+    /**
+     * 获取网络上下行速率,格式{"UP": "123KB/S, "DOWN": "345KB/S"}
+     *
+     * @author xuyuxiang
+     * @date 2022/9/1 23:51
+     */
+    public static Map<String, String> getNetworkUpRate() {
+        Map<String, String> result = new HashMap<>();
+        try {
+            boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
+            
+            if (isWindows) {
+                // Windows系统使用netstat命令
+                getWindowsNetworkRate(result);
+            } else {
+                // Linux系统读取/proc/net/dev文件
+                getLinuxNetworkRate(result);
+            }
+        } catch (Exception e) {
+            log.info(">>> 网络测速失败,原因:{}", e.getMessage());
+            // 提供默认值,避免上层调用出现空指针异常
+            result.put("UP", "0 B/S");
+            result.put("DOWN", "0 B/S");
+        }
+        return result;
+    }
+
+    /**
+     * 获取Windows系统网络速率
+     */
+    private static void getWindowsNetworkRate(Map<String, String> result) throws Exception {
+        Process pro1 = null;
+        Process pro2 = null;
+        Runtime r = Runtime.getRuntime();
+        BufferedReader input1 = null;
+        BufferedReader input2 = null;
+        try {
+            // 第一次读取
+            pro1 = r.exec("netstat -e");
+            input1 = new BufferedReader(new InputStreamReader(pro1.getInputStream()));
+            long[] result1 = readWindowsLine(input1);
+            Thread.sleep(SLEEP_SECONDS * 1000);
+            
+            // 第二次读取
+            pro2 = r.exec("netstat -e");
+            input2 = new BufferedReader(new InputStreamReader(pro2.getInputStream()));
+            long[] result2 = readWindowsLine(input2);
+            
+            // 计算速率
+            String upSpeed = FileUtil.readableFileSize(Convert.toLong(NumberUtil
+                    .div(NumberUtil.sub(result2[0], result1[0]), SLEEP_SECONDS)));
+            String downSpeed = FileUtil.readableFileSize(Convert.toLong(NumberUtil
+                    .div(NumberUtil.sub(result2[1], result1[1]), SLEEP_SECONDS)));
+            result.put("UP", upSpeed + (upSpeed.endsWith("B")?"/S":"B/S"));
+            result.put("DOWN", downSpeed + (downSpeed.endsWith("B")?"/S":"B/S"));
+        } finally {
+            // 关闭资源
+            if (input1 != null) {
+                try {
+                    input1.close();
+                } catch (IOException e) {
+                    log.warn("关闭输入流1失败: {}", e.getMessage());
+                }
+            }
+            if (input2 != null) {
+                try {
+                    input2.close();
+                } catch (IOException e) {
+                    log.warn("关闭输入流2失败: {}", e.getMessage());
+                }
+            }
+            Optional.ofNullable(pro1).ifPresent(Process::destroy);
+            Optional.ofNullable(pro2).ifPresent(Process::destroy);
+        }
+    }
+
+    /**
+     * 获取Linux系统网络速率
+     */
+    private static void getLinuxNetworkRate(Map<String, String> result) {
+        try {
+            // 第一次读取
+            Map<String, long[]> networkData1 = readLinuxNetworkData();
+            Thread.sleep(SLEEP_SECONDS * 1000);
+            
+            // 第二次读取
+            Map<String, long[]> networkData2 = readLinuxNetworkData();
+            
+            // 计算所有网络接口的总和
+            long rx1 = 0, tx1 = 0, rx2 = 0, tx2 = 0;
+            for (Map.Entry<String, long[]> entry : networkData1.entrySet()) {
+                // 跳过本地回环接口
+                if (!entry.getKey().equals("lo")) {
+                    rx1 += entry.getValue()[0];
+                    tx1 += entry.getValue()[1];
+                }
+            }
+            
+            for (Map.Entry<String, long[]> entry : networkData2.entrySet()) {
+                // 跳过本地回环接口
+                if (!entry.getKey().equals("lo")) {
+                    rx2 += entry.getValue()[0];
+                    tx2 += entry.getValue()[1];
+                }
+            }
+            
+            // 计算速率
+            String upSpeed = FileUtil.readableFileSize(Convert.toLong(NumberUtil
+                    .div(NumberUtil.sub(tx2, tx1), SLEEP_SECONDS)));
+            String downSpeed = FileUtil.readableFileSize(Convert.toLong(NumberUtil
+                    .div(NumberUtil.sub(rx2, rx1), SLEEP_SECONDS)));
+            result.put("UP", upSpeed + (upSpeed.endsWith("B")?"/S":"B/S"));
+            result.put("DOWN", downSpeed + (downSpeed.endsWith("B")?"/S":"B/S"));
+        } catch (Exception e) {
+            log.info(">>> Linux网络测速失败,原因:{}", e.getMessage());
+            // 出现异常时提供默认值
+            result.put("UP", "0 B/S");
+            result.put("DOWN", "0 B/S");
+        }
+    }
+
+    /**
+     * 读取Linux系统网络数据
+     */
+    private static Map<String, long[]> readLinuxNetworkData() throws Exception {
+        Map<String, long[]> networkData = new HashMap<>();
+        File netDevFile = new File("/proc/net/dev");
+        
+        if (!netDevFile.exists() || !netDevFile.canRead()) {
+            log.warn("无法访问/proc/net/dev文件");
+            return networkData;
+        }
+        
+        try (BufferedReader reader = new BufferedReader(new FileReader(netDevFile))) {
+            String line;
+            int lineNum = 0;
+            
+            // 跳过前两行标题
+            while ((line = reader.readLine()) != null && lineNum < 2) {
+                lineNum++;
+            }
+            
+            // 读取网络接口数据
+            while ((line = reader.readLine()) != null) {
+                line = line.trim();
+                if (line.contains(":")) {
+                    // 格式: eth0: 12345 67 89 ... 54321 12 34 ...
+                    String[] parts = line.split(":");
+                    String interfaceName = parts[0].trim();
+                    String[] values = parts[1].trim().split("\\s+");
+                    if (values.length >= 9) {
+                        try {
+                            long rx = Long.parseLong(values[0]);  // 接收字节
+                            long tx = Long.parseLong(values[8]);  // 发送字节
+                            networkData.put(interfaceName, new long[]{rx, tx});
+                        } catch (NumberFormatException e) {
+                            log.warn("解析网络数据失败,interface: {}, data: {}", interfaceName, line);
+                        }
+                    }
+                }
+            }
+        }
+        return networkData;
+    }
+
+    /**
+     * 读取Windows系统网络数据
+     */
+    private static long[] readWindowsLine(BufferedReader input) {
+        long[] arr = new long[2];
+        try {
+            // 获取windows环境下的网口上下行速率
+            input.readLine();
+            input.readLine();
+            input.readLine();
+            input.readLine();
+            StringTokenizer tokenStat = new StringTokenizer(input.readLine());
+            tokenStat.nextToken();
+            arr[0] = Long.parseLong(tokenStat.nextToken());
+            arr[1] = Long.parseLong(tokenStat.nextToken());
+        } catch (Exception e) {
+            log.warn("读取Windows网络信息失败: {}", e.getMessage());
+        }
+        return arr;
+    }
+
+    private static String formatNumber(double f) {
+        return new Formatter().format("%.2f", f).toString();
+    }
+}

+ 241 - 0
tr-framework/src/main/java/cn/tr/core/utils/IpUtil.java

@@ -0,0 +1,241 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.io.FileUtil;
+import lombok.experimental.UtilityClass;
+import org.lionsoul.ip2region.xdb.LongByteArray;
+import org.lionsoul.ip2region.xdb.Searcher;
+import org.lionsoul.ip2region.xdb.Version;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * IP地址查询工具类
+ * <p>
+ * 提供基于ip2region数据库的离线IP地址定位功能,
+ * 支持IPv4和IPv6地址查询
+ * </p>
+ *
+ * @author xuyuxiang
+ * @since 2022/4/27
+ */
+@UtilityClass
+public class IpUtil {
+
+    /** IPv4地址查询器 */
+    private static Searcher ipV4searcher;
+
+    /** IPv6地址查询器 */
+    private static Searcher ipV6searcher;
+
+    /** 初始化完成标志 */
+    private static final AtomicBoolean initialized = new AtomicBoolean(false);
+
+    /** IP数据库文件名 */
+    private static final String DB_FILE_NAME = "/ip2region.xdb";
+
+    /** 最大初始化等待时间(毫秒) */
+    private static final int MAX_WAIT_TIME = 20000;
+
+    /** 每次等待间隔(毫秒) */
+    private static final int WAIT_INTERVAL = 100;
+
+    /**
+     * 静态初始化块
+     * <p>
+     * 使用虚拟线程异步初始化IP地址查询器
+     * </p>
+     */
+    static {
+        // 使用虚拟线程进行异步初始化
+        Thread.ofVirtual().start(() -> {
+            try {
+                initializeSearcher();
+                initialized.set(true);
+            } catch (Exception e) {
+                System.err.println("IP查询器初始化失败: " + e.getMessage());
+                e.printStackTrace();
+            }
+        });
+    }
+
+    /**
+     * 初始化IP地址查询器
+     * <p>
+     * 从资源目录加载ip2region.xdb数据库文件到临时目录,
+     * 并创建IPv4和IPv6查询器实例
+     * </p>
+     */
+    private static void initializeSearcher() {
+        try {
+            File existFile = getDatabaseFile();
+
+            // 1、从 dbPath 加载整个 xdb 到内存
+            LongByteArray longByteArray = Searcher.loadContentFromFile(existFile.getPath());
+
+            // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象
+            ipV4searcher = Searcher.newWithBuffer(Version.IPv4, longByteArray);
+            // 注意:如果数据库支持IPv6,则初始化ipV6searcher
+            // ipV6searcher = Searcher.newWithBuffer(Version.IPv6, longByteArray);
+        } catch (Exception e) {
+            throw new RuntimeException(String.format("IPUtil初始化失败,原因:%s", e.getMessage()), e);
+        }
+    }
+
+    /**
+     * 获取数据库文件
+     * <p>
+     * 从资源目录加载ip2region.xdb数据库文件到临时目录
+     * </p>
+     *
+     * @return 数据库文件
+     */
+    private static File getDatabaseFile() {
+        File existFile = FileUtil.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + DB_FILE_NAME);
+
+        // 如果临时目录中不存在xdb文件,则从资源目录复制
+        if (!FileUtil.exist(existFile)) {
+            try (InputStream resourceAsStream = IpUtil.class.getResourceAsStream(DB_FILE_NAME)) {
+                if (resourceAsStream == null) {
+                    throw new RuntimeException("无法在资源目录中找到IP数据库文件: " + DB_FILE_NAME);
+                }
+                FileUtil.writeFromStream(resourceAsStream, existFile);
+            } catch (Exception e) {
+                throw new RuntimeException("复制IP数据库文件失败: " + e.getMessage(), e);
+            }
+        }
+        return existFile;
+    }
+
+    /**
+     * 根据IPv6地址离线获取城市信息
+     *
+     * @param ip IPv6地址
+     * @return 城市信息,格式为"国家|区域|省份|城市|ISP",查询失败返回"未知"
+     */
+    public static String getCityInfoIpV6(String ip) {
+        try {
+            // 等待初始化完成
+            waitForInitialization();
+
+            if (ip == null || ip.trim().isEmpty()) {
+                return "未知";
+            }
+
+            ip = ip.trim();
+
+            // 执行查询
+            if (ipV6searcher != null) {
+                String region = ipV6searcher.search(ip);
+                return formatRegionInfo(region);
+            } else {
+                // 如果没有IPv6查询器,降级使用IPv4查询器或返回未知
+                return "未知";
+            }
+        } catch (Exception e) {
+            return "未知";
+        }
+    }
+
+    /**
+     * 根据IPv4地址离线获取城市信息
+     *
+     * @param ip IPv4地址
+     * @return 城市信息,格式为"国家|区域|省份|城市|ISP",查询失败返回"未知"
+     */
+    public static String getCityInfoIpV4(String ip) {
+        try {
+            // 等待初始化完成
+            waitForInitialization();
+
+            if (ip == null || ip.trim().isEmpty()) {
+                return "未知";
+            }
+
+            ip = ip.trim();
+
+            // 执行查询
+            String region = ipV4searcher.search(ip);
+            return formatRegionInfo(region);
+        } catch (Exception e) {
+            return "未知";
+        }
+    }
+
+    /**
+     * 根据IP地址自动判断版本并获取城市信息
+     * <p>
+     * 自动识别输入的IP地址是IPv4还是IPv6格式,并调用相应的查询方法
+     * </p>
+     *
+     * @param ip IP地址,支持IPv4和IPv6格式
+     * @return 城市信息,格式为"国家|区域|省份|城市|ISP",查询失败返回"未知"
+     */
+    public static String getCityInfo(String ip) {
+        // 处理空值情况
+        if (ip == null || ip.isEmpty()) {
+            return "未知";
+        }
+
+        // 判断IP地址类型并调用相应方法
+        if (isIPv6Address(ip)) {
+            return getCityInfoIpV6(ip);
+        } else {
+            return getCityInfoIpV4(ip);
+        }
+    }
+
+    /**
+     * 判断是否为IPv6地址格式
+     * <p>
+     * 通过检测IP地址中是否包含冒号(:)来判断是否为IPv6地址
+     * </p>
+     *
+     * @param ip IP地址
+     * @return true表示是IPv6地址,false表示不是
+     */
+    private static boolean isIPv6Address(String ip) {
+        return ip != null && ip.contains(":");
+    }
+
+    /**
+     * 等待初始化完成
+     * <p>
+     * 在查询器初始化完成前阻塞当前线程
+     * </p>
+     */
+    private static void waitForInitialization() {
+        // 简单的轮询等待,增加等待时间
+        int attempts = 0;
+        int maxAttempts = MAX_WAIT_TIME / WAIT_INTERVAL;
+
+        while (!initialized.get()) {
+            if (attempts++ > maxAttempts) { // 最多等待指定时间
+                throw new RuntimeException("IP查询器初始化超时");
+            }
+            try {
+                Thread.sleep(WAIT_INTERVAL);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new RuntimeException("等待初始化被中断");
+            }
+        }
+    }
+
+    /**
+     * 格式化区域信息
+     * <p>
+     * 移除区域信息中的默认值"0"
+     * </p>
+     *
+     * @param region 原始区域信息
+     * @return 格式化后的区域信息
+     */
+    private static String formatRegionInfo(String region) {
+        if (region == null || region.isEmpty()) {
+            return "未知";
+        }
+        return region.replace("0|", "").replace("|0", "");
+    }
+}

+ 287 - 0
tr-framework/src/main/java/cn/tr/core/utils/JsonUtils.java

@@ -0,0 +1,287 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.experimental.UtilityClass;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * JSON 工具类
+ * <p>
+ * 提供JSON序列化和反序列化的便捷方法,基于Jackson实现
+ * 支持对象与JSON字符串/字节数组之间的相互转换
+ * </p>
+ *
+ * @author 芋道源码
+ */
+@UtilityClass
+public class JsonUtils {
+
+    /**
+     * JSON对象映射器实例
+     * <p>
+     * 用于执行JSON序列化和反序列化操作
+     * 可通过 {@link #init(ObjectMapper)} 方法进行自定义配置
+     * </p>
+     */
+    private static ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 初始化 objectMapper 属性
+     * <p>
+     * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
+     * 允许使用Spring配置的ObjectMapper替代默认实例
+     * </p>
+     *
+     * @param objectMapper ObjectMapper 对象
+     */
+    public static void init(ObjectMapper objectMapper) {
+        JsonUtils.objectMapper = objectMapper;
+    }
+
+    /**
+     * 将对象序列化为JSON字符串
+     * <p>
+     * 使用Jackson的writeValueAsString方法将Java对象转换为JSON格式字符串
+     * </p>
+     *
+     * @param object 待序列化的对象,如果为null则返回null
+     * @return JSON字符串表示形式
+     * @throws RuntimeException 当序列化过程中发生错误时抛出
+     */
+    public static String toJsonString(Object object) {
+        if (Objects.isNull(object)) {
+            return null;
+        }
+        try {
+            return objectMapper.writeValueAsString(object);
+        } catch (Exception e) {
+            throw new RuntimeException("序列化对象为JSON字符串失败", e);
+        }
+    }
+
+    /**
+     * 将对象序列化为JSON字节数组
+     * <p>
+     * 使用Jackson的writeValueAsBytes方法将Java对象转换为JSON格式字节数组
+     * </p>
+     *
+     * @param object 待序列化的对象
+     * @return JSON字节数组表示形式
+     * @throws JsonProcessingException JSON处理异常
+     */
+    public static byte[] toJsonByte(Object object) throws JsonProcessingException {
+        return objectMapper.writeValueAsBytes(object);
+    }
+
+    /**
+     * 将对象序列化为格式化的JSON字符串
+     * <p>
+     * 使用默认的美化打印器将Java对象转换为格式化的JSON字符串,便于阅读
+     * </p>
+     *
+     * @param object 待序列化的对象
+     * @return 格式化的JSON字符串
+     * @throws JsonProcessingException JSON处理异常
+     */
+    public static String toJsonPrettyString(Object object) throws JsonProcessingException {
+        return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
+    }
+
+    /**
+     * 将JSON字符串反序列化为指定类型的对象
+     * <p>
+     * 使用Jackson的readValue方法将JSON字符串转换为指定类型的Java对象
+     * </p>
+     *
+     * @param text  JSON字符串,如果为空则返回null
+     * @param clazz 目标类型Class
+     * @param <T>   泛型类型
+     * @return 反序列化后的对象
+     * @throws RuntimeException 当反序列化过程中发生错误时抛出
+     */
+    public static <T> T parseObject(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, clazz);
+        } catch (IOException e) {
+            throw new RuntimeException("反序列化JSON字符串为对象失败: " + text, e);
+        }
+    }
+
+    /**
+     * 将JSON字节数组反序列化为指定类型的对象
+     * <p>
+     * 使用Jackson的readValue方法将JSON字节数组转换为指定类型的Java对象
+     * </p>
+     *
+     * @param bytes JSON字节数组,如果为空则返回null
+     * @param clazz 目标类型Class
+     * @param <T>   泛型类型
+     * @return 反序列化后的对象
+     * @throws RuntimeException 当反序列化过程中发生错误时抛出
+     */
+    public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
+        if (ArrayUtil.isEmpty(bytes)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(bytes, clazz);
+        } catch (IOException e) {
+            throw new RuntimeException("反序列化JSON字节数组为对象失败", e);
+        }
+    }
+
+    /**
+     * 将JSON字符串反序列化为复杂类型对象(使用TypeReference)
+     * <p>
+     * 支持泛型类型如List&lt;User&gt;、Map&lt;String, User&gt;等复杂类型的反序列化
+     * </p>
+     *
+     * @param text          JSON字符串,如果为空则返回null
+     * @param typeReference 目标类型引用,用于处理泛型类型
+     * @param <T>           泛型类型
+     * @return 反序列化后的对象
+     * @throws RuntimeException 当反序列化过程中发生错误时抛出
+     */
+    public static <T> T parseObject(String text, TypeReference<T> typeReference) throws JsonProcessingException {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            return objectMapper.readValue(text, typeReference);
+        } catch (IOException e) {
+            throw new RuntimeException("反序列化JSON字符串为复杂类型对象失败: " + text, e);
+        }
+    }
+
+    /**
+     * 将JSON字符串反序列化为Map对象
+     * <p>
+     * 将JSON字符串转换为Map&lt;String, Object&gt;对象,适用于处理动态JSON结构
+     * </p>
+     *
+     * @param text JSON字符串,如果为空则返回空Map
+     * @return Map对象
+     * @throws RuntimeException 当反序列化过程中发生错误时抛出
+     */
+    public static Map<String, Object> parseMap(String text) {
+        if (StrUtil.isEmpty(text)) {
+            return new HashMap<>();
+        }
+        try {
+            return objectMapper.readValue(text, Map.class);
+        } catch (IOException e) {
+            throw new RuntimeException("反序列化JSON字符串为Map失败: " + text, e);
+        }
+    }
+
+    /**
+     * 将JSON字符串反序列化为List对象
+     * <p>
+     * 将JSON数组字符串转换为指定元素类型的List对象
+     * </p>
+     *
+     * @param text  JSON字符串,如果为空则返回空List
+     * @param clazz List元素类型
+     * @param <T>   泛型类型
+     * @return List对象
+     * @throws RuntimeException 当反序列化过程中发生错误时抛出
+     */
+    public static <T> List<T> parseArray(String text, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            throw new RuntimeException("反序列化JSON字符串为数组失败: " + text, e);
+        }
+    }
+
+    /**
+     * 将JSON字符串解析为JsonNode树结构
+     * <p>
+     * 将JSON字符串转换为JsonNode对象,提供对JSON数据的树形访问方式
+     * </p>
+     *
+     * @param text JSON字符串,如果为空则返回null
+     * @return JsonNode对象
+     * @throws JsonProcessingException JSON处理异常
+     */
+    public static JsonNode parseTree(String text) throws JsonProcessingException {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        return objectMapper.readTree(text);
+    }
+
+    /**
+     * 将JSON字节数组解析为JsonNode树结构
+     * <p>
+     * 将JSON字节数组转换为JsonNode对象,提供对JSON数据的树形访问方式
+     * </p>
+     *
+     * @param text JSON字节数组,如果为空则返回null
+     * @return JsonNode对象
+     * @throws IOException IO异常
+     */
+    public static JsonNode parseTree(byte[] text) throws IOException {
+        if (ArrayUtil.isEmpty(text)) {
+            return null;
+        }
+        return objectMapper.readTree(text);
+    }
+
+    /**
+     * 判断字符串是否为有效的JSON格式
+     * <p>
+     * 通过尝试解析JSON字符串来验证其格式有效性
+     * </p>
+     *
+     * @param text 待检测的字符串,如果为空则返回false
+     * @return 是否为有效JSON格式
+     */
+    public static boolean isJson(String text) {
+        // 处理空值情况
+        if (StrUtil.isBlank(text)) {
+            return false;
+        }
+        try {
+            // 使用Jackson ObjectMapper来验证JSON格式
+            objectMapper.readTree(text);
+            return true;
+        } catch (Exception e) {
+            // 解析失败说明不是有效的JSON
+            return false;
+        }
+    }
+
+    /**
+     * 判断字符串是否为JSONArray类型的字符串(首尾都为中括号)
+     * <p>
+     * 通过检查字符串是否以'['开头并以']'结尾来判断是否为JSON数组格式
+     * </p>
+     *
+     * @param str 字符串,如果为空则返回false
+     * @return 是否为JSONArray类型字符串
+     */
+    public static boolean isTypeJSONArray(String str) {
+        if (StrUtil.isBlank(str)) {
+            return false;
+        }
+        return StrUtil.isWrap(StrUtil.trim(str), '[', ']');
+    }
+}

+ 73 - 0
tr-framework/src/main/java/cn/tr/core/utils/PswUtils.java

@@ -0,0 +1,73 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.BCrypt;
+import lombok.experimental.UtilityClass;
+
+/**
+ * @ClassName : PswUtils
+ * @Description : 密码工具类
+ * @Author : LF
+ * @Date: 2023年03月24日
+ */
+@UtilityClass
+public class PswUtils {
+    /**
+     * 生成BCryptPasswordEncoder密码
+     *
+     * @param password 密码
+     * @return 加密字符串
+     */
+    public static String encryptPassword(String password) {
+        return BCrypt.hashpw(password);
+    }
+
+    /**
+     * 判断密码是否相同
+     *
+     * @param rawPassword     真实密码
+     * @param encodedPassword 加密后字符
+     * @return 结果
+     */
+    public static boolean matchesPassword(String rawPassword, String encodedPassword) {
+        return BCrypt.checkpw(rawPassword, encodedPassword);
+    }
+
+    /**
+     * 校验密码
+     *
+     * @param password 密码
+     */
+    public static void validatePsw(String password) {
+        if(StrUtil.length(password)<6){
+            throw new UnsupportedOperationException("密码长度不得少于6位");
+        }
+    }
+
+    /**
+     * 校验密码强度
+     * 密码必须不少于8位,且必须包含字母、数字和特殊符号
+     *
+     * @param password 密码
+     */
+    public static void validateStrongPsw(String password) {
+        if (StrUtil.length(password) < 8) {
+            throw new UnsupportedOperationException("密码长度不得少于8位且必须包含字母、数字和特殊符号");
+        }
+
+        // 检查是否包含字母
+        if (!password.matches(".*[a-zA-Z].*")) {
+            throw new UnsupportedOperationException("密码长度不得少于8位且必须包含字母、数字和特殊符号");
+        }
+
+        // 检查是否包含数字
+        if (!password.matches(".*\\d.*")) {
+            throw new UnsupportedOperationException("密码长度不得少于8位且必须包含字母、数字和特殊符号");
+        }
+
+        // 检查是否包含特殊符号
+        if (!password.matches(".*[^a-zA-Z0-9].*")) {
+            throw new UnsupportedOperationException("密码长度不得少于8位且必须包含字母、数字和特殊符号");
+        }
+    }
+}

+ 227 - 0
tr-framework/src/main/java/cn/tr/core/utils/ServletUtils.java

@@ -0,0 +1,227 @@
+package cn.tr.core.utils;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+/**
+ * Servlet工具类
+ * 提供Servlet相关的便捷操作方法,适用于JDK 21环境
+ *
+ * @author lf
+ */
+public class ServletUtils {
+
+    /**
+     * 获取请求参数值
+     *
+     * @param name 参数名
+     * @return 参数值,如果请求不存在或参数不存在则返回null
+     */
+    public static String getParameter(String name) {
+        return Optional.ofNullable(getRequest())
+                .map(request -> request.getParameter(name))
+                .orElse(null);
+    }
+
+    /**
+     * 获取请求头信息
+     *
+     * @param name 请求头名称
+     * @return 请求头值,如果请求不存在或请求头不存在则返回null
+     */
+    public static String getHeader(String name) {
+        return Optional.ofNullable(getRequest())
+                .map(request -> request.getHeader(name))
+                .orElse(null);
+    }
+
+    /**
+     * 返回JSON格式响应数据
+     * 设置响应状态为200,Content-Type为application/json,并将对象序列化为JSON字符串输出
+     *
+     * @param response HttpServletResponse对象
+     * @param object 待序列化的对象
+     * @throws IOException IO异常
+     */
+    public static void writeJSON(HttpServletResponse response, Object object) throws IOException {
+        // 设置响应状态和内容类型
+        response.setStatus(HttpStatus.OK.value());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+
+        // 序列化对象并写入响应
+        String content = JsonUtils.toJsonString(object);
+        try (PrintWriter writer = response.getWriter()) {
+            writer.write(content);
+            writer.flush();
+        }
+    }
+
+    /**
+     * 返回附件下载响应(默认不关闭流)
+     *
+     * @param response HttpServletResponse对象
+     * @param filename 下载文件名
+     * @param content 附件内容字节数组
+     * @throws IOException IO异常
+     */
+    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
+        writeAttachment(response, filename, content, false);
+    }
+
+    /**
+     * 返回附件下载响应
+     *
+     * @param response HttpServletResponse对象
+     * @param filename 下载文件名
+     * @param content 附件内容字节数组
+     * @param close 是否关闭输出流
+     * @throws IOException IO异常
+     */
+    public static void writeAttachment(HttpServletResponse response, String filename, byte[] content, boolean close) throws IOException {
+        // 设置下载响应头
+        response.setHeader("Content-Disposition", "attachment;filename=" +
+                URLEncoder.encode(filename, StandardCharsets.UTF_8));
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+
+        // 输出附件内容
+        OutputStream outputStream = response.getOutputStream();
+        try {
+            outputStream.write(content);
+            outputStream.flush();
+        } finally {
+            if (close) {
+                outputStream.close();
+            }
+        }
+    }
+
+    /**
+     * 获取User-Agent信息
+     *
+     * @param request HttpServletRequest对象
+     * @return User-Agent字符串,如果不存在则返回空字符串
+     */
+    public static String getUserAgent(HttpServletRequest request) {
+        return Optional.ofNullable(request)
+                .map(req -> req.getHeader("User-Agent"))
+                .orElse("");
+    }
+
+    /**
+     * 获取当前请求的User-Agent信息
+     *
+     * @return User-Agent字符串,如果请求不存在或User-Agent不存在则返回null
+     */
+    public static String getUserAgent() {
+        return Optional.ofNullable(getRequest())
+                .map(ServletUtils::getUserAgent)
+                .orElse(null);
+    }
+
+    /**
+     * 获取当前线程绑定的请求对象
+     *
+     * @return HttpServletRequest对象,如果不存在则返回null
+     */
+    public static HttpServletRequest getRequest() {
+        try {
+            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+            if (requestAttributes instanceof ServletRequestAttributes) {
+                return ((ServletRequestAttributes) requestAttributes).getRequest();
+            }
+            return null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取当前请求的客户端IP地址
+     *
+     * @return 客户端IP地址,如果请求不存在则返回null
+     */
+    public static String getClientIP() {
+        return getClientIP(getRequest());
+    }
+
+    /**
+     * 获取指定请求的客户端IP地址
+     * 按优先级依次尝试从HTTP头中获取真实客户端IP:
+     * 1. X-Forwarded-For (负载均衡器/代理服务器转发的真实IP)
+     * 2. Proxy-Client-IP (Apache代理服务器)
+     * 3. WL-Proxy-Client-IP (WebLogic代理服务器)
+     * 4. RemoteAddr (直接连接的客户端IP)
+     *
+     * @param request HttpServletRequest对象
+     * @return 客户端IP地址,如果请求不存在则返回null
+     */
+    public static String getClientIP(HttpServletRequest request) {
+        if (request == null) {
+            return null;
+        }
+
+        // 尝试从常见HTTP头获取真实客户端IP
+        String ip = request.getHeader("X-Forwarded-For");
+        if (isValidIp(ip)) {
+            // X-Forwarded-For可能包含多个IP,取第一个
+            if (ip.contains(",")) {
+                ip = ip.split(",")[0].trim();
+            }
+            return ip;
+        }
+
+        // 尝试其他代理服务器头部
+        ip = request.getHeader("Proxy-Client-IP");
+        if (isValidIp(ip)) {
+            return ip;
+        }
+
+        ip = request.getHeader("WL-Proxy-Client-IP");
+        if (isValidIp(ip)) {
+            return ip;
+        }
+
+        // 最后使用直接连接的客户端IP
+        return request.getRemoteAddr();
+    }
+
+    /**
+     * 判断请求内容类型是否为JSON
+     *
+     * @param request ServletRequest对象
+     * @return 是否为JSON请求
+     */
+    public static boolean isJsonRequest(ServletRequest request) {
+        if (request == null) {
+            return false;
+        }
+
+        String contentType = request.getContentType();
+        return contentType != null &&
+                contentType.toLowerCase().startsWith(MediaType.APPLICATION_JSON_VALUE.toLowerCase());
+    }
+
+    /**
+     * 验证IP地址是否有效
+     *
+     * @param ip IP地址字符串
+     * @return true表示IP有效,false表示IP无效
+     */
+    private static boolean isValidIp(String ip) {
+        return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip);
+    }
+}

+ 53 - 0
tr-framework/src/main/java/cn/tr/core/utils/UserUtil.java

@@ -0,0 +1,53 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.lang.Pair;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.awt.*;
+import java.util.function.Function;
+
+/**
+ * @ClassName : UserUtil
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年09月05日
+ */
+
+public class UserUtil {
+    /**
+     * 根据
+     */
+    public static Function<String, Pair<String,String>> userNickNameSupplier= userId->null;
+
+    public static String getNickName(String userId){
+        if(StrUtil.isBlank(userId)){
+            return null;
+        }
+        Pair<String, String> nickNameAndGender = getNickNameAndGender(userId);
+        return ObjectUtil.isNull(nickNameAndGender)?null:nickNameAndGender.getKey();
+    }
+
+    public static Pair<String,String> getNickNameAndGender(String userId){
+        if(StrUtil.isBlank(userId)){
+            return null;
+        }
+        return  userNickNameSupplier.apply(userId);
+    }
+
+    public static String getAvatar(String userId){
+        if(StrUtil.isBlank(userId)){
+            return null;
+        }
+        Pair<String, String> nickNameAndGender = getNickNameAndGender(userId);
+        if(nickNameAndGender==null){
+            return getAvatar("匿名",null);
+        }else {
+            return getAvatar(nickNameAndGender.getKey(),nickNameAndGender.getValue());
+        }
+    }
+
+    public static String getAvatar(String nickname,String gender){
+        return null;
+    }
+}

+ 145 - 0
tr-framework/src/main/java/cn/tr/core/utils/ValidationUtils.java

@@ -0,0 +1,145 @@
+package cn.tr.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.experimental.UtilityClass;
+import org.springframework.util.StringUtils;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * 校验工具类
+ * <p>
+ * 提供常用数据格式校验和Bean Validation功能的便捷方法
+ * 包括手机号、URL、XML名称等格式校验,以及基于注解的Bean校验
+ * </p>
+ *
+ * @author 芋道源码
+ */
+@UtilityClass
+public class ValidationUtils {
+
+    /**
+     * 默认Validator实例
+     * 使用静态初始化确保在类加载时创建,避免重复创建开销
+     */
+    private static final Validator validator = createValidator();
+
+    /**
+     * 手机号正则表达式模式
+     * 支持中国大陆手机号格式,包括+86和0086前缀
+     * 匹配13-19开头的11位数字手机号
+     */
+    private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1[3-9]\\d{9}$");
+
+    /**
+     * URL正则表达式模式
+     * 支持http、https、ftp等协议的URL格式校验
+     */
+    private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]");
+
+    /**
+     * XML NCName正则表达式模式
+     * 符合XML NCName规范的名称格式校验
+     * 必须以字母或下划线开头,后续可包含字母、数字、下划线、连字符、点号
+     */
+    private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*");
+
+    /**
+     * 创建Validator实例
+     * <p>
+     * 使用默认配置构建Validator工厂并获取Validator实例
+     * 该方法在类初始化时调用一次
+     * </p>
+     *
+     * @return Validator实例
+     */
+    private static Validator createValidator() {
+        return Validation.buildDefaultValidatorFactory().getValidator();
+    }
+
+    /**
+     * 校验手机号格式
+     * <p>
+     * 支持中国大陆手机号格式校验,包括以下格式:
+     * - 11位数字:13812345678
+     * - 带国家代码:+8613812345678 或 008613812345678
+     * </p>
+     *
+     * @param mobile 待校验的手机号字符串
+     * @return true表示格式正确,false表示格式错误或为空
+     */
+    public static boolean isMobile(String mobile) {
+        return StringUtils.hasText(mobile)
+                && PATTERN_MOBILE.matcher(mobile).matches();
+    }
+
+    /**
+     * 校验URL格式
+     * <p>
+     * 支持http、https、ftp、file等协议的URL格式校验
+     * </p>
+     *
+     * @param url 待校验的URL字符串
+     * @return true表示格式正确,false表示格式错误或为空
+     */
+    public static boolean isURL(String url) {
+        return StringUtils.hasText(url)
+                && PATTERN_URL.matcher(url).matches();
+    }
+
+    /**
+     * 校验XML NCName格式
+     * <p>
+     * 校验字符串是否符合XML NCName(Non-Colon Name)规范:
+     * - 必须以字母或下划线开头
+     * - 后续字符可包含字母、数字、下划线、连字符、点号
+     * - 不能包含冒号
+     * </p>
+     *
+     * @param str 待校验的字符串
+     * @return true表示格式正确,false表示格式错误或为空
+     */
+    public static boolean isXmlNCName(String str) {
+        return StringUtils.hasText(str)
+                && PATTERN_XML_NCNAME.matcher(str).matches();
+    }
+
+    /**
+     * 使用默认Validator校验对象
+     * <p>
+     * 基于Bean Validation规范对对象进行校验,
+     * 如果校验失败会抛出ConstraintViolationException异常
+     * </p>
+     *
+     * @param object 待校验的对象
+     * @param groups 校验组,可指定特定的校验组
+     * @throws ConstraintViolationException 当校验失败时抛出
+     */
+    public static void validate(Object object, Class<?>... groups) {
+        validate(validator, object, groups);
+    }
+
+    /**
+     * 使用指定Validator校验对象
+     * <p>
+     * 基于Bean Validation规范对对象进行校验,
+     * 如果校验失败会抛出ConstraintViolationException异常
+     * </p>
+     *
+     * @param validator 指定的Validator实例
+     * @param object    待校验的对象
+     * @param groups    校验组,可指定特定的校验组
+     * @throws ConstraintViolationException 当校验失败时抛出
+     */
+    public static void validate(Validator validator, Object object, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (CollUtil.isNotEmpty(constraintViolations)) {
+            throw new ConstraintViolationException(constraintViolations);
+        }
+    }
+}

+ 19 - 0
tr-framework/src/main/java/cn/tr/core/validation/Insert.java

@@ -0,0 +1,19 @@
+package cn.tr.core.validation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据新增时校验注解
+ * <p>
+ * 用于标记在实体类字段上,表示该字段只在数据新增时进行校验
+ * 通常与 validation groups 配合使用,实现新增和更新时的不同校验规则
+ * </p>
+ *
+ * @author LF
+ * @since 2023年03月27日
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Insert {
+}

+ 19 - 0
tr-framework/src/main/java/cn/tr/core/validation/Update.java

@@ -0,0 +1,19 @@
+package cn.tr.core.validation;
+
+import java.lang.annotation.*;
+
+/**
+ * 数据更新时校验注解
+ * <p>
+ * 用于标记在实体类字段上,表示该字段只在数据更新时进行校验
+ * 通常与 validation groups 配合使用,实现新增和更新时的不同校验规则
+ * </p>
+ *
+ * @author LF
+ * @since 2023年03月27日
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Update {
+}

+ 2 - 0
tr-framework/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1,2 @@
+cn.tr.core.config.RedisConfig
+cn.tr.core.config.FaviconInterceptor

BIN
tr-framework/src/main/resources/ip2region.xdb


+ 61 - 0
tr-framework/src/test/java/cn/tr/core/utils/CommonNetWorkInfoUtilTest.java

@@ -0,0 +1,61 @@
+package cn.tr.core.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CommonNetWorkInfoUtilTest {
+
+    @Test
+    public void testGetNetworkUpRate() {
+        // 测试网络速率获取方法
+        Map<String, String> networkRate = CommonNetWorkInfoUtil.getNetworkUpRate();
+        
+        // 验证返回结果不为null
+        assertNotNull(networkRate);
+        
+        // 验证包含上行和下行速率键
+        assertTrue(networkRate.containsKey("UP"));
+        assertTrue(networkRate.containsKey("DOWN"));
+        
+        // 验证速率格式包含单位
+        String upRate = networkRate.get("UP");
+        String downRate = networkRate.get("DOWN");
+        
+        assertNotNull(upRate);
+        assertNotNull(downRate);
+        
+        // 验证格式包含速率单位(B/S或类似单位)
+        assertTrue(upRate.contains("B/S") || upRate.contains("/S"));
+        assertTrue(downRate.contains("B/S") || downRate.contains("/S"));
+        
+        System.out.println("网络上行速率: " + upRate);
+        System.out.println("网络下行速率: " + downRate);
+    }
+
+    @Test
+    public void testFormatNumber() {
+        // 通过反射访问私有方法formatNumber进行测试
+        try {
+            java.lang.reflect.Method method = CommonNetWorkInfoUtil.class.getDeclaredMethod(
+                "formatNumber", double.class);
+            method.setAccessible(true);
+            
+            double testValue = 123.456;
+            String result = (String) method.invoke(null, testValue);
+            
+            assertEquals("123.46", result); // 默认保留两位小数
+        } catch (Exception e) {
+            fail("无法访问formatNumber私有方法: " + e.getMessage());
+        }
+    }
+
+    @Test
+    public void testReadInLine() {
+        // 由于readInLine是私有方法且依赖系统命令输出,这里主要测试方法是否存在
+        // 实际测试会在getNetworkUpRate调用中间接验证
+        assertNotNull(CommonNetWorkInfoUtil.class);
+    }
+}

+ 77 - 0
tr-framework/src/test/java/cn/tr/core/utils/IpUtilTest.java

@@ -0,0 +1,77 @@
+package cn.tr.core.utils;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class IpUtilTest {
+
+    @Test
+    public void testGetCityInfoIpV4() {
+        // 测试IPv4地址查询
+        String ip = "117.159.35.156";
+        String result = IpUtil.getCityInfo(ip);
+        assertNotNull(result);
+        assertNotEquals("未知", result);
+    }
+
+    @Test
+    public void testGetCityInfoIpV6() {
+        // 测试IPv6地址查询
+        String ip = "1002:003B:456C:678D:890E:0012:234F:56G7";
+        String result = IpUtil.getCityInfoIpV6(ip);
+        assertNotNull(result);
+        assertNotEquals("未知", result);
+    }
+
+    @Test
+    public void testInvalidIpV4() {
+        // 测试无效IPv4地址
+        String ip = "999.999.999.999";
+        String result = IpUtil.getCityInfoIpV4(ip);
+        assertEquals("未知", result);
+    }
+
+    @Test
+    public void testInvalidIpV6() {
+        // 测试无效IPv6地址
+        String ip = "invalid:ipv6:address";
+        String result = IpUtil.getCityInfoIpV6(ip);
+        assertEquals("未知", result);
+    }
+
+    @Test
+    public void testLocalhostIpV4() {
+        // 测试本地回环地址
+        String ip = "127.0.0.1";
+        String result = IpUtil.getCityInfoIpV4(ip);
+        assertNotNull(result);
+    }
+
+    @Test
+    public void testLocalhostIpV6() {
+        // 测试IPv6本地回环地址
+        String ip = "::1";
+        String result = IpUtil.getCityInfoIpV6(ip);
+        assertNotNull(result);
+    }
+
+    @Test
+    public void testEmptyIp() {
+        // 测试空IP地址
+        String ip = "";
+        String resultV4 = IpUtil.getCityInfoIpV4(ip);
+        String resultV6 = IpUtil.getCityInfoIpV6(ip);
+        assertEquals("未知", resultV4);
+        assertEquals("未知", resultV6);
+    }
+
+    @Test
+    public void testNullIp() {
+        // 测试null IP地址
+        String resultV4 = IpUtil.getCityInfoIpV4(null);
+        String resultV6 = IpUtil.getCityInfoIpV6(null);
+        assertEquals("未知", resultV4);
+        assertEquals("未知", resultV6);
+    }
+}

+ 99 - 0
tr-framework/src/test/java/cn/tr/core/utils/JsonUtilsTest.java

@@ -0,0 +1,99 @@
+package cn.tr.core.utils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeAll;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+/**
+ * JsonUtils 测试类
+ */
+public class JsonUtilsTest {
+
+    @Test
+    public void testJsonUtils() {
+        // 测试对象序列化和反序列化
+        Map<String, Object> testMap = new HashMap<>();
+        testMap.put("name", "张三");
+        testMap.put("age", 25);
+        testMap.put("skills", Arrays.asList("Java", "Python", "JavaScript"));
+        
+        // 测试 toJsonString 和 parseObject
+        String jsonString = JsonUtils.toJsonString(testMap);
+        assertNotNull(jsonString);
+        System.out.println("序列化结果: " + jsonString);
+        
+        Map<String, Object> parsedMap = JsonUtils.parseMap(jsonString);
+        assertEquals(testMap.get("name"), parsedMap.get("name"));
+        assertEquals(testMap.get("age"), parsedMap.get("age"));
+        
+        // 测试 isJson 方法
+        assertTrue(JsonUtils.isJson(jsonString));
+        assertFalse(JsonUtils.isJson("invalid json"));
+        assertFalse(JsonUtils.isJson(""));
+        assertFalse(JsonUtils.isJson(null));
+        
+        // 测试对象序列化
+        Person person = new Person("李四", 30);
+        String personJson = JsonUtils.toJsonString(person);
+        assertNotNull(personJson);
+        
+        Person parsedPerson = JsonUtils.parseObject(personJson, Person.class);
+        assertEquals(person.getName(), parsedPerson.getName());
+        assertEquals(person.getAge(), parsedPerson.getAge());
+        
+        // 测试 parseArray
+        String arrayJson = "[{\"name\":\"王五\",\"age\":28},{\"name\":\"赵六\",\"age\":35}]";
+        List<Person> personList = JsonUtils.parseArray(arrayJson, Person.class);
+        assertEquals(2, personList.size());
+        assertEquals("王五", personList.get(0).getName());
+        
+        // 测试 isTypeJSONArray
+        assertTrue(JsonUtils.isTypeJSONArray(arrayJson));
+        assertFalse(JsonUtils.isTypeJSONArray("{\"key\":\"value\"}"));
+        assertFalse(JsonUtils.isTypeJSONArray(""));
+        
+        // 测试 parseObject with TypeReference
+        try {
+            Map<String, Object> complexMap = JsonUtils.parseObject(jsonString, new TypeReference<Map<String, Object>>() {});
+            assertNotNull(complexMap);
+            assertEquals(testMap.get("name"), complexMap.get("name"));
+        } catch (Exception e) {
+            fail("TypeReference解析失败: " + e.getMessage());
+        }
+        
+        // 测试空值处理
+        assertNull(JsonUtils.toJsonString(null));
+        assertNull(JsonUtils.parseObject("", Person.class));
+        assertNull(JsonUtils.parseObject((String) null, Person.class));
+        
+        // 测试 parseTree
+        try {
+            var jsonNode = JsonUtils.parseTree(jsonString);
+            assertNotNull(jsonNode);
+            assertEquals("张三", jsonNode.get("name").asText());
+        } catch (Exception e) {
+            fail("parseTree解析失败: " + e.getMessage());
+        }
+    }
+    
+    // 测试用的简单类
+    static class Person {
+        private String name;
+        private Integer age;
+        
+        public Person() {}
+        
+        public Person(String name, Integer age) {
+            this.name = name;
+            this.age = age;
+        }
+        
+        public String getName() { return name; }
+        public void setName(String name) { this.name = name; }
+        public Integer getAge() { return age; }
+        public void setAge(Integer age) { this.age = age; }
+    }
+}

+ 149 - 0
tr-framework/src/test/java/cn/tr/core/utils/ServletUtilsTest.java

@@ -0,0 +1,149 @@
+package cn.tr.core.utils;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.http.MediaType;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * ServletUtils 测试类
+ */
+public class ServletUtilsTest {
+
+    @Test
+    public void testGetParameter() {
+        // 创建Mock请求
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setParameter("testParam", "testValue");
+        request.setParameter("nullParam", (String) null);
+
+        // 设置请求上下文
+        ServletRequestAttributes attributes = new ServletRequestAttributes(request);
+        RequestContextHolder.setRequestAttributes(attributes);
+
+        try {
+            // 测试正常情况
+            assertEquals("testValue", ServletUtils.getParameter("testParam"));
+
+            // 测试参数不存在情况
+            assertNull(ServletUtils.getParameter("nullParam"));
+
+            // 测试不存在的参数
+            assertNull(ServletUtils.getParameter("nonExistentParam"));
+        } finally {
+
+        }
+    }
+
+    @Test
+    public void testGetHeader() {
+        // 创建Mock请求
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader("User-Agent", "Mozilla/5.0");
+        request.addHeader("Authorization", "Bearer token123");
+
+        // 设置请求上下文
+        ServletRequestAttributes attributes = new ServletRequestAttributes(request);
+        RequestContextHolder.setRequestAttributes(attributes);
+
+        try {
+            // 测试正常情况
+            assertEquals("Mozilla/5.0", ServletUtils.getHeader("User-Agent"));
+            assertEquals("Bearer token123", ServletUtils.getHeader("Authorization"));
+
+            // 测试不存在的header
+            assertNull(ServletUtils.getHeader("Non-Existent"));
+        } finally {
+
+        }
+    }
+
+    @Test
+    public void testGetUserAgent() {
+        // 测试正常情况
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader("User-Agent", "Mozilla/5.0");
+        assertEquals("Mozilla/5.0", ServletUtils.getUserAgent(request));
+
+        // 测试null请求
+        assertEquals("", ServletUtils.getUserAgent(null));
+
+        // 测试没有User-Agent的情况
+        MockHttpServletRequest emptyRequest = new MockHttpServletRequest();
+        assertEquals("", ServletUtils.getUserAgent(emptyRequest));
+    }
+
+    @Test
+    public void testGetClientIP() {
+        String clientIP = "192.168.1.100";
+
+        // 测试 X-Forwarded-For
+        MockHttpServletRequest request1 = new MockHttpServletRequest();
+        request1.addHeader("X-Forwarded-For", clientIP);
+        assertEquals(clientIP, ServletUtils.getClientIP(request1));
+
+        // 测试 Proxy-Client-IP
+        MockHttpServletRequest request2 = new MockHttpServletRequest();
+        request2.addHeader("Proxy-Client-IP", clientIP);
+        assertEquals(clientIP, ServletUtils.getClientIP(request2));
+
+        // 测试 WL-Proxy-Client-IP
+        MockHttpServletRequest request3 = new MockHttpServletRequest();
+        request3.addHeader("WL-Proxy-Client-IP", clientIP);
+        assertEquals(clientIP, ServletUtils.getClientIP(request3));
+
+        // 测试 RemoteAddr
+        MockHttpServletRequest request4 = new MockHttpServletRequest();
+        request4.setRemoteAddr(clientIP);
+        assertEquals(clientIP, ServletUtils.getClientIP(request4));
+
+        // 测试 null 请求
+        assertNull(ServletUtils.getClientIP(null));
+    }
+
+    @Test
+    public void testIsJsonRequest() {
+        // 测试 JSON 请求
+        MockHttpServletRequest jsonRequest = new MockHttpServletRequest();
+        jsonRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        assertTrue(ServletUtils.isJsonRequest(jsonRequest));
+
+        // 测试带字符集的 JSON 请求
+        MockHttpServletRequest jsonWithCharsetRequest = new MockHttpServletRequest();
+        jsonWithCharsetRequest.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
+        assertTrue(ServletUtils.isJsonRequest(jsonWithCharsetRequest));
+
+        // 测试非 JSON 请求
+        MockHttpServletRequest nonJsonRequest = new MockHttpServletRequest();
+        nonJsonRequest.setContentType(MediaType.TEXT_HTML_VALUE);
+        assertFalse(ServletUtils.isJsonRequest(nonJsonRequest));
+
+        // 测试 null 请求
+        assertFalse(ServletUtils.isJsonRequest(null));
+
+        // 测试 null content type
+        MockHttpServletRequest nullContentTypeRequest = new MockHttpServletRequest();
+        nullContentTypeRequest.setContentType(null);
+        assertFalse(ServletUtils.isJsonRequest(nullContentTypeRequest));
+    }
+
+    // 测试用的简单类
+    static class Person {
+        private String name;
+        private Integer age;
+
+        public Person() {}
+
+        public Person(String name, Integer age) {
+            this.name = name;
+            this.age = age;
+        }
+
+        public String getName() { return name; }
+        public void setName(String name) { this.name = name; }
+        public Integer getAge() { return age; }
+        public void setAge(Integer age) { this.age = age; }
+    }
+}

+ 127 - 0
tr-framework/src/test/java/cn/tr/core/utils/TreeBuilderTest.java

@@ -0,0 +1,127 @@
+package cn.tr.core.utils;
+
+import cn.tr.core.tree.TreeBuilder;
+import cn.tr.core.tree.TreeNode;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * TreeBuilder 工具类测试类
+ */
+public class TreeBuilderTest {
+
+    private List<TestTreeNode> testNodes;
+
+    /**
+     * 测试用的树节点实现类
+     */
+    static class TestTreeNode extends TreeNode<TestTreeNode, Long> {
+        public TestTreeNode() {}
+
+        public TestTreeNode(Long id, Long parentId, String name) {
+            this.setId(id);
+            this.setParentId(parentId);
+            this.setName(name);
+        }
+    }
+
+    @BeforeEach
+    void setUp() {
+        // 初始化测试数据
+        testNodes = new ArrayList<>();
+        // 根节点
+        testNodes.add(new TestTreeNode(1L, 0L, "根节点1"));
+        testNodes.add(new TestTreeNode(2L, 0L, "根节点2"));
+        // 第一层子节点
+        testNodes.add(new TestTreeNode(3L, 1L, "子节点1-1"));
+        testNodes.add(new TestTreeNode(4L, 1L, "子节点1-2"));
+        testNodes.add(new TestTreeNode(5L, 2L, "子节点2-1"));
+        // 第二层子节点
+        testNodes.add(new TestTreeNode(6L, 3L, "孙节点1-1-1"));
+        testNodes.add(new TestTreeNode(7L, 3L, "孙节点1-1-2"));
+    }
+
+    @Test
+    void testBuildTree() {
+        // 测试构建树结构
+        List<TestTreeNode> tree = TreeBuilder.buildTree(testNodes);
+
+        // 验证根节点数量
+        assertEquals(2, tree.size());
+
+        // 验证第一个根节点
+        TestTreeNode rootNode1 = tree.get(0);
+        assertEquals(1L, rootNode1.getId());
+        assertEquals("根节点1", rootNode1.getName());
+        assertTrue(rootNode1.isRoot());
+        assertFalse(rootNode1.isLeaf());
+
+        // 验证根节点的子节点
+        assertEquals(2, rootNode1.getChildren().size());
+        TestTreeNode child1 = rootNode1.getChildren().get(0);
+        assertEquals(3L, child1.getId());
+        assertEquals(1L, child1.getParentId());
+        assertEquals("子节点1-1", child1.getName());
+
+        // 验证孙节点
+        assertEquals(2, child1.getChildren().size());
+        TestTreeNode grandChild1 = child1.getChildren().get(0);
+        assertEquals(6L, grandChild1.getId());
+        assertEquals(3L, grandChild1.getParentId());
+        assertEquals("孙节点1-1-1", grandChild1.getName());
+        assertTrue(grandChild1.isLeaf());
+    }
+
+    @Test
+    void testBuildTreeWithRootParentId() {
+        // 添加一个具有特定根父ID的节点
+        testNodes.add(new TestTreeNode(8L, 0L, "特殊根节点"));
+        testNodes.add(new TestTreeNode(9L, 8L, "特殊子节点"));
+
+        // 使用指定的根父ID构建树
+        List<TestTreeNode> tree = TreeBuilder.buildTreeWithRootParentId(testNodes, 0L);
+
+        // 验证结果
+        assertEquals(1, tree.size());
+        assertEquals(8L, tree.get(0).getId());
+        assertEquals("特殊根节点", tree.get(0).getName());
+        assertEquals(1, tree.get(0).getChildren().size());
+    }
+
+    @Test
+    void testFlattenTree() {
+        // 构建树结构
+        List<TestTreeNode> tree = TreeBuilder.buildTree(testNodes);
+
+        // 扁平化树结构
+        List<TestTreeNode> flatList = TreeBuilder.flattenTree(tree);
+
+        // 验证扁平化结果包含所有节点
+        assertEquals(7, flatList.size());
+
+        // 验证所有原始节点都在扁平化列表中
+        for (TestTreeNode node : testNodes) {
+            assertTrue(flatList.stream().anyMatch(n -> n.getId().equals(node.getId())));
+        }
+    }
+
+    @Test
+    void testBuildTreeWithEmptyList() {
+        // 测试空列表情况
+        List<TestTreeNode> emptyList = new ArrayList<>();
+        List<TestTreeNode> tree = TreeBuilder.buildTree(emptyList);
+        assertTrue(tree.isEmpty());
+    }
+
+    @Test
+    void testBuildTreeWithNullList() {
+        // 测试null列表情况
+        List<TestTreeNode> tree = TreeBuilder.buildTree(null);
+        assertTrue(tree.isEmpty());
+    }
+}

+ 95 - 0
tr-framework/src/test/java/cn/tr/core/utils/ValidationUtilsTest.java

@@ -0,0 +1,95 @@
+package cn.tr.core.utils;
+
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ValidationUtilsTest {
+
+    // 测试用的实体类
+    static class TestEntity {
+        @NotBlank(message = "姓名不能为空")
+        private String name;
+        
+        @NotNull(message = "年龄不能为空")
+        private Integer age;
+
+        public TestEntity(String name, Integer age) {
+            this.name = name;
+            this.age = age;
+        }
+
+        // Getters and setters
+        public String getName() { return name; }
+        public void setName(String name) { this.name = name; }
+        public Integer getAge() { return age; }
+        public void setAge(Integer age) { this.age = age; }
+    }
+
+    @Test
+    public void testIsMobile() {
+        // 有效手机号
+        assertTrue(ValidationUtils.isMobile("13812345678"));
+        assertTrue(ValidationUtils.isMobile("15912345678"));
+        
+        // 无效手机号
+        assertFalse(ValidationUtils.isMobile("12345678901"));
+        assertFalse(ValidationUtils.isMobile("abc"));
+        assertFalse(ValidationUtils.isMobile(""));
+        assertFalse(ValidationUtils.isMobile(null));
+    }
+
+    @Test
+    public void testIsURL() {
+        // 有效URL
+        assertTrue(ValidationUtils.isURL("https://www.example.com"));
+        assertTrue(ValidationUtils.isURL("http://example.com"));
+        assertTrue(ValidationUtils.isURL("ftp://files.example.com"));
+        
+        // 无效URL
+        assertFalse(ValidationUtils.isURL("not_a_url"));
+        assertFalse(ValidationUtils.isURL(""));
+        assertFalse(ValidationUtils.isURL(null));
+    }
+
+    @Test
+    public void testIsXmlNCName() {
+        // 有效的XML NCName
+        assertTrue(ValidationUtils.isXmlNCName("validName"));
+        assertTrue(ValidationUtils.isXmlNCName("_valid_name"));
+        assertTrue(ValidationUtils.isXmlNCName("valid-name"));
+        
+        // 无效的XML NCName
+        assertFalse(ValidationUtils.isXmlNCName("123invalid"));
+        assertFalse(ValidationUtils.isXmlNCName("-invalid"));
+        assertFalse(ValidationUtils.isXmlNCName(""));
+        assertFalse(ValidationUtils.isXmlNCName(null));
+    }
+
+    @Test
+    public void testValidateSuccess() {
+        // 验证通过的情况
+        TestEntity entity = new TestEntity("张三", 25);
+        assertDoesNotThrow(() -> ValidationUtils.validate(entity));
+    }
+
+    @Test
+    public void testValidateFailure() {
+        // 验证失败的情况
+        TestEntity entity = new TestEntity("", null);
+        assertThrows(ConstraintViolationException.class, () -> ValidationUtils.validate(entity));
+    }
+
+    @Test
+    public void testValidateWithCustomValidator() {
+        // 使用自定义validator验证
+        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        TestEntity entity = new TestEntity("李四", 30);
+        assertDoesNotThrow(() -> ValidationUtils.validate(validator, entity));
+    }
+}

+ 22 - 0
tr-modules-api/pom.xml

@@ -0,0 +1,22 @@
+<?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">
+    <parent>
+        <artifactId>phototherapy</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <version>${revision}</version>
+    <artifactId>tr-modules-api</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>tr-module-system-api</module>
+        <module>tr-module-export-api</module>
+<!--        <module>tr-module-quartz-api</module>-->
+    </modules>
+
+
+</project>

+ 41 - 0
tr-modules-api/tr-module-export-api/pom.xml

@@ -0,0 +1,41 @@
+<?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">
+    <parent>
+        <artifactId>tr-modules-api</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-module-export-api</artifactId>
+    <version>${revision}</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-module-system-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-extra</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 53 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/api/ExcelApi.java

@@ -0,0 +1,53 @@
+package cn.tr.module.api;
+
+
+import cn.hutool.core.codec.Base64;
+import org.apache.logging.log4j.util.Base64Util;
+
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @ClassName : ExcelApi
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年06月05日
+ */
+
+public interface ExcelApi {
+    /**
+     * 导出文件
+     * @param aClass 导出类
+     * @param data   导出数据
+     * @return 返回base64
+     */
+    default <T> String exportExcel(Class<T> aClass, Collection<T> data){
+        return "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,"+ Base64.encode(doExportExcel(aClass, data));
+    };
+
+    /**
+     * 导出文件
+     * @param aClass 导出类
+     * @param data   导出数据
+     * @return 文件字节码数组
+     */
+    <T> byte[] doExportExcel(Class<T> aClass, Collection<T> data);
+
+    default  <T> String importExcel(String filename, InputStream inputStream, Class<T> aClass) throws Exception {
+        return importExcel(filename,inputStream,aClass,null);
+    }
+
+    /**
+     * 导入文件
+     * @param filename    文件名称
+     * @param inputStream 数据流
+     * @param aClass      文件类
+     * @param  customObj  自定义参数
+     * @return            导入记录id
+     * @throws Exception
+     */
+    <T> String importExcel(String filename, InputStream inputStream, Class<T> aClass, Map<String,Object> customObj) throws Exception;
+
+
+}

+ 26 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/constant/ExcelConstant.java

@@ -0,0 +1,26 @@
+package cn.tr.module.constant;
+
+/**
+ * @ClassName : ExportConstant
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年05月20日
+ */
+
+public interface ExcelConstant {
+    /**
+     * 表格最大行数
+     */
+    Integer MAX_ROW=65535;
+
+
+    String ROW_PARSE_TOPIC="/export/biz/excel";
+
+    String SHEET_FINISH_TOPIC="/export/biz/excel/callback";
+
+    String SHEET_TOTAL_COUNT="sheet_total_count";
+    String SHEET_SUCCESS_COUNT="sheet_success_count";
+    String SHEET_FAIL_COUNT="sheet_fail_count";
+
+    String CUSTOM_OBJ_SHEET_ID="sheetId";
+}

+ 26 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/annotation/ExcelCellMerge.java

@@ -0,0 +1,26 @@
+package cn.tr.module.export.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * @Interface : ExcelCellMerge
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年11月08日
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelCellMerge {
+    /**
+     * 是否自动合并
+     * @return
+     */
+    boolean autoMerge() default true;
+
+    /**
+     * 当单元格内容为空时是否合并
+     * @return
+     */
+    boolean mergeBlank() default false;
+}

+ 33 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/annotation/ExcelPropertySupport.java

@@ -0,0 +1,33 @@
+package cn.tr.module.export.annotation;
+
+
+import cn.tr.module.export.handler.NoneCascadeSelectConverter;
+
+import java.lang.annotation.*;
+
+/**
+ * @@interface : ExcelDict
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年05月19日
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelPropertySupport {
+    /**
+     * 字典码
+     * 1、导出时会根据字典码自动生成下拉菜单
+     * 2、导出时会根据字段值自动返回字典值
+     */
+    String dictCode() default "";
+
+
+    ExcelSelect select() default @ExcelSelect(converter = NoneCascadeSelectConverter.class);
+    /**
+     * 导出模板批注
+     */
+    String comment() default "";
+
+    ExcelCellMerge merge()default @ExcelCellMerge(autoMerge = false,mergeBlank = false);
+}

+ 44 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/annotation/ExcelSelect.java

@@ -0,0 +1,44 @@
+package cn.tr.module.export.annotation;
+
+import cn.tr.module.export.handler.AbstractCascadeSelectConverter;
+import cn.tr.module.export.handler.NoneCascadeSelectConverter;
+
+import java.lang.annotation.*;
+
+/**
+ * @ClassName : ExcelCombo
+ * @Description : 对于excel转入转出集合的支持
+ * @Author : LF
+ * @Date: 2023年06月01日
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelSelect {
+    /**
+     * 集合处理参数
+     */
+    String[] param() default "";
+
+    /**
+     * 集合转换类
+     */
+    Class<? extends AbstractCascadeSelectConverter<?>> converter() default NoneCascadeSelectConverter.class;
+
+    /**
+     * 级联时的级联属性名称
+     * <pre>
+     *     省-> 市-> 区
+     *     省的 cascade 为 市
+     *     市的 cascade 为 区
+     * <pre/>
+     */
+    String cascade() default "";
+
+    /**
+     * 是否允许字典值自定义
+     * 即是否用户输入的内容在字典中不存在
+     */
+    boolean allowCustom() default false;
+
+}

+ 37 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/bean/ExportResult.java

@@ -0,0 +1,37 @@
+package cn.tr.module.export.bean;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.module.export.enums.ExportType;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @ClassName : ExportResult
+ * @Description : 文件导出结果
+ * @Author : LF
+ * @Date: 2023年06月15日
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExportResult {
+    private String fileName;
+    private String base64;
+    private ExportType type;
+    public String getFileName() {
+        if(CollectionUtil.size(StrUtil.split(fileName,"."))>2){
+            return fileName;
+        }
+        return fileName+type.getSuffix();
+    }
+
+    public static ExportResult of(String fileName,String base64){
+        return new ExportResult(fileName,base64,ExportType.EXCEL);
+    }
+
+    public static ExportResult of(String fileName,String base64,ExportType type){
+        return new ExportResult(fileName,base64,type);
+    }
+}

+ 27 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/converter/BooleanConverter.java

@@ -0,0 +1,27 @@
+package cn.tr.module.export.converter;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * @ClassName : BooleanConverter
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年05月19日
+ */
+
+public class BooleanConverter implements Converter<Boolean> {
+    @Override
+    public Class<Boolean> supportJavaTypeKey() {
+        return Boolean.class;
+    }
+
+    @Override
+    public WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        WriteCellData<Object> cellData = new WriteCellData<>("");
+        cellData.setStringValue(Boolean.TRUE.equals(value)?"是":"否");
+        return cellData;
+    }
+}

+ 54 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/converter/DictCascadeSelectConverter.java

@@ -0,0 +1,54 @@
+package cn.tr.module.export.converter;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.lang.Pair;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.tr.module.export.handler.AbstractCascadeSelectConverter;
+import cn.tr.module.api.sys.dict.SysDictApi;
+
+import java.util.*;
+
+/**
+ * @ClassName : DictComboConverter
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年06月01日
+ */
+
+public class DictCascadeSelectConverter extends AbstractCascadeSelectConverter<String> {
+    private final SysDictApi dictApi;
+
+    public DictCascadeSelectConverter() {
+        this.dictApi = SpringUtil.getBean(SysDictApi.class);
+    }
+
+    @Override
+    public Map<String, List<String>> buildCascadeSelectList(Collection<String> params) {
+        String dictCode = CollectionUtil.getFirst(params);
+        Map<String, List<String>> result = new HashMap<>();
+        List<Map<String, Map<String, List<String>>>> dicts = dictApi.findChildrenDictsByCode(dictCode);
+        if(CollectionUtil.isNotEmpty(dicts)){
+            for (Map<String, Map<String, List<String>>> dict : dicts) {
+                for (Map<String, List<String>> value : dict.values()) {
+                    value.forEach((k,v)->{
+                        if(CollectionUtil.isNotEmpty(value)){
+                            result.put(k,v);
+                        }
+                    });
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public  List<Pair<String, String>>  doGetSelectPairs(Collection<String> params) {
+        String dictCode = CollectionUtil.getFirst(params);
+        return dictApi.findAllChildrenDictsByCode(dictCode);
+    }
+
+    @Override
+    public Class<String> doSupportJavaTypeKey() {
+        return String.class;
+    }
+}

+ 49 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/converter/SysUserCascadeSelectConverter.java

@@ -0,0 +1,49 @@
+package cn.tr.module.export.converter;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.lang.Pair;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.tr.module.api.sys.user.SysUserApi;
+import cn.tr.module.export.handler.AbstractCascadeSelectConverter;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @ClassName : DictComboConverter
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年06月01日
+ */
+
+public class SysUserCascadeSelectConverter extends AbstractCascadeSelectConverter<String> {
+    private final SysUserApi sysUserApi;
+
+    public SysUserCascadeSelectConverter() {
+        this.sysUserApi = SpringUtil.getBean(SysUserApi.class);
+    }
+
+    @Override
+    public Map<String, List<String>> buildCascadeSelectList(Collection<String> params) {
+        Map<String, List<String>> result = new HashMap<>();
+        List<Pair<String, String>> userIdAndNickName = sysUserApi.selectAllUserIdAndNickName();
+        if(CollectionUtil.isNotEmpty(userIdAndNickName)){
+            for (Pair<String, String> pair : userIdAndNickName) {
+                result.put(pair.getValue(),null);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public  List<Pair<String, String>>  doGetSelectPairs(Collection<String> params) {
+        return sysUserApi.selectAllUserIdAndNickName();
+    }
+
+    @Override
+    public Class<String> doSupportJavaTypeKey() {
+        return String.class;
+    }
+}

+ 19 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/enums/ExportType.java

@@ -0,0 +1,19 @@
+package cn.tr.module.export.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @Enum : ExportType
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年09月25日
+ */
+@AllArgsConstructor
+@Getter
+public enum ExportType {
+    DOCX(".docx"),
+    EXCEL(".xlsx");
+    @Getter
+    private String suffix;
+}

+ 182 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/AbstractCascadeSelectConverter.java

@@ -0,0 +1,182 @@
+package cn.tr.module.export.handler;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Pair;
+import cn.hutool.core.thread.ThreadUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.module.export.annotation.ExcelPropertySupport;
+import cn.tr.module.export.annotation.ExcelSelect;
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.Cell;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.data.WriteCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+import java.util.*;
+
+/**
+ * @ClassName : AbstractCascadeSelectConverter
+ * @Description : 级联选择框构建
+ * @Author : LF
+ * @Date: 2023年06月01日
+ */
+
+public abstract class AbstractCascadeSelectConverter<T> implements Converter<T> {
+    public static final ThreadLocal<Map<String,List<Pair<String, String>>>> threadLocal = ThreadUtil.createThreadLocal(false);
+    @Override
+    public Class<T> supportJavaTypeKey() {
+        return doSupportJavaTypeKey();
+    }
+
+    @Override
+    public T convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        if(cellData==null||(StrUtil.isEmpty(cellData.getStringValue())&&ObjectUtil.isNull(cellData.getNumberValue()))){
+            return null;
+        }
+        CellDataTypeEnum type = cellData.getType();
+        ExcelPropertySupport excelPropertySupport = contentProperty.getField().getAnnotation(ExcelPropertySupport.class);
+        String content = null;
+        if(CellDataTypeEnum.NUMBER.equals(type)){
+            content=String.valueOf(cellData.getNumberValue());
+        }else{
+            content=cellData.getStringValue();
+        }
+        //校验是否存在
+        T javaValue = excelConverterJavaValue(content,getSelectPairs(getParams(excelPropertySupport)));
+        if(ObjectUtil.isNull(javaValue)&&ObjectUtil.isNotNull(excelPropertySupport)){
+            ExcelSelect select = excelPropertySupport.select();
+            if(select.allowCustom()){
+                return (T) content;
+            }
+            throw new UnsupportedOperationException("所填值{"+content+"}不合法,请重新下载模板导入");
+        }
+        return javaValue;
+    }
+
+
+    @Override
+    public WriteCellData<?> convertToExcelData(T value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        WriteCellData<String> cellData = new WriteCellData<>("");
+        //java 转execl,java中填写的是key值,填写value(key)放入execl中
+        ExcelPropertySupport excelPropertySupport = contentProperty.getField().getAnnotation(ExcelPropertySupport.class);
+        if(excelPropertySupport!=null){
+            cellData.setStringValue(javaConverterExcelCellValue(value,getSelectPairs(getParams(excelPropertySupport)),excelPropertySupport.select()));
+        }
+        return cellData;
+    }
+
+    private Collection<String> getParams(ExcelPropertySupport propertySupport){
+        if(propertySupport!=null){
+            return Arrays.asList(propertySupport.select().param());
+        }
+        return null;
+    }
+
+    /**
+     * 根据所给的值将java的值转变为execl所展示的值
+     * @param content execl内容
+     * @param selectValues 选择框内容
+     * @return
+     */
+    public T excelConverterJavaValue(String content,List<Pair<String, String>> selectValues){
+        if(doSupportJavaTypeKey().equals(Boolean.class)){
+            if(StrUtil.equals(content,"1")||StrUtil.equals(content,"true")){
+                return Convert.convert(doSupportJavaTypeKey(),Boolean.TRUE);
+            }else {
+                return Convert.convert(doSupportJavaTypeKey(),Boolean.FALSE);
+            }
+        }
+        if (CollectionUtil.isEmpty(selectValues)) {
+            return null;
+        }
+        for (Pair<String, String> pair : selectValues) {
+            if (ObjectUtil.equals(content, pair.getValue())) {
+                return (T) pair.getKey();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 构建菜单获取级联列表
+     * @param params 查询列表参数集合
+     * @return List<Pair<String,List<String>>> 级联列表
+     * <pre>
+     * [{"河南省",["郑州市","新乡市","平顶山市"]},
+     * {"河北省",["石家庄市","邯郸市"]}]
+     *
+     * Pair中的value值为当前字段所需要的列表,若不存在上级级联,返回一个Pair即可,Key为''或null
+     * [{"",["是","否"]}]
+     * <pre/>
+     */
+    public Map<String, List<String>>  buildCascadeSelectList(Collection<String> params){
+        HashMap<String, List<String>> result = new HashMap<>();
+        List<Pair<String, String>> selectPairs = getSelectPairs(params);
+        for (Pair<String, String> selectPair : selectPairs) {
+            result.put(selectPair.getValue(),null);
+        }
+        return result;
+    };
+
+
+    /**
+     * 获取选择框中的键值对
+     * @param params
+     * @return  在excel转java 和 java转excel时使用
+     */
+    public List<Pair<String, String>>  getSelectPairs(Collection<String> params){
+        String key=this.getClass().getSimpleName();
+        if(CollectionUtil.isNotEmpty(params)){
+            key=key+"-"+CollectionUtil.join(params,"-");
+        }
+        Map<String,List<Pair<String, String>>> selectPairsMap = threadLocal.get();
+        if(selectPairsMap==null){
+            selectPairsMap=new WeakHashMap<>();
+        }
+        List<Pair<String, String>> result=selectPairsMap.get(key);
+        if(ObjectUtil.isNull(result)){
+            result=doGetSelectPairs(params);
+        }
+        selectPairsMap.put(key,result);
+        threadLocal.set(selectPairsMap);
+        return result;
+    };
+
+
+    /**
+     * 获取选择框中的键值对
+     * @param params
+     * @return
+     * <pre>
+     *     java : excel
+     *     key:  value
+     *     <pre/>
+     */
+    public abstract List<Pair<String, String>> doGetSelectPairs(Collection<String> params);
+
+    /**
+     * 根据所给的值将java的值转变为execl所展示的值
+     * @param value  java所给的value值
+     * @param selectValues 选择框键值对
+     * @return
+     */
+    public String javaConverterExcelCellValue(T value,List<Pair<String, String>> selectValues,ExcelSelect excelSelect){
+        if(ObjectUtil.isNull(value)||CollectionUtil.isEmpty(selectValues)){
+            return "";
+        }
+        for (Pair<String, String> pair : selectValues) {
+            if (ObjectUtil.equals(value, pair.getKey())) {
+                return pair.getValue();
+            }
+        }
+        if(excelSelect!=null&&excelSelect.allowCustom()){
+            return String.valueOf(value);
+        }
+        return null;
+    };
+
+    public abstract Class<T> doSupportJavaTypeKey();
+}

+ 65 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/AbstractDataHandler.java

@@ -0,0 +1,65 @@
+package cn.tr.module.export.handler;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.tr.core.utils.ValidationUtils;
+import cn.tr.module.register.ExportSampleRegister;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import lombok.Getter;
+import org.springframework.util.ReflectionUtils;
+
+import java.util.*;
+import java.util.Map;
+
+/**
+ * @ClassName : AbstractDataHandler
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年05月23日
+ */
+
+public abstract class AbstractDataHandler<T> implements DataHandler<T> {
+    public static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+    @Getter
+    private Class<T> aClass;
+
+    public AbstractDataHandler(Class<T> aClass) {
+        this.aClass = aClass;
+        List<T> sample = getSample();
+        if(CollectionUtil.isEmpty(sample)){
+            sample=new ArrayList<>();
+        }else {
+            sample=new ArrayList<>(sample);
+        }
+        sample.add(buildDefaultOb());
+        ExportSampleRegister.registerSample(aClass,sample);
+    }
+
+    private T buildDefaultOb(){
+        T t = ReflectUtil.newInstance(aClass);
+        ReflectionUtils.doWithFields(aClass,field -> {
+            if(String.class.equals(field.getType())){
+                field.setAccessible(true);
+                field.set(t,"……");
+            }
+        });
+        return t;
+    }
+    /**
+     * 模板中的样例数组
+     * @return
+     */
+    public List<T> getSample(){
+        return null;
+    }
+
+    @Override
+    public void handle(T obj, Map<String,Object> customObj) {
+        ValidationUtils.validate(validator,obj);
+        doHandle(obj);
+    }
+
+    public abstract void doHandle(T obj);
+}

+ 20 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/DataHandler.java

@@ -0,0 +1,20 @@
+package cn.tr.module.export.handler;
+
+import java.util.Map;
+
+/**
+ * @ClassName : DataHandler
+ * @Description : 导入数据的处理器
+ * @Author : LF
+ * @Date: 2023年05月23日
+ */
+
+public interface DataHandler<T> {
+
+    /**
+     * 对导入对象进行处理
+     * @param obj       解析对象
+     * @param customObj 自定义参数
+     */
+    void handle(T obj, Map<String,Object> customObj);
+}

+ 27 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/export/handler/NoneCascadeSelectConverter.java

@@ -0,0 +1,27 @@
+package cn.tr.module.export.handler;
+
+
+import cn.hutool.core.lang.Pair;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @ClassName : NoneComboConverter
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年06月01日
+ */
+
+public class NoneCascadeSelectConverter extends AbstractCascadeSelectConverter<Void> {
+
+    @Override
+    public List<Pair<String, String>> doGetSelectPairs(Collection<String> params) {
+        return null;
+    }
+
+    @Override
+    public Class<Void> doSupportJavaTypeKey() {
+        return Void.class;
+    }
+}

+ 21 - 0
tr-modules-api/tr-module-export-api/src/main/java/cn/tr/module/register/ExportSampleRegister.java

@@ -0,0 +1,21 @@
+package cn.tr.module.register;
+import java.util.*;
+/**
+ * @ClassName : ExportSampleRegister
+ * @Description; 导出的样例注册
+ * @Author : LF
+ * @Date: 2023年09月20日
+ */
+
+public class ExportSampleRegister {
+
+    private static Map<Class<?>,Collection<?>> sampleMap=new HashMap<>();
+
+    public static <T>  void registerSample(Class<T>  tClass,Collection<T>  sample){
+        sampleMap.put(tClass,sample);
+    }
+
+    public static <T> Collection<T> getSample(Class<T> tClass){
+        return (Collection<T>) sampleMap.get(tClass);
+    }
+}

+ 14 - 0
tr-modules-api/tr-module-quartz-api/pom.xml

@@ -0,0 +1,14 @@
+<?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">
+    <parent>
+        <artifactId>tr-modules-api</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-module-quartz-api</artifactId>
+
+</project>

+ 22 - 0
tr-modules-api/tr-module-quartz-api/src/main/java/cn/tr/module/api/quartz/JointJobQuartzApi.java

@@ -0,0 +1,22 @@
+package cn.tr.module.api.quartz;
+
+import cn.tr.module.api.quartz.dto.JointJobQuartzDTO;
+
+import java.util.Collection;
+
+/**
+ * @ClassName : JointJobQuartzApi
+ * @Description : 数据对接所使用的定时任务的api
+ * @Author : LF
+ * @Date: 2024年05月08日
+ */
+
+public interface JointJobQuartzApi {
+    void addOrUpdateJointJob(JointJobQuartzDTO source) throws Exception;
+
+    void startJointJob(String jointJobId)throws Exception;
+
+    void pauseJointJob(String jointJobId) throws Exception;
+
+    void delJointJobs(Collection<String> jointJobIds) throws Exception;
+}

+ 34 - 0
tr-modules-api/tr-module-quartz-api/src/main/java/cn/tr/module/api/quartz/dto/JointJobQuartzDTO.java

@@ -0,0 +1,34 @@
+package cn.tr.module.api.quartz.dto;
+
+/**
+ * @ClassName : JointJobQuartzDTO
+ * @Description :
+ * @Author : LF
+ * @Date: 2024年05月08日
+ */
+
+public class JointJobQuartzDTO {
+    private String jointJobId;
+    private String cron;
+
+    public JointJobQuartzDTO(String jointJobId, String cron) {
+        this.jointJobId = jointJobId;
+        this.cron = cron;
+    }
+
+    public String getJointJobId() {
+        return jointJobId;
+    }
+
+    public void setJointJobId(String jointJobId) {
+        this.jointJobId = jointJobId;
+    }
+
+    public String getCron() {
+        return cron;
+    }
+
+    public void setCron(String cron) {
+        this.cron = cron;
+    }
+}

+ 21 - 0
tr-modules-api/tr-module-system-api/pom.xml

@@ -0,0 +1,21 @@
+<?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">
+    <parent>
+        <artifactId>tr-modules-api</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-module-system-api</artifactId>
+    <version>${revision}</version>
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 41 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/dict/SysDictApi.java

@@ -0,0 +1,41 @@
+package cn.tr.module.api.sys.dict;
+
+import cn.hutool.core.lang.Pair;
+
+import java.util.*;
+/**
+ * @ClassName : SysDictApi
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年05月19日
+ */
+
+public interface SysDictApi {
+    /**
+     * 查找字典值
+     * @param parentCode 父级编码(值域)
+     * @param dictCode 字典编码
+     * @return
+     */
+    @Deprecated
+    String findDictItem(String parentCode,String dictCode);
+
+    /**
+     * 查找字典值
+     * @param parentCode 父级编码(值域)
+     * @param dictCode 字典编码
+     * @return
+     */
+    String findDictItem(String dictCode);
+
+    /**
+     * 找到字典码下的所有字典值
+     * @param dictCode
+     * @return
+     */
+    List<Pair<String,String>> findAllChildrenDictsByCode(String dictCode);
+
+    List<Map<String, Map<String,List<String>>>> findChildrenDictsByCode(String dictCode);
+
+    Map<String, String> findAllChildrenDictsMapByCode(String dictCode);
+}

+ 45 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/log/annotation/OperateLog.java

@@ -0,0 +1,45 @@
+package cn.tr.module.api.sys.log.annotation;
+
+import cn.tr.module.api.sys.log.enums.LoginType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 操作日志注解
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OperateLog {
+
+    // ========== 模块字段 ==========
+
+    /**
+     * 操作名
+     *
+     * 为空时,会尝试读取 {@link io.swagger.annotations.ApiOperation#value()} ()} 属性
+     */
+    String name() default "";
+
+    LoginType loginType() default  LoginType.oper;
+
+    // ========== 开关字段 ==========
+
+    /**
+     * 是否记录操作日志
+     */
+    boolean enable() default true;
+    /**
+     * 是否记录方法参数
+     */
+    boolean logArgs() default true;
+    /**
+     * 是否记录方法结果的数据
+     */
+    boolean logResultData() default true;
+
+}

+ 64 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/log/bo/OperateLogBO.java

@@ -0,0 +1,64 @@
+package cn.tr.module.api.sys.log.bo;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 操作日志
+ *
+ * @author 芋道源码
+ */
+@Data
+public class OperateLogBO {
+    /**
+     * 操作名称
+     */
+    private String opName;
+    /**
+     * 用户IP
+     */
+    private String userIp;
+    /**
+     * 用户地址
+     */
+    private String loginLocation;
+    /**
+     * 类名称
+     */
+    private String javaClass;
+    /**
+     * 方法名称
+     */
+    private String javaMethod;
+    /**
+     * 操作时间
+     */
+    private Date opTime;
+    /**
+     * 执行时长
+     */
+    private Integer duration;
+    /**
+     * 操作用户
+     */
+    private String opUser;
+    /**
+     * 请求参数
+     */
+    private String requestArgs;
+    /**
+     * 浏览器UserAgent
+     */
+    private String userAgent;
+    /**
+     * 响应结果
+     */
+    private String resultJson;
+
+    /**
+     * 日志类型
+     * {@link cn.tr.module.api.sys.log.enums.LoginType}
+     */
+    private String type;
+}

+ 30 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/log/enums/LoginType.java

@@ -0,0 +1,30 @@
+package cn.tr.module.api.sys.log.enums;
+
+import lombok.Getter;
+
+/**
+ * 操作日志的操作类型
+ *
+ * @author ruoyi
+ */
+@Getter
+public enum LoginType {
+
+    /**
+     * 操作日志
+     */
+    oper,
+    /**
+     * 异常日志
+     */
+    exception,
+    /**
+     * 登录日志
+     */
+    login,
+    /**
+     * 登出日志
+     */
+    logout;
+
+}

+ 14 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/notice/SysNoticeApi.java

@@ -0,0 +1,14 @@
+package cn.tr.module.api.sys.notice;
+
+
+import java.util.*;
+/**
+ * @ClassName : SysNoticeApi
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年11月06日
+ */
+
+public interface SysNoticeApi {
+    List<SysNoticeBO> selectList();
+}

+ 45 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/notice/SysNoticeBO.java

@@ -0,0 +1,45 @@
+package cn.tr.module.api.sys.notice;
+
+import lombok.Data;
+import lombok.ToString;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 系统公告传输对象
+ *
+ * @author lf
+ * @date  2023/11/06 10:12
+ **/
+@Data
+@ToString
+public class SysNoticeBO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private String id;
+
+    /**
+     * 标题
+     */
+    private String title;
+
+    /**
+     * 类型
+     */
+    private String type;
+
+    /**
+     * 内容
+     */
+    private String content;
+
+
+    /**
+     * 发布人昵称
+     */
+    private String updateNickName;
+
+    /**
+     * 发布时间
+     */
+    private Date updateTime;
+}

+ 41 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/storage/StoragePojo.java

@@ -0,0 +1,41 @@
+package cn.tr.module.api.sys.storage;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @ClassName : StoragePojo
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年07月19日
+ */
+@Data
+public class StoragePojo implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /**
+     * 存储记录id
+     */
+    private String id;
+
+    /**
+     * 文件名称
+     */
+    private String realName;
+
+    /**
+     * 绝对路径
+     */
+    private String absolutePath;
+
+
+    /**
+     * 文件大小(KB)
+     */
+    private Integer size;
+
+    /**
+     * 文件后缀
+     */
+    private String suffix;
+}

+ 53 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/storage/SysStorageApi.java

@@ -0,0 +1,53 @@
+package cn.tr.module.api.sys.storage;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @ClassName : SysStorageApi
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年05月18日
+ */
+
+public interface SysStorageApi {
+    /**
+     * 上传文件
+     * @param content 文件内容
+     * @param filename 上传文件名称
+     * @return  文件id
+     */
+    default String upload(byte[] content,String filename) throws Exception{
+        return upload(content, filename, null);
+    };
+
+    /**
+     * 上传文件
+     * @param content 文件内容
+     * @param filename 上传文件名称
+     * @param millis 过期时间(毫秒)
+     * @return  文件id
+     */
+    String upload(byte[] content, String filename,Long millis) throws Exception;
+
+    /**
+     * 删除文件
+     * @param fileId
+     */
+    void removeById(String fileId);
+
+    /**
+     * 获取文件
+     * @param recordId 文件记录id
+     * @return
+     */
+    byte[] obtainContent(String recordId) throws Exception;
+
+    /**
+     * 批量查询存储文件信息
+     * @param storageIds 存储文件id集合
+     * @return
+     */
+    Map<String,StoragePojo> findStorage(Collection<String> storageIds);
+}

+ 13 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/tenant/SysTenantApi.java

@@ -0,0 +1,13 @@
+package cn.tr.module.api.sys.tenant;
+
+import java.util.*;
+/**
+ * @ClassName : SysTenantApi
+ * @Description :
+ * @Author : LF
+ * @Date: 2024年03月11日
+ */
+
+public interface SysTenantApi {
+    List<SysTenantPojo> selectAll();
+}

+ 36 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/tenant/SysTenantPojo.java

@@ -0,0 +1,36 @@
+package cn.tr.module.api.sys.tenant;
+
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @ClassName : SysTenantPO
+ * @Description : 租户
+ * @Author : LF
+ * @Date: 2023年04月05日
+ */
+@Data
+public class SysTenantPojo implements Serializable {
+    private static final long serialVersionUID = -4624795887273192599L;
+    /**
+     * 租户id
+     */
+    private String tenantId;
+
+    /**
+     * 租户名称
+     */
+    private String name;
+
+    /**
+     * 租户类型
+     */
+    private String type;
+
+    /**
+     * 租户状态 0、开启 1、关闭
+     */
+    private Boolean disable;
+}

+ 21 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/user/SysUserApi.java

@@ -0,0 +1,21 @@
+package cn.tr.module.api.sys.user;
+
+import cn.hutool.core.lang.Pair;
+
+import java.util.*;
+/**
+ * @ClassName : SysUserApi
+ * @Description :
+ * @Author : LF
+ * @Date: 2023年09月28日
+ */
+
+public interface SysUserApi {
+    /**
+     * 查询所有用户的id和昵称
+     * @return
+     */
+    List<Pair<String,String >> selectAllUserIdAndNickName();
+
+    Collection<String> findUserPermission(String userId);
+}

+ 81 - 0
tr-modules-api/tr-module-system-api/src/main/java/cn/tr/module/api/sys/user/dto/SysQuickMenuDTO.java

@@ -0,0 +1,81 @@
+package cn.tr.module.api.sys.user.dto;
+
+import cn.tr.module.api.sys.storage.StoragePojo;
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 菜单对象 sys_menu
+ *
+ * @author tr
+ * @date 2022-11-23 15:34:37
+ */
+@Data
+@ToString
+public class SysQuickMenuDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+    * 主键
+    */
+    private String id;
+
+    /**
+     * 菜单名称
+     */
+    private String name;
+
+    /**
+     * 上级菜单路由地址
+     */
+    private String parentId;
+
+    /**
+     * 上级菜单路由地址
+     */
+    private String parentRoutePath;
+
+    /**
+     * 路由地址
+     */
+    private String routePath;
+
+    /**
+     * 组件路径
+     */
+    private String component;
+
+    /**
+     * 是否外链 0否;1是
+     */
+    private Boolean linkExternal;
+
+    /**
+     * 外部链接
+     */
+    private String linkUrl;
+
+    /**
+     * 快捷入口图标
+     */
+    private String quickMenuIcon;
+
+    /**
+     * 快捷入口排序
+     */
+    private Integer quickMenuSort;
+
+    /**
+     * 快捷入口图标文件
+     */
+    private StoragePojo iconFile;
+
+    /**
+     * 快捷入口是否已选中
+     */
+    private boolean quickSelected;
+}

+ 24 - 0
tr-modules/pom.xml

@@ -0,0 +1,24 @@
+<?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">
+    <parent>
+        <artifactId>phototherapy</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-modules</artifactId>
+    <packaging>pom</packaging>
+    <version>${revision}</version>
+    <modules>
+        <module>tr-module-system</module>
+        <module>tr-module-export</module>
+<!--        <module>tr-module-ai</module>-->
+<!--        <module>tr-module-jgliu</module>-->
+<!--        <module>tr-module-platform</module>-->
+    </modules>
+
+
+</project>

+ 66 - 0
tr-modules/tr-module-ai/pom.xml

@@ -0,0 +1,66 @@
+<?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">
+    <parent>
+        <groupId>cn.tr</groupId>
+        <artifactId>tr-modules</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>tr-module-ai</artifactId>
+    <version>${revision}</version>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-satoken</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-sse</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-classic</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-reactor</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-spring-boot-starter</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-classic</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-web</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 11 - 0
tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/AiSpringBootApplication.java

@@ -0,0 +1,11 @@
+package cn.tr.module.ai;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AiSpringBootApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(AiSpringBootApplication.class);
+    }
+}

+ 26 - 0
tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/controller/ChatController.java

@@ -0,0 +1,26 @@
+package cn.tr.module.ai.controller;
+
+import cn.tr.module.ai.service.Assistant;
+import cn.tr.plugin.sse.ReactiveStreamConverter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import reactor.core.publisher.Flux;
+
+
+@RestController
+public class ChatController {
+
+    @Autowired
+    Assistant assistant;
+
+    @GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public SseEmitter model(@RequestParam(value = "message", defaultValue = "能为我介绍一下驼人医疗器械集团吗?不少于200字") String message) {
+        Flux<String> flux = assistant.chat(message);
+        return ReactiveStreamConverter.toSseEmitter(flux,
+            data -> System.out.println(data)); // 数据处理回调
+    }
+}

+ 16 - 0
tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/service/Assistant.java

@@ -0,0 +1,16 @@
+package cn.tr.module.ai.service;
+
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.spring.AiService;
+import dev.langchain4j.service.spring.AiServiceWiringMode;
+import reactor.core.publisher.Flux;
+
+@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
+           streamingChatModel = "openAiStreamingChatModel")
+public interface Assistant {
+
+    @SystemMessage("你是驼人的专用医疗专家")
+    @UserMessage("{{it}}")
+    Flux<String> chat(String userMessage);
+}

+ 43 - 0
tr-modules/tr-module-ai/src/main/java/cn/tr/module/ai/service/BookingTools.java

@@ -0,0 +1,43 @@
+package cn.tr.module.ai.service;
+
+import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
+import dev.langchain4j.model.chat.listener.ChatModelListener;
+import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
+import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class BookingTools {
+    @Tool
+    public String getAppName() {
+        return "测试APP";
+    }
+
+    @Bean
+    ChatModelListener chatModelListener() {
+
+        return new ChatModelListener() {
+
+
+            @Override
+            public void onRequest(ChatModelRequestContext requestContext) {
+                log.info("onRequest(): {}", requestContext.chatRequest());
+            }
+
+            @Override
+            public void onResponse(ChatModelResponseContext responseContext) {
+                log.info("onResponse(): {}", responseContext.chatResponse());
+            }
+
+            @Override
+            public void onError(ChatModelErrorContext errorContext) {
+                log.info("onError(): {}", errorContext.error().getMessage());
+            }
+        };
+    }
+
+}

+ 22 - 0
tr-modules/tr-module-ai/src/main/resources/application.yml

@@ -0,0 +1,22 @@
+server:
+  shutdown: graceful
+  port: 8083
+  servlet:
+    context-path: /api
+langchain4j:
+  open-ai:
+    chat-model:
+      api-key: "sk-9dd052fd430d4fc89c790e6f76df6340"  # 替换为您的DeepSeek API密钥
+      model-name: "deepseek-chat"       # 使用DeepSeek模型名称
+      base-url: "https://api.deepseek.com/v1"  # 添加DeepSeek API的基础URL
+      log-requests: true
+      log-responses: true
+    streaming-chat-model:
+      api-key: "sk-9dd052fd430d4fc89c790e6f76df6340"  # 替换为您的DeepSeek API密钥
+      model-name: "deepseek-chat"       # 使用DeepSeek模型名称
+      base-url: "https://api.deepseek.com/v1"  # 添加DeepSeek API的基础URL
+      log-requests: true
+      log-responses: true
+tr:
+  satoken:
+    enable: true

+ 68 - 0
tr-modules/tr-module-export/pom.xml

@@ -0,0 +1,68 @@
+<?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">
+    <parent>
+        <artifactId>tr-modules</artifactId>
+        <groupId>cn.tr</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-module-export</artifactId>
+    <version>${revision}</version>
+
+    <description>文件导出中心</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-framework</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-biz-tenant</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-satoken</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-spring-boot-starter-plugin-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-module-export-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.tr</groupId>
+            <artifactId>tr-module-system-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-poi</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+        </dependency>
+    </dependencies>
+</project>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio