装柜回归测试
This commit is contained in:
+27
-3
@@ -30,6 +30,14 @@ public interface WarehousePalletLocationClient {
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody SelectiveReleaseRequest request);
|
||||
|
||||
@PostMapping("/transfer")
|
||||
ApiResponse<List<TransferResult>> transfer(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody TransferRequest request);
|
||||
|
||||
@PostMapping("/settlements")
|
||||
ApiResponse<SettlementResult> settle(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody SettlementRequest request);
|
||||
|
||||
record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId,
|
||||
String inboundCode, String idempotencyKey, long operatorId,
|
||||
BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, String palletType,
|
||||
@@ -39,8 +47,8 @@ public interface WarehousePalletLocationClient {
|
||||
record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId,
|
||||
String reason) {}
|
||||
record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId,
|
||||
long operatorId, String reason, int totalPackageCount,
|
||||
List<Long> locationIds, Integer releaseLocationCount) {}
|
||||
String idempotencyKey, long operatorId, String reason,
|
||||
List<Long> locationIds) {}
|
||||
record AllocationResult(long reservationId, String reservationNo, int allocationNo,
|
||||
long palletId, String palletCode, long locationId, String locationCode,
|
||||
BigDecimal allocatedVolumeCbm, String state) {}
|
||||
@@ -49,5 +57,21 @@ public interface WarehousePalletLocationClient {
|
||||
long locationId, String locationCode, String state) {}
|
||||
record SelectiveReleaseResult(long reservationId, long occupancyId, int allocationNo,
|
||||
long palletId, long locationId, String locationCode,
|
||||
int releasedPackageCount) {}
|
||||
int allocatedPackageCount) {}
|
||||
record TransferItem(long locationId, int remainingPackageCount) {}
|
||||
record TransferRequest(long tenantId, String sourceService, long sourceBusinessId,
|
||||
long targetBusinessId, String targetInboundCode, String idempotencyKey,
|
||||
long operatorId, List<TransferItem> items) {}
|
||||
record TransferResult(long reservationId, String reservationNo, long occupancyId, int allocationNo,
|
||||
long warehouseResourceTenantId, long warehouseId, long palletId,
|
||||
String palletCode, BigDecimal palletCapacityCbm, long locationId,
|
||||
String locationCode, BigDecimal allocatedVolumeCbm,
|
||||
int remainingPackageCount, String state) {}
|
||||
record SettlementTransferItem(long reservationId, int remainingPackageCount) {}
|
||||
record SettlementRequest(long warehouseId, String sourceService, long sourceBusinessId,
|
||||
long targetBusinessId, int sourcePackageCount, int targetPackageCount,
|
||||
String targetInboundCode, String idempotencyKey, long operatorId,
|
||||
String operatorName, String requestId, List<Long> releasedReservationIds,
|
||||
List<SettlementTransferItem> remainingItems) {}
|
||||
record SettlementResult(List<SelectiveReleaseResult> released, List<TransferResult> transferred) {}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class MerchantInboundBillingController {
|
||||
@GetMapping("/{orderId}/inbound-billing")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
@Operation(summary = "查询订单入库结果和最新费用",
|
||||
description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
||||
description = "客户只读查看已达到 INBOUNDED + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||
|
||||
@@ -20,7 +20,10 @@ public final class InboundProcessStatus {
|
||||
public static final String INBOUNDING = "INBOUNDING";
|
||||
/** 已入库完成,费用已发送给客户。 */
|
||||
public static final String INBOUNDED = "INBOUNDED";
|
||||
/** 二期处理完成,等待三期分柜。 */
|
||||
/**
|
||||
* 历史兼容状态。二、三期已统一使用 {@link #INBOUNDED},不得再写入或对外返回此值。
|
||||
*/
|
||||
@Deprecated(forRemoval = false)
|
||||
public static final String WAIT_SPLIT = "WAIT_SPLIT";
|
||||
/** 已进入装柜作业,PDA 可执行拣货。 */
|
||||
public static final String LOADING = "LOADING";
|
||||
@@ -33,7 +36,7 @@ public final class InboundProcessStatus {
|
||||
|
||||
public static final Set<String> VALUES = Set.of(
|
||||
WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
|
||||
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_SPLIT,
|
||||
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED,
|
||||
LOADING, LOADED, DELIVERED, VOIDED
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
@@ -17,20 +18,30 @@ public final class LoadingPickDtos {
|
||||
public record PickRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotNull @Min(1) Integer pickedPackageCount,
|
||||
@Pattern(regexp = "AUTO|MANUAL") String releaseMode,
|
||||
@JsonAlias("reservationIds") List<@Min(1) Long> locationIds,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {
|
||||
public PickRequest(Long warehouseId, Integer pickedPackageCount, List<Long> locationIds,
|
||||
Integer version, String operationId) {
|
||||
this(warehouseId, pickedPackageCount, null, locationIds, version, operationId);
|
||||
}
|
||||
}
|
||||
|
||||
/** 部分拣货为该 PDA 明细的最终实际拣出箱数,不是增量。 */
|
||||
public record PartialPickRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotNull @Min(1) Integer finalPickedPackageCount,
|
||||
@Pattern(regexp = "AUTO|MANUAL") String releaseMode,
|
||||
@JsonAlias("reservationIds") List<@Min(1) Long> locationIds,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {
|
||||
public PartialPickRequest(Long warehouseId, Integer finalPickedPackageCount, List<Long> locationIds,
|
||||
Integer version, String operationId) {
|
||||
this(warehouseId, finalPickedPackageCount, null, locationIds, version, operationId);
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳拣只能发生在尚未拣出任何箱数的 PDA 明细上。 */
|
||||
|
||||
@@ -14,7 +14,7 @@ public record UnifiedInboundQuery(
|
||||
Long tenantId,
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT",
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
|
||||
@@ -29,6 +29,8 @@ public class InboundPalletAllocationEntity {
|
||||
private Integer pickedPackageCount;
|
||||
private Long locationId;
|
||||
private String locationCode;
|
||||
/** 仓库资源唯一预占标识;A0 多托盘场景不能使用 locationId 作为唯一标识。 */
|
||||
private Long reservationId;
|
||||
private String reservationNo;
|
||||
private String allocationStatus;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -23,6 +23,10 @@ public class LoadingPickItemEntity {
|
||||
private Integer plannedPackageCount;
|
||||
private Integer pickedPackageCount;
|
||||
private String itemStatus;
|
||||
/** AUTO 按比例释放,MANUAL 按指定库位释放。 */
|
||||
private String releaseMode;
|
||||
/** 人工选择的库位 ID,JSON 数组;空数组表示明确不释放。 */
|
||||
private String releaseLocationIds;
|
||||
@Version
|
||||
private Integer version;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -37,4 +37,22 @@ public interface InboundPalletAllocationMapper extends BaseMapper<InboundPalletA
|
||||
""")
|
||||
int consume(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("count") int count);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET allocation_status = 'TRANSFERRED', updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE id = #{id} AND tenant_id = #{tenantId} AND inbound_id = #{inboundId}
|
||||
AND allocation_status = 'CONFIRMED' AND picked_package_count < allocated_package_count
|
||||
""")
|
||||
int markTransferred(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET allocation_status = 'RELEASED', updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE tenant_id = #{tenantId} AND inbound_id = #{inboundId}
|
||||
AND reservation_id = #{reservationId} AND allocation_status = 'CONFIRMED'
|
||||
""")
|
||||
int markReleased(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("reservationId") long reservationId);
|
||||
}
|
||||
|
||||
+219
-4
@@ -2,7 +2,9 @@ package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -11,12 +13,13 @@ import java.util.List;
|
||||
public interface InboundSplitSettlementMapper {
|
||||
@Select("""
|
||||
SELECT id, tenant_id AS tenantId, inbound_code AS inboundCode, inbound_role AS inboundRole,
|
||||
process_status AS processStatus, version
|
||||
process_status AS processStatus, total_package_count AS totalPackageCount,
|
||||
package_count AS packageCount, version
|
||||
FROM fl_inbound_order
|
||||
WHERE tenant_id = #{accountId} AND id = #{inboundId} AND deleted = 0
|
||||
WHERE id = #{inboundId} AND deleted = 0
|
||||
FOR UPDATE
|
||||
""")
|
||||
ParentRow lockParent(@Param("accountId") long accountId, @Param("inboundId") long inboundId);
|
||||
ParentRow lockParent(@Param("inboundId") long inboundId);
|
||||
|
||||
@Select("""
|
||||
SELECT id
|
||||
@@ -28,6 +31,179 @@ public interface InboundSplitSettlementMapper {
|
||||
""")
|
||||
Long childId(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_order (
|
||||
id, tenant_id, task_id, order_id, order_no, inbound_code, process_status,
|
||||
order_status_snapshot, measure_review_status, fee_status, fee_review_status, notification_status,
|
||||
expected_arrival_at, customer_id, customer_name, company_id, company_name,
|
||||
warehouse_id, warehouse_name, delivery_warehouse_id, delivery_warehouse_name,
|
||||
business_type, channel_code, channel_name, carrier_id, carrier_name, tax_mode,
|
||||
pricing_tax_mode, container_mode, customs_inspection, destination_type,
|
||||
destination_warehouse_id, destination_code, destination_name, hold_flag,
|
||||
current_hold_reason, total_package_count, actual_gross_weight_kg,
|
||||
chargeable_weight_kg, actual_volume_cbm, chargeable_volume_cbm, pallet_count,
|
||||
volume_weight_kg, actual_net_weight_kg, measure_completed_at, inbounded_at,
|
||||
express_company_code, express_company_name, express_tracking_no, public_remark,
|
||||
created_by, created_by_name, created_at, updated_by, updated_at, deleted, version,
|
||||
inbound_role, parent_inbound_id, root_inbound_id, split_sequence, split_status, package_count
|
||||
)
|
||||
SELECT #{childId}, tenant_id, task_id, order_id, order_no, #{childInboundCode}, 'INBOUNDED',
|
||||
order_status_snapshot, 'APPROVED', fee_status, fee_review_status, notification_status,
|
||||
expected_arrival_at, customer_id, customer_name, company_id, company_name,
|
||||
warehouse_id, warehouse_name, delivery_warehouse_id, delivery_warehouse_name,
|
||||
business_type, channel_code, channel_name, carrier_id, carrier_name, tax_mode,
|
||||
pricing_tax_mode, container_mode, customs_inspection, destination_type,
|
||||
destination_warehouse_id, destination_code, destination_name, hold_flag,
|
||||
current_hold_reason, #{packageCount}, actual_gross_weight_kg,
|
||||
chargeable_weight_kg, actual_volume_cbm, chargeable_volume_cbm, pallet_count,
|
||||
volume_weight_kg, actual_net_weight_kg, measure_completed_at, inbounded_at,
|
||||
express_company_code, express_company_name, express_tracking_no, public_remark,
|
||||
#{operatorId}, #{operatorName}, CURRENT_TIMESTAMP(3), #{operatorId}, CURRENT_TIMESTAMP(3), 0, 0,
|
||||
'CHILD', id, id, 1, 'NOT_SPLIT', #{packageCount}
|
||||
FROM fl_inbound_order
|
||||
WHERE id = #{parentInboundId} AND tenant_id = #{accountId} AND deleted = 0
|
||||
""")
|
||||
int cloneChild(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
|
||||
@Param("childId") long childId, @Param("childInboundCode") String childInboundCode,
|
||||
@Param("packageCount") int packageCount, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_split_batch
|
||||
(id, tenant_id, parent_inbound_id, operation_id, idempotency_key, expected_parent_version,
|
||||
package_count, status, remark, created_by, created_by_name, created_at, version)
|
||||
VALUES
|
||||
(#{batchId}, #{accountId}, #{parentInboundId}, #{operationId}, #{idempotencyKey},
|
||||
#{expectedParentVersion}, #{packageCount}, 'PREPARED', #{remark}, #{operatorId},
|
||||
#{operatorName}, CURRENT_TIMESTAMP(3), 0)
|
||||
""")
|
||||
int insertBatch(@Param("batchId") long batchId, @Param("accountId") long accountId,
|
||||
@Param("parentInboundId") long parentInboundId, @Param("operationId") String operationId,
|
||||
@Param("idempotencyKey") String idempotencyKey,
|
||||
@Param("expectedParentVersion") int expectedParentVersion,
|
||||
@Param("packageCount") int packageCount, @Param("remark") String remark,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_split_item
|
||||
(id, tenant_id, split_batch_id, parent_inbound_id, child_inbound_id, line_no,
|
||||
source_inbound_detail_id, source_measurement_id, source_measurement_item_id, package_count, created_at)
|
||||
VALUES
|
||||
(#{splitItemId}, #{accountId}, #{batchId}, #{parentInboundId}, #{childInboundId}, #{lineNo},
|
||||
#{inboundDetailId}, #{measurementId}, #{measurementItemId}, #{packageCount}, CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertSplitItem(@Param("splitItemId") long splitItemId, @Param("accountId") long accountId,
|
||||
@Param("batchId") long batchId, @Param("parentInboundId") long parentInboundId,
|
||||
@Param("childInboundId") long childInboundId, @Param("lineNo") int lineNo,
|
||||
@Param("inboundDetailId") long inboundDetailId, @Param("measurementId") long measurementId,
|
||||
@Param("measurementItemId") long measurementItemId,
|
||||
@Param("packageCount") int packageCount);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_measurement
|
||||
(id, tenant_id, inbound_id, split_batch_id, source_measurement_id, source_type, source_code,
|
||||
external_event_no, external_request_hash, device_code, warehouse_code, measured_at, status,
|
||||
raw_payload_json, original_document_no, original_gross_weight_kg, original_volume_cbm,
|
||||
actual_goods_name, total_package_count, total_volume_raw_cbm, total_volume_cbm,
|
||||
volume_weight_kg, created_by, created_at, updated_at, version)
|
||||
SELECT #{childMeasurementId}, tenant_id, #{childInboundId}, #{batchId}, id, source_type, source_code,
|
||||
NULL, NULL, device_code, warehouse_code, measured_at, 'APPROVED',
|
||||
raw_payload_json, original_document_no, original_gross_weight_kg, original_volume_cbm,
|
||||
actual_goods_name, #{packageCount}, NULL, NULL, NULL, #{operatorId},
|
||||
CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3), 0
|
||||
FROM fl_inbound_measurement
|
||||
WHERE id = #{sourceMeasurementId} AND tenant_id = #{accountId} AND inbound_id = #{parentInboundId}
|
||||
AND status = 'APPROVED'
|
||||
""")
|
||||
int cloneMeasurement(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
|
||||
@Param("sourceMeasurementId") long sourceMeasurementId,
|
||||
@Param("childInboundId") long childInboundId, @Param("childMeasurementId") long childMeasurementId,
|
||||
@Param("batchId") long batchId, @Param("packageCount") int packageCount,
|
||||
@Param("operatorId") long operatorId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_measurement_item
|
||||
(id, tenant_id, measurement_id, split_item_id, source_measurement_item_id, inbound_detail_id,
|
||||
external_detail_no, box_code, item_no, box_specification, package_count, length_cm, width_cm,
|
||||
height_cm, girth_cm, unit_weight_kg, unit_volume_cbm, volume_raw_cbm, volume_cbm,
|
||||
total_weight_kg, total_volume_cbm, volume_weight_kg, calculation_rule_version,
|
||||
source_image_file_id, machine_no, device_measured_at, over_limit_warning,
|
||||
outer_length_cm, outer_width_cm, outer_height_cm, remark, created_at, updated_at)
|
||||
SELECT #{childMeasurementItemId}, tenant_id, #{childMeasurementId}, #{splitItemId}, id, inbound_detail_id,
|
||||
NULL, NULL, CAST(#{lineNo} AS CHAR), box_specification, #{packageCount}, length_cm, width_cm,
|
||||
height_cm, girth_cm, unit_weight_kg, unit_volume_cbm, volume_raw_cbm, volume_cbm,
|
||||
CASE WHEN unit_weight_kg IS NULL THEN NULL ELSE unit_weight_kg * #{packageCount} END,
|
||||
unit_volume_cbm * #{packageCount}, volume_weight_kg / NULLIF(package_count, 0) * #{packageCount},
|
||||
calculation_rule_version, source_image_file_id, machine_no, device_measured_at,
|
||||
over_limit_warning, outer_length_cm, outer_width_cm, outer_height_cm, remark,
|
||||
CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3)
|
||||
FROM fl_inbound_measurement_item
|
||||
WHERE id = #{sourceMeasurementItemId} AND tenant_id = #{accountId}
|
||||
AND measurement_id = #{sourceMeasurementId}
|
||||
""")
|
||||
int cloneMeasurementItem(@Param("accountId") long accountId, @Param("sourceMeasurementId") long sourceMeasurementId,
|
||||
@Param("sourceMeasurementItemId") long sourceMeasurementItemId,
|
||||
@Param("childMeasurementId") long childMeasurementId, @Param("splitItemId") long splitItemId,
|
||||
@Param("childMeasurementItemId") long childMeasurementItemId,
|
||||
@Param("lineNo") int lineNo, @Param("packageCount") int packageCount);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET package_count = GREATEST(COALESCE(package_count, total_package_count) - #{packageCount}, 0),
|
||||
split_status = 'PARTIALLY_SPLIT', updated_by = #{operatorId}, version = version + 1
|
||||
WHERE id = #{parentInboundId} AND tenant_id = #{accountId} AND deleted = 0
|
||||
AND inbound_role = 'MASTER' AND process_status = 'INBOUNDED' AND version = #{expectedVersion}
|
||||
""")
|
||||
int markParentPartiallySplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
|
||||
@Param("packageCount") int packageCount, @Param("operatorId") long operatorId,
|
||||
@Param("expectedVersion") int expectedVersion);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_split_batch
|
||||
SET status = 'APPLIED', applied_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{batchId} AND tenant_id = #{accountId} AND status = 'PREPARED'
|
||||
""")
|
||||
int applyBatch(@Param("accountId") long accountId, @Param("batchId") long batchId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_split_history
|
||||
(id, tenant_id, split_batch_id, operation_id, action_type, after_snapshot,
|
||||
operator_id, operator_name, operation_reason, created_at)
|
||||
VALUES
|
||||
(#{historyId}, #{accountId}, #{batchId}, #{operationId}, 'APPLY', #{afterSnapshot},
|
||||
#{operatorId}, #{operatorName}, #{reason}, CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertHistory(@Param("historyId") long historyId, @Param("accountId") long accountId,
|
||||
@Param("batchId") long batchId, @Param("operationId") String operationId,
|
||||
@Param("afterSnapshot") String afterSnapshot, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName, @Param("reason") String reason);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan_item plan_item
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id = plan_item.id
|
||||
SET plan_item.package_count = pick_item.picked_package_count,
|
||||
plan_item.updated_by = #{operatorId}, plan_item.updated_by_name = #{operatorName},
|
||||
plan_item.updated_at = CURRENT_TIMESTAMP(3), plan_item.version = plan_item.version + 1
|
||||
WHERE plan_item.id = #{planItemId} AND plan_item.deleted = 0
|
||||
AND pick_item.item_status = 'PARTIAL'
|
||||
AND pick_item.picked_package_count > 0
|
||||
""")
|
||||
int convergePartialPlanItem(@Param("planItemId") long planItemId, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan_item plan_item
|
||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id = plan_item.id
|
||||
SET plan_item.deleted = 1, plan_item.removed_by = #{operatorId},
|
||||
plan_item.removed_by_name = #{operatorName}, plan_item.removed_at = CURRENT_TIMESTAMP(3),
|
||||
plan_item.updated_by = #{operatorId}, plan_item.updated_by_name = #{operatorName},
|
||||
plan_item.updated_at = CURRENT_TIMESTAMP(3), plan_item.version = plan_item.version + 1
|
||||
WHERE plan_item.id = #{planItemId} AND plan_item.deleted = 0
|
||||
AND pick_item.item_status = 'SKIPPED' AND pick_item.picked_package_count = 0
|
||||
""")
|
||||
int removeSkippedPlanItem(@Param("planItemId") long planItemId, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Select({"<script>", """
|
||||
SELECT item.id AS measurementItemId, measurement.id AS measurementId,
|
||||
item.inbound_detail_id AS inboundDetailId, item.package_count AS packageCount
|
||||
@@ -46,8 +222,47 @@ public interface InboundSplitSettlementMapper {
|
||||
@Param("inboundId") long inboundId,
|
||||
@Param("detailIds") List<Long> detailIds);
|
||||
|
||||
@Select("""
|
||||
SELECT item.id AS measurementItemId, measurement.id AS measurementId,
|
||||
item.inbound_detail_id AS inboundDetailId, item.package_count AS packageCount
|
||||
FROM fl_inbound_measurement measurement
|
||||
JOIN fl_inbound_measurement_item item ON item.measurement_id = measurement.id
|
||||
WHERE measurement.tenant_id = #{accountId} AND measurement.inbound_id = #{inboundId}
|
||||
AND measurement.status = 'APPROVED' AND item.package_count > 0
|
||||
AND measurement.id = (
|
||||
SELECT MAX(latest.id) FROM fl_inbound_measurement latest
|
||||
WHERE latest.tenant_id = #{accountId} AND latest.inbound_id = #{inboundId}
|
||||
AND latest.status = 'APPROVED')
|
||||
ORDER BY item.id ASC
|
||||
FOR UPDATE
|
||||
""")
|
||||
List<MeasurementItemRow> lockLatestApprovedItemsForInbound(@Param("accountId") long accountId,
|
||||
@Param("inboundId") long inboundId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_measurement
|
||||
SET total_package_count = #{packageCount}, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{measurementId} AND tenant_id = #{accountId} AND inbound_id = #{inboundId}
|
||||
AND status = 'APPROVED'
|
||||
""")
|
||||
int updateSourceMeasurementPackageCount(@Param("accountId") long accountId,
|
||||
@Param("inboundId") long inboundId,
|
||||
@Param("measurementId") long measurementId,
|
||||
@Param("packageCount") int packageCount);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_measurement_item
|
||||
SET package_count = #{packageCount}, updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE id = #{measurementItemId} AND measurement_id = #{measurementId} AND tenant_id = #{accountId}
|
||||
""")
|
||||
int updateSourceMeasurementItemPackageCount(@Param("accountId") long accountId,
|
||||
@Param("measurementId") long measurementId,
|
||||
@Param("measurementItemId") long measurementItemId,
|
||||
@Param("packageCount") int packageCount);
|
||||
|
||||
record ParentRow(Long id, Long tenantId, String inboundCode, String inboundRole,
|
||||
String processStatus, Integer version) {
|
||||
String processStatus, Integer totalPackageCount, Integer packageCount,
|
||||
Integer version) {
|
||||
}
|
||||
|
||||
record MeasurementItemRow(Long measurementItemId, Long measurementId, Long inboundDetailId,
|
||||
|
||||
@@ -97,6 +97,7 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
SET item_status = CASE WHEN picked_package_count + #{count} = planned_package_count
|
||||
THEN 'PICKED' ELSE 'PICKING' END,
|
||||
picked_package_count = picked_package_count + #{count},
|
||||
release_mode = #{releaseMode}, release_location_ids = #{releaseLocationIds},
|
||||
version = version + 1
|
||||
WHERE id = #{id} AND version = #{version}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item
|
||||
@@ -106,12 +107,17 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0)
|
||||
AND picked_package_count + #{count} <= planned_package_count
|
||||
""")
|
||||
int pick(@Param("id") long id, @Param("count") int count, @Param("version") int version,
|
||||
int pick(@Param("id") long id, @Param("count") int count,
|
||||
@Param("releaseMode") String releaseMode,
|
||||
@Param("releaseLocationIds") String releaseLocationIds,
|
||||
@Param("version") int version,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_pick_item
|
||||
SET picked_package_count = #{finalCount}, item_status = 'PARTIAL', version = version + 1
|
||||
SET picked_package_count = #{finalCount}, item_status = 'PARTIAL',
|
||||
release_mode = #{releaseMode}, release_location_ids = #{releaseLocationIds},
|
||||
version = version + 1
|
||||
WHERE id = #{id} AND version = #{version}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item
|
||||
JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id
|
||||
@@ -122,7 +128,10 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
AND picked_package_count < #{finalCount}
|
||||
AND #{finalCount} < planned_package_count
|
||||
""")
|
||||
int partial(@Param("id") long id, @Param("finalCount") int finalCount, @Param("version") int version,
|
||||
int partial(@Param("id") long id, @Param("finalCount") int finalCount,
|
||||
@Param("releaseMode") String releaseMode,
|
||||
@Param("releaseLocationIds") String releaseLocationIds,
|
||||
@Param("version") int version,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Update("""
|
||||
|
||||
+67
@@ -53,6 +53,7 @@ public class InboundPalletAllocationRepository {
|
||||
remaining -= allocatedPackages;
|
||||
value.setLocationId(source.locationId());
|
||||
value.setLocationCode(source.locationCode());
|
||||
value.setReservationId(source.reservationId());
|
||||
// 仓储确认响应只返回 reservationId;沿用预占阶段的预占单号快照。
|
||||
value.setReservationNo(source.reservationNo() != null ? source.reservationNo()
|
||||
: reservationNos.get(source.palletId()));
|
||||
@@ -73,6 +74,72 @@ public class InboundPalletAllocationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/** 父单快照在仓库资源转移前标记为已转移,后续锁查询不会再将其当作可用资源。 */
|
||||
public void markTransferred(long tenantId, long inboundId, List<InboundPalletAllocationEntity> allocations,
|
||||
List<Long> releasedReservationIds) {
|
||||
for (InboundPalletAllocationEntity allocation : allocations) {
|
||||
if (releasedReservationIds.contains(allocation.getReservationId())) {
|
||||
continue;
|
||||
}
|
||||
int allocated = allocation.getAllocatedPackageCount() == null ? 0 : allocation.getAllocatedPackageCount();
|
||||
int picked = allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount();
|
||||
if (allocated <= picked) {
|
||||
continue;
|
||||
}
|
||||
if (allocation.getId() == null || mapper.markTransferred(allocation.getId(), tenantId, inboundId) != 1) {
|
||||
throw new IllegalStateException("库位资源转移状态已被并发修改");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void markReleased(long tenantId, long inboundId, List<Long> reservationIds) {
|
||||
for (Long reservationId : reservationIds) {
|
||||
if (reservationId == null || mapper.markReleased(tenantId, inboundId, reservationId) != 1) {
|
||||
throw new IllegalStateException("库位释放状态已被并发修改");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 依据已经锁定且已扣减本次拣货数量的父单分配,预先固化子单快照。
|
||||
* 仓库 transfer 是本地事务最后一个外部动作,成功后不再执行本地写入,避免跨服务重试时目标子单变化。
|
||||
*/
|
||||
@Transactional
|
||||
public void copyRemainingToChild(long tenantId, long inboundId, List<InboundPalletAllocationEntity> parent) {
|
||||
mapper.delete(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (InboundPalletAllocationEntity source : parent) {
|
||||
int allocated = source.getAllocatedPackageCount() == null ? 0 : source.getAllocatedPackageCount();
|
||||
int picked = source.getPickedPackageCount() == null ? 0 : source.getPickedPackageCount();
|
||||
int remaining = allocated - picked;
|
||||
if (remaining <= 0) {
|
||||
continue;
|
||||
}
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setInboundId(inboundId);
|
||||
value.setAllocationNo(source.getAllocationNo());
|
||||
value.setWarehouseId(source.getWarehouseId());
|
||||
value.setWarehouseResourceTenantId(source.getWarehouseResourceTenantId());
|
||||
value.setPalletId(source.getPalletId());
|
||||
value.setPalletCode(source.getPalletCode());
|
||||
value.setPalletCapacityCbm(source.getPalletCapacityCbm());
|
||||
value.setAllocatedVolumeCbm(source.getAllocatedVolumeCbm());
|
||||
value.setAllocatedPackageCount(remaining);
|
||||
value.setPickedPackageCount(0);
|
||||
value.setLocationId(source.getLocationId());
|
||||
value.setLocationCode(source.getLocationCode());
|
||||
value.setReservationId(source.getReservationId());
|
||||
value.setReservationNo(source.getReservationNo());
|
||||
value.setAllocationStatus("CONFIRMED");
|
||||
value.setCreatedAt(now);
|
||||
value.setUpdatedAt(now);
|
||||
mapper.insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String state) {
|
||||
return "OCCUPIED".equals(state) ? "CONFIRMED" : state;
|
||||
}
|
||||
|
||||
@@ -99,12 +99,14 @@ public class LoadingPickRepository {
|
||||
.eq(LoadingPickItemEntity::getLoadingPlanItemId, planItemId));
|
||||
}
|
||||
|
||||
public int pick(long itemId, int count, int version, long warehouseId) {
|
||||
return items.pick(itemId, count, version, warehouseId);
|
||||
public int pick(long itemId, int count, String releaseMode, String releaseLocationIds,
|
||||
int version, long warehouseId) {
|
||||
return items.pick(itemId, count, releaseMode, releaseLocationIds, version, warehouseId);
|
||||
}
|
||||
|
||||
public int partial(long itemId, int finalCount, int version, long warehouseId) {
|
||||
return items.partial(itemId, finalCount, version, warehouseId);
|
||||
public int partial(long itemId, int finalCount, String releaseMode, String releaseLocationIds,
|
||||
int version, long warehouseId) {
|
||||
return items.partial(itemId, finalCount, releaseMode, releaseLocationIds, version, warehouseId);
|
||||
}
|
||||
|
||||
public int skip(long itemId, int version, long warehouseId) {
|
||||
@@ -119,6 +121,18 @@ public class LoadingPickRepository {
|
||||
return items.unfinishedCount(planId) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳拣释放候选前,必须保证同一入库单没有已拣或部分拣货的明细,避免把仍在装柜的货物
|
||||
* 重新暴露给智能装柜。
|
||||
*/
|
||||
public boolean hasPickedOrPartialItem(long taskId, Long inboundId) {
|
||||
if (inboundId == null) {
|
||||
return false;
|
||||
}
|
||||
return items(taskId).stream().anyMatch(item -> inboundId.equals(item.getInboundId())
|
||||
&& ("PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus())));
|
||||
}
|
||||
|
||||
public LoadingPickHistoryEntity history(String operationId) {
|
||||
return histories.selectOne(new LambdaQueryWrapper<LoadingPickHistoryEntity>()
|
||||
.eq(LoadingPickHistoryEntity::getOperationId, operationId));
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
|
||||
/** 装柜中 PDA 拣货任务及明细同步。 */
|
||||
@Service
|
||||
@@ -39,6 +40,8 @@ public class LoadingPickService {
|
||||
private LoadingPickRepository repository;
|
||||
@Resource
|
||||
private LoadingListService loadingListService;
|
||||
@Resource
|
||||
private PdaPickSettlementService settlementService;
|
||||
|
||||
@Transactional
|
||||
public void begin(long planId, String containerNo, long operatorId, String operatorName,
|
||||
@@ -138,9 +141,10 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) {
|
||||
ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds());
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash(taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
+ "|" + request.pickedPackageCount());
|
||||
+ "|" + request.pickedPackageCount() + "|" + release.mode() + "|" + release.json());
|
||||
requireNoTaskOperation(operationId);
|
||||
LoadingPickHistoryEntity replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
@@ -155,7 +159,8 @@ public class LoadingPickService {
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
|
||||
}
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), request.version(), request.warehouseId()) != 1) {
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), release.mode(), release.json(),
|
||||
request.version(), request.warehouseId()) != 1) {
|
||||
throw conflict("拣货数量超过计划箱数或明细版本已变化");
|
||||
}
|
||||
LoadingPickHistoryEntity history = new LoadingPickHistoryEntity();
|
||||
@@ -168,16 +173,17 @@ public class LoadingPickService {
|
||||
history.setOperatorId(operatorId);
|
||||
history.setOperatorName(operatorName);
|
||||
repository.insert(history);
|
||||
touchAndSync(task, operatorId, operatorName);
|
||||
touchSettleFinishAndSync(task, operatorId, operatorName);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Task partial(long taskId, long itemId, PartialPickRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds());
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash("PARTIAL|" + taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
+ "|" + request.finalPickedPackageCount());
|
||||
+ "|" + request.finalPickedPackageCount() + "|" + release.mode() + "|" + release.json());
|
||||
requireNoTaskOperation(operationId);
|
||||
LoadingPickHistoryEntity replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
@@ -186,12 +192,13 @@ public class LoadingPickService {
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
if (repository.partial(itemId, request.finalPickedPackageCount(), request.version(), request.warehouseId()) != 1) {
|
||||
if (repository.partial(itemId, request.finalPickedPackageCount(), release.mode(), release.json(),
|
||||
request.version(), request.warehouseId()) != 1) {
|
||||
throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
|
||||
}
|
||||
insertHistory(taskId, itemId, operationId, requestHash, "PARTIAL",
|
||||
request.finalPickedPackageCount(), null, operatorId, operatorName);
|
||||
touchAndSync(task, operatorId, operatorName);
|
||||
touchSettleFinishAndSync(task, operatorId, operatorName);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
|
||||
@@ -208,13 +215,16 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
|
||||
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
|
||||
}
|
||||
if (repository.skip(itemId, request.version(), request.warehouseId()) != 1) {
|
||||
throw conflict("跳拣仅允许未开始拣货的待拣明细,或明细版本已变化");
|
||||
}
|
||||
insertHistory(taskId, itemId, operationId, requestHash, "SKIP", 0, reason,
|
||||
operatorId, operatorName);
|
||||
touchAndSync(task, operatorId, operatorName);
|
||||
touchSettleFinishAndSync(task, operatorId, operatorName);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
|
||||
@@ -300,6 +310,16 @@ public class LoadingPickService {
|
||||
repository.update(item);
|
||||
}
|
||||
|
||||
private void settleAndFinish(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
||||
if (!repository.allTaskItemsResolved(task.getId())) {
|
||||
return;
|
||||
}
|
||||
settlementService.settle(task.getId(), operatorId, operatorName);
|
||||
if (repository.allPlanItemsPicked(task.getLoadingPlanId())) {
|
||||
loadingListService.finishFromPda(task.getLoadingPlanId(), task.getId(), operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
|
||||
private LoadingPickTaskEntity requiredTask(long taskId) {
|
||||
LoadingPickTaskEntity task = repository.task(taskId);
|
||||
if (task == null) {
|
||||
@@ -333,10 +353,12 @@ public class LoadingPickService {
|
||||
return task;
|
||||
}
|
||||
|
||||
private void requireWarehouseItem(long taskId, long itemId, long warehouseId) {
|
||||
if (repository.item(taskId, itemId, warehouseId) == null) {
|
||||
private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) {
|
||||
LoadingPickItemEntity item = repository.item(taskId, itemId, warehouseId);
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在或不属于当前仓库");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private Task view(LoadingPickTaskEntity task, Long warehouseId) {
|
||||
@@ -360,10 +382,15 @@ public class LoadingPickService {
|
||||
workflowStatus, task.getVersion(), assignment, review, items);
|
||||
}
|
||||
|
||||
private void touchAndSync(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
||||
/**
|
||||
* 先触碰仍处于 LOADING 的任务,再结算并推进装柜计划。计划完成触发器会同步把任务改为
|
||||
* COMPLETED,因此绝不能在 finishFromPda 之后再以 LOADING 状态触碰任务。
|
||||
*/
|
||||
private void touchSettleFinishAndSync(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
||||
if (repository.touchTask(task.getId(), task.getVersion()) != 1) {
|
||||
throw conflict("PDA任务版本已变化");
|
||||
}
|
||||
settleAndFinish(task, operatorId, operatorName);
|
||||
repository.syncInboundProjections(task.getId(), operatorId, operatorName);
|
||||
}
|
||||
|
||||
@@ -434,6 +461,25 @@ public class LoadingPickService {
|
||||
}
|
||||
}
|
||||
|
||||
private static ReleaseSelection releaseSelection(String requestedMode, List<Long> requestedLocationIds) {
|
||||
String mode = requestedMode == null || requestedMode.isBlank()
|
||||
? (requestedLocationIds == null ? "AUTO" : "MANUAL") : requestedMode.trim().toUpperCase();
|
||||
if (!"AUTO".equals(mode) && !"MANUAL".equals(mode)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "releaseMode只能是AUTO或MANUAL");
|
||||
}
|
||||
List<Long> ids = requestedLocationIds == null ? List.of() : requestedLocationIds.stream().sorted().toList();
|
||||
if (new HashSet<>(ids).size() != ids.size()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "释放库位不能重复");
|
||||
}
|
||||
if ("AUTO".equals(mode) && !ids.isEmpty()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "AUTO模式不能指定释放库位");
|
||||
}
|
||||
String json = ids.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(",", "[", "]"));
|
||||
return new ReleaseSelection(mode, json);
|
||||
}
|
||||
|
||||
private record ReleaseSelection(String mode, String json) {}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
+2
-1
@@ -114,7 +114,8 @@ public class MerchantInboundBillingService {
|
||||
}
|
||||
|
||||
private static boolean isPublished(InboundOrderEntity inbound) {
|
||||
return InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus())
|
||||
return (InboundProcessStatus.INBOUNDED.equals(inbound.getProcessStatus())
|
||||
|| "WAIT_SPLIT".equals(inbound.getProcessStatus()))
|
||||
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
|
||||
&& "CONFIRMED".equals(inbound.getFeeStatus());
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** PDA任务结束时的纯结算规则,不包含数据库写入。 */
|
||||
@@ -26,8 +27,14 @@ final class PdaPickSettlementPlan {
|
||||
.filter(item -> "PARTIAL".equals(item.getItemStatus()))
|
||||
.map(PdaPickSettlementPlan::splitLine)
|
||||
.collect(Collectors.groupingBy(SplitLine::inboundId));
|
||||
Set<Long> inboundsWithPickedOrPartial = items.stream()
|
||||
.filter(item -> "PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus()))
|
||||
.map(LoadingPickItemEntity::getInboundId)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
List<Long> skippedPlanItemIds = items.stream()
|
||||
.filter(item -> "SKIPPED".equals(item.getItemStatus()))
|
||||
.filter(item -> !inboundsWithPickedOrPartial.contains(item.getInboundId()))
|
||||
.map(LoadingPickItemEntity::getLoadingPlanItemId)
|
||||
.toList();
|
||||
return new Resolution(partialByInbound, skippedPlanItemIds);
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.MeasurementItemRow;
|
||||
import com.baiyue.firstleg.mapper.InboundSplitSettlementMapper.ParentRow;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** PDA 任务结算:未装柜箱数生成二级子入库单,并转移其库位资源归属。 */
|
||||
@Service
|
||||
public class PdaPickSettlementService {
|
||||
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
|
||||
private static final String SOURCE_SERVICE = "firstleg-service";
|
||||
|
||||
@Resource
|
||||
private LoadingPickRepository picks;
|
||||
@Resource
|
||||
private InboundSplitSettlementMapper splits;
|
||||
@Resource
|
||||
private InboundPalletAllocationRepository allocations;
|
||||
@Resource
|
||||
private WarehousePalletLocationClient warehouse;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
/**
|
||||
* 同一父单最多生成一张子单。子单承接该父单所有未装柜数量,而不是只承接 PARTIAL 行的差额。
|
||||
* 纯跳单才恢复智能装柜候选;同单存在已拣或部分拣货时,跳单行随未装柜数量一并归入子单。
|
||||
*/
|
||||
@Transactional
|
||||
public void settle(long taskId, long operatorId, String operatorName) {
|
||||
if (!picks.allTaskItemsResolved(taskId)) {
|
||||
return;
|
||||
}
|
||||
List<LoadingPickItemEntity> taskItems = picks.items(taskId);
|
||||
PdaPickSettlementPlan.Resolution resolution = PdaPickSettlementPlan.resolve(taskItems);
|
||||
for (Long planItemId : resolution.skippedPlanItemIds()) {
|
||||
// 重复 settle(例如复核)时该行已逻辑删除,0 行是可接受的幂等结果。
|
||||
int changed = splits.removeSkippedPlanItem(planItemId, operatorId, operatorName);
|
||||
if (changed != 0 && changed != 1) {
|
||||
throw conflict("跳单明细已被并发修改,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, Integer> pickedByInbound = pickedByInbound(taskItems);
|
||||
Set<Long> newlySplitInbounds = new java.util.HashSet<>();
|
||||
pickedByInbound.entrySet().stream().sorted(Map.Entry.comparingByKey())
|
||||
.forEach(entry -> {
|
||||
if (settleInbound(entry.getKey(), entry.getValue(), taskItems, taskId, operatorId, operatorName)) {
|
||||
newlySplitInbounds.add(entry.getKey());
|
||||
}
|
||||
});
|
||||
for (List<PdaPickSettlementPlan.SplitLine> lines : resolution.partialByInbound().values()) {
|
||||
for (PdaPickSettlementPlan.SplitLine line : lines) {
|
||||
if (newlySplitInbounds.contains(line.inboundId())) {
|
||||
requireAffected(splits.convergePartialPlanItem(line.loadingPlanItemId(), operatorId, operatorName),
|
||||
"部分拣货装柜明细");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settleInbound(long parentInboundId, int pickedPackageCount,
|
||||
List<LoadingPickItemEntity> taskItems, long taskId,
|
||||
long operatorId, String operatorName) {
|
||||
ParentRow parent = splits.lockParent(parentInboundId);
|
||||
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|
||||
|| !"MASTER".equals(parent.inboundRole()) || !"INBOUNDED".equals(parent.processStatus())) {
|
||||
throw conflict("待拆分主入库单不在可拆分状态");
|
||||
}
|
||||
Long existingChild = splits.childId(parent.tenantId(), parentInboundId);
|
||||
if (existingChild != null) {
|
||||
return false;
|
||||
}
|
||||
int parentAvailable = count(parent.packageCount(), "主入库单可用箱数");
|
||||
if (pickedPackageCount <= 0 || pickedPackageCount > parentAvailable) {
|
||||
throw conflict("本次已拣箱数与主入库单可用箱数不一致");
|
||||
}
|
||||
int childPackageCount = parentAvailable - pickedPackageCount;
|
||||
List<InboundPalletAllocationEntity> confirmedAllocations = new ArrayList<>(allocations.lockConfirmed(
|
||||
parent.tenantId(), parentInboundId));
|
||||
confirmedAllocations.sort(Comparator.comparing(InboundPalletAllocationEntity::getAllocationNo,
|
||||
Comparator.nullsLast(Integer::compareTo)));
|
||||
consumePickedAllocations(parent.tenantId(), parentInboundId, confirmedAllocations, pickedPackageCount);
|
||||
List<Long> releasedReservationIds = releasedReservationIds(confirmedAllocations);
|
||||
List<WarehousePalletLocationClient.SettlementTransferItem> transferItems =
|
||||
remainingTransferItems(confirmedAllocations);
|
||||
int allocationRemaining = transferItems.stream()
|
||||
.mapToInt(WarehousePalletLocationClient.SettlementTransferItem::remainingPackageCount).sum();
|
||||
if (allocationRemaining != childPackageCount) {
|
||||
throw conflict("父入库单库位剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
long warehouseId = warehouseId(confirmedAllocations);
|
||||
if (childPackageCount == 0) {
|
||||
if (!transferItems.isEmpty() || releasedReservationIds.isEmpty()) {
|
||||
throw conflict("全量拣货后库位资源结算不完整");
|
||||
}
|
||||
allocations.markReleased(parent.tenantId(), parentInboundId, releasedReservationIds);
|
||||
settleWarehouseResources(parentInboundId, 0L, "", taskId, operatorId, operatorName,
|
||||
warehouseId, parentAvailable, 0, releasedReservationIds, List.of());
|
||||
return false;
|
||||
}
|
||||
|
||||
List<MeasurementItemRow> measurements = splits.lockLatestApprovedItemsForInbound(
|
||||
parent.tenantId(), parentInboundId);
|
||||
if (measurements.isEmpty()) {
|
||||
throw conflict("主入库单缺少已审核测量明细,不能生成子入库单");
|
||||
}
|
||||
long sourceMeasurementId = measurements.get(0).measurementId();
|
||||
if (measurements.stream().anyMatch(item -> !Objects.equals(sourceMeasurementId, item.measurementId()))) {
|
||||
throw conflict("主入库单存在多个已审核测量批次,不能自动拆分");
|
||||
}
|
||||
Map<Long, Integer> pickedByDetail = pickedByDetail(taskItems, parentInboundId);
|
||||
int pickedByMeasurement = pickedByDetail.values().stream().mapToInt(Integer::intValue).sum();
|
||||
if (pickedByMeasurement != pickedPackageCount) {
|
||||
throw conflict("PDA 已拣明细与入库单已拣箱数不一致");
|
||||
}
|
||||
List<MeasurementSplitLine> measurementLines = measurementLines(measurements, pickedByDetail);
|
||||
int measuredRemaining = measurementLines.stream().mapToInt(MeasurementSplitLine::remainingPackageCount).sum();
|
||||
if (measuredRemaining != childPackageCount) {
|
||||
throw conflict("主入库单测量明细剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
|
||||
if (transferItems.isEmpty()) {
|
||||
throw conflict("父入库单可转移库位剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
|
||||
long childId = IdWorker.getId();
|
||||
long batchId = IdWorker.getId();
|
||||
String operationId = "PDA-SPLIT-" + taskId + '-' + parentInboundId;
|
||||
String idempotencyKey = "pda-task-" + taskId;
|
||||
String childCode = childInboundCode(parent.inboundCode(), childId);
|
||||
int parentVersion = parent.version() == null ? 0 : parent.version();
|
||||
requireAffected(splits.cloneChild(parent.tenantId(), parentInboundId, childId, childCode, childPackageCount,
|
||||
operatorId, operatorName), "子入库单");
|
||||
requireAffected(splits.insertBatch(batchId, parent.tenantId(), parentInboundId, operationId,
|
||||
idempotencyKey, parentVersion, childPackageCount, "PDA拣货后未装柜箱数", operatorId, operatorName), "拆分批次");
|
||||
long childMeasurementId = IdWorker.getId();
|
||||
requireAffected(splits.cloneMeasurement(parent.tenantId(), parentInboundId, sourceMeasurementId,
|
||||
childId, childMeasurementId, batchId, childPackageCount, operatorId), "子入库测量批次");
|
||||
|
||||
int lineNo = 1;
|
||||
for (MeasurementSplitLine line : measurementLines) {
|
||||
if (line.remainingPackageCount() <= 0) {
|
||||
continue;
|
||||
}
|
||||
long splitItemId = IdWorker.getId();
|
||||
requireAffected(splits.insertSplitItem(splitItemId, parent.tenantId(), batchId, parentInboundId,
|
||||
childId, lineNo, line.item().inboundDetailId(), sourceMeasurementId,
|
||||
line.item().measurementItemId(), line.remainingPackageCount()), "拆分明细");
|
||||
requireAffected(splits.cloneMeasurementItem(parent.tenantId(), sourceMeasurementId,
|
||||
line.item().measurementItemId(), childMeasurementId, splitItemId, IdWorker.getId(), lineNo,
|
||||
line.remainingPackageCount()), "子入库测量明细");
|
||||
lineNo++;
|
||||
}
|
||||
requireAffected(splits.updateSourceMeasurementPackageCount(parent.tenantId(), parentInboundId,
|
||||
sourceMeasurementId, pickedPackageCount), "主入库测量批次");
|
||||
for (MeasurementSplitLine line : measurementLines) {
|
||||
requireAffected(splits.updateSourceMeasurementItemPackageCount(parent.tenantId(), sourceMeasurementId,
|
||||
line.item().measurementItemId(), line.pickedPackageCount()), "主入库测量明细");
|
||||
}
|
||||
requireAffected(splits.markParentPartiallySplit(parent.tenantId(), parentInboundId, childPackageCount,
|
||||
operatorId, parentVersion), "主入库单拆分状态");
|
||||
requireAffected(splits.applyBatch(parent.tenantId(), batchId), "拆分批次生效");
|
||||
requireAffected(splits.insertHistory(IdWorker.getId(), parent.tenantId(), batchId,
|
||||
operationId + "-APPLY", "{\"childInboundId\":" + childId + ",\"packageCount\":"
|
||||
+ childPackageCount + "}", operatorId, operatorName, "PDA拣货自动生成子入库单"), "拆分审计");
|
||||
|
||||
allocations.copyRemainingToChild(parent.tenantId(), childId, confirmedAllocations);
|
||||
allocations.markTransferred(parent.tenantId(), parentInboundId, confirmedAllocations,
|
||||
releasedReservationIds);
|
||||
allocations.markReleased(parent.tenantId(), parentInboundId, releasedReservationIds);
|
||||
settleWarehouseResources(parentInboundId, childId, childCode, taskId, operatorId, operatorName,
|
||||
warehouseId, parentAvailable, childPackageCount, releasedReservationIds, transferItems);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Map<Long, Integer> pickedByInbound(List<LoadingPickItemEntity> taskItems) {
|
||||
return taskItems.stream().filter(item -> "PICKED".equals(item.getItemStatus())
|
||||
|| "PARTIAL".equals(item.getItemStatus()))
|
||||
.collect(Collectors.groupingBy(LoadingPickItemEntity::getInboundId,
|
||||
Collectors.summingInt(item -> count(item.getPickedPackageCount(), "PDA已拣箱数"))));
|
||||
}
|
||||
|
||||
private static Map<Long, Integer> pickedByDetail(List<LoadingPickItemEntity> taskItems, long inboundId) {
|
||||
Map<Long, Integer> result = new HashMap<>();
|
||||
for (LoadingPickItemEntity item : taskItems) {
|
||||
if (item.getInboundId() == null || item.getInboundId() != inboundId
|
||||
|| !("PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus()))) {
|
||||
continue;
|
||||
}
|
||||
if (item.getInboundDetailId() == null) {
|
||||
throw conflict("PDA 已拣明细缺少入库单明细标识");
|
||||
}
|
||||
result.merge(item.getInboundDetailId(), count(item.getPickedPackageCount(), "PDA已拣箱数"), Integer::sum);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<MeasurementSplitLine> measurementLines(List<MeasurementItemRow> measurements,
|
||||
Map<Long, Integer> pickedByDetail) {
|
||||
Set<Long> measuredDetailIds = measurements.stream().map(MeasurementItemRow::inboundDetailId)
|
||||
.collect(Collectors.toSet());
|
||||
if (!measuredDetailIds.containsAll(pickedByDetail.keySet())) {
|
||||
throw conflict("PDA 已拣明细未匹配已审核测量明细");
|
||||
}
|
||||
List<MeasurementSplitLine> result = new ArrayList<>();
|
||||
for (MeasurementItemRow item : measurements) {
|
||||
int measured = count(item.packageCount(), "测量明细箱数");
|
||||
int picked = pickedByDetail.getOrDefault(item.inboundDetailId(), 0);
|
||||
if (picked > measured) {
|
||||
throw conflict("PDA 已拣箱数超过测量明细箱数");
|
||||
}
|
||||
result.add(new MeasurementSplitLine(item, picked, measured - picked));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void consumePickedAllocations(long accountId, long inboundId,
|
||||
List<InboundPalletAllocationEntity> confirmed, int pickedCount) {
|
||||
int toConsume = pickedCount;
|
||||
for (InboundPalletAllocationEntity allocation : confirmed) {
|
||||
int allocated = count(allocation.getAllocatedPackageCount(), "库位已分配箱数");
|
||||
int picked = allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount();
|
||||
int available = allocated - picked;
|
||||
if (available <= 0 || toConsume == 0) {
|
||||
continue;
|
||||
}
|
||||
int consumed = Math.min(available, toConsume);
|
||||
if (allocation.getId() == null) {
|
||||
throw conflict("父入库单库位分配快照缺少标识");
|
||||
}
|
||||
allocations.consume(accountId, inboundId, allocation.getId(), consumed);
|
||||
allocation.setPickedPackageCount(picked + consumed);
|
||||
toConsume -= consumed;
|
||||
}
|
||||
if (toConsume != 0) {
|
||||
throw conflict("父入库单已确认库位可用箱数不足以扣减本次已拣箱数");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Long> releasedReservationIds(List<InboundPalletAllocationEntity> allocations) {
|
||||
return allocations.stream().filter(item -> remaining(item) == 0).map(PdaPickSettlementService::reservationId)
|
||||
.sorted().toList();
|
||||
}
|
||||
|
||||
private static List<WarehousePalletLocationClient.SettlementTransferItem> remainingTransferItems(
|
||||
List<InboundPalletAllocationEntity> allocations) {
|
||||
return allocations.stream().filter(item -> remaining(item) > 0)
|
||||
.map(item -> new WarehousePalletLocationClient.SettlementTransferItem(reservationId(item), remaining(item)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static int remaining(InboundPalletAllocationEntity allocation) {
|
||||
return count(allocation.getAllocatedPackageCount(), "库位已分配箱数")
|
||||
- (allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount());
|
||||
}
|
||||
|
||||
private static long reservationId(InboundPalletAllocationEntity allocation) {
|
||||
if (allocation.getReservationId() == null || allocation.getReservationId() <= 0) {
|
||||
throw conflict("库位资源缺少唯一reservationId,请先重新确认托盘库位分配");
|
||||
}
|
||||
return allocation.getReservationId();
|
||||
}
|
||||
|
||||
private static long warehouseId(List<InboundPalletAllocationEntity> allocations) {
|
||||
if (allocations.isEmpty() || allocations.get(0).getWarehouseId() == null || allocations.get(0).getWarehouseId() <= 0
|
||||
|| allocations.stream().anyMatch(item -> !Objects.equals(allocations.get(0).getWarehouseId(), item.getWarehouseId()))) {
|
||||
throw conflict("PDA结算要求父入库单分配资源属于同一仓库");
|
||||
}
|
||||
return allocations.get(0).getWarehouseId();
|
||||
}
|
||||
|
||||
private void settleWarehouseResources(long parentInboundId, long childInboundId, String childCode,
|
||||
long taskId, long operatorId, String operatorName, long warehouseId,
|
||||
int sourcePackageCount, int targetPackageCount,
|
||||
List<Long> releasedReservationIds,
|
||||
List<WarehousePalletLocationClient.SettlementTransferItem> remainingItems) {
|
||||
ApiResponse<WarehousePalletLocationClient.SettlementResult> response;
|
||||
try {
|
||||
response = warehouse.settle(internalToken, new WarehousePalletLocationClient.SettlementRequest(
|
||||
warehouseId, SOURCE_SERVICE, parentInboundId, childInboundId,
|
||||
sourcePackageCount, targetPackageCount, childCode, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||
operatorId, operatorName, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||
releasedReservationIds, remainingItems));
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源");
|
||||
}
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| response.data().released() == null || response.data().transferred() == null
|
||||
|| !sameReleasedReservations(releasedReservationIds, response.data().released())
|
||||
|| !sameTransferredResources(remainingItems, response.data().transferred())) {
|
||||
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
|
||||
throw new BizException(code, response == null || response.message() == null
|
||||
? "仓储服务原子结算PDA库位资源失败" : response.message());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameReleasedReservations(List<Long> expected,
|
||||
List<WarehousePalletLocationClient.SelectiveReleaseResult> actual) {
|
||||
return expected.equals(actual.stream().map(WarehousePalletLocationClient.SelectiveReleaseResult::reservationId)
|
||||
.sorted().toList());
|
||||
}
|
||||
|
||||
private static boolean sameTransferredResources(List<WarehousePalletLocationClient.SettlementTransferItem> expected,
|
||||
List<WarehousePalletLocationClient.TransferResult> actual) {
|
||||
Map<Long, Integer> expectedByLocation = expected.stream().collect(Collectors.toMap(
|
||||
WarehousePalletLocationClient.SettlementTransferItem::reservationId,
|
||||
WarehousePalletLocationClient.SettlementTransferItem::remainingPackageCount));
|
||||
Map<Long, Integer> actualByLocation = actual.stream().collect(Collectors.toMap(
|
||||
WarehousePalletLocationClient.TransferResult::reservationId,
|
||||
WarehousePalletLocationClient.TransferResult::remainingPackageCount,
|
||||
(left, right) -> -1));
|
||||
return expectedByLocation.equals(actualByLocation);
|
||||
}
|
||||
|
||||
private static String childInboundCode(String parentCode, long childId) {
|
||||
String prefix = parentCode == null ? "INBOUND" : parentCode.trim();
|
||||
if (prefix.isEmpty()) {
|
||||
prefix = "INBOUND";
|
||||
}
|
||||
String suffix = "-S" + Long.toUnsignedString(childId).substring(0, 6);
|
||||
return prefix.substring(0, Math.min(prefix.length(), 40 - suffix.length())) + suffix;
|
||||
}
|
||||
|
||||
private static int count(Integer value, String resource) {
|
||||
if (value == null || value < 0) {
|
||||
throw conflict(resource + "不合法");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void requireAffected(int affected, String resource) {
|
||||
if (affected != 1) {
|
||||
throw conflict(resource + "已被并发修改,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private record MeasurementSplitLine(MeasurementItemRow item, int pickedPackageCount,
|
||||
int remainingPackageCount) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,9 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedInboundQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
/** 仅兼容旧客户端筛选值,响应和 OpenAPI 均只使用 INBOUNDED。 */
|
||||
private static final String LEGACY_WAIT_SPLIT = "WAIT_SPLIT";
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
|
||||
@Resource private InboundOrderRepository inbounds;
|
||||
@@ -134,8 +136,7 @@ public class UnifiedInboundQueryService {
|
||||
if (status == null) return null;
|
||||
return switch (status) {
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE";
|
||||
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED" -> "INBOUNDED";
|
||||
case "WAIT_SPLIT" -> "WAIT_SPLIT";
|
||||
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED", LEGACY_WAIT_SPLIT -> "INBOUNDED";
|
||||
case "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
@@ -146,10 +147,9 @@ public class UnifiedInboundQueryService {
|
||||
return switch (mainStatus) {
|
||||
case "PENDING_REVIEW" -> "APPROVE_ORDER";
|
||||
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null;
|
||||
case "INBOUNDED" -> !"INBOUNDED".equals(processStatus) ? null
|
||||
case "INBOUNDED" -> !("INBOUNDED".equals(processStatus) || LEGACY_WAIT_SPLIT.equals(processStatus)) ? null
|
||||
: "CONFIRMED".equals(preloadStatus) ? null
|
||||
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
|
||||
case "WAIT_SPLIT" -> null;
|
||||
case "LOADING" -> "COMPLETE_LOADING";
|
||||
case "LOADED" -> "COMPLETE_DELIVERY";
|
||||
case "DELIVERED" -> null;
|
||||
@@ -165,8 +165,7 @@ public class UnifiedInboundQueryService {
|
||||
if (!StringUtils.hasText(mainStatus)) return List.of();
|
||||
return switch (mainStatus) {
|
||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
|
||||
case "INBOUNDED" -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED");
|
||||
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
|
||||
case "INBOUNDED", LEGACY_WAIT_SPLIT -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED", LEGACY_WAIT_SPLIT);
|
||||
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
|
||||
default -> List.of("__NO_MATCH__");
|
||||
};
|
||||
@@ -183,7 +182,9 @@ public class UnifiedInboundQueryService {
|
||||
}
|
||||
|
||||
private void validate(UnifiedInboundQuery query) {
|
||||
if (StringUtils.hasText(query.mainStatus()) && !MAIN_STATUSES.contains(query.mainStatus())) {
|
||||
if (StringUtils.hasText(query.mainStatus())
|
||||
&& !MAIN_STATUSES.contains(query.mainStatus())
|
||||
&& !LEGACY_WAIT_SPLIT.equals(query.mainStatus())) {
|
||||
throw new com.baiyue.common.core.exception.BizException(
|
||||
com.baiyue.common.core.api.ResultCode.BAD_REQUEST, "统一订单主状态不合法");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user