From c962f614e64b3ec6698d633faeafdec5fb4585a0 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 21 Oct 2026 16:40:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SmartDispatchController.java | 2 +- .../mapper/DispatchCandidatePageMapper.java | 3 +- .../service/DispatchAggregateService.java | 3 +- .../service/OutsourcedBusinessService.java | 8 +- .../DispatchVehicleDocumentController.java | 76 ++++ .../model/OwnedBusinessScope.java | 9 +- .../service/OwnedBusinessProjection.java | 3 +- .../service/OwnedBusinessQueryService.java | 3 +- .../dispatch-vehicle-documents-v1.yaml | 379 ++++++++++++++++++ .../openapi/overseas-business-v1.yaml | 3 +- .../SmartDispatchOpenApiContractTest.java | 3 + .../SmartDispatchControllerTest.java | 10 + .../DispatchRegionalCandidatesTest.java | 47 +++ scripts/agents-index-contract.ps1 | 138 +++++-- 14 files changed, 635 insertions(+), 52 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/controller/DispatchVehicleDocumentController.java create mode 100644 lastleg-service/src/main/resources/openapi/dispatch-vehicle-documents-v1.yaml create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRegionalCandidatesTest.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java index 41a988862..223270899 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java @@ -26,7 +26,7 @@ public class SmartDispatchController { /** 读取实际候选,不在草稿域复制来源筛选逻辑。 */ @GetMapping("/candidates") @PreAuthorize("hasAuthority('"+OverseasPermissions.DISPATCH_LIST+"')") - public ApiResponse candidates(@AuthenticationPrincipal AuthenticatedUser user,@RequestParam String warehouseId, + public ApiResponse candidates(@AuthenticationPrincipal AuthenticatedUser user,@RequestParam(required=false) String warehouseId, @RequestParam(required=false)List planIds,@RequestParam(required=false)String orderNo, @RequestParam(required=false)String destinationWarehouseCode,@RequestParam(required=false)String sourceStage,@RequestParam(required=false)String draftToken, @RequestParam(defaultValue="1")int page,@RequestParam(defaultValue="20")int size,@RequestParam(defaultValue="ALL")String warehouseTab) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidatePageMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidatePageMapper.java index 3e92404a7..307e5bc80 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidatePageMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidatePageMapper.java @@ -50,7 +50,8 @@ public interface DispatchCandidatePageMapper { } /** 仓库页签也是服务端筛选,分页后不能再用本地筛选伪装完整结果。 */ private static String filtered(boolean count) { - return " FROM ("+rows(count)+") candidates WHERE overseasWarehouseId=#{query.warehouseId}" + return " FROM ("+rows(count)+") candidates WHERE 1=1" + +" AND overseasWarehouseId=#{query.warehouseId}" +" AND fbaWarehouseCode=#{query.destinationWarehouseCode}" +" AND LOWER(CONCAT(COALESCE(overseasWarehouseCode,''),' ',COALESCE(overseasWarehouseName,''))) REGEXP 'lax|洛杉矶|los angeles'" +" AND LOWER(CONCAT(COALESCE(overseasWarehouseCode,''),' ',COALESCE(overseasWarehouseName,''))) REGEXP 'nyc|jfk|(^|[^a-z])ny([^a-z]|$)|纽约|new[ -]?york'"; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java index a97d8c45b..3693e5a3b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java @@ -146,7 +146,8 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA @Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ) public DispatchPlanningModels.CandidateList candidates(Actor actor,DispatchPlanningModels.CandidateQuery query) { requirePlatform(actor); - if(query==null || query.warehouseId()==null || !query.warehouseId().matches("[1-9][0-9]*")) throw bad("请选择真实海外起运仓"); + // 未指定起运仓时按区域浏览;写入草稿仍由来源确定唯一真实起运仓。 + if(query==null || query.warehouseId()!=null && !query.warehouseId().matches("[1-9][0-9]*")) throw bad("起运仓筛选无效"); if(query.planIds()!=null && (query.planIds().size()>500 || query.planIds().stream().anyMatch(id->id==null || id<=0))) throw bad("计划筛选不合法"); if(query.page()<1 || query.size()<1 || query.size()>50 || !Set.of("ALL","LAX","NYC").contains(Objects.toString(query.warehouseTab(),"")))throw bad("候选分页或仓库页签不合法"); var normalized=new DispatchPlanningModels.CandidateQuery(query.warehouseId(),query.planIds(),trim(query.orderNo()), diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/outsourcedbusiness/service/OutsourcedBusinessService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/outsourcedbusiness/service/OutsourcedBusinessService.java index 185b14bae..620a910f2 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/outsourcedbusiness/service/OutsourcedBusinessService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/outsourcedbusiness/service/OutsourcedBusinessService.java @@ -37,7 +37,7 @@ public class OutsourcedBusinessService { public HistoryPage history(OwnedBusinessModels.Actor actor,String number,Integer page,Integer size) { var result=queries.history(actor,number,page,size,SCOPE); return new HistoryPage(result.records().stream().map(h->new History(h.eventSource(),h.recordVersion(),h.dispatchRecordVersion(),h.action(),h.operatorName(), - h.occurredAt(),h.reason(),h.operations()==null?null:operations(h.operations()),h.photos().stream().map(this::photo).toList())).toList(),result.page(),result.size(),result.total()); + h.occurredAt(),h.reason(),h.operations()==null?null:operations(h.operations()),h.photos().stream().filter(p->"LOADING".equals(p.category())).map(this::photo).toList())).toList(),result.page(),result.size(),result.total()); } /** 外配只生成金额和到达的规范内部请求,不接收归属或隐藏油耗写入。 */ public Detail save(OwnedBusinessModels.Actor actor,String number,SaveOperations request) { @@ -71,11 +71,11 @@ public class OutsourcedBusinessService { } /** 回执公开形状不带内部里程、油耗或油耗照片摘要。 */ private Detail detail(OwnedBusinessModels.Detail detail) { - return new Detail(row(detail.row(),projection.carrierName(detail)),detail.dispatch(),detail.cargo(),detail.photos().stream().map(this::photo).toList()); + return new Detail(row(detail.row(),projection.carrierName(detail)),detail.dispatch(),detail.cargo(),detail.photos().stream().filter(p->"LOADING".equals(p.category())).map(this::photo).toList()); } /** 临时车辆合法保留空vehicleId;承运商缺失保留null供页面显示破折号。 */ private Row row(OwnedBusinessModels.Row r,String carrierName) { - if(!java.util.Set.of("OWNED","DISPATCHED").contains(r.vehicle().ownership()) || r.photoSummary().fuelCount()!=0)throw conflict("外配运营快照的车辆归属或照片不合法"); + if(!java.util.Set.of("OWNED","DISPATCHED").contains(r.vehicle().ownership()))throw conflict("外配运营快照的车辆归属或照片不合法"); return new Row(r.dispatchNo(),r.stage(),r.plannedDepartureAt(),r.loadingCompletedAt(),r.departedAt(),r.deliveryCompletedAt(),r.origin(), new Vehicle(r.vehicle().vehicleId(),r.vehicle().plate(),r.vehicle().ownership(),carrierName==null || carrierName.isBlank()?null:carrierName),r.driver(),r.destinations(), r.actualPalletCount(),operations(r.operations()),r.unitPrice(),r.recordVersion(),r.dispatchRecordVersion(),new PhotoSummary(r.photoSummary().loadingCount()),r.actions(),r.sourceIssues()); @@ -85,7 +85,7 @@ public class OutsourcedBusinessService { if(ops.actualDistanceMiles()!=null || ops.fuelConsumptionLitresPer100Km()!=null)throw conflict("外配业务不能包含里程或油耗登记"); return new Operations(ops.money(),ops.arrivals()); } - /** 外配照片枚举只有五框,异常油耗历史不能借复用向外输出。 */ + /** 外配照片枚举只有五框,排车油耗图片保留在共用记录中但不在外配入口输出。 */ private Photo photo(OwnedBusinessModels.Photo p) { SCOPE.requireSlot(p.slot());return new Photo(PhotoSlot.valueOf(p.slot().name()),p.category(),p.fileId(),p.fileName(),p.contentType(),p.sizeBytes(),p.uploadedAt(),p.savedAt()); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/controller/DispatchVehicleDocumentController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/controller/DispatchVehicleDocumentController.java new file mode 100644 index 000000000..e098d7cc4 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/controller/DispatchVehicleDocumentController.java @@ -0,0 +1,76 @@ +package com.baiyue.lastleg.fleet.ownedbusiness.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*; +import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessScope; +import com.baiyue.lastleg.fleet.ownedbusiness.service.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import org.springframework.http.*; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.util.List; + +/** 排车资料入口复用车队原照片记录;签收及BOL仍由派送单管理。 */ +@RestController +@Validated +@RequestMapping("/api/platform/v1/last-leg/dispatches/{dispatchNo}/vehicle-documents") +public class DispatchVehicleDocumentController { + private static final OwnedBusinessScope SCOPE=OwnedBusinessScope.DISPATCH; + private final OwnedBusinessQueryService queries; + private final OwnedBusinessPhotoService photos; + private final OwnedBusinessSupport access; + /** 沿已有服务复用来源校验、文件归属、版本和命令账本。 */ + public DispatchVehicleDocumentController(OwnedBusinessQueryService queries,OwnedBusinessPhotoService photos,OwnedBusinessSupport access) { + this.queries=queries;this.photos=photos;this.access=access; + } + public record Documents(String dispatchNo,String recordVersion,boolean editable,List photos) {} + /** 只输出油耗资料,避免把车队签收照片当作派送签收文件。 */ + private Documents documents(Detail value) { + return new Documents(value.row().dispatchNo(),value.row().recordVersion(),value.row().actions().upload().allowed(), + value.photos().stream().filter(p->"FUEL".equals(p.category())).toList()); + } + /** 排车列表权限包含车辆资料只读,不扩大派送单客户访问权限。 */ + @GetMapping + @PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:list')") + public ApiResponse get(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo) { + return ApiResponse.success(documents(queries.detail(access.actor(user,SCOPE),dispatchNo,SCOPE))); + } + /** 上传和引用保存分开,沿原请求键防止重传。 */ + @PostMapping(value="/{slot}/uploads",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')") + public ApiResponse upload(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable PhotoSlot slot, + @RequestParam @Size(min=8,max=128) String idempotencyKey,@RequestPart("file") MultipartFile file) { + return ApiResponse.success(photos.upload(access.actor(user,SCOPE),dispatchNo,slot,idempotencyKey,file,SCOPE)); + } + /** 只更新既有附加资料版本,不修改排车阶段或签收状态。 */ + @PutMapping("/{slot}") + @PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')") + public ApiResponse bind(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable PhotoSlot slot,@Valid @RequestBody BindPhoto request) { + return ApiResponse.success(documents(photos.bind(access.actor(user,SCOPE),dispatchNo,slot,request,SCOPE))); + } + /** 预览只读取本车当前已经绑定的照片。 */ + @GetMapping("/{slot}/files/{fileId}/content") + @PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:list')") + public ResponseEntity content(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable PhotoSlot slot,@PathVariable @Positive long fileId) { + var value=photos.content(access.actor(user,SCOPE),dispatchNo,slot,fileId,null,SCOPE); + return ResponseEntity.ok().cacheControl(CacheControl.noStore()).contentType(MediaType.parseMediaType(value.photo().contentType())) + .header("X-Content-Type-Options","nosniff").body(value.content()); + } + /** 恢复仅返回油耗上传结果,不通过排车入口暴露其他运营命令。 */ + @GetMapping("/uploads/{idempotencyKey}") + @PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')") + public ApiResponse recover(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable String idempotencyKey) { + var actor=access.actor(user,SCOPE); + access.require(actor,SCOPE.uploadPermission()); + var command=photos.command(actor,dispatchNo,idempotencyKey,SCOPE); + if(!"UPLOAD_PHOTO".equals(command.action()) || !"SUCCEEDED".equals(command.state()) || command.result()==null) + throw OwnedBusinessSupport.conflict("原上传结果尚未确认,请保留原请求稍后核对"); + var value=access.read(access.write(command.result()),Photo.class);SCOPE.requireSlot(value.slot()); + return ApiResponse.success(value); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/model/OwnedBusinessScope.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/model/OwnedBusinessScope.java index fabd6344b..7f17f7c55 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/model/OwnedBusinessScope.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/model/OwnedBusinessScope.java @@ -7,7 +7,8 @@ import com.baiyue.common.core.api.ResultCode; /** 仅服务端固定入口选择的作用域;不得成为浏览器请求字段或权限别名。 */ public enum OwnedBusinessScope { OWNED(false,"platform:fleet:owned-business:"), - OUTSOURCED(true,"platform:fleet:outsourced-business:"); + OUTSOURCED(true,"platform:fleet:outsourced-business:"), + DISPATCH(false,"platform:overseas-warehouse:dispatch:"); private final boolean pooledCargo; private final String prefix; /** 业务入口按是否实际接入拼车货物分类,与车辆归属独立。 */ @@ -19,9 +20,9 @@ public enum OwnedBusinessScope { /** 运营写权限独立,外配权限不映射成自有权限。 */ public String editPermission(){return prefix+"edit";} /** 照片写权限独立,不扩大到原排车。 */ - public String uploadPermission(){return prefix+"upload";} + public String uploadPermission(){return prefix+(this==DISPATCH?"edit":"upload");} /** 外配只支持装车五框,服务调用同样不能绕过。 */ - public void requireSlot(PhotoSlot slot){if(slot==null || this==OUTSOURCED && !"LOADING".equals(slot.category()))throw new BizException(ResultCode.BAD_REQUEST,"当前业务不支持该照片框");} + public void requireSlot(PhotoSlot slot){if(slot==null || this==OUTSOURCED && !"LOADING".equals(slot.category()) || this==DISPATCH && !"FUEL".equals(slot.category()))throw new BizException(ResultCode.BAD_REQUEST,"当前业务不支持该照片框");} /** 不存在只显示当前业务归属,不泄露内部身份。 */ - public BizException missing(){return new BizException(ResultCode.NOT_FOUND,"排车单不存在或不属于当前"+(this==OWNED?"自有业务":"外配任务"));} + public BizException missing(){return new BizException(ResultCode.NOT_FOUND,this==DISPATCH?"排车单不存在":"排车单不存在或不属于当前"+(this==OWNED?"自有业务":"外配任务"));} } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessProjection.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessProjection.java index e32e526a7..d6edec772 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessProjection.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessProjection.java @@ -53,8 +53,7 @@ public class OwnedBusinessProjection { var resources=codec.read(source.getResourcesJson(),Resources.class); if(resources.vehicle()==null)throw OwnedBusinessSupport.conflict("原车辆快照缺失"); var ops=operations(record);var photos=photos(record);int pallets=palletCount(source); - if(scope==OwnedBusinessScope.OUTSOURCED && (ops.actualDistanceMiles()!=null || ops.fuelConsumptionLitresPer100Km()!=null - || photos.stream().anyMatch(p->!"LOADING".equals(p.category()))))throw OwnedBusinessSupport.conflict("外配运营记录含不支持的里程、油耗或照片"); + if(scope==OwnedBusinessScope.OUTSOURCED && (ops.actualDistanceMiles()!=null || ops.fuelConsumptionLitresPer100Km()!=null))throw OwnedBusinessSupport.conflict("外配运营记录含不支持的里程、油耗或照片"); var arrivals=new HashMap();for(var a:ops.arrivals())arrivals.put(a.destinationKey(),a.actualArrivedAt()); var destinations=new LinkedHashMap(); for(var item:cargo) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessQueryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessQueryService.java index f21ece7a1..05e4a0045 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessQueryService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/ownedbusiness/service/OwnedBusinessQueryService.java @@ -93,9 +93,10 @@ public class OwnedBusinessQueryService { if(source.getOwnedVehicleOwnership()==null)throw conflict("本单缺少装车时车辆归属证据,不能归入"+(scope==OwnedBusinessScope.OWNED?"自有":"外配")+"业务"); if(!Set.of("OWNED","DISPATCHED").contains(source.getOwnedVehicleOwnership()))throw conflict("装车车辆归属不合法"); if(source.getPooledCargo()==null)throw conflict("本单缺少实际拼车分类依据"); - if(scope.isPooledCargo()!=source.getPooledCargo())throw scope.missing(); + if(scope!=OwnedBusinessScope.DISPATCH && scope.isPooledCargo()!=source.getPooledCargo())throw scope.missing(); if(!"ACTIVE".equals(source.getStatus()) || source.getStage()==null || !Set.of("AFTER_LOADING","DELIVERING","DELIVERED").contains(source.getStage()) || source.getLoadingCompletedAt()==null || source.getLoadingConfirmationJson()==null)throw conflict("本单尚无合法装车完成事实"); + if(scope==OwnedBusinessScope.DISPATCH && !Set.of("DELIVERING","DELIVERED").contains(source.getStage()))throw conflict("仅派送中和配送完成可管理车辆资料"); var resource=codec.read(source.getResourcesJson(),DispatchContracts.Resources.class).vehicle(); boolean master=resource!=null && resource.mode()!=null && "MASTER".equals(resource.mode().name()) && "MASTER".equals(source.getOwnedResourceMode()) && resource.vehicleId()!=null && Objects.equals(source.getOwnedVehicleId(),resource.vehicleId()); diff --git a/lastleg-service/src/main/resources/openapi/dispatch-vehicle-documents-v1.yaml b/lastleg-service/src/main/resources/openapi/dispatch-vehicle-documents-v1.yaml new file mode 100644 index 000000000..1bc04a1e7 --- /dev/null +++ b/lastleg-service/src/main/resources/openapi/dispatch-vehicle-documents-v1.yaml @@ -0,0 +1,379 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "排车车辆资料", + "version": "1.0.0", + "description": "仅平台有效排车DELIVERING/DELIVERED可用;兼容有无拼车货物,沿装车归属证据校验。共享原车队照片、版本、命令与历史,不新建附件存储。派送文件仍使用delivery-orders documents;不把车队RECEIPT照片当签收。列表权限只读,排车edit独立写;恢复还需list。" + }, + "security": [ + { + "bearerAuth": [] + } + ], + "paths": { + "/api/platform/v1/last-leg/dispatches/{dispatchNo}/vehicle-documents": { + "get": { + "operationId": "getDispatchVehicleDocuments", + "summary": "读取本车油耗图片;复用车队附加记录", + "x-permission-code": "platform:overseas-warehouse:dispatch:list", + "parameters": [ + { + "$ref": "#/components/parameters/dispatchNo" + } + ], + "responses": { + "200": { + "description": "成功;业务错误沿ApiResponse返回A0400/A0403/A0404/A0409及诊断编号", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/Documents" + } + } + } + } + } + } + } + } + }, + "/api/platform/v1/last-leg/dispatches/{dispatchNo}/vehicle-documents/{slot}/uploads": { + "post": { + "operationId": "uploadDispatchVehiclePhoto", + "summary": "准备本车油耗图片;尚未绑定保存", + "x-permission-code": "platform:overseas-warehouse:dispatch:edit", + "parameters": [ + { + "$ref": "#/components/parameters/dispatchNo" + }, + { + "$ref": "#/components/parameters/slot" + } + ], + "responses": { + "200": { + "description": "成功;业务错误沿ApiResponse返回A0400/A0403/A0404/A0409及诊断编号", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/Photo" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file", + "idempotencyKey" + ], + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "非空JPG/PNG,最大10MiB" + }, + "idempotencyKey": { + "type": "string", + "minLength": 8, + "maxLength": 128 + } + } + } + } + } + } + } + }, + "/api/platform/v1/last-leg/dispatches/{dispatchNo}/vehicle-documents/{slot}": { + "put": { + "operationId": "bindDispatchVehiclePhoto", + "summary": "按附加版本绑定原上传文件,不改变排车或签收状态", + "x-permission-code": "platform:overseas-warehouse:dispatch:edit", + "parameters": [ + { + "$ref": "#/components/parameters/dispatchNo" + }, + { + "$ref": "#/components/parameters/slot" + } + ], + "responses": { + "200": { + "description": "成功;业务错误沿ApiResponse返回A0400/A0403/A0404/A0409及诊断编号", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/Documents" + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BindPhoto" + } + } + } + } + } + }, + "/api/platform/v1/last-leg/dispatches/{dispatchNo}/vehicle-documents/{slot}/files/{fileId}/content": { + "get": { + "operationId": "readDispatchVehiclePhoto", + "summary": "只读取本车当前绑定图片", + "x-permission-code": "platform:overseas-warehouse:dispatch:list", + "parameters": [ + { + "$ref": "#/components/parameters/dispatchNo" + }, + { + "$ref": "#/components/parameters/slot" + }, + { + "$ref": "#/components/parameters/fileId" + } + ], + "responses": { + "200": { + "description": "no-store图片内容", + "content": { + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/api/platform/v1/last-leg/dispatches/{dispatchNo}/vehicle-documents/uploads/{idempotencyKey}": { + "get": { + "operationId": "recoverDispatchVehiclePhoto", + "summary": "只恢复当前操作者原油耗上传成功结果,未知结果拒绝重传", + "x-permission-code": "platform:overseas-warehouse:dispatch:edit", + "parameters": [ + { + "$ref": "#/components/parameters/dispatchNo" + }, + { + "$ref": "#/components/parameters/idempotencyKey" + } + ], + "responses": { + "200": { + "description": "成功;业务错误沿ApiResponse返回A0400/A0403/A0404/A0409及诊断编号", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/Photo" + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "parameters": { + "dispatchNo": { + "name": "dispatchNo", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + "slot": { + "name": "slot", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "FUEL_START", + "FUEL_END" + ] + } + }, + "fileId": { + "name": "fileId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + "idempotencyKey": { + "name": "idempotencyKey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + }, + "schemas": { + "Photo": { + "type": "object", + "required": [ + "slot", + "fileId", + "fileName", + "contentType", + "sizeBytes", + "uploadedAt" + ], + "properties": { + "slot": { + "type": "string", + "enum": [ + "FUEL_START", + "FUEL_END" + ] + }, + "category": { + "type": "string", + "enum": [ + "FUEL" + ] + }, + "fileId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "contentType": { + "type": "string" + }, + "sizeBytes": { + "type": "string" + }, + "uploadedAt": { + "type": "string" + }, + "savedAt": { + "type": "string", + "nullable": true + } + } + }, + "Documents": { + "type": "object", + "required": [ + "dispatchNo", + "recordVersion", + "editable", + "photos" + ], + "properties": { + "dispatchNo": { + "type": "string" + }, + "recordVersion": { + "type": "string" + }, + "editable": { + "type": "boolean" + }, + "photos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Photo" + } + } + } + }, + "BindPhoto": { + "type": "object", + "required": [ + "recordVersion", + "idempotencyKey", + "fileId" + ], + "properties": { + "recordVersion": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)$" + }, + "idempotencyKey": { + "type": "string", + "minLength": 8, + "maxLength": 128 + }, + "fileId": { + "type": "string" + } + } + } + } + } +} diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 448375c73..f6ffb7d10 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -920,7 +920,8 @@ paths: parameters: - name: warehouseId in: query - required: true + description: 可选真实起运仓;未指定时按 warehouseTab 跨仓分页浏览,草稿及建单仍要求同一起运仓。 + required: false schema: type: string - name: planIds diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/SmartDispatchOpenApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/SmartDispatchOpenApiContractTest.java index 1d1e13557..1bf6c5120 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/SmartDispatchOpenApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/SmartDispatchOpenApiContractTest.java @@ -38,6 +38,9 @@ class SmartDispatchOpenApiContractTest { assertThat(map(paths.get(base+"/drafts"))).containsKey("get"); var candidate=map(map(paths.get(base+"/candidates")).get("get")); var parameterNames=((List)candidate.get("parameters")).stream().map(SmartDispatchOpenApiContractTest::map).map(p->p.get("name")).toList(); + var warehouseParameter=((List)candidate.get("parameters")).stream().map(SmartDispatchOpenApiContractTest::map) + .filter(p->"warehouseId".equals(p.get("name"))).findFirst().orElseThrow(); + assertThat(warehouseParameter.get("required")).isEqualTo(false); assertThat(parameterNames).contains("warehouseId","destinationWarehouseCode","draftToken","page","size","warehouseTab").doesNotContain("customerId","fbaWarehouseCode"); assertThat(map(map(schemas.get("DispatchCandidateList")).get("properties")).keySet()) .containsExactlyInAnyOrder("candidates","snapshotAt","complete","total","page","size","pages"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchControllerTest.java index 80849d595..dc37b0973 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchControllerTest.java @@ -45,6 +45,16 @@ class SmartDispatchControllerTest { .andExpect(status().isOk()); verify(aggregate).candidates(actor,new CandidateQuery("92",null,null,"LAX9",null,null,1,20,"ALL"));verifyNoInteractions(planning); } + /** 区域入口无需隐藏默认仓,全部与城市均由服务端分页。 */ + @Test void candidateHttpQueryAllowsRegionalBrowsingWithoutWarehouse()throws Exception { + var mvc=MockMvcBuilders.standaloneSetup(controller).setCustomArgumentResolvers(new AuthenticationPrincipalArgumentResolver()).build(); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(platform(),null,List.of())); + for(String tab:List.of("ALL","LAX","NYC")) { + mvc.perform(get("/api/platform/v1/last-leg/dispatches/candidates").param("warehouseTab",tab)).andExpect(status().isOk()); + verify(aggregate).candidates(actor,new CandidateQuery(null,null,null,null,null,null,1,20,tab)); + } + verifyNoInteractions(planning); + } /** 每个写入都将冻结DTO原样传递,拥有者只能来自认证主体。 */ @Test void allDraftRoutesDelegateFrozenVersionedRequests(){ var create=new CreateDraft("create","92");var container=new CreateContainer(0L,"container","d",15); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRegionalCandidatesTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRegionalCandidatesTest.java new file mode 100644 index 000000000..95dbffcf3 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRegionalCandidatesTest.java @@ -0,0 +1,47 @@ +package com.baiyue.lastleg.dispatch.service; + +import com.baiyue.lastleg.dispatch.mapper.DispatchCandidatePageMapper; +import com.baiyue.lastleg.dispatch.port.DispatchContracts.Actor; +import com.baiyue.lastleg.dispatch.port.DispatchPlanningModels.CandidateQuery; +import com.baiyue.lastleg.dispatch.port.DispatchTypes.ActorMode; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; +import java.util.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** 区域候选的读取范围与单仓写入职责分离,不依赖测试环境数据。 */ +class DispatchRegionalCandidatesTest { + /** 真实服务接受不指定仓的三种区域,保留服务端分页总数;非法仓和区域仍拒绝。 */ + @Test void serviceAcceptsRegionalQueriesAndRejectsMalformedScopes() { + var sources=mock(DispatchService.class);var appointments=mock(DispatchAppointmentAllocations.class); + var service=new DispatchAggregateService(null,null,sources,null,null,null,new com.fasterxml.jackson.databind.ObjectMapper()); + ReflectionTestUtils.setField(service,"appointments",appointments); + var actor=new Actor("7",ActorMode.PLATFORM,null); + when(sources.candidatePage(eq("7"),any())).thenReturn(new DispatchService.CandidatePage(List.of(),41)); + when(sources.selectionGroups(List.of())).thenReturn(Map.of());when(appointments.candidates(List.of())).thenReturn(Map.of()); + for(String tab:List.of("ALL","LAX","NYC")) { + var query=new CandidateQuery(null,null,null,null,null,null,2,20,tab); + var result=service.candidates(actor,query); + assertThat(result.total()).isEqualTo(41);assertThat(result.page()).isEqualTo(2);assertThat(result.pages()).isEqualTo(3); + verify(sources).candidatePage("7",query); + } + assertThatThrownBy(()->service.candidates(actor,new CandidateQuery("bad",null,null,null,null,null,1,20,"ALL"))).hasMessageContaining("起运仓筛选无效"); + assertThatThrownBy(()->service.candidates(actor,new CandidateQuery(null,null,null,null,null,null,1,20,"bad"))).hasMessageContaining("仓库页签不合法"); + } + /** 解析实际Mapper动态SQL,统计和分页采用相同区域条件,单仓消费者继续绑定真实仓。 */ + @Test void countAndPageUseOptionalWarehouseAndTheSameRegionPredicate() { + var config=new Configuration();config.addMapper(DispatchCandidatePageMapper.class); + for(String method:List.of("count","page")) for(String tab:List.of("ALL","LAX","NYC")) for(String warehouse:List.of("ALL_WAREHOUSES","321")) { + var query=new CandidateQuery(warehouse.equals("321")?warehouse:null,null,null,null,null,null,1,20,tab); + var sql=config.getMappedStatement(DispatchCandidatePageMapper.class.getName()+"."+method).getBoundSql(Map.of("query",query,"offset",0)).getSql(); + if(query.warehouseId()==null)assertThat(sql).doesNotContain("overseasWarehouseId=?"); + else assertThat(sql).contains("overseasWarehouseId=?"); + if(tab.equals("LAX"))assertThat(sql).contains("REGEXP 'lax|洛杉矶|los angeles'"); + if(tab.equals("NYC"))assertThat(sql).contains("REGEXP 'nyc|jfk|"); + if(tab.equals("ALL"))assertThat(sql).doesNotContain("REGEXP 'lax|", "REGEXP 'nyc|"); + if(method.equals("page"))assertThat(sql).contains("LIMIT ? OFFSET ?"); + } + } +} diff --git a/scripts/agents-index-contract.ps1 b/scripts/agents-index-contract.ps1 index e4b4b357f..ae20134ad 100644 --- a/scripts/agents-index-contract.ps1 +++ b/scripts/agents-index-contract.ps1 @@ -141,6 +141,10 @@ $backendAgentsPath = Join-Path $backendRoot 'AGENTS.md' $rootAgents = Read-RequiredText $rootAgentsPath $businessIndexPath = Join-Path $workspaceRoot 'docs/业务快速索引.md' $businessIndex = Read-RequiredText $businessIndexPath +$businessRulesPath = Join-Path $workspaceRoot 'docs/业务实现边界.md' +$businessRules = Read-RequiredText $businessRulesPath +# 业务断言检查实际正文,不再要求导航表复制规则;路由校验仍使用短索引。 +$businessFacts = $businessIndex + "`n" + $businessRules Assert-Contains $rootAgents 'docs/业务快速索引.md' '根按需业务索引入口' Assert-MarkdownLinks $rootAgentsPath $rootAgents '根导航与文档链接' Assert-MarkdownTableUnique $businessIndexPath '## 业务快速路由表' 0 '详细业务路由表' @@ -273,6 +277,66 @@ Write-Host "定向检索样本:$($lookupSamples.Count) 项,命中行共 $loo $scriptsAgents = Read-RequiredText (Join-Path $backendRoot 'scripts/AGENTS.md') $engineeringConstraints = Read-RequiredText (Join-Path $workspaceRoot 'docs/AI工程执行与协作约束.md') $docsIndex = Read-RequiredText (Join-Path $workspaceRoot 'docs/README.md') + +# 只检查明确列出的当前入口和迁移正文,不递归扫描历史资料。 +$overseasDocEntryPath = Join-Path $workspaceRoot 'docs/海外仓业务/README.md' +$overseasDocEntry = Read-RequiredText $overseasDocEntryPath +$overseasDocRulesPath = Join-Path $workspaceRoot 'docs/海外仓业务/业务规则.md' +$overseasDocRules = Read-RequiredText $overseasDocRulesPath +$financeDocEntryPath = Join-Path $workspaceRoot 'docs/财务管理/README.md' +$financeDocEntry = Read-RequiredText $financeDocEntryPath +$financeP1Path = Join-Path $workspaceRoot 'docs/财务管理/财务对账结算P1闭环.md' +$financeP1 = Read-RequiredText $financeP1Path +foreach ($document in @( + @{ Name = '业务导航'; Path = $businessIndexPath; Text = $businessIndex; Limit = 12500 }, + @{ Name = '文档总入口'; Path = (Join-Path $workspaceRoot 'docs/README.md'); Text = $docsIndex; Limit = 3500 }, + @{ Name = '海外仓文档入口'; Path = $overseasDocEntryPath; Text = $overseasDocEntry; Limit = 2000 }, + @{ Name = '财务文档入口'; Path = $financeDocEntryPath; Text = $financeDocEntry; Limit = 1800 } +)) { + Assert-MarkdownLinks $document.Path $document.Text $document.Name + $checked++ + if (($document.Text -replace "`r`n", "`n").Length -gt $document.Limit) { + Add-Failure "$($document.Name) 超出入口预算 $($document.Limit),请将正文归回主题主文档" + } +} +foreach ($document in @( + @{ Name = '跨端实现边界'; Path = $businessRulesPath; Text = $businessRules }, + @{ Name = '海外仓业务正文'; Path = $overseasDocRulesPath; Text = $overseasDocRules }, + @{ Name = '财务当前闭环'; Path = $financeP1Path; Text = $financeP1 } +)) { Assert-MarkdownLinks $document.Path $document.Text $document.Name } +Assert-TableRowWidth $businessIndexPath '## 业务快速路由表' 260 '详细业务导航表' +$businessRouteSection = (($businessIndex -split '(?m)^## 业务快速路由表\r?$', 2)[1] -split '(?m)^## ', 2)[0] +foreach ($topic in [regex]::Matches($businessRules, '(?m)^## (?.+?)\r?$')) { + $prefix = '| ' + $topic.Groups['name'].Value + ' |' + $hits = @($businessRouteSection -split '\r?\n' | Where-Object { $_.StartsWith($prefix) }) + $checked++ + if ($hits.Count -ne 1) { Add-Failure "业务正文必须有唯一导航入口:$($topic.Groups['name'].Value)" } +} +Assert-Contains $rootAgents '备选导航' '索引不是串行读取清单' +Assert-Contains $rootAgents '命中正文即停止扩展' '正文命中后的读取停止条件' +Assert-Contains $financeDocEntry '财务对账结算P1闭环.md#客户资金账户' '当前资金账户正文入口' +Assert-NotContains $financeDocEntry '钱包不做' '财务入口不混入历史排除范围' +Assert-Contains $financeP1 '首批固定为 CNY' '资金账户币种规则迁入现行正文' +Assert-Contains $financeP1 '充值先归还已用授信或负余额' '资金账户充值规则迁入现行正文' +Assert-Contains $financeP1 '新账户默认授信为 `0.00`' '资金账户默认授信规则迁入现行正文' +$docLookupCharacters = 0 +$docLookupSamples = @( + @{ Key = '客户头程订单批量导入'; Text = $businessIndex; Entry = 'apps/client-web/src/modules/first-leg/AGENTS.md' }, + @{ Key = '客户资金账户、充值、授信与业务扣费'; Text = $businessIndex; Entry = 'apps/client-web/AGENTS.md' }, + @{ Key = '客户账单、客户对账单、账单确认'; Text = $businessIndex; Entry = 'apps/client-web/src/modules/finance/views/FinanceBillsView.vue' }, + @{ Key = '客户资金账户、充值、授信、冻结与业务扣费'; Text = $financeDocEntry; Entry = '财务对账结算P1闭环.md#客户资金账户' }, + @{ Key = '海外排车、出库与派送改造'; Text = $overseasDocEntry; Entry = '排车列表统一作业改造契约.md' }, + @{ Key = '本土卡车与散板表单'; Text = $overseasDocEntry; Entry = '业务规则.md#本土散板简化下单' } +) +foreach ($sample in $docLookupSamples) { + $hits = @($sample.Text -split '\r?\n' | Where-Object { $_.StartsWith('| ' + $sample.Key + ' |') }) + $checked++ + if ($hits.Count -ne 1) { Add-Failure "文档路由样本必须唯一命中:$($sample.Key)"; continue } + Assert-Contains $hits[0] $sample.Entry "文档路由样本 $($sample.Key)" + $docLookupCharacters += $hits[0].Length +} +Write-Host "文档路由样本:$($docLookupSamples.Count) 项,命中行共 $docLookupCharacters 字符(不含上层指引或按需正文,不代表端到端耗时)。" + $testTaskRegistry = Read-RequiredText (Join-Path $workspaceRoot 'docs/独立任务协作登记.md') $devTaskRegistry = Read-RequiredText (Join-Path $workspaceRoot 'docs/开发环境独立任务协作登记.md') $mapperScanContract = Read-RequiredText (Join-Path $backendRoot 'scripts/mapper-scan-contract.mjs') @@ -282,7 +346,7 @@ Assert-Contains $rootAgents '源码结构、接口与 DTO、字段、状态机 Assert-Contains $rootAgents '数据库记录、上传文件、缓存、会话、消息、搜索索引和其他运行状态均视为可丢弃' '根开发运行状态范围' Assert-Contains $rootAgents '后端 MyBatis Mapper 新增、移动或扫描遗漏' '根 Mapper 快速入口' Assert-Contains $rootAgents 'node scripts/mapper-scan-contract.mjs' '根 Mapper 静态门禁' -Assert-Contains $businessIndex '脚本管理、全量验证与测试造数' '根脚本管理快速入口' +Assert-Contains $businessFacts '脚本管理、全量验证与测试造数' '根脚本管理快速入口' Assert-Contains $rootAgents 'baiyue-firstleg/scripts/AGENTS.md' '根脚本最近指引' Assert-Contains $engineeringConstraints '禁止为旧代码、旧调用或开发环境存量保留旧接口/旧字段别名' '工程无兼容执行规则' Assert-Contains $engineeringConstraints '不授权自动重置数据库、删除数据/文件、执行 SQL、部署或修改环境' '无兼容安全边界' @@ -340,7 +404,7 @@ foreach ($businessDocsEntry in @( 'docs/国内货柜履约/README.md', 'docs/海外仓业务/README.md' )) { - Assert-Contains $businessIndex $businessDocsEntry "根业务域入口 $businessDocsEntry" + Assert-Contains $businessFacts $businessDocsEntry "根业务域入口 $businessDocsEntry" } foreach ($removedPhaseDocsEntry in @('docs/一期/', 'docs/二期/', 'docs/三期/', 'docs/四期/')) { Assert-NotContains $rootAgents $removedPhaseDocsEntry "根已删除阶段目录 $removedPhaseDocsEntry" @@ -348,18 +412,18 @@ foreach ($removedPhaseDocsEntry in @('docs/一期/', 'docs/二期/', 'docs/三 Assert-NotContains $rootAgents '不含出库和派送' '根海外仓全流程范围' # 新增稳定业务能力必须从根表和目标部署单元直接命中。 -Assert-Contains $businessIndex '入库扣货、解除与凭证' '根入库扣货路由' +Assert-Contains $businessFacts '入库扣货、解除与凭证' '根入库扣货路由' Assert-Contains $adminAgents '入库扣货、解除与凭证历史' 'Admin 入库扣货路由' Assert-Contains $backendAgents '入库单扣货、解除、凭证与履约门禁' '后端入库扣货路由' -Assert-Contains $businessIndex '客户资金账户、充值、授信与业务扣费' '根客户资金账户路由' +Assert-Contains $businessFacts '客户资金账户、充值、授信与业务扣费' '根客户资金账户路由' Assert-Contains $adminAgents '客户资金账户与柜级公共费用分摊' 'Admin 客户资金账户路由' Assert-Contains $clientAgents '客户资金账户、授信与充值申请' 'Client 客户资金账户路由' Assert-Contains $backendAgents '客户资金账户、充值、授信、冻结、结算与跨区资金指令' '后端客户资金账户路由' -Assert-Contains $businessIndex '顶层授权快照' '根海外顶层授权快照' +Assert-Contains $businessFacts '顶层授权快照' '根海外顶层授权快照' Assert-Contains $backendAgents '顶层授权快照' '后端海外顶层授权快照' -Assert-Contains $businessIndex '目的港码头拖车与还空预警' '根目的港码头拖车路由' -Assert-Contains $businessIndex '与订舱级拖车预约是不同生命周期' '根两类拖车边界' -Assert-Contains $businessIndex '尾程订单控制与柜级入仓计划转换' '根尾程订单控制路由' +Assert-Contains $businessFacts '目的港码头拖车与还空预警' '根目的港码头拖车路由' +Assert-Contains $businessFacts '与订舱级拖车预约是不同生命周期' '根两类拖车边界' +Assert-Contains $businessFacts '尾程订单控制与柜级入仓计划转换' '根尾程订单控制路由' Assert-Contains $adminAgents '目的港码头拖车与还空预警' 'Admin 目的港码头拖车路由' Assert-Contains $adminAgents '尾程订单控制与柜级入仓计划转换' 'Admin 尾程订单控制路由' Assert-Contains $backendAgents '目的港码头拖车与还空预警' '后端目的港码头拖车路由' @@ -385,7 +449,7 @@ $firstlegAgents = $firstlegEntry + "`n" + $firstlegIndex $lastlegAgents = $lastlegEntry + "`n" + $lastlegIndex $warehouseAgents = Read-RequiredText (Join-Path $backendRoot 'warehouse/AGENTS.md') $coloadOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/coload-groups-v1.yaml') -Assert-Contains $businessIndex 'modules/unified-orders/api/coLoadGroups.ts' '根同柜不可拆组入口' +Assert-Contains $businessFacts 'modules/unified-orders/api/coLoadGroups.ts' '根同柜不可拆组入口' Assert-Contains $adminAgents '成员数是入仓片段数,不等于订单票数' 'Admin 同柜成员语义' Assert-Contains $backendAgents 'loading/coload/controller/PlatformColoadGroupController.java' '后端同柜不可拆组入口' Assert-Contains $firstlegAgents '服务端允许待审核、待测量/测量中、待分柜及装柜中订单' 'Firstleg 同柜建组边界' @@ -409,7 +473,7 @@ Assert-Contains $lastlegAgents '/api/platform/v1/last-leg/inbound-plans/convert- # 本轮新增的批量下单、测量计费、智能装柜、账单联查与导出重试必须从最近指引直达。 $fileAgents = Read-RequiredText (Join-Path $backendRoot 'file/AGENTS.md') $financeAgents = Read-RequiredText (Join-Path $backendRoot 'finance/AGENTS.md') -Assert-Contains $businessIndex '客户头程订单批量导入' '根批量下单快速路由' +Assert-Contains $businessFacts '客户头程订单批量导入' '根批量下单快速路由' Assert-Contains $orderAgents 'MerchantBatchImportService.java' 'Order 批量导入入口' Assert-Contains $orderAgents '仓库目录必须翻页读取全部候选' 'Order 批量模板全量仓库约束' Assert-Contains $clientAgents '中文批量导入' 'Client 批量导入入口' @@ -435,7 +499,7 @@ $orderDeclarationService = Read-RequiredText (Join-Path $backendRoot 'order/src/ $unifiedOrderQueryService = Read-RequiredText (Join-Path $backendRoot 'order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java') $firstlegDeclarationService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/OrderDeclarationSyncService.java') $adminDeclarationEditor = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/unified-orders/components/OrderDeclarationEditorDialog.vue') -Assert-Contains $businessIndex '平台修改客户原始申报与履约渠道' '根订单申报快速路由' +Assert-Contains $businessFacts '平台修改客户原始申报与履约渠道' '根订单申报快速路由' Assert-Contains $adminAgents '客户原始申报资料编辑与渠道变更' 'Admin 订单申报快速路由' Assert-Contains $backendAgents '平台修改客户原始申报、履约渠道变更与跨服务同步' '后端订单申报快速路由' Assert-Contains $orderAgents '平台版本化修改客户原始申报' 'Order 申报编辑入口' @@ -448,7 +512,7 @@ Assert-Contains $orderDeclarationService 'ORDER_DECLARATION_UPDATED_V1' 'Order Assert-Contains $firstlegDeclarationService 'mapper.byEvent(event.tenantId(), event.eventId())' 'Firstleg 申报 eventId 幂等' Assert-Contains $firstlegDeclarationService 'event.version() < current.orderVersion()' 'Firstleg 申报版本单调性' Assert-Contains $adminDeclarationEditor '仓库/PDA 实测数据不会被覆盖' 'Admin 申报事实边界提示' -Assert-Contains $businessIndex '统一列表仍固定显示 `PENDING_REVIEW` 并提供审核动作' '根待审核订单优先状态' +Assert-Contains $businessFacts '统一列表仍固定显示 `PENDING_REVIEW` 并提供审核动作' '根待审核订单优先状态' Assert-Contains $orderAgents '统一主状态/下一动作仍固定为 `PENDING_REVIEW/APPROVE_ORDER`' 'Order 待审核优先状态' Assert-Contains $unifiedOrderQueryService 'order.getStatus() == OrderStatus.SUBMITTED' 'Order 待审核状态判断' Assert-Contains $unifiedOrderQueryService 'pendingReview ? "PENDING_REVIEW" : completed ? "COMPLETED" : summary.mainStatus()' 'Order 待审核主状态优先于完成态与履约投影' @@ -461,14 +525,14 @@ $fbaPricingService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/mai $inboundFeeDetailService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java') $adminUnifiedOrdersApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/unified-orders/api/unifiedOrders.ts') $adminCarrierRatesView = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue') -Assert-Contains $businessIndex '头程订单、批量审核预览与入仓码关系' '根批量审核预览路由' +Assert-Contains $businessFacts '头程订单、批量审核预览与入仓码关系' '根批量审核预览路由' Assert-Contains $adminAgents '订单列表、批量审核预览、详情、商品图片与入库费用' 'Admin 批量审核预览路由' Assert-Contains $backendAgents '平台订单批量审核预览' '后端批量审核预览路由' Assert-Contains $orderAgents '平台批量审核预览' 'Order 批量审核预览入口' Assert-Contains $orderOpenApi '/api/platform/v1/orders/first-leg/batch-review-preview:' 'Order 批量审核预览 OpenAPI' Assert-Contains $batchReviewPreviewService '固定两次批量查询生成审核弹窗快照' 'Order 批量审核禁止逐票查询' Assert-Contains $adminUnifiedOrdersApi '/platform/v1/orders/first-leg/batch-review-preview' 'Admin 批量审核预览 API' -Assert-Contains $businessIndex '客户专属报价、柜级阶梯运费与费用调整' '根客户专属报价路由' +Assert-Contains $businessFacts '客户专属报价、柜级阶梯运费与费用调整' '根客户专属报价路由' Assert-Contains $adminAgents '客户专属价格簿、柜级阶梯运费与受控调价' 'Admin 客户专属报价路由' Assert-Contains $backendAgents '客户专属价格簿、柜级阶梯运费与受控调价' '后端客户专属报价路由' Assert-Contains $firstlegAgents '客户专属价格簿、柜级阶梯运费与调价边界' 'Firstleg 客户专属报价入口' @@ -485,8 +549,8 @@ Assert-Path 'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricing/ # 海外异常处理必须贯通证据、原出库来源和库存恢复,不能退化为仅修改派送状态。 $exceptionOrderService = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderService.java') $inventoryCommandService = Read-RequiredText (Join-Path $backendRoot 'lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java') -Assert-Contains $businessIndex '海外出库、排车、派送、异常与回访' '根海外异常库存恢复路由' -Assert-Contains $businessIndex 'exceptionorder/' '根海外异常服务入口' +Assert-Contains $businessFacts '海外出库、排车、派送、异常与回访' '根海外异常库存恢复路由' +Assert-Contains $businessFacts 'exceptionorder/' '根海外异常服务入口' Assert-Contains $lastlegIndex '退回海外仓须把实收按原出库来源恢复到服务端校验的真实托盘/库位' 'Lastleg 海外异常库存恢复规则' Assert-Contains $adminOverseasAgents '库存恢复、异常完成与派送 `RETURNED` 由服务端原子推进' 'Admin 海外异常原子边界' Assert-Contains $backendAgents '海外异常退回、库存恢复与客户回访' '后端海外异常库存恢复路由' @@ -503,9 +567,9 @@ $pendingReviewOrderService = Read-RequiredText (Join-Path $backendRoot 'order/sr $merchantInboundBillingService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/service/MerchantInboundBillingService.java') $clientSession = Read-RequiredText (Join-Path $workspaceRoot 'apps/client-web/src/api/session.ts') $clientHttp = Read-RequiredText (Join-Path $workspaceRoot 'apps/client-web/src/api/http.ts') -Assert-Contains $businessIndex '`PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING`' '根智能装柜四态候选' -Assert-Contains $businessIndex '能否加入以服务端 `canAssign` 为准' '根智能装柜资格边界' -Assert-Contains $businessIndex '人工加入允许跨渠道,智能推荐仍按柜次渠道' '根智能装柜渠道边界' +Assert-Contains $businessFacts '`PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING`' '根智能装柜四态候选' +Assert-Contains $businessFacts '能否加入以服务端 `canAssign` 为准' '根智能装柜资格边界' +Assert-Contains $businessFacts '人工加入允许跨渠道,智能推荐仍按柜次渠道' '根智能装柜渠道边界' Assert-Contains $backendAgents '/pending-review-assignments' '后端待审核装柜占位入口' Assert-Contains $backendAgents '/unpicked-items' '后端未拣整批移除入口' Assert-Contains $firstlegAgents '任一成员已拣则整组保留' 'Firstleg 同柜组整批移除边界' @@ -516,7 +580,7 @@ Assert-Contains $firstlegOpenApi '/api/platform/v1/first-leg/smart-loading/plans Assert-Contains $firstlegOpenApi '/api/platform/v1/first-leg/smart-loading/plans/{planId}/unpicked-items:' '未拣整批移除 OpenAPI' Assert-Contains $pendingReviewOrderService 'orders.pendingReviewPage' 'Order 待审核候选权威查询' Assert-Contains $pendingReviewOrderService '"PENDING_REVIEW"' 'Order 待审核候选状态映射' -Assert-Contains $businessIndex '客户入库结果与头程费用确认' '根客户入库费用确认入口' +Assert-Contains $businessFacts '客户入库结果与头程费用确认' '根客户入库费用确认入口' Assert-Contains $firstlegAgents 'expectedFeeVersion' 'Firstleg 客户费用版本确认' Assert-Contains $clientAgents 'components/InboundBillingPanel.vue' 'Client 入库费用确认入口' Assert-Contains $firstlegOpenApi '/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing/confirm:' '客户费用确认 OpenAPI' @@ -536,7 +600,7 @@ $smartLoadingService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/m $inboundSchema = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/db/phase2-inbound-schema.sql') $adminWeightPanel = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.vue') $clientOrdersView = Read-RequiredText (Join-Path $workspaceRoot 'apps/client-web/src/modules/first-leg/views/FirstLegOrdersView.vue') -Assert-Contains $businessIndex '称重确认、整票库位块与部分拣货资源结算' '根称重库位块路由' +Assert-Contains $businessFacts '称重确认、整票库位块与部分拣货资源结算' '根称重库位块路由' Assert-Contains $backendAgents '称重确认、整票库位块与 PDA 资源结算' '后端称重库位块路由' Assert-Contains $firstlegAgents 'MeasurementLocationOccupationService.java' 'Firstleg 称重库位块入口' Assert-Contains $firstlegAgents '最新测量批次的正数 `totalVolumeCbm/originalVolumeCbm` 取较大值' 'Firstleg 库位块测量批次体积选择' @@ -556,7 +620,7 @@ Assert-Contains $inboundSchema 'UNIQUE KEY uk_pallet_allocation_weight (tenant_i Assert-Contains $adminWeightPanel 'row.locationCode' 'Admin 称重卡片库位消费' Assert-Contains $adminWeightPanel 'putawayStatusLabel(row.slotStatus)' 'Admin 称重卡片槽位状态' Assert-NotContains $adminWeightPanel 'class="putaway-guide"' 'Admin 已移除整票预留引导区' -Assert-Contains $businessIndex '`totalPackageCount/remainingPackageCount`' '根智能装柜暂估数量' +Assert-Contains $businessFacts '`totalPackageCount/remainingPackageCount`' '根智能装柜暂估数量' Assert-Contains $firstlegAgents '`totalPackageCount/remainingPackageCount`' 'Firstleg 智能装柜暂估数量' Assert-Contains $adminAgents '`totalPackageCount/remainingPackageCount`' 'Admin 智能装柜暂估数量' Assert-Contains $firstlegOpenApi 'required: [id, packageCount, totalPackageCount, remainingPackageCount' '智能装柜计划数量 OpenAPI' @@ -575,7 +639,7 @@ $iamMemberService = Read-RequiredText (Join-Path $backendRoot 'iam/src/main/java $adminCustomersView = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/customers/pages/CustomersView.vue') $clientCompanyProfile = Read-RequiredText (Join-Path $workspaceRoot 'apps/client-web/src/modules/tenant-management/views/CompanyProfileView.vue') $clientMembersView = Read-RequiredText (Join-Path $workspaceRoot 'apps/client-web/src/modules/tenant-management/views/MembersView.vue') -Assert-Contains $businessIndex '公司联系电话/邮箱是企业资料,不作为成员登录标识' '根企业联系人边界' +Assert-Contains $businessFacts '公司联系电话/邮箱是企业资料,不作为成员登录标识' '根企业联系人边界' Assert-Contains $adminAgents '公司联系电话只作企业联系信息,不修改管理员/成员登录手机号' 'Admin 企业联系人边界' Assert-Contains $clientAgents '成员登录手机号可选,修改时同步 Auth、撤销该账号会话并审计' 'Client 成员登录手机号边界' Assert-Contains $authAgents '登录标识与企业联系人解耦' 'Auth 登录标识入口' @@ -589,7 +653,7 @@ Assert-Contains $clientCompanyProfile '不会修改任何成员的登录手机 Assert-Contains $clientMembersView '用于该成员手机号登录;修改后会同步认证账号并退出该成员现有会话' 'Client 成员登录手机号提示' $fileAgents = Read-RequiredText (Join-Path $backendRoot 'file/AGENTS.md') $qualificationOpenApi = Read-RequiredText (Join-Path $backendRoot 'file/src/main/resources/openapi/qualification-required-package-v1.yaml') -Assert-Contains $businessIndex '法人身份证选填,可在已有营业执照后单独补传或替换' '根法人身份证单独上传语义' +Assert-Contains $businessFacts '法人身份证选填,可在已有营业执照后单独补传或替换' '根法人身份证单独上传语义' Assert-Contains $fileAgents '法人身份证选填,客户可在已有营业执照后通过 `/api/merchant/v1/files/qualifications` 单独新增或替换' 'File 法人身份证单独上传入口' Assert-Contains $qualificationOpenApi '/api/merchant/v1/files/qualifications:' '客户单文件资质 OpenAPI' Assert-Contains $qualificationOpenApi 'LEGAL_PERSON_ID 可在已有营业执照后单独新增或替换' '客户法人身份证单独上传契约' @@ -605,8 +669,8 @@ $adminLastMileOrders = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-w $loadingListsOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/loading-lists-v1.yaml') $pdaLoadingApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/api/pda-loading.js') $cabinetEvidenceService = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/java/com/baiyue/firstleg/loading/service/CabinetLoadingEvidenceService.java') -Assert-Contains $businessIndex '拖车预约由 `RELEASED` 订舱拥有,不按客户、柜次、装柜计划或真实柜号归属' '根订舱级拖车语义' -Assert-Contains $businessIndex '证据以 `taskId + warehouseId` 进入任务专用 API' '根柜级证据入口' +Assert-Contains $businessFacts '拖车预约由 `RELEASED` 订舱拥有,不按客户、柜次、装柜计划或真实柜号归属' '根订舱级拖车语义' +Assert-Contains $businessFacts '证据以 `taskId + warehouseId` 进入任务专用 API' '根柜级证据入口' Assert-Contains $adminAgents '订舱级拖车预约' 'Admin 拖车快速入口' Assert-Contains $adminAgents '不选择或提交客户、柜次、装柜计划或真实集装箱号' 'Admin 拖车订舱级边界' Assert-Contains $backendAgents '订舱级拖车' '后端拖车快速入口' @@ -645,7 +709,7 @@ Assert-Contains $adminAgents '确认预装和创建 PDA 任务前必须补齐' ' Assert-Contains $pdaAgents '当前 `taskId + warehouseId` 走任务专用 API' '国内 PDA 柜级证据入口' Assert-Contains $pdaLoadingApi '/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/evidence?warehouseId=' '国内 PDA 证据查询 API' Assert-NotContains $pdaLoadingApi '/inbounds/${encodeURIComponent(inboundId)}/loading/evidence' '国内 PDA 旧入仓证据 API' -Assert-Contains $businessIndex '上传以幂等键绑定包含文件内容的请求哈希' '根装柜证据上传重放边界' +Assert-Contains $businessFacts '上传以幂等键绑定包含文件内容的请求哈希' '根装柜证据上传重放边界' Assert-Contains $firstlegAgents '完全重放返回当前视图且不得再次上传 File' 'Firstleg 装柜证据重放边界' Assert-Contains $cabinetEvidenceService 'uploadHash(warehouseId, expectedVersion, evidenceType, sealNo, remark, multipart)' '装柜证据上传请求哈希' Assert-Contains $cabinetEvidenceService 'digest.update(multipart.getBytes())' '装柜证据文件内容哈希' @@ -819,7 +883,7 @@ foreach ($overseasFulfillmentRoute in @( foreach ($overseasFulfillmentRootEntry in @( 'OutboundsView.vue', 'DeliveryOrdersView.vue', 'ExceptionOrdersView.vue', 'CustomerVisitsView.vue' )) { - Assert-Contains $businessIndex $overseasFulfillmentRootEntry '根海外履约正式业务路由' + Assert-Contains $businessFacts $overseasFulfillmentRootEntry '根海外履约正式业务路由' } Assert-Contains $adminAgents '海外排车、出库与派送' 'Admin 海外出库派送入口' Assert-Contains $adminAgents '海外异常退回与客户回访' 'Admin 海外异常回访入口' @@ -936,8 +1000,8 @@ $containerCustomsOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/s $adminContainerCustomsApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/container-loading/api/platformContainerCustoms.ts') $clientContainerCustomsApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/client-web/src/modules/first-leg/api/customs.ts') $customsFileOpenApi = Read-RequiredText (Join-Path $backendRoot 'file/src/main/resources/openapi/customs-file-and-export-v1.yaml') -Assert-Contains $businessIndex 'ContainerCustomsPanel.vue' '根整柜报关 Admin 入口' -Assert-Contains $businessIndex 'CustomsDocumentsDialog.vue' '根整柜报关 Client 入口' +Assert-Contains $businessFacts 'ContainerCustomsPanel.vue' '根整柜报关 Admin 入口' +Assert-Contains $businessFacts 'CustomsDocumentsDialog.vue' '根整柜报关 Client 入口' Assert-Contains $adminAgents 'components/ContainerCustomsPanel.vue' 'Admin 整柜报关入口' Assert-Contains $adminAgents 'complete-all-buy' 'Admin 全买单柜入口' Assert-Contains $clientAgents 'components/CustomsDocumentsDialog.vue' 'Client 退税报关资料入口' @@ -981,7 +1045,7 @@ Assert-Contains $lastlegBusinessOpenApi 'snapshotVersion' 'Lastleg 整柜计划 Assert-Contains $lastlegBusinessOpenApi 'snapshotHash' 'Lastleg 整柜计划快照哈希' Assert-Contains $lastlegBusinessOpenApi 'sourceContainerBookingId' 'Lastleg 稳定来源柜身份' Assert-Contains $adminOverseasAgents '/orders/container-summaries' 'Admin 柜级摘要目标入口' -Assert-Contains $businessIndex '尾程订单控制与柜级入仓计划转换' '根柜级计划入口' +Assert-Contains $businessFacts '尾程订单控制与柜级入仓计划转换' '根柜级计划入口' Assert-Contains $backendAgents '分页返回全部稳定来源柜' '后端全柜工作台范围' Assert-Contains $adminOverseasAgents '服务端分页返回全部稳定来源柜' 'Admin 全柜工作台范围' Assert-Contains $lastlegAgents '分页返回全部稳定来源柜' 'Lastleg 全柜工作台范围' @@ -1027,12 +1091,12 @@ $financeEventConsumer = Read-RequiredText (Join-Path $backendRoot 'finance/src/m $financePhase1Service = Read-RequiredText (Join-Path $backendRoot 'finance/src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java') $overseasFinancePhase0ContractPath = Join-Path $workspaceRoot 'docs/财务管理/海外仓费用统一接入阶段0技术契约.md' $overseasFinancePhase0Contract = Read-RequiredText $overseasFinancePhase0ContractPath -Assert-Contains $businessIndex '财务管理与柜级公共费用分摊' '根柜费分摊快速路由' -Assert-Contains $businessIndex '海外业务费用来源与客户财务' '根海外费用快速路由' +Assert-Contains $businessFacts '财务管理与柜级公共费用分摊' '根柜费分摊快速路由' +Assert-Contains $businessFacts '海外业务费用来源与客户财务' '根海外费用快速路由' Assert-Contains $adminAgents '财务、客户资金账户与柜级公共费用分摊' 'Admin 柜费分摊快速路由' Assert-Contains $backendAgents '柜级公共费用跨客户分摊' '后端柜费分摊快速路由' Assert-Contains $backendAgents '海外业务费用来源跨区入池' '后端海外费用快速路由' -Assert-Contains $businessIndex '海外仓费用统一接入阶段0技术契约.md' '根海外费用阶段 0 按需入口' +Assert-Contains $businessFacts '海外仓费用统一接入阶段0技术契约.md' '根海外费用阶段 0 按需入口' Assert-Contains $financeAgents '海外仓费用统一接入阶段0技术契约.md' 'Finance 海外费用阶段 0 按需入口' Assert-Contains $lastlegAgents '海外仓费用统一接入阶段0技术契约.md' 'Lastleg 海外费用阶段 0 按需入口' Assert-Contains $lastlegAgents '不能因文档存在宣称已实现' '阶段 0 设计与代码事实边界' @@ -1066,16 +1130,16 @@ Assert-Contains $financeBusinessClient '/internal/finance/v1/business-fee-source Assert-Contains $financeBusinessClient '/internal/finance/v1/wallet-command-events' 'Finance 资金内部入口' Assert-Contains $financeAgents '`eventId + payloadHash` 防重放冲突' 'Finance 海外事件幂等边界' Assert-Contains $financeAgents '`PAYABLE + DIRECT_PAID` 审核版本必须生成 `BUSINESS_DIRECT_PAID`' 'Finance 直接已付隔离边界' -Assert-Contains $businessIndex '入库扣货、解除与凭证' '根入库扣货快速路由' +Assert-Contains $businessFacts '入库扣货、解除与凭证' '根入库扣货快速路由' Assert-Contains $adminAgents '入库扣货、解除与凭证历史' 'Admin 入库扣货快速路由' Assert-Contains $backendAgents '入库单扣货、解除、凭证与履约门禁' '后端入库扣货快速路由' Assert-Contains $firstlegAgents '入库单扣货、履约门禁与凭证' 'Firstleg 入库扣货入口' -Assert-Contains $businessIndex '客户资金账户、充值、授信与业务扣费' '根客户钱包快速路由' +Assert-Contains $businessFacts '客户资金账户、充值、授信与业务扣费' '根客户钱包快速路由' Assert-Contains $backendAgents '客户资金账户、充值、授信、冻结、结算与跨区资金指令' '后端客户钱包快速路由' Assert-Contains $lastlegAgents '海外业务资金指令与结果' 'Lastleg 跨区钱包入口' Assert-Contains $lastlegAgents '拆柜打托成本与客户应收分摊' 'Lastleg 拆柜打托费用入口' Assert-Contains $clientAgents '换标申请必须提交正数 CNY `chargeAmount`' 'Client 换标钱包金额边界' -Assert-Contains $businessIndex '统一认证与服务通信契约.md' '根统一认证契约入口' +Assert-Contains $businessFacts '统一认证与服务通信契约.md' '根统一认证契约入口' Assert-Contains $backendAgents '各端共用 Gateway 和 Redis 会话' '后端统一认证目标' $unifiedCommunication = Read-RequiredText (Join-Path $workspaceRoot 'docs/统一认证与服务通信契约.md') Assert-Contains $unifiedCommunication 'JWT 不携带角色或权限集合' '统一 JWT 最小身份边界'