fix(lastleg): filter domestic first-leg orders by sailing stage
This commit is contained in:
@@ -18,6 +18,8 @@ public final class LastlegOrderRequests {
|
||||
private LastlegOrderRequests() {
|
||||
}
|
||||
|
||||
public enum SailingStage { BEFORE_DEPARTURE, AFTER_DEPARTURE }
|
||||
|
||||
public record Query(
|
||||
@Min(1) Long page,
|
||||
@Min(1) @Max(200) Long size,
|
||||
@@ -34,7 +36,8 @@ public final class LastlegOrderRequests {
|
||||
Boolean abnormal,
|
||||
Boolean detained,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo
|
||||
LocalDateTime createdTo,
|
||||
SailingStage sailingStage
|
||||
) {
|
||||
public Query {
|
||||
if (page == null || page == 0) page = 1L;
|
||||
|
||||
@@ -16,6 +16,16 @@ import java.math.BigDecimal;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
// Manual node snapshots replace container nodes permanently for the order.
|
||||
String SAILING_STAGE_FILTER_SQL = "(ll_order.order_type='FIRST_LEG' AND ll_order.source_region='FIRSTLEG' 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})";
|
||||
|
||||
@Select("<script>SELECT r.lastleg_order_id,r.relabel_no,r.status,r.notified_package_count AS quantity,r.created_at " +
|
||||
"FROM ll_relabel_application r JOIN ll_order o ON o.id=r.lastleg_order_id AND o.customer_id=r.customer_id " +
|
||||
"WHERE r.lastleg_order_id IN <foreach collection='orderIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
|
||||
@@ -584,6 +584,8 @@ 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(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())
|
||||
.like(StringUtils.hasText(query.lxCode()), LastlegOrder::getLxCode, query.lxCode())
|
||||
.like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo())
|
||||
|
||||
@@ -214,6 +214,7 @@ paths:
|
||||
description: 权限 platform:overseas-warehouse:last-mile-order:list;平台管理员省略 customerId 时查询全部客户。containerNode 来自稳定 sourceContainerBookingId 对应的柜级船节点门禁;grossWeightKg/netWeightKg 由头程冻结货物明细聚合。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: sailingStage, in: query, description: '可选开船阶段。仅匹配 FIRST_LEG 且来源 FIRSTLEG;未传不限制类型。按实际柜级节点序号>=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 } }
|
||||
- { name: orderNo, in: query, schema: { type: string } }
|
||||
@@ -343,6 +344,8 @@ paths:
|
||||
summary: 导出筛选后的尾程订单
|
||||
description: 权限 platform:overseas-warehouse:last-mile-order:export;筛选参数与列表一致,返回 UTF-8 CSV。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: sailingStage, in: query, description: '可选开船阶段。仅匹配 FIRST_LEG 且来源 FIRSTLEG;未传不限制类型。按实际柜级节点序号>=40判断开船后;人工节点覆盖后仅按该票 ACTUAL_DEPARTURE 非空判断,不回退柜级。无实际开船事实归开船前。', schema: { type: string, enum: [BEFORE_DEPARTURE, AFTER_DEPARTURE] } }
|
||||
responses: { '200': { description: CSV 文件, content: { text/csv: { schema: { type: string, format: binary } } } } }
|
||||
/api/platform/v1/last-leg/orders/container-summaries:
|
||||
get:
|
||||
@@ -1172,6 +1175,8 @@ paths:
|
||||
summary: 客户分页查询自己的海外尾程订单
|
||||
description: 权限 merchant:overseas-warehouse:last-mile-order:list;列表权限包含详情只读,客户范围只取认证 Token。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: sailingStage, in: query, description: '可选开船阶段。仅匹配 FIRST_LEG 且来源 FIRSTLEG;未传不限制类型。按实际柜级节点序号>=40判断开船后;人工节点覆盖后仅按该票 ACTUAL_DEPARTURE 非空判断,不回退柜级。无实际开船事实归开船前。', schema: { type: string, enum: [BEFORE_DEPARTURE, AFTER_DEPARTURE] } }
|
||||
responses: { '200': { description: 当前客户海外尾程订单分页 }, '403': { description: 缺少海外尾程订单查看权限 } }
|
||||
/api/merchant/v1/last-leg/orders/{id}:
|
||||
get:
|
||||
|
||||
+3
-3
@@ -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);
|
||||
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void orderQueryHasOneResolvableConstructorForRequestParameterBinding() {
|
||||
assertEquals(16, BeanUtils.getResolvableConstructor(LastlegOrderRequests.Query.class).getParameterCount());
|
||||
assertEquals(17, BeanUtils.getResolvableConstructor(LastlegOrderRequests.Query.class).getParameterCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -138,7 +138,7 @@ class LastlegOrderControllerTest {
|
||||
void platformOperatorCanExportAllCustomersWithoutFilters() {
|
||||
byte[] csv = "all customers".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
LastlegOrderRequests.Query emptyQuery = new LastlegOrderRequests.Query(
|
||||
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, null, null, null, null);
|
||||
when(service.exportCsv(null, emptyQuery)).thenReturn(csv);
|
||||
|
||||
var response = controller.export(user(0L, "PLATFORM"), null, emptyQuery);
|
||||
|
||||
+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);
|
||||
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantListUsesOnlyAuthenticatedTenant() {
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
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.LastlegOrderRequests.SailingStage;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.sql.DriverManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class LastlegOrderSailingStageTest {
|
||||
@Test
|
||||
void filtersBeforePaginationByActualDepartureAndKeepsOtherTypesOnlyInAll() throws Exception {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "sailing-test"), LastlegOrder.class);
|
||||
try (var db = DriverManager.getConnection("jdbc:h2:mem:sailing;MODE=MySQL")) {
|
||||
var sql = db.createStatement();
|
||||
sql.execute("CREATE TABLE ll_order (id BIGINT, customer_id VARCHAR, order_type VARCHAR, source_region VARCHAR, " +
|
||||
"source_container_booking_id VARCHAR, manual_node_override BOOLEAN, current_status VARCHAR)");
|
||||
sql.execute("CREATE TABLE ll_container_shipping_gate (container_booking_id VARCHAR, node_sequence INT, actual_at TIMESTAMP)");
|
||||
sql.execute("CREATE TABLE ll_order_manual_node (order_id BIGINT, customer_id VARCHAR, node_code VARCHAR, node_at TIMESTAMP)");
|
||||
sql.execute("INSERT INTO ll_container_shipping_gate VALUES ('loaded',30,CURRENT_TIMESTAMP)," +
|
||||
"('departed',40,CURRENT_TIMESTAMP),('arrived',60,CURRENT_TIMESTAMP),('estimated',40,NULL)");
|
||||
sql.execute("INSERT INTO ll_order VALUES " +
|
||||
"(1,'A','FIRST_LEG','FIRSTLEG',NULL,NULL,'PENDING_ASSIGNMENT')," +
|
||||
"(2,'A','FIRST_LEG','FIRSTLEG','loaded',FALSE,'INBOUND_PLANNED')," +
|
||||
"(3,'A','FIRST_LEG','FIRSTLEG','departed',FALSE,'PENDING_ASSIGNMENT')," +
|
||||
"(4,'A','FIRST_LEG','FIRSTLEG','arrived',FALSE,'INBOUND_COMPLETED')," +
|
||||
"(5,'A','FIRST_LEG','FIRSTLEG','estimated',FALSE,'PENDING_ASSIGNMENT')," +
|
||||
"(6,'A','FIRST_LEG','FIRSTLEG','arrived',TRUE,'PENDING_ASSIGNMENT')," +
|
||||
"(7,'A','FIRST_LEG','FIRSTLEG','loaded',TRUE,'PENDING_ASSIGNMENT')," +
|
||||
"(8,'B','FIRST_LEG','FIRSTLEG','departed',FALSE,'PENDING_ASSIGNMENT')," +
|
||||
"(9,'A','FIRST_LEG','CUSTOMER','departed',FALSE,'PENDING_ASSIGNMENT')");
|
||||
sql.execute("INSERT INTO ll_order_manual_node VALUES (6,'A','ESTIMATED_DEPARTURE',CURRENT_TIMESTAMP)," +
|
||||
"(6,'A','ACTUAL_DEPARTURE',NULL),(6,'B','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP)," +
|
||||
"(7,'A','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP)");
|
||||
int id = 10;
|
||||
for (String type : List.of("ONE_CLICK_DROPSHIP", "RELABEL_APPLICATION", "WAREHOUSE_TRANSFER",
|
||||
"LOCAL_LOOSEBOARD", "RETURN_RELABEL", "CARGO_DISCARD")) {
|
||||
try (var insert = db.prepareStatement("INSERT INTO ll_order VALUES (?,'A',?,'FIRSTLEG','departed',FALSE,'PENDING_ASSIGNMENT')")) {
|
||||
insert.setInt(1, id++); insert.setString(2, type); insert.executeUpdate();
|
||||
}
|
||||
}
|
||||
assertEquals(List.of(1L, 2L, 5L, 6L), find(db, null, SailingStage.BEFORE_DEPARTURE, null));
|
||||
assertEquals(List.of(3L, 4L, 7L, 8L), find(db, null, SailingStage.AFTER_DEPARTURE, null));
|
||||
assertEquals(List.of(3L, 4L, 7L), find(db, "A", SailingStage.AFTER_DEPARTURE, null));
|
||||
assertEquals(List.of(3L, 7L), find(db, "A", SailingStage.AFTER_DEPARTURE, "PENDING_ASSIGNMENT"));
|
||||
assertEquals(15, find(db, null, null, null).size());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
LambdaQueryWrapper<LastlegOrder> filter = ReflectionTestUtils.invokeMethod(
|
||||
new LastlegOrderService(), "filter", customerId, query);
|
||||
String where = filter.getSqlSegment();
|
||||
var matcher = Pattern.compile("#\\{ew.paramNameValuePairs.([^}]+)}").matcher(where);
|
||||
List<Object> parameters = new ArrayList<>();
|
||||
StringBuilder boundSql = new StringBuilder();
|
||||
while (matcher.find()) {
|
||||
parameters.add(filter.getParamNameValuePairs().get(matcher.group(1)));
|
||||
matcher.appendReplacement(boundSql, "?");
|
||||
}
|
||||
matcher.appendTail(boundSql);
|
||||
try (var statement = db.prepareStatement("SELECT id FROM ll_order" +
|
||||
(where.isBlank() ? "" : " WHERE " + boundSql) + " ORDER BY id")) {
|
||||
for (int i = 0; i < parameters.size(); i++) statement.setObject(i + 1, parameters.get(i));
|
||||
List<Long> ids = new ArrayList<>();
|
||||
try (var rows = statement.executeQuery()) { while (rows.next()) ids.add(rows.getLong(1)); }
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -375,7 +375,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, "", "PENDING_ASSIGNMENT", null, "LAX", 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);
|
||||
@@ -401,7 +401,7 @@ class LastlegOrderServiceTest {
|
||||
|
||||
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);
|
||||
null, null, null, null, null)), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(csv.contains("\"否\",\"是\""));
|
||||
assertTrue(csv.contains("\"待分配\""));
|
||||
@@ -553,7 +553,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);
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
@@ -577,7 +577,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);
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
@@ -609,7 +609,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, input, 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