fix(orders): filter tax mode before unified order pagination
This commit is contained in:
@@ -34,7 +34,7 @@ public interface FirstlegUnifiedOrderClient {
|
||||
String feeReviewStatus, String channelName, String containerNo, String cabinetNo,
|
||||
LocalDate sailingDate, Boolean loaded, Boolean fullContainer,
|
||||
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 DetailRequest(long tenantId, long orderId) {}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -301,6 +301,7 @@ public class OrderRepository {
|
||||
List<OrderStatus> statuses) {
|
||||
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
|
||||
.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())
|
||||
|
||||
+1
-1
@@ -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+" 必须为正整数字符串");} }
|
||||
|
||||
@@ -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<FirstlegUnifiedOrderClient.Summary> source = require(
|
||||
call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效");
|
||||
List<FirstlegUnifiedOrderClient.Summary> 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())
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+82
@@ -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(
|
||||
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<FirstlegUnifiedOrderClient.Query> 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<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) {
|
||||
|
||||
Reference in New Issue
Block a user