优化
This commit is contained in:
+14
-14
@@ -9,7 +9,7 @@ import java.util.List;
|
||||
import static com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
|
||||
import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
|
||||
|
||||
/** 两端装车完成即发车:所有中间状态只存在于同一事务,原请求只恢复最终结果。 */
|
||||
/** 装车完成停在装车后;资料齐全时由资料服务在保存事务中推进派送。 */
|
||||
@Service
|
||||
public class DispatchLoadingDepartureService {
|
||||
private final DispatchAggregateService workflow;
|
||||
@@ -24,31 +24,31 @@ public class DispatchLoadingDepartureService {
|
||||
this.workflow=workflow;this.delivery=delivery;this.departure=departure;this.commands=commands;this.hashes=hashes;
|
||||
}
|
||||
|
||||
/** 原键恢复先于阶段校验;任一步失败均回滚装载、子单、费用、发车与全部账本。 */
|
||||
/** 两端装车完成只保存真实实载,原请求精确恢复装车后结果。 */
|
||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||
public CommandResult complete(Actor actor,String number,DispatchCommands.LoadingComplete request) {
|
||||
workflow.requireLoadingPermission(actor);
|
||||
return workflow.loadingComplete(actor,number,request);
|
||||
}
|
||||
|
||||
/** 仅供装车资料齐全的保存事务调用;派送准备和发车失败回滚最后一张绑定。 */
|
||||
@Transactional(propagation=Propagation.MANDATORY,isolation=Isolation.READ_COMMITTED)
|
||||
public CommandResult departAfterDocuments(Actor actor,String number,String key) {
|
||||
var context=workflow.lockForArrangement(actor,number);
|
||||
var identity=new CommandIdentity(Action.LOADING_COMPLETE,request.idempotencyKey(),request.recordVersion(),
|
||||
hashes.command(actor,Action.LOADING_COMPLETE,number,request.recordVersion(),request));
|
||||
var replay=commands.replay(actor,number,identity);if(replay.isPresent())return replay.get();
|
||||
if(context.stage()!=Stage.AFTER_LOADING || context.status()!=Status.ACTIVE)
|
||||
throw new BizException(ResultCode.CONFLICT,"当前阶段不能按装车资料推进派送");
|
||||
// 先取得本车单据锁再申请真实来源锁,后续所有步骤保持同一个整车父锁。
|
||||
delivery.departureFacts(context,true);
|
||||
var loaded=workflow.loadingComplete(actor,number,new DispatchCommands.LoadingComplete(request.recordVersion(),
|
||||
stepKey(number,request.idempotencyKey(),"LOAD"),request.manifestHash(),request.measurements(),request.remark()));
|
||||
context=workflow.preview(actor,number);
|
||||
String arrangementKey=stepKey(number,request.idempotencyKey(),"ARRANGE");
|
||||
String arrangementKey=stepKey(number,key,"ARRANGE");
|
||||
var fact=delivery.prepareForDeparture(context,actor,arrangementKey);
|
||||
var arrangementIdentity=new CommandIdentity(Action.ARRANGE_DELIVERY,arrangementKey,context.recordVersion(),
|
||||
hashes.source(List.of(number,actor.operatorId(),context.recordVersion(),fact.arrangementBusinessHash())));
|
||||
var arranged=workflow.recordArrangement(context,actor,arrangementIdentity,fact,false);
|
||||
var departed=departure.depart(actor,number,new DispatchCommands.Depart(arranged.recordVersion(),
|
||||
stepKey(number,request.idempotencyKey(),"DEPART"),request.remark()));
|
||||
if(departed.stage()!=Stage.DELIVERING || !(loaded.result() instanceof DispatchResults.Loaded))
|
||||
stepKey(number,key,"DEPART"),"五张装车照片已齐全,自动进入派送中"));
|
||||
if(departed.stage()!=Stage.DELIVERING)
|
||||
throw new BizException(ResultCode.CONFLICT,"装车发运结果未核平");
|
||||
var result=new CommandResult(number,Action.LOADING_COMPLETE,request.idempotencyKey(),departed.recordVersion(),
|
||||
departed.status(),departed.stage(),departed.occurredAt(),Effect.APPLIED,departed.counts(),loaded.result(),false,false);
|
||||
commands.append(context.dispatchId(),actor,identity,result);return result;
|
||||
return departed;
|
||||
}
|
||||
|
||||
/** 固定长度内部键绑定原请求和步骤,不向客户端暴露中间命令作为恢复入口。 */
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dispatch.controller.DispatchActorFactory;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.service.DispatchLoadingDocumentService;
|
||||
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;
|
||||
|
||||
/** 平台排车装车资料入口,与另一端共用五框和自动派送门禁。 */
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/api/platform/v1/last-leg/dispatches/{dispatchNo}/loading-documents")
|
||||
public class DispatchLoadingDocumentController {
|
||||
private final DispatchLoadingDocumentService service;
|
||||
/** Controller只负责会话与输入映射,所有资料和状态规则归服务。 */
|
||||
public DispatchLoadingDocumentController(DispatchLoadingDocumentService service){this.service=service;}
|
||||
/** 读取本车已保存照片,不以设备缓存作为上传事实。 */
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:list')")
|
||||
public ApiResponse<DispatchLoadingDocumentService.Documents> get(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo) {
|
||||
return ApiResponse.success(service.get(DispatchActorFactory.platform(user),dispatchNo));
|
||||
}
|
||||
/** 上传先记录原请求,成功回执后再保存本车槽位引用。 */
|
||||
@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(service.upload(DispatchActorFactory.platform(user),dispatchNo,slot,idempotencyKey,file));
|
||||
}
|
||||
/** 最后一框保存成功时自动进入派送中,不新增人工发车按钮。 */
|
||||
@PutMapping("/{slot}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')")
|
||||
public ApiResponse<DispatchLoadingDocumentService.Documents> bind(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,
|
||||
@PathVariable PhotoSlot slot,@Valid @RequestBody BindPhoto request) {
|
||||
return ApiResponse.success(service.bind(DispatchActorFactory.platform(user),dispatchNo,slot,request));
|
||||
}
|
||||
/** 原上传仅允许当前有权限的原操作者恢复。 */
|
||||
@GetMapping("/uploads/{idempotencyKey}")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')")
|
||||
public ApiResponse<Photo> recover(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable String idempotencyKey) {
|
||||
return ApiResponse.success(service.recover(DispatchActorFactory.platform(user),dispatchNo,idempotencyKey));
|
||||
}
|
||||
/** 每次预览核对本车当前绑定,文件不得跨车跨仓读取。 */
|
||||
@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=service.content(DispatchActorFactory.platform(user),dispatchNo,slot,fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore()).contentType(MediaType.parseMediaType(value.photo().contentType()))
|
||||
.header("X-Content-Type-Options","nosniff").body(value.content());
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dispatch.controller.DispatchActorFactory;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.service.DispatchLoadingDocumentService;
|
||||
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;
|
||||
|
||||
/** 绑定仓PDA装车资料入口,与另一端共用五框和自动派送门禁。 */
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/api/pda/v1/last-leg/dispatches/{dispatchNo}/loading-documents")
|
||||
public class PdaDispatchLoadingDocumentController {
|
||||
private final DispatchLoadingDocumentService service;
|
||||
/** Controller只负责会话与输入映射,所有资料和状态规则归服务。 */
|
||||
public PdaDispatchLoadingDocumentController(DispatchLoadingDocumentService service){this.service=service;}
|
||||
/** 读取本车已保存照片,不以设备缓存作为上传事实。 */
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public ApiResponse<DispatchLoadingDocumentService.Documents> get(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo) {
|
||||
return ApiResponse.success(service.get(DispatchActorFactory.pda(user),dispatchNo));
|
||||
}
|
||||
/** 上传先记录原请求,成功回执后再保存本车槽位引用。 */
|
||||
@PostMapping(value="/{slot}/uploads",consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
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(service.upload(DispatchActorFactory.pda(user),dispatchNo,slot,idempotencyKey,file));
|
||||
}
|
||||
/** 最后一框保存成功时自动进入派送中,不新增人工发车按钮。 */
|
||||
@PutMapping("/{slot}")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public ApiResponse<DispatchLoadingDocumentService.Documents> bind(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,
|
||||
@PathVariable PhotoSlot slot,@Valid @RequestBody BindPhoto request) {
|
||||
return ApiResponse.success(service.bind(DispatchActorFactory.pda(user),dispatchNo,slot,request));
|
||||
}
|
||||
/** 原上传仅允许当前有权限的原操作者恢复。 */
|
||||
@GetMapping("/uploads/{idempotencyKey}")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public ApiResponse<Photo> recover(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable String idempotencyKey) {
|
||||
return ApiResponse.success(service.recover(DispatchActorFactory.pda(user),dispatchNo,idempotencyKey));
|
||||
}
|
||||
/** 每次预览核对本车当前绑定,文件不得跨车跨仓读取。 */
|
||||
@GetMapping("/{slot}/files/{fileId}/content")
|
||||
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
|
||||
public ResponseEntity<byte[]> content(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@PathVariable PhotoSlot slot,@PathVariable @Positive long fileId) {
|
||||
var value=service.content(DispatchActorFactory.pda(user),dispatchNo,slot,fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore()).contentType(MediaType.parseMediaType(value.photo().contentType()))
|
||||
.header("X-Content-Type-Options","nosniff").body(value.content());
|
||||
}
|
||||
}
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Actor;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Context;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchTypes.*;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchTypes.Stage;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchAggregateService;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchLoadingDepartureService;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.gateway.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.repository.OwnedBusinessRepository;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import static com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessSupport.*;
|
||||
|
||||
/** Admin与绑定仓PDA共用原整车五框照片;不复制派送单或文件引用。 */
|
||||
@Service
|
||||
public class DispatchLoadingDocumentService {
|
||||
public static final List<PhotoSlot> SLOTS=List.of(PhotoSlot.LOADING_SCENE,PhotoSlot.LOADING_LIST,
|
||||
PhotoSlot.PRE_DEPARTURE_CHECK,PhotoSlot.DRIVER_LICENSE,PhotoSlot.RECEIPT);
|
||||
private final DispatchAggregateService workflow;
|
||||
private final DispatchLoadingDepartureService departure;
|
||||
private final OwnedBusinessRepository repository;
|
||||
private final OwnedBusinessCommandService commands;
|
||||
private final OwnedBusinessProjection projection;
|
||||
private final OwnedBusinessSupport codec;
|
||||
private final OwnedBusinessFileGateway files;
|
||||
/** 复用现有附加表、文件归属校验和上传意图账本。 */
|
||||
public DispatchLoadingDocumentService(DispatchAggregateService workflow,DispatchLoadingDepartureService departure,
|
||||
OwnedBusinessRepository repository,OwnedBusinessCommandService commands,OwnedBusinessProjection projection,
|
||||
OwnedBusinessSupport codec,OwnedBusinessFileGateway files) {
|
||||
this.workflow=workflow;this.departure=departure;this.repository=repository;this.commands=commands;
|
||||
this.projection=projection;this.codec=codec;this.files=files;
|
||||
}
|
||||
public record Documents(String dispatchNo,String recordVersion,String dispatchRecordVersion,
|
||||
String stage,boolean editable,boolean complete,List<Photo> photos) {}
|
||||
public record Binding(PhotoSlot slot,BindPhoto request) {}
|
||||
public record Download(Photo photo,byte[] content) {}
|
||||
|
||||
/** GET不写入照片或推进阶段;PDA每次重验冻结仓。 */
|
||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public Documents get(Actor actor,String number) {
|
||||
require(actor,false);var context=context(actor,number,false);
|
||||
return documents(context,repository.record(context.dispatchId()),canWrite(actor));
|
||||
}
|
||||
|
||||
/** 先落稳定上传意图,再调用文件服务,未知结果不能换键重传。 */
|
||||
public Photo upload(Actor actor,String number,PhotoSlot slot,String key,MultipartFile multipart) {
|
||||
require(actor,true);slot(slot);codec.key(key);var context=context(actor,number,false);
|
||||
if(multipart==null || multipart.isEmpty() || multipart.getSize()>HttpOwnedBusinessFileGateway.MAX_SIZE)
|
||||
throw bad("照片须为非空JPG/PNG,最大10MiB");
|
||||
byte[] bytes;try{bytes=multipart.getBytes();}catch(IOException failure){throw bad("无法读取待上传照片");}
|
||||
var payload=new OwnedBusinessFileGateway.Upload(multipart.getOriginalFilename(),multipart.getContentType(),bytes);
|
||||
HttpOwnedBusinessFileGateway.validate(payload);
|
||||
var identity=new OwnedBusinessPhotoService.UploadIdentity(slot,payload.fileName(),payload.contentType(),bytes.length,codec.digest(bytes));
|
||||
var reservation=commands.reserveUpload(context.dispatchId(),operator(actor),key,identity);
|
||||
if(!reservation.created())return uploaded(reservation.command());
|
||||
try {
|
||||
var result=photo(slot,files.upload(context.dispatchId(),slot,actor.operatorId(),payload));
|
||||
commands.finishUpload(reservation.command(),"SUCCEEDED",result,null);return result;
|
||||
} catch(RuntimeException failure) {
|
||||
try{commands.finishUpload(reservation.command(),"UNKNOWN_RESULT",null,"DISPATCH_LOADING_UPLOAD_UNKNOWN");}
|
||||
catch(RuntimeException recording){failure.addSuppressed(recording);}
|
||||
throw conflict("照片上传结果未知,请保留原请求键核对");
|
||||
}
|
||||
}
|
||||
|
||||
/** 原上传恢复只允许原操作者和本车装车槽位。 */
|
||||
public Photo recover(Actor actor,String number,String key) {
|
||||
require(actor,true);codec.key(key);var context=context(actor,number,false);
|
||||
var command=commands.existing(context.dispatchId(),key,operator(actor));
|
||||
if(command==null)throw new BizException(ResultCode.NOT_FOUND,"未找到本车原上传请求");
|
||||
return uploaded(command);
|
||||
}
|
||||
|
||||
/** 锁顺序为车次、排车、附加行;第五张绑定与派送发车同事务成功或回滚。 */
|
||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||
public Documents bind(Actor actor,String number,PhotoSlot slot,BindPhoto request) {
|
||||
require(actor,true);slot(slot);if(request==null)throw bad("照片绑定请求缺失");
|
||||
codec.key(request.idempotencyKey());long version=codec.version(request.recordVersion()),fileId=codec.version(request.fileId());
|
||||
if(fileId<=0)throw bad("文件编号无效");
|
||||
var context=context(actor,number,true);var operator=operator(actor);var identity=new Binding(slot,request);
|
||||
var old=commands.existing(context.dispatchId(),request.idempotencyKey(),operator);
|
||||
if(old!=null) {
|
||||
commands.same(old,"BIND_LOADING_DOCUMENT",version,identity);
|
||||
if(!"SUCCEEDED".equals(old.getStatus()))throw conflict("原资料保存结果尚未确认");
|
||||
return codec.read(old.getResultJson(),Documents.class);
|
||||
}
|
||||
// 文件必须属于本排车与指定槽位,不能拿其他车或签收资料充数。
|
||||
var verified=photo(slot,files.metadata(context.dispatchId(),slot,fileId));
|
||||
var seed=new OwnedBusinessRows.Record();seed.setId(IdWorker.getId());seed.setDispatchId(context.dispatchId());
|
||||
seed.setCreatedBy(actor.operatorId());seed.setUpdatedBy(actor.operatorId());seed.setCreatedAt(codec.utc(Instant.now()));seed.setUpdatedAt(seed.getCreatedAt());
|
||||
var row=repository.lock(seed);
|
||||
if(row==null || row.getRecordVersion()==null || row.getRecordVersion()!=version)throw conflict("装车资料已被其他操作更新,请刷新后核对");
|
||||
var photos=new EnumMap<PhotoSlot,Photo>(PhotoSlot.class);
|
||||
for(var value:projection.photos(row))photos.put(value.slot(),value);
|
||||
photos.put(slot,new Photo(slot,slot.category(),verified.fileId(),verified.fileName(),verified.contentType(),verified.sizeBytes(),verified.uploadedAt(),Instant.now().toString()));
|
||||
row.setPhotosJson(codec.write(List.copyOf(photos.values())));row.setRecordVersion(Math.addExact(version,1));
|
||||
row.setUpdatedBy(actor.operatorId());row.setUpdatedAt(codec.utc(Instant.now()));
|
||||
if(repository.update(row,version)!=1)throw conflict("装车资料版本竞争失败");
|
||||
var history=new OwnedBusinessRows.History();history.setId(IdWorker.getId());history.setDispatchId(context.dispatchId());
|
||||
history.setRecordVersion(row.getRecordVersion());history.setAction("BIND_LOADING_DOCUMENT");history.setOperatorId(actor.operatorId());
|
||||
history.setReason("保存装车照片 "+(SLOTS.indexOf(slot)+1));history.setOccurredAt(row.getUpdatedAt());
|
||||
history.setSnapshotJson(codec.write(new Snapshot(projection.operations(row),projection.photos(row),actor.operatorId())));repository.history(history);
|
||||
if(context.stage()==Stage.AFTER_LOADING && complete(List.copyOf(photos.values()))) {
|
||||
departure.departAfterDocuments(actor,number,request.idempotencyKey());
|
||||
context=workflow.preview(actor,number);
|
||||
}
|
||||
var result=documents(context,row,true);
|
||||
commands.success(context.dispatchId(),operator,request.idempotencyKey(),"BIND_LOADING_DOCUMENT",version,identity,result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 预览必须引用当前本车已绑定文件,下载不会暴露未保存上传。 */
|
||||
@Transactional(readOnly=true)
|
||||
public Download content(Actor actor,String number,PhotoSlot slot,long fileId) {
|
||||
require(actor,false);slot(slot);var context=context(actor,number,false);
|
||||
var photo=projection.photos(repository.record(context.dispatchId())).stream()
|
||||
.filter(p->p.slot()==slot && String.valueOf(fileId).equals(p.fileId())).findFirst()
|
||||
.orElseThrow(()->new BizException(ResultCode.NOT_FOUND,"本车未绑定该装车照片"));
|
||||
return new Download(photo,files.content(context.dispatchId(),slot,fileId));
|
||||
}
|
||||
|
||||
/** 阶段与真实装载证据均须存在,PDA仓隔离由主流程统一复核。 */
|
||||
private Context context(Actor actor,String number,boolean lock) {
|
||||
var value=lock?workflow.lockForArrangement(actor,number):workflow.preview(actor,number);
|
||||
if(value.status()!=Status.ACTIVE || !Set.of(Stage.AFTER_LOADING,Stage.DELIVERING,Stage.DELIVERED).contains(value.stage())
|
||||
|| value.loadingCompletedAt()==null || value.loadingCompletionHash()==null)throw conflict("装车完成后才能管理装车资料");
|
||||
return value;
|
||||
}
|
||||
/** 返回当前共享附加版本,五个固定框各有一份已保存图片才算齐全。 */
|
||||
private Documents documents(Context context,OwnedBusinessRows.Record row,boolean editable) {
|
||||
var photos=projection.photos(row).stream().filter(p->SLOTS.contains(p.slot())).toList();
|
||||
return new Documents(context.dispatchNo(),row==null?"0":row.getRecordVersion().toString(),Long.toString(context.recordVersion()),
|
||||
context.stage().name(),editable,complete(photos),photos);
|
||||
}
|
||||
/** 不按总张数判断,重复槽位和空文件引用不能满足五框。 */
|
||||
static boolean complete(List<Photo> photos) {
|
||||
return SLOTS.stream().allMatch(slot->photos.stream().anyMatch(p->p.slot()==slot && p.fileId()!=null && !p.fileId().isBlank()));
|
||||
}
|
||||
/** 平台读写权限分离,PDA沿既有仓作业身份授权。 */
|
||||
private void require(Actor actor,boolean write) {
|
||||
if(actor==null || actor.operatorId()==null || !allowed(actor,write))throw new BizException(ResultCode.FORBIDDEN,"无装车资料操作权限");
|
||||
}
|
||||
/** 上传按钮只反映当前主体的实际写权限。 */
|
||||
private boolean canWrite(Actor actor){return allowed(actor,true);}
|
||||
/** 权限直接读取当前会话,不给PDA补造平台权限。 */
|
||||
private boolean allowed(Actor actor,boolean write) {
|
||||
var auth=SecurityContextHolder.getContext().getAuthentication();if(auth==null || actor==null)return false;
|
||||
var authorities=auth.getAuthorities().stream().map(a->a.getAuthority()).toList();
|
||||
return actor.mode()==ActorMode.PDA?actor.boundWarehouseId()!=null && authorities.contains("lastleg:pda:operate")
|
||||
&& authorities.contains("ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR")
|
||||
:authorities.contains("platform:overseas-warehouse:dispatch:"+(write?"edit":"list"));
|
||||
}
|
||||
/** 账本只消费可信操作者,权限由本服务独立校验。 */
|
||||
private OwnedBusinessModels.Actor operator(Actor actor){return new OwnedBusinessModels.Actor(actor.operatorId(),actor.operatorId(),Set.of());}
|
||||
/** 油耗照片不能替代装车五框。 */
|
||||
private void slot(PhotoSlot slot){if(!SLOTS.contains(slot))throw bad("请选择有效的装车照片框");}
|
||||
/** 恢复回执仍须属于上传动作及装车五框。 */
|
||||
private Photo uploaded(OwnedBusinessRows.Command command) {
|
||||
if(!"UPLOAD_PHOTO".equals(command.getAction()) || !"SUCCEEDED".equals(command.getStatus()) || command.getResultJson()==null)
|
||||
throw conflict("原上传结果尚未确认,请保留原请求");
|
||||
var result=codec.read(command.getResultJson(),Photo.class);slot(result.slot());return result;
|
||||
}
|
||||
/** 上传回执不标记已绑定,实际保存时间只在绑定事务填写。 */
|
||||
private Photo photo(PhotoSlot slot,OwnedBusinessFileGateway.Evidence value) {
|
||||
return new Photo(slot,slot.category(),Long.toString(value.fileId()),value.fileName(),value.contentType(),Long.toString(value.sizeBytes()),value.uploadedAt(),null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user