第三期装柜验收优化修改
This commit is contained in:
@@ -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,用于幂等重放');
|
||||
|
||||
+6
-1
@@ -38,7 +38,12 @@ public interface WarehousePalletLocationClient {
|
||||
|
||||
@PostMapping("/settlements")
|
||||
ApiResponse<SettlementResult> settle(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody SettlementRequest request);
|
||||
@RequestBody SettlementRequest request);
|
||||
|
||||
@GetMapping("/settlements/target")
|
||||
ApiResponse<Long> settlementTarget(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestParam("sourceBusinessId") long sourceBusinessId,
|
||||
@RequestParam("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@GetMapping("/occupied-resources")
|
||||
ApiResponse<List<OccupiedResource>> occupiedResources(
|
||||
|
||||
+10
@@ -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<Task> 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<List<Operation>> operations(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
@@ -8,7 +8,7 @@ public enum SmartLoadingStatus {
|
||||
EDITING,
|
||||
/** 拣货中:已生成拣货任务,仍可追加或调整未拣货明细。 */
|
||||
LOADING,
|
||||
/** 装柜完成:PDA 拣货完成后自动完成,计划、明细和货柜信息只读。 */
|
||||
/** 装柜完成:PDA 人工确认装柜完成后进入,计划、明细和货柜信息只读。 */
|
||||
COMPLETED;
|
||||
|
||||
/** 装柜完成前可维护集装箱号和航次。 */
|
||||
|
||||
@@ -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<String> boxCodes,
|
||||
Integer plannedPackageCount, Integer pickedPackageCount,
|
||||
|
||||
@@ -182,8 +182,8 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
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,
|
||||
|
||||
@@ -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<ContainerBookingEntity> 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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<LoadingPickItemEntity> taskItems = repository.items(taskId);
|
||||
PdaPickSettlementPlan.Resolution resolution = PdaPickSettlementPlan.resolve(taskItems);
|
||||
for (List<PdaPickSettlementPlan.SplitLine> 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) {
|
||||
|
||||
@@ -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<Long> 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<Long> expected,
|
||||
List<WarehousePalletLocationClient.SelectiveReleaseResult> actual) {
|
||||
return expected.equals(actual.stream().map(WarehousePalletLocationClient.SelectiveReleaseResult::reservationId)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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]
|
||||
|
||||
+4
-2
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
+1
-1
@@ -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拣货记录的装柜明细仅允许收敛部分拣货实际箱数")
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -37,7 +37,7 @@ public class OrderFulfillmentSplitService {
|
||||
String hash = hash(req);
|
||||
List<OrderFulfillmentSplitEntity> 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<OrderFulfillmentSplitEntity> rows, String hash) {
|
||||
if (rows.stream().anyMatch(row -> !Objects.equals(hash, row.getRequestHash())))
|
||||
private void requireMatchingReplay(List<OrderFulfillmentSplitEntity> 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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
+9
@@ -83,6 +83,15 @@ public class InternalPalletLocationController {
|
||||
return ApiResponse.success(service.settle(request));
|
||||
}
|
||||
|
||||
@GetMapping("/settlements/target")
|
||||
@Operation(summary = "查询 PDA 结算目标", description = "跨服务补偿时复用已登记的子入库单资源 ID")
|
||||
public ApiResponse<Long> 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<List<LocationDtos.OccupiedResource>> occupiedResources(
|
||||
|
||||
+7
@@ -30,6 +30,13 @@ public interface PalletLocationSettlementOperationMapper extends BaseMapper<Pall
|
||||
@Param("sourceService") String sourceService, @Param("sourceBusinessId") long sourceBusinessId,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@Select("SELECT target_business_id FROM wh_pallet_location_settlement_operation "
|
||||
+ "WHERE tenant_id = #{tenantId} AND source_service = #{sourceService} "
|
||||
+ "AND source_business_id = #{sourceBusinessId} AND idempotency_key = #{idempotencyKey} LIMIT 1")
|
||||
Long findTargetBusinessId(@Param("tenantId") long tenantId, @Param("sourceService") String sourceService,
|
||||
@Param("sourceBusinessId") long sourceBusinessId,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_pallet_location_settlement_operation
|
||||
SET operation_status = 'COMPLETED', response_snapshot = #{responseSnapshot},
|
||||
|
||||
+5
@@ -31,6 +31,11 @@ public class PalletLocationSettlementOperationRepository {
|
||||
return mapper.lockByKey(PLATFORM_RESOURCE_TENANT_ID, FIRSTLEG_SOURCE_SERVICE, sourceBusinessId, idempotencyKey);
|
||||
}
|
||||
|
||||
public Long findTargetBusinessId(long sourceBusinessId, String idempotencyKey) {
|
||||
return mapper.findTargetBusinessId(PLATFORM_RESOURCE_TENANT_ID, FIRSTLEG_SOURCE_SERVICE,
|
||||
sourceBusinessId, idempotencyKey);
|
||||
}
|
||||
|
||||
public void complete(long id, String responseSnapshot) {
|
||||
mapper.complete(id, responseSnapshot);
|
||||
}
|
||||
|
||||
+8
@@ -238,6 +238,14 @@ public class PalletLocationAllocationService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 查询已创建的结算目标,供头程在跨服务事务回滚后恢复同一子入库单 ID。 */
|
||||
public Long settlementTarget(long sourceBusinessId, String idempotencyKey) {
|
||||
if (sourceBusinessId < 1 || idempotencyKey == null || idempotencyKey.isBlank()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "结算查询参数不能为空");
|
||||
}
|
||||
return settlementOperations.findTargetBusinessId(sourceBusinessId, idempotencyKey.trim());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PALLET_LOCATION_REBIND_TEMPORARY", resourceType = "LOCATION_OCCUPANCY",
|
||||
resourceId = "#request.occupancyId", description = "A0临时库位换绑到普通库位")
|
||||
|
||||
Reference in New Issue
Block a user