fix(order): project exact customer fulfillment progress in detail
This commit is contained in:
@@ -20,7 +20,7 @@ public interface FirstlegUnifiedOrderClient {
|
||||
ApiResponse<WorkbenchFilters> workbenchFilters(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody WorkbenchFilterRequest request);
|
||||
|
||||
record WorkbenchFilterRequest(long tenantId, String mainStatus) {}
|
||||
record WorkbenchFilterRequest(long tenantId, String mainStatus, List<Long> orderIds) {}
|
||||
record WorkbenchFilters(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
||||
List<Long> customsOrderIds, List<com.baiyue.order.dto.MerchantOrderProgress> progress) {}
|
||||
|
||||
|
||||
@@ -11,6 +11,6 @@ import java.util.List;
|
||||
public interface LastlegOrderProgressClient {
|
||||
@PostMapping
|
||||
ApiResponse<Response> read(@RequestHeader("X-Internal-Service-Token") String token, @RequestBody Request request);
|
||||
record Request(long customerId) {}
|
||||
record Request(long customerId, List<Long> orderIds) {}
|
||||
record Response(List<MerchantOrderProgress> progress) {}
|
||||
}
|
||||
|
||||
@@ -51,10 +51,13 @@ public class MerchantUnifiedOrderController {
|
||||
@GetMapping("/{orderId}")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
@Operation(summary = "我的统一订单详情", description = "仅返回当前客户订单,不包含平台内部备注。")
|
||||
public ApiResponse<UnifiedOrderResponse.Detail> detail(
|
||||
public ApiResponse<UnifiedOrderResponse.MerchantDetail> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long orderId) {
|
||||
return ApiResponse.success(service.detail(OrderScopeGuard.merchant(user), orderId, false));
|
||||
long customerId=OrderScopeGuard.merchant(user);
|
||||
var detail=service.detail(customerId,orderId,false);
|
||||
return ApiResponse.success(new UnifiedOrderResponse.MerchantDetail(detail.summary(),detail.order(),detail.timeline(),
|
||||
detail.transportFragments(),workbench.detailProgress(customerId,orderId)));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/fulfillment-tree")
|
||||
|
||||
@@ -50,6 +50,9 @@ public final class UnifiedOrderResponse {
|
||||
public record Detail(Row summary, OrderDetailResponse order,
|
||||
List<TimelineEvent> timeline,
|
||||
List<TransportFragment> transportFragments) {}
|
||||
public record MerchantDetail(Row summary, OrderDetailResponse order,
|
||||
List<TimelineEvent> timeline, List<TransportFragment> transportFragments,
|
||||
MerchantOrderProgress progress) {}
|
||||
public record TransportFragment(String inboundCode, String cabinetNo, String containerNo,
|
||||
List<TransportNode> nodes) {}
|
||||
public record TransportNode(String standardNode, Integer nodeSequence, String providerCode,
|
||||
|
||||
@@ -51,6 +51,17 @@ public class MerchantOrderWorkbenchRepository {
|
||||
return mapper.selectList(new QueryWrapper<OrderEntity>().select("id","root_order_id","status")
|
||||
.eq("tenant_id",customerId).ne("status","CANCELLED"));
|
||||
}
|
||||
/** Exact owned order and, only when it is a root, its own customer's active fragments. */
|
||||
public List<OrderEntity> detailProgressScope(long customerId,long orderId) {
|
||||
var selected=mapper.selectOne(new QueryWrapper<OrderEntity>().select("id","root_order_id","status")
|
||||
.eq("tenant_id",customerId).eq("id",orderId));
|
||||
if(selected==null) return List.of();
|
||||
if(selected.getRootOrderId()!=null && selected.getRootOrderId()!=orderId) return List.of(selected);
|
||||
var scope=new java.util.ArrayList<OrderEntity>();scope.add(selected);
|
||||
scope.addAll(mapper.selectList(new QueryWrapper<OrderEntity>().select("id","root_order_id","status")
|
||||
.eq("tenant_id",customerId).eq("root_order_id",orderId).ne("id",orderId).ne("status","CANCELLED").orderByAsc("id")));
|
||||
return List.copyOf(scope);
|
||||
}
|
||||
|
||||
private long progressCount(QueryWrapper<OrderEntity> base,java.util.Map<String,List<Long>> progressIds,String stage) {
|
||||
return mapper.selectCount(ids(base.clone(),progressIds.get(stage)));
|
||||
|
||||
@@ -42,12 +42,12 @@ public class MerchantOrderWorkbenchService {
|
||||
List<MerchantOrderProgress> tailProgress;
|
||||
try {
|
||||
var response=firstleg.workbenchFilters(internalToken,
|
||||
new FirstlegUnifiedOrderClient.WorkbenchFilterRequest(customerId,status));
|
||||
new FirstlegUnifiedOrderClient.WorkbenchFilterRequest(customerId,status,null));
|
||||
if(response==null||!response.successful()||response.data()==null) throw new IllegalStateException();
|
||||
filters=response.data();
|
||||
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));
|
||||
var tail=lastleg.read(internalToken,new LastlegOrderProgressClient.Request(customerId,null));
|
||||
if(tail==null||!tail.successful()||tail.data()==null||tail.data().progress()==null) throw new IllegalStateException();
|
||||
tailProgress=tail.data().progress();
|
||||
validateFacts(filters.progress(),1,3);
|
||||
@@ -66,6 +66,34 @@ public class MerchantOrderWorkbenchService {
|
||||
source.records().stream().map(o->progress.getOrDefault(o.getId(),new MerchantOrderProgress(o.getId(),"none",false))).toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly=true)
|
||||
public MerchantOrderProgress detailProgress(long customerId,long orderId) {
|
||||
var scope=repository.detailProgressScope(customerId,orderId);
|
||||
var selected=scope.stream().filter(o->o.getId()==orderId).findFirst()
|
||||
.orElseThrow(()->new BizException(ResultCode.NOT_FOUND,"订单不存在"));
|
||||
if(selected.getStatus()==OrderStatus.CANCELLED) return new MerchantOrderProgress(orderId,"none",false);
|
||||
if(Set.of(OrderStatus.DRAFT,OrderStatus.SUBMITTED,OrderStatus.COMPLETED).contains(selected.getStatus()))
|
||||
return project(scope,List.of(),List.of()).get(orderId);
|
||||
var firstFacts=new ArrayList<MerchantOrderProgress>();var tailFacts=new ArrayList<MerchantOrderProgress>();
|
||||
var ids=scope.stream().map(OrderEntity::getId).toList();
|
||||
try {
|
||||
for(int start=0;start<ids.size();start+=200) {
|
||||
var batch=ids.subList(start,Math.min(start+200,ids.size()));
|
||||
var first=firstleg.workbenchFilters(internalToken,new FirstlegUnifiedOrderClient.WorkbenchFilterRequest(customerId,null,batch));
|
||||
if(first==null || !first.successful() || first.data()==null || first.data().progress()==null) throw new IllegalStateException();
|
||||
var tail=lastleg.read(internalToken,new LastlegOrderProgressClient.Request(customerId,batch));
|
||||
if(tail==null || !tail.successful() || tail.data()==null || tail.data().progress()==null) throw new IllegalStateException();
|
||||
validateFacts(first.data().progress(),1,3);validateFacts(tail.data().progress(),4,5);
|
||||
if(Stream.concat(first.data().progress().stream(),tail.data().progress().stream()).anyMatch(p->!batch.contains(p.orderId())))
|
||||
throw new IllegalStateException("Progress response exceeds owned order scope");
|
||||
firstFacts.addAll(first.data().progress());tailFacts.addAll(tail.data().progress());
|
||||
}
|
||||
} catch(RuntimeException failure) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"订单进度查询暂时不可用,请重试");
|
||||
}
|
||||
return project(scope,firstFacts,tailFacts).get(orderId);
|
||||
}
|
||||
|
||||
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()))
|
||||
|
||||
@@ -58,13 +58,7 @@ paths:
|
||||
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}
|
||||
items: {$ref: '#/components/schemas/MerchantOrderProgress'}
|
||||
|
||||
/internal/order/v1/fulfillment-completions:
|
||||
post:
|
||||
@@ -538,7 +532,7 @@ paths:
|
||||
/api/merchant/v1/orders/{orderId}:
|
||||
get:
|
||||
summary: 客户统一订单详情
|
||||
description: 仅返回登录客户所属订单及客户可见时间线,不返回平台内部备注。
|
||||
description: 仅返回登录客户所属订单及客户可见时间线,不返回平台内部备注。progress使用与工作台相同的权威投影,按认证客户和精确订单读取;根单只汇总本客户有效子单,子单只投影自身。依赖不可用时失败,不回退delivery快照。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UnifiedOrderId'
|
||||
responses:
|
||||
@@ -546,7 +540,7 @@ paths:
|
||||
description: 成功或业务失败,按响应体 code 判断
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/UnifiedOrderDetailEnvelope'}
|
||||
schema: {$ref: '#/components/schemas/MerchantOrderDetailEnvelope'}
|
||||
'403': {description: 需要 merchant:order:view 权限}
|
||||
/api/merchant/v1/orders/first-leg:
|
||||
get:
|
||||
@@ -1138,6 +1132,26 @@ components:
|
||||
size: {type: integer, format: int64, minimum: 1}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
MerchantOrderProgress:
|
||||
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}
|
||||
MerchantOrderDetailEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string}
|
||||
data: {$ref: '#/components/schemas/MerchantOrderDetail'}
|
||||
MerchantOrderDetail:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/UnifiedOrderDetail'}
|
||||
- type: object
|
||||
required: [progress]
|
||||
properties:
|
||||
progress: {$ref: '#/components/schemas/MerchantOrderProgress'}
|
||||
UnifiedOrderDetailEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user