1
This commit is contained in:
@@ -28,7 +28,7 @@ final class ExportSchemaContracts {
|
||||
column("mainStatus", "订单主状态", ExportColumnType.STRING),
|
||||
column("customerName", "客户", ExportColumnType.STRING),
|
||||
column("channelName", "渠道", ExportColumnType.STRING),
|
||||
column("channelChanged", "曾变更渠道", ExportColumnType.BOOLEAN),
|
||||
column("channelChanged", "曾变更渠道", ExportColumnType.STRING),
|
||||
column("receivingWarehouseName", "定位仓库", ExportColumnType.STRING),
|
||||
column("destinationWarehouseName", "交货仓", ExportColumnType.STRING),
|
||||
column("expectedShipDate", "预计出运日期", ExportColumnType.DATE),
|
||||
@@ -44,7 +44,7 @@ final class ExportSchemaContracts {
|
||||
column("skuNos", "SKU", ExportColumnType.STRING),
|
||||
column("taxMode", "报关方式", ExportColumnType.STRING),
|
||||
column("containerMode", "装柜方式", ExportColumnType.STRING),
|
||||
column("customsInspection", "是否商检", ExportColumnType.BOOLEAN),
|
||||
column("customsInspection", "是否商检", ExportColumnType.STRING),
|
||||
column("containerNo", "集装箱号", ExportColumnType.STRING),
|
||||
column("cabinetNo", "柜次号", ExportColumnType.STRING),
|
||||
column("vesselName", "船名", ExportColumnType.STRING),
|
||||
|
||||
@@ -114,7 +114,7 @@ components:
|
||||
mainStatus: {type: string, title: 订单主状态}
|
||||
customerName: {type: string, title: 客户}
|
||||
channelName: {type: string, title: 渠道}
|
||||
channelChanged: {type: boolean, title: 曾变更渠道}
|
||||
channelChanged: {type: string, title: 曾变更渠道, description: 是或否}
|
||||
receivingWarehouseName: {type: string, title: 定位仓库}
|
||||
destinationWarehouseName: {type: string, title: 交货仓}
|
||||
expectedShipDate: {type: string, format: date, title: 预计出运日期}
|
||||
@@ -130,7 +130,7 @@ components:
|
||||
skuNos: {type: string, title: SKU}
|
||||
taxMode: {type: string, title: 报关方式}
|
||||
containerMode: {type: string, title: 装柜方式}
|
||||
customsInspection: {type: boolean, title: 是否商检}
|
||||
customsInspection: {type: string, title: 是否商检, description: 是或否}
|
||||
containerNo: {type: string, title: 集装箱号}
|
||||
cabinetNo: {type: string, title: 柜次号}
|
||||
vesselName: {type: string, title: 船名}
|
||||
|
||||
+2
@@ -57,6 +57,8 @@ class DomesticWarehouseOrderExportContractTest {
|
||||
assertThat(columns).containsKeys("expectedVolumeCbm", "expectedArrivalAt")
|
||||
.doesNotContainKeys("orderId", "tenantId", "customerId", "inboundId", "receivingWarehouseId",
|
||||
"destinationWarehouseId");
|
||||
assertThat(map(columns.get("channelChanged")).get("type")).isEqualTo("string");
|
||||
assertThat(map(columns.get("customsInspection")).get("type")).isEqualTo("string");
|
||||
assertThat(source).contains("platform:order:export", "selectedOrderNos 非空时优先", "no-store");
|
||||
assertThat(paths).containsKey("/api/platform/v1/files/export-tasks/{taskId}/retry");
|
||||
assertThat(map(map(schemas.get("ExportTaskRetryState")).get("properties")))
|
||||
|
||||
@@ -157,6 +157,9 @@ class GenericXlsxExportRunnerTest {
|
||||
Map<String, Object> row = new java.util.LinkedHashMap<>();
|
||||
row.put("inboundCode", "IN-1"); row.put("orderNo", "ORD-1");
|
||||
row.put("expectedVolumeCbm", "1.25"); row.put("expectedArrivalAt", "2026-09-03T12:30:00");
|
||||
row.put("mainStatus", "待审核"); row.put("channelChanged", "否");
|
||||
row.put("feeReviewStatus", "未提交"); row.put("taxMode", "买单报关");
|
||||
row.put("containerMode", "拼箱"); row.put("customsInspection", "否");
|
||||
var batch = new ExportBatchResponse(1, "sha256:domestic-orders", "snapshot-orders",
|
||||
columns, List.of(row), null, false, 1L);
|
||||
when(provider.batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST), any(), any()))
|
||||
@@ -173,6 +176,12 @@ class GenericXlsxExportRunnerTest {
|
||||
assertEquals("预计到货时间", sheet.getRow(0).getCell(9).getStringCellValue());
|
||||
assertEquals("预计体积(m³)", sheet.getRow(0).getCell(10).getStringCellValue());
|
||||
assertEquals(1.25d, sheet.getRow(1).getCell(10).getNumericCellValue());
|
||||
assertEquals("待审核", sheet.getRow(1).getCell(2).getStringCellValue());
|
||||
assertEquals("否", sheet.getRow(1).getCell(5).getStringCellValue());
|
||||
assertEquals("未提交", sheet.getRow(1).getCell(17).getStringCellValue());
|
||||
assertEquals("买单报关", sheet.getRow(1).getCell(19).getStringCellValue());
|
||||
assertEquals("拼箱", sheet.getRow(1).getCell(20).getStringCellValue());
|
||||
assertEquals("否", sheet.getRow(1).getCell(21).getStringCellValue());
|
||||
}
|
||||
ArgumentCaptor<ExportBatchRequest> request = ArgumentCaptor.forClass(ExportBatchRequest.class);
|
||||
verify(provider).batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST),
|
||||
|
||||
@@ -23,6 +23,8 @@ public final class InboundMeasurementResponses {
|
||||
Integer expectedPalletCount,
|
||||
String processStatus,
|
||||
String measureReviewStatus,
|
||||
@Schema(description = "当前最新费用版本是否已由客户确认;确认后测量数据不可再修改")
|
||||
boolean customerFeeConfirmed,
|
||||
@Schema(description = "测量完成后的推荐托盘类型,仅为建议,不代表已分配托盘",
|
||||
allowableValues = "STANDARD", example = "STANDARD")
|
||||
String recommendedPalletType,
|
||||
|
||||
@@ -109,7 +109,7 @@ public class InboundMeasurementService {
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inbound.getId());
|
||||
LatestDeclaration latestDeclaration = loadLatestDeclaration(inbound);
|
||||
return toDetail(inbound, measurement, items, weights, allocationSlots, allocationsByWeight,
|
||||
review, latestDeclaration);
|
||||
review, latestDeclaration, customerFeeConfirmed(tenantId, inbound.getId()));
|
||||
}
|
||||
|
||||
public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
||||
@@ -236,10 +236,9 @@ public class InboundMeasurementService {
|
||||
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) {
|
||||
if (customerConfirmed) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"客户已确认且该票已装柜完成,不能修改" + subject);
|
||||
"客户已确认费用,不能修改" + subject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,12 +568,14 @@ public class InboundMeasurementService {
|
||||
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationSlots,
|
||||
Map<Long, com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationsByWeight,
|
||||
MeasureReviewRecordEntity review,
|
||||
LatestDeclaration latestDeclaration) {
|
||||
LatestDeclaration latestDeclaration,
|
||||
boolean customerFeeConfirmed) {
|
||||
InboundMeasurementResponses.DeclarationSnapshot declaration = latestDeclaration.snapshot();
|
||||
return new InboundMeasurementResponses.Detail(
|
||||
inbound.getId(), inbound.getInboundCode(), customerDisplayName(inbound), inbound.getChannelCode(),
|
||||
inbound.getChannelName(), inbound.getFulfillmentType(), inbound.getPalletCount(),
|
||||
inbound.getProcessStatus(), inbound.getMeasureReviewStatus(),
|
||||
customerFeeConfirmed,
|
||||
RECOMMENDED_PALLET_TYPE,
|
||||
review == null ? null : review.getReviewReason(), review == null ? null : review.getReviewerId(),
|
||||
review == null ? null : review.getReviewerName(), measurement == null ? null : measurement.getId(),
|
||||
@@ -603,6 +604,11 @@ public class InboundMeasurementService {
|
||||
toPalletLocationGuide(allocationSlots));
|
||||
}
|
||||
|
||||
private boolean customerFeeConfirmed(long tenantId, long inboundId) {
|
||||
var latestFee = pricingRepository.findLatestFee(tenantId, inboundId);
|
||||
return latestFee != null && latestFee.getCustomerConfirmedAt() != null;
|
||||
}
|
||||
|
||||
private static InboundMeasurementResponses.PalletLocationGuide toPalletLocationGuide(
|
||||
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> slots) {
|
||||
if (slots.isEmpty()) return null;
|
||||
|
||||
@@ -1018,8 +1018,7 @@ paths:
|
||||
summary: 更新箱规计算规则
|
||||
description: >-
|
||||
独立更新当前测量批次全部明细的箱规计算规则并重算体积,不覆盖整批测量数据,
|
||||
不改变测量提交或审核状态。客户未确认或该票尚未装柜完成时均可修改;
|
||||
仅当客户已确认费用且该票已装柜完成时冻结。
|
||||
不改变测量提交或审核状态。客户确认费用后立即冻结,不得再修改。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -1036,8 +1035,7 @@ paths:
|
||||
summary: 更新仓库外箱长宽高
|
||||
description: >-
|
||||
独立更新当前测量批次指定明细的仓库外箱尺寸并重算体积,不覆盖整批测量数据,
|
||||
不改变测量提交或审核状态。客户未确认或该票尚未装柜完成时均可修改;
|
||||
仅当客户已确认费用且该票已装柜完成时冻结。
|
||||
不改变测量提交或审核状态。客户确认费用后立即冻结,不得再修改。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -1905,7 +1903,7 @@ components:
|
||||
type: object
|
||||
description: >-
|
||||
入库测量权威详情。尺寸证据天然归属 items,称重证据天然归属 weightResults,响应不提供统一 images。
|
||||
required: [inboundId, inboundVersion, items, weightResults]
|
||||
required: [inboundId, inboundVersion, customerFeeConfirmed, items, weightResults]
|
||||
properties:
|
||||
inboundId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
inboundCode: {type: string, nullable: true, description: 当前测量详情唯一入仓码,UI业务主展示字段}
|
||||
@@ -1914,6 +1912,7 @@ components:
|
||||
channelName: {type: string, nullable: true}
|
||||
processStatus: {type: string, nullable: true}
|
||||
measureReviewStatus: {type: string, nullable: true}
|
||||
customerFeeConfirmed: {type: boolean, description: 当前最新费用版本已由客户确认,确认后测量数据不可再修改}
|
||||
recommendedPalletType: {type: string, nullable: true}
|
||||
reviewReason: {type: string, nullable: true}
|
||||
reviewerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
|
||||
+20
-3
@@ -124,8 +124,8 @@ class InboundMeasurementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void locksCalculationStrategyOnlyAfterCustomerConfirmationAndLoadingCompletion() {
|
||||
InboundOrderEntity inbound = inbound("LOADED");
|
||||
void locksCalculationStrategyAsSoonAsCustomerConfirmsFee() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setCustomerConfirmedAt(LocalDateTime.now());
|
||||
when(repository.lockInbound(1L, 10L)).thenReturn(inbound);
|
||||
@@ -134,7 +134,7 @@ class InboundMeasurementServiceTest {
|
||||
assertThatThrownBy(() -> service.updateCalculationStrategy(1L, 10L,
|
||||
new InboundMeasurementRequests.UpdateCalculationStrategyRequest(30L, 5, "ACTUAL", null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("客户已确认且该票已装柜完成");
|
||||
.hasMessageContaining("客户已确认费用");
|
||||
|
||||
verify(repository, never()).lockMeasurement(anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
@@ -245,6 +245,23 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReportsCustomerFeeConfirmationForTheMeasurementLock() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setCustomerConfirmedAt(LocalDateTime.now());
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||
when(pricingRepository.findLatestFee(1L, 10L)).thenReturn(fee);
|
||||
|
||||
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
|
||||
|
||||
assertThat(result.customerFeeConfirmed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReportsOrderContractNotReadyInsteadOfExposingDownstreamBadRequest() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
|
||||
@@ -51,6 +51,7 @@ public final class DispatchRequests {
|
||||
|
||||
public record Update(@Size(max = 64) String vehiclePlate, @Size(max = 128) String driverName,
|
||||
@Size(max = 64) String driverPhone, @Size(max = 255) String carrierName,
|
||||
LocalDateTime departAt,
|
||||
@Size(max = 64) String fbaAppointmentNo,
|
||||
@Size(max = 32) String fbaAppointmentStatus,
|
||||
@Size(max = 32) String vehicleAppointmentStatus,
|
||||
|
||||
+32
-4
@@ -29,6 +29,14 @@ public interface DispatchCandidateMapper {
|
||||
"MIN(l.order_id) AS orderId," +
|
||||
"MIN(ip.customer_id) AS customerId,MIN(o.customer_name) AS customerName," +
|
||||
"MIN(ip.plan_no) AS planNo,MIN(o.order_type) AS orderType,MIN(o.volume_weight_kg) AS volumeWeightKg," +
|
||||
"COALESCE(NULLIF(SUM(CASE WHEN COALESCE(l.volume_cbm,0)>0 THEN " +
|
||||
"COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," +
|
||||
"(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " +
|
||||
"* COALESCE(a.allocated_volume_cbm,0) / l.volume_cbm ELSE " +
|
||||
"COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," +
|
||||
"(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " +
|
||||
"/ NULLIF((SELECT COUNT(*) FROM ll_inbound_plan_pallet_allocation a2 WHERE a2.line_id=a.line_id),0) END),0)," +
|
||||
"MIN(o.volume_weight_kg)) AS grossWeightKg," +
|
||||
"MIN(ip.container_no) AS containerNo," +
|
||||
"(SELECT MAX(fa.appointment_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||
@@ -38,7 +46,13 @@ public interface DispatchCandidateMapper {
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND pos.available_packages > 0) ELSE CAST(ip.overseas_warehouse_id AS CHAR) END AS overseasWarehouseId," +
|
||||
"AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')," +
|
||||
"(SELECT NULLIF(TRIM(source_order.overseas_warehouse_id),'') FROM ll_order source_order " +
|
||||
"WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS overseasWarehouseId," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_code) " +
|
||||
"FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " +
|
||||
"ELSE MIN(o.overseas_warehouse_code) END AS overseasWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_name) " +
|
||||
"FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " +
|
||||
@@ -115,6 +129,14 @@ public interface DispatchCandidateMapper {
|
||||
"MIN(l.order_id) AS orderId," +
|
||||
"MIN(ip.customer_id) AS customerId,MIN(o.customer_name) AS customerName," +
|
||||
"MIN(ip.plan_no) AS planNo,MIN(o.order_type) AS orderType,MIN(o.volume_weight_kg) AS volumeWeightKg," +
|
||||
"COALESCE(NULLIF(SUM(CASE WHEN COALESCE(l.volume_cbm,0)>0 THEN " +
|
||||
"COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," +
|
||||
"(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " +
|
||||
"* COALESCE(a.allocated_volume_cbm,0) / l.volume_cbm ELSE " +
|
||||
"COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," +
|
||||
"(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " +
|
||||
"/ NULLIF((SELECT COUNT(*) FROM ll_inbound_plan_pallet_allocation a2 WHERE a2.line_id=a.line_id),0) END),0)," +
|
||||
"MIN(o.volume_weight_kg)) AS grossWeightKg," +
|
||||
"MIN(ip.container_no) AS containerNo," +
|
||||
"(SELECT MAX(fa.appointment_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||
@@ -124,7 +146,13 @@ public interface DispatchCandidateMapper {
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND pos.available_packages > 0) ELSE CAST(ip.overseas_warehouse_id AS CHAR) END AS overseasWarehouseId," +
|
||||
"AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')," +
|
||||
"(SELECT NULLIF(TRIM(source_order.overseas_warehouse_id),'') FROM ll_order source_order " +
|
||||
"WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS overseasWarehouseId," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_code) " +
|
||||
"FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " +
|
||||
"ELSE MIN(o.overseas_warehouse_code) END AS overseasWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_name) " +
|
||||
"FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " +
|
||||
@@ -185,8 +213,8 @@ public interface DispatchCandidateMapper {
|
||||
|
||||
@Select("<script>" +
|
||||
"SELECT NULL AS planId,NULL AS planPalletId,pos.pallet_id AS warehousePalletId,a.lastleg_order_id AS orderId," +
|
||||
"a.customer_id AS customerId,NULL AS customerName,NULL AS planNo,'RELABEL_APPLICATION' AS orderType,NULL AS volumeWeightKg,NULL AS containerNo," +
|
||||
"NULL AS appointmentAt,NULL AS expectedLoadingAt,a.overseas_warehouse_id AS overseasWarehouseId,MIN(ri.source_warehouse_name) AS overseasWarehouseName," +
|
||||
"a.customer_id AS customerId,NULL AS customerName,NULL AS planNo,'RELABEL_APPLICATION' AS orderType,NULL AS volumeWeightKg,NULL AS grossWeightKg,NULL AS containerNo," +
|
||||
"NULL AS appointmentAt,NULL AS expectedLoadingAt,a.overseas_warehouse_id AS overseasWarehouseId,MIN(ri.source_warehouse_code) AS overseasWarehouseCode,MIN(ri.source_warehouse_name) AS overseasWarehouseName," +
|
||||
"MIN(COALESCE(NULLIF(ri.source_inbound_order_no,''),NULLIF(a.customer_order_no,''),a.relabel_no)) AS orderNo,a.relabel_no AS orderRefs,MIN(ri.source_inbound_code) AS inboundCode,pos.pallet_no AS palletNo,NULL AS destinationWarehouseId," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseName," +
|
||||
|
||||
+4
-2
@@ -13,10 +13,12 @@ public record DispatchPalletCandidate(
|
||||
String planNo,
|
||||
String orderType,
|
||||
BigDecimal volumeWeightKg,
|
||||
BigDecimal grossWeightKg,
|
||||
String containerNo,
|
||||
LocalDateTime appointmentAt,
|
||||
LocalDateTime expectedLoadingAt,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String orderNo,
|
||||
String orderRefs,
|
||||
@@ -43,7 +45,7 @@ public record DispatchPalletCandidate(
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
String fbaWarehouseCode, Integer packageCount, BigDecimal volumeCbm,
|
||||
String sourceStage) {
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, overseasWarehouseId,
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, null, overseasWarehouseId, null,
|
||||
overseasWarehouseName, orderNo, orderRefs, inboundCode, palletNo, destinationWarehouseId,
|
||||
destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode, packageCount, volumeCbm,
|
||||
sourceStage, null, null, null, null, null, null, null);
|
||||
@@ -54,7 +56,7 @@ public record DispatchPalletCandidate(
|
||||
String destinationWarehouseId, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String fbaWarehouseCode, Integer packageCount,
|
||||
BigDecimal volumeCbm, String sourceStage) {
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, null, null, orderNo, orderRefs, inboundCode,
|
||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, null, null, null, null, orderNo, orderRefs, inboundCode,
|
||||
palletNo, destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
|
||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@@ -536,6 +536,7 @@ public class DispatchService {
|
||||
dispatch.setDriverName(trimToNull(request.driverName()));
|
||||
dispatch.setDriverPhone(trimToNull(request.driverPhone()));
|
||||
dispatch.setCarrierName(trimToNull(request.carrierName()));
|
||||
dispatch.setDepartAt(request.departAt());
|
||||
dispatch.setRoute(itemRoute(assignedItems));
|
||||
dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo()));
|
||||
dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus()));
|
||||
|
||||
+10
-1
@@ -12,6 +12,7 @@ public record InboundPlanListResponse(
|
||||
String inboundCode,
|
||||
String lxCode,
|
||||
String orderType,
|
||||
Long overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String warehouseId,
|
||||
@@ -34,4 +35,12 @@ public record InboundPlanListResponse(
|
||||
String dispatchStatus,
|
||||
LocalDateTime createdAt,
|
||||
Integer version
|
||||
) { }
|
||||
) {
|
||||
public InboundPlanListResponse withReceivingWarehouse(String warehouseCode, String warehouseName) {
|
||||
return new InboundPlanListResponse(lineId, planId, planNo, customerId, orderId, inboundCode, lxCode,
|
||||
orderType, overseasWarehouseId, warehouseCode, warehouseName, warehouseId, this.warehouseCode,
|
||||
this.warehouseName, fbaWarehouseCode, customerName, packageCount, pickedPackageCount, volumeCbm,
|
||||
palletCount, assigned, mergeAllowed, urgent, status, appointmentNo, appointmentStatus, appointmentAt,
|
||||
expectedLoadingAt, dispatchStatus, createdAt, version);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ public interface InboundPlanQueryMapper {
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
"<if test='q.lxCode != null and q.lxCode != \"\"'> AND o.lx_code LIKE CONCAT('%',#{q.lxCode},'%')</if>" +
|
||||
"<if test='q.status != null and q.status != \"\"'> AND p.status=#{q.status}</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND o.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND p.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND l.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.warehouse != null and q.warehouse != \"\"'> AND (l.destination_warehouse_id LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_code LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_name LIKE CONCAT('%',#{q.warehouse},'%'))</if>" +
|
||||
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code LIKE CONCAT('%',#{q.fbaWarehouseCode},'%')</if>" +
|
||||
@@ -38,7 +38,7 @@ public interface InboundPlanQueryMapper {
|
||||
|
||||
@Select("<script>SELECT l.id lineId,p.id planId,p.plan_no planNo,p.customer_id customerId," +
|
||||
"l.order_id orderId,l.inbound_code inboundCode,o.lx_code lxCode,o.order_type orderType," +
|
||||
"o.overseas_warehouse_code overseasWarehouseCode,o.overseas_warehouse_name overseasWarehouseName," +
|
||||
"p.overseas_warehouse_id overseasWarehouseId,o.overseas_warehouse_code overseasWarehouseCode,o.overseas_warehouse_name overseasWarehouseName," +
|
||||
"l.destination_warehouse_id warehouseId,l.destination_warehouse_code warehouseCode," +
|
||||
"l.destination_warehouse_name warehouseName,l.fba_warehouse_code fbaWarehouseCode," +
|
||||
"o.customer_name customerName,l.required_package_count packageCount," +
|
||||
|
||||
+11
@@ -179,10 +179,21 @@ public class InboundPlanService {
|
||||
long total = queryMapper.count(customerId, query);
|
||||
List<InboundPlanListResponse> records = total == 0 ? List.of() : queryMapper.rows(
|
||||
customerId, query, (query.page() - 1) * query.size(), query.size());
|
||||
Map<Long, WarehouseResourceGateway.WarehouseReference> receivingWarehouses = new HashMap<>();
|
||||
records = records.stream().map(record -> canonicalizeReceivingWarehouse(record, receivingWarehouses)).toList();
|
||||
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||||
return new PageResult<>(records, query.page(), query.size(), total, pages);
|
||||
}
|
||||
|
||||
private InboundPlanListResponse canonicalizeReceivingWarehouse(InboundPlanListResponse record,
|
||||
Map<Long, WarehouseResourceGateway.WarehouseReference> cache) {
|
||||
Long warehouseId = record.overseasWarehouseId();
|
||||
if (warehouseId == null) return record.withReceivingWarehouse(null, null);
|
||||
WarehouseResourceGateway.WarehouseReference warehouse = cache.computeIfAbsent(warehouseId,
|
||||
id -> requiredReceivingWarehouse(String.valueOf(id)));
|
||||
return record.withReceivingWarehouse(warehouse.warehouseCode(), warehouse.warehouseName());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ContainerInboundPlanPreviewResponse previewContainer(String sourceContainerBookingId) {
|
||||
String bookingId = trimToNull(sourceContainerBookingId);
|
||||
|
||||
+27
-2
@@ -62,17 +62,42 @@ public interface InventoryQueryMapper {
|
||||
"<when test='q.inventoryStatus == \"PROCUREMENT\"'> AND b.procurement_packages > 0</when>" +
|
||||
"</choose>";
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_balance" + BALANCE_FILTER +
|
||||
@Select("<script>SELECT MIN(id) id," +
|
||||
"customer_id,MAX(customer_name) customer_name," +
|
||||
"overseas_warehouse_id,MAX(overseas_warehouse_code) overseas_warehouse_code," +
|
||||
"MAX(overseas_warehouse_name) overseas_warehouse_name," +
|
||||
"destination_warehouse_id,destination_warehouse_code," +
|
||||
"MAX(destination_warehouse_name) destination_warehouse_name," +
|
||||
"NULL inbound_order_id,NULL inbound_order_no,NULL inbound_code," +
|
||||
"sku_code,MAX(product_name_zh) product_name_zh,MAX(product_name_en) product_name_en," +
|
||||
"COALESCE(SUM(available_packages),0) available_packages," +
|
||||
"COALESCE(SUM(hold_packages),0) hold_packages," +
|
||||
"COALESCE(SUM(allocated_packages),0) allocated_packages," +
|
||||
"COALESCE(SUM(picked_packages),0) picked_packages," +
|
||||
"COALESCE(SUM(in_transit_packages),0) in_transit_packages," +
|
||||
"COALESCE(SUM(receiving_packages),0) receiving_packages," +
|
||||
"COALESCE(SUM(procurement_packages),0) procurement_packages," +
|
||||
"MAX(updated_at) updated_at FROM ll_inventory_balance" + BALANCE_FILTER +
|
||||
" GROUP BY customer_id,overseas_warehouse_id,destination_warehouse_id,destination_warehouse_code,sku_code" +
|
||||
" ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InventoryBalance> balances(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_inventory_balance" + BALANCE_FILTER + "</script>")
|
||||
@Select("<script>SELECT COUNT(*) FROM (SELECT 1 FROM ll_inventory_balance" + BALANCE_FILTER +
|
||||
" GROUP BY customer_id,overseas_warehouse_id,destination_warehouse_id,destination_warehouse_code,sku_code" +
|
||||
") grouped_balances</script>")
|
||||
long countBalances(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_balance" + BALANCE_FILTER +
|
||||
" ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InventoryBalance> sourceBalances(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) dimension_count," +
|
||||
"COALESCE(SUM(available_packages),0) available_packages," +
|
||||
"COALESCE(SUM(hold_packages),0) hold_packages," +
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ public class InventoryQueryService {
|
||||
final int batchSize = 1000;
|
||||
long offset = 0;
|
||||
while (true) {
|
||||
List<InventoryBalanceView> batch = mapper.balances(customerId, query, offset, batchSize).stream().map(this::balanceView).toList();
|
||||
List<InventoryBalanceView> batch = mapper.sourceBalances(customerId, query, offset, batchSize).stream().map(this::balanceView).toList();
|
||||
if (batch.isEmpty()) break;
|
||||
for (InventoryBalanceView v : batch) output.write((
|
||||
String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()),
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.baiyue.lastleg.model;
|
||||
|
||||
/** Standard main-process statuses for a last-leg order. */
|
||||
public enum LastlegOrderStatus {
|
||||
DRAFT("草稿"),
|
||||
PENDING_REVIEW("待审核"),
|
||||
APPROVED("审核通过"),
|
||||
DESTINATION_ASSIGNED("已分配目的仓"),
|
||||
INBOUND_PLANNED("已生成入库计划"),
|
||||
INBOUND_MEASURING("入库测量中"),
|
||||
PENDING_ASSIGNMENT("待分配"),
|
||||
PENDING_INBOUND("待入库"),
|
||||
@@ -16,6 +20,7 @@ public enum LastlegOrderStatus {
|
||||
PARTIALLY_RECEIVED("部分收货"),
|
||||
RECEIVED("已收货"),
|
||||
IN_TRANSIT("运输中"),
|
||||
PROCESSING("处理中"),
|
||||
COMPLETED("已完成"),
|
||||
CANCELLED("已取消"),
|
||||
PENDING_DELIVERY("待派送"),
|
||||
@@ -31,4 +36,13 @@ public enum LastlegOrderStatus {
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public static String labelOf(String status) {
|
||||
if (status == null || status.isBlank()) return status;
|
||||
try {
|
||||
return valueOf(status).label();
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
|
||||
import com.baiyue.lastleg.mapper.ContainerArrivalTimeOperationMapper;
|
||||
import com.baiyue.lastleg.model.LastlegOrderType;
|
||||
import com.baiyue.lastleg.model.LastlegOrderStatus;
|
||||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||||
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -584,12 +585,12 @@ public class LastlegOrderService {
|
||||
csv(order.getSourceOrderNo()), csv(order.getCustomerName()), csv(order.getOverseasWarehouseName()),
|
||||
csv(order.getTransferWarehouseName()), csv(order.getTransferWarehouseCode()), csv(order.getFbaWarehouseCode()),
|
||||
csv(order.getContainerNo()), csv(StringUtils.hasText(order.getSourceContainerBookingId())
|
||||
? containerNodes.get(order.getSourceContainerBookingId()) : null), csv(order.getAbnormal()), csv(order.getDetained()),
|
||||
? containerNodes.get(order.getSourceContainerBookingId()) : null), csv(yesOrNo(order.getAbnormal())), csv(yesOrNo(order.getDetained())),
|
||||
csv(order.getPalletCount()), csv(order.getPackageCount()), csv(order.getReceivedPackageCount()),
|
||||
csv(packageDifference(order)), csv(order.getVolumeCbm()), csv(order.getProductQuantity()),
|
||||
csv(order.getVolumeWeightKg()), csv(order.getEstimatedArrivalAt()), csv(order.getActualArrivalAt()),
|
||||
csv(order.getEstimatedWarehouseAt()), csv(order.getInboundOrderNo()), csv(order.getRelabelOrderNo()), csv(relabelBusinessStatus(order.getRelabelStatus())),
|
||||
csv(order.getPalletMergeFlag()), csv(order.getCurrentStatus()), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()),
|
||||
csv(order.getPalletMergeFlag()), csv(LastlegOrderStatus.labelOf(order.getCurrentStatus())), csv(order.getSubOrderStatus()), csv(order.getPrivateRemark()),
|
||||
csv(order.getCustomerRemark()), csv(order.getWarehouseRemark()))) + "\r\n";
|
||||
output.write(line.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
@@ -730,6 +731,11 @@ public class LastlegOrderService {
|
||||
? "COMPLETED" : "PENDING_PROCESSING";
|
||||
}
|
||||
|
||||
private static String yesOrNo(Boolean value) {
|
||||
if (value == null) return null;
|
||||
return value ? "是" : "否";
|
||||
}
|
||||
|
||||
private static Long parseLong(String value) {
|
||||
try { return StringUtils.hasText(value) ? Long.parseLong(value.trim()) : null; }
|
||||
catch (NumberFormatException ignored) { return null; }
|
||||
|
||||
@@ -1204,6 +1204,32 @@ CREATE TABLE IF NOT EXISTS ll_inbound_fba_appointment_item (
|
||||
KEY idx_ll_fba_appointment_item_order (order_id)
|
||||
) ENGINE=InnoDB COMMENT='FBA入仓预约明细';
|
||||
|
||||
-- Upgrade the pre-customer-scope appointment table created by older deployments.
|
||||
SET @ddl = (SELECT IF(
|
||||
NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_inbound_fba_appointment' AND column_name='customer_id')
|
||||
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_inbound_fba_appointment' AND column_name='tenant_id'),
|
||||
'ALTER TABLE ll_inbound_fba_appointment CHANGE COLUMN tenant_id customer_id VARCHAR(128) NOT NULL COMMENT ''客户租户ID''',
|
||||
'SELECT 1'));
|
||||
PREPARE fba_appointment_customer_scope FROM @ddl; EXECUTE fba_appointment_customer_scope; DEALLOCATE PREPARE fba_appointment_customer_scope;
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_inbound_fba_appointment' AND column_name='expected_loading_at'),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE ll_inbound_fba_appointment ADD COLUMN expected_loading_at DATETIME(3) NULL COMMENT ''预计装车时间'' AFTER appointment_at'));
|
||||
PREPARE fba_appointment_expected_loading FROM @ddl; EXECUTE fba_appointment_expected_loading; DEALLOCATE PREPARE fba_appointment_expected_loading;
|
||||
|
||||
UPDATE ll_inbound_fba_appointment
|
||||
SET expected_loading_at=appointment_at
|
||||
WHERE expected_loading_at IS NULL;
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_inbound_fba_appointment' AND column_name='expected_loading_at' AND is_nullable='YES'),
|
||||
'ALTER TABLE ll_inbound_fba_appointment MODIFY COLUMN expected_loading_at DATETIME(3) NOT NULL COMMENT ''预计装车时间''',
|
||||
'SELECT 1'));
|
||||
PREPARE fba_appointment_expected_loading_required FROM @ddl; EXECUTE fba_appointment_expected_loading_required; DEALLOCATE PREPARE fba_appointment_expected_loading_required;
|
||||
|
||||
ALTER TABLE ll_loading_order_status_history
|
||||
MODIFY COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '状态历史主键',
|
||||
MODIFY COLUMN order_id BIGINT NOT NULL COMMENT '尾程订单 ID',
|
||||
|
||||
@@ -463,7 +463,7 @@ paths:
|
||||
/api/platform/v1/last-leg/inbound-plans:
|
||||
get:
|
||||
summary: 平台分页查询海外入库订单
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;列表中的 overseasWarehouseCode/overseasWarehouseName 只取尾程订单预分配接柜仓事实,warehouseCode/warehouseName 仍表示目的仓。
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;列表中的 overseasWarehouseCode/overseasWarehouseName 按入库计划冻结接柜仓ID从海外仓权威目录解析,warehouseCode/warehouseName 仍表示目的仓。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||
@@ -2162,10 +2162,12 @@ components:
|
||||
planNo: { type: string, nullable: true }
|
||||
orderType: { type: string, nullable: true }
|
||||
volumeWeightKg: { type: number, nullable: true }
|
||||
grossWeightKg: { type: number, nullable: true, description: 候选托盘实际毛重(千克);按入库计划分配比例汇总,缺失时回退体积重 }
|
||||
containerNo: { type: string, nullable: true }
|
||||
appointmentAt: { type: string, format: date-time, nullable: true }
|
||||
expectedLoadingAt: { type: string, format: date-time, nullable: true }
|
||||
overseasWarehouseId: { type: string }
|
||||
overseasWarehouseCode: { type: string, nullable: true, description: 实际海外接柜仓编码,用于智能排车区域筛选 }
|
||||
overseasWarehouseName: { type: string }
|
||||
orderNo: { type: string }
|
||||
orderRefs: { type: string }
|
||||
@@ -2230,12 +2232,13 @@ components:
|
||||
palletIds: { type: array, items: { type: string } }
|
||||
DispatchUpdate:
|
||||
type: object
|
||||
description: 普通编辑字段;不包含 status、departAt 或路线,路线由已排托盘关联的 FBA 仓自动汇总。
|
||||
description: 普通编辑字段;不包含 status 或路线,路线由已排托盘关联的 FBA 仓自动汇总。
|
||||
properties:
|
||||
vehiclePlate: { type: string, nullable: true, maxLength: 64 }
|
||||
driverName: { type: string, nullable: true, maxLength: 128 }
|
||||
driverPhone: { type: string, nullable: true, maxLength: 64 }
|
||||
carrierName: { type: string, nullable: true, maxLength: 255 }
|
||||
departAt: { type: string, format: date-time, nullable: true }
|
||||
fbaAppointmentNo: { type: string, nullable: true, maxLength: 64 }
|
||||
fbaAppointmentStatus: { type: string, nullable: true, maxLength: 32 }
|
||||
vehicleAppointmentStatus: { type: string, nullable: true, maxLength: 32 }
|
||||
@@ -2918,8 +2921,9 @@ components:
|
||||
orderId: { type: string, pattern: '^[0-9]+$' }
|
||||
inboundCode: { type: string }
|
||||
orderType: { type: string, nullable: true }
|
||||
overseasWarehouseCode: { type: string, description: 接柜仓编码,取自尾程订单预分配接收仓事实 }
|
||||
overseasWarehouseName: { type: string, description: 接柜仓名称,取自尾程订单预分配接收仓事实 }
|
||||
overseasWarehouseId: { type: string, pattern: '^[0-9]+$', nullable: true, description: 入库计划冻结的接柜海外仓ID,不是FBA目的仓ID }
|
||||
overseasWarehouseCode: { type: string, description: 按入库计划冻结接柜仓ID从海外仓权威目录解析的编码 }
|
||||
overseasWarehouseName: { type: string, description: 按入库计划冻结接柜仓ID从海外仓权威目录解析的名称 }
|
||||
warehouseId: { type: string, nullable: true }
|
||||
warehouseCode: { type: string, nullable: true }
|
||||
warehouseName: { type: string, nullable: true }
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ class PlatformDispatchOutboundContractTest {
|
||||
assertTrue(Arrays.stream(DispatchController.class.getDeclaredMethods())
|
||||
.noneMatch(method -> "depart".equals(method.getName())));
|
||||
assertEquals(Set.of("vehiclePlate", "driverName", "driverPhone", "carrierName",
|
||||
"fbaAppointmentNo", "fbaAppointmentStatus", "vehicleAppointmentStatus", "estimatedArrivalAt"),
|
||||
"departAt", "fbaAppointmentNo", "fbaAppointmentStatus", "vehicleAppointmentStatus", "estimatedArrivalAt"),
|
||||
Arrays.stream(DispatchRequests.Update.class.getRecordComponents())
|
||||
.map(component -> component.getName()).collect(java.util.stream.Collectors.toSet()));
|
||||
}
|
||||
|
||||
+24
-3
@@ -12,8 +12,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class DispatchCandidateMapperContractTest {
|
||||
private static final List<String> CONSTRUCTOR_ALIASES = List.of(
|
||||
"planId", "planPalletId", "warehousePalletId", "orderId", "customerId", "customerName",
|
||||
"planNo", "orderType", "volumeWeightKg", "containerNo", "appointmentAt", "expectedLoadingAt",
|
||||
"overseasWarehouseId", "overseasWarehouseName", "orderNo", "orderRefs", "inboundCode", "palletNo",
|
||||
"planNo", "orderType", "volumeWeightKg", "grossWeightKg", "containerNo", "appointmentAt", "expectedLoadingAt",
|
||||
"overseasWarehouseId", "overseasWarehouseCode", "overseasWarehouseName", "orderNo", "orderRefs", "inboundCode", "palletNo",
|
||||
"destinationWarehouseId", "destinationWarehouseCode", "destinationWarehouseName", "fbaWarehouseCode",
|
||||
"packageCount", "volumeCbm", "sourceStage", "cartonMarks", "relabelCartonMarks",
|
||||
"destinationType", "deliveryAddress", "postalCode", "contactName", "contactPhone"
|
||||
@@ -70,7 +70,12 @@ class DispatchCandidateMapperContractTest {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL"));
|
||||
assertTrue(sql.contains("pos.overseas_warehouse_id"));
|
||||
assertTrue(sql.contains("CAST(ip.overseas_warehouse_id AS CHAR)"));
|
||||
assertTrue(sql.contains("NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')"));
|
||||
assertTrue(sql.contains("source_order.id=ip.source_order_id"));
|
||||
assertTrue(sql.contains("source_order.customer_id=ip.customer_id"));
|
||||
assertTrue(sql.contains("source_order.overseas_warehouse_id"));
|
||||
assertTrue(sql.contains("AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("b.overseas_warehouse_code"));
|
||||
assertTrue(sql.contains("ll_inventory_balance"));
|
||||
}
|
||||
}
|
||||
@@ -89,6 +94,22 @@ class DispatchCandidateMapperContractTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateGrossWeightIsAllocatedByPalletAndFallsBackToVolumeWeight() throws Exception {
|
||||
for (Method method : List.of(
|
||||
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
|
||||
String.class, String.class, String.class),
|
||||
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
assertTrue(sql.contains("SUM(d.gross_weight_kg)"));
|
||||
assertTrue(sql.contains("inbound.gross_weight_kg"));
|
||||
assertTrue(sql.contains("ll_inbound_order inbound"));
|
||||
assertTrue(sql.contains("a.allocated_volume_cbm"));
|
||||
assertTrue(sql.contains("a2.line_id=a.line_id"));
|
||||
assertTrue(sql.contains("MIN(o.volume_weight_kg)) AS grossWeightKg"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertConstructorColumnOrder(Method method) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
int previous = -1;
|
||||
|
||||
+3
@@ -31,12 +31,15 @@ class SmartDispatchOpenApiContractTest {
|
||||
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:smart-dispatch"));
|
||||
assertTrue(yaml.contains("SmartDispatchPreArrange"));
|
||||
assertTrue(yaml.contains("SmartDispatchCandidate"));
|
||||
assertTrue(yaml.contains("overseasWarehouseCode: { type: string, nullable: true, description: 实际海外接柜仓编码"));
|
||||
assertTrue(yaml.contains("grossWeightKg: { type: number, nullable: true, description: 候选托盘实际毛重"));
|
||||
String preArrangeSchema = yaml.substring(yaml.indexOf(" SmartDispatchPreArrange:"),
|
||||
yaml.indexOf(" SmartDispatchDraftPallet:"));
|
||||
String updateSchema = yaml.substring(yaml.indexOf(" DispatchUpdate:"),
|
||||
yaml.indexOf(" DeliveryTransfer:"));
|
||||
assertFalse(preArrangeSchema.contains("route:"));
|
||||
assertFalse(updateSchema.contains("route:"));
|
||||
assertTrue(updateSchema.contains("departAt: { type: string, format: date-time, nullable: true }"));
|
||||
assertTrue(yaml.contains("name: planIds"));
|
||||
String planIdsParameter = yaml.substring(yaml.indexOf("name: planIds"),
|
||||
yaml.indexOf("name: orderNo", yaml.indexOf("name: planIds")));
|
||||
|
||||
+5
-2
@@ -370,13 +370,16 @@ class DispatchServiceTest {
|
||||
DispatchItem ont8 = item(11L, "ONT8");
|
||||
DispatchItem abe4 = item(12L, "ABE4");
|
||||
when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(ont8, abe4, item(13L, "ONT8")));
|
||||
LocalDateTime departAt = LocalDateTime.of(2026, 9, 18, 10, 30);
|
||||
DispatchRequests.Update request = new DispatchRequests.Update("ABC-1", "Driver", null, null,
|
||||
null, null, null, LocalDateTime.now());
|
||||
departAt, null, null, null, LocalDateTime.now());
|
||||
|
||||
DispatchResponse result = service.update("PC1", request);
|
||||
|
||||
assertEquals(DispatchService.PENDING_DEPARTURE, dispatch.getStatus());
|
||||
assertEquals("ABC-1", result.vehiclePlate());
|
||||
assertEquals(departAt, result.departAt());
|
||||
assertEquals(departAt, dispatch.getDepartAt());
|
||||
assertEquals("ONT8,ABE4", result.route());
|
||||
assertEquals("ONT8,ABE4", dispatch.getRoute());
|
||||
verify(orders).updateById(dispatch);
|
||||
@@ -390,7 +393,7 @@ class DispatchServiceTest {
|
||||
dispatch.setStatus("DEPARTED");
|
||||
when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch);
|
||||
DispatchRequests.Update request = new DispatchRequests.Update(null, null, null, null,
|
||||
null, null, null, null);
|
||||
null, null, null, null, null);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.update("PC1", request));
|
||||
|
||||
+2
@@ -28,6 +28,8 @@ class InboundPlanSchemaContractTest {
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
|
||||
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
|
||||
assertTrue(schema.contains("expected_loading_at"));
|
||||
assertTrue(schema.contains("change column tenant_id customer_id"));
|
||||
assertTrue(schema.contains("set expected_loading_at=appointment_at"));
|
||||
assertTrue(schema.contains("planned_arrival_date"));
|
||||
assertTrue(schema.contains("creation_mode varchar(32)"));
|
||||
assertTrue(schema.contains("modify column creation_mode varchar(32)"));
|
||||
|
||||
+3
@@ -16,6 +16,9 @@ class InboundPlanQueryMapperContractTest {
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("p.version version"));
|
||||
assertTrue(sql.contains("l.order_id orderId,l.inbound_code inboundCode,o.lx_code lxCode,o.order_type orderType"));
|
||||
assertTrue(sql.contains("p.overseas_warehouse_id overseasWarehouseId"));
|
||||
assertTrue(sql.contains("p.overseas_warehouse_id=#{q.overseasWarehouseId}"));
|
||||
assertTrue(sql.contains("WHERE <if test='customerId != null and customerId != \"\"'>di.customer_id=#{customerId} AND </if>di.order_id=l.order_id"));
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.service;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import jakarta.validation.Validation;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -22,12 +23,14 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundPlanPaginationTest {
|
||||
@Mock InboundPlanQueryMapper queryMapper;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
private InboundPlanService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundPlanService();
|
||||
ReflectionTestUtils.setField(service, "queryMapper", queryMapper);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -48,19 +51,22 @@ class InboundPlanPaginationTest {
|
||||
null, "PENDING_ASSIGNMENT", null, null, "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
|
||||
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
|
||||
3L, "IN-1", null, "FIRST_LEG", "US-LAX-01", "洛杉矶仓", "W1", "ONT", "安大略仓",
|
||||
3L, "IN-1", null, "FIRST_LEG", 193000000000200003L, "ACY2", "错误目的仓", "W1", "ONT", "安大略仓",
|
||||
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, true, true, true, "PENDING_ASSIGNMENT", null, null, null,
|
||||
null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0);
|
||||
when(queryMapper.count("100", query)).thenReturn(11L);
|
||||
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
|
||||
var result = service.page("100", query);
|
||||
|
||||
assertThat(result.records()).containsExactly(row);
|
||||
assertThat(result.records()).containsExactly(row.withReceivingWarehouse("US-LAX-01", "洛杉矶仓"));
|
||||
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
|
||||
assertThat(result.records().get(0).appointmentStatus()).isNull();
|
||||
assertThat(result.records().get(0).overseasWarehouseCode()).isEqualTo("US-LAX-01");
|
||||
assertThat(result.records().get(0).overseasWarehouseName()).isEqualTo("洛杉矶仓");
|
||||
assertThat(result.records().get(0).overseasWarehouseId()).isEqualTo(193000000000200003L);
|
||||
assertThat(result.records().get(0).assigned()).isTrue();
|
||||
assertThat(result.records().get(0).mergeAllowed()).isTrue();
|
||||
assertThat(result.records().get(0).urgent()).isTrue();
|
||||
@@ -76,9 +82,9 @@ class InboundPlanPaginationTest {
|
||||
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
|
||||
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
|
||||
assertThat(sql).contains("di.customer_id=#{customerId} AND </if>di.order_id=l.order_id");
|
||||
assertThat(sql).contains("o.overseas_warehouse_code overseasWarehouseCode",
|
||||
assertThat(sql).contains("p.overseas_warehouse_id overseasWarehouseId", "o.overseas_warehouse_code overseasWarehouseCode",
|
||||
"o.overseas_warehouse_name overseasWarehouseName",
|
||||
"o.overseas_warehouse_id=#{q.overseasWarehouseId}");
|
||||
"p.overseas_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).contains("p.assigned_flag assigned", "p.merge_required mergeAllowed",
|
||||
"p.urgent_flag urgent");
|
||||
assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}");
|
||||
|
||||
+15
@@ -88,6 +88,21 @@ class InventorySchemaContractTest {
|
||||
void balanceQueriesHideZeroRowsAndMutationsKeepWarehouseDimensionCanonical() throws Exception {
|
||||
assertTrue(InventoryQueryMapper.BALANCE_FILTER.contains("procurement_packages)>0"));
|
||||
assertTrue(InventoryQueryMapper.JOINED_BALANCE_FILTER.contains("b.procurement_packages)>0"));
|
||||
String balancesSql = String.join(" ", InventoryQueryMapper.class
|
||||
.getMethod("balances", String.class, InventoryQueryRequests.BalanceQuery.class, long.class, long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertTrue(balancesSql.contains("SUM(available_packages)"));
|
||||
assertTrue(balancesSql.contains("GROUP BY customer_id,overseas_warehouse_id,destination_warehouse_id,destination_warehouse_code,sku_code"));
|
||||
assertTrue(balancesSql.contains("NULL inbound_order_id"));
|
||||
String balanceCountSql = String.join(" ", InventoryQueryMapper.class
|
||||
.getMethod("countBalances", String.class, InventoryQueryRequests.BalanceQuery.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertTrue(balanceCountSql.contains("FROM (SELECT 1 FROM ll_inventory_balance"));
|
||||
assertTrue(balanceCountSql.contains("GROUP BY customer_id,overseas_warehouse_id,destination_warehouse_id,destination_warehouse_code,sku_code"));
|
||||
String sourceBalancesSql = String.join(" ", InventoryQueryMapper.class
|
||||
.getMethod("sourceBalances", String.class, InventoryQueryRequests.BalanceQuery.class, long.class, long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertFalse(sourceBalancesSql.contains("GROUP BY"));
|
||||
String bindSql = String.join(" ", com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper.class
|
||||
.getMethod("bindInboundPlan", String.class, String.class, Long.class, String.class,
|
||||
String.class, java.time.LocalDateTime.class)
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ class InventoryQueryServiceTest {
|
||||
@Test
|
||||
void balanceExportUsesInboundOrderPackageColumnsOnly() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
|
||||
when(mapper.balances("100", query, 0, 10_000)).thenReturn(List.of());
|
||||
when(mapper.sourceBalances("100", query, 0, 10_000)).thenReturn(List.of());
|
||||
|
||||
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
|
||||
import com.baiyue.lastleg.mapper.ContainerArrivalTimeOperationMapper;
|
||||
import com.baiyue.lastleg.model.LastlegOrderStatus;
|
||||
import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||||
@@ -37,6 +38,7 @@ import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -387,6 +389,31 @@ class LastlegOrderServiceTest {
|
||||
assertEquals(25, page.getValue().getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportUsesChineseBooleanAndMainStatusLabels() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
||||
order.setAbnormal(false);
|
||||
order.setDetained(true);
|
||||
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 1000);
|
||||
resultPage.setRecords(List.of(order));
|
||||
resultPage.setTotal(1);
|
||||
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
||||
|
||||
String csv = new String(service.exportCsv(null, new LastlegOrderRequests.Query(
|
||||
1L, 20L, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null)), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(csv.contains("\"否\",\"是\""));
|
||||
assertTrue(csv.contains("\"待分配\""));
|
||||
assertFalse(csv.contains("\"false\""));
|
||||
assertFalse(csv.contains("\"PENDING_ASSIGNMENT\""));
|
||||
assertEquals("草稿", LastlegOrderStatus.labelOf("DRAFT"));
|
||||
assertEquals("审核通过", LastlegOrderStatus.labelOf("APPROVED"));
|
||||
assertEquals("已分配目的仓", LastlegOrderStatus.labelOf("DESTINATION_ASSIGNED"));
|
||||
assertEquals("已生成入库计划", LastlegOrderStatus.labelOf("INBOUND_PLANNED"));
|
||||
assertEquals("处理中", LastlegOrderStatus.labelOf("PROCESSING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageWarehouseFilterMatchesFrozenWarehouseName() {
|
||||
assertWarehouseFilter("洛杉矶仓", "洛杉矶仓", true);
|
||||
|
||||
+34
-7
@@ -49,7 +49,7 @@ public class DomesticWarehouseOrderExportProviderService {
|
||||
column("mainStatus", "订单主状态", ExportColumnType.STRING, 16),
|
||||
column("customerName", "客户", ExportColumnType.STRING, 24),
|
||||
column("channelName", "渠道", ExportColumnType.STRING, 20),
|
||||
column("channelChanged", "曾变更渠道", ExportColumnType.BOOLEAN, 12),
|
||||
column("channelChanged", "曾变更渠道", ExportColumnType.STRING, 12),
|
||||
column("receivingWarehouseName", "定位仓库", ExportColumnType.STRING, 20),
|
||||
column("destinationWarehouseName", "交货仓", ExportColumnType.STRING, 20),
|
||||
column("expectedShipDate", "预计出运日期", ExportColumnType.DATE, 14),
|
||||
@@ -65,7 +65,7 @@ public class DomesticWarehouseOrderExportProviderService {
|
||||
column("skuNos", "SKU", ExportColumnType.STRING, 28),
|
||||
column("taxMode", "报关方式", ExportColumnType.STRING, 16),
|
||||
column("containerMode", "装柜方式", ExportColumnType.STRING, 14),
|
||||
column("customsInspection", "是否商检", ExportColumnType.BOOLEAN, 12),
|
||||
column("customsInspection", "是否商检", ExportColumnType.STRING, 12),
|
||||
column("containerNo", "集装箱号", ExportColumnType.STRING, 20),
|
||||
column("cabinetNo", "柜次号", ExportColumnType.STRING, 20),
|
||||
column("vesselName", "船名", ExportColumnType.STRING, 18),
|
||||
@@ -126,16 +126,16 @@ public class DomesticWarehouseOrderExportProviderService {
|
||||
private Map<String, Object> row(UnifiedOrderResponse.Row value) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("inboundCode", value.inboundCode()); row.put("orderNo", value.orderNo());
|
||||
row.put("mainStatus", value.mainStatus()); row.put("customerName", value.customerName());
|
||||
row.put("channelName", value.channelName()); row.put("channelChanged", value.channelChanged()); row.put("receivingWarehouseName", value.receivingWarehouseName());
|
||||
row.put("mainStatus", mainStatusLabel(value.mainStatus())); row.put("customerName", value.customerName());
|
||||
row.put("channelName", value.channelName()); row.put("channelChanged", yesOrNo(value.channelChanged())); row.put("receivingWarehouseName", value.receivingWarehouseName());
|
||||
row.put("destinationWarehouseName", value.destinationWarehouseName()); row.put("expectedShipDate", value.expectedShipDate());
|
||||
row.put("expectedArrivalAt", value.expectedArrivalAt()); row.put("expectedVolumeCbm", value.expectedVolumeCbm());
|
||||
row.put("totalPackageCount", value.totalPackageCount()); row.put("totalQuantity", value.totalQuantity());
|
||||
row.put("actualGrossWeightKg", value.actualGrossWeightKg()); row.put("actualVolumeCbm", value.actualVolumeCbm());
|
||||
row.put("chargeableWeightKg", value.chargeableWeightKg()); row.put("chargeableVolumeCbm", value.chargeableVolumeCbm());
|
||||
row.put("feeReviewStatus", value.feeReviewStatus()); row.put("skuNos", join(value.skuNos()));
|
||||
row.put("taxMode", value.taxMode()); row.put("containerMode", value.containerMode());
|
||||
row.put("customsInspection", value.customsInspection());
|
||||
row.put("feeReviewStatus", feeReviewStatusLabel(value.feeReviewStatus())); row.put("skuNos", join(value.skuNos()));
|
||||
row.put("taxMode", taxModeLabel(value.taxMode())); row.put("containerMode", containerModeLabel(value.containerMode()));
|
||||
row.put("customsInspection", yesOrNo(value.customsInspection()));
|
||||
var transport = value.transport();
|
||||
row.put("containerNo", transport == null ? null : transport.containerNo());
|
||||
row.put("cabinetNo", transport == null ? null : transport.cabinetNo());
|
||||
@@ -233,6 +233,33 @@ public class DomesticWarehouseOrderExportProviderService {
|
||||
}
|
||||
|
||||
private static String join(List<String> values) { return values == null ? null : String.join(",", values); }
|
||||
private static String mainStatusLabel(String value) {
|
||||
return label(value, Map.ofEntries(
|
||||
Map.entry("DRAFT", "草稿"), Map.entry("SUBMITTED", "待审核"), Map.entry("APPROVED", "已审核"),
|
||||
Map.entry("ACCEPTED", "已受理"), Map.entry("REJECTED", "已驳回"), Map.entry("PROCESSING", "处理中"),
|
||||
Map.entry("COMPLETED", "已完成"), Map.entry("CANCELLED", "已取消"),
|
||||
Map.entry("PENDING_REVIEW", "待审核"), Map.entry("WAIT_MEASURE", "待测量数据"),
|
||||
Map.entry("WAIT_PALLETIZE", "待一件代发组托"), Map.entry("INBOUNDED", "已入库"),
|
||||
Map.entry("WAIT_SPLIT", "待分柜"), Map.entry("LOADING", "装柜中"),
|
||||
Map.entry("LOADED", "装柜完成"), Map.entry("DELIVERED", "派送完成")));
|
||||
}
|
||||
private static String feeReviewStatusLabel(String value) {
|
||||
return label(value, Map.ofEntries(
|
||||
Map.entry("NOT_SUBMITTED", "未提交"), Map.entry("UNRECORDED", "未录入"),
|
||||
Map.entry("PENDING", "待审核"), Map.entry("APPROVED", "审核通过"),
|
||||
Map.entry("REJECTED", "审核驳回"), Map.entry("CONFIRMED", "已确认")));
|
||||
}
|
||||
private static String taxModeLabel(String value) {
|
||||
return label(value, Map.of("BUY_DECLARATION", "买单报关", "TAX_REFUND", "退税报关"));
|
||||
}
|
||||
private static String containerModeLabel(String value) {
|
||||
return label(value, Map.of("FULL_CONTAINER", "整柜", "FCL", "整柜", "LCL", "拼箱"));
|
||||
}
|
||||
private static String yesOrNo(Boolean value) { return value == null ? null : value ? "是" : "否"; }
|
||||
private static String label(String value, Map<String, String> labels) {
|
||||
if (value == null) return null;
|
||||
return labels.getOrDefault(value.trim().toUpperCase(), value);
|
||||
}
|
||||
private static ExportColumn column(String key, String title, ExportColumnType type, int width) {
|
||||
return new ExportColumn(key, title, type, width);
|
||||
}
|
||||
|
||||
@@ -527,10 +527,13 @@ public class MerchantBatchImportService {
|
||||
private static <T> Map<String, T> optionMap(List<T> values, Function<T, String> labeler) {
|
||||
Map<String, T> result = new LinkedHashMap<>();
|
||||
for (T value : values) {
|
||||
String label = labeler.apply(value);
|
||||
if (result.putIfAbsent(label, value) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "模板选项显示名称重复,请联系平台处理");
|
||||
String baseLabel = labeler.apply(value);
|
||||
String label = baseLabel;
|
||||
int duplicateNumber = 2;
|
||||
while (result.containsKey(label)) {
|
||||
label = baseLabel + "(同名选项 " + duplicateNumber++ + ")";
|
||||
}
|
||||
result.put(label, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+24
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchResponse;
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportColumnType;
|
||||
import com.baiyue.order.dto.UnifiedOrderQuery;
|
||||
import com.baiyue.order.dto.UnifiedOrderResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -130,6 +131,29 @@ class DomesticWarehouseOrderExportProviderServiceTest {
|
||||
.hasMessageContaining("does not match");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportsChineseDisplayValuesForStatusAndEnumColumns() {
|
||||
UnifiedOrderResponse.Row row = row("ORD-1", "RK-1", "客户甲");
|
||||
when(row.mainStatus()).thenReturn("PENDING_REVIEW");
|
||||
when(row.channelChanged()).thenReturn(false);
|
||||
when(row.feeReviewStatus()).thenReturn("NOT_SUBMITTED");
|
||||
when(row.taxMode()).thenReturn("BUY_DECLARATION");
|
||||
when(row.containerMode()).thenReturn("LCL");
|
||||
when(row.customsInspection()).thenReturn(false);
|
||||
when(orders.platformRowsByOrderNos(List.of("ORD-1"))).thenReturn(List.of(row));
|
||||
|
||||
ExportBatchResponse response = service.exportBatch(request("1009",
|
||||
Map.of("selectedOrderNos", List.of("ORD-1")), null, null, 100));
|
||||
|
||||
assertThat(response.rows().get(0)).containsEntry("mainStatus", "待审核")
|
||||
.containsEntry("channelChanged", "否").containsEntry("feeReviewStatus", "未提交")
|
||||
.containsEntry("taxMode", "买单报关").containsEntry("containerMode", "拼箱")
|
||||
.containsEntry("customsInspection", "否");
|
||||
assertThat(response.columns()).filteredOn(column -> column.key().equals("channelChanged")
|
||||
|| column.key().equals("customsInspection")).extracting(column -> column.type())
|
||||
.containsOnly(ExportColumnType.STRING);
|
||||
}
|
||||
|
||||
private ExportBatchRequest request(String taskId, Map<String, Object> parameters,
|
||||
String cursor, String snapshot, int limit) {
|
||||
return new ExportBatchRequest(taskId, "0", "PLATFORM", "7001", 1,
|
||||
|
||||
@@ -229,6 +229,26 @@ class MerchantBatchImportServiceTest {
|
||||
verify(warehouses).page("Bearer test", 2, 200, "ACTIVE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateKeepsDuplicateDisplayNamesAsDistinctBusinessOptions() throws Exception {
|
||||
var domestic = new MerchantWarehouseCatalogClient.Warehouse(21L, "CN-YW", "义乌收货仓",
|
||||
"DOMESTIC", "CN", "ACTIVE");
|
||||
var firstFba = new MerchantWarehouseCatalogClient.Warehouse(22L, "ATL6", "ATL6",
|
||||
"FBA", "US", "ACTIVE");
|
||||
var secondFba = new MerchantWarehouseCatalogClient.Warehouse(24L, "ATL6", "ATL6",
|
||||
"FBA", "US", "ACTIVE");
|
||||
when(warehouses.page("Bearer test", 1, 200, "ACTIVE"))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(domestic, firstFba, secondFba), 1, 200, 3, 1)));
|
||||
|
||||
byte[] content = service.template(88L, "Bearer test");
|
||||
|
||||
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
|
||||
var options = workbook.getSheet("选项数据");
|
||||
assertThat(hasCellValue(options, "ATL6")).isTrue();
|
||||
assertThat(hasCellValue(options, "ATL6(同名选项 2)")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void templateFailsClosedWhenLaterWarehousePageCannotBeRead() {
|
||||
List<MerchantWarehouseCatalogClient.Warehouse> firstPage = new ArrayList<>();
|
||||
|
||||
@@ -630,9 +630,10 @@ WHERE l.fba_warehouse_code IS NULL AND o.fba_warehouse_code IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_inbound_fba_appointment (
|
||||
id BIGINT NOT NULL COMMENT '主键ID',
|
||||
tenant_id VARCHAR(128) NOT NULL COMMENT '租户ID',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
appointment_no VARCHAR(64) NOT NULL COMMENT '预约单号',
|
||||
appointment_at DATETIME(3) NOT NULL COMMENT '预约时间',
|
||||
expected_loading_at DATETIME(3) NOT NULL COMMENT '预计装车时间',
|
||||
appointment_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '预约状态',
|
||||
appointment_remark VARCHAR(1000) NULL COMMENT '预约备注',
|
||||
fba_warehouse_code VARCHAR(128) NOT NULL COMMENT 'FBA仓库编码',
|
||||
@@ -641,8 +642,8 @@ CREATE TABLE IF NOT EXISTS ll_inbound_fba_appointment (
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_ll_fba_appointment_no (tenant_id, appointment_no),
|
||||
KEY idx_ll_fba_appointment_list (tenant_id, appointment_status, fba_warehouse_code, created_at)
|
||||
UNIQUE KEY uk_ll_fba_appointment_no (customer_id, appointment_no),
|
||||
KEY idx_ll_fba_appointment_list (customer_id, appointment_status, fba_warehouse_code, created_at)
|
||||
) ENGINE=InnoDB COMMENT='FBA入仓预约单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_inbound_fba_appointment_item (
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ public class WarehouseResourceService {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请刷新后重试");
|
||||
}
|
||||
if ("SCRAPPED".equals(existing.getStatus()) && !"SCRAPPED".equals(request.status())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "SCRAPPED pallet cannot be reused");
|
||||
throw new BizException(ResultCode.CONFLICT, "已报废的托盘不允许修改为其他状态");
|
||||
}
|
||||
String palletCode = StringUtils.hasText(request.palletCode()) ? request.palletCode().trim()
|
||||
: existing.getPalletCode();
|
||||
|
||||
+1
-1
@@ -332,7 +332,7 @@ class WarehouseResourceServiceTest {
|
||||
assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40002L,
|
||||
new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null, null, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("cannot be reused");
|
||||
.hasMessageContaining("已报废的托盘不允许修改为其他状态");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user