优化
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@
|
||||
| 货柜与全局智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`service/SmartLoadingService.java`、`repository/SmartLoadingRepository.java`、`client/PendingReviewOrderClient.java`、`loading/`;全局候选合并本服务履约投影与 Order `/internal/order/v1/pending-review-orders` 权威待审核页,工作台柜次从 `/inbound-candidates`、`/containers` 进入;普通箱数、待审核占位、同柜组和全部未拣移除分别走 `/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`,明细规格拆分留在服务端 |
|
||||
| 货柜资料、PDA 证据与业务总览 | `controller/PlatformContainerController.java` 的 `/{id}/overview`、`/{id}/documents/{documentType}`,聚合 `mapper/ContainerOverviewMapper.java`;契约 `src/main/resources/openapi/phase3-container-overview-v1.yaml`,四类资料固定为目的港清关、国内出口报关、DO、PU 的当前唯一版本 |
|
||||
| PDA 拣货与装柜、证据幂等上传 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java`、`loading/service/CabinetLoadingEvidenceService.java`;证据写入和 multipart 上传均先以任务内幂等键匹配请求哈希,上传哈希覆盖仓库、版本、证据类型、锁码、备注、文件名、媒体类型、大小与文件内容;完全重放返回当前视图且不得再次上传 File,相同键不同内容返回冲突 |
|
||||
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType`(`FBA` 或 `OVERSEAS`),OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`loading/mapper/CabinetLoadingEvidenceMapper.java`、`client/EventBridgeClient.java`;当前发布 V3,从 `fl_inbound_order.destination_type` 冻结 `orders[].destinationType`(`FBA` 或 `OVERSEAS`),OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数,并冻结物理柜五类有效装柜照片引用;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 海外暂扣状态回写 | 同一内部事件入口、消费者 `service/OverseasInboundOrderDetainedConsumer.java`;按来源入仓单 ID 更新 `fl_inbound_order.hold_flag/current_hold_reason`,复用跨区 Inbox 并写 `fl_inbound_hold_record` |
|
||||
| 海外派送完成回写 | 同一内部事件入口、消费者 `service/OverseasDeliveryCompletedConsumer.java`;按来源入仓单、订单、入仓码和 FBA 仓校验后推进当前入仓单 `LOADED→DELIVERED`,同一 Order 全部有效入仓单完成后才经 `OrderFulfillmentCompletionClient` 推进 Order `COMPLETED` |
|
||||
|
||||
+28
-4
@@ -10,6 +10,7 @@ import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.FirstLegLoadingPlanCompletedV2;
|
||||
import com.baiyue.common.event.FirstLegLoadingPlanCompletedV3;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.firstleg.entity.InboundOutboxEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||
@@ -19,6 +20,7 @@ import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper.PalletRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPalletLabelMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper.LabelRow;
|
||||
import com.baiyue.firstleg.loading.mapper.CabinetLoadingEvidenceMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -42,11 +44,12 @@ import java.util.Set;
|
||||
/** 在装柜完成事务内按客户及可信客户名称快照写跨区业务 Outbox。 */
|
||||
@Service
|
||||
public class LoadingCompletedOutboxService {
|
||||
private static final long HANDOVER_VERSION = 2L;
|
||||
private static final long HANDOVER_VERSION = 3L;
|
||||
@Resource private LoadingCompletedEventMapper snapshotMapper;
|
||||
@Resource private LoadingCartonManifestMapper cartonManifestMapper;
|
||||
@Resource private LoadingPalletLabelMapper palletLabelMapper;
|
||||
@Resource private LoadingPalletLabelService palletLabelService;
|
||||
@Resource private CabinetLoadingEvidenceMapper cabinetEvidenceMapper;
|
||||
@Resource private InboundOutboxMapper outboxMapper;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@@ -110,21 +113,42 @@ public class LoadingCompletedOutboxService {
|
||||
.filter(item -> Objects.equals(item.getSourceInboundId(), value.get(0).getSourceInboundId())).toList(),
|
||||
standardLabels.stream().filter(item -> Objects.equals(item.getInboundId(), value.get(0).getSourceInboundId())).toList(),
|
||||
cartonMarks, palletManifests)).toList();
|
||||
var payload = new FirstLegLoadingPlanCompletedV2(String.valueOf(first.getLoadingPlanId()),
|
||||
var payload = new FirstLegLoadingPlanCompletedV3(String.valueOf(first.getLoadingPlanId()),
|
||||
first.getPlanNo(), string(first.getContainerBookingId()),
|
||||
ContainerNumbers.requireCanonical(first.getContainerNo()), first.getCabinetNo(),
|
||||
string(first.getWarehouseId()), first.getWarehouseName(), iso(first.getEstimatedPortArrivalAt()),
|
||||
iso(first.getActualPortArrivalAt()), iso(first.getEstimatedWarehouseArrivalAt()), orders);
|
||||
iso(first.getActualPortArrivalAt()), iso(first.getEstimatedWarehouseArrivalAt()), orders,
|
||||
loadingEvidence(first.getContainerBookingId()));
|
||||
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + first.getLoadingPlanId() + ':' + accountId + ':' + HANDOVER_VERSION;
|
||||
String traceId = EventIdentifiers.newUuid();
|
||||
return new EventEnvelope(EventIdentifiers.newUuid(), EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED, 2,
|
||||
return new EventEnvelope(EventIdentifiers.newUuid(), EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED, 3,
|
||||
Instant.now(), RegionCode.CN, RegionCode.OVERSEAS, String.valueOf(accountId), "LOADING_PLAN",
|
||||
String.valueOf(first.getLoadingPlanId()), first.getAggregateVersion(), HANDOVER_VERSION,
|
||||
traceId, eventKey, null, payloadNode, hash).requireValid();
|
||||
}
|
||||
|
||||
private List<FirstLegLoadingPlanCompletedV3.LoadingEvidence> loadingEvidence(Long containerBookingId) {
|
||||
if (containerBookingId == null || containerBookingId <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "装柜计划缺少柜次,不能冻结装柜照片");
|
||||
}
|
||||
List<FirstLegLoadingPlanCompletedV3.LoadingEvidence> result = cabinetEvidenceMapper
|
||||
.byContainer(containerBookingId).stream()
|
||||
.filter(value -> Integer.valueOf(1).equals(value.getActiveFlag()) && "ACTIVE".equals(value.getStatus()))
|
||||
.map(value -> new FirstLegLoadingPlanCompletedV3.LoadingEvidence(
|
||||
value.getEvidenceType(), value.getFileId(), value.getFileName()))
|
||||
.sorted(Comparator.comparing(FirstLegLoadingPlanCompletedV3.LoadingEvidence::evidenceType))
|
||||
.toList();
|
||||
Set<String> types = result.stream().map(FirstLegLoadingPlanCompletedV3.LoadingEvidence::evidenceType)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (result.size() != 5 || !types.equals(Set.of(
|
||||
"PHOTO_STEP_1", "PHOTO_STEP_2", "PHOTO_STEP_3", "PHOTO_STEP_4", "SEAL"))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "国内装柜五类照片不完整,不能生成尾程交接事件");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private FirstLegLoadingPlanCompletedV2.Order order(List<Row> values, List<PalletRow> palletRows,
|
||||
List<com.baiyue.firstleg.mapper.LoadingPalletLabelMapper.LabelRow> standardLabels,
|
||||
Map<Long, List<String>> cartonMarks,
|
||||
|
||||
+19
-2
@@ -10,6 +10,8 @@ import com.baiyue.firstleg.mapper.LoadingCompletedEventMapper.PalletRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingCartonManifestMapper.LabelRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingPalletLabelMapper;
|
||||
import com.baiyue.firstleg.loading.mapper.CabinetLoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.loading.entity.CabinetLoadingEvidenceEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -33,6 +35,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
private final LoadingCartonManifestMapper manifests = mock(LoadingCartonManifestMapper.class);
|
||||
private final LoadingPalletLabelMapper palletLabels = mock(LoadingPalletLabelMapper.class);
|
||||
private final LoadingPalletLabelService palletLabelService = mock(LoadingPalletLabelService.class);
|
||||
private final CabinetLoadingEvidenceMapper cabinetEvidence = mock(CabinetLoadingEvidenceMapper.class);
|
||||
private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
|
||||
private final LoadingCompletedOutboxService service = new LoadingCompletedOutboxService();
|
||||
|
||||
@@ -41,14 +44,25 @@ class LoadingCompletedOutboxServiceTest {
|
||||
ReflectionTestUtils.setField(service,"cartonManifestMapper",manifests);
|
||||
ReflectionTestUtils.setField(service,"palletLabelMapper",palletLabels);
|
||||
ReflectionTestUtils.setField(service,"palletLabelService",palletLabelService);
|
||||
ReflectionTestUtils.setField(service,"cabinetEvidenceMapper",cabinetEvidence);
|
||||
ReflectionTestUtils.setField(service,"outboxMapper",outbox);
|
||||
ReflectionTestUtils.setField(service,"objectMapper",mapper);
|
||||
when(palletLabels.labels(anyLong())).thenReturn(List.of());
|
||||
when(cabinetEvidence.byContainer(anyLong())).thenReturn(List.of(
|
||||
evidence("PHOTO_STEP_1", 1), evidence("PHOTO_STEP_2", 2), evidence("PHOTO_STEP_3", 3),
|
||||
evidence("PHOTO_STEP_4", 4), evidence("SEAL", 5)));
|
||||
when(manifests.labelPool(anyLong(), anyLong())).thenAnswer(invocation -> List.of(
|
||||
label(101, "BOX-101"), label(102, "BOX-102"),
|
||||
label(202, "BOX-202"), label(302, "BOX-302")));
|
||||
}
|
||||
|
||||
private static CabinetLoadingEvidenceEntity evidence(String type, long fileId) {
|
||||
CabinetLoadingEvidenceEntity value = new CabinetLoadingEvidenceEntity();
|
||||
value.setEvidenceType(type); value.setFileId(fileId); value.setFileName(type + ".jpg");
|
||||
value.setStatus("ACTIVE"); value.setActiveFlag(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Test void groupsByAccountAndKeepsAllOrdersAndItemsStable() throws Exception {
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(row(22,300,302),row(11,200,202),row(11,100,102),row(11,100,101)));
|
||||
when(snapshots.palletRows(90L)).thenReturn(List.of(
|
||||
@@ -79,9 +93,12 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(first.payload().at("/actualPortArrivalAt").asText()).isEqualTo("2026-10-01T08:30");
|
||||
assertThat(first.payload().at("/estimatedWarehouseArrivalAt").asText()).isEqualTo("2026-10-03T09:00");
|
||||
assertThat(first.payload().at("/sourceContainerBookingId").asText()).isEqualTo("91");
|
||||
assertThat(first.payload().get("loadingEvidence")).hasSize(5);
|
||||
assertThat(first.payload().at("/loadingEvidence/0/evidenceType").asText()).isEqualTo("PHOTO_STEP_1");
|
||||
assertThat(first.payload().at("/loadingEvidence/4/evidenceType").asText()).isEqualTo("SEAL");
|
||||
assertThat(first.payload().has("containerBookingId")).isFalse();
|
||||
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
|
||||
assertThat(first.eventVersion()).isEqualTo(2);
|
||||
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:3");
|
||||
assertThat(first.eventVersion()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test void requiresFrozenPhysicalContainerNumberBeforePublishing() {
|
||||
|
||||
+12
-1
@@ -39,7 +39,7 @@ class TaillegRealProducerAcceptanceTest {
|
||||
|
||||
@Test
|
||||
void completedLoadingPlanPublishesFirstLegAndDropshipOrders() {
|
||||
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + PLAN + ":" + CUSTOMER + ":2";
|
||||
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + PLAN + ":" + CUSTOMER + ":3";
|
||||
Integer existing = jdbc.queryForObject("SELECT COUNT(*) FROM fl_inbound_outbox WHERE event_key=?",
|
||||
Integer.class, eventKey);
|
||||
if (existing == 0) {
|
||||
@@ -51,6 +51,8 @@ class TaillegRealProducerAcceptanceTest {
|
||||
assertEquals(2, jdbc.queryForObject("SELECT JSON_LENGTH(JSON_EXTRACT(payload_json,'$.payload.orders')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", Integer.class,
|
||||
eventKey));
|
||||
assertEquals(5, jdbc.queryForObject("SELECT JSON_LENGTH(JSON_EXTRACT(payload_json,'$.payload.loadingEvidence')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", Integer.class, eventKey));
|
||||
assertEquals("真实验收客户", jdbc.queryForObject(
|
||||
"SELECT JSON_UNQUOTE(JSON_EXTRACT(payload_json,'$.payload.orders[0].customerName')) " +
|
||||
"FROM fl_inbound_outbox WHERE event_key=?", String.class, eventKey));
|
||||
@@ -91,6 +93,15 @@ class TaillegRealProducerAcceptanceTest {
|
||||
990022901L, PLAN, BOOKING, MARKER + "-CAB", MARKER + "-CNTR");
|
||||
seedPickItem(990022911L, 990022321L, FIRST_INBOUND, 990022421L, 3);
|
||||
seedPickItem(990022912L, 990022322L, DROPSHIP_INBOUND, 990022422L, 2);
|
||||
int evidenceIndex = 0;
|
||||
for (String type : new String[] {"PHOTO_STEP_1", "PHOTO_STEP_2", "PHOTO_STEP_3", "PHOTO_STEP_4", "SEAL"}) {
|
||||
long id = 990023000L + evidenceIndex;
|
||||
jdbc.update("INSERT IGNORE INTO fl_loading_cabinet_evidence(id,container_booking_id,source_task_id," +
|
||||
"evidence_type,file_id,file_name,status,active_flag,operation_key,request_hash,created_by,updated_by) " +
|
||||
"VALUES(?,?,?,?,?,?,'ACTIVE',1,?,REPEAT('a',64),1,1)",
|
||||
id, BOOKING, 990022901L, type, id + 1000, type + ".jpg", MARKER + "-EVIDENCE-" + type);
|
||||
evidenceIndex++;
|
||||
}
|
||||
jdbc.update("UPDATE fl_loading_plan SET plan_status='COMPLETED',confirmed_at=NOW(3),version=version+1 WHERE id=?", PLAN);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user