feat(personal): derive ownership from login context

This commit is contained in:
2026-07-12 02:02:55 +08:00
parent 12a3b1cbd3
commit ed6bf4eaf9
4 changed files with 177 additions and 0 deletions
@@ -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<String> 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<SearchScope> scope, LocalDate dateFrom,
LocalDate dateTo, List<Long> itemIds, String outputFormat) {
}
public record CitationResponse(String domain, String sourceId, String title, String excerpt,
LocalDateTime capturedAt) {
}
public record AskResponse(long sessionId, String answer, List<CitationResponse> 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<String> tags, LocalDateTime capturedAt, LocalDateTime parsedAt) {
}
public record PageResponse<T>(List<T> rows, long total, int pageNum, int pageSize) {
}
public record DownloadUrlResponse(String url, LocalDateTime expiresAt) {
}
public record PersonalSearchRequest(String queryText, List<SearchScope> scope, LocalDate dateFrom,
LocalDate dateTo, List<Long> itemIds, Integer limit) {
}
public record SearchHitResponse(String domain, String sourceId, String title, String excerpt,
LocalDateTime capturedAt, double score) {
}
public record PersonalSearchResponse(String queryText, List<SearchHitResponse> hits) {
}
public record SessionResponse(long id, String title, String defaultScope, LocalDateTime updatedAt) {
}
public record SessionDetailResponse(long id, String title, List<ChatMessageResponse> messages) {
}
public record ChatMessageResponse(long id, String role, String content, List<CitationResponse> citations,
LocalDateTime createdAt) {
}
}
@@ -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("个人知识空间需要有效登录身份");
}
}
}
@@ -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);
}
}
@@ -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<String> 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> 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());
}
}
}