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);
|
||||
|
||||
Reference in New Issue
Block a user