diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/domain/PersonalAssistantDto.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/domain/PersonalAssistantDto.java new file mode 100644 index 00000000..b55adf24 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/domain/PersonalAssistantDto.java @@ -0,0 +1,72 @@ +package org.dromara.aihr.personal.domain; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public final class PersonalAssistantDto { + + private PersonalAssistantDto() { + } + + public record TextItemRequest(String title, String content, LocalDateTime capturedAt, List tags) { + } + + public record UrlItemRequest(String url, String title, LocalDateTime capturedAt) { + } + + public record ItemCreatedResponse(long itemId, String status, Long duplicateOf) { + } + + public enum SearchScope { + PERSONAL, + ENTERPRISE + } + + public record AskRequest(Long sessionId, String queryText, List scope, LocalDate dateFrom, + LocalDate dateTo, List itemIds, String outputFormat) { + } + + public record CitationResponse(String domain, String sourceId, String title, String excerpt, + LocalDateTime capturedAt) { + } + + public record AskResponse(long sessionId, String answer, List citations, String model, + String promptVersion) { + } + + public record SpaceResponse(long spaceId, String status, long quotaBytes, long usedBytes, int itemCount) { + } + + public record ItemResponse(long id, String sourceType, String title, String originalUrl, String mimeType, + long sizeBytes, String status, String errorCode, String errorMessage, String summary, + List tags, LocalDateTime capturedAt, LocalDateTime parsedAt) { + } + + public record PageResponse(List rows, long total, int pageNum, int pageSize) { + } + + public record DownloadUrlResponse(String url, LocalDateTime expiresAt) { + } + + public record PersonalSearchRequest(String queryText, List scope, LocalDate dateFrom, + LocalDate dateTo, List itemIds, Integer limit) { + } + + public record SearchHitResponse(String domain, String sourceId, String title, String excerpt, + LocalDateTime capturedAt, double score) { + } + + public record PersonalSearchResponse(String queryText, List hits) { + } + + public record SessionResponse(long id, String title, String defaultScope, LocalDateTime updatedAt) { + } + + public record SessionDetailResponse(long id, String title, List messages) { + } + + public record ChatMessageResponse(long id, String role, String content, List citations, + LocalDateTime createdAt) { + } +} diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/support/PersonalOwner.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/support/PersonalOwner.java new file mode 100644 index 00000000..d5033851 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/support/PersonalOwner.java @@ -0,0 +1,10 @@ +package org.dromara.aihr.personal.support; + +public record PersonalOwner(String tenantId, long userId, String extPartyId) { + + public PersonalOwner { + if (tenantId == null || tenantId.isBlank() || userId <= 0) { + throw new IllegalStateException("个人知识空间需要有效登录身份"); + } + } +} diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/support/PersonalOwnerProvider.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/support/PersonalOwnerProvider.java new file mode 100644 index 00000000..f458c044 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/support/PersonalOwnerProvider.java @@ -0,0 +1,18 @@ +package org.dromara.aihr.personal.support; + +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.satoken.utils.LoginHelper; +import org.springframework.stereotype.Component; + +@Component +public class PersonalOwnerProvider { + + public PersonalOwner current() { + LoginUser user = LoginHelper.getLoginUser(); + if (user == null) { + throw new IllegalStateException("请先登录"); + } + long userId = user.getUserId() == null ? 0L : user.getUserId(); + return new PersonalOwner(user.getTenantId(), userId, null); + } +} diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalOwnerProviderTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalOwnerProviderTest.java new file mode 100644 index 00000000..7e99d3e3 --- /dev/null +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalOwnerProviderTest.java @@ -0,0 +1,77 @@ +package org.dromara.aihr.personal; + +import org.dromara.aihr.personal.domain.PersonalAssistantDto; +import org.dromara.aihr.personal.support.PersonalOwner; +import org.dromara.aihr.personal.support.PersonalOwnerProvider; +import org.dromara.common.core.domain.model.LoginUser; +import org.dromara.common.satoken.utils.LoginHelper; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Tag("dev") +class PersonalOwnerProviderTest { + + private static final Set FORBIDDEN_OWNER_FIELDS = Set.of( + "tenantId", "ownerUserId", "ownerExtPartyId" + ); + + @Test + void requestDtosDoNotExposeOwnerOrTenantFields() { + var requestTypes = Arrays.stream(PersonalAssistantDto.class.getDeclaredClasses()) + .filter(Class::isRecord) + .filter(type -> type.getSimpleName().endsWith("Request")) + .toList(); + + assertEquals(4, requestTypes.size()); + requestTypes.forEach(type -> { + var componentNames = Arrays.stream(type.getRecordComponents()) + .map(component -> component.getName()) + .collect(Collectors.toSet()); + assertEquals(Set.of(), componentNames.stream() + .filter(FORBIDDEN_OWNER_FIELDS::contains) + .collect(Collectors.toSet())); + }); + } + + @Test + void personalOwnerRejectsInvalidLoginIdentity() { + var blankTenant = assertThrows(IllegalStateException.class, + () -> new PersonalOwner(" ", 1L, null)); + assertEquals("个人知识空间需要有效登录身份", blankTenant.getMessage()); + + var missingUser = assertThrows(IllegalStateException.class, + () -> new PersonalOwner("000000", 0L, null)); + assertEquals("个人知识空间需要有效登录身份", missingUser.getMessage()); + } + + @Test + void providerRequiresLoginAndDerivesOwnerFromLoginUser() { + var provider = new PersonalOwnerProvider(); + try (MockedStatic loginHelper = Mockito.mockStatic(LoginHelper.class)) { + loginHelper.when(LoginHelper::getLoginUser).thenReturn(null); + var notLoggedIn = assertThrows(IllegalStateException.class, provider::current); + assertEquals("请先登录", notLoggedIn.getMessage()); + + var loginUser = new LoginUser(); + loginHelper.when(LoginHelper::getLoginUser).thenReturn(loginUser); + var invalidIdentity = assertThrows(IllegalStateException.class, provider::current); + assertEquals("个人知识空间需要有效登录身份", invalidIdentity.getMessage()); + + loginUser.setTenantId("000000"); + loginUser.setUserId(42L); + loginUser.setUsername("13800000000"); + loginHelper.when(LoginHelper::getLoginUser).thenReturn(loginUser); + + assertEquals(new PersonalOwner("000000", 42L, null), provider.current()); + } + } +}