fix(order): paginate merchant workbench with scoped queue counts
This commit is contained in:
+9
@@ -43,6 +43,7 @@ public class InternalInboundOrderController {
|
||||
private InboundCartonLabelService cartonLabels;
|
||||
@Resource
|
||||
private UnifiedInboundQueryService unifiedQueryService;
|
||||
@Resource private com.baiyue.firstleg.service.MerchantWorkbenchFilterService workbenchFilters;
|
||||
@Resource
|
||||
private OrderDeclarationSyncService declarationSync;
|
||||
@Resource
|
||||
@@ -148,6 +149,14 @@ public class InternalInboundOrderController {
|
||||
return ApiResponse.success(remarksSync.sync(command));
|
||||
}
|
||||
|
||||
@PostMapping("/unified-orders/workbench-filters")
|
||||
public ApiResponse<com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Response> workbenchFilters(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request request) {
|
||||
requireToken(token);
|
||||
return ApiResponse.success(workbenchFilters.read(request));
|
||||
}
|
||||
|
||||
@PostMapping("/unified-orders/query")
|
||||
public ApiResponse<PageResult<UnifiedInboundSummary>> unifiedPage(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.util.List;
|
||||
|
||||
/** Customer-scoped filter identifiers; never a replacement order list or cross-schema query. */
|
||||
public final class MerchantWorkbenchFilters {
|
||||
private MerchantWorkbenchFilters() {}
|
||||
|
||||
public record Request(@NotNull @Positive Long tenantId,
|
||||
@Pattern(regexp = "WAIT_MEASURE|WAIT_PALLETIZE|WAIT_SPLIT|LOADING|LOADED|DELIVERED") String mainStatus) {}
|
||||
|
||||
public record Response(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
||||
List<Long> customsOrderIds) {}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface MerchantWorkbenchFilterMapper {
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT order_id FROM fl_inbound_order
|
||||
WHERE tenant_id=#{tenantId} AND deleted=0 AND order_id IS NOT NULL
|
||||
AND process_status IN
|
||||
<foreach collection="statuses" item="status" open="(" separator="," close=")">#{status}</foreach>
|
||||
ORDER BY order_id
|
||||
</script>
|
||||
""")
|
||||
List<Long> byProcessStatuses(@Param("tenantId") long tenantId, @Param("statuses") List<String> statuses);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT i.order_id
|
||||
FROM fl_inbound_order i
|
||||
JOIN fl_inbound_fee f ON f.tenant_id=i.tenant_id AND f.inbound_id=i.id
|
||||
WHERE i.tenant_id=#{tenantId} AND i.deleted=0 AND i.order_id IS NOT NULL
|
||||
AND f.fee_status='SENT' AND f.customer_confirmed_at IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_inbound_fee newer
|
||||
WHERE newer.tenant_id=f.tenant_id AND newer.inbound_id=f.inbound_id
|
||||
AND (newer.fee_version>f.fee_version OR (newer.fee_version=f.fee_version AND newer.id>f.id)))
|
||||
ORDER BY i.order_id
|
||||
""")
|
||||
List<Long> pendingFees(@Param("tenantId") long tenantId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT i.order_id
|
||||
FROM fl_inbound_order i
|
||||
JOIN fl_loading_plan p ON p.id=(
|
||||
SELECT latest.id FROM fl_loading_plan latest
|
||||
JOIN fl_loading_plan_item li ON li.loading_plan_id=latest.id AND li.deleted=0
|
||||
JOIN fl_inbound_order own ON own.id=li.inbound_id AND own.tenant_id=li.account_id AND own.deleted=0
|
||||
WHERE latest.plan_status='COMPLETED' AND own.tenant_id=i.tenant_id
|
||||
AND own.order_id=i.order_id AND own.tax_mode='TAX_REFUND'
|
||||
ORDER BY COALESCE(latest.confirmed_at,latest.updated_at) DESC,latest.id DESC LIMIT 1
|
||||
)
|
||||
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
|
||||
LEFT JOIN fl_customs_container c ON c.loading_plan_id=p.id
|
||||
WHERE i.tenant_id=#{tenantId} AND i.deleted=0 AND i.tax_mode='TAX_REFUND'
|
||||
AND i.process_status IN ('LOADED','DELIVERED')
|
||||
AND (c.id IS NULL OR c.status='WAITING_CUSTOMER_DOCUMENTS')
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item item
|
||||
JOIN fl_inbound_order member ON member.id=item.inbound_id
|
||||
AND member.tenant_id=item.account_id AND member.deleted=0
|
||||
WHERE item.loading_plan_id=p.id AND item.deleted=0
|
||||
AND member.tenant_id=i.tenant_id AND member.tax_mode='TAX_REFUND'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_customs_customer_document_inbound scope
|
||||
JOIN fl_customs_customer_document d ON d.id=scope.document_id
|
||||
AND d.customs_container_id=c.id AND d.tenant_id=i.tenant_id AND d.status='ACTIVE'
|
||||
WHERE scope.inbound_id=member.id AND scope.tenant_id=i.tenant_id
|
||||
)
|
||||
)
|
||||
ORDER BY i.order_id
|
||||
""")
|
||||
List<Long> pendingCustoms(@Param("tenantId") long tenantId);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.dto.MerchantWorkbenchFilters;
|
||||
import com.baiyue.firstleg.mapper.MerchantWorkbenchFilterMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class MerchantWorkbenchFilterService {
|
||||
@Resource private MerchantWorkbenchFilterMapper mapper;
|
||||
@Resource private UnifiedInboundQueryService unified;
|
||||
|
||||
public MerchantWorkbenchFilters.Response read(MerchantWorkbenchFilters.Request request) {
|
||||
return new MerchantWorkbenchFilters.Response(
|
||||
request.mainStatus() == null ? null : mapper.byProcessStatuses(request.tenantId(),
|
||||
unified.internalStatuses(request.mainStatus())),
|
||||
mapper.pendingFees(request.tenantId()), mapper.pendingCustoms(request.tenantId()));
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,7 @@ public class UnifiedInboundQueryService {
|
||||
return mainStatus != null && Set.of("LOADING", "LOADED", "DELIVERED").contains(mainStatus);
|
||||
}
|
||||
|
||||
private List<String> internalStatuses(String mainStatus) {
|
||||
List<String> internalStatuses(String mainStatus) {
|
||||
if (!StringUtils.hasText(mainStatus)) return List.of();
|
||||
return switch (mainStatus) {
|
||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
|
||||
@@ -352,6 +352,37 @@ paths:
|
||||
operatorName: {type: string, nullable: true, maxLength: 128}
|
||||
responses:
|
||||
'200': {description: data 返回已同步的预装明细数量}
|
||||
/internal/firstleg/v1/inbounds/unified-orders/workbench-filters:
|
||||
post:
|
||||
summary: 查询单一客户工作台履约筛选标识(Order内部)
|
||||
description: 只读返回命中状态、最新待确认费用及待上传资料的订单标识。客户必须明确且为正数;不创建报关聚合,不跨Schema读取。
|
||||
security: [{internalServiceToken: []}]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [tenantId]
|
||||
properties:
|
||||
tenantId: {type: integer, format: int64, minimum: 1}
|
||||
mainStatus: {type: string, nullable: true, enum: [WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]}
|
||||
responses:
|
||||
'200':
|
||||
description: 按响应code判断,标识只供Order在自身客户范围内过滤、计数和分页
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
data:
|
||||
type: object
|
||||
required: [feeOrderIds, customsOrderIds]
|
||||
properties:
|
||||
matchingStatusOrderIds: {type: array, nullable: true, items: {type: integer, format: int64}}
|
||||
feeOrderIds: {type: array, items: {type: integer, format: int64}}
|
||||
customsOrderIds: {type: array, items: {type: integer, format: int64}}
|
||||
/internal/firstleg/v1/inbounds/unified-orders/query:
|
||||
post:
|
||||
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
||||
|
||||
+11
@@ -40,9 +40,20 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
@MockBean
|
||||
private OrderRemarksSyncService remarksSync;
|
||||
@MockBean private com.baiyue.firstleg.service.OrderFulfillmentSnapshotService fulfillmentSnapshot;
|
||||
@MockBean private com.baiyue.firstleg.service.MerchantWorkbenchFilterService workbenchFilters;
|
||||
@Resource
|
||||
private InternalInboundOrderController controller;
|
||||
|
||||
@Test void workbenchFiltersRejectMissingAndWrongInternalIdentity() {
|
||||
var request = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request(11L, "LOADED");
|
||||
assertThatThrownBy(() -> controller.workbenchFilters(null, request)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> controller.workbenchFilters("wrong", request)).isInstanceOf(BizException.class);
|
||||
org.mockito.Mockito.verifyNoInteractions(workbenchFilters);
|
||||
var response = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Response(List.of(1L), List.of(), List.of());
|
||||
when(workbenchFilters.read(request)).thenReturn(response);
|
||||
assertThat(controller.workbenchFilters("internal-token", request).data()).isEqualTo(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionSnapshotRequiresInternalTokenAndDelegatesExactCommand() {
|
||||
var command = new com.baiyue.firstleg.dto.OrderFulfillmentCompleted(11, 21,
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.firstleg.mapper.MerchantWorkbenchFilterMapper;
|
||||
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 java.util.List;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MerchantWorkbenchFilterMapperTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private MerchantWorkbenchFilterMapper mapper;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var ds=new JdbcDataSource();ds.setURL("jdbc:h2:mem:workbench-facts-"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
jdbc=new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE fl_inbound_order(id BIGINT PRIMARY KEY,tenant_id BIGINT,order_id BIGINT,deleted INT DEFAULT 0,tax_mode VARCHAR(32),process_status VARCHAR(32))");
|
||||
jdbc.execute("CREATE TABLE fl_inbound_fee(id BIGINT PRIMARY KEY,tenant_id BIGINT,inbound_id BIGINT,fee_version INT,fee_status VARCHAR(32),customer_confirmed_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_plan(id BIGINT PRIMARY KEY,container_booking_id BIGINT,plan_status VARCHAR(32),confirmed_at TIMESTAMP,updated_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_plan_item(loading_plan_id BIGINT,inbound_id BIGINT,account_id BIGINT,deleted INT DEFAULT 0)");
|
||||
jdbc.execute("CREATE TABLE fl_container_booking(id BIGINT PRIMARY KEY,record_status VARCHAR(32))");
|
||||
jdbc.execute("CREATE TABLE fl_customs_container(id BIGINT PRIMARY KEY,loading_plan_id BIGINT,status VARCHAR(40))");
|
||||
jdbc.execute("CREATE TABLE fl_customs_customer_document(id BIGINT PRIMARY KEY,customs_container_id BIGINT,tenant_id BIGINT,status VARCHAR(32))");
|
||||
jdbc.execute("CREATE TABLE fl_customs_customer_document_inbound(document_id BIGINT,inbound_id BIGINT,tenant_id BIGINT)");
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(MerchantWorkbenchFilterMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(MerchantWorkbenchFilterMapper.class);
|
||||
jdbc.update("INSERT INTO fl_inbound_order VALUES(1,11,101,0,'TAX_REFUND','LOADED'),(2,11,102,0,'TAX_REFUND','DELIVERED'),(3,22,103,0,'TAX_REFUND','LOADED'),(4,11,104,1,'TAX_REFUND','LOADED'),(5,11,105,0,'BUY_DECLARATION','LOADED')");
|
||||
jdbc.update("INSERT INTO fl_container_booking VALUES(100,'ACTIVE')");
|
||||
jdbc.update("INSERT INTO fl_loading_plan VALUES(100,100,'COMPLETED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)");
|
||||
jdbc.update("INSERT INTO fl_loading_plan_item VALUES(100,1,11,0),(100,1,11,0),(100,2,11,0),(100,3,22,0),(100,4,11,0),(100,5,11,0)");
|
||||
}
|
||||
|
||||
@Test void latestFeeVersionAndCustomerScopeDetermineConfirmationQueue() {
|
||||
jdbc.update("INSERT INTO fl_inbound_fee VALUES(10,11,1,1,'SENT',NULL),(11,11,1,2,'DRAFT',NULL),(12,11,2,1,'SENT',NULL),(13,22,3,1,'SENT',NULL),(14,11,4,1,'SENT',NULL)");
|
||||
assertThat(mapper.pendingFees(11)).containsExactly(102L);
|
||||
jdbc.update("INSERT INTO fl_inbound_fee VALUES(15,11,2,1,'SENT',CURRENT_TIMESTAMP)");
|
||||
assertThat(mapper.pendingFees(11)).isEmpty();
|
||||
assertThat(mapper.pendingFees(22)).containsExactly(103L);
|
||||
assertThat(mapper.byProcessStatuses(11,List.of("LOADED"))).containsExactly(101L,105L);
|
||||
}
|
||||
|
||||
@Test void absentAggregateIsReadOnlyAndWaitingForOwnDocumentsNotAnotherCustomer() {
|
||||
assertThat(mapper.pendingCustoms(11)).containsExactly(101L,102L);
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM fl_customs_container",Integer.class)).isZero();
|
||||
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'WAITING_CUSTOMER_DOCUMENTS')");
|
||||
jdbc.update("INSERT INTO fl_customs_customer_document VALUES(201,200,11,'ACTIVE')");
|
||||
jdbc.update("INSERT INTO fl_customs_customer_document_inbound VALUES(201,1,11)");
|
||||
assertThat(mapper.pendingCustoms(11)).containsExactly(101L,102L);
|
||||
jdbc.update("INSERT INTO fl_customs_customer_document_inbound VALUES(201,2,11)");
|
||||
assertThat(mapper.pendingCustoms(11)).isEmpty();
|
||||
assertThat(mapper.pendingCustoms(22)).containsExactly(103L);
|
||||
}
|
||||
|
||||
@Test void foreignOrDeletedDocumentsCannotSatisfyOwnCoverageAndClosedContainersAreExcluded() {
|
||||
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'WAITING_CUSTOMER_DOCUMENTS')");
|
||||
jdbc.update("INSERT INTO fl_customs_customer_document VALUES(201,200,22,'ACTIVE'),(202,200,11,'DELETED')");
|
||||
jdbc.update("INSERT INTO fl_customs_customer_document_inbound VALUES(201,1,11),(202,2,11)");
|
||||
assertThat(mapper.pendingCustoms(11)).containsExactly(101L,102L);
|
||||
for(String status:List.of("READY_TO_START","DECLARING","COMPLETED")) {
|
||||
jdbc.update("UPDATE fl_customs_container SET status=?",status);
|
||||
assertThat(mapper.pendingCustoms(11)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Test void onlyLatestCompletedPlanCanCreateTheCurrentUploadQueue() {
|
||||
jdbc.update("INSERT INTO fl_customs_container VALUES(200,100,'WAITING_CUSTOMER_DOCUMENTS')");
|
||||
jdbc.update("INSERT INTO fl_container_booking VALUES(101,'ACTIVE')");
|
||||
jdbc.update("INSERT INTO fl_loading_plan VALUES(101,101,'COMPLETED',TIMESTAMP '2099-01-01 00:00:00',CURRENT_TIMESTAMP)");
|
||||
jdbc.update("INSERT INTO fl_loading_plan_item VALUES(101,1,11,0)");
|
||||
jdbc.update("INSERT INTO fl_customs_container VALUES(201,101,'COMPLETED')");
|
||||
assertThat(mapper.pendingCustoms(11)).containsExactly(102L);
|
||||
jdbc.update("UPDATE fl_container_booking SET record_status='CANCELLED' WHERE id=100");
|
||||
assertThat(mapper.pendingCustoms(11)).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user