diff --git a/openspec/config.yaml b/openspec/config.yaml
index 3e4642c..af3ede9 100644
--- a/openspec/config.yaml
+++ b/openspec/config.yaml
@@ -6,6 +6,8 @@ context: |
- 涉及模块结构、API、实体等变更时同步更新README.md
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
- 禁止创建git操作task
+ - 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行
+ - 优先使用提问工具对用户进行提问
rules:
proposal:
diff --git a/spring-boot-service-template-common/pom.xml b/spring-boot-service-template-common/pom.xml
index a7ea47a..8d734e6 100644
--- a/spring-boot-service-template-common/pom.xml
+++ b/spring-boot-service-template-common/pom.xml
@@ -11,6 +11,14 @@
spring-boot-service-template-common
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
diff --git a/spring-boot-service-template-common/src/test/java/com/lanyuanxiaoyao/service/template/common/helper/ObjectHelperTest.java b/spring-boot-service-template-common/src/test/java/com/lanyuanxiaoyao/service/template/common/helper/ObjectHelperTest.java
new file mode 100644
index 0000000..2649a3f
--- /dev/null
+++ b/spring-boot-service-template-common/src/test/java/com/lanyuanxiaoyao/service/template/common/helper/ObjectHelperTest.java
@@ -0,0 +1,263 @@
+package com.lanyuanxiaoyao.service.template.common.helper;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.Arguments;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("ObjectHelper Tests")
+class ObjectHelperTest {
+
+ @Nested
+ @DisplayName("isNull/isNotNull")
+ class NullTests {
+
+ @Test
+ @DisplayName("isNull with null object returns true")
+ void isNull_withNull_returnsTrue() {
+ assertThat(ObjectHelper.isNull(null)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isNull with non-null object returns false")
+ void isNull_withNonNull_returnsFalse() {
+ assertThat(ObjectHelper.isNull(new Object())).isFalse();
+ }
+
+ @Test
+ @DisplayName("isNotNull with null object returns false")
+ void isNotNull_withNull_returnsFalse() {
+ assertThat(ObjectHelper.isNotNull(null)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isNotNull with non-null object returns true")
+ void isNotNull_withNonNull_returnsTrue() {
+ assertThat(ObjectHelper.isNotNull(new Object())).isTrue();
+ }
+ }
+
+ @Nested
+ @DisplayName("isEmpty/isNotEmpty")
+ class EmptyTests {
+
+ static Stream emptyObjects() {
+ return Stream.of(
+ Arguments.of("null", null, true),
+ Arguments.of("empty collection", Collections.emptyList(), true),
+ Arguments.of("non-empty collection", List.of("a"), false),
+ Arguments.of("empty map", Collections.emptyMap(), true),
+ Arguments.of("non-empty map", Map.of("key", "value"), false),
+ Arguments.of("empty string", "", true),
+ Arguments.of("non-empty string", "text", false),
+ Arguments.of("empty char sequence", new StringBuilder(), true),
+ Arguments.of("non-empty char sequence", new StringBuilder("text"), false),
+ Arguments.of("empty object array", new Object[]{}, true),
+ Arguments.of("non-empty object array", new Object[]{"a"}, false),
+ Arguments.of("empty byte array", new byte[]{}, true),
+ Arguments.of("non-empty byte array", new byte[]{1}, false),
+ Arguments.of("empty short array", new short[]{}, true),
+ Arguments.of("non-empty short array", new short[]{1}, false),
+ Arguments.of("empty int array", new int[]{}, true),
+ Arguments.of("non-empty int array", new int[]{1}, false),
+ Arguments.of("empty long array", new long[]{}, true),
+ Arguments.of("non-empty long array", new long[]{1L}, false),
+ Arguments.of("empty float array", new float[]{}, true),
+ Arguments.of("non-empty float array", new float[]{1.0f}, false),
+ Arguments.of("empty double array", new double[]{}, true),
+ Arguments.of("non-empty double array", new double[]{1.0}, false),
+ Arguments.of("empty char array", new char[]{}, true),
+ Arguments.of("non-empty char array", new char[]{'a'}, false),
+ Arguments.of("empty boolean array", new boolean[]{}, true),
+ Arguments.of("non-empty boolean array", new boolean[]{true}, false),
+ Arguments.of("empty optional", Optional.empty(), true),
+ Arguments.of("non-empty optional", Optional.of("value"), false),
+ Arguments.of("non-empty object", new Object(), false)
+ );
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("emptyObjects")
+ @DisplayName("isEmpty returns correct result")
+ void isEmpty_withVariousObjects_returnsCorrectResult(String description, Object obj, boolean expected) {
+ assertThat(ObjectHelper.isEmpty(obj)).isEqualTo(expected);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("emptyObjects")
+ @DisplayName("isNotEmpty returns opposite of isEmpty")
+ void isNotEmpty_withVariousObjects_returnsCorrectResult(String description, Object obj, boolean expected) {
+ assertThat(ObjectHelper.isNotEmpty(obj)).isEqualTo(!expected);
+ }
+ }
+
+ @Nested
+ @DisplayName("defaultIfNull")
+ class DefaultIfNullTests {
+
+ @Test
+ @DisplayName("defaultIfNull with null returns default value")
+ void defaultIfNull_withNull_returnsDefault() {
+ assertThat(ObjectHelper.defaultIfNull(null, "default")).isEqualTo("default");
+ }
+
+ @Test
+ @DisplayName("defaultIfNull with non-null returns original value")
+ void defaultIfNull_withNonNull_returnsOriginal() {
+ assertThat(ObjectHelper.defaultIfNull("value", "default")).isEqualTo("value");
+ }
+
+ @Test
+ @DisplayName("defaultIfNull with null and null default returns null")
+ void defaultIfNull_withBothNull_returnsNull() {
+ String result = ObjectHelper.defaultIfNull(null, null);
+ assertThat(result).isNull();
+ }
+ }
+
+ @Nested
+ @DisplayName("isComparable")
+ class ComparableTests {
+
+ @Test
+ @DisplayName("isComparable with enum class returns true")
+ void isComparable_withEnum_returnsTrue() {
+ assertThat(ObjectHelper.isComparable(TestEnum.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isComparable with String class returns true")
+ void isComparable_withString_returnsTrue() {
+ assertThat(ObjectHelper.isComparable(String.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isComparable with Integer class returns true")
+ void isComparable_withInteger_returnsTrue() {
+ assertThat(ObjectHelper.isComparable(Integer.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isComparable with primitive class returns true")
+ void isComparable_withPrimitive_returnsTrue() {
+ assertThat(ObjectHelper.isComparable(int.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isComparable with non-comparable class returns false")
+ void isComparable_withNonComparable_returnsFalse() {
+ assertThat(ObjectHelper.isComparable(Object.class)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isComparable with null class returns false")
+ void isComparable_withNull_returnsFalse() {
+ Class> nullClass = null;
+ assertThat(ObjectHelper.isComparable(nullClass)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isComparable with enum object returns true")
+ void isComparable_withEnumObject_returnsTrue() {
+ assertThat(ObjectHelper.isComparable(TestEnum.VALUE)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isComparable with null object returns false")
+ void isComparable_withNullObject_returnsFalse() {
+ Object nullObj = null;
+ assertThat(ObjectHelper.isComparable(nullObj)).isFalse();
+ }
+ }
+
+ @Nested
+ @DisplayName("isCollection")
+ class CollectionTests {
+
+ @Test
+ @DisplayName("isCollection with List class returns true")
+ void isCollection_withList_returnsTrue() {
+ assertThat(ObjectHelper.isCollection(List.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isCollection with Set class returns true")
+ void isCollection_withSet_returnsTrue() {
+ assertThat(ObjectHelper.isCollection(Set.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isCollection with non-collection class returns false")
+ void isCollection_withNonCollection_returnsFalse() {
+ assertThat(ObjectHelper.isCollection(String.class)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isCollection with null class returns false")
+ void isCollection_withNull_returnsFalse() {
+ Class> nullClass = null;
+ assertThat(ObjectHelper.isCollection(nullClass)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isCollection with ArrayList object returns true")
+ void isCollection_withArrayListObject_returnsTrue() {
+ assertThat(ObjectHelper.isCollection(new ArrayList<>())).isTrue();
+ }
+
+ @Test
+ @DisplayName("isCollection with null object returns false")
+ void isCollection_withNullObject_returnsFalse() {
+ Object nullObj = null;
+ assertThat(ObjectHelper.isCollection(nullObj)).isFalse();
+ }
+ }
+
+ @Nested
+ @DisplayName("isString")
+ class StringTests {
+
+ @Test
+ @DisplayName("isString with String class returns true")
+ void isString_withStringClass_returnsTrue() {
+ assertThat(ObjectHelper.isString(String.class)).isTrue();
+ }
+
+ @Test
+ @DisplayName("isString with non-string class returns false")
+ void isString_withNonStringClass_returnsFalse() {
+ assertThat(ObjectHelper.isString(Integer.class)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isString with null class returns false")
+ void isString_withNullClass_returnsFalse() {
+ Class> nullClass = null;
+ assertThat(ObjectHelper.isString(nullClass)).isFalse();
+ }
+
+ @Test
+ @DisplayName("isString with string object returns true")
+ void isString_withStringObject_returnsTrue() {
+ assertThat(ObjectHelper.isString("text")).isTrue();
+ }
+
+ @Test
+ @DisplayName("isString with null object returns false")
+ void isString_withNullObject_returnsFalse() {
+ Object nullObj = null;
+ assertThat(ObjectHelper.isString(nullObj)).isFalse();
+ }
+ }
+
+ private enum TestEnum {
+ VALUE
+ }
+}
diff --git a/spring-boot-service-template-database/pom.xml b/spring-boot-service-template-database/pom.xml
index 845b57f..7f3ba0f 100644
--- a/spring-boot-service-template-database/pom.xml
+++ b/spring-boot-service-template-database/pom.xml
@@ -43,6 +43,31 @@
org.mapstruct
mapstruct
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-data-jpa-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-webmvc-test
+ test
+
+
+ com.h2database
+ h2
+ test
+
+
+ com.github.gavlyukovskiy
+ p6spy-spring-boot-starter
+ test
+
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/helper/SnowflakeHelperTest.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/helper/SnowflakeHelperTest.java
new file mode 100644
index 0000000..d4465da
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/helper/SnowflakeHelperTest.java
@@ -0,0 +1,104 @@
+package com.lanyuanxiaoyao.service.template.database.helper;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DisplayName("SnowflakeHelper Tests")
+class SnowflakeHelperTest {
+
+ @Nested
+ @DisplayName("ID Generation")
+ class IdGenerationTests {
+
+ @Test
+ @DisplayName("next returns positive number")
+ void next_returnsPositiveNumber() {
+ long id = SnowflakeHelper.next();
+ assertThat(id).isPositive();
+ }
+
+ @Test
+ @DisplayName("next returns monotonically increasing IDs")
+ void next_returnsMonotonicallyIncreasingIds() {
+ long id1 = SnowflakeHelper.next();
+ long id2 = SnowflakeHelper.next();
+ long id3 = SnowflakeHelper.next();
+
+ assertThat(id2).isGreaterThan(id1);
+ assertThat(id3).isGreaterThan(id2);
+ }
+ }
+
+ @Nested
+ @DisplayName("Uniqueness")
+ class UniquenessTests {
+
+ @Test
+ @DisplayName("batch generation produces unique IDs")
+ void batchGeneration_producesUniqueIds() {
+ int count = 10000;
+ Set ids = Collections.synchronizedSet(new HashSet<>());
+
+ for (int i = 0; i < count; i++) {
+ ids.add(SnowflakeHelper.next());
+ }
+
+ assertThat(ids).hasSize(count);
+ }
+
+ @Test
+ @DisplayName("concurrent generation produces unique IDs")
+ void concurrentGeneration_producesUniqueIds() throws InterruptedException {
+ int threadCount = 10;
+ int idsPerThread = 1000;
+ Set ids = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+ for (int i = 0; i < threadCount; i++) {
+ executor.submit(() -> {
+ try {
+ for (int j = 0; j < idsPerThread; j++) {
+ ids.add(SnowflakeHelper.next());
+ }
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ latch.await();
+ executor.shutdown();
+
+ assertThat(ids).hasSize(threadCount * idsPerThread);
+ }
+ }
+
+ @Nested
+ @DisplayName("Performance")
+ class PerformanceTests {
+
+ @RepeatedTest(5)
+ @DisplayName("generates 1000 IDs quickly")
+ void generatesIdsQuickly() {
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < 1000; i++) {
+ SnowflakeHelper.next();
+ }
+ long elapsed = System.currentTimeMillis() - start;
+ assertThat(elapsed).isLessThan(1000);
+ }
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/IntegrationTestConfiguration.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/IntegrationTestConfiguration.java
new file mode 100644
index 0000000..a67e024
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/IntegrationTestConfiguration.java
@@ -0,0 +1,11 @@
+package com.lanyuanxiaoyao.service.template.database.integration;
+
+import com.blinkfox.fenix.EnableFenix;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@SpringBootApplication
+@EnableFenix
+@EnableJpaAuditing
+public class IntegrationTestConfiguration {
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/QueryBuilder.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/QueryBuilder.java
new file mode 100644
index 0000000..43dbcee
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/QueryBuilder.java
@@ -0,0 +1,151 @@
+package com.lanyuanxiaoyao.service.template.database.integration;
+
+import com.lanyuanxiaoyao.service.template.database.entity.Query;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+public class QueryBuilder {
+ private List nullEqual;
+ private List notNullEqual;
+ private List empty;
+ private List notEmpty;
+ private Map equal;
+ private Map notEqual;
+ private Map like;
+ private Map notLike;
+ private Map contain;
+ private Map notContain;
+ private Map startWith;
+ private Map notStartWith;
+ private Map endWith;
+ private Map notEndWith;
+ private Map great;
+ private Map less;
+ private Map greatEqual;
+ private Map lessEqual;
+ private Map> inside;
+ private Map> notInside;
+ private Map between;
+ private Map notBetween;
+
+ public QueryBuilder nullEqual(List nullEqual) {
+ this.nullEqual = nullEqual;
+ return this;
+ }
+
+ public QueryBuilder notNullEqual(List notNullEqual) {
+ this.notNullEqual = notNullEqual;
+ return this;
+ }
+
+ public QueryBuilder empty(List empty) {
+ this.empty = empty;
+ return this;
+ }
+
+ public QueryBuilder notEmpty(List notEmpty) {
+ this.notEmpty = notEmpty;
+ return this;
+ }
+
+ public QueryBuilder equal(Map equal) {
+ this.equal = equal;
+ return this;
+ }
+
+ public QueryBuilder notEqual(Map notEqual) {
+ this.notEqual = notEqual;
+ return this;
+ }
+
+ public QueryBuilder like(Map like) {
+ this.like = like;
+ return this;
+ }
+
+ public QueryBuilder notLike(Map notLike) {
+ this.notLike = notLike;
+ return this;
+ }
+
+ public QueryBuilder contain(Map contain) {
+ this.contain = contain;
+ return this;
+ }
+
+ public QueryBuilder notContain(Map notContain) {
+ this.notContain = notContain;
+ return this;
+ }
+
+ public QueryBuilder startWith(Map startWith) {
+ this.startWith = startWith;
+ return this;
+ }
+
+ public QueryBuilder notStartWith(Map notStartWith) {
+ this.notStartWith = notStartWith;
+ return this;
+ }
+
+ public QueryBuilder endWith(Map endWith) {
+ this.endWith = endWith;
+ return this;
+ }
+
+ public QueryBuilder notEndWith(Map notEndWith) {
+ this.notEndWith = notEndWith;
+ return this;
+ }
+
+ public QueryBuilder great(Map great) {
+ this.great = great;
+ return this;
+ }
+
+ public QueryBuilder less(Map less) {
+ this.less = less;
+ return this;
+ }
+
+ public QueryBuilder greatEqual(Map greatEqual) {
+ this.greatEqual = greatEqual;
+ return this;
+ }
+
+ public QueryBuilder lessEqual(Map lessEqual) {
+ this.lessEqual = lessEqual;
+ return this;
+ }
+
+ public QueryBuilder inside(Map> inside) {
+ this.inside = inside;
+ return this;
+ }
+
+ public QueryBuilder notInside(Map> notInside) {
+ this.notInside = notInside;
+ return this;
+ }
+
+ public QueryBuilder between(Map between) {
+ this.between = between;
+ return this;
+ }
+
+ public QueryBuilder notBetween(Map notBetween) {
+ this.notBetween = notBetween;
+ return this;
+ }
+
+ public Query build() {
+ return new Query(new Query.Queryable(
+ nullEqual, notNullEqual, empty, notEmpty, equal, notEqual,
+ like, notLike, contain, notContain, startWith, notStartWith,
+ endWith, notEndWith, great, less, greatEqual, lessEqual,
+ inside, notInside, between, notBetween
+ ), null, null);
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SimpleControllerSupportIntegrationTest.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SimpleControllerSupportIntegrationTest.java
new file mode 100644
index 0000000..429ac19
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SimpleControllerSupportIntegrationTest.java
@@ -0,0 +1,208 @@
+package com.lanyuanxiaoyao.service.template.database.integration;
+
+import tools.jackson.databind.ObjectMapper;
+import com.lanyuanxiaoyao.service.template.database.entity.Query;
+import com.lanyuanxiaoyao.service.template.database.integration.controller.TestController;
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
+import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
+import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jpa.test.autoconfigure.AutoConfigureDataJpa;
+import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(TestController.class)
+@AutoConfigureDataJpa
+@ActiveProfiles("test")
+@Import({IntegrationTestConfiguration.class, TestService.class})
+@DisplayName("SimpleControllerSupport Integration Tests")
+class SimpleControllerSupportIntegrationTest {
+
+ @Autowired
+ MockMvc mockMvc;
+
+ @Autowired
+ ObjectMapper objectMapper;
+
+ @Autowired
+ TestRepository repository;
+
+ @BeforeEach
+ void cleanUp() {
+ repository.deleteAll();
+ }
+
+ @Nested
+ @DisplayName("POST /save")
+ class SaveTests {
+
+ @Test
+ @DisplayName("save with valid data returns id")
+ void save_withValidData_returnsId() throws Exception {
+ var request = new TestController.SaveItem("张三", 25, TestStatus.ACTIVE, 5000.0, "java");
+
+ mockMvc.perform(post("/test/save")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0))
+ .andExpect(jsonPath("$.message").value("OK"))
+ .andExpect(jsonPath("$.data").isNumber());
+ }
+
+ @Test
+ @DisplayName("saveItemMapper converts correctly")
+ void saveItemMapper_convertsCorrectly() throws Exception {
+ var request = new TestController.SaveItem("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring");
+
+ String response = mockMvc.perform(post("/test/save")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andReturn().getResponse().getContentAsString();
+
+ Long id = objectMapper.readTree(response).get("data").asLong();
+ TestEntity saved = repository.findById(id).orElseThrow();
+
+ assertThat(saved.getName()).isEqualTo("张三");
+ assertThat(saved.getAge()).isEqualTo(25);
+ assertThat(saved.getStatus()).isEqualTo(TestStatus.ACTIVE);
+ assertThat(saved.getSalary()).isEqualTo(5000.0);
+ assertThat(saved.getTags()).isEqualTo("java,spring");
+ }
+ }
+
+ @Nested
+ @DisplayName("GET /list")
+ class ListTests {
+
+ @Test
+ @DisplayName("list returns all entities")
+ void list_returnsAllEntities() throws Exception {
+ repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+ repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
+
+ mockMvc.perform(get("/test/list"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0))
+ .andExpect(jsonPath("$.message").value("OK"))
+ .andExpect(jsonPath("$.data.items").isArray())
+ .andExpect(jsonPath("$.data.items.length()").value(2))
+ .andExpect(jsonPath("$.data.total").value(2));
+ }
+
+ @Test
+ @DisplayName("listItemMapper converts correctly")
+ void listItemMapper_convertsCorrectly() throws Exception {
+ var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+
+ mockMvc.perform(get("/test/list"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.items[0].id").value(entity.getId()))
+ .andExpect(jsonPath("$.data.items[0].name").value("张三"))
+ .andExpect(jsonPath("$.data.items[0].age").value(25))
+ .andExpect(jsonPath("$.data.items[0].status").value("ACTIVE"));
+ }
+ }
+
+ @Nested
+ @DisplayName("POST /list")
+ class ListWithQueryTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+ repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
+ }
+
+ @Test
+ @DisplayName("list with query filters correctly")
+ void listWithQuery_filtersCorrectly() throws Exception {
+ var query = new QueryBuilder().equal(java.util.Map.of("name", "张三")).build();
+
+ mockMvc.perform(post("/test/list")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(query)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.items.length()").value(1))
+ .andExpect(jsonPath("$.data.items[0].name").value("张三"));
+ }
+
+ @Test
+ @DisplayName("list with pagination returns correct page")
+ void listWithPagination_returnsCorrectPage() throws Exception {
+ var query = new Query(null, null, new Query.Pageable(1, 1));
+
+ mockMvc.perform(post("/test/list")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(query)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.items.length()").value(1))
+ .andExpect(jsonPath("$.data.total").value(2));
+ }
+ }
+
+ @Nested
+ @DisplayName("GET /detail/{id}")
+ class DetailTests {
+
+ @Test
+ @DisplayName("detail with existing id returns entity")
+ void detail_withExistingId_returnsEntity() throws Exception {
+ var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"));
+
+ mockMvc.perform(get("/test/detail/{id}", entity.getId()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0))
+ .andExpect(jsonPath("$.data.id").value(entity.getId()))
+ .andExpect(jsonPath("$.data.name").value("张三"))
+ .andExpect(jsonPath("$.data.age").value(25))
+ .andExpect(jsonPath("$.data.status").value("ACTIVE"))
+ .andExpect(jsonPath("$.data.salary").value(5000.0))
+ .andExpect(jsonPath("$.data.tags").value("java,spring"));
+ }
+
+ @Test
+ @DisplayName("detailItemMapper converts correctly")
+ void detailItemMapper_convertsCorrectly() throws Exception {
+ var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"));
+
+ mockMvc.perform(get("/test/detail/{id}", entity.getId()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.id").value(entity.getId()))
+ .andExpect(jsonPath("$.data.name").value("张三"))
+ .andExpect(jsonPath("$.data.tags").value("java,spring"));
+ }
+ }
+
+ @Nested
+ @DisplayName("GET /remove/{id}")
+ class RemoveTests {
+
+ @Test
+ @DisplayName("remove deletes entity")
+ void remove_deletesEntity() throws Exception {
+ var entity = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+
+ mockMvc.perform(get("/test/remove/{id}", entity.getId()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0))
+ .andExpect(jsonPath("$.data").isEmpty());
+
+ assertThat(repository.findById(entity.getId())).isEmpty();
+ }
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SimpleServiceSupportIntegrationTest.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SimpleServiceSupportIntegrationTest.java
new file mode 100644
index 0000000..5b4c9e6
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SimpleServiceSupportIntegrationTest.java
@@ -0,0 +1,662 @@
+package com.lanyuanxiaoyao.service.template.database.integration;
+
+import com.lanyuanxiaoyao.service.template.database.entity.Query;
+import com.lanyuanxiaoyao.service.template.database.exception.IdNotFoundException;
+import com.lanyuanxiaoyao.service.template.database.exception.NotCollectionException;
+import com.lanyuanxiaoyao.service.template.database.exception.NotComparableException;
+import com.lanyuanxiaoyao.service.template.database.exception.NotStringException;
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
+import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
+import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
+import jakarta.persistence.EntityManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DataJpaTest
+@ActiveProfiles("test")
+@Import({IntegrationTestConfiguration.class, TestService.class})
+@DisplayName("SimpleServiceSupport Integration Tests")
+class SimpleServiceSupportIntegrationTest {
+
+ @Autowired
+ TestService service;
+
+ @Autowired
+ TestRepository repository;
+
+ @Autowired
+ EntityManager entityManager;
+
+ @BeforeEach
+ void cleanUp() {
+ repository.deleteAllInBatch();
+ }
+
+ @Nested
+ @DisplayName("CRUD Operations")
+ class CrudTests {
+
+ @Test
+ @DisplayName("save with new entity returns generated id")
+ void save_withNewEntity_returnsId() {
+ var entity = new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring");
+
+ Long id = service.save(entity);
+
+ assertThat(id).isNotNull().isPositive();
+ var saved = repository.findById(id).orElseThrow();
+ assertThat(saved.getName()).isEqualTo("张三");
+ assertThat(saved.getAge()).isEqualTo(25);
+ assertThat(saved.getStatus()).isEqualTo(TestStatus.ACTIVE);
+ }
+
+ @Test
+ @DisplayName("save with existing entity updates non-null fields")
+ void save_withExistingEntity_updatesFields() {
+ var entity = new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java");
+ Long id = service.save(entity);
+
+ var update = new TestEntity(null, 26, null, 6000.0, null);
+ update.setId(id);
+ service.save(update);
+
+ var updated = repository.findById(id).orElseThrow();
+ assertThat(updated.getName()).isEqualTo("张三");
+ assertThat(updated.getAge()).isEqualTo(26);
+ assertThat(updated.getStatus()).isEqualTo(TestStatus.ACTIVE);
+ assertThat(updated.getSalary()).isEqualTo(6000.0);
+ }
+
+ @Test
+ @DisplayName("save multiple entities saves all")
+ void save_multipleEntities_savesAll() {
+ var entities = List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
+ );
+
+ service.save(entities);
+
+ assertThat(repository.count()).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("count returns correct total")
+ void count_returnsCorrectTotal() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
+ ));
+
+ Long count = service.count();
+
+ assertThat(count).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("list returns all entities")
+ void list_returnsAllEntities() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python")
+ ));
+
+ List result = service.list();
+
+ assertThat(result).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("list with ids returns matching entities")
+ void list_withIds_returnsMatching() {
+ var e1 = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+ var e2 = repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
+ repository.save(new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go"));
+
+ List result = service.list(Set.of(e1.getId(), e2.getId()));
+
+ assertThat(result).hasSize(2)
+ .extracting(TestEntity::getName)
+ .containsExactlyInAnyOrder("张三", "李四");
+ }
+
+ @Test
+ @DisplayName("detail with existing id returns entity")
+ void detail_withExistingId_returnsEntity() {
+ var saved = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+
+ TestEntity result = service.detail(saved.getId());
+
+ assertThat(result).isNotNull();
+ assertThat(result.getName()).isEqualTo("张三");
+ }
+
+ @Test
+ @DisplayName("detail with non-existing id returns null")
+ void detail_withNonExistingId_returnsNull() {
+ TestEntity result = service.detail(999L);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ @DisplayName("detailOrThrow with non-existing id throws exception")
+ void detailOrThrow_withNonExistingId_throwsException() {
+ assertThatThrownBy(() -> service.detailOrThrow(999L))
+ .isInstanceOf(IdNotFoundException.class);
+ }
+
+ @Test
+ @DisplayName("remove deletes entity")
+ void remove_deletesEntity() {
+ var saved = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+ repository.flush();
+ entityManager.clear();
+
+ service.remove(saved.getId());
+ repository.flush();
+
+ var nativeQuery = entityManager.createNativeQuery("SELECT COUNT(*) FROM test_entity WHERE id = ?");
+ nativeQuery.setParameter(1, saved.getId());
+ var count = ((Number) nativeQuery.getSingleResult()).longValue();
+ assertThat(count).isZero();
+ }
+
+ @Test
+ @DisplayName("remove multiple entities deletes all")
+ void remove_multipleEntities_deletesAll() {
+ var e1 = repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+ var e2 = repository.save(new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"));
+
+ service.remove(Set.of(e1.getId(), e2.getId()));
+ repository.flush();
+
+ assertThat(repository.count()).isZero();
+ }
+ }
+
+ @Nested
+ @DisplayName("Equality Conditions")
+ class EqualityConditionTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, null),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+ }
+
+ @Test
+ @DisplayName("equal condition filters correctly")
+ void equalCondition_filtersCorrectly() {
+ var query = new QueryBuilder().equal(Map.of("name", "张三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getName()).isEqualTo("张三");
+ }
+
+ @Test
+ @DisplayName("notEqual condition filters correctly")
+ void notEqualCondition_filtersCorrectly() {
+ var query = new QueryBuilder().notEqual(Map.of("name", "张三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).noneMatch(e -> "张三".equals(e.getName()));
+ }
+
+ @Test
+ @DisplayName("nullEqual condition filters null values")
+ void nullEqualCondition_filtersNullValues() {
+ var query = new QueryBuilder().nullEqual(List.of("tags")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getTags()).isNull();
+ }
+
+ @Test
+ @DisplayName("notNullEqual condition filters non-null values")
+ void notNullEqualCondition_filtersNonNullValues() {
+ var query = new QueryBuilder().notNullEqual(List.of("tags")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getTags() != null);
+ }
+ }
+
+ @Nested
+ @DisplayName("String Conditions")
+ class StringConditionTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java,spring"),
+ new TestEntity("李四三", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+ }
+
+ @Test
+ @DisplayName("like condition filters correctly")
+ void likeCondition_filtersCorrectly() {
+ var query = new QueryBuilder().like(Map.of("name", "%三%")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("notLike condition filters correctly")
+ void notLikeCondition_filtersCorrectly() {
+ var query = new QueryBuilder().notLike(Map.of("name", "%三%")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getName()).isEqualTo("王五");
+ }
+
+ @Test
+ @DisplayName("contain condition filters correctly")
+ void containCondition_filtersCorrectly() {
+ var query = new QueryBuilder().contain(Map.of("name", "三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("notContain condition filters correctly")
+ void notContainCondition_filtersCorrectly() {
+ var query = new QueryBuilder().notContain(Map.of("name", "三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ }
+
+ @Test
+ @DisplayName("startWith condition filters correctly")
+ void startWithCondition_filtersCorrectly() {
+ var query = new QueryBuilder().startWith(Map.of("name", "张")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getName()).isEqualTo("张三");
+ }
+
+ @Test
+ @DisplayName("notStartWith condition filters correctly")
+ void notStartWithCondition_filtersCorrectly() {
+ var query = new QueryBuilder().notStartWith(Map.of("name", "张")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).noneMatch(e -> e.getName().startsWith("张"));
+ }
+
+ @Test
+ @DisplayName("endWith condition filters correctly")
+ void endWithCondition_filtersCorrectly() {
+ var query = new QueryBuilder().endWith(Map.of("name", "三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("notEndWith condition filters correctly")
+ void notEndWithCondition_filtersCorrectly() {
+ var query = new QueryBuilder().notEndWith(Map.of("name", "三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getName()).isEqualTo("王五");
+ }
+ }
+
+ @Nested
+ @DisplayName("Comparison Conditions")
+ class ComparisonConditionTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 35, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 45, TestStatus.DELETED, 7000.0, "go")
+ ));
+ }
+
+ @Test
+ @DisplayName("great condition filters correctly")
+ void greatCondition_filtersCorrectly() {
+ var query = new QueryBuilder().great(Map.of("age", 30)).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getAge() > 30);
+ }
+
+ @Test
+ @DisplayName("less condition filters correctly")
+ void lessCondition_filtersCorrectly() {
+ var query = new QueryBuilder().less(Map.of("age", 40)).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getAge() < 40);
+ }
+
+ @Test
+ @DisplayName("greatEqual condition filters correctly")
+ void greatEqualCondition_filtersCorrectly() {
+ var query = new QueryBuilder().greatEqual(Map.of("age", 35)).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getAge() >= 35);
+ }
+
+ @Test
+ @DisplayName("lessEqual condition filters correctly")
+ void lessEqualCondition_filtersCorrectly() {
+ var query = new QueryBuilder().lessEqual(Map.of("age", 35)).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getAge() <= 35);
+ }
+
+ @Test
+ @DisplayName("between condition filters correctly")
+ void betweenCondition_filtersCorrectly() {
+ var query = new QueryBuilder().between(Map.of("age", new Query.Queryable.Between(25, 35))).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getAge() >= 25 && e.getAge() <= 35);
+ }
+
+ @Test
+ @DisplayName("notBetween condition filters correctly")
+ void notBetweenCondition_filtersCorrectly() {
+ var query = new QueryBuilder().notBetween(Map.of("age", new Query.Queryable.Between(26, 44))).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ }
+ }
+
+ @Nested
+ @DisplayName("Collection Conditions")
+ class CollectionConditionTests {
+
+ @Test
+ @DisplayName("inside condition filters correctly")
+ void insideCondition_filtersCorrectly() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+
+ var query = new QueryBuilder().inside(Map.of("age", List.of(25, 35))).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ }
+
+ @Test
+ @DisplayName("notInside condition filters correctly")
+ void notInsideCondition_filtersCorrectly() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+
+ var query = new QueryBuilder().notInside(Map.of("age", List.of(25, 35))).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getAge()).isEqualTo(30);
+ }
+
+ @Test
+ @DisplayName("empty condition filters correctly")
+ void emptyCondition_filtersCorrectly() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+
+ var query = new QueryBuilder().empty(List.of("tags")).build();
+
+ assertThatThrownBy(() -> service.list(query))
+ .isInstanceOf(NotCollectionException.class);
+ }
+
+ @Test
+ @DisplayName("notEmpty condition filters correctly")
+ void notEmptyCondition_filtersCorrectly() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+
+ var query = new QueryBuilder().notEmpty(List.of("tags")).build();
+
+ assertThatThrownBy(() -> service.list(query))
+ .isInstanceOf(NotCollectionException.class);
+ }
+ }
+
+ @Nested
+ @DisplayName("Enum Condition")
+ class EnumConditionTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.DELETED, 7000.0, "go")
+ ));
+ }
+
+ @Test
+ @DisplayName("enum condition filters correctly")
+ void enumCondition_filtersCorrectly() {
+ var query = new QueryBuilder().equal(Map.of("status", "ACTIVE")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(1);
+ assertThat(result.items().get(0).getStatus()).isEqualTo(TestStatus.ACTIVE);
+ }
+ }
+
+ @Nested
+ @DisplayName("Combined Conditions")
+ class CombinedConditionTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.saveAll(List.of(
+ new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"),
+ new TestEntity("李四", 30, TestStatus.INACTIVE, 6000.0, "python"),
+ new TestEntity("王五", 35, TestStatus.ACTIVE, 7000.0, "go"),
+ new TestEntity("赵六", 40, TestStatus.ACTIVE, 8000.0, "rust")
+ ));
+ }
+
+ @Test
+ @DisplayName("combined conditions filter correctly")
+ void combinedConditions_filterCorrectly() {
+ var query = new QueryBuilder()
+ .equal(Map.of("status", "ACTIVE"))
+ .great(Map.of("age", 30))
+ .build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(2);
+ assertThat(result.items()).allMatch(e -> e.getStatus() == TestStatus.ACTIVE && e.getAge() > 30);
+ }
+ }
+
+ @Nested
+ @DisplayName("Pagination and Sorting")
+ class PaginationSortingTests {
+
+ @BeforeEach
+ void setUp() {
+ repository.deleteAllInBatch();
+ entityManager.flush();
+ entityManager.clear();
+ for (int i = 1; i <= 25; i++) {
+ repository.save(new TestEntity("用户" + i, 20 + i, TestStatus.ACTIVE, 5000.0 + i * 100, "tag" + i));
+ }
+ }
+
+ @Test
+ @DisplayName("default pagination returns first page with 10 items")
+ void defaultPagination_returnsFirstPage() {
+ var query = new Query(null, null, null);
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(10);
+ assertThat(result.total()).isEqualTo(25);
+ }
+
+ @Test
+ @DisplayName("custom pagination returns correct page")
+ void customPagination_returnsCorrectPage() {
+ var query = new Query(null, null, new Query.Pageable(1, 5));
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(5);
+ assertThat(result.total()).isEqualTo(25);
+ }
+
+ @Test
+ @DisplayName("custom sorting sorts correctly")
+ void customSorting_sortsCorrectly() {
+ var query = new Query(null, List.of(new Query.Sortable("age", Query.Sortable.Direction.ASC)), new Query.Pageable(1, 10));
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(10);
+ assertThat(result.items().get(0).getAge()).isEqualTo(21);
+ assertThat(result.items()).isSortedAccordingTo((a, b) -> a.getAge().compareTo(b.getAge()));
+ }
+
+ @Test
+ @DisplayName("multiple sort fields sort correctly")
+ void multipleSortFields_sortCorrectly() {
+ repository.deleteAllInBatch();
+ entityManager.flush();
+ entityManager.clear();
+
+ repository.saveAll(List.of(
+ new TestEntity("A", 20, TestStatus.ACTIVE, 5000.0, null),
+ new TestEntity("B", 30, TestStatus.ACTIVE, 3000.0, null),
+ new TestEntity("C", 30, TestStatus.ACTIVE, 4000.0, null)
+ ));
+
+ var query = new Query(null, List.of(
+ new Query.Sortable("age", Query.Sortable.Direction.ASC),
+ new Query.Sortable("salary", Query.Sortable.Direction.DESC)
+ ), new Query.Pageable(1, 10));
+
+ var result = service.list(query);
+
+ assertThat(result.items()).hasSize(3);
+ assertThat(result.items().get(0).getName()).isEqualTo("A");
+ assertThat(result.items().get(1).getName()).isEqualTo("C");
+ assertThat(result.items().get(2).getName()).isEqualTo("B");
+ }
+ }
+
+ @Nested
+ @DisplayName("Exception Handling")
+ class ExceptionTests {
+
+ @Test
+ @DisplayName("like on non-string field throws NotStringException")
+ void likeOnNonStringField_throwsNotStringException() {
+ repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+
+ var query = new QueryBuilder().like(Map.of("age", "%5%")).build();
+
+ assertThatThrownBy(() -> service.list(query))
+ .isInstanceOf(NotStringException.class);
+ }
+
+ @Test
+ @DisplayName("great on enum field with enum value throws exception")
+ void greatOnEnumField_withEnumValue_throwsException() {
+ repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+
+ var query = new QueryBuilder().great(Map.of("status", TestStatus.ACTIVE)).build();
+
+ assertThatThrownBy(() -> service.list(query))
+ .isInstanceOf(org.springframework.dao.InvalidDataAccessApiUsageException.class)
+ .hasMessageContaining("枚举类型字段需要 String 类型的值");
+ }
+
+ @Test
+ @DisplayName("empty on non-collection field throws NotCollectionException")
+ void emptyOnNonCollectionField_throwsNotCollectionException() {
+ repository.save(new TestEntity("张三", 25, TestStatus.ACTIVE, 5000.0, "java"));
+
+ var query = new QueryBuilder().empty(List.of("name")).build();
+
+ assertThatThrownBy(() -> service.list(query))
+ .isInstanceOf(NotCollectionException.class);
+ }
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SoftDeleteIntegrationTest.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SoftDeleteIntegrationTest.java
new file mode 100644
index 0000000..bee43c7
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/SoftDeleteIntegrationTest.java
@@ -0,0 +1,194 @@
+package com.lanyuanxiaoyao.service.template.database.integration;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.repository.TestSoftDeleteRepository;
+import com.lanyuanxiaoyao.service.template.database.integration.service.TestSoftDeleteService;
+import jakarta.persistence.EntityManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DataJpaTest
+@ActiveProfiles("test")
+@Import({IntegrationTestConfiguration.class, TestSoftDeleteService.class})
+@DisplayName("@SoftDelete Integration Tests")
+class SoftDeleteIntegrationTest {
+
+ @Autowired
+ TestSoftDeleteService service;
+
+ @Autowired
+ TestSoftDeleteRepository repository;
+
+ @Autowired
+ EntityManager entityManager;
+
+ @BeforeEach
+ void cleanUp() {
+ repository.deleteAllInBatch();
+ }
+
+ @Nested
+ @DisplayName("Save and Delete Behavior")
+ class SaveDeleteTests {
+
+ @Test
+ @DisplayName("save entity sets deleted to false")
+ void saveEntity_setsDeletedFalse() {
+ var entity = new TestSoftDeleteEntity("张三", 25);
+
+ Long id = service.save(entity);
+
+ var saved = repository.findById(id).orElseThrow();
+ assertThat(saved.getName()).isEqualTo("张三");
+
+ var nativeQuery = entityManager.createNativeQuery("SELECT deleted FROM test_soft_delete_entity WHERE id = ?");
+ nativeQuery.setParameter(1, id);
+ var deleted = (Boolean) nativeQuery.getSingleResult();
+ assertThat(deleted).isFalse();
+ }
+
+ @Test
+ @DisplayName("remove sets deleted to true")
+ void remove_setsDeletedTrue() {
+ var entity = new TestSoftDeleteEntity("张三", 25);
+ Long id = service.save(entity);
+ repository.flush();
+ entityManager.clear();
+
+ service.remove(id);
+ repository.flush();
+
+ var nativeQuery = entityManager.createNativeQuery("SELECT deleted FROM test_soft_delete_entity WHERE id = ?");
+ nativeQuery.setParameter(1, id);
+ var deleted = (Boolean) nativeQuery.getSingleResult();
+ assertThat(deleted).isTrue();
+ }
+
+ @Test
+ @DisplayName("batch remove sets deleted to true")
+ void batchRemove_setsDeletedTrue() {
+ var e1 = new TestSoftDeleteEntity("张三", 25);
+ var e2 = new TestSoftDeleteEntity("李四", 30);
+ Long id1 = service.save(e1);
+ Long id2 = service.save(e2);
+
+ service.remove(Set.of(id1, id2));
+
+ var nativeQuery = entityManager.createNativeQuery("SELECT COUNT(*) FROM test_soft_delete_entity WHERE deleted = true");
+ var count = ((Number) nativeQuery.getSingleResult()).longValue();
+ assertThat(count).isEqualTo(2);
+
+ assertThat(repository.findAll()).isEmpty();
+ }
+ }
+
+ @Nested
+ @DisplayName("Query Behavior")
+ class QueryTests {
+
+ @Test
+ @DisplayName("list filters out deleted entities")
+ void list_filtersDeletedEntities() {
+ var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
+ repository.save(new TestSoftDeleteEntity("李四", 30));
+
+ entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
+ .setParameter(1, e1.getId())
+ .executeUpdate();
+ entityManager.clear();
+
+ List result = service.list();
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getName()).isEqualTo("李四");
+ }
+
+ @Test
+ @DisplayName("count excludes deleted entities")
+ void count_excludesDeletedEntities() {
+ var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
+ repository.save(new TestSoftDeleteEntity("李四", 30));
+
+ entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
+ .setParameter(1, e1.getId())
+ .executeUpdate();
+ entityManager.clear();
+
+ Long count = service.count();
+
+ assertThat(count).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("detail returns null for deleted entity")
+ void detail_returnsNullForDeletedEntity() {
+ var entity = repository.save(new TestSoftDeleteEntity("张三", 25));
+
+ entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
+ .setParameter(1, entity.getId())
+ .executeUpdate();
+ entityManager.clear();
+
+ TestSoftDeleteEntity result = service.detail(entity.getId());
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ @DisplayName("query with conditions filters deleted entities")
+ void queryWithConditions_filtersDeletedEntities() {
+ var e1 = repository.save(new TestSoftDeleteEntity("张三", 25));
+ repository.save(new TestSoftDeleteEntity("李四", 30));
+
+ entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
+ .setParameter(1, e1.getId())
+ .executeUpdate();
+ entityManager.clear();
+
+ var query = new QueryBuilder().equal(Map.of("name", "张三")).build();
+
+ var result = service.list(query);
+
+ assertThat(result.items()).isEmpty();
+ }
+ }
+
+ @Nested
+ @DisplayName("Restore Behavior")
+ class RestoreTests {
+
+ @Test
+ @DisplayName("restore deleted entity makes it visible again")
+ void restore_makesEntityVisible() {
+ var entity = repository.save(new TestSoftDeleteEntity("张三", 25));
+
+ entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = true WHERE id = ?")
+ .setParameter(1, entity.getId())
+ .executeUpdate();
+ entityManager.clear();
+
+ assertThat(service.list()).isEmpty();
+
+ entityManager.createNativeQuery("UPDATE test_soft_delete_entity SET deleted = false WHERE id = ?")
+ .setParameter(1, entity.getId())
+ .executeUpdate();
+ entityManager.clear();
+
+ List result = service.list();
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getName()).isEqualTo("张三");
+ }
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/controller/TestController.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/controller/TestController.java
new file mode 100644
index 0000000..a99244c
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/controller/TestController.java
@@ -0,0 +1,41 @@
+package com.lanyuanxiaoyao.service.template.database.integration.controller;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestStatus;
+import com.lanyuanxiaoyao.service.template.database.integration.service.TestService;
+import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
+import java.util.function.Function;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/test")
+public class TestController extends SimpleControllerSupport {
+ public TestController(TestService service) {
+ super(service);
+ }
+
+ @Override
+ protected Function saveItemMapper() {
+ return item -> new TestEntity(item.name, item.age, item.status, item.salary, item.tags);
+ }
+
+ @Override
+ protected Function listItemMapper() {
+ return entity -> new ListItem(entity.getId(), entity.getName(), entity.getAge(), entity.getStatus());
+ }
+
+ @Override
+ protected Function detailItemMapper() {
+ return entity -> new DetailItem(entity.getId(), entity.getName(), entity.getAge(), entity.getStatus(), entity.getSalary(), entity.getTags());
+ }
+
+ public record SaveItem(String name, Integer age, TestStatus status, Double salary, String tags) {
+ }
+
+ public record ListItem(Long id, String name, Integer age, TestStatus status) {
+ }
+
+ public record DetailItem(Long id, String name, Integer age, TestStatus status, Double salary, String tags) {
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/controller/TestSoftDeleteController.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/controller/TestSoftDeleteController.java
new file mode 100644
index 0000000..9cbde42
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/controller/TestSoftDeleteController.java
@@ -0,0 +1,40 @@
+package com.lanyuanxiaoyao.service.template.database.integration.controller;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.service.TestSoftDeleteService;
+import com.lanyuanxiaoyao.service.template.database.controller.SimpleControllerSupport;
+import java.util.function.Function;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/test-soft-delete")
+public class TestSoftDeleteController extends SimpleControllerSupport {
+ public TestSoftDeleteController(TestSoftDeleteService service) {
+ super(service);
+ }
+
+ @Override
+ protected Function saveItemMapper() {
+ return item -> new TestSoftDeleteEntity(item.name, item.age);
+ }
+
+ @Override
+ protected Function listItemMapper() {
+ return entity -> new ListItem(entity.getId(), entity.getName(), entity.getAge());
+ }
+
+ @Override
+ protected Function detailItemMapper() {
+ return entity -> new DetailItem(entity.getId(), entity.getName(), entity.getAge());
+ }
+
+ public record SaveItem(String name, Integer age) {
+ }
+
+ public record ListItem(Long id, String name, Integer age) {
+ }
+
+ public record DetailItem(Long id, String name, Integer age) {
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestEntity.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestEntity.java
new file mode 100644
index 0000000..7a3265d
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestEntity.java
@@ -0,0 +1,42 @@
+package com.lanyuanxiaoyao.service.template.database.integration.entity;
+
+import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
+import com.lanyuanxiaoyao.service.template.database.entity.SnowflakeId;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.FieldNameConstants;
+
+@Entity
+@Table(name = "test_entity")
+@Getter
+@Setter
+@ToString
+@FieldNameConstants
+@NoArgsConstructor
+@AllArgsConstructor
+public class TestEntity extends SimpleEntity {
+ @SnowflakeId
+ @Column(comment = "名称")
+ private String name;
+
+ @Column(comment = "年龄")
+ private Integer age;
+
+ @Enumerated(EnumType.STRING)
+ @Column(comment = "状态")
+ private TestStatus status;
+
+ @Column(comment = "薪资")
+ private Double salary;
+
+ @Column(comment = "标签")
+ private String tags;
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestSoftDeleteEntity.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestSoftDeleteEntity.java
new file mode 100644
index 0000000..02b4dbe
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestSoftDeleteEntity.java
@@ -0,0 +1,33 @@
+package com.lanyuanxiaoyao.service.template.database.integration.entity;
+
+import com.lanyuanxiaoyao.service.template.database.entity.SimpleEntity;
+import com.lanyuanxiaoyao.service.template.database.entity.SnowflakeId;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.FieldNameConstants;
+import org.hibernate.annotations.SoftDelete;
+import org.hibernate.annotations.SoftDeleteType;
+
+@Entity
+@Table(name = "test_soft_delete_entity")
+@SoftDelete(strategy = SoftDeleteType.DELETED)
+@Getter
+@Setter
+@ToString
+@FieldNameConstants
+@NoArgsConstructor
+@AllArgsConstructor
+public class TestSoftDeleteEntity extends SimpleEntity {
+ @SnowflakeId
+ @Column(comment = "名称")
+ private String name;
+
+ @Column(comment = "年龄")
+ private Integer age;
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestStatus.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestStatus.java
new file mode 100644
index 0000000..f3a90d6
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/entity/TestStatus.java
@@ -0,0 +1,7 @@
+package com.lanyuanxiaoyao.service.template.database.integration.entity;
+
+public enum TestStatus {
+ ACTIVE,
+ INACTIVE,
+ DELETED
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/repository/TestRepository.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/repository/TestRepository.java
new file mode 100644
index 0000000..9c93583
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/repository/TestRepository.java
@@ -0,0 +1,9 @@
+package com.lanyuanxiaoyao.service.template.database.integration.repository;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
+import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TestRepository extends SimpleRepository {
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/repository/TestSoftDeleteRepository.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/repository/TestSoftDeleteRepository.java
new file mode 100644
index 0000000..1ba41b9
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/repository/TestSoftDeleteRepository.java
@@ -0,0 +1,9 @@
+package com.lanyuanxiaoyao.service.template.database.integration.repository;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
+import com.lanyuanxiaoyao.service.template.database.repository.SimpleRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface TestSoftDeleteRepository extends SimpleRepository {
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/service/TestService.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/service/TestService.java
new file mode 100644
index 0000000..82d05a1
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/service/TestService.java
@@ -0,0 +1,13 @@
+package com.lanyuanxiaoyao.service.template.database.integration.service;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.repository.TestRepository;
+import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TestService extends SimpleServiceSupport {
+ public TestService(TestRepository repository) {
+ super(repository);
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/service/TestSoftDeleteService.java b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/service/TestSoftDeleteService.java
new file mode 100644
index 0000000..417ef07
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/java/com/lanyuanxiaoyao/service/template/database/integration/service/TestSoftDeleteService.java
@@ -0,0 +1,13 @@
+package com.lanyuanxiaoyao.service.template.database.integration.service;
+
+import com.lanyuanxiaoyao.service.template.database.integration.entity.TestSoftDeleteEntity;
+import com.lanyuanxiaoyao.service.template.database.integration.repository.TestSoftDeleteRepository;
+import com.lanyuanxiaoyao.service.template.database.service.SimpleServiceSupport;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TestSoftDeleteService extends SimpleServiceSupport {
+ public TestSoftDeleteService(TestSoftDeleteRepository repository) {
+ super(repository);
+ }
+}
diff --git a/spring-boot-service-template-database/src/test/resources/application-test.yaml b/spring-boot-service-template-database/src/test/resources/application-test.yaml
new file mode 100644
index 0000000..969f709
--- /dev/null
+++ b/spring-boot-service-template-database/src/test/resources/application-test.yaml
@@ -0,0 +1,20 @@
+spring:
+ datasource:
+ url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+ driver-class-name: org.h2.Driver
+ username: sa
+ password:
+
+ jpa:
+ hibernate:
+ ddl-auto: create-drop
+
+decorator:
+ datasource:
+ p6spy:
+ logging: slf4j
+ log-format: "%(executionTime) ms | %(sqlSingleLine)"
+
+logging:
+ level:
+ p6spy: INFO