Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
@@ -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) {}
|
||||
}
|
||||
+2
@@ -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) {}
|
||||
|
||||
+8
-4
@@ -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) {}
|
||||
}
|
||||
|
||||
+7
@@ -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())
|
||||
|
||||
+10
-11
@@ -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>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)>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))>=#{query.expectedArrivalStart}</if>
|
||||
<if test="query.expectedArrivalEnd != null">AND DATE(MIN(eligible.expectedArrivalAt))<=#{query.expectedArrivalEnd}</if>
|
||||
<if test="query.createdStart != null or query.createdEnd != null">
|
||||
<if test="query.createdStart != null">AND DATE(MIN(eligible.createdAt))>=#{query.createdStart}</if>
|
||||
<if test="query.createdEnd != null">AND DATE(MIN(eligible.createdAt))<=#{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 >= #{query.expectedArrivalStart}</if>
|
||||
<if test="query.expectedArrivalEnd != null">AND filter_inbound.expected_arrival_at < DATE_ADD(#{query.expectedArrivalEnd},INTERVAL 1 DAY)</if>
|
||||
<if test="query.createdStart != null">AND filter_inbound.created_at >= #{query.createdStart}</if>
|
||||
<if test="query.createdEnd != null">AND filter_inbound.created_at < 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
|
||||
|
||||
Reference in New Issue
Block a user