This commit is contained in:
2026-08-14 14:26:56 +08:00
parent 06c264db0a
commit 261a8bf2df
65 changed files with 1040 additions and 670 deletions
@@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -43,9 +44,32 @@ public class InternalLoadingEvidenceController {
return ApiResponse.success(service.validate(tenantId, inboundId, fileId));
}
@GetMapping("/{tenantId}/{inboundId}/{fileId}/content")
public ResponseEntity<byte[]> download(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@PathVariable long tenantId, @PathVariable long inboundId, @PathVariable long fileId) {
requireToken(token);
if (tenantId <= 0 || inboundId <= 0 || fileId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "租户、入库单和文件ID必须大于0");
var value = service.download(tenantId, inboundId, fileId);
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
.contentType(safeImageMediaType(value.contentType())).contentLength(value.content().length)
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
.filename(value.fileName(), StandardCharsets.UTF_8).build().toString()).body(value.content());
}
private void requireToken(String token) {
if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) {
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
}
}
private static MediaType safeImageMediaType(String value) {
if (value == null || value.isBlank()) return MediaType.APPLICATION_OCTET_STREAM;
try {
MediaType mediaType = MediaType.parseMediaType(value);
return "image".equalsIgnoreCase(mediaType.getType()) ? mediaType : MediaType.APPLICATION_OCTET_STREAM;
} catch (org.springframework.http.InvalidMediaTypeException exception) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
}
@@ -36,7 +36,7 @@ public class PlatformLoadingEvidenceController {
requirePlatform(user); requirePositive(tenantId, inboundId, fileId);
var value = service.download(tenantId, inboundId, fileId);
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
.contentType(MediaType.parseMediaType(value.contentType())).contentLength(value.content().length)
.contentType(safeImageMediaType(value.contentType())).contentLength(value.content().length)
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline()
.filename(value.fileName(), StandardCharsets.UTF_8).build().toString()).body(value.content());
}
@@ -51,4 +51,13 @@ public class PlatformLoadingEvidenceController {
private static void requirePositive(long... ids) {
for (long id : ids) if (id <= 0) throw new BizException(ResultCode.BAD_REQUEST, "业务ID必须大于0");
}
private static MediaType safeImageMediaType(String value) {
if (value == null || value.isBlank()) return MediaType.APPLICATION_OCTET_STREAM;
try {
MediaType mediaType = MediaType.parseMediaType(value);
return "image".equalsIgnoreCase(mediaType.getType()) ? mediaType : MediaType.APPLICATION_OCTET_STREAM;
} catch (InvalidMediaTypeException exception) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
}
@@ -0,0 +1,52 @@
package com.baiyue.file.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.file.service.LoadingEvidenceService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(InternalLoadingEvidenceController.class)
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
class InternalLoadingEvidenceControllerTest {
@MockBean LoadingEvidenceService service;
@Resource InternalLoadingEvidenceController controller;
@Test void downloadsOwnedEvidenceBehindInternalToken() {
when(service.download(81L, 91L, 101L)).thenReturn(
new LoadingEvidenceService.Download("door.jpg", "image/jpeg", new byte[]{1, 2, 3}));
var response = controller.download("internal-token", 81L, 91L, 101L);
assertThat(response.getBody()).containsExactly(1, 2, 3);
assertThat(response.getHeaders().getContentType().toString()).isEqualTo("image/jpeg");
assertThat(response.getHeaders().getCacheControl()).contains("no-store");
verify(service).download(81L, 91L, 101L);
}
@Test void rejectsInvalidTokenBeforeReadingEvidence() {
assertThatThrownBy(() -> controller.download("wrong", 81L, 91L, 101L))
.isInstanceOf(BizException.class).hasMessageContaining("凭证无效");
verifyNoInteractions(service);
}
@Test void fallsBackForInvalidOrNonImageContentTypes() {
when(service.download(81L, 91L, 101L)).thenReturn(
new LoadingEvidenceService.Download("door.jpg", "not a/media/type", new byte[]{1}));
when(service.download(81L, 91L, 102L)).thenReturn(
new LoadingEvidenceService.Download("door.txt", "text/plain", new byte[]{2}));
assertThat(controller.download("internal-token", 81L, 91L, 101L).getHeaders().getContentType())
.isEqualTo(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM);
assertThat(controller.download("internal-token", 81L, 91L, 102L).getHeaders().getContentType())
.isEqualTo(org.springframework.http.MediaType.APPLICATION_OCTET_STREAM);
}
}