This commit is contained in:
2026-10-09 17:36:24 +08:00
parent 74bf0e73c5
commit 261cdc87cb
27 changed files with 636 additions and 45 deletions
@@ -22,7 +22,7 @@ public interface FirstlegUnifiedOrderClient {
record WorkbenchFilterRequest(long tenantId, String mainStatus) {}
record WorkbenchFilters(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
List<Long> customsOrderIds) {}
List<Long> customsOrderIds, List<com.baiyue.order.dto.MerchantOrderProgress> progress) {}
@PostMapping("/query")
ApiResponse<PageResult<Summary>> page(@RequestHeader("X-Internal-Service-Token") String token,
@@ -0,0 +1,16 @@
package com.baiyue.order.client;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.order.dto.MerchantOrderProgress;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(name="lastleg-service", contextId="orderLastlegProgressClient",
url="${baiyue.clients.lastleg.url:}", path="/internal/lastleg/v1/customer-order-progress")
public interface LastlegOrderProgressClient {
@PostMapping
ApiResponse<Response> read(@RequestHeader("X-Internal-Service-Token") String token, @RequestBody Request request);
record Request(long customerId) {}
record Response(List<MerchantOrderProgress> progress) {}
}
@@ -0,0 +1,11 @@
package com.baiyue.order.dto;
/** Customer progress is a read projection, not another writable order state machine. */
public record MerchantOrderProgress(long orderId, String stage, boolean partial) {
public static final java.util.List<String> STAGES = java.util.List.of("none", "loaded", "declared", "cleared", "scheduled", "delivering", "delivered");
public int rank() {
int rank = STAGES.indexOf(stage);
if (rank < 0 || orderId <= 0) throw new IllegalArgumentException("Invalid order progress");
return rank;
}
}
@@ -13,7 +13,7 @@ public record MerchantOrderWorkbenchQuery(
@Size(max=128) String trackingNumber,
@Pattern(regexp="DRAFT|PENDING_REVIEW|WAIT_MEASURE|WAIT_PALLETIZE|WAIT_SPLIT|LOADING|LOADED|DELIVERED|COMPLETED|CANCELLED") String mainStatus,
LocalDateTime createdFrom, LocalDateTime createdTo,
@Pattern(regexp="all|fee|customs|draft") String queue,
@Pattern(regexp="all|fee|customs|draft|loaded|declared|cleared|scheduled|delivering|delivered") String queue,
@Min(1) long page, @Min(1) @Max(200) long size) {
public MerchantOrderWorkbenchQuery {
if (page==0) page=1;
@@ -4,6 +4,14 @@ import com.baiyue.common.core.api.PageResult;
import java.util.List;
public record MerchantOrderWorkbenchResponse(PageResult<UnifiedOrderResponse.Row> page,
Counts queues, List<String> channelNames) {
public record Counts(long all, long fee, long customs, long draft) {}
Counts queues, List<String> channelNames, List<MerchantOrderProgress> progress) {
public record Counts(long all, long fee, long customs, long draft, long loaded, long declared,
long cleared, long scheduled, long delivering, long delivered) {
public long forQueue(String queue) {
return switch(queue) { case "fee" -> fee; case "customs" -> customs; case "draft" -> draft;
case "loaded" -> loaded; case "declared" -> declared; case "cleared" -> cleared;
case "scheduled" -> scheduled; case "delivering" -> delivering; case "delivered" -> delivered;
case "all" -> all; default -> throw new IllegalArgumentException("Invalid queue"); };
}
}
}
@@ -21,14 +21,18 @@ public class MerchantOrderWorkbenchRepository {
@Transactional(readOnly=true)
public Result read(long customerId, MerchantOrderWorkbenchQuery q, List<Long> statusIds,
List<Long> feeIds, List<Long> customsIds) {
List<Long> feeIds, List<Long> customsIds, java.util.Map<String,List<Long>> progressIds) {
var base=base(customerId,q,statusIds);
long all=mapper.selectCount(base.clone());
long fee=mapper.selectCount(ids(base.clone(),feeIds));
long customs=mapper.selectCount(ids(base.clone(),customsIds));
long draft=mapper.selectCount(base.clone().eq("status","DRAFT"));
var counts=new Counts(all,fee,customs,draft);
long total=switch(q.queue()) {case "fee"->fee;case "customs"->customs;case "draft"->draft;default->all;};
var counts=new Counts(all,fee,customs,draft,
progressCount(base,progressIds,"loaded"), progressCount(base,progressIds,"declared"),
progressCount(base,progressIds,"cleared"), progressCount(base,progressIds,"scheduled"),
progressCount(base,progressIds,"delivering"), progressCount(base,progressIds,"delivered"));
long total=counts.forQueue(q.queue());
if (progressIds.containsKey(q.queue())) ids(base,progressIds.get(q.queue()));
if ("fee".equals(q.queue())) ids(base,feeIds);
if ("customs".equals(q.queue())) ids(base,customsIds);
if ("draft".equals(q.queue())) base.eq("status","DRAFT");
@@ -43,6 +47,15 @@ public class MerchantOrderWorkbenchRepository {
total/q.size()+(total%q.size()==0?0:1)),counts,channels);
}
public List<OrderEntity> progressScope(long customerId) {
return mapper.selectList(new QueryWrapper<OrderEntity>().select("id","root_order_id","status")
.eq("tenant_id",customerId).ne("status","CANCELLED"));
}
private long progressCount(QueryWrapper<OrderEntity> base,java.util.Map<String,List<Long>> progressIds,String stage) {
return mapper.selectCount(ids(base.clone(),progressIds.get(stage)));
}
private QueryWrapper<OrderEntity> base(long customerId, MerchantOrderWorkbenchQuery q, List<Long> statusIds) {
var w=new QueryWrapper<OrderEntity>().eq("tenant_id",customerId);
String direct=switch(q.mainStatus()==null?"":q.mainStatus()) {
@@ -4,40 +4,109 @@ import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.FirstlegUnifiedOrderClient;
import com.baiyue.order.client.LastlegOrderProgressClient;
import com.baiyue.order.dto.MerchantOrderProgress;
import com.baiyue.order.dto.MerchantOrderWorkbenchQuery;
import com.baiyue.order.dto.MerchantOrderWorkbenchResponse;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.MerchantOrderWorkbenchRepository;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Set;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MerchantOrderWorkbenchService {
@Resource private MerchantOrderWorkbenchRepository repository;
@Resource private FirstlegUnifiedOrderClient firstleg;
@Resource private UnifiedOrderQueryService unified;
@Resource private LastlegOrderProgressClient lastleg;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
private static final Set<String> DIRECT=Set.of("DRAFT","PENDING_REVIEW","COMPLETED","CANCELLED");
@Transactional(readOnly=true)
public MerchantOrderWorkbenchResponse page(long customerId,MerchantOrderWorkbenchQuery query) {
if(query.createdFrom()!=null&&query.createdTo()!=null&&query.createdFrom().isAfter(query.createdTo()))
throw new BizException(ResultCode.BAD_REQUEST,"创建时间起止范围不合法");
String status=query.mainStatus()==null||DIRECT.contains(query.mainStatus())?null:query.mainStatus();
FirstlegUnifiedOrderClient.WorkbenchFilters filters;
List<MerchantOrderProgress> tailProgress;
try {
var response=firstleg.workbenchFilters(internalToken,
new FirstlegUnifiedOrderClient.WorkbenchFilterRequest(customerId,status));
if(response==null||!response.successful()||response.data()==null) throw new IllegalStateException();
filters=response.data();
if(filters.feeOrderIds()==null||filters.customsOrderIds()==null
if(filters.feeOrderIds()==null||filters.customsOrderIds()==null||filters.progress()==null
||status!=null&&filters.matchingStatusOrderIds()==null) throw new IllegalStateException();
var tail=lastleg.read(internalToken,new LastlegOrderProgressClient.Request(customerId));
if(tail==null||!tail.successful()||tail.data()==null||tail.data().progress()==null) throw new IllegalStateException();
tailProgress=tail.data().progress();
validateFacts(filters.progress(),1,3);
validateFacts(tailProgress,4,5);
} catch(RuntimeException failure) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"订单待办查询暂时不可用,请重试");
}
var result=repository.read(customerId,query,filters.matchingStatusOrderIds(),filters.feeOrderIds(),filters.customsOrderIds());
var progress=project(repository.progressScope(customerId),filters.progress(),tailProgress);
var progressIds=new LinkedHashMap<String,List<Long>>();
for(String stage:MerchantOrderProgress.STAGES.subList(1,7))
progressIds.put(stage,progress.values().stream().filter(p->stage.equals(p.stage())).map(MerchantOrderProgress::orderId).toList());
var result=repository.read(customerId,query,filters.matchingStatusOrderIds(),filters.feeOrderIds(),filters.customsOrderIds(),progressIds);
var source=result.page();
return new MerchantOrderWorkbenchResponse(new PageResult<>(unified.merchantWorkbenchRows(customerId,source.records()),
source.page(),source.size(),source.total(),source.pages()),result.queues(),result.channelNames());
source.page(),source.size(),source.total(),source.pages()),result.queues(),result.channelNames(),
source.records().stream().map(o->progress.getOrDefault(o.getId(),new MerchantOrderProgress(o.getId(),"none",false))).toList());
}
private static void validateFacts(List<MerchantOrderProgress> facts, int min, int max) {
var seen=new java.util.HashSet<Long>();
for(var fact:facts) if(fact==null||fact.rank()<min||fact.rank()>max||!seen.add(fact.orderId()))
throw new IllegalStateException("Invalid progress response");
}
static Map<Long,MerchantOrderProgress> project(
List<OrderEntity> scope,
List<MerchantOrderProgress> firstleg,
List<MerchantOrderProgress> lastleg) {
var scopeById=scope.stream().collect(Collectors.toMap(OrderEntity::getId,o->o));
var facts=new HashMap<Long,MerchantOrderProgress>();
Stream.concat(firstleg.stream(),lastleg.stream()).forEach(p->
facts.merge(p.orderId(),p,(a,b)->a.rank()>b.rank()?a:b));
var result=new LinkedHashMap<Long,MerchantOrderProgress>();
for(var order:scope) {
var status=order.getStatus();
var fact=status==OrderStatus.COMPLETED
? new MerchantOrderProgress(order.getId(),"delivered",false)
: status==OrderStatus.DRAFT||status==OrderStatus.SUBMITTED
? new MerchantOrderProgress(order.getId(),"none",false)
: facts.getOrDefault(order.getId(),new MerchantOrderProgress(order.getId(),"none",false));
result.put(order.getId(),fact);
}
var children=scope.stream().filter(o->o.getRootOrderId()!=null && !o.getRootOrderId().equals(o.getId()))
.collect(Collectors.groupingBy(OrderEntity::getRootOrderId));
children.forEach((root,orders)->{
var own=result.get(root);
if(own==null||"delivered".equals(own.stage())) return;
var rootOrder=scopeById.get(root);
if(rootOrder.getStatus()==OrderStatus.DRAFT||rootOrder.getStatus()==OrderStatus.SUBMITTED) return;
var fragments=new ArrayList<MerchantOrderProgress>();
fragments.add(own); orders.forEach(o->fragments.add(result.get(o.getId())));
// Completion remains exclusively owned by Order's trusted root completion gate.
int max=fragments.stream().mapToInt(MerchantOrderProgress::rank).max().orElse(0);
if(max==6) max=5;
final int rank=max;
boolean partial=fragments.stream().anyMatch(p->p.rank()<rank||p.partial());
result.put(root,new MerchantOrderProgress(root,
MerchantOrderProgress.STAGES.get(rank),partial));
});
return result;
}
}
+21 -4
View File
@@ -11,7 +11,7 @@ paths:
summary: 客户订单工作台分页和筛选后待办数量
description: >-
仅按登录客户查询,权限merchant:order:list。草稿优先,再按创建时间和订单ID降序稳定分页。
入仓码/SKU采用或匹配;所有条件先筛选再计数。四类数量共用当前筛选,不从当前页行数计算。
入仓码/SKU采用或匹配;所有条件先筛选再计数。十类数量共用当前筛选,不从当前页行数计算。
默认包含草稿和有效正式单;履约子状态以Firstleg为准,待审核和已完成以Order为准。
security: [{bearerAuth: []}]
parameters:
@@ -23,7 +23,7 @@ paths:
- {name: mainStatus, in: query, schema: {type: string, enum: [DRAFT, PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED, CANCELLED]}}
- {name: createdFrom, in: query, schema: {type: string, format: date-time}}
- {name: createdTo, in: query, schema: {type: string, format: date-time}}
- {name: queue, in: query, schema: {type: string, enum: [all, fee, customs, draft], default: all}}
- {name: queue, in: query, schema: {type: string, enum: [all, fee, customs, draft, loaded, declared, cleared, scheduled, delivering, delivered], default: all}}
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 200, default: 10}}
responses:
@@ -37,18 +37,35 @@ paths:
code: {type: string}
data:
type: object
required: [page, queues, channelNames]
required: [page, queues, channelNames, progress]
properties:
page: {$ref: '#/components/schemas/UnifiedOrderPage'}
queues:
type: object
required: [all, fee, customs, draft]
required: [all, fee, customs, draft, loaded, declared, cleared, scheduled, delivering, delivered]
properties:
all: {type: integer, format: int64, minimum: 0}
fee: {type: integer, format: int64, minimum: 0}
customs: {type: integer, format: int64, minimum: 0}
draft: {type: integer, format: int64, minimum: 0}
loaded: {type: integer, format: int64, minimum: 0}
declared: {type: integer, format: int64, minimum: 0}
cleared: {type: integer, format: int64, minimum: 0}
scheduled: {type: integer, format: int64, minimum: 0}
delivering: {type: integer, format: int64, minimum: 0}
delivered: {type: integer, format: int64, minimum: 0}
channelNames: {type: array, items: {type: string}, description: 当前客户完整渠道筛选项,不限于当前页}
progress:
type: array
description: 与当前页订单一一对应的客户履约进度;分类互斥,待办可重叠。分片未齐时partial为true,完成只认Order可信回写。
items:
type: object
required: [orderId, stage, partial]
properties:
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
stage: {type: string, enum: [none, loaded, declared, cleared, scheduled, delivering, delivered]}
partial: {type: boolean}
/internal/order/v1/fulfillment-completions:
post:
security: []
@@ -68,11 +68,16 @@ class MerchantOrderWorkbenchRepositoryTest {
private MerchantOrderWorkbenchQuery query(String keyword,String queue,long page) {
return new MerchantOrderWorkbenchQuery(keyword,null,null,null,null,null,null,null,queue,page,10);
}
private java.util.Map<String,List<Long>> progressIds() {
var map=new java.util.LinkedHashMap<String,List<Long>>();
for(String key:List.of("loaded","declared","cleared","scheduled","delivering","delivered")) map.put(key,List.of());
return map;
}
@Test void readsBeyondTheOldTwoHundredLimitWithStableDraftPrefixAndCorrectTotals() {
var first=repository.read(11,query(null,"all",1),null,List.of(210L),List.of(211L));
var first=repository.read(11,query(null,"all",1),null,List.of(210L),List.of(211L), progressIds());
assertThat(first.page().total()).isEqualTo(212);
assertThat(first.page().records().subList(0,3)).extracting(OrderEntity::getId).containsExactly(3L,2L,1L);
var last=repository.read(11,query(null,"all",22),null,List.of(210L),List.of(211L));
var last=repository.read(11,query(null,"all",22),null,List.of(210L),List.of(211L), progressIds());
assertThat(last.page().records()).extracting(OrderEntity::getId).containsExactly(5L,4L);
assertThat(last.page().pages()).isEqualTo(22);
assertThat(last.queues().all()).isEqualTo(212);
@@ -80,31 +85,44 @@ class MerchantOrderWorkbenchRepositoryTest {
assertThat(last.queues().fee()).isEqualTo(1);
assertThat(last.queues().customs()).isEqualTo(1);
assertThat(last.channelNames()).containsExactly("AIR","SEA");
assertThat(repository.read(11,query(null,"all",23),null,List.of(),List.of()).page().records()).isEmpty();
assertThat(repository.read(11,query(null,"all",23),null,List.of(),List.of(), progressIds()).page().records()).isEmpty();
}
@Test void filtersBeforeCountingAndPagingAndNeverUsesForeignSkuOrQueueIds() {
var sku=repository.read(11,query("SPECIAL","all",1),null,List.of(210L,214L),List.of(215L));
var sku=repository.read(11,query("SPECIAL","all",1),null,List.of(210L,214L),List.of(215L), progressIds());
assertThat(sku.page().total()).isEqualTo(1);
assertThat(sku.page().records()).extracting(OrderEntity::getId).containsExactly(210L);
assertThat(sku.queues().fee()).isEqualTo(1);assertThat(sku.queues().customs()).isZero();
var fee=repository.read(11,query(null,"fee",1),null,List.of(210L,214L),List.of());
var fee=repository.read(11,query(null,"fee",1),null,List.of(210L,214L),List.of(), progressIds());
assertThat(fee.page().records()).extracting(OrderEntity::getId).containsExactly(210L);
assertThat(repository.read(11,query(null,"fee",1),null,List.of(),List.of()).page().records()).isEmpty();
var drafts=repository.read(11,query(null,"draft",1),null,List.of(),List.of());
assertThat(repository.read(11,query(null,"fee",1),null,List.of(),List.of(), progressIds()).page().records()).isEmpty();
var drafts=repository.read(11,query(null,"draft",1),null,List.of(),List.of(), progressIds());
assertThat(drafts.page().total()).isEqualTo(3);
}
@Test void combinesWarehouseChannelDestinationTrackingDatesAndStatusBeforePaging() {
var from=java.time.LocalDateTime.of(2026,10,2,0,0);
var to=from.plusDays(1).minusNanos(1);
var q=new MerchantOrderWorkbenchQuery("IN-210",10L,"SEA","FBA","TRACK-210","LOADED",from,to,"all",1,10);
assertThat(repository.read(11,q,List.of(210L,214L),List.of(),List.of()).page().records())
assertThat(repository.read(11,q,List.of(210L,214L),List.of(),List.of(), progressIds()).page().records())
.extracting(OrderEntity::getId).containsExactly(210L);
assertThat(repository.read(11,q,List.of(),List.of(),List.of()).page().total()).isZero();
assertThat(repository.read(11,q,List.of(),List.of(),List.of(), progressIds()).page().total()).isZero();
jdbc.update("UPDATE ord_order SET status='SUBMITTED' WHERE id=210");
assertThat(repository.read(11,q,List.of(210L),List.of(),List.of()).page().total()).isZero();
assertThat(repository.read(11,q,List.of(210L),List.of(),List.of(), progressIds()).page().total()).isZero();
var pending=new MerchantOrderWorkbenchQuery(null,null,null,null,null,"PENDING_REVIEW",null,null,"all",1,10);
assertThat(repository.read(11,pending,null,List.of(),List.of()).page().records()).extracting(OrderEntity::getId).containsExactly(210L);
assertThat(repository.read(11,pending,null,List.of(),List.of(), progressIds()).page().records()).extracting(OrderEntity::getId).containsExactly(210L);
var outside=new MerchantOrderWorkbenchQuery(null,null,null,null,null,null,to.plusDays(1),null,"all",1,10);
assertThat(repository.read(11,outside,null,List.of(),List.of()).page().total()).isZero();
assertThat(repository.read(11,outside,null,List.of(),List.of(), progressIds()).page().total()).isZero();
}
@Test void progressQueuesShareFiltersAndCountBeforePagination() {
var ids=progressIds();ids.put("cleared",List.of(210L,211L,214L));ids.put("delivering",List.of(212L));
var result=repository.read(11,query(null,"cleared",1),null,List.of(210L),List.of(),ids);
assertThat(result.page().total()).isEqualTo(2);
assertThat(result.queues().cleared()).isEqualTo(2);
assertThat(result.queues().delivering()).isEqualTo(1);
assertThat(result.page().records()).extracting(OrderEntity::getId).containsExactly(211L,210L);
var filtered=repository.read(11,query("SPECIAL","cleared",1),null,List.of(210L),List.of(),ids);
assertThat(filtered.page().total()).isEqualTo(1);
assertThat(filtered.queues().fee()).isEqualTo(1);
assertThat(repository.progressScope(11)).hasSize(212);
}
}
@@ -19,11 +19,14 @@ import static org.mockito.ArgumentMatchers.*;
class MerchantOrderWorkbenchServiceTest {
private final FirstlegUnifiedOrderClient firstleg = mock(FirstlegUnifiedOrderClient.class);
private final MerchantOrderWorkbenchRepository repository = mock(MerchantOrderWorkbenchRepository.class);
private final com.baiyue.order.client.LastlegOrderProgressClient lastleg = mock(com.baiyue.order.client.LastlegOrderProgressClient.class);
private final UnifiedOrderQueryService unified = mock(UnifiedOrderQueryService.class);
private final MerchantOrderWorkbenchService service = new MerchantOrderWorkbenchService();
@BeforeEach void setup() {
ReflectionTestUtils.setField(service, "firstleg", firstleg);
ReflectionTestUtils.setField(service, "lastleg", lastleg);
when(lastleg.read(anyString(),any())).thenReturn(ApiResponse.success(new com.baiyue.order.client.LastlegOrderProgressClient.Response(List.of())));
ReflectionTestUtils.setField(service, "repository", repository);
ReflectionTestUtils.setField(service, "unified", unified);
ReflectionTestUtils.setField(service, "internalToken", "test-service-token");
@@ -33,10 +36,10 @@ class MerchantOrderWorkbenchServiceTest {
}
@Test void combinesScopedFiltersWithDatabasePageWithoutReplacingTotals() {
var q = query("LOADED");
var filters = new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(1L),List.of(2L),List.of(3L));
var filters = new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(1L),List.of(2L),List.of(3L), List.of());
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(filters));
when(repository.read(11,q,filters.matchingStatusOrderIds(),filters.feeOrderIds(),filters.customsOrderIds()))
.thenReturn(new MerchantOrderWorkbenchRepository.Result(new PageResult<>(List.of(),22,10,210,21),new Counts(210,2,1,0),List.of("SEA")));
when(repository.read(eq(11L),eq(q),eq(filters.matchingStatusOrderIds()),eq(filters.feeOrderIds()),eq(filters.customsOrderIds()),anyMap()))
.thenReturn(new MerchantOrderWorkbenchRepository.Result(new PageResult<>(List.of(),22,10,210,21),new Counts(210,2,1,0,0,0,0,0,0,0),List.of("SEA")));
when(unified.merchantWorkbenchRows(11,List.of())).thenReturn(List.of());
var result = service.page(11,q);
assertThat(result.page().total()).isEqualTo(210);
@@ -47,9 +50,10 @@ class MerchantOrderWorkbenchServiceTest {
}
@Test void unavailableOrIncompleteFulfillmentFiltersCannotProduceMisleadingEmptyQueues() {
for(var filters : List.of(
new FirstlegUnifiedOrderClient.WorkbenchFilters(null,List.of(),List.of()),
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),null,List.of()),
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),List.of(),null))) {
new FirstlegUnifiedOrderClient.WorkbenchFilters(null,List.of(),List.of(), List.of()),
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),null,List.of(), List.of()),
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),List.of(),null, List.of()),
new FirstlegUnifiedOrderClient.WorkbenchFilters(List.of(),List.of(),List.of(),null))) {
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(filters));
assertThatThrownBy(()->service.page(11,query("LOADED"))).isInstanceOf(BizException.class).hasMessageContaining("暂时不可用");
}
@@ -63,4 +67,29 @@ class MerchantOrderWorkbenchServiceTest {
assertThatThrownBy(()->service.page(11,q)).isInstanceOf(BizException.class).hasMessageContaining("时间");
verifyNoInteractions(firstleg,repository,unified);
}
@Test void tailFailureDoesNotReturnPartialQueueCounts() {
when(firstleg.workbenchFilters(anyString(),any())).thenReturn(ApiResponse.success(
new FirstlegUnifiedOrderClient.WorkbenchFilters(null,List.of(),List.of(),List.of())));
when(lastleg.read(anyString(),any())).thenThrow(new IllegalStateException());
assertThatThrownBy(()->service.page(11,query(null))).isInstanceOf(BizException.class);
verifyNoInteractions(repository,unified);
}
private com.baiyue.order.entity.OrderEntity order(long id,Long root,com.baiyue.order.entity.OrderStatus status) {
var o=new com.baiyue.order.entity.OrderEntity();o.setId(id);o.setRootOrderId(root);o.setStatus(status);return o;
}
@Test void currentStageIsExclusiveAndRootRequiresEveryFragmentWithTrustedCompletion() {
var status=com.baiyue.order.entity.OrderStatus.APPROVED;
var scope=List.of(order(1L,1L,status),order(2L,1L,status),order(3L,null,com.baiyue.order.entity.OrderStatus.COMPLETED),
order(4L,null,com.baiyue.order.entity.OrderStatus.SUBMITTED));
var first=List.of(new com.baiyue.order.dto.MerchantOrderProgress(1L,"cleared",false),
new com.baiyue.order.dto.MerchantOrderProgress(2L,"loaded",false));
var tail=List.of(new com.baiyue.order.dto.MerchantOrderProgress(2L,"delivering",false),
new com.baiyue.order.dto.MerchantOrderProgress(4L,"scheduled",false),
new com.baiyue.order.dto.MerchantOrderProgress(999L,"delivering",false));
var projected=MerchantOrderWorkbenchService.project(scope,first,tail);
assertThat(projected.get(1L).stage()).isEqualTo("delivering");assertThat(projected.get(1L).partial()).isTrue();
assertThat(projected.get(2L).partial()).isFalse();assertThat(projected.get(3L).stage()).isEqualTo("delivered");
assertThat(projected.get(4L).stage()).isEqualTo("none");assertThat(projected).doesNotContainKey(999L);
}
}