fix: complete phase5 overseas deployment contract
This commit is contained in:
+2
-2
@@ -72,7 +72,7 @@ public class PlatformLoadingListController {
|
||||
return ApiResponse.success(service.lifecycle(planId, warehouseId));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改装柜运输字段", description = "已推送后可在装柜完成前首次补录集装箱号;已有柜号的纠正仅限 EDITING 且 PDA 尚未领取或产生物理作业事实,并必须提供原因和版本;柜号不能清空。")
|
||||
@Operation(summary = "修改装柜运输字段", description = "可在装柜完成前首次补录集装箱号;已有柜号的纠正仅限 EDITING 且 PDA 尚未领取或产生物理作业事实,并必须提供原因和版本;柜号不能清空。")
|
||||
@PatchMapping("/loading-plans/{planId}/transport")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||
public ApiResponse<PreloadDetail> updateTransport(
|
||||
@@ -86,7 +86,7 @@ public class PlatformLoadingListController {
|
||||
planId, warehouseId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部关联入库预装票据;要求已推送且关联订舱已放舱,真实集装箱号可在PDA确认装柜完成前补录。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
||||
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部已分配入库预装票据;要求关联订舱已放舱,真实集装箱号可在PDA确认装柜完成前补录。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
||||
@PostMapping("/loading-plans/{planId}/confirm")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
|
||||
|
||||
-4
@@ -18,8 +18,6 @@ public class PlatformSmartLoadingController {
|
||||
public ApiResponse<Plan> draftByBooking(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long bookingId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.draftByBooking(bookingId));}
|
||||
@GetMapping("/plans/{planId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<Plan> detail(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.detail(planId));}
|
||||
@PostMapping("/plans") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:create')")
|
||||
public ApiResponse<Plan> create(@AuthenticationPrincipal AuthenticatedUser u,@Valid @RequestBody CreateRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.create(r.bookingId(),r.remark(),u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> assign(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assign(planId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/inbound-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
@@ -36,6 +34,4 @@ public class PlatformSmartLoadingController {
|
||||
public ApiResponse<Plan> warehouseRemark(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody WarehouseRemarkRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.updateWarehouseRemark(planId,itemId,r,u.userId(),u.username()));}
|
||||
@DeleteMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
|
||||
public ApiResponse<Plan> delete(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody DeleteRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.delete(planId,itemId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/push-preload") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:push')")
|
||||
public ApiResponse<Plan> push(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@RequestHeader("Idempotency-Key") String key,@Valid @RequestBody PushRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.pushPreload(planId,key,r,u.userId(),u.username()));}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ public final class LoadingListDtos {
|
||||
}
|
||||
|
||||
public record PreloadReadiness(
|
||||
boolean preloadPushed,
|
||||
boolean containerNoReady,
|
||||
int preloadLinkCount,
|
||||
int confirmedPreloadCount,
|
||||
|
||||
@@ -15,7 +15,6 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm,
|
||||
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes,
|
||||
Boolean locked, String blockReason) {}
|
||||
public record CreateRequest(@NotNull @Positive Long bookingId, @Size(max=500) String remark) {}
|
||||
public record AssignRequest(@NotNull @Positive Long inboundId,@NotNull @Positive Long inboundDetailId,
|
||||
@NotNull @Positive Integer packageCount,@NotNull @Min(0) Integer expectedVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record AssignInboundRequest(@NotNull @Positive Long inboundId,
|
||||
@@ -35,7 +34,6 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
public record WarehouseRemarkRequest(@Size(max=1000) String warehouseRemark,
|
||||
@NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record PushRequest(@NotNull @Min(0) Integer expectedVersion) {}
|
||||
public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo,
|
||||
Integer version,Boolean canAssign,Boolean canEdit,String blockedReason,String finalCompletionStatus,
|
||||
Long pdaTaskId,String pdaTaskStatus,Integer pdaTaskVersion,List<LoadingPlanItemEntity> items) {}
|
||||
|
||||
@@ -3,7 +3,6 @@ import com.baomidou.mybatisplus.annotation.*; import lombok.Getter; import lombo
|
||||
@Getter @Setter @TableName("fl_loading_plan")
|
||||
public class LoadingPlanEntity {
|
||||
@TableId(type=IdType.ASSIGN_ID) private Long id; private String planNo; private Long containerBookingId; private String planStatus;
|
||||
private String pushIdempotencyKey; private Long preloadPushedBy; private String preloadPushedByName; private LocalDateTime preloadPushedAt;
|
||||
private Long preloadCompletedBy; private String preloadCompletedByName; private LocalDateTime preloadCompletedAt; private Long confirmedBy;
|
||||
private String confirmedByName; private LocalDateTime confirmedAt; private String remark; private Long createdBy; private String createdByName;
|
||||
private LocalDateTime createdAt; private Long updatedBy; private String updatedByName; private LocalDateTime updatedAt; @Version private Integer version;
|
||||
|
||||
+2
-7
@@ -17,7 +17,7 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
|
||||
PreloadSnapshot preload(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Select("SELECT p.id,p.container_booking_id containerBookingId,b.container_no containerNo,"
|
||||
+ "p.plan_status status,p.version,p.preload_pushed_at preloadPushedAt FROM fl_loading_plan p JOIN fl_container_booking b "
|
||||
+ "p.plan_status status,p.version FROM fl_loading_plan p JOIN fl_container_booking b "
|
||||
+ "ON b.id=p.container_booking_id WHERE p.id=#{loadingPlanId}")
|
||||
LoadingPlanSnapshot loadingPlan(@Param("loadingPlanId") long loadingPlanId);
|
||||
|
||||
@@ -86,11 +86,6 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
|
||||
|
||||
record PreloadSnapshot(Long id, Long tenantId, Long inboundId, String status, Integer version) {}
|
||||
record LoadingPlanSnapshot(Long id, Long containerBookingId, String containerNo,
|
||||
String status, Integer version, java.time.LocalDateTime preloadPushedAt) {
|
||||
public LoadingPlanSnapshot(Long id, Long containerBookingId, String containerNo,
|
||||
String status, Integer version) {
|
||||
this(id, containerBookingId, containerNo, status, version, null);
|
||||
}
|
||||
}
|
||||
String status, Integer version) {}
|
||||
record AllocationScope(Long tenantId, Long inboundId, Integer allocatedPackageCount) {}
|
||||
}
|
||||
|
||||
+3
-4
@@ -109,14 +109,13 @@ public class LoadingModelBridgeService {
|
||||
return repository.byLoadingPlan(loadingPlanId);
|
||||
}
|
||||
|
||||
/** Adds a newly allocated inbound to an already pushed EDITING/LOADING plan. */
|
||||
/** Adds a newly allocated inbound to the container's automatically created plan. */
|
||||
@Transactional
|
||||
public void bindIncrementalAllocation(long loadingPlanId, long tenantId, long inboundId,
|
||||
long operatorId, String operatorName, String operationId) {
|
||||
if (repository.byPair(tenantId, inboundId, loadingPlanId) != null) return;
|
||||
var plan = repository.loadingPlan(loadingPlanId);
|
||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
|
||||
if (plan.preloadPushedAt() == null) return;
|
||||
if (!Set.of("EDITING", "LOADING").contains(plan.status())) {
|
||||
throw conflict("PDA任务正在最终确认或已经完成,不能新增预装关联");
|
||||
}
|
||||
@@ -168,8 +167,8 @@ public class LoadingModelBridgeService {
|
||||
public List<Link> requireAllocatedLinksForConfirmation(long loadingPlanId) {
|
||||
var loadingPlan = repository.loadingPlan(loadingPlanId);
|
||||
if (loadingPlan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
|
||||
if (loadingPlan.preloadPushedAt() == null || !"EDITING".equals(loadingPlan.status())) {
|
||||
throw conflict("仅已推送且仍为EDITING的预装计划可以确认");
|
||||
if (!"EDITING".equals(loadingPlan.status())) {
|
||||
throw conflict("仅编辑中的预装计划可以确认");
|
||||
}
|
||||
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
|
||||
if (scopes.isEmpty()) throw conflict("装柜计划尚未分配入库单货物");
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.List;
|
||||
public interface LoadingListQueryMapper {
|
||||
String AGGREGATE_COLUMNS = """
|
||||
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version, p.preload_pushed_at,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.vessel_name, b.voyage_no, b.container_type,
|
||||
(SELECT COUNT(*) FROM fl_preload_loading_plan_link preload_link
|
||||
@@ -49,7 +49,7 @@ public interface LoadingListQueryMapper {
|
||||
|
||||
String AGGREGATE_GROUP = """
|
||||
p.id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version, p.preload_pushed_at,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.vessel_name, b.voyage_no, b.container_type
|
||||
""";
|
||||
@@ -61,10 +61,7 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
WHERE p.preload_pushed_at IS NOT NULL
|
||||
AND p.plan_status IN ('EDITING', 'LOADING')
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0)
|
||||
WHERE p.plan_status IN ('EDITING', 'LOADING')
|
||||
<if test="query.status != null and query.status != ''">AND p.plan_status = #{query.status}</if>
|
||||
<if test="query.customerId != null">AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId})</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
@@ -227,7 +224,7 @@ public interface LoadingListQueryMapper {
|
||||
UPDATE fl_loading_plan
|
||||
SET updated_by = #{operatorId}, updated_by_name = #{operatorName},
|
||||
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{planId} AND plan_status = 'EDITING' AND preload_pushed_at IS NOT NULL
|
||||
WHERE id = #{planId} AND plan_status = 'EDITING'
|
||||
AND version = #{expectedVersion}
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
||||
JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
||||
@@ -249,7 +246,7 @@ public interface LoadingListQueryMapper {
|
||||
SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo},
|
||||
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
|
||||
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
|
||||
WHERE p.id = #{planId} AND p.plan_status='EDITING' AND p.preload_pushed_at IS NOT NULL
|
||||
WHERE p.id = #{planId} AND p.plan_status='EDITING'
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
||||
JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
||||
WHERE task.loading_plan_id=p.id)
|
||||
@@ -271,7 +268,7 @@ public interface LoadingListQueryMapper {
|
||||
SET p.updated_by=#{operatorId}, p.updated_by_name=#{operatorName},
|
||||
p.updated_at=CURRENT_TIMESTAMP(3), p.version=p.version+1
|
||||
WHERE p.id=#{planId} AND p.plan_status IN ('EDITING','LOADING')
|
||||
AND p.preload_pushed_at IS NOT NULL AND p.version=#{expectedVersion}
|
||||
AND p.version=#{expectedVersion}
|
||||
AND b.container_no IS NULL
|
||||
""")
|
||||
int touchForInitialContainerFill(@Param("planId") long planId,
|
||||
@@ -286,7 +283,7 @@ public interface LoadingListQueryMapper {
|
||||
b.updated_by=#{operatorId}, b.updated_by_name=#{operatorName},
|
||||
b.updated_at=CURRENT_TIMESTAMP(3), b.version=b.version+1
|
||||
WHERE p.id=#{planId} AND p.plan_status IN ('EDITING','LOADING')
|
||||
AND p.preload_pushed_at IS NOT NULL AND b.container_no IS NULL
|
||||
AND b.container_no IS NULL
|
||||
AND #{containerNo} IS NOT NULL
|
||||
""")
|
||||
int fillInitialContainerNo(@Param("planId") long planId,
|
||||
@@ -331,7 +328,7 @@ public interface LoadingListQueryMapper {
|
||||
UPDATE fl_loading_plan
|
||||
SET updated_by = #{operatorId}, updated_by_name = #{operatorName},
|
||||
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{planId} AND plan_status = 'EDITING' AND preload_pushed_at IS NOT NULL
|
||||
WHERE id = #{planId} AND plan_status = 'EDITING'
|
||||
AND version = #{expectedVersion}
|
||||
""")
|
||||
int touchForPreloadConfirmation(@Param("planId") long planId,
|
||||
@@ -385,7 +382,6 @@ public interface LoadingListQueryMapper {
|
||||
private String createdByName;
|
||||
private LocalDateTime createdAt;
|
||||
private Integer version;
|
||||
private LocalDateTime preloadPushedAt;
|
||||
private String containerNo;
|
||||
private String cabinetNo;
|
||||
private Long warehouseId;
|
||||
|
||||
@@ -7,17 +7,10 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl
|
||||
WHERE id=#{planId} AND plan_status IN ('EDITING','LOADING') AND version=#{expectedVersion}
|
||||
""")
|
||||
int touchEditing(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan SET push_idempotency_key=#{key},
|
||||
preload_pushed_by=#{operatorId},preload_pushed_by_name=#{operatorName},preload_pushed_at=CURRENT_TIMESTAMP(3),
|
||||
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{planId} AND plan_status='EDITING' AND push_idempotency_key IS NULL AND version=#{expectedVersion}
|
||||
""")
|
||||
int pushPreload(@Param("planId") long planId,@Param("key") String key,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan SET plan_status='LOADING',updated_by=#{operatorId},updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{planId} AND plan_status='EDITING' AND preload_pushed_at IS NOT NULL
|
||||
WHERE id=#{planId} AND plan_status='EDITING'
|
||||
""")
|
||||
int startPicking(@Param("planId") long planId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,6 @@ public class LoadingListRepository {
|
||||
}
|
||||
|
||||
private PreloadReadiness readiness(AggregateRow row) {
|
||||
boolean pushed = row.getPreloadPushedAt() != null;
|
||||
boolean containerReady = org.springframework.util.StringUtils.hasText(row.getContainerNo());
|
||||
int linkCount = Math.toIntExact(value(row.getPreloadLinkCount()));
|
||||
int confirmedCount = Math.toIntExact(value(row.getConfirmedPreloadCount()));
|
||||
@@ -157,19 +156,18 @@ public class LoadingListRepository {
|
||||
boolean editing = "EDITING".equals(row.getPlanStatus());
|
||||
boolean taskCreated = row.getPdaTaskId() != null;
|
||||
java.util.ArrayList<String> blockers = new java.util.ArrayList<>();
|
||||
if (!pushed) blockers.add("PRELOAD_NOT_PUSHED");
|
||||
if (!editing) blockers.add("LOADING_PLAN_NOT_EDITING");
|
||||
if (linkCount == 0) blockers.add("PRELOAD_LINK_MISSING");
|
||||
if (!oceanLinked) blockers.add("OCEAN_BOOKING_NOT_LINKED");
|
||||
else if (!released) blockers.add("OCEAN_BOOKING_NOT_RELEASED");
|
||||
if (taskCreated) blockers.add("PDA_TASK_ALREADY_CREATED");
|
||||
boolean allConfirmed = linkCount > 0 && confirmedCount == linkCount;
|
||||
boolean prerequisitesReady = pushed && editing && linkCount > 0 && released;
|
||||
boolean prerequisitesReady = editing && linkCount > 0 && released;
|
||||
boolean canConfirm = prerequisitesReady && !taskCreated && !allConfirmed;
|
||||
if (allConfirmed) blockers.add("PRELOAD_ALREADY_CONFIRMED");
|
||||
String confirmationStatus = allConfirmed ? "CONFIRMED"
|
||||
: confirmedCount > 0 ? "PARTIALLY_CONFIRMED" : "PENDING";
|
||||
return new PreloadReadiness(pushed, containerReady, linkCount, confirmedCount,
|
||||
return new PreloadReadiness(containerReady, linkCount, confirmedCount,
|
||||
row.getOceanBookingId(), row.getOceanBookingStatus(), row.getPdaTaskId(),
|
||||
row.getPdaTaskStatus(), confirmationStatus, canConfirm,
|
||||
prerequisitesReady && allConfirmed, blockers);
|
||||
|
||||
@@ -99,9 +99,6 @@ public class SmartLoadingRepository {
|
||||
return items.updateWarehouseRemark(planId, itemId, touchedPlanVersion, expectedItemVersion,
|
||||
remark, operatorId, operatorName);
|
||||
}
|
||||
public int push(long planId, String key, int version, long operatorId, String operatorName) {
|
||||
return plans.pushPreload(planId, key, version, operatorId, operatorName);
|
||||
}
|
||||
public int startPicking(long planId, long operatorId, String operatorName) {
|
||||
return plans.startPicking(planId, operatorId, operatorName);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public class ContainerService {
|
||||
@Resource private OceanBookingRepository oceanBookings;
|
||||
@Resource private WarehouseSupplierSnapshotService suppliers;
|
||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Resource private SmartLoadingService smartLoadingService;
|
||||
@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) {
|
||||
@@ -63,7 +64,9 @@ public class ContainerService {
|
||||
value.setContainerType(specification.containerType()); value.setCapacityCbm(specification.capacityCbm());
|
||||
value.setMaxWeightKg(specification.maxWeightKg()); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0); repository.insert(value);
|
||||
value.setPlanStatus(SmartLoadingStatus.WAIT_LOADING.name()); return value;
|
||||
var plan = smartLoadingService.createForContainer(value.getId(), operatorId, operatorName);
|
||||
value.setPlanId(plan.id()); value.setPlanNo(plan.planNo()); value.setPlanStatus(plan.status());
|
||||
return value;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -593,9 +593,8 @@ public class LoadingListService {
|
||||
throw conflict("仅装柜完成前允许首次补录真实集装箱号");
|
||||
}
|
||||
if (!initialContainerFill && !"EDITING".equals(row.getPlanStatus())) {
|
||||
throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号");
|
||||
throw conflict("仅PDA尚未开始的编辑中预装计划可纠错真实集装箱号");
|
||||
}
|
||||
if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段");
|
||||
if (!initialContainerFill && repository.hasPdaWorkStarted(row.getPlanId())) {
|
||||
throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public class LoadingPickService {
|
||||
var booking = smartLoadingRepository.booking(containerBookingId);
|
||||
if (booking == null) return;
|
||||
var plan = smartLoadingRepository.planByBooking(containerBookingId);
|
||||
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||
if (plan == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||
var links = loadingModelBridge.byLoadingPlan(plan.getId());
|
||||
if (links.isEmpty() || links.stream().anyMatch(link -> !"CONFIRMED".equals(link.preloadStatus()))) return;
|
||||
var allocation = oceanBookingRepository.allocationByContainer(containerBookingId);
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** 智能装柜的候选、推荐/人工分配、删除和不可逆推送编排。 */
|
||||
/** 智能装柜的自动计划、候选、推荐、人工分配和删除编排。 */
|
||||
@Service
|
||||
public class SmartLoadingService {
|
||||
private static final BigDecimal CAPACITY_LIMIT_RATIO = new BigDecimal("1.10");
|
||||
@@ -129,15 +129,13 @@ public class SmartLoadingService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PLAN_CREATE", resourceType = "LOADING_PLAN",
|
||||
description = "创建智能装柜计划", captureBefore = false)
|
||||
public Plan create(long bookingId, String remark, long operatorId, String operatorName) {
|
||||
public Plan createForContainer(long bookingId, long operatorId, String operatorName) {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
LoadingPlanEntity existing = repository.planByBooking(bookingId);
|
||||
if (existing != null) return draftView(existing, booking);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setPlanNo(documentNumbers.next("LP"));
|
||||
plan.setContainerBookingId(bookingId); plan.setPlanStatus("EDITING"); plan.setRemark(trim(remark));
|
||||
plan.setContainerBookingId(bookingId); plan.setPlanStatus("EDITING");
|
||||
plan.setCreatedBy(operatorId); plan.setCreatedByName(operatorName); plan.setUpdatedBy(operatorId);
|
||||
plan.setUpdatedByName(operatorName); plan.setVersion(0);
|
||||
try {
|
||||
@@ -542,69 +540,6 @@ public class SmartLoadingService {
|
||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PUSH_PRELOAD", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "推送智能装柜计划到预装清单")
|
||||
public Plan pushPreload(long planId, String idempotencyKey, PushRequest request, long operatorId, String operatorName) {
|
||||
String key = trim(idempotencyKey);
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
|
||||
String requestHash = sha256("PUSH_PRELOAD|" + planId + '|' + request.expectedVersion());
|
||||
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
if (key.equals(plan.getPushIdempotencyKey())) {
|
||||
requirePushReplay(planId, key, requestHash);
|
||||
return view(plan, booking);
|
||||
}
|
||||
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
|
||||
if (repository.operation(key) != null) throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||
List<GroupSnapshot> groups = requireCompleteColoadAllocations(planId, items);
|
||||
ensurePendingPreloads(items, operatorId, operatorName);
|
||||
checkPlanCapacity(booking, items);
|
||||
try {
|
||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||
try {
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key,
|
||||
json(values("version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash)), operatorId, operatorName);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
}
|
||||
LoadingPlanEntity pushed = repository.plan(planId);
|
||||
loadingModelBridge.bindAllocatedBeforeLoading(planId, operatorId, operatorName, key);
|
||||
for (GroupSnapshot group : groups) {
|
||||
// The physical group remains LOCKED through PDA work; it is consumed only after the
|
||||
// full group has been picked and the cabinet completion worker reaches its final stage.
|
||||
requireSameGroup(group, requiredGroup(group.members().get(0).tenantId(),
|
||||
group.members().get(0).inboundId()), planId);
|
||||
}
|
||||
return view(pushed, requireBooking(pushed.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private void requirePushReplay(long planId, String key, String requestHash) {
|
||||
LoadingPlanHistoryEntity history = repository.operation(key);
|
||||
if (history == null
|
||||
|| !Objects.equals(history.getLoadingPlanId(), planId)
|
||||
|| !Objects.equals(history.getActionType(), "PUSH_PRELOAD")
|
||||
|| history.getAfterSnapshot() == null
|
||||
|| !sameRequestHash(history.getAfterSnapshot(), requestHash)) {
|
||||
throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePendingPreloads(List<LoadingPlanItemEntity> items, long operatorId, String operatorName) {
|
||||
if (items.isEmpty()) throw validation("装柜明细不能为空");
|
||||
if (items.stream().anyMatch(item -> item.getAccountId() == null || item.getInboundId() == null)) {
|
||||
throw conflict("装柜明细缺少客户或入库单稳定归属,不能推送预装计划");
|
||||
}
|
||||
items.stream().collect(java.util.stream.Collectors.toMap(
|
||||
item -> item.getAccountId() + "|" + item.getInboundId(), item -> item, (left, right) -> left))
|
||||
.values().forEach(item -> splitPreloadService.ensurePendingForLoadingAllocation(
|
||||
item.getAccountId(), item.getInboundId(), operatorId, operatorName));
|
||||
}
|
||||
|
||||
private List<Candidate> enrichColoadCandidates(ContainerBookingEntity booking, LoadingPlanEntity plan,
|
||||
List<Candidate> values) {
|
||||
Map<String, GroupSnapshot> snapshots = new HashMap<>();
|
||||
|
||||
Reference in New Issue
Block a user