feat(lastleg): query commercial sources independently of fulfillment types
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -74,6 +74,9 @@ public record LastlegOrderSummary(
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt,
|
||||
@Schema(description = "同一主单关联的全部换标子单及各自状态,按创建时间排序")
|
||||
List<RelabelSubOrder> relabelApplications
|
||||
List<RelabelSubOrder> relabelApplications,
|
||||
BusinessSource businessSource
|
||||
) {
|
||||
@Schema(description = "从正式单据关联读取的商业来源;与履约类型 orderType 独立")
|
||||
public record BusinessSource(String businessType, String label, String businessOrderNo, String externalBookingNo) { }
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.util.Arrays;
|
||||
final class LastlegExportColumns {
|
||||
private LastlegExportColumns() { }
|
||||
private static final Map<String, String> 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"),
|
||||
|
||||
@@ -16,6 +16,25 @@ import java.math.BigDecimal;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
// 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("<script>SELECT ll_order.id AS orderId," + BUSINESS_TYPE_SQL + " AS businessType," +
|
||||
BUSINESS_ORDER_NO_SQL + " AS businessOrderNo," + EXTERNAL_BOOKING_NO_SQL + " AS externalBookingNo," +
|
||||
"EXISTS(SELECT 1" + POOL_SOURCE_WHERE + ") AS poolSource FROM ll_order WHERE ll_order.id IN " +
|
||||
"<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
|
||||
List<BusinessSourceRow> findBusinessSources(@Param("orderIds") List<Long> 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 " +
|
||||
|
||||
@@ -100,11 +100,12 @@ public class LastlegOrderService {
|
||||
Map<Long, String> dispatchStatuses = dispatchStatuses(page.getRecords());
|
||||
Map<Long, LastlegOrderMapper.OrderWeightRow> weights = orderWeights(page.getRecords());
|
||||
Map<Long, List<com.baiyue.lastleg.dto.RelabelSubOrder>> relabels = relabelSubOrders(page.getRecords());
|
||||
Map<Long, LastlegOrderSummary.BusinessSource> 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<Long, LastlegOrderSummary.BusinessSource> businessSources(List<LastlegOrder> 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<LastlegOrderMapper.PalletMergeRelationRow> relations,
|
||||
String containerNode, String dispatchStatus,
|
||||
LastlegOrderMapper.OrderWeightRow weight,
|
||||
List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications) {
|
||||
List<com.baiyue.lastleg.dto.RelabelSubOrder> relabelApplications,
|
||||
LastlegOrderSummary.BusinessSource businessSource) {
|
||||
List<LastlegOrderMapper.PalletMergeRelationRow> 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) {
|
||||
|
||||
@@ -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 权限写入,不能信任客户端值。
|
||||
|
||||
@@ -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}:
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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() {
|
||||
|
||||
+13
@@ -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));
|
||||
|
||||
+75
@@ -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<LastlegOrder> orders = sources.stream().map(row -> { var o = new LastlegOrder(); o.setId(row.orderId()); return o; }).toList();
|
||||
Map<Long, LastlegOrderSummary.BusinessSource> 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<Long> 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<LastlegOrder> filter = ReflectionTestUtils.invokeMethod(new LastlegOrderService(),"filter",customer,query);
|
||||
var matcher = Pattern.compile("#\\{ew.paramNameValuePairs.([^}]+)}").matcher(filter.getSqlSegment());
|
||||
var parameters = new ArrayList<Object>(); 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<parameters.size();i++)statement.setObject(i+1,parameters.get(i));
|
||||
var ids = new ArrayList<Long>(); try(var rows=statement.executeQuery()){while(rows.next())ids.add(rows.getLong(1));} return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -59,7 +59,7 @@ class LastlegOrderSailingStageTest {
|
||||
|
||||
private List<Long> 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<LastlegOrder> filter = ReflectionTestUtils.invokeMethod(
|
||||
new LastlegOrderService(), "filter", customerId, query);
|
||||
String where = filter.getSqlSegment();
|
||||
|
||||
+5
-5
@@ -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<LastlegOrder>(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<com.baomidou.mybatisplus.core.conditions.Wrapper<LastlegOrder>> filter =
|
||||
ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
|
||||
|
||||
Reference in New Issue
Block a user