feat(admin): add blob share and task admin apis
This commit is contained in:
@@ -5,7 +5,11 @@ import com.yoyuzh.auth.CustomUserDetailsService;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.common.ApiResponse;
|
||||
import com.yoyuzh.common.PageResponse;
|
||||
import com.yoyuzh.files.core.FileEntityType;
|
||||
import com.yoyuzh.files.tasks.BackgroundTask;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskFailureCategory;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskType;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -61,6 +65,49 @@ public class AdminController {
|
||||
return ApiResponse.success(adminService.listFiles(page, size, query, ownerQuery));
|
||||
}
|
||||
|
||||
@GetMapping("/file-blobs")
|
||||
public ApiResponse<PageResponse<AdminFileBlobResponse>> fileBlobs(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "") String userQuery,
|
||||
@RequestParam(required = false) Long storagePolicyId,
|
||||
@RequestParam(defaultValue = "") String objectKey,
|
||||
@RequestParam(required = false) FileEntityType entityType) {
|
||||
return ApiResponse.success(adminService.listFileBlobs(page, size, userQuery, storagePolicyId, objectKey, entityType));
|
||||
}
|
||||
|
||||
@GetMapping("/shares")
|
||||
public ApiResponse<PageResponse<AdminShareResponse>> shares(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "") String userQuery,
|
||||
@RequestParam(defaultValue = "") String fileName,
|
||||
@RequestParam(defaultValue = "") String token,
|
||||
@RequestParam(required = false) Boolean passwordProtected,
|
||||
@RequestParam(required = false) Boolean expired) {
|
||||
return ApiResponse.success(adminService.listShares(page, size, userQuery, fileName, token, passwordProtected, expired));
|
||||
}
|
||||
|
||||
@DeleteMapping("/shares/{shareId}")
|
||||
public ApiResponse<Void> deleteShare(@PathVariable Long shareId) {
|
||||
adminService.deleteShare(shareId);
|
||||
return ApiResponse.success();
|
||||
}
|
||||
|
||||
@GetMapping("/tasks")
|
||||
public ApiResponse<PageResponse<AdminTaskResponse>> tasks(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "") String userQuery,
|
||||
@RequestParam(required = false) BackgroundTaskType type,
|
||||
@RequestParam(required = false) BackgroundTaskStatus status,
|
||||
@RequestParam(required = false) BackgroundTaskFailureCategory failureCategory,
|
||||
@RequestParam(required = false) AdminTaskLeaseState leaseState) {
|
||||
return ApiResponse.success(adminService.listTasks(page, size, userQuery, type, status, failureCategory, leaseState));
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}")
|
||||
public ApiResponse<AdminTaskResponse> task(@PathVariable Long taskId) {
|
||||
return ApiResponse.success(adminService.getTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/storage-policies")
|
||||
public ApiResponse<List<AdminStoragePolicyResponse>> storagePolicies() {
|
||||
return ApiResponse.success(adminService.listStoragePolicies());
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.yoyuzh.files.core.FileEntityType;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record AdminFileBlobResponse(
|
||||
Long entityId,
|
||||
Long blobId,
|
||||
String objectKey,
|
||||
FileEntityType entityType,
|
||||
Long storagePolicyId,
|
||||
Long size,
|
||||
String contentType,
|
||||
Integer referenceCount,
|
||||
long linkedStoredFileCount,
|
||||
long linkedOwnerCount,
|
||||
String sampleOwnerUsername,
|
||||
String sampleOwnerEmail,
|
||||
Long createdByUserId,
|
||||
String createdByUsername,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime blobCreatedAt,
|
||||
boolean blobMissing,
|
||||
boolean orphanRisk,
|
||||
boolean referenceMismatch
|
||||
) {
|
||||
}
|
||||
@@ -1,29 +1,42 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yoyuzh.auth.AuthTokenInvalidationService;
|
||||
import com.yoyuzh.auth.PasswordPolicy;
|
||||
import com.yoyuzh.auth.RefreshTokenService;
|
||||
import com.yoyuzh.auth.RegistrationInviteService;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRole;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.auth.RefreshTokenService;
|
||||
import com.yoyuzh.auth.UserRole;
|
||||
import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.common.ErrorCode;
|
||||
import com.yoyuzh.common.PageResponse;
|
||||
import com.yoyuzh.files.core.FileBlobRepository;
|
||||
import com.yoyuzh.config.RedisCacheNames;
|
||||
import com.yoyuzh.files.core.FileEntity;
|
||||
import com.yoyuzh.files.core.FileEntityRepository;
|
||||
import com.yoyuzh.files.core.FileEntityType;
|
||||
import com.yoyuzh.files.core.FileService;
|
||||
import com.yoyuzh.files.core.StoredFile;
|
||||
import com.yoyuzh.files.core.StoredFileEntityRepository;
|
||||
import com.yoyuzh.files.core.StoredFileRepository;
|
||||
import com.yoyuzh.files.core.FileBlobRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicy;
|
||||
import com.yoyuzh.files.policy.StoragePolicyRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicyService;
|
||||
import com.yoyuzh.files.share.FileShareLink;
|
||||
import com.yoyuzh.files.share.FileShareLinkRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTask;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskFailureCategory;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskService;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskType;
|
||||
import com.yoyuzh.transfer.OfflineTransferSessionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
@@ -34,8 +47,12 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -47,6 +64,7 @@ public class AdminService {
|
||||
private final FileService fileService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
private final AuthTokenInvalidationService authTokenInvalidationService;
|
||||
private final RegistrationInviteService registrationInviteService;
|
||||
private final OfflineTransferSessionRepository offlineTransferSessionRepository;
|
||||
private final AdminMetricsService adminMetricsService;
|
||||
@@ -54,7 +72,10 @@ public class AdminService {
|
||||
private final StoragePolicyService storagePolicyService;
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
private final StoredFileEntityRepository storedFileEntityRepository;
|
||||
private final BackgroundTaskRepository backgroundTaskRepository;
|
||||
private final BackgroundTaskService backgroundTaskService;
|
||||
private final FileShareLinkRepository fileShareLinkRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
public AdminSummaryResponse getSummary() {
|
||||
@@ -98,6 +119,92 @@ public class AdminService {
|
||||
return new PageResponse<>(items, result.getTotalElements(), page, size);
|
||||
}
|
||||
|
||||
public PageResponse<AdminFileBlobResponse> listFileBlobs(int page,
|
||||
int size,
|
||||
String userQuery,
|
||||
Long storagePolicyId,
|
||||
String objectKey,
|
||||
FileEntityType entityType) {
|
||||
Page<FileEntity> result = fileEntityRepository.searchAdminEntities(
|
||||
normalizeQuery(userQuery),
|
||||
storagePolicyId,
|
||||
normalizeQuery(objectKey),
|
||||
entityType,
|
||||
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
);
|
||||
List<AdminFileBlobResponse> items = result.getContent().stream()
|
||||
.map(this::toFileBlobResponse)
|
||||
.toList();
|
||||
return new PageResponse<>(items, result.getTotalElements(), page, size);
|
||||
}
|
||||
|
||||
public PageResponse<AdminShareResponse> listShares(int page,
|
||||
int size,
|
||||
String userQuery,
|
||||
String fileName,
|
||||
String token,
|
||||
Boolean passwordProtected,
|
||||
Boolean expired) {
|
||||
Page<FileShareLink> result = fileShareLinkRepository.searchAdminShares(
|
||||
normalizeQuery(userQuery),
|
||||
normalizeQuery(fileName),
|
||||
normalizeQuery(token),
|
||||
passwordProtected,
|
||||
expired,
|
||||
LocalDateTime.now(),
|
||||
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
);
|
||||
List<AdminShareResponse> items = result.getContent().stream()
|
||||
.map(this::toAdminShareResponse)
|
||||
.toList();
|
||||
return new PageResponse<>(items, result.getTotalElements(), page, size);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteShare(Long shareId) {
|
||||
FileShareLink shareLink = fileShareLinkRepository.findById(shareId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "share not found"));
|
||||
fileShareLinkRepository.delete(shareLink);
|
||||
}
|
||||
|
||||
public PageResponse<AdminTaskResponse> listTasks(int page,
|
||||
int size,
|
||||
String userQuery,
|
||||
BackgroundTaskType type,
|
||||
BackgroundTaskStatus status,
|
||||
BackgroundTaskFailureCategory failureCategory,
|
||||
AdminTaskLeaseState leaseState) {
|
||||
String failureCategoryPattern = failureCategory == null
|
||||
? null
|
||||
: "\"failureCategory\":\"" + failureCategory.name() + "\"";
|
||||
Page<BackgroundTask> result = backgroundTaskRepository.searchAdminTasks(
|
||||
normalizeQuery(userQuery),
|
||||
type,
|
||||
status,
|
||||
failureCategoryPattern,
|
||||
leaseState == null ? null : leaseState.name(),
|
||||
LocalDateTime.now(),
|
||||
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
);
|
||||
Map<Long, User> ownerById = userRepository.findAllById(result.getContent().stream()
|
||||
.map(BackgroundTask::getUserId)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(User::getId, user -> user));
|
||||
List<AdminTaskResponse> items = result.getContent().stream()
|
||||
.map(task -> toAdminTaskResponse(task, ownerById.get(task.getUserId())))
|
||||
.toList();
|
||||
return new PageResponse<>(items, result.getTotalElements(), page, size);
|
||||
}
|
||||
|
||||
public AdminTaskResponse getTask(Long taskId) {
|
||||
BackgroundTask task = backgroundTaskRepository.findById(taskId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "task not found"));
|
||||
User owner = userRepository.findById(task.getUserId()).orElse(null);
|
||||
return toAdminTaskResponse(task, owner);
|
||||
}
|
||||
|
||||
@Cacheable(cacheNames = RedisCacheNames.STORAGE_POLICIES, key = "'all'")
|
||||
public List<AdminStoragePolicyResponse> listStoragePolicies() {
|
||||
return storagePolicyRepository.findAll(Sort.by(Sort.Direction.DESC, "defaultPolicy")
|
||||
.and(Sort.by(Sort.Direction.DESC, "enabled"))
|
||||
@@ -108,6 +215,7 @@ public class AdminService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = RedisCacheNames.STORAGE_POLICIES, allEntries = true)
|
||||
public AdminStoragePolicyResponse createStoragePolicy(AdminStoragePolicyUpsertRequest request) {
|
||||
StoragePolicy policy = new StoragePolicy();
|
||||
policy.setDefaultPolicy(false);
|
||||
@@ -116,6 +224,7 @@ public class AdminService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = RedisCacheNames.STORAGE_POLICIES, allEntries = true)
|
||||
public AdminStoragePolicyResponse updateStoragePolicy(Long policyId, AdminStoragePolicyUpsertRequest request) {
|
||||
StoragePolicy policy = getRequiredStoragePolicy(policyId);
|
||||
applyStoragePolicyUpsert(policy, request);
|
||||
@@ -123,10 +232,11 @@ public class AdminService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@CacheEvict(cacheNames = RedisCacheNames.STORAGE_POLICIES, allEntries = true)
|
||||
public AdminStoragePolicyResponse updateStoragePolicyStatus(Long policyId, boolean enabled) {
|
||||
StoragePolicy policy = getRequiredStoragePolicy(policyId);
|
||||
if (policy.isDefaultPolicy() && !enabled) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "默认存储策略不能停用");
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "榛樿瀛樺偍绛栫暐涓嶈兘鍋滅敤");
|
||||
}
|
||||
policy.setEnabled(enabled);
|
||||
return toStoragePolicyResponse(storagePolicyRepository.save(policy));
|
||||
@@ -137,10 +247,10 @@ public class AdminService {
|
||||
StoragePolicy sourcePolicy = getRequiredStoragePolicy(request.sourcePolicyId());
|
||||
StoragePolicy targetPolicy = getRequiredStoragePolicy(request.targetPolicyId());
|
||||
if (sourcePolicy.getId().equals(targetPolicy.getId())) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "源存储策略和目标存储策略不能相同");
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "婧愬瓨鍌ㄧ瓥鐣ュ拰鐩爣瀛樺偍绛栫暐涓嶈兘鐩稿悓");
|
||||
}
|
||||
if (!targetPolicy.isEnabled()) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "目标存储策略必须处于启用状态");
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "target storage policy must be enabled");
|
||||
}
|
||||
|
||||
long candidateEntityCount = fileEntityRepository.countByStoragePolicyIdAndEntityType(
|
||||
@@ -152,7 +262,7 @@ public class AdminService {
|
||||
FileEntityType.VERSION
|
||||
);
|
||||
|
||||
java.util.Map<String, Object> state = new java.util.LinkedHashMap<>();
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
state.put("sourcePolicyId", sourcePolicy.getId());
|
||||
state.put("sourcePolicyName", sourcePolicy.getName());
|
||||
state.put("targetPolicyId", targetPolicy.getId());
|
||||
@@ -164,7 +274,7 @@ public class AdminService {
|
||||
state.put("entityType", FileEntityType.VERSION.name());
|
||||
state.put("message", "storage policy migration skeleton queued; worker will validate and recount candidates without moving object data");
|
||||
|
||||
java.util.Map<String, Object> privateState = new java.util.LinkedHashMap<>(state);
|
||||
Map<String, Object> privateState = new LinkedHashMap<>(state);
|
||||
privateState.put("taskType", BackgroundTaskType.STORAGE_POLICY_MIGRATION.name());
|
||||
|
||||
return backgroundTaskService.createQueuedTask(
|
||||
@@ -179,7 +289,7 @@ public class AdminService {
|
||||
@Transactional
|
||||
public void deleteFile(Long fileId) {
|
||||
StoredFile storedFile = storedFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在"));
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "file not found"));
|
||||
fileService.delete(storedFile.getUser(), fileId);
|
||||
}
|
||||
|
||||
@@ -194,6 +304,7 @@ public class AdminService {
|
||||
public AdminUserResponse updateUserBanned(Long userId, boolean banned) {
|
||||
User user = getRequiredUser(userId);
|
||||
user.setBanned(banned);
|
||||
authTokenInvalidationService.revokeAccessTokensForUser(user.getId());
|
||||
user.setActiveSessionId(UUID.randomUUID().toString());
|
||||
user.setDesktopActiveSessionId(UUID.randomUUID().toString());
|
||||
user.setMobileActiveSessionId(UUID.randomUUID().toString());
|
||||
@@ -208,6 +319,7 @@ public class AdminService {
|
||||
}
|
||||
User user = getRequiredUser(userId);
|
||||
user.setPasswordHash(passwordEncoder.encode(newPassword));
|
||||
authTokenInvalidationService.revokeAccessTokensForUser(user.getId());
|
||||
user.setActiveSessionId(UUID.randomUUID().toString());
|
||||
user.setDesktopActiveSessionId(UUID.randomUUID().toString());
|
||||
user.setMobileActiveSessionId(UUID.randomUUID().toString());
|
||||
@@ -293,9 +405,93 @@ public class AdminService {
|
||||
);
|
||||
}
|
||||
|
||||
private AdminFileBlobResponse toFileBlobResponse(FileEntity entity) {
|
||||
var blob = fileBlobRepository.findByObjectKey(entity.getObjectKey()).orElse(null);
|
||||
long linkedStoredFileCount = storedFileEntityRepository.countByFileEntityId(entity.getId());
|
||||
long linkedOwnerCount = storedFileEntityRepository.countDistinctOwnersByFileEntityId(entity.getId());
|
||||
return new AdminFileBlobResponse(
|
||||
entity.getId(),
|
||||
blob == null ? null : blob.getId(),
|
||||
entity.getObjectKey(),
|
||||
entity.getEntityType(),
|
||||
entity.getStoragePolicyId(),
|
||||
entity.getSize(),
|
||||
StringUtils.hasText(entity.getContentType()) ? entity.getContentType() : blob == null ? null : blob.getContentType(),
|
||||
entity.getReferenceCount(),
|
||||
linkedStoredFileCount,
|
||||
linkedOwnerCount,
|
||||
storedFileEntityRepository.findSampleOwnerUsernameByFileEntityId(entity.getId()),
|
||||
storedFileEntityRepository.findSampleOwnerEmailByFileEntityId(entity.getId()),
|
||||
entity.getCreatedBy() == null ? null : entity.getCreatedBy().getId(),
|
||||
entity.getCreatedBy() == null ? null : entity.getCreatedBy().getUsername(),
|
||||
entity.getCreatedAt(),
|
||||
blob == null ? null : blob.getCreatedAt(),
|
||||
blob == null,
|
||||
linkedStoredFileCount == 0,
|
||||
entity.getReferenceCount() == null || entity.getReferenceCount() != linkedStoredFileCount
|
||||
);
|
||||
}
|
||||
|
||||
private AdminShareResponse toAdminShareResponse(FileShareLink shareLink) {
|
||||
StoredFile file = shareLink.getFile();
|
||||
User owner = shareLink.getOwner();
|
||||
boolean expired = shareLink.getExpiresAt() != null && shareLink.getExpiresAt().isBefore(LocalDateTime.now());
|
||||
return new AdminShareResponse(
|
||||
shareLink.getId(),
|
||||
shareLink.getToken(),
|
||||
shareLink.getShareNameOrDefault(),
|
||||
shareLink.hasPassword(),
|
||||
expired,
|
||||
shareLink.getCreatedAt(),
|
||||
shareLink.getExpiresAt(),
|
||||
shareLink.getMaxDownloads(),
|
||||
shareLink.getDownloadCountOrZero(),
|
||||
shareLink.getViewCountOrZero(),
|
||||
shareLink.isAllowImportEnabled(),
|
||||
shareLink.isAllowDownloadEnabled(),
|
||||
owner.getId(),
|
||||
owner.getUsername(),
|
||||
owner.getEmail(),
|
||||
file.getId(),
|
||||
file.getFilename(),
|
||||
file.getPath(),
|
||||
file.getContentType(),
|
||||
file.getSize(),
|
||||
file.isDirectory()
|
||||
);
|
||||
}
|
||||
|
||||
private AdminTaskResponse toAdminTaskResponse(BackgroundTask task, User owner) {
|
||||
Map<String, Object> state = parseState(task.getPublicStateJson());
|
||||
return new AdminTaskResponse(
|
||||
task.getId(),
|
||||
task.getType(),
|
||||
task.getStatus(),
|
||||
task.getUserId(),
|
||||
owner == null ? null : owner.getUsername(),
|
||||
owner == null ? null : owner.getEmail(),
|
||||
task.getPublicStateJson(),
|
||||
task.getCorrelationId(),
|
||||
task.getErrorMessage(),
|
||||
task.getAttemptCount(),
|
||||
task.getMaxAttempts(),
|
||||
task.getNextRunAt(),
|
||||
task.getLeaseOwner(),
|
||||
task.getLeaseExpiresAt(),
|
||||
task.getHeartbeatAt(),
|
||||
task.getCreatedAt(),
|
||||
task.getUpdatedAt(),
|
||||
task.getFinishedAt(),
|
||||
readStringState(state, "failureCategory"),
|
||||
readBooleanState(state, "retryScheduled"),
|
||||
readStringState(state, "workerOwner"),
|
||||
resolveLeaseState(task)
|
||||
);
|
||||
}
|
||||
|
||||
private void applyStoragePolicyUpsert(StoragePolicy policy, AdminStoragePolicyUpsertRequest request) {
|
||||
if (policy.isDefaultPolicy() && !request.enabled()) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "默认存储策略不能停用");
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "榛樿瀛樺偍绛栫暐涓嶈兘鍋滅敤");
|
||||
}
|
||||
validateStoragePolicyRequest(request);
|
||||
policy.setName(request.name().trim());
|
||||
@@ -313,12 +509,12 @@ public class AdminService {
|
||||
|
||||
private User getRequiredUser(Long userId) {
|
||||
return userRepository.findById(userId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "用户不存在"));
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "user not found"));
|
||||
}
|
||||
|
||||
private StoragePolicy getRequiredStoragePolicy(Long policyId) {
|
||||
return storagePolicyRepository.findById(policyId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "存储策略不存在"));
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.UNKNOWN, "storage policy not found"));
|
||||
}
|
||||
|
||||
private String normalizeQuery(String query) {
|
||||
@@ -342,14 +538,51 @@ public class AdminService {
|
||||
return prefix.trim();
|
||||
}
|
||||
|
||||
private Map<String, Object> parseState(String json) {
|
||||
if (!StringUtils.hasText(json)) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<LinkedHashMap<String, Object>>() {
|
||||
});
|
||||
} catch (JsonProcessingException ex) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private String readStringState(Map<String, Object> state, String key) {
|
||||
Object value = state.get(key);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Boolean readBooleanState(Map<String, Object> state, String key) {
|
||||
Object value = state.get(key);
|
||||
if (value instanceof Boolean boolValue) {
|
||||
return boolValue;
|
||||
}
|
||||
if (value instanceof String stringValue) {
|
||||
return Boolean.parseBoolean(stringValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private AdminTaskLeaseState resolveLeaseState(BackgroundTask task) {
|
||||
if (!StringUtils.hasText(task.getLeaseOwner()) || task.getLeaseExpiresAt() == null) {
|
||||
return AdminTaskLeaseState.NONE;
|
||||
}
|
||||
return task.getLeaseExpiresAt().isBefore(LocalDateTime.now())
|
||||
? AdminTaskLeaseState.EXPIRED
|
||||
: AdminTaskLeaseState.ACTIVE;
|
||||
}
|
||||
|
||||
private void validateStoragePolicyRequest(AdminStoragePolicyUpsertRequest request) {
|
||||
if (request.type() == com.yoyuzh.files.policy.StoragePolicyType.LOCAL
|
||||
&& request.credentialMode() != com.yoyuzh.files.policy.StoragePolicyCredentialMode.NONE) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "本地存储策略必须使用 NONE 凭证模式");
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "鏈湴瀛樺偍绛栫暐蹇呴』浣跨敤 NONE 鍑瘉妯″紡");
|
||||
}
|
||||
if (request.type() == com.yoyuzh.files.policy.StoragePolicyType.S3_COMPATIBLE
|
||||
&& !StringUtils.hasText(request.bucketName())) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "S3 存储策略必须提供 bucketName");
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "S3 瀛樺偍绛栫暐蹇呴』鎻愪緵 bucketName");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record AdminShareResponse(
|
||||
Long id,
|
||||
String token,
|
||||
String shareName,
|
||||
boolean passwordProtected,
|
||||
boolean expired,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime expiresAt,
|
||||
Integer maxDownloads,
|
||||
long downloadCount,
|
||||
long viewCount,
|
||||
boolean allowImport,
|
||||
boolean allowDownload,
|
||||
Long ownerId,
|
||||
String ownerUsername,
|
||||
String ownerEmail,
|
||||
Long fileId,
|
||||
String fileName,
|
||||
String filePath,
|
||||
String fileContentType,
|
||||
long fileSize,
|
||||
boolean directory
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
public enum AdminTaskLeaseState {
|
||||
ACTIVE,
|
||||
EXPIRED,
|
||||
NONE
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskType;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record AdminTaskResponse(
|
||||
Long id,
|
||||
BackgroundTaskType type,
|
||||
BackgroundTaskStatus status,
|
||||
Long userId,
|
||||
String ownerUsername,
|
||||
String ownerEmail,
|
||||
String publicStateJson,
|
||||
String correlationId,
|
||||
String errorMessage,
|
||||
Integer attemptCount,
|
||||
Integer maxAttempts,
|
||||
LocalDateTime nextRunAt,
|
||||
String leaseOwner,
|
||||
LocalDateTime leaseExpiresAt,
|
||||
LocalDateTime heartbeatAt,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt,
|
||||
LocalDateTime finishedAt,
|
||||
String failureCategory,
|
||||
Boolean retryScheduled,
|
||||
String workerOwner,
|
||||
AdminTaskLeaseState leaseState
|
||||
) {
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.yoyuzh.files.core;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -12,4 +17,28 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, Long> {
|
||||
long countByStoragePolicyIdAndEntityType(Long storagePolicyId, FileEntityType entityType);
|
||||
|
||||
List<FileEntity> findByStoragePolicyIdAndEntityTypeOrderByIdAsc(Long storagePolicyId, FileEntityType entityType);
|
||||
|
||||
@EntityGraph(attributePaths = {"createdBy"})
|
||||
@Query("""
|
||||
select entity from FileEntity entity
|
||||
where (:storagePolicyId is null or entity.storagePolicyId = :storagePolicyId)
|
||||
and (:entityType is null or entity.entityType = :entityType)
|
||||
and (:objectKey is null or :objectKey = ''
|
||||
or lower(entity.objectKey) like lower(concat('%', :objectKey, '%')))
|
||||
and (:userQuery is null or :userQuery = '' or exists (
|
||||
select 1 from StoredFileEntity relation
|
||||
join relation.storedFile storedFile
|
||||
join storedFile.user owner
|
||||
where relation.fileEntity = entity
|
||||
and (
|
||||
lower(owner.username) like lower(concat('%', :userQuery, '%'))
|
||||
or lower(owner.email) like lower(concat('%', :userQuery, '%'))
|
||||
)
|
||||
))
|
||||
""")
|
||||
Page<FileEntity> searchAdminEntities(@Param("userQuery") String userQuery,
|
||||
@Param("storagePolicyId") Long storagePolicyId,
|
||||
@Param("objectKey") String objectKey,
|
||||
@Param("entityType") FileEntityType entityType,
|
||||
Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -14,4 +14,31 @@ public interface StoredFileEntityRepository extends JpaRepository<StoredFileEnti
|
||||
""")
|
||||
long countDistinctStoredFilesByStoragePolicyIdAndEntityType(@Param("storagePolicyId") Long storagePolicyId,
|
||||
@Param("entityType") FileEntityType entityType);
|
||||
|
||||
long countByFileEntityId(Long fileEntityId);
|
||||
|
||||
@Query("""
|
||||
select count(distinct relation.storedFile.user.id)
|
||||
from StoredFileEntity relation
|
||||
where relation.fileEntity.id = :fileEntityId
|
||||
""")
|
||||
long countDistinctOwnersByFileEntityId(@Param("fileEntityId") Long fileEntityId);
|
||||
|
||||
@Query("""
|
||||
select min(owner.username)
|
||||
from StoredFileEntity relation
|
||||
join relation.storedFile storedFile
|
||||
join storedFile.user owner
|
||||
where relation.fileEntity.id = :fileEntityId
|
||||
""")
|
||||
String findSampleOwnerUsernameByFileEntityId(@Param("fileEntityId") Long fileEntityId);
|
||||
|
||||
@Query("""
|
||||
select min(owner.email)
|
||||
from StoredFileEntity relation
|
||||
join relation.storedFile storedFile
|
||||
join storedFile.user owner
|
||||
where relation.fileEntity.id = :fileEntityId
|
||||
""")
|
||||
String findSampleOwnerEmailByFileEntityId(@Param("fileEntityId") Long fileEntityId);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -17,4 +21,31 @@ public interface FileShareLinkRepository extends JpaRepository<FileShareLink, Lo
|
||||
|
||||
@EntityGraph(attributePaths = {"owner", "file", "file.user", "file.blob"})
|
||||
Optional<FileShareLink> findByIdAndOwnerId(Long id, Long ownerId);
|
||||
|
||||
@EntityGraph(attributePaths = {"owner", "file", "file.user", "file.primaryEntity", "file.blob"})
|
||||
@Query("""
|
||||
select share from FileShareLink share
|
||||
join share.owner owner
|
||||
join share.file file
|
||||
where (:userQuery is null or :userQuery = ''
|
||||
or lower(owner.username) like lower(concat('%', :userQuery, '%'))
|
||||
or lower(owner.email) like lower(concat('%', :userQuery, '%')))
|
||||
and (:fileName is null or :fileName = ''
|
||||
or lower(file.filename) like lower(concat('%', :fileName, '%')))
|
||||
and (:token is null or :token = ''
|
||||
or lower(share.token) like lower(concat('%', :token, '%')))
|
||||
and (:passwordProtected is null
|
||||
or (:passwordProtected = true and share.passwordHash is not null and share.passwordHash <> '')
|
||||
or (:passwordProtected = false and (share.passwordHash is null or share.passwordHash = '')))
|
||||
and (:expired is null
|
||||
or (:expired = true and share.expiresAt is not null and share.expiresAt < :now)
|
||||
or (:expired = false and (share.expiresAt is null or share.expiresAt >= :now)))
|
||||
""")
|
||||
Page<FileShareLink> searchAdminShares(@Param("userQuery") String userQuery,
|
||||
@Param("fileName") String fileName,
|
||||
@Param("token") String token,
|
||||
@Param("passwordProtected") Boolean passwordProtected,
|
||||
@Param("expired") Boolean expired,
|
||||
@Param("now") LocalDateTime now,
|
||||
Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,36 @@ public interface BackgroundTaskRepository extends JpaRepository<BackgroundTask,
|
||||
|
||||
Page<BackgroundTask> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||
|
||||
@Query("""
|
||||
select task from BackgroundTask task
|
||||
where (:userQuery is null or :userQuery = '' or exists (
|
||||
select 1 from User owner
|
||||
where owner.id = task.userId
|
||||
and (
|
||||
lower(owner.username) like lower(concat('%', :userQuery, '%'))
|
||||
or lower(owner.email) like lower(concat('%', :userQuery, '%'))
|
||||
)
|
||||
))
|
||||
and (:type is null or task.type = :type)
|
||||
and (:status is null or task.status = :status)
|
||||
and (:failureCategoryPattern is null or lower(task.publicStateJson) like lower(concat('%', :failureCategoryPattern, '%')))
|
||||
and (:leaseState is null
|
||||
or (:leaseState = 'ACTIVE' and task.leaseOwner is not null and task.leaseExpiresAt is not null and task.leaseExpiresAt > :now)
|
||||
or (:leaseState = 'EXPIRED' and task.leaseOwner is not null and task.leaseExpiresAt is not null and task.leaseExpiresAt <= :now)
|
||||
or (:leaseState = 'NONE' and (task.leaseOwner is null or task.leaseExpiresAt is null)))
|
||||
""")
|
||||
Page<BackgroundTask> searchAdminTasks(@Param("userQuery") String userQuery,
|
||||
@Param("type") BackgroundTaskType type,
|
||||
@Param("status") BackgroundTaskStatus status,
|
||||
@Param("failureCategoryPattern") String failureCategoryPattern,
|
||||
@Param("leaseState") String leaseState,
|
||||
@Param("now") LocalDateTime now,
|
||||
Pageable pageable);
|
||||
|
||||
Optional<BackgroundTask> findByIdAndUserId(Long id, Long userId);
|
||||
|
||||
boolean existsByCorrelationId(String correlationId);
|
||||
|
||||
List<BackgroundTask> findByStatusOrderByCreatedAtAsc(BackgroundTaskStatus status, Pageable pageable);
|
||||
|
||||
List<BackgroundTask> findByStatusOrderByUpdatedAtAsc(BackgroundTaskStatus status);
|
||||
|
||||
@@ -2,15 +2,28 @@ package com.yoyuzh.admin;
|
||||
|
||||
import com.yoyuzh.PortalBackendApplication;
|
||||
import com.yoyuzh.admin.AdminMetricsStateRepository;
|
||||
import com.yoyuzh.auth.RefreshTokenRepository;
|
||||
import com.yoyuzh.auth.User;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.files.core.FileBlob;
|
||||
import com.yoyuzh.files.core.FileBlobRepository;
|
||||
import com.yoyuzh.files.core.FileEntity;
|
||||
import com.yoyuzh.files.core.FileEntityRepository;
|
||||
import com.yoyuzh.files.core.FileEntityType;
|
||||
import com.yoyuzh.files.core.StoredFile;
|
||||
import com.yoyuzh.files.core.StoredFileEntity;
|
||||
import com.yoyuzh.files.core.StoredFileEntityRepository;
|
||||
import com.yoyuzh.files.core.StoredFileRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicy;
|
||||
import com.yoyuzh.files.policy.StoragePolicyRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicyType;
|
||||
import com.yoyuzh.files.share.FileShareLink;
|
||||
import com.yoyuzh.files.share.FileShareLinkRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTask;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskFailureCategory;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskType;
|
||||
import com.yoyuzh.transfer.OfflineTransferSessionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -25,6 +38,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
@@ -64,6 +78,16 @@ class AdminControllerIntegrationTest {
|
||||
@Autowired
|
||||
private FileBlobRepository fileBlobRepository;
|
||||
@Autowired
|
||||
private FileEntityRepository fileEntityRepository;
|
||||
@Autowired
|
||||
private StoredFileEntityRepository storedFileEntityRepository;
|
||||
@Autowired
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
@Autowired
|
||||
private BackgroundTaskRepository backgroundTaskRepository;
|
||||
@Autowired
|
||||
private RefreshTokenRepository refreshTokenRepository;
|
||||
@Autowired
|
||||
private OfflineTransferSessionRepository offlineTransferSessionRepository;
|
||||
@Autowired
|
||||
private AdminMetricsStateRepository adminMetricsStateRepository;
|
||||
@@ -79,12 +103,21 @@ class AdminControllerIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
backgroundTaskRepository.deleteAll();
|
||||
fileShareLinkRepository.deleteAll();
|
||||
storedFileEntityRepository.deleteAll();
|
||||
offlineTransferSessionRepository.deleteAll();
|
||||
refreshTokenRepository.deleteAll();
|
||||
storedFileRepository.deleteAll();
|
||||
fileEntityRepository.deleteAll();
|
||||
fileBlobRepository.deleteAll();
|
||||
userRepository.deleteAll();
|
||||
adminMetricsStateRepository.deleteAll();
|
||||
|
||||
Long defaultPolicyId = storagePolicyRepository.findFirstByDefaultPolicyTrueOrderByIdAsc()
|
||||
.map(StoragePolicy::getId)
|
||||
.orElse(null);
|
||||
|
||||
portalUser = new User();
|
||||
portalUser.setUsername("alice");
|
||||
portalUser.setEmail("alice@example.com");
|
||||
@@ -102,6 +135,16 @@ class AdminControllerIntegrationTest {
|
||||
secondaryUser = userRepository.save(secondaryUser);
|
||||
|
||||
FileBlob reportBlob = createBlob("blobs/admin-report", "application/pdf", 1024L);
|
||||
FileEntity reportEntity = createEntity(
|
||||
"blobs/admin-report",
|
||||
FileEntityType.VERSION,
|
||||
defaultPolicyId,
|
||||
1024L,
|
||||
"application/pdf",
|
||||
1,
|
||||
portalUser,
|
||||
LocalDateTime.now().minusMinutes(10)
|
||||
);
|
||||
storedFile = new StoredFile();
|
||||
storedFile.setUser(portalUser);
|
||||
storedFile.setFilename("report.pdf");
|
||||
@@ -110,10 +153,22 @@ class AdminControllerIntegrationTest {
|
||||
storedFile.setSize(1024L);
|
||||
storedFile.setDirectory(false);
|
||||
storedFile.setBlob(reportBlob);
|
||||
storedFile.setPrimaryEntity(reportEntity);
|
||||
storedFile.setCreatedAt(LocalDateTime.now());
|
||||
storedFile = storedFileRepository.save(storedFile);
|
||||
createRelation(storedFile, reportEntity, "PRIMARY");
|
||||
|
||||
FileBlob notesBlob = createBlob("blobs/admin-notes", "text/plain", 256L);
|
||||
FileEntity notesEntity = createEntity(
|
||||
"blobs/admin-notes",
|
||||
FileEntityType.VERSION,
|
||||
defaultPolicyId,
|
||||
256L,
|
||||
"text/plain",
|
||||
1,
|
||||
secondaryUser,
|
||||
LocalDateTime.now().minusHours(3)
|
||||
);
|
||||
secondaryFile = new StoredFile();
|
||||
secondaryFile.setUser(secondaryUser);
|
||||
secondaryFile.setFilename("notes.txt");
|
||||
@@ -122,8 +177,10 @@ class AdminControllerIntegrationTest {
|
||||
secondaryFile.setSize(256L);
|
||||
secondaryFile.setDirectory(false);
|
||||
secondaryFile.setBlob(notesBlob);
|
||||
secondaryFile.setPrimaryEntity(notesEntity);
|
||||
secondaryFile.setCreatedAt(LocalDateTime.now().minusHours(2));
|
||||
secondaryFile = storedFileRepository.save(secondaryFile);
|
||||
createRelation(secondaryFile, notesEntity, "PRIMARY");
|
||||
}
|
||||
|
||||
private FileBlob createBlob(String objectKey, String contentType, long size) {
|
||||
@@ -135,6 +192,35 @@ class AdminControllerIntegrationTest {
|
||||
return fileBlobRepository.save(blob);
|
||||
}
|
||||
|
||||
private FileEntity createEntity(String objectKey,
|
||||
FileEntityType entityType,
|
||||
Long storagePolicyId,
|
||||
long size,
|
||||
String contentType,
|
||||
int referenceCount,
|
||||
User createdBy,
|
||||
LocalDateTime createdAt) {
|
||||
FileEntity entity = new FileEntity();
|
||||
entity.setObjectKey(objectKey);
|
||||
entity.setEntityType(entityType);
|
||||
entity.setStoragePolicyId(storagePolicyId);
|
||||
entity.setSize(size);
|
||||
entity.setContentType(contentType);
|
||||
entity.setReferenceCount(referenceCount);
|
||||
entity.setCreatedBy(createdBy);
|
||||
entity.setCreatedAt(createdAt);
|
||||
return fileEntityRepository.save(entity);
|
||||
}
|
||||
|
||||
private StoredFileEntity createRelation(StoredFile storedFile, FileEntity entity, String role) {
|
||||
StoredFileEntity relation = new StoredFileEntity();
|
||||
relation.setStoredFile(storedFile);
|
||||
relation.setFileEntity(entity);
|
||||
relation.setEntityRole(role);
|
||||
relation.setCreatedAt(LocalDateTime.now());
|
||||
return storedFileEntityRepository.save(relation);
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListUsersAndSummary() throws Exception {
|
||||
@@ -325,6 +411,121 @@ class AdminControllerIntegrationTest {
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListFileBlobsWithRiskSignals() throws Exception {
|
||||
Long defaultPolicyId = storagePolicyRepository.findFirstByDefaultPolicyTrueOrderByIdAsc()
|
||||
.map(StoragePolicy::getId)
|
||||
.orElse(null);
|
||||
createEntity(
|
||||
"blobs/missing-preview",
|
||||
FileEntityType.THUMBNAIL,
|
||||
defaultPolicyId,
|
||||
4096L,
|
||||
"image/webp",
|
||||
2,
|
||||
secondaryUser,
|
||||
LocalDateTime.now().minusMinutes(1)
|
||||
);
|
||||
|
||||
mockMvc.perform(get("/api/admin/file-blobs?page=0&size=10&objectKey=missing-preview&entityType=THUMBNAIL"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].objectKey").value("blobs/missing-preview"))
|
||||
.andExpect(jsonPath("$.data.items[0].entityType").value("THUMBNAIL"))
|
||||
.andExpect(jsonPath("$.data.items[0].createdByUsername").value("bob"))
|
||||
.andExpect(jsonPath("$.data.items[0].blobMissing").value(true))
|
||||
.andExpect(jsonPath("$.data.items[0].orphanRisk").value(true))
|
||||
.andExpect(jsonPath("$.data.items[0].referenceMismatch").value(true))
|
||||
.andExpect(jsonPath("$.data.items[0].linkedStoredFileCount").value(0))
|
||||
.andExpect(jsonPath("$.data.items[0].linkedOwnerCount").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListAndDeleteShares() throws Exception {
|
||||
FileShareLink share = new FileShareLink();
|
||||
share.setOwner(secondaryUser);
|
||||
share.setFile(secondaryFile);
|
||||
share.setToken("secret-token");
|
||||
share.setShareName("Bob Private Notes");
|
||||
share.setPasswordHash("hashed-secret");
|
||||
share.setExpiresAt(LocalDateTime.now().minusHours(1));
|
||||
share.setMaxDownloads(5);
|
||||
share.setDownloadCount(2L);
|
||||
share.setViewCount(4L);
|
||||
share.setAllowImport(false);
|
||||
share.setAllowDownload(true);
|
||||
share.setCreatedAt(LocalDateTime.now().minusMinutes(5));
|
||||
share = fileShareLinkRepository.save(share);
|
||||
|
||||
mockMvc.perform(get("/api/admin/shares?page=0&size=10&userQuery=bob&fileName=notes&token=secret&passwordProtected=true&expired=true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].id").value(share.getId()))
|
||||
.andExpect(jsonPath("$.data.items[0].shareName").value("Bob Private Notes"))
|
||||
.andExpect(jsonPath("$.data.items[0].ownerUsername").value("bob"))
|
||||
.andExpect(jsonPath("$.data.items[0].fileName").value("notes.txt"))
|
||||
.andExpect(jsonPath("$.data.items[0].passwordProtected").value(true))
|
||||
.andExpect(jsonPath("$.data.items[0].expired").value(true))
|
||||
.andExpect(jsonPath("$.data.items[0].allowImport").value(false))
|
||||
.andExpect(jsonPath("$.data.items[0].allowDownload").value(true));
|
||||
|
||||
mockMvc.perform(delete("/api/admin/shares/{shareId}", share.getId()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
assertThat(fileShareLinkRepository.findById(share.getId())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListAndInspectTasks() throws Exception {
|
||||
BackgroundTask task = new BackgroundTask();
|
||||
task.setType(BackgroundTaskType.MEDIA_META);
|
||||
task.setStatus(BackgroundTaskStatus.RUNNING);
|
||||
task.setUserId(portalUser.getId());
|
||||
task.setPublicStateJson("""
|
||||
{"failureCategory":"TRANSIENT_INFRASTRUCTURE","retryScheduled":true,"workerOwner":"media-worker-1"}
|
||||
""");
|
||||
task.setPrivateStateJson("""
|
||||
{"internal":"secret"}
|
||||
""");
|
||||
task.setCorrelationId("task-media-meta-1");
|
||||
task.setAttemptCount(1);
|
||||
task.setMaxAttempts(3);
|
||||
task.setLeaseOwner("worker-a");
|
||||
task.setLeaseExpiresAt(LocalDateTime.now().plusMinutes(5));
|
||||
task.setHeartbeatAt(LocalDateTime.now().minusSeconds(30));
|
||||
task.setCreatedAt(LocalDateTime.now().minusMinutes(2));
|
||||
task.setUpdatedAt(LocalDateTime.now().minusSeconds(20));
|
||||
task = backgroundTaskRepository.save(task);
|
||||
|
||||
mockMvc.perform(get("/api/admin/tasks?page=0&size=10&userQuery=alice&type=MEDIA_META&status=RUNNING&failureCategory=TRANSIENT_INFRASTRUCTURE&leaseState=ACTIVE"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].id").value(task.getId()))
|
||||
.andExpect(jsonPath("$.data.items[0].type").value("MEDIA_META"))
|
||||
.andExpect(jsonPath("$.data.items[0].status").value("RUNNING"))
|
||||
.andExpect(jsonPath("$.data.items[0].ownerUsername").value("alice"))
|
||||
.andExpect(jsonPath("$.data.items[0].failureCategory").value("TRANSIENT_INFRASTRUCTURE"))
|
||||
.andExpect(jsonPath("$.data.items[0].retryScheduled").value(true))
|
||||
.andExpect(jsonPath("$.data.items[0].workerOwner").value("media-worker-1"))
|
||||
.andExpect(jsonPath("$.data.items[0].leaseState").value("ACTIVE"));
|
||||
|
||||
mockMvc.perform(get("/api/admin/tasks/{taskId}", task.getId()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.id").value(task.getId()))
|
||||
.andExpect(jsonPath("$.data.correlationId").value("task-media-meta-1"))
|
||||
.andExpect(jsonPath("$.data.ownerEmail").value("alice@example.com"))
|
||||
.andExpect(jsonPath("$.data.failureCategory").value("TRANSIENT_INFRASTRUCTURE"))
|
||||
.andExpect(jsonPath("$.data.leaseState").value("ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "admin")
|
||||
void shouldAllowConfiguredAdminToListStoragePolicies() throws Exception {
|
||||
@@ -442,8 +643,12 @@ class AdminControllerIntegrationTest {
|
||||
"enabled": false
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.msg").value("默认存储策略不能停用"));
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
assertThat(storagePolicyRepository.findById(defaultPolicy.getId()))
|
||||
.get()
|
||||
.extracting(StoragePolicy::isEnabled)
|
||||
.isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yoyuzh.auth.AuthTokenInvalidationService;
|
||||
import com.yoyuzh.auth.RefreshTokenService;
|
||||
import com.yoyuzh.auth.RegistrationInviteService;
|
||||
import com.yoyuzh.auth.UserRepository;
|
||||
import com.yoyuzh.config.RedisCacheNames;
|
||||
import com.yoyuzh.files.core.FileBlobRepository;
|
||||
import com.yoyuzh.files.core.FileEntityRepository;
|
||||
import com.yoyuzh.files.core.FileService;
|
||||
import com.yoyuzh.files.core.StoredFileEntityRepository;
|
||||
import com.yoyuzh.files.core.StoredFileRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicy;
|
||||
import com.yoyuzh.files.policy.StoragePolicyCapabilities;
|
||||
import com.yoyuzh.files.policy.StoragePolicyCredentialMode;
|
||||
import com.yoyuzh.files.policy.StoragePolicyRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicyService;
|
||||
import com.yoyuzh.files.policy.StoragePolicyType;
|
||||
import com.yoyuzh.files.share.FileShareLinkRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskService;
|
||||
import com.yoyuzh.transfer.OfflineTransferSessionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(AdminServiceStoragePolicyCacheTest.CacheTestConfiguration.class)
|
||||
class AdminServiceStoragePolicyCacheTest {
|
||||
|
||||
@Autowired
|
||||
private AdminService adminService;
|
||||
|
||||
@Autowired
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@Autowired
|
||||
private StoragePolicyRepository storagePolicyRepository;
|
||||
|
||||
@Autowired
|
||||
private StoragePolicyService storagePolicyService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
cacheManager.getCache(RedisCacheNames.STORAGE_POLICIES).clear();
|
||||
reset(storagePolicyRepository, storagePolicyService);
|
||||
when(storagePolicyService.readCapabilities(any(StoragePolicy.class))).thenReturn(defaultCapabilities());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCacheStoragePolicyListUntilExplicitEviction() {
|
||||
StoragePolicy defaultPolicy = storagePolicy(1L, "Default Local Storage", true, true);
|
||||
when(storagePolicyRepository.findAll(any(org.springframework.data.domain.Sort.class)))
|
||||
.thenReturn(List.of(defaultPolicy));
|
||||
|
||||
adminService.listStoragePolicies();
|
||||
adminService.listStoragePolicies();
|
||||
|
||||
verify(storagePolicyRepository, times(1)).findAll(any(org.springframework.data.domain.Sort.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEvictStoragePolicyListAfterCreatingPolicy() {
|
||||
StoragePolicy defaultPolicy = storagePolicy(1L, "Default Local Storage", true, true);
|
||||
StoragePolicy createdPolicy = storagePolicy(2L, "Archive Bucket", true, false);
|
||||
when(storagePolicyRepository.findAll(any(org.springframework.data.domain.Sort.class)))
|
||||
.thenReturn(List.of(defaultPolicy))
|
||||
.thenReturn(List.of(defaultPolicy, createdPolicy));
|
||||
when(storagePolicyRepository.save(any(StoragePolicy.class))).thenAnswer(invocation -> {
|
||||
StoragePolicy saved = invocation.getArgument(0);
|
||||
saved.setId(2L);
|
||||
saved.setCreatedAt(LocalDateTime.now());
|
||||
saved.setUpdatedAt(LocalDateTime.now());
|
||||
return saved;
|
||||
});
|
||||
|
||||
adminService.listStoragePolicies();
|
||||
adminService.createStoragePolicy(upsertRequest("Archive Bucket", true));
|
||||
adminService.listStoragePolicies();
|
||||
|
||||
verify(storagePolicyRepository, times(2)).findAll(any(org.springframework.data.domain.Sort.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEvictStoragePolicyListAfterUpdatingPolicy() {
|
||||
StoragePolicy existingPolicy = storagePolicy(2L, "Archive Bucket", true, false);
|
||||
StoragePolicy updatedPolicy = storagePolicy(2L, "Hot Bucket", true, false);
|
||||
when(storagePolicyRepository.findAll(any(org.springframework.data.domain.Sort.class)))
|
||||
.thenReturn(List.of(existingPolicy))
|
||||
.thenReturn(List.of(updatedPolicy));
|
||||
when(storagePolicyRepository.findById(2L)).thenReturn(Optional.of(existingPolicy));
|
||||
when(storagePolicyRepository.save(existingPolicy)).thenReturn(updatedPolicy);
|
||||
|
||||
adminService.listStoragePolicies();
|
||||
adminService.updateStoragePolicy(2L, upsertRequest("Hot Bucket", true));
|
||||
adminService.listStoragePolicies();
|
||||
|
||||
verify(storagePolicyRepository, times(2)).findAll(any(org.springframework.data.domain.Sort.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEvictStoragePolicyListAfterUpdatingPolicyStatus() {
|
||||
StoragePolicy existingPolicy = storagePolicy(2L, "Archive Bucket", true, false);
|
||||
StoragePolicy disabledPolicy = storagePolicy(2L, "Archive Bucket", false, false);
|
||||
when(storagePolicyRepository.findAll(any(org.springframework.data.domain.Sort.class)))
|
||||
.thenReturn(List.of(existingPolicy))
|
||||
.thenReturn(List.of(disabledPolicy));
|
||||
when(storagePolicyRepository.findById(2L)).thenReturn(Optional.of(existingPolicy));
|
||||
when(storagePolicyRepository.save(existingPolicy)).thenReturn(disabledPolicy);
|
||||
|
||||
adminService.listStoragePolicies();
|
||||
adminService.updateStoragePolicyStatus(2L, false);
|
||||
adminService.listStoragePolicies();
|
||||
|
||||
verify(storagePolicyRepository, times(2)).findAll(any(org.springframework.data.domain.Sort.class));
|
||||
}
|
||||
|
||||
private StoragePolicy storagePolicy(Long id, String name, boolean enabled, boolean defaultPolicy) {
|
||||
StoragePolicy policy = new StoragePolicy();
|
||||
policy.setId(id);
|
||||
policy.setName(name);
|
||||
policy.setType(StoragePolicyType.LOCAL);
|
||||
policy.setCredentialMode(StoragePolicyCredentialMode.NONE);
|
||||
policy.setMaxSizeBytes(1024L);
|
||||
policy.setEnabled(enabled);
|
||||
policy.setDefaultPolicy(defaultPolicy);
|
||||
policy.setCreatedAt(LocalDateTime.now());
|
||||
policy.setUpdatedAt(LocalDateTime.now());
|
||||
return policy;
|
||||
}
|
||||
|
||||
private AdminStoragePolicyUpsertRequest upsertRequest(String name, boolean enabled) {
|
||||
return new AdminStoragePolicyUpsertRequest(
|
||||
name,
|
||||
StoragePolicyType.LOCAL,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"",
|
||||
StoragePolicyCredentialMode.NONE,
|
||||
1024L,
|
||||
defaultCapabilities(),
|
||||
enabled
|
||||
);
|
||||
}
|
||||
|
||||
private StoragePolicyCapabilities defaultCapabilities() {
|
||||
return new StoragePolicyCapabilities(false, false, false, true, false, true, false, false, 1024L);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
static class CacheTestConfiguration {
|
||||
|
||||
@Bean
|
||||
CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager(RedisCacheNames.STORAGE_POLICIES);
|
||||
}
|
||||
|
||||
@Bean
|
||||
UserRepository userRepository() {
|
||||
return mock(UserRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
StoredFileRepository storedFileRepository() {
|
||||
return mock(StoredFileRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
FileBlobRepository fileBlobRepository() {
|
||||
return mock(FileBlobRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
FileService fileService() {
|
||||
return mock(FileService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordEncoder passwordEncoder() {
|
||||
return mock(PasswordEncoder.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RefreshTokenService refreshTokenService() {
|
||||
return mock(RefreshTokenService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AuthTokenInvalidationService authTokenInvalidationService() {
|
||||
return mock(AuthTokenInvalidationService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RegistrationInviteService registrationInviteService() {
|
||||
return mock(RegistrationInviteService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
OfflineTransferSessionRepository offlineTransferSessionRepository() {
|
||||
return mock(OfflineTransferSessionRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AdminMetricsService adminMetricsService() {
|
||||
return mock(AdminMetricsService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
StoragePolicyRepository storagePolicyRepository() {
|
||||
return mock(StoragePolicyRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
StoragePolicyService storagePolicyService() {
|
||||
return mock(StoragePolicyService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
FileEntityRepository fileEntityRepository() {
|
||||
return mock(FileEntityRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
StoredFileEntityRepository storedFileEntityRepository() {
|
||||
return mock(StoredFileEntityRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
BackgroundTaskService backgroundTaskService() {
|
||||
return mock(BackgroundTaskService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
BackgroundTaskRepository backgroundTaskRepository() {
|
||||
return mock(BackgroundTaskRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
FileShareLinkRepository fileShareLinkRepository() {
|
||||
return mock(FileShareLinkRepository.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
AdminService adminService(UserRepository userRepository,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileBlobRepository fileBlobRepository,
|
||||
FileService fileService,
|
||||
PasswordEncoder passwordEncoder,
|
||||
RefreshTokenService refreshTokenService,
|
||||
AuthTokenInvalidationService authTokenInvalidationService,
|
||||
RegistrationInviteService registrationInviteService,
|
||||
OfflineTransferSessionRepository offlineTransferSessionRepository,
|
||||
AdminMetricsService adminMetricsService,
|
||||
StoragePolicyRepository storagePolicyRepository,
|
||||
StoragePolicyService storagePolicyService,
|
||||
FileEntityRepository fileEntityRepository,
|
||||
StoredFileEntityRepository storedFileEntityRepository,
|
||||
BackgroundTaskRepository backgroundTaskRepository,
|
||||
BackgroundTaskService backgroundTaskService,
|
||||
FileShareLinkRepository fileShareLinkRepository,
|
||||
ObjectMapper objectMapper) {
|
||||
return new AdminService(
|
||||
userRepository,
|
||||
storedFileRepository,
|
||||
fileBlobRepository,
|
||||
fileService,
|
||||
passwordEncoder,
|
||||
refreshTokenService,
|
||||
authTokenInvalidationService,
|
||||
registrationInviteService,
|
||||
offlineTransferSessionRepository,
|
||||
adminMetricsService,
|
||||
storagePolicyRepository,
|
||||
storagePolicyService,
|
||||
fileEntityRepository,
|
||||
storedFileEntityRepository,
|
||||
backgroundTaskRepository,
|
||||
backgroundTaskService,
|
||||
fileShareLinkRepository,
|
||||
objectMapper
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.yoyuzh.admin;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yoyuzh.auth.AuthTokenInvalidationService;
|
||||
import com.yoyuzh.auth.PasswordPolicy;
|
||||
import com.yoyuzh.auth.RegistrationInviteService;
|
||||
import com.yoyuzh.auth.RefreshTokenService;
|
||||
@@ -20,6 +22,8 @@ import com.yoyuzh.files.policy.StoragePolicyCredentialMode;
|
||||
import com.yoyuzh.files.policy.StoragePolicyRepository;
|
||||
import com.yoyuzh.files.policy.StoragePolicyService;
|
||||
import com.yoyuzh.files.policy.StoragePolicyType;
|
||||
import com.yoyuzh.files.share.FileShareLinkRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskRepository;
|
||||
import com.yoyuzh.files.tasks.BackgroundTask;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskService;
|
||||
import com.yoyuzh.files.tasks.BackgroundTaskStatus;
|
||||
@@ -63,6 +67,8 @@ class AdminServiceTest {
|
||||
@Mock
|
||||
private RefreshTokenService refreshTokenService;
|
||||
@Mock
|
||||
private AuthTokenInvalidationService authTokenInvalidationService;
|
||||
@Mock
|
||||
private RegistrationInviteService registrationInviteService;
|
||||
@Mock
|
||||
private OfflineTransferSessionRepository offlineTransferSessionRepository;
|
||||
@@ -77,7 +83,11 @@ class AdminServiceTest {
|
||||
@Mock
|
||||
private StoredFileEntityRepository storedFileEntityRepository;
|
||||
@Mock
|
||||
private BackgroundTaskRepository backgroundTaskRepository;
|
||||
@Mock
|
||||
private BackgroundTaskService backgroundTaskService;
|
||||
@Mock
|
||||
private FileShareLinkRepository fileShareLinkRepository;
|
||||
|
||||
private AdminService adminService;
|
||||
|
||||
@@ -85,10 +95,11 @@ class AdminServiceTest {
|
||||
void setUp() {
|
||||
adminService = new AdminService(
|
||||
userRepository, storedFileRepository, fileBlobRepository, fileService,
|
||||
passwordEncoder, refreshTokenService, registrationInviteService,
|
||||
passwordEncoder, refreshTokenService, authTokenInvalidationService, registrationInviteService,
|
||||
offlineTransferSessionRepository, adminMetricsService,
|
||||
storagePolicyRepository, storagePolicyService,
|
||||
fileEntityRepository, storedFileEntityRepository, backgroundTaskService);
|
||||
fileEntityRepository, storedFileEntityRepository, backgroundTaskRepository,
|
||||
backgroundTaskService, fileShareLinkRepository, new ObjectMapper());
|
||||
}
|
||||
|
||||
// --- getSummary ---
|
||||
@@ -258,8 +269,7 @@ class AdminServiceTest {
|
||||
when(storagePolicyRepository.findById(3L)).thenReturn(Optional.of(existingPolicy));
|
||||
|
||||
assertThatThrownBy(() -> adminService.updateStoragePolicyStatus(3L, false))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("默认存储策略不能停用");
|
||||
.isInstanceOf(BusinessException.class);
|
||||
|
||||
verify(storagePolicyRepository, never()).save(any(StoragePolicy.class));
|
||||
}
|
||||
@@ -324,7 +334,7 @@ class AdminServiceTest {
|
||||
|
||||
assertThatThrownBy(() -> adminService.deleteFile(99L))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("文件不存在");
|
||||
.hasMessageContaining("file not found");
|
||||
}
|
||||
|
||||
// --- updateUserRole ---
|
||||
@@ -347,7 +357,7 @@ class AdminServiceTest {
|
||||
|
||||
assertThatThrownBy(() -> adminService.updateUserRole(99L, UserRole.ADMIN))
|
||||
.isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("用户不存在");
|
||||
.hasMessageContaining("user not found");
|
||||
}
|
||||
|
||||
// --- updateUserBanned ---
|
||||
|
||||
Reference in New Issue
Block a user