优化
This commit is contained in:
@@ -4,12 +4,21 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/** Validates that a referenced File object belongs to the exact loading business resource. */
|
||||
@FeignClient(name = "file-service", contextId = "firstlegLoadingDocumentFileClient",
|
||||
url = "${baiyue.clients.file.url:}", path = "/internal/file/v1/business-files")
|
||||
public interface LoadingDocumentFileClient {
|
||||
@PostMapping(value = "/{tenantId}/{businessType}/{businessId}", consumes = "multipart/form-data")
|
||||
ApiResponse<Metadata> upload(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@PathVariable long tenantId, @PathVariable String businessType,
|
||||
@PathVariable String businessId, @RequestPart("file") MultipartFile file);
|
||||
|
||||
@GetMapping("/{tenantId}/{businessType}/{businessId}/{fileId}")
|
||||
ApiResponse<Metadata> metadata(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long tenantId, @PathVariable String businessType,
|
||||
|
||||
+18
@@ -19,11 +19,15 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Tag(name = "货柜基础资料") @Validated @RestController
|
||||
@RequestMapping("/api/platform/v1/containers")
|
||||
@@ -87,6 +91,20 @@ public class PlatformContainerController {
|
||||
id, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{id}/clearance/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@Operation(summary = "直接上传或替换货柜目的港清关资料")
|
||||
public ApiResponse<BusinessDocument> uploadClearance(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@RequestPart("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestPart(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(loadingLists.uploadContainerClearance(
|
||||
id, user.userId(), user.username(), key, expectedVersion, operationReason, file));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/clearance/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@Operation(summary = "确认货柜目的港清关完成")
|
||||
|
||||
+20
@@ -19,7 +19,10 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -32,6 +35,8 @@ import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/** 基于同一装柜计划聚合提供平台预装清单和已确认装柜清单。 */
|
||||
@Tag(name = "平台预装与装柜清单", description = "预装单向推进和已确认装柜清单只读查询")
|
||||
@@ -116,6 +121,21 @@ public class PlatformLoadingListController {
|
||||
planId, warehouseId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "直接上传或替换装柜清单出口报关文档")
|
||||
@PostMapping(value = "/loading-lists/{planId}/customs/document/upload",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
public ApiResponse<BusinessDocument> uploadDeclaration(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long planId,
|
||||
@RequestParam @Positive long warehouseId, @RequestHeader("Idempotency-Key") String key,
|
||||
@RequestPart("file") MultipartFile file,
|
||||
@RequestPart("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestPart(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.uploadLoadingDeclaration(planId, warehouseId,
|
||||
user.userId(), user.username(), key, expectedVersion, operationReason, file));
|
||||
}
|
||||
|
||||
@Operation(summary = "确认装柜清单出口报关完成")
|
||||
@PostMapping("/loading-lists/{planId}/customs/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
|
||||
+6
-7
@@ -1,7 +1,7 @@
|
||||
package com.baiyue.firstleg.customs.service;
|
||||
|
||||
import com.baiyue.firstleg.customs.entity.CustomsCaseEntity;
|
||||
import com.baiyue.firstleg.customs.repository.CustomsRepository;
|
||||
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.baiyue.firstleg.outbound.dispatch.dto.OutboundDispatchDtos.GateItem;
|
||||
import com.baiyue.firstleg.outbound.dispatch.gate.OutboundDispatchGateContributor;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class CustomsDispatchGateContributor implements OutboundDispatchGateContributor {
|
||||
@Resource private CustomsRepository repository;
|
||||
@Resource private LoadingListRepository repository;
|
||||
|
||||
@Override public String code() { return "CUSTOMS_CLEARANCE"; }
|
||||
|
||||
@@ -18,13 +18,12 @@ public class CustomsDispatchGateContributor implements OutboundDispatchGateContr
|
||||
if (context.tenantId() <= 0 || context.customerId() <= 0) {
|
||||
return gate(false, "INVALID_CUSTOMER_SCOPE");
|
||||
}
|
||||
CustomsCaseEntity value = repository.gateCase(context.tenantId(), context.orderId(), context.inboundId(),
|
||||
context.containerBookingId());
|
||||
ContainerDocumentEntity value = repository.document(context.containerBookingId(), "CUSTOMS_DECLARATION");
|
||||
if (value == null) return gate(false, "MISSING");
|
||||
return gate("CLEARANCE_COMPLETED".equals(value.getStatus()), value.getStatus());
|
||||
return gate(value.getCompletedAt() != null, value.getCompletedAt() == null ? "UPLOADED" : "COMPLETED");
|
||||
}
|
||||
|
||||
private static GateItem gate(boolean passed, String actual) {
|
||||
return new GateItem("CUSTOMS_CLEARANCE", "报关放行", passed, actual, "CLEARANCE_COMPLETED");
|
||||
return new GateItem("CUSTOMS_CLEARANCE", "报关完成", passed, actual, "COMPLETED");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,9 @@ public final class InboundProcessStatus {
|
||||
public static final String FEE_CONFIRMING = "FEE_CONFIRMING";
|
||||
/** 费用已确认,正在执行托盘、库位占用和入库收口。 */
|
||||
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";
|
||||
@@ -36,7 +33,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_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_SPLIT,
|
||||
LOADING, LOADED, DELIVERED, VOIDED
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
|
||||
+2
-13
@@ -16,27 +16,16 @@ public interface LoadingCompletionGateMapper {
|
||||
COALESCE(SUM(CASE WHEN EXISTS (
|
||||
SELECT 1 FROM fl_truck_appointment t
|
||||
WHERE t.container_booking_id=p.container_booking_id AND t.loading_plan_id=p.id
|
||||
AND t.status='COMPLETED') THEN 1 ELSE 0 END),0) truckReady,
|
||||
COALESCE(SUM(CASE WHEN EXISTS (
|
||||
SELECT 1 FROM fl_customs_case c
|
||||
JOIN fl_customs_receipt r ON r.customs_case_id=c.id
|
||||
WHERE c.tenant_id=l.tenant_id AND c.inbound_id=l.inbound_id
|
||||
AND c.container_booking_id=p.container_booking_id
|
||||
AND c.status='CLEARANCE_COMPLETED' AND r.receipt_type='RELEASED' AND r.file_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_customs_receipt newer
|
||||
WHERE newer.submission_batch_id=r.submission_batch_id
|
||||
AND newer.receipt_type=r.receipt_type AND newer.receipt_version>r.receipt_version))
|
||||
THEN 1 ELSE 0 END),0) customsReady
|
||||
AND t.status='COMPLETED') THEN 1 ELSE 0 END),0) truckReady
|
||||
FROM fl_preload_loading_plan_link l
|
||||
JOIN fl_loading_plan p ON p.id=l.loading_plan_id
|
||||
WHERE l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}
|
||||
""")
|
||||
GateSnapshot snapshot(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
record GateSnapshot(Integer linkCount, Integer oceanReady, Integer truckReady, Integer customsReady) {
|
||||
record GateSnapshot(Integer linkCount, Integer oceanReady, Integer truckReady) {
|
||||
public boolean allBookingReady() { return count(linkCount) > 0 && count(linkCount) == count(oceanReady); }
|
||||
public boolean allTruckReady() { return count(linkCount) > 0 && count(linkCount) == count(truckReady); }
|
||||
public boolean allCustomsReady() { return count(linkCount) > 0 && count(linkCount) == count(customsReady); }
|
||||
private static int count(Integer value) { return value == null ? 0 : value; }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -11,14 +11,13 @@ import org.springframework.stereotype.Service;
|
||||
public class LoadingCompletionGateService {
|
||||
@Resource private LoadingCompletionGateMapper mapper;
|
||||
|
||||
public void requireBookingTransportAndCustoms(long tenantId, long inboundId) {
|
||||
public void requireBookingAndTransport(long tenantId, long inboundId) {
|
||||
LoadingCompletionGateMapper.GateSnapshot gate = mapper.snapshot(tenantId, inboundId);
|
||||
if (gate == null || gate.linkCount() == null || gate.linkCount() == 0) {
|
||||
throw blocked("未找到有效柜次关联");
|
||||
}
|
||||
if (!gate.allBookingReady()) throw blocked("订舱尚未完成(必须为 RELEASED)");
|
||||
if (!gate.allTruckReady()) throw blocked("拖车尚未完成(必须为 COMPLETED)");
|
||||
if (!gate.allCustomsReady()) throw blocked("缺少有效报关 RELEASED 放行回执");
|
||||
}
|
||||
|
||||
private static BizException blocked(String message) {
|
||||
|
||||
+2
-2
@@ -111,7 +111,7 @@ public class LoadingDeliveryService {
|
||||
}
|
||||
if (!"LOADING".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜中");
|
||||
loadingModelBridge.requireCompleted(tenantId, inboundId);
|
||||
completionGate.requireBookingTransportAndCustoms(tenantId, inboundId);
|
||||
completionGate.requireBookingAndTransport(tenantId, inboundId);
|
||||
requireCompletedPicking(tenantId, inboundId);
|
||||
requireCompletionEvidence(tenantId, inboundId);
|
||||
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||
@@ -132,7 +132,7 @@ public class LoadingDeliveryService {
|
||||
throw new BizException(ResultCode.CONFLICT, "实装履约分片不在装柜中");
|
||||
}
|
||||
loadingModelBridge.requireCompleted(tenantId, target.sourceInboundId());
|
||||
completionGate.requireBookingTransportAndCustoms(tenantId, target.sourceInboundId());
|
||||
completionGate.requireBookingAndTransport(tenantId, target.sourceInboundId());
|
||||
requireCompletedPicking(tenantId, target.sourceInboundId());
|
||||
requireCompletionEvidence(tenantId, target.sourceInboundId());
|
||||
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
|
||||
|
||||
@@ -92,6 +92,10 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
WHERE id=#{id} AND version=#{expectedVersion}
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_plan p
|
||||
WHERE p.container_booking_id=fl_container_booking.id AND p.plan_status='COMPLETED')
|
||||
AND (#{containerNo} IS NOT NULL OR NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_pick_task t
|
||||
JOIN fl_loading_plan p ON p.id=t.loading_plan_id
|
||||
WHERE p.container_booking_id=fl_container_booking.id))
|
||||
""")
|
||||
int updateTransport(@Param("id") long id, @Param("containerNo") String containerNo,
|
||||
@Param("voyageNo") String voyageNo, @Param("expectedVersion") int expectedVersion,
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Update;
|
||||
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'INBOUNDED', measure_review_status = 'APPROVED',
|
||||
SET process_status = 'WAIT_PRICING', measure_review_status = 'APPROVED',
|
||||
actual_gross_weight_kg = (SELECT COALESCE(SUM(item.total_weight_kg), 0)
|
||||
FROM fl_inbound_measurement_item item
|
||||
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
|
||||
@@ -21,7 +21,6 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
volume_weight_kg = (SELECT COALESCE(SUM(item.volume_weight_kg), 0)
|
||||
FROM fl_inbound_measurement_item item
|
||||
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
|
||||
inbounded_at = COALESCE(inbounded_at, CURRENT_TIMESTAMP(3)),
|
||||
updated_by = #{operatorId}, version = version + 1
|
||||
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
|
||||
AND process_status = 'MEASURE_PENDING' AND measure_review_status = 'PENDING'
|
||||
@@ -69,7 +68,7 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'INBOUNDED', fee_status = 'SENT', notification_status = 'PENDING',
|
||||
SET process_status = 'WAIT_SPLIT', fee_status = 'SENT', notification_status = 'PENDING',
|
||||
inbounded_at = CURRENT_TIMESTAMP(3), updated_by = #{operatorId}, version = version + 1
|
||||
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
|
||||
AND process_status = 'INBOUNDING' AND fee_status = 'CONFIRMED'
|
||||
|
||||
@@ -213,6 +213,8 @@ public interface LoadingListQueryMapper {
|
||||
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
|
||||
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
|
||||
WHERE p.id = #{planId} AND p.plan_status = 'LOADING'
|
||||
AND (#{containerNo} IS NOT NULL OR NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_pick_task t WHERE t.loading_plan_id = p.id))
|
||||
""")
|
||||
int updateTransport(@Param("planId") long planId, @Param("containerNo") String containerNo,
|
||||
@Param("voyageNo") String voyageNo, @Param("operatorId") long operatorId,
|
||||
|
||||
@@ -23,7 +23,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.process_status='INBOUNDED'
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED')
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
AND i.inbound_detail_id IS NOT NULL
|
||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
@@ -61,7 +61,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status='INBOUNDED'
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED')
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
AND i.inbound_detail_id=#{detailId}
|
||||
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
|
||||
|
||||
@@ -209,7 +209,7 @@ public class FbaPricingRepository {
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
|
||||
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
||||
if (!"INBOUNDED".equals(processStatus)) {
|
||||
if (!List.of("WAIT_SPLIT", "INBOUNDED").contains(processStatus)) {
|
||||
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
|
||||
}
|
||||
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
||||
@@ -229,7 +229,7 @@ public class FbaPricingRepository {
|
||||
public int markInboundConfirmed(long tenantId, long inboundId) {
|
||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
||||
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "INBOUNDED"))
|
||||
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED"))
|
||||
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
||||
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
||||
}
|
||||
|
||||
+2
-1
@@ -58,7 +58,8 @@ public class AutomaticInboundCompletionService {
|
||||
}
|
||||
|
||||
private static boolean completed(InboundOrderEntity order) {
|
||||
return "INBOUNDED".equals(order.getProcessStatus()) && "SENT".equals(order.getFeeStatus());
|
||||
return ("WAIT_SPLIT".equals(order.getProcessStatus()) || "INBOUNDED".equals(order.getProcessStatus()))
|
||||
&& "SENT".equals(order.getFeeStatus());
|
||||
}
|
||||
|
||||
private InboundOrderEntity requireOrder(long tenantId, long inboundId) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -31,6 +32,7 @@ public class ContainerService {
|
||||
@Resource private ContainerRepository repository;
|
||||
@Resource private WarehouseMasterDataClient warehouses;
|
||||
@Resource private SmartLoadingRepository smartLoadingRepository;
|
||||
@Resource private LoadingPickRepository loadingPickRepository;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@Autowired(required = false) private ContainerTransportHistoryMapper transportHistories;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@@ -86,6 +88,10 @@ public class ContainerService {
|
||||
requireVersion(current, request.version());
|
||||
if (status(current) == SmartLoadingStatus.COMPLETED) throw conflict("装柜完成后货柜全部只读,不能修改");
|
||||
String containerNo = trim(request.containerNo());
|
||||
LoadingPlanEntity plan = smartLoadingRepository.planByBooking(id);
|
||||
if (containerNo == null && plan != null && loadingPickRepository.taskByPlan(plan.getId()) != null) {
|
||||
throw conflict("PDA拣货任务已创建,不能清空真实集装箱号");
|
||||
}
|
||||
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
|
||||
try {
|
||||
if (repository.updateTransport(id, containerNo, trim(request.voyageNo()), request.version(),
|
||||
|
||||
@@ -82,8 +82,8 @@ public class FbaPricingService {
|
||||
throw conflict("入库单状态已变化,费用试算未保存");
|
||||
}
|
||||
complete(token, fee.getId(), now);
|
||||
String nextStatus = "INBOUNDED".equals(inbound.getProcessStatus())
|
||||
? "INBOUNDED" : "FEE_CONFIRMING";
|
||||
String nextStatus = isCompletedInbound(inbound.getProcessStatus())
|
||||
? inbound.getProcessStatus() : "FEE_CONFIRMING";
|
||||
return response(inbound, fee, rate.getUnitPrice(), nextStatus);
|
||||
}
|
||||
|
||||
@@ -114,8 +114,8 @@ public class FbaPricingService {
|
||||
String hash = hash("SEND", inboundId, current.getId().toString());
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(
|
||||
tenantId, operatorId, inboundId, "FEE_SEND", idempotencyKey, hash);
|
||||
String nextProcessStatus = "INBOUNDED".equals(inbound.getProcessStatus())
|
||||
? "INBOUNDED" : "WAIT_PALLET";
|
||||
String nextProcessStatus = isCompletedInbound(inbound.getProcessStatus())
|
||||
? inbound.getProcessStatus() : "WAIT_PALLET";
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
feeDetails.publishFreight(tenantId, inboundId, token.getResultResourceId(),
|
||||
operatorId, null, LocalDateTime.now(clock));
|
||||
@@ -144,7 +144,7 @@ public class FbaPricingService {
|
||||
AdjustFeeRequest request, String idempotencyKey) {
|
||||
InboundPricingEntity inbound = repository.findInbound(tenantId, inboundId);
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
if (!"INBOUNDED".equals(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) {
|
||||
if (!isCompletedInbound(inbound.getProcessStatus()) || !"SENT".equals(inbound.getFeeStatus())) {
|
||||
throw conflict("只能修改已完成入库且已发布的费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
@@ -156,7 +156,7 @@ public class FbaPricingService {
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(tenantId, operatorId, inboundId,
|
||||
"FEE_ADJUST", idempotencyKey, hash("ADJUST", inboundId, discriminator));
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
return response(tenantId, token.getResultResourceId(), "INBOUNDED");
|
||||
return response(tenantId, token.getResultResourceId(), inbound.getProcessStatus());
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
@@ -204,12 +204,12 @@ public class FbaPricingService {
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.ADJUSTED,
|
||||
tenantId, inboundId, adjusted.getId(), nextVersion, now);
|
||||
complete(token, adjusted.getId(), now);
|
||||
return response(tenantId, adjusted.getId(), "INBOUNDED");
|
||||
return response(tenantId, adjusted.getId(), inbound.getProcessStatus());
|
||||
}
|
||||
|
||||
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDED").contains(inbound.getProcessStatus())
|
||||
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED").contains(inbound.getProcessStatus())
|
||||
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
|
||||
throw conflict("当前入库单状态不允许费用试算");
|
||||
}
|
||||
@@ -234,6 +234,10 @@ public class FbaPricingService {
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private static boolean isCompletedInbound(String processStatus) {
|
||||
return "WAIT_SPLIT".equals(processStatus) || "INBOUNDED".equals(processStatus);
|
||||
}
|
||||
|
||||
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||
try {
|
||||
return PricingDestinationType.valueOf(inbound.getDestinationType());
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ public class InboundCompletionOrchestrator {
|
||||
var published = feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId,
|
||||
operatorName, LocalDateTime.now());
|
||||
repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId,
|
||||
"INBOUNDING", "INBOUNDED", "SEND_FEE_COMPLETE", null, operatorId, operatorName));
|
||||
"INBOUNDING", "WAIT_SPLIT", "SEND_FEE_COMPLETE", null, operatorId, operatorName));
|
||||
financeOutbox.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, tenantId,
|
||||
inboundId, feeId, published.getSourceFeeVersion(), LocalDateTime.now());
|
||||
idempotency.complete(context, inboundId);
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
@@ -103,7 +104,18 @@ public class LoadingListService {
|
||||
String key, DocumentAttachRequest request) {
|
||||
AggregateRow plan = requireBookingPlan(bookingId);
|
||||
return attachDocument(plan, DESTINATION_CLEARANCE, CLEARANCE_FILE_TYPE,
|
||||
Long.toString(bookingId), operatorId, operatorName, key, request);
|
||||
bookingId, Long.toString(bookingId), operatorId, operatorName, key, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "CONTAINER_CLEARANCE_DOCUMENT_UPLOAD", resourceType = "CONTAINER",
|
||||
resourceId = "#bookingId", description = "直接上传目的港清关资料")
|
||||
public BusinessDocument uploadContainerClearance(long bookingId, long operatorId, String operatorName,
|
||||
String key, int expectedVersion, String operationReason,
|
||||
MultipartFile file) {
|
||||
AggregateRow plan = requireBookingPlan(bookingId);
|
||||
return uploadDocument(plan, DESTINATION_CLEARANCE, CLEARANCE_FILE_TYPE, bookingId,
|
||||
Long.toString(bookingId), operatorId, operatorName, key, expectedVersion, operationReason, file);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -111,7 +123,7 @@ public class LoadingListService {
|
||||
resourceId = "#bookingId", description = "确认目的港清关完成")
|
||||
public BusinessDocument completeContainerClearance(long bookingId, long operatorId, String operatorName,
|
||||
String key, DocumentCompleteRequest request) {
|
||||
return completeDocument(requireBookingPlan(bookingId), DESTINATION_CLEARANCE,
|
||||
return completeDocument(requireBookingPlan(bookingId), DESTINATION_CLEARANCE, bookingId,
|
||||
operatorId, operatorName, key, request);
|
||||
}
|
||||
|
||||
@@ -132,7 +144,20 @@ public class LoadingListService {
|
||||
requireWarehouse(plan, warehouseId);
|
||||
requireCompletedPlan(plan);
|
||||
return attachDocument(plan, CUSTOMS_DECLARATION, DECLARATION_FILE_TYPE,
|
||||
Long.toString(planId), operatorId, operatorName, key, request);
|
||||
planId, Long.toString(planId), operatorId, operatorName, key, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "LOADING_CUSTOMS_DOCUMENT_UPLOAD", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "直接上传出口报关文档")
|
||||
public BusinessDocument uploadLoadingDeclaration(long planId, long warehouseId, long operatorId,
|
||||
String operatorName, String key, int expectedVersion,
|
||||
String operationReason, MultipartFile file) {
|
||||
AggregateRow plan = requirePlan(planId);
|
||||
requireWarehouse(plan, warehouseId);
|
||||
requireCompletedPlan(plan);
|
||||
return uploadDocument(plan, CUSTOMS_DECLARATION, DECLARATION_FILE_TYPE, planId,
|
||||
Long.toString(planId), operatorId, operatorName, key, expectedVersion, operationReason, file);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -144,28 +169,55 @@ public class LoadingListService {
|
||||
AggregateRow plan = requirePlan(planId);
|
||||
requireWarehouse(plan, warehouseId);
|
||||
requireCompletedPlan(plan);
|
||||
return completeDocument(plan, CUSTOMS_DECLARATION, operatorId, operatorName, key, request);
|
||||
return completeDocument(plan, CUSTOMS_DECLARATION, planId, operatorId, operatorName, key, request);
|
||||
}
|
||||
|
||||
private BusinessDocument attachDocument(AggregateRow plan, String documentType, String businessType,
|
||||
String businessId, long operatorId, String operatorName,
|
||||
long identityId, String businessId, long operatorId, String operatorName,
|
||||
String key, DocumentAttachRequest request) {
|
||||
String requestHash = sha256("ATTACH|" + documentType + '|' + plan.getPlanId() + '|'
|
||||
String requestHash = sha256("ATTACH|" + documentType + '|' + identityId + '|'
|
||||
+ request.fileId() + '|' + request.expectedVersion() + '|' + trim(request.operationReason()));
|
||||
String operationId = operationId("ATTACH_" + documentType, plan.getPlanId(), key);
|
||||
String operationId = operationId("ATTACH_" + documentType, identityId, key);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
requireSameRequest(replay, requestHash);
|
||||
return requireDocument(plan.getContainerBookingId(), documentType);
|
||||
}
|
||||
ContainerDocumentEntity current = repository.document(plan.getContainerBookingId(), documentType);
|
||||
if (current == null && request.expectedVersion() != 0) throw conflict("资料版本已变化,请刷新后重试");
|
||||
if (current != null && !java.util.Objects.equals(current.getVersion(), request.expectedVersion())) {
|
||||
throw conflict("资料版本已变化,请刷新后重试");
|
||||
}
|
||||
if (current != null && current.getCompletedAt() != null) throw conflict("已完成的资料不可替换");
|
||||
requireReplaceableDocument(current, request.expectedVersion());
|
||||
LoadingDocumentFileClient.Metadata metadata = validateDocumentFile(
|
||||
businessType, businessId, request.fileId());
|
||||
return persistDocument(plan, documentType, current, metadata, operatorId, operatorName,
|
||||
operationId, "ATTACH_" + documentType, requestHash, request.operationReason());
|
||||
}
|
||||
|
||||
private BusinessDocument uploadDocument(AggregateRow plan, String documentType, String businessType,
|
||||
long identityId, String businessId, long operatorId,
|
||||
String operatorName, String key, int expectedVersion,
|
||||
String operationReason, MultipartFile file) {
|
||||
requireUploadFile(file);
|
||||
String fileHash = multipartHash(file);
|
||||
String requestHash = sha256("UPLOAD|" + documentType + '|' + identityId + '|'
|
||||
+ expectedVersion + '|' + trim(operationReason) + '|' + fileHash);
|
||||
String operationId = operationId("UPLOAD_" + documentType, identityId, key);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
requireSameRequest(replay, requestHash);
|
||||
return requireDocument(plan.getContainerBookingId(), documentType);
|
||||
}
|
||||
ContainerDocumentEntity current = repository.document(plan.getContainerBookingId(), documentType);
|
||||
requireReplaceableDocument(current, expectedVersion);
|
||||
LoadingDocumentFileClient.Metadata metadata = uploadDocumentFile(
|
||||
businessType, businessId, operatorId, file, fileHash);
|
||||
return persistDocument(plan, documentType, current, metadata, operatorId, operatorName,
|
||||
operationId, "UPLOAD_" + documentType, requestHash, operationReason);
|
||||
}
|
||||
|
||||
private BusinessDocument persistDocument(AggregateRow plan, String documentType,
|
||||
ContainerDocumentEntity current,
|
||||
LoadingDocumentFileClient.Metadata metadata,
|
||||
long operatorId, String operatorName, String operationId,
|
||||
String actionType, String requestHash, String operationReason) {
|
||||
Map<String, Object> beforeSnapshot = documentSnapshot(current);
|
||||
ContainerDocumentEntity value = current == null ? new ContainerDocumentEntity() : current;
|
||||
if (current == null) value.setId(IdWorker.getId());
|
||||
@@ -179,19 +231,19 @@ public class LoadingListService {
|
||||
throw conflict("资料已被其他操作上传,请刷新后重试");
|
||||
}
|
||||
ContainerDocumentEntity saved = requireDocumentEntity(plan.getContainerBookingId(), documentType);
|
||||
repository.appendHistory(plan.getPlanId(), operationId, "ATTACH_" + documentType,
|
||||
repository.appendHistory(plan.getPlanId(), operationId, actionType,
|
||||
plan.getPlanStatus(), plan.getPlanStatus(), json(beforeSnapshot),
|
||||
json(values("requestHash", requestHash, "document", documentSnapshot(saved))),
|
||||
operatorId, trim(operatorName), trim(request.operationReason()));
|
||||
operatorId, trim(operatorName), trim(operationReason));
|
||||
return document(saved);
|
||||
}
|
||||
|
||||
private BusinessDocument completeDocument(AggregateRow plan, String documentType, long operatorId,
|
||||
private BusinessDocument completeDocument(AggregateRow plan, String documentType, long identityId, long operatorId,
|
||||
String operatorName, String key,
|
||||
DocumentCompleteRequest request) {
|
||||
String requestHash = sha256("COMPLETE|" + documentType + '|' + plan.getPlanId() + '|'
|
||||
String requestHash = sha256("COMPLETE|" + documentType + '|' + identityId + '|'
|
||||
+ request.expectedVersion() + '|' + trim(request.operationReason()));
|
||||
String operationId = operationId("COMPLETE_" + documentType, plan.getPlanId(), key);
|
||||
String operationId = operationId("COMPLETE_" + documentType, identityId, key);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
requireSameRequest(replay, requestHash);
|
||||
@@ -224,7 +276,8 @@ public class LoadingListService {
|
||||
|| !businessType.equals(response.data().businessType())
|
||||
|| !businessId.equals(response.data().businessId())
|
||||
|| !StringUtils.hasText(response.data().fileName())
|
||||
|| !StringUtils.hasText(response.data().sha256())) {
|
||||
|| response.data().sha256() == null
|
||||
|| !response.data().sha256().matches("[0-9a-fA-F]{64}")) {
|
||||
throw badRequest("附件不存在、归属不匹配或文件元数据不完整");
|
||||
}
|
||||
return response.data();
|
||||
@@ -233,6 +286,48 @@ public class LoadingListService {
|
||||
}
|
||||
}
|
||||
|
||||
private LoadingDocumentFileClient.Metadata uploadDocumentFile(String businessType, String businessId,
|
||||
long operatorId, MultipartFile file,
|
||||
String expectedHash) {
|
||||
try {
|
||||
var uploaded = documentFiles.upload(internalToken, operatorId, 0, businessType, businessId, file);
|
||||
if (uploaded == null || !uploaded.successful() || uploaded.data() == null
|
||||
|| uploaded.data().fileId() == null || uploaded.data().fileId() <= 0) {
|
||||
throw badRequest("附件上传失败");
|
||||
}
|
||||
LoadingDocumentFileClient.Metadata metadata = validateDocumentFile(
|
||||
businessType, businessId, uploaded.data().fileId());
|
||||
if (!expectedHash.equalsIgnoreCase(metadata.sha256())) {
|
||||
throw badRequest("附件内容哈希校验失败");
|
||||
}
|
||||
return metadata;
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireReplaceableDocument(ContainerDocumentEntity current, int expectedVersion) {
|
||||
if (current == null && expectedVersion != 0) throw conflict("资料版本已变化,请刷新后重试");
|
||||
if (current != null && !java.util.Objects.equals(current.getVersion(), expectedVersion)) {
|
||||
throw conflict("资料版本已变化,请刷新后重试");
|
||||
}
|
||||
if (current != null && current.getCompletedAt() != null) throw conflict("已完成的资料不可替换");
|
||||
}
|
||||
|
||||
private static void requireUploadFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) throw badRequest("必须选择上传文件");
|
||||
}
|
||||
|
||||
private static String multipartHash(MultipartFile file) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(file.getBytes()));
|
||||
} catch (java.io.IOException exception) {
|
||||
throw badRequest("读取上传文件失败");
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private AggregateRow requireBookingPlan(long bookingId) {
|
||||
AggregateRow row = repository.aggregateByBooking(bookingId);
|
||||
if (row == null) throw notFound("货柜或关联装柜计划不存在");
|
||||
@@ -286,6 +381,9 @@ public class LoadingListService {
|
||||
}
|
||||
|
||||
requireLoadingMutable(before);
|
||||
if (containerNo == null && pickRepository.taskByPlan(planId) != null) {
|
||||
throw conflict("PDA拣货任务已创建,不能清空真实集装箱号");
|
||||
}
|
||||
if (repository.updateTransport(planId, request.expectedVersion(), containerNo, voyageNo,
|
||||
operatorId, trim(operatorName)) != 1) {
|
||||
throw conflict("预装计划已被其他操作修改,请刷新后重试");
|
||||
|
||||
@@ -21,8 +21,7 @@ import java.time.LocalDateTime;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 INBOUNDED;
|
||||
* 计费、库位和其他资料不作为本版本的强制门禁。
|
||||
* 测量审核应用服务。审核通过后进入内部计费编排,完成费用发布和入库后再进入 WAIT_SPLIT。
|
||||
*/
|
||||
@Service
|
||||
public class MeasurementReviewService {
|
||||
@@ -100,7 +99,7 @@ public class MeasurementReviewService {
|
||||
repository.insertReview(reviewRecord(tenantId, inboundId, request.measurementId(), result,
|
||||
request.reason(), false, null, false, operatorId, operatorName));
|
||||
repository.insertHistory(history(tenantId, inboundId, "MEASURE_PENDING",
|
||||
approved ? "INBOUNDED" : "MEASURE_REJECTED",
|
||||
approved ? "WAIT_PRICING" : "MEASURE_REJECTED",
|
||||
approved ? "MEASURE_APPROVE" : "MEASURE_REJECT", request.reason(), operatorId, operatorName));
|
||||
idempotency.complete(context, inboundId);
|
||||
}
|
||||
|
||||
+10
-10
@@ -28,9 +28,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedInboundQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
/** 仅兼容旧客户端筛选值,响应和 OpenAPI 均只使用 INBOUNDED。 */
|
||||
private static final String LEGACY_WAIT_SPLIT = "WAIT_SPLIT";
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
|
||||
@Resource private InboundOrderRepository inbounds;
|
||||
@@ -140,9 +138,9 @@ public class UnifiedInboundQueryService {
|
||||
static String mainStatus(String status) {
|
||||
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", LEGACY_WAIT_SPLIT -> "INBOUNDED";
|
||||
case "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
|
||||
case "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
@@ -152,7 +150,7 @@ 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) || LEGACY_WAIT_SPLIT.equals(processStatus)) ? null
|
||||
case "INBOUNDED", "WAIT_SPLIT" -> !("INBOUNDED".equals(processStatus) || "WAIT_SPLIT".equals(processStatus)) ? null
|
||||
: "CONFIRMED".equals(preloadStatus) ? null
|
||||
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
|
||||
case "LOADING" -> "COMPLETE_LOADING";
|
||||
@@ -169,8 +167,10 @@ public class UnifiedInboundQueryService {
|
||||
private List<String> internalStatuses(String mainStatus) {
|
||||
if (!StringUtils.hasText(mainStatus)) return List.of();
|
||||
return switch (mainStatus) {
|
||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
|
||||
case "INBOUNDED", LEGACY_WAIT_SPLIT -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED", LEGACY_WAIT_SPLIT);
|
||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
|
||||
case "INBOUNDED" -> List.of("INBOUNDED");
|
||||
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
|
||||
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
|
||||
default -> List.of("__NO_MATCH__");
|
||||
};
|
||||
@@ -189,7 +189,7 @@ public class UnifiedInboundQueryService {
|
||||
private void validate(UnifiedInboundQuery query) {
|
||||
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, "统一订单主状态不合法");
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ public interface SplitInboundMapper {
|
||||
|
||||
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
|
||||
+ "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 "
|
||||
+ "AND process_status='INBOUNDED' AND version=#{expectedVersion}")
|
||||
+ "AND process_status IN ('WAIT_SPLIT','INBOUNDED') AND version=#{expectedVersion}")
|
||||
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class SplitPreloadService {
|
||||
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, SMART_SPLIT, key);
|
||||
if (replay != null) return replay;
|
||||
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
|
||||
if (!"INBOUNDED".equals(inbound.processStatus())) {
|
||||
if (!isWaitingSplit(inbound.processStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "仅待分柜订单可以生成预装清单");
|
||||
}
|
||||
PreloadPlanEntity plan = repository.findPlan(tenantId, inboundId);
|
||||
@@ -69,7 +69,7 @@ public class SplitPreloadService {
|
||||
}
|
||||
PreloadPlanResponse result = response(repository.findPlan(tenantId, inboundId));
|
||||
return complete(tenantId, inboundId, operatorId, SMART_SPLIT, key,
|
||||
new SplitActionResponse("INBOUNDED", result));
|
||||
new SplitActionResponse("WAIT_SPLIT", result));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -85,12 +85,12 @@ public class SplitPreloadService {
|
||||
// confirmation is submitted; retain the linked-container hard gate.
|
||||
loadingModelBridge.requireStarted(tenantId, inboundId);
|
||||
} else {
|
||||
requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认");
|
||||
requireWaitingSplit(inbound, "仅待分柜订单可以提交预装清单确认");
|
||||
}
|
||||
PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
|
||||
if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
|
||||
new SplitActionResponse(loading ? "LOADING" : "INBOUNDED", response(plan)));
|
||||
new SplitActionResponse(loading ? "LOADING" : "WAIT_SPLIT", response(plan)));
|
||||
}
|
||||
if (!PreloadStatus.PENDING.equals(plan.getStatus())
|
||||
|| repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING,
|
||||
@@ -98,7 +98,7 @@ public class SplitPreloadService {
|
||||
throw concurrent();
|
||||
}
|
||||
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
|
||||
new SplitActionResponse(loading ? "LOADING" : "INBOUNDED",
|
||||
new SplitActionResponse(loading ? "LOADING" : "WAIT_SPLIT",
|
||||
response(repository.findPlan(tenantId, inboundId))));
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ public class SplitPreloadService {
|
||||
return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key,
|
||||
new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId))));
|
||||
}
|
||||
requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以确认预装清单");
|
||||
requireWaitingSplit(inbound, "仅待分柜订单可以确认预装清单");
|
||||
PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
|
||||
if (PreloadStatus.CONFIRMED.equals(plan.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "预装清单已确认但订单状态尚未推进,请刷新重试");
|
||||
@@ -127,7 +127,7 @@ public class SplitPreloadService {
|
||||
if (repository.beginLoading(tenantId, inboundId, operatorId, inbound.version()) != 1) {
|
||||
throw concurrent();
|
||||
}
|
||||
addHistory(tenantId, inboundId, "INBOUNDED", "LOADING", CONFIRM_PRELOAD,
|
||||
addHistory(tenantId, inboundId, inbound.processStatus(), "LOADING", CONFIRM_PRELOAD,
|
||||
operatorId, operatorName);
|
||||
return complete(tenantId, inboundId, operatorId, CONFIRM_PRELOAD, key,
|
||||
new SplitActionResponse("LOADING", response(repository.findPlan(tenantId, inboundId))));
|
||||
@@ -172,8 +172,12 @@ public class SplitPreloadService {
|
||||
return plan;
|
||||
}
|
||||
|
||||
private static void requireStatus(InboundSnapshot inbound, String expected, String message) {
|
||||
if (!expected.equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, message);
|
||||
private static void requireWaitingSplit(InboundSnapshot inbound, String message) {
|
||||
if (!isWaitingSplit(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
|
||||
private static boolean isWaitingSplit(String status) {
|
||||
return "WAIT_SPLIT".equals(status) || "INBOUNDED".equals(status);
|
||||
}
|
||||
|
||||
private void addHistory(long tenantId, long inboundId, String from, String to, String action,
|
||||
|
||||
Reference in New Issue
Block a user