diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index cea834d1c..016643e6c 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -5166,7 +5166,7 @@ BEGIN AND pick_task.loading_plan_id = OLD.id WHERE item.loading_plan_id = OLD.id AND item.deleted = 0 AND (pick_task.id IS NULL - OR pick_item.picked_package_count <> pick_item.planned_package_count)) THEN + OR pick_item.picked_package_count <> item.package_count)) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '仍有未完成拣货明细,不能完成装柜'; END IF; SET NEW.confirmed_at = COALESCE(NEW.confirmed_at, CURRENT_TIMESTAMP(3)); @@ -9334,7 +9334,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_operation ( UNIQUE KEY uk_loading_pick_operation_id (operation_id), KEY idx_loading_pick_operation_task (pick_task_id, created_at), CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), - CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH')) + CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH', 'COMPLETE_LOADING')) ) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计'; DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update; @@ -9546,7 +9546,7 @@ BEGIN END IF; ALTER TABLE fl_loading_pick_operation ADD CONSTRAINT chk_loading_pick_operation_action - CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH')); + CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH', 'COMPLETE_LOADING')); END$$ DELIMITER ; CALL phase3_pda_replace_operation_check(); @@ -12296,7 +12296,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'pick_item_id', 'PDA拣货明细ID,同时作为本表主键'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'updated_at', '明细分配记录最后更新时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'version', '乐观锁版本号,每次更新递增'); -CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'action_type', '动作类型:CLAIM领取、ASSIGN分配、REVIEW复核、BATCH批量提交'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'action_type', '动作类型:CLAIM领取、ASSIGN分配、REVIEW复核、BATCH批量提交、COMPLETE_LOADING人工确认装柜完成'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'created_at', '不可变操作审计创建时间'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'id', 'PDA任务操作审计记录ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'operation_id', '全局唯一操作ID,用于幂等重放'); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java index 0dc4222f3..86d0e8481 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java @@ -38,7 +38,12 @@ public interface WarehousePalletLocationClient { @PostMapping("/settlements") ApiResponse settle(@RequestHeader("X-Internal-Service-Token") String token, - @RequestBody SettlementRequest request); + @RequestBody SettlementRequest request); + + @GetMapping("/settlements/target") + ApiResponse settlementTarget(@RequestHeader("X-Internal-Service-Token") String token, + @RequestParam("sourceBusinessId") long sourceBusinessId, + @RequestParam("idempotencyKey") String idempotencyKey); @GetMapping("/occupied-resources") ApiResponse> occupiedResources( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java index f21a58484..44f6bd0b9 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingPdaController.java @@ -8,6 +8,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest; +import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.Operation; import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot; @@ -137,6 +138,15 @@ public class PlatformLoadingPdaController { return ApiResponse.success(service.review(taskId, request, user.userId(), user.username())); } + @PostMapping("/tasks/{taskId}/complete") + @PreAuthorize("hasAuthority('platform:firstleg:pda:work')") + public ApiResponse complete(@AuthenticationPrincipal AuthenticatedUser user, + @PathVariable @Positive long taskId, + @Valid @RequestBody CompleteLoadingRequest request) { + FirstlegScopeGuard.platform(user); + return ApiResponse.success(service.complete(taskId, request, user.userId(), user.username())); + } + @GetMapping("/tasks/{taskId}/operations") @PreAuthorize("hasAuthority('platform:firstleg:pda:view')") public ApiResponse> operations(@AuthenticationPrincipal AuthenticatedUser user, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/domain/SmartLoadingStatus.java b/firstleg/src/main/java/com/baiyue/firstleg/domain/SmartLoadingStatus.java index ed7edf37b..3e0a48601 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/domain/SmartLoadingStatus.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/domain/SmartLoadingStatus.java @@ -8,7 +8,7 @@ public enum SmartLoadingStatus { EDITING, /** 拣货中:已生成拣货任务,仍可追加或调整未拣货明细。 */ LOADING, - /** 装柜完成:PDA 拣货完成后自动完成,计划、明细和货柜信息只读。 */ + /** 装柜完成:PDA 人工确认装柜完成后进入,计划、明细和货柜信息只读。 */ COMPLETED; /** 装柜完成前可维护集装箱号和航次。 */ diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingPickDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingPickDtos.java index bd13fdacc..9b189b5a8 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingPickDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingPickDtos.java @@ -98,6 +98,15 @@ public final class LoadingPickDtos { ) { } + /** PDA 人工确认装柜完成。拣货记录只会锁定货物,确认后才结算资源并完成装柜。 */ + public record CompleteLoadingRequest( + @NotNull @Min(0) Integer expectedTaskVersion, + @NotBlank @Size(max = 128) String operationId, + @Size(max = 500) String remark + ) { + } + + public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId, String orderNo, String inboundCode, String cargoName, List boxCodes, Integer plannedPackageCount, Integer pickedPackageCount, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java index fa91f0706..5f9b4f43e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPickItemMapper.java @@ -182,8 +182,8 @@ public interface LoadingPickItemMapper extends BaseMapper WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0) AND item_status IN ('PENDING', 'PICKING') - AND picked_package_count < #{finalCount} - AND #{finalCount} < planned_package_count + AND picked_package_count < #{finalCount} + AND #{finalCount} < planned_package_count """) int partial(@Param("id") long id, @Param("finalCount") int finalCount, @Param("releaseMode") String releaseMode, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java index 9ac55c0f1..e7b455dfa 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java @@ -12,7 +12,9 @@ import com.baiyue.firstleg.dto.ContainerDtos.Query; import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest; import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest; import com.baiyue.firstleg.entity.ContainerBookingEntity; +import com.baiyue.firstleg.entity.LoadingPlanEntity; import com.baiyue.firstleg.repository.ContainerRepository; +import com.baiyue.firstleg.repository.SmartLoadingRepository; import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DuplicateKeyException; @@ -25,6 +27,8 @@ import java.time.LocalDateTime; public class ContainerService { @Resource private ContainerRepository repository; @Resource private WarehouseMasterDataClient warehouses; + @Resource private SmartLoadingRepository smartLoadingRepository; + @Resource private LoadingPickService loadingPickService; @Value("${baiyue.security.internal-service-token:}") private String internalToken; @Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId; public PageResult page(Query query) { @@ -83,7 +87,19 @@ public class ContainerService { if (repository.updateTransport(id, containerNo, trim(request.voyageNo()), request.version(), operatorId, operatorName) != 1) throw conflict("货柜状态或版本已变化,请刷新后重试"); } catch (DuplicateKeyException ex) { throw conflict("集装箱号已存在"); } - return required(id); + ContainerBookingEntity updated = required(id); + beginPdaTaskAfterContainerBound(updated, containerNo, operatorId, operatorName); + return updated; + } + + /** 预装清单先推送、后录入集装箱号时,补建对应的 PDA 拣货任务。 */ + private void beginPdaTaskAfterContainerBound(ContainerBookingEntity booking, String containerNo, + long operatorId, String operatorName) { + if (containerNo == null) return; + LoadingPlanEntity plan = smartLoadingRepository.planByBooking(booking.getId()); + if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return; + loadingPickService.begin(plan.getId(), containerNo, operatorId, operatorName, + smartLoadingRepository.items(plan.getId())); } private ContainerBookingEntity required(long id) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java index ff9c3430e..33a64875d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java @@ -107,11 +107,11 @@ public class LoadingListService { } /** - * 最后一箱 PDA 拣货完成后自动调用。服务与数据库触发器均以计划全部有效明细为口径复核。 + * PDA 人工确认装柜完成后调用。服务与数据库触发器均以计划全部有效明细为口径复核。 */ @Transactional @BusinessAudit(event = "PDA_COMPLETE_LOADING", resourceType = "LOADING_PLAN", - resourceId = "#planId", description = "PDA拣货完成后自动生成装柜清单") + resourceId = "#planId", description = "PDA确认装柜完成后生成装柜清单") public void finishFromPda(long planId, long pickTaskId, long operatorId, String operatorName) { String operationId = "PDA-COMPLETE-" + planId + '-' + pickTaskId; if (repository.history(operationId) != null) { @@ -139,7 +139,7 @@ public class LoadingListService { "LOADING", "COMPLETED", json(snapshot(before)), json(values("source", "PDA", "pickTaskId", pickTaskId, "status", "COMPLETED", "version", before.getVersion() + 1)), operatorId, trim(operatorName), - "全部箱数已完成PDA拣货"); + "PDA已人工确认装柜完成"); } private AggregateRow requirePlan(long planId) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java index 5746fc90a..7dfea54af 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java @@ -14,6 +14,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.Review; import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest; +import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.Operation; import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest; @@ -327,39 +328,37 @@ public class LoadingPickService { @Transactional public Task review(long taskId, ReviewRequest request, long operatorId, String operatorName) { + return complete(taskId, new CompleteLoadingRequest(request.expectedTaskVersion(), request.operationId(), + request.remark()), operatorId, operatorName); + } + + /** + * PDA 人工点击装柜完成后的唯一结算入口。 + * 逐项拣货仅记录和锁定库存;此处才执行拆单、资源释放、计划完成和装柜清单生成。 + */ + @Transactional + public Task complete(long taskId, CompleteLoadingRequest request, long operatorId, String operatorName) { String operationId = operation(request.operationId()); String remark = trim(request.remark()); - String requestHash = hash("REVIEW|" + taskId + '|' + request.expectedTaskVersion() + '|' + remark); + String requestHash = hash("COMPLETE|" + taskId + '|' + request.expectedTaskVersion() + '|' + remark); Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId); if (replay != null) return replay; LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId); if (!request.expectedTaskVersion().equals(task.getVersion())) { throw conflict("PDA任务版本已变化,请刷新后复核"); } - if (repository.review(taskId) != null) throw conflict("PDA任务已经复核"); - List taskItems = repository.items(taskId); - PdaPickSettlementPlan.Resolution resolution = PdaPickSettlementPlan.resolve(taskItems); - for (List lines : resolution.partialByInbound().values()) { - for (PdaPickSettlementPlan.SplitLine line : lines) { - if (repository.settlePartial(line.loadingPlanItemId(), line.pickItemId(), - line.pickedPackageCount(), line.pickedPackageCount() + line.remainingPackageCount(), - operatorId, operatorName) != 1) { - throw conflict("部分拣货结算版本已变化"); - } - } - } - for (Long planItemId : resolution.skippedPlanItemIds()) { - if (repository.settleSkipped(planItemId, operatorId, operatorName) != 1) { - throw conflict("跳拣结算版本已变化"); - } + if (!repository.allTaskItemsResolved(taskId)) { + throw conflict("存在尚未处理的PDA拣货明细,不能确认装柜完成"); } + if (repository.review(taskId) != null) throw conflict("PDA任务已经确认装柜完成"); + settlementService.settle(taskId, operatorId, operatorName); if (!repository.allPlanItemsPicked(task.getLoadingPlanId())) { throw conflict("柜次仍有未完成PDA拣货的箱数"); } if (repository.approve(taskId, operatorId, operatorName, remark) != 1) { throw conflict("PDA任务已经复核"); } - insertOperation(taskId, operationId, requestHash, "REVIEW", operatorId, operatorName); + insertOperation(taskId, operationId, requestHash, "COMPLETE_LOADING", operatorId, operatorName); loadingListService.finishFromPda(task.getLoadingPlanId(), taskId, operatorId, operatorName); repository.syncInboundProjections(taskId, operatorId, operatorName); return view(requiredTask(taskId), null); @@ -408,13 +407,7 @@ public class LoadingPickService { } private void settleAndFinish(LoadingPickTaskEntity task, long operatorId, String operatorName) { - if (!repository.allTaskItemsResolved(task.getId())) { - return; - } - settlementService.settle(task.getId(), operatorId, operatorName); - if (repository.allPlanItemsPicked(task.getLoadingPlanId())) { - loadingListService.finishFromPda(task.getLoadingPlanId(), task.getId(), operatorId, operatorName); - } + // 拣货完成仅锁定明细。资源结算与装柜完成必须由 PDA 人工点击“装柜完成”触发。 } /** 进入PDA拣货任务即将预装柜计划推进为拣货中;状态推进和历史写入保持幂等。 */ @@ -736,8 +729,7 @@ public class LoadingPickService { } /** - * 先触碰仍处于 LOADING 的任务,再结算并推进装柜计划。计划完成触发器会同步把任务改为 - * COMPLETED,因此绝不能在 finishFromPda 之后再以 LOADING 状态触碰任务。 + * 逐项拣货后仅更新任务版本与投影,不能自动结算或推进装柜计划。 */ private void touchSettleFinishAndSync(LoadingPickTaskEntity task, long operatorId, String operatorName) { if (repository.touchTask(task.getId(), task.getVersion()) != 1) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java index 5b838da68..c436cec15 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/PdaPickSettlementService.java @@ -161,7 +161,11 @@ public class PdaPickSettlementService { OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, measurementLines, pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId, "pda-task-" + taskId, operatorId, operatorName); - long remainderChildId = childPackageCount > 0 ? IdWorker.getId() : 0L; + String settlementKey = "PDA-SETTLE-" + taskId + '-' + parentInboundId; + long remainderChildId = childPackageCount > 0 ? existingSettlementTarget(parentInboundId, settlementKey) : 0L; + if (childPackageCount > 0 && remainderChildId == 0L) { + remainderChildId = IdWorker.getId(); + } long batchId = IdWorker.getId(); String idempotencyKey = "pda-task-" + taskId; int rootVersion = root.version() == null ? 0 : root.version(); @@ -421,6 +425,16 @@ public class PdaPickSettlementService { } } + private long existingSettlementTarget(long sourceInboundId, String idempotencyKey) { + try { + ApiResponse response = warehouse.settlementTarget(internalToken, sourceInboundId, idempotencyKey); + return response != null && response.successful() && response.data() != null + ? response.data() : 0L; + } catch (FeignException exception) { + return 0L; + } + } + private static boolean sameReleasedReservations(List expected, List actual) { return expected.equals(actual.stream().map(WarehousePalletLocationClient.SelectiveReleaseResult::reservationId) diff --git a/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql b/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql index 40374faa2..6ca40eccd 100644 --- a/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql +++ b/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql @@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_operation ( UNIQUE KEY uk_loading_pick_operation_id (operation_id), KEY idx_loading_pick_operation_task (pick_task_id, created_at), CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), - CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'REVIEW', 'BATCH')) + CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'REVIEW', 'BATCH', 'COMPLETE_LOADING')) ) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计'; DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update; diff --git a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql index 4f74aef0f..e211f671d 100644 --- a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql +++ b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql @@ -653,7 +653,7 @@ BEGIN AND pick_task.loading_plan_id = OLD.id WHERE item.loading_plan_id = OLD.id AND item.deleted = 0 AND (pick_task.id IS NULL - OR pick_item.picked_package_count <> pick_item.planned_package_count)) THEN + OR pick_item.picked_package_count <> item.package_count)) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '仍有未完成拣货明细,不能完成装柜'; END IF; SET NEW.confirmed_at = COALESCE(NEW.confirmed_at, CURRENT_TIMESTAMP(3)); diff --git a/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml b/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml index 9510a3756..3b675da2b 100644 --- a/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml +++ b/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml @@ -82,13 +82,23 @@ paths: '200': {description: Completed task, identical replay result, or A0409 when any snapshot changed} /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/review: post: - summary: Review all resolved items, settle partial and skipped quantities, then complete the plan + summary: 人工确认装柜完成的兼容入口 parameters: [{$ref: '#/components/parameters/TaskId'}] requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ReviewRequest'}}} responses: - '200': {description: Completed task or A0409 when items or task version changed} + '200': {description: 操作员明确确认后完成装柜} + /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete: + post: + summary: PDA拣货全部处理后人工确认装柜完成 + description: PDA拣货只锁定数量和仓储资源,本接口执行结算、释放资源并生成装柜清单。 + parameters: [{$ref: '#/components/parameters/TaskId'}] + requestBody: + required: true + content: {application/json: {schema: {$ref: '#/components/schemas/CompleteLoadingRequest'}}} + responses: + '200': {description: 完成任务;存在未处理明细或版本冲突时返回A0409} /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/operations: get: summary: Read immutable task-level claim, assignment and review audit records @@ -198,6 +208,13 @@ components: expectedTaskVersion: {type: integer, minimum: 0} operationId: {type: string, maxLength: 128} remark: {type: string, maxLength: 500} + CompleteLoadingRequest: + type: object + required: [expectedTaskVersion, operationId] + properties: + expectedTaskVersion: {type: integer, minimum: 0} + operationId: {type: string, maxLength: 128} + remark: {type: string, maxLength: 500} InventorySnapshot: type: object required: [taskId, taskVersion, containerNo, warehouseId, capturedAt, snapshotToken, inbounds] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java index fae9ad829..f642ce24a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PdaConvergenceOpenApiContractTest.java @@ -9,7 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; class PdaConvergenceOpenApiContractTest { @Test - void publishesClaimAssignWorkAndReviewRoutes() throws Exception { + void publishesClaimAssignWorkAndManualCompletionRoutes() throws Exception { String yaml = new ClassPathResource("openapi/phase3-pda-convergence-v1.yaml") .getContentAsString(StandardCharsets.UTF_8); assertThat(yaml) @@ -22,11 +22,13 @@ class PdaConvergenceOpenApiContractTest { .contains("/tasks/{taskId}/items/{itemId}/skip:") .contains("/tasks/{taskId}/batch:") .contains("/tasks/{taskId}/review:") + .contains("/tasks/{taskId}/complete:") .contains("/tasks/{taskId}/operations:") .contains("expectedAssignmentVersion") .contains("BatchRequest") .contains("inventorySnapshotToken") .contains("InventorySnapshot") - .contains("expectedTaskVersion"); + .contains("expectedTaskVersion") + .contains("CompleteLoadingRequest"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java index fea0a5ce9..95a85c1e7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformLoadingPdaControllerContractTest.java @@ -22,6 +22,7 @@ class PlatformLoadingPdaControllerContractTest { authority("skip", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, long.class, SkipRequest.class); authority("batch", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, String.class, BatchRequest.class); authority("review", "platform:firstleg:pda:review", AuthenticatedUser.class, long.class, ReviewRequest.class); + authority("complete", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, CompleteLoadingRequest.class); authority("operations", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java index 641826fa5..d1b2946e2 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java @@ -41,7 +41,7 @@ class Phase3FinalUnifiedSqlContractTest { .contains("trg_inbound_split_detail_before_insert") .contains("trg_inbound_split_detail_before_update") .contains("trg_inbound_split_detail_before_delete") - .contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH'))") + .contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH', 'COMPLETE_LOADING'))") .contains("phase3_loading_evidence_add_column") .contains("DROP INDEX uk_loading_evidence_type") .contains("DROP INDEX uk_loading_evidence_seal") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java index 3e69f50fb..1d17aeea9 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java @@ -116,7 +116,7 @@ class SmartLoadingSqlContractTest { .contains("IF OLD.plan_status = 'EDITING' AND NEW.plan_status = 'LOADING' THEN") .contains("ELSEIF OLD.plan_status = 'LOADING' AND NEW.plan_status = 'COMPLETED' THEN") .contains("LEFT JOIN fl_loading_pick_item pick_item") - .contains("pick_item.picked_package_count <> pick_item.planned_package_count") + .contains("pick_item.picked_package_count <> item.package_count") .contains("IF OLD.plan_status = 'LOADING' AND NEW.plan_status = 'COMPLETED' THEN") .contains("SET frozen_at = COALESCE(frozen_at, NEW.confirmed_at, CURRENT_TIMESTAMP(3))") .contains("已有PDA拣货记录的装柜明细仅允许收敛部分拣货实际箱数") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java index ea5edf19f..efb6bdceb 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerServiceTest.java @@ -7,7 +7,9 @@ import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest; import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest; import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest; import com.baiyue.firstleg.entity.ContainerBookingEntity; +import com.baiyue.firstleg.entity.LoadingPlanEntity; import com.baiyue.firstleg.repository.ContainerRepository; +import com.baiyue.firstleg.repository.SmartLoadingRepository; import jakarta.annotation.Resource; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.BeforeEach; @@ -33,6 +35,8 @@ import static org.mockito.Mockito.when; class ContainerServiceTest { @MockBean private ContainerRepository repository; @MockBean private WarehouseMasterDataClient warehouses; + @MockBean private SmartLoadingRepository smartLoadingRepository; + @MockBean private LoadingPickService loadingPickService; @Resource private ContainerService service; @BeforeEach @@ -118,6 +122,27 @@ class ContainerServiceTest { .updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"); } + @Test + @DisplayName("预装清单推送后再绑定集装箱号会创建PDA拣货任务") + void startsPdaTaskWhenContainerIsBoundAfterPreloadPush() { + ContainerBookingEntity current = booking("EDITING", 2); + LoadingPlanEntity plan = new LoadingPlanEntity(); + plan.setId(31L); + plan.setPlanStatus("EDITING"); + plan.setPreloadPushedAt(LocalDateTime.of(2026, 8, 11, 20, 0)); + when(repository.find(11L)).thenReturn(current); + when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin")) + .thenReturn(1); + when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan); + + service.updateTransport(11L, 9L, "admin", + new TransportUpdateRequest("MSCU1234567", "V002", 2)); + + verify(loadingPickService).begin(org.mockito.ArgumentMatchers.eq(31L), + org.mockito.ArgumentMatchers.eq("MSCU1234567"), org.mockito.ArgumentMatchers.eq(9L), + org.mockito.ArgumentMatchers.eq("admin"), any()); + } + @Test @DisplayName("已确认货柜全部只读") void rejectsConfirmedUpdate() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java index bb8a88b28..9d4c31fb4 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java @@ -109,7 +109,7 @@ class LoadingListServiceTest { verify(repository).advance(10L, "LOADING", "COMPLETED", 2, 9L, "operator"); verify(repository).appendHistory(eq(10L), eq("PDA-COMPLETE-10-21"), eq("PDA_COMPLETE_LOADING"), eq("LOADING"), eq("COMPLETED"), anyString(), anyString(), - eq(9L), eq("operator"), eq("全部箱数已完成PDA拣货")); + eq(9L), eq("operator"), eq("PDA已人工确认装柜完成")); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index 8ecee2989..10c0aa093 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -8,6 +8,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest; +import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest; import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity; @@ -67,7 +68,7 @@ class LoadingPickServiceTest { } @Test - void touchesTaskBeforeCompletingLastPickedItem() { + void keepsResourcesLockedAfterLastPickedItemUntilManualCompletion() { LoadingPickTaskEntity task = task(); LoadingPickItemEntity item = item(); preparePick(task, item, "pick-2"); @@ -76,11 +77,10 @@ class LoadingPickServiceTest { service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-2"), 9L, "operator"); - InOrder order = inOrder(repository, settlementService, loadingListService); - order.verify(repository).touchTask(20L, 0); - order.verify(settlementService).settle(20L, 9L, "operator"); - order.verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator"); - order.verify(repository).syncInboundProjections(20L, 9L, "operator"); + verify(repository).touchTask(20L, 0); + verify(settlementService, never()).settle(20L, 9L, "operator"); + verify(loadingListService, never()).finishFromPda(10L, 20L, 9L, "operator"); + verify(repository).syncInboundProjections(20L, 9L, "operator"); verify(repository).insert(any(LoadingPickHistoryEntity.class)); } @@ -131,7 +131,7 @@ class LoadingPickServiceTest { } @Test - void touchesTaskBeforeCompletingLastPartialItem() { + void keepsPartialPickLockedUntilManualCompletion() { LoadingPickTaskEntity task = task(); LoadingPickItemEntity item = item(); preparePartial(task, item, "partial-1"); @@ -141,11 +141,10 @@ class LoadingPickServiceTest { service.partial(20L, 30L, new PartialPickRequest(101L, 1, List.of(), 0, "partial-1"), 9L, "operator"); - InOrder order = inOrder(repository, settlementService, loadingListService); - order.verify(repository).touchTask(20L, 0); - order.verify(settlementService).settle(20L, 9L, "operator"); - order.verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator"); - order.verify(repository).syncInboundProjections(20L, 9L, "operator"); + verify(repository).touchTask(20L, 0); + verify(settlementService, never()).settle(20L, 9L, "operator"); + verify(loadingListService, never()).finishFromPda(10L, 20L, 9L, "operator"); + verify(repository).syncInboundProjections(20L, 9L, "operator"); } @Test @@ -171,7 +170,7 @@ class LoadingPickServiceTest { } @Test - void reviewSettlesResolvedItemsAndCompletesPlan() { + void manualCompletionSettlesResolvedItemsAndCompletesPlan() { LoadingPickTaskEntity task = task(); LoadingPickItemEntity item = item(); item.setPlannedPackageCount(5); @@ -181,15 +180,15 @@ class LoadingPickServiceTest { when(repository.task(20L)).thenReturn(task); when(repository.items(20L)).thenReturn(List.of(item)); when(repository.items(20L, 101L)).thenReturn(List.of(item)); - when(repository.settlePartial(40L, 30L, 3, 5, 9L, "operator")).thenReturn(1); + when(repository.allTaskItemsResolved(20L)).thenReturn(true); when(repository.allPlanItemsPicked(10L)).thenReturn(true); when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1); - service.review(20L, new ReviewRequest(0, "review-1", "checked"), 9L, "operator"); + service.complete(20L, new CompleteLoadingRequest(0, "complete-1", "checked"), 9L, "operator"); verify(repository).insert(any(LoadingPickOperationEntity.class)); verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator"); - verify(settlementService, never()).settle(20L, 9L, "operator"); + verify(settlementService).settle(20L, 9L, "operator"); verify(repository).syncInboundProjections(20L, 9L, "operator"); } @@ -237,8 +236,8 @@ class LoadingPickServiceTest { verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L); verify(repository, times(1)).skip(31L, 0, 101L); verify(repository, times(1)).touchTask(20L, 0); - verify(settlementService, times(1)).settle(20L, 9L, "operator"); - verify(loadingListService, times(1)).finishFromPda(10L, 20L, 9L, "operator"); + verify(settlementService, never()).settle(20L, 9L, "operator"); + verify(loadingListService, never()).finishFromPda(10L, 20L, 9L, "operator"); verify(repository, times(1)).syncInboundProjections(20L, 9L, "operator"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java index 56925a23e..a53069524 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/PdaPickSettlementServiceTest.java @@ -79,6 +79,30 @@ class PdaPickSettlementServiceTest { verify(splits).convergePartialPlanItem(21L, 9L, "operator"); } + @Test + void reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback() { + LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 3, 1, "PARTIAL"); + resolved(List.of(item)); + when(splits.lockParent(31L)).thenReturn(parent(3)); + when(splits.lockLatestApprovedItemsForInbound(10001L, 31L)) + .thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 3))); + when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation -> + List.of(allocation(1L, 1, 501L, 3))); + when(warehouse.settlementTarget("internal-token", 31L, "PDA-SETTLE-1-31")) + .thenReturn(ApiResponse.success(999L)); + when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success( + new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 2))))); + writes(2, 1); + + service.settle(1L, 9L, "operator"); + + verify(splits).cloneChildWithPurpose(eq(10001L), eq(31L), eq(31L), eq(999L), + eq(1002L), eq("ORDER-REMAINDER"), eq("IN-001-1"), eq(2), + eq(1), eq("REMAINDER"), eq(9L), eq("operator")); + verify(warehouse).settle(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request -> + request.targetBusinessId() == 999L && request.targetPackageCount() == 2)); + } + @Test void repeatedSettleWithExistingChildDoesNotConsumeOrTransferAgain() { LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 10, 6, "PARTIAL"); diff --git a/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java b/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java index 9f2e31b2d..a46cf63b6 100644 --- a/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java +++ b/order/src/main/java/com/baiyue/order/service/OrderFulfillmentSplitService.java @@ -37,7 +37,7 @@ public class OrderFulfillmentSplitService { String hash = hash(req); List replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey()); if (!replay.isEmpty()) { - requireMatchingReplay(replay, hash); + requireMatchingReplay(replay, req); return response(req, splits.lockOrder(req.tenantId(), req.parentOrderId()), replay); } @@ -45,7 +45,7 @@ public class OrderFulfillmentSplitService { if (root == null) throw new BizException(ResultCode.NOT_FOUND, "根订单不存在或不属于该客户"); replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey()); if (!replay.isEmpty()) { - requireMatchingReplay(replay, hash); + requireMatchingReplay(replay, req); return response(req, root, replay); } if (!"MASTER".equals(root.getOrderRole())) throw new BizException(ResultCode.CONFLICT, "根订单层级非法"); @@ -174,14 +174,33 @@ public class OrderFulfillmentSplitService { || r.lines().stream().mapToInt(OrderSplitDtos.QuantityLine::packageCount).sum() != r.sourcePackageCount()) throw new BizException(ResultCode.VALIDATION_FAILED, "来源明细箱数必须唯一且合计等于来源箱数"); } - private void requireMatchingReplay(List rows, String hash) { - if (rows.stream().anyMatch(row -> !Objects.equals(hash, row.getRequestHash()))) + private void requireMatchingReplay(List rows, OrderSplitDtos.CreateRequest request) { + String details = sourceDetails(request); + boolean mismatch = rows.stream().anyMatch(row -> + !Objects.equals(row.getParentOrderId(), request.parentOrderId()) + || !Objects.equals(row.getSourceOrderId(), request.sourceOrderId()) + || !Objects.equals(row.getLoadingPlanId(), request.loadingPlanId()) + || !Objects.equals(row.getContainerBookingId(), request.containerBookingId()) + || !Objects.equals(row.getSourcePackageCount(), request.sourcePackageCount()) + || !Objects.equals(row.getLoadedPackageCount(), request.loadedPackageCount()) + || !Objects.equals(row.getRemainingPackageCount(), request.remainingPackageCount()) + || !Objects.equals(row.getOperationId(), request.operationId()) + || !sameJson(row.getSourceDetailJson(), details)); + if (mismatch) throw new BizException(ResultCode.CONFLICT, "幂等键对应的请求内容不一致"); } private String sourceDetails(OrderSplitDtos.CreateRequest request) { try { return json.writeValueAsString(request.lines()); } catch (Exception exception) { throw new BizException(ResultCode.INTERNAL_ERROR, "无法保存来源明细快照"); } } + + private boolean sameJson(String left, String right) { + try { + return json.readTree(left).equals(json.readTree(right)); + } catch (Exception exception) { + return Objects.equals(left, right); + } + } private String hash(Object value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(json.writeValueAsString(value).getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(); for (byte b : digest) result.append(String.format("%02x", b)); return result.toString(); diff --git a/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java b/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java index 00175cd9c..1597c37c4 100644 --- a/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/OrderFulfillmentSplitServiceTest.java @@ -94,6 +94,31 @@ class OrderFulfillmentSplitServiceTest { verify(orders, never()).insert(any(OrderEntity.class)); } + @Test + void replaysSameBusinessSplitWhenExpectedVersionChangedAfterACompensatingRetry() { + OrderFulfillmentSplitEntity loaded = new OrderFulfillmentSplitEntity(); + loaded.setParentOrderId(100L); loaded.setSourceOrderId(100L); loaded.setChildOrderId(100L); + loaded.setLoadingPlanId(701L); loaded.setContainerBookingId(801L); + loaded.setSourcePackageCount(20); loaded.setLoadedPackageCount(8); loaded.setRemainingPackageCount(12); + loaded.setOperationId("op-key-retry"); loaded.setSourceDetailJson("[{\"sourceDetailId\":501,\"packageCount\":10},{\"sourceDetailId\":502,\"packageCount\":10}]"); + loaded.setSplitRole("LOADED"); + OrderFulfillmentSplitEntity remainder = new OrderFulfillmentSplitEntity(); + remainder.setParentOrderId(100L); remainder.setSourceOrderId(100L); remainder.setChildOrderId(200L); + remainder.setLoadingPlanId(701L); remainder.setContainerBookingId(801L); + remainder.setSourcePackageCount(20); remainder.setLoadedPackageCount(8); remainder.setRemainingPackageCount(12); + remainder.setOperationId("op-key-retry"); remainder.setSourceDetailJson(loaded.getSourceDetailJson()); + remainder.setSplitRole("REMAINDER"); + when(splits.findByIdempotency(10, 100, "key-retry")).thenReturn(List.of(loaded, remainder)); + when(splits.lockOrder(10, 100)).thenReturn(parent()); + + OrderSplitDtos.CreateRequest retry = new OrderSplitDtos.CreateRequest(10L, 100L, 100L, 701L, 801L, + 20, 8, 12, 99, "key-retry", "op-key-retry", List.of( + new OrderSplitDtos.QuantityLine(501L, 10), new OrderSplitDtos.QuantityLine(502L, 10)), "another-operator"); + + assertThat(service.split(retry, 9).remainingPackageCount()).isEqualTo(12); + verify(orders, never()).insert(any(OrderEntity.class)); + } + @Test void rejectsConsumedOrMismatchedRemainderSource() { OrderFulfillmentSplitEntity source = new OrderFulfillmentSplitEntity(); diff --git a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPalletLocationController.java b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPalletLocationController.java index 7e38c63c3..c50811308 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPalletLocationController.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/controller/InternalPalletLocationController.java @@ -83,6 +83,15 @@ public class InternalPalletLocationController { return ApiResponse.success(service.settle(request)); } + @GetMapping("/settlements/target") + @Operation(summary = "查询 PDA 结算目标", description = "跨服务补偿时复用已登记的子入库单资源 ID") + public ApiResponse settlementTarget( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @RequestParam long sourceBusinessId, @RequestParam String idempotencyKey) { + verify(token); + return ApiResponse.success(service.settlementTarget(sourceBusinessId, idempotencyKey)); + } + @GetMapping("/occupied-resources") @Operation(summary = "查询业务当前占用资源", description = "PDA提交前使用的只读实时库存事实") public ApiResponse> occupiedResources( diff --git a/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletLocationSettlementOperationMapper.java b/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletLocationSettlementOperationMapper.java index 24579c4e9..25b57d2c2 100644 --- a/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletLocationSettlementOperationMapper.java +++ b/warehouse/src/main/java/com/baiyue/warehouse/mapper/PalletLocationSettlementOperationMapper.java @@ -30,6 +30,13 @@ public interface PalletLocationSettlementOperationMapper extends BaseMapper