第三期装柜验收优化修改
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user