This commit is contained in:
2026-09-03 21:25:53 +08:00
parent 171f450e59
commit dd1682a83b
24 changed files with 937 additions and 58 deletions
@@ -0,0 +1,33 @@
package com.baiyue.firstleg.client;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/** Order-owned pending-review rows for the SmartLoading read model. */
@FeignClient(name = "order-service", contextId = "firstlegPendingReviewOrderClient",
url = "${baiyue.clients.order.url:}", path = "/internal/order/v1/pending-review-orders")
public interface PendingReviewOrderClient {
@GetMapping
ApiResponse<PageResult<Row>> page(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam(required = false) List<String> channelCodes,
@RequestParam(required = false) Long warehouseId,
@RequestParam(required = false) Long customerId,
@RequestParam(required = false) String inboundCode,
@RequestParam(required = false) LocalDate createdStart,
@RequestParam(required = false) LocalDate createdEnd,
@RequestParam long page,
@RequestParam long size);
record Row(Long orderId, String orderNo, String inboundCode, Long customerId,
String customerName, Long warehouseId, String channelCode, String channelName,
int totalPackageCount, LocalDateTime createdAt, String mainStatus) {}
}
@@ -24,6 +24,8 @@ public class PlatformSmartLoadingController {
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')")
public ApiResponse<Plan> assignInbound(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignInboundRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignInbound(planId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/pending-review-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> assignPendingReview(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignPendingReviewRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignPendingReview(planId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/group-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> assignGroup(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignGroupRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignGroup(planId,r,u.userId(),u.username()));}
@DeleteMapping("/plans/{planId}/group-assignments/{groupId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
@@ -7,7 +7,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record InboundCandidateQuery(
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode,
LocalDate expectedArrivalStart, LocalDate expectedArrivalEnd, CandidateOrderStatus orderStatus,
LocalDate createdStart, LocalDate createdEnd, CandidateOrderStatus orderStatus,
@Min(1) long page, @Min(1) @Max(200) long size) {
public InboundCandidateQuery {
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
@@ -32,7 +32,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record WorkbenchContainerQuery(
@NotEmpty @Size(max=9) List<@NotBlank String> channelGroupCodes,
@Positive Long warehouseId, @Positive Long customerId, @Size(max=40) String inboundCode,
LocalDate expectedArrivalStart, LocalDate expectedArrivalEnd, CandidateOrderStatus orderStatus,
LocalDate createdStart, LocalDate createdEnd, CandidateOrderStatus orderStatus,
@Min(1) long page, @Min(1) @Max(200) long size) {
public WorkbenchContainerQuery {
channelGroupCodes = channelGroupCodes == null ? List.of() : List.copyOf(channelGroupCodes);
@@ -53,6 +53,10 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record AssignInboundRequest(@NotNull @Positive Long inboundId,
@NotNull @Positive Integer packageCount,@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max=128) String operationId) {}
public record AssignPendingReviewRequest(@NotNull @Positive Long customerId,
@NotNull @Positive Long orderId,@NotBlank @Size(max=40) String inboundCode,
@NotNull @Positive Integer packageCount,
@NotNull @Min(0) Integer expectedVersion,@NotBlank @Size(max=128) String operationId) {}
public record AssignGroupRequest(@NotNull @Positive Long inboundId,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max=128) String operationId) {}
@@ -27,9 +27,12 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
int allocatedPackages(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("loadingPlanId") long loadingPlanId);
@Select("SELECT account_id tenantId,inbound_id inboundId,COALESCE(SUM(package_count),0) allocatedPackageCount "
+ "FROM fl_loading_plan_item WHERE loading_plan_id=#{loadingPlanId} AND deleted=0 "
+ "GROUP BY account_id,inbound_id HAVING SUM(package_count)>0")
@Select("SELECT item.account_id tenantId,item.inbound_id inboundId,"
+ "COALESCE(SUM(item.package_count),0) allocatedPackageCount,MAX(inbound.process_status) processStatus "
+ "FROM fl_loading_plan_item item JOIN fl_inbound_order inbound "
+ "ON inbound.tenant_id=item.account_id AND inbound.id=item.inbound_id AND inbound.deleted=0 "
+ "WHERE item.loading_plan_id=#{loadingPlanId} AND item.deleted=0 "
+ "GROUP BY item.account_id,item.inbound_id HAVING SUM(item.package_count)>0")
List<AllocationScope> allocatedScopes(@Param("loadingPlanId") long loadingPlanId);
@Update("UPDATE fl_preload_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName},"
@@ -87,5 +90,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) {}
record AllocationScope(Long tenantId, Long inboundId, Integer allocatedPackageCount) {}
record AllocationScope(Long tenantId, Long inboundId, Integer allocatedPackageCount,
String processStatus) {}
}
@@ -81,6 +81,10 @@ public class LoadingModelBridgeService {
if (loadingPlan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
if (scopes.isEmpty()) throw conflict("装柜计划尚未分配入库单货物,不能开始PDA作业");
if ("EDITING".equals(loadingPlan.status())
&& scopes.stream().anyMatch(scope -> !"WAIT_SPLIT".equals(scope.processStatus()))) {
throw conflict("装柜清单仍有订单未完成审核和测量,不能开始PDA作业");
}
if (!"EDITING".equals(loadingPlan.status())) {
if (Set.of("LOADING", "COMPLETED").contains(loadingPlan.status())
&& scopes.stream().allMatch(scope -> repository.byPair(
@@ -172,6 +176,9 @@ public class LoadingModelBridgeService {
}
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
if (scopes.isEmpty()) throw conflict("装柜计划尚未分配入库单货物");
if (scopes.stream().anyMatch(scope -> !"WAIT_SPLIT".equals(scope.processStatus()))) {
throw conflict("装柜清单仍有订单未完成审核和测量,不能确认预装");
}
List<Link> links = repository.byLoadingPlan(loadingPlanId);
boolean complete = scopes.stream().allMatch(scope -> links.stream().anyMatch(link ->
Objects.equals(link.tenantId(), scope.tenantId())
@@ -85,11 +85,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
CASE WHEN current_group.id IS NULL THEN eligible.inboundId END inboundId,
MIN(eligible.expectedArrivalAt) sortArrivalAt,MIN(eligible.inboundId) sortInboundId
FROM (
SELECT detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,
SELECT detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,detail.createdAt,
SUM(detail.availablePackageCount) availablePackageCount
FROM (
SELECT o.tenant_id accountId,o.id inboundId,o.inbound_code inboundCode,
o.expected_arrival_at expectedArrivalAt,i.inbound_detail_id,
o.expected_arrival_at expectedArrivalAt,o.created_at createdAt,i.inbound_detail_id,
SUM(i.package_count)
-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
WHERE li.account_id=o.tenant_id AND li.inbound_id=o.id
@@ -111,10 +111,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
AND o.process_status='WAIT_SPLIT'
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
GROUP BY o.tenant_id,o.id,o.inbound_code,o.expected_arrival_at,i.inbound_detail_id
GROUP BY o.tenant_id,o.id,o.inbound_code,o.expected_arrival_at,o.created_at,i.inbound_detail_id
HAVING availablePackageCount&gt;0
) detail
GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt
GROUP BY detail.accountId,detail.inboundId,detail.inboundCode,detail.expectedArrivalAt,detail.createdAt
) eligible
LEFT JOIN fl_coload_group_member current_member
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
@@ -136,10 +136,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">
AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)&gt;0
</if>
<if test="query.expectedArrivalStart != null or query.expectedArrivalEnd != null">
AND MIN(eligible.expectedArrivalAt) IS NOT NULL
<if test="query.expectedArrivalStart != null">AND DATE(MIN(eligible.expectedArrivalAt))&gt;=#{query.expectedArrivalStart}</if>
<if test="query.expectedArrivalEnd != null">AND DATE(MIN(eligible.expectedArrivalAt))&lt;=#{query.expectedArrivalEnd}</if>
<if test="query.createdStart != null or query.createdEnd != null">
<if test="query.createdStart != null">AND DATE(MIN(eligible.createdAt))&gt;=#{query.createdStart}</if>
<if test="query.createdEnd != null">AND DATE(MIN(eligible.createdAt))&lt;=#{query.createdEnd}</if>
</if>
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
</script>
@@ -217,7 +216,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
AND b.channel_code IN
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
<if test="query.warehouseId != null">AND b.warehouse_id=#{query.warehouseId}</if>
<if test="query.customerId != null or query.inboundCode != null or query.expectedArrivalStart != null or query.expectedArrivalEnd != null or query.orderStatus != null">
<if test="query.customerId != null or query.inboundCode != null or query.createdStart != null or query.createdEnd != null or query.orderStatus != null">
AND EXISTS (
SELECT 1 FROM fl_loading_plan_item filter_item
JOIN fl_inbound_order filter_inbound ON filter_inbound.tenant_id=filter_item.account_id
@@ -225,8 +224,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
WHERE filter_item.loading_plan_id=p.id AND filter_item.deleted=0
<if test="query.customerId != null">AND filter_item.account_id=#{query.customerId}</if>
<if test="query.inboundCode != null and query.inboundCode != ''">AND filter_item.inbound_code LIKE CONCAT('%',#{query.inboundCode},'%')</if>
<if test="query.expectedArrivalStart != null">AND filter_inbound.expected_arrival_at &gt;= #{query.expectedArrivalStart}</if>
<if test="query.expectedArrivalEnd != null">AND filter_inbound.expected_arrival_at &lt; DATE_ADD(#{query.expectedArrivalEnd},INTERVAL 1 DAY)</if>
<if test="query.createdStart != null">AND filter_inbound.created_at &gt;= #{query.createdStart}</if>
<if test="query.createdEnd != null">AND filter_inbound.created_at &lt; DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)</if>
<if test="orderStatus != null">
<choose>
<when test="orderStatus == 'PENDING_REVIEW'">AND filter_inbound.order_status_snapshot IN ('SUBMITTED','PENDING_REVIEW')</when>
@@ -68,6 +68,12 @@ public class SmartLoadingRepository {
.eq(InboundOrderEntity::getTenantId, accountId)
.eq(InboundOrderEntity::getId, inboundId));
}
public InboundOrderEntity inboundForUpdate(long accountId, long inboundId) {
return inbounds.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, accountId)
.eq(InboundOrderEntity::getId, inboundId)
.last("FOR UPDATE"));
}
public List<LoadingPlanItemEntity> activeItemsByInbound(long planId, long accountId, long inboundId) {
return items.selectList(new LambdaQueryWrapper<LoadingPlanItemEntity>()
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
@@ -7,6 +7,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.firstleg.dto.SmartLoadingDtos.*;
import com.baiyue.firstleg.client.OrderCargoClient;
import com.baiyue.firstleg.client.PendingReviewOrderClient;
import com.baiyue.firstleg.entity.*;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
@@ -54,6 +55,8 @@ public class SmartLoadingService {
);
@Resource private SmartLoadingRepository repository;
@Resource private OrderCargoClient orderCargoClient;
@Resource private PendingReviewOrderClient pendingReviewOrders;
@Resource private InboundOrderCreationService inboundOrderCreationService;
@Resource private LoadingPickService loadingPickService;
@Resource private LoadingModelBridgeService loadingModelBridge;
@Resource private SplitPreloadService splitPreloadService;
@@ -113,11 +116,14 @@ public class SmartLoadingService {
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
@Transactional(readOnly = true)
public PageResult<InboundCandidate> inboundCandidates(InboundCandidateQuery query) {
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
throw validation("预计到货开始日期不能晚于结束日期");
if (query.createdStart() != null && query.createdEnd() != null
&& query.createdStart().isAfter(query.createdEnd())) {
throw validation("创建开始日期不能晚于结束日期");
}
Set<String> actualChannels = actualChannels(query.channelGroupCodes());
if (query.orderStatus() == CandidateOrderStatus.PENDING_REVIEW) {
return pendingReviewCandidates(query, actualChannels);
}
if (query.orderStatus() != null && query.orderStatus() != CandidateOrderStatus.WAIT_SPLIT) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
@@ -149,10 +155,33 @@ public class SmartLoadingService {
return new PageResult<>(records, unitPage.page(), unitPage.size(), unitPage.total(), unitPage.pages());
}
private PageResult<InboundCandidate> pendingReviewCandidates(InboundCandidateQuery query,
Set<String> actualChannels) {
ApiResponse<PageResult<PendingReviewOrderClient.Row>> response;
try {
response = pendingReviewOrders.page(internalToken, List.copyOf(actualChannels), query.warehouseId(),
query.customerId(), query.inboundCode(), query.createdStart(), query.createdEnd(),
query.page(), query.size());
} catch (RuntimeException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "待审核订单服务暂时不可用");
}
if (response == null || !response.successful() || response.data() == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "待审核订单查询响应无效");
}
PageResult<PendingReviewOrderClient.Row> source = response.data();
List<InboundCandidate> records = source.records().stream().map(row ->
new InboundCandidate(row.customerId(), null, row.orderId(), row.orderNo(), row.inboundCode(),
row.warehouseId(), row.channelCode(), row.channelName(), null, null, null,
row.customerName(), null, row.totalPackageCount(), null, null, null, null,
null, null, null, null, false, null,
"PENDING_REVIEW", null, List.of())).toList();
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
}
public PageResult<WorkbenchContainer> containers(WorkbenchContainerQuery query) {
if (query.expectedArrivalStart() != null && query.expectedArrivalEnd() != null
&& query.expectedArrivalStart().isAfter(query.expectedArrivalEnd())) {
throw validation("预计到货开始日期不能晚于结束日期");
if (query.createdStart() != null && query.createdEnd() != null
&& query.createdStart().isAfter(query.createdEnd())) {
throw validation("创建开始日期不能晚于结束日期");
}
return repository.workbenchContainers(query, actualChannels(query.channelGroupCodes()));
}
@@ -349,6 +378,86 @@ public class SmartLoadingService {
return assignInbound(planId, request, operatorId, operatorName, false);
}
/**
* Reserves declared cartons in a cabinet before order review. Review status is not an allocation
* gate; confirmation/PDA remains blocked until the resulting inbound reaches WAIT_SPLIT.
*/
@Transactional
@BusinessAudit(event = "SMART_LOADING_PENDING_REVIEW_ASSIGN", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "待审核订单加入装柜清单")
public Plan assignPendingReview(long planId, AssignPendingReviewRequest request,
long operatorId, String operatorName) {
String operation = operation(request.operationId());
String requestHash = sha256("ASSIGN_PENDING_REVIEW|" + planId + '|' + request.customerId()
+ '|' + request.orderId() + '|' + request.inboundCode() + '|' + request.packageCount()
+ '|' + request.expectedVersion());
Plan replay = replay(operation, planId, "ADD_PENDING_REVIEW_ORDER", null, requestHash);
if (replay != null) return replay;
PendingReviewOrderClient.Row pending = requirePendingReviewOrder(request);
var snapshot = loadInboundSnapshot(request.customerId(), request.orderId());
if (!Objects.equals(snapshot.tenantId(), request.customerId())
|| !Objects.equals(snapshot.orderId(), request.orderId())
|| !Objects.equals(snapshot.inboundCode(), request.inboundCode())) {
throw conflict("待审核订单快照已变化,请刷新后重试");
}
long inboundId = inboundOrderCreationService.prepareFromDraft(snapshot);
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
if (!"EDITING".equals(plan.getPlanStatus())) {
throw conflict("待审核订单只能加入编辑中的装柜清单");
}
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
InboundOrderEntity lockedInbound = repository.inboundForUpdate(request.customerId(), inboundId);
if (lockedInbound == null || !Set.of("PREPARING", "WAIT_MEASURE", "WAIT_SPLIT")
.contains(lockedInbound.getProcessStatus())) {
throw conflict("订单当前状态不能加入装柜清单");
}
if (!repository.activeItemsByInbound(request.customerId(), inboundId).isEmpty()) {
throw conflict("该入仓码已被有效柜次占用");
}
List<DeclaredCargo> declared = loadDeclaredCargo(request.customerId(), request.orderId());
List<Candidate> candidates = declared.stream().map(value -> declaredCandidate(
pending, snapshot, inboundId, value.cargo())).toList();
List<InboundAllocation> allocations = proportionalAllocations(candidates, request.packageCount());
List<LoadingPlanItemEntity> current = new ArrayList<>(repository.items(planId));
List<LoadingPlanItemEntity> created = new ArrayList<>();
int sequence = current.size();
for (InboundAllocation allocation : allocations) {
if (allocation.packageCount() == 0) continue;
Candidate candidate = allocation.candidate();
requireSameChannel(booking, candidate);
requireSameCustomerScope(booking, candidate);
checkRouteAndCapacity(booking, current, candidate, allocation.packageCount(), true);
OrderCargoClient.CargoBinding cargo = declared.stream()
.map(DeclaredCargo::cargo)
.filter(value -> Objects.equals(value.cargoId(), candidate.inboundDetailId()))
.findFirst().orElseThrow(() -> conflict("订单货物明细已变化,请刷新后重试"));
LoadingPlanItemEntity item = snapshot(planId, candidate, cargo, allocation.packageCount(),
operatorId, operatorName, ++sequence);
current.add(item);
created.add(item);
}
if (created.isEmpty()) throw validation("订单没有可加入装柜清单的箱数");
splitPreloadService.ensurePendingForPreAllocation(request.customerId(), inboundId,
operatorId, operatorName);
touch(plan, operatorId, operatorName);
try {
created.forEach(repository::insert);
} catch (DuplicateKeyException exception) {
throw conflict("该入仓码已被并发加入其他柜次");
}
history(planId, null, "ADD_PENDING_REVIEW_ORDER", plan.getPlanStatus(), plan.getPlanStatus(),
operation, json(values("version", request.expectedVersion(), "orderId", request.orderId(),
"inboundCode", request.inboundCode())),
json(values("requestHash", requestHash, "inboundId", inboundId,
"packageCount", request.packageCount(), "provisional", true)), operatorId, operatorName);
loadingModelBridge.bindIncrementalAllocation(planId, request.customerId(), inboundId,
operatorId, operatorName, operation);
created.forEach(item -> loadingPickService.sync(planId, item));
return view(repository.plan(planId), booking);
}
private Plan assignInbound(long planId, AssignInboundRequest request,
long operatorId, String operatorName, boolean wholeGroupAssignment) {
String operation = operation(request.operationId());
@@ -902,6 +1011,85 @@ public class SmartLoadingService {
throw unavailable();
}
}
private PendingReviewOrderClient.Row requirePendingReviewOrder(AssignPendingReviewRequest request) {
try {
ApiResponse<PageResult<PendingReviewOrderClient.Row>> response = pendingReviewOrders.page(
internalToken, null, null, request.customerId(), request.inboundCode(), null, null, 1, 2);
if (response == null || !response.successful() || response.data() == null) throw unavailable();
return response.data().records().stream()
.filter(row -> Objects.equals(row.orderId(), request.orderId())
&& Objects.equals(row.customerId(), request.customerId())
&& Objects.equals(row.inboundCode(), request.inboundCode())
&& "PENDING_REVIEW".equals(row.mainStatus()))
.findFirst().orElseThrow(() -> conflict("订单状态或入仓码已变化,请刷新候选后重试"));
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable();
}
}
private com.baiyue.firstleg.dto.OrderApprovedInboundCommand loadInboundSnapshot(long customerId,
long orderId) {
try {
ApiResponse<com.baiyue.firstleg.dto.OrderApprovedInboundCommand> response =
orderCargoClient.inboundSnapshot(internalToken, customerId, orderId);
if (response == null || !response.successful() || response.data() == null) throw unavailable();
return response.data();
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable();
}
}
private List<DeclaredCargo> loadDeclaredCargo(long customerId, long orderId) {
try {
ApiResponse<List<OrderCargoClient.CargoLabelBinding>> response =
orderCargoClient.listForCartonLabels(internalToken, customerId, orderId);
if (response == null || !response.successful() || response.data() == null
|| response.data().isEmpty()) throw conflict("订单没有可加入装柜清单的货物");
List<DeclaredCargo> result = new ArrayList<>();
for (OrderCargoClient.CargoLabelBinding label : response.data()) {
if (label.cargoId() == null || label.packageCount() == null || label.packageCount() <= 0) {
throw conflict("订单货物箱数不完整,请先修正订单");
}
ApiResponse<OrderCargoClient.CargoBinding> detail = orderCargoClient.detail(
internalToken, customerId, orderId, label.cargoId());
if (detail == null || !detail.successful() || detail.data() == null
|| !Objects.equals(detail.data().cargoId(), label.cargoId())
|| !Objects.equals(detail.data().orderId(), orderId)) {
throw conflict("订单货物明细已变化,请刷新后重试");
}
result.add(new DeclaredCargo(detail.data()));
}
return List.copyOf(result);
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable();
}
}
private Candidate declaredCandidate(PendingReviewOrderClient.Row pending,
com.baiyue.firstleg.dto.OrderApprovedInboundCommand snapshot,
long inboundId, OrderCargoClient.CargoBinding cargo) {
int boxes = cargo.originalPackageCount() == null ? 0 : cargo.originalPackageCount();
if (boxes <= 0) throw conflict("订单货物箱数不完整,请先修正订单");
BigDecimal divisor = BigDecimal.valueOf(boxes);
BigDecimal unitGross = unit(cargo.originalGrossWeightKg()).divide(divisor, 6, RoundingMode.HALF_UP);
BigDecimal unitNet = unit(cargo.netWeightKg()).divide(divisor, 6, RoundingMode.HALF_UP);
BigDecimal unitVolume = unit(cargo.originalVolumeCbm()).divide(divisor, 6, RoundingMode.HALF_UP);
return new Candidate(snapshot.tenantId(), inboundId, cargo.cargoId(), snapshot.orderId(),
snapshot.orderNo(), snapshot.inboundCode(), snapshot.warehouseId(), snapshot.channelCode(),
snapshot.channelName(), snapshot.destinationWarehouseId(), snapshot.destinationCode(),
snapshot.destinationName(), snapshot.customerName(), cargo.cargoName(), boxes,
unitGross, unitNet, unitVolume, null, null, null, null, false, null,
pending.mainStatus(), snapshot.expectedArrivalAt());
}
private record DeclaredCargo(OrderCargoClient.CargoBinding cargo) {}
private void checkRouteAndCapacity(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c,
int count, boolean requireSameDestination){
requireSameChannel(b, c);
@@ -72,6 +72,36 @@ public class SplitPreloadService {
ensureLoadingAllocationLifecycle(tenantId, inboundId, operatorId, operatorName, false);
}
/** Creates a provisional preload lifecycle for an order selected before review/measurement. */
@Transactional
public void ensurePendingForPreAllocation(long tenantId, long inboundId, long operatorId,
String operatorName) {
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
if (!Set.of("PREPARING", "WAIT_MEASURE", "WAIT_SPLIT").contains(inbound.processStatus())) {
throw new BizException(ResultCode.CONFLICT, "订单当前状态不能加入装柜清单");
}
PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId);
if (plan != null) {
if (!PreloadStatus.PENDING.equals(plan.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "预装计划已进入确认流程,不能修改智能装柜分配");
}
return;
}
plan = new PreloadPlanEntity();
plan.setTenantId(tenantId); plan.setInboundId(inboundId); plan.setStatus(PreloadStatus.PENDING);
plan.setVersion(0); plan.setItemsJson("[]"); plan.setRemark("订单审核前加入装柜清单自动建立");
plan.setCreatedBy(operatorId); plan.setCreatedByName(operatorName);
plan.setUpdatedBy(operatorId); plan.setUpdatedByName(operatorName);
try {
repository.createPlan(plan);
} catch (DuplicateKeyException exception) {
PreloadPlanEntity concurrent = repository.findPlan(tenantId, inboundId);
if (concurrent == null || !PreloadStatus.PENDING.equals(concurrent.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "预装计划已被并发修改,请刷新后重试");
}
}
}
/**
* A running PDA task may restore or add an unpicked allocation after the plan-level preload
* confirmation. A running PDA task owns the mutation lock, so CONFIRMING and CONFIRMED