feat: add inbound hold lifecycle
This commit is contained in:
+40
@@ -10,9 +10,12 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.InboundOrderDetail;
|
||||
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.CommandRequest;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.Detail;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.InboundCartonLabelPrintService;
|
||||
import com.baiyue.firstleg.service.InboundOrderQueryService;
|
||||
import com.baiyue.firstleg.service.InboundHoldService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -39,6 +42,7 @@ import java.util.List;
|
||||
@RequestMapping("/api/platform/inbounds")
|
||||
public class PlatformInboundOrderController {
|
||||
@Resource private InboundOrderQueryService service;
|
||||
@Resource private InboundHoldService holdService;
|
||||
@Resource private InboundCartonLabelPrintService cartonLabelPrintService;
|
||||
|
||||
@Operation(summary = "分页查询入库单",
|
||||
@@ -68,6 +72,42 @@ public class PlatformInboundOrderController {
|
||||
return ApiResponse.success(service.detail(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询入库单扣货快照与不可变历史")
|
||||
@GetMapping("/{inboundId}/hold")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:list')")
|
||||
public ApiResponse<Detail> holdDetail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
return ApiResponse.success(holdService.detail(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "扣货入库单")
|
||||
@PostMapping("/{inboundId}/hold")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:hold')")
|
||||
public ApiResponse<Detail> hold(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @org.springframework.web.bind.annotation.RequestBody CommandRequest request) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
return ApiResponse.success(holdService.hold(tenantId, inboundId, user.userId(), user.username(),
|
||||
idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "解除入库单扣货")
|
||||
@PostMapping("/{inboundId}/hold/release")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:hold')")
|
||||
public ApiResponse<Detail> releaseHold(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @org.springframework.web.bind.annotation.RequestBody CommandRequest request) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
return ApiResponse.success(holdService.release(tenantId, inboundId, user.userId(), user.username(),
|
||||
idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "生成入仓箱唛 PDF",
|
||||
description = "首次调用按订单货物明细冻结整单箱号;重复打印及指定箱下标补打复用原箱号映射。")
|
||||
@PostMapping("/{inboundId}/carton-label-pdf")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** Platform inbound hold command and immutable history contracts. */
|
||||
public final class InboundHoldDtos {
|
||||
private InboundHoldDtos() { }
|
||||
|
||||
public record CommandRequest(
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 1000) String reason,
|
||||
@Positive Long evidenceFileId
|
||||
) { }
|
||||
|
||||
public record HistoryItem(
|
||||
Long id,
|
||||
String action,
|
||||
String reason,
|
||||
Long evidenceFileId,
|
||||
String operatorName,
|
||||
LocalDateTime operatedAt
|
||||
) { }
|
||||
|
||||
public record Detail(
|
||||
Long inboundId,
|
||||
Long tenantId,
|
||||
boolean holdFlag,
|
||||
String currentHoldReason,
|
||||
Integer version,
|
||||
boolean canHold,
|
||||
boolean canRelease,
|
||||
String actionBlockReason,
|
||||
List<HistoryItem> history
|
||||
) { }
|
||||
}
|
||||
+5
@@ -12,6 +12,7 @@ import com.baiyue.firstleg.loading.domain.LoadingEvidenceType;
|
||||
import com.baiyue.firstleg.loading.entity.CabinetLoadingEvidenceEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.CabinetLoadingEvidenceMapper;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.service.InboundHoldGuard;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
@@ -41,10 +42,12 @@ public class CabinetLoadingEvidenceService {
|
||||
@Resource private LoadingPickRepository loadingPicks;
|
||||
@Resource private LoadingEvidenceFileClient files;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private InboundHoldGuard inboundHoldGuard;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
@Transactional
|
||||
public EvidenceList upsert(long taskId, String key, UpsertRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
return upsert(taskId, key, request, operatorId, operatorName, hash(request));
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ public class CabinetLoadingEvidenceService {
|
||||
public EvidenceList upload(long taskId, long warehouseId, String key, int expectedVersion,
|
||||
String evidenceType, String sealNo, String remark, MultipartFile multipart,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
Scope scope = scope(taskId, warehouseId, operatorId, true);
|
||||
requireKey(key);
|
||||
String requestHash = uploadHash(warehouseId, expectedVersion, evidenceType, sealNo, remark, multipart);
|
||||
@@ -128,6 +132,7 @@ public class CabinetLoadingEvidenceService {
|
||||
@Transactional
|
||||
public EvidenceList voidEvidence(long taskId, long evidenceId, VoidRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
Scope scope = scope(taskId, request.warehouseId(), operatorId, true);
|
||||
CabinetLoadingEvidenceEntity value = mapper.byContainer(scope.containerBookingId()).stream()
|
||||
.filter(item -> Objects.equals(item.getId(), evidenceId)).findFirst()
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.service.FbaPricingService;
|
||||
import com.baiyue.firstleg.service.InboundHoldGuard;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -43,6 +44,7 @@ public class LoadingDeliveryService {
|
||||
@Resource private CabinetLoadingEvidenceService cabinetEvidenceService;
|
||||
@Resource private FbaPricingService pricingService;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private InboundHoldGuard inboundHoldGuard;
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse completeLoading(long tenantId, long inboundId, long operatorId,
|
||||
@@ -54,6 +56,7 @@ public class LoadingDeliveryService {
|
||||
long operatorId, String operatorName, String key,
|
||||
CompleteLoadingRequest request,
|
||||
boolean acceptedPdaCompletion) {
|
||||
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, COMPLETE_LOADING,
|
||||
key, hash, acceptedPdaCompletion);
|
||||
@@ -106,6 +109,7 @@ public class LoadingDeliveryService {
|
||||
* every external/business hard gate must pass before the PDA task is mutated.
|
||||
*/
|
||||
public void requirePdaCompletionReady(long tenantId, long inboundId) {
|
||||
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
||||
Snapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if (!"LOADING".equals(inbound.processStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "PDA确认装柜完成前订单必须处于LOADING");
|
||||
@@ -144,6 +148,7 @@ public class LoadingDeliveryService {
|
||||
@Transactional
|
||||
public PickingResponse startPicking(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String key, PickingRequest request) {
|
||||
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, "START_PICKING", key, hash);
|
||||
if (replay != null) return pickResponse(tenantId, inboundId);
|
||||
@@ -171,6 +176,7 @@ public class LoadingDeliveryService {
|
||||
@Transactional
|
||||
public PickingResponse completePicking(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String key, PickingRequest request) {
|
||||
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
||||
String hash = hash(request);
|
||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, "COMPLETE_PICKING", key, hash);
|
||||
if (replay != null) return pickResponse(tenantId, inboundId);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/** Authoritative fail-closed hold and physical-work predicates. */
|
||||
@Mapper
|
||||
public interface InboundHoldGateMapper {
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM fl_inbound_order inbound
|
||||
WHERE inbound.tenant_id=#{tenantId} AND inbound.id=#{inboundId} AND inbound.deleted=0
|
||||
AND inbound.hold_flag=1
|
||||
""")
|
||||
int countHeldInbound(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
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=#{planId} AND item.deleted=0 AND inbound.hold_flag=1
|
||||
""")
|
||||
int countHeldByPlan(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM fl_loading_pick_task task
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=task.loading_plan_id AND item.deleted=0
|
||||
JOIN fl_inbound_order inbound
|
||||
ON inbound.tenant_id=item.account_id AND inbound.id=item.inbound_id AND inbound.deleted=0
|
||||
WHERE task.id=#{taskId} AND inbound.hold_flag=1
|
||||
""")
|
||||
int countHeldByTask(@Param("taskId") long taskId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM fl_loading_plan plan
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=plan.id AND item.deleted=0
|
||||
JOIN fl_inbound_order inbound
|
||||
ON inbound.tenant_id=item.account_id AND inbound.id=item.inbound_id AND inbound.deleted=0
|
||||
WHERE plan.container_booking_id=#{containerBookingId} AND inbound.hold_flag=1
|
||||
""")
|
||||
int countHeldByContainer(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@Select("""
|
||||
SELECT CASE WHEN
|
||||
inbound.process_status IN ('LOADED','DELIVERED')
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item plan_item
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id=plan_item.id
|
||||
WHERE plan_item.account_id=inbound.tenant_id AND plan_item.inbound_id=inbound.id
|
||||
AND plan_item.deleted=0 AND pick_item.picked_package_count>0
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item plan_item
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id=plan_item.id
|
||||
JOIN fl_loading_work_pallet_carton carton ON carton.pick_item_id=pick_item.id
|
||||
WHERE plan_item.account_id=inbound.tenant_id AND plan_item.inbound_id=inbound.id
|
||||
AND plan_item.deleted=0
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM fl_outbound_dispatch dispatch
|
||||
WHERE dispatch.tenant_id=inbound.tenant_id AND dispatch.inbound_id=inbound.id
|
||||
AND dispatch.status='WAITING_FOR_LOADING'
|
||||
) THEN 1 ELSE 0 END
|
||||
FROM fl_inbound_order inbound
|
||||
WHERE inbound.tenant_id=#{tenantId} AND inbound.id=#{inboundId} AND inbound.deleted=0
|
||||
""")
|
||||
Integer hasPhysicalWork(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
}
|
||||
@@ -24,6 +24,19 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
@Param("detained") boolean detained, @Param("reason") String reason,
|
||||
@Param("sourceVersion") long sourceVersion, @Param("operatorId") long operatorId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET hold_flag=#{holdFlag}, current_hold_reason=#{reason},
|
||||
updated_by=#{operatorId}, version=version+1
|
||||
WHERE id=#{inboundId} AND tenant_id=#{tenantId} AND deleted=0
|
||||
AND version=#{expectedVersion} AND hold_flag=#{expectedHoldFlag}
|
||||
""")
|
||||
int updateManualHold(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("expectedVersion") int expectedVersion,
|
||||
@Param("expectedHoldFlag") boolean expectedHoldFlag,
|
||||
@Param("holdFlag") boolean holdFlag, @Param("reason") String reason,
|
||||
@Param("operatorId") long operatorId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = CASE WHEN fulfillment_type='ONE_CLICK_DROPSHIP'
|
||||
|
||||
@@ -49,6 +49,23 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
List<Long> occupiedOrderIds(@Param("query") InboundCandidateQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT inbound.order_id
|
||||
FROM fl_inbound_order inbound
|
||||
WHERE inbound.deleted=0 AND inbound.hold_flag=1 AND inbound.order_id IS NOT NULL
|
||||
AND inbound.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
<if test="query.warehouseId != null">AND inbound.warehouse_id=#{query.warehouseId}</if>
|
||||
<if test="query.customerId != null">AND inbound.tenant_id=#{query.customerId}</if>
|
||||
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">
|
||||
AND inbound.inbound_code=#{query.inboundCode}
|
||||
</if>
|
||||
</script>
|
||||
""")
|
||||
List<Long> heldOrderIds(@Param("query") InboundCandidateQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels);
|
||||
|
||||
@Select("""
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
|
||||
@@ -105,6 +122,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
batch.total_volume_cbm,source.total_package_count
|
||||
) i ON i.tenant_id=o.tenant_id AND i.inbound_id=o.id AND i.measurement_id=m.id
|
||||
WHERE (#{warehouseId} IS NULL OR o.warehouse_id=#{warehouseId}) AND o.process_status='WAIT_SPLIT'
|
||||
AND COALESCE(o.hold_flag,0)=0
|
||||
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'))
|
||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
@@ -148,6 +166,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
AND split_item.parent_inbound_id=o.id),0) availablePackageCount
|
||||
FROM fl_inbound_order o
|
||||
WHERE (#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId})
|
||||
AND COALESCE(o.hold_flag,0)=0
|
||||
AND o.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
@@ -242,6 +261,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
LEFT JOIN fl_coload_group current_group
|
||||
ON current_group.id=current_member.group_id AND current_group.group_status='ACTIVE'
|
||||
WHERE o.process_status='WAIT_SPLIT'
|
||||
AND COALESCE(o.hold_flag,0)=0
|
||||
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'))
|
||||
AND (
|
||||
@@ -282,7 +302,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
ON current_member.tenant_id=o.tenant_id AND current_member.inbound_id=o.id AND current_member.active_flag=1
|
||||
LEFT JOIN fl_coload_group current_group
|
||||
ON current_group.id=current_member.group_id AND current_group.group_status IN ('ACTIVE','LOCKED')
|
||||
WHERE o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
|
||||
WHERE COALESCE(o.hold_flag,0)=0
|
||||
AND o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
|
||||
'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','LOADING')
|
||||
AND (
|
||||
<foreach collection="units" item="unit" separator=" OR ">
|
||||
@@ -388,6 +409,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
batch.total_volume_cbm,source.total_package_count
|
||||
) i ON i.tenant_id=o.tenant_id AND i.inbound_id=o.id AND i.measurement_id=m.id
|
||||
WHERE o.id=#{inboundId}
|
||||
AND COALESCE(o.hold_flag,0)=0
|
||||
AND (o.process_status='WAIT_SPLIT' OR (o.process_status='LOADING' AND EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item same_plan_item
|
||||
WHERE same_plan_item.loading_plan_id=#{planId}
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ public interface OutboundDispatchReadMapper {
|
||||
@Select("""
|
||||
SELECT DISTINCT inbound.order_id orderId,inbound.order_no orderNo,inbound.id inboundId,
|
||||
inbound.inbound_code inboundCode,inbound.tenant_id tenantId,inbound.customer_id customerId,
|
||||
inbound.customer_name customerName,inbound.process_status inboundStatus,
|
||||
inbound.customer_name customerName,inbound.process_status inboundStatus,inbound.hold_flag holdFlag,
|
||||
booking.id containerBookingId,booking.cabinet_no cabinetNo,booking.container_no containerNo,
|
||||
plan.id loadingPlanId,plan.plan_status loadingPlanStatus
|
||||
FROM fl_loading_plan_item item
|
||||
@@ -29,7 +29,7 @@ public interface OutboundDispatchReadMapper {
|
||||
@Select("""
|
||||
SELECT inbound.order_id orderId,inbound.order_no orderNo,inbound.id inboundId,
|
||||
inbound.inbound_code inboundCode,inbound.tenant_id tenantId,inbound.customer_id customerId,
|
||||
inbound.customer_name customerName,inbound.process_status inboundStatus,
|
||||
inbound.customer_name customerName,inbound.process_status inboundStatus,inbound.hold_flag holdFlag,
|
||||
booking.id containerBookingId,booking.cabinet_no cabinetNo,booking.container_no containerNo,
|
||||
plan.id loadingPlanId,plan.plan_status loadingPlanStatus
|
||||
FROM fl_loading_plan_item item
|
||||
@@ -178,7 +178,7 @@ public interface OutboundDispatchReadMapper {
|
||||
@Param("truckAppointmentId") Long truckAppointmentId);
|
||||
|
||||
record Binding(Long orderId, String orderNo, Long inboundId, String inboundCode, Long tenantId,
|
||||
Long customerId, String customerName, String inboundStatus, Long containerBookingId,
|
||||
Long customerId, String customerName, String inboundStatus, Boolean holdFlag, Long containerBookingId,
|
||||
String cabinetNo, String containerNo, Long loadingPlanId, String loadingPlanStatus) {}
|
||||
record IdStatus(Long id, String status) {}
|
||||
record OceanBinding(Long id, String status, Long customerId) {}
|
||||
|
||||
+2
@@ -179,6 +179,8 @@ public class OutboundDispatchService {
|
||||
OutboundDispatchReadMapper.OceanBinding ocean, OutboundDispatchReadMapper.Evidence evidence,
|
||||
List<GateItem> containerContributorGates) {
|
||||
List<GateItem> items = new ArrayList<>();
|
||||
items.add(item("INBOUND_NOT_HELD", "入库单未扣货", !Boolean.TRUE.equals(binding.holdFlag()),
|
||||
Boolean.TRUE.equals(binding.holdFlag()) ? "HELD" : "RELEASED", "RELEASED"));
|
||||
items.add(item("INBOUND_LOADED", "入库单装柜完成", "LOADED".equals(binding.inboundStatus()),
|
||||
binding.inboundStatus(), "LOADED"));
|
||||
items.add(item("LOADING_PLAN_COMPLETED", "柜次装柜计划完成", "COMPLETED".equals(binding.loadingPlanStatus()),
|
||||
|
||||
@@ -60,7 +60,9 @@ public class SmartLoadingRepository {
|
||||
return candidates.planColoadMembers(planId);
|
||||
}
|
||||
public List<Long> occupiedOrderIds(InboundCandidateQuery query, Set<String> actualChannels) {
|
||||
return candidates.occupiedOrderIds(query, actualChannels);
|
||||
return java.util.stream.Stream.concat(candidates.occupiedOrderIds(query, actualChannels).stream(),
|
||||
candidates.heldOrderIds(query, actualChannels).stream())
|
||||
.distinct().toList();
|
||||
}
|
||||
public Candidate candidate(long planId, long inboundId, long detailId) {
|
||||
return candidates.candidate(planId, inboundId, detailId);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.mapper.InboundHoldGateMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Central fail-closed guard used by every physical fulfillment write path. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InboundHoldGuard {
|
||||
private final InboundHoldGateMapper mapper;
|
||||
|
||||
public void requireInboundNotHeld(long tenantId, long inboundId) {
|
||||
if (mapper.countHeldInbound(tenantId, inboundId) > 0) throw held();
|
||||
}
|
||||
|
||||
public void requirePlanNotHeld(long planId) {
|
||||
if (mapper.countHeldByPlan(planId) > 0) throw held();
|
||||
}
|
||||
|
||||
public void requireTaskNotHeld(long taskId) {
|
||||
if (mapper.countHeldByTask(taskId) > 0) throw held();
|
||||
}
|
||||
|
||||
public void requireContainerNotHeld(long containerBookingId) {
|
||||
if (mapper.countHeldByContainer(containerBookingId) > 0) throw held();
|
||||
}
|
||||
|
||||
public boolean hasPhysicalWork(long tenantId, long inboundId) {
|
||||
Integer value = mapper.hasPhysicalWork(tenantId, inboundId);
|
||||
return value == null || value != 0;
|
||||
}
|
||||
|
||||
private static BizException held() {
|
||||
return new BizException(ResultCode.CONFLICT, "入库单已扣货,当前履约操作已阻断");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.CommandRequest;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.Detail;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.HistoryItem;
|
||||
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundHoldRecordMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Platform-owned inbound hold snapshot and immutable command history. */
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InboundHoldService {
|
||||
private static final String HOLD = "HOLD";
|
||||
private static final String RELEASE = "RELEASE";
|
||||
|
||||
private final InboundOrderMapper inbounds;
|
||||
private final InboundHoldRecordMapper history;
|
||||
private final InboundIdempotencyService idempotency;
|
||||
private final InboundHoldGuard guard;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Detail detail(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = owned(tenantId, inboundId);
|
||||
return view(inbound);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail hold(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String idempotencyKey, CommandRequest request) {
|
||||
return change(tenantId, inboundId, operatorId, operatorName, idempotencyKey, request, true);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail release(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String idempotencyKey, CommandRequest request) {
|
||||
return change(tenantId, inboundId, operatorId, operatorName, idempotencyKey, request, false);
|
||||
}
|
||||
|
||||
private Detail change(long tenantId, long inboundId, long operatorId, String operatorName,
|
||||
String idempotencyKey, CommandRequest request, boolean targetHold) {
|
||||
String action = targetHold ? HOLD : RELEASE;
|
||||
String reason = request.reason().trim();
|
||||
String canonical = action + '|' + tenantId + '|' + inboundId + '|' + request.expectedVersion()
|
||||
+ '|' + reason + '|' + (request.evidenceFileId() == null ? "" : request.evidenceFileId());
|
||||
InboundIdempotencyService.Context command = idempotency.begin(
|
||||
tenantId, operatorId, inboundId, "INBOUND_" + action, idempotencyKey, canonical);
|
||||
if (command.replayed()) return detail(tenantId, inboundId);
|
||||
|
||||
InboundOrderEntity inbound = inbounds.selectActiveByIdForUpdate(inboundId);
|
||||
if (inbound == null || inbound.getTenantId() == null || inbound.getTenantId() != tenantId) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在或不属于当前客户");
|
||||
}
|
||||
boolean currentHold = Boolean.TRUE.equals(inbound.getHoldFlag());
|
||||
if (currentHold == targetHold) {
|
||||
throw conflict(targetHold ? "入库单已处于扣货状态" : "入库单当前未扣货");
|
||||
}
|
||||
if (inbound.getVersion() == null || !inbound.getVersion().equals(request.expectedVersion())) {
|
||||
throw conflict("入库单版本已变化,请刷新后重试");
|
||||
}
|
||||
if (guard.hasPhysicalWork(tenantId, inboundId)) {
|
||||
throw conflict("入库单已发生实物拣货、装柜或出仓,禁止手工扣货或解除");
|
||||
}
|
||||
String currentReason = targetHold ? reason : null;
|
||||
if (inbounds.updateManualHold(tenantId, inboundId, request.expectedVersion(), currentHold,
|
||||
targetHold, currentReason, operatorId) != 1) {
|
||||
throw conflict("入库单状态或版本已变化,请刷新后重试");
|
||||
}
|
||||
InboundHoldRecordEntity record = new InboundHoldRecordEntity();
|
||||
record.setId(IdWorker.getId());
|
||||
record.setTenantId(tenantId);
|
||||
record.setInboundId(inboundId);
|
||||
record.setHoldAction(action);
|
||||
record.setReason(reason);
|
||||
record.setEvidenceFileId(request.evidenceFileId());
|
||||
record.setOperatorId(operatorId);
|
||||
record.setOperatorName(operatorName);
|
||||
history.insert(record);
|
||||
idempotency.complete(command, inboundId);
|
||||
return detail(tenantId, inboundId);
|
||||
}
|
||||
|
||||
private InboundOrderEntity owned(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = inbounds.selectById(inboundId);
|
||||
if (inbound == null || inbound.getTenantId() == null || inbound.getTenantId() != tenantId
|
||||
|| Boolean.TRUE.equals(inbound.getDeleted())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在或不属于当前客户");
|
||||
}
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private Detail view(InboundOrderEntity inbound) {
|
||||
boolean physical = guard.hasPhysicalWork(inbound.getTenantId(), inbound.getId());
|
||||
boolean held = Boolean.TRUE.equals(inbound.getHoldFlag());
|
||||
String blockReason = physical ? "入库单已发生实物拣货、装柜或出仓" : null;
|
||||
List<HistoryItem> records = history.selectList(new LambdaQueryWrapper<InboundHoldRecordEntity>()
|
||||
.eq(InboundHoldRecordEntity::getTenantId, inbound.getTenantId())
|
||||
.eq(InboundHoldRecordEntity::getInboundId, inbound.getId())
|
||||
.orderByDesc(InboundHoldRecordEntity::getOperatedAt)
|
||||
.orderByDesc(InboundHoldRecordEntity::getId))
|
||||
.stream().map(row -> new HistoryItem(row.getId(), row.getHoldAction(), row.getReason(),
|
||||
row.getEvidenceFileId(), row.getOperatorName(), row.getOperatedAt())).toList();
|
||||
return new Detail(inbound.getId(), inbound.getTenantId(), held,
|
||||
held ? inbound.getCurrentHoldReason() : null, inbound.getVersion(), !held && !physical,
|
||||
held && !physical, blockReason, records);
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,8 @@ public class LoadingListService {
|
||||
@Lazy
|
||||
private LoadingPickService loadingPickService;
|
||||
@Resource
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
@Resource
|
||||
private LoadingCompletedOutboxService loadingCompletedOutboxService;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
@@ -491,6 +493,7 @@ public class LoadingListService {
|
||||
public PreloadConfirmationResponse confirmPreload(long planId, long operatorId, String operatorName,
|
||||
String idempotencyKey,
|
||||
ConfirmPreloadRequest request) {
|
||||
inboundHoldGuard.requirePlanNotHeld(planId);
|
||||
String requestHash = sha256("CONFIRM_PRELOAD|" + planId + '|' + request.expectedVersion());
|
||||
String operationId = operationId("CONFIRM_PRELOAD", planId, idempotencyKey);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
|
||||
@@ -104,6 +104,8 @@ public class LoadingPickService {
|
||||
private PdaPickSettlementService settlementService;
|
||||
@Resource
|
||||
private InboundPalletAllocationRepository palletAllocations;
|
||||
@Resource
|
||||
private InboundHoldGuard inboundHoldGuard;
|
||||
|
||||
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||
@Transactional
|
||||
@@ -112,6 +114,7 @@ public class LoadingPickService {
|
||||
if (booking == null) return;
|
||||
var plan = smartLoadingRepository.planByBooking(containerBookingId);
|
||||
if (plan == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||
inboundHoldGuard.requirePlanNotHeld(plan.getId());
|
||||
var links = loadingModelBridge.byLoadingPlan(plan.getId());
|
||||
if (links.isEmpty() || links.stream().anyMatch(link -> !"CONFIRMED".equals(link.preloadStatus()))) return;
|
||||
var allocation = oceanBookingRepository.allocationByContainer(containerBookingId);
|
||||
@@ -124,6 +127,7 @@ public class LoadingPickService {
|
||||
}
|
||||
|
||||
public List<Link> requirePreloadConfirmationReady(long loadingPlanId) {
|
||||
inboundHoldGuard.requirePlanNotHeld(loadingPlanId);
|
||||
List<Link> links = loadingModelBridge.requireAllocatedLinksForConfirmation(loadingPlanId);
|
||||
for (Link link : links) {
|
||||
if (!Set.of("PENDING", "CONFIRMING").contains(link.preloadStatus())) {
|
||||
@@ -145,6 +149,7 @@ public class LoadingPickService {
|
||||
@Transactional
|
||||
public void begin(long planId, String containerNo, long operatorId, String operatorName,
|
||||
List<LoadingPlanItemEntity> planItems) {
|
||||
inboundHoldGuard.requirePlanNotHeld(planId);
|
||||
LoadingPickTaskEntity task = repository.taskByPlan(planId);
|
||||
if (task == null) {
|
||||
task = new LoadingPickTaskEntity();
|
||||
@@ -242,6 +247,7 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task claim(long taskId, ClaimRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = operation(request.operationId());
|
||||
String requestHash = hash("CLAIM|" + taskId + '|' + request.expectedAssignmentVersion());
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
@@ -271,6 +277,7 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = operation(request.operationId());
|
||||
String assigneeName = request.assigneeName().trim();
|
||||
String requestHash = hash("ASSIGN|" + taskId + '|' + request.assigneeId() + '|'
|
||||
@@ -294,6 +301,7 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash(taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
+ "|" + request.pickedPackageCount());
|
||||
@@ -338,6 +346,7 @@ public class LoadingPickService {
|
||||
@Transactional
|
||||
public Task partial(long taskId, long itemId, PartialPickRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash("PARTIAL|" + taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
+ "|" + request.finalPickedPackageCount());
|
||||
@@ -368,6 +377,7 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task skip(long taskId, long itemId, SkipRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = request.operationId().trim();
|
||||
String reason = trim(request.reason());
|
||||
String requestHash = hash("SKIP|" + taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
@@ -405,6 +415,7 @@ public class LoadingPickService {
|
||||
@Transactional
|
||||
public Task batch(long taskId, String idempotencyKey, BatchRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = operation(idempotencyKey);
|
||||
String requestHash = batchRequestHash(taskId, request);
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
@@ -451,6 +462,7 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task review(long taskId, ReviewRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
return complete(taskId, new CompleteLoadingRequest(request.expectedTaskVersion(), request.operationId(),
|
||||
request.remark()), operatorId, operatorName);
|
||||
}
|
||||
@@ -461,6 +473,7 @@ public class LoadingPickService {
|
||||
*/
|
||||
@Transactional
|
||||
public Task complete(long taskId, CompleteLoadingRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
String operationId = operation(request.operationId());
|
||||
String remark = trim(request.remark());
|
||||
String requestHash = hash("COMPLETE|" + taskId + '|' + request.expectedTaskVersion() + '|' + remark);
|
||||
|
||||
@@ -29,9 +29,11 @@ import java.util.Objects;
|
||||
public class LoadingWorkPalletService {
|
||||
@Resource private LoadingWorkPalletMapper pallets;
|
||||
@Resource private LoadingWorkPalletCartonMapper cartons;
|
||||
@Resource private InboundHoldGuard inboundHoldGuard;
|
||||
|
||||
@Transactional
|
||||
public Pallet open(long taskId, OpenRequest request, long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
TaskContext context = requireWorkTask(taskId, request.warehouseId(), operatorId);
|
||||
String code = request.palletCode().trim();
|
||||
String operationId = request.operationId().trim();
|
||||
@@ -68,6 +70,7 @@ public class LoadingWorkPalletService {
|
||||
@Transactional
|
||||
public Pallet scan(long taskId, long palletId, ScanRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
requireWorkTask(taskId, request.warehouseId(), operatorId);
|
||||
LoadingWorkPalletEntity target = requireOpenPallet(taskId, palletId, request.warehouseId());
|
||||
String operationId = request.operationId().trim();
|
||||
@@ -114,6 +117,7 @@ public class LoadingWorkPalletService {
|
||||
@Transactional
|
||||
public Pallet seal(long taskId, long palletId, SealRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
inboundHoldGuard.requireTaskNotHeld(taskId);
|
||||
requireWorkTask(taskId, request.warehouseId(), operatorId);
|
||||
LoadingWorkPalletEntity replay = pallets.selectBySealOperation(request.operationId().trim());
|
||||
if (replay != null) {
|
||||
|
||||
@@ -58,6 +58,7 @@ public class SmartLoadingService {
|
||||
@Resource private PendingReviewOrderClient pendingReviewOrders;
|
||||
@Resource private InboundOrderCreationService inboundOrderCreationService;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@Resource private InboundHoldGuard inboundHoldGuard;
|
||||
@Resource private LoadingModelBridgeService loadingModelBridge;
|
||||
@Resource private SplitPreloadService splitPreloadService;
|
||||
@Resource private ColoadGroupAssignmentPort coloadGroups;
|
||||
@@ -432,6 +433,7 @@ public class SmartLoadingService {
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
Candidate candidate = repository.candidate(planId, request.inboundId(), request.inboundDetailId());
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
inboundHoldGuard.requireInboundNotHeld(candidate.accountId(), candidate.inboundId());
|
||||
if (!wholeGroupAssignment) requireStandalone(candidate.accountId(), candidate.inboundId());
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
var task = loadingPickService.mutationSnapshot(planId);
|
||||
@@ -513,6 +515,7 @@ public class SmartLoadingService {
|
||||
throw conflict("待审核订单快照已变化,请刷新后重试");
|
||||
}
|
||||
long inboundId = inboundOrderCreationService.prepareFromDraft(snapshot);
|
||||
inboundHoldGuard.requireInboundNotHeld(request.customerId(), inboundId);
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
if (!"EDITING".equals(plan.getPlanStatus())) {
|
||||
throw conflict("待审核订单只能加入编辑中的装柜清单");
|
||||
@@ -583,6 +586,9 @@ public class SmartLoadingService {
|
||||
if (replay != null) return replay;
|
||||
|
||||
InboundOrderEntity projection = repository.inbound(request.inboundId());
|
||||
if (projection != null) {
|
||||
inboundHoldGuard.requireInboundNotHeld(projection.getTenantId(), projection.getId());
|
||||
}
|
||||
if (!wholeGroupAssignment && projection != null && Set.of("PREPARING", "WAIT_MEASURE",
|
||||
"MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED", "WAIT_PRICING",
|
||||
"FEE_CONFIRMING", "INBOUNDING").contains(projection.getProcessStatus())) {
|
||||
@@ -597,6 +603,7 @@ public class SmartLoadingService {
|
||||
: List.copyOf(frozenDetails);
|
||||
if (details.isEmpty()) throw conflict("入仓码不存在、已分配完或尚未完成入库");
|
||||
Candidate first = details.get(0);
|
||||
inboundHoldGuard.requireInboundNotHeld(first.accountId(), first.inboundId());
|
||||
if (!wholeGroupAssignment) requireStandalone(first.accountId(), first.inboundId());
|
||||
if (!repository.activeItemsByInbound(planId, first.accountId(), request.inboundId()).isEmpty()) {
|
||||
throw conflict("该入仓码已在当前柜次中,请先调整或移除现有分配");
|
||||
@@ -728,6 +735,7 @@ public class SmartLoadingService {
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
InboundOrderEntity selected = repository.inbound(request.inboundId());
|
||||
if (selected == null) throw conflict("同柜不可拆组成员当前不可分配");
|
||||
inboundHoldGuard.requireInboundNotHeld(selected.getTenantId(), selected.getId());
|
||||
GroupSnapshot discovered = requiredGroup(selected.getTenantId(), selected.getId());
|
||||
Map<Long, List<Candidate>> memberCandidates = requireWholeGroupCandidates(
|
||||
planId, booking, discovered, repository.items(planId));
|
||||
@@ -851,6 +859,7 @@ public class SmartLoadingService {
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
inboundHoldGuard.requireInboundNotHeld(item.getAccountId(), item.getInboundId());
|
||||
requireStandalone(item.getAccountId(), item.getInboundId());
|
||||
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
|
||||
LoadingPickService.MutationSnapshot pickSnapshot = loadingPickService.mutationSnapshot(planId);
|
||||
|
||||
Reference in New Issue
Block a user