优化
This commit is contained in:
-19
@@ -37,25 +37,6 @@ public class DeliveryTransferController {
|
||||
return ApiResponse.success(dispatchDelivery.preview(actor(user),dispatchNo).groups());
|
||||
}
|
||||
|
||||
/** 一次快照提供全部客户目的地、费用证据及既有安排的完整数组。 */
|
||||
@GetMapping("/dispatches/{dispatchNo}/delivery-arrangement")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
|
||||
public ApiResponse<ArrangementPreview> arrangement(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(dispatchDelivery.preview(actor(user),dispatchNo));
|
||||
}
|
||||
|
||||
/** 平台一次安排本车全部客户,身份只从认证上下文获取。 */
|
||||
@PostMapping("/dispatches/{dispatchNo}/transfer-delivery")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_TRANSFER_DELIVERY + "')")
|
||||
@Operation(summary = "按真实客户生成出库单并按客户目的地生成派送单")
|
||||
public ApiResponse<CommandResult> transfer(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String dispatchNo,
|
||||
@Valid @RequestBody ArrangeDelivery request) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(dispatchDelivery.arrange(actor(user),dispatchNo,request));
|
||||
}
|
||||
|
||||
@PostMapping("/outbound-orders/one-click-dropship")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.OUTBOUND_CREATE + "')")
|
||||
@Operation(summary = "Create one-click dropship outbound and push a PDA picking task")
|
||||
|
||||
+48
-3
@@ -126,6 +126,51 @@ public class DispatchDeliveryService implements DispatchDeliveryPort {
|
||||
var input=inputs.get(group.key());var coverage=fees.coverage(group,input,actor.operatorId(),external.get(group.customerId()));
|
||||
fees.validate(group,input,coverage,external.get(group.customerId()));coverages.put(group.key(),coverage);
|
||||
}
|
||||
var fact=createDocuments(context,actor,request.idempotencyKey(),businessHash,groups,inputs,external,coverages);
|
||||
return execution.recordArrangement(context,actor,identity,fact,false);
|
||||
}
|
||||
|
||||
/** 装车父事务内自动生成全车单据;未知派送资料和未确认费用保持未登记。 */
|
||||
@Override
|
||||
@Transactional(propagation=Propagation.MANDATORY)
|
||||
public ArrangementFact prepareForDeparture(Context context,Actor actor,String commandKey) {
|
||||
if(actor==null || blank(actor.operatorId()) || blank(commandKey) || commandKey.length()>255)
|
||||
throw conflict("缺少装车操作身份或原请求标识");
|
||||
if(context==null || context.status()!=Status.ACTIVE || context.stage()!=Stage.AFTER_LOADING
|
||||
|| !context.pickingReconciled() || context.loadingCompletedAt()==null || blank(context.loadingCompletionHash())
|
||||
|| blank(context.cargoHash()) || context.cargo()==null || context.cargo().isEmpty())
|
||||
throw conflict("请先完成真实拣货核平及装车确认");
|
||||
if(actor.mode()==ActorMode.PDA && !Objects.equals(actor.boundWarehouseId(),context.warehouseId()))
|
||||
throw conflict("排车不属于当前绑定仓");
|
||||
var saved=repository.arrangement(context.dispatchId(),true);
|
||||
if(saved!=null) {
|
||||
if(!Objects.equals(saved.getOriginalCommandKey(),commandKey)
|
||||
|| !Objects.equals(saved.getCargoHash(),context.cargoHash())
|
||||
|| !Objects.equals(saved.getLoadingCompletionHash(),context.loadingCompletionHash()))
|
||||
throw conflict("本车已有不同装车命令生成的派送事实");
|
||||
var facts=departureFacts(context,true);
|
||||
if(!facts.depart().allowed())throw conflict(facts.depart().blockReason());
|
||||
return fact(saved);
|
||||
}
|
||||
// 不能把缺少安排事实的孤立单据补建成另一套;全部客户在同一事务中创建。
|
||||
if(!repository.outbounds(context.dispatchId(),true).isEmpty() || !repository.deliveries(context.dispatchId(),true).isEmpty())
|
||||
throw conflict("已存在关联单据但缺少完整安排事实,请核对");
|
||||
var groups=grouping.groups(context);var external=fees.external(context);
|
||||
var inputs=new HashMap<String,ArrangeGroup>();
|
||||
for(var group:groups) {
|
||||
String carrier=context.resources()==null || context.resources().vehicle()==null?null:context.resources().vehicle().carrierName();
|
||||
inputs.put(group.key(),new ArrangeGroup(group.key(),group.cargoHash(),null,null,null,carrier,null,null,
|
||||
null,null,null,null,null,external.get(group.customerId()).snapshotHash(),null,List.of(),List.of()));
|
||||
}
|
||||
String businessHash=grouping.hash(Arrays.asList(context.dispatchNo(),context.cargoHash(),context.loadingCompletionHash(),
|
||||
groups.stream().map(g->inputs.get(g.key())).toList()));
|
||||
return createDocuments(context,actor,commandKey,businessHash,groups,inputs,external,Map.of());
|
||||
}
|
||||
|
||||
/** 自动装车与内部安排复用同一建单、来源锁及真实外接报价发布路径,不推定原费覆盖。 */
|
||||
private ArrangementFact createDocuments(Context context,Actor actor,String commandKey,String businessHash,List<Group> groups,
|
||||
Map<String,ArrangeGroup> inputs,Map<String,com.baiyue.lastleg.fleet.trip.FleetBookingFreight.Preview> external,
|
||||
Map<String,DeliveryFeeCoverageModels.Evidence> coverages) {
|
||||
var now=Instant.now();String key="DLA"+IdWorker.getId();var outbounds=new LinkedHashMap<String,OutboundOrder>();
|
||||
for (var customer:groups.stream().map(Group::customerId).distinct().sorted().toList()) {
|
||||
var cargo=context.cargo().stream().filter(c->customer.equals(c.customerId())).toList();
|
||||
@@ -169,9 +214,9 @@ public class DispatchDeliveryService implements DispatchDeliveryPort {
|
||||
var arrangement=new DispatchDeliveryArrangement();arrangement.setId(IdWorker.getId());arrangement.setDispatchOrderId(context.dispatchId());
|
||||
arrangement.setDispatchNo(context.dispatchNo());arrangement.setArrangementKey(key);arrangement.setBusinessHash(businessHash);
|
||||
arrangement.setCargoHash(context.cargoHash());arrangement.setLoadingCompletionHash(context.loadingCompletionHash());
|
||||
arrangement.setOriginalCommandKey(request.idempotencyKey());arrangement.setOriginalOperatorId(actor.operatorId());
|
||||
arrangement.setOriginalCommandKey(commandKey);arrangement.setOriginalOperatorId(actor.operatorId());
|
||||
arrangement.setResultJson(write(result));arrangement.setCargoJson(write(context.cargo()));arrangement.setCreatedAt(local(now));repository.create(arrangement);
|
||||
return execution.recordArrangement(context,actor,identity,fact(arrangement),false);
|
||||
return fact(arrangement);
|
||||
}
|
||||
|
||||
/** 发车先取得全部出库锁,再取得全部派送锁,当前读取不申请来源锁。 */
|
||||
@@ -281,7 +326,7 @@ public class DispatchDeliveryService implements DispatchDeliveryPort {
|
||||
var destination=group.destination();d.setDeliveryGroupKey(grouping.documentGroupKey(destination));d.setDestinationType(destination.type());
|
||||
d.setDestinationWarehouseId(destination.warehouseId());d.setDestinationWarehouseCode(destination.warehouseCode());d.setDestinationWarehouseName(destination.warehouseName());
|
||||
d.setDeliveryAddress(destination.address());d.setPostalCode(destination.postalCode());d.setContactName(destination.contactName());d.setContactPhone(destination.contactPhone());
|
||||
d.setStatus("WAIT_DELIVERY");d.setVersion(0L);d.setDeliveryMethod(input.deliveryMethod());d.setPlannedDeliveryAt(local(input.plannedDeliveryAt()));
|
||||
d.setStatus("WAIT_DELIVERY");d.setVersion(0L);d.setDeliveryMethod(input.deliveryMethod());d.setPlannedDeliveryAt(input.plannedDeliveryAt()==null?null:local(input.plannedDeliveryAt()));
|
||||
d.setWaybillNo(input.waybillNo());d.setCarrierName(input.carrierName());d.setPickupMethod(input.pickupMethod());d.setLoadingType(input.loadingType());
|
||||
d.setVehicleType(input.vehicleType());d.setVehicleSize(input.vehicleSize());d.setAppointmentAccount(input.appointmentAccount());d.setSellerName(input.sellerName());d.setRemark(input.remark());
|
||||
if(context.resources()!=null){if(context.resources().vehicle()!=null)d.setVehiclePlate(context.resources().vehicle().plate());
|
||||
|
||||
+3
-9
@@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
@RequestMapping("/api/platform/v1/last-leg/dispatches")
|
||||
public class DispatchAggregateController {
|
||||
private final DispatchAggregateService service;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.service.DispatchDepartureService departure;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.service.DispatchLoadingDepartureService loadingDeparture;
|
||||
|
||||
/** 注入本域应用服务。 */
|
||||
public DispatchAggregateController(DispatchAggregateService service) { this.service=service; }
|
||||
@@ -71,17 +71,11 @@ public class DispatchAggregateController {
|
||||
public ApiResponse<com.baiyue.lastleg.dispatch.port.DispatchPlanningModels.LoadingPreview> loading(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo) {
|
||||
return ApiResponse.success(service.loading(DispatchActorFactory.platform(user),dispatchNo));
|
||||
}
|
||||
/** 显式确认装车,不复用最后扫码或实际发车操作。 */
|
||||
/** 显式确认装车后自动准备派送并发车,原请求恢复最终派送中结果。 */
|
||||
@PostMapping("/{dispatchNo}/loading-complete")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:loading-complete')")
|
||||
public ApiResponse<CommandResult> loadingComplete(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@RequestBody DispatchCommands.LoadingComplete request) {
|
||||
return ApiResponse.success(service.loadingComplete(DispatchActorFactory.platform(user),dispatchNo,request));
|
||||
}
|
||||
/** 与PDA共用实际发车服务和公共版本,未完整安排派送不能发车。 */
|
||||
@PostMapping("/{dispatchNo}/depart")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:depart')")
|
||||
public ApiResponse<CommandResult> depart(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@RequestBody DispatchCommands.Depart request) {
|
||||
return ApiResponse.success(departure.depart(DispatchActorFactory.platform(user),dispatchNo,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)
|
||||
|
||||
+6
-2
@@ -26,6 +26,9 @@ public interface DispatchAggregateMapper {
|
||||
String CURRENT_FILTER = """
|
||||
<if test='q.status != "ALL"'> AND d.status=#{q.status}</if>
|
||||
<if test='q.stage != null'> AND d.stage=#{q.stage}</if>
|
||||
<if test='q.section == "VEHICLE_INFO"'> AND d.stage='BEFORE_LOADING' AND d.review_state IN ('DRAFT','DISPATCH_REJECTED','LOADING_REJECTED')</if>
|
||||
<if test='q.section == "DISPATCH_REVIEW"'> AND d.stage='BEFORE_LOADING' AND d.review_state='DISPATCH_PENDING'</if>
|
||||
<if test='q.section == "LOADING_REVIEW"'> AND d.stage='BEFORE_LOADING' AND d.review_state='LOADING_PENDING'</if>
|
||||
<if test='q.reviewStatus != null and q.section == "DISPATCH_REVIEW"'> AND (
|
||||
""" + REVIEW_DISPATCH + ")=#{q.reviewStatus}</if>"
|
||||
+ "<if test='q.reviewStatus != null and q.section == \"LOADING_REVIEW\"'> AND (" + REVIEW_LOADING + ")=#{q.reviewStatus}</if>";
|
||||
@@ -76,14 +79,15 @@ public interface DispatchAggregateMapper {
|
||||
@Update("""
|
||||
UPDATE ll_dispatch_order SET cargo_revision=cargo_revision+1,cargo_hash=#{hash},item_count=#{items},
|
||||
pallet_count=#{pallets},package_count=#{packages},route=#{route},review_state='DRAFT',
|
||||
review_basis_hash=NULL,dispatch_review_json=#{pending},loading_review_json=#{pending},
|
||||
review_basis_hash=NULL,dispatch_review_json=#{dispatchReview},loading_review_json=#{loadingReview},
|
||||
record_version=record_version+1,updated_at=#{at}
|
||||
WHERE id=#{id} AND record_version=#{version} AND status='ACTIVE' AND stage='BEFORE_LOADING'
|
||||
AND picked_package_count=0 AND shipped_package_count=0
|
||||
""")
|
||||
int changeCargo(@Param("id") long id, @Param("version") long version, @Param("hash") String hash,
|
||||
@Param("items") int items, @Param("pallets") int pallets, @Param("packages") int packages,
|
||||
@Param("route") String route, @Param("pending") String pending, @Param("at") java.time.LocalDateTime at);
|
||||
@Param("route") String route, @Param("dispatchReview") String dispatchReview, @Param("loadingReview") String loadingReview,
|
||||
@Param("at") java.time.LocalDateTime at);
|
||||
|
||||
/** 父锁内按公共版本保存状态及审核依据,显式清空旧依据而不依赖实体非空更新策略。 */
|
||||
@Update("""
|
||||
|
||||
+10
-11
@@ -20,7 +20,7 @@ public class DispatchPdaController {
|
||||
@Resource private DispatchPdaQueryService queries;
|
||||
@Resource private DispatchPickingService picking;
|
||||
@Resource private DispatchAggregateService workflow;
|
||||
@Resource private DispatchDepartureService departure;
|
||||
@Resource private DispatchLoadingDepartureService loadingDeparture;
|
||||
|
||||
/** 批量读取当前绑定仓的整车作业。 */
|
||||
@GetMapping
|
||||
@@ -36,6 +36,13 @@ public class DispatchPdaController {
|
||||
return ApiResponse.success(queries.task(DispatchActorFactory.pda(user),dispatchNo));
|
||||
}
|
||||
|
||||
/** 理货审核与后台共用同一轮次、版本和幂等账本,驳回须填写原因。 */
|
||||
@PostMapping("/{dispatchNo}/loading-review")
|
||||
public ApiResponse<DispatchContracts.CommandResult> loadingReview(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,@Valid @RequestBody DispatchCommands.ReviewCommand request) {
|
||||
return ApiResponse.success(workflow.review(DispatchActorFactory.pda(user),dispatchNo,true,request));
|
||||
}
|
||||
|
||||
/** 共用公共版本和全局请求键执行一次真实库存扫码。 */
|
||||
@PostMapping("/{dispatchNo}/items/{itemId}/pick")
|
||||
public ApiResponse<DispatchContracts.CommandResult> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@@ -50,18 +57,10 @@ public class DispatchPdaController {
|
||||
return ApiResponse.success(workflow.loading(DispatchActorFactory.pda(user),dispatchNo));
|
||||
}
|
||||
|
||||
/** 最后一次扫码不代替显式装车确认,双方使用同一正式命令。 */
|
||||
/** 真实扫码核平后确认装车即发车,两端使用同一事务命令。 */
|
||||
@PostMapping("/{dispatchNo}/loading-complete")
|
||||
public ApiResponse<DispatchContracts.CommandResult> loadingComplete(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String dispatchNo,@RequestBody DispatchCommands.LoadingComplete request) {
|
||||
return ApiResponse.success(workflow.loadingComplete(DispatchActorFactory.pda(user),dispatchNo,request));
|
||||
}
|
||||
|
||||
/** 与平台共用实际发车服务,不执行第二次库存扣减。 */
|
||||
@PostMapping("/{dispatchNo}/depart")
|
||||
public ApiResponse<DispatchContracts.CommandResult> depart(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@Valid @RequestBody DispatchCommands.Depart request) {
|
||||
return ApiResponse.success(departure.depart(DispatchActorFactory.pda(user),dispatchNo,request));
|
||||
return ApiResponse.success(loadingDeparture.complete(DispatchActorFactory.pda(user),dispatchNo,request));
|
||||
}
|
||||
|
||||
/** 原动作权限与绑定仓同时核对未知结果,不按单客户任务恢复。 */
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public interface DispatchWholePdaReadMapper {
|
||||
/** 精确单号或有界阶段列表均先在SQL限制绑定仓。 */
|
||||
@Select("<script>SELECT * FROM ll_dispatch_order WHERE overseas_warehouse_id=#{warehouse} AND status='ACTIVE' "
|
||||
+"<choose><when test='number != null'>AND dispatch_no=#{number} </when><when test='stage != null'>AND stage=#{stage} </when>"
|
||||
+"<otherwise>AND stage IN('LOADING','AFTER_LOADING','DELIVERING') </otherwise></choose>ORDER BY updated_at DESC,id DESC LIMIT 201</script>")
|
||||
+"<otherwise>AND (stage IN('LOADING','AFTER_LOADING','DELIVERING') OR (stage='BEFORE_LOADING' AND review_state='LOADING_PENDING')) </otherwise></choose>ORDER BY updated_at DESC,id DESC LIMIT 201</script>")
|
||||
List<DispatchAggregate> headers(@Param("warehouse")String warehouse,@Param("number")String number,@Param("stage")String stage);
|
||||
String ROOTS="<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>";
|
||||
/** 一次读取选中整车所有真实客户货物。 */
|
||||
|
||||
@@ -24,9 +24,6 @@ public final class DispatchApi {
|
||||
public static final String LOADING_REVIEW = "/{dispatchNo}/loading-review";
|
||||
public static final String RETURN_TO_REVIEW = "/{dispatchNo}/return-to-review";
|
||||
public static final String LOADING_COMPLETE = "/{dispatchNo}/loading-complete";
|
||||
public static final String DEPART = "/{dispatchNo}/depart";
|
||||
public static final String DELIVERY_ARRANGEMENT = "/{dispatchNo}/delivery-arrangement";
|
||||
public static final String TRANSFER_DELIVERY = "/{dispatchNo}/transfer-delivery";
|
||||
|
||||
/** 端点容器不允许实例化。 */
|
||||
private DispatchApi() { }
|
||||
|
||||
+3
@@ -7,6 +7,9 @@ import static com.baiyue.lastleg.dispatch.port.DispatchDeliveryModels.*;
|
||||
|
||||
/** 派送域实现的整车安排和单据事实边界,禁止回写排车持久化实体。 */
|
||||
public interface DispatchDeliveryPort {
|
||||
/** 装车完成父事务自动准备全车真实客户单据,不要求人工安排或补造费用。 */
|
||||
ArrangementFact prepareForDeparture(Context context, Actor actor, String commandKey);
|
||||
|
||||
/** 一次预览返回同快照下全部客户目的地及费用证据。 */
|
||||
ArrangementPreview preview(Context context);
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ public final class DispatchViews {
|
||||
List<OrderDetail> orderDetails) { }
|
||||
|
||||
public record PdaTask(String taskNo, String dispatchNo, String warehouseId, Status status,
|
||||
Stage stage, long recordVersion, Reviews reviews, Counts counts,
|
||||
Stage stage, long recordVersion, Reviews reviews, Resources resources, Counts counts,
|
||||
boolean pickingReconciled, List<PdaItem> items, Map<Action, Gate> actions) { }
|
||||
|
||||
public record PdaItem(Long dispatchItemId, String customerId, String customerName, String palletNo,
|
||||
|
||||
+2
-2
@@ -46,8 +46,8 @@ public class DispatchAggregateRepository {
|
||||
public int removeItem(long itemId) {return items.deleteById(itemId);}
|
||||
/** 原子更新货量并作废旧审核依据。 */
|
||||
public int changeCargo(long id, long version, String hash, int itemCount, int pallets, int packages,
|
||||
String route, String pending, LocalDateTime at) {
|
||||
return mapper.changeCargo(id, version, hash, itemCount, pallets, packages, route, pending, at);
|
||||
String route, String dispatchReview, String loadingReview, LocalDateTime at) {
|
||||
return mapper.changeCargo(id, version, hash, itemCount, pallets, packages, route, dispatchReview, loadingReview, at);
|
||||
}
|
||||
/** 追加不可变业务快照。 */
|
||||
public int history(long id, DispatchAggregate row, String action, String operator, String reason, String snapshot, LocalDateTime at) {
|
||||
|
||||
+50
-10
@@ -400,7 +400,14 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
/** 调度与理货审核按同一轮次、同一事实摘要串行执行;理货通过才进入装车。 */
|
||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||
public CommandResult review(Actor actor,String number,boolean loading,DispatchCommands.ReviewCommand request) {
|
||||
var row=lockPlatform(actor,number);Action action=loading?Action.LOADING_REVIEW:Action.DISPATCH_REVIEW;
|
||||
Action action=loading?Action.LOADING_REVIEW:Action.DISPATCH_REVIEW;
|
||||
// PDA仅可处理本绑定仓理货审核,原键重试也先重新核验权限和范围。
|
||||
DispatchAggregate row;
|
||||
if(actor!=null && actor.mode()==ActorMode.PDA) {
|
||||
if(!loading || actor.boundWarehouseId()==null || !hasPermission(actor,action))
|
||||
throw new BizException(ResultCode.FORBIDDEN,"仅当前绑定仓理货员可执行理货审核");
|
||||
required(actor,number,false);fleet.lockDispatchShell(number);row=required(actor,number,true);
|
||||
} else row=lockPlatform(actor,number);
|
||||
var identity=identity(actor,number,action,request.recordVersion(),request.idempotencyKey(),request);
|
||||
var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get();
|
||||
requireEditing(row,request.recordVersion());
|
||||
@@ -422,6 +429,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
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())));
|
||||
}
|
||||
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,"理货审核驳回,请客服修改后重新提交调度审核")));
|
||||
}
|
||||
return saveCommand(row,actor,identity,request.reason());
|
||||
}
|
||||
|
||||
@@ -517,7 +530,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
List.copyOf(pallets),row.getLoadingConfirmationJson()==null?null:read(row.getLoadingConfirmationJson(),ConfirmedLoad.class),gate,List.copyOf(missing));
|
||||
}
|
||||
|
||||
/** 最后一次扫码只核平,显式装车完成才保存实载并进入装车后。 */
|
||||
/** 内部实载步骤:扫码核平后保存装载证据,中间装车后状态由外层同事务继续推进发车。 */
|
||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||
public CommandResult loadingComplete(Actor actor,String number,DispatchCommands.LoadingComplete request) {
|
||||
required(actor,number,false);fleet.lockDispatchShell(number);var row=required(actor,number,true);
|
||||
@@ -610,7 +623,15 @@ 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);
|
||||
row.setDispatchReviewJson(write(invalid));row.setLoadingReviewJson(write(invalid));
|
||||
var previous=reviews(row);
|
||||
row.setDispatchReviewJson(write(retainRejection(previous.dispatchReview(),invalid)));
|
||||
row.setLoadingReviewJson(write(retainRejection(previous.loadingReview(),invalid)));
|
||||
}
|
||||
/** 客服改资料期间保留本轮驳回原因;下一轮提交统一重置,不复用批准。 */
|
||||
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;
|
||||
}
|
||||
/** 临时取证、有效期和实际主档资格必须在提交及通过时再次核验。 */
|
||||
void verifyReviewResources(DispatchAggregate row) {
|
||||
@@ -726,7 +747,8 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
int pallets=(int)all.stream().map(i -> i.getWarehousePalletId()==null ? "PLAN:"+i.getPlanPalletId() : "PALLET:"+i.getWarehousePalletId()).distinct().count();
|
||||
String route=all.stream().map(this::destinationLabel).filter(Objects::nonNull).distinct().sorted().collect(Collectors.joining(","));
|
||||
var digest=all.stream().sorted(Comparator.comparing(DispatchItem::getSourceKey)).map(i -> List.of(i.getSourceKey(),i.getSourceVersion(),i.getCustomerId(),i.getPackageCount())).toList();
|
||||
if(repository.changeCargo(row.getId(),row.getRecordVersion(),hashes.source(digest),all.size(),pallets,packages,route,write(pending(row.getReviewRound())),now())!=1) throw conflict("排车版本已变化,未保存本次货物");
|
||||
invalidate(row,"排车货物已修改,请客服重新提交审核");
|
||||
if(repository.changeCargo(row.getId(),row.getRecordVersion(),hashes.source(digest),all.size(),pallets,packages,route,row.getDispatchReviewJson(),row.getLoadingReviewJson(),now())!=1) throw conflict("排车版本已变化,未保存本次货物");
|
||||
var stops=all.stream().map(i -> new FleetExecutionModels.DispatchStop(destinationLabel(i),i.getDeliveryAddress(),destination(i).key()))
|
||||
.collect(Collectors.toMap(FleetExecutionModels.DispatchStop::destinationKey,s->s,(a,b)->a,TreeMap::new)).values().stream().toList();
|
||||
fleet.updateDispatchCargo(row.getDispatchNo(),packages,stops);
|
||||
@@ -775,12 +797,14 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
// 到仓取货和本土卡车沿运输任务执行,不能进入仓内拣货、扣库存和转派送链。
|
||||
if (row.getTaskType()!=null && !"DELIVERY".equals(row.getTaskType())) {
|
||||
var blocked=new EnumMap<Action,Gate>(Action.class);
|
||||
for(var action:Action.values())blocked.put(action,new Gate(false,"TRANSPORT_TASK","请在排车运输任务中操作"));
|
||||
for(var action:Action.values())if(action!=Action.ARRANGE_DELIVERY && action!=Action.DEPART)
|
||||
blocked.put(action,new Gate(false,"TRANSPORT_TASK","请在排车运输任务中操作"));
|
||||
return Collections.unmodifiableMap(blocked);
|
||||
}
|
||||
var gates=new EnumMap<Action,Gate>(Action.class); boolean active=Status.ACTIVE.name().equals(row.getStatus());
|
||||
Stage stage=Stage.valueOf(row.getStage()); ReviewState review=ReviewState.valueOf(row.getReviewState());
|
||||
for(var action:Action.values()) {
|
||||
if(action==Action.ARRANGE_DELIVERY || action==Action.DEPART)continue;
|
||||
boolean state=switch(action) {
|
||||
case CREATE -> actor.mode()==ActorMode.PLATFORM;
|
||||
case EDIT,ADD_CARGO,REMOVE_CARGO,CANCEL -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0;
|
||||
@@ -799,11 +823,20 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
return Collections.unmodifiableMap(gates);
|
||||
}
|
||||
|
||||
/** 装车发运及原结果恢复都按当前装车权限与PDA绑定身份校验。 */
|
||||
void requireLoadingPermission(Actor actor) {
|
||||
if(actor==null || actor.operatorId()==null || !hasPermission(actor,Action.LOADING_COMPLETE))
|
||||
throw new BizException(ResultCode.FORBIDDEN,"当前无装车完成权限");
|
||||
}
|
||||
|
||||
/** 主体类型和正式动作权限逐项判断,不把list权限等同写权限。 */
|
||||
private static boolean hasPermission(Actor actor,Action action) {
|
||||
if(action==Action.ARRANGE_DELIVERY || action==Action.DEPART)return false;
|
||||
var auth=SecurityContextHolder.getContext().getAuthentication(); if(auth==null || actor==null) return false;
|
||||
if(actor.mode()==ActorMode.PDA) return Set.of(Action.PICK,Action.LOADING_COMPLETE,Action.DEPART).contains(action)
|
||||
&& auth.getAuthorities().stream().anyMatch(a->"lastleg:pda:operate".equals(a.getAuthority()));
|
||||
if(actor.mode()==ActorMode.PDA) return Set.of(Action.LOADING_REVIEW,Action.PICK,Action.LOADING_COMPLETE,Action.DEPART).contains(action)
|
||||
&& actor.boundWarehouseId()!=null
|
||||
&& auth.getAuthorities().stream().anyMatch(a->"lastleg:pda:operate".equals(a.getAuthority()))
|
||||
&& auth.getAuthorities().stream().anyMatch(a->"ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR".equals(a.getAuthority()));
|
||||
if(action==Action.PICK) return false;
|
||||
String permission=switch(action) {
|
||||
case CREATE -> PREFIX+"create"; case CANCEL -> PREFIX+"cancel";
|
||||
@@ -814,6 +847,9 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
return auth.getAuthorities().stream().anyMatch(a->permission.equals(a.getAuthority()));
|
||||
}
|
||||
|
||||
/** PDA审核读取整车冻结资源,与后台共用相同快照,查询不更改主档。 */
|
||||
Resources resources(DispatchAggregate row) { return read(row.getResourcesJson(),Resources.class); }
|
||||
|
||||
/** 统计按真整车去重并补齐全部零项。 */
|
||||
private static DispatchViews.Statistics statistics(List<DispatchAggregateMapper.Summary> groups) {
|
||||
var stages=new EnumMap<Stage,Long>(Stage.class); for(var stage:Stage.values()) stages.put(stage,0L);
|
||||
@@ -821,9 +857,13 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
for(var group:groups) {
|
||||
if(Status.CANCELLED.name().equals(group.status())) { cancelled+=group.quantity(); continue; }
|
||||
active+=group.quantity(); var stage=Stage.valueOf(group.stage()); stages.merge(stage,group.quantity(),Long::sum);
|
||||
if(stage==Stage.BEFORE_LOADING) { before+=group.quantity(); var review=ReviewState.valueOf(group.reviewState());
|
||||
dispatch[review==ReviewState.DRAFT?0:review==ReviewState.DISPATCH_PENDING?1:review==ReviewState.DISPATCH_REJECTED?3:2]+=group.quantity();
|
||||
loading[review==ReviewState.LOADING_PENDING?1:review==ReviewState.LOADING_REJECTED?3:review==ReviewState.APPROVED?2:0]+=group.quantity(); }
|
||||
if(stage==Stage.BEFORE_LOADING) {
|
||||
// 三分区表示当前待办,驳回只能计入客服处理,不能留在审核队列。
|
||||
var review=ReviewState.valueOf(group.reviewState());
|
||||
if(canSubmitReview(review.name()))before+=group.quantity();
|
||||
else if(review==ReviewState.DISPATCH_PENDING)dispatch[1]+=group.quantity();
|
||||
else if(review==ReviewState.LOADING_PENDING)loading[1]+=group.quantity();
|
||||
}
|
||||
}
|
||||
return new DispatchViews.Statistics(active+cancelled,active,cancelled,Collections.unmodifiableMap(stages),new DispatchViews.BeforeLoadingCounts(before,
|
||||
new DispatchViews.ReviewCounts(dispatch[0],dispatch[1],dispatch[2],dispatch[3]),new DispatchViews.ReviewCounts(loading[0],loading[1],loading[2],loading[3])));
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.dispatch.port.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.*;
|
||||
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;
|
||||
private final DispatchDeliveryPort delivery;
|
||||
private final DispatchDepartureService departure;
|
||||
private final DispatchCommandLedger commands;
|
||||
private final DispatchCommandHasher hashes;
|
||||
|
||||
/** 复用装车、派送和发车的真实规则,不另建库存或运输状态实现。 */
|
||||
public DispatchLoadingDepartureService(DispatchAggregateService workflow,DispatchDeliveryPort delivery,
|
||||
DispatchDepartureService departure,DispatchCommandLedger commands,DispatchCommandHasher hashes) {
|
||||
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);
|
||||
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();
|
||||
// 先取得本车单据锁再申请真实来源锁,后续所有步骤保持同一个整车父锁。
|
||||
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");
|
||||
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))
|
||||
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;
|
||||
}
|
||||
|
||||
/** 固定长度内部键绑定原请求和步骤,不向客户端暴露中间命令作为恢复入口。 */
|
||||
private String stepKey(String number,String original,String step) {
|
||||
return "AUTO_"+step+"_"+hashes.source(List.of(number,original,step));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -89,7 +89,7 @@ public class DispatchPdaQueryService {
|
||||
else actions.put(Action.PICK,new Gate(false,"PICKING_RECONCILED","本车全部来源已核平,请显式确认装车"));
|
||||
var counts=new Counts(n(root.getItemCount()),n(root.getPalletCount()),n(root.getPackageCount()),n(root.getPickedPackageCount()),n(root.getShippedPackageCount()));
|
||||
// 整车作业直接以排车业务号识别;真实客户子taskId只在服务端审计保存。
|
||||
result.add(new DispatchViews.PdaTask(root.getDispatchNo(),root.getDispatchNo(),root.getOverseasWarehouseId(),Status.valueOf(root.getStatus()),Stage.valueOf(root.getStage()),root.getRecordVersion(),workflow.reviews(root),counts,complete,List.copyOf(detail),Collections.unmodifiableMap(actions)));
|
||||
result.add(new DispatchViews.PdaTask(root.getDispatchNo(),root.getDispatchNo(),root.getOverseasWarehouseId(),Status.valueOf(root.getStatus()),Stage.valueOf(root.getStage()),root.getRecordVersion(),workflow.reviews(root),workflow.resources(root),counts,complete,List.copyOf(detail),Collections.unmodifiableMap(actions)));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user