fix: unify same-carton order metrics

This commit is contained in:
2026-09-11 16:50:10 +08:00
parent 87ae82a114
commit b6c66565b9
10 changed files with 72 additions and 14 deletions
@@ -140,8 +140,10 @@ public class InternalOrderCargoController {
// 申报重量/体积必须属于当前订单。拆分子单未物化独立货物事实时不得复制根单整单总量。
List<OrderCargoEntity> cargoes = orders.findCargo(tenantId, orderId);
return ApiResponse.success(new MeasurementDeclarationSnapshot(
order.getReferenceId(), sum(cargoes, OrderCargoEntity::getGrossWeightKg),
sum(cargoes, OrderCargoEntity::getVolumeCbm),
order.getReferenceId(), OrderCargoPackageCounts.physicalMetric(
cargoes, order.getFulfillmentType(), OrderCargoEntity::getGrossWeightKg),
OrderCargoPackageCounts.physicalMetric(
cargoes, order.getFulfillmentType(), OrderCargoEntity::getVolumeCbm),
cargoes.stream().map(OrderCargoEntity::getCargoName).filter(Objects::nonNull)
.map(String::trim).filter(value -> !value.isEmpty()).distinct()
.collect(Collectors.joining("、"))));
@@ -229,7 +229,7 @@ public final class OrderRequests {
@Schema(description = "Product image file ID / 产品图片文件ID") Long productImageFileId,
@Schema(description = "Authoritative wood-product decision frozen with this cargo line / 木制品权威判定,随货物行冻结", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull Boolean woodenProduct,
@Schema(description = "Mixed-carton group. Cargo lines with the same value share physical cartons / 混箱组合号;相同组合号的货物行属于同一批物理箱", maxLength = 32)
@Schema(description = "Client-generated same-carton key. Lines with the same value share carton count, dimensions, weight and volume / 客户端添加同箱 SKU 时生成;同组物理箱字段只汇总一次", maxLength = 32)
@Size(max = 32) String packingGroupNo
) {
public Cargo(String cargoName, String hsCode, String packageType, int packageCount,
@@ -107,9 +107,12 @@ public class MerchantInboundOrderListService {
int packageCount = OrderCargoPackageCounts.physical(cargoes, order.getFulfillmentType());
int totalQuantity = cargoes.stream().map(OrderCargoEntity::getTotalQuantity)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
BigDecimal grossWeight = sum(cargoes, OrderCargoEntity::getGrossWeightKg);
BigDecimal netWeight = sum(cargoes, OrderCargoEntity::getNetWeightKg);
BigDecimal volume = sum(cargoes, OrderCargoEntity::getVolumeCbm);
BigDecimal grossWeight = OrderCargoPackageCounts.physicalMetric(
cargoes, order.getFulfillmentType(), OrderCargoEntity::getGrossWeightKg);
BigDecimal netWeight = OrderCargoPackageCounts.physicalMetric(
cargoes, order.getFulfillmentType(), OrderCargoEntity::getNetWeightKg);
BigDecimal volume = OrderCargoPackageCounts.physicalMetric(
cargoes, order.getFulfillmentType(), OrderCargoEntity::getVolumeCbm);
BigDecimal declaredAmount = sum(cargoes, OrderCargoEntity::getDeclaredValue);
List<MerchantInboundOrderRow.CargoColumn> cargoColumns = cargoes.stream()
@@ -798,9 +798,12 @@ public class OrderApplicationService {
|| !Objects.equals(first.packageType(), cargo.packageType())
|| !sameDecimal(first.lengthCm(), cargo.lengthCm())
|| !sameDecimal(first.widthCm(), cargo.widthCm())
|| !sameDecimal(first.heightCm(), cargo.heightCm()))) {
|| !sameDecimal(first.heightCm(), cargo.heightCm())
|| !sameDecimal(first.grossWeightKg(), cargo.grossWeightKg())
|| !sameDecimal(first.netWeightKg(), cargo.netWeightKg())
|| !sameDecimal(first.volumeCbm(), cargo.volumeCbm()))) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"同一混箱组合的箱数、包装类型和箱规必须一致:" + group);
"同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致");
}
}
}
@@ -7,6 +7,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.math.BigDecimal;
import java.util.function.Function;
/** Calculates physical cartons without double-counting SKU lines in a mixed-carton group. */
public final class OrderCargoPackageCounts {
@@ -21,4 +23,15 @@ public final class OrderCargoPackageCounts {
return countedGroups.add(group) ? count : 0;
}).sum();
}
/** Sums a physical-carton metric once per same-carton group. */
public static BigDecimal physicalMetric(List<OrderCargoEntity> cargoes, String fulfillmentType,
Function<OrderCargoEntity, BigDecimal> getter) {
Set<String> countedGroups = new HashSet<>();
List<BigDecimal> values = cargoes.stream().filter(Objects::nonNull).filter(cargo -> {
String group = StringUtils.hasText(cargo.getPackingGroupNo()) ? cargo.getPackingGroupNo().trim() : null;
return !"ONE_CLICK_DROPSHIP".equals(fulfillmentType) || group == null || countedGroups.add(group);
}).map(getter).filter(Objects::nonNull).toList();
return values.isEmpty() ? null : values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
@@ -508,9 +508,12 @@ public class UnifiedOrderQueryService {
Integer totalPackageCount = hasPackageCount
? OrderCargoPackageCounts.physical(cargoes, fulfillmentType)
: null;
BigDecimal grossWeightKg = sum(cargoes, OrderCargoEntity::getGrossWeightKg);
BigDecimal netWeightKg = sum(cargoes, OrderCargoEntity::getNetWeightKg);
BigDecimal volumeCbm = sum(cargoes, OrderCargoEntity::getVolumeCbm);
BigDecimal grossWeightKg = OrderCargoPackageCounts.physicalMetric(
cargoes, fulfillmentType, OrderCargoEntity::getGrossWeightKg);
BigDecimal netWeightKg = OrderCargoPackageCounts.physicalMetric(
cargoes, fulfillmentType, OrderCargoEntity::getNetWeightKg);
BigDecimal volumeCbm = OrderCargoPackageCounts.physicalMetric(
cargoes, fulfillmentType, OrderCargoEntity::getVolumeCbm);
return new CargoSummary(skuNos, shipmentIds, referenceIds, totalQuantity, totalPackageCount,
grossWeightKg, netWeightKg, volumeCbm);
}
@@ -520,6 +523,7 @@ public class UnifiedOrderQueryService {
List<BigDecimal> values = cargoes.stream().map(value).filter(Objects::nonNull).toList();
return values.isEmpty() ? null : values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
private String inboundMainStatus(String status) {
if (status == null) return null;
@@ -1262,7 +1262,7 @@ components:
heightCm: {type: number, minimum: 0, description: 箱规高(厘米)}
unitsPerPackage: {type: integer, minimum: 1, description: 每箱数量;提交时必填}
packageCount: {type: integer, minimum: 1, description: 箱数}
packingGroupNo: {type: string, maxLength: 32, description: 一件代发混箱组合号;相同组合号的货物行共享同一批物理箱,组内箱数、包装类型和箱规必须一致}
packingGroupNo: {type: string, maxLength: 32, description: 一件代发同箱关联键,由客户端在添加同箱 SKU 时生成;同组货物行共享物理箱,箱数、包装类型、箱规、重量和体积必须一致且汇总时只计算一次}
grossWeightKg: {type: number, exclusiveMinimum: true, minimum: 0, description: 总毛重}
volumeCbm: {type: number, exclusiveMinimum: true, minimum: 0, description: 总体积}
totalQuantity: {type: integer, minimum: 1, description: 总数量;提交时必填且必须等于每箱数量 × 箱数}
@@ -4,6 +4,7 @@ import com.baiyue.order.entity.OrderCargoEntity;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
@@ -18,6 +19,16 @@ class OrderCargoPackageCountsTest {
List.of(skuA, skuB, separate), "ONE_CLICK_DROPSHIP")).isEqualTo(14);
assertThat(OrderCargoPackageCounts.physical(
List.of(skuA, skuB, separate), "STANDARD")).isEqualTo(24);
skuA.setGrossWeightKg(new BigDecimal("12.5"));
skuB.setGrossWeightKg(new BigDecimal("12.5"));
separate.setGrossWeightKg(new BigDecimal("3.0"));
assertThat(OrderCargoPackageCounts.physicalMetric(
List.of(skuA, skuB, separate), "ONE_CLICK_DROPSHIP", OrderCargoEntity::getGrossWeightKg))
.isEqualByComparingTo("15.5");
assertThat(OrderCargoPackageCounts.physicalMetric(
List.of(skuA, skuB, separate), "STANDARD", OrderCargoEntity::getGrossWeightKg))
.isEqualByComparingTo("28.0");
}
private OrderCargoEntity cargo(int count, String group) {
@@ -131,6 +131,28 @@ class UnifiedOrderQueryServiceTest {
verify(orders).findCargoByOrderIds(10001L, List.of(77L));
}
@Test
void dropshipMixedSkuRowsCountSharedCartonVolumeOnlyOnce() {
UnifiedOrderQuery query = query(null);
OrderEntity order = order(OrderStatus.APPROVED);
order.setFulfillmentType("ONE_CLICK_DROPSHIP");
OrderCargoEntity first = new OrderCargoEntity();
first.setOrderId(77L); first.setPackageCount(10); first.setPackingGroupNo("PG-1");
first.setVolumeCbm(new BigDecimal("0.375"));
OrderCargoEntity second = new OrderCargoEntity();
second.setOrderId(77L); second.setPackageCount(10); second.setPackingGroupNo("PG-1");
second.setVolumeCbm(new BigDecimal("0.375"));
when(orders.unifiedPage(eq(10001L), eq(query), any()))
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
when(firstleg.summaries(eq("internal-token"), any())).thenReturn(ApiResponse.success(List.of()));
when(orders.findCargoByOrderIds(10001L, List.of(77L))).thenReturn(List.of(first, second));
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
assertThat(row.totalPackageCount()).isEqualTo(10);
assertThat(row.expectedVolumeCbm()).isEqualByComparingTo("0.375");
});
}
@Test
void listAggregatesShipmentAndReferenceIdsFromAllCargoLines() {
UnifiedOrderQuery query = query(null);
+2 -2
View File
@@ -454,9 +454,9 @@ function buildPayload(context, profile, master, productImageFileId, customerAddr
hsCode: '8518210000', customsHsCode: '8518210000', packageType: 'CARTON',
packingGroupNo: dropship ? mixedPackingGroupNo : null,
lengthCm: carton.lengthCm, widthCm: carton.widthCm, heightCm: carton.heightCm,
unitsPerPackage, packageCount, grossWeightKg: (10 + index) * packageCount,
unitsPerPackage, packageCount, grossWeightKg: (dropship ? 10 : 10 + index) * packageCount,
volumeCbm: cartonVolumeCbm * packageCount, totalQuantity: unitsPerPackage * packageCount,
netWeightKg: (9 + index) * packageCount,
netWeightKg: (dropship ? 9 : 9 + index) * packageCount,
unitDeclaredValue: 8.5, declaredValue: unitsPerPackage * packageCount * 8.5, currency: 'USD',
material: '塑料 / Plastic', usage: '接口验收 / API acceptance',
model: `MODEL-${box.boxType}`, brand: 'TEST-NO-LOGO',