修改提交
This commit is contained in:
+21
-1
@@ -13,11 +13,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
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.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import jakarta.validation.Valid;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
|
||||
@Tag(name = "客户端入库结果与费用")
|
||||
@RestController
|
||||
@@ -31,12 +36,27 @@ public class MerchantInboundBillingController {
|
||||
@Operation(summary = "查询订单入库结果和最新费用",
|
||||
description = "客户按登录态客户范围只读查看;入库流程达到 WAIT_SPLIT 后,"
|
||||
+ "在 LOADING、LOADED、DELIVERED 仍持续可见,且始终要求 APPROVED + SENT "
|
||||
+ "并存在已发布入库运费明细,不需要客户确认费用。")
|
||||
+ "并存在已发布入库运费明细;同时返回当前费用版本的客户确认状态。")
|
||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||
}
|
||||
|
||||
@PostMapping("/{orderId}/inbound-billing/confirm")
|
||||
@PreAuthorize("hasAuthority('merchant:order:edit')")
|
||||
@Operation(summary = "客户确认已发布的入库费用",
|
||||
description = "仅允许当前客户确认 SENT 状态的最新费用版本;确认后该版本永久锁定,平台不得再修改。")
|
||||
public ApiResponse<FeeSnapshotResponse> confirm(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long orderId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody MerchantInboundBillingResponse.ConfirmFeeRequest request) {
|
||||
long tenantId = FirstlegScopeGuard.merchant(user);
|
||||
String operatorName = user.displayName() == null ? user.username() : user.displayName();
|
||||
return ApiResponse.success(service.confirmFee(tenantId, user.userId(), operatorName,
|
||||
orderId, request.expectedFeeVersion(), idempotencyKey));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/inbound-billing/images/{fileId}")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
@Operation(summary = "查看已发布计费明细中的测量或称重图片")
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ public class PlatformFbaPricingController {
|
||||
return ApiResponse.success(service.calculate(scope, user.userId(), inboundId,
|
||||
request == null ? null : request.pricingAt(),
|
||||
request == null ? null : request.selectedWeightKg(),
|
||||
request == null ? null : request.selectedVolumeCbm(), idempotencyKey));
|
||||
request == null ? null : request.selectedVolumeCbm(),
|
||||
request == null ? null : request.weightSource(),
|
||||
request == null ? null : request.volumeSource(), idempotencyKey));
|
||||
}
|
||||
|
||||
@PostMapping("/confirm")
|
||||
|
||||
@@ -23,7 +23,7 @@ public final class CustomerDimensionConfigDtos {
|
||||
) { }
|
||||
|
||||
public record DefaultCalculationModeUpdateRequest(
|
||||
@NotBlank @Pattern(regexp = "ADD|MULTIPLY") String defaultCalculationMode
|
||||
@NotBlank @Pattern(regexp = "ACTUAL|ADD|MULTIPLY") String defaultCalculationMode
|
||||
) { }
|
||||
|
||||
public record RuleUpdateRequest(
|
||||
|
||||
@@ -86,6 +86,7 @@ public final class FbaPricingDtos {
|
||||
public record FeeSnapshotResponse(
|
||||
Long feeId, String feeNo, Integer feeVersion, String feeStatus,
|
||||
Long inboundId, String inboundCode,
|
||||
Long measurementId, String measurementSource,
|
||||
Long chargeRuleId, String ruleCode, Integer chargeRuleVersion,
|
||||
Long carrierRateId, String rateCode, Integer carrierRateVersion, Long priceBookVersionId,
|
||||
Long customerId, String customerName, Long originWarehouseId, String originWarehouseName,
|
||||
@@ -100,6 +101,8 @@ public final class FbaPricingDtos {
|
||||
BigDecimal totalAmount, String currency,
|
||||
LocalDateTime rateEffectiveFrom, LocalDateTime rateEffectiveTo,
|
||||
LocalDateTime calculatedAt, LocalDateTime confirmedAt, LocalDateTime sentAt,
|
||||
String nextProcessStatus
|
||||
String nextProcessStatus, String customerConfirmationStatus,
|
||||
Long customerConfirmedBy, String customerConfirmedByName,
|
||||
LocalDateTime customerConfirmedAt
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -50,13 +50,14 @@ public final class InboundMeasurementRequests {
|
||||
@NotNull @DecimalMin(value = "0.001", message = "箱规高度必须大于等于 0.001 cm")
|
||||
@DecimalMax(value = "999999999.999", message = "箱规高度不能超过 999,999,999.999 cm")
|
||||
@Digits(integer = 9, fraction = 3, message = "箱规高度最多 9 位整数、3 位小数") BigDecimal heightCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 4) BigDecimal unitWeightKg,
|
||||
@Positive Long intelligentImageFileId,
|
||||
@Size(max = 500) String overLimitWarning,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerLengthCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerWidthCm,
|
||||
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerHeightCm,
|
||||
@Positive Long chargeRuleId,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "ADD|MULTIPLY") String coefficientMode,
|
||||
@jakarta.validation.constraints.Pattern(regexp = "ACTUAL|ADD|MULTIPLY") String coefficientMode,
|
||||
@Size(max = 500) String remark
|
||||
) {
|
||||
public ItemRequest(Long inboundDetailId, String itemNo, String boxSpecification, Integer packageCount,
|
||||
@@ -65,7 +66,7 @@ public final class InboundMeasurementRequests {
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, null, remark);
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ public final class InboundMeasurementRequests {
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, null, remark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ public final class InboundMeasurementResponses {
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
BigDecimal calculatedOuterLengthCm,
|
||||
BigDecimal calculatedOuterWidthCm,
|
||||
BigDecimal calculatedOuterHeightCm,
|
||||
String calculationRuleVersion,
|
||||
Long chargeRuleId,
|
||||
String coefficientMode,
|
||||
String remark,
|
||||
|
||||
@@ -35,6 +35,7 @@ public record MeasurementBatchCommand(
|
||||
BigDecimal lengthCm,
|
||||
BigDecimal widthCm,
|
||||
BigDecimal heightCm,
|
||||
BigDecimal unitWeightKg,
|
||||
Long intelligentImageFileId,
|
||||
String overLimitWarning,
|
||||
BigDecimal outerLengthCm,
|
||||
@@ -44,13 +45,23 @@ public record MeasurementBatchCommand(
|
||||
String coefficientMode,
|
||||
String remark
|
||||
) {
|
||||
public Item(Long inboundDetailId, String itemNo, String boxSpecification, int packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String coefficientMode, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, coefficientMode, remark);
|
||||
}
|
||||
|
||||
public Item(Long inboundDetailId, String itemNo, String boxSpecification, int packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
Long intelligentImageFileId, String overLimitWarning,
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
Long chargeRuleId, String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
chargeRuleId, null, remark);
|
||||
}
|
||||
|
||||
@@ -60,7 +71,7 @@ public record MeasurementBatchCommand(
|
||||
BigDecimal outerLengthCm, BigDecimal outerWidthCm, BigDecimal outerHeightCm,
|
||||
String remark) {
|
||||
this(inboundDetailId, itemNo, boxSpecification, packageCount, lengthCm, widthCm, heightCm,
|
||||
intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, intelligentImageFileId, overLimitWarning, outerLengthCm, outerWidthCm, outerHeightCm,
|
||||
null, null, remark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
@@ -16,11 +18,14 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
BillingBasis billingBasis,
|
||||
FeeSnapshotResponse latestFee,
|
||||
PublishedFeeDetail publishedFeeDetail) {
|
||||
public record ConfirmFeeRequest(@NotNull @Positive Integer expectedFeeVersion) {
|
||||
}
|
||||
|
||||
public record MeasurementSummary(
|
||||
Long measurementId, String sourceType, String measurementStatus,
|
||||
Integer actualPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg,
|
||||
BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName) {
|
||||
BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName,
|
||||
String calculationSource) {
|
||||
}
|
||||
|
||||
public record ImageReference(
|
||||
@@ -32,7 +37,8 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
|
||||
Long id, String itemNo, String boxSpecification, Integer packageCount,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
BigDecimal unitVolumeCbm, BigDecimal totalVolumeCbm,
|
||||
BigDecimal volumeWeightKg) {
|
||||
BigDecimal volumeWeightKg, Long chargeRuleId, String coefficientMode,
|
||||
String calculationRuleVersion) {
|
||||
}
|
||||
|
||||
public record WeightResult(
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 客户固定箱规的内径和长、宽、高尺寸增加值配置。 */
|
||||
/** 客户固定箱规的内径和每侧加宽配置。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_customer_dimension_config")
|
||||
|
||||
@@ -18,6 +18,8 @@ public class InboundFeeEntity {
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Long measurementId;
|
||||
private String measurementSource;
|
||||
private String feeNo;
|
||||
private Integer feeVersion;
|
||||
private Long chargeRuleId;
|
||||
@@ -67,6 +69,9 @@ public class InboundFeeEntity {
|
||||
private LocalDateTime confirmedAt;
|
||||
private Long sentBy;
|
||||
private LocalDateTime sentAt;
|
||||
private Long customerConfirmedBy;
|
||||
private String customerConfirmedByName;
|
||||
private LocalDateTime customerConfirmedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ public class InboundMeasurementItemEntity {
|
||||
private BigDecimal outerLengthCm;
|
||||
private BigDecimal outerWidthCm;
|
||||
private BigDecimal outerHeightCm;
|
||||
private BigDecimal manualOuterLengthCm;
|
||||
private BigDecimal manualOuterWidthCm;
|
||||
private BigDecimal manualOuterHeightCm;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -331,6 +331,20 @@ public class FbaPricingRepository {
|
||||
.set(InboundFeeEntity::getSentAt, now));
|
||||
}
|
||||
|
||||
public int confirmFeeByCustomer(long tenantId, long feeId, int expectedFeeVersion,
|
||||
long operatorId, String operatorName, LocalDateTime now) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
.eq(InboundFeeEntity::getTenantId, tenantId)
|
||||
.eq(InboundFeeEntity::getId, feeId)
|
||||
.eq(InboundFeeEntity::getFeeVersion, expectedFeeVersion)
|
||||
.eq(InboundFeeEntity::getFeeStatus, "SENT")
|
||||
.isNull(InboundFeeEntity::getCustomerConfirmedAt)
|
||||
.set(InboundFeeEntity::getCustomerConfirmedBy, operatorId)
|
||||
.set(InboundFeeEntity::getCustomerConfirmedByName, operatorName)
|
||||
.set(InboundFeeEntity::getCustomerConfirmedAt, now)
|
||||
.set(InboundFeeEntity::getUpdatedAt, now));
|
||||
}
|
||||
|
||||
public int voidSentFee(long tenantId, long inboundId, long feeId, int feeVersion,
|
||||
LocalDateTime now) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
|
||||
+8
-7
@@ -26,7 +26,8 @@ import java.util.Map;
|
||||
@Service
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
public static final String DEFAULT_CALCULATION_MODE = "ADD";
|
||||
public static final String DEFAULT_CALCULATION_MODE = "ACTUAL";
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
@Resource
|
||||
@@ -115,7 +116,7 @@ public class CustomerDimensionConfigService {
|
||||
requirePositive(rule.innerLengthCm(), "箱规内长");
|
||||
requirePositive(rule.innerWidthCm(), "箱规内宽");
|
||||
requirePositive(rule.innerHeightCm(), "箱规内高");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规尺寸增加值");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规单侧加宽");
|
||||
if (rule.version() == null || rule.version() < 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "箱规版本号无效");
|
||||
}
|
||||
@@ -222,10 +223,10 @@ public class CustomerDimensionConfigService {
|
||||
}
|
||||
|
||||
private static RuleResponse ruleResponse(CustomerDimensionConfigEntity value) {
|
||||
BigDecimal addition = value.getEdgeAdditionCm();
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(addition);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(addition);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(addition);
|
||||
BigDecimal edgePair = value.getEdgeAdditionCm().multiply(TWO);
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(edgePair);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(edgePair);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(edgePair);
|
||||
BigDecimal volume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
@@ -253,7 +254,7 @@ public class CustomerDimensionConfigService {
|
||||
private static String normalizeCalculationMode(String value) {
|
||||
if (value == null || value.isBlank()) return DEFAULT_CALCULATION_MODE;
|
||||
String normalized = value.trim().toUpperCase();
|
||||
if (!"ADD".equals(normalized) && !"MULTIPLY".equals(normalized)) {
|
||||
if (!"ACTUAL".equals(normalized) && !"ADD".equals(normalized) && !"MULTIPLY".equals(normalized)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "默认箱规计算方式无效");
|
||||
}
|
||||
return normalized;
|
||||
|
||||
+6
-15
@@ -16,6 +16,7 @@ import java.util.List;
|
||||
/** Resolves dimensions to the smallest containing rule, capped at the largest configured tier. */
|
||||
@Service
|
||||
public class CustomerDimensionWeightingService {
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
private final CustomerDimensionConfigRepository repository;
|
||||
@@ -60,7 +61,7 @@ public class CustomerDimensionWeightingService {
|
||||
return rules.stream()
|
||||
.filter(rule -> rule.contains(measured))
|
||||
.findFirst()
|
||||
.map(Rule::weighted)
|
||||
.map(rule -> rule.weightedActualDimensions(lengthCm, widthCm, heightCm))
|
||||
.orElseGet(() -> rules.get(rules.size() - 1)
|
||||
.weightedActualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
@@ -122,22 +123,12 @@ public class CustomerDimensionWeightingService {
|
||||
&& measured.get(2).compareTo(inner.get(2)) <= 0;
|
||||
}
|
||||
|
||||
private WeightedDimensions weighted() {
|
||||
BigDecimal outerLength = innerLengthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerWidth = innerWidthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerHeight = innerHeightCm.add(edgeAdditionCm);
|
||||
BigDecimal rawVolume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(boxType, version, edgeAdditionCm, outerLength, outerWidth, outerHeight,
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version);
|
||||
}
|
||||
|
||||
private WeightedDimensions weightedActualDimensions(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal outerLength = lengthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerWidth = widthCm.add(edgeAdditionCm);
|
||||
BigDecimal outerHeight = heightCm.add(edgeAdditionCm);
|
||||
BigDecimal edgePair = edgeAdditionCm.multiply(TWO);
|
||||
BigDecimal outerLength = lengthCm.add(edgePair);
|
||||
BigDecimal outerWidth = widthCm.add(edgePair);
|
||||
BigDecimal outerHeight = heightCm.add(edgePair);
|
||||
BigDecimal rawVolume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
|
||||
@@ -38,6 +38,8 @@ public class FbaPricingService {
|
||||
@Resource
|
||||
private FbaPricingRepository repository;
|
||||
@Resource
|
||||
private com.baiyue.firstleg.repository.InboundMeasurementRepository measurementRepository;
|
||||
@Resource
|
||||
private ActivePriceBookEstimator priceBookEstimator;
|
||||
@Resource
|
||||
private Clock clock;
|
||||
@@ -56,15 +58,41 @@ public class FbaPricingService {
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
LocalDateTime requestedAt, BigDecimal selectedWeightKg,
|
||||
BigDecimal selectedVolumeCbm, String idempotencyKey) {
|
||||
return calculate(tenantId, operatorId, inboundId, requestedAt, selectedWeightKg,
|
||||
selectedVolumeCbm, null, null, idempotencyKey);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId,
|
||||
LocalDateTime requestedAt, BigDecimal selectedWeightKg,
|
||||
BigDecimal selectedVolumeCbm, String weightSource,
|
||||
String volumeSource, String idempotencyKey) {
|
||||
String discriminator = (requestedAt == null ? "CURRENT" : requestedAt.toString()) + ":"
|
||||
+ (selectedWeightKg == null ? "ACTUAL_WEIGHT" : selectedWeightKg) + ":"
|
||||
+ (selectedVolumeCbm == null ? "ACTUAL_VOLUME" : selectedVolumeCbm);
|
||||
if (weightSource != null || volumeSource != null) {
|
||||
discriminator += ":" + (weightSource == null ? "UNSPECIFIED" : weightSource)
|
||||
+ ":" + (volumeSource == null ? "UNSPECIFIED" : volumeSource);
|
||||
}
|
||||
String hash = hash("CALCULATE", inboundId, discriminator);
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(
|
||||
tenantId, operatorId, inboundId, "FEE_CALCULATE", idempotencyKey, hash);
|
||||
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
|
||||
|
||||
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
|
||||
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
||||
if ("OUTER".equalsIgnoreCase(volumeSource) && measurement == null) {
|
||||
throw conflict("外箱规测量批次不存在,不能生成对客费用");
|
||||
}
|
||||
if ("OUTER".equalsIgnoreCase(volumeSource)) {
|
||||
var outerItems = measurementRepository.listItems(tenantId, measurement.getId());
|
||||
boolean incomplete = outerItems.isEmpty() || outerItems.stream().anyMatch(item ->
|
||||
item.getOuterLengthCm() == null || item.getOuterLengthCm().signum() <= 0
|
||||
|| item.getOuterWidthCm() == null || item.getOuterWidthCm().signum() <= 0
|
||||
|| item.getOuterHeightCm() == null || item.getOuterHeightCm().signum() <= 0
|
||||
|| !StringUtils.hasText(item.getCalculationRuleVersion()));
|
||||
if (incomplete) throw conflict("外箱规数据或绑定的计算规则不完整,不能生成对客费用");
|
||||
}
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(new EstimateRequest(
|
||||
inbound.getWarehouseId(), inbound.getChannelCode(), inbound.getDestinationCode(),
|
||||
destinationType(inbound), inbound.getDestinationWarehouseId(), inbound.getDestinationName(), null,
|
||||
@@ -84,6 +112,8 @@ public class FbaPricingService {
|
||||
|
||||
InboundFeeEntity fee = snapshot(tenantId, operatorId, inbound, estimate, rate, version,
|
||||
weight, volume, converted, chargeable, subtotal, additional, total, now);
|
||||
fee.setMeasurementId(measurement == null ? null : measurement.getId());
|
||||
fee.setMeasurementSource("OUTER".equalsIgnoreCase(volumeSource) ? "OUTER" : volumeSource);
|
||||
repository.supersedeCalculated(tenantId, inboundId);
|
||||
repository.insertFee(fee);
|
||||
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
|
||||
@@ -102,6 +132,10 @@ public class FbaPricingService {
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirm(long tenantId, long operatorId, long inboundId,
|
||||
String idempotencyKey) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())) {
|
||||
throw conflict("订单已进入装柜流程,不能再修改或发送费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null) throw conflict("入库单尚无可确认的费用试算");
|
||||
String hash = hash("CONFIRM", inboundId, current.getId().toString());
|
||||
@@ -121,6 +155,9 @@ 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("订单已进入装柜流程,不能再修改或发送费用");
|
||||
}
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null) throw conflict("入库单尚无费用快照");
|
||||
String hash = hash("SEND", inboundId, current.getId().toString());
|
||||
@@ -151,6 +188,39 @@ public class FbaPricingService {
|
||||
return response(tenantId, fee.getId(), null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirmByCustomer(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, int expectedFeeVersion,
|
||||
String idempotencyKey) {
|
||||
InboundFeeEntity current = repository.findLatestFee(tenantId, inboundId);
|
||||
if (current == null || !"SENT".equals(current.getFeeStatus())) {
|
||||
throw conflict("费用尚未发送给客户,不能确认");
|
||||
}
|
||||
if (!Integer.valueOf(expectedFeeVersion).equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重新确认");
|
||||
}
|
||||
String hash = hash("CUSTOMER_CONFIRM", inboundId,
|
||||
current.getId() + ":" + expectedFeeVersion);
|
||||
InboundPricingIdempotencyEntity token = beginIdempotency(tenantId, operatorId, inboundId,
|
||||
"CUSTOMER_FEE_CONFIRM", idempotencyKey, hash);
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
return response(tenantId, token.getResultResourceId(), null);
|
||||
}
|
||||
if (current.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("客户已确认该费用,不允许重复确认");
|
||||
}
|
||||
String confirmedName = StringUtils.hasText(operatorName)
|
||||
? operatorName.trim() : String.valueOf(operatorId);
|
||||
if (confirmedName.length() > 64) confirmedName = confirmedName.substring(0, 64);
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (repository.confirmFeeByCustomer(tenantId, current.getId(), expectedFeeVersion,
|
||||
operatorId, confirmedName, now) != 1) {
|
||||
throw conflict("客户已确认或费用版本已变化,请刷新后重试");
|
||||
}
|
||||
complete(token, current.getId(), now);
|
||||
return response(tenantId, current.getId(), null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse adjustPublished(long tenantId, long operatorId, long inboundId,
|
||||
AdjustFeeRequest request, String idempotencyKey) {
|
||||
@@ -163,6 +233,9 @@ public class FbaPricingService {
|
||||
if (current == null || !List.of("CALCULATED", "CONFIRMED", "SENT").contains(current.getFeeStatus())) {
|
||||
throw conflict("入库单没有可修改的初算或已发布费用");
|
||||
}
|
||||
if (current.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("客户已确认该费用,不允许再修改");
|
||||
}
|
||||
if (!request.expectedFeeVersion().equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重试");
|
||||
}
|
||||
@@ -237,7 +310,7 @@ public class FbaPricingService {
|
||||
|
||||
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus())
|
||||
if (!feeEditableBeforeLoading(inbound.getProcessStatus())
|
||||
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
|
||||
throw conflict("当前入库单状态不允许费用试算");
|
||||
}
|
||||
@@ -250,11 +323,6 @@ public class FbaPricingService {
|
||||
if (!"FBA".equals(inbound.getDestinationType()) && !"OVERSEAS".equals(inbound.getDestinationType())) {
|
||||
throw conflict("当前目的地类型不支持自动计费");
|
||||
}
|
||||
if (!"APPROVED".equals(inbound.getMeasureReviewStatus())) throw conflict("只能消费审核通过的测量快照");
|
||||
if (inbound.getActualNetWeightKg() == null || inbound.getActualNetWeightKg().signum() <= 0
|
||||
|| inbound.getActualVolumeCbm() == null || inbound.getActualVolumeCbm().signum() <= 0) {
|
||||
throw conflict("审核通过的测量快照必须包含大于零的货物净重和体积");
|
||||
}
|
||||
if (inbound.getCustomerId() == null || inbound.getCustomerId() <= 0) {
|
||||
throw conflict("入库单缺少订单客户归属,不能生成对客费用");
|
||||
}
|
||||
@@ -268,6 +336,11 @@ public class FbaPricingService {
|
||||
return "WAIT_SPLIT".equals(processStatus);
|
||||
}
|
||||
|
||||
private static boolean feeEditableBeforeLoading(String processStatus) {
|
||||
return List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(processStatus);
|
||||
}
|
||||
|
||||
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||
try {
|
||||
return PricingDestinationType.valueOf(inbound.getDestinationType());
|
||||
@@ -383,6 +456,7 @@ public class FbaPricingService {
|
||||
BigDecimal unitPrice, String nextStatus) {
|
||||
return new FeeSnapshotResponse(fee.getId(), fee.getFeeNo(), fee.getFeeVersion(), fee.getFeeStatus(),
|
||||
fee.getInboundId(), inbound == null ? null : inbound.getInboundCode(),
|
||||
fee.getMeasurementId(), fee.getMeasurementSource(),
|
||||
fee.getChargeRuleId(), fee.getRuleCode(), fee.getChargeRuleVersion(),
|
||||
fee.getCarrierRateId(), fee.getRateCode(), fee.getCarrierRateVersion(), fee.getPriceBookVersionId(),
|
||||
fee.getCustomerId(), fee.getCustomerName(), fee.getOriginWarehouseId(), fee.getOriginWarehouseName(),
|
||||
@@ -394,7 +468,9 @@ public class FbaPricingService {
|
||||
fee.getConvertedQuantity(), fee.getChargeableQuantity(), fee.getChargeUnit(), unitPrice,
|
||||
fee.getSubtotalAmount(), fee.getAdditionalAmount(), fee.getTotalAmount(), fee.getCurrency(),
|
||||
fee.getRateEffectiveFrom(), fee.getRateEffectiveTo(), fee.getCalculatedAt(), fee.getConfirmedAt(),
|
||||
fee.getSentAt(), nextStatus);
|
||||
fee.getSentAt(), nextStatus,
|
||||
fee.getCustomerConfirmedAt() == null ? "PENDING_CONFIRMATION" : "CONFIRMED",
|
||||
fee.getCustomerConfirmedBy(), fee.getCustomerConfirmedByName(), fee.getCustomerConfirmedAt());
|
||||
}
|
||||
|
||||
private static String hash(String operation, long inboundId, String discriminator) {
|
||||
|
||||
@@ -67,10 +67,22 @@ public class InboundMeasurementService {
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
InboundMeasurementEntity measurement = repository.findLatestMeasurement(tenantId, inboundId);
|
||||
return detail(tenantId, inbound, measurement);
|
||||
}
|
||||
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId, long measurementId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
InboundMeasurementEntity measurement = repository.findMeasurement(tenantId, inboundId, measurementId);
|
||||
if (measurement == null) throw new BizException(ResultCode.NOT_FOUND, "费用绑定的测量批次不存在");
|
||||
return detail(tenantId, inbound, measurement);
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.Detail detail(long tenantId, InboundOrderEntity inbound,
|
||||
InboundMeasurementEntity measurement) {
|
||||
List<InboundMeasurementItemEntity> items = measurement == null
|
||||
? List.of() : repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inboundId);
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inboundId);
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inbound.getId());
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inbound.getId());
|
||||
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration = loadLatestDeclaration(inbound);
|
||||
return toDetail(inbound, measurement, items, weights, review, latestDeclaration);
|
||||
}
|
||||
@@ -119,7 +131,7 @@ public class InboundMeasurementService {
|
||||
actualGoodsName,
|
||||
operatorId, request.items().stream().map(item -> new MeasurementBatchCommand.Item(
|
||||
item.inboundDetailId(), item.itemNo(), item.boxSpecification(), item.packageCount(), item.lengthCm(),
|
||||
item.widthCm(), item.heightCm(), item.intelligentImageFileId(),
|
||||
item.widthCm(), item.heightCm(), item.unitWeightKg(), item.intelligentImageFileId(),
|
||||
item.overLimitWarning(), item.outerLengthCm(), item.outerWidthCm(), item.outerHeightCm(),
|
||||
item.chargeRuleId(), item.coefficientMode(), item.remark())).toList());
|
||||
InboundMeasurementEntity saved = batchService.saveBatch(command);
|
||||
@@ -154,9 +166,6 @@ public class InboundMeasurementService {
|
||||
inbound.setProcessStatus("MEASURE_PENDING");
|
||||
inbound.setMeasureReviewStatus("PENDING");
|
||||
inbound.setMeasureCompletedAt(LocalDateTime.now());
|
||||
if (measurement.getTotalPackageCount() != null) {
|
||||
inbound.setTotalPackageCount(measurement.getTotalPackageCount());
|
||||
}
|
||||
inbound.setActualGrossWeightKg(confirmedTotals.grossWeightKg());
|
||||
inbound.setActualNetWeightKg(confirmedTotals.netWeightKg());
|
||||
inbound.setPalletCount(confirmedTotals.palletCount());
|
||||
@@ -534,36 +543,21 @@ public class InboundMeasurementService {
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.Item toItem(InboundMeasurementItemEntity item) {
|
||||
String overrideVersion = item.getCalculationRuleVersion();
|
||||
boolean legacyFullOverride = "WAREHOUSE_OUTER_OVERRIDE_V1".equals(overrideVersion);
|
||||
boolean lengthOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'L');
|
||||
boolean widthOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'W');
|
||||
boolean heightOverridden = legacyFullOverride || warehouseOverrideContains(overrideVersion, 'H');
|
||||
return new InboundMeasurementResponses.Item(
|
||||
item.getId(), item.getInboundDetailId(), item.getItemNo(), item.getBoxSpecification(), item.getPackageCount(),
|
||||
item.getLengthCm(), item.getWidthCm(), item.getHeightCm(), item.getGirthCm(),
|
||||
item.getUnitVolumeCbm(),
|
||||
item.getTotalVolumeCbm(), item.getVolumeWeightKg(), item.getSourceImageFileId(),
|
||||
item.getOverLimitWarning(), lengthOverridden ? item.getOuterLengthCm() : null,
|
||||
widthOverridden ? item.getOuterWidthCm() : null,
|
||||
heightOverridden ? item.getOuterHeightCm() : null,
|
||||
item.getOverLimitWarning(), item.getManualOuterLengthCm(),
|
||||
item.getManualOuterWidthCm(), item.getManualOuterHeightCm(),
|
||||
item.getOuterLengthCm(), item.getOuterWidthCm(), item.getOuterHeightCm(),
|
||||
item.getCalculationRuleVersion(),
|
||||
item.getChargeRuleId(), item.getCoefficientMode(),
|
||||
item.getRemark(), item.getBoxCode(), item.getScannedShipmentId(),
|
||||
item.getPackageCount(),
|
||||
item.getMachineNo(), item.getDeviceMeasuredAt(), item.getUpdatedAt());
|
||||
}
|
||||
|
||||
private boolean warehouseOverrideContains(String calculationRuleVersion, char dimension) {
|
||||
if (calculationRuleVersion == null
|
||||
|| !calculationRuleVersion.startsWith("WAREHOUSE_OUTER_OVERRIDE_")
|
||||
|| !calculationRuleVersion.endsWith("_V1")) {
|
||||
return false;
|
||||
}
|
||||
String dimensions = calculationRuleVersion.substring(
|
||||
"WAREHOUSE_OUTER_OVERRIDE_".length(), calculationRuleVersion.length() - 3);
|
||||
return dimensions.indexOf(dimension) >= 0;
|
||||
}
|
||||
|
||||
private void requireConfirmedPackageTotal(InboundOrderEntity inbound,
|
||||
List<InboundMeasurementItemEntity> items) {
|
||||
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class MeasurementBatchService {
|
||||
private static final BigDecimal MAX_RAW_VOLUME_CBM = new BigDecimal("999999999999.999999");
|
||||
private static final BigDecimal MAX_TOTAL_VOLUME_CBM = new BigDecimal("99999999999999.9999");
|
||||
private static final Pattern SPECIFICATION_COEFFICIENT_PATTERN = Pattern.compile(
|
||||
"(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?);系数:(\\d+(?:\\.\\d+)?)");
|
||||
"(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?);系数:(\\d+(?:\\.\\d+)?)(?:;重量:(\\d+(?:\\.\\d+)?))?");
|
||||
@Resource
|
||||
private InboundMeasurementRepository repository;
|
||||
@Resource
|
||||
@@ -167,7 +167,8 @@ public class MeasurementBatchService {
|
||||
: requestedMode == null ? findDefaultCoefficientRule(command.tenantId(), customerId) : null;
|
||||
String coefficientMode = coefficientRule != null ? "MULTIPLY" : requestedMode;
|
||||
boolean useDimensionRule = "ADD".equals(coefficientMode);
|
||||
BigDecimal addition = weighted.edgeAdditionCm();
|
||||
BigDecimal addition = weighted.edgeAdditionCm() == null
|
||||
? null : weighted.edgeAdditionCm().multiply(BigDecimal.valueOf(2));
|
||||
boolean hasMatchedAddition = useDimensionRule && weighted.boxType() != null;
|
||||
// ADD 只从匹配箱规取得加边值,外尺寸仍以本次实测尺寸为基准,
|
||||
// 不能把箱规边界尺寸当成本次货物的实际外尺寸。
|
||||
@@ -176,7 +177,7 @@ public class MeasurementBatchService {
|
||||
BigDecimal outerHeight = hasMatchedAddition ? baseHeight.add(addition) : baseHeight;
|
||||
if (coefficientRule != null) {
|
||||
BigDecimal specificationCoefficient = resolveSpecificationCoefficient(
|
||||
coefficientRule, baseLength, baseWidth, baseHeight);
|
||||
coefficientRule, baseLength, baseWidth, baseHeight, source.unitWeightKg());
|
||||
outerLength = applyCoefficient(outerLength, specificationCoefficient);
|
||||
outerWidth = applyCoefficient(outerWidth, specificationCoefficient);
|
||||
outerHeight = applyCoefficient(outerHeight, specificationCoefficient);
|
||||
@@ -222,13 +223,13 @@ public class MeasurementBatchService {
|
||||
: useDimensionRule ? weighted.calculationRuleVersion() : "ACTUAL_DIMENSION_V1");
|
||||
item.setSourceImageFileId(source.intelligentImageFileId());
|
||||
item.setOverLimitWarning(source.overLimitWarning());
|
||||
// Override fields retain the operator's base values so repeated saves do not compound the strategy.
|
||||
item.setOuterLengthCm((source.outerLengthCm() != null ? source.outerLengthCm() : outerLength)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm((source.outerWidthCm() != null ? source.outerWidthCm() : outerWidth)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm((source.outerHeightCm() != null ? source.outerHeightCm() : outerHeight)
|
||||
.setScale(3, RoundingMode.HALF_UP));
|
||||
// 人工外箱值是策略计算基数;最终外箱值单独落库,避免覆盖基数或重复叠加策略。
|
||||
item.setManualOuterLengthCm(source.outerLengthCm());
|
||||
item.setManualOuterWidthCm(source.outerWidthCm());
|
||||
item.setManualOuterHeightCm(source.outerHeightCm());
|
||||
item.setOuterLengthCm(outerLength.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterWidthCm(outerWidth.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setOuterHeightCm(outerHeight.setScale(3, RoundingMode.HALF_UP));
|
||||
item.setRemark(source.remark());
|
||||
item.setCreatedAt(LocalDateTime.now());
|
||||
return item;
|
||||
@@ -250,7 +251,7 @@ public class MeasurementBatchService {
|
||||
private String normalizeMode(String mode) {
|
||||
if (mode == null || mode.isBlank()) return null;
|
||||
String normalized = mode.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!Set.of("ADD", "MULTIPLY").contains(normalized)) {
|
||||
if (!Set.of("ACTUAL", "ADD", "MULTIPLY").contains(normalized)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "系数模式仅支持 ADD 或 MULTIPLY");
|
||||
}
|
||||
return normalized;
|
||||
@@ -287,14 +288,14 @@ public class MeasurementBatchService {
|
||||
|
||||
private BigDecimal resolveSpecificationCoefficient(CustomerChargeRuleEntity rule,
|
||||
BigDecimal length, BigDecimal width,
|
||||
BigDecimal height) {
|
||||
BigDecimal height, BigDecimal unitWeightKg) {
|
||||
List<SpecificationCoefficient> tiers = parseSpecificationCoefficients(rule.getRemark());
|
||||
if (tiers.isEmpty()) {
|
||||
return rule.getLengthFactor() == null ? BigDecimal.ONE : rule.getLengthFactor();
|
||||
}
|
||||
List<BigDecimal> measured = sortedDimensions(length, width, height);
|
||||
return tiers.stream()
|
||||
.filter(tier -> tier.contains(measured))
|
||||
.filter(tier -> tier.contains(measured, unitWeightKg))
|
||||
.findFirst()
|
||||
.orElse(tiers.get(tiers.size() - 1))
|
||||
.coefficient();
|
||||
@@ -308,7 +309,8 @@ public class MeasurementBatchService {
|
||||
List<BigDecimal> dimensions = sortedDimensions(new BigDecimal(matcher.group(2)),
|
||||
new BigDecimal(matcher.group(3)), new BigDecimal(matcher.group(4)));
|
||||
BigDecimal coefficient = new BigDecimal(matcher.group(5));
|
||||
if (coefficient.signum() > 0) tiers.add(new SpecificationCoefficient(dimensions, coefficient));
|
||||
BigDecimal weight = matcher.group(6) == null ? null : new BigDecimal(matcher.group(6));
|
||||
if (coefficient.signum() > 0) tiers.add(new SpecificationCoefficient(dimensions, coefficient, weight));
|
||||
}
|
||||
tiers.sort(Comparator.comparing(SpecificationCoefficient::volume));
|
||||
return tiers;
|
||||
@@ -320,15 +322,16 @@ public class MeasurementBatchService {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
private record SpecificationCoefficient(List<BigDecimal> dimensions, BigDecimal coefficient) {
|
||||
private record SpecificationCoefficient(List<BigDecimal> dimensions, BigDecimal coefficient, BigDecimal maxWeightKg) {
|
||||
private BigDecimal volume() {
|
||||
return dimensions.get(0).multiply(dimensions.get(1)).multiply(dimensions.get(2));
|
||||
}
|
||||
|
||||
private boolean contains(List<BigDecimal> measured) {
|
||||
private boolean contains(List<BigDecimal> measured, BigDecimal unitWeightKg) {
|
||||
return measured.get(0).compareTo(dimensions.get(0)) <= 0
|
||||
&& measured.get(1).compareTo(dimensions.get(1)) <= 0
|
||||
&& measured.get(2).compareTo(dimensions.get(2)) <= 0;
|
||||
&& measured.get(2).compareTo(dimensions.get(2)) <= 0
|
||||
&& (maxWeightKg == null || unitWeightKg == null || unitWeightKg.compareTo(maxWeightKg) <= 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-9
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Service
|
||||
public class MerchantInboundBillingService {
|
||||
@@ -55,13 +56,31 @@ public class MerchantInboundBillingService {
|
||||
platformDetail.files().stream()
|
||||
.filter(file -> "PUBLIC".equals(file.visibility()) || "MERCHANT".equals(file.visibility()))
|
||||
.toList(), List.of(), List.of());
|
||||
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
||||
FeeSnapshotResponse fee = pricing.latest(tenantId, inbound.getId());
|
||||
InboundMeasurementResponses.Detail measurement = fee.measurementId() == null || fee.measurementId() <= 0
|
||||
? measurements.detail(tenantId, inbound.getId())
|
||||
: measurements.detail(tenantId, inbound.getId(), fee.measurementId());
|
||||
var measurementItems = measurement.items().stream()
|
||||
.map(item -> new MerchantInboundBillingResponse.MeasurementItem(
|
||||
item.id(), item.itemNo(), item.boxSpecification(), item.packageCount(),
|
||||
requireOuterDimension(item.calculatedOuterLengthCm(), item.itemNo(), "长"),
|
||||
requireOuterDimension(item.calculatedOuterWidthCm(), item.itemNo(), "宽"),
|
||||
requireOuterDimension(item.calculatedOuterHeightCm(), item.itemNo(), "高"),
|
||||
item.unitVolumeCbm(), item.totalVolumeCbm(), item.volumeWeightKg(),
|
||||
item.chargeRuleId(), item.coefficientMode(), item.calculationRuleVersion()))
|
||||
.toList();
|
||||
int outerPackageCount = measurementItems.stream()
|
||||
.map(MerchantInboundBillingResponse.MeasurementItem::packageCount)
|
||||
.filter(java.util.Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
BigDecimal outerVolume = sum(measurementItems.stream()
|
||||
.map(MerchantInboundBillingResponse.MeasurementItem::totalVolumeCbm).toList());
|
||||
BigDecimal outerVolumeWeight = sum(measurementItems.stream()
|
||||
.map(MerchantInboundBillingResponse.MeasurementItem::volumeWeightKg).toList());
|
||||
var summary = new MerchantInboundBillingResponse.MeasurementSummary(
|
||||
measurement.measurementId(), measurement.sourceType(), measurement.measurementStatus(),
|
||||
measurement.actualPackageCount(), measurement.actualGrossWeightKg(),
|
||||
outerPackageCount, measurement.actualGrossWeightKg(),
|
||||
measurement.actualNetWeightKg(),
|
||||
measurement.actualVolumeCbm(), measurement.volumeWeightKg(), measurement.actualGoodsName());
|
||||
outerVolume, outerVolumeWeight, measurement.actualGoodsName(), "OUTER");
|
||||
var measurementImages = measurement.items().stream()
|
||||
.filter(item -> item.intelligentImageFileId() != null)
|
||||
.map(item -> new MerchantInboundBillingResponse.ImageReference(
|
||||
@@ -73,12 +92,6 @@ public class MerchantInboundBillingService {
|
||||
.map(weight -> new MerchantInboundBillingResponse.ImageReference(
|
||||
weight.fileId(), "WEIGHT", null, weight.id(), previewUrl(orderId, weight.fileId())))
|
||||
.toList();
|
||||
var measurementItems = measurement.items().stream()
|
||||
.map(item -> new MerchantInboundBillingResponse.MeasurementItem(
|
||||
item.id(), item.itemNo(), item.boxSpecification(), item.packageCount(),
|
||||
item.lengthCm(), item.widthCm(), item.heightCm(), item.unitVolumeCbm(),
|
||||
item.totalVolumeCbm(), item.volumeWeightKg()))
|
||||
.toList();
|
||||
var weightResults = measurement.weightResults().stream()
|
||||
.map(weight -> new MerchantInboundBillingResponse.WeightResult(
|
||||
weight.id(), weight.fileId(), weight.palletId(), weight.palletCode(), weight.palletType(),
|
||||
@@ -96,6 +109,14 @@ public class MerchantInboundBillingService {
|
||||
weightResults, measurementImages, weightImages, basis, fee, publishedFee(publishedFee));
|
||||
}
|
||||
|
||||
public FeeSnapshotResponse confirmFee(long tenantId, long operatorId, String operatorName,
|
||||
long orderId, int expectedFeeVersion,
|
||||
String idempotencyKey) {
|
||||
InboundOrderEntity inbound = requirePublished(tenantId, orderId);
|
||||
return pricing.confirmByCustomer(tenantId, operatorId, operatorName, inbound.getId(),
|
||||
expectedFeeVersion, idempotencyKey);
|
||||
}
|
||||
|
||||
public ImageDownload downloadImage(long tenantId, long orderId, long fileId) {
|
||||
InboundOrderEntity inbound = requirePublished(tenantId, orderId);
|
||||
InboundMeasurementResponses.Detail measurement = measurements.detail(tenantId, inbound.getId());
|
||||
@@ -134,6 +155,19 @@ public class MerchantInboundBillingService {
|
||||
+ "/inbound-billing/images/" + fileId;
|
||||
}
|
||||
|
||||
private static BigDecimal requireOuterDimension(BigDecimal value, String itemNo, String dimension) {
|
||||
if (value == null || value.signum() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"外箱规数据不完整,明细 " + (itemNo == null ? "—" : itemNo) + " 缺少外箱" + dimension);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static BigDecimal sum(List<BigDecimal> values) {
|
||||
return values.stream().filter(java.util.Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
private String chargeableBasis(FeeSnapshotResponse fee) {
|
||||
int compared = ("KG".equals(fee.chargeMode()) ? fee.actualWeightKg() : fee.actualVolumeCbm())
|
||||
.compareTo(fee.convertedQuantity());
|
||||
|
||||
Reference in New Issue
Block a user