fix(orders): filter tax mode before unified order pagination

This commit is contained in:
2026-10-02 14:44:51 +08:00
parent 2d73ce9f37
commit f532e5ee7e
16 changed files with 250 additions and 15 deletions
@@ -35,7 +35,9 @@ public record UnifiedInboundQuery(
LocalDateTime createdFrom, LocalDateTime createdFrom,
LocalDateTime createdTo, LocalDateTime createdTo,
@Min(1) long page, @Min(1) long page,
@Min(1) @Max(200) long size @Min(1) @Max(200) long size,
@Schema(allowableValues = {"TAX_REFUND", "BUY_DECLARATION"})
@jakarta.validation.constraints.Pattern(regexp = "TAX_REFUND|BUY_DECLARATION") String taxMode
) { ) {
public UnifiedInboundQuery { public UnifiedInboundQuery {
if (page == 0) page = 1; if (page == 0) page = 1;
@@ -118,6 +118,7 @@ public class InboundOrderRepository {
public Page<InboundOrderEntity> pageUnifiedEntities(UnifiedInboundQuery query, List<String> processStatuses) { public Page<InboundOrderEntity> pageUnifiedEntities(UnifiedInboundQuery query, List<String> processStatuses) {
LambdaQueryWrapper<InboundOrderEntity> wrapper = new LambdaQueryWrapper<InboundOrderEntity>() LambdaQueryWrapper<InboundOrderEntity> wrapper = new LambdaQueryWrapper<InboundOrderEntity>()
.eq(query.tenantId() != null, InboundOrderEntity::getTenantId, query.tenantId()) .eq(query.tenantId() != null, InboundOrderEntity::getTenantId, query.tenantId())
.eq(query.taxMode() != null, InboundOrderEntity::getTaxMode, query.taxMode())
.in(processStatuses != null && !processStatuses.isEmpty(), InboundOrderEntity::getProcessStatus, processStatuses) .in(processStatuses != null && !processStatuses.isEmpty(), InboundOrderEntity::getProcessStatus, processStatuses)
.like(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo()) .like(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo())
.like(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode, query.inboundCode()) .like(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode, query.inboundCode())
@@ -222,6 +222,10 @@ public class UnifiedInboundQueryService {
} }
private void validate(UnifiedInboundQuery query) { private void validate(UnifiedInboundQuery query) {
if (query.taxMode() != null && !java.util.Set.of("TAX_REFUND", "BUY_DECLARATION").contains(query.taxMode())) {
throw new com.baiyue.common.core.exception.BizException(
com.baiyue.common.core.api.ResultCode.BAD_REQUEST, "taxMode仅支持TAX_REFUND或BUY_DECLARATION");
}
if (StringUtils.hasText(query.mainStatus()) if (StringUtils.hasText(query.mainStatus())
&& !MAIN_STATUSES.contains(query.mainStatus()) && !MAIN_STATUSES.contains(query.mainStatus())
) { ) {
@@ -1557,6 +1557,7 @@ components:
UnifiedInboundQuery: UnifiedInboundQuery:
type: object type: object
properties: properties:
taxMode: {type: string, enum: [TAX_REFUND, BUY_DECLARATION], nullable: true, description: 按税务模式在数据库计数和分页前过滤;未传不限制}
tenantId: {type: integer, format: int64, nullable: true} tenantId: {type: integer, format: int64, nullable: true}
keyword: {type: string, maxLength: 64, nullable: true} keyword: {type: string, maxLength: 64, nullable: true}
orderNo: {type: string, maxLength: 64, nullable: true} orderNo: {type: string, maxLength: 64, nullable: true}
@@ -79,7 +79,7 @@ class InternalInboundOrderReferenceControllerTest {
UnifiedInboundQuery request = new UnifiedInboundQuery( UnifiedInboundQuery request = new UnifiedInboundQuery(
10001L, null, null, "WAIT_SPLIT", null, null, null, 10001L, null, null, "WAIT_SPLIT", 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, 1, 20); null, null, 1, 20, null);
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
assertThat(controller.unifiedPage("internal-token", request).data().records()).isEmpty(); assertThat(controller.unifiedPage("internal-token", request).data().records()).isEmpty();
@@ -57,7 +57,7 @@ class InboundOrderRepositoryTest {
10001L, null, null, "LOADING", 30001L, 40001L, 50001L, 10001L, null, null, "LOADING", 30001L, 40001L, 50001L,
"RK-", "APPROVED", LocalDate.of(2026, 8, 6), "APPROVED", "美森", "RK-", "APPROVED", LocalDate.of(2026, 8, 6), "APPROVED", "美森",
"MSCU", "CAB", LocalDate.of(2026, 8, 8), true, true, "UNRECORDED", "MSCU", "CAB", LocalDate.of(2026, 8, 8), true, true, "UNRECORDED",
List.of(77L, 78L), null, null, 1, 20); List.of(77L, 78L), null, null, 1, 20, null);
repository.pageUnifiedEntities(query, List.of("LOADING")); repository.pageUnifiedEntities(query, List.of("LOADING"));
@@ -0,0 +1,82 @@
package com.baiyue.firstleg.repository;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
/** Real mapper + pagination interceptor: tax filtering applies to COUNT and records before LIMIT. */
class UnifiedInboundTaxModePaginationTest {
private InboundOrderRepository repository;
@BeforeEach
void setup() throws Exception {
var dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:tax-page-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
var jdbc = new JdbcTemplate(dataSource);
var columns = new ArrayList<String>();
for (Class<?> type = InboundOrderEntity.class; type != Object.class; type = type.getSuperclass()) {
for (var field : type.getDeclaredFields()) {
var annotation = field.getAnnotation(TableField.class);
if (Modifier.isStatic(field.getModifiers()) || annotation != null && !annotation.exist()) continue;
String name = field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
Class<?> valueType = field.getType();
String sqlType = valueType == Long.class || valueType == long.class ? "BIGINT"
: valueType == Integer.class || valueType == int.class || valueType == Boolean.class || valueType == boolean.class ? "INT"
: valueType == java.math.BigDecimal.class ? "DECIMAL(20,6)"
: valueType == java.time.LocalDateTime.class ? "TIMESTAMP"
: valueType == java.time.LocalDate.class ? "DATE" : "VARCHAR(2000)";
columns.add(name + " " + sqlType + (name.equals("id") ? " PRIMARY KEY" : ""));
}
}
jdbc.execute("CREATE TABLE fl_inbound_order (" + String.join(",", columns) + ")");
for (int id = 1; id <= 7; id++) {
jdbc.update("INSERT INTO fl_inbound_order(id,tenant_id,process_status,tax_mode,deleted,created_at) VALUES(?,?,?,?,?,?)",
id, id == 5 || id == 7 ? 22 : 11, "LOADED", id == 2 || id == 3 || id == 6 || id == 7 ? "TAX_REFUND" : "BUY_DECLARATION",
id == 3 ? 1 : 0, java.time.LocalDateTime.of(2026, 10, 2, 12, 0));
}
var config = new MybatisConfiguration();
config.setMapUnderscoreToCamelCase(true); config.addMapper(InboundOrderMapper.class);
var pagination = new MybatisPlusInterceptor();
pagination.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
var factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource); factory.setConfiguration(config); factory.setPlugins(pagination);
var session = new SqlSessionTemplate(factory.getObject());
repository = new InboundOrderRepository();
ReflectionTestUtils.setField(repository, "orders", session.getMapper(InboundOrderMapper.class));
}
@Test
void taxModeFiltersCountsAndPagesWithoutLeakingAnotherCustomerOrDeletedRows() {
assertPage("TAX_REFUND", 1, 2, 6);
assertPage("TAX_REFUND", 2, 2, 2);
assertPage("BUY_DECLARATION", 1, 2, 4);
assertPage("BUY_DECLARATION", 2, 2, 1);
assertPage(null, 1, 4, 6);
assertPage(null, 2, 4, 4);
}
private void assertPage(String taxMode, long page, long total, long expectedId) {
var query = new UnifiedInboundQuery(11L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, page, 1, taxMode);
var result = repository.pageUnifiedEntities(query, List.of("LOADED"));
assertThat(result.getTotal()).isEqualTo(total);
assertThat(result.getRecords()).singleElement().satisfies(row -> {
assertThat(row.getId()).isEqualTo(expectedId);
assertThat(row.getTenantId()).isEqualTo(11L);
if (taxMode != null) assertThat(row.getTaxMode()).isEqualTo(taxMode);
});
}
}
@@ -24,6 +24,18 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
class UnifiedInboundQueryServiceTest { class UnifiedInboundQueryServiceTest {
@ParameterizedTest
@ValueSource(strings = {"", " ", "tax_refund", "TAX_REFUND ", "OTHER", "TAX_INCLUDED"})
void illegalTaxModeFailsBeforeRepository(String taxMode) {
var repository = mock(InboundOrderRepository.class);
var service = new UnifiedInboundQueryService();
ReflectionTestUtils.setField(service, "inbounds", repository);
var query = new UnifiedInboundQuery(10001L, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, 1, 20, taxMode);
assertThatThrownBy(() -> service.page(query)).isInstanceOf(BizException.class).hasMessageContaining("taxMode");
verifyNoInteractions(repository);
}
@Test @Test
void mapsAllCompatibleInternalStatesToProductMainStatuses() { void mapsAllCompatibleInternalStatesToProductMainStatuses() {
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_MEASURE")).isEqualTo("WAIT_MEASURE"); assertThat(UnifiedInboundQueryService.mainStatus("WAIT_MEASURE")).isEqualTo("WAIT_MEASURE");
@@ -121,7 +133,7 @@ class UnifiedInboundQueryServiceTest {
UnifiedInboundQuery query = new UnifiedInboundQuery( UnifiedInboundQuery query = new UnifiedInboundQuery(
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, "UNKNOWN", null, null, null, null, null, null, "INVALID", null, null, "UNKNOWN", null, null, null, null, null, null, "INVALID",
null, null, null, 1, 20); null, null, null, 1, 20, null);
assertThatThrownBy(() -> new UnifiedInboundQueryService().page(query)) assertThatThrownBy(() -> new UnifiedInboundQueryService().page(query))
.isInstanceOf(BizException.class); .isInstanceOf(BizException.class);
@@ -34,7 +34,7 @@ public interface FirstlegUnifiedOrderClient {
String feeReviewStatus, String channelName, String containerNo, String cabinetNo, String feeReviewStatus, String channelName, String containerNo, String cabinetNo,
LocalDate sailingDate, Boolean loaded, Boolean fullContainer, LocalDate sailingDate, Boolean loaded, Boolean fullContainer,
String measurementReviewStatus, List<Long> candidateOrderIds, String measurementReviewStatus, List<Long> candidateOrderIds,
LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size) {} LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size, String taxMode) {}
record BatchRequest(Long tenantId, List<Long> orderIds) {} record BatchRequest(Long tenantId, List<Long> orderIds) {}
record DetailRequest(long tenantId, long orderId) {} record DetailRequest(long tenantId, long orderId) {}
@@ -33,7 +33,9 @@ public record UnifiedOrderQuery(
LocalDateTime createdFrom, LocalDateTime createdFrom,
LocalDateTime createdTo, LocalDateTime createdTo,
@Min(1) long page, @Min(1) long page,
@Min(1) @Max(200) long size @Min(1) @Max(200) long size,
@Schema(allowableValues = {"TAX_REFUND", "BUY_DECLARATION"})
@jakarta.validation.constraints.Pattern(regexp = "TAX_REFUND|BUY_DECLARATION") String taxMode
) { ) {
public UnifiedOrderQuery { public UnifiedOrderQuery {
if (page == 0) page = 1; if (page == 0) page = 1;
@@ -301,6 +301,7 @@ public class OrderRepository {
List<OrderStatus> statuses) { List<OrderStatus> statuses) {
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>() LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
.eq(tenantId != null, OrderEntity::getTenantId, tenantId) .eq(tenantId != null, OrderEntity::getTenantId, tenantId)
.eq(query.taxMode() != null, OrderEntity::getTaxMode, query.taxMode())
.in(OrderEntity::getStatus, statuses) .in(OrderEntity::getStatus, statuses)
.like(StringUtils.hasText(query.orderNo()), OrderEntity::getOrderNo, query.orderNo()) .like(StringUtils.hasText(query.orderNo()), OrderEntity::getOrderNo, query.orderNo())
.like(StringUtils.hasText(query.inboundCode()), OrderEntity::getInboundCode, query.inboundCode()) .like(StringUtils.hasText(query.inboundCode()), OrderEntity::getInboundCode, query.inboundCode())
@@ -282,7 +282,7 @@ public class DomesticWarehouseOrderExportProviderService {
string("inboundCode"), string("internalOrderStatus"), date("expectedArrivalDate"), string("inboundCode"), string("internalOrderStatus"), date("expectedArrivalDate"),
string("feeReviewStatus"), string("channelName"), string("containerNo"), string("cabinetNo"), string("feeReviewStatus"), string("channelName"), string("containerNo"), string("cabinetNo"),
date("sailingDate"), bool("loaded"), bool("fullContainer"), string("measurementReviewStatus"), date("sailingDate"), bool("loaded"), bool("fullContainer"), string("measurementReviewStatus"),
string("fbaCode"), dateTime("createdFrom"), dateTime("createdTo"), page, limit); string("fbaCode"), dateTime("createdFrom"), dateTime("createdTo"), page, limit, null);
} }
private String string(String key) { Object value = parameters.get(key); return value == null || String.valueOf(value).isBlank() ? null : String.valueOf(value).trim(); } private String string(String key) { Object value = parameters.get(key); return value == null || String.valueOf(value).isBlank() ? null : String.valueOf(value).trim(); }
private Long number(String key) { String value = string(key); if (value == null) return null; try { long parsed=Long.parseLong(value); if(parsed<=0)throw new NumberFormatException(); return parsed; } catch(NumberFormatException ex){throw new BizException(ResultCode.VALIDATION_FAILED,key+" 必须为正整数字符串");} } private Long number(String key) { String value = string(key); if (value == null) return null; try { long parsed=Long.parseLong(value); if(parsed<=0)throw new NumberFormatException(); return parsed; } catch(NumberFormatException ex){throw new BizException(ResultCode.VALIDATION_FAILED,key+" 必须为正整数字符串");} }
@@ -159,7 +159,7 @@ public class UnifiedOrderQueryService {
query.internalOrderStatus(), query.expectedArrivalDate(), query.feeReviewStatus(), query.channelName(), query.internalOrderStatus(), query.expectedArrivalDate(), query.feeReviewStatus(), query.channelName(),
query.containerNo(), query.cabinetNo(), query.sailingDate(), query.loaded(), query.fullContainer(), query.containerNo(), query.cabinetNo(), query.sailingDate(), query.loaded(), query.fullContainer(),
query.measurementReviewStatus(), candidateOrderIds, query.createdFrom(), query.createdTo(), query.measurementReviewStatus(), candidateOrderIds, query.createdFrom(), query.createdTo(),
query.page(), query.size()); query.page(), query.size(), query.taxMode());
PageResult<FirstlegUnifiedOrderClient.Summary> source = require( PageResult<FirstlegUnifiedOrderClient.Summary> source = require(
call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效"); call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效");
List<FirstlegUnifiedOrderClient.Summary> summaries = validateSummaries( List<FirstlegUnifiedOrderClient.Summary> summaries = validateSummaries(
@@ -555,6 +555,9 @@ public class UnifiedOrderQueryService {
} }
private void validate(UnifiedOrderQuery query) { private void validate(UnifiedOrderQuery query) {
if (query.taxMode() != null && !Set.of("TAX_REFUND", "BUY_DECLARATION").contains(query.taxMode())) {
throw new BizException(ResultCode.BAD_REQUEST, "taxMode仅支持TAX_REFUND或BUY_DECLARATION");
}
if (StringUtils.hasText(query.mainStatus()) if (StringUtils.hasText(query.mainStatus())
&& !MAIN_STATUSES.contains(query.mainStatus()) && !MAIN_STATUSES.contains(query.mainStatus())
) { ) {
@@ -329,6 +329,7 @@ paths:
relation(仅有关联时返回;PARENT 含 childCount,CHILD 含 rootOrderId、parentOrderId 与 splitSequence)。 relation(仅有关联时返回;PARENT 含 childCount,CHILD 含 rootOrderId、parentOrderId 与 splitSequence)。
coLoadGroup 仅平台响应返回,由 Order 经 Firstleg 内部批量只读契约聚合;成员业务标识由 Order 权威订单片段补齐。 coLoadGroup 仅平台响应返回,由 Order 经 Firstleg 内部批量只读契约聚合;成员业务标识由 Order 权威订单片段补齐。
parameters: parameters:
- $ref: '#/components/parameters/UnifiedTaxMode'
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- $ref: '#/components/parameters/UnifiedKeyword' - $ref: '#/components/parameters/UnifiedKeyword'
- $ref: '#/components/parameters/UnifiedOrderNo' - $ref: '#/components/parameters/UnifiedOrderNo'
@@ -444,6 +445,7 @@ paths:
有入库单时同时返回 inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion 有入库单时同时返回 inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
才是 C04 loading/complete 的 expectedVersion,loadingVersion 当前等于 pickingVersion(兼容别名)。 才是 C04 loading/complete 的 expectedVersion,loadingVersion 当前等于 pickingVersion(兼容别名)。
parameters: parameters:
- $ref: '#/components/parameters/UnifiedTaxMode'
- $ref: '#/components/parameters/UnifiedKeyword' - $ref: '#/components/parameters/UnifiedKeyword'
- $ref: '#/components/parameters/UnifiedOrderNo' - $ref: '#/components/parameters/UnifiedOrderNo'
- $ref: '#/components/parameters/UnifiedMainStatus' - $ref: '#/components/parameters/UnifiedMainStatus'
@@ -1488,6 +1490,12 @@ components:
in: query in: query
required: false required: false
schema: {type: string, maxLength: 64} schema: {type: string, maxLength: 64}
UnifiedTaxMode:
name: taxMode
in: query
required: false
description: 严格按税务模式在数据库计数和分页前过滤;未传查询全部税务模式,空串或其他值拒绝
schema: {type: string, enum: [TAX_REFUND, BUY_DECLARATION]}
UnifiedOrderNo: UnifiedOrderNo:
name: orderNo name: orderNo
in: query in: query
@@ -0,0 +1,82 @@
package com.baiyue.order.repository;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.mapper.OrderMapper;
import com.baiyue.order.dto.UnifiedOrderQuery;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
/** Real mapper + pagination interceptor: tax filtering applies to COUNT and records before LIMIT. */
class UnifiedOrderTaxModePaginationTest {
private OrderRepository repository;
@BeforeEach
void setup() throws Exception {
var dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:tax-page-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
var jdbc = new JdbcTemplate(dataSource);
var columns = new ArrayList<String>();
for (Class<?> type = OrderEntity.class; type != Object.class; type = type.getSuperclass()) {
for (var field : type.getDeclaredFields()) {
var annotation = field.getAnnotation(TableField.class);
if (Modifier.isStatic(field.getModifiers()) || annotation != null && !annotation.exist()) continue;
String name = field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
Class<?> valueType = field.getType();
String sqlType = valueType == Long.class || valueType == long.class ? "BIGINT"
: valueType == Integer.class || valueType == int.class || valueType == Boolean.class || valueType == boolean.class ? "INT"
: valueType == java.math.BigDecimal.class ? "DECIMAL(20,6)"
: valueType == java.time.LocalDateTime.class ? "TIMESTAMP"
: valueType == java.time.LocalDate.class ? "DATE" : "VARCHAR(2000)";
columns.add(name + " " + sqlType + (name.equals("id") ? " PRIMARY KEY" : ""));
}
}
jdbc.execute("CREATE TABLE ord_order (" + String.join(",", columns) + ")");
for (int id = 1; id <= 7; id++) {
jdbc.update("INSERT INTO ord_order(id,tenant_id,status,tax_mode,deleted,created_at) VALUES(?,?,?,?,?,?)",
id, id == 5 || id == 7 ? 22 : 11, "APPROVED", id == 2 || id == 3 || id == 6 || id == 7 ? "TAX_REFUND" : "BUY_DECLARATION",
id == 3 ? 1 : 0, java.time.LocalDateTime.of(2026, 10, 2, 12, 0));
}
var config = new MybatisConfiguration();
config.setMapUnderscoreToCamelCase(true); config.addMapper(OrderMapper.class);
var pagination = new MybatisPlusInterceptor();
pagination.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
var factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource); factory.setConfiguration(config); factory.setPlugins(pagination);
var session = new SqlSessionTemplate(factory.getObject());
repository = new OrderRepository();
ReflectionTestUtils.setField(repository, "orderMapper", session.getMapper(OrderMapper.class));
}
@Test
void taxModeFiltersCountsAndPagesWithoutLeakingAnotherCustomerOrDeletedRows() {
assertPage("TAX_REFUND", 1, 2, 6);
assertPage("TAX_REFUND", 2, 2, 2);
assertPage("BUY_DECLARATION", 1, 2, 4);
assertPage("BUY_DECLARATION", 2, 2, 1);
assertPage(null, 1, 4, 6);
assertPage(null, 2, 4, 4);
}
private void assertPage(String taxMode, long page, long total, long expectedId) {
var query = new UnifiedOrderQuery(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, page, 1, taxMode);
var result = repository.unifiedPage(11L, query, List.of(com.baiyue.order.entity.OrderStatus.APPROVED));
assertThat(result.total()).isEqualTo(total);
assertThat(result.records()).singleElement().satisfies(row -> {
assertThat(row.getId()).isEqualTo(expectedId);
assertThat(row.getTenantId()).isEqualTo(11L);
if (taxMode != null) assertThat(row.getTaxMode()).isEqualTo(taxMode);
});
}
}
@@ -292,7 +292,7 @@ class UnifiedOrderQueryServiceTest {
UnifiedOrderQuery query = new UnifiedOrderQuery( UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, "PENDING_REVIEW", null, null, null, "RK-77", null, null, null, "PENDING_REVIEW", null, null, null, "RK-77", null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, 1, 20); null, null, null, 1, 20, null);
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED)))) when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); .thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -512,7 +512,7 @@ class UnifiedOrderQueryServiceTest {
UnifiedOrderQuery query = new UnifiedOrderQuery( UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, null, null, null, null, "RK-77", null, null, null, null, null, null, null, "RK-77", null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20); null, null, 1, 20, null);
when(orders.unifiedPage(eq(10001L), eq(query), any())) when(orders.unifiedPage(eq(10001L), eq(query), any()))
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); .thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -785,7 +785,7 @@ class UnifiedOrderQueryServiceTest {
UnifiedOrderQuery query = new UnifiedOrderQuery( UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, "INBOUNDED", null, null, null, null, null, null, null, "INBOUNDED", 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, 4, 10); null, null, 4, 10, null);
var summary = summary("INBOUNDED", null); var summary = summary("INBOUNDED", null);
when(summary.inbound().holdFlag()).thenReturn(false); when(summary.inbound().holdFlag()).thenReturn(false);
when(summary.inbound().currentHoldReason()).thenReturn(null); when(summary.inbound().currentHoldReason()).thenReturn(null);
@@ -909,7 +909,7 @@ class UnifiedOrderQueryServiceTest {
statuses.forEach(status -> service.page(null, new UnifiedOrderQuery( statuses.forEach(status -> service.page(null, new UnifiedOrderQuery(
"CC-2026", "FL-77", status, null, null, null, null, null, "CC-2026", "FL-77", status, 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, 3, 20))); null, null, 3, 20, null)));
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor = ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class); ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
@@ -931,7 +931,7 @@ class UnifiedOrderQueryServiceTest {
null, null, "LOADING", 30001L, 11L, 22L, "RK-77", "APPROVED", null, null, "LOADING", 30001L, 11L, 22L, "RK-77", "APPROVED",
LocalDate.of(2026, 8, 6), "APPROVED", "美森", "MSCU", "CAB-1", LocalDate.of(2026, 8, 6), "APPROVED", "美森", "MSCU", "CAB-1",
LocalDate.of(2026, 8, 8), true, true, "UNRECORDED", "FBA-1", LocalDate.of(2026, 8, 8), true, true, "UNRECORDED", "FBA-1",
null, null, 1, 20); null, null, 1, 20, null);
when(orders.findOrderIdsBySkuNo(10001L, "FBA-1")).thenReturn(List.of(77L)); when(orders.findOrderIdsBySkuNo(10001L, "FBA-1")).thenReturn(List.of(77L));
when(firstleg.page(eq("internal-token"), any())) when(firstleg.page(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 1, 20, 0, 0))); .thenReturn(ApiResponse.success(new PageResult<>(List.of(), 1, 20, 0, 0)));
@@ -959,7 +959,7 @@ class UnifiedOrderQueryServiceTest {
UnifiedOrderQuery query = new UnifiedOrderQuery( UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, "UNKNOWN", null, null, null, null, null, null, "INVALID", null, null, "UNKNOWN", null, null, null, null, null, null, "INVALID", null,
null, null, 1, 20); null, null, 1, 20, null);
assertThatThrownBy(() -> service.page(10001L, query)) assertThatThrownBy(() -> service.page(10001L, query))
.isInstanceOf(BizException.class); .isInstanceOf(BizException.class);
@@ -970,7 +970,44 @@ class UnifiedOrderQueryServiceTest {
private UnifiedOrderQuery query(String status) { private UnifiedOrderQuery query(String status) {
return new UnifiedOrderQuery(null, null, status, null, null, null, return new UnifiedOrderQuery(null, null, status, 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, 1, 20); null, null, 1, 20, null);
}
private UnifiedOrderQuery taxQuery(String status, String taxMode) {
return new UnifiedOrderQuery(null, null, status, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, 2, 1, taxMode);
}
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.NullSource
@org.junit.jupiter.params.provider.ValueSource(strings = {"TAX_REFUND", "BUY_DECLARATION"})
void taxModeRetainsTenantAndPaginationOnBothQueryBranches(String taxMode) {
var direct = taxQuery(null, taxMode);
when(orders.unifiedPage(eq(10001L), eq(direct), any()))
.thenReturn(new PageResult<>(List.of(), 2, 1, 0, 0));
service.page(10001L, direct);
verify(orders).unifiedPage(eq(10001L), eq(direct), any());
org.mockito.Mockito.verify(firstleg, org.mockito.Mockito.never()).page(any(), any());
when(firstleg.page(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 2, 1, 0, 0)));
service.page(10001L, taxQuery("LOADED", taxMode));
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captured = ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
verify(firstleg).page(eq("internal-token"), captured.capture());
assertThat(captured.getValue().taxMode()).isEqualTo(taxMode);
assertThat(captured.getValue().tenantId()).isEqualTo(10001L);
assertThat(captured.getValue().page()).isEqualTo(2);
assertThat(captured.getValue().size()).isEqualTo(1);
}
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(strings = {"", " ", "tax_refund", "TAX_REFUND ", "OTHER", "TAX_INCLUDED"})
void illegalTaxModeFailsBeforeEitherRepositoryOrFirstleg(String taxMode) {
for (String status : List.of("PENDING_REVIEW", "LOADED")) {
assertThatThrownBy(() -> service.page(10001L, taxQuery(status, taxMode)))
.isInstanceOf(BizException.class).hasMessageContaining("taxMode");
}
org.mockito.Mockito.verifyNoInteractions(orders, firstleg);
} }
private void assertServiceUnavailable(org.assertj.core.api.ThrowableAssert.ThrowingCallable invocation) { private void assertServiceUnavailable(org.assertj.core.api.ThrowableAssert.ThrowingCallable invocation) {