优化
This commit is contained in:
+6
@@ -77,6 +77,12 @@ 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("/{dispatchNo}/exceptions")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:edit')")
|
||||
public ApiResponse<CommandResult> markException(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@RequestBody DispatchCommands.MarkException request) {
|
||||
return ApiResponse.success(service.markException(DispatchActorFactory.platform(user),dispatchNo,request));
|
||||
}
|
||||
/** 本车证据先上传;维护和当前待审权限分别由服务端核验,引用随对应命令保存。 */
|
||||
@PostMapping(value="/{dispatchNo}/evidence/{kind}",consumes=org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAnyAuthority('platform:overseas-warehouse:dispatch:edit','platform:fleet:trip:dispatch-review','platform:fleet:trip:loading-review')")
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ public class DispatchAggregate {
|
||||
private String dispatchReviewJson;
|
||||
private String loadingReviewJson;
|
||||
private String resourcesJson;
|
||||
private String exceptionsJson;
|
||||
private String vehiclePlate;
|
||||
private String route;
|
||||
private LocalDateTime plannedDepartureAt;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/** 车辆、司机及审核补充证据属于本张整车,不能借司机主档或客户文件授权。 */
|
||||
public interface DispatchEvidenceGateway {
|
||||
enum Kind { VEHICLE, DRIVER, DRIVER_PHOTO }
|
||||
enum Kind { VEHICLE, DRIVER, DRIVER_PHOTO, EXCEPTION }
|
||||
record File(Long fileId,String fileName,String contentType,long sizeBytes) { }
|
||||
/** 上传只写本车、本类证据。 */
|
||||
File upload(long dispatchId,Kind kind,String operator,MultipartFile file);
|
||||
|
||||
+2
-2
@@ -83,9 +83,9 @@ public class HttpDispatchEvidenceGateway implements DispatchEvidenceGateway {
|
||||
|| !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/图片证据规则。 */
|
||||
/** 司机本人照片与异常证据只接受图片;车辆和驾驶证沿现有PDF/图片证据规则。 */
|
||||
private static boolean supportedType(Kind kind,String contentType) {
|
||||
return TYPES.contains(Objects.toString(contentType,"")) && (kind!=Kind.DRIVER_PHOTO || !"application/pdf".equals(contentType));
|
||||
return TYPES.contains(Objects.toString(contentType,"")) && ((kind!=Kind.DRIVER_PHOTO && kind!=Kind.EXCEPTION) || !"application/pdf".equals(contentType));
|
||||
}
|
||||
/** 无效证据不能作为已核实资料。 */
|
||||
private static ResponseStatusException invalid(){return new ResponseStatusException(HttpStatus.BAD_REQUEST,"证据须为本车有效PDF、JPG或PNG,最大10MiB");}
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ public interface DispatchAggregateMapper {
|
||||
UPDATE ll_dispatch_order SET status=#{r.status},stage=#{r.stage},record_version=#{r.recordVersion},
|
||||
review_state=#{r.reviewState},review_round=#{r.reviewRound},review_basis_hash=#{r.reviewBasisHash},
|
||||
dispatch_review_json=#{r.dispatchReviewJson},loading_review_json=#{r.loadingReviewJson},
|
||||
cargo_hash=#{r.cargoHash},cargo_revision=#{r.cargoRevision},resources_json=#{r.resourcesJson},vehicle_plate=#{r.vehiclePlate},planned_departure_at=#{r.plannedDepartureAt},
|
||||
exceptions_json=#{r.exceptionsJson},cargo_hash=#{r.cargoHash},cargo_revision=#{r.cargoRevision},resources_json=#{r.resourcesJson},vehicle_plate=#{r.vehiclePlate},planned_departure_at=#{r.plannedDepartureAt},
|
||||
estimated_arrival_at=#{r.estimatedArrivalAt},remark=#{r.remark},loading_completed_at=#{r.loadingCompletedAt},
|
||||
loading_completion_hash=#{r.loadingCompletionHash},loading_confirmation_json=#{r.loadingConfirmationJson},arrangement_key=#{r.arrangementKey},
|
||||
arrangement_business_hash=#{r.arrangementBusinessHash},departed_at=#{r.departedAt},
|
||||
|
||||
@@ -73,6 +73,13 @@ public final class DispatchCommands {
|
||||
public LoadingComplete { recordVersion = requireVersion(recordVersion); }
|
||||
}
|
||||
|
||||
/** 派送异常仅登记事实,不改变派送、签收或库存。 */
|
||||
public record MarkException(Long recordVersion, String idempotencyKey, ExceptionType exceptionType,
|
||||
ReturnDirection returnDirection, String description, List<Long> imageFileIds) {
|
||||
/** 必填版本用于阻止过期页面覆盖当前排车。 */
|
||||
public MarkException { recordVersion = requireVersion(recordVersion); }
|
||||
}
|
||||
|
||||
public record Depart(Long recordVersion, String idempotencyKey, String remark) {
|
||||
/** 实际发车不得接受缺省版本。 */
|
||||
public Depart { recordVersion = requireVersion(recordVersion); }
|
||||
|
||||
@@ -9,6 +9,8 @@ public final class DispatchTypes {
|
||||
public enum Stage { BEFORE_LOADING, LOADING, AFTER_LOADING, DELIVERING, DELIVERED }
|
||||
public enum ReviewState { DRAFT, DISPATCH_PENDING, DISPATCH_REJECTED, LOADING_PENDING, LOADING_REJECTED, APPROVED }
|
||||
public enum ReviewStatus { NOT_SUBMITTED, PENDING, APPROVED, REJECTED }
|
||||
public enum ExceptionType { REFUSED, PARTIAL_RECEIPT, VEHICLE_EXCEPTION }
|
||||
public enum ReturnDirection { OVERSEAS_WAREHOUSE, OTHER }
|
||||
public enum Decision { APPROVED, REJECTED }
|
||||
public enum ResourceMode { MASTER, EXTERNAL }
|
||||
public enum ActorMode { PLATFORM, PDA }
|
||||
@@ -18,6 +20,6 @@ public final class DispatchTypes {
|
||||
public enum Effect { APPLIED, EXISTING }
|
||||
public enum Action {
|
||||
CREATE, EDIT, ADD_CARGO, REMOVE_CARGO, CANCEL, SUBMIT_REVIEW, DISPATCH_REVIEW,
|
||||
LOADING_REVIEW, RETURN_TO_REVIEW, PICK, LOADING_COMPLETE, ARRANGE_DELIVERY, DEPART
|
||||
LOADING_REVIEW, RETURN_TO_REVIEW, PICK, LOADING_COMPLETE, ARRANGE_DELIVERY, DEPART, MARK_EXCEPTION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,11 @@ public final class DispatchViews {
|
||||
|
||||
public record DispatchInfo(String route, Instant plannedDepartureAt, Instant departedAt,
|
||||
Instant estimatedArrivalAt, List<FbaAppointment> fbaAppointments,
|
||||
List<BatchReference> appointmentBatches) { }
|
||||
List<BatchReference> appointmentBatches, List<ExceptionRecord> exceptions) { }
|
||||
|
||||
/** 排车级异常记录;退货去向只描述安排,不产生退货入库事实。 */
|
||||
public record ExceptionRecord(ExceptionType exceptionType, ReturnDirection returnDirection,
|
||||
String description, List<Long> imageFileIds, Instant occurredAt) { }
|
||||
|
||||
public record Documents(String dispatchNo, List<DispatchDeliveryModels.OutboundReference> outbounds,
|
||||
List<DispatchDeliveryModels.DeliveryReference> deliveries,
|
||||
|
||||
+42
-2
@@ -238,7 +238,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
/** 上传只返回本车证据;待审按当前审核权限授权,客服阶段按维护权限授权。 */
|
||||
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());
|
||||
requirePlatform(actor);var row=required(actor,number,false);
|
||||
if(kind==com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.EXCEPTION) {
|
||||
requireExceptionAllowed(actor,row,row.getRecordVersion());
|
||||
return evidence.upload(row.getId(),kind,actor.operatorId(),file);
|
||||
}
|
||||
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,"无当前阶段附件上传权限");
|
||||
@@ -663,6 +668,40 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
for(var id:ids)evidence.metadata(dispatch,kind,id);
|
||||
}
|
||||
|
||||
/** 仅派送中登记异常;原键重试先恢复,避免追加重复记录。 */
|
||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||
public CommandResult markException(Actor actor,String number,DispatchCommands.MarkException request) {
|
||||
var row=lockPlatform(actor,number);
|
||||
if(!hasPermission(actor,Action.MARK_EXCEPTION))throw new BizException(ResultCode.FORBIDDEN,"无标记异常权限");
|
||||
var identity=identity(actor,number,Action.MARK_EXCEPTION,request.recordVersion(),request.idempotencyKey(),request);
|
||||
var replay=commands.replay(actor,number,identity);if(replay.isPresent())return replay.get();
|
||||
requireExceptionAllowed(actor,row,request.recordVersion());
|
||||
if(request.exceptionType()==null)throw bad("请选择异常类型");
|
||||
var description=trim(request.description());
|
||||
if(description!=null && description.length()>1000)throw bad("异常说明不能超过1000字");
|
||||
var ids=request.imageFileIds()==null?List.<Long>of():request.imageFileIds();
|
||||
if(ids.size()>20 || ids.stream().anyMatch(id->id==null || id<=0) || ids.stream().distinct().count()!=ids.size())throw bad("异常图片引用无效或超过20张");
|
||||
// 文件须属于本车异常类别,不能借用其他车辆或审核附件。
|
||||
for(var id:ids)evidence.metadata(row.getId(),com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.EXCEPTION,id);
|
||||
var records=new ArrayList<>(exceptionRecords(row));
|
||||
records.add(new DispatchViews.ExceptionRecord(request.exceptionType(),request.returnDirection(),description,List.copyOf(ids),Instant.now()));
|
||||
row.setExceptionsJson(write(records));
|
||||
return saveCommand(row,actor,identity,description);
|
||||
}
|
||||
|
||||
/** 异常登记复用排车维护权限;版本、任务类型与阶段由服务端再次核验。 */
|
||||
private void requireExceptionAllowed(Actor actor,DispatchAggregate row,long version) {
|
||||
if(!hasPermission(actor,Action.MARK_EXCEPTION))throw new BizException(ResultCode.FORBIDDEN,"无标记异常权限");
|
||||
if(row.getRecordVersion()!=version)throw conflict("排车版本已变化,请刷新后再标记异常");
|
||||
if(!actions(actor,row).get(Action.MARK_EXCEPTION).allowed())throw conflict("仅派送中的仓库派送排车可以标记异常");
|
||||
}
|
||||
|
||||
/** 尚未登记时为空集合,已登记记录按保存顺序完整回看。 */
|
||||
private List<DispatchViews.ExceptionRecord> exceptionRecords(DispatchAggregate row) {
|
||||
if(row.getExceptionsJson()==null)return List.of();
|
||||
return Arrays.asList(read(row.getExceptionsJson(),DispatchViews.ExceptionRecord[].class));
|
||||
}
|
||||
|
||||
/** 同一只读快照返回分页、总数和基集分布,不能在前端过滤当前页。 */
|
||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public DispatchViews.Page page(Actor actor, DispatchViews.Query query) {
|
||||
@@ -699,7 +738,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var documents=new DispatchViews.Documents(number,repository.outbounds(row.getId()),deliveries,instant(row.getCreatedAt()),instant(row.getUpdatedAt()),row.getRemark());
|
||||
return new DispatchViews.Detail(number,row.getTaskType()==null?"DELIVERY":row.getTaskType(),Status.valueOf(row.getStatus()),Stage.valueOf(row.getStage()),row.getRecordVersion(),ReviewState.valueOf(row.getReviewState()),
|
||||
reviews(row),counts(row),reconciled(row),actions(actor,row),new DispatchViews.VehicleInfo(resources.vehicle(),resources.driver()),
|
||||
new DispatchViews.DispatchInfo(row.getRoute(),instant(row.getPlannedDepartureAt()),instant(row.getDepartedAt()),instant(row.getEstimatedArrivalAt()),appointments,batchRefs),
|
||||
new DispatchViews.DispatchInfo(row.getRoute(),instant(row.getPlannedDepartureAt()),instant(row.getDepartedAt()),instant(row.getEstimatedArrivalAt()),appointments,batchRefs,exceptionRecords(row)),
|
||||
documents,orderDetails(cargo));
|
||||
}
|
||||
|
||||
@@ -817,6 +856,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
for(var action:Action.values()) {
|
||||
if(action==Action.ARRANGE_DELIVERY || action==Action.DEPART)continue;
|
||||
boolean state=switch(action) {
|
||||
case MARK_EXCEPTION -> active && stage==Stage.DELIVERING;
|
||||
case CREATE -> actor.mode()==ActorMode.PLATFORM;
|
||||
case EDIT,ADD_CARGO,REMOVE_CARGO,CANCEL -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0;
|
||||
case SUBMIT_REVIEW -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0 && canSubmitReview(row.getReviewState());
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ public final class DispatchCommandCodec {
|
||||
case DEPART -> DispatchResults.Departed.class;
|
||||
case ARRANGE_DELIVERY -> DispatchDeliveryModels.ArrangementResult.class;
|
||||
case CREATE, EDIT, ADD_CARGO, REMOVE_CARGO, CANCEL, SUBMIT_REVIEW,
|
||||
DISPATCH_REVIEW, LOADING_REVIEW, RETURN_TO_REVIEW -> DispatchResults.Saved.class;
|
||||
DISPATCH_REVIEW, LOADING_REVIEW, RETURN_TO_REVIEW, MARK_EXCEPTION -> DispatchResults.Saved.class;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,6 @@ public record ContainerArrivalTimeView(
|
||||
String sourceContainerBookingId,
|
||||
LocalDateTime estimatedWarehouseArrivalAt,
|
||||
LocalDateTime actualWarehouseArrivalAt,
|
||||
int version
|
||||
int version,
|
||||
long planCount
|
||||
) { }
|
||||
|
||||
@@ -210,12 +210,19 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
@Select("SELECT * FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
|
||||
List<LastlegOrder> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
/** 人工时间及计划冻结时间优先;返回计划数量供各页面统一判断默认日期。 */
|
||||
@Select("<script>SELECT o.source_container_booking_id AS sourceContainerBookingId," +
|
||||
"CASE WHEN COUNT(o.estimated_warehouse_at)=COUNT(*) AND COUNT(DISTINCT o.estimated_warehouse_at)=1 " +
|
||||
"AND (MAX(o.arrival_time_version)>0 OR EXISTS(SELECT 1 FROM ll_inbound_plan p " +
|
||||
"JOIN ll_order po ON po.id=p.source_order_id AND po.customer_id=p.customer_id " +
|
||||
"WHERE po.source_container_booking_id=o.source_container_booking_id)) " +
|
||||
"THEN MAX(o.estimated_warehouse_at) ELSE NULL END AS estimatedWarehouseArrivalAt," +
|
||||
"(SELECT MIN(s.receiving_at) FROM ll_receiving_container_session s " +
|
||||
"WHERE s.source_container_booking_id=o.source_container_booking_id AND s.receiving_at IS NOT NULL) AS actualWarehouseArrivalAt," +
|
||||
"CASE WHEN COUNT(DISTINCT o.arrival_time_version)=1 THEN MAX(o.arrival_time_version) ELSE -1 END AS version " +
|
||||
"CASE WHEN COUNT(DISTINCT o.arrival_time_version)=1 THEN MAX(o.arrival_time_version) ELSE -1 END AS version," +
|
||||
"(SELECT COUNT(*) FROM ll_inbound_plan p JOIN ll_order po " +
|
||||
"ON po.id=p.source_order_id AND po.customer_id=p.customer_id " +
|
||||
"WHERE po.source_container_booking_id=o.source_container_booking_id) AS planCount " +
|
||||
"FROM ll_order o WHERE o.source_container_booking_id IN " +
|
||||
"<foreach collection='bookingIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"GROUP BY o.source_container_booking_id</script>")
|
||||
|
||||
Reference in New Issue
Block a user