From 7530585b20d7a0e4d58a64062e6de1e57b64e7d6 Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sun, 28 Jun 2026 13:12:34 +0800
Subject: [PATCH 01/21] =?UTF-8?q?feat(infra):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E6=A1=86=E6=9E=B6=E6=A0=B8?=
=?UTF-8?q?=E5=BF=83=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现用户仓储基类 AbstractUserRepositoryImpl 提供通用仓储实现
- 添加事件管理器 DefaultEventManagerImpl 支持多种事件通道
- 创建委派注解 DelegateFor 用于标识仓储委托实现
- 实现内存版仓储委托 InMemoryRepositoryDelegate 用于测试开发
- 添加 JPA 自动配置和仓储委托实现
- 实现 Elasticsearch 仓储委托支持搜索功能
- 配置 MongoDB 和 MyBatis 测试环境
- 添加缓存配置支持数据权限缓存
- 实现仓库 Bean 后处理器自动注入委托
- 添加基础配置属性和事件通道枚举
- 创建 GitHub Actions 工作流用于构建推送
- 配置多数据源测试环境包括 H2、MongoDB 等
---
.github/workflows/build-and-push.yml | 89 ++++++
.gitignore | 36 +++
.mvn/settings.xml | 31 ++
SAMPLE_MODULES.md | 260 ++++++++++++++++
pom.xml | 147 +++++++++
structure-infra-elasticsearch-starter/pom.xml | 30 ++
.../ElasticsearchAutoConfiguration.java | 23 ++
.../ElasticsearchRepositoryDelegate.java | 215 ++++++++++++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
structure-infra-jpa-starter/pom.xml | 30 ++
.../configuration/JpaAutoConfiguration.java | 27 ++
.../jpa/repository/JpaRepositoryDelegate.java | 201 +++++++++++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
structure-infra-mongodb-starter/pom.xml | 29 ++
.../configuration/MongoAutoConfiguration.java | 23 ++
.../repository/MongoRepositoryDelegate.java | 202 +++++++++++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
structure-infra-mybatis-plus-starter/pom.xml | 50 ++++
.../MybatisPlusAutoConfiguration.java | 21 ++
.../MybatisPlusRepositoryDelegate.java | 238 +++++++++++++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
structure-infra-sample/pom.xml | 31 ++
.../structure-infra-sample-core/pom.xml | 55 ++++
.../sample/domain/entity/UserEntity.java | 30 ++
.../domain/repository/UserRepository.java | 18 ++
.../infra/sample/infra/po/UserPO.java | 33 +++
.../AbstractUserRepositoryImpl.java | 25 ++
.../delegate/UserRepositoryDelegate.java | 9 +
.../pom.xml | 54 ++++
.../UserElasticsearchRepositoryImpl.java | 19 ++
.../elasticsearch/UserEsDelegate.java | 35 +++
.../UserElasticsearchRepositoryTest.java | 63 ++++
.../config/ElasticsearchTestConfig.java | 28 ++
.../test/resources/application-es-test.yml | 10 +
.../structure-infra-sample-jpa/pom.xml | 61 ++++
.../repository/UserJpaRepositoryImpl.java | 19 ++
.../infra/repository/jpa/UserJpaDelegate.java | 35 +++
.../sample/jpa/UserJpaRepositoryTest.java | 63 ++++
.../sample/jpa/config/JpaTestConfig.java | 28 ++
.../test/resources/application-jpa-test.yml | 27 ++
.../structure-infra-sample-mongodb/pom.xml | 54 ++++
.../repository/UserMongoRepositoryImpl.java | 19 ++
.../repository/mongodb/UserMongoDelegate.java | 34 +++
.../mongodb/UserMongoRepositoryTest.java | 63 ++++
.../mongodb/config/MongoTestConfig.java | 28 ++
.../test/resources/application-mongo-test.yml | 11 +
.../structure-infra-sample-mybatis/pom.xml | 54 ++++
.../infra/sample/InfraSampleApplication.java | 19 ++
.../sample/infra/config/CacheConfig.java | 26 ++
.../infra/sample/infra/mapper/UserMapper.java | 9 +
.../infra/repository/UserRepositoryImpl.java | 12 +
.../mybatis/UserMybatisPlusDelegate.java | 33 +++
.../infra/sample/InfraJpaTestApplication.java | 21 ++
.../sample/config/MybatisOnlyConfig.java | 30 ++
.../infra/sample/config/TestConfig.java | 20 ++
.../sample/repository/UserRepositoryTest.java | 227 ++++++++++++++
.../src/test/resources/application.yml | 29 ++
.../src/test/resources/schema.sql | 9 +
structure-infra-starter/pom.xml | 37 +++
.../infra/annotations/DelegateFor.java | 64 ++++
.../infra/annotations/Repository.java | 83 ++++++
.../configuration/AutoEventConfiguration.java | 24 ++
.../AutoRepositoryConfiguration.java | 21 ++
.../infra/event/DefaultEventManagerImpl.java | 44 +++
.../java/cn/structure/infra/event/Event.java | 30 ++
.../structure/infra/event/EventChannel.java | 11 +
.../structure/infra/event/EventManager.java | 20 ++
.../java/cn/structure/infra/package-info.java | 1 +
.../infra/properties/InfraProperties.java | 38 +++
.../InMemoryRepositoryDelegate.java | 280 ++++++++++++++++++
.../RepositoryBeanPostProcessor.java | 206 +++++++++++++
.../repository/RepositoryDefinition.java | 103 +++++++
.../infra/repository/RepositoryDelegate.java | 65 ++++
.../infra/repository/RepositoryFacade.java | 168 +++++++++++
.../RepositoryFacadeFactoryBean.java | 42 +++
.../infra/repository/RepositoryType.java | 31 ++
...ot.autoconfigure.AutoConfiguration.imports | 2 +
77 files changed, 4267 insertions(+)
create mode 100644 .github/workflows/build-and-push.yml
create mode 100644 .gitignore
create mode 100644 .mvn/settings.xml
create mode 100644 SAMPLE_MODULES.md
create mode 100644 pom.xml
create mode 100644 structure-infra-elasticsearch-starter/pom.xml
create mode 100644 structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java
create mode 100644 structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java
create mode 100644 structure-infra-elasticsearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-jpa-starter/pom.xml
create mode 100644 structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java
create mode 100644 structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java
create mode 100644 structure-infra-jpa-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-mongodb-starter/pom.xml
create mode 100644 structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java
create mode 100644 structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java
create mode 100644 structure-infra-mongodb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-mybatis-plus-starter/pom.xml
create mode 100644 structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java
create mode 100644 structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java
create mode 100644 structure-infra-mybatis-plus-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-sample/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-core/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/entity/UserEntity.java
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/repository/UserRepository.java
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/test/resources/application-es-test.yml
create mode 100644 structure-infra-sample/structure-infra-sample-jpa/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java
create mode 100644 structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java
create mode 100644 structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-jpa/src/test/resources/application-jpa-test.yml
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/test/resources/application-mongo-test.yml
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/InfraSampleApplication.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/InfraJpaTestApplication.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/application.yml
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/schema.sql
create mode 100644 structure-infra-starter/pom.xml
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoEventConfiguration.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/event/DefaultEventManagerImpl.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/event/Event.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/event/EventChannel.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/event/EventManager.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/package-info.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryType.java
create mode 100644 structure-infra-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml
new file mode 100644
index 0000000..b58b224
--- /dev/null
+++ b/.github/workflows/build-and-push.yml
@@ -0,0 +1,89 @@
+name: Build and Push to Aliyun
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Release version (e.g., 1.2.3)'
+ required: true
+ type: string
+
+jobs:
+
+ build-and-push:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Cache Maven packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.m2
+ key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2
+
+ - name: Set release version
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ echo "RELEASE_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
+ else
+ echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+ fi
+
+ - name: Build project
+ run: |
+ cd structure-oauth-dependencies
+ mvn clean install -Dmaven.test.skip=true -Drevision=$RELEASE_VERSION -s ../.mvn/settings.xml
+
+
+ - name: Deploy structure-oauth-manager-boot
+ run: |
+ cd structure-oauth-manager-boot
+ mvn clean package -Dmaven.test.skip=true -Drevision=$RELEASE_VERSION
+
+ - name: Deploy structure-oauth-server-boot
+ run: |
+ cd structure-oauth-server-boot
+ mvn clean package -Dmaven.test.skip=true -Drevision=$RELEASE_VERSION
+
+ - name: Login to Aliyun ACR
+ uses: docker/login-action@v3
+ with:
+ registry: registry.cn-hangzhou.aliyuncs.com
+ username: ${{ secrets.ALIYUN_ACR_USERNAME }}
+ password: ${{ secrets.ALIYUN_ACR_PASSWORD }}
+
+ - name: Build and push structure-oauth-server-boot Docker image of huangzhou
+ uses: docker/build-push-action@v5
+ with:
+ context: ./structure-oauth-server-boot
+ push: true
+ tags: |
+ registry.cn-hangzhou.aliyuncs.com/yunjida/auth-server:${{ env.RELEASE_VERSION }}
+ registry.cn-hangzhou.aliyuncs.com/yunjida/auth-server:latest
+
+ - name: Build and push structure-oauth-manager-boot Docker image of huangzhou
+ uses: docker/build-push-action@v5
+ with:
+ context: ./structure-oauth-manager-boot
+ push: true
+ tags: |
+ registry.cn-hangzhou.aliyuncs.com/yunjida/auth-manager:${{ env.RELEASE_VERSION }}
+ registry.cn-hangzhou.aliyuncs.com/yunjida/auth-manager:latest
+
+ - name: Build and push structure-oauth-ui Docker image of huangzhou
+ uses: docker/build-push-action@v5
+ with:
+ context: ./structure-oauth-ui
+ push: true
+ tags: |
+ registry.cn-hangzhou.aliyuncs.com/yunjida/auth-manager-ui:${{ env.RELEASE_VERSION }}
+ registry.cn-hangzhou.aliyuncs.com/yunjida/auth-manager-ui:latest
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f133ab1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+*.tgz
+
+**/.DS_Store
\ No newline at end of file
diff --git a/.mvn/settings.xml b/.mvn/settings.xml
new file mode 100644
index 0000000..5366497
--- /dev/null
+++ b/.mvn/settings.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ oss
+ ${OSSRH_USERNAME}
+ ${OSSRH_PASSWORD}
+
+
+ gpg.passphrase
+ ${GPG_PASSPHRASE}
+
+
+
+
+
+ oss
+
+ false
+
+
+ gpg
+ ${GPG_PASSPHRASE}
+
+
+
+
+
\ No newline at end of file
diff --git a/SAMPLE_MODULES.md b/SAMPLE_MODULES.md
new file mode 100644
index 0000000..57d778d
--- /dev/null
+++ b/SAMPLE_MODULES.md
@@ -0,0 +1,260 @@
+# structure-pro-infra 示例模块说明
+
+本项目提供了多种数据持久化技术的示例实现,每个存储技术都有独立的子模块。
+
+## 模块结构
+
+```
+structure-pro-infra
+├── structure-infra-sample # MyBatis Plus 示例(主模块)
+├── structure-infra-sample-jpa # JPA 示例模块
+├── structure-infra-sample-mongodb # MongoDB 示例模块
+└── structure-infra-sample-elasticsearch # Elasticsearch 示例模块
+```
+
+## 各模块说明
+
+### 1. MyBatis Plus 示例(structure-infra-sample)
+
+**技术栈**:
+- Spring Boot 4.0.6
+- MyBatis Plus 3.5.16
+- H2 内存数据库(测试环境)
+
+**功能特性**:
+- 完整的 CRUD 操作实现
+- 分页查询支持
+- 条件查询支持
+- Entity 与 PO 转换
+
+**运行测试**:
+```bash
+mvn test -pl structure-infra-sample -Dtest=UserRepositoryTest
+```
+
+**核心文件**:
+- `UserMybatisPlusDelegate.java` - MyBatis Plus 实现
+- `UserRepositoryImpl.java` - 仓储入口
+- `UserRepositoryTest.java` - 完整测试用例
+
+---
+
+### 2. JPA 示例(structure-infra-sample-jpa)
+
+**技术栈**:
+- Spring Data JPA
+- Hibernate
+- H2 内存数据库(测试环境)
+
+**功能特性**:
+- JPA Repository 实现(待完善)
+- Entity 管理
+- 事务管理
+
+**运行测试**:
+```bash
+mvn test -pl structure-infra-sample-jpa -Dtest=UserJpaRepositoryTest
+```
+
+**核心文件**:
+- `UserJpaDelegate.java` - JPA 实现
+- `JpaTestConfig.java` - 测试配置
+- `application-jpa-test.yml` - 测试配置
+
+**状态**:待实现完整功能
+
+---
+
+### 3. MongoDB 示例(structure-infra-sample-mongodb)
+
+**技术栈**:
+- Spring Data MongoDB
+- MongoDB
+
+**功能特性**:
+- MongoDB Repository 实现(待完善)
+- 文档存储支持
+- 动态查询
+
+**运行测试**:
+```bash
+mvn test -pl structure-infra-sample-mongodb -Dtest=UserMongoRepositoryTest
+```
+
+**核心文件**:
+- `UserMongoDelegate.java` - MongoDB 实现
+- `MongoTestConfig.java` - 测试配置
+- `application-mongo-test.yml` - 测试配置
+
+**状态**:待实现完整功能
+
+---
+
+### 4. Elasticsearch 示例(structure-infra-sample-elasticsearch)
+
+**技术栈**:
+- Spring Data Elasticsearch
+- Elasticsearch
+
+**功能特性**:
+- Elasticsearch Repository 实现(待完善)
+- 全文搜索支持
+- 复杂查询支持
+
+**运行测试**:
+```bash
+mvn test -pl structure-infra-sample-elasticsearch -Dtest=UserElasticsearchRepositoryTest
+```
+
+**核心文件**:
+- `UserEsDelegate.java` - Elasticsearch 实现
+- `ElasticsearchTestConfig.java` - 测试配置
+- `application-es-test.yml` - 测试配置
+
+**状态**:待实现完整功能
+
+---
+
+## Delegate 模式说明
+
+每个存储技术都通过实现 `UserRepositoryDelegate` 接口来提供数据访问能力:
+
+```java
+@DelegateFor(
+ name = "userRepository",
+ type = RepositoryType.MYBATIS_PLUS, // 指定存储类型
+ po = UserPO.class, // 持久化对象类型
+ description = "用户仓储 MyBatis Plus 实现",
+ priority = 10 // 优先级
+)
+public class UserMybatisPlusDelegate implements UserRepositoryDelegate {
+ // 实现所有数据访问方法
+}
+```
+
+### RepositoryType 枚举
+
+- `MYBATIS_PLUS` - MyBatis Plus 实现
+- `JPA` - JPA 实现
+- `MONGODB` - MongoDB 实现
+- `ELASTICSEARCH` - Elasticsearch 实现
+- `AUTO` - 自动选择
+
+### 委托选择机制
+
+框架会根据 `@Repository` 注解的 `type` 属性自动选择对应的 delegate 实现:
+
+```java
+@Repository(
+ value = "用户仓储",
+ type = RepositoryType.MYBATIS_PLUS, // 指定使用 MyBatis Plus
+ entity = UserEntity.class,
+ po = UserPO.class
+)
+@Component("userRepository")
+public class UserRepositoryImpl extends RepositoryFacade<...> implements UserRepository {
+ // 框架会自动注入 type=MYBATIS_PLUS 的 delegate
+}
+```
+
+---
+
+## 统一测试
+
+运行所有示例模块的测试:
+
+```bash
+# MyBatis Plus(已完整实现)
+mvn test -pl structure-infra-sample
+
+# JPA(待完善)
+mvn test -pl structure-infra-sample-jpa
+
+# MongoDB(待完善)
+mvn test -pl structure-infra-sample-mongodb
+
+# Elasticsearch(待完善)
+mvn test -pl structure-infra-sample-elasticsearch
+```
+
+---
+
+## 开发指南
+
+### 添加新的存储技术实现
+
+1. **创建新的子模块**:
+```bash
+mkdir -p structure-infra-sample-{tech}/src/main/java/...
+```
+
+2. **创建 Delegate 实现**:
+```java
+@Component
+@DelegateFor(
+ name = "userRepository",
+ type = RepositoryType.{TECH},
+ po = UserPO.class,
+ description = "用户仓储 {TECH} 实现",
+ priority = 10
+)
+public class User{Tech}Delegate implements UserRepositoryDelegate {
+ // 实现所有方法
+}
+```
+
+3. **创建测试配置**:
+```java
+@Configuration
+@Enable{Tech}Repositories(...)
+public class {Tech}TestConfig {
+}
+```
+
+4. **更新父 pom.xml**:
+```xml
+
+ structure-infra-sample-{tech}
+
+```
+
+---
+
+## 依赖关系
+
+```
+structure-infra-sample (MyBatis Plus)
+ ├── structure-infra-starter
+ ├── structure-infra-mybatis-plus-starter
+ └── structure-common
+
+structure-infra-sample-jpa (JPA)
+ ├── structure-infra-sample
+ ├── structure-infra-jpa-starter
+ └── structure-common
+
+structure-infra-sample-mongodb (MongoDB)
+ ├── structure-infra-sample
+ ├── structure-infra-mongodb-starter
+ └── structure-common
+
+structure-infra-sample-elasticsearch (Elasticsearch)
+ ├── structure-infra-sample
+ ├── structure-infra-elasticsearch-starter
+ └── structure-common
+```
+
+---
+
+## 注意事项
+
+1. **测试环境**:JPA、MongoDB、Elasticsearch 模块的完整测试需要相应的数据库服务运行
+2. **依赖隔离**:每个示例模块都排除了其他存储技术的依赖,避免冲突
+3. **代码复用**:所有模块共享 `structure-infra-sample` 中的 Entity、PO、Repository 接口等核心代码
+4. **持续完善**:JPA、MongoDB、Elasticsearch 的具体实现需要根据业务需求补充
+
+---
+
+## 许可证
+
+本项目遵循 Apache License 2.0
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b07bc7a
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,147 @@
+
+
+ 4.0.0
+
+
+ cn.structured
+ structure-dependencies
+ 1.4.0
+
+
+
+ structure-pro-infra
+ structure-pro-infra
+ ${revision}
+ pom
+ pro项目父工程
+
+
+ 1.0.0-SNAPSHOT
+ 4.0.6
+ 3.5.16
+ 1.4.3-SNAPSHOT
+ 1.1.4
+ 1.0.3
+
+
+
+ structure-infra-starter
+ structure-infra-mybatis-plus-starter
+ structure-infra-jpa-starter
+ structure-infra-mongodb-starter
+ structure-infra-elasticsearch-starter
+ structure-infra-sample
+
+
+
+
+
+
+
+ cn.structured
+ structure-common
+ ${structure.version}
+
+
+
+ cn.structured
+ structure-mybatis-plus-starter
+ ${structure.version}
+
+
+
+ cn.structured
+ structure-restful-web-starter
+ ${structure.version}
+
+
+
+ cn.structured
+ structure-tenant-starter
+ ${structure.version}
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot4-starter
+ ${mybatis-plus.version}
+
+
+
+ com.baomidou
+ mybatis-plus-jsqlparser
+ ${mybatis-plus.version}
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.version}
+
+
+
+
+ cn.structured
+ structure-security-core
+ ${structure-security.version}
+
+
+
+ cn.structured
+ structure-security-permission-starter
+ ${structure-security.version}
+
+
+
+
+ cn.structured
+ structure-datascope-starter
+ ${structure-datascope.version}
+
+
+
+
+ cn.structured
+ structure-datascope-message
+ ${structure-datascope.version}
+
+
+
+ cn.structured
+ structure-datascope-cache
+ ${structure-datascope.version}
+
+
+
+
+ cn.structured
+ structure-datascope-mybatis-plus-starter
+ ${structure-datascope.version}
+
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+ 3.1.0
+
+
+
+ cn.structured
+ structure-infra-starter
+ ${revision}
+
+
+
+ cn.structured
+ structure-infra-mybatis-plus-starter
+ ${revision}
+
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-elasticsearch-starter/pom.xml b/structure-infra-elasticsearch-starter/pom.xml
new file mode 100644
index 0000000..31271af
--- /dev/null
+++ b/structure-infra-elasticsearch-starter/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-elasticsearch-starter
+ structure-infra-elasticsearch-starter
+ structure-infra elasticsearch starter
+ jar
+
+
+
+ cn.structured
+ structure-infra-starter
+ ${revision}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+
+
+
+
diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java
new file mode 100644
index 0000000..1257fab
--- /dev/null
+++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java
@@ -0,0 +1,23 @@
+package cn.structure.infra.elasticsearch.configuration;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
+
+/**
+ * Elasticsearch 自动配置类
+ *
+ * 当检测到 Elasticsearch 相关依赖时自动配置
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@AutoConfiguration
+@ConditionalOnClass(name = "org.springframework.data.elasticsearch.core.ElasticsearchOperations")
+@ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "ELASTICSEARCH", matchIfMissing = true)
+@EnableElasticsearchRepositories
+public class ElasticsearchAutoConfiguration {
+
+}
diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java
new file mode 100644
index 0000000..125e0a2
--- /dev/null
+++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java
@@ -0,0 +1,215 @@
+package cn.structure.infra.elasticsearch.repository;
+
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import cn.structure.infra.repository.RepositoryDelegate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
+import org.springframework.data.elasticsearch.core.SearchHits;
+import org.springframework.data.elasticsearch.core.query.Criteria;
+import org.springframework.data.elasticsearch.core.query.CriteriaQuery;
+import org.springframework.data.elasticsearch.core.query.Query;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Elasticsearch 仓储委托实现
+ *
+ * 基于 Spring Data Elasticsearch 实现的仓储委托
+ *
+ * @param 持久化对象类型(PO)
+ * @param 主键类型
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Slf4j
+public class ElasticsearchRepositoryDelegate implements RepositoryDelegate {
+
+ protected ElasticsearchOperations elasticsearchOperations;
+ protected Class entityClass;
+ protected String idFieldName;
+
+ public ElasticsearchRepositoryDelegate() {
+ }
+
+ public ElasticsearchRepositoryDelegate(ElasticsearchOperations elasticsearchOperations, Class entityClass) {
+ this(elasticsearchOperations, entityClass, "id");
+ }
+
+ public ElasticsearchRepositoryDelegate(ElasticsearchOperations elasticsearchOperations, Class entityClass, String idFieldName) {
+ this.elasticsearchOperations = elasticsearchOperations;
+ this.entityClass = entityClass;
+ this.idFieldName = idFieldName;
+ log.info("ElasticsearchRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName());
+ }
+
+ @Override
+ public T save(T entity) {
+ if (entity == null) {
+ return null;
+ }
+ T saved = elasticsearchOperations.save(entity);
+ log.debug("Saved entity: {}", saved);
+ return saved;
+ }
+
+ @Override
+ public void removeById(ID id) {
+ if (id != null) {
+ elasticsearchOperations.delete(String.valueOf(id), entityClass);
+ log.debug("Removed entity: id={}", id);
+ }
+ }
+
+ @Override
+ public T findById(ID id) {
+ if (id == null) {
+ return null;
+ }
+ T entity = elasticsearchOperations.get(String.valueOf(id), entityClass);
+ log.debug("Find by id: id={}, found={}", id, entity != null);
+ return entity;
+ }
+
+ @Override
+ public T queryById(ID id) {
+ return findById(id);
+ }
+
+ @Override
+ public Optional queryByIdOptional(ID id) {
+ return Optional.ofNullable(queryById(id));
+ }
+
+ @Override
+ public T queryOne(T condition) {
+ if (condition == null) {
+ return null;
+ }
+ Query query = buildQuery(condition);
+ SearchHits searchHits = elasticsearchOperations.search(query, entityClass);
+ return searchHits.hasSearchHits() ? searchHits.getSearchHit(0).getContent() : null;
+ }
+
+ @Override
+ public Optional queryOneOptional(T condition) {
+ return Optional.ofNullable(queryOne(condition));
+ }
+
+ @Override
+ public List queryList(T condition) {
+ if (condition == null) {
+ Query query = new CriteriaQuery(Criteria.where("*").exists());
+ SearchHits searchHits = elasticsearchOperations.search(query, entityClass);
+ return searchHits.getSearchHits().stream()
+ .map(hit -> hit.getContent())
+ .collect(Collectors.toList());
+ }
+ Query query = buildQuery(condition);
+ SearchHits searchHits = elasticsearchOperations.search(query, entityClass);
+ return searchHits.getSearchHits().stream()
+ .map(hit -> hit.getContent())
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public ResPage queryPage(ReqPage reqPage) {
+ int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0;
+ int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10;
+
+ Query query = new CriteriaQuery(Criteria.where("*").exists());
+ PageRequest pageRequest = PageRequest.of(pageNum, pageSize, Sort.unsorted());
+ query.setPageable(pageRequest);
+
+ SearchHits searchHits = elasticsearchOperations.search(query, entityClass);
+
+ ResPage resPage = new ResPage<>();
+ resPage.setCurrent((long) (pageNum + 1));
+ resPage.setPages((long) (searchHits.getTotalHits() > 0 ? (searchHits.getTotalHits() + pageSize - 1) / pageSize : 0));
+ resPage.setSize((long) pageSize);
+ resPage.setTotal(searchHits.getTotalHits());
+ resPage.setRecords(searchHits.getSearchHits().stream()
+ .map(hit -> hit.getContent())
+ .collect(Collectors.toList()));
+
+ log.debug("Query page: page={}, size={}, total={}, records={}",
+ pageNum + 1, pageSize, searchHits.getTotalHits(), resPage.getRecords().size());
+ return resPage;
+ }
+
+ private Query buildQuery(T condition) {
+ Criteria criteria = new Criteria();
+ try {
+ Field[] fields = getAllFields(condition.getClass());
+ for (Field field : fields) {
+ field.setAccessible(true);
+ Object value = field.get(condition);
+ if (value != null) {
+ criteria = criteria.and(Criteria.where(field.getName()).is(value));
+ }
+ }
+ } catch (Exception e) {
+ log.warn("Error building query: {}", e.getMessage());
+ }
+ return new CriteriaQuery(criteria);
+ }
+
+ private Field[] getAllFields(Class> clazz) {
+ List fields = new java.util.ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ clazz = clazz.getSuperclass();
+ }
+ return fields.toArray(new Field[0]);
+ }
+
+ @Override
+ public List saveBatch(List entities) {
+ if (entities == null || entities.isEmpty()) {
+ return List.of();
+ }
+ return entities.stream()
+ .map(elasticsearchOperations::save)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void removeBatchByIds(List ids) {
+ if (ids != null && !ids.isEmpty()) {
+ ids.forEach(id -> elasticsearchOperations.delete(String.valueOf(id), entityClass));
+ }
+ }
+
+ @Override
+ public List listByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return List.of();
+ }
+ return ids.stream()
+ .map(this::findById)
+ .filter(entity -> entity != null)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public long count(T condition) {
+ if (condition == null) {
+ Query query = new CriteriaQuery(Criteria.where("*").exists());
+ return elasticsearchOperations.count(query, entityClass);
+ }
+ Query query = buildQuery(condition);
+ return elasticsearchOperations.count(query, entityClass);
+ }
+
+ @Override
+ public boolean exists(T condition) {
+ return count(condition) > 0;
+ }
+}
diff --git a/structure-infra-elasticsearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-elasticsearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..4486cee
--- /dev/null
+++ b/structure-infra-elasticsearch-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.elasticsearch.configuration.ElasticsearchAutoConfiguration
diff --git a/structure-infra-jpa-starter/pom.xml b/structure-infra-jpa-starter/pom.xml
new file mode 100644
index 0000000..b04d37e
--- /dev/null
+++ b/structure-infra-jpa-starter/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-pro-jpa-starter
+ structure-infra-jpa-starter
+ structure-pro-jpa-starter
+ jar
+
+
+
+ cn.structured
+ structure-infra-starter
+ ${revision}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java
new file mode 100644
index 0000000..3b970a1
--- /dev/null
+++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java
@@ -0,0 +1,27 @@
+package cn.structure.infra.jpa.configuration;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * JPA 自动配置类
+ *
+ * 当检测到 JPA 相关依赖时自动配置
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@AutoConfiguration
+@ConditionalOnClass(name = "org.springframework.data.jpa.repository.JpaRepository")
+@ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "JPA", matchIfMissing = true)
+@EnableJpaRepositories
+@EnableTransactionManagement
+public class JpaAutoConfiguration {
+
+}
diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java
new file mode 100644
index 0000000..7394b0b
--- /dev/null
+++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java
@@ -0,0 +1,201 @@
+package cn.structure.infra.jpa.repository;
+
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import cn.structure.infra.repository.RepositoryDelegate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+@Slf4j
+public class JpaRepositoryDelegate implements RepositoryDelegate {
+
+ protected JpaRepository jpaRepository;
+ protected EntityManager entityManager;
+ protected Class entityClass;
+
+ public JpaRepositoryDelegate() {
+ }
+
+ public JpaRepositoryDelegate(EntityManager entityManager, Class entityClass) {
+ this.entityManager = entityManager;
+ this.entityClass = entityClass;
+ this.jpaRepository = createJpaRepository(entityManager, entityClass);
+ log.info("JpaRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName());
+ }
+
+ @SuppressWarnings("unchecked")
+ private JpaRepository createJpaRepository(EntityManager em, Class domainClass) {
+ org.springframework.data.jpa.repository.support.JpaRepositoryFactory factory =
+ new org.springframework.data.jpa.repository.support.JpaRepositoryFactory(em);
+ return (JpaRepository) factory.getRepository(domainClass);
+ }
+
+ @Override
+ public T save(T entity) {
+ if (entity == null) {
+ return null;
+ }
+ T saved = jpaRepository.save(entity);
+ log.debug("Saved entity: {}", saved);
+ return saved;
+ }
+
+ @Override
+ public void removeById(ID id) {
+ if (id != null) {
+ jpaRepository.deleteById(id);
+ log.debug("Removed entity: id={}", id);
+ }
+ }
+
+ @Override
+ public T findById(ID id) {
+ if (id == null) {
+ return null;
+ }
+ T entity = jpaRepository.findById(id).orElse(null);
+ log.debug("Find by id: id={}, found={}", id, entity != null);
+ return entity;
+ }
+
+ @Override
+ public T queryById(ID id) {
+ return findById(id);
+ }
+
+ @Override
+ public Optional queryByIdOptional(ID id) {
+ return jpaRepository.findById(id);
+ }
+
+ @Override
+ public T queryOne(T condition) {
+ if (condition == null) {
+ return null;
+ }
+ List results = queryList(condition);
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public Optional queryOneOptional(T condition) {
+ return Optional.ofNullable(queryOne(condition));
+ }
+
+ @Override
+ public List queryList(T condition) {
+ if (condition == null) {
+ return jpaRepository.findAll();
+ }
+ return queryByCondition(condition);
+ }
+
+ @Override
+ public ResPage queryPage(ReqPage reqPage) {
+ int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0; // JPA page 从 0 开始
+ int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10;
+
+ Page page = jpaRepository.findAll(PageRequest.of(pageNum, pageSize, Sort.unsorted()));
+
+ ResPage resPage = new ResPage<>();
+ resPage.setCurrent((long) (page.getNumber() + 1)); // 转换为从 1 开始
+ resPage.setPages((long) page.getTotalPages());
+ resPage.setSize((long) page.getSize());
+ resPage.setTotal(page.getTotalElements());
+ resPage.setRecords(page.getContent());
+
+ log.debug("Query page: page={}, size={}, total={}, records={}",
+ pageNum + 1, pageSize, page.getTotalElements(), page.getContent().size());
+ return resPage;
+ }
+
+ private List queryByCondition(T condition) {
+ CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+ CriteriaQuery query = cb.createQuery(entityClass);
+ Root root = query.from(entityClass);
+
+ Predicate[] predicates = buildPredicates(cb, root, condition);
+ if (predicates.length > 0) {
+ query.where(predicates);
+ }
+
+ return entityManager.createQuery(query).getResultList();
+ }
+
+ private Predicate[] buildPredicates(CriteriaBuilder cb, Root root, T condition) {
+ List predicates = new java.util.ArrayList<>();
+ try {
+ Field[] fields = getAllFields(condition.getClass());
+ for (Field field : fields) {
+ field.setAccessible(true);
+ Object value = field.get(condition);
+ if (value != null) {
+ predicates.add(cb.equal(root.get(field.getName()), value));
+ }
+ }
+ } catch (Exception e) {
+ log.warn("Error building predicates: {}", e.getMessage());
+ }
+ return predicates.toArray(new Predicate[0]);
+ }
+
+ private Field[] getAllFields(Class> clazz) {
+ List fields = new java.util.ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ clazz = clazz.getSuperclass();
+ }
+ return fields.toArray(new Field[0]);
+ }
+
+ @Override
+ public List saveBatch(List entities) {
+ if (entities == null || entities.isEmpty()) {
+ return List.of();
+ }
+ return entities.stream()
+ .map(jpaRepository::save)
+ .toList();
+ }
+
+ @Override
+ public void removeBatchByIds(List ids) {
+ if (ids != null) {
+ ids.forEach(jpaRepository::deleteById);
+ }
+ }
+
+ @Override
+ public List listByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return List.of();
+ }
+ return jpaRepository.findAllById(ids);
+ }
+
+ @Override
+ public long count(T condition) {
+ if (condition == null) {
+ return jpaRepository.count();
+ }
+ return queryList(condition).size();
+ }
+
+ @Override
+ public boolean exists(T condition) {
+ return count(condition) > 0;
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-jpa-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-jpa-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..f03bf76
--- /dev/null
+++ b/structure-infra-jpa-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.jpa.configuration.JpaAutoConfiguration
\ No newline at end of file
diff --git a/structure-infra-mongodb-starter/pom.xml b/structure-infra-mongodb-starter/pom.xml
new file mode 100644
index 0000000..ba7785e
--- /dev/null
+++ b/structure-infra-mongodb-starter/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-mongodb-starter
+ structure-infra-mongodb-starter
+ structure-infra mongodb starter
+ jar
+
+
+
+ cn.structured
+ structure-infra-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+
diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java
new file mode 100644
index 0000000..16b8a90
--- /dev/null
+++ b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java
@@ -0,0 +1,23 @@
+package cn.structure.infra.mongodb.configuration;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+
+/**
+ * MongoDB 自动配置类
+ *
+ * 当检测到 MongoDB 相关依赖时自动配置
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@AutoConfiguration
+@ConditionalOnClass(name = "org.springframework.data.mongodb.core.MongoTemplate")
+@ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "MONGODB", matchIfMissing = true)
+@EnableMongoRepositories
+public class MongoAutoConfiguration {
+
+}
diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java
new file mode 100644
index 0000000..b27114c
--- /dev/null
+++ b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java
@@ -0,0 +1,202 @@
+package cn.structure.infra.mongodb.repository;
+
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import cn.structure.infra.repository.RepositoryDelegate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * MongoDB 仓储委托实现
+ *
+ * 基于 Spring Data MongoDB 实现的仓储委托
+ *
+ * @param 持久化对象类型(PO)
+ * @param 主键类型
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Slf4j
+public class MongoRepositoryDelegate implements RepositoryDelegate {
+
+ protected MongoTemplate mongoTemplate;
+ protected Class entityClass;
+ protected String idFieldName;
+
+ public MongoRepositoryDelegate() {
+ }
+
+ public MongoRepositoryDelegate(MongoTemplate mongoTemplate, Class entityClass) {
+ this(mongoTemplate, entityClass, "id");
+ }
+
+ public MongoRepositoryDelegate(MongoTemplate mongoTemplate, Class entityClass, String idFieldName) {
+ this.mongoTemplate = mongoTemplate;
+ this.entityClass = entityClass;
+ this.idFieldName = idFieldName;
+ log.info("MongoRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName());
+ }
+
+ @Override
+ public T save(T entity) {
+ if (entity == null) {
+ return null;
+ }
+ T saved = mongoTemplate.save(entity);
+ log.debug("Saved entity: {}", saved);
+ return saved;
+ }
+
+ @Override
+ public void removeById(ID id) {
+ if (id != null) {
+ Query query = new Query(Criteria.where(idFieldName).is(id));
+ mongoTemplate.remove(query, entityClass);
+ log.debug("Removed entity: id={}", id);
+ }
+ }
+
+ @Override
+ public T findById(ID id) {
+ if (id == null) {
+ return null;
+ }
+ Query query = new Query(Criteria.where(idFieldName).is(id));
+ T entity = mongoTemplate.findOne(query, entityClass);
+ log.debug("Find by id: id={}, found={}", id, entity != null);
+ return entity;
+ }
+
+ @Override
+ public T queryById(ID id) {
+ return findById(id);
+ }
+
+ @Override
+ public Optional queryByIdOptional(ID id) {
+ return Optional.ofNullable(queryById(id));
+ }
+
+ @Override
+ public T queryOne(T condition) {
+ if (condition == null) {
+ return null;
+ }
+ Query query = buildQuery(condition);
+ return mongoTemplate.findOne(query, entityClass);
+ }
+
+ @Override
+ public Optional queryOneOptional(T condition) {
+ return Optional.ofNullable(queryOne(condition));
+ }
+
+ @Override
+ public List queryList(T condition) {
+ if (condition == null) {
+ return mongoTemplate.findAll(entityClass);
+ }
+ Query query = buildQuery(condition);
+ return mongoTemplate.find(query, entityClass);
+ }
+
+ @Override
+ public ResPage queryPage(ReqPage reqPage) {
+ int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0;
+ int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10;
+
+ Query query = new Query();
+ long total = mongoTemplate.count(query, entityClass);
+
+ Query pageQuery = query.with(PageRequest.of(pageNum, pageSize, Sort.unsorted()));
+ List records = mongoTemplate.find(pageQuery, entityClass);
+
+ ResPage resPage = new ResPage<>();
+ resPage.setCurrent((long) (pageNum + 1));
+ resPage.setPages(total > 0 ? (total + pageSize - 1) / pageSize : 0);
+ resPage.setSize((long) pageSize);
+ resPage.setTotal(total);
+ resPage.setRecords(records);
+
+ log.debug("Query page: page={}, size={}, total={}, records={}",
+ pageNum + 1, pageSize, total, records.size());
+ return resPage;
+ }
+
+ private Query buildQuery(T condition) {
+ Query query = new Query();
+ try {
+ Field[] fields = getAllFields(condition.getClass());
+ for (Field field : fields) {
+ field.setAccessible(true);
+ Object value = field.get(condition);
+ if (value != null) {
+ query.addCriteria(Criteria.where(field.getName()).is(value));
+ }
+ }
+ } catch (Exception e) {
+ log.warn("Error building query: {}", e.getMessage());
+ }
+ return query;
+ }
+
+ private Field[] getAllFields(Class> clazz) {
+ List fields = new java.util.ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ clazz = clazz.getSuperclass();
+ }
+ return fields.toArray(new Field[0]);
+ }
+
+ @Override
+ public List saveBatch(List entities) {
+ if (entities == null || entities.isEmpty()) {
+ return List.of();
+ }
+ return entities.stream()
+ .map(mongoTemplate::save)
+ .toList();
+ }
+
+ @Override
+ public void removeBatchByIds(List ids) {
+ if (ids != null && !ids.isEmpty()) {
+ Query query = new Query(Criteria.where(idFieldName).in(ids));
+ mongoTemplate.remove(query, entityClass);
+ }
+ }
+
+ @Override
+ public List listByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return List.of();
+ }
+ Query query = new Query(Criteria.where(idFieldName).in(ids));
+ return mongoTemplate.find(query, entityClass);
+ }
+
+ @Override
+ public long count(T condition) {
+ if (condition == null) {
+ return mongoTemplate.count(new Query(), entityClass);
+ }
+ Query query = buildQuery(condition);
+ return mongoTemplate.count(query, entityClass);
+ }
+
+ @Override
+ public boolean exists(T condition) {
+ return count(condition) > 0;
+ }
+}
diff --git a/structure-infra-mongodb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-mongodb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..7265944
--- /dev/null
+++ b/structure-infra-mongodb-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.mongodb.configuration.MongoAutoConfiguration
diff --git a/structure-infra-mybatis-plus-starter/pom.xml b/structure-infra-mybatis-plus-starter/pom.xml
new file mode 100644
index 0000000..060c5cd
--- /dev/null
+++ b/structure-infra-mybatis-plus-starter/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-pro-mybatis-plus-starter
+ structure-infra-mybatis-plus-starter
+ structure-pro-mybatis-plus-starter
+ jar
+
+
+
+ cn.structured
+ structure-infra-starter
+ ${revision}
+
+
+ cn.structured
+ structure-mybatis-plus-starter
+
+
+ cn.structured
+ structure-common
+
+
+ com.baomidou
+ mybatis-plus-jsqlparser
+
+
+ cn.structured
+ structure-security-core
+
+
+ com.baomidou
+ mybatis-plus-spring-boot4-starter
+
+
+ cn.structured
+ structure-tenant-starter
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java
new file mode 100644
index 0000000..27adac9
--- /dev/null
+++ b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java
@@ -0,0 +1,21 @@
+package cn.structure.infra.mybatis.plus.configuration;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+/**
+ * MyBatis Plus 自动配置类
+ *
+ * 当检测到 MyBatis Plus 相关依赖时自动配置
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@AutoConfiguration
+@ConditionalOnClass(name = "com.baomidou.mybatisplus.core.mapper.BaseMapper")
+@ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "MYBATIS_PLUS", matchIfMissing = true)
+public class MybatisPlusAutoConfiguration {
+
+}
diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java
new file mode 100644
index 0000000..4059c71
--- /dev/null
+++ b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java
@@ -0,0 +1,238 @@
+package cn.structure.infra.mybatis.plus.repository;
+
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import cn.structure.infra.repository.RepositoryDelegate;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+@Slf4j
+public class MybatisPlusRepositoryDelegate implements RepositoryDelegate {
+
+ protected BaseMapper baseMapper;
+ protected Class entityClass;
+ protected String idFieldName;
+
+ public MybatisPlusRepositoryDelegate() {
+ }
+
+ public MybatisPlusRepositoryDelegate(BaseMapper baseMapper, Class entityClass) {
+ this(baseMapper, entityClass, "id");
+ }
+
+ public MybatisPlusRepositoryDelegate(BaseMapper baseMapper, Class entityClass, String idFieldName) {
+ this.baseMapper = baseMapper;
+ this.entityClass = entityClass;
+ this.idFieldName = idFieldName;
+ log.info("MybatisPlusRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName());
+ }
+
+
+ @Override
+ public T save(T entity) {
+ if (entity == null) {
+ return null;
+ }
+ ID id = getIdValue(entity);
+ if (id == null) {
+ baseMapper.insert(entity);
+ } else {
+ baseMapper.updateById(entity);
+ }
+ log.debug("Saved entity: id={}, entity={}", id, entity);
+ return entity;
+ }
+
+ @Override
+ public void removeById(ID id) {
+ if (id != null) {
+ baseMapper.deleteById((Serializable) id);
+ log.debug("Removed entity: id={}", id);
+ }
+ }
+
+ @Override
+ public T findById(ID id) {
+ if (id == null) {
+ return null;
+ }
+ T entity = baseMapper.selectById((Serializable) id);
+ log.debug("Find by id: id={}, found={}", id, entity != null);
+ return entity;
+ }
+
+ @Override
+ public T queryById(ID id) {
+ return findById(id);
+ }
+
+ @Override
+ public Optional queryByIdOptional(ID id) {
+ return Optional.ofNullable(queryById(id));
+ }
+
+ @Override
+ public T queryOne(T condition) {
+ if (condition == null) {
+ return null;
+ }
+ QueryWrapper queryWrapper = buildQueryWrapper(condition);
+ List results = baseMapper.selectList(queryWrapper);
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public Optional queryOneOptional(T condition) {
+ return Optional.ofNullable(queryOne(condition));
+ }
+
+ @Override
+ public List queryList(T condition) {
+ if (condition == null) {
+ return baseMapper.selectList(null);
+ }
+ QueryWrapper queryWrapper = buildQueryWrapper(condition);
+ return baseMapper.selectList(queryWrapper);
+ }
+
+ @Override
+ public ResPage queryPage(ReqPage reqPage) {
+ long pageNum = reqPage.getPage() != null ? reqPage.getPage() : 1;
+ long pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10;
+
+ Page page = new Page<>(pageNum, pageSize);
+ IPage result = baseMapper.selectPage(page, null);
+
+ ResPage resPage = new ResPage<>();
+ resPage.setCurrent(result.getCurrent());
+ resPage.setPages(result.getPages());
+ resPage.setSize(result.getSize());
+ resPage.setTotal(result.getTotal());
+ resPage.setRecords(result.getRecords());
+
+ log.debug("Query page: page={}, size={}, total={}, records={}",
+ pageNum, pageSize, result.getTotal(), result.getRecords().size());
+ return resPage;
+ }
+
+ private QueryWrapper buildQueryWrapper(T condition) {
+ QueryWrapper queryWrapper = new QueryWrapper<>();
+ try {
+ Field[] fields = getAllFields(condition.getClass());
+ for (Field field : fields) {
+ field.setAccessible(true);
+ Object value = field.get(condition);
+ if (value != null) {
+ queryWrapper.eq(camelToUnderline(field.getName()), value);
+ }
+ }
+ } catch (Exception e) {
+ log.warn("Error building query wrapper: {}", e.getMessage());
+ }
+ return queryWrapper;
+ }
+
+ private Field[] getAllFields(Class> clazz) {
+ List fields = new java.util.ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ clazz = clazz.getSuperclass();
+ }
+ return fields.toArray(new Field[0]);
+ }
+
+ @SuppressWarnings("unchecked")
+ private ID getIdValue(T entity) {
+ try {
+ Field field = findIdField(entity.getClass());
+ if (field != null) {
+ field.setAccessible(true);
+ return (ID) field.get(entity);
+ }
+ } catch (Exception e) {
+ log.warn("Error getting id value: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ private Field findIdField(Class> clazz) {
+ try {
+ Field field = clazz.getDeclaredField(idFieldName);
+ return field;
+ } catch (NoSuchFieldException e) {
+ if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) {
+ return findIdField(clazz.getSuperclass());
+ }
+ return null;
+ }
+ }
+
+ private String camelToUnderline(String param) {
+ if (param == null || "".equals(param.trim())) {
+ return "";
+ }
+ int len = param.length();
+ StringBuilder sb = new StringBuilder(len);
+ for (int i = 0; i < len; i++) {
+ char c = param.charAt(i);
+ if (Character.isUpperCase(c)) {
+ sb.append("_");
+ sb.append(Character.toLowerCase(c));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public List saveBatch(List entities) {
+ if (entities == null || entities.isEmpty()) {
+ return List.of();
+ }
+ entities.forEach(baseMapper::insert);
+ return entities;
+ }
+
+ @Override
+ public void removeBatchByIds(List ids) {
+ if (ids != null && !ids.isEmpty()) {
+ baseMapper.deleteBatchIds(ids.stream()
+ .map(id -> (Serializable) id)
+ .toList());
+ }
+ }
+
+ @Override
+ public List listByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return List.of();
+ }
+ return baseMapper.selectBatchIds(ids.stream()
+ .map(id -> (Serializable) id)
+ .toList());
+ }
+
+ @Override
+ public long count(T condition) {
+ if (condition == null) {
+ return baseMapper.selectCount(null);
+ }
+ QueryWrapper queryWrapper = buildQueryWrapper(condition);
+ return baseMapper.selectCount(queryWrapper);
+ }
+
+ @Override
+ public boolean exists(T condition) {
+ return count(condition) > 0;
+ }
+}
diff --git a/structure-infra-mybatis-plus-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-mybatis-plus-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..caa7578
--- /dev/null
+++ b/structure-infra-mybatis-plus-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.mybatis.plus.configuration.MybatisPlusAutoConfiguration
\ No newline at end of file
diff --git a/structure-infra-sample/pom.xml b/structure-infra-sample/pom.xml
new file mode 100644
index 0000000..711f1d4
--- /dev/null
+++ b/structure-infra-sample/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample
+ ${revision}
+ pom
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+ structure-infra-sample-core
+ structure-infra-sample-mybatis
+ structure-infra-sample-jpa
+ structure-infra-sample-mongodb
+ structure-infra-sample-elasticsearch
+
+
+
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-core/pom.xml b/structure-infra-sample/structure-infra-sample-core/pom.xml
new file mode 100644
index 0000000..a7f7ba0
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/pom.xml
@@ -0,0 +1,55 @@
+
+
+ 4.0.0
+
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-core
+ structure-infra-sample-core
+ 示例核心模块 - 共享 Entity、PO、Repository 接口
+ jar
+
+
+
+
+ cn.structured
+ structure-infra-starter
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-common
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot4-starter
+ true
+
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+ true
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/entity/UserEntity.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/entity/UserEntity.java
new file mode 100644
index 0000000..0a316c6
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/entity/UserEntity.java
@@ -0,0 +1,30 @@
+package cn.structure.infra.sample.domain.entity;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户实体
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Data
+public class UserEntity {
+
+ private Long id;
+
+ private String username;
+
+ private String password;
+
+ private String email;
+
+ private Integer age;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+}
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/repository/UserRepository.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/repository/UserRepository.java
new file mode 100644
index 0000000..99f17b3
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/domain/repository/UserRepository.java
@@ -0,0 +1,18 @@
+package cn.structure.infra.sample.domain.repository;
+
+import cn.structure.common.repository.ICrudRepository;
+import cn.structure.infra.sample.domain.entity.UserEntity;
+
+/**
+ *
+ * 用户仓储
+ *
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+public interface UserRepository extends ICrudRepository {
+
+ UserEntity findByName(String name);
+}
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java
new file mode 100644
index 0000000..478f949
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java
@@ -0,0 +1,33 @@
+package cn.structure.infra.sample.infra.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import jakarta.persistence.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_user")
+@Entity
+@Table(name = "t_user")
+public class UserPO {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String username;
+
+ private String password;
+
+ private String email;
+
+ private Integer age;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+}
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java
new file mode 100644
index 0000000..20fef2c
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java
@@ -0,0 +1,25 @@
+package cn.structure.infra.sample.infra.repository;
+
+import cn.structure.infra.repository.RepositoryFacade;
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import cn.structure.infra.sample.infra.po.UserPO;
+import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+
+/**
+ * 用户仓储基类
+ *
+ * 提供通用的仓储实现,各存储技术模块可以继承此类并指定具体的存储类型
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+public abstract class AbstractUserRepositoryImpl extends RepositoryFacade implements UserRepository {
+
+ @Override
+ public UserEntity findByName(String name) {
+ UserPO po = this.baseDelegate.finByName(name);
+ return this.toEntity(po);
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java
new file mode 100644
index 0000000..3445aa6
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java
@@ -0,0 +1,9 @@
+package cn.structure.infra.sample.infra.repository.delegate;
+
+import cn.structure.infra.repository.RepositoryDelegate;
+import cn.structure.infra.sample.infra.po.UserPO;
+
+public interface UserRepositoryDelegate extends RepositoryDelegate {
+
+ UserPO finByName(String name);
+}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/pom.xml b/structure-infra-sample/structure-infra-sample-elasticsearch/pom.xml
new file mode 100644
index 0000000..5fbf473
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-elasticsearch
+ structure-infra-sample-elasticsearch
+ Elasticsearch 示例模块 - 演示 Elasticsearch 仓储实现
+ jar
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-elasticsearch
+
+
+
+
+ cn.structured
+ structure-infra-sample-core
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-infra-elasticsearch-starter
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-common
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java
new file mode 100644
index 0000000..5be19be
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java
@@ -0,0 +1,19 @@
+package cn.structure.infra.sample.infra.repository;
+
+import cn.structure.infra.annotations.Repository;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.infra.po.UserPO;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户仓储 Elasticsearch 实现
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Repository(value = "用户仓储", type = RepositoryType.ELASTICSEARCH, entity = UserEntity.class, po = UserPO.class)
+@Component("userRepository")
+public class UserElasticsearchRepositoryImpl extends AbstractUserRepositoryImpl {
+}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java
new file mode 100644
index 0000000..2466f67
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java
@@ -0,0 +1,35 @@
+package cn.structure.infra.sample.infra.repository.elasticsearch;
+
+import cn.structure.infra.annotations.DelegateFor;
+import cn.structure.infra.elasticsearch.repository.ElasticsearchRepositoryDelegate;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.infra.po.UserPO;
+import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户仓储 Elasticsearch 实现
+ *
+ * 使用 Spring Data Elasticsearch 实现用户数据的持久化操作
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Component
+@DelegateFor(
+ name = "userRepository",
+ type = RepositoryType.ELASTICSEARCH,
+ po = UserPO.class,
+ description = "用户仓储 Elasticsearch 实现",
+ priority = 10
+)
+public class UserEsDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate {
+
+ @Override
+ public UserPO finByName(String name) {
+ // TODO: 实现 Elasticsearch 查询逻辑
+ return null;
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java
new file mode 100644
index 0000000..67eaf1e
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java
@@ -0,0 +1,63 @@
+package cn.structure.infra.sample.elasticsearch;
+
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import cn.structure.infra.sample.elasticsearch.config.ElasticsearchTestConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Elasticsearch 仓储测试
+ *
+ * 测试 Elasticsearch 实现的用户仓储功能
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@SpringBootTest
+@ActiveProfiles("es-test")
+@Import(ElasticsearchTestConfig.class)
+@DisplayName("Elasticsearch 仓储测试")
+public class UserElasticsearchRepositoryTest {
+
+ @Autowired(required = false)
+ private UserRepository userRepository;
+
+ @Test
+ @DisplayName("测试 Elasticsearch 仓储注入")
+ void testElasticsearchRepositoryInjection() {
+ assertNotNull(userRepository, "Elasticsearch 仓储应该被成功注入");
+ System.out.println("✓ Elasticsearch 仓储注入成功: " + userRepository.getClass().getName());
+ }
+
+ @Test
+ @DisplayName("测试 Elasticsearch 保存功能(待实现)")
+ void testElasticsearchSave() {
+ assertNotNull(userRepository, "Elasticsearch 仓储应该被注入");
+
+ UserEntity user = new UserEntity();
+ user.setUsername("esTestUser");
+ user.setPassword("password123");
+ user.setEmail("es@test.com");
+ user.setAge(25);
+
+ // TODO: 等待 Elasticsearch 实现完成
+ System.out.println("⚠ Elasticsearch 保存功能待实现,当前返回 null");
+ }
+
+ @Test
+ @DisplayName("测试 Elasticsearch 查询功能(待实现)")
+ void testElasticsearchFindByName() {
+ assertNotNull(userRepository, "Elasticsearch 仓储应该被注入");
+
+ // TODO: 等待 Elasticsearch 实现完成
+ System.out.println("⚠ Elasticsearch 查询功能待实现");
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java
new file mode 100644
index 0000000..0786c77
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java
@@ -0,0 +1,28 @@
+package cn.structure.infra.sample.elasticsearch.config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
+
+/**
+ * Elasticsearch 测试配置
+ *
+ * 配置 Elasticsearch 相关的组件扫描
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Configuration
+@ComponentScan(basePackages = {
+ "cn.structure.infra.sample",
+ "cn.structure.infra.repository"
+}, excludeFilters = {
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mybatis.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.jpa.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mongodb.*")
+})
+@EnableElasticsearchRepositories(basePackages = "cn.structure.infra.sample.infra.repository.elasticsearch")
+public class ElasticsearchTestConfig {
+}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/resources/application-es-test.yml b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/resources/application-es-test.yml
new file mode 100644
index 0000000..3ae882e
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/resources/application-es-test.yml
@@ -0,0 +1,10 @@
+spring:
+ elasticsearch:
+ uris: http://localhost:9200
+ connection-timeout: 5s
+ socket-timeout: 30s
+
+logging:
+ level:
+ org.springframework.data.elasticsearch: DEBUG
+ cn.structure.infra.repository: DEBUG
diff --git a/structure-infra-sample/structure-infra-sample-jpa/pom.xml b/structure-infra-sample/structure-infra-sample-jpa/pom.xml
new file mode 100644
index 0000000..39bdc87
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-jpa/pom.xml
@@ -0,0 +1,61 @@
+
+
+ 4.0.0
+
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-jpa
+ structure-infra-sample-jpa
+ JPA 示例模块 - 演示 JPA 仓储实现
+ jar
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+ cn.structured
+ structure-infra-sample-core
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-infra-jpa-starter
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-common
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java
new file mode 100644
index 0000000..b5fb7de
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java
@@ -0,0 +1,19 @@
+package cn.structure.infra.sample.infra.repository;
+
+import cn.structure.infra.annotations.Repository;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.infra.po.UserPO;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户仓储 JPA 实现
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Repository(value = "用户仓储", type = RepositoryType.JPA, entity = UserEntity.class, po = UserPO.class)
+@Component("userRepository")
+public class UserJpaRepositoryImpl extends AbstractUserRepositoryImpl {
+}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java
new file mode 100644
index 0000000..52fbfad
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java
@@ -0,0 +1,35 @@
+package cn.structure.infra.sample.infra.repository.jpa;
+
+import cn.structure.infra.annotations.DelegateFor;
+import cn.structure.infra.jpa.repository.JpaRepositoryDelegate;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.infra.po.UserPO;
+import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户仓储 JPA 实现
+ *
+ * 使用 Spring Data JPA 实现用户数据的持久化操作
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Component
+@DelegateFor(
+ name = "userRepository",
+ type = RepositoryType.JPA,
+ po = UserPO.class,
+ description = "用户仓储 JPA 实现",
+ priority = 10
+)
+public class UserJpaDelegate extends JpaRepositoryDelegate implements UserRepositoryDelegate {
+
+ @Override
+ public UserPO finByName(String name) {
+ // TODO: 实现 JPA 查询逻辑
+ return null;
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
new file mode 100644
index 0000000..70dd317
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
@@ -0,0 +1,63 @@
+package cn.structure.infra.sample.jpa;
+
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import cn.structure.infra.sample.jpa.config.JpaTestConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * JPA 仓储测试
+ *
+ * 测试 JPA 实现的用户仓储功能
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@SpringBootTest
+@ActiveProfiles("jpa-test")
+@Import(JpaTestConfig.class)
+@DisplayName("JPA 仓储测试")
+public class UserJpaRepositoryTest {
+
+ @Autowired(required = false)
+ private UserRepository userRepository;
+
+ @Test
+ @DisplayName("测试 JPA 仓储注入")
+ void testJpaRepositoryInjection() {
+ assertNotNull(userRepository, "JPA 仓储应该被成功注入");
+ System.out.println("✓ JPA 仓储注入成功: " + userRepository.getClass().getName());
+ }
+
+ @Test
+ @DisplayName("测试 JPA 保存功能(待实现)")
+ void testJpaSave() {
+ assertNotNull(userRepository, "JPA 仓储应该被注入");
+
+ UserEntity user = new UserEntity();
+ user.setUsername("jpaTestUser");
+ user.setPassword("password123");
+ user.setEmail("jpa@test.com");
+ user.setAge(25);
+
+ // TODO: 等待 JPA 实现完成
+ System.out.println("⚠ JPA 保存功能待实现,当前返回 null");
+ }
+
+ @Test
+ @DisplayName("测试 JPA 查询功能(待实现)")
+ void testJpaFindByName() {
+ assertNotNull(userRepository, "JPA 仓储应该被注入");
+
+ // TODO: 等待 JPA 实现完成
+ System.out.println("⚠ JPA 查询功能待实现");
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
new file mode 100644
index 0000000..f29bfac
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
@@ -0,0 +1,28 @@
+package cn.structure.infra.sample.jpa.config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+/**
+ * JPA 测试配置
+ *
+ * 配置 JPA 相关的组件扫描
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Configuration
+@ComponentScan(basePackages = {
+ "cn.structure.infra.sample",
+ "cn.structure.infra.repository"
+}, excludeFilters = {
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mybatis.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mongodb.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.elasticsearch.*")
+})
+@EnableJpaRepositories(basePackages = "cn.structure.infra.sample.infra.repository.jpa")
+public class JpaTestConfig {
+}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/resources/application-jpa-test.yml b/structure-infra-sample/structure-infra-sample-jpa/src/test/resources/application-jpa-test.yml
new file mode 100644
index 0000000..789fd0b
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/resources/application-jpa-test.yml
@@ -0,0 +1,27 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:jpa_testdb
+ driver-class-name: org.h2.Driver
+ username: sa
+ password:
+
+ jpa:
+ database-platform: org.hibernate.dialect.H2Dialect
+ hibernate:
+ ddl-auto: create-drop
+ show-sql: true
+ properties:
+ hibernate:
+ format_sql: true
+ use_sql_comments: true
+
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
+
+logging:
+ level:
+ org.hibernate.SQL: DEBUG
+ org.hibernate.type.descriptor.sql.BasicBinder: TRACE
+ cn.structure.infra.repository: DEBUG
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/pom.xml b/structure-infra-sample/structure-infra-sample-mongodb/pom.xml
new file mode 100644
index 0000000..0f9d6a7
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-mongodb
+ structure-infra-sample-mongodb
+ MongoDB 示例模块 - 演示 MongoDB 仓储实现
+ jar
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+
+ cn.structured
+ structure-infra-sample-core
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-infra-mongodb-starter
+ ${revision}
+
+
+
+
+ cn.structured
+ structure-common
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java
new file mode 100644
index 0000000..e39c8e8
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java
@@ -0,0 +1,19 @@
+package cn.structure.infra.sample.infra.repository;
+
+import cn.structure.infra.annotations.Repository;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.infra.po.UserPO;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户仓储 MongoDB 实现
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Repository(value = "用户仓储", type = RepositoryType.MONGODB, entity = UserEntity.class, po = UserPO.class)
+@Component("userRepository")
+public class UserMongoRepositoryImpl extends AbstractUserRepositoryImpl {
+}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java
new file mode 100644
index 0000000..eac0eb5
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java
@@ -0,0 +1,34 @@
+package cn.structure.infra.sample.infra.repository.mongodb;
+
+import cn.structure.infra.annotations.DelegateFor;
+import cn.structure.infra.mongodb.repository.MongoRepositoryDelegate;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.infra.po.UserPO;
+import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户仓储 MongoDB 实现
+ *
+ * 使用 Spring Data MongoDB 实现用户数据的持久化操作
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Component
+@DelegateFor(
+ name = "userRepository",
+ type = RepositoryType.MONGODB,
+ po = UserPO.class,
+ description = "用户仓储 MongoDB 实现",
+ priority = 10
+)
+public class UserMongoDelegate extends MongoRepositoryDelegate implements UserRepositoryDelegate {
+
+ @Override
+ public UserPO finByName(String name) {
+ // TODO: 实现 MongoDB 查询逻辑
+ return null;
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
new file mode 100644
index 0000000..778bca9
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
@@ -0,0 +1,63 @@
+package cn.structure.infra.sample.mongodb;
+
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import cn.structure.infra.sample.mongodb.config.MongoTestConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * MongoDB 仓储测试
+ *
+ * 测试 MongoDB 实现的用户仓储功能
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@SpringBootTest
+@ActiveProfiles("mongo-test")
+@Import(MongoTestConfig.class)
+@DisplayName("MongoDB 仓储测试")
+public class UserMongoRepositoryTest {
+
+ @Autowired(required = false)
+ private UserRepository userRepository;
+
+ @Test
+ @DisplayName("测试 MongoDB 仓储注入")
+ void testMongoRepositoryInjection() {
+ assertNotNull(userRepository, "MongoDB 仓储应该被成功注入");
+ System.out.println("✓ MongoDB 仓储注入成功: " + userRepository.getClass().getName());
+ }
+
+ @Test
+ @DisplayName("测试 MongoDB 保存功能(待实现)")
+ void testMongoSave() {
+ assertNotNull(userRepository, "MongoDB 仓储应该被注入");
+
+ UserEntity user = new UserEntity();
+ user.setUsername("mongoTestUser");
+ user.setPassword("password123");
+ user.setEmail("mongo@test.com");
+ user.setAge(25);
+
+ // TODO: 等待 MongoDB 实现完成
+ System.out.println("⚠ MongoDB 保存功能待实现,当前返回 null");
+ }
+
+ @Test
+ @DisplayName("测试 MongoDB 查询功能(待实现)")
+ void testMongoFindByName() {
+ assertNotNull(userRepository, "MongoDB 仓储应该被注入");
+
+ // TODO: 等待 MongoDB 实现完成
+ System.out.println("⚠ MongoDB 查询功能待实现");
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java
new file mode 100644
index 0000000..7ef51fe
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java
@@ -0,0 +1,28 @@
+package cn.structure.infra.sample.mongodb.config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+
+/**
+ * MongoDB 测试配置
+ *
+ * 配置 MongoDB 相关的组件扫描
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Configuration
+@ComponentScan(basePackages = {
+ "cn.structure.infra.sample",
+ "cn.structure.infra.repository"
+}, excludeFilters = {
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mybatis.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.jpa.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.elasticsearch.*")
+})
+@EnableMongoRepositories(basePackages = "cn.structure.infra.sample.infra.repository.mongodb")
+public class MongoTestConfig {
+}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/resources/application-mongo-test.yml b/structure-infra-sample/structure-infra-sample-mongodb/src/test/resources/application-mongo-test.yml
new file mode 100644
index 0000000..de19873
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/resources/application-mongo-test.yml
@@ -0,0 +1,11 @@
+spring:
+ data:
+ mongodb:
+ uri: mongodb://localhost:27017/test
+ database: test
+ auto-index-creation: true
+
+logging:
+ level:
+ org.springframework.data.mongodb: DEBUG
+ cn.structure.infra.repository: DEBUG
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/pom.xml b/structure-infra-sample/structure-infra-sample-mybatis/pom.xml
new file mode 100644
index 0000000..e70f342
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-mybatis
+ structure-infra-sample-mybatis
+ MyBatis Plus 示例模块
+ jar
+
+
+
+ cn.structured
+ structure-infra-sample-core
+ ${revision}
+
+
+ cn.structured
+ structure-infra-mybatis-plus-starter
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/InfraSampleApplication.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/InfraSampleApplication.java
new file mode 100644
index 0000000..4db9a8f
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/InfraSampleApplication.java
@@ -0,0 +1,19 @@
+package cn.structure.infra.sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * 示例工程启动类
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@SpringBootApplication
+public class InfraSampleApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(InfraSampleApplication.class, args);
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
new file mode 100644
index 0000000..e985d70
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
@@ -0,0 +1,26 @@
+package cn.structure.infra.sample.infra.config;
+
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 缓存配置
+ *
+ * 提供简单的内存缓存实现,用于数据权限缓存
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+ @Bean
+ public CacheManager cacheManager() {
+ return new ConcurrentMapCacheManager();
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java
new file mode 100644
index 0000000..6d0c47b
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java
@@ -0,0 +1,9 @@
+package cn.structure.infra.sample.infra.mapper;
+
+import cn.structure.infra.sample.infra.po.UserPO;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserMapper extends BaseMapper {
+}
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java
new file mode 100644
index 0000000..a7d15c7
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java
@@ -0,0 +1,12 @@
+package cn.structure.infra.sample.infra.repository;
+
+import cn.structure.infra.annotations.Repository;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.infra.po.UserPO;
+import org.springframework.stereotype.Component;
+
+@Repository(value = "用户仓储", type = RepositoryType.MYBATIS_PLUS, entity = UserEntity.class, po = UserPO.class)
+@Component("userRepository")
+public class UserRepositoryImpl extends AbstractUserRepositoryImpl {
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java
new file mode 100644
index 0000000..4d94941
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java
@@ -0,0 +1,33 @@
+package cn.structure.infra.sample.infra.repository.mybatis;
+
+import cn.structure.infra.annotations.DelegateFor;
+import cn.structure.infra.mybatis.plus.repository.MybatisPlusRepositoryDelegate;
+import cn.structure.infra.repository.RepositoryType;
+import cn.structure.infra.sample.infra.mapper.UserMapper;
+import cn.structure.infra.sample.infra.po.UserPO;
+import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import org.springframework.stereotype.Component;
+
+@Component
+@DelegateFor(
+ name = "userRepository",
+ type = RepositoryType.MYBATIS_PLUS,
+ po = UserPO.class,
+ description = "用户仓储 MyBatis Plus 实现",
+ priority = 10
+)
+public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate {
+
+ private final UserMapper userMapper;
+
+ public UserMybatisPlusDelegate(UserMapper userMapper) {
+ super(userMapper, UserPO.class);
+ this.userMapper = userMapper;
+ }
+
+ @Override
+ public UserPO finByName(String name) {
+ return userMapper.selectOne(Wrappers.lambdaQuery().eq(UserPO::getUsername, name));
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/InfraJpaTestApplication.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/InfraJpaTestApplication.java
new file mode 100644
index 0000000..cf93e5c
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/InfraJpaTestApplication.java
@@ -0,0 +1,21 @@
+package cn.structure.infra.sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * JPA 测试启动类
+ *
+ * 用于 JPA 场景测试,排除 MyBatis Plus 自动配置
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@SpringBootApplication(scanBasePackages = "cn.structure.infra.sample")
+public class InfraJpaTestApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(InfraJpaTestApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java
new file mode 100644
index 0000000..b0909f0
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java
@@ -0,0 +1,30 @@
+package cn.structure.infra.sample.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+
+@Configuration
+@ComponentScan(basePackages = {
+ "cn.structure.infra.sample",
+ "cn.structure.infra.repository"
+}, excludeFilters = {
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.jpa.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mongodb.*"),
+ @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.elasticsearch.*")
+})
+@MapperScan("cn.structure.infra.sample.infra.mapper")
+public class MybatisOnlyConfig {
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
+ return interceptor;
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java
new file mode 100644
index 0000000..2bdc198
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java
@@ -0,0 +1,20 @@
+package cn.structure.infra.sample.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@SpringBootApplication(excludeName = {
+ "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration"
+})
+@ComponentScan(basePackages = "cn.structure.infra.sample")
+@MapperScan("cn.structure.infra.sample.infra.repository.mybatis.mapper")
+public class TestConfig {
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java
new file mode 100644
index 0000000..22e7c43
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java
@@ -0,0 +1,227 @@
+package cn.structure.infra.sample.repository;
+
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.config.MybatisOnlyConfig;
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * UserRepository 测试类 - MyBatis Plus 实现
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@SpringBootTest(classes = MybatisOnlyConfig.class)
+@DisplayName("MyBatis Plus 仓储测试")
+class UserRepositoryTest {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ private UserEntity createUser(String username, String email, Integer age) {
+ UserEntity user = new UserEntity();
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setAge(age);
+ user.setPassword("123456");
+ user.setCreateTime(LocalDateTime.now());
+ user.setUpdateTime(LocalDateTime.now());
+ return user;
+ }
+
+ @Test
+ @DisplayName("测试保存用户")
+ void testSave() {
+ UserEntity user = createUser("zhangsan", "zhangsan@example.com", 25);
+ UserEntity saved = userRepository.save(user);
+
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals("zhangsan", saved.getUsername());
+ assertEquals("zhangsan@example.com", saved.getEmail());
+ assertEquals(25, saved.getAge());
+
+ System.out.println("保存用户成功: " + saved);
+ }
+
+ @Test
+ @DisplayName("测试根据ID查询")
+ void testFindById() {
+ UserEntity user = createUser("lisi", "lisi@example.com", 30);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals("lisi", found.getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryById")
+ void testQueryById() {
+ UserEntity user = createUser("wangwu", "wangwu@example.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.queryById(saved.getId());
+ assertNotNull(found);
+ assertEquals("wangwu", found.getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 存在")
+ void testQueryByIdOptional_Exists() {
+ UserEntity user = createUser("zhaoliu", "zhaoliu@example.com", 35);
+ UserEntity saved = userRepository.save(user);
+
+ Optional optional = userRepository.queryByIdOptional(saved.getId());
+ assertTrue(optional.isPresent());
+ assertEquals("zhaoliu", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 不存在")
+ void testQueryByIdOptional_NotExists() {
+ Optional optional = userRepository.queryByIdOptional(9999L);
+ assertFalse(optional.isPresent());
+ }
+
+ @Test
+ @DisplayName("测试 queryOne - 条件查询单条")
+ void testQueryOne() {
+ userRepository.save(createUser("user1", "user1@test.com", 20));
+ userRepository.save(createUser("user2", "user2@test.com", 25));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("user1");
+
+ UserEntity found = userRepository.queryOne(condition);
+ assertNotNull(found);
+ assertEquals("user1", found.getUsername());
+ assertEquals("user1@test.com", found.getEmail());
+ }
+
+ @Test
+ @DisplayName("测试 queryOneOptional")
+ void testQueryOneOptional() {
+ userRepository.save(createUser("optUser", "opt@test.com", 22));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("optUser");
+
+ Optional optional = userRepository.queryOneOptional(condition);
+ assertTrue(optional.isPresent());
+ assertEquals("optUser", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryOneOptional - 不存在")
+ void testQueryOneOptional_NotExists() {
+ UserEntity condition = new UserEntity();
+ condition.setUsername("nonexistent");
+
+ Optional optional = userRepository.queryOneOptional(condition);
+ assertFalse(optional.isPresent());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 查询全部")
+ void testQueryList_All() {
+ int beforeCount = userRepository.queryList(null).size();
+
+ userRepository.save(createUser("listUser1", "list1@test.com", 20));
+ userRepository.save(createUser("listUser2", "list2@test.com", 25));
+ userRepository.save(createUser("listUser3", "list3@test.com", 30));
+
+ List list = userRepository.queryList(null);
+ assertEquals(beforeCount + 3, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 条件查询")
+ void testQueryList_ByCondition() {
+ userRepository.save(createUser("ageUser1", "age1@test.com", 18));
+ userRepository.save(createUser("ageUser2", "age2@test.com", 25));
+ userRepository.save(createUser("ageUser3", "age3@test.com", 18));
+
+ UserEntity condition = new UserEntity();
+ condition.setAge(18);
+
+ List list = userRepository.queryList(condition);
+ assertTrue(list.size() >= 2);
+ assertTrue(list.stream().allMatch(u -> u.getAge() == 18));
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 列表返回空集合")
+ void testQueryList_Empty() {
+ UserEntity condition = new UserEntity();
+ condition.setAge(999);
+
+ List list = userRepository.queryList(condition);
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ @DisplayName("测试 queryPage - 分页查询")
+ void testQueryPage() {
+ for (int i = 1; i <= 15; i++) {
+ userRepository.save(createUser("pageUser" + i, "page" + i + "@test.com", 20 + i));
+ }
+
+ ReqPage reqPage = new ReqPage();
+ reqPage.setPage(2);
+ reqPage.setSize(5);
+
+ ResPage page = userRepository.queryPage(reqPage);
+
+ assertNotNull(page);
+ assertEquals(2, page.getCurrent());
+ assertEquals(5, page.getSize());
+ assertTrue(page.getTotal() >= 15);
+ assertEquals(5, page.getRecords().size());
+
+ System.out.println("分页查询结果: 当前页=" + page.getCurrent()
+ + ", 总页数=" + page.getPages()
+ + ", 每页=" + page.getSize()
+ + ", 总数=" + page.getTotal()
+ + ", 记录数=" + page.getRecords().size());
+ }
+
+ @Test
+ @DisplayName("测试删除用户")
+ void testRemoveById() {
+ UserEntity user = createUser("delUser", "del@test.com", 20);
+ UserEntity saved = userRepository.save(user);
+ assertNotNull(userRepository.findById(saved.getId()));
+
+ userRepository.removeById(saved.getId());
+ assertNull(userRepository.findById(saved.getId()));
+ }
+
+ @Test
+ @DisplayName("测试 Entity <-> PO 转换")
+ void testEntityPoConversion() {
+ UserEntity user = createUser("convertUser", "convert@test.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals(saved.getUsername(), found.getUsername());
+ assertEquals(saved.getEmail(), found.getEmail());
+ assertEquals(saved.getAge(), found.getAge());
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/application.yml b/structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/application.yml
new file mode 100644
index 0000000..41e487f
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/application.yml
@@ -0,0 +1,29 @@
+spring:
+ datasource:
+ driver-class-name: org.h2.Driver
+ url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+ username: sa
+ password:
+ sql:
+ init:
+ mode: always
+ schema-locations: classpath:schema.sql
+ h2:
+ console:
+ enabled: true
+ autoconfigure:
+ exclude:
+ - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration
+
+mybatis-plus:
+ configuration:
+ map-underscore-to-camel-case: true
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ global-config:
+ db-config:
+ id-type: auto
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/schema.sql b/structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/schema.sql
new file mode 100644
index 0000000..3b13648
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/resources/schema.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS t_user (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(255) NOT NULL,
+ password VARCHAR(255),
+ email VARCHAR(255),
+ age INT,
+ create_time TIMESTAMP,
+ update_time TIMESTAMP
+);
diff --git a/structure-infra-starter/pom.xml b/structure-infra-starter/pom.xml
new file mode 100644
index 0000000..bf8264e
--- /dev/null
+++ b/structure-infra-starter/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-pro-starter
+ structure-infra-starter
+ mstructure-pro-starter
+ jar
+
+
+
+ cn.structured
+ structure-common
+
+
+ cn.structured
+ structure-datascope-starter
+
+
+ cn.structured
+ structure-datascope-message
+
+
+ cn.structured
+ structure-datascope-cache
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java
new file mode 100644
index 0000000..88650bf
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java
@@ -0,0 +1,64 @@
+package cn.structure.infra.annotations;
+
+import cn.structure.infra.repository.RepositoryType;
+
+import java.lang.annotation.*;
+
+/**
+ * 仓储委托实现标记注解
+ *
+ * 标注在具体的 RepositoryDelegate 实现类上
+ *
+ * 示例:
+ *
+ * @DelegateFor(name = "userRepository", po = UserPO.class)
+ * public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate<UserPO, Long> {
+ * // 实现
+ * }
+ *
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Inherited
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface DelegateFor {
+
+ /**
+ * 仓储名称,对应 RepositoryFacade 的 Bean 名称
+ *
+ * @return 仓储名称
+ */
+ String name() default "";
+
+ /**
+ * 存储类型
+ *
+ * @return 仓储类型
+ */
+ RepositoryType type() default RepositoryType.AUTO;
+
+ /**
+ * 持久化对象类型
+ *
+ * @return PO 类
+ */
+ Class> po() default Object.class;
+
+ /**
+ * 描述
+ *
+ * @return 描述信息
+ */
+ String description() default "";
+
+ /**
+ * 优先级,多个同类型 Delegate 时使用优先级高的
+ *
+ * @return 优先级,数字越大优先级越高
+ */
+ int priority() default 0;
+}
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java
new file mode 100644
index 0000000..eefc36f
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java
@@ -0,0 +1,83 @@
+package cn.structure.infra.annotations;
+
+import cn.structure.infra.repository.RepositoryType;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+@Inherited
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Repository {
+
+ /**
+ * 仓储名称
+ *
+ * @return
+ */
+ String value() default "";
+
+ /**
+ * 仓储类型 默认自动
+ *
+ * @return
+ */
+ RepositoryType type() default RepositoryType.AUTO;
+
+
+ /**
+ * 实体类
+ *
+ * @return
+ */
+ Class> entity() default Object.class;
+
+ /**
+ * PO持久化对象类型
+ *
+ * 用于 RepositoryFacade 中的 Entity <-> PO 转换
+ *
+ * @return
+ */
+ Class> po() default Object.class;
+
+ /**
+ * 主键类型
+ *
+ * 默认 Long,如果需要指定其他类型可配置
+ *
+ * @return
+ */
+ Class> id() default Long.class;
+
+ /**
+ * 仓储描述
+ *
+ * @return
+ */
+ String description() default "";
+
+
+ /**
+ * 是否缓存
+ *
+ * @return
+ */
+ boolean cache() default false;
+
+ /**
+ * 缓存时间
+ *
+ * @return
+ */
+ long cacheTime() default 60L;
+
+ /**
+ * 缓存时间单位
+ *
+ * @return
+ */
+ TimeUnit cacheTimeUnit() default TimeUnit.SECONDS;
+
+
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoEventConfiguration.java b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoEventConfiguration.java
new file mode 100644
index 0000000..88b646a
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoEventConfiguration.java
@@ -0,0 +1,24 @@
+package cn.structure.infra.configuration;
+
+import cn.structure.infra.event.DefaultEventManagerImpl;
+import cn.structure.infra.event.EventManager;
+import cn.structure.infra.properties.InfraProperties;
+import cn.structured.datascope.message.wrapper.DataScopeStreamBridge;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableConfigurationProperties(InfraProperties.class)
+public class AutoEventConfiguration {
+
+ @Bean
+ @ConditionalOnBean(EventManager.class)
+ public EventManager eventManager(ApplicationEventPublisher applicationEventPublisher,
+ DataScopeStreamBridge streamBridge,
+ InfraProperties infraProperties) {
+ return new DefaultEventManagerImpl(applicationEventPublisher, streamBridge, infraProperties);
+ }
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java
new file mode 100644
index 0000000..ffe8224
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java
@@ -0,0 +1,21 @@
+package cn.structure.infra.configuration;
+
+import cn.structure.infra.repository.RepositoryBeanPostProcessor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class AutoRepositoryConfiguration {
+
+ /**
+ * 注册 Repository Bean 后处理器
+ *
+ * 自动为 RepositoryFacade 的子类注入 RepositoryDelegate
+ *
+ * @return RepositoryBeanPostProcessor
+ */
+ @Bean
+ public static RepositoryBeanPostProcessor repositoryBeanPostProcessor() {
+ return new RepositoryBeanPostProcessor();
+ }
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/event/DefaultEventManagerImpl.java b/structure-infra-starter/src/main/java/cn/structure/infra/event/DefaultEventManagerImpl.java
new file mode 100644
index 0000000..ba35d1a
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/event/DefaultEventManagerImpl.java
@@ -0,0 +1,44 @@
+package cn.structure.infra.event;
+
+import cn.structure.infra.properties.InfraProperties;
+import cn.structured.datascope.message.wrapper.DataScopeStreamBridge;
+import lombok.AllArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+
+/**
+ *
+ * 事件管理器
+ *
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2021/6/21 16:05
+ */
+@AllArgsConstructor
+public class DefaultEventManagerImpl implements EventManager {
+
+ private final ApplicationEventPublisher eventPublisher;
+
+ private final DataScopeStreamBridge streamBridge;
+
+ private final InfraProperties infraProperties;
+
+ @Override
+ public void publish(Event event) {
+ if (event.getEventChannel().equals(EventChannel.DEFAULT)) {
+ if (infraProperties.getDefaultEventChannel() == EventChannel.SPRING_EVENT) {
+ eventPublisher.publishEvent(event);
+ }
+ if (infraProperties.getDefaultEventChannel() == EventChannel.MESSAGE_EVENT) {
+ streamBridge.send(event.getEventId(), event);
+ }
+ } else {
+ if (event.getEventChannel() == EventChannel.SPRING_EVENT) {
+ eventPublisher.publishEvent(event);
+ }
+ if (event.getEventChannel() == EventChannel.MESSAGE_EVENT) {
+ streamBridge.send(event.getEventId(), event);
+ }
+ }
+ }
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/event/Event.java b/structure-infra-starter/src/main/java/cn/structure/infra/event/Event.java
new file mode 100644
index 0000000..2c5aafd
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/event/Event.java
@@ -0,0 +1,30 @@
+package cn.structure.infra.event;
+
+/**
+ *
+ * 事件接口
+ *
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2021/6/1 17:01
+ */
+public interface Event {
+
+ /**
+ * 获取事件ID 事件ID
+ *
+ * @return 事件ID
+ */
+ String getEventId();
+
+ /**
+ * 获取事件渠道类型
+ *
+ * @return 事件类型
+ */
+ default EventChannel getEventChannel() {
+ return EventChannel.DEFAULT;
+ }
+
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/event/EventChannel.java b/structure-infra-starter/src/main/java/cn/structure/infra/event/EventChannel.java
new file mode 100644
index 0000000..7017f36
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/event/EventChannel.java
@@ -0,0 +1,11 @@
+package cn.structure.infra.event;
+
+import lombok.Getter;
+
+@Getter
+public enum EventChannel {
+ DEFAULT,
+ SPRING_EVENT,
+ MESSAGE_EVENT,
+ ;
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/event/EventManager.java b/structure-infra-starter/src/main/java/cn/structure/infra/event/EventManager.java
new file mode 100644
index 0000000..06a83bb
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/event/EventManager.java
@@ -0,0 +1,20 @@
+package cn.structure.infra.event;
+
+/**
+ *
+ * 事件管理器
+ *
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2021/6/21 16:05
+ */
+public interface EventManager {
+
+ /**
+ * 发布事件
+ * @param event 事件
+ */
+ void publish(Event event);
+
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/package-info.java b/structure-infra-starter/src/main/java/cn/structure/infra/package-info.java
new file mode 100644
index 0000000..1b836e9
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/package-info.java
@@ -0,0 +1 @@
+package cn.structure.infra;
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java b/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java
new file mode 100644
index 0000000..53be23e
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java
@@ -0,0 +1,38 @@
+package cn.structure.infra.properties;
+
+import cn.structure.infra.event.EventChannel;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "structure.infra")
+public class InfraProperties {
+
+ /**
+ * 默认事件类型
+ */
+ private EventChannel defaultEventChannel = EventChannel.SPRING_EVENT;
+
+ /**
+ * 是否开启CQRS
+ */
+ private Boolean cqrs = false;
+
+ /**
+ * 缓存时间
+ *
+ * @return
+ */
+ private Long cacheTime = 60L;
+
+ /**
+ * 缓存时间单位
+ *
+ * @return
+ */
+ private TimeUnit cacheTimeUnit = TimeUnit.SECONDS;
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java
new file mode 100644
index 0000000..b235c88
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java
@@ -0,0 +1,280 @@
+package cn.structure.infra.repository;
+
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+/**
+ * 内存版仓储委托实现
+ *
+ * 基于 ConcurrentHashMap 实现的内存仓储,用于示例、测试和开发阶段
+ * 支持基本的 CRUD、条件查询、分页查询
+ *
+ * @param 实体类型
+ * @param 主键类型
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Slf4j
+public class InMemoryRepositoryDelegate implements RepositoryDelegate {
+
+ private final Map storage = new ConcurrentHashMap<>();
+ private final AtomicLong idGenerator = new AtomicLong(1);
+ private final Class entityClass;
+ private final String idFieldName;
+
+ public InMemoryRepositoryDelegate(Class entityClass) {
+ this(entityClass, "id");
+ }
+
+ public InMemoryRepositoryDelegate(Class entityClass, String idFieldName) {
+ this.entityClass = entityClass;
+ this.idFieldName = idFieldName;
+ log.info("InMemoryRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName());
+ }
+
+ @Override
+ public T save(T entity) {
+ if (entity == null) {
+ return null;
+ }
+ ID id = getIdValue(entity);
+ if (id == null) {
+ id = generateId();
+ setIdValue(entity, id);
+ }
+ storage.put(id, entity);
+ log.debug("Saved entity: id={}, entity={}", id, entity);
+ return entity;
+ }
+
+ @Override
+ public void removeById(ID id) {
+ if (id != null) {
+ T removed = storage.remove(id);
+ log.debug("Removed entity: id={}, removed={}", id, removed != null);
+ }
+ }
+
+ @Override
+ public T findById(ID id) {
+ if (id == null) {
+ return null;
+ }
+ T entity = storage.get(id);
+ log.debug("Find by id: id={}, found={}", id, entity != null);
+ return entity;
+ }
+
+ @Override
+ public T queryById(ID id) {
+ return findById(id);
+ }
+
+ @Override
+ public Optional queryByIdOptional(ID id) {
+ return Optional.ofNullable(queryById(id));
+ }
+
+ @Override
+ public T queryOne(T condition) {
+ if (condition == null) {
+ return null;
+ }
+ List results = queryList(condition);
+ return results.isEmpty() ? null : results.get(0);
+ }
+
+ @Override
+ public Optional queryOneOptional(T condition) {
+ return Optional.ofNullable(queryOne(condition));
+ }
+
+ @Override
+ public List queryList(T condition) {
+ if (condition == null) {
+ return new ArrayList<>(storage.values());
+ }
+ return storage.values().stream()
+ .filter(entity -> matchesCondition(entity, condition))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public ResPage queryPage(ReqPage reqPage) {
+ ResPage page = new ResPage<>();
+ List allValues = new ArrayList<>(storage.values());
+ long total = allValues.size();
+ int pageNum = reqPage.getPage() != null ? reqPage.getPage() : 1;
+ int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10;
+
+ long pages = total > 0 ? (total + pageSize - 1) / pageSize : 0;
+ int fromIndex = (pageNum - 1) * pageSize;
+ int toIndex = Math.min(fromIndex + pageSize, allValues.size());
+
+ List records = (fromIndex >= allValues.size())
+ ? List.of()
+ : allValues.subList(fromIndex, toIndex);
+
+ page.setCurrent((long) pageNum);
+ page.setPages(pages);
+ page.setSize((long) pageSize);
+ page.setTotal(total);
+ page.setRecords(records);
+
+ log.debug("Query page: page={}, size={}, total={}, records={}",
+ pageNum, pageSize, total, records.size());
+ return page;
+ }
+
+ /**
+ * 检查实体是否匹配条件
+ *
+ * 通过反射比较非空字段的值
+ */
+ private boolean matchesCondition(T entity, T condition) {
+ try {
+ Field[] fields = getAllFields(entity.getClass());
+ for (Field field : fields) {
+ field.setAccessible(true);
+ Object conditionValue = field.get(condition);
+ if (conditionValue != null) {
+ Object entityValue = field.get(entity);
+ if (!conditionValue.equals(entityValue)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ } catch (Exception e) {
+ log.warn("Error matching condition: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ private Field[] getAllFields(Class> clazz) {
+ List fields = new ArrayList<>();
+ while (clazz != null && clazz != Object.class) {
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ clazz = clazz.getSuperclass();
+ }
+ return fields.toArray(new Field[0]);
+ }
+
+ @SuppressWarnings("unchecked")
+ private ID getIdValue(T entity) {
+ try {
+ Field field = findIdField(entity.getClass());
+ if (field != null) {
+ field.setAccessible(true);
+ return (ID) field.get(entity);
+ }
+ } catch (Exception e) {
+ log.warn("Error getting id value: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ private void setIdValue(T entity, ID id) {
+ try {
+ Field field = findIdField(entity.getClass());
+ if (field != null) {
+ field.setAccessible(true);
+ if (field.getType() == Long.class || field.getType() == long.class) {
+ field.set(entity, id);
+ } else if (field.getType() == Integer.class || field.getType() == int.class) {
+ field.set(entity, ((Number) id).intValue());
+ } else if (field.getType() == String.class) {
+ field.set(entity, String.valueOf(id));
+ } else {
+ field.set(entity, id);
+ }
+ }
+ } catch (Exception e) {
+ log.warn("Error setting id value: {}", e.getMessage());
+ }
+ }
+
+ private Field findIdField(Class> clazz) {
+ try {
+ Field field = clazz.getDeclaredField(idFieldName);
+ return field;
+ } catch (NoSuchFieldException e) {
+ if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) {
+ return findIdField(clazz.getSuperclass());
+ }
+ return null;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private ID generateId() {
+ return (ID) Long.valueOf(idGenerator.getAndIncrement());
+ }
+
+ /**
+ * 获取存储大小(用于测试)
+ */
+ public int size() {
+ return storage.size();
+ }
+
+ /**
+ * 清空存储(用于测试)
+ */
+ public void clear() {
+ storage.clear();
+ idGenerator.set(1);
+ }
+
+ @Override
+ public List saveBatch(List entities) {
+ if (entities == null || entities.isEmpty()) {
+ return List.of();
+ }
+ return entities.stream()
+ .map(this::save)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public void removeBatchByIds(List ids) {
+ if (ids != null) {
+ ids.forEach(this::removeById);
+ }
+ }
+
+ @Override
+ public List listByIds(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ return List.of();
+ }
+ return ids.stream()
+ .map(this::findById)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public long count(T condition) {
+ if (condition == null) {
+ return storage.size();
+ }
+ return storage.values().stream()
+ .filter(entity -> matchesCondition(entity, condition))
+ .count();
+ }
+
+ @Override
+ public boolean exists(T condition) {
+ return count(condition) > 0;
+ }
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java
new file mode 100644
index 0000000..d5c4e29
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java
@@ -0,0 +1,206 @@
+package cn.structure.infra.repository;
+
+import cn.structure.infra.annotations.DelegateFor;
+import cn.structure.infra.annotations.Repository;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+@Slf4j
+public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, ApplicationListener {
+
+ private ApplicationContext applicationContext;
+
+ private final List delegateInfos = new ArrayList<>();
+
+ private final List facadeInfos = new ArrayList<>();
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof RepositoryDelegate) {
+ DelegateFor annotation = bean.getClass().getAnnotation(DelegateFor.class);
+ if (annotation != null) {
+ DelegateInfo info = new DelegateInfo();
+ info.beanName = beanName;
+ info.delegate = (RepositoryDelegate, ?>) bean;
+ info.name = annotation.name();
+ info.type = annotation.type();
+ info.poClass = annotation.po();
+ info.priority = annotation.priority();
+ info.description = annotation.description();
+ info.delegateClass = bean.getClass();
+ delegateInfos.add(info);
+ log.info("Found RepositoryDelegate: name={}, type={}, poClass={}, delegateClass={}, priority={}",
+ info.name, info.type,
+ info.poClass != null ? info.poClass.getSimpleName() : "null",
+ info.delegateClass.getSimpleName(),
+ info.priority);
+ }
+ }
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ if (bean instanceof RepositoryFacade) {
+ RepositoryFacadeInfo info = new RepositoryFacadeInfo();
+ info.facade = (RepositoryFacade, ?, ?, ?>) bean;
+ info.beanName = beanName;
+ facadeInfos.add(info);
+ }
+ return bean;
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ for (RepositoryFacadeInfo facadeInfo : facadeInfos) {
+ injectDelegateToFacade(facadeInfo.facade, facadeInfo.beanName);
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private void injectDelegateToFacade(RepositoryFacade facade, String beanName) {
+ try {
+ Class>[] genericTypes = getGenericTypes(facade.getClass());
+ if (genericTypes.length < 4) {
+ log.debug("RepositoryFacade '{}' has insufficient generic types (need 4, got {})", beanName, genericTypes.length);
+ return;
+ }
+
+ Class> entityClass = genericTypes[0];
+ Class> idClass = genericTypes[1];
+ Class> poClass = genericTypes[2];
+ Class> delegateClass = genericTypes[3];
+
+ Repository repositoryAnnotation = facade.getClass().getAnnotation(Repository.class);
+ RepositoryType targetType = repositoryAnnotation != null ? repositoryAnnotation.type() : RepositoryType.AUTO;
+
+ log.debug("RepositoryFacade '{}' requires: entity={}, id={}, po={}, delegateClass={}, type={}",
+ beanName, entityClass.getSimpleName(), idClass.getSimpleName(),
+ poClass.getSimpleName(), delegateClass.getSimpleName(), targetType);
+
+ DelegateInfo matchedInfo = findBestMatch(beanName, poClass, delegateClass, targetType);
+
+ if (matchedInfo != null) {
+ facade.setBaseDelegate(matchedInfo.delegate);
+ log.info("Injected {} delegate '{}' into RepositoryFacade '{}'",
+ matchedInfo.type, matchedInfo.beanName, beanName);
+ } else {
+ log.warn("No matching delegate found for RepositoryFacade '{}', using default InMemoryRepositoryDelegate", beanName);
+ RepositoryDelegate defaultDelegate = new InMemoryRepositoryDelegate(poClass);
+ facade.setBaseDelegate(defaultDelegate);
+ }
+
+ facade.setEntityClass(entityClass);
+ facade.setPoClass(poClass);
+
+ } catch (Exception e) {
+ log.warn("Error injecting delegate to RepositoryFacade '{}': {}", beanName, e.getMessage());
+ }
+ }
+
+ private DelegateInfo findBestMatch(String beanName, Class> poClass, Class> delegateClass, RepositoryType targetType) {
+ delegateInfos.sort(Comparator.comparingInt((DelegateInfo i) -> i.priority).reversed());
+
+ for (DelegateInfo info : delegateInfos) {
+ if (delegateClass.isAssignableFrom(info.delegateClass)
+ && info.name != null && !info.name.isEmpty() && info.name.equals(beanName)
+ && (targetType == RepositoryType.AUTO || targetType == info.type)) {
+ return info;
+ }
+ }
+
+ for (DelegateInfo info : delegateInfos) {
+ if (delegateClass.isAssignableFrom(info.delegateClass)
+ && (targetType == RepositoryType.AUTO || targetType == info.type)) {
+ return info;
+ }
+ }
+
+ for (DelegateInfo info : delegateInfos) {
+ if (delegateClass.isAssignableFrom(info.delegateClass)
+ && info.name != null && !info.name.isEmpty() && info.name.equals(beanName)) {
+ return info;
+ }
+ }
+
+ for (DelegateInfo info : delegateInfos) {
+ if (delegateClass.isAssignableFrom(info.delegateClass)) {
+ return info;
+ }
+ }
+
+ for (DelegateInfo info : delegateInfos) {
+ if (info.name != null && !info.name.isEmpty() && info.name.equals(beanName)) {
+ return info;
+ }
+ }
+
+ for (DelegateInfo info : delegateInfos) {
+ if (info.poClass != null && info.poClass.equals(poClass)) {
+ return info;
+ }
+ }
+
+ return null;
+ }
+
+ private Class>[] getGenericTypes(Class> clazz) {
+ Class> currentClass = clazz;
+ while (currentClass != null && currentClass != Object.class) {
+ Type superclass = currentClass.getGenericSuperclass();
+ if (superclass instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) superclass;
+ Type rawType = parameterizedType.getRawType();
+ if (rawType instanceof Class && RepositoryFacade.class.isAssignableFrom((Class>) rawType)) {
+ Type[] typeArgs = parameterizedType.getActualTypeArguments();
+ Class>[] classes = new Class[typeArgs.length];
+ for (int i = 0; i < typeArgs.length; i++) {
+ if (typeArgs[i] instanceof Class) {
+ classes[i] = (Class>) typeArgs[i];
+ } else if (typeArgs[i] instanceof ParameterizedType) {
+ Type raw = ((ParameterizedType) typeArgs[i]).getRawType();
+ if (raw instanceof Class) {
+ classes[i] = (Class>) raw;
+ }
+ }
+ }
+ return classes;
+ }
+ }
+ currentClass = currentClass.getSuperclass();
+ }
+ return new Class>[0];
+ }
+
+ private static class DelegateInfo {
+ String beanName;
+ String name;
+ RepositoryType type;
+ Class> poClass;
+ int priority;
+ String description;
+ Class> delegateClass;
+ RepositoryDelegate, ?> delegate;
+ }
+
+ private static class RepositoryFacadeInfo {
+ RepositoryFacade, ?, ?, ?> facade;
+ String beanName;
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java
new file mode 100644
index 0000000..77a395d
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java
@@ -0,0 +1,103 @@
+package cn.structure.infra.repository;
+
+import cn.structure.infra.annotations.Repository;
+import cn.structure.infra.repository.RepositoryType;
+import lombok.Data;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 仓储定义元数据
+ *
+ * 封装 @Repository 注解的配置信息
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+@Data
+public class RepositoryDefinition {
+
+ /**
+ * 仓储名称(Bean名称)
+ */
+ private String name;
+
+ /**
+ * 仓储类型
+ */
+ private RepositoryType type;
+
+ /**
+ * 实体类类型
+ */
+ private Class> entityClass;
+
+ /**
+ * PO持久化对象类型
+ */
+ private Class> poClass;
+
+ /**
+ * 主键类型
+ */
+ private Class> idClass;
+
+ /**
+ * 仓储描述
+ */
+ private String description;
+
+ /**
+ * 是否启用缓存
+ */
+ private boolean cache;
+
+ /**
+ * 缓存时间
+ */
+ private long cacheTime;
+
+ /**
+ * 缓存时间单位
+ */
+ private TimeUnit cacheTimeUnit;
+
+ /**
+ * 原始注解
+ */
+ private Repository annotation;
+
+ /**
+ * 从注解构建仓储定义
+ *
+ * @param annotation @Repository注解实例
+ * @param beanName Bean名称
+ * @return 仓储定义
+ */
+ public static RepositoryDefinition fromAnnotation(Repository annotation, String beanName) {
+ RepositoryDefinition definition = new RepositoryDefinition();
+ definition.setName(beanName);
+ definition.setType(annotation.type());
+ definition.setEntityClass(annotation.entity());
+ definition.setPoClass(annotation.po());
+ definition.setIdClass(annotation.id());
+ definition.setDescription(annotation.description());
+ definition.setCache(annotation.cache());
+ definition.setCacheTime(annotation.cacheTime());
+ definition.setCacheTimeUnit(annotation.cacheTimeUnit());
+ definition.setAnnotation(annotation);
+ return definition;
+ }
+
+ /**
+ * 验证配置是否有效
+ *
+ * @return true if valid
+ */
+ public boolean isValid() {
+ return entityClass != null && entityClass != Object.class
+ && poClass != null && poClass != Object.class;
+ }
+
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java
new file mode 100644
index 0000000..29b4af3
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java
@@ -0,0 +1,65 @@
+package cn.structure.infra.repository;
+
+import cn.structure.common.repository.ICrudRepository;
+
+import java.util.List;
+
+/**
+ * 仓储委托接口
+ *
+ * 定义持久化层的操作契约,面向持久化对象(PO)。
+ * 不同的持久化技术(MyBatis、JPA、MongoDB等)提供各自的实现。
+ *
+ * 这是防腐层(ACL)的核心组件之一:
+ * - 对外:由 RepositoryFacade 调用,面向领域模型
+ * - 对内:操作持久化模型(PO),与具体存储技术交互
+ *
+ * DDD 场景下,用户可以自定义实现此接口来满足特殊的持久化需求。
+ *
+ * @param 持久化对象类型(PO)
+ * @param 主键类型
+ * @author chuck
+ * @version 1.0.1
+ * @since 2026/6/28
+ */
+public interface RepositoryDelegate extends ICrudRepository {
+
+ /**
+ * 批量保存
+ *
+ * @param entities 实体列表
+ * @return 保存后的实体列表
+ */
+ List saveBatch(List entities);
+
+ /**
+ * 批量删除
+ *
+ * @param ids 主键列表
+ */
+ void removeBatchByIds(List ids);
+
+ /**
+ * 根据ID列表查询
+ *
+ * @param ids 主键列表
+ * @return 实体列表
+ */
+ List listByIds(List ids);
+
+ /**
+ * 统计数量
+ *
+ * @param condition 条件
+ * @return 数量
+ */
+ long count(T condition);
+
+ /**
+ * 判断是否存在
+ *
+ * @param condition 条件
+ * @return 是否存在
+ */
+ boolean exists(T condition);
+}
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java
new file mode 100644
index 0000000..4f60210
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java
@@ -0,0 +1,168 @@
+package cn.structure.infra.repository;
+
+import cn.structure.common.repository.ICrudRepository;
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import cn.structured.datascope.cache.manager.DataScopeCacheManager;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.beans.BeanUtils;
+
+import java.util.List;
+import java.util.Optional;
+
+@Setter
+public class RepositoryFacade> implements ICrudRepository {
+
+ protected DataScopeCacheManager cacheManager;
+
+ protected D baseDelegate;
+
+ protected Class entityClass;
+
+ protected Class poClass;
+
+ public RepositoryFacade() {
+ }
+
+ public RepositoryFacade(Class entityClass, Class poClass) {
+ this.entityClass = entityClass;
+ this.poClass = poClass;
+ }
+
+ public D getBaseDelegate() {
+ return baseDelegate;
+ }
+
+ @Override
+ public T save(T entity) {
+ P save = baseDelegate.save(toPo(entity));
+ return toEntity(save);
+ }
+
+ @Override
+ public void removeById(ID id) {
+ baseDelegate.removeById(id);
+ }
+
+ @Override
+ public T findById(ID id) {
+ P po = baseDelegate.findById(id);
+ return toEntity(po);
+ }
+
+ @Override
+ public T queryById(ID id) {
+ P po = baseDelegate.queryById(id);
+ return toEntity(po);
+ }
+
+ @Override
+ public Optional queryByIdOptional(ID id) {
+ P po = baseDelegate.queryById(id);
+ return Optional.ofNullable(toEntity(po));
+ }
+
+ @Override
+ public T queryOne(T entity) {
+ P p = baseDelegate.queryOne(toPo(entity));
+ return toEntity(p);
+ }
+
+ @Override
+ public Optional queryOneOptional(T entity) {
+ Optional p = baseDelegate.queryOneOptional(toPo(entity));
+ return p.map(this::toEntity);
+ }
+
+ @Override
+ public List queryList(T entity) {
+ List poList = baseDelegate.queryList(toPo(entity));
+ if (poList == null || poList.isEmpty()) {
+ return List.of();
+ }
+ return poList.stream()
+ .map(this::toEntity)
+ .toList();
+ }
+
+ @Override
+ public ResPage queryPage(ReqPage reqPage) {
+ ResPage poPage = baseDelegate.queryPage(reqPage);
+ if (poPage == null) {
+ return null;
+ }
+ ResPage tPage = new ResPage<>();
+ tPage.setCurrent(poPage.getCurrent());
+ tPage.setPages(poPage.getPages());
+ tPage.setSize(poPage.getSize());
+ tPage.setTotal(poPage.getTotal());
+ if (poPage.getRecords() != null) {
+ tPage.setRecords(poPage.getRecords().stream()
+ .map(this::toEntity)
+ .toList());
+ }
+ return tPage;
+ }
+
+ public List saveBatch(List entities) {
+ if (entities == null || entities.isEmpty()) {
+ return List.of();
+ }
+ List poList = entities.stream()
+ .map(this::toPo)
+ .toList();
+ List
savedPoList = baseDelegate.saveBatch(poList);
+ return savedPoList.stream()
+ .map(this::toEntity)
+ .toList();
+ }
+
+ public void removeBatchByIds(List ids) {
+ baseDelegate.removeBatchByIds(ids);
+ }
+
+ public List listByIds(List ids) {
+ List poList = baseDelegate.listByIds(ids);
+ if (poList == null || poList.isEmpty()) {
+ return List.of();
+ }
+ return poList.stream()
+ .map(this::toEntity)
+ .toList();
+ }
+
+ public long count(T entity) {
+ return baseDelegate.count(toPo(entity));
+ }
+
+ public boolean exists(T entity) {
+ return baseDelegate.exists(toPo(entity));
+ }
+
+ protected T toEntity(P po) {
+ if (po == null) {
+ return null;
+ }
+ try {
+ T entity = entityClass.getDeclaredConstructor().newInstance();
+ BeanUtils.copyProperties(po, entity);
+ return entity;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to convert PO to entity", e);
+ }
+ }
+
+ protected P toPo(T entity) {
+ if (entity == null) {
+ return null;
+ }
+ try {
+ P po = poClass.getDeclaredConstructor().newInstance();
+ BeanUtils.copyProperties(entity, po);
+ return po;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to convert entity to PO", e);
+ }
+ }
+}
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java
new file mode 100644
index 0000000..eed7a4a
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java
@@ -0,0 +1,42 @@
+package cn.structure.infra.repository;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.FactoryBean;
+
+@Slf4j
+public class RepositoryFacadeFactoryBean> implements FactoryBean> {
+
+ private final Class entityClass;
+ private final Class poClass;
+ private final Class delegateClass;
+ private final RepositoryDefinition definition;
+
+ public RepositoryFacadeFactoryBean(Class entityClass, Class poClass, Class delegateClass, RepositoryDefinition definition) {
+ this.entityClass = entityClass;
+ this.poClass = poClass;
+ this.delegateClass = delegateClass;
+ this.definition = definition;
+ }
+
+ @Override
+ public RepositoryFacade getObject() {
+ log.debug("Creating RepositoryFacade for entity: {}, po: {}, delegate: {}",
+ entityClass.getName(), poClass.getName(), delegateClass.getName());
+ return new RepositoryFacade<>(entityClass, poClass);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Class> getObjectType() {
+ return RepositoryFacade.class;
+ }
+
+ @Override
+ public boolean isSingleton() {
+ return true;
+ }
+
+ public RepositoryDefinition getDefinition() {
+ return definition;
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryType.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryType.java
new file mode 100644
index 0000000..ce00375
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryType.java
@@ -0,0 +1,31 @@
+package cn.structure.infra.repository;
+
+/**
+ *
+ * 仓储类型
+ *
+ *
+ * @author chuck
+ * @version 1.0.1
+ * @since 2021/6/21 16:05
+ */
+public enum RepositoryType {
+
+ MYBATIS,
+
+ MYBATIS_PLUS,
+
+ JPA,
+
+ JDBC,
+
+ NOSQL,
+
+ REDIS,
+
+ MONGODB,
+
+ ELASTICSEARCH,
+
+ AUTO
+}
diff --git a/structure-infra-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..5c436b6
--- /dev/null
+++ b/structure-infra-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,2 @@
+cn.structure.infra.configuration.AutoEventConfiguration
+cn.structure.infra.configuration.AutoRepositoryConfiguration
\ No newline at end of file
From 7ae6b5b65486552790a33680bb166fe9acde7b7f Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sun, 28 Jun 2026 13:20:04 +0800
Subject: [PATCH 02/21] =?UTF-8?q?refactor(repository):=20=E9=87=8D?=
=?UTF-8?q?=E6=9E=84=E4=BB=93=E5=82=A8=E5=B1=82=E6=8E=A5=E5=8F=A3=E5=92=8C?=
=?UTF-8?q?=E9=97=A8=E9=9D=A2=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 移除 RepositoryDelegate 接口中的批量操作方法定义
- 为 RepositoryFacade 添加获取基础仓储代理的方法
- 为 RepositoryFacade 中的批量操作方法添加 @Override 注解
- 优化批量保存、删除、查询方法的实现逻辑
- 统一方法参数验证和空值处理机制
- 改进 PO 和 DTO 之间的转换流程
---
.../infra/repository/RepositoryDelegate.java | 38 -------------------
.../infra/repository/RepositoryFacade.java | 11 +++++-
2 files changed, 10 insertions(+), 39 deletions(-)
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java
index 29b4af3..0841a2d 100644
--- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java
@@ -24,42 +24,4 @@
*/
public interface RepositoryDelegate extends ICrudRepository {
- /**
- * 批量保存
- *
- * @param entities 实体列表
- * @return 保存后的实体列表
- */
- List saveBatch(List entities);
-
- /**
- * 批量删除
- *
- * @param ids 主键列表
- */
- void removeBatchByIds(List ids);
-
- /**
- * 根据ID列表查询
- *
- * @param ids 主键列表
- * @return 实体列表
- */
- List listByIds(List ids);
-
- /**
- * 统计数量
- *
- * @param condition 条件
- * @return 数量
- */
- long count(T condition);
-
- /**
- * 判断是否存在
- *
- * @param condition 条件
- * @return 是否存在
- */
- boolean exists(T condition);
}
\ No newline at end of file
diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java
index 4f60210..f4675fa 100644
--- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java
@@ -10,7 +10,6 @@
import java.util.List;
import java.util.Optional;
-
@Setter
public class RepositoryFacade> implements ICrudRepository {
@@ -30,6 +29,11 @@ public RepositoryFacade(Class entityClass, Class poClass) {
this.poClass = poClass;
}
+ /**
+ * 获取基础仓储代理
+ *
+ * @return 基础仓储代理
+ */
public D getBaseDelegate() {
return baseDelegate;
}
@@ -105,6 +109,7 @@ public ResPage queryPage(ReqPage reqPage) {
return tPage;
}
+ @Override
public List saveBatch(List entities) {
if (entities == null || entities.isEmpty()) {
return List.of();
@@ -118,10 +123,12 @@ public List saveBatch(List entities) {
.toList();
}
+ @Override
public void removeBatchByIds(List ids) {
baseDelegate.removeBatchByIds(ids);
}
+ @Override
public List listByIds(List ids) {
List poList = baseDelegate.listByIds(ids);
if (poList == null || poList.isEmpty()) {
@@ -132,10 +139,12 @@ public List listByIds(List ids) {
.toList();
}
+ @Override
public long count(T entity) {
return baseDelegate.count(toPo(entity));
}
+ @Override
public boolean exists(T entity) {
return baseDelegate.exists(toPo(entity));
}
From 01ee1c92969811c8e24a1cd2e6314f4985f5b3fc Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sun, 28 Jun 2026 16:55:56 +0800
Subject: [PATCH 03/21] =?UTF-8?q?refactor(infra):=20=E9=87=8D=E6=9E=84?=
=?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E5=B1=82=E7=9A=84=E7=BC=93?=
=?UTF-8?q?=E5=AD=98=E9=85=8D=E7=BD=AE=E5=92=8C=E6=95=B0=E6=8D=AE=E8=AE=BF?=
=?UTF-8?q?=E9=97=AE=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将缓存配置从 mybatis 模块迁移到 core 模块
- 移除 JPA 仓库实现中的 Spring Data JPA 依赖,改用原生 EntityManager 实现
- 更新测试配置类,替换 @EnableJpaRepositories 等注解为 @SpringBootApplication
- 在核心模块中添加 MongoDB 和 Elasticsearch 的可选依赖
- 注释掉 sample 模块的构建配置
- 升级 Jakarta Persistence API 版本至 3.2.0
- 完善用户实体类的多数据库注解支持
- 优化 JPA 仓库测试类,添加完整的 CRUD 操作验证
---
pom.xml | 4 +-
.../jpa/repository/JpaRepositoryDelegate.java | 66 ++--
.../structure-infra-sample-core/pom.xml | 14 +
.../sample/infra/config/CacheConfig.java | 11 +-
.../infra/sample/infra/po/UserPO.java | 3 +
.../elasticsearch/UserEsDelegate.java | 19 +-
.../UserElasticsearchRepositoryTest.java | 38 +--
.../config/ElasticsearchTestConfig.java | 20 +-
.../infra/repository/jpa/UserJpaDelegate.java | 19 +-
.../sample/jpa/UserJpaRepositoryTest.java | 312 ++++++++++++++++--
.../sample/jpa/config/JpaTestConfig.java | 21 +-
.../repository/mongodb/UserMongoDelegate.java | 19 +-
.../mongodb/UserMongoRepositoryTest.java | 38 +--
.../mongodb/config/MongoTestConfig.java | 20 +-
14 files changed, 398 insertions(+), 206 deletions(-)
rename structure-infra-sample/{structure-infra-sample-mybatis => structure-infra-sample-core}/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java (76%)
diff --git a/pom.xml b/pom.xml
index b07bc7a..cf3868a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
structure-infra-jpa-starter
structure-infra-mongodb-starter
structure-infra-elasticsearch-starter
- structure-infra-sample
+
@@ -127,7 +127,7 @@
jakarta.persistence
jakarta.persistence-api
- 3.1.0
+ 3.2.0
diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java
index 7394b0b..9f5ca1b 100644
--- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java
+++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java
@@ -5,11 +5,11 @@
import cn.structure.infra.repository.RepositoryDelegate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Sort;
-import org.springframework.data.jpa.repository.JpaRepository;
import jakarta.persistence.EntityManager;
+import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
@@ -22,7 +22,6 @@
@Slf4j
public class JpaRepositoryDelegate implements RepositoryDelegate {
- protected JpaRepository jpaRepository;
protected EntityManager entityManager;
protected Class entityClass;
@@ -32,23 +31,15 @@ public JpaRepositoryDelegate() {
public JpaRepositoryDelegate(EntityManager entityManager, Class entityClass) {
this.entityManager = entityManager;
this.entityClass = entityClass;
- this.jpaRepository = createJpaRepository(entityManager, entityClass);
log.info("JpaRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName());
}
- @SuppressWarnings("unchecked")
- private JpaRepository createJpaRepository(EntityManager em, Class domainClass) {
- org.springframework.data.jpa.repository.support.JpaRepositoryFactory factory =
- new org.springframework.data.jpa.repository.support.JpaRepositoryFactory(em);
- return (JpaRepository) factory.getRepository(domainClass);
- }
-
@Override
public T save(T entity) {
if (entity == null) {
return null;
}
- T saved = jpaRepository.save(entity);
+ T saved = entityManager.merge(entity);
log.debug("Saved entity: {}", saved);
return saved;
}
@@ -56,8 +47,11 @@ public T save(T entity) {
@Override
public void removeById(ID id) {
if (id != null) {
- jpaRepository.deleteById(id);
- log.debug("Removed entity: id={}", id);
+ T entity = findById(id);
+ if (entity != null) {
+ entityManager.remove(entity);
+ log.debug("Removed entity: id={}", id);
+ }
}
}
@@ -66,7 +60,7 @@ public T findById(ID id) {
if (id == null) {
return null;
}
- T entity = jpaRepository.findById(id).orElse(null);
+ T entity = entityManager.find(entityClass, id);
log.debug("Find by id: id={}, found={}", id, entity != null);
return entity;
}
@@ -78,7 +72,7 @@ public T queryById(ID id) {
@Override
public Optional queryByIdOptional(ID id) {
- return jpaRepository.findById(id);
+ return Optional.ofNullable(findById(id));
}
@Override
@@ -98,30 +92,41 @@ public Optional queryOneOptional(T condition) {
@Override
public List queryList(T condition) {
if (condition == null) {
- return jpaRepository.findAll();
+ return findAll();
}
return queryByCondition(condition);
}
@Override
public ResPage queryPage(ReqPage reqPage) {
- int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0; // JPA page 从 0 开始
+ int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0;
int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10;
- Page page = jpaRepository.findAll(PageRequest.of(pageNum, pageSize, Sort.unsorted()));
+ List allResults = findAll();
+ int start = pageNum * pageSize;
+ int end = Math.min(start + pageSize, allResults.size());
+
+ List pageContent = start < allResults.size() ? allResults.subList(start, end) : List.of();
ResPage resPage = new ResPage<>();
- resPage.setCurrent((long) (page.getNumber() + 1)); // 转换为从 1 开始
- resPage.setPages((long) page.getTotalPages());
- resPage.setSize((long) page.getSize());
- resPage.setTotal(page.getTotalElements());
- resPage.setRecords(page.getContent());
+ resPage.setCurrent((long) (pageNum + 1));
+ resPage.setPages((long) ((allResults.size() + pageSize - 1) / pageSize));
+ resPage.setSize((long) pageSize);
+ resPage.setTotal((long) allResults.size());
+ resPage.setRecords(pageContent);
log.debug("Query page: page={}, size={}, total={}, records={}",
- pageNum + 1, pageSize, page.getTotalElements(), page.getContent().size());
+ pageNum + 1, pageSize, allResults.size(), pageContent.size());
return resPage;
}
+ private List findAll() {
+ CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+ CriteriaQuery query = cb.createQuery(entityClass);
+ query.from(entityClass);
+ return entityManager.createQuery(query).getResultList();
+ }
+
private List queryByCondition(T condition) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(entityClass);
@@ -167,14 +172,14 @@ public List saveBatch(List entities) {
return List.of();
}
return entities.stream()
- .map(jpaRepository::save)
+ .map(entityManager::merge)
.toList();
}
@Override
public void removeBatchByIds(List ids) {
if (ids != null) {
- ids.forEach(jpaRepository::deleteById);
+ ids.forEach(this::removeById);
}
}
@@ -183,13 +188,16 @@ public List listByIds(List ids) {
if (ids == null || ids.isEmpty()) {
return List.of();
}
- return jpaRepository.findAllById(ids);
+ return ids.stream()
+ .map(this::findById)
+ .filter(java.util.Objects::nonNull)
+ .toList();
}
@Override
public long count(T condition) {
if (condition == null) {
- return jpaRepository.count();
+ return findAll().size();
}
return queryList(condition).size();
}
diff --git a/structure-infra-sample/structure-infra-sample-core/pom.xml b/structure-infra-sample/structure-infra-sample-core/pom.xml
index a7f7ba0..0ae0475 100644
--- a/structure-infra-sample/structure-infra-sample-core/pom.xml
+++ b/structure-infra-sample/structure-infra-sample-core/pom.xml
@@ -44,6 +44,20 @@
true
+
+
+ org.springframework.data
+ spring-data-mongodb
+ true
+
+
+
+
+ org.springframework.data
+ spring-data-elasticsearch
+ true
+
+
org.projectlombok
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
similarity index 76%
rename from structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
rename to structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
index e985d70..56d423a 100644
--- a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
@@ -6,15 +6,6 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-/**
- * 缓存配置
- *
- * 提供简单的内存缓存实现,用于数据权限缓存
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Configuration
@EnableCaching
public class CacheConfig {
@@ -23,4 +14,4 @@ public class CacheConfig {
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
-}
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java
index 478f949..8250b28 100644
--- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.annotation.TableName;
import jakarta.persistence.*;
import lombok.Data;
+import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
@@ -12,6 +13,8 @@
@TableName("t_user")
@Entity
@Table(name = "t_user")
+@Document(collection = "t_user")
+@org.springframework.data.elasticsearch.annotations.Document(indexName = "t_user")
public class UserPO {
@Id
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java
index 2466f67..dc35e5d 100644
--- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java
@@ -5,17 +5,9 @@
import cn.structure.infra.repository.RepositoryType;
import cn.structure.infra.sample.infra.po.UserPO;
import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.stereotype.Component;
-/**
- * 用户仓储 Elasticsearch 实现
- *
- * 使用 Spring Data Elasticsearch 实现用户数据的持久化操作
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Component
@DelegateFor(
name = "userRepository",
@@ -26,10 +18,15 @@
)
public class UserEsDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate {
+ public UserEsDelegate(ElasticsearchOperations elasticsearchOperations) {
+ super(elasticsearchOperations, UserPO.class);
+ }
+
@Override
public UserPO finByName(String name) {
- // TODO: 实现 Elasticsearch 查询逻辑
- return null;
+ UserPO condition = new UserPO();
+ condition.setUsername(name);
+ return queryOne(condition);
}
}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java
index 67eaf1e..dcbe92d 100644
--- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java
@@ -7,25 +7,14 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
-/**
- * Elasticsearch 仓储测试
- *
- * 测试 Elasticsearch 实现的用户仓储功能
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
-@SpringBootTest
+@SpringBootTest(classes = ElasticsearchTestConfig.class)
@ActiveProfiles("es-test")
-@Import(ElasticsearchTestConfig.class)
@DisplayName("Elasticsearch 仓储测试")
-public class UserElasticsearchRepositoryTest {
+class UserElasticsearchRepositoryTest {
@Autowired(required = false)
private UserRepository userRepository;
@@ -38,26 +27,9 @@ void testElasticsearchRepositoryInjection() {
}
@Test
- @DisplayName("测试 Elasticsearch 保存功能(待实现)")
- void testElasticsearchSave() {
+ @DisplayName("测试 Elasticsearch Delegate 类型")
+ void testElasticsearchDelegateType() {
assertNotNull(userRepository, "Elasticsearch 仓储应该被注入");
-
- UserEntity user = new UserEntity();
- user.setUsername("esTestUser");
- user.setPassword("password123");
- user.setEmail("es@test.com");
- user.setAge(25);
-
- // TODO: 等待 Elasticsearch 实现完成
- System.out.println("⚠ Elasticsearch 保存功能待实现,当前返回 null");
- }
-
- @Test
- @DisplayName("测试 Elasticsearch 查询功能(待实现)")
- void testElasticsearchFindByName() {
- assertNotNull(userRepository, "Elasticsearch 仓储应该被注入");
-
- // TODO: 等待 Elasticsearch 实现完成
- System.out.println("⚠ Elasticsearch 查询功能待实现");
+ System.out.println("✓ Elasticsearch 仓储实现类: " + userRepository.getClass().getName());
}
}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java
index 0786c77..b31dbc1 100644
--- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchTestConfig.java
@@ -1,20 +1,19 @@
package cn.structure.infra.sample.elasticsearch.config;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
-import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
-/**
- * Elasticsearch 测试配置
- *
- * 配置 Elasticsearch 相关的组件扫描
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Configuration
+@SpringBootApplication(excludeName = {
+ "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration",
+ "com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration",
+ "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration"
+})
@ComponentScan(basePackages = {
"cn.structure.infra.sample",
"cn.structure.infra.repository"
@@ -23,6 +22,5 @@
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.jpa.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mongodb.*")
})
-@EnableElasticsearchRepositories(basePackages = "cn.structure.infra.sample.infra.repository.elasticsearch")
public class ElasticsearchTestConfig {
}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java
index 52fbfad..de73778 100644
--- a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java
@@ -5,17 +5,9 @@
import cn.structure.infra.repository.RepositoryType;
import cn.structure.infra.sample.infra.po.UserPO;
import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Component;
-/**
- * 用户仓储 JPA 实现
- *
- * 使用 Spring Data JPA 实现用户数据的持久化操作
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Component
@DelegateFor(
name = "userRepository",
@@ -26,10 +18,15 @@
)
public class UserJpaDelegate extends JpaRepositoryDelegate implements UserRepositoryDelegate {
+ public UserJpaDelegate(EntityManager entityManager) {
+ super(entityManager, UserPO.class);
+ }
+
@Override
public UserPO finByName(String name) {
- // TODO: 实现 JPA 查询逻辑
- return null;
+ UserPO condition = new UserPO();
+ condition.setUsername(name);
+ return queryOne(condition);
}
}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
index 70dd317..ab2f708 100644
--- a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
@@ -3,61 +3,307 @@
import cn.structure.infra.sample.domain.entity.UserEntity;
import cn.structure.infra.sample.domain.repository.UserRepository;
import cn.structure.infra.sample.jpa.config.JpaTestConfig;
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
-/**
- * JPA 仓储测试
- *
- * 测试 JPA 实现的用户仓储功能
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
-@SpringBootTest
+@SpringBootTest(classes = JpaTestConfig.class)
@ActiveProfiles("jpa-test")
-@Import(JpaTestConfig.class)
@DisplayName("JPA 仓储测试")
-public class UserJpaRepositoryTest {
+class UserJpaRepositoryTest {
- @Autowired(required = false)
+ @Autowired
private UserRepository userRepository;
+ private UserEntity createUser(String username, String email, Integer age) {
+ UserEntity user = new UserEntity();
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setAge(age);
+ user.setPassword("123456");
+ user.setCreateTime(LocalDateTime.now());
+ user.setUpdateTime(LocalDateTime.now());
+ return user;
+ }
+
+ @Test
+ @DisplayName("测试保存用户")
+ void testSave() {
+ UserEntity user = createUser("zhangsan", "zhangsan@example.com", 25);
+ UserEntity saved = userRepository.save(user);
+
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals("zhangsan", saved.getUsername());
+ assertEquals("zhangsan@example.com", saved.getEmail());
+ assertEquals(25, saved.getAge());
+
+ System.out.println("保存用户成功: " + saved);
+ }
+
@Test
- @DisplayName("测试 JPA 仓储注入")
- void testJpaRepositoryInjection() {
- assertNotNull(userRepository, "JPA 仓储应该被成功注入");
- System.out.println("✓ JPA 仓储注入成功: " + userRepository.getClass().getName());
+ @DisplayName("测试根据ID查询")
+ void testFindById() {
+ UserEntity user = createUser("lisi", "lisi@example.com", 30);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals("lisi", found.getUsername());
}
@Test
- @DisplayName("测试 JPA 保存功能(待实现)")
- void testJpaSave() {
- assertNotNull(userRepository, "JPA 仓储应该被注入");
+ @DisplayName("测试 queryById")
+ void testQueryById() {
+ UserEntity user = createUser("wangwu", "wangwu@example.com", 28);
+ UserEntity saved = userRepository.save(user);
- UserEntity user = new UserEntity();
- user.setUsername("jpaTestUser");
- user.setPassword("password123");
- user.setEmail("jpa@test.com");
- user.setAge(25);
+ UserEntity found = userRepository.queryById(saved.getId());
+ assertNotNull(found);
+ assertEquals("wangwu", found.getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 存在")
+ void testQueryByIdOptional_Exists() {
+ UserEntity user = createUser("zhaoliu", "zhaoliu@example.com", 35);
+ UserEntity saved = userRepository.save(user);
+
+ Optional optional = userRepository.queryByIdOptional(saved.getId());
+ assertTrue(optional.isPresent());
+ assertEquals("zhaoliu", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 不存在")
+ void testQueryByIdOptional_NotExists() {
+ Optional optional = userRepository.queryByIdOptional(9999L);
+ assertFalse(optional.isPresent());
+ }
+
+ @Test
+ @DisplayName("测试 queryOne - 条件查询单条")
+ void testQueryOne() {
+ userRepository.save(createUser("user1", "user1@test.com", 20));
+ userRepository.save(createUser("user2", "user2@test.com", 25));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("user1");
+
+ UserEntity found = userRepository.queryOne(condition);
+ assertNotNull(found);
+ assertEquals("user1", found.getUsername());
+ assertEquals("user1@test.com", found.getEmail());
+ }
+
+ @Test
+ @DisplayName("测试 queryOneOptional")
+ void testQueryOneOptional() {
+ userRepository.save(createUser("optUser", "opt@test.com", 22));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("optUser");
+
+ Optional optional = userRepository.queryOneOptional(condition);
+ assertTrue(optional.isPresent());
+ assertEquals("optUser", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryOneOptional - 不存在")
+ void testQueryOneOptional_NotExists() {
+ UserEntity condition = new UserEntity();
+ condition.setUsername("nonexistent");
+
+ Optional optional = userRepository.queryOneOptional(condition);
+ assertFalse(optional.isPresent());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 查询全部")
+ void testQueryList_All() {
+ int beforeCount = userRepository.queryList(null).size();
+
+ userRepository.save(createUser("listUser1", "list1@test.com", 20));
+ userRepository.save(createUser("listUser2", "list2@test.com", 25));
+ userRepository.save(createUser("listUser3", "list3@test.com", 30));
+
+ List list = userRepository.queryList(null);
+ assertEquals(beforeCount + 3, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 条件查询")
+ void testQueryList_ByCondition() {
+ userRepository.save(createUser("ageUser1", "age1@test.com", 18));
+ userRepository.save(createUser("ageUser2", "age2@test.com", 25));
+ userRepository.save(createUser("ageUser3", "age3@test.com", 18));
+
+ UserEntity condition = new UserEntity();
+ condition.setAge(18);
+
+ List list = userRepository.queryList(condition);
+ assertTrue(list.size() >= 2);
+ assertTrue(list.stream().allMatch(u -> u.getAge() == 18));
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 列表返回空集合")
+ void testQueryList_Empty() {
+ UserEntity condition = new UserEntity();
+ condition.setAge(999);
+
+ List list = userRepository.queryList(condition);
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ @DisplayName("测试 queryPage - 分页查询")
+ void testQueryPage() {
+ for (int i = 1; i <= 15; i++) {
+ userRepository.save(createUser("pageUser" + i, "page" + i + "@test.com", 20 + i));
+ }
+
+ ReqPage reqPage = new ReqPage();
+ reqPage.setPage(2);
+ reqPage.setSize(5);
+
+ ResPage page = userRepository.queryPage(reqPage);
+
+ assertNotNull(page);
+ assertEquals(2, page.getCurrent());
+ assertEquals(5, page.getSize());
+ assertTrue(page.getTotal() >= 15);
+ assertEquals(5, page.getRecords().size());
+
+ System.out.println("分页查询结果: 当前页=" + page.getCurrent()
+ + ", 总页数=" + page.getPages()
+ + ", 每页=" + page.getSize()
+ + ", 总数=" + page.getTotal()
+ + ", 记录数=" + page.getRecords().size());
+ }
+
+ @Test
+ @DisplayName("测试删除用户")
+ void testRemoveById() {
+ UserEntity user = createUser("delUser", "del@test.com", 20);
+ UserEntity saved = userRepository.save(user);
+ assertNotNull(userRepository.findById(saved.getId()));
+
+ userRepository.removeById(saved.getId());
+ assertNull(userRepository.findById(saved.getId()));
+ }
+
+ @Test
+ @DisplayName("测试 Entity <-> PO 转换")
+ void testEntityPoConversion() {
+ UserEntity user = createUser("convertUser", "convert@test.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals(saved.getUsername(), found.getUsername());
+ assertEquals(saved.getEmail(), found.getEmail());
+ assertEquals(saved.getAge(), found.getAge());
+ }
+
+ @Test
+ @DisplayName("测试批量保存")
+ void testSaveBatch() {
+ UserEntity user1 = createUser("batch1", "batch1@test.com", 20);
+ UserEntity user2 = createUser("batch2", "batch2@test.com", 25);
+ UserEntity user3 = createUser("batch3", "batch3@test.com", 30);
+
+ List savedList = userRepository.saveBatch(List.of(user1, user2, user3));
+ assertNotNull(savedList);
+ assertEquals(3, savedList.size());
+ savedList.forEach(u -> assertNotNull(u.getId()));
+ }
+
+ @Test
+ @DisplayName("测试批量删除")
+ void testRemoveBatchByIds() {
+ UserEntity user1 = userRepository.save(createUser("batchDel1", "bd1@test.com", 20));
+ UserEntity user2 = userRepository.save(createUser("batchDel2", "bd2@test.com", 25));
+
+ assertNotNull(userRepository.findById(user1.getId()));
+ assertNotNull(userRepository.findById(user2.getId()));
+
+ userRepository.removeBatchByIds(List.of(user1.getId(), user2.getId()));
+
+ assertNull(userRepository.findById(user1.getId()));
+ assertNull(userRepository.findById(user2.getId()));
+ }
+
+ @Test
+ @DisplayName("测试根据ID列表查询")
+ void testListByIds() {
+ UserEntity user1 = userRepository.save(createUser("listId1", "lid1@test.com", 20));
+ UserEntity user2 = userRepository.save(createUser("listId2", "lid2@test.com", 25));
+ UserEntity user3 = userRepository.save(createUser("listId3", "lid3@test.com", 30));
+
+ List list = userRepository.listByIds(List.of(user1.getId(), user3.getId()));
+ assertNotNull(list);
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 count - 全部数量")
+ void testCount_All() {
+ long beforeCount = userRepository.count(null);
+
+ userRepository.save(createUser("count1", "count1@test.com", 20));
+ userRepository.save(createUser("count2", "count2@test.com", 25));
+
+ long afterCount = userRepository.count(null);
+ assertEquals(beforeCount + 2, afterCount);
+ }
+
+ @Test
+ @DisplayName("测试 count - 条件数量")
+ void testCount_ByCondition() {
+ userRepository.save(createUser("countAge1", "ca1@test.com", 22));
+ userRepository.save(createUser("countAge2", "ca2@test.com", 22));
+ userRepository.save(createUser("countAge3", "ca3@test.com", 33));
+
+ UserEntity condition = new UserEntity();
+ condition.setAge(22);
+
+ long count = userRepository.count(condition);
+ assertTrue(count >= 2);
+ }
+
+ @Test
+ @DisplayName("测试 exists - 存在")
+ void testExists_True() {
+ UserEntity user = userRepository.save(createUser("existsUser", "exists@test.com", 20));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("existsUser");
- // TODO: 等待 JPA 实现完成
- System.out.println("⚠ JPA 保存功能待实现,当前返回 null");
+ assertTrue(userRepository.exists(condition));
}
@Test
- @DisplayName("测试 JPA 查询功能(待实现)")
- void testJpaFindByName() {
- assertNotNull(userRepository, "JPA 仓储应该被注入");
+ @DisplayName("测试 exists - 不存在")
+ void testExists_False() {
+ UserEntity condition = new UserEntity();
+ condition.setUsername("nonexistent_user");
- // TODO: 等待 JPA 实现完成
- System.out.println("⚠ JPA 查询功能待实现");
+ assertFalse(userRepository.exists(condition));
}
}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
index f29bfac..8cc975c 100644
--- a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
@@ -1,20 +1,20 @@
package cn.structure.infra.sample.jpa.config;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-/**
- * JPA 测试配置
- *
- * 配置 JPA 相关的组件扫描
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Configuration
+@SpringBootApplication(excludeName = {
+ "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration",
+ "com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration"
+})
@ComponentScan(basePackages = {
"cn.structure.infra.sample",
"cn.structure.infra.repository"
@@ -23,6 +23,5 @@
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mongodb.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.elasticsearch.*")
})
-@EnableJpaRepositories(basePackages = "cn.structure.infra.sample.infra.repository.jpa")
public class JpaTestConfig {
}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java
index eac0eb5..1f29d97 100644
--- a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java
@@ -5,17 +5,9 @@
import cn.structure.infra.repository.RepositoryType;
import cn.structure.infra.sample.infra.po.UserPO;
import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate;
+import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
-/**
- * 用户仓储 MongoDB 实现
- *
- * 使用 Spring Data MongoDB 实现用户数据的持久化操作
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Component
@DelegateFor(
name = "userRepository",
@@ -26,9 +18,14 @@
)
public class UserMongoDelegate extends MongoRepositoryDelegate implements UserRepositoryDelegate {
+ public UserMongoDelegate(MongoTemplate mongoTemplate) {
+ super(mongoTemplate, UserPO.class);
+ }
+
@Override
public UserPO finByName(String name) {
- // TODO: 实现 MongoDB 查询逻辑
- return null;
+ UserPO condition = new UserPO();
+ condition.setUsername(name);
+ return queryOne(condition);
}
}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
index 778bca9..4a38526 100644
--- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
@@ -7,25 +7,14 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
-/**
- * MongoDB 仓储测试
- *
- * 测试 MongoDB 实现的用户仓储功能
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
-@SpringBootTest
+@SpringBootTest(classes = MongoTestConfig.class)
@ActiveProfiles("mongo-test")
-@Import(MongoTestConfig.class)
@DisplayName("MongoDB 仓储测试")
-public class UserMongoRepositoryTest {
+class UserMongoRepositoryTest {
@Autowired(required = false)
private UserRepository userRepository;
@@ -38,26 +27,9 @@ void testMongoRepositoryInjection() {
}
@Test
- @DisplayName("测试 MongoDB 保存功能(待实现)")
- void testMongoSave() {
+ @DisplayName("测试 MongoDB Delegate 类型")
+ void testMongoDelegateType() {
assertNotNull(userRepository, "MongoDB 仓储应该被注入");
-
- UserEntity user = new UserEntity();
- user.setUsername("mongoTestUser");
- user.setPassword("password123");
- user.setEmail("mongo@test.com");
- user.setAge(25);
-
- // TODO: 等待 MongoDB 实现完成
- System.out.println("⚠ MongoDB 保存功能待实现,当前返回 null");
- }
-
- @Test
- @DisplayName("测试 MongoDB 查询功能(待实现)")
- void testMongoFindByName() {
- assertNotNull(userRepository, "MongoDB 仓储应该被注入");
-
- // TODO: 等待 MongoDB 实现完成
- System.out.println("⚠ MongoDB 查询功能待实现");
+ System.out.println("✓ MongoDB 仓储实现类: " + userRepository.getClass().getName());
}
}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java
index 7ef51fe..310887f 100644
--- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MongoTestConfig.java
@@ -1,20 +1,19 @@
package cn.structure.infra.sample.mongodb.config;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
-import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
-/**
- * MongoDB 测试配置
- *
- * 配置 MongoDB 相关的组件扫描
- *
- * @author chuck
- * @version 1.0.1
- * @since 2026/6/28
- */
@Configuration
+@SpringBootApplication(excludeName = {
+ "org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration",
+ "com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration",
+ "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration"
+})
@ComponentScan(basePackages = {
"cn.structure.infra.sample",
"cn.structure.infra.repository"
@@ -23,6 +22,5 @@
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.jpa.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.elasticsearch.*")
})
-@EnableMongoRepositories(basePackages = "cn.structure.infra.sample.infra.repository.mongodb")
public class MongoTestConfig {
}
From 0314a0f25c3e9513e4f6b1ee2836216b438a7806 Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sun, 28 Jun 2026 17:41:56 +0800
Subject: [PATCH 04/21] =?UTF-8?q?test(infra):=20=E6=B7=BB=E5=8A=A0JPA?=
=?UTF-8?q?=E5=92=8CMongoDB/Elasticsearch=E6=B5=8B=E8=AF=95=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E5=8F=8A=E5=AE=8C=E6=95=B4=E6=B5=8B=E8=AF=95=E7=94=A8?=
=?UTF-8?q?=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为JPA测试配置添加数据源、实体管理器和事务管理器
- 新增MockElasticsearchConfiguration提供Elasticsearch模板模拟实现
- 新增MockMongoConfiguration提供MongoDB模板模拟实现并支持基本查询操作
- 为JPA测试类添加事务注解确保测试数据隔离
- 补充MongoDB仓储的完整CRUD操作测试用例
- 添加Elasticsearch仓储详细功能测试覆盖各种查询场景
- 扩展MongoDB测试用例验证分页、条件查询和批量操作功能
---
...UserElasticsearchDetailRepositoryTest.java | 205 ++++++++++++
.../MockElasticsearchConfiguration.java | 103 ++++++
.../sample/jpa/UserJpaRepositoryTest.java | 1 +
.../sample/jpa/config/JpaTestConfig.java | 50 ++-
.../UserMongoRepositoryInjectionTest.java | 34 ++
.../mongodb/UserMongoRepositoryTest.java | 294 +++++++++++++++++-
.../config/MockMongoConfiguration.java | 98 ++++++
.../mongodb/config/MongoTestConfig.java | 3 +-
8 files changed, 777 insertions(+), 11 deletions(-)
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java
new file mode 100644
index 0000000..861a4e1
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java
@@ -0,0 +1,205 @@
+package cn.structure.infra.sample.elasticsearch;
+
+import cn.structure.infra.sample.domain.entity.UserEntity;
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import cn.structure.infra.sample.elasticsearch.config.ElasticsearchTestConfig;
+import cn.structure.infra.sample.elasticsearch.config.MockElasticsearchConfiguration;
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest(classes = ElasticsearchTestConfig.class)
+@ActiveProfiles("es-test")
+@Import(MockElasticsearchConfiguration.class)
+@DisplayName("Elasticsearch 仓储详细测试")
+class UserElasticsearchDetailRepositoryTest {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ private UserEntity createUser(String username, String email, Integer age) {
+ UserEntity user = new UserEntity();
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setAge(age);
+ user.setPassword("123456");
+ user.setCreateTime(LocalDateTime.now());
+ user.setUpdateTime(LocalDateTime.now());
+ return user;
+ }
+
+ @Test
+ @DisplayName("测试保存用户")
+ void testSave() {
+ UserEntity user = createUser("zhangsan", "zhangsan@example.com", 25);
+ UserEntity saved = userRepository.save(user);
+
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals("zhangsan", saved.getUsername());
+ assertEquals("zhangsan@example.com", saved.getEmail());
+ assertEquals(25, saved.getAge());
+ }
+
+ @Test
+ @DisplayName("测试根据ID查询")
+ void testFindById() {
+ UserEntity user = createUser("lisi", "lisi@example.com", 30);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals("lisi", found.getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryById")
+ void testQueryById() {
+ UserEntity user = createUser("wangwu", "wangwu@example.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.queryById(saved.getId());
+ assertNotNull(found);
+ assertEquals("wangwu", found.getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 存在")
+ void testQueryByIdOptional_Exists() {
+ UserEntity user = createUser("zhaoliu", "zhaoliu@example.com", 35);
+ UserEntity saved = userRepository.save(user);
+
+ Optional optional = userRepository.queryByIdOptional(saved.getId());
+ assertTrue(optional.isPresent());
+ assertEquals("zhaoliu", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 不存在")
+ void testQueryByIdOptional_NotExists() {
+ Optional optional = userRepository.queryByIdOptional(9999L);
+ assertFalse(optional.isPresent());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 查询全部")
+ void testQueryList_All() {
+ int beforeCount = userRepository.queryList(null).size();
+
+ userRepository.save(createUser("listUser1", "list1@test.com", 20));
+ userRepository.save(createUser("listUser2", "list2@test.com", 25));
+ userRepository.save(createUser("listUser3", "list3@test.com", 30));
+
+ List list = userRepository.queryList(null);
+ assertEquals(beforeCount + 3, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 queryPage - 分页查询")
+ void testQueryPage() {
+ for (int i = 1; i <= 15; i++) {
+ userRepository.save(createUser("pageUser" + i, "page" + i + "@test.com", 20 + i));
+ }
+
+ ReqPage reqPage = new ReqPage();
+ reqPage.setPage(2);
+ reqPage.setSize(5);
+
+ ResPage page = userRepository.queryPage(reqPage);
+
+ assertNotNull(page);
+ assertEquals(2, page.getCurrent());
+ assertEquals(5, page.getSize());
+ assertTrue(page.getTotal() >= 15);
+ assertEquals(5, page.getRecords().size());
+ }
+
+ @Test
+ @DisplayName("测试删除用户")
+ void testRemoveById() {
+ UserEntity user = createUser("delUser", "del@test.com", 20);
+ UserEntity saved = userRepository.save(user);
+ assertNotNull(userRepository.findById(saved.getId()));
+
+ userRepository.removeById(saved.getId());
+ assertNull(userRepository.findById(saved.getId()));
+ }
+
+ @Test
+ @DisplayName("测试 Entity <-> PO 转换")
+ void testEntityPoConversion() {
+ UserEntity user = createUser("convertUser", "convert@test.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals(saved.getUsername(), found.getUsername());
+ assertEquals(saved.getEmail(), found.getEmail());
+ assertEquals(saved.getAge(), found.getAge());
+ }
+
+ @Test
+ @DisplayName("测试批量保存")
+ void testSaveBatch() {
+ UserEntity user1 = createUser("batch1", "batch1@test.com", 20);
+ UserEntity user2 = createUser("batch2", "batch2@test.com", 25);
+ UserEntity user3 = createUser("batch3", "batch3@test.com", 30);
+
+ List savedList = userRepository.saveBatch(List.of(user1, user2, user3));
+ assertNotNull(savedList);
+ assertEquals(3, savedList.size());
+ savedList.forEach(u -> assertNotNull(u.getId()));
+ }
+
+ @Test
+ @DisplayName("测试批量删除")
+ void testRemoveBatchByIds() {
+ UserEntity user1 = userRepository.save(createUser("batchDel1", "bd1@test.com", 20));
+ UserEntity user2 = userRepository.save(createUser("batchDel2", "bd2@test.com", 25));
+
+ assertNotNull(userRepository.findById(user1.getId()));
+ assertNotNull(userRepository.findById(user2.getId()));
+
+ userRepository.removeBatchByIds(List.of(user1.getId(), user2.getId()));
+
+ assertNull(userRepository.findById(user1.getId()));
+ assertNull(userRepository.findById(user2.getId()));
+ }
+
+ @Test
+ @DisplayName("测试根据ID列表查询")
+ void testListByIds() {
+ UserEntity user1 = userRepository.save(createUser("listId1", "lid1@test.com", 20));
+ UserEntity user2 = userRepository.save(createUser("listId2", "lid2@test.com", 25));
+ UserEntity user3 = userRepository.save(createUser("listId3", "lid3@test.com", 30));
+
+ List list = userRepository.listByIds(List.of(user1.getId(), user3.getId()));
+ assertNotNull(list);
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 count - 全部数量")
+ void testCount_All() {
+ long beforeCount = userRepository.count(null);
+
+ userRepository.save(createUser("count1", "count1@test.com", 20));
+ userRepository.save(createUser("count2", "count2@test.com", 25));
+
+ long afterCount = userRepository.count(null);
+ assertEquals(beforeCount + 2, afterCount);
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java
new file mode 100644
index 0000000..c46add9
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java
@@ -0,0 +1,103 @@
+package cn.structure.infra.sample.elasticsearch.config;
+
+import cn.structure.infra.sample.infra.po.UserPO;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
+import org.springframework.data.elasticsearch.core.SearchHit;
+import org.springframework.data.elasticsearch.core.SearchHits;
+import org.springframework.data.elasticsearch.core.SearchHitsImpl;
+import org.springframework.data.elasticsearch.core.TotalHitsRelation;
+import org.springframework.data.elasticsearch.core.query.Query;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@TestConfiguration
+public class MockElasticsearchConfiguration {
+
+ private final Map dataStore = new HashMap<>();
+ private long idGenerator = 1;
+
+ @Bean
+ @Primary
+ public ElasticsearchTemplate elasticsearchTemplate() {
+ ElasticsearchTemplate template = mock(ElasticsearchTemplate.class);
+
+ when(template.save(any(UserPO.class))).thenAnswer(invocation -> {
+ UserPO po = invocation.getArgument(0);
+ if (po.getId() == null || po.getId() == 0) {
+ po.setId(idGenerator++);
+ }
+ dataStore.put(po.getId(), po);
+ return po;
+ });
+
+ when(template.get(anyString(), any(Class.class))).thenAnswer(invocation -> {
+ String id = invocation.getArgument(0);
+ return dataStore.get(Long.valueOf(id));
+ });
+
+ when(template.delete(anyString(), any(Class.class))).thenAnswer(invocation -> {
+ String id = invocation.getArgument(0);
+ dataStore.remove(Long.valueOf(id));
+ return id;
+ });
+
+ when(template.search(any(Query.class), any(Class.class))).thenAnswer(invocation -> {
+ Query query = invocation.getArgument(0);
+ List allData = new ArrayList<>(dataStore.values());
+
+ Pageable pageable = query.getPageable();
+ List pageData = new ArrayList<>(allData);
+ if (pageable != null && pageable.isPaged()) {
+ int pageNum = pageable.getPageNumber();
+ int pageSize = pageable.getPageSize();
+ int fromIndex = pageNum * pageSize;
+ int toIndex = Math.min(fromIndex + pageSize, allData.size());
+ if (fromIndex >= allData.size()) {
+ pageData = new ArrayList<>();
+ } else {
+ pageData = new ArrayList<>(allData.subList(fromIndex, toIndex));
+ }
+ }
+
+ List> searchHits = new ArrayList<>();
+ for (UserPO po : pageData) {
+ SearchHit hit = mock(SearchHit.class);
+ when(hit.getId()).thenReturn(String.valueOf(po.getId()));
+ when(hit.getContent()).thenReturn(po);
+ searchHits.add(hit);
+ }
+
+ return new SearchHitsImpl<>(
+ allData.size(),
+ TotalHitsRelation.EQUAL_TO,
+ 0.0f,
+ Duration.ZERO,
+ null,
+ null,
+ searchHits,
+ null,
+ null,
+ null
+ );
+ });
+
+ when(template.count(any(Query.class), any(Class.class))).thenAnswer(invocation -> {
+ return (long) dataStore.size();
+ });
+
+ return template;
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
index ab2f708..67fa66c 100644
--- a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java
@@ -20,6 +20,7 @@
@SpringBootTest(classes = JpaTestConfig.class)
@ActiveProfiles("jpa-test")
+@Transactional
@DisplayName("JPA 仓储测试")
class UserJpaRepositoryTest {
diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
index 8cc975c..8309a98 100644
--- a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
+++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java
@@ -1,9 +1,20 @@
package cn.structure.infra.sample.jpa.config;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+import java.util.Properties;
@Configuration
@SpringBootApplication(excludeName = {
@@ -13,7 +24,9 @@
"org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration",
"org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration",
"org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration",
- "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration"
+ "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration",
+ "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration",
+ "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"
})
@ComponentScan(basePackages = {
"cn.structure.infra.sample",
@@ -23,5 +36,40 @@
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mongodb.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.elasticsearch.*")
})
+@EnableTransactionManagement
public class JpaTestConfig {
+
+ @Bean
+ public DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setType(EmbeddedDatabaseType.H2)
+ .setName("jpa_testdb")
+ .build();
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
+ LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
+ em.setDataSource(dataSource);
+ em.setPackagesToScan("cn.structure.infra.sample.infra.po");
+
+ HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
+ em.setJpaVendorAdapter(vendorAdapter);
+
+ Properties properties = new Properties();
+ properties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
+ properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
+ properties.setProperty("hibernate.show_sql", "true");
+ properties.setProperty("hibernate.format_sql", "true");
+ em.setJpaProperties(properties);
+
+ return em;
+ }
+
+ @Bean
+ public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
+ JpaTransactionManager transactionManager = new JpaTransactionManager();
+ transactionManager.setEntityManagerFactory(entityManagerFactory.getObject());
+ return transactionManager;
+ }
}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java
new file mode 100644
index 0000000..9b1ea04
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java
@@ -0,0 +1,34 @@
+package cn.structure.infra.sample.mongodb;
+
+import cn.structure.infra.sample.domain.repository.UserRepository;
+import cn.structure.infra.sample.mongodb.config.MongoTestConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@SpringBootTest(classes = MongoTestConfig.class)
+@ActiveProfiles("mongo-test")
+@DisplayName("MongoDB 仓储基础测试 - 验证 Bean 注入")
+class UserMongoRepositoryInjectionTest {
+
+ @Autowired(required = false)
+ private UserRepository userRepository;
+
+ @Test
+ @DisplayName("测试 MongoDB 仓储注入")
+ void testMongoRepositoryInjection() {
+ assertNotNull(userRepository, "MongoDB 仓储应该被成功注入");
+ System.out.println("✓ MongoDB 仓储注入成功: " + userRepository.getClass().getName());
+ }
+
+ @Test
+ @DisplayName("测试 MongoDB Delegate 类型")
+ void testMongoDelegateType() {
+ assertNotNull(userRepository, "MongoDB 仓储应该被注入");
+ System.out.println("✓ MongoDB 仓储实现类: " + userRepository.getClass().getName());
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
index 4a38526..743a8e0 100644
--- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java
@@ -3,33 +3,309 @@
import cn.structure.infra.sample.domain.entity.UserEntity;
import cn.structure.infra.sample.domain.repository.UserRepository;
import cn.structure.infra.sample.mongodb.config.MongoTestConfig;
+import cn.structure.infra.sample.mongodb.config.MockMongoConfiguration;
+import cn.structure.common.vo.ReqPage;
+import cn.structure.common.vo.ResPage;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(classes = MongoTestConfig.class)
@ActiveProfiles("mongo-test")
+@Import(MockMongoConfiguration.class)
@DisplayName("MongoDB 仓储测试")
class UserMongoRepositoryTest {
- @Autowired(required = false)
+ @Autowired
private UserRepository userRepository;
+ private UserEntity createUser(String username, String email, Integer age) {
+ UserEntity user = new UserEntity();
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setAge(age);
+ user.setPassword("123456");
+ user.setCreateTime(LocalDateTime.now());
+ user.setUpdateTime(LocalDateTime.now());
+ return user;
+ }
+
+ @Test
+ @DisplayName("测试保存用户")
+ void testSave() {
+ UserEntity user = createUser("zhangsan", "zhangsan@example.com", 25);
+ UserEntity saved = userRepository.save(user);
+
+ assertNotNull(saved);
+ assertNotNull(saved.getId());
+ assertEquals("zhangsan", saved.getUsername());
+ assertEquals("zhangsan@example.com", saved.getEmail());
+ assertEquals(25, saved.getAge());
+
+ System.out.println("保存用户成功: " + saved);
+ }
+
+ @Test
+ @DisplayName("测试根据ID查询")
+ void testFindById() {
+ UserEntity user = createUser("lisi", "lisi@example.com", 30);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals("lisi", found.getUsername());
+ }
+
@Test
- @DisplayName("测试 MongoDB 仓储注入")
- void testMongoRepositoryInjection() {
- assertNotNull(userRepository, "MongoDB 仓储应该被成功注入");
- System.out.println("✓ MongoDB 仓储注入成功: " + userRepository.getClass().getName());
+ @DisplayName("测试 queryById")
+ void testQueryById() {
+ UserEntity user = createUser("wangwu", "wangwu@example.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.queryById(saved.getId());
+ assertNotNull(found);
+ assertEquals("wangwu", found.getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 存在")
+ void testQueryByIdOptional_Exists() {
+ UserEntity user = createUser("zhaoliu", "zhaoliu@example.com", 35);
+ UserEntity saved = userRepository.save(user);
+
+ Optional optional = userRepository.queryByIdOptional(saved.getId());
+ assertTrue(optional.isPresent());
+ assertEquals("zhaoliu", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryByIdOptional - 不存在")
+ void testQueryByIdOptional_NotExists() {
+ Optional optional = userRepository.queryByIdOptional(9999L);
+ assertFalse(optional.isPresent());
}
@Test
- @DisplayName("测试 MongoDB Delegate 类型")
- void testMongoDelegateType() {
- assertNotNull(userRepository, "MongoDB 仓储应该被注入");
- System.out.println("✓ MongoDB 仓储实现类: " + userRepository.getClass().getName());
+ @DisplayName("测试 queryOne - 条件查询单条")
+ void testQueryOne() {
+ userRepository.save(createUser("user1", "user1@test.com", 20));
+ userRepository.save(createUser("user2", "user2@test.com", 25));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("user1");
+
+ UserEntity found = userRepository.queryOne(condition);
+ assertNotNull(found);
+ assertEquals("user1", found.getUsername());
+ assertEquals("user1@test.com", found.getEmail());
+ }
+
+ @Test
+ @DisplayName("测试 queryOneOptional")
+ void testQueryOneOptional() {
+ userRepository.save(createUser("optUser", "opt@test.com", 22));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("optUser");
+
+ Optional optional = userRepository.queryOneOptional(condition);
+ assertTrue(optional.isPresent());
+ assertEquals("optUser", optional.get().getUsername());
+ }
+
+ @Test
+ @DisplayName("测试 queryOneOptional - 不存在")
+ void testQueryOneOptional_NotExists() {
+ UserEntity condition = new UserEntity();
+ condition.setUsername("nonexistent");
+
+ Optional optional = userRepository.queryOneOptional(condition);
+ assertFalse(optional.isPresent());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 查询全部")
+ void testQueryList_All() {
+ int beforeCount = userRepository.queryList(null).size();
+
+ userRepository.save(createUser("listUser1", "list1@test.com", 20));
+ userRepository.save(createUser("listUser2", "list2@test.com", 25));
+ userRepository.save(createUser("listUser3", "list3@test.com", 30));
+
+ List list = userRepository.queryList(null);
+ assertEquals(beforeCount + 3, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 条件查询")
+ void testQueryList_ByCondition() {
+ userRepository.save(createUser("ageUser1", "age1@test.com", 18));
+ userRepository.save(createUser("ageUser2", "age2@test.com", 25));
+ userRepository.save(createUser("ageUser3", "age3@test.com", 18));
+
+ UserEntity condition = new UserEntity();
+ condition.setAge(18);
+
+ List list = userRepository.queryList(condition);
+ assertTrue(list.size() >= 2);
+ assertTrue(list.stream().allMatch(u -> u.getAge() == 18));
+ }
+
+ @Test
+ @DisplayName("测试 queryList - 列表返回空集合")
+ void testQueryList_Empty() {
+ UserEntity condition = new UserEntity();
+ condition.setAge(999);
+
+ List list = userRepository.queryList(condition);
+ assertNotNull(list);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ @DisplayName("测试 queryPage - 分页查询")
+ void testQueryPage() {
+ for (int i = 1; i <= 15; i++) {
+ userRepository.save(createUser("pageUser" + i, "page" + i + "@test.com", 20 + i));
+ }
+
+ ReqPage reqPage = new ReqPage();
+ reqPage.setPage(2);
+ reqPage.setSize(5);
+
+ ResPage page = userRepository.queryPage(reqPage);
+
+ assertNotNull(page);
+ assertEquals(2, page.getCurrent());
+ assertEquals(5, page.getSize());
+ assertTrue(page.getTotal() >= 15);
+ assertEquals(5, page.getRecords().size());
+
+ System.out.println("分页查询结果: 当前页=" + page.getCurrent()
+ + ", 总页数=" + page.getPages()
+ + ", 每页=" + page.getSize()
+ + ", 总数=" + page.getTotal()
+ + ", 记录数=" + page.getRecords().size());
+ }
+
+ @Test
+ @DisplayName("测试删除用户")
+ void testRemoveById() {
+ UserEntity user = createUser("delUser", "del@test.com", 20);
+ UserEntity saved = userRepository.save(user);
+ assertNotNull(userRepository.findById(saved.getId()));
+
+ userRepository.removeById(saved.getId());
+ assertNull(userRepository.findById(saved.getId()));
+ }
+
+ @Test
+ @DisplayName("测试 Entity <-> PO 转换")
+ void testEntityPoConversion() {
+ UserEntity user = createUser("convertUser", "convert@test.com", 28);
+ UserEntity saved = userRepository.save(user);
+
+ UserEntity found = userRepository.findById(saved.getId());
+ assertNotNull(found);
+ assertEquals(saved.getId(), found.getId());
+ assertEquals(saved.getUsername(), found.getUsername());
+ assertEquals(saved.getEmail(), found.getEmail());
+ assertEquals(saved.getAge(), found.getAge());
+ }
+
+ @Test
+ @DisplayName("测试批量保存")
+ void testSaveBatch() {
+ UserEntity user1 = createUser("batch1", "batch1@test.com", 20);
+ UserEntity user2 = createUser("batch2", "batch2@test.com", 25);
+ UserEntity user3 = createUser("batch3", "batch3@test.com", 30);
+
+ List savedList = userRepository.saveBatch(List.of(user1, user2, user3));
+ assertNotNull(savedList);
+ assertEquals(3, savedList.size());
+ savedList.forEach(u -> assertNotNull(u.getId()));
+ }
+
+ @Test
+ @DisplayName("测试批量删除")
+ void testRemoveBatchByIds() {
+ UserEntity user1 = userRepository.save(createUser("batchDel1", "bd1@test.com", 20));
+ UserEntity user2 = userRepository.save(createUser("batchDel2", "bd2@test.com", 25));
+
+ assertNotNull(userRepository.findById(user1.getId()));
+ assertNotNull(userRepository.findById(user2.getId()));
+
+ userRepository.removeBatchByIds(List.of(user1.getId(), user2.getId()));
+
+ assertNull(userRepository.findById(user1.getId()));
+ assertNull(userRepository.findById(user2.getId()));
+ }
+
+ @Test
+ @DisplayName("测试根据ID列表查询")
+ void testListByIds() {
+ UserEntity user1 = userRepository.save(createUser("listId1", "lid1@test.com", 20));
+ UserEntity user2 = userRepository.save(createUser("listId2", "lid2@test.com", 25));
+ UserEntity user3 = userRepository.save(createUser("listId3", "lid3@test.com", 30));
+
+ List list = userRepository.listByIds(List.of(user1.getId(), user3.getId()));
+ assertNotNull(list);
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ @DisplayName("测试 count - 全部数量")
+ void testCount_All() {
+ long beforeCount = userRepository.count(null);
+
+ userRepository.save(createUser("count1", "count1@test.com", 20));
+ userRepository.save(createUser("count2", "count2@test.com", 25));
+
+ long afterCount = userRepository.count(null);
+ assertEquals(beforeCount + 2, afterCount);
+ }
+
+ @Test
+ @DisplayName("测试 count - 条件数量")
+ void testCount_ByCondition() {
+ userRepository.save(createUser("countAge1", "ca1@test.com", 22));
+ userRepository.save(createUser("countAge2", "ca2@test.com", 22));
+ userRepository.save(createUser("countAge3", "ca3@test.com", 33));
+
+ UserEntity condition = new UserEntity();
+ condition.setAge(22);
+
+ long count = userRepository.count(condition);
+ assertTrue(count >= 2);
+ }
+
+ @Test
+ @DisplayName("测试 exists - 存在")
+ void testExists_True() {
+ UserEntity user = userRepository.save(createUser("existsUser", "exists@test.com", 20));
+
+ UserEntity condition = new UserEntity();
+ condition.setUsername("existsUser");
+
+ assertTrue(userRepository.exists(condition));
+ }
+
+ @Test
+ @DisplayName("测试 exists - 不存在")
+ void testExists_False() {
+ UserEntity condition = new UserEntity();
+ condition.setUsername("nonexistent_user");
+
+ assertFalse(userRepository.exists(condition));
}
}
diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java
new file mode 100644
index 0000000..0ddf975
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java
@@ -0,0 +1,98 @@
+package cn.structure.infra.sample.mongodb.config;
+
+import cn.structure.infra.sample.infra.po.UserPO;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Query;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@TestConfiguration
+public class MockMongoConfiguration {
+
+ private final Map dataStore = new HashMap<>();
+ private long idGenerator = 1;
+
+ @Bean
+ @Primary
+ public MongoTemplate mongoTemplate() {
+ MongoTemplate template = mock(MongoTemplate.class);
+
+ when(template.save(any(UserPO.class))).thenAnswer(invocation -> {
+ UserPO po = invocation.getArgument(0);
+ if (po.getId() == null || po.getId() == 0) {
+ po.setId(idGenerator++);
+ }
+ dataStore.put(po.getId(), po);
+ return po;
+ });
+
+ when(template.findById(any(Long.class), any(Class.class))).thenAnswer(invocation -> {
+ Long id = invocation.getArgument(0);
+ return dataStore.get(id);
+ });
+
+ when(template.findById(anyString(), any(Class.class))).thenAnswer(invocation -> {
+ String id = invocation.getArgument(0);
+ return dataStore.get(Long.valueOf(id));
+ });
+
+ when(template.find(any(Query.class), any(Class.class))).thenAnswer(invocation -> {
+ Query query = invocation.getArgument(0);
+ return filterByQuery(query);
+ });
+
+ when(template.remove(any(Query.class), any(Class.class))).thenAnswer(invocation -> {
+ Query query = invocation.getArgument(0);
+ List toRemove = filterByQuery(query);
+ toRemove.forEach(po -> dataStore.remove(po.getId()));
+ return null;
+ });
+
+ when(template.count(any(Query.class), any(Class.class))).thenAnswer(invocation -> {
+ Query query = invocation.getArgument(0);
+ return (long) filterByQuery(query).size();
+ });
+
+ return template;
+ }
+
+ private List