This commit is contained in:
2026-11-05 16:04:10 +08:00
parent 5a8918e2e4
commit d1ade7e9dd
22 changed files with 316 additions and 37 deletions
@@ -11,8 +11,8 @@ public interface DispatchWholePdaReadMapper {
record Picked(Long itemId,long packages) { }
record Invalid(Long dispatchId,long quantity) { }
record Carton(Long itemId,Long cartonId,String cartonMark,String status,long productQuantity) { }
/** 精确单号或有界阶段列表均先在SQL限制绑定仓。 */
@Select("<script>SELECT * FROM ll_dispatch_order WHERE overseas_warehouse_id=#{warehouse} AND status='ACTIVE' "
/** 绑定仓仅下发仓内作业;本土卡车与到仓取货排车继续在后台执行。 */
@Select("<script>SELECT * FROM ll_dispatch_order WHERE overseas_warehouse_id=#{warehouse} AND status='ACTIVE' AND COALESCE(task_type,'DELIVERY') NOT IN ('LOCAL_TRUCK','PICKUP') "
+"<choose><when test='number != null'>AND dispatch_no=#{number} </when><when test='stage != null'>AND stage=#{stage} </when>"
+"<otherwise>AND (stage IN('LOADING','AFTER_LOADING','DELIVERING') OR (stage='BEFORE_LOADING' AND review_state='LOADING_PENDING')) </otherwise></choose>ORDER BY updated_at DESC,id DESC LIMIT 201</script>")
List<DispatchAggregate> headers(@Param("warehouse")String warehouse,@Param("number")String number,@Param("stage")String stage);
@@ -962,8 +962,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
return batches.stream().collect(Collectors.toMap(
b -> List.of(b.batchKey(),b.customerId(),b.destinationKey()),b->b,(a,b)->a,LinkedHashMap::new)).values().stream().map(b -> {
var confirmed=confirmedAppointments.stream().filter(w->w.batchKey().equals(b.batchKey()) && w.customerId().equals(b.customerId()) && w.destinationKey().equals(b.destinationKey())).findFirst().orElse(null);
return new DispatchViews.FbaAppointment(b.batchKey(),b.batchNo(),confirmed==null?null:confirmed.confirmationNo(),b.destinationKey(),b.warehouseCode(),
confirmed==null?"PENDING":"SUCCESS",confirmed==null?null:confirmed.startAt(),confirmed==null?"尚无当前有效收货确认":null,b.customerId(),List.of());
return DispatchDestinationAppointments.booked(b,confirmed);
}).toList();
}
@@ -26,13 +26,13 @@ final class DispatchDestinationAppointments {
if (maintenance != null) for (var entry : maintenance.destinationAppointments()) entered.put(entry.destinationKey(),entry.appointmentNo());
var result = new ArrayList<FbaAppointment>();
destinations.forEach((key,code) -> {
var confirmed = booked.stream().filter(a -> key.equals(a.destinationKey()) && "SUCCESS".equals(a.status()) && a.appointmentNo() != null && !a.appointmentNo().isBlank()).toList();
var confirmed = booked.stream().filter(a -> key.equals(a.destinationKey()) && ("SUCCESS".equals(a.status()) || "REGISTERED".equals(a.status())) && a.appointmentNo() != null && !a.appointmentNo().isBlank()).toList();
var numbers = confirmed.stream().map(FbaAppointment::appointmentNo).distinct().toList();
// 草稿暂未填写不能遮挡后来取得的唯一正式确认号,已填写值仍保留。
var enteredNumber = entered.get(key);
var number = enteredNumber != null ? enteredNumber : numbers.size() == 1 ? numbers.get(0) : null;
var matching = confirmed.stream().filter(a -> Objects.equals(number,a.appointmentNo())).toList();
var at = matching.stream().map(FbaAppointment::confirmedAt).filter(Objects::nonNull).min(Comparator.naturalOrder()).orElse(null);
var at = matching.stream().filter(a -> "SUCCESS".equals(a.status())).map(FbaAppointment::confirmedAt).filter(Objects::nonNull).min(Comparator.naturalOrder()).orElse(null);
var shipments = cargoShipments.stream().filter(a -> key.equals(a.destinationKey())).map(com.baiyue.lastleg.dispatch.mapper.DispatchReadMapper.CargoShipment::shipmentNo).filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()).distinct().sorted().toList();
result.add(new FbaAppointment(null,null,number,key,code,number == null ? "PENDING" : at == null ? "REGISTERED" : "SUCCESS",at,
number == null ? "请填写目的仓 " + code + " 的预约号" : null,null,shipments));
@@ -40,6 +40,15 @@ final class DispatchDestinationAppointments {
return List.copyOf(result);
}
/** 已约批次可回显已登记编号;只有当前有效收货确认才能提供审核用确认时间。 */
static FbaAppointment booked(com.baiyue.lastleg.dispatch.mapper.DispatchReadMapper.BatchRow batch, ReceivingWindow confirmed) {
var registered = "SUCCESS".equals(batch.status()) && batch.confirmationNo() != null && !batch.confirmationNo().isBlank();
var number = confirmed != null ? confirmed.confirmationNo() : registered ? batch.confirmationNo() : null;
return new FbaAppointment(batch.batchKey(),batch.batchNo(),number,batch.destinationKey(),batch.warehouseCode(),
confirmed != null ? "SUCCESS" : number != null ? "REGISTERED" : "PENDING",confirmed == null ? null : confirmed.startAt(),
confirmed == null ? "尚无当前有效收货确认" : null,batch.customerId(),List.of());
}
/** 保存允许暂缺编号,但不能把别的目的仓写入本车。 */
static void validate(Maintenance maintenance, List<FbaAppointment> destinations) {
if (maintenance == null) return;
@@ -26,7 +26,7 @@ public class DispatchPdaQueryService {
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
public DispatchViews.PdaTask task(Actor actor,String number) {
require(actor);if(number==null || number.isBlank())throw new BizException(ResultCode.BAD_REQUEST,"请输入排车单号");
var rows=repository.headers(actor.boundWarehouseId(),number,null);if(rows.size()!=1)throw new BizException(ResultCode.NOT_FOUND,"当前绑定仓无此排车作业");return assemble(actor,rows).get(0);
var rows=repository.headers(actor.boundWarehouseId(),number,null);if(rows.size()!=1)throw new BizException(ResultCode.NOT_FOUND,"当前绑定仓无此排车作业");var tasks=assemble(actor,rows);if(tasks.isEmpty())throw new BizException(ResultCode.NOT_FOUND,"当前绑定仓无此排车作业");return tasks.get(0);
}
/** 按收货已确认的实托批量读取;未交接的原PLAN不推算实收仓位或箱数。 */
private List<DispatchViews.PdaTask> assemble(Actor actor,List<DispatchAggregate> roots) {
@@ -42,6 +42,8 @@ public class DispatchPdaQueryService {
var consumed=hasRelabel?sourceOrigins.pickedAll(ids).stream().collect(Collectors.toMap(p->p.sourceId(),p->p.packages())):Map.<Long,Long>of();
for(var root:roots) {
var remote=workflow.remoteItemIds(root);var items=cargo.getOrDefault(root.getId(),List.of()).stream().filter(i->!remote.contains(i.getId())).toList();var detail=new ArrayList<DispatchViews.PdaItem>();
// 纯异地取货没有仓内拣货,不向PDA推送空任务。
if(items.isEmpty())continue;
boolean sourceValid=true;var expectedHold=new HashMap<String,Long>();
for(var source:frozen)if(Objects.equals(source.getDispatchOrderId(),root.getId())) {
long used=consumed.getOrDefault(source.getId(),0L),left=n(source.getPackageCount())-used;
@@ -15,7 +15,7 @@ public interface InboundPlanQueryMapper {
String SCHEDULED_DISPATCH = "EXISTS (SELECT 1 FROM ll_dispatch_item di" +
" JOIN ll_dispatch_order dispatch ON dispatch.id=di.dispatch_order_id" +
" WHERE di.customer_id=p.customer_id AND di.order_id=l.order_id" +
" AND dispatch.status&lt;&gt;'CANCELLED')";
" AND dispatch.status&lt;&gt;'CANCELLED' AND COALESCE(dispatch.task_type,'DELIVERY') NOT IN ('PICKUP','LOCAL_TRUCK'))";
String FROM_AND_FILTER = " FROM ll_inbound_plan_line l" +
" JOIN ll_inbound_plan p ON p.id=l.plan_id" +
@@ -51,14 +51,15 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
String externalBookingNo, boolean poolSource) { }
// Manual node snapshots replace container nodes permanently for the order.
String SAILING_STAGE_FILTER_SQL = "(ll_order.order_type='FIRST_LEG' AND " +
String SAILING_STAGE_VALUE_SQL = "(ll_order.order_type='FIRST_LEG' AND " +
"(CASE WHEN COALESCE(ll_order.manual_node_override,FALSE)=TRUE THEN " +
"CASE WHEN EXISTS (SELECT 1 FROM ll_order_manual_node n WHERE n.order_id=ll_order.id " +
"AND n.customer_id=ll_order.customer_id AND n.node_code='ACTUAL_DEPARTURE' AND n.node_at IS NOT NULL) " +
"THEN 1 ELSE 0 END ELSE " +
"CASE WHEN EXISTS (SELECT 1 FROM ll_container_shipping_gate g " +
"WHERE g.container_booking_id=ll_order.source_container_booking_id " +
"AND g.node_sequence>=40 AND g.actual_at IS NOT NULL) THEN 1 ELSE 0 END END)={0})";
"AND g.node_sequence>=40 AND g.actual_at IS NOT NULL) THEN 1 ELSE 0 END END)";
String SAILING_STAGE_FILTER_SQL = SAILING_STAGE_VALUE_SQL + "={0})";
@Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity, " +
"CASE WHEN NOT EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=r.id AND ri.operation_type&lt;&gt;'CARTON_LABEL') THEN '箱' ELSE '件' END AS quantity_unit,r.created_at " +
@@ -71,11 +72,18 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
@Select("<script>SELECT o.id AS orderId,CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di " +
"JOIN ll_dispatch_order d ON d.id=di.dispatch_order_id " +
"WHERE di.customer_id=o.customer_id AND di.order_id=o.id AND d.status!='CANCELLED') " +
"WHERE di.customer_id=o.customer_id AND di.order_id=o.id AND d.status!='CANCELLED' AND COALESCE(d.task_type,'DELIVERY') NOT IN ('PICKUP','LOCAL_TRUCK')) " +
"THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END AS dispatchStatus FROM ll_order o WHERE o.id IN " +
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
List<DispatchStatusRow> findDispatchStatuses(@Param("orderIds") List<Long> orderIds);
/** 已开船来源键用于聚合前去重,保持客户隔离和人工节点覆盖规则。 */
@Select("<script>SELECT DISTINCT customer_id,source_order_no FROM ll_order WHERE source_order_no IS NOT NULL " +
"<if test='customer != null'>AND customer_id=#{customer} </if>AND " +
SAILING_STAGE_VALUE_SQL + "=1)" + "</script>")
List<DepartedSource> departedSources(@Param("customer") String customer);
record DepartedSource(String customerId, String sourceOrderNo) { }
record DispatchStatusRow(Long orderId, String dispatchStatus) { }
record OrderWeightRow(Long orderId, BigDecimal grossWeightKg, BigDecimal netWeightKg) { }
@@ -0,0 +1,19 @@
package com.baiyue.lastleg.repository;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import org.springframework.stereotype.Repository;
import java.util.Set;
import java.util.stream.Collectors;
/** 尾程实际船节点优先于头程摘要中缺失的开船时间。 */
@Repository
public class PreDepartureOrderRepository {
private final LastlegOrderMapper mapper;
/** 仅读本服务尾程事实,不读取订单服务数据库。 */
public PreDepartureOrderRepository(LastlegOrderMapper mapper) { this.mapper = mapper; }
/** 按客户与来源订单组成去重键,在分页前排除已开船记录。 */
public Set<String> departedKeys(String customer) {
return mapper.departedSources(customer).stream()
.map(row -> row.customerId() + ":" + row.sourceOrderNo()).collect(Collectors.toSet());
}
}
@@ -93,6 +93,7 @@ public class LastlegOrderService {
@Resource private ReturnOrderReferenceResolver returnOrderReferences;
@Resource private com.baiyue.lastleg.transfer.service.TransferQuantityService transferQuantities;
@Resource private OrderPreDepartureClient orderPreDepartureClient;
@Resource private com.baiyue.lastleg.repository.PreDepartureOrderRepository preDepartureOrders;
@Value("${baiyue.security.internal-service-token:}") private String internalServiceToken;
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
@@ -101,12 +102,7 @@ public class LastlegOrderService {
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query, String relabelNo) {
if (query.sailingStage() == LastlegOrderRequests.SailingStage.BEFORE_DEPARTURE) {
Long customer = parseLong(customerId);
var response = orderPreDepartureClient.page(internalServiceToken, customer, query.orderNo(), query.inboundCode(),
query.page().intValue(), query.size().intValue());
if (response == null || response.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "订单服务暂时不可用");
List<LastlegOrderSummary> rows = response.data().records().stream().map(this::preDepartureSummary).toList();
return new PageResult<>(rows, response.data().page(), response.data().size(), response.data().total(), response.data().pages());
return preDeparturePage(customerId, query);
}
if (query.sailingStage() == null && relabelNo == null) return aggregateAllPage(customerId, query);
Page<LastlegOrder> page = orderMapper.selectPage(
@@ -115,6 +111,27 @@ public class LastlegOrderService {
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
}
/** 开船前先扣除尾程已有实际开船事实,再分页,避免两页签重复及总数失真。 */
private PageResult<LastlegOrderSummary> preDeparturePage(String customerId, LastlegOrderRequests.Query query) {
var departed = preDepartureOrders.departedKeys(customerId);
var records = new java.util.ArrayList<LastlegOrderSummary>();
for (int sourcePage = 1; ; sourcePage++) {
var response = orderPreDepartureClient.page(internalServiceToken, parseLong(customerId),
query.orderNo(), query.inboundCode(), sourcePage, 2000);
if (response == null || !response.successful() || response.data() == null)
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "订单服务暂时不可用");
response.data().records().stream()
.filter(row -> !departed.contains(orderKey(String.valueOf(row.customerId()), row.orderNo())))
.map(this::preDepartureSummary).forEach(records::add);
if (sourcePage >= response.data().pages()) break;
}
int from = (int)Math.min((query.page() - 1) * query.size(), records.size());
int to = (int)Math.min(from + query.size(), records.size());
long total = records.size();
return new PageResult<>(records.subList(from, to), query.page(), query.size(), total,
total == 0 ? 0 : (total + query.size() - 1) / query.size());
}
/** 全部及已开船列表使用同一组业务事实,避免聚合列表丢失并托、排车和换标信息。 */
private List<LastlegOrderSummary> nativeSummaries(List<LastlegOrder> orders) {
Map<Long, List<LastlegOrderMapper.PalletMergeRelationRow>> mergeRelations = palletMergeRelations(orders);
@@ -9,6 +9,23 @@ import static org.assertj.core.api.Assertions.*;
/** 本车同目的仓跨客户只填一次,先约自动带号,先排可暂缺。 */
class DispatchDestinationAppointmentsTest {
/** 已预约的编号不能因尚无有效收货确认而丢失,也不能冒充确认时间。 */
@Test void registeredBatchNumberIsVisibleWithoutInventingReceivingConfirmation() {
var batch=new com.baiyue.lastleg.dispatch.mapper.DispatchReadMapper.BatchRow(1L,2L,"FBA_APPOINTMENT:1","AP-1","A","SOURCE","FBA:ONT8",22,"ISA-BOOKED","SUCCESS","ONT8",java.time.LocalDateTime.parse("2026-11-05T10:00:00"),1L);
var booked=DispatchDestinationAppointments.booked(batch,null);
assertThat(booked.status()).isEqualTo("REGISTERED");
assertThat(booked.confirmedAt()).isNull();
var rows=resolve(List.of(item("A","ONT8")),maintenance(new DestinationAppointment("FBA:ONT8",null)),List.of(booked));
assertThat(rows).singleElement().satisfies(a->{
assertThat(a.appointmentNo()).isEqualTo("ISA-BOOKED");
assertThat(a.status()).isEqualTo("REGISTERED");assertThat(a.confirmedAt()).isNull();
});
var window=new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.ReceivingWindow("FBA_APPOINTMENT:1","AP-1","A","FBA:ONT8","ISA-CURRENT",java.time.Instant.parse("2026-11-06T10:00:00Z"),null,"AG-1",1);
var confirmed=DispatchDestinationAppointments.booked(batch,window);
assertThat(confirmed.appointmentNo()).isEqualTo("ISA-CURRENT");
assertThat(confirmed.confirmedAt()).isEqualTo(window.startAt());
assertThat(confirmed.status()).isEqualTo("SUCCESS");
}
@Test void groupsMultipleCustomersAndPalletsByDestination() {
var rows=resolve(List.of(item("A","ONT8"),item("B","ONT8"),item("A","LGB8")),null,List.of());
assertThat(rows).extracting(FbaAppointment::destinationWarehouseCode).containsExactly("LGB8","ONT8");
@@ -62,6 +62,13 @@ class DispatchPdaReadPersistenceTest {
assertThat(task.items()).allSatisfy(item->assertThat(item.positions()).hasSize(1));
assertThatThrownBy(()->f.tx.execute(s->queries.task(new DispatchContracts.Actor("foreign",ActorMode.PDA,"99"),created.dispatchNo())))
.isInstanceOf(com.baiyue.common.core.exception.BizException.class).hasMessageContaining("当前绑定仓无此排车作业");
for (String type : List.of("LOCAL_TRUCK", "PICKUP")) {
f.db.update("UPDATE ll_dispatch_order SET task_type=?", type);
assertThat(f.tx.<List<DispatchViews.PdaTask>>execute(s->queries.list(actor,null))).isEmpty();
assertThatThrownBy(()->f.tx.execute(s->queries.task(actor,created.dispatchNo())))
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
}
f.db.update("UPDATE ll_dispatch_order SET task_type='DELIVERY'");
f.db.update("DELETE FROM ll_dispatch_pda_pick_order WHERE customer_id='202'");
assertThat(f.tx.execute(s->queries.task(actor,created.dispatchNo())).pickingReconciled()).isFalse();
}
@@ -32,7 +32,7 @@ class InboundPlanDispatchStatusPersistenceTest {
db.execute("CREATE TABLE ll_order_detail(order_id BIGINT,customer_id VARCHAR(20),gross_weight_kg DECIMAL(18,3),net_weight_kg DECIMAL(18,3))");
db.execute("CREATE TABLE ll_inbound_order(id BIGINT,customer_id VARCHAR(20),gross_weight_kg DECIMAL(18,3),net_weight_kg DECIMAL(18,3))");
db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT,order_id BIGINT)");
db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,status VARCHAR(30),stage VARCHAR(30))");
db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,status VARCHAR(30),stage VARCHAR(30),task_type VARCHAR(30) DEFAULT 'DELIVERY')");
db.update("INSERT INTO ll_inbound_plan VALUES(1,'LIP-1','A',90,TRUE,FALSE,FALSE,'RECEIVED',CURRENT_TIMESTAMP,0)");
db.update("INSERT INTO ll_inbound_plan_line VALUES(2,1,3,'IN-1','91','ABE4','ABE4','ABE4',1,1,1)");
db.update("INSERT INTO ll_order VALUES(3,'A','LX-1','FIRST_LEG','LA','洛杉矶仓','客户A')");
@@ -64,6 +64,15 @@ class InboundPlanDispatchStatusPersistenceTest {
assertStatus("UNSCHEDULED");
}
@Test
void pickupTripDoesNotCountAsOutboundScheduling() {
dispatch(10, "ACTIVE", "DELIVERED", "A");
db.update("UPDATE ll_dispatch_order SET task_type='PICKUP'");
assertStatus("UNSCHEDULED");
dispatch(11, "ACTIVE", "BEFORE_LOADING", "A");
assertStatus("SCHEDULED");
}
@Test
void departedAndCompletedDispatchesRemainScheduled() {
dispatch(10, "ACTIVE", "DELIVERING", "A");
@@ -110,7 +119,7 @@ class InboundPlanDispatchStatusPersistenceTest {
}
private void dispatch(long id, String status, String stage, String itemOwner) {
db.update("INSERT INTO ll_dispatch_order VALUES(?,?,?)", id, status, stage);
db.update("INSERT INTO ll_dispatch_order(id,status,stage) VALUES(?,?,?)", id, status, stage);
db.update("INSERT INTO ll_dispatch_item VALUES(?,?,?,3)", id, itemOwner, id);
}
@@ -53,6 +53,18 @@ class LastlegOrderSailingStageTest {
assertEquals(List.of(3L, 4L, 7L, 8L, 9L), find(db, null, SailingStage.AFTER_DEPARTURE, null));
assertEquals(List.of(3L, 4L, 7L, 9L), find(db, "A", SailingStage.AFTER_DEPARTURE, null));
assertEquals(List.of(3L, 7L, 9L), find(db, "A", SailingStage.AFTER_DEPARTURE, "PENDING_ASSIGNMENT"));
// 去重仓储执行真实Mapper,沿用客户隔离和人工覆盖后的实际开船事实。
sql.execute("ALTER TABLE ll_order ADD source_order_no VARCHAR(64)");
sql.execute("UPDATE ll_order SET source_order_no=CONCAT('SOURCE-',id)");
var config = new MybatisConfiguration(); config.setMapUnderscoreToCamelCase(true);
config.addMapper(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);
var factory = new com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean();
factory.setDataSource(new org.springframework.jdbc.datasource.DriverManagerDataSource("jdbc:h2:mem:sailing;MODE=MySQL", "", ""));
factory.setConfiguration(config);
var mapper = new org.mybatis.spring.SqlSessionTemplate(factory.getObject()).getMapper(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);
assertEquals(java.util.Set.of("A:SOURCE-3", "A:SOURCE-4", "A:SOURCE-7", "A:SOURCE-9"),
new com.baiyue.lastleg.repository.PreDepartureOrderRepository(mapper).departedKeys("A"));
assertEquals(java.util.stream.LongStream.rangeClosed(1, 14).boxed().toList(), find(db, null, null, null)); // Discard applications have their own workflow and are excluded.
}
}
@@ -79,6 +79,7 @@ class LastlegOrderServiceTest {
@Mock WarehouseResourceGateway warehouseResourceGateway;
@Mock ContainerShippingGateMapper containerShippingGates;
@Mock OrderPreDepartureClient preDeparture;
@Mock com.baiyue.lastleg.repository.PreDepartureOrderRepository preDepartureOrders;
@Mock ReturnOrderReferenceResolver returnOrderReferences;
@Mock com.baiyue.lastleg.returnreceipt.service.ReturnReceiptService returnReceipts;
LastlegOrderService service;
@@ -118,6 +119,27 @@ class LastlegOrderServiceTest {
assertEquals(new java.math.BigDecimal("22"), summary.netWeightKg());
}
/** 已清关来源即使订单服务仍返回开船前记录,也应先去重再分页。 */
@Test void excludesDepartedSourcesBeforePagination() {
when(preDepartureOrders.departedKeys("101")).thenReturn(java.util.Set.of("101:EXXHK2611050001"));
var departed = preDepartureRow(1L, "EXXHK2611050001");
var waiting = preDepartureRow(2L, "WAITING-2");
when(preDeparture.page(any(), eq(101L), any(), any(), eq(1), eq(2000)))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(departed, waiting), 1, 2000, 2, 1)));
var query = new LastlegOrderRequests.Query(1L, 1L, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
LastlegOrderRequests.SailingStage.BEFORE_DEPARTURE, null, null, null);
var page = service.page("101", query);
assertEquals(1L, page.total());
assertEquals(1L, page.pages());
assertEquals("WAITING-2", page.records().get(0).sourceOrderNo());
}
private OrderPreDepartureClient.Row preDepartureRow(long id, String no) {
return new OrderPreDepartureClient.Row(id,101L,no,"IN-"+id,"WAIT_SPLIT", "客户",null,null,null,
null,2,null,null,null,null,null,null,null,null,null,null);
}
@BeforeEach
void setUp() {
com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo(
@@ -144,6 +166,7 @@ class LastlegOrderServiceTest {
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
ReflectionTestUtils.setField(service, "containerShippingGates", containerShippingGates);
ReflectionTestUtils.setField(service, "orderPreDepartureClient", preDeparture);
ReflectionTestUtils.setField(service, "preDepartureOrders", preDepartureOrders);
lenient().when(preDeparture.page(any(), any(), any(), any(), anyInt(), anyInt()))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 1, 2000, 0, 0)));
}