Add offline transfer history and tighten anonymous access

This commit is contained in:
yoyuzh
2026-04-02 16:30:04 +08:00
parent 97edc4cc32
commit 2cdda3c305
13 changed files with 626 additions and 82 deletions

View File

@@ -36,6 +36,16 @@ public interface OfflineTransferSessionRepository extends JpaRepository<OfflineT
""")
List<OfflineTransferSession> findAllExpiredWithFiles(@Param("now") Instant now);
@Query("""
select distinct session
from OfflineTransferSession session
left join fetch session.files
where session.senderUserId = :senderUserId and session.expiresAt >= :now
order by session.expiresAt desc
""")
List<OfflineTransferSession> findActiveWithFilesBySenderUserId(@Param("senderUserId") Long senderUserId,
@Param("now") Instant now);
@Query("""
select coalesce(sum(file.size), 0)
from OfflineTransferFile file

View File

@@ -35,21 +35,31 @@ public class TransferController {
@PostMapping("/sessions")
public ApiResponse<TransferSessionResponse> createSession(@AuthenticationPrincipal UserDetails userDetails,
@Valid @RequestBody CreateTransferSessionRequest request) {
requireAuthenticatedUser(userDetails);
User sender = userDetailsService.loadDomainUser(userDetails.getUsername());
User sender = loadAuthenticatedUser(userDetails);
return ApiResponse.success(transferService.createSession(sender, request));
}
@Operation(summary = "通过取件码查找快传会话")
@GetMapping("/sessions/lookup")
public ApiResponse<LookupTransferSessionResponse> lookupSession(@RequestParam String pickupCode) {
return ApiResponse.success(transferService.lookupSession(pickupCode));
public ApiResponse<LookupTransferSessionResponse> lookupSession(@AuthenticationPrincipal UserDetails userDetails,
@RequestParam String pickupCode) {
return ApiResponse.success(transferService.lookupSession(userDetails != null, pickupCode));
}
@Operation(summary = "加入快传会话")
@PostMapping("/sessions/{sessionId}/join")
public ApiResponse<TransferSessionResponse> joinSession(@PathVariable String sessionId) {
return ApiResponse.success(transferService.joinSession(sessionId));
public ApiResponse<TransferSessionResponse> joinSession(@AuthenticationPrincipal UserDetails userDetails,
@PathVariable String sessionId) {
return ApiResponse.success(transferService.joinSession(userDetails != null, sessionId));
}
@Operation(summary = "查看当前用户的离线快传列表")
@GetMapping("/sessions/offline/mine")
public ApiResponse<java.util.List<TransferSessionResponse>> listOfflineSessions(@AuthenticationPrincipal UserDetails userDetails) {
requireAuthenticatedUser(userDetails);
return ApiResponse.success(transferService.listOfflineSessions(
userDetailsService.loadDomainUser(userDetails.getUsername())
));
}
@Operation(summary = "上传离线快传文件")
@@ -70,9 +80,10 @@ public class TransferController {
@Operation(summary = "下载离线快传文件")
@GetMapping("/sessions/{sessionId}/files/{fileId}/download")
public ResponseEntity<?> downloadOfflineFile(@PathVariable String sessionId,
public ResponseEntity<?> downloadOfflineFile(@AuthenticationPrincipal UserDetails userDetails,
@PathVariable String sessionId,
@PathVariable String fileId) {
return transferService.downloadOfflineFile(sessionId, fileId);
return transferService.downloadOfflineFile(userDetails != null, sessionId, fileId);
}
@Operation(summary = "把离线快传文件存入网盘")
@@ -112,4 +123,11 @@ public class TransferController {
throw new BusinessException(ErrorCode.NOT_LOGGED_IN, "用户未登录");
}
}
private User loadAuthenticatedUser(UserDetails userDetails) {
if (userDetails == null) {
return null;
}
return userDetailsService.loadDomainUser(userDetails.getUsername());
}
}

View File

@@ -59,12 +59,15 @@ public class TransferService {
pruneExpiredSessions();
adminMetricsService.recordTransferUsage(request.files().stream().mapToLong(TransferFileItem::size).sum());
if (request.mode() == TransferMode.OFFLINE) {
if (sender == null) {
throw new BusinessException(ErrorCode.NOT_LOGGED_IN, "离线快传需要登录后使用");
}
return createOfflineSession(sender, request);
}
return createOnlineSession(request);
}
public LookupTransferSessionResponse lookupSession(String pickupCode) {
public LookupTransferSessionResponse lookupSession(boolean authenticated, String pickupCode) {
pruneExpiredSessions();
String normalizedPickupCode = normalizePickupCode(pickupCode);
@@ -73,11 +76,14 @@ public class TransferService {
return onlineSession.toLookupResponse();
}
OfflineTransferSession offlineSession = getRequiredOfflineReadySessionByPickupCode(normalizedPickupCode);
OfflineTransferSession offlineSession = offlineTransferSessionRepository.findWithFilesByPickupCode(normalizedPickupCode)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "取件码不存在或已失效"));
ensureAuthenticatedForOfflineTransfer(authenticated);
validateOfflineReadySession(offlineSession, "取件码不存在或已失效");
return toLookupResponse(offlineSession);
}
public TransferSessionResponse joinSession(String sessionId) {
public TransferSessionResponse joinSession(boolean authenticated, String sessionId) {
pruneExpiredSessions();
TransferSession onlineSession = sessionStore.findById(sessionId).orElse(null);
@@ -90,10 +96,20 @@ public class TransferService {
return onlineSession.toSessionResponse();
}
OfflineTransferSession offlineSession = getRequiredOfflineReadySession(sessionId);
OfflineTransferSession offlineSession = offlineTransferSessionRepository.findWithFilesBySessionId(sessionId)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "快传会话不存在或已失效"));
ensureAuthenticatedForOfflineTransfer(authenticated);
validateOfflineReadySession(offlineSession, "离线快传会话不存在或已失效");
return toSessionResponse(offlineSession);
}
public List<TransferSessionResponse> listOfflineSessions(User sender) {
pruneExpiredSessions();
return offlineTransferSessionRepository.findActiveWithFilesBySenderUserId(sender.getId(), Instant.now()).stream()
.map(this::toSessionResponse)
.toList();
}
@Transactional
public void uploadOfflineFile(User sender, String sessionId, String fileId, MultipartFile multipartFile) {
pruneExpiredSessions();
@@ -155,8 +171,9 @@ public class TransferService {
return session.poll(TransferRole.from(role), Math.max(0, after));
}
public ResponseEntity<?> downloadOfflineFile(String sessionId, String fileId) {
public ResponseEntity<?> downloadOfflineFile(boolean authenticated, String sessionId, String fileId) {
pruneExpiredSessions();
ensureAuthenticatedForOfflineTransfer(authenticated);
OfflineTransferSession session = getRequiredOfflineReadySession(sessionId);
OfflineTransferFile file = getRequiredOfflineFile(session, fileId);
ensureOfflineFileUploaded(file);
@@ -314,24 +331,14 @@ public class TransferService {
private OfflineTransferSession getRequiredOfflineReadySession(String sessionId) {
OfflineTransferSession session = offlineTransferSessionRepository.findWithFilesBySessionId(sessionId)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "离线快传会话不存在或已失效"));
if (session.isExpired(Instant.now())) {
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "离线快传会话不存在或已失效");
}
if (!session.isReady()) {
throw new BusinessException(ErrorCode.UNKNOWN, "离线快传仍在上传中,请稍后再试");
}
validateOfflineReadySession(session, "离线快传会话不存在或已失效");
return session;
}
private OfflineTransferSession getRequiredOfflineReadySessionByPickupCode(String pickupCode) {
OfflineTransferSession session = offlineTransferSessionRepository.findWithFilesByPickupCode(pickupCode)
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "取件码不存在或已失效"));
if (session.isExpired(Instant.now())) {
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "取件码不存在或已失效");
}
if (!session.isReady()) {
throw new BusinessException(ErrorCode.UNKNOWN, "离线快传仍在上传中,请稍后再试");
}
validateOfflineReadySession(session, "取件码不存在或已失效");
return session;
}
@@ -365,6 +372,21 @@ public class TransferService {
return normalized;
}
private void ensureAuthenticatedForOfflineTransfer(boolean authenticated) {
if (!authenticated) {
throw new BusinessException(ErrorCode.NOT_LOGGED_IN, "离线快传需要登录后使用");
}
}
private void validateOfflineReadySession(OfflineTransferSession session, String notFoundMessage) {
if (session.isExpired(Instant.now())) {
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, notFoundMessage);
}
if (!session.isReady()) {
throw new BusinessException(ErrorCode.UNKNOWN, "离线快传仍在上传中,请稍后再试");
}
}
private String normalizeContentType(String contentType) {
String normalized = Objects.requireNonNullElse(contentType, "").trim();
return normalized.isEmpty() ? "application/octet-stream" : normalized;