diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java index dfa3fdff9..ccea8652c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/UnifiedInboundQuery.java @@ -35,7 +35,9 @@ public record UnifiedInboundQuery( LocalDateTime createdFrom, LocalDateTime createdTo, @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 { if (page == 0) page = 1; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java index 00ea515d7..cd38a4284 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundOrderRepository.java @@ -118,6 +118,7 @@ public class InboundOrderRepository { public Page pageUnifiedEntities(UnifiedInboundQuery query, List processStatuses) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(query.tenantId() != null, InboundOrderEntity::getTenantId, query.tenantId()) + .eq(query.taxMode() != null, InboundOrderEntity::getTaxMode, query.taxMode()) .in(processStatuses != null && !processStatuses.isEmpty(), InboundOrderEntity::getProcessStatus, processStatuses) .like(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo()) .like(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode, query.inboundCode()) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java index 7a6787457..e4848fdd0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/UnifiedInboundQueryService.java @@ -222,6 +222,10 @@ public class UnifiedInboundQueryService { } 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()) && !MAIN_STATUSES.contains(query.mainStatus()) ) { diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 026d8a674..d70822b29 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -1557,6 +1557,7 @@ components: UnifiedInboundQuery: type: object properties: + taxMode: {type: string, enum: [TAX_REFUND, BUY_DECLARATION], nullable: true, description: 按税务模式在数据库计数和分页前过滤;未传不限制} tenantId: {type: integer, format: int64, nullable: true} keyword: {type: string, maxLength: 64, nullable: true} orderNo: {type: string, maxLength: 64, nullable: true} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java index 63985272b..59a083a37 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalInboundOrderReferenceControllerTest.java @@ -79,7 +79,7 @@ class InternalInboundOrderReferenceControllerTest { UnifiedInboundQuery request = new UnifiedInboundQuery( 10001L, null, null, "WAIT_SPLIT", 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)); assertThat(controller.unifiedPage("internal-token", request).data().records()).isEmpty(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java index 99ef11f61..2199be89a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundOrderRepositoryTest.java @@ -57,7 +57,7 @@ class InboundOrderRepositoryTest { 10001L, null, null, "LOADING", 30001L, 40001L, 50001L, "RK-", "APPROVED", LocalDate.of(2026, 8, 6), "APPROVED", "美森", "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")); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/UnifiedInboundTaxModePaginationTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/UnifiedInboundTaxModePaginationTest.java new file mode 100644 index 000000000..a6d8dcd1c --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/UnifiedInboundTaxModePaginationTest.java @@ -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(); + 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); + }); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java index 6bcd6c210..e67875c2f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/UnifiedInboundQueryServiceTest.java @@ -24,6 +24,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; 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 void mapsAllCompatibleInternalStatesToProductMainStatuses() { assertThat(UnifiedInboundQueryService.mainStatus("WAIT_MEASURE")).isEqualTo("WAIT_MEASURE"); @@ -121,7 +133,7 @@ class UnifiedInboundQueryServiceTest { UnifiedInboundQuery query = new UnifiedInboundQuery( null, null, null, null, null, null, null, null, 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)) .isInstanceOf(BizException.class); diff --git a/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java b/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java index 87ea63aec..0faf2484f 100644 --- a/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java +++ b/order/src/main/java/com/baiyue/order/client/FirstlegUnifiedOrderClient.java @@ -34,7 +34,7 @@ public interface FirstlegUnifiedOrderClient { String feeReviewStatus, String channelName, String containerNo, String cabinetNo, LocalDate sailingDate, Boolean loaded, Boolean fullContainer, String measurementReviewStatus, List candidateOrderIds, - LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size) {} + LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size, String taxMode) {} record BatchRequest(Long tenantId, List orderIds) {} record DetailRequest(long tenantId, long orderId) {} diff --git a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java index c052e646d..39fd95ede 100644 --- a/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java +++ b/order/src/main/java/com/baiyue/order/dto/UnifiedOrderQuery.java @@ -33,7 +33,9 @@ public record UnifiedOrderQuery( LocalDateTime createdFrom, LocalDateTime createdTo, @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 { if (page == 0) page = 1; diff --git a/order/src/main/java/com/baiyue/order/repository/OrderRepository.java b/order/src/main/java/com/baiyue/order/repository/OrderRepository.java index 34f5670ad..d329f517b 100644 --- a/order/src/main/java/com/baiyue/order/repository/OrderRepository.java +++ b/order/src/main/java/com/baiyue/order/repository/OrderRepository.java @@ -301,6 +301,7 @@ public class OrderRepository { List statuses) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(tenantId != null, OrderEntity::getTenantId, tenantId) + .eq(query.taxMode() != null, OrderEntity::getTaxMode, query.taxMode()) .in(OrderEntity::getStatus, statuses) .like(StringUtils.hasText(query.orderNo()), OrderEntity::getOrderNo, query.orderNo()) .like(StringUtils.hasText(query.inboundCode()), OrderEntity::getInboundCode, query.inboundCode()) diff --git a/order/src/main/java/com/baiyue/order/service/DomesticWarehouseOrderExportProviderService.java b/order/src/main/java/com/baiyue/order/service/DomesticWarehouseOrderExportProviderService.java index 0abda5e62..7283fc5c2 100644 --- a/order/src/main/java/com/baiyue/order/service/DomesticWarehouseOrderExportProviderService.java +++ b/order/src/main/java/com/baiyue/order/service/DomesticWarehouseOrderExportProviderService.java @@ -282,7 +282,7 @@ public class DomesticWarehouseOrderExportProviderService { string("inboundCode"), string("internalOrderStatus"), date("expectedArrivalDate"), string("feeReviewStatus"), string("channelName"), string("containerNo"), string("cabinetNo"), 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 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+" 必须为正整数字符串");} } diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index 2b9c264ca..c266bc268 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -159,7 +159,7 @@ public class UnifiedOrderQueryService { query.internalOrderStatus(), query.expectedArrivalDate(), query.feeReviewStatus(), query.channelName(), query.containerNo(), query.cabinetNo(), query.sailingDate(), query.loaded(), query.fullContainer(), query.measurementReviewStatus(), candidateOrderIds, query.createdFrom(), query.createdTo(), - query.page(), query.size()); + query.page(), query.size(), query.taxMode()); PageResult source = require( call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效"); List summaries = validateSummaries( @@ -555,6 +555,9 @@ public class UnifiedOrderQueryService { } 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()) && !MAIN_STATUSES.contains(query.mainStatus()) ) { diff --git a/order/src/main/resources/openapi/order-v1.yaml b/order/src/main/resources/openapi/order-v1.yaml index c1ea54b99..b5a7fdad2 100644 --- a/order/src/main/resources/openapi/order-v1.yaml +++ b/order/src/main/resources/openapi/order-v1.yaml @@ -329,6 +329,7 @@ paths: relation(仅有关联时返回;PARENT 含 childCount,CHILD 含 rootOrderId、parentOrderId 与 splitSequence)。 coLoadGroup 仅平台响应返回,由 Order 经 Firstleg 内部批量只读契约聚合;成员业务标识由 Order 权威订单片段补齐。 parameters: + - $ref: '#/components/parameters/UnifiedTaxMode' - {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - $ref: '#/components/parameters/UnifiedKeyword' - $ref: '#/components/parameters/UnifiedOrderNo' @@ -444,6 +445,7 @@ paths: 有入库单时同时返回 inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion 才是 C04 loading/complete 的 expectedVersion,loadingVersion 当前等于 pickingVersion(兼容别名)。 parameters: + - $ref: '#/components/parameters/UnifiedTaxMode' - $ref: '#/components/parameters/UnifiedKeyword' - $ref: '#/components/parameters/UnifiedOrderNo' - $ref: '#/components/parameters/UnifiedMainStatus' @@ -1488,6 +1490,12 @@ components: in: query required: false schema: {type: string, maxLength: 64} + UnifiedTaxMode: + name: taxMode + in: query + required: false + description: 严格按税务模式在数据库计数和分页前过滤;未传查询全部税务模式,空串或其他值拒绝 + schema: {type: string, enum: [TAX_REFUND, BUY_DECLARATION]} UnifiedOrderNo: name: orderNo in: query diff --git a/order/src/test/java/com/baiyue/order/repository/UnifiedOrderTaxModePaginationTest.java b/order/src/test/java/com/baiyue/order/repository/UnifiedOrderTaxModePaginationTest.java new file mode 100644 index 000000000..7d53ae1ac --- /dev/null +++ b/order/src/test/java/com/baiyue/order/repository/UnifiedOrderTaxModePaginationTest.java @@ -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(); + 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); + }); + } +} diff --git a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java index f65377b3d..aaf0e2840 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -292,7 +292,7 @@ class UnifiedOrderQueryServiceTest { UnifiedOrderQuery query = new UnifiedOrderQuery( null, null, "PENDING_REVIEW", null, null, null, "RK-77", 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)))) .thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); @@ -512,7 +512,7 @@ class UnifiedOrderQueryServiceTest { UnifiedOrderQuery query = new UnifiedOrderQuery( null, null, null, null, null, null, "RK-77", 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())) .thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0)); @@ -785,7 +785,7 @@ class UnifiedOrderQueryServiceTest { UnifiedOrderQuery query = new UnifiedOrderQuery( null, null, "INBOUNDED", 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); when(summary.inbound().holdFlag()).thenReturn(false); when(summary.inbound().currentHoldReason()).thenReturn(null); @@ -909,7 +909,7 @@ class UnifiedOrderQueryServiceTest { statuses.forEach(status -> service.page(null, new UnifiedOrderQuery( "CC-2026", "FL-77", status, 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 captor = ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class); @@ -931,7 +931,7 @@ class UnifiedOrderQueryServiceTest { null, null, "LOADING", 30001L, 11L, 22L, "RK-77", "APPROVED", LocalDate.of(2026, 8, 6), "APPROVED", "美森", "MSCU", "CAB-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(firstleg.page(eq("internal-token"), any())) .thenReturn(ApiResponse.success(new PageResult<>(List.of(), 1, 20, 0, 0))); @@ -959,7 +959,7 @@ class UnifiedOrderQueryServiceTest { UnifiedOrderQuery query = new UnifiedOrderQuery( null, null, null, null, null, null, null, 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)) .isInstanceOf(BizException.class); @@ -970,7 +970,44 @@ class UnifiedOrderQueryServiceTest { private UnifiedOrderQuery query(String status) { return new UnifiedOrderQuery(null, null, status, 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 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) {