优化
This commit is contained in:
+2
-2
@@ -77,9 +77,9 @@ public class DispatchAggregateController {
|
||||
public ApiResponse<CommandResult> loadingComplete(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@RequestBody DispatchCommands.LoadingComplete request) {
|
||||
return ApiResponse.success(loadingDeparture.complete(DispatchActorFactory.platform(user),dispatchNo,request));
|
||||
}
|
||||
/** 本车证据先上传,主单引用随后走版本化编辑。 */
|
||||
/** 本车证据先上传;维护和当前待审权限分别由服务端核验,引用随对应命令保存。 */
|
||||
@PostMapping(value="/{dispatchNo}/evidence/{kind}",consumes=org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')")
|
||||
@PreAuthorize("hasAnyAuthority('platform:overseas-warehouse:dispatch:edit','platform:fleet:trip:dispatch-review','platform:fleet:trip:loading-review')")
|
||||
public ApiResponse<com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File> uploadEvidence(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,
|
||||
@PathVariable com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,@RequestPart("file")org.springframework.web.multipart.MultipartFile file) {
|
||||
return ApiResponse.success(service.uploadEvidence(DispatchActorFactory.platform(user),dispatchNo,kind,file));
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@ package com.baiyue.lastleg.dispatch.gateway;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/** 临时车辆和司机证据属于本张整车,不能借司机主档或客户文件授权。 */
|
||||
/** 车辆、司机及审核补充证据属于本张整车,不能借司机主档或客户文件授权。 */
|
||||
public interface DispatchEvidenceGateway {
|
||||
enum Kind { VEHICLE, DRIVER }
|
||||
enum Kind { VEHICLE, DRIVER, DRIVER_PHOTO }
|
||||
record File(Long fileId,String fileName,String contentType,long sizeBytes) { }
|
||||
/** 上传只写本车、本类证据。 */
|
||||
File upload(long dispatchId,Kind kind,String operator,MultipartFile file);
|
||||
|
||||
+6
-2
@@ -29,7 +29,7 @@ public class HttpDispatchEvidenceGateway implements DispatchEvidenceGateway {
|
||||
private static String type(Kind kind) { return "OVERSEAS_DISPATCH_"+Objects.requireNonNull(kind).name()+"_EVIDENCE"; }
|
||||
/** 本车证据上传完成后仍以File返回元数据核对复合归属。 */
|
||||
@Override public File upload(long dispatch,Kind kind,String operator,MultipartFile file) {
|
||||
if(file==null || file.isEmpty() || file.getSize()>MAX || !TYPES.contains(Objects.toString(file.getContentType(),"")))throw invalid();
|
||||
if(file==null || file.isEmpty() || file.getSize()>MAX || !supportedType(kind,file.getContentType()))throw invalid();
|
||||
var body=new MultipartBodyBuilder();
|
||||
try { body.part("file",new ByteArrayResource(file.getBytes()) {
|
||||
/** 保留用户文件名供审阅,不把路径当业务事实。 */
|
||||
@@ -80,9 +80,13 @@ public class HttpDispatchEvidenceGateway implements DispatchEvidenceGateway {
|
||||
var value=envelope.data();
|
||||
if(!Objects.equals(value.tenantId(),0L) || !type(kind).equals(value.businessType()) || !Long.toString(dispatch).equals(value.businessId())
|
||||
|| value.fileId()==null || value.fileId()<=0 || id!=null&&!id.equals(value.fileId()) || value.fileName()==null || value.fileName().isBlank()
|
||||
|| !TYPES.contains(Objects.toString(value.contentType(),"")) || value.sizeBytes()==null || value.sizeBytes()<=0 || value.sizeBytes()>MAX)throw invalid();
|
||||
|| !supportedType(kind,value.contentType()) || value.sizeBytes()==null || value.sizeBytes()<=0 || value.sizeBytes()>MAX)throw invalid();
|
||||
return new File(value.fileId(),value.fileName(),value.contentType(),value.sizeBytes());
|
||||
}
|
||||
/** 司机本人照片只接受图片;车辆和驾驶证沿现有PDF/图片证据规则。 */
|
||||
private static boolean supportedType(Kind kind,String contentType) {
|
||||
return TYPES.contains(Objects.toString(contentType,"")) && (kind!=Kind.DRIVER_PHOTO || !"application/pdf".equals(contentType));
|
||||
}
|
||||
/** 无效证据不能作为已核实资料。 */
|
||||
private static ResponseStatusException invalid(){return new ResponseStatusException(HttpStatus.BAD_REQUEST,"证据须为本车有效PDF、JPG或PNG,最大10MiB");}
|
||||
/** File明确拒绝归属或文件格式属于业务错误,不能伪装成服务不可用。 */
|
||||
|
||||
+3
-3
@@ -49,9 +49,9 @@ public final class DispatchCommands {
|
||||
}
|
||||
|
||||
public record ReviewCommand(Long recordVersion, String idempotencyKey, Decision decision,
|
||||
String reason) {
|
||||
/** 两级审核都执行相同的必填版本约束。 */
|
||||
public ReviewCommand { recordVersion = requireVersion(recordVersion); }
|
||||
String reason, ReviewAttachments attachments) {
|
||||
/** 两级审核都执行相同的必填版本约束,未传附件表示没有本次补充。 */
|
||||
public ReviewCommand { recordVersion = requireVersion(recordVersion); attachments = attachments == null ? ReviewAttachments.empty() : attachments; }
|
||||
}
|
||||
|
||||
/** 只有零真实扫码时可退回,保留原单号与审核轮次历史。 */
|
||||
|
||||
+21
-1
@@ -41,7 +41,27 @@ public final class DispatchContracts {
|
||||
String contactName, String contactPhone) { }
|
||||
|
||||
public record Review(ReviewStatus status, String reason, String reviewerId, Instant reviewedAt,
|
||||
long reviewRound, String basisHash, String invalidatedReason) { }
|
||||
long reviewRound, String basisHash, String invalidatedReason, ReviewAttachments attachments) {
|
||||
/** 无补充附件时返回明确空集合,旧决定不得从资源快照自动继承。 */
|
||||
public Review { attachments = attachments == null ? ReviewAttachments.empty() : attachments; }
|
||||
}
|
||||
|
||||
/** 本次审核补充证据按分类归属本车,不修改车辆或司机业务资料。 */
|
||||
public record ReviewAttachments(List<Long> vehicleFileIds, List<Long> driverPhotoFileIds, List<Long> licenseFileIds) {
|
||||
/** 每类附件有界、不可变且不允许空身份或重复引用。 */
|
||||
public ReviewAttachments {
|
||||
vehicleFileIds = checked(vehicleFileIds); driverPhotoFileIds = checked(driverPhotoFileIds); licenseFileIds = checked(licenseFileIds);
|
||||
}
|
||||
/** 未传附件表示本次没有补充证据。 */
|
||||
public static ReviewAttachments empty() { return new ReviewAttachments(List.of(), List.of(), List.of()); }
|
||||
/** 只规范引用形状;本车和类别归属由审核服务逐个验证。 */
|
||||
private static List<Long> checked(List<Long> ids) {
|
||||
if (ids == null) return List.of();
|
||||
if (ids.size() > 20 || ids.stream().anyMatch(id -> id == null || id <= 0) || ids.stream().distinct().count() != ids.size())
|
||||
throw new IllegalArgumentException("每类审核附件最多20个,文件引用须有效且不重复");
|
||||
return List.copyOf(ids);
|
||||
}
|
||||
}
|
||||
|
||||
public record Reviews(Review dispatchReview, Review loadingReview) { }
|
||||
|
||||
|
||||
+20
-9
@@ -235,10 +235,13 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
return saveCommand(row,actor,identity,request.reason());
|
||||
}
|
||||
|
||||
/** 上传不修改主单引用;返回本车文件ID后由公共版本编辑命令保存选择。 */
|
||||
/** 上传只返回本车证据;待审按当前审核权限授权,客服阶段按维护权限授权。 */
|
||||
public com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File uploadEvidence(Actor actor,String number,
|
||||
com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,org.springframework.web.multipart.MultipartFile file) {
|
||||
requirePlatform(actor);var row=required(actor,number,false);requireEditing(row,row.getRecordVersion());
|
||||
Action permission = ReviewState.DISPATCH_PENDING.name().equals(row.getReviewState()) ? Action.DISPATCH_REVIEW
|
||||
: ReviewState.LOADING_PENDING.name().equals(row.getReviewState()) ? Action.LOADING_REVIEW : Action.EDIT;
|
||||
if(!hasPermission(actor,permission))throw new BizException(ResultCode.FORBIDDEN,"无当前阶段附件上传权限");
|
||||
return evidence.upload(row.getId(),kind,actor.operatorId(),file);
|
||||
}
|
||||
/** 列表权限含详情只读;仅冻结MASTER证照走司机归属,其余保持本车附件复合归属。 */
|
||||
@@ -392,7 +395,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
if(n(row.getPackageCount())<=0 || repository.cargo(row.getId()).isEmpty())throw bad("空排车不能提交审核");
|
||||
long round=row.getReviewRound()+1;String basis=basis(row);
|
||||
row.setReviewRound(round);row.setReviewBasisHash(basis);row.setReviewState(ReviewState.DISPATCH_PENDING.name());
|
||||
row.setDispatchReviewJson(write(new Review(ReviewStatus.PENDING,null,null,null,round,basis,null)));
|
||||
row.setDispatchReviewJson(write(new Review(ReviewStatus.PENDING,null,null,null,round,basis,null,null)));
|
||||
row.setLoadingReviewJson(write(pending(round)));
|
||||
return saveCommand(row,actor,identity,request.remark());
|
||||
}
|
||||
@@ -420,20 +423,21 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var dispatch=reviews(row).dispatchReview();
|
||||
if(dispatch.status()!=ReviewStatus.APPROVED || dispatch.reviewRound()!=row.getReviewRound() || !Objects.equals(dispatch.basisHash(),row.getReviewBasisHash()))throw conflict("调度审核尚未通过或依据已失效");
|
||||
}
|
||||
verifyReviewAttachments(row.getId(),request.attachments());
|
||||
if(request.decision()==Decision.APPROVED)verifyReviewResources(row);
|
||||
var reviewed=new Review(request.decision()==Decision.APPROVED?ReviewStatus.APPROVED:ReviewStatus.REJECTED,trim(request.reason()),actor.operatorId(),Instant.now().truncatedTo(ChronoUnit.MILLIS),row.getReviewRound(),row.getReviewBasisHash(),null);
|
||||
var reviewed=new Review(request.decision()==Decision.APPROVED?ReviewStatus.APPROVED:ReviewStatus.REJECTED,trim(request.reason()),actor.operatorId(),Instant.now().truncatedTo(ChronoUnit.MILLIS),row.getReviewRound(),row.getReviewBasisHash(),null,request.attachments());
|
||||
if(loading) {
|
||||
row.setLoadingReviewJson(write(reviewed));row.setReviewState(request.decision()==Decision.APPROVED?ReviewState.APPROVED.name():ReviewState.LOADING_REJECTED.name());
|
||||
if(request.decision()==Decision.APPROVED)row.setStage(Stage.LOADING.name());
|
||||
} else {
|
||||
row.setDispatchReviewJson(write(reviewed));row.setReviewState(request.decision()==Decision.APPROVED?ReviewState.LOADING_PENDING.name():ReviewState.DISPATCH_REJECTED.name());
|
||||
row.setLoadingReviewJson(write(request.decision()==Decision.APPROVED?new Review(ReviewStatus.PENDING,null,null,null,row.getReviewRound(),row.getReviewBasisHash(),null):pending(row.getReviewRound())));
|
||||
row.setLoadingReviewJson(write(request.decision()==Decision.APPROVED?new Review(ReviewStatus.PENDING,null,null,null,row.getReviewRound(),row.getReviewBasisHash(),null,null):pending(row.getReviewRound())));
|
||||
}
|
||||
if(request.decision()==Decision.REJECTED) {
|
||||
// 任一级驳回均回客服处理,旧调度批准不再作为可继续理货的有效依据。
|
||||
row.setStage(Stage.BEFORE_LOADING.name());row.setReviewBasisHash(null);
|
||||
if(loading)row.setDispatchReviewJson(write(new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,
|
||||
row.getReviewRound(),null,"理货审核驳回,请客服修改后重新提交调度审核")));
|
||||
row.getReviewRound(),null,"理货审核驳回,请客服修改后重新提交调度审核",reviews(row).dispatchReview().attachments())));
|
||||
}
|
||||
return saveCommand(row,actor,identity,request.reason());
|
||||
}
|
||||
@@ -622,7 +626,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
/** 作废原因进入两级显式记录,旧审核证据仍保留在不可变历史中。 */
|
||||
private void invalidate(DispatchAggregate row,String reason) {
|
||||
row.setReviewState(ReviewState.DRAFT.name());row.setReviewBasisHash(null);
|
||||
var invalid=new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,row.getReviewRound(),null,reason);
|
||||
var invalid=new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,row.getReviewRound(),null,reason,null);
|
||||
var previous=reviews(row);
|
||||
row.setDispatchReviewJson(write(retainRejection(previous.dispatchReview(),invalid)));
|
||||
row.setLoadingReviewJson(write(retainRejection(previous.loadingReview(),invalid)));
|
||||
@@ -630,9 +634,16 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
/** 客服改资料期间保留本轮驳回原因;下一轮提交统一重置,不复用批准。 */
|
||||
private static Review retainRejection(Review previous,Review invalid) {
|
||||
return previous.status()==ReviewStatus.REJECTED
|
||||
?new Review(previous.status(),previous.reason(),previous.reviewerId(),previous.reviewedAt(),previous.reviewRound(),null,invalid.invalidatedReason())
|
||||
:invalid;
|
||||
?new Review(previous.status(),previous.reason(),previous.reviewerId(),previous.reviewedAt(),previous.reviewRound(),null,invalid.invalidatedReason(),previous.attachments())
|
||||
:new Review(invalid.status(),null,null,null,invalid.reviewRound(),null,invalid.invalidatedReason(),previous.attachments());
|
||||
}
|
||||
/** 审核附件逐个验证本车与种类;MASTER档案证照不能冒充本次上传证据。 */
|
||||
private void verifyReviewAttachments(long dispatch,ReviewAttachments attachments) {
|
||||
for(var id:attachments.vehicleFileIds())evidence.metadata(dispatch,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.VEHICLE,id);
|
||||
for(var id:attachments.driverPhotoFileIds())evidence.metadata(dispatch,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER_PHOTO,id);
|
||||
for(var id:attachments.licenseFileIds())evidence.metadata(dispatch,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,id);
|
||||
}
|
||||
|
||||
/** 临时取证、有效期和实际主档资格必须在提交及通过时再次核验。 */
|
||||
void verifyReviewResources(DispatchAggregate row) {
|
||||
var r=read(row.getResourcesJson(),Resources.class);var v=r.vehicle();var d=r.driver();
|
||||
@@ -923,7 +934,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
/** 当前审核记录,不以版本相等代替轮次有效性。 */
|
||||
Reviews reviews(DispatchAggregate row) { return new Reviews(read(row.getDispatchReviewJson(),Review.class),read(row.getLoadingReviewJson(),Review.class)); }
|
||||
/** 未提交的审核不伪造待审核。 */
|
||||
private static Review pending(long round) { return new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,round,null,null); }
|
||||
private static Review pending(long round) { return new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,round,null,null,null); }
|
||||
/** 整车真实统计。 */
|
||||
private static Counts counts(DispatchAggregate row) { return new Counts(n(row.getItemCount()),n(row.getPalletCount()),n(row.getPackageCount()),n(row.getPickedPackageCount()),n(row.getShippedPackageCount())); }
|
||||
/** 空单永远不能核平为装车完成。 */
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public class TransportTaskProjection {
|
||||
row.setOverseasWarehouseCode(trip.getWarehouseCode());row.setOverseasWarehouseName(trip.getWarehouseName());
|
||||
row.setStatus("ACTIVE");row.setStage("BEFORE_LOADING");row.setRecordVersion(trip.getRecordVersion());row.setCargoRevision(0L);
|
||||
row.setCargoHash(trip.getRequestHash());row.setReviewState("DRAFT");row.setReviewRound(0L);
|
||||
var review=new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,0,null,null);
|
||||
var review=new Review(ReviewStatus.NOT_SUBMITTED,null,null,null,0,null,null,null);
|
||||
row.setDispatchReviewJson(write(review));row.setLoadingReviewJson(write(review));
|
||||
resources(row,trip);row.setPlannedDepartureAt(trip.getPlannedDepartureAt());row.setEstimatedArrivalAt(trip.getExpectedDriverEndAt());
|
||||
row.setRoute(("PICKUP".equals(trip.getTripPurpose())?"到仓取货":"本土卡车")+" · "+items+"单 · "+trip.getWarehouseCode());row.setItemCount(items);row.setPalletCount(pallets);row.setPackageCount(packages);
|
||||
|
||||
Reference in New Issue
Block a user