diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java index 9be19ed6f..e1a1b37a2 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java @@ -37,7 +37,10 @@ public final class LastlegOrderRequests { Boolean detained, LocalDateTime createdFrom, LocalDateTime createdTo, - SailingStage sailingStage + SailingStage sailingStage, + @Size(max = 64) String businessType, + @Size(max = 128) String businessOrderNo, + @Size(max = 128) String externalBookingNo ) { public Query { if (page == null || page == 0) page = 1L; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java index 5d7ef8417..d640aad0b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java @@ -74,6 +74,9 @@ public record LastlegOrderSummary( LocalDateTime createdAt, LocalDateTime updatedAt, @Schema(description = "同一主单关联的全部换标子单及各自状态,按创建时间排序") - List relabelApplications + List relabelApplications, + BusinessSource businessSource ) { + @Schema(description = "从正式单据关联读取的商业来源;与履约类型 orderType 独立") + public record BusinessSource(String businessType, String label, String businessOrderNo, String externalBookingNo) { } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportColumns.java b/lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportColumns.java index 22ae6a31f..a70411217 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportColumns.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportColumns.java @@ -10,7 +10,7 @@ import java.util.Arrays; final class LastlegExportColumns { private LastlegExportColumns() { } private static final Map DEFINITIONS = Map.ofEntries( - Map.entry("LAST_MILE_ORDER_LIST", "orderNo:尾程单号,orderTypeName:订单类型,inboundCode:入仓码,sourceOrderNo:头程单号,customerName:货主,overseasWarehouseName:海外仓,transferWarehouseName:转仓,transferWarehouseCode:转仓编码,fbaWarehouseCode:FBA仓库,containerNo:集装箱号,containerNode:货柜节点,abnormal:是否异常,detained:是否暂扣,palletCount:托数:INTEGER,packageCount:客户预报箱数:INTEGER,receivedPackageCount:实收箱数:INTEGER,packageDifference:差异箱数:INTEGER,volumeCbm:体积(m³):DECIMAL,productQuantity:数量:INTEGER,volumeWeightKg:体积重量:DECIMAL,estimatedArrivalAt:预计到港时间:DATETIME,actualArrivalAt:实际到港时间:DATETIME,estimatedWarehouseAt:预计到仓:DATETIME,inboundOrderNo:入库单号,relabelOrderNo:换标单号,relabelStatus:换标状态,palletMergeFlag:合托标记,currentStatus:主流程状态,subOrderStatus:子单状态,privateRemark:备注(仅自己),customerRemark:备注(客户),warehouseRemark:备注(仓库)"), + Map.entry("LAST_MILE_ORDER_LIST", "orderNo:尾程单号,orderTypeName:履约类型,businessSource.label:业务来源,businessSource.businessOrderNo:关联商业单号,businessSource.externalBookingNo:外接预订号,inboundCode:入仓码,sourceOrderNo:来源订单号,customerName:货主,overseasWarehouseName:海外仓,transferWarehouseName:转仓,transferWarehouseCode:转仓编码,fbaWarehouseCode:FBA仓库,containerNo:集装箱号,containerNode:货柜节点,abnormal:是否异常,detained:是否暂扣,palletCount:托数:INTEGER,packageCount:客户预报箱数:INTEGER,receivedPackageCount:实收箱数:INTEGER,packageDifference:差异箱数:INTEGER,volumeCbm:体积(m³):DECIMAL,productQuantity:数量:INTEGER,volumeWeightKg:体积重量:DECIMAL,estimatedArrivalAt:预计到港时间:DATETIME,actualArrivalAt:实际到港时间:DATETIME,estimatedWarehouseAt:预计到仓:DATETIME,inboundOrderNo:入库单号,relabelOrderNo:换标单号,relabelStatus:换标状态,palletMergeFlag:合托标记,currentStatus:主流程状态,subOrderStatus:子单状态,privateRemark:备注(仅自己),customerRemark:备注(客户),warehouseRemark:备注(仓库)"), Map.entry("OVERSEAS_INVENTORY_BALANCE", "customerName:客户,overseasWarehouseCode:海外仓编码,overseasWarehouseName:海外仓,destinationWarehouseCode:目的仓编码,destinationWarehouseName:目的仓,inboundOrderNo:入库单号,inboundCode:入仓码,skuCode:SKU,productNameZh:中文品名,productNameEn:英文品名,totalPackages:总箱数:INTEGER,availablePackages:可用箱数:INTEGER,holdPackages:暂扣箱数:INTEGER,allocatedPackages:已分配箱数:INTEGER,pickedPackages:已拣箱数:INTEGER,inTransitPackages:在途箱数:INTEGER,receivingPackages:收货中箱数:INTEGER,procurementPackages:采购箱数:INTEGER"), Map.entry("OVERSEAS_INVENTORY_LEDGER", "occurredAt:发生时间:DATETIME,customerName:客户,destinationWarehouseCode:目的仓编码,inboundOrderNo:入库单号,inboundCode:入仓码,businessType:业务类型,businessOrderNo:业务单号,outboundOrderNo:出库单号,palletNo:托盘号,batchNo:批次号,statusFrom:原状态,statusTo:新状态,beforePackages:变动前箱数:INTEGER,deltaPackages:变动箱数:INTEGER,afterPackages:变动后箱数:INTEGER,remark:备注"), Map.entry("OVERSEAS_QUOTE_LIST", "name:报价名称,warehouseCode:合作仓编码,warehouseName:合作仓名称,providerName:报价方,currency:币种,effectiveStart:生效开始:DATE,effectiveEnd:生效结束:DATE,versionCode:版本号,status:状态,updatedAt:更新时间:DATETIME"), diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java index e41a99b4f..eb19f1692 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java @@ -16,6 +16,25 @@ import java.math.BigDecimal; @Mapper public interface LastlegOrderMapper extends BaseMapper { + // Correlate both order identity and customer; never infer a business source from remarks. + String POOL_SOURCE_WHERE = " FROM ll_fleet_pool_order_link l WHERE l.lastleg_order_id=ll_order.id " + + "AND l.customer_id=ll_order.customer_id AND ll_order.order_type='WAREHOUSE_TRANSFER'"; + String BUSINESS_TYPE_SQL = "COALESCE((SELECT l.business_type" + POOL_SOURCE_WHERE + "),ll_order.order_type)"; + String BUSINESS_ORDER_NO_SQL = "COALESCE((SELECT l.business_order_no" + POOL_SOURCE_WHERE + + "),CASE WHEN ll_order.order_type='LOCAL_LOOSEBOARD' THEN ll_order.source_order_no END)"; + String EXTERNAL_BOOKING_NO_SQL = "(SELECT b.booking_no FROM ll_fleet_external_booking b " + + "WHERE b.customer_id=ll_order.customer_id AND b.incoming_no=ll_order.source_order_no " + + "AND ll_order.order_type='WAREHOUSE_TRANSFER')"; + + @Select("") + List findBusinessSources(@Param("orderIds") List orderIds); + + record BusinessSourceRow(Long orderId, String businessType, String businessOrderNo, + 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 " + "(CASE WHEN COALESCE(ll_order.manual_node_override,FALSE)=TRUE THEN " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java index a2e1807ff..973f30864 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java @@ -100,11 +100,12 @@ public class LastlegOrderService { Map dispatchStatuses = dispatchStatuses(page.getRecords()); Map weights = orderWeights(page.getRecords()); Map> relabels = relabelSubOrders(page.getRecords()); + Map businessSources = businessSources(page.getRecords()); return new PageResult<>(page.getRecords().stream().map(order -> summary(order, mergeRelations.get(order.getId()), StringUtils.hasText(order.getSourceContainerBookingId()) ? containerNodes.get(order.getSourceContainerBookingId()) : null, dispatchStatuses.getOrDefault(order.getId(), "UNSCHEDULED"), weights.get(order.getId()), - relabels.getOrDefault(order.getId(), List.of()))).toList(), + relabels.getOrDefault(order.getId(), List.of()), businessSources.get(order.getId()))).toList(), page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); } @@ -360,7 +361,8 @@ public class LastlegOrderService { .sorted(java.util.Comparator.comparingInt(value -> MANUAL_NODE_CODES.indexOf(value.nodeCode()))).toList(); return new LastlegOrderDetailResponse(summary(order, palletMergeRelations(List.of(order)).get(order.getId()), containerNode(order), dispatchStatuses(List.of(order)).getOrDefault(order.getId(), "UNSCHEDULED"), - orderWeights(List.of(order)).get(order.getId()), relabelSubOrders(List.of(order)).getOrDefault(order.getId(), List.of())), manualNodes, cargo, history.stream() + orderWeights(List.of(order)).get(order.getId()), relabelSubOrders(List.of(order)).getOrDefault(order.getId(), List.of()), + businessSources(List.of(order)).get(order.getId())), manualNodes, cargo, history.stream() .map(value -> new LastlegOrderDetailResponse.History(value.getId(), value.getFromStatus(), value.getToStatus(), value.getRemark(), value.getCreatedAt())).toList(), fees, operations.stream() .map(value -> new LastlegOrderDetailResponse.Operation(value.getId(), value.getOperationNode(), @@ -549,6 +551,9 @@ public class LastlegOrderService { .or().apply("EXISTS (SELECT 1 FROM ll_relabel_application r WHERE r.lastleg_order_id=ll_order.id " + "AND r.customer_id=ll_order.customer_id AND r.relabel_no LIKE CONCAT('%',{0},'%'))", relabelNo)) .eq(StringUtils.hasText(query.orderType()), LastlegOrder::getOrderType, query.orderType()) + .apply(StringUtils.hasText(query.businessType()), LastlegOrderMapper.BUSINESS_TYPE_SQL + "={0}", query.businessType()) + .apply(StringUtils.hasText(query.businessOrderNo()), LastlegOrderMapper.BUSINESS_ORDER_NO_SQL + " LIKE CONCAT('%',{0},'%')", query.businessOrderNo()) + .apply(StringUtils.hasText(query.externalBookingNo()), LastlegOrderMapper.EXTERNAL_BOOKING_NO_SQL + " LIKE CONCAT('%',{0},'%')", query.externalBookingNo()) .apply(query.sailingStage() != null, LastlegOrderMapper.SAILING_STAGE_FILTER_SQL, query.sailingStage() == LastlegOrderRequests.SailingStage.AFTER_DEPARTURE ? 1 : 0) .like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode()) @@ -623,10 +628,23 @@ public class LastlegOrderService { return rows == null ? Map.of() : rows.stream().collect(Collectors.groupingBy(com.baiyue.lastleg.dto.RelabelSubOrder::lastlegOrderId)); } + private Map businessSources(List orders) { + if (orders.isEmpty()) return Map.of(); + return orderMapper.findBusinessSources(orders.stream().map(LastlegOrder::getId).toList()).stream() + .collect(Collectors.toMap(LastlegOrderMapper.BusinessSourceRow::orderId, row -> { + String label = "US_TRUCK".equals(row.businessType()) ? "本土卡车" : LastlegOrderType.labelOf(row.businessType()); + if (row.poolSource()) label += " · 拼车需求池"; + else if ("LOCAL_LOOSEBOARD".equals(row.businessType())) label += " · 独立下单"; + else if (StringUtils.hasText(row.externalBookingNo())) label += " · 外接来货"; + return new LastlegOrderSummary.BusinessSource(row.businessType(), label, row.businessOrderNo(), row.externalBookingNo()); + })); + } + private LastlegOrderSummary summary(LastlegOrder o, List relations, String containerNode, String dispatchStatus, LastlegOrderMapper.OrderWeightRow weight, - List relabelApplications) { + List relabelApplications, + LastlegOrderSummary.BusinessSource businessSource) { List related = relations == null ? List.of() : relations; return new LastlegOrderSummary(o.getId(), displayOrderNo(o), o.getOrderType(), LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(), @@ -644,7 +662,7 @@ public class LastlegOrderService { o.getPalletMergeFlag(), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::mergeGroupNo).distinct().findFirst().orElse(null), related.stream().map(LastlegOrderMapper.PalletMergeRelationRow::relatedInboundNo).filter(Objects::nonNull).distinct().toList(), o.getCurrentStatus(), o.getSubOrderStatus(), o.getPrivateRemark(), - o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt(), relabelApplications); + o.getCustomerRemark(), o.getWarehouseRemark(), o.getCreatedAt(), o.getUpdatedAt(), relabelApplications, businessSource); } private static Integer packageDifference(LastlegOrder order) { diff --git a/lastleg-service/src/main/resources/openapi/lastleg-exports-v1.yaml b/lastleg-service/src/main/resources/openapi/lastleg-exports-v1.yaml index dba8fc6c2..47152332a 100644 --- a/lastleg-service/src/main/resources/openapi/lastleg-exports-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/lastleg-exports-v1.yaml @@ -6,7 +6,7 @@ info: Admin 导出统一通过 POST /api/platform/v1/files/export-tasks 创建 XLSX 任务。 File 按导出类型校验原业务 export 权限,通过内部令牌分批读取 Lastleg;Provider 不经公网 Gateway。 parameters 与对应业务列表筛选一致,不包含 page/size;进销存支持 customerName。 - 尾程订单支持 relabelNo 和 sailingStage,库存余额按来源入库单展开,不导出内部数据库 ID。 + 尾程订单支持 relabelNo、sailingStage、businessType、businessOrderNo、externalBookingNo;导出履约类型、业务来源、关联商业单号和外接预订号,与列表同一投影。库存余额按来源入库单展开;所有导出均不包含内部数据库 ID。 每批最多 200 行,总行数上限 200000;车队报价及出车记录保留业务查询的 5000 条限制。 snapshotToken 绑定任务、申请人、执行版本和筛选条件,不表示数据库历史快照。 TRIP_OPERATIONS_LIST 的 costVisible 仅由 File 根据申请人的 platform:fleet:trip:list 权限写入,不能信任客户端值。 diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 99eb066fc..4d3dbde60 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -211,9 +211,12 @@ paths: /api/platform/v1/last-leg/orders: get: summary: 分页查询尾程订单 - description: 权限 platform:overseas-warehouse:last-mile-order:list;平台管理员省略 customerId 时查询全部客户。containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。 + description: 权限 platform:overseas-warehouse:last-mile-order:list;平台管理员省略 customerId 时查询全部客户。containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。businessSource 返回 businessType、label、businessOrderNo、externalBookingNo;商业类型优先取同客户同尾程单的池来源桥,独立散板取自身来源单号,普通转运不推断为散板。orderType 保留履约类型;无关联单号为空。 security: [{ bearerAuth: [] }] parameters: + - { name: businessType, in: query, description: 商业类型;本土散板同时覆盖独立和池来源,仓储转运排除已关联散板或卡车商业单, schema: { type: string, maxLength: 64 } } + - { name: businessOrderNo, in: query, description: 正式关联商业单号包含匹配,不解析备注, schema: { type: string, maxLength: 128 } } + - { name: externalBookingNo, in: query, description: 正式外接预订号包含匹配,不解析备注, schema: { type: string, maxLength: 128 } } - { name: sailingStage, in: query, description: '可选开船阶段。仅匹配头程订单类型 FIRST_LEG;未传不限制类型。按实际柜级节点序号>=40判断开船后;人工节点覆盖后仅按该票 ACTUAL_DEPARTURE 非空判断,不回退柜级。无实际开船事实归开船前。', schema: { type: string, enum: [BEFORE_DEPARTURE, AFTER_DEPARTURE] } } - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } - { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } } @@ -233,7 +236,7 @@ paths: /api/platform/v1/last-leg/orders/{id}: get: summary: 查询尾程订单详情 - description: 权限 platform:overseas-warehouse:last-mile-order:list;返回订单、货物、状态历史、费用明细和操作记录。订单中的 containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。 + description: 权限 platform:overseas-warehouse:last-mile-order:list;返回订单、货物、状态历史、费用明细和操作记录。订单中的 containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。businessSource 返回 businessType、label、businessOrderNo、externalBookingNo;商业类型优先取同客户同尾程单的池来源桥,独立散板取自身来源单号,普通转运不推断为散板。orderType 保留履约类型;无关联单号为空。 security: [{ bearerAuth: [] }] parameters: - { name: id, in: path, required: true, schema: { type: integer, format: int64 } } @@ -1168,6 +1171,9 @@ paths: description: 权限 merchant:overseas-warehouse:last-mile-order:list;列表权限包含详情只读,客户范围只取认证 Token。 security: [{ bearerAuth: [] }] parameters: + - { name: businessType, in: query, description: 商业类型;本土散板同时覆盖独立和池来源,仓储转运排除已关联散板或卡车商业单, schema: { type: string, maxLength: 64 } } + - { name: businessOrderNo, in: query, description: 正式关联商业单号包含匹配,不解析备注, schema: { type: string, maxLength: 128 } } + - { name: externalBookingNo, in: query, description: 正式外接预订号包含匹配,不解析备注, schema: { type: string, maxLength: 128 } } - { name: sailingStage, in: query, description: '可选开船阶段。仅匹配头程订单类型 FIRST_LEG;未传不限制类型。按实际柜级节点序号>=40判断开船后;人工节点覆盖后仅按该票 ACTUAL_DEPARTURE 非空判断,不回退柜级。无实际开船事实归开船前。', schema: { type: string, enum: [BEFORE_DEPARTURE, AFTER_DEPARTURE] } } responses: { '200': { description: 当前客户海外尾程订单分页 }, '403': { description: 缺少海外尾程订单查看权限 } } /api/merchant/v1/last-leg/orders/{id}: diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/controller/LastlegOrderControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/controller/LastlegOrderControllerTest.java index b05e0e1b0..cfe7a9ddf 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/controller/LastlegOrderControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/controller/LastlegOrderControllerTest.java @@ -38,11 +38,11 @@ class LastlegOrderControllerTest { private final LastlegOrderCommandService commandService = mock(LastlegOrderCommandService.class); private final LastlegOrderController controller = controller(); private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query( - 1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + 1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); @Test void orderQueryHasOneResolvableConstructorForRequestParameterBinding() { - assertEquals(17, BeanUtils.getResolvableConstructor(LastlegOrderRequests.Query.class).getParameterCount()); + assertEquals(20, BeanUtils.getResolvableConstructor(LastlegOrderRequests.Query.class).getParameterCount()); } @Test diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/controller/MerchantLastlegOrderControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/controller/MerchantLastlegOrderControllerTest.java index d04fa2097..3f01b95ba 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/controller/MerchantLastlegOrderControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/controller/MerchantLastlegOrderControllerTest.java @@ -18,7 +18,7 @@ class MerchantLastlegOrderControllerTest { private final LastlegOrderService service = mock(LastlegOrderService.class); private final MerchantLastlegOrderController controller = controller(); private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query( - 1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + 1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); @Test void merchantListUsesOnlyAuthenticatedTenant() { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/export/LastlegExportProviderServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/export/LastlegExportProviderServiceTest.java index 726ee8912..2ab08ed38 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/export/LastlegExportProviderServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/export/LastlegExportProviderServiceTest.java @@ -75,6 +75,19 @@ class LastlegExportProviderServiceTest { assertTrue(result.columns().stream().noneMatch(c -> c.key().equals("id") || c.key().equals("customerId"))); } + @Test void exportsBusinessReferencesUsingTheSameFiltersAndProjectionAsTheList() { + var order = new ObjectMapper().findAndRegisterModules().convertValue(Map.of( + "id", "1", "orderNo", "WT-1", "orderTypeName", "仓储转运", "businessSource", Map.of( + "businessType", "LOCAL_LOOSEBOARD", "label", "本土散板 · 拼车需求池", "businessOrderNo", "LB-1", "externalBookingNo", "FB-1")), com.baiyue.lastleg.dto.LastlegOrderSummary.class); + when(orders.page(isNull(), any(), isNull())).thenReturn(new PageResult<>(List.of(order),1,1,1,1)); + var result = service.batch("LAST_MILE_ORDER_LIST",request(Map.of("businessType","LOCAL_LOOSEBOARD","businessOrderNo","LB-1","externalBookingNo","FB-1"),null,null)); + verify(orders).page(isNull(),argThat(q -> "LOCAL_LOOSEBOARD".equals(q.businessType()) && "LB-1".equals(q.businessOrderNo()) && "FB-1".equals(q.externalBookingNo())),isNull()); + assertEquals("仓储转运",result.rows().get(0).get("orderTypeName")); + assertEquals("本土散板 · 拼车需求池",result.rows().get(0).get("businessSource.label")); + assertEquals("LB-1",result.rows().get(0).get("businessSource.businessOrderNo")); + assertEquals("FB-1",result.rows().get(0).get("businessSource.externalBookingNo")); + } + @Test void keepsCustomerScopeAndLedgerCustomerFilter() { when(inventory.ledgers(eq("200"), any())).thenReturn(new PageResult<>(List.of(),1,1,0,0)); service.batch("OVERSEAS_INVENTORY_LEDGER", request(Map.of("customerId","200","customerName","客户"),null,null)); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderBusinessSourceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderBusinessSourceTest.java new file mode 100644 index 000000000..cd21e2847 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderBusinessSourceTest.java @@ -0,0 +1,75 @@ +package com.baiyue.lastleg.service; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baiyue.lastleg.dto.LastlegOrderRequests; +import com.baiyue.lastleg.dto.LastlegOrderSummary; +import com.baiyue.lastleg.entity.LastlegOrder; +import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import java.util.*; +import java.util.regex.Pattern; +import static org.junit.jupiter.api.Assertions.*; + +class LastlegOrderBusinessSourceTest { + @Test void projectsAndFiltersFormalSourcesWithinTheOrderCustomer() throws Exception { + var config = new MybatisConfiguration(); + config.setEnvironment(new Environment("test", new JdbcTransactionFactory(), + new UnpooledDataSource("org.h2.Driver", "jdbc:h2:mem:business-source;MODE=MySQL", "sa", ""))); + config.addMapper(LastlegOrderMapper.class); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(config, "source-test"), LastlegOrder.class); + try (var session = new SqlSessionFactoryBuilder().build(config).openSession()) { + var db = session.getConnection(); var sql = db.createStatement(); + sql.execute("CREATE TABLE ll_order (id BIGINT, customer_id VARCHAR, order_type VARCHAR, source_order_no VARCHAR)"); + sql.execute("CREATE TABLE ll_fleet_pool_order_link (lastleg_order_id BIGINT, customer_id VARCHAR, business_type VARCHAR, business_order_no VARCHAR)"); + sql.execute("CREATE TABLE ll_fleet_external_booking (customer_id VARCHAR, incoming_no VARCHAR, booking_no VARCHAR)"); + sql.execute("INSERT INTO ll_order VALUES (1,'A','LOCAL_LOOSEBOARD','LB-DIRECT'),(2,'A','WAREHOUSE_TRANSFER','WT-POOL')," + + "(3,'A','WAREHOUSE_TRANSFER','WT-TRUCK'),(4,'A','WAREHOUSE_TRANSFER','WT-NORMAL')," + + "(5,'B','WAREHOUSE_TRANSFER','WT-POOL'),(6,'A','WAREHOUSE_TRANSFER','WT-EXTERNAL')"); + sql.execute("INSERT INTO ll_fleet_pool_order_link VALUES (2,'A','LOCAL_LOOSEBOARD','LB-POOL')," + + "(3,'A','US_TRUCK','TRUCK-POOL'),(5,'B','LOCAL_LOOSEBOARD','LB-OTHER')," + + "(4,'B','LOCAL_LOOSEBOARD','LB-WRONG-CUSTOMER')"); + sql.execute("INSERT INTO ll_fleet_external_booking VALUES ('A','WT-POOL','FB-POOL'),('A','WT-TRUCK','FB-TRUCK')," + + "('B','WT-POOL','FB-OTHER'),('A','WT-EXTERNAL','FB-EXTERNAL')"); + var mapper = session.getMapper(LastlegOrderMapper.class); + var sources = mapper.findBusinessSources(List.of(1L,2L,3L,4L,5L,6L)); + assertEquals(6, sources.size()); + var service = new LastlegOrderService(); ReflectionTestUtils.setField(service,"orderMapper",mapper); + List orders = sources.stream().map(row -> { var o = new LastlegOrder(); o.setId(row.orderId()); return o; }).toList(); + Map values = ReflectionTestUtils.invokeMethod(service,"businessSources",orders); + assertEquals(new LastlegOrderSummary.BusinessSource("LOCAL_LOOSEBOARD","本土散板 · 独立下单","LB-DIRECT",null),values.get(1L)); + assertEquals(new LastlegOrderSummary.BusinessSource("LOCAL_LOOSEBOARD","本土散板 · 拼车需求池","LB-POOL","FB-POOL"),values.get(2L)); + assertEquals(new LastlegOrderSummary.BusinessSource("US_TRUCK","本土卡车 · 拼车需求池","TRUCK-POOL","FB-TRUCK"),values.get(3L)); + assertEquals(new LastlegOrderSummary.BusinessSource("WAREHOUSE_TRANSFER","仓储转运",null,null),values.get(4L)); + assertEquals("FB-OTHER",values.get(5L).externalBookingNo()); + assertEquals(new LastlegOrderSummary.BusinessSource("WAREHOUSE_TRANSFER","仓储转运 · 外接来货",null,"FB-EXTERNAL"),values.get(6L)); + assertEquals(List.of(1L,2L),find(db,"A","LOCAL_LOOSEBOARD",null,null)); + assertEquals(List.of(1L,2L,5L),find(db,null,"LOCAL_LOOSEBOARD",null,null)); + assertEquals(List.of(4L,6L),find(db,"A","WAREHOUSE_TRANSFER",null,null)); + assertEquals(List.of(3L),find(db,"A","US_TRUCK",null,null)); + assertEquals(List.of(2L),find(db,"A",null,"LB-POOL","FB-POOL")); + assertEquals(List.of(),find(db,"A",null,"LB-OTHER",null)); + assertEquals(List.of(),find(db,"A",null,null,"FB-OTHER")); + assertEquals(List.of(),find(db,"A",null,"' OR 1=1 --",null)); + } + } + private List find(java.sql.Connection db,String customer,String businessType,String businessOrderNo,String bookingNo) throws Exception { + var query = new LastlegOrderRequests.Query(1L,20L,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,businessType,businessOrderNo,bookingNo); + LambdaQueryWrapper filter = ReflectionTestUtils.invokeMethod(new LastlegOrderService(),"filter",customer,query); + var matcher = Pattern.compile("#\\{ew.paramNameValuePairs.([^}]+)}").matcher(filter.getSqlSegment()); + var parameters = new ArrayList(); var bound = new StringBuilder(); + while(matcher.find()) { parameters.add(filter.getParamNameValuePairs().get(matcher.group(1))); matcher.appendReplacement(bound,"?"); } + matcher.appendTail(bound); + try(var statement = db.prepareStatement("SELECT id FROM ll_order WHERE " + bound + " ORDER BY id")) { + for(int i=0;i(); try(var rows=statement.executeQuery()){while(rows.next())ids.add(rows.getLong(1));} return ids; + } + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderSailingStageTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderSailingStageTest.java index d6b55754c..02cd882a6 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderSailingStageTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderSailingStageTest.java @@ -59,7 +59,7 @@ class LastlegOrderSailingStageTest { private List find(java.sql.Connection db, String customerId, SailingStage stage, String status) throws Exception { var query = new LastlegOrderRequests.Query(1L, 20L, null, null, null, null, null, null, - status, null, null, null, null, null, null, null, stage); + status, null, null, null, null, null, null, null, stage, null, null, null); LambdaQueryWrapper filter = ReflectionTestUtils.invokeMethod( new LastlegOrderService(), "filter", customerId, query); String where = filter.getSqlSegment(); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java index 02ea84783..39e6982a9 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java @@ -96,7 +96,7 @@ class LastlegOrderServiceTest { LastlegOrderSummary summary = ReflectionTestUtils.invokeMethod( service, "summary", order, List.of(), null, "UNSCHEDULED", - new LastlegOrderMapper.OrderWeightRow(11L, new java.math.BigDecimal("30"), new java.math.BigDecimal("22")), List.of()); + new LastlegOrderMapper.OrderWeightRow(11L, new java.math.BigDecimal("30"), new java.math.BigDecimal("22")), List.of(), null); assertNotNull(summary); assertEquals("BOOKING-1", summary.sourceContainerBookingId()); @@ -379,7 +379,7 @@ class LastlegOrderServiceTest { @Test void pageUsesPaginationAndPlatformFiltersWithoutCustomerScope() { var query = new LastlegOrderRequests.Query(3L, 25L, "LL-", "FIRST_LEG", "IN-", null, - null, "", "PENDING_ASSIGNMENT", null, "LAX", null, null, null, null, null, null); + null, "", "PENDING_ASSIGNMENT", null, "LAX", null, null, null, null, null, null, null, null, null); var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page(3, 25); resultPage.setTotal(0); when(orders.selectPage(any(), any())).thenReturn(resultPage); @@ -536,7 +536,7 @@ class LastlegOrderServiceTest { new LastlegOrderMapper.PalletMergeRelationRow(11L, "INBOUND-2", "PMG-1"), new LastlegOrderMapper.PalletMergeRelationRow(11L, "INBOUND-3", "PMG-1"))); var query = new LastlegOrderRequests.Query(1L, 10L, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, null, null, null, null, null); var result = service.page(null, query); @@ -560,7 +560,7 @@ class LastlegOrderServiceTest { resultPage.setTotal(3); when(orders.selectPage(any(), any())).thenReturn(resultPage); var query = new LastlegOrderRequests.Query(1L, 10L, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null, null, null, null, null, null); var result = service.page(null, query); @@ -592,7 +592,7 @@ class LastlegOrderServiceTest { when(orders.selectPage(any(), any())).thenReturn(resultPage); var result = service.page(null, new LastlegOrderRequests.Query(1L, 10L, null, null, null, null, - null, null, null, null, input, null, null, null, null, null, null)); + null, null, null, null, input, null, null, null, null, null, null, null, null, null)); ArgumentCaptor> filter = ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);