merge: sync test and stabilize full acceptance

This commit is contained in:
2026-09-09 21:14:25 +08:00
40 changed files with 595 additions and 134 deletions
@@ -79,7 +79,7 @@ public class PlatformInboundMeasurementController {
@BusinessAudit(event = "INBOUND_MEASUREMENT_DRAFT_SAVE", resourceType = "INBOUND",
resourceId = "#inboundId", description = "保存入库测量草稿")
@Operation(summary = "保存测量草稿", description = "仅在待测量、测量中或审核驳回状态保存人工测量批次和服务端计算明细;"
@Operation(summary = "保存测量草稿", description = "订单待审核但货物已到仓时可先保存;也可在待测量、测量中或审核驳回状态保存人工测量批次和服务端计算明细;"
+ "进入待审核后批次冻结,确认箱数修正须使用 measurement/items/{itemId}/confirmed-package-count 专用接口。")
@PutMapping("/measurement/draft")
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:warehouse-suggestion:edit',"
@@ -96,7 +96,7 @@ public class PlatformInboundMeasurementController {
@BusinessAudit(event = "INBOUND_MEASUREMENT_SUBMIT", resourceType = "INBOUND",
resourceId = "#inboundId", description = "确认测量并提交审核")
@Operation(summary = "确认测量并提交审核",
description = "校验测量图片和全部 OCR 重量后,最多推进到 MEASURE_PENDING。")
description = "校验测量图片和全部 OCR 重量后,最多推进到 MEASURE_PENDING;订单未审核时只保留待审核测量,不能审核通过或进入后续履约。")
@PostMapping("/measurement/submit")
@PreAuthorize("hasAuthority('platform:firstleg:inbound:measure:submit')")
public ApiResponse<InboundMeasurementResponses.MeasurementSubmitted> submit(
@@ -34,9 +34,9 @@ import java.util.Set;
public class CartonMeasurementService {
private static final Logger log = LoggerFactory.getLogger(CartonMeasurementService.class);
private static final Set<String> DEVICE_WRITABLE_STATUSES = Set.of(
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
private static final Set<String> MANUAL_COUNT_EDITABLE_STATUSES = Set.of(
"WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
private static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024;
@@ -219,6 +219,7 @@ public class FbaPricingService {
/**
* 柜内明细变更后,按最新有效柜内成员重新命中区域重量阶梯。
* 仅使用当前费用快照冻结的选定重量、体积和测量来源,绝不回退到入库单实测值。
* 已发送或客户已确认的费用继续参与柜级计费量聚合,但自身快照保持不变。
*/
@Transactional
public ContainerFeeRecalculationResult recalculateUnlockedContainerCustomerFees(
@@ -257,19 +258,22 @@ public class FbaPricingService {
notReady = true;
continue;
}
if ("SENT".equals(current.getFeeStatus()) || current.getCustomerConfirmedAt() != null) {
throw conflict("柜内存在已发送或客户已确认费用,不能重算阶梯费用");
}
if (!List.of("CALCULATED", "CONFIRMED").contains(current.getFeeStatus())) {
boolean locked = List.of("CONFIRMED", "SENT").contains(current.getFeeStatus())
|| current.getCustomerConfirmedAt() != null;
if (!locked && !"CALCULATED".equals(current.getFeeStatus())) {
throw conflict("柜内费用状态已变化,不能重算阶梯费用");
}
BigDecimal weight = requiredFrozenQuantity(current.getActualWeightKg(), "重量");
BigDecimal volume = requiredFrozenQuantity(current.getActualVolumeCbm(), "体积");
members.add(new ContainerFeeMember(inbound, current,
weight.setScale(3, RoundingMode.HALF_UP), volume.setScale(4, RoundingMode.HALF_UP),
chargeableWeight(weight, volume)));
chargeableWeight(weight, volume), locked));
}
if (notReady) return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.NOT_READY, 0);
List<ContainerFeeMember> unlockedMembers = members.stream().filter(member -> !member.locked()).toList();
if (unlockedMembers.isEmpty()) {
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.NOT_APPLICABLE, 0);
}
InboundPricingIdempotencyEntity token = beginContainerRecalculation(
tenantId, containerBookingId, key, normalizedTrigger);
if ("COMPLETED".equals(token.getStatus())) {
@@ -281,7 +285,7 @@ public class FbaPricingService {
.map(member -> new ContainerTierAmountAllocator.Member(member.inbound().getId(), member.chargeableQuantity()))
.toList();
List<ContainerFeeQuote> quotes = new ArrayList<>();
for (ContainerFeeMember member : members) {
for (ContainerFeeMember member : unlockedMembers) {
PricingPolicy policy = pricingPolicy(member.inbound(), now);
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(tenantId, new EstimateRequest(
member.inbound().getWarehouseId(), member.inbound().getChannelCode(), member.inbound().getDestinationCode(),
@@ -371,7 +375,8 @@ public class FbaPricingService {
}
private record ContainerFeeMember(InboundPricingEntity inbound, InboundFeeEntity current,
BigDecimal weight, BigDecimal volume, BigDecimal chargeableQuantity) { }
BigDecimal weight, BigDecimal volume, BigDecimal chargeableQuantity,
boolean locked) { }
private record ContainerFeeQuote(ContainerFeeMember member, PricingPolicy policy,
ActivePriceBookEstimator.Estimate estimate) { }
@@ -47,7 +47,8 @@ import java.util.Set;
public class InboundMeasurementOpenApiService {
private static final Logger log = LoggerFactory.getLogger(InboundMeasurementOpenApiService.class);
static final String CALLBACK_PATH = "/openapi/v1/inbound-measurements";
private static final Set<String> ACCEPTABLE_INBOUND_STATUSES = Set.of("WAIT_MEASURE", "MEASURE_REJECTED");
private static final Set<String> ACCEPTABLE_INBOUND_STATUSES = Set.of(
"PREPARING", "WAIT_MEASURE", "MEASURE_REJECTED");
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
private static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
private static final String CALCULATION_RULE_VERSION = "V1_CBM_CEILING_4_GIRTH_L2WH";
@@ -49,9 +49,9 @@ public class InboundMeasurementService {
/** OCR 结果无效或不完整,必须人工录入或修正。 */
private static final String WEIGHT_NEEDS_MANUAL = "NEEDS_MANUAL";
private static final Set<String> MEASUREMENT_DRAFT_EDITABLE_STATUSES = Set.of(
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
private static final Set<String> WEIGHT_EDITABLE_STATUSES = Set.of(
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
/** 测量完成后的默认普通托盘类型;实际拆托和库位占用由提交编排服务执行。 */
private static final String RECOMMENDED_PALLET_TYPE = "STANDARD";
private static final BigDecimal LB_TO_KG = new BigDecimal("0.45359237");
@@ -167,7 +167,7 @@ public class InboundMeasurementService {
long tenantId, long inboundId, long operatorId,
InboundMeasurementRequests.SubmitRequest request) {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
if (!Set.of("WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED")
if (!Set.of("PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED")
.contains(inbound.getProcessStatus())) {
throw new BizException(ResultCode.CONFLICT, "测量已提交或审核完成,不能重复提交或修改冻结结果");
}
@@ -384,7 +384,7 @@ public class InboundMeasurementService {
inbound.setActualGrossWeightKg(totals.grossWeightKg());
inbound.setActualNetWeightKg(totals.netWeightKg());
inbound.setPalletCount(totals.palletCount());
if ("WAIT_MEASURE".equals(inbound.getProcessStatus())) {
if (Set.of("PREPARING", "WAIT_MEASURE").contains(inbound.getProcessStatus())) {
inbound.setProcessStatus("MEASURING");
}
inbound.setUpdatedBy(operatorId);
@@ -40,7 +40,7 @@ public class InboundOrderCreationService {
long inboundId = requireMatchingInboundCode(existing, command.inboundCode());
FulfillmentTaskEntity task = fulfillmentTasks.provision(command, LocalDateTime.now());
linkTask(existing, task.getId());
upgradePreparing(existing, command);
approvePreparedInbound(existing, command);
savePickupSnapshot(existing.getId(), command, LocalDateTime.now());
return inboundId;
}
@@ -132,7 +132,7 @@ public class InboundOrderCreationService {
}
LocalDateTime now = LocalDateTime.now();
InboundOrderEntity inbound = newInbound(command, now, null, "PREPARING", "DRAFT");
InboundOrderEntity inbound = newInbound(command, now, null, "PREPARING", "SUBMITTED");
try {
orders.insert(inbound);
savePickupSnapshot(inbound.getId(), command, now);
@@ -153,6 +153,7 @@ public class InboundOrderCreationService {
.eq(InboundOrderEntity::getTenantId, existing.getTenantId())
.eq(InboundOrderEntity::getId, existing.getId())
.eq(InboundOrderEntity::getProcessStatus, "PREPARING")
.set(InboundOrderEntity::getOrderStatusSnapshot, "SUBMITTED")
.set(InboundOrderEntity::getOrderNo, command.orderNo())
.set(InboundOrderEntity::getCustomerId, command.customerId())
.set(InboundOrderEntity::getCustomerName, command.customerName())
@@ -175,13 +176,16 @@ public class InboundOrderCreationService {
.setSql("version = version + 1"));
}
private void upgradePreparing(InboundOrderEntity existing, OrderApprovedInboundCommand command) {
if (!"PREPARING".equals(existing.getProcessStatus())) return;
private void approvePreparedInbound(InboundOrderEntity existing, OrderApprovedInboundCommand command) {
String currentStatus = existing.getProcessStatus();
if (currentStatus == null || !java.util.Set.of("PREPARING", "MEASURING", "MEASURE_PENDING")
.contains(currentStatus)) return;
String approvedStatus = "PREPARING".equals(currentStatus) ? "WAIT_MEASURE" : currentStatus;
var update = new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, existing.getTenantId())
.eq(InboundOrderEntity::getId, existing.getId())
.eq(InboundOrderEntity::getProcessStatus, "PREPARING")
.set(InboundOrderEntity::getProcessStatus, "WAIT_MEASURE")
.eq(InboundOrderEntity::getProcessStatus, currentStatus)
.set(InboundOrderEntity::getProcessStatus, approvedStatus)
.set(InboundOrderEntity::getOrderStatusSnapshot, "APPROVED")
.set(InboundOrderEntity::getTotalPackageCount, command.totalPackageCount())
.set(InboundOrderEntity::getFulfillmentType, fulfillmentType(command.fulfillmentType()))
@@ -189,12 +193,12 @@ public class InboundOrderCreationService {
.setSql("version = version + 1");
if (orders.update(null, update) != 1) {
InboundOrderEntity current = findByOrder(existing.getTenantId(), existing.getOrderId(), true);
if (current == null || !"WAIT_MEASURE".equals(current.getProcessStatus())) {
if (current == null || !"APPROVED".equals(current.getOrderStatusSnapshot())) {
throw new BizException(ResultCode.CONFLICT, "预入库单审核升级发生并发冲突");
}
return;
}
addHistory(existing.getId(), existing.getTenantId(), "PREPARING", "WAIT_MEASURE",
addHistory(existing.getId(), existing.getTenantId(), currentStatus, approvedStatus,
"ORDER_APPROVED", LocalDateTime.now());
}
@@ -43,7 +43,7 @@ public class MeasurementBatchService {
/** 已接收第三方测量数据,待平台提交审核。 */
static final String STATUS_RECEIVED = "RECEIVED";
private static final Set<String> EDITABLE_INBOUND_STATUSES = Set.of(
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
private static final BigDecimal MAX_RAW_VOLUME_CBM = new BigDecimal("999999999999.999999");
private static final BigDecimal MAX_TOTAL_VOLUME_CBM = new BigDecimal("99999999999999.9999");
@@ -64,6 +64,9 @@ public class MeasurementReviewService {
InboundOrderEntity order = requireOrder(tenantId, inboundId);
requirePendingReview(order);
if (!"APPROVED".equals(order.getOrderStatusSnapshot())) {
throw new BizException(ResultCode.CONFLICT, "订单尚未审核通过,已到仓测量数据可保留但暂不能审核");
}
InboundMeasurementEntity measurement = repository.findMeasurement(
tenantId, inboundId, request.measurementId());
if (measurement == null) {