优化
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ public class SmartDispatchController {
|
||||
/** 读取实际候选,不在草稿域复制来源筛选逻辑。 */
|
||||
@GetMapping("/candidates")
|
||||
@PreAuthorize("hasAuthority('"+OverseasPermissions.DISPATCH_LIST+"')")
|
||||
public ApiResponse<CandidateList> candidates(@AuthenticationPrincipal AuthenticatedUser user,@RequestParam String warehouseId,
|
||||
public ApiResponse<CandidateList> candidates(@AuthenticationPrincipal AuthenticatedUser user,@RequestParam(required=false) String warehouseId,
|
||||
@RequestParam(required=false)List<Long> 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) {
|
||||
|
||||
+2
-1
@@ -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"
|
||||
+"<if test='query.warehouseId != null'> AND overseasWarehouseId=#{query.warehouseId}</if>"
|
||||
+"<if test='query.destinationWarehouseCode != null'> AND fbaWarehouseCode=#{query.destinationWarehouseCode}</if>"
|
||||
+"<if test=\"query.warehouseTab == 'LAX'\"> AND LOWER(CONCAT(COALESCE(overseasWarehouseCode,''),' ',COALESCE(overseasWarehouseName,''))) REGEXP 'lax|洛杉矶|los angeles'</if>"
|
||||
+"<if test=\"query.warehouseTab == 'NYC'\"> AND LOWER(CONCAT(COALESCE(overseasWarehouseCode,''),' ',COALESCE(overseasWarehouseName,''))) REGEXP 'nyc|jfk|(^|[^a-z])ny([^a-z]|$)|纽约|new[ -]?york'</if>";
|
||||
|
||||
+2
-1
@@ -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()),
|
||||
|
||||
+4
-4
@@ -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());
|
||||
}
|
||||
|
||||
+76
@@ -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<Photo> 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<Documents> 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<Photo> 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<Documents> 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<byte[]> 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<Photo> 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);
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -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?"自有业务":"外配任务"));}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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<String,String>();for(var a:ops.arrivals())arrivals.put(a.destinationKey(),a.actualArrivedAt());
|
||||
var destinations=new LinkedHashMap<String,Destination>();
|
||||
for(var item:cargo) {
|
||||
|
||||
+2
-1
@@ -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());
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -920,7 +920,8 @@ paths:
|
||||
parameters:
|
||||
- name: warehouseId
|
||||
in: query
|
||||
required: true
|
||||
description: 可选真实起运仓;未指定时按 warehouseTab 跨仓分页浏览,草稿及建单仍要求同一起运仓。
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: planIds
|
||||
|
||||
+3
@@ -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");
|
||||
|
||||
+10
@@ -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);
|
||||
|
||||
+47
@@ -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 ?");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user