fix(file): make ocean booking documents platform owned

This commit is contained in:
2026-09-09 11:16:28 +08:00
parent 3627239708
commit 156520b560
6 changed files with 50 additions and 31 deletions
@@ -9,13 +9,13 @@ import static org.assertj.core.api.Assertions.assertThat;
class OceanBookingFileContractTest {
@Test
void freezesInternalCustomerOwnedBookingDocumentContract() throws Exception {
void freezesInternalPlatformOwnedBookingDocumentContract() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/openapi/ocean-booking-file-v1.yaml"));
assertThat(yaml)
.contains("/internal/file/v1/business-files/{tenantId}/OCEAN_BOOKING_DOCUMENT/{oceanBookingId}:")
.contains("ownerScope: {type: string, enum: [CUSTOMER]}")
.contains("description: 订舱customerId")
.contains("ownerScope: {type: string, enum: [PLATFORM]}")
.contains("description: 平台固定0,与订舱客户快照无关")
.contains("description: oceanBookingId")
.contains("x-firstleg-document-types: [BOOKING_CONFIRMATION, BILL_OF_LADING]")
.contains("x-lifecycle-owner: FIRSTLEG")
@@ -59,11 +59,11 @@ class InternalBusinessFileControllerTest {
@Test void unicodeSpecialFileNameUsesUtf8AttachmentDisposition() {
String fileName = "订舱 确认 🚢 #[]()+,最终版.xlsx";
when(service.download(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 93L))
when(service.download(0L, "OCEAN_BOOKING_DOCUMENT", "61", 93L))
.thenReturn(new InternalBusinessFileService.Download(fileName,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", new byte[]{4}));
var response = controller.download("internal-token", 10001L,
var response = controller.download("internal-token", 0L,
"OCEAN_BOOKING_DOCUMENT", "61", 93L);
assertThat(response.getHeaders().getContentDisposition().getType()).isEqualTo("attachment");
@@ -75,46 +75,61 @@ class InternalBusinessFileServiceTest {
verify(storage).store(11L, "51", file, BusinessFileCategory.OVERSEAS_UNLOAD_PHOTO);
}
@Test void oceanBookingDocumentUsesCustomerAndBookingOwnership() {
@Test void oceanBookingDocumentUsesPlatformAndBookingOwnership() {
MockMultipartFile file = new MockMultipartFile("file", "booking.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx".getBytes());
when(storage.store(10001L, "61", file, BusinessFileCategory.OCEAN_BOOKING_DOCUMENT))
.thenReturn(new BusinessFileStorage.Stored("business-files/10001/61/booking",
when(storage.store(0L, "61", file, BusinessFileCategory.OCEAN_BOOKING_DOCUMENT))
.thenReturn(new BusinessFileStorage.Stored("business-files/0/61/booking",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
file.getSize(), "f".repeat(64), "booking.xlsx"));
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
((FileObjectEntity) invocation.getArgument(0)).setId(111L); return 1;
});
var metadata = service.upload(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file);
var metadata = service.upload(0L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file);
assertThat(metadata).extracting(InternalBusinessFileService.Metadata::ownerScope,
InternalBusinessFileService.Metadata::tenantId,
InternalBusinessFileService.Metadata::businessType,
InternalBusinessFileService.Metadata::businessId)
.containsExactly("CUSTOMER", 10001L, "OCEAN_BOOKING_DOCUMENT", "61");
assertThatThrownBy(() -> service.upload(0L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file))
.isInstanceOf(BizException.class).hasMessageContaining("明确客户");
.containsExactly("PLATFORM", 0L, "OCEAN_BOOKING_DOCUMENT", "61");
assertThatThrownBy(() -> service.upload(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file))
.isInstanceOf(BizException.class).hasMessageContaining("必须归属平台");
}
@Test void oceanBookingDocumentRejectsWrongCustomerBookingAndTruckOwnership() {
FileObjectEntity value = new FileObjectEntity(); value.setId(111L); value.setTenantId(10001L);
FileObjectEntity value = new FileObjectEntity(); value.setId(111L); value.setTenantId(0L);
value.setBusinessType("OCEAN_BOOKING_DOCUMENT"); value.setBusinessId("61");
value.setOriginalName("booking.xlsx"); value.setObjectKey("business-files/10001/61/booking");
value.setOriginalName("booking.xlsx"); value.setObjectKey("business-files/0/61/booking");
value.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
value.setSha256("f".repeat(64)); value.setSizeBytes(8L);
when(files.selectOne(any())).thenReturn(value, null, null, null);
when(files.selectOne(any())).thenReturn(value, null, null);
assertThat(service.metadata(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 111L).ownerScope())
.isEqualTo("CUSTOMER");
assertThat(service.metadata(0L, "OCEAN_BOOKING_DOCUMENT", "61", 111L).ownerScope())
.isEqualTo("PLATFORM");
assertThatThrownBy(() -> service.metadata(10002L, "OCEAN_BOOKING_DOCUMENT", "61", 111L))
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
assertThatThrownBy(() -> service.metadata(10001L, "OCEAN_BOOKING_DOCUMENT", "62", 111L))
.isInstanceOf(BizException.class).hasMessageContaining("必须归属平台");
assertThatThrownBy(() -> service.metadata(0L, "OCEAN_BOOKING_DOCUMENT", "62", 111L))
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
assertThatThrownBy(() -> service.metadata(10001L, "TRUCK_APPOINTMENT_ATTACHMENT", "61", 111L))
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
}
@Test void bookingFileScopeRemainsPlatformRegardlessOfOptionalCustomerSnapshot() {
FileObjectEntity value = new FileObjectEntity(); value.setId(111L); value.setTenantId(0L);
value.setBusinessType("OCEAN_BOOKING_DOCUMENT"); value.setBusinessId("61");
value.setOriginalName("booking.bin"); value.setObjectKey("business-files/0/61/object");
when(files.selectOne(any())).thenReturn(value);
when(storage.read(value.getObjectKey())).thenReturn(new byte[]{1, 2});
// Customer snapshot is deliberately absent from File commands before and after clearing it.
assertThat(service.metadata(0L, "OCEAN_BOOKING_DOCUMENT", "61", 111L).ownerScope()).isEqualTo("PLATFORM");
assertThat(service.download(0L, "OCEAN_BOOKING_DOCUMENT", "61", 111L).content()).containsExactly(1, 2);
assertThat(service.metadata(0L, "OCEAN_BOOKING_DOCUMENT", "61", 111L).fileId()).isEqualTo(111L);
assertThatThrownBy(() -> service.download(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 111L))
.isInstanceOf(BizException.class).hasMessageContaining("必须归属平台");
verify(storage, times(1)).read(value.getObjectKey());
}
@Test void rejectsNegativeTenantAndInvalidOwnershipInputs() {
MockMultipartFile file = new MockMultipartFile("file", "a.txt", "text/plain", "x".getBytes());
assertThatThrownBy(() -> service.upload(-1L, "CONTAINER_CLEARANCE_DOCUMENT", "41", 7L, file))
@@ -269,7 +284,7 @@ class InternalBusinessFileServiceTest {
@Test void concurrentRepeatedUploadsCreateDistinctImmutableObjectsForFirstlegToVersion() throws Exception {
MockMultipartFile file = new MockMultipartFile("file", "booking.bin",
"application/x-booking", new byte[]{1, 2, 3});
when(storage.store(10001L, "61", file, BusinessFileCategory.OCEAN_BOOKING_DOCUMENT))
when(storage.store(0L, "61", file, BusinessFileCategory.OCEAN_BOOKING_DOCUMENT))
.thenReturn(new BusinessFileStorage.Stored("business-files/10001/61/object",
"application/x-booking", 3, "a".repeat(64), "booking.bin"));
AtomicLong ids = new AtomicLong(200);
@@ -280,9 +295,9 @@ class InternalBusinessFileServiceTest {
var executor = Executors.newFixedThreadPool(2);
try {
var firstFuture = executor.submit(() ->
service.upload(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file));
service.upload(0L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file));
var secondFuture = executor.submit(() ->
service.upload(10001L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file));
service.upload(0L, "OCEAN_BOOKING_DOCUMENT", "61", 7L, file));
var first = firstFuture.get();
var second = secondFuture.get();