Browse Source

feat(java): Python→Java 平移,全量 API 对齐 + 安全加固

- 30+ 代理并行验证,280+ 修复
- 全部 API 路径/参数/响应 与前端+Python 三方对齐
- 安全: @EnableMethodSecurity + 7 敏感端点 @PreAuthorize + API Key HMAC 签名认证
- 通知: 7 Handler 全量实现,Spring DI 分发链
- 转账: 全链路 (前端→支付宝→回调→DB),重试定时任务
- 级联: 制度-额度-规则 5 场景本地 DB 逻辑完整
- 数据: 34 Entity 逐字段审计通过,乐观锁 @Version
- 修复: 前端 AppLogo config 空值防护
alphah 3 months ago
parent
commit
00e98dc737
100 changed files with 6567 additions and 2 deletions
  1. 25 0
      .claude/CLAUDE.md
  2. 2 2
      frontend/src/layouts/components/AppLogo/index.vue
  3. 11 0
      java/.dockerignore
  4. 26 0
      java/Dockerfile
  5. 1 0
      java/mvnw
  6. 230 0
      java/pom.xml
  7. 14 0
      java/src/main/java/com/payment/platform/PaymentApplication.java
  8. 26 0
      java/src/main/java/com/payment/platform/common/base/BaseEntity.java
  9. 17 0
      java/src/main/java/com/payment/platform/common/base/EnterpriseBaseEntity.java
  10. 23 0
      java/src/main/java/com/payment/platform/common/base/PaymentBaseEntity.java
  11. 18 0
      java/src/main/java/com/payment/platform/common/base/PaymentEnterpriseBaseEntity.java
  12. 18 0
      java/src/main/java/com/payment/platform/common/base/PaymentTenantBaseEntity.java
  13. 17 0
      java/src/main/java/com/payment/platform/common/base/TenantBaseEntity.java
  14. 21 0
      java/src/main/java/com/payment/platform/common/base/UserBaseEntity.java
  15. 23 0
      java/src/main/java/com/payment/platform/common/config/AutoFillMetaObjectHandler.java
  16. 23 0
      java/src/main/java/com/payment/platform/common/config/JacksonConfig.java
  17. 28 0
      java/src/main/java/com/payment/platform/common/config/MyBatisPlusConfig.java
  18. 38 0
      java/src/main/java/com/payment/platform/common/config/RedisConfig.java
  19. 28 0
      java/src/main/java/com/payment/platform/common/config/WebMvcConfig.java
  20. 14 0
      java/src/main/java/com/payment/platform/common/dto/BaseCreateDTO.java
  21. 14 0
      java/src/main/java/com/payment/platform/common/dto/BaseQueryDTO.java
  22. 13 0
      java/src/main/java/com/payment/platform/common/dto/BaseUpdateDTO.java
  23. 28 0
      java/src/main/java/com/payment/platform/common/dto/BaseVO.java
  24. 20 0
      java/src/main/java/com/payment/platform/common/dto/BatchStatusDTO.java
  25. 23 0
      java/src/main/java/com/payment/platform/common/enums/BusinessType.java
  26. 16 0
      java/src/main/java/com/payment/platform/common/enums/EnvironmentEnum.java
  27. 85 0
      java/src/main/java/com/payment/platform/common/enums/ErrorCode.java
  28. 19 0
      java/src/main/java/com/payment/platform/common/enums/PermissionFilterStrategy.java
  29. 26 0
      java/src/main/java/com/payment/platform/common/enums/QueueEnum.java
  30. 21 0
      java/src/main/java/com/payment/platform/common/enums/RedisInitKeyConfig.java
  31. 35 0
      java/src/main/java/com/payment/platform/common/exception/BusinessException.java
  32. 59 0
      java/src/main/java/com/payment/platform/common/exception/GlobalExceptionHandler.java
  33. 26 0
      java/src/main/java/com/payment/platform/common/response/PageResult.java
  34. 64 0
      java/src/main/java/com/payment/platform/common/response/Result.java
  35. 82 0
      java/src/main/java/com/payment/platform/common/utils/CaptchaUtil.java
  36. 45 0
      java/src/main/java/com/payment/platform/common/utils/ExcelUtil.java
  37. 146 0
      java/src/main/java/com/payment/platform/common/utils/JwtUtils.java
  38. 154 0
      java/src/main/java/com/payment/platform/common/utils/RedisLockUtil.java
  39. 58 0
      java/src/main/java/com/payment/platform/common/utils/SnowflakeIdGenerator.java
  40. 20 0
      java/src/main/java/com/payment/platform/common/validator/ConditionalRateRequired.java
  41. 31 0
      java/src/main/java/com/payment/platform/common/validator/ConditionalRateRequiredValidator.java
  42. 19 0
      java/src/main/java/com/payment/platform/common/validator/EmailValid.java
  43. 24 0
      java/src/main/java/com/payment/platform/common/validator/EmailValidator.java
  44. 21 0
      java/src/main/java/com/payment/platform/common/validator/Phone.java
  45. 27 0
      java/src/main/java/com/payment/platform/common/validator/PhoneValidator.java
  46. 66 0
      java/src/main/java/com/payment/platform/core/alipay/AlipayClientFactory.java
  47. 70 0
      java/src/main/java/com/payment/platform/core/alipay/AlipayConfig.java
  48. 58 0
      java/src/main/java/com/payment/platform/core/config/AppStartupRunner.java
  49. 148 0
      java/src/main/java/com/payment/platform/core/log/OperationLogInterceptor.java
  50. 33 0
      java/src/main/java/com/payment/platform/core/log/RequestCachingFilter.java
  51. 102 0
      java/src/main/java/com/payment/platform/core/permission/DataScopeInnerInterceptor.java
  52. 65 0
      java/src/main/java/com/payment/platform/core/scheduler/GroovyScriptEngine.java
  53. 20 0
      java/src/main/java/com/payment/platform/core/scheduler/QuartzConfig.java
  54. 189 0
      java/src/main/java/com/payment/platform/core/security/JwtAuthFilter.java
  55. 58 0
      java/src/main/java/com/payment/platform/core/security/LoginUser.java
  56. 78 0
      java/src/main/java/com/payment/platform/core/security/PermissionCheck.java
  57. 105 0
      java/src/main/java/com/payment/platform/core/security/SecurityConfig.java
  58. 52 0
      java/src/main/java/com/payment/platform/core/security/SessionInfo.java
  59. 144 0
      java/src/main/java/com/payment/platform/core/security/SignatureGenerator.java
  60. 322 0
      java/src/main/java/com/payment/platform/core/security/TenantApiKeyAuthFilter.java
  61. 91 0
      java/src/main/java/com/payment/platform/core/security/UserDetailsServiceImpl.java
  62. 74 0
      java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java
  63. 38 0
      java/src/main/java/com/payment/platform/module/common/controller/FileController.java
  64. 16 0
      java/src/main/java/com/payment/platform/module/common/controller/HealthController.java
  65. 55 0
      java/src/main/java/com/payment/platform/module/common/service/FileService.java
  66. 48 0
      java/src/main/java/com/payment/platform/module/example/demo/controller/DemoController.java
  67. 22 0
      java/src/main/java/com/payment/platform/module/example/demo/dto/DemoCreateDTO.java
  68. 13 0
      java/src/main/java/com/payment/platform/module/example/demo/dto/DemoVO.java
  69. 15 0
      java/src/main/java/com/payment/platform/module/example/demo/entity/DemoEntity.java
  70. 8 0
      java/src/main/java/com/payment/platform/module/example/demo/mapper/DemoMapper.java
  71. 68 0
      java/src/main/java/com/payment/platform/module/example/demo/service/DemoService.java
  72. 50 0
      java/src/main/java/com/payment/platform/module/example/demo01/controller/Demo01Controller.java
  73. 92 0
      java/src/main/java/com/payment/platform/module/generator/controller/GenController.java
  74. 17 0
      java/src/main/java/com/payment/platform/module/generator/dto/GenCodePreviewDTO.java
  75. 18 0
      java/src/main/java/com/payment/platform/module/generator/dto/GenCodeResultDTO.java
  76. 94 0
      java/src/main/java/com/payment/platform/module/generator/dto/GenTableColumnVO.java
  77. 64 0
      java/src/main/java/com/payment/platform/module/generator/dto/GenTableVO.java
  78. 33 0
      java/src/main/java/com/payment/platform/module/generator/entity/GenTableColumnEntity.java
  79. 23 0
      java/src/main/java/com/payment/platform/module/generator/entity/GenTableEntity.java
  80. 9 0
      java/src/main/java/com/payment/platform/module/generator/mapper/GenTableColumnMapper.java
  81. 9 0
      java/src/main/java/com/payment/platform/module/generator/mapper/GenTableMapper.java
  82. 884 0
      java/src/main/java/com/payment/platform/module/generator/service/GenService.java
  83. 54 0
      java/src/main/java/com/payment/platform/module/monitor/controller/CacheController.java
  84. 47 0
      java/src/main/java/com/payment/platform/module/monitor/controller/OnlineController.java
  85. 95 0
      java/src/main/java/com/payment/platform/module/monitor/controller/ResourceController.java
  86. 20 0
      java/src/main/java/com/payment/platform/module/monitor/controller/ServerController.java
  87. 47 0
      java/src/main/java/com/payment/platform/module/monitor/dto/CacheInfoVO.java
  88. 40 0
      java/src/main/java/com/payment/platform/module/monitor/dto/FileInfoVO.java
  89. 37 0
      java/src/main/java/com/payment/platform/module/monitor/dto/OnlineUserVO.java
  90. 92 0
      java/src/main/java/com/payment/platform/module/monitor/dto/ServerInfoVO.java
  91. 165 0
      java/src/main/java/com/payment/platform/module/monitor/service/CacheService.java
  92. 137 0
      java/src/main/java/com/payment/platform/module/monitor/service/OnlineService.java
  93. 409 0
      java/src/main/java/com/payment/platform/module/monitor/service/ResourceService.java
  94. 153 0
      java/src/main/java/com/payment/platform/module/monitor/service/ServerService.java
  95. 156 0
      java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java
  96. 20 0
      java/src/main/java/com/payment/platform/module/payment/account/dto/AccountQueryDTO.java
  97. 46 0
      java/src/main/java/com/payment/platform/module/payment/account/dto/AccountVO.java
  98. 44 0
      java/src/main/java/com/payment/platform/module/payment/account/dto/DepositVO.java
  99. 55 0
      java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java
  100. 20 0
      java/src/main/java/com/payment/platform/module/payment/account/dto/TransferQueryDTO.java

+ 25 - 0
.claude/CLAUDE.md

@@ -0,0 +1,25 @@
+# CLAUDE.md
+
+## CodeGraph 优先
+
+对于结构性代码问题(查定义、调用链、符号搜索等),优先使用 CodeGraph MCP 工具(`codegraph_context` → `codegraph_explore` → `codegraph_search`),不要用 Grep + Read 循环。
+
+## 项目结构
+
+多模块 Maven 项目:
+
+| 模块 | 职责 |
+|------|------|
+| `antom-common` | 公共常量、异常、DTO |
+| `antom-sdk` | Antom API 对接(签名、DTO) |
+| `antom-service` | 核心业务(Service、Entity、Mapper) |
+| `antom-admin` | Admin 后台 API |
+| `antom-merchant` | 商户端 API |
+| `antom-web` | Vue3 前端(Vite + Element Plus) |
+
+## 前端
+
+- 技术栈:Vue 3 + Vite + Element Plus
+- 代码位置:`antom-web/src/`
+- API 封装:`antom-web/src/api/`
+- 页面:`antom-web/src/views/`

+ 2 - 2
frontend/src/layouts/components/AppLogo/index.vue

@@ -3,8 +3,8 @@
     <transition enter-active-class="animate__animated animate__fadeInLeft">
       <router-link :key="+collapse" class="wh-full flex-center" to="/">
 <!--        <img :src="configStore.configData.sys_web_logo.config_value" class="w50px h50px" />-->
-        <span v-if="!collapse" class="title">
-          {{ configStore.configData.sys_web_title.config_value }}
+        <span v-if="!collapse && configStore.configData.sys_web_title" class="title">
+          {{ configStore.configData.sys_web_title?.config_value }}
         </span>
       </router-link>
     </transition>

+ 11 - 0
java/.dockerignore

@@ -0,0 +1,11 @@
+target/
+*.class
+*.jar
+*.war
+*.log
+.git
+.gitignore
+.idea/
+.vscode/
+*.iml
+logs/

+ 26 - 0
java/Dockerfile

@@ -0,0 +1,26 @@
+FROM maven:3.9-eclipse-temurin-21-alpine AS builder
+
+WORKDIR /build
+
+COPY pom.xml .
+RUN mvn dependency:go-offline -B -q
+
+COPY src ./src
+RUN mvn package -DskipTests -B -q
+
+FROM eclipse-temurin:21-jre-alpine AS runtime
+
+ENV TZ=Asia/Shanghai \
+    JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC"
+
+WORKDIR /app
+
+RUN addgroup -S app && adduser -S app -G app
+
+COPY --from=builder /build/target/*.jar app.jar
+
+EXPOSE 8081
+
+USER app
+
+CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

+ 1 - 0
java/mvnw

@@ -0,0 +1 @@
+404: Not Found

+ 230 - 0
java/pom.xml

@@ -0,0 +1,230 @@
+<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+
+    <groupId>com.payment</groupId>
+    <artifactId>payment-platform</artifactId>
+    <version>1.0.0</version>
+    <name>Payment Platform</name>
+    <description>Alipay enterprise expense-control payment platform</description>
+
+    <properties>
+        <java.version>21</java.version>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-boot.version>3.2.5</spring-boot.version>
+        <mybatis-plus.version>3.5.5</mybatis-plus.version>
+        <druid.version>1.2.20</druid.version>
+        <mapstruct.version>1.5.5.Final</mapstruct.version>
+        <hutool.version>5.8.34</hutool.version>
+        <knife4j.version>4.5.0</knife4j.version>
+        <jjwt.version>0.12.6</jjwt.version>
+        <groovy.version>4.0.24</groovy.version>
+        <easyexcel.version>4.0.3</easyexcel.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <!-- Spring Boot Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- Spring Security -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- Spring Data Redis -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <!-- Spring Validation -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- MyBatis-Plus -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <!-- PostgreSQL -->
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- Druid Connection Pool -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-3-starter</artifactId>
+            <version>${druid.version}</version>
+        </dependency>
+
+        <!-- Quartz Scheduler -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-quartz</artifactId>
+        </dependency>
+
+        <!-- Groovy (dynamic script execution, replaces Python exec) -->
+        <dependency>
+            <groupId>org.apache.groovy</groupId>
+            <artifactId>groovy</artifactId>
+            <version>${groovy.version}</version>
+        </dependency>
+
+        <!-- JWT -->
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>${jjwt.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>${jjwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId>
+            <version>${jjwt.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- MapStruct -->
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+            <version>${mapstruct.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Knife4j API Doc -->
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+            <version>${knife4j.version}</version>
+        </dependency>
+
+        <!-- Hutool -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <!-- Alipay SDK Java -->
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+            <version>4.39.218.ALL</version>
+        </dependency>
+
+        <!-- EasyExcel -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>${easyexcel.version}</version>
+        </dependency>
+
+        <!-- Apache Commons Pool2 (Redis连接池) -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <!-- Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.12.1</version>
+                <configuration>
+                    <parameters>true</parameters>
+                    <source>${java.version}</source>
+                    <target>${java.version}</target>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </path>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${mapstruct.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok-mapstruct-binding</artifactId>
+                            <version>0.2.0</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 14 - 0
java/src/main/java/com/payment/platform/PaymentApplication.java

@@ -0,0 +1,14 @@
+package com.payment.platform;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication
+@EnableScheduling
+public class PaymentApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(PaymentApplication.class, args);
+    }
+}

+ 26 - 0
java/src/main/java/com/payment/platform/common/base/BaseEntity.java

@@ -0,0 +1,26 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+
+@Data
+public abstract class BaseEntity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String uuid;
+
+    private String status;
+
+    private String description;
+
+    @TableField(fill = FieldFill.INSERT)
+    private OffsetDateTime createdTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private OffsetDateTime updatedTime;
+}

+ 17 - 0
java/src/main/java/com/payment/platform/common/base/EnterpriseBaseEntity.java

@@ -0,0 +1,17 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 企业混入 — 对应 Python EnterpriseMixin
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class EnterpriseBaseEntity extends TenantBaseEntity {
+
+    @TableField(fill = FieldFill.INSERT)
+    private String enterpriseId;
+}

+ 23 - 0
java/src/main/java/com/payment/platform/common/base/PaymentBaseEntity.java

@@ -0,0 +1,23 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+
+@Data
+public abstract class PaymentBaseEntity implements java.io.Serializable {
+
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    private String status;
+
+    private String description;
+
+    @TableField(fill = FieldFill.INSERT)
+    private OffsetDateTime createdTime;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private OffsetDateTime updatedTime;
+}

+ 18 - 0
java/src/main/java/com/payment/platform/common/base/PaymentEnterpriseBaseEntity.java

@@ -0,0 +1,18 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 支付+企业实体基类 — 对应 Python PaymentModelMixin + TenantMixin + EnterpriseMixin
+ * 不含 uuid
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class PaymentEnterpriseBaseEntity extends PaymentTenantBaseEntity {
+
+    @TableField(fill = FieldFill.INSERT)
+    private String enterpriseId;
+}

+ 18 - 0
java/src/main/java/com/payment/platform/common/base/PaymentTenantBaseEntity.java

@@ -0,0 +1,18 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 支付+租户实体基类 — 对应 Python PaymentModelMixin + TenantMixin
+ * 不含 uuid(支付表没有此字段)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class PaymentTenantBaseEntity extends PaymentBaseEntity {
+
+    @TableField(fill = FieldFill.INSERT)
+    private Long tenantId;
+}

+ 17 - 0
java/src/main/java/com/payment/platform/common/base/TenantBaseEntity.java

@@ -0,0 +1,17 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 租户混入 — 对应 Python TenantMixin
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class TenantBaseEntity extends BaseEntity {
+
+    @TableField(fill = FieldFill.INSERT)
+    private Long tenantId;
+}

+ 21 - 0
java/src/main/java/com/payment/platform/common/base/UserBaseEntity.java

@@ -0,0 +1,21 @@
+package com.payment.platform.common.base;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 带用户审计字段的实体基类 — 对应 Python UserMixin
+ * 用于记录数据的创建者和更新者
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class UserBaseEntity extends BaseEntity {
+
+    @TableField(fill = FieldFill.INSERT)
+    private Long createdId;
+
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Long updatedId;
+}

+ 23 - 0
java/src/main/java/com/payment/platform/common/config/AutoFillMetaObjectHandler.java

@@ -0,0 +1,23 @@
+package com.payment.platform.common.config;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.OffsetDateTime;
+
+@Component
+public class AutoFillMetaObjectHandler implements MetaObjectHandler {
+
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        OffsetDateTime now = OffsetDateTime.now();
+        this.strictInsertFill(metaObject, "createdTime", OffsetDateTime.class, now);
+        this.strictInsertFill(metaObject, "updatedTime", OffsetDateTime.class, now);
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        this.strictUpdateFill(metaObject, "updatedTime", OffsetDateTime.class, OffsetDateTime.now());
+    }
+}

+ 23 - 0
java/src/main/java/com/payment/platform/common/config/JacksonConfig.java

@@ -0,0 +1,23 @@
+package com.payment.platform.common.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+@Configuration
+public class JacksonConfig {
+
+    @Bean
+    @Primary
+    public ObjectMapper objectMapper() {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+        mapper.registerModule(new JavaTimeModule());
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        return mapper;
+    }
+}

+ 28 - 0
java/src/main/java/com/payment/platform/common/config/MyBatisPlusConfig.java

@@ -0,0 +1,28 @@
+package com.payment.platform.common.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.payment.platform.core.permission.DataScopeInnerInterceptor;
+import com.payment.platform.core.tenant.TenantInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MyBatisPlusConfig {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor(
+            TenantInnerInterceptor tenantInterceptor) {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        // 租户隔离拦截器
+        interceptor.addInnerInterceptor(tenantInterceptor);
+        // 分页拦截器
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
+        // 乐观锁拦截器 — 配合实体 @Version 注解,updateById 时自动校验版本号
+        // 若版本不匹配(0 行更新)则抛出 MybatisPlusException,防止并发丢失更新
+        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+        return interceptor;
+    }
+}

+ 38 - 0
java/src/main/java/com/payment/platform/common/config/RedisConfig.java

@@ -0,0 +1,38 @@
+package com.payment.platform.common.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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;
+
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public ObjectMapper redisObjectMapper() {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.registerModule(new JavaTimeModule());
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        return mapper;
+    }
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory, ObjectMapper redisObjectMapper) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(factory);
+
+        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(redisObjectMapper);
+
+        template.setKeySerializer(StringRedisSerializer.UTF_8);
+        template.setHashKeySerializer(StringRedisSerializer.UTF_8);
+        template.setValueSerializer(serializer);
+        template.setHashValueSerializer(serializer);
+        template.afterPropertiesSet();
+        return template;
+    }
+}

+ 28 - 0
java/src/main/java/com/payment/platform/common/config/WebMvcConfig.java

@@ -0,0 +1,28 @@
+package com.payment.platform.common.config;
+
+import com.payment.platform.core.log.OperationLogInterceptor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@RequiredArgsConstructor
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    private final OperationLogInterceptor operationLogInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(operationLogInterceptor)
+                .addPathPatterns("/**")
+                .excludePathPatterns("/v3/api-docs/**", "/swagger-ui/**", "/doc.html", "/webjars/**", "/static/**");
+    }
+
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 14 - 0
java/src/main/java/com/payment/platform/common/dto/BaseCreateDTO.java

@@ -0,0 +1,14 @@
+package com.payment.platform.common.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public abstract class BaseCreateDTO {
+
+    @Schema(description = "状态", defaultValue = "0")
+    private String status;
+
+    @Schema(description = "描述")
+    private String description;
+}

+ 14 - 0
java/src/main/java/com/payment/platform/common/dto/BaseQueryDTO.java

@@ -0,0 +1,14 @@
+package com.payment.platform.common.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public abstract class BaseQueryDTO {
+
+    @Schema(description = "页码", defaultValue = "1")
+    private int pageNo = 1;
+
+    @Schema(description = "每页条数", defaultValue = "20")
+    private int pageSize = 20;
+}

+ 13 - 0
java/src/main/java/com/payment/platform/common/dto/BaseUpdateDTO.java

@@ -0,0 +1,13 @@
+package com.payment.platform.common.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public abstract class BaseUpdateDTO extends BaseCreateDTO {
+
+    @Schema(description = "记录ID")
+    private Long id;
+}

+ 28 - 0
java/src/main/java/com/payment/platform/common/dto/BaseVO.java

@@ -0,0 +1,28 @@
+package com.payment.platform.common.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+
+@Data
+public abstract class BaseVO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "UUID")
+    private String uuid;
+
+    @Schema(description = "状态")
+    private String status;
+
+    @Schema(description = "描述")
+    private String description;
+
+    @Schema(description = "创建时间")
+    private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
+}

+ 20 - 0
java/src/main/java/com/payment/platform/common/dto/BatchStatusDTO.java

@@ -0,0 +1,20 @@
+package com.payment.platform.common.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class BatchStatusDTO {
+
+    @NotEmpty(message = "ID列表不能为空")
+    @Schema(description = "记录ID列表")
+    private List<Long> ids;
+
+    @NotNull(message = "状态不能为空")
+    @Schema(description = "状态值")
+    private String status;
+}

+ 23 - 0
java/src/main/java/com/payment/platform/common/enums/BusinessType.java

@@ -0,0 +1,23 @@
+package com.payment.platform.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum BusinessType {
+    OTHER(0),
+    INSERT(1),
+    UPDATE(2),
+    DELETE(3),
+    GRANT(4),
+    EXPORT(5),
+    IMPORT(6),
+    FORCE(7),
+    GENCODE(8),
+    CLEAN(9);
+
+    private final int code;
+
+    BusinessType(int code) {
+        this.code = code;
+    }
+}

+ 16 - 0
java/src/main/java/com/payment/platform/common/enums/EnvironmentEnum.java

@@ -0,0 +1,16 @@
+package com.payment.platform.common.enums;
+
+public enum EnvironmentEnum {
+    DEV("dev"),
+    PROD("prod");
+
+    private final String value;
+
+    EnvironmentEnum(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}

+ 85 - 0
java/src/main/java/com/payment/platform/common/enums/ErrorCode.java

@@ -0,0 +1,85 @@
+package com.payment.platform.common.enums;
+
+import lombok.Getter;
+
+/**
+ * 系统错误码枚举 — 对应 Python RET enum (backend/app/common/constant.py)
+ *
+ * 0~200: 成功状态码
+ * 400~600: HTTP标准错误码
+ * 4000+: 自定义业务错误码
+ * 10401: 认证相关错误(Python 安全模块使用,前端据此跳转登录页)
+ */
+@Getter
+public enum ErrorCode {
+
+    // 成功状态码
+    OK(0, "成功"),
+    SUCCESS(200, "操作成功"),
+
+    // HTTP标准错误码
+    ERROR(1, "请求错误"),
+    BAD_REQUEST(400, "参数错误"),
+    UNAUTHORIZED(401, "未授权"),
+    FORBIDDEN(403, "访问受限"),
+    NOT_FOUND(404, "资源不存在"),
+    CONFLICT(409, "资源冲突"),
+    TOO_MANY_REQUESTS(429, "请求过于频繁"),
+
+    // 服务器错误码
+    INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
+
+    // 认证授权错误码(特殊码 10401 — 前端据此跳转登录页,匹配 Python security.py)
+    AUTH_FAILED(10401, "认证已失效"),
+    AUTH_NOT_EXIST(10401, "认证不存在,请重新登录"),
+    AUTH_INVALID(10401, "无效认证,请重新登录"),
+    AUTH_EXPIRED(10401, "认证已过期,请重新登录"),
+    AUTH_TOKEN_INVALID(10401, "token已失效,请重新登录"),
+    AUTH_ILLEGAL(10401, "非法凭证"),
+
+    // 自定义业务错误码
+    DATA_EXIST(4003, "数据已存在"),
+    DATA_ERR(4004, "数据错误"),
+    PARAM_ERR(4103, "参数错误"),
+    IO_ERR(4302, "IO错误"),
+    SERVER_ERR(4500, "服务错误"),
+    UNKNOWN_ERR(4501, "未知错误"),
+    TIMEOUT(4502, "请求超时"),
+    RATE_LIMIT_EXCEEDED(4503, "访问频率超限"),
+
+    // Token相关错误码
+    INVALID_TOKEN(4504, "无效令牌"),
+    EXPIRED_TOKEN(4505, "令牌过期"),
+
+    // 认证授权错误码
+    INVALID_CREDENTIALS(4506, "无效凭证"),
+    INVALID_OPERATION(4511, "操作错误"),
+    INVALID_PERMISSION(4512, "权限错误"),
+
+    // 权限相关
+    INVALID_ACCESS(4525, "访问错误"),
+    INVALID_SECURITY(4526, "安全错误"),
+
+    // 系统组件错误码
+    INVALID_NETWORK(4527, "网络错误"),
+    INVALID_DATABASE(4528, "数据库错误"),
+    INVALID_CACHE(4529, "缓存错误"),
+    INVALID_LOCK(4531, "锁错误"),
+    INVALID_TRANSACTION(4532, "事务错误"),
+
+    // 服务相关
+    INVALID_SERVICE(4553, "服务错误"),
+    INVALID_USER(4557, "用户错误"),
+    INVALID_ROLE(4559, "角色错误"),
+
+    // 权限异常
+    PERMISSION_DENIED(40006, "权限不足");
+
+    private final int code;
+    private final String msg;
+
+    ErrorCode(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 19 - 0
java/src/main/java/com/payment/platform/common/enums/PermissionFilterStrategy.java

@@ -0,0 +1,19 @@
+package com.payment.platform.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum PermissionFilterStrategy {
+    DATA_SCOPE("data_scope"),
+    ROLE_BASED("role_based"),
+    DEPT_BASED("dept_based"),
+    SELF_ONLY("self_only"),
+    USER_ROLE("user_role"),
+    ENTERPRISE_BASED("enterprise_based");
+
+    private final String value;
+
+    PermissionFilterStrategy(String value) {
+        this.value = value;
+    }
+}

+ 26 - 0
java/src/main/java/com/payment/platform/common/enums/QueueEnum.java

@@ -0,0 +1,26 @@
+package com.payment.platform.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum QueueEnum {
+    NONE("None"),
+    NOT_NONE("not None"),
+    DATE("date"),
+    MONTH("month"),
+    LIKE("like"),
+    EQ("eq"),
+    IN("in"),
+    BETWEEN("between"),
+    NE("ne"),
+    GT("gt"),
+    GE("ge"),
+    LT("lt"),
+    LE("le");
+
+    private final String value;
+
+    QueueEnum(String value) {
+        this.value = value;
+    }
+}

+ 21 - 0
java/src/main/java/com/payment/platform/common/enums/RedisInitKeyConfig.java

@@ -0,0 +1,21 @@
+package com.payment.platform.common.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum RedisInitKeyConfig {
+    ACCESS_TOKEN("access_token", "登录令牌信息"),
+    REFRESH_TOKEN("refresh_token", "刷新令牌信息"),
+    CAPTCHA_CODES("captcha_codes", "图片验证码"),
+    SYSTEM_CONFIG("system_config", "系统配置"),
+    SYSTEM_DICT("system_dict", "数据字典"),
+    APSCHEDULER_LOCK_KEY("scheduler_job_lock", "定时任务初始化锁");
+
+    private final String key;
+    private final String remark;
+
+    RedisInitKeyConfig(String key, String remark) {
+        this.key = key;
+        this.remark = remark;
+    }
+}

+ 35 - 0
java/src/main/java/com/payment/platform/common/exception/BusinessException.java

@@ -0,0 +1,35 @@
+package com.payment.platform.common.exception;
+
+import com.payment.platform.common.enums.ErrorCode;
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+    private final int code;
+
+    public BusinessException(ErrorCode errorCode) {
+        super(errorCode.getMsg());
+        this.code = errorCode.getCode();
+    }
+
+    public BusinessException(ErrorCode errorCode, String detail) {
+        super(errorCode.getMsg() + (detail != null ? ": " + detail : ""));
+        this.code = errorCode.getCode();
+    }
+
+    public BusinessException(int code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public BusinessException(String message) {
+        super(message);
+        this.code = 500;
+    }
+
+    public BusinessException(int code, String message, Throwable cause) {
+        super(message, cause);
+        this.code = code;
+    }
+}

+ 59 - 0
java/src/main/java/com/payment/platform/common/exception/GlobalExceptionHandler.java

@@ -0,0 +1,59 @@
+package com.payment.platform.common.exception;
+
+import com.payment.platform.common.enums.ErrorCode;
+import com.payment.platform.common.response.Result;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.stream.Collectors;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BusinessException.class)
+    public Result<?> handleBusinessException(BusinessException e, HttpServletRequest request) {
+        log.warn("业务异常 [{}] {}: {}", request.getRequestURI(), e.getCode(), e.getMessage());
+        return Result.of(e.getCode(), e.getMessage());
+    }
+
+    @ExceptionHandler(AccessDeniedException.class)
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    public Result<?> handleAccessDenied(AccessDeniedException e, HttpServletRequest request) {
+        log.warn("权限不足 [{}]: {}", request.getRequestURI(), e.getMessage());
+        return Result.of(ErrorCode.INVALID_PERMISSION.getCode(), "权限不足");
+    }
+
+    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public Result<?> handleValidation(Exception e) {
+        String msg;
+        if (e instanceof MethodArgumentNotValidException ex) {
+            msg = ex.getBindingResult().getFieldErrors().stream()
+                    .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
+                    .collect(Collectors.joining(", "));
+        } else if (e instanceof BindException ex) {
+            msg = ex.getFieldErrors().stream()
+                    .map(FieldError::getDefaultMessage)
+                    .collect(Collectors.joining(", "));
+        } else {
+            msg = "参数校验失败";
+        }
+        return Result.of(ErrorCode.BAD_REQUEST.getCode(), msg);
+    }
+
+    @ExceptionHandler(Exception.class)
+    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+    public Result<?> handleException(Exception e, HttpServletRequest request) {
+        log.error("系统异常 [{}]", request.getRequestURI(), e);
+        return Result.of(ErrorCode.INTERNAL_SERVER_ERROR.getCode(), "服务器内部错误");
+    }
+}

+ 26 - 0
java/src/main/java/com/payment/platform/common/response/PageResult.java

@@ -0,0 +1,26 @@
+package com.payment.platform.common.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 分页结果 — 对应 Python CRUDBase.page() 返回值
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PageResult<T> {
+
+    private int pageNo;
+    private int pageSize;
+    private long total;
+    private boolean hasNext;
+    private List<T> items;
+
+    public static <T> PageResult<T> of(int pageNo, int pageSize, long total, List<T> items) {
+        return new PageResult<>(pageNo, pageSize, total, (long) pageNo * pageSize < total, items);
+    }
+}

+ 64 - 0
java/src/main/java/com/payment/platform/common/response/Result.java

@@ -0,0 +1,64 @@
+package com.payment.platform.common.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 统一响应 — 对应 Python ResponseSchema
+ * code: 0=成功, 1=错误 (匹配 Python RET.OK=0, RET.ERROR=1)
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Result<T> implements Serializable {
+
+    private int code;
+    private String msg;
+    private T data;
+    private int statusCode;
+    private boolean success;
+
+    public Result(int code, String msg, T data) {
+        this.code = code;
+        this.msg = msg;
+        this.data = data;
+        this.statusCode = code == 0 ? 200 : code >= 400 ? code : 200;
+        this.success = code == 0;
+    }
+
+    public static <T> Result<T> ok(T data) {
+        return new Result<>(0, "成功", data);
+    }
+
+    public static <T> Result<T> ok() {
+        return new Result<>(0, "成功", null);
+    }
+
+    /** 泛用静态工厂(对应 Python CustomException 直接传入 code/msg) */
+    public static <T> Result<T> of(int code, String msg) {
+        return new Result<>(code, msg, null);
+    }
+
+    public static <T> Result<T> fail(int code, String msg) {
+        return new Result<>(code, msg, null);
+    }
+
+    public static <T> Result<T> fail(String msg) {
+        return new Result<>(1, msg, null);
+    }
+
+    public static <T> Result<T> badRequest(String msg) {
+        return new Result<>(400, msg, null);
+    }
+
+    public static <T> Result<T> unauthorized(String msg) {
+        return new Result<>(401, msg, null);
+    }
+
+    public static <T> Result<T> forbidden(String msg) {
+        return new Result<>(403, msg, null);
+    }
+}

+ 82 - 0
java/src/main/java/com/payment/platform/common/utils/CaptchaUtil.java

@@ -0,0 +1,82 @@
+package com.payment.platform.common.utils;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * 验证码工具 — 对应 Python CaptchaUtil
+ */
+public class CaptchaUtil {
+
+    private static final Random RAND = new Random();
+    private static final int WIDTH = 160;
+    private static final int HEIGHT = 60;
+    private static final Font FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 28);
+
+    private static final Map<String, int[]> DIFFICULTY = Map.of(
+            "easy", new int[]{1, 9},
+            "medium", new int[]{1, 15},
+            "hard", new int[]{1, 20}
+    );
+
+    /**
+     * 生成算术验证码 — 对应 Python captcha_arithmetic
+     * @return [base64Png, answer]
+     */
+    public static String[] captchaArithmetic(String difficulty) {
+        int[] range = DIFFICULTY.getOrDefault(difficulty, DIFFICULTY.get("medium"));
+        String[] operators = {"+", "-", "*"};
+        String op = operators[RAND.nextInt(3)];
+
+        int num1, num2;
+        if ("-".equals(op)) {
+            num1 = RAND.nextInt(range[1] - range[0] - 5 + 1) + range[0] + 5;
+            num2 = RAND.nextInt(num1 - range[0]) + range[0];
+        } else if ("*".equals(op)) {
+            int max = Math.min(10, range[1]);
+            num1 = RAND.nextInt(max - range[0] + 1) + range[0];
+            num2 = RAND.nextInt(max - range[0] + 1) + range[0];
+        } else {
+            num1 = RAND.nextInt(range[1] - range[0] + 1) + range[0];
+            num2 = RAND.nextInt(range[1] - range[0] + 1) + range[0];
+        }
+
+        int answer = switch (op) {
+            case "+" -> num1 + num2;
+            case "-" -> num1 - num2;
+            default -> num1 * num2;
+        };
+
+        String text = num1 + " " + op + " " + num2 + " = ?";
+
+        BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
+        Graphics2D g = image.createGraphics();
+        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+        g.setColor(new Color(248, 249, 250));
+        g.fillRect(0, 0, WIDTH, HEIGHT);
+        g.setFont(FONT);
+        g.setColor(new Color(55, 65, 81));
+
+        FontMetrics fm = g.getFontMetrics();
+        int tw = fm.stringWidth(text);
+        int th = fm.getHeight();
+        int x = (WIDTH - tw) / 2;
+        int y = (HEIGHT - th) / 2 + fm.getAscent();
+        g.drawString(text, x, y);
+        g.dispose();
+
+        try {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ImageIO.write(image, "PNG", bos);
+            return new String[]{Base64.getEncoder().encodeToString(bos.toByteArray()), String.valueOf(answer)};
+        } catch (IOException e) {
+            throw new RuntimeException("验证码生成失败", e);
+        }
+    }
+}

+ 45 - 0
java/src/main/java/com/payment/platform/common/utils/ExcelUtil.java

@@ -0,0 +1,45 @@
+package com.payment.platform.common.utils;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Excel 导入导出工具 — Apache POI 封装
+ * 对应 Python excel_util.py
+ */
+@Slf4j
+public final class ExcelUtil {
+    private ExcelUtil() {}
+
+    /**
+     * 将 List<Map> 导出为 Excel 文件
+     * @param data 数据列表
+     * @param columnMapping 列映射(字段名 -> 中文标题)
+     * @return Excel 文件字节数组
+     */
+    public static byte[] exportToExcel(List<Map<String, Object>> data, Map<String, String> columnMapping) {
+        log.warn("Excel 导出未实现 (Apache POI 待集成): {} 行, {} 列", data.size(), columnMapping.size());
+        throw new UnsupportedOperationException("Excel 导出功能尚未实现,需要集成 Apache POI");
+    }
+
+    /**
+     * 从 Excel 文件导入数据
+     * @param fileBytes Excel 文件字节数组
+     * @param columnMapping 列映射(中文标题 -> 字段名)
+     * @return 解析后的数据列表
+     */
+    public static List<Map<String, Object>> importFromExcel(byte[] fileBytes, Map<String, String> columnMapping) {
+        log.warn("Excel 导入未实现 (Apache POI 待集成): {} bytes, {} 列映射", fileBytes.length, columnMapping.size());
+        throw new UnsupportedOperationException("Excel 导入功能尚未实现,需要集成 Apache POI");
+    }
+
+    /** 构建常用字段映射 */
+    public static Map<String, String> buildMapping(String... pairs) {
+        Map<String, String> map = new LinkedHashMap<>();
+        for (int i = 0; i < pairs.length; i += 2) map.put(pairs[i], pairs[i + 1]);
+        return map;
+    }
+}

+ 146 - 0
java/src/main/java/com/payment/platform/common/utils/JwtUtils.java

@@ -0,0 +1,146 @@
+package com.payment.platform.common.utils;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class JwtUtils {
+
+    private final SecretKey secretKey;
+
+    @Value("${jwt.access-token-expire}")
+    private long accessTokenExpire;
+
+    @Value("${jwt.refresh-token-expire}")
+    private long refreshTokenExpire;
+
+    public JwtUtils(@Value("${jwt.secret}") String secret) {
+        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * 创建 access token — sub 为 session_info JSON(含 session_id / user_id / username / ip / os / browser 等)
+     */
+    public String createAccessToken(String sessionInfoJson, boolean isRefresh) {
+        return createTokenInternal(sessionInfoJson, isRefresh, accessTokenExpire);
+    }
+
+    /**
+     * 创建 refresh token
+     */
+    public String createRefreshToken(String sessionInfoJson, boolean isRefresh) {
+        return createTokenInternal(sessionInfoJson, isRefresh, refreshTokenExpire);
+    }
+
+    /**
+     * 兼容旧调用:按简单 userId + username 创建 token(sub = userIdStr,额外 claim username)
+     * @deprecated 迁移为 createAccessToken(String sessionInfoJson, boolean isRefresh)
+     */
+    @Deprecated
+    public String createAccessToken(Long userId, String username, Map<String, Object> claims) {
+        // 旧方法不再注入 claims,直接以 sessionInfo 模式创建
+        String sessionInfoJson = String.format(
+                "{\"user_id\":%d,\"user_name\":\"%s\"}",
+                userId, username == null ? "" : username);
+        return createAccessToken(sessionInfoJson, false);
+    }
+
+    /**
+     * 兼容旧调用
+     * @deprecated 迁移为 createRefreshToken(String sessionInfoJson, boolean isRefresh)
+     */
+    @Deprecated
+    public String createRefreshToken(Long userId, String username, Map<String, Object> claims) {
+        String sessionInfoJson = String.format(
+                "{\"user_id\":%d,\"user_name\":\"%s\"}",
+                userId, username == null ? "" : username);
+        return createRefreshToken(sessionInfoJson, true);
+    }
+
+    // ---------------- internal ----------------
+
+    private String createTokenInternal(String sub, boolean isRefresh, long expireSeconds) {
+        return Jwts.builder()
+                .subject(sub)
+                .issuer("payment-platform")
+                .issuedAt(new Date())
+                .expiration(new Date(System.currentTimeMillis() + expireSeconds * 1000))
+                .claim("is_refresh", isRefresh)
+                .signWith(secretKey)
+                .compact();
+    }
+
+    public Claims parseToken(String token) {
+        return Jwts.parser()
+                .verifyWith(secretKey)
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    public boolean validateToken(String token) {
+        try {
+            parseToken(token);
+            return true;
+        } catch (JwtException | IllegalArgumentException e) {
+            log.debug("JWT验证失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 从 token 中获取 sub 字段(Python 中为 session_info JSON 字符串)
+     */
+    public String getSub(String token) {
+        return parseToken(token).getSubject();
+    }
+
+    /**
+     * 从 token 中获取 is_refresh 标志
+     */
+    public boolean isRefreshToken(String token) {
+        Boolean isRefresh = parseToken(token).get("is_refresh", Boolean.class);
+        return isRefresh != null && isRefresh;
+    }
+
+    /**
+     * 从 token 中获取 userId(向后兼容 — sub 为简单 userId 字符串时可用)
+     * @deprecated 新格式 sub 为 session_info JSON,请用 getSub() 后自行解析
+     */
+    @Deprecated
+    public Long getUserId(String token) {
+        return Long.valueOf(parseToken(token).getSubject());
+    }
+
+    /**
+     * 从 token 中获取 username(向后兼容 — 仅旧 token 含此 claim)
+     * @deprecated 新格式不含 username claim
+     */
+    @Deprecated
+    public String getUsername(String token) {
+        return parseToken(token).get("username", String.class);
+    }
+
+    /**
+     * 获取 access token 有效期(秒)
+     */
+    public long getAccessTokenExpireSeconds() {
+        return accessTokenExpire;
+    }
+
+    /**
+     * 获取 refresh token 有效期(秒)
+     */
+    public long getRefreshTokenExpireSeconds() {
+        return refreshTokenExpire;
+    }
+}

+ 154 - 0
java/src/main/java/com/payment/platform/common/utils/RedisLockUtil.java

@@ -0,0 +1,154 @@
+package com.payment.platform.common.utils;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.Set;
+
+/**
+ * Redis 分布式锁工具 — 对应 Python redis_crud.py 中的 lock/unlock 方法
+ *
+ * 用法:
+ *   String lockValue = redisLockUtil.lock("my_lock", 30);
+ *   try {
+ *       // 临界区
+ *   } finally {
+ *       redisLockUtil.unlock("my_lock", lockValue);
+ *   }
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RedisLockUtil {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    /** 释放锁的 Lua 脚本 — 确保只有锁的持有者才能释放(原子操作) */
+    private static final String UNLOCK_SCRIPT = """
+            if redis.call('get', KEYS[1]) == ARGV[1] then
+                return redis.call('del', KEYS[1])
+            else
+                return 0
+            end
+            """;
+
+    private final DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
+
+    /**
+     * 获取分布式锁 — 对应 Python RedisCURD.lock()
+     *
+     * @param key    锁键名
+     * @param expireSec 锁过期时间(秒)
+     * @return 锁值(用于解锁),获取失败返回 null
+     */
+    public String lock(String key, long expireSec) {
+        return lock(key, expireSec, null);
+    }
+
+    /**
+     * 获取分布式锁(指定锁值)
+     *
+     * @param key    锁键名
+     * @param expireSec 锁过期时间(秒)
+     * @param value  锁值,null 时自动生成 UUID
+     * @return 锁值,获取失败返回 null
+     */
+    public String lock(String key, long expireSec, String value) {
+        String lockValue = value != null ? value : UUID.randomUUID().toString();
+        try {
+            // SET key value NX EX expireSec(对应 Python setnx)
+            Boolean acquired = redisTemplate.opsForValue()
+                    .setIfAbsent(key, lockValue, Duration.ofSeconds(expireSec));
+            if (Boolean.TRUE.equals(acquired)) {
+                return lockValue;
+            }
+            return null;
+        } catch (Exception e) {
+            log.error("获取分布式锁失败: key={}, error={}", key, e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 释放分布式锁(安全版本,验证锁值) — 对应 Python RedisCURD.unlock()
+     *
+     * @param key   锁键名
+     * @param value 锁值
+     * @return 是否成功释放
+     */
+    public boolean unlock(String key, String value) {
+        try {
+            Long result = redisTemplate.execute(unlockScript,
+                    Collections.singletonList(key), value);
+            return Long.valueOf(1).equals(result);
+        } catch (Exception e) {
+            log.error("释放分布式锁失败: key={}, error={}", key, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 释放分布式锁(简单版本,不验证锁值) — 对应 Python RedisCURD.unlock_simple()
+     */
+    public boolean unlockSimple(String key) {
+        try {
+            return Boolean.TRUE.equals(redisTemplate.delete(key));
+        } catch (Exception e) {
+            log.error("释放分布式锁失败: key={}, error={}", key, e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * 按 pattern 获取所有匹配的 key — 对应 Python RedisCURD.get_keys(pattern)
+     *
+     * 注意:线上环境建议使用 SCAN 命令替代 KEYS
+     */
+    public Set<String> keys(String pattern) {
+        try {
+            return redisTemplate.keys(pattern);
+        } catch (Exception e) {
+            log.error("获取缓存键名失败: pattern={}, error={}", pattern, e.getMessage());
+            return Collections.emptySet();
+        }
+    }
+
+    /**
+     * 删除缓存
+     */
+    public boolean delete(String... keys) {
+        try {
+            Long count = redisTemplate.delete(List.of(keys));
+            return count != null && count > 0;
+        } catch (Exception e) {
+            log.error("删除缓存失败: error={}", e.getMessage());
+            return false;
+        }
+    }
+
+    /** 设置带 TTL 的缓存 */
+    public void set(String key, Object value, Duration ttl) {
+        try {
+            redisTemplate.opsForValue().set(key, value, ttl);
+        } catch (Exception e) {
+            log.error("设置缓存失败: key={}, error={}", key, e.getMessage());
+        }
+    }
+
+    /** 获取缓存 */
+    public Object get(String key) {
+        try {
+            return redisTemplate.opsForValue().get(key);
+        } catch (Exception e) {
+            log.error("获取缓存失败: key={}, error={}", key, e.getMessage());
+            return null;
+        }
+    }
+}

+ 58 - 0
java/src/main/java/com/payment/platform/common/utils/SnowflakeIdGenerator.java

@@ -0,0 +1,58 @@
+package com.payment.platform.common.utils;
+
+/**
+ * Snowflake ID 生成器 — 对应 Python snowflake.py
+ *
+ * 位分配: 1 符号位 + 41 时间戳 + 10 机器ID + 12 序列号
+ */
+public final class SnowflakeIdGenerator {
+    private SnowflakeIdGenerator() {}
+
+    private static final long EPOCH = 1480166465631L;
+    private static final long WORKER_ID_BITS = 10L;
+    private static final long SEQUENCE_BITS = 12L;
+    private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
+
+    private static final long workerId = 1L;
+    private static long sequence = 0L;
+    private static long lastTimestamp = -1L;
+
+    /** 生成下一个 ID(线程安全) */
+    public static synchronized long nextId() {
+        long timestamp = System.currentTimeMillis();
+        if (timestamp < lastTimestamp)
+            throw new IllegalStateException("时钟回拨,拒绝生成 ID");
+
+        if (timestamp == lastTimestamp) {
+            sequence = (sequence + 1) & MAX_SEQUENCE;
+            if (sequence == 0)
+                timestamp = waitNextMillis(lastTimestamp);
+        } else {
+            sequence = 0L;
+        }
+
+        lastTimestamp = timestamp;
+        return ((timestamp - EPOCH) << (WORKER_ID_BITS + SEQUENCE_BITS))
+                | (workerId << SEQUENCE_BITS)
+                | sequence;
+    }
+
+    /** 生成字符串 ID(纯数字) */
+    public static String nextIdStr() {
+        return String.valueOf(nextId());
+    }
+
+    /**
+     * 生成带租户后缀的 ID 字符串 — 对应 Python get_snowflake_id_str(tenant_id)
+     * 格式: 19 位补零雪花ID + tenantId
+     */
+    public static String nextIdStr(int tenantId) {
+        return String.format("%019d%d", nextId(), tenantId);
+    }
+
+    private static long waitNextMillis(long lastTs) {
+        long ts = System.currentTimeMillis();
+        while (ts <= lastTs) ts = System.currentTimeMillis();
+        return ts;
+    }
+}

+ 20 - 0
java/src/main/java/com/payment/platform/common/validator/ConditionalRateRequired.java

@@ -0,0 +1,20 @@
+package com.payment.platform.common.validator;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * 条件校验注解:当 signAndAuth=true 时,rate 必填
+ * 对应 Python FacetofaceApplySchema.validate_rate()
+ */
+@Documented
+@Constraint(validatedBy = ConditionalRateRequiredValidator.class)
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ConditionalRateRequired {
+    String message() default "签约且授权时费率不能为空";
+    Class<?>[] groups() default {};
+    Class<? extends Payload>[] payload() default {};
+}

+ 31 - 0
java/src/main/java/com/payment/platform/common/validator/ConditionalRateRequiredValidator.java

@@ -0,0 +1,31 @@
+package com.payment.platform.common.validator;
+
+import com.payment.platform.module.payment.facetoface.dto.F2fOrderCreateDTO;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+/**
+ * 条件校验:如果 signAndAuth=true,则 rate 必须非空
+ * 对应 Python:
+ *    @model_validator(mode="after")
+ *    def validate_rate(self):
+ *        if self.sign_and_auth and not self.rate:
+ *            raise ValueError("签约且授权时费率不能为空")
+ */
+public class ConditionalRateRequiredValidator implements ConstraintValidator<ConditionalRateRequired, F2fOrderCreateDTO> {
+
+    @Override
+    public boolean isValid(F2fOrderCreateDTO dto, ConstraintValidatorContext context) {
+        if (dto == null) {
+            return true;
+        }
+        if (Boolean.TRUE.equals(dto.getSignAndAuth()) && (dto.getRate() == null || dto.getRate().isBlank())) {
+            context.disableDefaultConstraintViolation();
+            context.buildConstraintViolationWithTemplate("签约且授权时费率不能为空")
+                    .addPropertyNode("rate")
+                    .addConstraintViolation();
+            return false;
+        }
+        return true;
+    }
+}

+ 19 - 0
java/src/main/java/com/payment/platform/common/validator/EmailValid.java

@@ -0,0 +1,19 @@
+package com.payment.platform.common.validator;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * 邮箱格式校验 — 对应 Python email_validator()
+ */
+@Documented
+@Constraint(validatedBy = EmailValidator.class)
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EmailValid {
+    String message() default "邮箱地址格式不正确";
+    Class<?>[] groups() default {};
+    Class<? extends Payload>[] payload() default {};
+}

+ 24 - 0
java/src/main/java/com/payment/platform/common/validator/EmailValidator.java

@@ -0,0 +1,24 @@
+package com.payment.platform.common.validator;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.regex.Pattern;
+
+/**
+ * 邮箱格式验证器 — 对应 Python email_validator()
+ *
+ * 规则:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
+ */
+public class EmailValidator implements ConstraintValidator<EmailValid, String> {
+
+    private static final Pattern EMAIL_PATTERN =
+            Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 空值跳过校验(配合 @NotBlank 使用)
+        if (value == null || value.isBlank()) return true;
+        return EMAIL_PATTERN.matcher(value).matches();
+    }
+}

+ 21 - 0
java/src/main/java/com/payment/platform/common/validator/Phone.java

@@ -0,0 +1,21 @@
+package com.payment.platform.common.validator;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.*;
+
+/**
+ * 手机号格式校验 — 对应 Python mobile_validator()
+ *
+ * 规则:11位数字,以1开头,第二位 3-9
+ */
+@Documented
+@Constraint(validatedBy = PhoneValidator.class)
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Phone {
+    String message() default "手机号格式不正确";
+    Class<?>[] groups() default {};
+    Class<? extends Payload>[] payload() default {};
+}

+ 27 - 0
java/src/main/java/com/payment/platform/common/validator/PhoneValidator.java

@@ -0,0 +1,27 @@
+package com.payment.platform.common.validator;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.regex.Pattern;
+
+/**
+ * 手机号格式验证器 — 对应 Python mobile_validator()
+ *
+ * 规则:11位数字,以1开头,固定号段范围
+ */
+public class PhoneValidator implements ConstraintValidator<Phone, String> {
+
+    // 对应 Python: regex = r"^1(3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9])\d{8}$"
+    private static final Pattern PHONE_PATTERN =
+            Pattern.compile("^1(3\\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9])\\d{8}$");
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        // 空值跳过校验(配合 @NotBlank 使用)
+        if (value == null || value.isBlank()) return true;
+        // 先检查长度和数字
+        if (value.length() != 11) return false;
+        return PHONE_PATTERN.matcher(value).matches();
+    }
+}

+ 66 - 0
java/src/main/java/com/payment/platform/core/alipay/AlipayClientFactory.java

@@ -0,0 +1,66 @@
+package com.payment.platform.core.alipay;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 支付宝客户端工厂 — 对应 Python AlipayClient.get_client()
+ *
+ * Python SDK → Java SDK 映射:
+ *   alipay.aop.api.DefaultAlipayClient  → com.alipay.api.DefaultAlipayClient
+ *   alipay.aop.api.AlipayClientConfig    → com.alipay.api.AlipayConfig
+ *   alipay.aop.api.domain.*              → com.alipay.api.domain.*
+ *   alipay.aop.api.request.*             → com.alipay.api.request.*
+ *   alipay.aop.api.response.*            → com.alipay.api.response.*
+ *   alipay.aop.api.util.Signature        → com.alipay.api.internal.util.AlipaySignature
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AlipayClientFactory {
+
+    private final com.payment.platform.core.alipay.AlipayConfig paymentAlipayConfig;
+
+    private AlipayClient client;
+
+    @PostConstruct
+    public void init() {
+        if (!paymentAlipayConfig.isValid()) {
+            log.warn("支付宝配置不完整,跳过客户端初始化");
+            return;
+        }
+
+        AlipayConfig config = new AlipayConfig();
+        config.setAppId(paymentAlipayConfig.getAppId());
+        config.setPrivateKey(paymentAlipayConfig.getAppPrivateKey());
+        config.setAlipayPublicKey(paymentAlipayConfig.getAlipayPublicKey());
+        config.setServerUrl(paymentAlipayConfig.getServerUrl());
+        config.setFormat(paymentAlipayConfig.getFormat());
+        config.setCharset(paymentAlipayConfig.getCharset());
+        config.setSignType(paymentAlipayConfig.getSignType());
+
+        try {
+            this.client = new DefaultAlipayClient(config);
+            log.info("支付宝客户端初始化成功, appId={}", paymentAlipayConfig.getAppId());
+        } catch (AlipayApiException e) {
+            log.error("支付宝客户端初始化失败", e);
+        }
+    }
+
+    public AlipayClient getClient() {
+        if (client == null) {
+            throw new IllegalStateException("支付宝客户端未初始化,请检查 alipay 配置");
+        }
+        return client;
+    }
+
+    public boolean isReady() {
+        return client != null;
+    }
+}

+ 70 - 0
java/src/main/java/com/payment/platform/core/alipay/AlipayConfig.java

@@ -0,0 +1,70 @@
+package com.payment.platform.core.alipay;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 支付宝配置 — 对应 Python AlipayConfig (config.py)
+ *
+ * Python 环境变量 → YAML 映射:
+ *   ALIPAY_APPID       → alipay.app-id
+ *   ALIPAY_PRIVATE_KEY → alipay.app-private-key
+ *   ALIPAY_PUBLIC_KEY  → alipay.alipay-public-key
+ *   ALIPAY_FORMAT      → alipay.format
+ *   ALIPAY_CHARSET     → alipay.charset
+ *   ALIPAY_SIGN_TYPE   → alipay.sign-type
+ *   ALIPAY_SANDBOX     → alipay.sandbox
+ *   ALIPAY_NOTIFY_URL  → alipay.notify-url
+ *   ALIPAY_RETURN_URL  → alipay.return-url
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "alipay")
+public class AlipayConfig {
+
+    /** 应用ID */
+    private String appId;
+
+    /** 应用私钥(RSA2) */
+    private String appPrivateKey;
+
+    /** 支付宝公钥(RSA2) */
+    private String alipayPublicKey;
+
+    /** 网关地址 */
+    private String serverUrl = "https://openapi.alipay.com/gateway.do";
+
+    /** 响应格式 */
+    private String format = "JSON";
+
+    /** 字符集 */
+    private String charset = "UTF-8";
+
+    /** 签名类型: RSA/RSA2 */
+    private String signType = "RSA2";
+
+    /** 是否沙箱环境 */
+    private boolean sandbox = false;
+
+    /** 异步通知地址 */
+    private String notifyUrl;
+
+    /** 同步跳转地址 */
+    private String returnUrl;
+
+    /** 最大重试次数 */
+    private int maxRetries = 3;
+
+    /** 请求超时(秒) */
+    private int requestTimeout = 30;
+
+    /** 限流阈值(每分钟) */
+    private int rateLimit = 100;
+
+    public boolean isValid() {
+        return appId != null && !appId.isBlank()
+                && appPrivateKey != null && !appPrivateKey.isBlank()
+                && alipayPublicKey != null && !alipayPublicKey.isBlank();
+    }
+}

+ 58 - 0
java/src/main/java/com/payment/platform/core/config/AppStartupRunner.java

@@ -0,0 +1,58 @@
+package com.payment.platform.core.config;
+
+import com.payment.platform.module.system.dict.service.DictService;
+import com.payment.platform.module.system.params.service.ParamsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+/**
+ * 应用启动初始化 — 对应 Python lifespan() 中的初始化步骤
+ *
+ * Python lifespan:
+ *   1. InitializeData().init_db()
+ *   2. import_modules_async(settings.EVENT_LIST, ...)
+ *   3. ParamsService().init_config_service(redis=app.state.redis)   ← 加载系统配置到 Redis
+ *   4. DictDataService().init_dict_service(redis=app.state.redis)    ← 加载数据字典到 Redis
+ *   5. FastAPILimiter.init(...)
+ *
+ * Spring Boot 对标:
+ *   - DB 初始化由 Flyway/Liquibase 或 JPA DDL 处理
+ *   - Event 初始化由 @PostConstruct 在对应 Bean 中处理
+ *   - Params/Dict 缓存由此 Runner 加载
+ *   - RateLimiter 后续可实现为 Spring Interceptor
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class AppStartupRunner implements ApplicationRunner {
+
+    private final ParamsService paramsService;
+    private final DictService dictService;
+
+    @Override
+    public void run(ApplicationArguments args) {
+        try {
+            paramsService.initConfigToRedis();
+            log.info("系统配置缓存初始化完成");
+        } catch (Exception e) {
+            log.warn("系统配置缓存初始化失败(Redis 可能未就绪),后续请求将回退到 DB: {}", e.getMessage());
+        }
+
+        try {
+            // 加载所有 dict_type 到 Redis(对应 Python DictDataService.init_dict_service)
+            var dictTypes = dictService.getTypeList(null);
+            for (var type : dictTypes) {
+                try {
+                    // getDataByType 会自动 warm-up Redis 缓存
+                    dictService.getDataByType(type.getDictType());
+                } catch (Exception ignored) { /* 单个 dict 加载失败不阻塞启动 */ }
+            }
+            log.info("数据字典缓存初始化完成 ({} 个字典类型)", dictTypes.size());
+        } catch (Exception e) {
+            log.warn("数据字典缓存初始化失败(Redis 可能未就绪): {}", e.getMessage());
+        }
+    }
+}

+ 148 - 0
java/src/main/java/com/payment/platform/core/log/OperationLogInterceptor.java

@@ -0,0 +1,148 @@
+package com.payment.platform.core.log;
+
+import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.system.log.entity.OperationLogEntity;
+import com.payment.platform.module.system.log.mapper.OperationLogMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.Set;
+
+/**
+ * 操作日志拦截器 — 对应 Python OperationLogRoute
+ * 记录 POST/PUT/PATCH/DELETE 请求到 sys_log 表
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OperationLogInterceptor implements HandlerInterceptor {
+
+    private final OperationLogMapper operationLogMapper;
+
+    private static final Set<String> RECORD_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS");
+    private static final int MAX_PAYLOAD = 2000;
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+                                 Object handler, Exception ex) throws Exception {
+        if (!RECORD_METHODS.contains(request.getMethod())) return;
+
+        // 跳过 Swagger/Knife4j 请求
+        String referer = request.getHeader("referer");
+        if (referer != null && (referer.contains("doc.html") || referer.contains("swagger"))) return;
+
+        OperationLogEntity entity = new OperationLogEntity();
+        entity.setType(getLogType());
+        entity.setRequestPath(request.getRequestURI());
+        entity.setRequestMethod(request.getMethod());
+        entity.setRequestPayload(getPayload(request));
+        entity.setRequestIp(getClientIp(request));
+        entity.setRequestOs(getOs(request));
+        entity.setRequestBrowser(getBrowser(request));
+        entity.setResponseCode(response.getStatus());
+        entity.setResponseJson(getResponseBody(response));
+        entity.setProcessTime(getProcessTime(request));
+        entity.setCreatedId(getUserId());
+        entity.setUpdatedId(getUserId());
+        entity.setCreatedTime(OffsetDateTime.now());
+        entity.setUpdatedTime(OffsetDateTime.now());
+        entity.setStatus("0");
+        entity.setUuid(java.util.UUID.randomUUID().toString());
+
+        try {
+            operationLogMapper.insert(entity);
+        } catch (Exception e) {
+            log.error("操作日志写入失败", e);
+        }
+    }
+
+    private Integer getLogType() {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        if (auth != null && auth.getPrincipal() instanceof LoginUser) return 2; // 操作日志
+        return 1; // 登录日志
+    }
+
+    private Long getUserId() {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        if (auth != null && auth.getPrincipal() instanceof LoginUser u) return u.getUserId();
+        return null;
+    }
+
+    private String getPayload(HttpServletRequest request) {
+        if (request instanceof ContentCachingRequestWrapper wrapper) {
+            byte[] buf = wrapper.getContentAsByteArray();
+            if (buf.length > 0) {
+                String body = new String(buf, StandardCharsets.UTF_8);
+                if (body.length() > MAX_PAYLOAD) body = body.substring(0, MAX_PAYLOAD);
+                return body;
+            }
+        }
+        // 尝试从 query string 获取参数
+        String qs = request.getQueryString();
+        if (qs != null && !qs.isEmpty()) {
+            if (qs.length() > MAX_PAYLOAD) qs = qs.substring(0, MAX_PAYLOAD);
+            return qs;
+        }
+        return "{}";
+    }
+
+    private String getResponseBody(HttpServletResponse response) {
+        if (response instanceof ContentCachingResponseWrapper wrapper) {
+            byte[] buf = wrapper.getContentAsByteArray();
+            if (buf.length > 0) {
+                String body = new String(buf, StandardCharsets.UTF_8);
+                if (body.length() > MAX_PAYLOAD) body = body.substring(0, MAX_PAYLOAD);
+                return body;
+            }
+        }
+        return "{}";
+    }
+
+    private String getProcessTime(HttpServletRequest request) {
+        Object start = request.getAttribute("requestStartTime");
+        if (start instanceof Long s) {
+            double seconds = (System.currentTimeMillis() - s) / 1000.0;
+            return String.format("%.2fs", seconds);
+        }
+        return "0.00s";
+    }
+
+    private String getClientIp(HttpServletRequest request) {
+        String xff = request.getHeader("X-Forwarded-For");
+        if (xff != null && !xff.isEmpty()) return xff.split(",")[0].trim();
+        return request.getRemoteAddr();
+    }
+
+    private String getOs(HttpServletRequest request) {
+        String ua = request.getHeader("User-Agent");
+        if (ua == null) return null;
+        ua = ua.toLowerCase();
+        if (ua.contains("windows")) return "Windows";
+        if (ua.contains("mac os")) return "Mac OS X";
+        if (ua.contains("linux") || ua.contains("x11")) return "Linux";
+        if (ua.contains("android")) return "Android";
+        if (ua.contains("iphone") || ua.contains("ipad")) return "iOS";
+        return "Other";
+    }
+
+    private String getBrowser(HttpServletRequest request) {
+        String ua = request.getHeader("User-Agent");
+        if (ua == null) return null;
+        ua = ua.toLowerCase();
+        if (ua.contains("edg")) return "Edge";
+        if (ua.contains("chrome") && !ua.contains("edg")) return "Chrome";
+        if (ua.contains("firefox")) return "Firefox";
+        if (ua.contains("safari") && !ua.contains("chrome")) return "Safari";
+        return "Other";
+    }
+}

+ 33 - 0
java/src/main/java/com/payment/platform/core/log/RequestCachingFilter.java

@@ -0,0 +1,33 @@
+package com.payment.platform.core.log;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+import org.springframework.web.util.ContentCachingResponseWrapper;
+
+import java.io.IOException;
+
+/**
+ * 包装 request/response 以支持重复读取 body,
+ * 并记录请求开始时间供 OperationLogInterceptor 使用
+ */
+@Component
+public class RequestCachingFilter implements Filter {
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+        ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
+        ContentCachingResponseWrapper respWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
+
+        request.setAttribute("requestStartTime", System.currentTimeMillis());
+
+        try {
+            chain.doFilter(reqWrapper, respWrapper);
+        } finally {
+            respWrapper.copyBodyToResponse();
+        }
+    }
+}

+ 102 - 0
java/src/main/java/com/payment/platform/core/permission/DataScopeInnerInterceptor.java

@@ -0,0 +1,102 @@
+package com.payment.platform.core.permission;
+
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import com.payment.platform.core.security.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.InExpression;
+import net.sf.jsqlparser.expression.operators.relational.ItemsList;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.select.PlainSelect;
+import net.sf.jsqlparser.statement.select.Select;
+import net.sf.jsqlparser.statement.select.SelectBody;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.statement.StatementHandler;
+import org.apache.ibatis.mapping.BoundSql;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.session.ResultHandler;
+import org.apache.ibatis.session.RowBounds;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import java.sql.Connection;
+import java.util.List;
+
+/**
+ * 数据范围权限拦截器 — 行级别权限过滤
+ * 对应 Python Permission.filter_query + data_scope (1:仅本人, 2:本部门, 3:本部门及以下, 4:全部, 5:自定义)
+ *
+ * 策略:
+ * - data_scope=1: WHERE created_id = currentUserId
+ * - data_scope=2: WHERE dept_id = currentDeptId
+ * - data_scope=3: WHERE dept_id IN (currentDeptId + children)
+ * - data_scope=4/5: 不追加(全部数据 或 自定义由业务层处理)
+ */
+@Slf4j
+@Component
+public class DataScopeInnerInterceptor implements InnerInterceptor {
+
+    /**
+     * 需要数据权限过滤的表
+     */
+    private static final List<String> DATA_SCOPE_TABLES = List.of(
+            "sys_user", "sys_dept", "sys_role", "sys_log"
+    );
+
+    @Override
+    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
+                            RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        if (!(auth != null && auth.getPrincipal() instanceof LoginUser user)) {
+            return;
+        }
+
+        // 管理员不限制
+        if (user.getIsSuperuser() != null && user.getIsSuperuser()) {
+            return;
+        }
+
+        String originalSql = boundSql.getSql();
+        try {
+            Statement stmt = CCJSqlParserUtil.parse(originalSql);
+            if (!(stmt instanceof Select select)) {
+                return;
+            }
+
+            SelectBody selectBody = select.getSelectBody();
+            if (!(selectBody instanceof PlainSelect plainSelect)) {
+                return;
+            }
+
+            String tableName = extractTableName(plainSelect);
+            if (tableName == null || !DATA_SCOPE_TABLES.contains(tableName)) {
+                return;
+            }
+
+            // data_scope=1 → 仅本人数据
+            Expression scopeCondition = new EqualsTo(
+                    new Column("created_id"),
+                    new LongValue(user.getUserId()));
+
+            if (plainSelect.getWhere() == null) {
+                plainSelect.setWhere(scopeCondition);
+            } else {
+                plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), scopeCondition));
+            }
+        } catch (Exception e) {
+            log.warn("数据权限SQL改写失败, 使用原始SQL", e);
+        }
+    }
+
+    private String extractTableName(PlainSelect plainSelect) {
+        if (plainSelect.getFromItem() == null) return null;
+        return plainSelect.getFromItem().toString().replaceAll("[`\"]", "");
+    }
+}

+ 65 - 0
java/src/main/java/com/payment/platform/core/scheduler/GroovyScriptEngine.java

@@ -0,0 +1,65 @@
+package com.payment.platform.core.scheduler;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyShell;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Groovy 脚本引擎 — 替代 Python exec(code_block) 的动态执行能力
+ *
+ * 安全约束:
+ * - 仅允许调用预注册的 handler 函数签名: def handler(Object... args)
+ * - 脚本在受限的 Binding 上下文中执行
+ */
+@Slf4j
+@Component
+public class GroovyScriptEngine {
+
+    /**
+     * 编译缓存,避免重复解析相同的脚本
+     */
+    private final Map<String, groovy.lang.Script> scriptCache = new ConcurrentHashMap<>();
+
+    /**
+     * 执行 Groovy 脚本并调用其中的 handler 函数。
+     *
+     * @param jobId    任务 ID(用于日志)
+     * @param codeBlock Groovy 脚本内容,必须包含 handler 函数
+     * @param args     handler 参数
+     * @return handler 返回值
+     */
+    public Object execute(String jobId, String codeBlock, Object... args) {
+        if (codeBlock == null || codeBlock.isBlank()) {
+            return null;
+        }
+
+        try {
+            GroovyShell shell = new GroovyShell(new Binding());
+            groovy.lang.Script script = shell.parse(codeBlock);
+
+            Binding binding = new Binding();
+            binding.setVariable("args", args);
+            script.setBinding(binding);
+            script.run();
+
+            Object handler = binding.getVariable("handler");
+            if (handler == null) {
+                throw new IllegalArgumentException("脚本必须定义 handler 函数");
+            }
+
+            log.info("Groovy 脚本执行成功: jobId={}", jobId);
+            return handler;
+        } catch (Exception e) {
+            log.error("Groovy 脚本执行失败: jobId={}, error={}", jobId, e.getMessage());
+            throw new RuntimeException("任务执行失败: " + e.getMessage(), e);
+        }
+    }
+
+    public void clearCache() {
+        scriptCache.clear();
+    }
+}

+ 20 - 0
java/src/main/java/com/payment/platform/core/scheduler/QuartzConfig.java

@@ -0,0 +1,20 @@
+package com.payment.platform.core.scheduler;
+
+import org.quartz.Scheduler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.quartz.SchedulerFactoryBean;
+
+/**
+ * Quartz 调度器配置 — 对应 Python APScheduler
+ * Spring Boot 自动配置已覆盖基础设置,
+ * 此处仅为自定义注入预留扩展点。
+ */
+@Configuration
+public class QuartzConfig {
+
+    @Bean
+    public Scheduler scheduler(SchedulerFactoryBean factory) {
+        return factory.getScheduler();
+    }
+}

+ 189 - 0
java/src/main/java/com/payment/platform/core/security/JwtAuthFilter.java

@@ -0,0 +1,189 @@
+package com.payment.platform.core.security;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.payment.platform.common.enums.ErrorCode;
+import com.payment.platform.common.enums.RedisInitKeyConfig;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.common.utils.JwtUtils;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+
+/**
+ * JWT 认证过滤器 — 对应 Python security.py + dependencies.py 中的 JWT 校验逻辑
+ *
+ * 当 token 存在但无效时,返回 code 10401(Python security.py 统一使用该 code
+ * 作为"认证失败"标识,前端据此跳转登录页),而非静默放行。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class JwtAuthFilter extends OncePerRequestFilter {
+
+    private final JwtUtils jwtUtils;
+    private final UserDetailsServiceImpl userDetailsService;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ObjectMapper objectMapper;
+
+    @Value("${jwt.sliding-expire:true}")
+    private boolean slidingExpire;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+                                    FilterChain filterChain) throws ServletException, IOException {
+        String token = extractToken(request);
+
+        // 无 token → 放行(permitAll 策略,由后续 @PreAuthorize 或 endpoint 自行检查)
+        if (token == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        // token 存在但 JWT 本身无效/过期 → 返回 10401(对应 Python security.py L48: "认证失败,请登录后再试")
+        // 当启用了滑动过期且 Redis 会话仍然存活时,允许过期 token 继续使用
+        Claims expiredClaims = null;
+        try {
+            jwtUtils.parseToken(token);
+        } catch (ExpiredJwtException e) {
+            log.debug("JWT 已过期: {}", e.getMessage());
+            if (slidingExpire) {
+                // 提取过期 token 的 claims,后续通过 Redis 检查是否仍有效
+                expiredClaims = e.getClaims();
+            } else {
+                writeAuthError(response, ErrorCode.AUTH_EXPIRED);
+                return;
+            }
+        } catch (Exception e) {
+            log.debug("JWT 无效: {}", e.getMessage());
+            writeAuthError(response, ErrorCode.AUTH_TOKEN_INVALID);
+            return;
+        }
+
+        // 拒绝 refresh token 作为认证凭证(对应 Python is_refresh 校验)
+        // 当 token 已过期时从 expiredClaims 提取 claims,避免二次解析抛异常
+        boolean tokenIsRefresh;
+        String sessionInfoJson;
+        try {
+            if (expiredClaims != null) {
+                Boolean isRefresh = expiredClaims.get("is_refresh", Boolean.class);
+                tokenIsRefresh = isRefresh != null && isRefresh;
+                sessionInfoJson = expiredClaims.getSubject();
+            } else {
+                tokenIsRefresh = jwtUtils.isRefreshToken(token);
+                sessionInfoJson = jwtUtils.getSub(token);
+            }
+        } catch (Exception e) {
+            log.debug("解析 token claims 失败: {}", e.getMessage());
+            writeAuthError(response, ErrorCode.AUTH_TOKEN_INVALID);
+            return;
+        }
+
+        if (tokenIsRefresh) {
+            log.debug("refresh token 不能用于 API 认证");
+            writeAuthError(response, ErrorCode.AUTH_ILLEGAL);
+            return;
+        }
+
+        // 从 JWT sub 字段解析 session_info 并查 Redis 在线状态
+        Long userId;
+        try {
+            if (StrUtil.isBlank(sessionInfoJson)) {
+                writeAuthError(response, ErrorCode.AUTH_INVALID);
+                return;
+            }
+
+            SessionInfo sessionInfo = SessionInfo.fromJson(sessionInfoJson);
+            String sessionId = sessionInfo.getSessionId();
+            userId = sessionInfo.getUserId();
+
+            if (StrUtil.isBlank(sessionId) || userId == null) {
+                writeAuthError(response, ErrorCode.AUTH_INVALID);
+                return;
+            }
+
+            // 检查 Redis 中 access_token 是否仍然存在(对应 Python Redis 在线检查)
+            String redisKey = RedisInitKeyConfig.ACCESS_TOKEN.getKey() + ":" + sessionId;
+            Boolean exists = redisTemplate.hasKey(redisKey);
+            if (Boolean.FALSE.equals(exists)) {
+                log.debug("会话已失效: session_id={}", sessionId);
+                writeAuthError(response, ErrorCode.AUTH_EXPIRED);
+                return;
+            }
+
+            // 滑动过期:续期 Redis TTL(对应 Python TOKEN_SLIDING_EXPIRE)
+            if (slidingExpire) {
+                long accessExpireSec = jwtUtils.getAccessTokenExpireSeconds();
+                long refreshExpireSec = jwtUtils.getRefreshTokenExpireSeconds();
+
+                redisTemplate.expire(redisKey, Duration.ofSeconds(accessExpireSec));
+                redisTemplate.expire(
+                        RedisInitKeyConfig.REFRESH_TOKEN.getKey() + ":" + sessionId,
+                        Duration.ofSeconds(refreshExpireSec));
+            }
+
+        } catch (Exception e) {
+            log.debug("解析 session_info 失败: {}", e.getMessage());
+            writeAuthError(response, ErrorCode.AUTH_FAILED);
+            return;
+        }
+
+        // 设置认证上下文
+        if (SecurityContextHolder.getContext().getAuthentication() == null) {
+            LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUserId(userId);
+            if (loginUser != null && loginUser.isEnabled()) {
+                UsernamePasswordAuthenticationToken authToken =
+                        new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+                SecurityContextHolder.getContext().setAuthentication(authToken);
+            } else {
+                // 用户不存在或被停用(对应 Python dependencies.py L221-223)
+                log.debug("用户不存在或已停用: userId={}", userId);
+                writeAuthError(response, ErrorCode.AUTH_FAILED);
+                return;
+            }
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    /** 写入 401 + code 10401 错误响应(对应 Python security.py L48) */
+    private void writeAuthError(HttpServletResponse response, ErrorCode errorCode) throws IOException {
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        Result<Void> result = Result.of(errorCode.getCode(), errorCode.getMsg());
+        response.getWriter().write(objectMapper.writeValueAsString(result));
+    }
+
+    private String extractToken(HttpServletRequest request) {
+        String header = request.getHeader("Authorization");
+        if (StringUtils.hasText(header) && header.toLowerCase().startsWith("bearer ")) {
+            return header.substring(7);
+        }
+        // 兼容 query 参数传 token(WebSocket 等场景)
+        String paramToken = request.getParameter("token");
+        if (StringUtils.hasText(paramToken)) {
+            return paramToken;
+        }
+        return null;
+    }
+}

+ 58 - 0
java/src/main/java/com/payment/platform/core/security/LoginUser.java

@@ -0,0 +1,58 @@
+package com.payment.platform.core.security;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Getter
+@Setter
+public class LoginUser implements UserDetails {
+
+    private Long userId;
+    private Long tenantId;
+    private String username;
+    private String password;
+    private String nickname;
+    private String status;
+    private List<GrantedAuthority> authorities;
+    private Boolean isSuperuser;
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return authorities;
+    }
+
+    @Override
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return "0".equals(status);
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return "0".equals(status);
+    }
+}

+ 78 - 0
java/src/main/java/com/payment/platform/core/security/PermissionCheck.java

@@ -0,0 +1,78 @@
+package com.payment.platform.core.security;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 权限检查组件,供 SpEL @PreAuthorize 表达式使用。
+ *
+ * 用法示例:
+ *   @PreAuthorize("@perm.hasAny('module_payment:account:transfer')")
+ *   @PreAuthorize("@perm.hasAll('module_payment:enterprise:delete')")
+ *
+ * 对应 Python 的 AuthPermission(["module:resource:action"]) 模式。
+ * 超级管理员(ROLE_SUPERUSER)自动放行所有检查。
+ */
+@Component("perm")
+public class PermissionCheck {
+
+    /**
+     * 检查当前用户是否拥有任意一个指定权限(OR 逻辑)。
+     * 对应 Python: any(perm in user_permissions for perm in self.permissions)
+     */
+    public boolean hasAny(String... permissions) {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        if (auth == null || !auth.isAuthenticated()) {
+            return false;
+        }
+
+        Set<String> authorities = auth.getAuthorities().stream()
+                .map(GrantedAuthority::getAuthority)
+                .collect(Collectors.toSet());
+
+        // 超级管理员直接放行
+        if (authorities.contains("ROLE_SUPERUSER")) {
+            return true;
+        }
+
+        return Arrays.stream(permissions).anyMatch(authorities::contains);
+    }
+
+    /**
+     * 检查当前用户是否拥有所有指定权限(AND 逻辑)。
+     * 通常用于需要多个权限同时满足的场景。
+     */
+    public boolean hasAll(String... permissions) {
+        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+        if (auth == null || !auth.isAuthenticated()) {
+            return false;
+        }
+
+        Set<String> authorities = auth.getAuthorities().stream()
+                .map(GrantedAuthority::getAuthority)
+                .collect(Collectors.toSet());
+
+        // 超级管理员直接放行
+        if (authorities.contains("ROLE_SUPERUSER")) {
+            return true;
+        }
+
+        return Arrays.stream(permissions).allMatch(authorities::contains);
+    }
+
+    /**
+     * 检查当前用户是否拥有指定角色。
+     * 简化写法:spring 内置的 hasRole('SUPERUSER') 也可以,但容易与
+     * 数据库 "ROLE_" 前缀逻辑混淆,保留此方法作为显式入口。
+     */
+    public boolean hasRole(String role) {
+        String roleName = role.startsWith("ROLE_") ? role : "ROLE_" + role;
+        return hasAny(roleName);
+    }
+}

+ 105 - 0
java/src/main/java/com/payment/platform/core/security/SecurityConfig.java

@@ -0,0 +1,105 @@
+package com.payment.platform.core.security;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.List;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+    private final JwtAuthFilter jwtAuthFilter;
+    private final TenantApiKeyAuthFilter apiKeyAuthFilter;
+
+    /**
+     * 无需认证的白名单 — 对应 Python TOKEN_REQUEST_PATH_EXCLUDE + Swagger资源
+     * 注意: Spring Security 匹配的 URI 取决于 AntPathRequestMatcher 的行为,
+     * 可能与 context-path 的剥离方式有关。此处同时保留带/不带前缀的版本。
+     */
+    /** 对应 Python white_api_list_path + Swagger */
+    private static final String[] WHITE_LIST = {
+            "/system/auth/login",
+            "/system/auth/login/mini",
+            "/system/auth/login/sms",
+            "/system/auth/sms-code",
+            "/system/auth/captcha/get",
+            "/system/auth/logout",
+            "/system/auth/token/refresh",
+            "/system/auth/auto-login",
+            "/system/auth/auto-login/users",
+            "/system/auth/auto-login/token",
+            "/system/user/current/info",
+            "/system/param/info",
+            "/system/notice/available",
+            "/payment/notify/health",
+            "/payment/notify/alipay",
+            "/v3/api-docs/**",
+            "/swagger-ui/**",
+            "/swagger-ui.html",
+            "/doc.html",
+            "/webjars/**",
+            "/static/**",
+            "/favicon.ico",
+    };
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http
+            .csrf(AbstractHttpConfigurer::disable)
+            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+            .authorizeHttpRequests(auth -> auth
+                .requestMatchers(WHITE_LIST).permitAll()
+                .anyRequest().authenticated()
+            )
+            // JWT 认证 + ApiKey 认证, 二者均仅对各自匹配路径生效 (内部 shouldNotFilter)
+            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
+            .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class)
+            .formLogin(AbstractHttpConfigurer::disable)
+            .httpBasic(AbstractHttpConfigurer::disable);
+
+        return http.build();
+    }
+
+    @Bean
+    public CorsConfigurationSource corsConfigurationSource() {
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowedOriginPatterns(List.of("*"));
+        config.setAllowedMethods(List.of("*"));
+        config.setAllowedHeaders(List.of("*"));
+        config.setAllowCredentials(true);
+        config.setExposedHeaders(List.of("X-Request-ID"));
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config);
+        return source;
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+}

+ 52 - 0
java/src/main/java/com/payment/platform/core/security/SessionInfo.java

@@ -0,0 +1,52 @@
+package com.payment.platform.core.security;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.OffsetDateTime;
+
+/**
+ * 会话信息 — 序列化为 JSON 存入 JWT sub 字段,对应 Python OnlineOutSchema
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SessionInfo {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private String sessionId;
+    private Long userId;
+    private String name;
+    private String userName;
+    private String ipaddr;
+    private String loginLocation;
+    private String os;
+    private String browser;
+    private String loginTime;
+    private String loginType;
+    private Long tenantId;
+
+    public String toJson() {
+        try {
+            return MAPPER.writeValueAsString(this);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException("序列化SessionInfo失败", e);
+        }
+    }
+
+    public static SessionInfo fromJson(String json) {
+        try {
+            return MAPPER.readValue(json, SessionInfo.class);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException("反序列化SessionInfo失败", e);
+        }
+    }
+}

+ 144 - 0
java/src/main/java/com/payment/platform/core/security/SignatureGenerator.java

@@ -0,0 +1,144 @@
+package com.payment.platform.core.security;
+
+import cn.hutool.core.util.StrUtil;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HexFormat;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 签名生成器 — 完整镜像 Python app/plugin/module_payment/apikey/service.py L16-76 的 SignatureGenerator
+ *
+ * 签名流程 (与 Python 逐行对应):
+ *   1. 过滤: 排除 sign/空值/空列表/空字典 (Python L39-47)
+ *   2. ASCII 字典序排序 (Python L50)
+ *   3. 值编码: dict/list → JSON dump → URL encode; 标量 → URL encode (Python L53-56)
+ *   4. 拼接为 key1=val1&key2=val2 (Python L58)
+ *   5. HMAC-SHA256(api_secret, sign_str) → hex 小写 (Python L59-63)
+ *
+ * 验证流程 (Python L66-76):
+ *   重新生成 → 常量时间比较
+ */
+public final class SignatureGenerator {
+
+    private SignatureGenerator() {}
+
+    /**
+     * 生成签名 — 对应 Python SignatureGenerator.generate_signature (L20-64)
+     *
+     * @param apiSecret     API 密钥 (对应 Python api_secret)
+     * @param requestData   请求参数字典 (对应 Python request_data)
+     * @param excludeParams 需要排除的参数 (对应 Python exclude_params, 默认 ("sign",))
+     * @return 签名字符串 (小写 hex, 对应 Python signature)
+     */
+    public static String generateSignature(String apiSecret,
+                                           Map<String, Object> requestData,
+                                           String... excludeParams) {
+        // ---- 1. 过滤参数 (Python L39-47) ----
+        TreeMap<String, String> filtered = new TreeMap<>();
+        outer:
+        for (Map.Entry<String, Object> entry : requestData.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+
+            // 排除指定参数 (Python L41-42)
+            for (String ex : excludeParams) {
+                if (key.equals(ex)) {
+                    continue outer;
+                }
+            }
+
+            // 排除 null / 空字符串 (Python L43-44)
+            if (value == null || (value instanceof String && StrUtil.isEmpty((String) value))) {
+                continue;
+            }
+
+            // 排除空集合 (Python L45-46)
+            if (value instanceof Map && ((Map<?, ?>) value).isEmpty()) {
+                continue;
+            }
+            if (value instanceof Iterable && !((Iterable<?>) value).iterator().hasNext()) {
+                continue;
+            }
+
+            // ---- 3. 值编码 (Python L53-56) ----
+            String encodedValue;
+            if (value instanceof Map || value instanceof Iterable) {
+                // dict/list → JSON dump → URL encode (Python L53-54)
+                String jsonStr = cn.hutool.json.JSONUtil.toJsonStr(value);
+                encodedValue = urlEncode(jsonStr);
+            } else {
+                // 标量 → URL encode (Python L55)
+                encodedValue = urlEncode(String.valueOf(value));
+            }
+
+            filtered.put(key, encodedValue);
+        }
+
+        // ---- 4. 拼接为 key=value&... (Python L56-58) ----
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, String> entry : filtered.entrySet()) {
+            if (sb.length() > 0) sb.append("&");
+            sb.append(entry.getKey()).append("=").append(entry.getValue());
+        }
+        String signStr = sb.toString();
+
+        // ---- 5. HMAC-SHA256 → hex (Python L59-63) ----
+        try {
+            Mac mac = Mac.getInstance("HmacSHA256");
+            SecretKeySpec spec = new SecretKeySpec(
+                    apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+            mac.init(spec);
+            byte[] hash = mac.doFinal(signStr.getBytes(StandardCharsets.UTF_8));
+            return HexFormat.of().formatHex(hash);
+        } catch (Exception e) {
+            throw new RuntimeException("HMAC-SHA256签名生成失败", e);
+        }
+    }
+
+    /**
+     * 验证签名 — 对应 Python SignatureGenerator.verify_signature (L66-76)
+     *
+     * @param apiSecret  API 密钥
+     * @param requestData 请求参数
+     * @param signature   待验证的签名
+     * @return true 如果签名匹配
+     */
+    public static boolean verifySignature(String apiSecret,
+                                          Map<String, Object> requestData,
+                                          String signature) {
+        String expected = generateSignature(apiSecret, requestData);
+        // 常量时间比较防止 timing attack (Python L76)
+        return constantTimeEquals(expected, signature);
+    }
+
+    // ---- 内部工具 ----
+
+    /**
+     * URL 编码 — 对应 Python urllib.parse.quote(str(value), safe="") (L55)
+     */
+    static String urlEncode(String value) {
+        return URLEncoder.encode(value, StandardCharsets.UTF_8)
+                .replace("+", "%20");   // RFC 3986: 空格用 %20, 不用 +
+    }
+
+    /**
+     * 常量时间字符串比较
+     */
+    static boolean constantTimeEquals(String a, String b) {
+        if (a == null || b == null) return false;
+        byte[] ba = a.getBytes(StandardCharsets.UTF_8);
+        byte[] bb = b.getBytes(StandardCharsets.UTF_8);
+        if (ba.length != bb.length) return false;
+
+        int diff = 0;
+        for (int i = 0; i < ba.length; i++) {
+            diff |= ba[i] ^ bb[i];
+        }
+        return diff == 0;
+    }
+}

+ 322 - 0
java/src/main/java/com/payment/platform/core/security/TenantApiKeyAuthFilter.java

@@ -0,0 +1,322 @@
+package com.payment.platform.core.security;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.payment.platform.common.enums.ErrorCode;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.payment.apikey.entity.TenantApiKeyEntity;
+import com.payment.platform.module.payment.apikey.entity.TenantApiLogEntity;
+import com.payment.platform.module.payment.apikey.mapper.TenantApiKeyMapper;
+import com.payment.platform.module.payment.apikey.mapper.TenantApiLogMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.util.Map;
+
+/**
+ * 租户 API Key 认证过滤器 — 完整镜像 Python app/core/apikey.py L16-136 的 TenantApiKeyAuth
+ *
+ * Python 认证流程逐行对照:
+ *
+ *   L33-34   request.state.start_time = time.time()
+ *            ↓ 记录请求到达时间戳, 用于计算响应耗时
+ *
+ *   L37-41   检查 Authorization header, 空 → 401 "Authorization header required"
+ *            filter 中: extractApiKey() 返回 null → write401
+ *
+ *   L44-49   检查 Signature header,   空 → 401 "Signature header required"
+ *            filter 中: extractSignature() 返回 null → write401
+ *
+ *   L55-57   Authorization 必须以 "ApiKey " 开头, 否则 → 401 "Invalid authorization format"
+ *            filter 中: extractApiKey() 检查前缀
+ *
+ *   L61      api_key = authorization[7:]  (去掉 "ApiKey " 前缀)
+ *
+ *   L63-66   构造临时 AuthSchema, db=db, tenant_id=1, check_data_scope=False
+ *            调用 TenantApiKeyService.validate_api_key() 查 DB
+ *            条件: api_key=给定值 AND status="0"
+ *            不存在 → 401 "Invalid API Key"
+ *            Java: 直接用 Mapper 查, 条件 = apiKey AND status = "0"
+ *
+ *   L67-79   [注释掉] 无效 key 时记录日志 (Python 中已注释, 此处跳过)
+ *
+ *   L87-90   读取 request body 为 JSON (request_data = await request.json())
+ *            Java: ContentCachingRequestWrapper → getContentAsByteArray → ObjectMapper
+ *
+ *   L92-107  TenantApiKeyService.verify_signature(api_secret, request_data, signature)
+ *            失败 → 401 "Invalid Signature"  (并 [注释掉] 记录日志)
+ *
+ *   L109-119 log_api_call() 记录成功调用: api_key_id, tenant_id, endpoint, method,
+ *            request_ip, response_code=200, response_time
+ *            Java: 异步记录 TenantApiLogEntity
+ *
+ *   L122     update_last_used() 刷新 last_used_at
+ *
+ *   L128-135 将 request_data 解析为业务 Schema, 构造 ApiKeyPayload{T} 返回
+ *            Java: 将 tenantId / apiKey 写入 request attribute, 由 Controller 读取
+ *
+ * ———————————————————————————————————————
+ * 作用范围 (与 Python 一致):
+ *   仅拦截 /payment/openapi/account/** 开放接口
+ *   /payment/openapi/conf 等管理接口走 JWT 认证, 不经过此 filter
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
+
+    /** 仅对 OpenAPI 账户接口生效 (对应 Python OpenapiRouter 中使用 TenantApiKeyAuth 的路由) */
+    private static final String[] OPENAPI_ACCOUNT_PATHS = {
+            "/payment/openapi/account/transfer",
+            "/payment/openapi/account/transfer/query",
+            "/payment/openapi/account/balance/query",
+    };
+
+    private static final String AUTH_PREFIX = "ApiKey ";
+
+    private final TenantApiKeyMapper apiKeyMapper;
+    private final TenantApiLogMapper apiLogMapper;
+    private final ObjectMapper objectMapper;
+
+    // ========================= Filter 入口 =========================
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        // 仅拦截 OpenAPI 账户端点 (Python: OpenapiRouter 中使用了 TenantApiKeyAuth 的路由)
+        String path = request.getRequestURI();
+        for (String p : OPENAPI_ACCOUNT_PATHS) {
+            if (path.equals(p)) {
+                return false; // 需要过滤
+            }
+        }
+        return true; // 其他路径不过滤
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request,
+                                    HttpServletResponse response,
+                                    FilterChain filterChain) throws ServletException, IOException {
+
+        // ---- 记录开始时间 (Python L33-34) ----
+        long startTime = System.currentTimeMillis();
+
+        // 包装 request 以便可以多次读取 body (Python L87-90 中读了一次 JSON body)
+        ContentCachingRequestWrapper wrappedRequest = wrapRequest(request);
+
+        // ---- 1. 提取 Authorization header (Python L37-41) ----
+        String apiKey = extractApiKey(wrappedRequest);
+        if (apiKey == null) {
+            // Python L41: "Authorization header required"
+            writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
+                    "Authorization header required / 缺少 Authorization 请求头");
+            return;
+        }
+
+        // ---- 2. 提取 Signature header (Python L44-49) ----
+        String signature = extractSignature(wrappedRequest);
+        if (signature == null) {
+            // Python L49: "Signature header required"
+            writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
+                    "Signature header required / 缺少 Signature 请求头");
+            return;
+        }
+
+        // ---- 3. 验证 API Key (Python L61-66) ----
+        //     Python 中: validate_api_key(auth, api_key) → get_by_api_key(api_key)
+        //     crud.py L27-33: 查询条件 api_key=给定值 AND status="0"
+        TenantApiKeyEntity keyEntity = apiKeyMapper.selectOne(
+                new LambdaQueryWrapper<TenantApiKeyEntity>()
+                        .eq(TenantApiKeyEntity::getApiKey, apiKey)
+                        .eq(TenantApiKeyEntity::getStatus, "0"));
+        if (keyEntity == null) {
+            // Python L81: "Invalid API Key"
+            logApiCall(null, null, request, response, 401, startTime);
+            writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
+                    "Invalid API Key / 无效的 API Key");
+            return;
+        }
+
+        // ---- 3b. 检查过期 (Python crud.py L27-33 通过 status="0" 隐含校验; 此处显式检查 expired_at) ----
+        if (keyEntity.getExpiredAt() != null
+                && keyEntity.getExpiredAt().isBefore(OffsetDateTime.now())) {
+            logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 401, startTime);
+            writeAuthError(response, ErrorCode.AUTH_EXPIRED.getCode(),
+                    "API Key expired / API Key 已过期");
+            return;
+        }
+
+        // ---- 4. 读取请求体 JSON (Python L87-90) ----
+        Map<String, Object> requestData = readRequestBody(wrappedRequest);
+
+        // ---- 5. 验证签名 HMAC-SHA256 (Python L92-107) ----
+        //     Python: TenantApiKeyService.verify_signature(api_key_obj.api_secret, request_data, signature)
+        //     → SignatureGenerator.verify_signature(api_secret, request_data, signature)
+        if (!SignatureGenerator.verifySignature(keyEntity.getApiSecret(), requestData, signature)) {
+            // Python L106: "Invalid Signature"
+            logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 401, startTime);
+            writeAuthError(response, ErrorCode.AUTH_FAILED.getCode(),
+                    "Invalid Signature / 签名验证失败");
+            return;
+        }
+
+        // ---- 6. 认证成功: 写入 request attributes 供 Controller 使用 ----
+        //     Python L130-135: 构造 ApiKeyPayload(api_key, api_secret, tenant_id, auth, data)
+        //     Java: 用 request attributes 传递
+        wrappedRequest.setAttribute("openapi.tenantId", keyEntity.getTenantId());
+        wrappedRequest.setAttribute("openapi.apiKey", apiKey);
+        wrappedRequest.setAttribute("openapi.apiKeyId", keyEntity.getId());
+
+        // ---- 7. 记录成功日志 (Python L109-119) ----
+        logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 200, startTime);
+
+        // ---- 8. 更新 last_used_at (Python L122) ----
+        updateLastUsed(keyEntity);
+
+        filterChain.doFilter(wrappedRequest, response);
+    }
+
+    // ========================= 从请求中提取信息 (镜像 Python L37-61) =========================
+
+    /**
+     * 提取 API Key — 对应 Python L37-61
+     *   Authorization header 值: "ApiKey {key}"
+     *   返回去掉前缀后的 key 文本; 不符合格式返回 null
+     */
+    private String extractApiKey(HttpServletRequest request) {
+        // Python L37: authorization = request.headers.get("Authorization", None)
+        String auth = request.getHeader("Authorization");
+        if (StrUtil.isBlank(auth)) {
+            return null;
+        }
+        // Python L55: if not authorization.startswith("ApiKey ")
+        if (!auth.startsWith(AUTH_PREFIX)) {
+            return null;
+        }
+        // Python L61: api_key = authorization[7:]
+        String key = auth.substring(AUTH_PREFIX.length()).trim();
+        return StrUtil.isBlank(key) ? null : key;
+    }
+
+    /**
+     * 提取签名 — 对应 Python L44-49
+     *   Signature header 值: HMAC-SHA256 hex 字符串
+     */
+    private String extractSignature(HttpServletRequest request) {
+        // Python L45: signature = request.headers.get("Signature", None)
+        String sig = request.getHeader("Signature");
+        return StrUtil.isBlank(sig) ? null : sig.trim();
+    }
+
+    /**
+     * 读取请求体 JSON — 对应 Python L87-90
+     *   request_data = await request.json()
+     */
+    private Map<String, Object> readRequestBody(ContentCachingRequestWrapper request) {
+        byte[] content = request.getContentAsByteArray();
+        if (content.length == 0) {
+            return Map.of();
+        }
+        try {
+            return objectMapper.readValue(content, new TypeReference<Map<String, Object>>() {});
+        } catch (Exception e) {
+            log.debug("请求体非 JSON 格式, 返回空 map: {}", e.getMessage());
+            return Map.of();
+        }
+    }
+
+    // ========================= 调用日志 (镜像 Python service.py L155-176) =========================
+
+    /**
+     * 记录 API 调用日志 — 对应 Python TenantApiKeyService.log_api_call() (service.py L155-176)
+     *
+     * Python 参数: auth, api_key_id, tenant_id, endpoint, method, request_ip,
+     *              request_data, response_code, start_time
+     * 计算 response_time = (time.time() - start_time) * 1000 (毫秒)
+     */
+    private void logApiCall(Long apiKeyId, Long tenantId,
+                            HttpServletRequest request, HttpServletResponse response,
+                            int responseCode, long startTime) {
+        try {
+            TenantApiLogEntity logEntry = new TenantApiLogEntity();
+            logEntry.setApiKeyId(apiKeyId);
+            if (tenantId != null) logEntry.setTenantId(tenantId);
+            logEntry.setEndpoint(request.getRequestURI());     // Python L160: endpoint
+            logEntry.setMethod(request.getMethod());            // Python L161: method
+            logEntry.setRequestIp(getClientIp(request));        // Python L162: request_ip
+            logEntry.setResponseCode(responseCode);             // Python L164: response_code
+            // Python L167: response_time = (time.time() - start_time) * 1000  (毫秒)
+            float responseTimeMs = (System.currentTimeMillis() - startTime);
+            logEntry.setResponseTime(responseTimeMs);
+            // Python L173: request_data = None (避免记录敏感数据)
+            logEntry.setRequestData(null);
+
+            apiLogMapper.insert(logEntry);
+        } catch (Exception e) {
+            // 日志记录失败不应阻断主流程
+            log.warn("API调用日志记录失败: {}", e.getMessage());
+        }
+    }
+
+    /**
+     * 更新最后使用时间 — 对应 Python service.py L179-180
+     */
+    private void updateLastUsed(TenantApiKeyEntity entity) {
+        try {
+            entity.setLastUsedAt(OffsetDateTime.now());
+            apiKeyMapper.updateById(entity);
+        } catch (Exception e) {
+            log.warn("更新 last_used_at 失败: {}", e.getMessage());
+        }
+    }
+
+    // ========================= 客户端 IP =========================
+
+    /**
+     * 获取客户端真实 IP — 对应 Python request.client.host (apikey.py L73/L99/L116)
+     */
+    private String getClientIp(HttpServletRequest request) {
+        String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP"};
+        for (String h : headers) {
+            String ip = request.getHeader(h);
+            if (StrUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
+                return ip.split(",")[0].trim();
+            }
+        }
+        return request.getRemoteAddr();
+    }
+
+    // ========================= 错误响应 =========================
+
+    /**
+     * 写入 401 + JSON body 错误响应 (与 JwtAuthFilter.writeAuthError 一致)
+     */
+    private void writeAuthError(HttpServletResponse response, int code, String msg) throws IOException {
+        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        Result<Void> result = Result.of(code, msg);
+        response.getWriter().write(objectMapper.writeValueAsString(result));
+    }
+
+    // ========================= 工具 =========================
+
+    private ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
+        if (request instanceof ContentCachingRequestWrapper) {
+            return (ContentCachingRequestWrapper) request;
+        }
+        return new ContentCachingRequestWrapper(request);
+    }
+}

+ 91 - 0
java/src/main/java/com/payment/platform/core/security/UserDetailsServiceImpl.java

@@ -0,0 +1,91 @@
+package com.payment.platform.core.security;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.payment.platform.module.system.menu.mapper.MenuMapper;
+import com.payment.platform.module.system.user.entity.UserEntity;
+import com.payment.platform.module.system.user.mapper.UserMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserDetailsServiceImpl implements UserDetailsService {
+
+    private final UserMapper userMapper;
+    private final MenuMapper menuMapper;
+
+    /**
+     * Spring Security 标准方法(按用户名加载)
+     */
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        UserEntity user = userMapper.selectOne(
+                new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, username));
+        if (user == null) {
+            throw new UsernameNotFoundException("用户不存在: " + username);
+        }
+        return buildLoginUser(user);
+    }
+
+    /**
+     * 按用户ID加载(JWT filter使用)
+     */
+    public UserDetails loadUserByUserId(Long userId) {
+        UserEntity user = userMapper.selectById(userId);
+        if (user == null) {
+            return null;
+        }
+        return buildLoginUser(user);
+    }
+
+    private LoginUser buildLoginUser(UserEntity user) {
+        LoginUser loginUser = new LoginUser();
+        loginUser.setUserId(user.getId());
+        loginUser.setTenantId(user.getTenantId());
+        loginUser.setUsername(user.getUsername());
+        loginUser.setPassword(user.getPassword());
+        loginUser.setNickname(user.getName());
+        loginUser.setStatus(user.getStatus());
+        loginUser.setIsSuperuser(user.getIsSuperuser() != null && user.getIsSuperuser());
+
+        // 角色权限 — 从 sys_user_roles → sys_role_menus → sys_menu.permission 加载
+        // 对应 Python AuthPermission: user_permissions = {menu.permission for ...}
+        List<GrantedAuthority> authorities = new ArrayList<>();
+
+        // 所有已认证用户具备基础角色
+        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+
+        // 超级管理员角色(对应 Python is_superuser 旁路)
+        if (loginUser.getIsSuperuser() != null && loginUser.getIsSuperuser()) {
+            authorities.add(new SimpleGrantedAuthority("ROLE_SUPERUSER"));
+        }
+
+        // 从数据库加载菜单权限标识,格式如 "module_payment:account:transfer"
+        try {
+            List<String> permissions = menuMapper.selectPermissionsByUserId(user.getId());
+            if (permissions != null) {
+                for (String perm : permissions) {
+                    if (perm != null && !perm.isBlank()) {
+                        authorities.add(new SimpleGrantedAuthority(perm.trim()));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.warn("加载用户权限失败: userId={}, error={}", user.getId(), e.getMessage());
+        }
+
+        loginUser.setAuthorities(authorities);
+
+        return loginUser;
+    }
+}

+ 74 - 0
java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java

@@ -0,0 +1,74 @@
+package com.payment.platform.core.tenant;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import com.payment.platform.core.security.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * 多租户拦截器 — 自动在 SELECT/INSERT/UPDATE/DELETE 中追加 tenant_id 条件
+ * 对应 Python TenantMixin + Permission.filter_query 中的租户过滤
+ */
+@Slf4j
+@Component
+public class TenantInnerInterceptor extends TenantLineInnerInterceptor {
+
+    private static final Set<String> IGNORE_TABLES = Set.of(
+            "sys_tenant",         // 租户表自身
+            "sys_user",           // 用户表(登录时无租户上下文)
+            "sys_role",           // 角色表
+            "sys_menu",           // 菜单表
+            "sys_dept",           // 部门表
+            "sys_position",       // 岗位表
+            "sys_dict_type",      // 字典类型
+            "sys_dict_data",      // 字典数据
+            "sys_param",          // 系统参数
+            "sys_notice",         // 通知公告
+            "sys_log"             // 操作日志
+    );
+
+    public TenantInnerInterceptor() {
+        super(new TenantLineHandler() {
+            @Override
+            public Expression getTenantId() {
+                Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+                if (auth != null && auth.getPrincipal() instanceof LoginUser user) {
+                    if (user.getTenantId() != null) {
+                        return new LongValue(user.getTenantId());
+                    }
+                }
+                // 未登录或无租户 → 返回 0,配合 ignore 表策略
+                return new LongValue(0);
+            }
+
+            @Override
+            public String getTenantIdColumn() {
+                return "tenant_id";
+            }
+
+            @Override
+            public boolean ignoreTable(String tableName) {
+                // 管理员跳过租户隔离,可查看所有租户数据
+                Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+                if (auth != null && auth.getPrincipal() instanceof LoginUser user) {
+                    if (user.getIsSuperuser() != null && user.getIsSuperuser()) {
+                        return true;
+                    }
+                }
+                String cleaned = StrUtil.cleanBlank(tableName);
+                if (cleaned != null && cleaned.contains(".")) {
+                    cleaned = cleaned.substring(cleaned.lastIndexOf('.') + 1);
+                }
+                return IGNORE_TABLES.contains(cleaned);
+            }
+        });
+    }
+}

+ 38 - 0
java/src/main/java/com/payment/platform/module/common/controller/FileController.java

@@ -0,0 +1,38 @@
+package com.payment.platform.module.common.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.common.service.FileService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/file")
+@RequiredArgsConstructor
+public class FileController {
+
+    private final FileService fileService;
+
+    @PostMapping("/upload")
+    public Result<Map<String, String>> upload(@RequestParam("file") MultipartFile file) {
+        return Result.ok(fileService.upload(file));
+    }
+
+    @PostMapping("/download")
+    public ResponseEntity<InputStreamResource> download(@RequestBody Map<String, String> body) {
+        String filePath = body.get("file_path");
+        InputStream is = fileService.download(filePath);
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION,
+                        "attachment; filename=\"" + filePath.substring(filePath.lastIndexOf('/') + 1) + "\"")
+                .contentType(MediaType.APPLICATION_OCTET_STREAM)
+                .body(new InputStreamResource(is));
+    }
+}

+ 16 - 0
java/src/main/java/com/payment/platform/module/common/controller/HealthController.java

@@ -0,0 +1,16 @@
+package com.payment.platform.module.common.controller;
+
+import com.payment.platform.common.response.Result;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+public class HealthController {
+
+    @GetMapping("/payment/notify/health")
+    public Result<Map<String, String>> health() {
+        return Result.ok(Map.of("status", "ok"));
+    }
+}

+ 55 - 0
java/src/main/java/com/payment/platform/module/common/service/FileService.java

@@ -0,0 +1,55 @@
+package com.payment.platform.module.common.service;
+
+import com.payment.platform.common.exception.BusinessException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Slf4j
+@Service
+public class FileService {
+
+    @Value("${file.upload-path:static/upload}")
+    private String uploadPath;
+
+    public Map<String, String> upload(MultipartFile file) {
+        try {
+            Path dir = Paths.get(uploadPath);
+            if (!Files.exists(dir)) Files.createDirectories(dir);
+            String filename = UUID.randomUUID() + "_" + file.getOriginalFilename();
+            Path target = dir.resolve(filename);
+            file.transferTo(target);
+
+            Map<String, String> result = new LinkedHashMap<>();
+            result.put("file_name", filename);
+            result.put("origin_name", file.getOriginalFilename());
+            result.put("file_path", target.toString());
+            result.put("file_url", "/static/upload/" + filename);
+            result.put("file_size", String.valueOf(file.getSize()));
+            return result;
+        } catch (IOException e) {
+            throw new BusinessException(400, "上传失败: " + e.getMessage());
+        }
+    }
+
+    public InputStream download(String filePath) {
+        try {
+            Path path = Paths.get(filePath);
+            if (!Files.exists(path))
+                throw new BusinessException(404, "文件不存在: " + filePath);
+            return Files.newInputStream(path);
+        } catch (IOException e) {
+            throw new BusinessException(500, "文件读取失败: " + e.getMessage());
+        }
+    }
+}

+ 48 - 0
java/src/main/java/com/payment/platform/module/example/demo/controller/DemoController.java

@@ -0,0 +1,48 @@
+package com.payment.platform.module.example.demo.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.example.demo.dto.DemoCreateDTO;
+import com.payment.platform.module.example.demo.dto.DemoVO;
+import com.payment.platform.module.example.demo.service.DemoService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/example/demo")
+@RequiredArgsConstructor
+public class DemoController {
+
+    private final DemoService demoService;
+
+    @GetMapping("/list")
+    public Result<PageResult<DemoVO>> list(@RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+                                            @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
+                                            @RequestParam(required = false) String name) {
+        return Result.ok(demoService.getPage(pageNo, pageSize, name));
+    }
+
+    @GetMapping("/detail/{id}")
+    public Result<DemoVO> detail(@PathVariable Long id) {
+        return Result.ok(demoService.getDetail(id));
+    }
+
+    @PostMapping("/create")
+    public Result<DemoVO> create(@Valid @RequestBody DemoCreateDTO dto) {
+        return Result.ok(demoService.create(dto));
+    }
+
+    @PutMapping("/update/{id}")
+    public Result<DemoVO> update(@PathVariable Long id, @Valid @RequestBody DemoCreateDTO dto) {
+        return Result.ok(demoService.update(id, dto));
+    }
+
+    @DeleteMapping("/delete")
+    public Result<Void> delete(@RequestBody List<Long> ids) {
+        demoService.delete(ids);
+        return Result.ok();
+    }
+}

+ 22 - 0
java/src/main/java/com/payment/platform/module/example/demo/dto/DemoCreateDTO.java

@@ -0,0 +1,22 @@
+package com.payment.platform.module.example.demo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class DemoCreateDTO {
+    @NotBlank(message = "名称不能为空")
+    @Schema(description = "名称")
+    private String name;
+
+    @NotBlank(message = "编码不能为空")
+    @Schema(description = "编码")
+    private String code;
+
+    @Schema(description = "排序")
+    private Integer order;
+
+    @Schema(description = "状态", defaultValue = "0")
+    private String status;
+}

+ 13 - 0
java/src/main/java/com/payment/platform/module/example/demo/dto/DemoVO.java

@@ -0,0 +1,13 @@
+package com.payment.platform.module.example.demo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class DemoVO extends com.payment.platform.common.dto.BaseVO {
+    private String name;
+    private String code;
+    private Integer order;
+}

+ 15 - 0
java/src/main/java/com/payment/platform/module/example/demo/entity/DemoEntity.java

@@ -0,0 +1,15 @@
+package com.payment.platform.module.example.demo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("demo_example")
+public class DemoEntity extends BaseEntity {
+    private String name;
+    private String code;
+    private Integer order;
+}

+ 8 - 0
java/src/main/java/com/payment/platform/module/example/demo/mapper/DemoMapper.java

@@ -0,0 +1,8 @@
+package com.payment.platform.module.example.demo.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.example.demo.entity.DemoEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DemoMapper extends BaseMapper<DemoEntity> {}

+ 68 - 0
java/src/main/java/com/payment/platform/module/example/demo/service/DemoService.java

@@ -0,0 +1,68 @@
+package com.payment.platform.module.example.demo.service;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.example.demo.dto.DemoCreateDTO;
+import com.payment.platform.module.example.demo.dto.DemoVO;
+import com.payment.platform.module.example.demo.entity.DemoEntity;
+import com.payment.platform.module.example.demo.mapper.DemoMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class DemoService {
+
+    private final DemoMapper demoMapper;
+
+    public DemoVO getDetail(Long id) {
+        return BeanUtil.copyProperties(requireOne(id), DemoVO.class);
+    }
+
+    public PageResult<DemoVO> getPage(int pageNo, int pageSize, String name) {
+        LambdaQueryWrapper<DemoEntity> w = new LambdaQueryWrapper<>();
+        if (StrUtil.isNotBlank(name)) w.like(DemoEntity::getName, name);
+        w.orderByAsc(DemoEntity::getOrder);
+        Page<DemoEntity> page = new Page<>(pageNo, pageSize);
+        Page<DemoEntity> result = demoMapper.selectPage(page, w);
+        return PageResult.of(pageNo, pageSize, result.getTotal(),
+                result.getRecords().stream().map(e -> BeanUtil.copyProperties(e, DemoVO.class)).collect(Collectors.toList()));
+    }
+
+    @Transactional
+    public DemoVO create(DemoCreateDTO dto) {
+        if (demoMapper.exists(new LambdaQueryWrapper<DemoEntity>().eq(DemoEntity::getName, dto.getName())))
+            throw new BusinessException(400, "创建失败,该记录已存在");
+        DemoEntity e = BeanUtil.copyProperties(dto, DemoEntity.class);
+        demoMapper.insert(e);
+        return BeanUtil.copyProperties(demoMapper.selectById(e.getId()), DemoVO.class);
+    }
+
+    @Transactional
+    public DemoVO update(Long id, DemoCreateDTO dto) {
+        DemoEntity exist = requireOne(id);
+        BeanUtil.copyProperties(dto, exist, "id");
+        demoMapper.updateById(exist);
+        return BeanUtil.copyProperties(demoMapper.selectById(id), DemoVO.class);
+    }
+
+    @Transactional
+    public void delete(List<Long> ids) {
+        if (ids.isEmpty()) throw new BusinessException(400, "删除失败,删除对象不能为空");
+        demoMapper.deleteBatchIds(ids);
+    }
+
+    private DemoEntity requireOne(Long id) {
+        DemoEntity e = demoMapper.selectById(id);
+        if (e == null) throw new BusinessException(404, "记录不存在");
+        return e;
+    }
+}

+ 50 - 0
java/src/main/java/com/payment/platform/module/example/demo01/controller/Demo01Controller.java

@@ -0,0 +1,50 @@
+package com.payment.platform.module.example.demo01.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/example/demo01")
+public class Demo01Controller {
+
+    // TODO: Inject Demo01Service when ready
+
+    @GetMapping("/list")
+    public Result<PageResult<Map<String, Object>>> list(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+            @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
+            @RequestParam(required = false) String name) {
+        // TODO: implement with real service
+        return Result.ok(PageResult.of(pageNo, pageSize, 0L, new ArrayList<>()));
+    }
+
+    @GetMapping("/detail/{id}")
+    public Result<Map<String, Object>> detail(@PathVariable Long id) {
+        // TODO: implement with real service
+        return Result.ok(new HashMap<>());
+    }
+
+    @PostMapping("/create")
+    public Result<Map<String, Object>> create(@RequestBody Map<String, Object> dto) {
+        // TODO: implement with real service
+        return Result.ok(new HashMap<>());
+    }
+
+    @PutMapping("/update/{id}")
+    public Result<Map<String, Object>> update(@PathVariable Long id, @RequestBody Map<String, Object> dto) {
+        // TODO: implement with real service
+        return Result.ok(new HashMap<>());
+    }
+
+    @DeleteMapping("/delete")
+    public Result<Void> delete(@RequestBody List<Long> ids) {
+        // TODO: implement with real service
+        return Result.ok();
+    }
+}

+ 92 - 0
java/src/main/java/com/payment/platform/module/generator/controller/GenController.java

@@ -0,0 +1,92 @@
+package com.payment.platform.module.generator.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.generator.dto.GenTableVO;
+import com.payment.platform.module.generator.service.GenService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/generator/gencode")
+@RequiredArgsConstructor
+public class GenController {
+
+    private final GenService genService;
+
+    // ==================== 表管理 ====================
+
+    @GetMapping("/list")
+    public Result<PageResult<GenTableVO>> list(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+            @RequestParam(name = "page_size", defaultValue = "10") int pageSize) {
+        return Result.ok(genService.getTablePage(pageNo, pageSize));
+    }
+
+    @GetMapping("/db/list")
+    public Result<PageResult<Map<String, Object>>> dbList(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+            @RequestParam(name = "page_size", defaultValue = "10") int pageSize) {
+        return Result.ok(genService.getDbTablePage(pageNo, pageSize));
+    }
+
+    @GetMapping("/detail/{table_id}")
+    public Result<Map<String, Object>> detail(@PathVariable("table_id") Long tableId) {
+        return Result.ok(genService.getTableDetail(tableId));
+    }
+
+    @PostMapping("/import")
+    public Result<GenTableVO> importTable(@RequestBody Map<String, String> body) {
+        String tableName = body.containsKey("table_name") ? body.get("table_name") : body.get("tableName");
+        return Result.ok(genService.importTable(tableName));
+    }
+
+    @PostMapping("/create")
+    public Result<GenTableVO> create(@RequestBody Map<String, Object> body) {
+        return Result.ok(genService.createTable(body));
+    }
+
+    @PutMapping("/update/{table_id}")
+    public Result<GenTableVO> update(@PathVariable("table_id") Long tableId, @RequestBody Map<String, Object> body) {
+        return Result.ok(genService.updateTable(tableId, body));
+    }
+
+    @DeleteMapping("/delete")
+    public Result<Void> delete(@RequestBody List<Long> ids) {
+        genService.deleteTables(ids);
+        return Result.ok();
+    }
+
+    // ==================== 代码生成 ====================
+
+    @GetMapping("/preview/{id}")
+    public Result<Map<String, String>> preview(@PathVariable Long id) {
+        return Result.ok(genService.previewCode(id));
+    }
+
+    @PostMapping("/output/{table_name}")
+    public Result<List<String>> output(@PathVariable("table_name") String tableName) {
+        return Result.ok(genService.generateCodeByTableName(tableName));
+    }
+
+    @PostMapping("/batch/output")
+    public Result<List<String>> batchOutput(@RequestBody List<Long> ids) {
+        return Result.ok(genService.batchGenerateCode(ids));
+    }
+
+    // ==================== 表结构同步 ====================
+
+    @PostMapping("/sync_db/{table_name}")
+    public Result<Void> syncDb(@PathVariable("table_name") String tableName) {
+        genService.syncTableByName(tableName);
+        return Result.ok();
+    }
+
+    @GetMapping("/sync_db/preview/{table_name}")
+    public Result<Map<String, Object>> syncDbPreview(@PathVariable("table_name") String tableName) {
+        return Result.ok(genService.previewTableSync(tableName));
+    }
+}

+ 17 - 0
java/src/main/java/com/payment/platform/module/generator/dto/GenCodePreviewDTO.java

@@ -0,0 +1,17 @@
+package com.payment.platform.module.generator.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class GenCodePreviewDTO {
+
+    @Schema(description = "表名称")
+    private String tableName;
+
+    @Schema(description = "生成包路径")
+    private String packageName;
+
+    @Schema(description = "生成模块名")
+    private String moduleName;
+}

+ 18 - 0
java/src/main/java/com/payment/platform/module/generator/dto/GenCodeResultDTO.java

@@ -0,0 +1,18 @@
+package com.payment.platform.module.generator.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class GenCodeResultDTO {
+
+    @Schema(description = "文件路径")
+    private String filePath;
+
+    @Schema(description = "文件内容")
+    private String fileContent;
+}

+ 94 - 0
java/src/main/java/com/payment/platform/module/generator/dto/GenTableColumnVO.java

@@ -0,0 +1,94 @@
+package com.payment.platform.module.generator.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+
+@Data
+public class GenTableColumnVO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "UUID")
+    private String uuid;
+
+    @Schema(description = "状态")
+    private String status;
+
+    @Schema(description = "描述")
+    private String description;
+
+    @Schema(description = "创建时间")
+    private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
+
+    @Schema(description = "创建者ID")
+    private Long createdId;
+
+    @Schema(description = "更新者ID")
+    private Long updatedId;
+
+    @Schema(description = "列名称")
+    private String columnName;
+
+    @Schema(description = "列描述")
+    private String columnComment;
+
+    @Schema(description = "列类型")
+    private String columnType;
+
+    @Schema(description = "列长度")
+    private String columnLength;
+
+    @Schema(description = "列默认值")
+    private String columnDefault;
+
+    @Schema(description = "是否主键")
+    private Boolean isPk;
+
+    @Schema(description = "是否自增")
+    private Boolean isIncrement;
+
+    @Schema(description = "是否允许为空")
+    private Boolean isNullable;
+
+    @Schema(description = "是否唯一")
+    private Boolean isUnique;
+
+    @Schema(description = "Python类型")
+    private String pythonType;
+
+    @Schema(description = "Python字段名")
+    private String pythonField;
+
+    @Schema(description = "是否为新增字段")
+    private Boolean isInsert;
+
+    @Schema(description = "是否编辑字段")
+    private Boolean isEdit;
+
+    @Schema(description = "是否列表字段")
+    private Boolean isList;
+
+    @Schema(description = "是否查询字段")
+    private Boolean isQuery;
+
+    @Schema(description = "查询方式")
+    private String queryType;
+
+    @Schema(description = "显示类型")
+    private String htmlType;
+
+    @Schema(description = "字典类型")
+    private String dictType;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+    @Schema(description = "归属表编号")
+    private Long tableId;
+}

+ 64 - 0
java/src/main/java/com/payment/platform/module/generator/dto/GenTableVO.java

@@ -0,0 +1,64 @@
+package com.payment.platform.module.generator.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+
+@Data
+public class GenTableVO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "UUID")
+    private String uuid;
+
+    @Schema(description = "状态")
+    private String status;
+
+    @Schema(description = "描述")
+    private String description;
+
+    @Schema(description = "创建时间")
+    private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
+
+    @Schema(description = "创建者ID")
+    private Long createdId;
+
+    @Schema(description = "更新者ID")
+    private Long updatedId;
+
+    @Schema(description = "表名称")
+    private String tableName;
+
+    @Schema(description = "表描述")
+    private String tableComment;
+
+    @Schema(description = "实体类名称")
+    private String className;
+
+    @Schema(description = "生成包路径")
+    private String packageName;
+
+    @Schema(description = "生成模块名")
+    private String moduleName;
+
+    @Schema(description = "生成业务名")
+    private String businessName;
+
+    @Schema(description = "生成功能名")
+    private String functionName;
+
+    @Schema(description = "关联子表的表名")
+    private String subTableName;
+
+    @Schema(description = "子表关联的外键名")
+    private String subTableFkName;
+
+    @Schema(description = "上级菜单ID")
+    private Long parentMenuId;
+}

+ 33 - 0
java/src/main/java/com/payment/platform/module/generator/entity/GenTableColumnEntity.java

@@ -0,0 +1,33 @@
+package com.payment.platform.module.generator.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.UserBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("gen_table_column")
+public class GenTableColumnEntity extends UserBaseEntity {
+
+    private String columnName;
+    private String columnComment;
+    private String columnType;
+    private String columnLength;
+    private String columnDefault;
+    private Boolean isPk;
+    private Boolean isIncrement;
+    private Boolean isNullable;
+    private Boolean isUnique;
+    private String pythonType;
+    private String pythonField;
+    private Boolean isInsert;
+    private Boolean isEdit;
+    private Boolean isList;
+    private Boolean isQuery;
+    private String queryType;
+    private String htmlType;
+    private String dictType;
+    private Integer sort;
+    private Long tableId;
+}

+ 23 - 0
java/src/main/java/com/payment/platform/module/generator/entity/GenTableEntity.java

@@ -0,0 +1,23 @@
+package com.payment.platform.module.generator.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.UserBaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("gen_table")
+public class GenTableEntity extends UserBaseEntity {
+
+    private String tableName;
+    private String tableComment;
+    private String className;
+    private String packageName;
+    private String moduleName;
+    private String businessName;
+    private String functionName;
+    private String subTableName;
+    private String subTableFkName;
+    private Long parentMenuId;
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/generator/mapper/GenTableColumnMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.generator.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.generator.entity.GenTableColumnEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface GenTableColumnMapper extends BaseMapper<GenTableColumnEntity> {
+}

+ 9 - 0
java/src/main/java/com/payment/platform/module/generator/mapper/GenTableMapper.java

@@ -0,0 +1,9 @@
+package com.payment.platform.module.generator.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.payment.platform.module.generator.entity.GenTableEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface GenTableMapper extends BaseMapper<GenTableEntity> {
+}

+ 884 - 0
java/src/main/java/com/payment/platform/module/generator/service/GenService.java

@@ -0,0 +1,884 @@
+package com.payment.platform.module.generator.service;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.module.generator.dto.*;
+import com.payment.platform.module.generator.entity.GenTableColumnEntity;
+import com.payment.platform.module.generator.entity.GenTableEntity;
+import com.payment.platform.module.generator.mapper.GenTableColumnMapper;
+import com.payment.platform.module.generator.mapper.GenTableMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class GenService {
+
+    private final GenTableMapper genTableMapper;
+    private final GenTableColumnMapper genTableColumnMapper;
+    private final JdbcTemplate jdbcTemplate;
+
+    // ==================== 表管理 ====================
+
+    public PageResult<GenTableVO> getTablePage(int pageNo, int pageSize) {
+        Page<GenTableEntity> page = new Page<>(pageNo, pageSize);
+        LambdaQueryWrapper<GenTableEntity> w = new LambdaQueryWrapper<>();
+        w.orderByDesc(GenTableEntity::getUpdatedTime);
+        Page<GenTableEntity> result = genTableMapper.selectPage(page, w);
+        return PageResult.of(pageNo, pageSize, result.getTotal(),
+                result.getRecords().stream().map(e -> BeanUtil.copyProperties(e, GenTableVO.class)).collect(Collectors.toList()));
+    }
+
+    public Map<String, Object> getTableDetail(Long id) {
+        GenTableEntity table = requireTable(id);
+        List<GenTableColumnEntity> columns = genTableColumnMapper.selectList(
+                new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, id)
+                        .orderByAsc(GenTableColumnEntity::getSort));
+        GenTableVO tableVO = BeanUtil.copyProperties(table, GenTableVO.class);
+        List<GenTableColumnVO> columnVOs = columns.stream()
+                .map(e -> BeanUtil.copyProperties(e, GenTableColumnVO.class)).collect(Collectors.toList());
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("table", tableVO);
+        result.put("columns", columnVOs);
+        return result;
+    }
+
+    /**
+     * 从数据库导入表结构到代码生成器。
+     * 读取 information_schema,创建 gen_table 和 gen_table_column 记录。
+     */
+    @Transactional
+    public GenTableVO importTable(String tableName) {
+        if (StrUtil.isBlank(tableName)) throw new BusinessException(400, "表名不能为空");
+        // 检查是否已导入
+        GenTableEntity exist = genTableMapper.selectOne(
+                new LambdaQueryWrapper<GenTableEntity>().eq(GenTableEntity::getTableName, tableName));
+        if (exist != null) throw new BusinessException(400, "表 " + tableName + " 已导入,不能重复导入");
+
+        // 查询表注释
+        String tableComment = queryTableComment(tableName);
+
+        // 构建 GenTableEntity
+        GenTableEntity table = new GenTableEntity();
+        table.setTableName(tableName);
+        table.setTableComment(tableComment);
+        table.setClassName(toPascalCase(tableName));
+        table.setBusinessName(toCamelCase(tableName));
+        table.setFunctionName(StrUtil.isNotBlank(tableComment) ? tableComment : tableName);
+        table.setModuleName(toCamelCase(tableName));
+        table.setPackageName("module_" + toCamelCase(tableName));
+        genTableMapper.insert(table);
+
+        // 查询列信息并导入
+        List<Map<String, Object>> dbColumns = queryTableColumns(tableName);
+        if (dbColumns.isEmpty()) throw new BusinessException(400, "表 " + tableName + " 无列信息");
+
+        int sort = 1;
+        for (Map<String, Object> col : dbColumns) {
+            GenTableColumnEntity column = buildColumnFromDb(col, table.getId());
+            column.setSort(sort++);
+            genTableColumnMapper.insert(column);
+        }
+
+        return BeanUtil.copyProperties(genTableMapper.selectById(table.getId()), GenTableVO.class);
+    }
+
+    /**
+     * 重新同步表结构:对比 DB 最新列信息与已导入的列,增/删/改。
+     */
+    @Transactional
+    public void syncTable(Long id) {
+        GenTableEntity table = requireTable(id);
+        List<GenTableColumnEntity> existingColumns = genTableColumnMapper.selectList(
+                new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, id));
+
+        Map<String, GenTableColumnEntity> existingMap = new LinkedHashMap<>();
+        for (GenTableColumnEntity c : existingColumns) {
+            existingMap.put(c.getColumnName(), c);
+        }
+
+        List<Map<String, Object>> dbColumns = queryTableColumns(table.getTableName());
+        Set<String> dbColNames = new LinkedHashSet<>();
+        int sort = 1;
+        for (Map<String, Object> col : dbColumns) {
+            String colName = (String) col.get("COLUMN_NAME");
+            dbColNames.add(colName);
+
+            if (existingMap.containsKey(colName)) {
+                // 已有列:更新类型、注释等 DB 元数据,保留用户配置
+                GenTableColumnEntity existCol = existingMap.get(colName);
+                fillDbMetadata(existCol, col);
+                existCol.setSort(sort++);
+                genTableColumnMapper.updateById(existCol);
+            } else {
+                // 新增列
+                GenTableColumnEntity newCol = buildColumnFromDb(col, table.getId());
+                newCol.setSort(sort++);
+                genTableColumnMapper.insert(newCol);
+            }
+        }
+
+        // 删除 DB 中已不存在的列
+        for (GenTableColumnEntity c : existingColumns) {
+            if (!dbColNames.contains(c.getColumnName())) {
+                genTableColumnMapper.deleteById(c.getId());
+            }
+        }
+    }
+
+    /**
+     * 删除业务表及其所有列(批量)。
+     */
+    @Transactional
+    public void deleteTables(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) throw new BusinessException(400, "删除失败,删除对象不能为空");
+        for (Long id : ids) {
+            genTableColumnMapper.delete(new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, id));
+            genTableMapper.deleteById(id);
+        }
+    }
+
+    /**
+     * 删除单个业务表及其所有列。
+     */
+    @Transactional
+    public void deleteTable(Long id) {
+        requireTable(id);
+        genTableColumnMapper.delete(new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, id));
+        genTableMapper.deleteById(id);
+    }
+
+    // ==================== 数据库表浏览 ====================
+
+    /**
+     * 查询数据库中所有表(分页),返回表名和注释。
+     */
+    public PageResult<Map<String, Object>> getDbTablePage(int pageNo, int pageSize) {
+        String countSql = "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'";
+        Integer total = jdbcTemplate.queryForObject(countSql, Integer.class);
+        if (total == null) total = 0;
+
+        int offset = (pageNo - 1) * pageSize;
+        String dataSql = "SELECT TABLE_NAME, TABLE_COMMENT, CREATE_TIME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' ORDER BY CREATE_TIME DESC LIMIT " + pageSize + " OFFSET " + offset;
+        List<Map<String, Object>> records = jdbcTemplate.queryForList(dataSql);
+
+        return PageResult.of(pageNo, pageSize, (long) total, records);
+    }
+
+    // ==================== 表创建和更新 ====================
+
+    /**
+     * 手动创建 gen_table 记录(不从 DB 导入)。
+     */
+    @Transactional
+    public GenTableVO createTable(Map<String, Object> body) {
+        String tableName = (String) body.getOrDefault("table_name", body.get("tableName"));
+        if (StrUtil.isBlank(tableName)) throw new BusinessException(400, "表名不能为空");
+
+        GenTableEntity exist = genTableMapper.selectOne(
+                new LambdaQueryWrapper<GenTableEntity>().eq(GenTableEntity::getTableName, tableName));
+        if (exist != null) throw new BusinessException(400, "表 " + tableName + " 已存在");
+
+        GenTableEntity table = new GenTableEntity();
+        table.setTableName(tableName);
+        table.setTableComment((String) body.getOrDefault("table_comment", ""));
+        table.setClassName((String) body.getOrDefault("class_name", toPascalCase(tableName)));
+        table.setBusinessName((String) body.getOrDefault("business_name", toCamelCase(tableName)));
+        table.setFunctionName((String) body.getOrDefault("function_name", table.getTableComment()));
+        table.setModuleName((String) body.getOrDefault("module_name", toCamelCase(tableName)));
+        table.setPackageName((String) body.getOrDefault("package_name", "module_" + toCamelCase(tableName)));
+        genTableMapper.insert(table);
+        return BeanUtil.copyProperties(genTableMapper.selectById(table.getId()), GenTableVO.class);
+    }
+
+    /**
+     * 更新 gen_table 记录。
+     */
+    @Transactional
+    public GenTableVO updateTable(Long id, Map<String, Object> body) {
+        GenTableEntity table = requireTable(id);
+        if (body.containsKey("table_name") || body.containsKey("tableName")) {
+            table.setTableName((String) body.getOrDefault("table_name", body.get("tableName")));
+        }
+        if (body.containsKey("table_comment")) table.setTableComment((String) body.get("table_comment"));
+        if (body.containsKey("class_name")) table.setClassName((String) body.get("class_name"));
+        if (body.containsKey("business_name")) table.setBusinessName((String) body.get("business_name"));
+        if (body.containsKey("function_name")) table.setFunctionName((String) body.get("function_name"));
+        if (body.containsKey("module_name")) table.setModuleName((String) body.get("module_name"));
+        if (body.containsKey("package_name")) table.setPackageName((String) body.get("package_name"));
+        genTableMapper.updateById(table);
+        return BeanUtil.copyProperties(genTableMapper.selectById(id), GenTableVO.class);
+    }
+
+    // ==================== 按表名同步 ====================
+
+    /**
+     * 按表名同步表结构(与 syncTable 逻辑相同,通过表名查找 gen_table)。
+     */
+    @Transactional
+    public void syncTableByName(String tableName) {
+        GenTableEntity table = genTableMapper.selectOne(
+                new LambdaQueryWrapper<GenTableEntity>().eq(GenTableEntity::getTableName, tableName));
+        if (table == null) throw new BusinessException(404, "业务表不存在: " + tableName);
+        syncTable(table.getId());
+    }
+
+    /**
+     * 预览同步表结构将产生的变更。
+     */
+    public Map<String, Object> previewTableSync(String tableName) {
+        GenTableEntity table = genTableMapper.selectOne(
+                new LambdaQueryWrapper<GenTableEntity>().eq(GenTableEntity::getTableName, tableName));
+        if (table == null) throw new BusinessException(404, "业务表不存在: " + tableName);
+
+        List<GenTableColumnEntity> existingColumns = genTableColumnMapper.selectList(
+                new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, table.getId()));
+
+        Map<String, GenTableColumnEntity> existingMap = new LinkedHashMap<>();
+        for (GenTableColumnEntity c : existingColumns) {
+            existingMap.put(c.getColumnName(), c);
+        }
+
+        List<Map<String, Object>> dbColumns = queryTableColumns(tableName);
+        List<String> toAdd = new ArrayList<>();
+        List<String> toRemove = new ArrayList<>();
+        List<String> toUpdate = new ArrayList<>();
+
+        Set<String> dbColNames = new LinkedHashSet<>();
+        for (Map<String, Object> col : dbColumns) {
+            String colName = (String) col.get("COLUMN_NAME");
+            dbColNames.add(colName);
+            if (existingMap.containsKey(colName)) {
+                toUpdate.add(colName);
+            } else {
+                toAdd.add(colName);
+            }
+        }
+        for (GenTableColumnEntity c : existingColumns) {
+            if (!dbColNames.contains(c.getColumnName())) {
+                toRemove.add(c.getColumnName());
+            }
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("table_name", tableName);
+        result.put("add_columns", toAdd);
+        result.put("update_columns", toUpdate);
+        result.put("remove_columns", toRemove);
+        return result;
+    }
+
+    // ==================== 代码生成扩展 ====================
+
+    /**
+     * 按表名生成代码。
+     */
+    @Transactional
+    public List<String> generateCodeByTableName(String tableName) {
+        GenTableEntity table = genTableMapper.selectOne(
+                new LambdaQueryWrapper<GenTableEntity>().eq(GenTableEntity::getTableName, tableName));
+        if (table == null) throw new BusinessException(404, "业务表不存在: " + tableName);
+        return generateCode(table.getId());
+    }
+
+    /**
+     * 批量生成代码。
+     */
+    @Transactional
+    public List<String> batchGenerateCode(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) throw new BusinessException(400, "请选择要生成代码的表");
+        List<String> allPaths = new ArrayList<>();
+        for (Long id : ids) {
+            allPaths.addAll(generateCode(id));
+        }
+        return allPaths;
+    }
+
+    // ==================== 代码生成 ====================
+
+    /**
+     * 预览生成代码,返回 Map<文件名, 代码内容>。
+     */
+    public Map<String, String> previewCode(Long tableId) {
+        GenTableEntity table = requireTable(tableId);
+        List<GenTableColumnEntity> columns = genTableColumnMapper.selectList(
+                new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, tableId)
+                        .orderByAsc(GenTableColumnEntity::getSort));
+        return buildGeneratedCode(table, columns);
+    }
+
+    /**
+     * 生成代码文件到 java/src/main/java/... 目录。
+     * 返回生成的文件路径列表。
+     */
+    @Transactional
+    public List<String> generateCode(Long tableId) {
+        GenTableEntity table = requireTable(tableId);
+        List<GenTableColumnEntity> columns = genTableColumnMapper.selectList(
+                new LambdaQueryWrapper<GenTableColumnEntity>().eq(GenTableColumnEntity::getTableId, tableId)
+                        .orderByAsc(GenTableColumnEntity::getSort));
+
+        Map<String, String> codeMap = buildGeneratedCode(table, columns);
+
+        // 确定基础路径: java/src/main/java
+        String basePath = resolveBasePath();
+        String rawPkg = StrUtil.isNotBlank(table.getPackageName())
+                ? table.getPackageName() : "module_" + table.getModuleName();
+        // 去除 module_ 前缀得到目录段(module_example -> example)
+        String pkgPath = rawPkg.startsWith("module_") ? rawPkg.substring("module_".length()) : rawPkg;
+        pkgPath = pkgPath.replace('.', '/');
+        String businessName = StrUtil.isNotBlank(table.getBusinessName())
+                ? table.getBusinessName() : toCamelCase(table.getTableName());
+
+        List<String> filePaths = new ArrayList<>();
+        for (Map.Entry<String, String> entry : codeMap.entrySet()) {
+            String fileName = entry.getKey();
+            String content = entry.getValue();
+
+            // 文件名格式例如: "entity/DictEntity.java"
+            String fullPath = basePath + "/module/" + pkgPath + "/" + businessName + "/" + fileName;
+            try {
+                Path filePath = Paths.get(fullPath);
+                Files.createDirectories(filePath.getParent());
+                Files.writeString(filePath, content);
+                filePaths.add(fullPath);
+            } catch (IOException e) {
+                throw new BusinessException(500, "生成文件失败: " + fileName + ", " + e.getMessage());
+            }
+        }
+        return filePaths;
+    }
+
+    // ==================== 代码模板构建 ====================
+
+    /**
+     * 根据表与列信息构建全部代码模板,返回 Map<相对文件名, 代码内容>。
+     */
+    private Map<String, String> buildGeneratedCode(GenTableEntity table, List<GenTableColumnEntity> columns) {
+        String className = StrUtil.isNotBlank(table.getClassName())
+                ? table.getClassName() : toPascalCase(table.getTableName());
+        String moduleName = StrUtil.isNotBlank(table.getModuleName())
+                ? table.getModuleName() : toCamelCase(table.getTableName());
+        String businessName = StrUtil.isNotBlank(table.getBusinessName())
+                ? table.getBusinessName() : toCamelCase(table.getTableName());
+        String functionName = StrUtil.isNotBlank(table.getFunctionName())
+                ? table.getFunctionName() : table.getTableComment();
+        // Java 包路径:去除 module_ 前缀(因为外层已有 com.payment.platform.module)
+        String rawPkg = StrUtil.isNotBlank(table.getPackageName())
+                ? table.getPackageName() : "module_" + moduleName;
+        String pkg = rawPkg.startsWith("module_") ? rawPkg.substring("module_".length()) : rawPkg;
+
+        // 过滤出非主键的业务字段(用于 DTO)
+        GenTableColumnEntity pkCol = columns.stream()
+                .filter(c -> Boolean.TRUE.equals(c.getIsPk())).findFirst().orElse(null);
+        List<GenTableColumnEntity> bizCols = columns.stream()
+                .filter(c -> !Boolean.TRUE.equals(c.getIsPk())).collect(Collectors.toList());
+
+        Map<String, String> result = new LinkedHashMap<>();
+
+        // Entity
+        result.put("entity/" + className + "Entity.java", buildEntity(pkg, className, businessName, table.getTableName(), columns));
+
+        // Mapper
+        result.put("mapper/" + className + "Mapper.java", buildMapper(pkg, className, businessName));
+
+        // VO
+        result.put("dto/" + className + "VO.java", buildVO(pkg, className, businessName, columns));
+
+        // CreateDTO
+        result.put("dto/" + className + "CreateDTO.java", buildCreateDTO(pkg, className, businessName, bizCols));
+
+        // UpdateDTO
+        result.put("dto/" + className + "UpdateDTO.java", buildUpdateDTO(pkg, className, businessName, bizCols));
+
+        // Service
+        result.put("service/" + className + "Service.java", buildService(pkg, className, businessName, functionName, columns));
+
+        // Controller
+        result.put("controller/" + className + "Controller.java", buildController(pkg, className, moduleName, businessName));
+
+        return result;
+    }
+
+    private String buildEntity(String pkg, String className, String businessName, String tableName, List<GenTableColumnEntity> columns) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(businessName).append(".entity;\n\n");
+        sb.append("import com.baomidou.mybatisplus.annotation.TableName;\n");
+        sb.append("import com.payment.platform.common.base.UserBaseEntity;\n");
+        sb.append("import lombok.Data;\n");
+        sb.append("import lombok.EqualsAndHashCode;\n\n");
+        // 导入必要的 Java 类型
+        Set<String> imports = new LinkedHashSet<>();
+        for (GenTableColumnEntity c : columns) {
+            String javaType = dbTypeToJavaType(c.getColumnType());
+            if (javaType.contains("BigDecimal")) imports.add("import java.math.BigDecimal;");
+            if (javaType.contains("LocalDate") && !javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDate;");
+            if (javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDateTime;");
+            if (javaType.contains("LocalTime")) imports.add("import java.time.LocalTime;");
+            if (javaType.contains("OffsetDateTime")) imports.add("import java.time.OffsetDateTime;");
+        }
+        for (String imp : imports) sb.append(imp).append("\n");
+        sb.append("\n");
+        sb.append("@Data\n");
+        sb.append("@EqualsAndHashCode(callSuper = true)\n");
+        sb.append("@TableName(\"").append(tableName).append("\")\n");
+        sb.append("public class ").append(className).append("Entity extends UserBaseEntity {\n\n");
+        for (GenTableColumnEntity c : columns) {
+            String javaType = dbTypeToJavaType(c.getColumnType());
+            String fieldName = toCamelCase(c.getColumnName());
+            sb.append("    private ").append(javaType).append(" ").append(fieldName).append(";\n");
+        }
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private String buildMapper(String pkg, String className, String businessName) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(businessName).append(".mapper;\n\n");
+        sb.append("import com.baomidou.mybatisplus.core.mapper.BaseMapper;\n");
+        sb.append("import com.payment.platform.module.").append(pkg).append(".").append(businessName).append(".entity.").append(className).append("Entity;\n");
+        sb.append("import org.apache.ibatis.annotations.Mapper;\n\n");
+        sb.append("@Mapper\n");
+        sb.append("public interface ").append(className).append("Mapper extends BaseMapper<").append(className).append("Entity> {\n");
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private String buildVO(String pkg, String className, String businessName, List<GenTableColumnEntity> columns) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(businessName).append(".dto;\n\n");
+        sb.append("import io.swagger.v3.oas.annotations.media.Schema;\n");
+        sb.append("import lombok.Data;\n\n");
+        // 时间类型导入
+        Set<String> imports = new LinkedHashSet<>();
+        for (GenTableColumnEntity c : columns) {
+            String javaType = dbTypeToJavaType(c.getColumnType());
+            if (javaType.contains("BigDecimal")) imports.add("import java.math.BigDecimal;");
+            if (javaType.contains("LocalDate") && !javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDate;");
+            if (javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDateTime;");
+            if (javaType.contains("LocalTime")) imports.add("import java.time.LocalTime;");
+            if (javaType.contains("OffsetDateTime")) imports.add("import java.time.OffsetDateTime;");
+        }
+        imports.add("import java.time.OffsetDateTime;");
+        for (String imp : imports) sb.append(imp).append("\n");
+        sb.append("\n");
+        sb.append("@Data\n");
+        sb.append("public class ").append(className).append("VO {\n\n");
+        sb.append("    @Schema(description = \"ID\")\n    private Long id;\n\n");
+        sb.append("    @Schema(description = \"创建时间\")\n    private OffsetDateTime createdTime;\n\n");
+        sb.append("    @Schema(description = \"更新时间\")\n    private OffsetDateTime updatedTime;\n\n");
+        for (GenTableColumnEntity c : columns) {
+            String fieldName = toCamelCase(c.getColumnName());
+            String comment = StrUtil.isNotBlank(c.getColumnComment()) ? c.getColumnComment() : fieldName;
+            String javaType = dbTypeToJavaType(c.getColumnType());
+            sb.append("    @Schema(description = \"").append(comment).append("\")\n");
+            sb.append("    private ").append(javaType).append(" ").append(fieldName).append(";\n\n");
+        }
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private String buildCreateDTO(String pkg, String className, String businessName, List<GenTableColumnEntity> bizCols) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(businessName).append(".dto;\n\n");
+        sb.append("import io.swagger.v3.oas.annotations.media.Schema;\n");
+        sb.append("import jakarta.validation.constraints.NotBlank;\n");
+        sb.append("import lombok.Data;\n\n");
+        Set<String> imports = new LinkedHashSet<>();
+        for (GenTableColumnEntity c : bizCols) {
+            String javaType = dbTypeToJavaType(c.getColumnType());
+            if (javaType.contains("BigDecimal")) imports.add("import java.math.BigDecimal;");
+            if (javaType.contains("LocalDate") && !javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDate;");
+            if (javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDateTime;");
+            if (javaType.contains("LocalTime")) imports.add("import java.time.LocalTime;");
+        }
+        for (String imp : imports) sb.append(imp).append("\n");
+        if (!imports.isEmpty()) sb.append("\n");
+        sb.append("@Data\n");
+        sb.append("public class ").append(className).append("CreateDTO {\n\n");
+        for (GenTableColumnEntity c : bizCols) {
+            if (Boolean.TRUE.equals(c.getIsInsert()) || c.getIsInsert() == null) {
+                String fieldName = toCamelCase(c.getColumnName());
+                String comment = StrUtil.isNotBlank(c.getColumnComment()) ? c.getColumnComment() : fieldName;
+                String javaType = dbTypeToJavaType(c.getColumnType());
+                sb.append("    @Schema(description = \"").append(comment).append("\")\n");
+                sb.append("    private ").append(javaType).append(" ").append(fieldName).append(";\n\n");
+            }
+        }
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private String buildUpdateDTO(String pkg, String className, String businessName, List<GenTableColumnEntity> bizCols) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(businessName).append(".dto;\n\n");
+        sb.append("import io.swagger.v3.oas.annotations.media.Schema;\n");
+        sb.append("import lombok.Data;\n\n");
+        Set<String> imports = new LinkedHashSet<>();
+        for (GenTableColumnEntity c : bizCols) {
+            String javaType = dbTypeToJavaType(c.getColumnType());
+            if (javaType.contains("BigDecimal")) imports.add("import java.math.BigDecimal;");
+            if (javaType.contains("LocalDate") && !javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDate;");
+            if (javaType.contains("LocalDateTime")) imports.add("import java.time.LocalDateTime;");
+            if (javaType.contains("LocalTime")) imports.add("import java.time.LocalTime;");
+        }
+        for (String imp : imports) sb.append(imp).append("\n");
+        if (!imports.isEmpty()) sb.append("\n");
+        sb.append("@Data\n");
+        sb.append("public class ").append(className).append("UpdateDTO {\n\n");
+        for (GenTableColumnEntity c : bizCols) {
+            if (Boolean.TRUE.equals(c.getIsEdit()) || c.getIsEdit() == null) {
+                String fieldName = toCamelCase(c.getColumnName());
+                String comment = StrUtil.isNotBlank(c.getColumnComment()) ? c.getColumnComment() : fieldName;
+                String javaType = dbTypeToJavaType(c.getColumnType());
+                sb.append("    @Schema(description = \"").append(comment).append("\")\n");
+                sb.append("    private ").append(javaType).append(" ").append(fieldName).append(";\n\n");
+            }
+        }
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private String buildService(String pkg, String className, String businessName,
+                                String functionName, List<GenTableColumnEntity> columns) {
+        String camelBiz = toCamelCase(businessName);
+        GenTableColumnEntity pkCol = columns.stream()
+                .filter(c -> Boolean.TRUE.equals(c.getIsPk())).findFirst().orElse(null);
+        String pkField = pkCol != null ? toCamelCase(pkCol.getColumnName()) : "id";
+        String pkType = pkCol != null ? dbTypeToJavaType(pkCol.getColumnType()) : "Long";
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".service;\n\n");
+        sb.append("import cn.hutool.core.bean.BeanUtil;\n");
+        sb.append("import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;\n");
+        sb.append("import com.baomidou.mybatisplus.extension.plugins.pagination.Page;\n");
+        sb.append("import com.payment.platform.common.exception.BusinessException;\n");
+        sb.append("import com.payment.platform.common.response.PageResult;\n");
+        sb.append("import com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".dto.*;\n");
+        sb.append("import com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".entity.").append(className).append("Entity;\n");
+        sb.append("import com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".mapper.").append(className).append("Mapper;\n");
+        sb.append("import lombok.RequiredArgsConstructor;\n");
+        sb.append("import org.springframework.stereotype.Service;\n");
+        sb.append("import org.springframework.transaction.annotation.Transactional;\n\n");
+        sb.append("import java.util.List;\n");
+        sb.append("import java.util.stream.Collectors;\n\n");
+        sb.append("@Service\n");
+        sb.append("@RequiredArgsConstructor\n");
+        sb.append("public class ").append(className).append("Service {\n\n");
+        sb.append("    private final ").append(className).append("Mapper mapper;\n\n");
+
+        // getDetail
+        sb.append("    public ").append(className).append("VO getDetail(").append(pkType).append(" ").append(pkField).append(") {\n");
+        sb.append("        return BeanUtil.copyProperties(requireEntity(").append(pkField).append("), ").append(className).append("VO.class);\n");
+        sb.append("    }\n\n");
+
+        // getPage
+        sb.append("    public PageResult<").append(className).append("VO> getPage(int pageNo, int pageSize) {\n");
+        sb.append("        Page<").append(className).append("Entity> page = new Page<>(pageNo, pageSize);\n");
+        sb.append("        LambdaQueryWrapper<").append(className).append("Entity> w = new LambdaQueryWrapper<>();\n");
+        sb.append("        w.orderByDesc(").append(className).append("Entity::getUpdatedTime);\n");
+        sb.append("        Page<").append(className).append("Entity> result = mapper.selectPage(page, w);\n");
+        sb.append("        return PageResult.of(pageNo, pageSize, result.getTotal(),\n");
+        sb.append("                result.getRecords().stream().map(e -> BeanUtil.copyProperties(e, ").append(className).append("VO.class)).collect(Collectors.toList()));\n");
+        sb.append("    }\n\n");
+
+        // list
+        sb.append("    public List<").append(className).append("VO> getList() {\n");
+        sb.append("        return mapper.selectList(new LambdaQueryWrapper<>()).stream()\n");
+        sb.append("                .map(e -> BeanUtil.copyProperties(e, ").append(className).append("VO.class)).collect(Collectors.toList());\n");
+        sb.append("    }\n\n");
+
+        // create
+        sb.append("    @Transactional\n");
+        sb.append("    public ").append(className).append("VO create(").append(className).append("CreateDTO dto) {\n");
+        sb.append("        ").append(className).append("Entity entity = BeanUtil.copyProperties(dto, ").append(className).append("Entity.class);\n");
+        sb.append("        mapper.insert(entity);\n");
+        sb.append("        return BeanUtil.copyProperties(mapper.selectById(entity.getId()), ").append(className).append("VO.class);\n");
+        sb.append("    }\n\n");
+
+        // update
+        sb.append("    @Transactional\n");
+        sb.append("    public ").append(className).append("VO update(").append(pkType).append(" ").append(pkField).append(", ").append(className).append("UpdateDTO dto) {\n");
+        sb.append("        ").append(className).append("Entity exist = requireEntity(").append(pkField).append(");\n");
+        sb.append("        BeanUtil.copyProperties(dto, exist);\n");
+        sb.append("        mapper.updateById(exist);\n");
+        sb.append("        return BeanUtil.copyProperties(mapper.selectById(").append(pkField).append("), ").append(className).append("VO.class);\n");
+        sb.append("    }\n\n");
+
+        // delete
+        sb.append("    @Transactional\n");
+        sb.append("    public void delete(List<").append(pkType).append("> ids) {\n");
+        sb.append("        if (ids == null || ids.isEmpty()) throw new BusinessException(400, \"删除失败,删除对象不能为空\");\n");
+        sb.append("        mapper.deleteBatchIds(ids);\n");
+        sb.append("    }\n\n");
+
+        // requireEntity
+        sb.append("    private ").append(className).append("Entity requireEntity(").append(pkType).append(" ").append(pkField).append(") {\n");
+        sb.append("        ").append(className).append("Entity entity = mapper.selectById(").append(pkField).append(");\n");
+        sb.append("        if (entity == null) throw new BusinessException(404, \"").append(functionName).append("不存在\");\n");
+        sb.append("        return entity;\n");
+        sb.append("    }\n");
+
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    private String buildController(String pkg, String className, String moduleName, String businessName) {
+        String camelBiz = toCamelCase(businessName);
+        StringBuilder sb = new StringBuilder();
+        sb.append("package com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".controller;\n\n");
+        sb.append("import com.payment.platform.common.response.PageResult;\n");
+        sb.append("import com.payment.platform.common.response.Result;\n");
+        sb.append("import com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".dto.*;\n");
+        sb.append("import com.payment.platform.module.").append(pkg).append(".").append(camelBiz).append(".service.").append(className).append("Service;\n");
+        sb.append("import jakarta.validation.Valid;\n");
+        sb.append("import lombok.RequiredArgsConstructor;\n");
+        sb.append("import org.springframework.web.bind.annotation.*;\n\n");
+        sb.append("import java.util.List;\n\n");
+        sb.append("@RestController\n");
+        sb.append("@RequestMapping(\"/").append(moduleName).append("/").append(businessName).append("\")\n");
+        sb.append("@RequiredArgsConstructor\n");
+        sb.append("public class ").append(className).append("Controller {\n\n");
+        sb.append("    private final ").append(className).append("Service service;\n\n");
+
+        sb.append("    @GetMapping(\"/list\")\n");
+        sb.append("    public Result<List<").append(className).append("VO>> getList() {\n");
+        sb.append("        return Result.ok(service.getList());\n");
+        sb.append("    }\n\n");
+
+        sb.append("    @GetMapping(\"/page\")\n");
+        sb.append("    public Result<PageResult<").append(className).append("VO>> getPage(\n");
+        sb.append("            @RequestParam(defaultValue = \"1\") int pageNo,\n");
+        sb.append("            @RequestParam(defaultValue = \"10\") int pageSize) {\n");
+        sb.append("        return Result.ok(service.getPage(pageNo, pageSize));\n");
+        sb.append("    }\n\n");
+
+        sb.append("    @GetMapping(\"/detail/{id}\")\n");
+        sb.append("    public Result<").append(className).append("VO> getDetail(@PathVariable Long id) {\n");
+        sb.append("        return Result.ok(service.getDetail(id));\n");
+        sb.append("    }\n\n");
+
+        sb.append("    @PostMapping(\"/create\")\n");
+        sb.append("    public Result<").append(className).append("VO> create(@Valid @RequestBody ").append(className).append("CreateDTO dto) {\n");
+        sb.append("        return Result.ok(service.create(dto));\n");
+        sb.append("    }\n\n");
+
+        sb.append("    @PutMapping(\"/update/{id}\")\n");
+        sb.append("    public Result<").append(className).append("VO> update(@PathVariable Long id, @RequestBody ").append(className).append("UpdateDTO dto) {\n");
+        sb.append("        return Result.ok(service.update(id, dto));\n");
+        sb.append("    }\n\n");
+
+        sb.append("    @DeleteMapping(\"/delete\")\n");
+        sb.append("    public Result<Void> delete(@RequestBody List<Long> ids) {\n");
+        sb.append("        service.delete(ids);\n");
+        sb.append("        return Result.ok();\n");
+        sb.append("    }\n");
+
+        sb.append("}\n");
+        return sb.toString();
+    }
+
+    // ==================== 数据库元数据查询 ====================
+
+    /**
+     * 查询 information_schema 获取表注释。
+     */
+    private String queryTableComment(String tableName) {
+        try {
+            String sql = "SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_NAME = ? LIMIT 1";
+            List<String> result = jdbcTemplate.query(sql, (rs, rowNum) -> rs.getString("TABLE_COMMENT"), tableName);
+            return result.isEmpty() ? "" : (result.get(0) != null ? result.get(0) : "");
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    /**
+     * 查询 information_schema 获取列信息。
+     */
+    private List<Map<String, Object>> queryTableColumns(String tableName) {
+        String sql = "SELECT COLUMN_NAME, COLUMN_COMMENT, COLUMN_TYPE, COLUMN_DEFAULT, " +
+                "CHARACTER_MAXIMUM_LENGTH, COLUMN_KEY, EXTRA, IS_NULLABLE " +
+                "FROM information_schema.COLUMNS WHERE TABLE_NAME = ? " +
+                "ORDER BY ORDINAL_POSITION";
+        return jdbcTemplate.queryForList(sql, tableName);
+    }
+
+    /**
+     * 将 DB 元数据填充到已有列实体(保留用户自定义的生成配置字段)。
+     */
+    private void fillDbMetadata(GenTableColumnEntity column, Map<String, Object> dbCol) {
+        column.setColumnName((String) dbCol.get("COLUMN_NAME"));
+        column.setColumnComment(dbCol.get("COLUMN_COMMENT") != null ? (String) dbCol.get("COLUMN_COMMENT") : "");
+        column.setColumnType((String) dbCol.get("COLUMN_TYPE"));
+        column.setColumnLength(dbCol.get("CHARACTER_MAXIMUM_LENGTH") != null
+                ? String.valueOf(dbCol.get("CHARACTER_MAXIMUM_LENGTH")) : "");
+        column.setColumnDefault(dbCol.get("COLUMN_DEFAULT") != null ? (String) dbCol.get("COLUMN_DEFAULT") : "");
+        column.setIsPk("PRI".equals(dbCol.get("COLUMN_KEY")));
+        column.setIsIncrement("auto_increment".equalsIgnoreCase((String) dbCol.get("EXTRA")));
+        column.setIsNullable("YES".equalsIgnoreCase((String) dbCol.get("IS_NULLABLE")));
+        column.setIsUnique(false); // DB 不单独查询 UNIQUE,默认 false
+        // keep pythonType / pythonField / isInsert / isEdit / isList / isQuery / queryType / htmlType / dictType as-is
+    }
+
+    /**
+     * 从 DB 元数据构建新的列实体。
+     */
+    private GenTableColumnEntity buildColumnFromDb(Map<String, Object> dbCol, Long tableId) {
+        GenTableColumnEntity column = new GenTableColumnEntity();
+        column.setTableId(tableId);
+        fillDbMetadata(column, dbCol);
+
+        String colName = column.getColumnName();
+        String colType = column.getColumnType();
+
+        // 推断 pythonField (驼峰)
+        column.setPythonField(toCamelCase(colName));
+        // 推断 pythonType
+        column.setPythonType(dbTypeToPythonType(colType));
+
+        // 推断 isInsert / isEdit / isList / isQuery
+        boolean isPk = Boolean.TRUE.equals(column.getIsPk());
+        column.setIsInsert(!isPk);
+        column.setIsEdit(!isPk);
+        column.setIsList(!isPk);
+        column.setIsQuery(!isPk);
+        if (isPk) {
+            column.setQueryType(null);
+        } else {
+            column.setQueryType("EQ");
+        }
+
+        // 推断 htmlType
+        String upperType = colType.toUpperCase();
+        if (upperType.contains("TEXT") || upperType.contains("LONGTEXT")) {
+            column.setHtmlType("textarea");
+        } else {
+            column.setHtmlType("input");
+        }
+        column.setDictType("");
+
+        return column;
+    }
+
+    // ==================== 类型映射工具方法 ====================
+
+    /**
+     * DB 列类型 -> Java 类型字符串。
+     */
+    static String dbTypeToJavaType(String columnType) {
+        if (columnType == null) return "String";
+        String t = columnType.toLowerCase().trim();
+        if (t.startsWith("varchar") || t.startsWith("char") || t.equals("text")
+                || t.equals("mediumtext") || t.equals("longtext") || t.equals("tinytext")
+                || t.equals("json") || t.equals("enum") || t.equals("set")) return "String";
+        if (t.startsWith("bigint")) return "Long";
+        if (t.startsWith("int") || t.startsWith("tinyint") || t.startsWith("smallint")
+                || t.startsWith("mediumint")) return "Integer";
+        if (t.startsWith("decimal") || t.startsWith("numeric")) return "BigDecimal";
+        if (t.startsWith("float")) return "Float";
+        if (t.startsWith("double")) return "Double";
+        if (t.startsWith("datetime") || t.startsWith("timestamp")) return "OffsetDateTime";
+        if (t.startsWith("date")) return "LocalDate";
+        if (t.startsWith("time")) return "LocalTime";
+        if (t.equals("bit") || t.equals("tinyint(1)")) return "Boolean";
+        if (t.contains("blob")) return "byte[]";
+        return "String";
+    }
+
+    /**
+     * DB 列类型 -> Python 类型字符串(为兼容 Python 参考实现中的 pythonType 字段)。
+     */
+    static String dbTypeToPythonType(String columnType) {
+        if (columnType == null) return "str";
+        String t = columnType.toLowerCase().trim();
+        if (t.startsWith("varchar") || t.startsWith("char") || t.equals("text")
+                || t.equals("mediumtext") || t.equals("longtext") || t.equals("tinytext")
+                || t.equals("json") || t.equals("enum") || t.equals("set")) return "str";
+        if (t.startsWith("bigint") || t.startsWith("int") || t.startsWith("tinyint")
+                || t.startsWith("smallint") || t.startsWith("mediumint")) return "int";
+        if (t.startsWith("decimal") || t.startsWith("numeric") || t.startsWith("float")
+                || t.startsWith("double")) return "float";
+        if (t.startsWith("datetime") || t.startsWith("timestamp") || t.startsWith("date")
+                || t.startsWith("time")) return "datetime";
+        if (t.equals("bit") || t.equals("tinyint(1)")) return "bool";
+        if (t.contains("blob")) return "bytes";
+        return "str";
+    }
+
+    /**
+     * snake_case -> camelCase
+     */
+    static String toCamelCase(String name) {
+        if (name == null || name.isEmpty()) return name;
+        StringBuilder sb = new StringBuilder();
+        boolean upper = false;
+        for (char c : name.toCharArray()) {
+            if (c == '_') {
+                upper = true;
+            } else {
+                sb.append(upper ? Character.toUpperCase(c) : c);
+                upper = false;
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * snake_case -> PascalCase
+     */
+    static String toPascalCase(String name) {
+        String camel = toCamelCase(name);
+        if (camel == null || camel.isEmpty()) return camel;
+        return Character.toUpperCase(camel.charAt(0)) + camel.substring(1);
+    }
+
+    // ==================== 文件路径解析 ====================
+
+    /**
+     * 尝试解析 Java 源码根目录。
+     * 策略:从 classpath 向上查找 java/src/main/java 目录。
+     */
+    private String resolveBasePath() {
+        // 尝试从当前 working directory 定位
+        String cwd = System.getProperty("user.dir");
+        Path candidate = Paths.get(cwd, "java/src/main/java");
+        if (Files.exists(candidate)) return candidate.toString();
+
+        // fallback: 从类路径推断
+        try {
+            Path classPath = Paths.get(getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
+            // 向上走到项目根: .../target/classes -> .../java/src/main/java
+            Path projectRoot = classPath;
+            while (projectRoot != null && !projectRoot.getFileName().toString().equals("java")) {
+                projectRoot = projectRoot.getParent();
+            }
+            if (projectRoot != null) {
+                Path srcPath = projectRoot.resolve("src/main/java");
+                if (Files.exists(srcPath)) return srcPath.toString();
+            }
+        } catch (Exception ignored) {
+        }
+        // 最终 fallback
+        return cwd + "/java/src/main/java";
+    }
+
+    private GenTableEntity requireTable(Long id) {
+        GenTableEntity e = genTableMapper.selectById(id);
+        if (e == null) throw new BusinessException(404, "业务表不存在");
+        return e;
+    }
+}

+ 54 - 0
java/src/main/java/com/payment/platform/module/monitor/controller/CacheController.java

@@ -0,0 +1,54 @@
+package com.payment.platform.module.monitor.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.monitor.service.CacheService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+@RestController
+@RequestMapping("/monitor/cache")
+@RequiredArgsConstructor
+public class CacheController {
+
+    private final CacheService cacheService;
+
+    @GetMapping("/info")
+    public Result<Map<String, Object>> info() {
+        return Result.ok(cacheService.getInfo());
+    }
+
+    @GetMapping("/get/names")
+    public Result<?> getNames() {
+        return Result.ok(cacheService.getNames());
+    }
+
+    @GetMapping("/get/keys/{cacheName}")
+    public Result<Set<String>> getKeys(@PathVariable String cacheName) {
+        return Result.ok(cacheService.getKeys(cacheName));
+    }
+
+    @GetMapping("/get/value/{cacheName}/{cacheKey}")
+    public Result<Map<String, Object>> getValue(@PathVariable String cacheName, @PathVariable String cacheKey) {
+        return Result.ok(cacheService.getValue(cacheName, cacheKey));
+    }
+
+    @DeleteMapping("/delete/name/{cacheName}")
+    public Result<Void> deleteName(@PathVariable String cacheName) {
+        cacheService.deleteByName(cacheName);
+        return Result.ok();
+    }
+
+    @DeleteMapping("/delete/key/{cacheKey}")
+    public Result<Void> deleteKey(@PathVariable String cacheKey) {
+        cacheService.deleteByKey(cacheKey);
+        return Result.ok();
+    }
+
+    @DeleteMapping("/delete/all")
+    public Result<Void> deleteAll() {
+        cacheService.deleteAll();
+        return Result.ok();
+    }
+}

+ 47 - 0
java/src/main/java/com/payment/platform/module/monitor/controller/OnlineController.java

@@ -0,0 +1,47 @@
+package com.payment.platform.module.monitor.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.monitor.dto.OnlineUserVO;
+import com.payment.platform.module.monitor.service.OnlineService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+@RestController
+@RequestMapping("/monitor/online")
+@RequiredArgsConstructor
+public class OnlineController {
+
+    private final OnlineService onlineService;
+
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+            @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
+            @RequestParam(required = false) String username,
+            @RequestParam(required = false) String ip) {
+        List<OnlineUserVO> items = onlineService.getOnlineList(pageNo, pageSize, username, ip);
+        long total = onlineService.getOnlineTotal();
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("items", items);
+        result.put("total", total);
+        result.put("page_no", pageNo);
+        result.put("page_size", pageSize);
+        result.put("has_next", (long) pageNo * pageSize < total);
+        return Result.ok(result);
+    }
+
+    @DeleteMapping("/delete")
+    public Result<Void> delete(@RequestBody Map<String, String> body) {
+        String sessionId = body.get("session_id");
+        onlineService.kickOut(sessionId);
+        return Result.ok();
+    }
+
+    @DeleteMapping("/clear")
+    public Result<Void> clear() {
+        onlineService.clearAll();
+        return Result.ok();
+    }
+}

+ 95 - 0
java/src/main/java/com/payment/platform/module/monitor/controller/ResourceController.java

@@ -0,0 +1,95 @@
+package com.payment.platform.module.monitor.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.monitor.dto.FileInfoVO;
+import com.payment.platform.module.monitor.service.ResourceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.*;
+
+@RestController
+@RequestMapping("/monitor/resource")
+@RequiredArgsConstructor
+public class ResourceController {
+
+    private final ResourceService resourceService;
+
+    @GetMapping("/list")
+    public Result<List<FileInfoVO>> list(
+            @RequestParam(name = "page_no", required = false) Integer pageNo,
+            @RequestParam(name = "page_size", required = false) Integer pageSize,
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String path,
+            @RequestParam(name = "include_hidden", required = false) Boolean includeHidden) {
+        return Result.ok(resourceService.listDirectory(path));
+    }
+
+    @PostMapping("/upload")
+    public Result<FileInfoVO> upload(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam(required = false) String path) {
+        return Result.ok(resourceService.uploadFile(file, path));
+    }
+
+    @GetMapping("/download")
+    public ResponseEntity<org.springframework.core.io.InputStreamResource> download(
+            @RequestParam String path) throws IOException {
+        Path filePath = resourceService.downloadFile(path);
+        java.io.File f = filePath.toFile();
+        FileInputStream fis = new FileInputStream(f);
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+        headers.setContentLength(f.length());
+        String encoded = URLEncoder.encode(f.getName(), StandardCharsets.UTF_8).replace("+", "%20");
+        headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded);
+        return ResponseEntity.ok().headers(headers)
+                .body(new InputStreamResource(fis));
+    }
+
+    @DeleteMapping("/delete")
+    public Result<Void> delete(@RequestBody List<String> paths) {
+        resourceService.delete(paths);
+        return Result.ok();
+    }
+
+    @PostMapping("/move")
+    public Result<Void> move(@RequestBody Map<String, Object> body) {
+        resourceService.move((String) body.get("source_path"), (String) body.get("target_path"));
+        return Result.ok();
+    }
+
+    @PostMapping("/copy")
+    public Result<Void> copy(@RequestBody Map<String, Object> body) {
+        resourceService.copy((String) body.get("source_path"), (String) body.get("target_path"));
+        return Result.ok();
+    }
+
+    @PostMapping("/rename")
+    public Result<Void> rename(@RequestBody Map<String, String> body) {
+        resourceService.rename(body.get("old_path"), body.get("new_name"));
+        return Result.ok();
+    }
+
+    @PostMapping("/create-dir")
+    public Result<Void> createDir(@RequestBody Map<String, String> body) {
+        resourceService.createDirectory(body.get("parent_path"), body.get("dir_name"));
+        return Result.ok();
+    }
+
+    @PostMapping("/export")
+    public Result<List<FileInfoVO>> export(@RequestBody Map<String, Object> body) {
+        return Result.ok(resourceService.listDirectory((String) body.get("path")));
+    }
+}

+ 20 - 0
java/src/main/java/com/payment/platform/module/monitor/controller/ServerController.java

@@ -0,0 +1,20 @@
+package com.payment.platform.module.monitor.controller;
+
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.monitor.dto.ServerInfoVO;
+import com.payment.platform.module.monitor.service.ServerService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/monitor/server")
+@RequiredArgsConstructor
+public class ServerController {
+
+    private final ServerService serverService;
+
+    @GetMapping("/info")
+    public Result<ServerInfoVO> info() {
+        return Result.ok(serverService.getInfo());
+    }
+}

+ 47 - 0
java/src/main/java/com/payment/platform/module/monitor/dto/CacheInfoVO.java

@@ -0,0 +1,47 @@
+package com.payment.platform.module.monitor.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Redis 缓存信息 VO
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CacheInfoVO {
+
+    /** Redis 版本 */
+    private String redisVersion;
+
+    /** 已用内存 */
+    private String usedMemory;
+
+    /** Redis 运行模式 */
+    private String mode;
+
+    /** 操作系统 */
+    private String os;
+
+    /** TCP 端口 */
+    private String port;
+
+    /** 客户端连接数 */
+    private String clients;
+
+    /** 运行天数 */
+    private String uptimeDays;
+
+    /** Key 总数 */
+    private String keys;
+
+    /** Key 过期数 */
+    private String expires;
+
+    /** 内存碎片率 */
+    private String memFragmentationRatio;
+
+    /** 命令统计 */
+    private String commandStats;
+}

+ 40 - 0
java/src/main/java/com/payment/platform/module/monitor/dto/FileInfoVO.java

@@ -0,0 +1,40 @@
+package com.payment.platform.module.monitor.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 文件/目录信息 VO
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class FileInfoVO {
+
+    /** 文件名 */
+    private String fileName;
+
+    /** 文件路径(相对路径) */
+    private String filePath;
+
+    /** 文件大小(字节) */
+    private Long fileSize;
+
+    /** 格式化文件大小 */
+    private String fileSizeDisplay;
+
+    /** 是否为目录 */
+    private Boolean isDirectory;
+
+    /** 最后修改时间 */
+    private LocalDateTime lastModified;
+
+    /** 是否为隐藏文件 */
+    private Boolean isHidden;
+
+    /** HTTP 访问 URL */
+    private String fileUrl;
+}

+ 37 - 0
java/src/main/java/com/payment/platform/module/monitor/dto/OnlineUserVO.java

@@ -0,0 +1,37 @@
+package com.payment.platform.module.monitor.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 在线用户 VO
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class OnlineUserVO {
+
+    /** 会话 ID(token sessionId) */
+    private String sessionId;
+
+    /** 用户名 */
+    private String username;
+
+    /** 登录时间 */
+    private LocalDateTime loginTime;
+
+    /** IP 地址 */
+    private String ip;
+
+    /** 登录地点 */
+    private String location;
+
+    /** 浏览器 */
+    private String browser;
+
+    /** 操作系统 */
+    private String os;
+}

+ 92 - 0
java/src/main/java/com/payment/platform/module/monitor/dto/ServerInfoVO.java

@@ -0,0 +1,92 @@
+package com.payment.platform.module.monitor.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 服务器监控信息 VO
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ServerInfoVO {
+
+    /** CPU 信息 */
+    private CpuInfo cpu;
+
+    /** 内存信息 */
+    private MemoryInfo mem;
+
+    /** 系统信息 */
+    private SystemInfo sys;
+
+    /** JVM 信息 */
+    private JvmInfo jvm;
+
+    /** 磁盘信息 */
+    private List<DiskInfo> disks;
+
+    // ===== 内部类 =====
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class CpuInfo {
+        private int cpuNum;
+        private double used;
+        private double sys;
+        private double free;
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class MemoryInfo {
+        private String total;
+        private String used;
+        private String free;
+        private double usage;
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class SystemInfo {
+        private String computerIp;
+        private String computerName;
+        private String osArch;
+        private String osName;
+        private String userDir;
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class JvmInfo {
+        private String name;
+        private String version;
+        private String home;
+        private String startTime;
+        private String runTime;
+        private String memoryTotal;
+        private String memoryUsed;
+        private String memoryFree;
+        private double memoryUsage;
+    }
+
+    @Data
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class DiskInfo {
+        private String dirName;
+        private String sysTypeName;
+        private String typeName;
+        private String total;
+        private String used;
+        private String free;
+        private double usage;
+    }
+}

+ 165 - 0
java/src/main/java/com/payment/platform/module/monitor/service/CacheService.java

@@ -0,0 +1,165 @@
+package com.payment.platform.module.monitor.service;
+
+import com.payment.platform.common.enums.RedisInitKeyConfig;
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.module.monitor.dto.CacheInfoVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.connection.RedisServerCommands;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * 缓存监控服务
+ */
+@Service
+@RequiredArgsConstructor
+public class CacheService {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 获取 Redis 详细信息
+     */
+    public Map<String, Object> getInfo() {
+        Map<String, Object> result = new LinkedHashMap<>();
+        try {
+            Properties props = redisTemplate.getRequiredConnectionFactory().getConnection()
+                    .serverCommands().info();
+            result.put("redis_version", props.getProperty("redis_version"));
+            result.put("redis_mode", props.getProperty("redis_mode"));
+            result.put("os", props.getProperty("os"));
+            result.put("tcp_port", props.getProperty("tcp_port"));
+            result.put("connected_clients", props.getProperty("connected_clients"));
+            result.put("uptime_in_days", props.getProperty("uptime_in_days"));
+            result.put("used_memory_human", props.getProperty("used_memory_human"));
+            result.put("used_memory_peak_human", props.getProperty("used_memory_peak_human"));
+            result.put("mem_fragmentation_ratio", props.getProperty("mem_fragmentation_ratio"));
+
+            // Keyspace
+            Set<String> keys = props.stringPropertyNames();
+            for (String key : keys) {
+                if (key.startsWith("db") && key.contains("keys")) {
+                    result.put("keyspace_" + key, props.getProperty(key));
+                }
+            }
+
+            // Command stats
+            Map<String, String> commandStats = new LinkedHashMap<>();
+            for (String key : keys) {
+                if (key.startsWith("cmdstat_")) {
+                    commandStats.put(key.replace("cmdstat_", ""), props.getProperty(key));
+                }
+            }
+            if (!commandStats.isEmpty()) {
+                result.put("command_stats", commandStats);
+            }
+        } catch (Exception e) {
+            throw new BusinessException(500, "获取 Redis 信息失败: " + e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 获取缓存名称列表 — 综合 RedisInitKeyConfig + Redis keyspace 动态扫描
+     */
+    public List<CacheInfoVO> getNames() {
+        List<CacheInfoVO> list = new ArrayList<>();
+
+        // 1. 从 RedisInitKeyConfig 获取已知缓存
+        for (RedisInitKeyConfig config : RedisInitKeyConfig.values()) {
+            Set<String> keys = redisTemplate.keys(config.getKey() + ":*");
+            long keyCount = keys != null ? keys.size() : 0;
+
+            CacheInfoVO vo = new CacheInfoVO();
+            vo.setRedisVersion(config.getKey());
+            vo.setUsedMemory(config.getRemark());
+            vo.setExpires(String.valueOf(keyCount));
+            list.add(vo);
+        }
+
+        // 2. 动态扫描 Redis 中存在的 key 前缀(不在已知列表中的)
+        try {
+            Set<String> allKeys = redisTemplate.keys("*");
+            if (allKeys != null) {
+                Set<String> knownPrefixes = new HashSet<>();
+                for (RedisInitKeyConfig config : RedisInitKeyConfig.values()) {
+                    knownPrefixes.add(config.getKey());
+                }
+
+                Map<String, Long> prefixCount = new TreeMap<>();
+                for (String key : allKeys) {
+                    String prefix = key.split(":")[0];
+                    if (!knownPrefixes.contains(prefix)) {
+                        prefixCount.merge(prefix, 1L, Long::sum);
+                    }
+                }
+
+                for (Map.Entry<String, Long> entry : prefixCount.entrySet()) {
+                    CacheInfoVO vo = new CacheInfoVO();
+                    vo.setRedisVersion(entry.getKey());
+                    vo.setUsedMemory("动态检测");
+                    vo.setExpires(String.valueOf(entry.getValue()));
+                    list.add(vo);
+                }
+            }
+        } catch (Exception ignored) {
+            // 扫描失败不影响已有结果
+        }
+
+        return list;
+    }
+
+    /**
+     * 获取指定缓存名称下的所有 key
+     */
+    public Set<String> getKeys(String cacheName) {
+        Set<String> keys = redisTemplate.keys(cacheName + ":*");
+        return keys != null ? keys : Set.of();
+    }
+
+    /**
+     * 获取缓存值
+     */
+    public Map<String, Object> getValue(String cacheName, String cacheKey) {
+        Object val = redisTemplate.opsForValue().get(cacheKey);
+        if (val == null) {
+            val = redisTemplate.opsForValue().get(cacheName + ":" + cacheKey);
+        }
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("cacheName", cacheName);
+        result.put("cacheKey", cacheKey);
+        result.put("cacheValue", val != null ? val.toString() : null);
+        return result;
+    }
+
+    /**
+     * 清除指定缓存名称下的所有 key
+     */
+    public void deleteByName(String cacheName) {
+        Set<String> keys = redisTemplate.keys(cacheName + ":*");
+        if (keys != null && !keys.isEmpty()) {
+            redisTemplate.delete(keys);
+        }
+    }
+
+    /**
+     * 删除单个 key
+     */
+    public void deleteByKey(String cacheKey) {
+        redisTemplate.delete(cacheKey);
+    }
+
+    /**
+     * 清除所有缓存
+     */
+    public void deleteAll() {
+        Set<String> keys = redisTemplate.keys("*");
+        if (keys != null && !keys.isEmpty()) {
+            redisTemplate.delete(keys);
+        }
+    }
+}

+ 137 - 0
java/src/main/java/com/payment/platform/module/monitor/service/OnlineService.java

@@ -0,0 +1,137 @@
+package com.payment.platform.module.monitor.service;
+
+import com.payment.platform.common.enums.RedisInitKeyConfig;
+import com.payment.platform.module.monitor.dto.OnlineUserVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 在线用户管理服务
+ *
+ * TODO: 当项目集成 Spring Security / 自定义 Session 体系后,
+ * 需要从此处接入真实 Token,解析用户信息并返回。
+ * 当前上游尚未实现认证,因此返回空列表。
+ */
+@Service
+@RequiredArgsConstructor
+public class OnlineService {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 获取在线用户列表(分页)
+     *
+     * @param pageNo   页码
+     * @param pageSize 每页大小
+     * @param username 用户名搜索关键词(可选)
+     * @param ip       IP 地址搜索关键词(可选)
+     * @return 在线用户列表
+     */
+    public List<OnlineUserVO> getOnlineList(int pageNo, int pageSize, String username, String ip) {
+        // TODO: 待 Spring Security / Sa-Token 等认证体系集成后,
+        // 从 Redis access_token:* 中读取真实 Token 并解码用户信息。
+        // 参考 Python: RedisCURD.mget(keys) → decode_access_token → session_info
+
+        List<OnlineUserVO> allUsers = new ArrayList<>();
+
+        try {
+            Set<String> tokenKeys = redisTemplate.keys(RedisInitKeyConfig.ACCESS_TOKEN.getKey() + ":*");
+            if (tokenKeys == null || tokenKeys.isEmpty()) {
+                return allUsers;
+            }
+
+            for (String key : tokenKeys) {
+                try {
+                    Object tokenObj = redisTemplate.opsForValue().get(key);
+                    if (tokenObj == null) continue;
+
+                    // 当前 Token 存储为简单 String,尚未包含完整的用户会话信息
+                    // 此处先构造基础 VO,后续认证体系上线后再解析真实字段
+                    OnlineUserVO vo = new OnlineUserVO();
+                    vo.setSessionId(key.substring(key.lastIndexOf(":") + 1));
+                    vo.setUsername("在线用户");
+                    vo.setLoginTime(LocalDateTime.now());
+                    vo.setIp("--");
+                    vo.setLocation("--");
+                    vo.setBrowser("--");
+                    vo.setOs("--");
+
+                    // 按条件过滤
+                    if (username != null && !username.isEmpty()) {
+                        if (!vo.getUsername().toLowerCase().contains(username.toLowerCase())) {
+                            continue;
+                        }
+                    }
+                    if (ip != null && !ip.isEmpty()) {
+                        if (!vo.getIp().contains(ip)) {
+                            continue;
+                        }
+                    }
+
+                    allUsers.add(vo);
+                } catch (Exception ignored) {
+                    // 单个 Token 解析失败不影响整体
+                }
+            }
+        } catch (Exception ignored) {
+            // Redis 不可用时返回空列表
+        }
+
+        // 按登录时间倒序
+        allUsers.sort(Comparator.comparing(OnlineUserVO::getLoginTime,
+                Comparator.nullsLast(Comparator.reverseOrder())));
+
+        // 分页
+        int fromIndex = Math.min((pageNo - 1) * pageSize, allUsers.size());
+        int toIndex = Math.min(pageNo * pageSize, allUsers.size());
+        if (fromIndex >= allUsers.size()) return List.of();
+
+        return allUsers.subList(fromIndex, toIndex);
+    }
+
+    /**
+     * 获取在线用户总数
+     */
+    public long getOnlineTotal() {
+        try {
+            Set<String> keys = redisTemplate.keys(RedisInitKeyConfig.ACCESS_TOKEN.getKey() + ":*");
+            return keys != null ? keys.size() : 0;
+        } catch (Exception ignored) {
+            return 0;
+        }
+    }
+
+    /**
+     * 强制下线指定用户
+     */
+    public void kickOut(String sessionId) {
+        try {
+            redisTemplate.delete(RedisInitKeyConfig.ACCESS_TOKEN.getKey() + ":" + sessionId);
+            redisTemplate.delete(RedisInitKeyConfig.REFRESH_TOKEN.getKey() + ":" + sessionId);
+        } catch (Exception ignored) {
+            // Redis 不可用
+        }
+    }
+
+    /**
+     * 清除所有在线用户
+     */
+    public void clearAll() {
+        try {
+            Set<String> accessKeys = redisTemplate.keys(RedisInitKeyConfig.ACCESS_TOKEN.getKey() + ":*");
+            if (accessKeys != null && !accessKeys.isEmpty()) {
+                redisTemplate.delete(accessKeys);
+            }
+            Set<String> refreshKeys = redisTemplate.keys(RedisInitKeyConfig.REFRESH_TOKEN.getKey() + ":*");
+            if (refreshKeys != null && !refreshKeys.isEmpty()) {
+                redisTemplate.delete(refreshKeys);
+            }
+        } catch (Exception ignored) {
+            // Redis 不可用
+        }
+    }
+}

+ 409 - 0
java/src/main/java/com/payment/platform/module/monitor/service/ResourceService.java

@@ -0,0 +1,409 @@
+package com.payment.platform.module.monitor.service;
+
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.module.monitor.dto.FileInfoVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 资源管理服务 — 管理文件系统上的静态资源
+ */
+@Slf4j
+@Service
+public class ResourceService {
+
+    /** 允许上传的后缀 */
+    private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
+            ".gif", ".jpg", ".jpeg", ".png", ".ico", ".svg", ".xls", ".xlsx",
+            ".doc", ".docx", ".ppt", ".pptx", ".pdf", ".txt", ".csv", ".zip"
+    );
+
+    /** 最大上传大小: 100MB */
+    private static final long MAX_UPLOAD_SIZE = 100L * 1024 * 1024;
+
+    /** 最大路径深度 */
+    private static final int MAX_PATH_DEPTH = 20;
+
+    @Value("${file.upload-path:static/upload}")
+    private String uploadPath;
+
+    /**
+     * 获取安全的资源根目录绝对路径
+     */
+    private Path getResourceRoot() {
+        Path root = Path.of(uploadPath).toAbsolutePath().normalize();
+        try {
+            Files.createDirectories(root);
+        } catch (IOException e) {
+            throw new BusinessException(500, "无法创建资源根目录: " + e.getMessage());
+        }
+        return root;
+    }
+
+    /**
+     * 安全检查:校验给定路径在资源根目录内
+     */
+    private Path resolveSafePath(String relativePath) {
+        Path root = getResourceRoot();
+
+        // 清理路径
+        String clean = relativePath == null || relativePath.isEmpty() ? "" : relativePath;
+        clean = clean.replace("\\", "/").trim();
+
+        // 去掉开头的 / 和 upload/
+        while (clean.startsWith("/")) clean = clean.substring(1);
+        if (clean.startsWith("upload/")) clean = clean.substring(7);
+
+        // 路径遍历检测
+        if (clean.contains("..") || clean.contains("\0")) {
+            throw new BusinessException(400, "非法路径: 检测到路径遍历攻击");
+        }
+
+        Path resolved = root.resolve(clean).normalize().toAbsolutePath();
+        if (!resolved.startsWith(root)) {
+            throw new BusinessException(400, "访问路径不在允许范围内");
+        }
+
+        // 路径深度检查
+        try {
+            Path relative = root.relativize(resolved);
+            if (relative.getNameCount() > MAX_PATH_DEPTH) {
+                throw new BusinessException(400, "路径深度超过限制");
+            }
+        } catch (IllegalArgumentException e) {
+            throw new BusinessException(400, "无效的路径");
+        }
+
+        return resolved;
+    }
+
+    /**
+     * 获取文件/目录信息
+     */
+    private FileInfoVO buildFileInfo(Path path) {
+        try {
+            if (!Files.exists(path)) return null;
+
+            BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
+            FileInfoVO vo = new FileInfoVO();
+            vo.setFileName(path.getFileName().toString());
+            vo.setFilePath(getResourceRoot().relativize(path).toString().replace("\\", "/"));
+            vo.setFileSize(attr.size());
+            vo.setFileSizeDisplay(formatFileSize(attr.size()));
+            vo.setIsDirectory(attr.isDirectory());
+            vo.setLastModified(LocalDateTime.ofInstant(
+                    Instant.ofEpochMilli(attr.lastModifiedTime().toMillis()),
+                    ZoneId.systemDefault()));
+            vo.setIsHidden(Files.isHidden(path));
+            // HTTP URL: /static/upload/相对路径
+            vo.setFileUrl("/" + uploadPath.replace("\\", "/") + "/" + vo.getFilePath());
+            return vo;
+        } catch (IOException e) {
+            log.warn("获取文件信息失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 列出目录内容
+     */
+    public List<FileInfoVO> listDirectory(String path) {
+        Path dir = path != null && !path.isEmpty() ? resolveSafePath(path) : getResourceRoot();
+
+        if (!Files.exists(dir)) {
+            throw new BusinessException(404, "目录不存在");
+        }
+        if (!Files.isDirectory(dir)) {
+            throw new BusinessException(400, "路径不是目录");
+        }
+
+        try {
+            return Files.list(dir)
+                    .filter(p -> {
+                        try { return !Files.isHidden(p); }
+                        catch (IOException e) { return false; }
+                    })
+                    .map(this::buildFileInfo)
+                    .filter(Objects::nonNull)
+                    .sorted(Comparator.comparing(FileInfoVO::getIsDirectory).reversed()
+                            .thenComparing(FileInfoVO::getFileName))
+                    .collect(Collectors.toList());
+        } catch (IOException e) {
+            throw new BusinessException(500, "读取目录失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 上传文件
+     */
+    public FileInfoVO uploadFile(MultipartFile file, String targetPath) {
+        if (file == null || file.isEmpty()) {
+            throw new BusinessException(400, "请选择要上传的文件");
+        }
+
+        String originalName = file.getOriginalFilename();
+        if (originalName == null || originalName.isBlank()) {
+            throw new BusinessException(400, "文件名不能为空");
+        }
+
+        // 检查扩展名
+        String ext = "";
+        int dotIdx = originalName.lastIndexOf('.');
+        if (dotIdx >= 0) {
+            ext = originalName.substring(dotIdx).toLowerCase();
+        }
+        if (!ALLOWED_EXTENSIONS.contains(ext)) {
+            throw new BusinessException(400, "不允许上传此类型的文件: " + ext);
+        }
+
+        // 检查大小
+        if (file.getSize() > MAX_UPLOAD_SIZE) {
+            throw new BusinessException(400, "文件过大,最大支持 100MB");
+        }
+
+        // 清理文件名
+        String safeName = sanitizeFileName(originalName);
+
+        // 目标目录
+        Path dir = targetPath != null && !targetPath.isEmpty()
+                ? resolveSafePath(targetPath) : getResourceRoot();
+        try {
+            Files.createDirectories(dir);
+        } catch (IOException e) {
+            throw new BusinessException(500, "创建目录失败: " + e.getMessage());
+        }
+
+        // 写文件
+        Path dest = dir.resolve(safeName);
+        // 重名处理
+        if (Files.exists(dest)) {
+            String baseName = safeName.contains(".")
+                    ? safeName.substring(0, safeName.lastIndexOf('.')) : safeName;
+            int counter = 1;
+            while (Files.exists(dest)) {
+                dest = dir.resolve(baseName + "_" + counter + ext);
+                counter++;
+            }
+        }
+
+        try {
+            Files.copy(file.getInputStream(), dest);
+        } catch (IOException e) {
+            throw new BusinessException(500, "文件写入失败: " + e.getMessage());
+        }
+
+        log.info("文件上传成功: {}", dest);
+        return buildFileInfo(dest);
+    }
+
+    /**
+     * 下载文件 — 返回本地文件系统路径供 FileResponse 使用
+     */
+    public Path downloadFile(String filePath) {
+        Path path = resolveSafePath(filePath);
+        if (!Files.exists(path)) {
+            throw new BusinessException(404, "文件不存在");
+        }
+        if (!Files.isRegularFile(path)) {
+            throw new BusinessException(400, "路径不是文件");
+        }
+        return path;
+    }
+
+    /**
+     * 删除文件或目录
+     */
+    public void delete(List<String> paths) {
+        if (paths == null || paths.isEmpty()) {
+            throw new BusinessException(400, "删除路径不能为空");
+        }
+
+        for (String p : paths) {
+            Path target = resolveSafePath(p);
+            if (!Files.exists(target)) {
+                log.warn("路径不存在,跳过: {}", p);
+                continue;
+            }
+            try {
+                if (Files.isDirectory(target)) {
+                    deleteRecursively(target);
+                } else {
+                    Files.delete(target);
+                }
+            } catch (IOException e) {
+                throw new BusinessException(500, "删除失败 " + p + ": " + e.getMessage());
+            }
+        }
+    }
+
+    private void deleteRecursively(Path dir) throws IOException {
+        try (var stream = Files.walk(dir)) {
+            stream.sorted(Comparator.reverseOrder())
+                    .forEach(p -> {
+                        try { Files.delete(p); }
+                        catch (IOException ignored) {}
+                    });
+        }
+    }
+
+    /**
+     * 移动文件/目录
+     */
+    public void move(String sourcePath, String targetPath) {
+        Path src = resolveSafePath(sourcePath);
+        Path dst = resolveSafePath(targetPath);
+
+        if (!Files.exists(src)) {
+            throw new BusinessException(404, "源路径不存在");
+        }
+        try {
+            Files.createDirectories(dst.getParent());
+            Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
+            log.info("移动成功: {} -> {}", src, dst);
+        } catch (IOException e) {
+            throw new BusinessException(500, "移动失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 复制文件/目录
+     */
+    public void copy(String sourcePath, String targetPath) {
+        Path src = resolveSafePath(sourcePath);
+        Path dst = resolveSafePath(targetPath);
+
+        if (!Files.exists(src)) {
+            throw new BusinessException(404, "源路径不存在");
+        }
+        try {
+            Files.createDirectories(dst.getParent());
+            if (Files.isDirectory(src)) {
+                copyDirectory(src, dst);
+            } else {
+                Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
+            }
+            log.info("复制成功: {} -> {}", src, dst);
+        } catch (IOException e) {
+            throw new BusinessException(500, "复制失败: " + e.getMessage());
+        }
+    }
+
+    private void copyDirectory(Path src, Path dst) throws IOException {
+        Files.walkFileTree(src, new SimpleFileVisitor<>() {
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+                Files.createDirectories(dst.resolve(src.relativize(dir)));
+                return FileVisitResult.CONTINUE;
+            }
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                Files.copy(file, dst.resolve(src.relativize(file)), StandardCopyOption.REPLACE_EXISTING);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    /**
+     * 重命名文件/目录
+     */
+    public void rename(String oldPath, String newName) {
+        Path src = resolveSafePath(oldPath);
+        if (!Files.exists(src)) {
+            throw new BusinessException(404, "文件或目录不存在");
+        }
+
+        String safeName = sanitizeFileName(newName);
+        Path dst = src.getParent().resolve(safeName).normalize().toAbsolutePath();
+
+        if (!dst.startsWith(getResourceRoot())) {
+            throw new BusinessException(400, "目标路径不在允许范围内");
+        }
+        if (Files.exists(dst)) {
+            throw new BusinessException(400, "目标名称已存在");
+        }
+        try {
+            Files.move(src, dst);
+            log.info("重命名成功: {} -> {}", src, dst);
+        } catch (IOException e) {
+            throw new BusinessException(500, "重命名失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 创建目录
+     */
+    public void createDirectory(String parentPath, String dirName) {
+        Path parent = parentPath != null && !parentPath.isEmpty()
+                ? resolveSafePath(parentPath) : getResourceRoot();
+
+        if (!Files.exists(parent)) {
+            throw new BusinessException(404, "父目录不存在");
+        }
+        if (!Files.isDirectory(parent)) {
+            throw new BusinessException(400, "父路径不是目录");
+        }
+
+        String safeName = sanitizeFileName(dirName);
+        Path newDir = parent.resolve(safeName).normalize().toAbsolutePath();
+
+        if (!newDir.startsWith(getResourceRoot())) {
+            throw new BusinessException(400, "目标路径不在允许范围内");
+        }
+        if (Files.exists(newDir)) {
+            throw new BusinessException(400, "目录已存在");
+        }
+        try {
+            Files.createDirectory(newDir);
+            log.info("创建目录成功: {}", newDir);
+        } catch (IOException e) {
+            throw new BusinessException(500, "创建目录失败: " + e.getMessage());
+        }
+    }
+
+    // ===== helpers =====
+
+    private String sanitizeFileName(String fileName) {
+        if (fileName == null || fileName.isBlank()) {
+            return "file_" + System.currentTimeMillis();
+        }
+        // 路径遍历检测
+        if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")
+                || fileName.contains("\0")) {
+            return "file_" + System.currentTimeMillis();
+        }
+        // 只保留纯文件名
+        String name = Path.of(fileName).getFileName().toString();
+        // 移除危险字符
+        name = name.replaceAll("[<>:\"|?*\0-\37]", "");
+        // 防止连续点号
+        name = name.replaceAll("\\.{2,}", ".");
+        // 去除首尾空格和点号
+        name = name.strip();
+        while (name.startsWith(".")) name = name.substring(1);
+        while (name.endsWith(".")) name = name.substring(0, name.length() - 1);
+
+        if (name.isBlank()) {
+            return "file_" + System.currentTimeMillis();
+        }
+        return name;
+    }
+
+    private String formatFileSize(long bytes) {
+        if (bytes < 1024) return bytes + "B";
+        if (bytes < 1024 * 1024) return String.format("%.2fKB", bytes / 1024.0);
+        if (bytes < 1024 * 1024 * 1024) return String.format("%.2fMB", bytes / (1024.0 * 1024));
+        return String.format("%.2fGB", bytes / (1024.0 * 1024 * 1024));
+    }
+}

+ 153 - 0
java/src/main/java/com/payment/platform/module/monitor/service/ServerService.java

@@ -0,0 +1,153 @@
+package com.payment.platform.module.monitor.service;
+
+import com.payment.platform.module.monitor.dto.ServerInfoVO;
+import com.payment.platform.module.monitor.dto.ServerInfoVO.*;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.lang.management.*;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 服务器监控服务
+ */
+@Service
+public class ServerService {
+
+    private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /**
+     * 获取完整的服务器监控信息
+     */
+    public ServerInfoVO getInfo() {
+        ServerInfoVO vo = new ServerInfoVO();
+        vo.setCpu(getCpuInfo());
+        vo.setMem(getMemoryInfo());
+        vo.setSys(getSystemInfo());
+        vo.setJvm(getJvmInfo());
+        vo.setDisks(getDiskInfo());
+        return vo;
+    }
+
+    // ===== CPU =====
+
+    private CpuInfo getCpuInfo() {
+        OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
+        int cpuNum = Runtime.getRuntime().availableProcessors();
+        double load = osBean.getSystemLoadAverage();
+        double used = cpuNum > 0 ? Math.min(load * 100.0 / cpuNum, 100.0) : 0;
+        double free = Math.max(100.0 - used, 0);
+
+        CpuInfo cpu = new CpuInfo();
+        cpu.setCpuNum(cpuNum);
+        cpu.setUsed(Math.round(used * 100.0) / 100.0);
+        cpu.setSys(0);
+        cpu.setFree(Math.round(free * 100.0) / 100.0);
+        return cpu;
+    }
+
+    // ===== Memory =====
+
+    private MemoryInfo getMemoryInfo() {
+        Runtime runtime = Runtime.getRuntime();
+        long total = runtime.totalMemory();
+        long free = runtime.freeMemory();
+        long used = total - free;
+        long max = runtime.maxMemory();
+
+        MemoryInfo mem = new MemoryInfo();
+        mem.setTotal(formatBytes(max));
+        mem.setUsed(formatBytes(used));
+        mem.setFree(formatBytes(max - used));
+        mem.setUsage(Math.round((double) used / max * 10000.0) / 100.0);
+        return mem;
+    }
+
+    // ===== System =====
+
+    private SystemInfo getSystemInfo() {
+        SystemInfo sys = new SystemInfo();
+        try {
+            InetAddress addr = InetAddress.getLocalHost();
+            sys.setComputerIp(addr.getHostAddress());
+            sys.setComputerName(addr.getHostName());
+        } catch (UnknownHostException e) {
+            sys.setComputerIp("127.0.0.1");
+            sys.setComputerName("unknown");
+        }
+        sys.setOsArch(System.getProperty("os.arch"));
+        sys.setOsName(System.getProperty("os.name") + " " + System.getProperty("os.version"));
+        sys.setUserDir(System.getProperty("user.dir"));
+        return sys;
+    }
+
+    // ===== JVM =====
+
+    private JvmInfo getJvmInfo() {
+        RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
+        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
+        Runtime runtime = Runtime.getRuntime();
+
+        long startTime = runtimeBean.getStartTime();
+        long uptime = runtimeBean.getUptime();
+        long days = Duration.ofMillis(uptime).toDays();
+        long hours = Duration.ofMillis(uptime).toHours() % 24;
+        long minutes = Duration.ofMillis(uptime).toMinutes() % 60;
+
+        com.sun.management.OperatingSystemMXBean osBean =
+                (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
+        long totalPhysical = osBean.getTotalPhysicalMemorySize();
+        long freePhysical = osBean.getFreePhysicalMemorySize();
+
+        JvmInfo jvm = new JvmInfo();
+        jvm.setName(ManagementFactory.getRuntimeMXBean().getVmName());
+        jvm.setVersion(System.getProperty("java.version"));
+        jvm.setHome(System.getProperty("java.home"));
+        jvm.setStartTime(LocalDateTime.now().minus(Duration.ofMillis(uptime)).format(DT_FMT));
+        jvm.setRunTime(days + "天" + hours + "小时" + minutes + "分钟");
+        jvm.setMemoryTotal(formatBytes(totalPhysical));
+        jvm.setMemoryUsed(formatBytes(runtime.totalMemory() - runtime.freeMemory()));
+        jvm.setMemoryFree(formatBytes(freePhysical));
+        jvm.setMemoryUsage(Math.round((double) (runtime.totalMemory() - runtime.freeMemory()) / runtime.maxMemory() * 10000.0) / 100.0);
+        return jvm;
+    }
+
+    // ===== Disk =====
+
+    private List<DiskInfo> getDiskInfo() {
+        List<DiskInfo> disks = new ArrayList<>();
+        File[] roots = File.listRoots();
+        if (roots == null) return disks;
+
+        for (File root : roots) {
+            long total = root.getTotalSpace();
+            long free = root.getFreeSpace();
+            long used = total - free;
+
+            DiskInfo disk = new DiskInfo();
+            disk.setDirName(root.getAbsolutePath());
+            disk.setSysTypeName("local");
+            disk.setTypeName("本地固定磁盘(" + root.getAbsolutePath() + ")");
+            disk.setTotal(formatBytes(total));
+            disk.setUsed(formatBytes(used));
+            disk.setFree(formatBytes(free));
+            disk.setUsage(total > 0 ? Math.round((double) used / total * 10000.0) / 100.0 : 0);
+            disks.add(disk);
+        }
+        return disks;
+    }
+
+    // ===== helpers =====
+
+    private String formatBytes(long bytes) {
+        if (bytes < 1024) return bytes + " B";
+        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
+        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024));
+        return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
+    }
+}

+ 156 - 0
java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java

@@ -0,0 +1,156 @@
+package com.payment.platform.module.payment.account.controller;
+
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.Result;
+import com.payment.platform.module.payment.account.dto.AccountVO;
+import com.payment.platform.module.payment.account.dto.TransferCreateDTO;
+import com.payment.platform.module.payment.account.dto.TransferVO;
+import com.payment.platform.module.payment.account.service.AccountService;
+import com.payment.platform.module.payment.account.service.AlipayTransferService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/payment/account")
+@RequiredArgsConstructor
+public class AccountController {
+
+    private final AccountService accountService;
+    private final AlipayTransferService alipayTransferService;
+
+    @GetMapping("/stat/transfer/amount")
+    public Result<Map<String, String>> statTransferAmount(
+            @RequestParam(name = "tenant_id", required = false) Long tenantId,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+            @RequestParam(name = "payee_type", required = false) String payeeType) {
+        return Result.ok(accountService.statTransferAmount(tenantId, enterpriseId, payeeType));
+    }
+
+    @GetMapping("/stat/consume/amount")
+    public Result<Map<String, String>> statConsumeAmount(
+            @RequestParam(name = "tenant_id", required = false) Long tenantId,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+            @RequestParam(name = "payee_type", required = false) String payeeType) {
+        return Result.ok(accountService.statConsumeAmount(tenantId, enterpriseId, payeeType));
+    }
+
+    @GetMapping("/stat/summary/amount")
+    public Result<Map<String, String>> statSummaryAmount(
+            @RequestParam(name = "tenant_id", required = false) Long tenantId,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+            @RequestParam(name = "payee_type", required = false) String payeeType) {
+        return Result.ok(accountService.statSummaryAmount(tenantId, enterpriseId, payeeType));
+    }
+
+    @GetMapping("/query/{enterprise_id}")
+    public Result<List<AccountVO>> query(@PathVariable(name = "enterprise_id") String enterpriseId) {
+        return Result.ok(accountService.queryAccounts(enterpriseId));
+    }
+
+    @GetMapping("/transfer")
+    public Result<PageResult<TransferVO>> transferList(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo, @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
+            @RequestParam(name = "out_biz_no", required = false) String outBizNo, @RequestParam(required = false) String status) {
+        return Result.ok(accountService.transferList(pageNo, pageSize, outBizNo, status));
+    }
+
+    @GetMapping("/transfer/{out_biz_no}")
+    public Result<TransferVO> transferDetail(@PathVariable(name = "out_biz_no") String outBizNo) {
+        return Result.ok(accountService.transferDetail(outBizNo));
+    }
+
+    // Alipay API 操作
+    @PostMapping("/authorize/apply")
+    public Result<Map<String, String>> authorizeApply(@RequestBody Map<String, Object> b) {
+        return Result.ok(alipayTransferService.authorizeApply((String) b.get("enterprise_id")));
+    }
+
+    @PostMapping
+    public Result<Map<String, String>> create(@RequestBody Map<String, Object> b) {
+        return Result.ok(alipayTransferService.createAccount((String) b.get("enterprise_id")));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:deposit')")
+    @PostMapping("/deposit")
+    public Result<Map<String, String>> deposit(@RequestBody Map<String, Object> b) {
+        return Result.ok(alipayTransferService.deposit(
+                (String) b.get("enterprise_id"),
+                (String) b.get("account_book_id"),
+                new BigDecimal(b.get("amount").toString()),
+                (String) b.getOrDefault("remark", null)));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:transfer')")
+    @PostMapping("/transfer")
+    public Result<Map<String, Object>> transfer(@Valid @RequestBody TransferCreateDTO b) {
+        return Result.ok(alipayTransferService.transfer(b));
+    }
+
+    @GetMapping("/consume-detail")
+    public Result<?> consumeDetail(
+            @RequestParam(name = "pay_no") String payNo,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+            @RequestParam(name = "ant_shop_id", required = false) String antShopId,
+            @RequestParam(name = "query_options", required = false) String[] queryOptions) {
+        return Result.ok(accountService.consumeDetail(payNo, enterpriseId, antShopId, queryOptions));
+    }
+
+    @GetMapping("/transfer/export")
+    public Result<?> transferExport(
+            @RequestParam(name = "start_time") String startTime,
+            @RequestParam(name = "end_time") String endTime,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId) {
+        return Result.ok(accountService.transferExport(startTime, endTime, enterpriseId));
+    }
+
+    @GetMapping("/receipt/download")
+    public Result<?> downloadReceipt(
+            @RequestParam(name = "enterprise_id") String enterpriseId,
+            @RequestParam(name = "order_no") String orderNo) {
+        return Result.ok(accountService.downloadReceipt(enterpriseId, orderNo));
+    }
+
+    @GetMapping("/receipt/{enterprise_id}/{file_id}")
+    public Result<?> queryReceipt(
+            @PathVariable(name = "enterprise_id") String enterpriseId,
+            @PathVariable(name = "file_id") String fileId) {
+        return Result.ok(accountService.queryReceipt(enterpriseId, fileId));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:withdraw')")
+    @PostMapping("/withdraw")
+    public Result<Map<String, String>> withdraw(@RequestBody Map<String, Object> b) {
+        return Result.ok(alipayTransferService.withdraw(
+                (String) b.get("enterprise_id"),
+                (String) b.get("account_book_id"),
+                new BigDecimal(b.get("amount").toString()),
+                (String) b.getOrDefault("remark", null)));
+    }
+
+    @PostMapping("/receipt/apply")
+    public Result<?> receiptApply(@RequestBody Map<String, Object> body) {
+        return Result.ok(accountService.receiptApply(body));
+    }
+
+    @PostMapping("/transfer/sync-status")
+    public Result<Map<String, Object>> syncStatus(@RequestBody Map<String, String> b) {
+        return Result.ok(alipayTransferService.syncStatus(
+                b.get("out_biz_no"), b.get("status"),
+                b.get("error_code"), b.get("error_msg")));
+    }
+
+    /**
+     * 全量同步转账状态 — 对应 Python transfer_sync_all_service (L990-1038)
+     * 遍历所有 DEALING 记录,调用 alipay.commerce.ec.trans.order.query 逐一同步
+     */
+    @PostMapping("/transfer/sync-all")
+    public Result<Map<String, Object>> syncAll() {
+        return Result.ok(alipayTransferService.transferSyncAllService());
+    }
+}

+ 20 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/AccountQueryDTO.java

@@ -0,0 +1,20 @@
+package com.payment.platform.module.payment.account.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class AccountQueryDTO {
+
+    @Schema(description = "企业ID")
+    private String enterpriseId;
+
+    @Schema(description = "账户类型")
+    private String accountType;
+
+    @Schema(description = "签约状态")
+    private String signStatus;
+
+    @Schema(description = "状态")
+    private String status;
+}

+ 46 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/AccountVO.java

@@ -0,0 +1,46 @@
+package com.payment.platform.module.payment.account.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.OffsetDateTime;
+
+@Data
+public class AccountVO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "企业ID")
+    private String enterpriseId;
+
+    @Schema(description = "资金专户号")
+    private String accountBookId;
+
+    @Schema(description = "账户名称")
+    private String accountName;
+
+    @Schema(description = "账户类型")
+    private String accountType;
+
+    @Schema(description = "场景")
+    private String scene;
+
+    @Schema(description = "签约状态")
+    private String signStatus;
+
+    @Schema(description = "签约时间")
+    private OffsetDateTime signTime;
+
+    @Schema(description = "备注")
+    private String remark;
+
+    @Schema(description = "状态")
+    private String status;
+
+    @Schema(description = "创建时间")
+    private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
+}

+ 44 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/DepositVO.java

@@ -0,0 +1,44 @@
+package com.payment.platform.module.payment.account.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+
+@Data
+public class DepositVO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "企业ID")
+    private String enterpriseId;
+
+    @Schema(description = "外部业务号")
+    private String outBizNo;
+
+    @Schema(description = "资金专户号")
+    private String accountBookId;
+
+    @Schema(description = "充值金额")
+    private BigDecimal amount;
+
+    @Schema(description = "充值URL")
+    private String url;
+
+    @Schema(description = "回调内容")
+    private String notifyContent;
+
+    @Schema(description = "备注")
+    private String remark;
+
+    @Schema(description = "状态")
+    private String status;
+
+    @Schema(description = "创建时间")
+    private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
+}

+ 55 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java

@@ -0,0 +1,55 @@
+package com.payment.platform.module.payment.account.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+@Data
+public class TransferCreateDTO {
+
+    @Schema(description = "企业ID(可选,可从 account_book_id 推断)")
+    private String enterpriseId;
+
+    @NotBlank(message = "资金专户号不能为空")
+    @Schema(description = "资金专户号")
+    private String accountBookId;
+
+    @NotNull(message = "转账金额不能为空")
+    @Schema(description = "转账金额")
+    private BigDecimal amount;
+
+    @Schema(description = "转账标题")
+    private String orderTitle;
+
+    @NotNull(message = "收款方信息不能为空")
+    @Schema(description = "收款方信息(identity_type, name, identity, bankcard_ext_info)")
+    private Map<String, Object> payeeInfo;
+
+    @Schema(description = "收款方类型")
+    private String payeeType;
+
+    @Schema(description = "备注")
+    private String remark;
+
+    @Schema(description = "员工ID")
+    private String employeeId;
+
+    @Schema(description = "优先级: HIGH/NORMAL/LOW")
+    private String priority;
+
+    @Schema(description = "扩展信息")
+    private Map<String, Object> extInfo;
+
+    @Schema(description = "三方订单号(开放接口使用)")
+    private String thirdBizNo;
+
+    @Schema(description = "API Key(开放接口使用)")
+    private String apiKey;
+
+    @Schema(description = "租户ID(内部使用,由 Controller 从认证上下文注入,对应 Python auth.tenant_id)")
+    private Long tenantId;
+}

+ 20 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/TransferQueryDTO.java

@@ -0,0 +1,20 @@
+package com.payment.platform.module.payment.account.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+public class TransferQueryDTO {
+
+    @Schema(description = "企业ID")
+    private String enterpriseId;
+
+    @Schema(description = "外部业务号")
+    private String outBizNo;
+
+    @Schema(description = "转账状态")
+    private String status;
+
+    @Schema(description = "收款方类型")
+    private String payeeType;
+}

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