This commit is contained in:
2026-09-15 13:36:58 +08:00
parent 1bb69698de
commit f9649c44e9
44 changed files with 647 additions and 63 deletions
@@ -0,0 +1,45 @@
package com.baiyue.common.event;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/** 装柜完成 V3:在 V2 货物快照上增加整柜五类国内装柜照片引用。 */
public record FirstLegLoadingPlanCompletedV3(
String loadingPlanId, String planNo,
@JsonProperty("sourceContainerBookingId") String containerBookingId,
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
List<FirstLegLoadingPlanCompletedV2.Order> orders, List<LoadingEvidence> loadingEvidence) {
public FirstLegLoadingPlanCompletedV3 {
if (loadingPlanId == null || loadingPlanId.isBlank()
|| containerBookingId == null || containerBookingId.isBlank()) {
throw new IllegalArgumentException("loadingPlanId and sourceContainerBookingId are required");
}
containerNo = ContainerNumbers.requireCanonical(containerNo);
orders = orders == null ? List.of() : List.copyOf(orders);
loadingEvidence = loadingEvidence == null ? List.of() : List.copyOf(loadingEvidence);
if (orders.isEmpty()) throw new IllegalArgumentException("orders are required");
var owner = orders.get(0);
if (orders.stream().anyMatch(order -> !owner.customerId().equals(order.customerId())
|| !owner.customerName().equals(order.customerName()))) {
throw new IllegalArgumentException("orders must share one immutable customerId/customerName snapshot");
}
if (loadingEvidence.size() != 5
|| loadingEvidence.stream().map(LoadingEvidence::evidenceType).distinct().count() != 5) {
throw new IllegalArgumentException("five unique loading evidence types are required");
}
}
public record LoadingEvidence(String evidenceType, long fileId, String fileName) {
public LoadingEvidence {
if (evidenceType == null || !evidenceType.matches("PHOTO_STEP_[1-4]|SEAL")) {
throw new IllegalArgumentException("unsupported loading evidence type");
}
if (fileId <= 0) throw new IllegalArgumentException("loading evidence fileId must be positive");
if (fileName == null || fileName.isBlank() || fileName.length() > 255) {
throw new IllegalArgumentException("loading evidence fileName is required");
}
fileName = fileName.trim();
}
}
}
@@ -5,6 +5,25 @@ info:
paths: {}
components:
schemas:
FirstLegLoadingPlanCompletedV3:
allOf:
- $ref: '#/components/schemas/FirstLegLoadingPlanCompletedV2'
- type: object
required: [loadingEvidence]
properties:
loadingEvidence:
type: array
minItems: 5
maxItems: 5
description: Exactly one active domestic loading photo reference for each of PHOTO_STEP_1 through PHOTO_STEP_4 and SEAL.
items: {$ref: '#/components/schemas/FirstLegLoadingEvidenceV3'}
FirstLegLoadingEvidenceV3:
type: object
required: [evidenceType, fileId, fileName]
properties:
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]}
fileId: {type: integer, format: int64, minimum: 1, description: Domestic File service identity; never exposed to PDA clients}
fileName: {type: string, minLength: 1, maxLength: 255}
FirstLegLoadingPlanCompletedV2:
type: object
required: [loadingPlanId, sourceContainerBookingId, containerNo, estimatedPortArrivalAt, actualPortArrivalAt, estimatedWarehouseArrivalAt, orders]
@@ -0,0 +1,46 @@
package com.baiyue.common.event;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.junit.jupiter.api.Test;
class FirstLegLoadingPlanCompletedV3Test {
@Test
void carriesExactlyFiveDomesticLoadingPhotoReferences() {
var json = new ObjectMapper().valueToTree(payload(evidence()));
assertThat(json.path("loadingEvidence")).hasSize(5);
assertThat(json.at("/loadingEvidence/4/evidenceType").asText()).isEqualTo("SEAL");
}
@Test
void rejectsMissingOrDuplicatePhotoTypes() {
assertThatThrownBy(() -> payload(evidence().subList(0, 4)))
.hasMessageContaining("five unique");
assertThatThrownBy(() -> payload(List.of(
photo("PHOTO_STEP_1", 1), photo("PHOTO_STEP_1", 2), photo("PHOTO_STEP_2", 3),
photo("PHOTO_STEP_3", 4), photo("SEAL", 5))))
.hasMessageContaining("five unique");
}
private FirstLegLoadingPlanCompletedV3 payload(List<FirstLegLoadingPlanCompletedV3.LoadingEvidence> photos) {
var order = new FirstLegLoadingPlanCompletedV2.Order(
"2", "2", "3", "ORD", "3", "RK", null, "ONT8", "FBA ONT8",
2, 20, 0, null, null, null, List.of(), "11", "客户甲", "FIRST_LEG", List.of(), "FBA");
return new FirstLegLoadingPlanCompletedV3(
"4", "P", "5", "ZCSU7881970", "CAB", "6", "WH",
null, null, null, List.of(order), photos);
}
private List<FirstLegLoadingPlanCompletedV3.LoadingEvidence> evidence() {
return List.of(photo("PHOTO_STEP_1", 1), photo("PHOTO_STEP_2", 2),
photo("PHOTO_STEP_3", 3), photo("PHOTO_STEP_4", 4), photo("SEAL", 5));
}
private FirstLegLoadingPlanCompletedV3.LoadingEvidence photo(String type, long id) {
return new FirstLegLoadingPlanCompletedV3.LoadingEvidence(type, id, type + ".jpg");
}
}