Merge test fixes into dev dropship flow
This commit is contained in:
+1
-1
@@ -41,7 +41,7 @@ public class InternalCustomerDimensionConfigController {
|
||||
@Valid @RequestBody InitializeRequest request) {
|
||||
requireToken(token);
|
||||
return ApiResponse.success(service.initializeDefault(
|
||||
request.tenantId(), request.operatorId(), request.initializationKey()));
|
||||
request.tenantId(), request.operatorId(), request.initializationKey(), request.customerName()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/defaults")
|
||||
|
||||
@@ -92,7 +92,6 @@ public class PlatformContainerController {
|
||||
@PathVariable @Positive long id,
|
||||
@PathVariable @Positive long fileId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireDomestic(id);
|
||||
var value = overview.downloadPhoto(id, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, org.springframework.http.ContentDisposition.inline()
|
||||
|
||||
+34
@@ -93,6 +93,40 @@ public class PlatformInboundMeasurementController {
|
||||
return ApiResponse.success(service.saveDraft(tenantId, inboundId, user.userId(), request));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASUREMENT_CALCULATION_STRATEGY_UPDATE", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "更新入库测量箱规计算规则")
|
||||
@Operation(summary = "更新箱规计算规则",
|
||||
description = "独立更新当前测量批次全部明细的箱规计算规则并重算体积;不改变测量提交或审核状态。"
|
||||
+ "仅当客户已确认费用且该票已装柜完成时冻结。")
|
||||
@PutMapping("/measurement/calculation-strategy")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:edit')")
|
||||
public ApiResponse<InboundMeasurementResponses.CalculationStrategyUpdated> updateCalculationStrategy(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@Valid @RequestBody InboundMeasurementRequests.UpdateCalculationStrategyRequest request) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
return ApiResponse.success(service.updateCalculationStrategy(tenantId, inboundId, request));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASUREMENT_OUTER_DIMENSIONS_UPDATE",
|
||||
resourceType = "INBOUND_MEASUREMENT_ITEM", resourceId = "#itemId",
|
||||
description = "更新仓库外箱长宽高")
|
||||
@Operation(summary = "更新仓库外箱长宽高",
|
||||
description = "独立更新当前测量批次指定明细的仓库外箱尺寸并重算体积;不改变测量提交或审核状态。"
|
||||
+ "仅当客户已确认费用且该票已装柜完成时冻结。")
|
||||
@PutMapping("/measurement/items/{itemId}/outer-dimensions")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:edit')")
|
||||
public ApiResponse<InboundMeasurementResponses.OuterDimensionsUpdated> updateOuterDimensions(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@PathVariable long inboundId,
|
||||
@PathVariable long itemId,
|
||||
@Valid @RequestBody InboundMeasurementRequests.UpdateOuterDimensionsRequest request) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
return ApiResponse.success(service.updateOuterDimensions(tenantId, inboundId, itemId, request));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_MEASUREMENT_SUBMIT", resourceType = "INBOUND",
|
||||
resourceId = "#inboundId", description = "确认测量并提交审核")
|
||||
@Operation(summary = "确认测量并提交审核",
|
||||
|
||||
+15
@@ -10,6 +10,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.InboundOrderDetail;
|
||||
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
||||
import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.CommandRequest;
|
||||
import com.baiyue.firstleg.dto.InboundHoldDtos.Detail;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
@@ -22,6 +23,7 @@ import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -29,6 +31,7 @@ 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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -160,6 +163,18 @@ public class PlatformInboundOrderController {
|
||||
return cartonLabelPrintService.generate(tenantId, inboundId, cartonIndex, requestId);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量生成入仓箱唛 PDF",
|
||||
description = "按所选客户和入库单聚合生成一个 PDF;首次调用冻结各入库单整单箱号。")
|
||||
@PostMapping("/carton-label-pdf")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:view')")
|
||||
public ResponseEntity<byte[]> printCartonLabelsBatch(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestBody @Size(min = 1, max = 200) List<InboundCartonLabels.PlatformBatchItem> items,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
|
||||
requirePlatform(user);
|
||||
return cartonLabelPrintService.generateForPlatformInbounds(items, requestId);
|
||||
}
|
||||
|
||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
}
|
||||
|
||||
+2
-1
@@ -22,7 +22,8 @@ public final class ContainerCustomsDtos {
|
||||
boolean frozen, FileView completion) {}
|
||||
|
||||
public record ContainerView(long aggregateId, long loadingPlanId, long containerBookingId,
|
||||
String planNo, String cabinetNo, String containerNo, String status, String merchantStatus,
|
||||
String planNo, String cabinetNo, String containerNo, String status, boolean hasActualCargo,
|
||||
String merchantStatus,
|
||||
int version, int taxRefundCount, int coveredTaxRefundCount,
|
||||
List<InboundView> inbounds, List<DocumentView> documents,
|
||||
LocalDateTime startedAt, LocalDateTime completedAt) {}
|
||||
|
||||
+11
@@ -34,6 +34,15 @@ public interface ContainerCustomsMapper {
|
||||
""")
|
||||
List<InboundRow> planInbounds(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
SELECT p.id loadingPlanId,p.plan_no planNo,p.container_booking_id containerBookingId,
|
||||
p.plan_status loadingPlanStatus,b.cabinet_no cabinetNo,b.container_no containerNo
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
|
||||
WHERE p.id=#{planId}
|
||||
""")
|
||||
PlanRow plan(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
SELECT p.id FROM fl_loading_plan p
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=p.id AND item.deleted=0
|
||||
@@ -331,6 +340,8 @@ public interface ContainerCustomsMapper {
|
||||
record InboundRow(Long loadingPlanId,String planNo,Long containerBookingId,String loadingPlanStatus,
|
||||
String cabinetNo,String containerNo,Long inboundId,Long orderId,String orderNo,String inboundCode,
|
||||
Long tenantId,String customerName,String taxMode) {}
|
||||
record PlanRow(Long loadingPlanId,String planNo,Long containerBookingId,String loadingPlanStatus,
|
||||
String cabinetNo,String containerNo) {}
|
||||
record ContainerRow(Long id,Long loadingPlanId,Long containerBookingId,String status,Long startedBy,
|
||||
String startedByName,LocalDateTime startedAt,Long completedBy,String completedByName,
|
||||
LocalDateTime completedAt,LocalDateTime createdAt,LocalDateTime updatedAt,Integer version) {}
|
||||
|
||||
+31
-10
@@ -44,6 +44,9 @@ public class ContainerCustomsService {
|
||||
|
||||
@Transactional
|
||||
public ContainerView platformView(long planId) {
|
||||
List<InboundRow> rows = actualPlanRows(planId);
|
||||
PlanRow plan = rows.isEmpty() ? requiredPlan(planId) : plan(rows.get(0));
|
||||
if (!"COMPLETED".equals(plan.loadingPlanStatus())) return null;
|
||||
ContainerRow container = ensure(planId);
|
||||
return view(container, null, false);
|
||||
}
|
||||
@@ -155,6 +158,7 @@ public class ContainerCustomsService {
|
||||
CommandRow replay = claim(operatorId, action, key, requestHash);
|
||||
if (replay != null) return platformView(planId);
|
||||
ensure(planId);
|
||||
if (actualPlanRows(planId).isEmpty()) throw conflict("当前装柜计划没有实际货物,无需报关");
|
||||
ContainerRow locked = mapper.lockContainer(planId);
|
||||
if (locked == null || !READY.equals(locked.status())) throw conflict("当前柜报关资料尚未齐全或已开始报关");
|
||||
if (!Objects.equals(locked.version(), request.expectedVersion())) throw concurrent();
|
||||
@@ -219,7 +223,8 @@ public class ContainerCustomsService {
|
||||
ContainerRow container = mapper.lockContainer(planId);
|
||||
if (container == null || !DECLARING.equals(container.status())) throw conflict("当前柜不在报关中");
|
||||
if (!Objects.equals(container.version(), request.expectedVersion())) throw concurrent();
|
||||
List<InboundRow> actual=planRows(planId);
|
||||
List<InboundRow> actual=actualPlanRows(planId);
|
||||
if(actual.isEmpty())throw conflict("当前装柜计划没有实际货物,无需报关");
|
||||
if(actual.stream().anyMatch(row->!"BUY_DECLARATION".equals(row.taxMode())))throw conflict("全买单柜完成只允许全部实际行均为BUY_DECLARATION");
|
||||
completeContainer(container, operatorId, operatorName, request.remark());
|
||||
completeCommand(operatorId, action, key, container.id());
|
||||
@@ -245,11 +250,11 @@ public class ContainerCustomsService {
|
||||
}
|
||||
|
||||
private ContainerRow ensure(long planId) {
|
||||
List<InboundRow> rows = planRows(planId);
|
||||
InboundRow first = rows.get(0);
|
||||
if (!"COMPLETED".equals(first.loadingPlanStatus())) throw conflict("装柜完成后才能处理报关资料");
|
||||
List<InboundRow> rows = actualPlanRows(planId);
|
||||
PlanRow plan = rows.isEmpty() ? requiredPlan(planId) : plan(rows.get(0));
|
||||
if (!"COMPLETED".equals(plan.loadingPlanStatus())) throw conflict("装柜完成后才能处理报关资料");
|
||||
boolean refund = rows.stream().anyMatch(row -> "TAX_REFUND".equals(row.taxMode()));
|
||||
mapper.insertContainer(IdWorker.getId(), planId, first.containerBookingId(), refund ? WAITING : READY);
|
||||
mapper.insertContainer(IdWorker.getId(), planId, plan.containerBookingId(), refund ? WAITING : READY);
|
||||
return refresh(planId);
|
||||
}
|
||||
|
||||
@@ -283,11 +288,27 @@ public class ContainerCustomsService {
|
||||
}
|
||||
|
||||
private List<InboundRow> planRows(long planId) {
|
||||
List<InboundRow> rows = mapper.planInbounds(planId);
|
||||
List<InboundRow> rows = actualPlanRows(planId);
|
||||
if (rows == null || rows.isEmpty()) throw notFound("未找到当前装柜计划的实际货物明细");
|
||||
return rows;
|
||||
}
|
||||
|
||||
private List<InboundRow> actualPlanRows(long planId) {
|
||||
List<InboundRow> rows = mapper.planInbounds(planId);
|
||||
return rows == null ? List.of() : rows;
|
||||
}
|
||||
|
||||
private PlanRow requiredPlan(long planId) {
|
||||
PlanRow plan = mapper.plan(planId);
|
||||
if (plan == null) throw notFound("装柜计划不存在或不可用");
|
||||
return plan;
|
||||
}
|
||||
|
||||
private PlanRow plan(InboundRow row) {
|
||||
return new PlanRow(row.loadingPlanId(), row.planNo(), row.containerBookingId(), row.loadingPlanStatus(),
|
||||
row.cabinetNo(), row.containerNo());
|
||||
}
|
||||
|
||||
private List<InboundRow> validateScope(long planId, long tenantId, List<Long> inboundIds) {
|
||||
if (inboundIds == null || inboundIds.isEmpty() || inboundIds.size() > 200) throw bad("至少选择一个且最多200个入仓码");
|
||||
Set<Long> requested = new LinkedHashSet<>(inboundIds);
|
||||
@@ -304,7 +325,8 @@ public class ContainerCustomsService {
|
||||
}
|
||||
|
||||
private ContainerView view(ContainerRow container, Long tenantId, boolean merchant) {
|
||||
List<InboundRow> rows = planRows(container.loadingPlanId());
|
||||
List<InboundRow> rows = actualPlanRows(container.loadingPlanId());
|
||||
PlanRow plan = rows.isEmpty() ? requiredPlan(container.loadingPlanId()) : plan(rows.get(0));
|
||||
List<DocumentRow> documents = mapper.documents(container.id(), tenantId);
|
||||
Set<Long> covered = documents.stream().flatMap(doc -> mapper.scopes(doc.id()).stream()).map(ScopeRow::inboundId).collect(Collectors.toSet());
|
||||
List<InboundRow> visibleRows=rows.stream().filter(row -> tenantId == null || (row.tenantId().equals(tenantId)&&"TAX_REFUND".equals(row.taxMode()))).toList();
|
||||
@@ -315,9 +337,8 @@ public class ContainerCustomsService {
|
||||
int refund = (int) visibleRows.stream().filter(row -> "TAX_REFUND".equals(row.taxMode())).count();
|
||||
int coveredRefund = (int) visibleRows.stream().filter(row -> covered.contains(row.inboundId())).count();
|
||||
String merchantStatus=tenantId==null?null:merchantStatus(container.status(),refund,coveredRefund);
|
||||
InboundRow first = rows.get(0);
|
||||
return new ContainerView(container.id(), container.loadingPlanId(), container.containerBookingId(), first.planNo(),
|
||||
first.cabinetNo(), first.containerNo(), container.status(), merchantStatus, container.version(), refund, coveredRefund,
|
||||
return new ContainerView(container.id(), container.loadingPlanId(), container.containerBookingId(), plan.planNo(),
|
||||
plan.cabinetNo(), plan.containerNo(), container.status(), !rows.isEmpty(), merchantStatus, container.version(), refund, coveredRefund,
|
||||
inbounds, documentViews, container.startedAt(), container.completedAt());
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,10 @@ public final class CustomerDimensionConfigDtos {
|
||||
public record InitializeRequest(
|
||||
@NotNull @Min(1) Long tenantId,
|
||||
@NotNull @Min(0) Long operatorId,
|
||||
@NotBlank @Size(max = 128) String initializationKey
|
||||
@NotBlank @Size(max = 128) String initializationKey,
|
||||
@NotBlank @Size(max = 128) String customerName
|
||||
) { }
|
||||
|
||||
public record InitializeResponse(Long configId, boolean createdByThisInitialization) { }
|
||||
public record InitializeResponse(Long configId, Long chargeRuleId,
|
||||
boolean createdByThisInitialization) { }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.baiyue.firstleg.dto;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
/** 稳定箱号分配、补打和测量解析契约。 */
|
||||
public final class InboundCartonLabels {
|
||||
private InboundCartonLabels() {
|
||||
@@ -12,6 +14,9 @@ public final class InboundCartonLabels {
|
||||
List<Label> labels) {
|
||||
}
|
||||
|
||||
public record PlatformBatchItem(@Min(1) long tenantId, @Min(1) long inboundId) {
|
||||
}
|
||||
|
||||
public record Label(Long id, Long inboundDetailId, Integer cargoItemNo, String cargoName,
|
||||
Integer detailPackageCount, String boxCode, Integer totalCount,
|
||||
Integer cartonIndex, String boxSpecification, BigDecimal lengthCm,
|
||||
|
||||
@@ -87,6 +87,26 @@ public final class InboundMeasurementRequests {
|
||||
) {
|
||||
}
|
||||
|
||||
public record UpdateCalculationStrategyRequest(
|
||||
@NotNull @Positive Long measurementId,
|
||||
@NotNull @PositiveOrZero Integer version,
|
||||
@NotBlank @jakarta.validation.constraints.Pattern(regexp = "ACTUAL|ADD|MULTIPLY") String coefficientMode,
|
||||
@Positive Long chargeRuleId
|
||||
) {
|
||||
}
|
||||
|
||||
public record UpdateOuterDimensionsRequest(
|
||||
@NotNull @Positive Long measurementId,
|
||||
@NotNull @PositiveOrZero Integer version,
|
||||
@DecimalMin("0.001") @DecimalMax("999999999.999")
|
||||
@Digits(integer = 9, fraction = 3) BigDecimal outerLengthCm,
|
||||
@DecimalMin("0.001") @DecimalMax("999999999.999")
|
||||
@Digits(integer = 9, fraction = 3) BigDecimal outerWidthCm,
|
||||
@DecimalMin("0.001") @DecimalMax("999999999.999")
|
||||
@Digits(integer = 9, fraction = 3) BigDecimal outerHeightCm
|
||||
) {
|
||||
}
|
||||
|
||||
public record ConfirmedPackageCountRequest(
|
||||
@NotNull @PositiveOrZero Integer measurementVersion,
|
||||
@NotNull @Positive @Max(1000000) Integer confirmedPackageCount
|
||||
|
||||
@@ -183,6 +183,16 @@ public final class InboundMeasurementResponses {
|
||||
public record DraftSaved(Long measurementId, Integer version, String status) {
|
||||
}
|
||||
|
||||
public record CalculationStrategyUpdated(
|
||||
Long measurementId, Integer version, String status, String coefficientMode, Long chargeRuleId
|
||||
) {
|
||||
}
|
||||
|
||||
public record OuterDimensionsUpdated(
|
||||
Long measurementId, Integer version, String status, Long itemId
|
||||
) {
|
||||
}
|
||||
|
||||
public record MeasurementSubmitted(Long measurementId, Integer version, String processStatus,
|
||||
String measureReviewStatus,
|
||||
@Schema(description = "测量完成后的推荐托盘类型,仅为建议,不代表已分配托盘",
|
||||
|
||||
@@ -38,4 +38,6 @@ public class CustomerChargeRuleEntity extends BaseEntity {
|
||||
private LocalDateTime effectiveTo;
|
||||
private String status;
|
||||
private String remark;
|
||||
/** 客户创建执行器的幂等键;人工创建的规则为空。 */
|
||||
private String initializationKey;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.CompletionTarget;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.service.FbaPricingService;
|
||||
import com.baiyue.firstleg.service.InboundHoldGuard;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -42,7 +41,6 @@ public class LoadingDeliveryService {
|
||||
@Resource private LoadingCompletionGateService completionGate;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private CabinetLoadingEvidenceService cabinetEvidenceService;
|
||||
@Resource private FbaPricingService pricingService;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private InboundHoldGuard inboundHoldGuard;
|
||||
|
||||
@@ -69,7 +67,6 @@ public class LoadingDeliveryService {
|
||||
}
|
||||
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
|
||||
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||
pricingService.autoConfirmAfterLoading(tenantId, inboundId);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse(inbound.processStatus()));
|
||||
}
|
||||
@@ -80,7 +77,6 @@ public class LoadingDeliveryService {
|
||||
requireCabinetEvidence(tenantId, inboundId);
|
||||
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
|
||||
pricingService.autoConfirmAfterLoading(tenantId, inboundId);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
@@ -124,7 +120,6 @@ public class LoadingDeliveryService {
|
||||
CompletionTarget target,
|
||||
boolean acceptedPdaCompletion) {
|
||||
if ("LOADED".equals(target.loadedChildStatus())) {
|
||||
pricingService.autoConfirmAfterLoading(tenantId, target.completionInboundId());
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
@@ -140,7 +135,6 @@ public class LoadingDeliveryService {
|
||||
repository.aggregateRootLoaded(tenantId, target.rootInboundId(), operatorId);
|
||||
addHistory(tenantId, target.completionInboundId(), "LOADING", "LOADED", COMPLETE_LOADING,
|
||||
request.remark(), operatorId, operatorName);
|
||||
pricingService.autoConfirmAfterLoading(tenantId, target.completionInboundId());
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,50 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
@Mapper
|
||||
public interface CustomerChargeRuleMapper extends BaseMapper<CustomerChargeRuleEntity> {}
|
||||
public interface CustomerChargeRuleMapper extends BaseMapper<CustomerChargeRuleEntity> {
|
||||
@Select("""
|
||||
SELECT id, tenant_id, rule_code, rule_version, customer_id, customer_name, business_type,
|
||||
charge_mode, tax_mode, conversion_factor, length_factor, width_factor, height_factor,
|
||||
default_rule, rounding_scale, rounding_mode, currency, effective_from, effective_to,
|
||||
status, remark, initialization_key, created_by, created_at, updated_by, updated_at,
|
||||
deleted, version
|
||||
FROM fl_customer_charge_rule
|
||||
WHERE tenant_id = #{tenantId} AND initialization_key = #{initializationKey} AND deleted = 0
|
||||
LIMIT 1
|
||||
""")
|
||||
CustomerChargeRuleEntity selectByInitializationKey(
|
||||
@Param("tenantId") long tenantId, @Param("initializationKey") String initializationKey);
|
||||
|
||||
@Select("""
|
||||
SELECT id, tenant_id, rule_code, rule_version, customer_id, customer_name, business_type,
|
||||
charge_mode, tax_mode, conversion_factor, length_factor, width_factor, height_factor,
|
||||
default_rule, rounding_scale, rounding_mode, currency, effective_from, effective_to,
|
||||
status, remark, initialization_key, created_by, created_at, updated_by, updated_at,
|
||||
deleted, version
|
||||
FROM fl_customer_charge_rule
|
||||
WHERE tenant_id = #{tenantId} AND initialization_key = #{initializationKey} AND deleted = 0
|
||||
LIMIT 1 FOR UPDATE
|
||||
""")
|
||||
CustomerChargeRuleEntity selectByInitializationKeyForUpdate(
|
||||
@Param("tenantId") long tenantId, @Param("initializationKey") String initializationKey);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM fl_customer_charge_rule
|
||||
WHERE tenant_id = #{tenantId} AND initialization_key = #{initializationKey}
|
||||
AND rule_code = 'DEFAULT-DIMENSION-WEIGHT' AND rule_version = 1
|
||||
AND customer_id = #{tenantId} AND business_type = 'FBA' AND charge_mode = 'CBM'
|
||||
AND tax_mode = 'TAX_INCLUDED' AND conversion_factor = 167.000000
|
||||
AND length_factor = 1.100000 AND width_factor = 1.100000 AND height_factor = 1.100000
|
||||
AND default_rule = 1 AND rounding_scale = 2 AND rounding_mode = 'HALF_UP'
|
||||
AND currency = 'CNY' AND effective_to IS NULL AND status = 'ACTIVE'
|
||||
AND deleted = 0 AND version = 0
|
||||
""")
|
||||
int deleteUnchangedInitializedRule(
|
||||
@Param("tenantId") long tenantId, @Param("initializationKey") String initializationKey);
|
||||
}
|
||||
|
||||
@@ -150,15 +150,13 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id
|
||||
JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id
|
||||
AND plan.id = pick_task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
JOIN fl_inbound_order inbound_order ON inbound_order.id = plan_item.inbound_id
|
||||
AND inbound_order.tenant_id = plan_item.account_id AND inbound_order.deleted = 0
|
||||
WHERE pick_item.pick_task_id = #{taskId} AND booking.warehouse_id = #{warehouseId}
|
||||
AND plan_item.deleted = 0
|
||||
WHERE pick_item.pick_task_id = #{taskId} AND plan_item.deleted = 0
|
||||
ORDER BY pick_item.id ASC
|
||||
""")
|
||||
java.util.List<LoadingPickItemEntity> selectTaskItemsWithAccount(@Param("taskId") long taskId,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
SELECT pick_item.*, plan_item.unit_type, plan_item.pallet_id, plan_item.pallet_no, plan_item.indivisible
|
||||
@@ -167,11 +165,10 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
JOIN fl_loading_plan_item plan_item ON plan_item.id = pick_item.loading_plan_item_id
|
||||
JOIN fl_loading_plan plan ON plan.id = plan_item.loading_plan_id
|
||||
AND plan.id = pick_task.loading_plan_id
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
JOIN fl_inbound_order inbound_order ON inbound_order.id = plan_item.inbound_id
|
||||
AND inbound_order.tenant_id = plan_item.account_id AND inbound_order.deleted = 0
|
||||
WHERE pick_item.pick_task_id = #{taskId} AND pick_item.id = #{itemId}
|
||||
AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0
|
||||
AND plan_item.deleted = 0
|
||||
""")
|
||||
LoadingPickItemEntity selectTaskItemWithAccount(@Param("taskId") long taskId,
|
||||
@Param("itemId") long itemId,
|
||||
@@ -185,10 +182,8 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
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
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id
|
||||
AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0)
|
||||
AND plan_item.deleted = 0)
|
||||
AND picked_package_count + #{count} <= planned_package_count
|
||||
""")
|
||||
int pick(@Param("id") long id, @Param("count") int count,
|
||||
@@ -201,10 +196,8 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
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
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id
|
||||
AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0)
|
||||
AND plan_item.deleted = 0)
|
||||
AND item_status IN ('PENDING', 'PICKING')
|
||||
AND picked_package_count < #{finalCount}
|
||||
AND #{finalCount} < planned_package_count
|
||||
@@ -218,13 +211,12 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
SET item_status = 'SKIPPED', 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
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
WHERE plan_item.id = fl_loading_pick_item.loading_plan_item_id
|
||||
AND booking.warehouse_id = #{warehouseId} AND plan_item.deleted = 0)
|
||||
AND plan_item.deleted = 0)
|
||||
AND item_status = 'PENDING' AND picked_package_count = 0
|
||||
""")
|
||||
int skip(@Param("id") long id, @Param("version") int version, @Param("warehouseId") long warehouseId);
|
||||
int skip(@Param("id") long id, @Param("version") int version,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
|
||||
@@ -50,7 +50,6 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||
WHERE booking.container_no = #{containerNo} AND booking.record_status = 'ACTIVE'
|
||||
AND task.task_status = 'LOADING'
|
||||
AND booking.warehouse_id = #{warehouseId}
|
||||
LIMIT 1
|
||||
""")
|
||||
LoadingPickTaskEntity selectLoadingByContainerAndAccount(@Param("containerNo") String containerNo,
|
||||
@@ -59,13 +58,13 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
||||
@Select("SELECT task.* FROM fl_loading_pick_task task JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id "
|
||||
+ "JOIN fl_container_booking booking ON booking.id=plan.container_booking_id "
|
||||
+ "WHERE task.task_status='LOADING' AND booking.record_status='ACTIVE' "
|
||||
+ "AND booking.cabinet_no=#{cabinetNo} AND booking.warehouse_id=#{warehouseId} LIMIT 1")
|
||||
+ "AND booking.cabinet_no=#{cabinetNo} LIMIT 1")
|
||||
LoadingPickTaskEntity selectLoadingByCabinet(@Param("cabinetNo") String cabinetNo, @Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("SELECT task.* FROM fl_loading_pick_task task JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id "
|
||||
+ "JOIN fl_container_booking booking ON booking.id=plan.container_booking_id "
|
||||
+ "WHERE task.task_status='LOADING' AND booking.record_status='ACTIVE' "
|
||||
+ "AND task.loading_plan_id=#{loadingPlanId} AND booking.warehouse_id=#{warehouseId} LIMIT 1")
|
||||
+ "AND task.loading_plan_id=#{loadingPlanId} LIMIT 1")
|
||||
LoadingPickTaskEntity selectLoadingByPlan(@Param("loadingPlanId") long loadingPlanId, @Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("SELECT booking.* FROM fl_loading_pick_task task "
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ public class OutboundDispatchService {
|
||||
|
||||
OutboundDispatchReadMapper.OceanBinding ocean = repository.ocean(containerBookingId);
|
||||
if (carrierSchedules != null) {
|
||||
if (ocean == null || ocean.id() == null || ocean.customerId() == null) {
|
||||
if (ocean == null || ocean.id() == null) {
|
||||
throw conflict("当前货柜未关联有效订舱,无法创建船司查询任务");
|
||||
}
|
||||
carrierSchedules.ensureFromOutboundConfirm(ocean.customerId(), ocean.id(), containerBookingId,
|
||||
@@ -129,7 +129,7 @@ public class OutboundDispatchService {
|
||||
if (!Objects.equals(visible.version(), request.expectedVersion())) throw concurrent();
|
||||
OutboundDispatchReadMapper.OceanBinding ocean = repository.ocean(containerBookingId);
|
||||
if (carrierSchedules != null) {
|
||||
if (ocean == null || ocean.id() == null || ocean.customerId() == null) {
|
||||
if (ocean == null || ocean.id() == null) {
|
||||
throw conflict("无法核验货柜后续船节点,禁止撤销国内出仓");
|
||||
}
|
||||
if (carrierSchedules.hasOutboundDepartureFact(ocean.customerId(), ocean.id(), containerBookingId)) {
|
||||
|
||||
+1
-2
@@ -111,8 +111,7 @@ public class PlatformCarrierScheduleQueryController {
|
||||
|
||||
@PostMapping("/{queryId}/manual-nodes")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@Operation(summary = "人工补录预计或实际标准船节点",
|
||||
description = "预计节点不推进实际进度;人工实际节点必须严格晚于当前最新实际节点,同级或更早节点返回 A0409。")
|
||||
@Operation(summary = "在跟踪任务内人工补录预计或实际标准船节点")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_NODE", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
resourceId = "#queryId", description = "人工补录船节点事实")
|
||||
public ApiResponse<Detail> manualNode(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.portschedule.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerNodeDetail;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
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.*;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/container-shipping-nodes")
|
||||
public class PlatformContainerShippingNodeController {
|
||||
@Resource private CarrierScheduleQueryService service;
|
||||
|
||||
@GetMapping("/{containerBookingId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||
public ApiResponse<ContainerNodeDetail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.containerNodeDetail(containerBookingId));
|
||||
}
|
||||
|
||||
@PostMapping("/{containerBookingId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@BusinessAudit(event = "CONTAINER_SHIPPING_NODE_MANUAL", resourceType = "CONTAINER_BOOKING",
|
||||
resourceId = "#containerBookingId", description = "人工修改货柜当前船节点")
|
||||
public ApiResponse<ContainerNodeDetail> add(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ContainerManualNodeRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.addContainerManualNode(containerBookingId, user.userId(),
|
||||
user.username(), key, request));
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -13,7 +13,7 @@ import java.util.List;
|
||||
public final class CarrierScheduleQueryDtos {
|
||||
private CarrierScheduleQueryDtos() {}
|
||||
|
||||
public record Query(@Positive Long customerId, @Positive Long oceanBookingId,
|
||||
public record Query(@Min(0) Long customerId, @Positive Long oceanBookingId,
|
||||
@Positive Long containerBookingId, @Size(max = 16) String carrierCode,
|
||||
@Size(max = 24) String status, Boolean expired,
|
||||
@Size(max = 128) String keyword, @Min(1) Long page,
|
||||
@@ -22,7 +22,7 @@ public final class CarrierScheduleQueryDtos {
|
||||
}
|
||||
|
||||
public record CreateRequest(@NotNull @Min(0) @Max(0) Integer expectedVersion,
|
||||
@NotNull @Positive Long customerId,
|
||||
@NotNull @Min(0) Long customerId,
|
||||
@NotNull @Positive Long oceanBookingId,
|
||||
@NotNull @Positive Long containerBookingId,
|
||||
@NotBlank @Size(max = 16) String carrierCode,
|
||||
@@ -43,6 +43,14 @@ public final class CarrierScheduleQueryDtos {
|
||||
@Size(max = 128) String locationName,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
|
||||
public record ContainerManualNodeRequest(@NotBlank @Size(max = 32) String standardNode,
|
||||
@NotNull LocalDateTime observedAt,
|
||||
LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt,
|
||||
@Size(max = 32) String locationCode,
|
||||
@Size(max = 128) String locationName,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
|
||||
public record ProviderObservationRequest(@NotBlank @Size(max = 32) String providerCode,
|
||||
@NotBlank @Size(max = 128) String providerEventId,
|
||||
@NotBlank @Size(max = 64) String providerNodeCode,
|
||||
@@ -97,6 +105,9 @@ public final class CarrierScheduleQueryDtos {
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash, LocalDateTime createdAt) {}
|
||||
|
||||
public record ContainerNodeDetail(Long containerBookingId, String currentNode,
|
||||
LocalDateTime currentNodeAt, List<NodeFact> nodes) {}
|
||||
|
||||
public record ExceptionFact(Long id, String providerCode, String providerEventId,
|
||||
String providerStatus, String exceptionCode, boolean active,
|
||||
LocalDateTime observedAt, String locationCode, String locationName,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ import java.time.LocalDateTime;
|
||||
public class CarrierScheduleNodeFactEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long queryId;
|
||||
private Long containerBookingId;
|
||||
private String eventKey;
|
||||
private String providerCode;
|
||||
private String providerEventId;
|
||||
|
||||
+12
-2
@@ -5,19 +5,29 @@ import com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleNodeFactMapper extends BaseMapper<CarrierScheduleNodeFactEntity> {
|
||||
@Insert("""
|
||||
INSERT INTO fl_carrier_schedule_node_fact
|
||||
(id,query_id,event_key,provider_code,provider_event_id,provider_node_code,standard_node,node_sequence,observed_at,
|
||||
(id,query_id,container_booking_id,event_key,provider_code,provider_event_id,provider_node_code,standard_node,node_sequence,observed_at,
|
||||
estimated_at,actual_at,location_code,location_name,source_reference,raw_payload_hash,created_at)
|
||||
VALUES
|
||||
(#{value.id},#{value.queryId},#{value.eventKey},#{value.providerCode},#{value.providerEventId},#{value.providerNodeCode},
|
||||
(#{value.id},#{value.queryId},#{value.containerBookingId},#{value.eventKey},#{value.providerCode},#{value.providerEventId},#{value.providerNodeCode},
|
||||
#{value.standardNode},#{value.nodeSequence},#{value.observedAt},#{value.estimatedAt},#{value.actualAt},
|
||||
#{value.locationCode},#{value.locationName},#{value.sourceReference},#{value.rawPayloadHash},
|
||||
CURRENT_TIMESTAMP(3))
|
||||
ON DUPLICATE KEY UPDATE id=id
|
||||
""")
|
||||
int insertIgnore(@Param("value") CarrierScheduleNodeFactEntity value);
|
||||
|
||||
@Select("SELECT * FROM fl_carrier_schedule_node_fact WHERE container_booking_id=#{containerBookingId} ORDER BY observed_at,id")
|
||||
List<CarrierScheduleNodeFactEntity> selectByContainer(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@Select("SELECT * FROM fl_carrier_schedule_node_fact WHERE container_booking_id=#{containerBookingId} AND event_key=#{eventKey} LIMIT 1")
|
||||
CarrierScheduleNodeFactEntity selectByContainerEvent(@Param("containerBookingId") long containerBookingId,
|
||||
@Param("eventKey") String eventKey);
|
||||
}
|
||||
|
||||
+14
-14
@@ -49,26 +49,26 @@ public interface CarrierScheduleQueryMapper extends BaseMapper<CarrierScheduleQu
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT q.container_booking_id AS containerBookingId,q.id AS queryId,q.version AS queryVersion,
|
||||
q.status AS queryStatus,q.latest_standard_node AS latestActualNode,
|
||||
q.latest_node_observed_at AS latestActualAt,
|
||||
SELECT b.id AS containerBookingId,q.id AS queryId,q.version AS queryVersion,
|
||||
q.status AS queryStatus,current.standard_node AS latestActualNode,
|
||||
current.actual_at AS latestActualAt,
|
||||
MAX(CASE WHEN f.standard_node='DEPARTED_WAREHOUSE' THEN f.actual_at END) AS departedWarehouseAt,
|
||||
MAX(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT' THEN f.actual_at END) AS departedOriginPortAt,
|
||||
MAX(CASE WHEN f.standard_node='ARRIVED_DESTINATION_PORT' THEN f.actual_at END) AS arrivedDestinationPortAt,
|
||||
MAX(CASE WHEN f.standard_node='PICKUP_READY' THEN f.actual_at END) AS pickupReadyAt,
|
||||
MAX(CASE WHEN f.standard_node='PICKED_UP' THEN f.actual_at END) AS pickedUpAt,
|
||||
MAX(CASE WHEN f.standard_node='EMPTY_RETURNED' THEN f.actual_at END) AS emptyReturnedAt
|
||||
FROM fl_carrier_schedule_query q
|
||||
JOIN (
|
||||
SELECT container_booking_id,MAX(id) AS query_id
|
||||
FROM fl_carrier_schedule_query
|
||||
WHERE container_booking_id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY container_booking_id
|
||||
) latest ON latest.query_id=q.id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact f ON f.query_id=q.id AND f.actual_at IS NOT NULL
|
||||
GROUP BY q.container_booking_id,q.id,q.version,q.status,q.latest_standard_node,q.latest_node_observed_at
|
||||
ORDER BY q.container_booking_id
|
||||
FROM fl_container_booking b
|
||||
JOIN fl_carrier_schedule_node_fact current ON current.id=(
|
||||
SELECT f2.id FROM fl_carrier_schedule_node_fact f2
|
||||
WHERE f2.container_booking_id=b.id AND f2.actual_at IS NOT NULL
|
||||
ORDER BY f2.observed_at DESC,f2.id DESC LIMIT 1)
|
||||
LEFT JOIN fl_carrier_schedule_query q ON q.id=current.query_id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact f ON f.container_booking_id=b.id AND f.actual_at IS NOT NULL
|
||||
WHERE b.id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY b.id,q.id,q.version,q.status,current.standard_node,current.actual_at
|
||||
ORDER BY b.id
|
||||
</script>
|
||||
""")
|
||||
List<ContainerActualFacts> selectContainerActualFacts(
|
||||
|
||||
+6
@@ -79,6 +79,12 @@ public class CarrierScheduleQueryRepository {
|
||||
.eq(CarrierScheduleNodeFactEntity::getQueryId, queryId)
|
||||
.eq(CarrierScheduleNodeFactEntity::getEventKey, eventKey));
|
||||
}
|
||||
public List<CarrierScheduleNodeFactEntity> nodesByContainer(long containerBookingId) {
|
||||
return nodes.selectByContainer(containerBookingId);
|
||||
}
|
||||
public CarrierScheduleNodeFactEntity nodeByContainer(long containerBookingId, String eventKey) {
|
||||
return nodes.selectByContainerEvent(containerBookingId, eventKey);
|
||||
}
|
||||
public boolean hasActualNode(long queryId, String standardNode) {
|
||||
return nodes.selectCount(new LambdaQueryWrapper<CarrierScheduleNodeFactEntity>()
|
||||
.eq(CarrierScheduleNodeFactEntity::getQueryId, queryId)
|
||||
|
||||
+18
-6
@@ -42,28 +42,40 @@ public class CarrierScheduleNodeOutboxService {
|
||||
|| !normalizedContainerNo.equals(query.getTrackingNo())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "船节点任务跟踪柜号与跨区事件柜号不一致");
|
||||
}
|
||||
publish(query.getId(), query.getOceanBookingId(), query.getContainerBookingId(), query.getVersion(),
|
||||
fact, normalizedContainerNo, "CARRIER_NODE:" + query.getId() + ':' + fact.getId());
|
||||
}
|
||||
|
||||
public void publishContainerNode(Long oceanBookingId, long containerBookingId,
|
||||
CarrierScheduleNodeFactEntity fact, String containerNo) {
|
||||
if (fact.getActualAt() == null) return;
|
||||
String normalizedContainerNo = ContainerNumbers.normalizeRequired(containerNo);
|
||||
publish(null, oceanBookingId, containerBookingId, 0, fact, normalizedContainerNo,
|
||||
"CONTAINER_NODE:" + containerBookingId + ':' + fact.getId());
|
||||
}
|
||||
|
||||
private void publish(Long queryId, Long oceanBookingId, Long containerBookingId, Integer version,
|
||||
CarrierScheduleNodeFactEntity fact, String normalizedContainerNo, String eventKey) {
|
||||
ZoneId zone = ZoneId.of(scheduleZone);
|
||||
var payload = new FirstLegContainerShippingNodeRecordedV1(
|
||||
string(query.getId()), string(query.getOceanBookingId()),
|
||||
string(query.getContainerBookingId()), normalizedContainerNo,
|
||||
string(queryId), string(oceanBookingId), string(containerBookingId), normalizedContainerNo,
|
||||
fact.getStandardNode(), fact.getNodeSequence(),
|
||||
fact.getObservedAt().atZone(zone).toInstant(), fact.getActualAt().atZone(zone).toInstant(),
|
||||
fact.getLocationCode(), fact.getLocationName(), fact.getProviderCode(), fact.getProviderEventId(),
|
||||
fact.getProviderNodeCode(), fact.getSourceReference(), fact.getRawPayloadHash());
|
||||
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||
String payloadHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||
long aggregateVersion = Math.max(0, query.getVersion() == null ? 0 : query.getVersion());
|
||||
String eventKey = "CARRIER_NODE:" + query.getId() + ':' + fact.getId();
|
||||
long aggregateVersion = Math.max(0, version == null ? 0 : version);
|
||||
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(),
|
||||
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED, 1,
|
||||
fact.getActualAt().atZone(zone).toInstant(), RegionCode.CN, RegionCode.OVERSEAS,
|
||||
"0", "CONTAINER_BOOKING", string(query.getContainerBookingId()),
|
||||
"0", "CONTAINER_BOOKING", string(containerBookingId),
|
||||
aggregateVersion, aggregateVersion + 1, EventIdentifiers.newUuid(), eventKey, null,
|
||||
payloadNode, payloadHash).requireValid();
|
||||
|
||||
InboundOutboxEntity row = new InboundOutboxEntity();
|
||||
row.setId(IdWorker.getId()); row.setTenantId(0L);
|
||||
row.setAggregateType(envelope.aggregateType()); row.setAggregateId(query.getContainerBookingId());
|
||||
row.setAggregateType(envelope.aggregateType()); row.setAggregateId(containerBookingId);
|
||||
row.setEventType(envelope.eventType()); row.setEventKey(eventKey); row.setEventVersion(1);
|
||||
row.setAggregateSequence(Math.toIntExact(aggregateVersion)); row.setPayloadHash(payloadHash);
|
||||
row.setPayloadJson(json(envelope)); row.setPublishStatus("PENDING"); row.setRetryCount(0);
|
||||
|
||||
+64
-20
@@ -8,6 +8,8 @@ import com.baiyue.common.event.ContainerNumbers;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerNodeDetail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
@@ -48,6 +50,7 @@ import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -101,15 +104,17 @@ public class CarrierScheduleQueryService {
|
||||
|
||||
/** Idempotently starts the canonical tracking task after domestic outbound confirmation. */
|
||||
@Transactional
|
||||
public CarrierScheduleQueryEntity ensureFromOutboundConfirm(long customerId, long oceanBookingId,
|
||||
public CarrierScheduleQueryEntity ensureFromOutboundConfirm(Long customerId, long oceanBookingId,
|
||||
long containerBookingId,
|
||||
long operatorId, String operatorName) {
|
||||
CarrierScheduleQueryEntity existing = repository.findByBusinessScope(customerId, oceanBookingId,
|
||||
containerBookingId);
|
||||
OceanBookingEntity booking = portSchedules.oceanBooking(oceanBookingId);
|
||||
ContainerBookingEntity container = portSchedules.containerBooking(containerBookingId);
|
||||
if (booking == null || container == null) throw new BizException(ResultCode.NOT_FOUND, "出仓订舱或柜次不存在");
|
||||
validateLinks(customerId, oceanBookingId, containerBookingId);
|
||||
long scopeCustomerId = booking.getCustomerId() == null ? 0L : booking.getCustomerId();
|
||||
if (customerId != null && customerId != scopeCustomerId) throw badRequest("customerId与订舱客户不一致");
|
||||
CarrierScheduleQueryEntity existing = repository.findByBusinessScope(scopeCustomerId, oceanBookingId,
|
||||
containerBookingId);
|
||||
validateLinks(scopeCustomerId, oceanBookingId, containerBookingId);
|
||||
String trackingType = "CONTAINER_NO";
|
||||
String trackingNo = normalizedContainerNo(container);
|
||||
if (existing != null) {
|
||||
@@ -120,7 +125,7 @@ public class CarrierScheduleQueryService {
|
||||
LocalDateTime now = now();
|
||||
CarrierScheduleQueryEntity value = new CarrierScheduleQueryEntity();
|
||||
value.setId(IdWorker.getId()); value.setQueryNo("CSQ" + value.getId());
|
||||
value.setCustomerId(customerId); value.setOceanBookingId(oceanBookingId);
|
||||
value.setCustomerId(scopeCustomerId); value.setOceanBookingId(oceanBookingId);
|
||||
value.setContainerBookingId(containerBookingId); value.setCarrierCode(strategy.carrierCode());
|
||||
value.setTrackingType(trackingType); value.setTrackingNo(trackingNo);
|
||||
value.setOfficialUrl(strategy.officialUrl()); value.setAccessMode(strategy.accessMode());
|
||||
@@ -136,7 +141,7 @@ public class CarrierScheduleQueryService {
|
||||
try {
|
||||
try { repository.insert(value); }
|
||||
catch (DuplicateKeyException exception) {
|
||||
CarrierScheduleQueryEntity concurrent = repository.findByBusinessScope(customerId, oceanBookingId,
|
||||
CarrierScheduleQueryEntity concurrent = repository.findByBusinessScope(scopeCustomerId, oceanBookingId,
|
||||
containerBookingId);
|
||||
if (concurrent != null) {
|
||||
requireFrozenTrackingIdentity(concurrent, trackingNo);
|
||||
@@ -145,7 +150,7 @@ public class CarrierScheduleQueryService {
|
||||
throw exception;
|
||||
}
|
||||
persistenceStage = "NODE_FACT";
|
||||
CarrierScheduleNodeFactEntity departed = addNode(value.getId(), "FIRSTLEG", "OUTBOUND_CONFIRM", "OUTBOUND_CONFIRM",
|
||||
CarrierScheduleNodeFactEntity departed = addNode(value, "FIRSTLEG", "OUTBOUND_CONFIRM", "OUTBOUND_CONFIRM",
|
||||
CarrierScheduleNode.DEPARTED_WAREHOUSE, now, null, now, null, null, "OUTBOUND_CONFIRM",
|
||||
sha256("OUTBOUND_CONFIRM|" + containerBookingId));
|
||||
if (departed != null) {
|
||||
@@ -176,7 +181,7 @@ public class CarrierScheduleQueryService {
|
||||
CarrierScheduleQueryEntity value = requiredLocked(id);
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(id, request.providerCode(), request.providerEventId(),
|
||||
CarrierScheduleNodeFactEntity fact = addNode(value, request.providerCode(), request.providerEventId(),
|
||||
request.providerNodeCode(), node,
|
||||
request.observedAt(), request.estimatedAt(), request.actualAt(), request.locationCode(),
|
||||
request.locationName(), request.sourceReference(), request.rawPayloadHash());
|
||||
@@ -217,10 +222,9 @@ public class CarrierScheduleQueryService {
|
||||
}
|
||||
CarrierScheduleQueryEntity value = requiredLockedVersion(id, request.expectedVersion());
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
requireManualActualAdvance(value, node, request.actualAt());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
String providerEventId = sha256("MANUAL|" + id + '|' + key);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(id, "MANUAL", providerEventId, node.name(), node,
|
||||
CarrierScheduleNodeFactEntity fact = addNode(value, "MANUAL", providerEventId, node.name(), node,
|
||||
request.observedAt(),
|
||||
request.estimatedAt(), request.actualAt(), request.locationCode(), request.locationName(),
|
||||
"MANUAL:" + operatorId, hash(operation, id, request));
|
||||
@@ -239,13 +243,42 @@ public class CarrierScheduleQueryService {
|
||||
return complete(id, operatorId, key, detail(after));
|
||||
}
|
||||
|
||||
private void requireManualActualAdvance(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
|
||||
LocalDateTime actualAt) {
|
||||
if (actualAt == null || value.getLatestStandardNode() == null) return;
|
||||
CarrierScheduleNode current = CarrierScheduleNode.require(value.getLatestStandardNode());
|
||||
if (node.sequence() <= current.sequence()) {
|
||||
throw conflict("人工实际节点必须严格晚于当前最新实际节点");
|
||||
public ContainerNodeDetail containerNodeDetail(long containerBookingId) {
|
||||
if (portSchedules.containerBooking(containerBookingId) == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
}
|
||||
return containerNodeDetail(containerBookingId, repository.nodesByContainer(containerBookingId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerNodeDetail addContainerManualNode(long containerBookingId, long operatorId,
|
||||
String operatorName, String key,
|
||||
ContainerManualNodeRequest request) {
|
||||
validateKey(key);
|
||||
if (request.estimatedAt() == null && request.actualAt() == null) {
|
||||
throw badRequest("estimatedAt和actualAt至少提供一个");
|
||||
}
|
||||
ContainerBookingEntity container = portSchedules.containerBooking(containerBookingId);
|
||||
if (container == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
String providerEventId = sha256("CONTAINER_MANUAL|" + containerBookingId + '|' + key);
|
||||
String payloadHash = hash("CONTAINER_MANUAL_NODE", containerBookingId, request);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(null, containerBookingId, "MANUAL", providerEventId,
|
||||
node.name(), node, request.observedAt(), request.estimatedAt(), request.actualAt(),
|
||||
request.locationCode(), request.locationName(), "MANUAL:" + operatorId, payloadHash);
|
||||
if (fact != null) nodeOutbox.publishContainerNode(container.getOceanBookingId(), containerBookingId,
|
||||
fact, normalizedContainerNo(container));
|
||||
return containerNodeDetail(containerBookingId, repository.nodesByContainer(containerBookingId));
|
||||
}
|
||||
|
||||
private ContainerNodeDetail containerNodeDetail(long containerBookingId,
|
||||
List<CarrierScheduleNodeFactEntity> facts) {
|
||||
CarrierScheduleNodeFactEntity current = facts.stream()
|
||||
.max(Comparator.comparing(CarrierScheduleNodeFactEntity::getObservedAt)
|
||||
.thenComparing(CarrierScheduleNodeFactEntity::getId)).orElse(null);
|
||||
return new ContainerNodeDetail(containerBookingId, current == null ? null : current.getStandardNode(),
|
||||
current == null ? null : current.getActualAt() != null ? current.getActualAt() : current.getEstimatedAt(),
|
||||
facts.stream().map(this::node).toList());
|
||||
}
|
||||
|
||||
private boolean advanceActualNode(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
|
||||
@@ -424,7 +457,8 @@ public class CarrierScheduleQueryService {
|
||||
private void validateLinks(long customerId, long oceanBookingId, Long containerBookingId) {
|
||||
OceanBookingEntity booking = portSchedules.oceanBooking(oceanBookingId);
|
||||
if (booking == null) throw new BizException(ResultCode.NOT_FOUND, "关联订舱不存在");
|
||||
if (!Objects.equals(booking.getCustomerId(), customerId)) throw badRequest("customerId与订舱客户不一致");
|
||||
long expectedCustomerId = booking.getCustomerId() == null ? 0L : booking.getCustomerId();
|
||||
if (expectedCustomerId != customerId) throw badRequest("customerId与订舱客户不一致");
|
||||
if (containerBookingId != null) {
|
||||
if (portSchedules.containerBooking(containerBookingId) == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "关联柜次不存在");
|
||||
@@ -490,13 +524,23 @@ public class CarrierScheduleQueryService {
|
||||
value.getReason(), value.getOperatorId(), value.getOperatorName(), value.getCreatedAt());
|
||||
}
|
||||
|
||||
private CarrierScheduleNodeFactEntity addNode(long queryId, String providerCode, String providerEventId,
|
||||
private CarrierScheduleNodeFactEntity addNode(CarrierScheduleQueryEntity query, String providerCode, String providerEventId,
|
||||
String providerNodeCode,
|
||||
CarrierScheduleNode node, LocalDateTime observedAt, LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash) {
|
||||
return addNode(query.getId(), query.getContainerBookingId(), providerCode, providerEventId,
|
||||
providerNodeCode, node, observedAt, estimatedAt, actualAt, locationCode, locationName,
|
||||
sourceReference, rawPayloadHash);
|
||||
}
|
||||
|
||||
private CarrierScheduleNodeFactEntity addNode(Long queryId, long containerBookingId,
|
||||
String providerCode, String providerEventId, String providerNodeCode,
|
||||
CarrierScheduleNode node, LocalDateTime observedAt, LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash) {
|
||||
CarrierScheduleNodeFactEntity fact = new CarrierScheduleNodeFactEntity();
|
||||
fact.setId(IdWorker.getId()); fact.setQueryId(queryId);
|
||||
fact.setId(IdWorker.getId()); fact.setQueryId(queryId); fact.setContainerBookingId(containerBookingId);
|
||||
fact.setEventKey(providerCode.trim().toUpperCase(Locale.ROOT) + ':' + providerEventId.trim());
|
||||
fact.setProviderCode(providerCode.trim().toUpperCase(Locale.ROOT));
|
||||
fact.setProviderEventId(providerEventId.trim());
|
||||
@@ -505,7 +549,7 @@ public class CarrierScheduleQueryService {
|
||||
fact.setActualAt(actualAt); fact.setLocationCode(trim(locationCode)); fact.setLocationName(trim(locationName));
|
||||
fact.setSourceReference(trim(sourceReference)); fact.setRawPayloadHash(rawPayloadHash);
|
||||
if (repository.insertIfAbsent(fact)) return fact;
|
||||
CarrierScheduleNodeFactEntity existing = repository.node(queryId, fact.getEventKey());
|
||||
CarrierScheduleNodeFactEntity existing = repository.nodeByContainer(containerBookingId, fact.getEventKey());
|
||||
if (existing != null && !Objects.equals(existing.getRawPayloadHash(), rawPayloadHash)) {
|
||||
throw conflict("相同providerEventId对应不同rawPayloadHash,拒绝覆盖既有节点事实");
|
||||
}
|
||||
|
||||
@@ -55,6 +55,19 @@ public class FbaPricingRepository {
|
||||
|
||||
public void insertRule(CustomerChargeRuleEntity rule) { rules.insert(rule); }
|
||||
|
||||
public CustomerChargeRuleEntity findRuleByInitializationKey(long tenantId, String initializationKey) {
|
||||
return rules.selectByInitializationKey(tenantId, initializationKey);
|
||||
}
|
||||
|
||||
public CustomerChargeRuleEntity findRuleByInitializationKeyForUpdate(
|
||||
long tenantId, String initializationKey) {
|
||||
return rules.selectByInitializationKeyForUpdate(tenantId, initializationKey);
|
||||
}
|
||||
|
||||
public int deleteUnchangedInitializedRule(long tenantId, String initializationKey) {
|
||||
return rules.deleteUnchangedInitializedRule(tenantId, initializationKey);
|
||||
}
|
||||
|
||||
public int clearDefaultRule(long tenantId, long customerId, long operatorId) {
|
||||
return rules.update(null, new LambdaUpdateWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||
@@ -285,22 +298,22 @@ public class FbaPricingRepository {
|
||||
.set(InboundFeeEntity::getUpdatedAt, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
public int markInboundAdjusted(long tenantId, long inboundId, String feeStatus,
|
||||
public int markInboundAdjusted(long tenantId, long inboundId, String processStatus, String feeStatus,
|
||||
java.math.BigDecimal selectedWeightKg,
|
||||
java.math.BigDecimal selectedVolumeCbm,
|
||||
String chargeUnit, java.math.BigDecimal chargeableQuantity) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId)
|
||||
.eq(InboundPricingEntity::getId, inboundId)
|
||||
.eq(InboundPricingEntity::getProcessStatus, "WAIT_SPLIT")
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.set(InboundPricingEntity::getFeeStatus, feeStatus);
|
||||
if ("KG".equals(chargeUnit)) {
|
||||
update.set(InboundPricingEntity::getChargeableWeightKg, chargeableQuantity);
|
||||
} else {
|
||||
update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeableQuantity);
|
||||
}
|
||||
applyChargeableSnapshot(update, selectedWeightKg, selectedVolumeCbm, chargeUnit, chargeableQuantity);
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
public int markInboundCalculated(long tenantId, long inboundId, String processStatus, String chargeMode,
|
||||
java.math.BigDecimal selectedWeightKg,
|
||||
java.math.BigDecimal selectedVolumeCbm,
|
||||
java.math.BigDecimal chargeable) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId)
|
||||
@@ -308,15 +321,17 @@ public class FbaPricingRepository {
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED", "SENT"))
|
||||
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
||||
if (!List.of("WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING").contains(processStatus)) {
|
||||
if (!List.of("WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED")
|
||||
.contains(processStatus)) {
|
||||
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
|
||||
}
|
||||
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
||||
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
|
||||
applyChargeableSnapshot(update, selectedWeightKg, selectedVolumeCbm, chargeMode, chargeable);
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
public int markInboundContainerFeeRecalculated(long tenantId, long inboundId, String processStatus,
|
||||
java.math.BigDecimal selectedWeightKg,
|
||||
java.math.BigDecimal selectedVolumeCbm,
|
||||
String chargeMode, java.math.BigDecimal chargeable) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId)
|
||||
@@ -324,11 +339,21 @@ public class FbaPricingRepository {
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.in(InboundPricingEntity::getFeeStatus, List.of("CALCULATED", "CONFIRMED"))
|
||||
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
||||
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
||||
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
|
||||
applyChargeableSnapshot(update, selectedWeightKg, selectedVolumeCbm, chargeMode, chargeable);
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
private void applyChargeableSnapshot(LambdaUpdateWrapper<InboundPricingEntity> update,
|
||||
java.math.BigDecimal selectedWeightKg,
|
||||
java.math.BigDecimal selectedVolumeCbm,
|
||||
String chargeUnit, java.math.BigDecimal chargeableQuantity) {
|
||||
// 列表同时展示两项:当前计费单位保存最终计费量,另一项保存本次计价选定值。
|
||||
update.set(InboundPricingEntity::getChargeableWeightKg,
|
||||
"KG".equals(chargeUnit) ? chargeableQuantity : selectedWeightKg)
|
||||
.set(InboundPricingEntity::getChargeableVolumeCbm,
|
||||
"CBM".equals(chargeUnit) ? chargeableQuantity : selectedVolumeCbm);
|
||||
}
|
||||
|
||||
public int confirmFee(long tenantId, long feeId, long operatorId, LocalDateTime now) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
.eq(InboundFeeEntity::getTenantId, tenantId).eq(InboundFeeEntity::getId, feeId)
|
||||
|
||||
+8
@@ -71,6 +71,14 @@ public class InboundMeasurementRepository {
|
||||
.eq(InboundMeasurementEntity::getId, measurementId));
|
||||
}
|
||||
|
||||
public InboundMeasurementEntity lockMeasurement(long tenantId, long inboundId, long measurementId) {
|
||||
return measurements.selectOne(new LambdaQueryWrapper<InboundMeasurementEntity>()
|
||||
.eq(InboundMeasurementEntity::getTenantId, tenantId)
|
||||
.eq(InboundMeasurementEntity::getInboundId, inboundId)
|
||||
.eq(InboundMeasurementEntity::getId, measurementId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public InboundMeasurementEntity findLatestMeasurement(long tenantId, long inboundId) {
|
||||
return measurements.selectOne(new LambdaQueryWrapper<InboundMeasurementEntity>()
|
||||
.eq(InboundMeasurementEntity::getTenantId, tenantId)
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 在客户创建执行器中幂等生成“箱规按权重系数计算”的默认规则。 */
|
||||
@Service
|
||||
public class CustomerChargeRuleInitializationService {
|
||||
static final String DEFAULT_RULE_CODE = "DEFAULT-DIMENSION-WEIGHT";
|
||||
static final BigDecimal DEFAULT_COEFFICIENT = new BigDecimal("1.100000");
|
||||
static final BigDecimal DEFAULT_CONVERSION_FACTOR = new BigDecimal("167.000000");
|
||||
static final String DEFAULT_REMARK = "SMALL:30,20,20;系数:1.1;重量:5;小箱:标准小箱;"
|
||||
+ "MEDIUM:40,30,30;系数:1.1;重量:8;中箱:标准中箱;"
|
||||
+ "LARGE:50,40,40;系数:1.1;重量:12;大箱:标准大箱;"
|
||||
+ "EXTRA_LARGE:60,50,50;系数:1.1;重量:15;特大箱:特大箱";
|
||||
|
||||
@Resource private FbaPricingRepository repository;
|
||||
|
||||
public CustomerChargeRuleEntity initializeDefault(long tenantId, long operatorId,
|
||||
String initializationKey, String customerName) {
|
||||
CustomerChargeRuleEntity existing = repository.findRuleByInitializationKey(
|
||||
tenantId, initializationKey);
|
||||
if (existing != null) return existing;
|
||||
|
||||
CustomerChargeRuleEntity value = new CustomerChargeRuleEntity();
|
||||
value.setId(IdWorker.getId());
|
||||
value.setTenantId(tenantId);
|
||||
value.setRuleCode(DEFAULT_RULE_CODE);
|
||||
value.setRuleVersion(1);
|
||||
value.setCustomerId(tenantId);
|
||||
value.setCustomerName(requireCustomerName(customerName));
|
||||
value.setBusinessType("FBA");
|
||||
value.setChargeMode("CBM");
|
||||
value.setTaxMode("TAX_INCLUDED");
|
||||
value.setConversionFactor(DEFAULT_CONVERSION_FACTOR);
|
||||
value.setLengthFactor(DEFAULT_COEFFICIENT);
|
||||
value.setWidthFactor(DEFAULT_COEFFICIENT);
|
||||
value.setHeightFactor(DEFAULT_COEFFICIENT);
|
||||
value.setDefaultRule(true);
|
||||
value.setRoundingScale(2);
|
||||
value.setRoundingMode("HALF_UP");
|
||||
value.setCurrency("CNY");
|
||||
value.setEffectiveFrom(LocalDateTime.now());
|
||||
value.setEffectiveTo(null);
|
||||
value.setStatus("ACTIVE");
|
||||
value.setRemark(DEFAULT_REMARK);
|
||||
value.setInitializationKey(initializationKey);
|
||||
value.setCreatedBy(operatorId);
|
||||
value.setUpdatedBy(operatorId);
|
||||
repository.insertRule(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
CustomerChargeRuleEntity lockForCompensation(long tenantId, String initializationKey) {
|
||||
return repository.findRuleByInitializationKeyForUpdate(tenantId, initializationKey);
|
||||
}
|
||||
|
||||
boolean isUnchangedDefault(CustomerChargeRuleEntity value) {
|
||||
return value != null
|
||||
&& value.getVersion() != null && value.getVersion() == 0
|
||||
&& DEFAULT_RULE_CODE.equals(value.getRuleCode()) && Integer.valueOf(1).equals(value.getRuleVersion())
|
||||
&& value.getTenantId().equals(value.getCustomerId())
|
||||
&& "FBA".equals(value.getBusinessType()) && "CBM".equals(value.getChargeMode())
|
||||
&& "TAX_INCLUDED".equals(value.getTaxMode())
|
||||
&& DEFAULT_CONVERSION_FACTOR.compareTo(value.getConversionFactor()) == 0
|
||||
&& DEFAULT_COEFFICIENT.compareTo(value.getLengthFactor()) == 0
|
||||
&& DEFAULT_COEFFICIENT.compareTo(value.getWidthFactor()) == 0
|
||||
&& DEFAULT_COEFFICIENT.compareTo(value.getHeightFactor()) == 0
|
||||
&& Boolean.TRUE.equals(value.getDefaultRule()) && Integer.valueOf(2).equals(value.getRoundingScale())
|
||||
&& "HALF_UP".equals(value.getRoundingMode()) && "CNY".equals(value.getCurrency())
|
||||
&& value.getEffectiveTo() == null && "ACTIVE".equals(value.getStatus())
|
||||
&& DEFAULT_REMARK.equals(value.getRemark());
|
||||
}
|
||||
|
||||
void deleteUnchangedDefault(long tenantId, String initializationKey) {
|
||||
if (repository.deleteUnchangedInitializedRule(tenantId, initializationKey) != 1) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户权重系数默认规则补偿删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static String requireCustomerName(String value) {
|
||||
if (value == null || value.isBlank() || value.trim().length() > 128) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "客户名称无效");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
+29
-7
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.RuleResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.RuleUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -32,6 +33,8 @@ public class CustomerDimensionConfigService {
|
||||
|
||||
@Resource
|
||||
private CustomerDimensionConfigRepository repository;
|
||||
@Resource
|
||||
private CustomerChargeRuleInitializationService chargeRuleInitialization;
|
||||
|
||||
public GroupResponse get(long tenantId) {
|
||||
return response(tenantId, requireCompleteRules(repository.findAll(tenantId)));
|
||||
@@ -78,18 +81,26 @@ public class CustomerDimensionConfigService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InitializeResponse initializeDefault(long tenantId, long operatorId, String initializationKey) {
|
||||
public InitializeResponse initializeDefault(long tenantId, long operatorId, String initializationKey,
|
||||
String customerName) {
|
||||
requireTenant(tenantId);
|
||||
String normalizedKey = requireInitializationKey(initializationKey);
|
||||
List<CustomerDimensionConfigEntity> existing = repository.findAll(tenantId);
|
||||
if (!existing.isEmpty()) {
|
||||
List<CustomerDimensionConfigEntity> complete = requireCompleteRules(existing);
|
||||
return initializationResponse(complete, normalizedKey);
|
||||
boolean ownedByKey = ownedByInitialization(complete, normalizedKey);
|
||||
CustomerChargeRuleEntity chargeRule = ownedByKey
|
||||
? chargeRuleInitialization.initializeDefault(
|
||||
tenantId, operatorId, normalizedKey, customerName)
|
||||
: null;
|
||||
return initializationResponse(complete, chargeRule, ownedByKey);
|
||||
}
|
||||
|
||||
repository.insertDefaults(defaultRows(tenantId, normalizedKey), operatorId);
|
||||
List<CustomerDimensionConfigEntity> persisted = requireCompleteRules(repository.findAll(tenantId));
|
||||
return initializationResponse(persisted, normalizedKey);
|
||||
CustomerChargeRuleEntity chargeRule = chargeRuleInitialization.initializeDefault(
|
||||
tenantId, operatorId, normalizedKey, customerName);
|
||||
return initializationResponse(persisted, chargeRule, true);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -99,6 +110,12 @@ public class CustomerDimensionConfigService {
|
||||
List<CustomerDimensionConfigEntity> locked =
|
||||
repository.findDefaultsForCompensation(tenantId, normalizedKey);
|
||||
if (!isUnchangedDefaultGroup(locked, normalizedKey)) return;
|
||||
CustomerChargeRuleEntity chargeRule = chargeRuleInitialization.lockForCompensation(
|
||||
tenantId, normalizedKey);
|
||||
if (chargeRule != null && !chargeRuleInitialization.isUnchangedDefault(chargeRule)) return;
|
||||
if (chargeRule != null) {
|
||||
chargeRuleInitialization.deleteUnchangedDefault(tenantId, normalizedKey);
|
||||
}
|
||||
if (repository.deleteUnchangedDefaults(tenantId, normalizedKey) != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户箱规默认配置补偿删除失败");
|
||||
}
|
||||
@@ -206,11 +223,16 @@ public class CustomerDimensionConfigService {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean ownedByInitialization(List<CustomerDimensionConfigEntity> values,
|
||||
String initializationKey) {
|
||||
return values.stream().allMatch(value -> initializationKey.equals(value.getInitializationKey()));
|
||||
}
|
||||
|
||||
private static InitializeResponse initializationResponse(List<CustomerDimensionConfigEntity> values,
|
||||
String initializationKey) {
|
||||
boolean ownedByKey = values.stream()
|
||||
.allMatch(value -> initializationKey.equals(value.getInitializationKey()));
|
||||
return new InitializeResponse(values.get(0).getId(), ownedByKey);
|
||||
CustomerChargeRuleEntity chargeRule,
|
||||
boolean ownedByKey) {
|
||||
return new InitializeResponse(values.get(0).getId(),
|
||||
chargeRule == null ? null : chargeRule.getId(), ownedByKey);
|
||||
}
|
||||
|
||||
private static GroupResponse response(long tenantId, List<CustomerDimensionConfigEntity> values) {
|
||||
|
||||
@@ -44,7 +44,6 @@ public class FbaPricingService {
|
||||
private static final BigDecimal ADJUST_TOLERANCE_PERCENT = new BigDecimal("10.00");
|
||||
private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
|
||||
private static final long AUTOMATIC_CONFIRM_OPERATOR_ID = 0L;
|
||||
private static final String AUTOMATIC_CONFIRM_OPERATOR_NAME = "装柜完成自动确认";
|
||||
private static final String CONTAINER_RECALCULATE_OPERATION = "CONTAINER_FEE_RECALCULATE";
|
||||
@Resource
|
||||
private FbaPricingRepository repository;
|
||||
@@ -99,8 +98,8 @@ public class FbaPricingService {
|
||||
if (published == null || !"SENT".equals(published.getFeeStatus())) {
|
||||
throw conflict("费用状态已变化,请刷新后重试");
|
||||
}
|
||||
if (published.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("客户已确认该费用,不允许再修改或重新发送");
|
||||
if (feeRevisionLocked(inbound, published)) {
|
||||
throw conflict("客户已确认且该票已装柜完成,不允许再修改或重新发送费用");
|
||||
}
|
||||
}
|
||||
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
||||
@@ -167,7 +166,7 @@ public class FbaPricingService {
|
||||
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
|
||||
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, fee, additional, now));
|
||||
if (repository.markInboundCalculated(tenantId, inboundId, inbound.getProcessStatus(),
|
||||
estimate.chargeUnit(), chargeable) != 1) {
|
||||
estimate.chargeUnit(), weight, volume, chargeable) != 1) {
|
||||
throw conflict("入库单状态已变化,费用试算未保存");
|
||||
}
|
||||
feeDetails.materializeCalculatedFreight(tenantId, inboundId, fee.getId(), operatorId, now);
|
||||
@@ -343,7 +342,8 @@ public class FbaPricingService {
|
||||
recalculated.getSubtotalAmount(), now));
|
||||
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, recalculated, additional, now));
|
||||
if (repository.markInboundContainerFeeRecalculated(tenantId, member.inbound().getId(),
|
||||
member.inbound().getProcessStatus(), estimate.chargeUnit(), member.chargeableQuantity()) != 1) {
|
||||
member.inbound().getProcessStatus(), member.current().getActualWeightKg(),
|
||||
member.current().getActualVolumeCbm(), estimate.chargeUnit(), member.chargeableQuantity()) != 1) {
|
||||
throw conflict("柜内入库单状态已变化,不能重算阶梯费用");
|
||||
}
|
||||
feeDetails.materializeCalculatedFreight(tenantId, member.inbound().getId(), recalculated.getId(),
|
||||
@@ -390,8 +390,8 @@ public class FbaPricingService {
|
||||
public FeeSnapshotResponse confirm(long tenantId, long operatorId, long inboundId,
|
||||
String idempotencyKey) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())) {
|
||||
throw conflict("订单已进入装柜流程,不能再修改或发送费用");
|
||||
if (!feeRevisionStatusSupported(inbound.getProcessStatus())) {
|
||||
throw conflict("当前入库单状态不允许确认费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null) throw conflict("入库单尚无可确认的费用试算");
|
||||
@@ -412,8 +412,8 @@ public class FbaPricingService {
|
||||
public FeeSnapshotResponse send(long tenantId, long operatorId, long inboundId,
|
||||
String idempotencyKey) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())) {
|
||||
throw conflict("订单已进入装柜流程,不能再修改或发送费用");
|
||||
if (!feeRevisionStatusSupported(inbound.getProcessStatus())) {
|
||||
throw conflict("当前入库单状态不允许发送费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null) throw conflict("入库单尚无费用快照");
|
||||
@@ -445,21 +445,6 @@ public class FbaPricingService {
|
||||
return response(tenantId, fee.getId(), null);
|
||||
}
|
||||
|
||||
/** 客户未操作时,装柜完成以系统身份确认最新一版已发送费用。 */
|
||||
@Transactional
|
||||
public void autoConfirmAfterLoading(long tenantId, long inboundId) {
|
||||
InboundFeeEntity current = repository.findLatestFeeForUpdate(tenantId, inboundId);
|
||||
if (current == null || !"SENT".equals(current.getFeeStatus())
|
||||
|| current.getCustomerConfirmedAt() != null) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (repository.confirmFeeByCustomer(tenantId, current.getId(), current.getFeeVersion(),
|
||||
AUTOMATIC_CONFIRM_OPERATOR_ID, AUTOMATIC_CONFIRM_OPERATOR_NAME, now) != 1) {
|
||||
throw conflict("装柜完成自动确认费用失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirmByCustomer(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, int expectedFeeVersion,
|
||||
@@ -498,15 +483,15 @@ public class FbaPricingService {
|
||||
AdjustFeeRequest request, String idempotencyKey) {
|
||||
InboundPricingEntity inbound = repository.findInbound(tenantId, inboundId);
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
if (!isCompletedInbound(inbound.getProcessStatus())) {
|
||||
throw conflict("只能在待分柜阶段修改初算或已发布费用");
|
||||
if (!feeRevisionStatusSupported(inbound.getProcessStatus())) {
|
||||
throw conflict("当前入库单状态不允许修改初算或已发布费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null || !List.of("CALCULATED", "CONFIRMED", "SENT").contains(current.getFeeStatus())) {
|
||||
throw conflict("入库单没有可修改的初算或已发布费用");
|
||||
}
|
||||
if (current.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("客户已确认该费用,不允许再修改");
|
||||
if (feeRevisionLocked(inbound, current)) {
|
||||
throw conflict("客户已确认且该票已装柜完成,不允许再修改费用");
|
||||
}
|
||||
if (!request.expectedFeeVersion().equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重试");
|
||||
@@ -554,6 +539,9 @@ public class FbaPricingService {
|
||||
adjusted.setConfirmedAt(published ? now : null);
|
||||
adjusted.setSentBy(published ? operatorId : null);
|
||||
adjusted.setSentAt(published ? now : null);
|
||||
adjusted.setCustomerConfirmedBy(null);
|
||||
adjusted.setCustomerConfirmedByName(null);
|
||||
adjusted.setCustomerConfirmedAt(null);
|
||||
adjusted.setCreatedAt(now);
|
||||
adjusted.setUpdatedAt(now);
|
||||
if (repository.supersedeCurrent(tenantId, current.getId(), current.getFeeStatus()) != 1) {
|
||||
@@ -576,8 +564,8 @@ public class FbaPricingService {
|
||||
extra.setRemark(request.reason().trim());
|
||||
repository.insertItem(extra);
|
||||
}
|
||||
if (repository.markInboundAdjusted(tenantId, inboundId, adjusted.getFeeStatus(),
|
||||
adjusted.getChargeUnit(), quantity) != 1) {
|
||||
if (repository.markInboundAdjusted(tenantId, inboundId, inbound.getProcessStatus(), adjusted.getFeeStatus(),
|
||||
adjusted.getActualWeightKg(), adjusted.getActualVolumeCbm(), adjusted.getChargeUnit(), quantity) != 1) {
|
||||
throw conflict("入库单状态已变化,费用调整未保存");
|
||||
}
|
||||
if (published) {
|
||||
@@ -594,7 +582,7 @@ public class FbaPricingService {
|
||||
|
||||
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())
|
||||
if (!feeRevisionStatusSupported(inbound.getProcessStatus())
|
||||
|| !List.of("UNPRICED", "CALCULATED", "SENT").contains(inbound.getFeeStatus())) {
|
||||
throw conflict("当前入库单状态不允许费用试算");
|
||||
}
|
||||
@@ -613,20 +601,23 @@ public class FbaPricingService {
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private static boolean isCompletedInbound(String processStatus) {
|
||||
return "WAIT_SPLIT".equals(processStatus);
|
||||
}
|
||||
|
||||
private static boolean preserveProcessStatusDuringPricing(String processStatus) {
|
||||
return List.of("WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING").contains(processStatus);
|
||||
return List.of("WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED")
|
||||
.contains(processStatus);
|
||||
}
|
||||
|
||||
private static boolean feeEditableBeforeLoading(String processStatus) {
|
||||
private static boolean feeRevisionStatusSupported(String processStatus) {
|
||||
return List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING")
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING",
|
||||
"LOADED", "DELIVERED")
|
||||
.contains(processStatus);
|
||||
}
|
||||
|
||||
private static boolean feeRevisionLocked(InboundPricingEntity inbound, InboundFeeEntity fee) {
|
||||
return fee != null && fee.getCustomerConfirmedAt() != null
|
||||
&& List.of("LOADED", "DELIVERED").contains(inbound.getProcessStatus());
|
||||
}
|
||||
|
||||
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||
try {
|
||||
return PricingDestinationType.valueOf(inbound.getDestinationType());
|
||||
|
||||
+21
@@ -92,6 +92,27 @@ public class InboundCartonLabelPrintService {
|
||||
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> generateForPlatformInbounds(
|
||||
List<InboundCartonLabels.PlatformBatchItem> items, String requestId) {
|
||||
if (items == null || items.isEmpty() || items.size() > 200
|
||||
|| items.stream().anyMatch(item -> item == null || item.tenantId() <= 0 || item.inboundId() <= 0)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "批量打印必须选择1至200个入库单");
|
||||
}
|
||||
List<InboundCartonLabels.PlatformBatchItem> distinctItems = items.stream().distinct().toList();
|
||||
List<InboundCartonLabels.Label> labels = new ArrayList<>();
|
||||
for (InboundCartonLabels.PlatformBatchItem item : distinctItems) {
|
||||
labels.addAll(labelService.list(item.tenantId(), item.inboundId(), null).labels());
|
||||
}
|
||||
if (labels.isEmpty()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "所选订单没有可生成的箱唛");
|
||||
}
|
||||
ResponseEntity<byte[]> response = generatePdf("carton-labels-" + labels.size() + ".pdf", labels, requestId);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.putAll(response.getHeaders());
|
||||
headers.set("X-Carton-Label-Order-Count", String.valueOf(distinctItems.size()));
|
||||
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
|
||||
}
|
||||
|
||||
/** 一件代发托盘标签:托盘号、入仓码、SKU箱数和托盘二维码。 */
|
||||
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String requestId) {
|
||||
if (pallet == null || !StringUtils.hasText(pallet.palletNo()) || pallet.items().isEmpty()) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundCartonLabelRepository;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -117,7 +118,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "预入库单补建后未能读取");
|
||||
}
|
||||
return prepared;
|
||||
} catch (FeignException exception) {
|
||||
} catch (FeignException | NoFallbackAvailableException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法补建预入库单");
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
.thenComparing(OrderCargoClient.CargoLabelBinding::cargoId,
|
||||
java.util.Comparator.nullsLast(Long::compareTo)))
|
||||
.toList();
|
||||
} catch (FeignException exception) {
|
||||
} catch (FeignException | NoFallbackAvailableException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法生成唛头");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.baiyue.firstleg.entity.InboundWeightRevisionEntity;
|
||||
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -61,6 +62,8 @@ public class InboundMeasurementService {
|
||||
@Resource
|
||||
private MeasurementBatchService batchService;
|
||||
@Resource
|
||||
private FbaPricingRepository pricingRepository;
|
||||
@Resource
|
||||
private FbaMasterDataService fbaMasterDataService;
|
||||
@Resource
|
||||
private OrderCargoClient orderCargoClient;
|
||||
@@ -162,6 +165,68 @@ public class InboundMeasurementService {
|
||||
return new InboundMeasurementResponses.DraftSaved(saved.getId(), saved.getVersion(), saved.getStatus());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementResponses.CalculationStrategyUpdated updateCalculationStrategy(
|
||||
long tenantId, long inboundId,
|
||||
InboundMeasurementRequests.UpdateCalculationStrategyRequest request) {
|
||||
InboundOrderEntity inbound = repository.lockInbound(tenantId, inboundId);
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
requireMeasurementRevisionEditable(tenantId, inboundId, inbound, "箱规计算规则");
|
||||
if ("MULTIPLY".equals(request.coefficientMode()) && request.chargeRuleId() == null) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "箱规系数模式必须选择计费规则");
|
||||
}
|
||||
InboundMeasurementEntity measurement = repository.lockMeasurement(
|
||||
tenantId, inboundId, request.measurementId());
|
||||
if (measurement == null) throw new BizException(ResultCode.NOT_FOUND, "测量批次不存在");
|
||||
requireVersion(request.version(), measurement.getVersion());
|
||||
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
||||
batchService.recalculateStrategy(tenantId, measurement, items,
|
||||
request.coefficientMode(), request.chargeRuleId());
|
||||
measurement.setUpdatedAt(LocalDateTime.now());
|
||||
if (!repository.updateMeasurement(measurement)) throw concurrentModification();
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
||||
if (!repository.updateInbound(inbound)) throw concurrentModification();
|
||||
Long appliedRuleId = "MULTIPLY".equals(request.coefficientMode()) ? request.chargeRuleId() : null;
|
||||
return new InboundMeasurementResponses.CalculationStrategyUpdated(
|
||||
measurement.getId(), measurement.getVersion(), measurement.getStatus(),
|
||||
request.coefficientMode(), appliedRuleId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementResponses.OuterDimensionsUpdated updateOuterDimensions(
|
||||
long tenantId, long inboundId, long itemId,
|
||||
InboundMeasurementRequests.UpdateOuterDimensionsRequest request) {
|
||||
InboundOrderEntity inbound = repository.lockInbound(tenantId, inboundId);
|
||||
if (inbound == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
requireMeasurementRevisionEditable(tenantId, inboundId, inbound, "仓库外箱尺寸");
|
||||
InboundMeasurementEntity measurement = repository.lockMeasurement(
|
||||
tenantId, inboundId, request.measurementId());
|
||||
if (measurement == null) throw new BizException(ResultCode.NOT_FOUND, "测量批次不存在");
|
||||
requireVersion(request.version(), measurement.getVersion());
|
||||
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
||||
batchService.recalculateOuterDimensions(tenantId, measurement, items, itemId,
|
||||
request.outerLengthCm(), request.outerWidthCm(), request.outerHeightCm());
|
||||
measurement.setUpdatedAt(LocalDateTime.now());
|
||||
if (!repository.updateMeasurement(measurement)) throw concurrentModification();
|
||||
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
||||
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
||||
if (!repository.updateInbound(inbound)) throw concurrentModification();
|
||||
return new InboundMeasurementResponses.OuterDimensionsUpdated(
|
||||
measurement.getId(), measurement.getVersion(), measurement.getStatus(), itemId);
|
||||
}
|
||||
|
||||
private void requireMeasurementRevisionEditable(long tenantId, long inboundId,
|
||||
InboundOrderEntity inbound, String subject) {
|
||||
var currentFee = pricingRepository.findLatestFeeForUpdate(tenantId, inboundId);
|
||||
boolean customerConfirmed = currentFee != null && currentFee.getCustomerConfirmedAt() != null;
|
||||
boolean loadingCompleted = Set.of("LOADED", "DELIVERED").contains(inbound.getProcessStatus());
|
||||
if (customerConfirmed && loadingCompleted) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"客户已确认且该票已装柜完成,不能修改" + subject);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundMeasurementResponses.MeasurementSubmitted submit(
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
@@ -180,6 +245,10 @@ public class InboundMeasurementService {
|
||||
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights);
|
||||
if (confirmedTotals.palletCount() == 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"请至少上传并确认一张称重图片后再提交测量审核");
|
||||
}
|
||||
|
||||
requireConfirmedPackageTotal(inbound, items);
|
||||
measurement.setStatus(STATUS_SUBMITTED);
|
||||
|
||||
@@ -246,7 +246,7 @@ public class LoadingPickService {
|
||||
|
||||
public InventorySnapshot inventorySnapshot(long taskId, long warehouseId) {
|
||||
LoadingPickTaskEntity task = requiredTask(taskId);
|
||||
List<LoadingPickItemEntity> items = warehouseScopedItems(task, warehouseId);
|
||||
List<LoadingPickItemEntity> items = taskItems(task, warehouseId);
|
||||
return buildInventorySnapshot(task, warehouseId, items);
|
||||
}
|
||||
|
||||
@@ -326,10 +326,7 @@ public class LoadingPickService {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
LoadingPickItemEntity item = repository.item(taskId, itemId, request.warehouseId());
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
|
||||
}
|
||||
LoadingPickItemEntity item = requireTaskItem(taskId, itemId, request.warehouseId());
|
||||
requirePackageUnit(item);
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), request.version(), request.warehouseId()) != 1) {
|
||||
throw conflict("拣货数量超过计划箱数或明细版本已变化");
|
||||
@@ -404,7 +401,7 @@ public class LoadingPickService {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
LoadingPickItemEntity item = requireTaskItem(taskId, itemId, request.warehouseId());
|
||||
requirePackageUnit(item);
|
||||
if (repository.partial(itemId, request.finalPickedPackageCount(), request.version(),
|
||||
request.warehouseId()) != 1) {
|
||||
@@ -437,7 +434,7 @@ public class LoadingPickService {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
LoadingPickItemEntity item = requireTaskItem(taskId, itemId, request.warehouseId());
|
||||
requirePackageUnit(item);
|
||||
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
|
||||
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
|
||||
@@ -940,10 +937,10 @@ public class LoadingPickService {
|
||||
return task;
|
||||
}
|
||||
|
||||
private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) {
|
||||
private LoadingPickItemEntity requireTaskItem(long taskId, long itemId, long warehouseId) {
|
||||
LoadingPickItemEntity item = repository.item(taskId, itemId, warehouseId);
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在或不属于当前仓库");
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在或不属于当前任务");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -953,16 +950,12 @@ public class LoadingPickService {
|
||||
if (booking == null || !Objects.equals(task.getContainerBookingId(), booking.getId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或柜次已非有效存续记录");
|
||||
}
|
||||
if (warehouseId != null && !Objects.equals(warehouseId, booking.getWarehouseId())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或不属于当前仓库");
|
||||
}
|
||||
List<LoadingPickItemEntity> source = warehouseId == null
|
||||
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
|
||||
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
|
||||
.collect(Collectors.groupingBy(CartonLabelRow::getPickItemId));
|
||||
Map<Long, Long> tenantByInbound = tenantByInbound(task.getId());
|
||||
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
|
||||
booking.getWarehouseId(), source, tenantByInbound);
|
||||
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(source, tenantByInbound);
|
||||
List<PickItem> items = source.stream().map(item -> {
|
||||
List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
|
||||
CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0);
|
||||
@@ -1011,11 +1004,11 @@ public class LoadingPickService {
|
||||
private List<BatchLine> validateBatchLines(LoadingPickTaskEntity task, BatchRequest request) {
|
||||
long taskId = task.getId();
|
||||
List<LoadingPickItemEntity> allItems = repository.items(taskId);
|
||||
List<LoadingPickItemEntity> scopedItems = repository.items(taskId, request.warehouseId());
|
||||
if (allItems.isEmpty() || scopedItems.size() != allItems.size()) {
|
||||
throw conflict("PDA任务柜次、仓库或客户归属不一致");
|
||||
List<LoadingPickItemEntity> taskItems = repository.items(taskId, request.warehouseId());
|
||||
if (allItems.isEmpty() || taskItems.size() != allItems.size()) {
|
||||
throw conflict("PDA任务柜次或客户归属不一致");
|
||||
}
|
||||
Map<Long, LoadingPickItemEntity> itemsById = scopedItems.stream().collect(Collectors.toMap(
|
||||
Map<Long, LoadingPickItemEntity> itemsById = taskItems.stream().collect(Collectors.toMap(
|
||||
LoadingPickItemEntity::getId, item -> item));
|
||||
Set<Long> unresolvedIds = allItems.stream().filter(item -> !isResolved(item))
|
||||
.map(LoadingPickItemEntity::getId).collect(Collectors.toSet());
|
||||
@@ -1033,7 +1026,7 @@ public class LoadingPickService {
|
||||
LoadingPickItemEntity item = itemsById.get(requestItem.itemId());
|
||||
if (item == null || item.getInboundId() == null || item.getLoadingPlanItemId() == null
|
||||
|| item.getInboundDetailId() == null) {
|
||||
throw conflict("PDA明细不属于当前任务、柜次、仓库或客户");
|
||||
throw conflict("PDA明细不属于当前任务、柜次或客户");
|
||||
}
|
||||
if (!"SKIP".equals(requestItem.action())) {
|
||||
inboundHasWork.put(item.getInboundId(), true);
|
||||
@@ -1096,7 +1089,7 @@ public class LoadingPickService {
|
||||
}
|
||||
result.add(new BatchLine(item, requestItem, action, request.warehouseId()));
|
||||
}
|
||||
validateWholePallets(result, scopedItems, request.warehouseId());
|
||||
validateWholePallets(result, taskItems, request.warehouseId());
|
||||
result.sort(Comparator.comparing(line -> line.item().getId()));
|
||||
return result;
|
||||
}
|
||||
@@ -1121,19 +1114,19 @@ public class LoadingPickService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<LoadingPickItemEntity> warehouseScopedItems(LoadingPickTaskEntity task, long warehouseId) {
|
||||
private List<LoadingPickItemEntity> taskItems(LoadingPickTaskEntity task, long warehouseId) {
|
||||
List<LoadingPickItemEntity> allItems = repository.items(task.getId());
|
||||
List<LoadingPickItemEntity> scopedItems = repository.items(task.getId(), warehouseId);
|
||||
if (allItems.isEmpty() || scopedItems.size() != allItems.size()) {
|
||||
throw conflict("PDA任务柜次、仓库或客户归属不一致");
|
||||
List<LoadingPickItemEntity> taskItems = repository.items(task.getId(), warehouseId);
|
||||
if (allItems.isEmpty() || taskItems.size() != allItems.size()) {
|
||||
throw conflict("PDA任务柜次或客户归属不一致");
|
||||
}
|
||||
return scopedItems;
|
||||
return taskItems;
|
||||
}
|
||||
|
||||
private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId,
|
||||
List<LoadingPickItemEntity> items) {
|
||||
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
|
||||
warehouseId, items, tenantByInbound(task.getId()));
|
||||
items, tenantByInbound(task.getId()));
|
||||
List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId)
|
||||
.filter(Objects::nonNull).distinct().sorted().map(inboundId -> {
|
||||
List<InventoryResource> resources = inventoryByInbound.getOrDefault(inboundId, List.of());
|
||||
@@ -1160,8 +1153,7 @@ public class LoadingPickService {
|
||||
}));
|
||||
}
|
||||
|
||||
private Map<Long, List<InventoryResource>> inventoryByInbound(long warehouseId,
|
||||
List<LoadingPickItemEntity> items,
|
||||
private Map<Long, List<InventoryResource>> inventoryByInbound(List<LoadingPickItemEntity> items,
|
||||
Map<Long, Long> tenantByInbound) {
|
||||
Map<Long, List<InventoryResource>> result = new HashMap<>();
|
||||
for (Long inboundId : items.stream().map(LoadingPickItemEntity::getInboundId)
|
||||
@@ -1169,7 +1161,6 @@ public class LoadingPickService {
|
||||
Long tenantId = tenantByInbound.get(inboundId);
|
||||
if (tenantId == null) throw conflict("PDA任务缺少入库单客户归属");
|
||||
List<InventoryResource> resources = palletAllocations.list(tenantId, inboundId).stream()
|
||||
.filter(resource -> Objects.equals(resource.getWarehouseId(), warehouseId))
|
||||
.filter(resource -> "CONFIRMED".equals(resource.getAllocationStatus()))
|
||||
.sorted(Comparator.comparing(InboundPalletAllocationEntity::getBlockId,
|
||||
Comparator.nullsLast(Long::compareTo))
|
||||
@@ -1222,7 +1213,7 @@ public class LoadingPickService {
|
||||
|
||||
private void validateInventorySnapshot(LoadingPickTaskEntity task, BatchRequest request,
|
||||
List<BatchLine> lines) {
|
||||
List<LoadingPickItemEntity> allItems = warehouseScopedItems(task, request.warehouseId());
|
||||
List<LoadingPickItemEntity> allItems = taskItems(task, request.warehouseId());
|
||||
InventorySnapshot current = buildInventorySnapshot(task, request.warehouseId(), allItems);
|
||||
if (!current.snapshotToken().equals(request.inventorySnapshotToken().trim())) {
|
||||
throw conflict("库存已变化,请刷新后重试");
|
||||
|
||||
@@ -93,6 +93,87 @@ public class MeasurementBatchService {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
void recalculateStrategy(long tenantId, InboundMeasurementEntity measurement,
|
||||
List<InboundMeasurementItemEntity> existingItems,
|
||||
String coefficientMode, Long chargeRuleId) {
|
||||
if (existingItems.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前测量批次没有可重算的明细");
|
||||
}
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
tenantId, measurement.getInboundId(), measurement.getId(), measurement.getVersion(),
|
||||
measurement.getSourceType(), measurement.getSourceCode(), measurement.getExternalEventNo(),
|
||||
measurement.getDeviceCode(), measurement.getWarehouseCode(), measurement.getMeasuredAt(),
|
||||
measurement.getRawPayloadJson(), measurement.getOriginalDocumentNo(),
|
||||
measurement.getOriginalGrossWeightKg(), measurement.getOriginalVolumeCbm(),
|
||||
measurement.getActualGoodsName(), measurement.getCreatedBy(),
|
||||
existingItems.stream().map(item -> new MeasurementBatchCommand.Item(
|
||||
item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), null, item.getSourceImageFileId(),
|
||||
item.getOverLimitWarning(), item.getManualOuterLengthCm(), item.getManualOuterWidthCm(),
|
||||
item.getManualOuterHeightCm(), chargeRuleId, coefficientMode, item.getRemark())).toList());
|
||||
List<InboundMeasurementItemEntity> recalculated = calculateItems(
|
||||
command, dimensionWeighting.load(tenantId));
|
||||
for (int index = 0; index < existingItems.size(); index++) {
|
||||
InboundMeasurementItemEntity target = existingItems.get(index);
|
||||
InboundMeasurementItemEntity source = recalculated.get(index);
|
||||
applyRecalculatedItem(target, source, false);
|
||||
}
|
||||
applyTotals(measurement, existingItems);
|
||||
}
|
||||
|
||||
void recalculateOuterDimensions(long tenantId, InboundMeasurementEntity measurement,
|
||||
List<InboundMeasurementItemEntity> existingItems, long itemId,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm) {
|
||||
InboundMeasurementItemEntity target = existingItems.stream()
|
||||
.filter(item -> item.getId() != null && item.getId() == itemId)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "测量明细不存在"));
|
||||
MeasurementBatchCommand command = new MeasurementBatchCommand(
|
||||
tenantId, measurement.getInboundId(), measurement.getId(), measurement.getVersion(),
|
||||
measurement.getSourceType(), measurement.getSourceCode(), measurement.getExternalEventNo(),
|
||||
measurement.getDeviceCode(), measurement.getWarehouseCode(), measurement.getMeasuredAt(),
|
||||
measurement.getRawPayloadJson(), measurement.getOriginalDocumentNo(),
|
||||
measurement.getOriginalGrossWeightKg(), measurement.getOriginalVolumeCbm(),
|
||||
measurement.getActualGoodsName(), measurement.getCreatedBy(),
|
||||
List.of(new MeasurementBatchCommand.Item(
|
||||
target.getInboundDetailId(), target.getItemNo(), target.getBoxSpecification(),
|
||||
target.getPackageCount(), target.getLengthCm(), target.getWidthCm(), target.getHeightCm(),
|
||||
null, target.getSourceImageFileId(), target.getOverLimitWarning(), outerLengthCm,
|
||||
outerWidthCm, outerHeightCm, target.getChargeRuleId(), target.getCoefficientMode(),
|
||||
target.getRemark())));
|
||||
InboundMeasurementItemEntity recalculated = calculateItems(
|
||||
command, dimensionWeighting.load(tenantId)).get(0);
|
||||
applyRecalculatedItem(target, recalculated, true);
|
||||
applyTotals(measurement, existingItems);
|
||||
}
|
||||
|
||||
private void applyRecalculatedItem(InboundMeasurementItemEntity target,
|
||||
InboundMeasurementItemEntity source,
|
||||
boolean updateManualDimensions) {
|
||||
target.setGirthCm(source.getGirthCm());
|
||||
target.setUnitVolumeCbm(source.getUnitVolumeCbm());
|
||||
target.setVolumeRawCbm(source.getVolumeRawCbm());
|
||||
target.setVolumeCbm(source.getVolumeCbm());
|
||||
target.setTotalVolumeCbm(source.getTotalVolumeCbm());
|
||||
target.setVolumeWeightKg(source.getVolumeWeightKg());
|
||||
target.setChargeRuleId(source.getChargeRuleId());
|
||||
target.setCoefficientMode(source.getCoefficientMode());
|
||||
target.setCalculationRuleVersion(source.getCalculationRuleVersion());
|
||||
target.setOuterLengthCm(source.getOuterLengthCm());
|
||||
target.setOuterWidthCm(source.getOuterWidthCm());
|
||||
target.setOuterHeightCm(source.getOuterHeightCm());
|
||||
if (updateManualDimensions) {
|
||||
target.setManualOuterLengthCm(source.getManualOuterLengthCm());
|
||||
target.setManualOuterWidthCm(source.getManualOuterWidthCm());
|
||||
target.setManualOuterHeightCm(source.getManualOuterHeightCm());
|
||||
}
|
||||
target.setUpdatedAt(LocalDateTime.now());
|
||||
if (!repository.updateItem(target)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工补录会整批替换可编辑测量项,但不能抹掉同一项已冻结的扫码身份与设备取证。
|
||||
* 只按入库货物明细和明细号精确匹配,避免把一个箱码猜测性地复制给其他测量项。
|
||||
|
||||
+11
-1
@@ -18,7 +18,11 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
SELECT b.id AS containerBookingId,b.container_no AS containerNo,b.ocean_booking_id AS oceanBookingId,
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
WHERE b.id=#{containerBookingId} FOR UPDATE
|
||||
WHERE b.id=#{containerBookingId}
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
FOR UPDATE
|
||||
""")
|
||||
Context lockContext(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@@ -27,6 +31,9 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
WHERE b.id=#{containerBookingId}
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
""")
|
||||
Context context(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@@ -37,6 +44,9 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
FROM fl_container_booking b
|
||||
WHERE b.id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
ORDER BY b.id
|
||||
</script>
|
||||
""")
|
||||
|
||||
+4
-2
@@ -45,8 +45,10 @@ public class ContainerWorkbenchService {
|
||||
public WorkbenchItem overseasDetail(long id, Set<String> permissions) {
|
||||
ContainerBookingEntity value = required(id);
|
||||
if (!repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国外工作台货柜不存在");
|
||||
View drayage = terminalDrayages.authorize(terminalDrayages.detail(id), permissions);
|
||||
return item("OVERSEAS", value, drayage.actualFacts(), drayage, permissions);
|
||||
View drayage = terminalDrayages.authorize(terminalDrayages.views(List.of(id)), permissions).stream()
|
||||
.filter(candidate -> candidate.containerBookingId() == id)
|
||||
.findFirst().orElse(null);
|
||||
return item("OVERSEAS", value, drayage == null ? null : drayage.actualFacts(), drayage, permissions);
|
||||
}
|
||||
|
||||
private PageResult<WorkbenchItem> page(String workbench, Query query, Set<String> permissions) {
|
||||
|
||||
+18
-16
@@ -5,7 +5,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.portschedule.domain.CarrierScheduleNode;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.ActualTimeRequest;
|
||||
@@ -123,7 +123,7 @@ public class TerminalDrayageService {
|
||||
requireVersion(value, request.expectedVersion());
|
||||
if (value.getFleetSupplierId() == null) throw conflict("请先选择有效码头车队");
|
||||
if (actualPickupAt(context, value) != null) throw conflict("实际提柜后不可修改预约提柜时间");
|
||||
if (context.facts().arrivedDestinationPortAt() != null
|
||||
if (context.facts() != null && context.facts().arrivedDestinationPortAt() != null
|
||||
&& request.pickupAppointmentAt().isBefore(context.facts().arrivedDestinationPortAt())) {
|
||||
throw badRequest("预约提柜时间不能早于实际到港时间");
|
||||
}
|
||||
@@ -149,7 +149,8 @@ public class TerminalDrayageService {
|
||||
throw conflict("选择码头车队并预约提柜后才能确认实际提柜");
|
||||
}
|
||||
if (actualPickupAt(context, value) != null) throw conflict("实际提柜时间已确认");
|
||||
if (context.facts().latestActualAt() != null && request.actualAt().isBefore(context.facts().latestActualAt())) {
|
||||
if (context.facts() != null && context.facts().latestActualAt() != null
|
||||
&& request.actualAt().isBefore(context.facts().latestActualAt())) {
|
||||
throw badRequest("实际提柜时间不能早于当前实际船节点时间");
|
||||
}
|
||||
advanceNode(context, CarrierScheduleNode.PICKED_UP, request.actualAt(), action, key,
|
||||
@@ -192,14 +193,11 @@ public class TerminalDrayageService {
|
||||
|
||||
private void advanceNode(WorkContext context, CarrierScheduleNode target, LocalDateTime actualAt,
|
||||
String action, String key, String reason, long operatorId, String operatorName) {
|
||||
if (context.facts().queryId() == null || context.facts().queryVersion() == null) {
|
||||
throw conflict("货柜尚未建立船节点跟踪任务");
|
||||
}
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
if (current != null && current.sequence() >= target.sequence()) return;
|
||||
carrierSchedules.addManualNode(context.facts().queryId(), operatorId, operatorName,
|
||||
carrierSchedules.addContainerManualNode(context.base().containerBookingId(), operatorId, operatorName,
|
||||
"td-" + hash(context.base().containerBookingId() + "|" + action + "|" + key.trim()),
|
||||
new ManualNodeRequest(context.facts().queryVersion(), target.name(), LocalDateTime.now(clock), null,
|
||||
new ContainerManualNodeRequest(target.name(), LocalDateTime.now(clock), null,
|
||||
actualAt, null, null, StringUtils.hasText(reason) ? reason.trim() : "码头拖车操作同步"));
|
||||
}
|
||||
|
||||
@@ -292,7 +290,7 @@ public class TerminalDrayageService {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (value != null && value.getFleetSupplierId() != null && actualPickupAt(context, value) == null
|
||||
&& actualEmptyReturnAt(context, value) == null && context.base().unloadingCompletedAt() == null) result.add("SEND_PICKUP_EMAIL");
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
if (current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence()
|
||||
&& current.sequence() < CarrierScheduleNode.PICKED_UP.sequence()
|
||||
&& actualPickupAt(context, value) == null) {
|
||||
@@ -312,7 +310,7 @@ public class TerminalDrayageService {
|
||||
if (context.base().unloadingCompletedAt() != null) return "UNLOADED";
|
||||
if (actualPickupAt(context, value) != null) return "PICKED_UP";
|
||||
if (value != null && value.getPickupAppointmentAt() != null) return "PICKUP_SCHEDULED";
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
return current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence()
|
||||
? "ARRIVED" : "WAITING_FOR_ARRIVAL";
|
||||
}
|
||||
@@ -322,7 +320,7 @@ public class TerminalDrayageService {
|
||||
}
|
||||
|
||||
private void requireAtLeast(WorkContext context, CarrierScheduleNode required, String message) {
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
if (current == null || current.sequence() < required.sequence()) throw conflict(message);
|
||||
}
|
||||
|
||||
@@ -330,13 +328,18 @@ public class TerminalDrayageService {
|
||||
return StringUtils.hasText(value) ? CarrierScheduleNode.require(value) : null;
|
||||
}
|
||||
|
||||
private CarrierScheduleNode currentNode(WorkContext context) {
|
||||
return context.facts() == null ? null : node(context.facts().latestActualNode());
|
||||
}
|
||||
|
||||
private LocalDateTime actualPickupAt(WorkContext context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualPickupAt() != null ? value.getActualPickupAt() : context.facts().pickedUpAt();
|
||||
return value != null && value.getActualPickupAt() != null ? value.getActualPickupAt()
|
||||
: context.facts() == null ? null : context.facts().pickedUpAt();
|
||||
}
|
||||
|
||||
private LocalDateTime actualEmptyReturnAt(WorkContext context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualEmptyReturnAt() != null
|
||||
? value.getActualEmptyReturnAt() : context.facts().emptyReturnedAt();
|
||||
? value.getActualEmptyReturnAt() : context.facts() == null ? null : context.facts().emptyReturnedAt();
|
||||
}
|
||||
|
||||
private WorkContext requiredContext(long id) {
|
||||
@@ -362,8 +365,7 @@ public class TerminalDrayageService {
|
||||
}
|
||||
|
||||
private WorkContext context(Context base, ContainerActualFacts facts) {
|
||||
if (base == null || !"ACTIVE".equals(base.recordStatus()) || facts == null
|
||||
|| facts.departedWarehouseAt() == null) return null;
|
||||
if (base == null || !"ACTIVE".equals(base.recordStatus())) return null;
|
||||
return new WorkContext(base, facts);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user