From 9ecb2fba41625e3edb94fc87845275a202742a36 Mon Sep 17 00:00:00 2001 From: let5sne Date: Sun, 12 Jul 2026 10:16:58 +0800 Subject: [PATCH] fix(personal): harden retrieval edge cases --- .../service/PersonalRetrievalService.java | 18 +++++++++-- .../personal/service/PersonalVectorStore.java | 24 ++++++++++++--- .../PersonalRetrievalServiceTest.java | 9 +++++- .../personal/PersonalVectorStoreTest.java | 30 +++++++++++++++++++ 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalRetrievalService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalRetrievalService.java index 54ac8b96..0e1c0367 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalRetrievalService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalRetrievalService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.DateTimeException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -205,9 +206,7 @@ public class PersonalRetrievalService { || request.queryText().trim().length() > 1000) { throw new IllegalArgumentException("PERSONAL_SEARCH_QUERY_INVALID"); } - if (request.dateFrom() != null && request.dateTo() != null && request.dateFrom().isAfter(request.dateTo())) { - throw new IllegalArgumentException("PERSONAL_SEARCH_DATE_RANGE_INVALID"); - } + validateDates(request.dateFrom(), request.dateTo()); List itemIds = request.itemIds() == null ? List.of() : request.itemIds().stream().distinct().toList(); if (itemIds.size() > 100 || itemIds.stream().anyMatch(id -> id == null || id <= 0)) { throw new IllegalArgumentException("PERSONAL_SEARCH_ITEM_SCOPE_INVALID"); @@ -231,6 +230,19 @@ public class PersonalRetrievalService { args.addAll(itemIds); } + private static void validateDates(LocalDate dateFrom, LocalDate dateTo) { + if (dateFrom != null && dateTo != null && dateFrom.isAfter(dateTo)) { + throw new IllegalArgumentException("PERSONAL_SEARCH_DATE_INVALID"); + } + if (dateTo != null) { + try { + dateTo.plusDays(1); + } catch (DateTimeException ex) { + throw new IllegalArgumentException("PERSONAL_SEARCH_DATE_INVALID"); + } + } + } + private static String excerpt(String content) { if (content == null) { return ""; diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalVectorStore.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalVectorStore.java index d395ba0e..a8f38468 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalVectorStore.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalVectorStore.java @@ -15,6 +15,7 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; +import java.time.DateTimeException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; @@ -135,9 +136,7 @@ public class PersonalVectorStore { requireOwner(owner); ArrayNode vector = parseVector(vectorJson); validateDimension(vector.size()); - if (dateFrom != null && dateTo != null && dateFrom.isAfter(dateTo)) { - throw new IllegalArgumentException("PERSONAL_VECTOR_DATE_RANGE_INVALID"); - } + validateDates(dateFrom, dateTo); List scopedItems = itemIds == null ? List.of() : itemIds.stream().distinct().toList(); if (scopedItems.size() > 100 || scopedItems.stream().anyMatch(id -> id == null || id <= 0)) { throw new IllegalArgumentException("PERSONAL_VECTOR_ITEM_SCOPE_INVALID"); @@ -239,7 +238,7 @@ public class PersonalVectorStore { } return new CollectionMetadata(size.asInt(), result.path("payload_schema")); } catch (Exception ex) { - log.warn("event=personal_vector_transport_failed exception={}", ex.getClass().getSimpleName()); + log.warn("event=personal_vector_collection_metadata_invalid exception={}", ex.getClass().getSimpleName()); throw unavailable(); } } @@ -308,6 +307,19 @@ public class PersonalVectorStore { } } + private static void validateDates(LocalDate dateFrom, LocalDate dateTo) { + if (dateFrom != null && dateTo != null && dateFrom.isAfter(dateTo)) { + throw new IllegalArgumentException("PERSONAL_SEARCH_DATE_INVALID"); + } + if (dateTo != null) { + try { + dateTo.plusDays(1); + } catch (DateTimeException ex) { + throw new IllegalArgumentException("PERSONAL_SEARCH_DATE_INVALID"); + } + } + } + private void requireOwner(PersonalOwner owner) { if (owner == null || owner.tenantId() == null || owner.tenantId().isBlank() || owner.userId() <= 0) { log.warn("event=personal_vector_owner_invalid"); @@ -330,6 +342,10 @@ public class PersonalVectorStore { return transport.send(new TransportRequest(method, path, body == null ? "" : objectMapper.writeValueAsString(body), Map.copyOf(headers))); } catch (Exception ex) { + if (ex instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + log.warn("event=personal_vector_transport_failed exception={}", ex.getClass().getSimpleName()); throw unavailable(); } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalRetrievalServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalRetrievalServiceTest.java index 42c5a550..bcc29b92 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalRetrievalServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalRetrievalServiceTest.java @@ -59,8 +59,10 @@ class PersonalRetrievalServiceTest { PersonalOwner owner = new PersonalOwner("t", 1, null); assertTrue(service.search(owner, new PersonalSearchRequest("q", List.of(SearchScope.ENTERPRISE), null, null, null, 10)).isEmpty()); assertThrows(IllegalArgumentException.class, () -> service.search(owner, new PersonalSearchRequest(" ", null, null, null, null, 10))); - assertThrows(IllegalArgumentException.class, () -> service.search(owner, new PersonalSearchRequest("q", null, + assertDateInvalid(() -> service.search(owner, new PersonalSearchRequest("q", null, LocalDate.of(2026, 2, 1), LocalDate.of(2026, 1, 1), null, 10))); + assertDateInvalid(() -> service.search(owner, new PersonalSearchRequest("q", null, + null, LocalDate.MAX, null, 10))); verifyNoInteractions(jdbc); } @@ -166,4 +168,9 @@ class PersonalRetrievalServiceTest { private SearchHitResponse hit(String id, String title) { return new SearchHitResponse("PERSONAL", id, title, title + " excerpt", LocalDateTime.of(2026, 1, 1, 0, 0), 1); } + + private void assertDateInvalid(org.junit.jupiter.api.function.Executable executable) { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, executable); + assertEquals("PERSONAL_SEARCH_DATE_INVALID", error.getMessage()); + } } diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalVectorStoreTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalVectorStoreTest.java index 60a8f815..4ba82926 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalVectorStoreTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalVectorStoreTest.java @@ -144,6 +144,36 @@ class PersonalVectorStoreTest { assertEquals(2, posts.get()); } + @Test + void rejectsOverflowingAndReversedDatesBeforeHttp() { + List seen = new ArrayList<>(); + PersonalVectorStore store = fixture(seen, request -> ok("{}")); + PersonalOwner owner = new PersonalOwner("t", 1, null); + + for (List dates : List.of( + java.util.Arrays.asList(null, LocalDate.MAX), + List.of(LocalDate.of(2026, 2, 1), LocalDate.of(2026, 1, 1)))) { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> store.query(owner, "[1,2]", 5, dates.get(0), dates.get(1), List.of())); + assertEquals("PERSONAL_SEARCH_DATE_INVALID", error.getMessage()); + } + assertTrue(seen.isEmpty()); + } + + @Test + void preservesThreadInterruptWhenTransportIsInterrupted() { + PersonalVectorStore store = PersonalVectorStore.forTest(properties(), mapper, request -> { + throw new InterruptedException("stop"); + }); + try { + assertTrue(store.query(new PersonalOwner("t", 1, null), "[1,2]", 5).isEmpty()); + assertTrue(Thread.currentThread().isInterrupted()); + } finally { + Thread.interrupted(); + } + assertFalse(Thread.currentThread().isInterrupted()); + } + @Test void preservesConfiguredQdrantBasePathPrefix() { PersonalKnowledgeProperties properties = properties();