feat(dispatch): 从当前排车提供内部散板需求池
This commit is contained in:
+9
@@ -36,6 +36,7 @@ import java.nio.charset.StandardCharsets;
|
||||
public class DispatchQueryController {
|
||||
@Resource private DispatchAggregateService aggregateService;
|
||||
@Resource private DispatchBolPrintService printService;
|
||||
@Resource private com.baiyue.lastleg.dispatch.service.DispatchCapacityDemandService capacityDemands;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
|
||||
@@ -47,6 +48,14 @@ public class DispatchQueryController {
|
||||
return ApiResponse.success(aggregateService.page(DispatchActorFactory.platform(user),query));
|
||||
}
|
||||
|
||||
/** 内部需求池沿独立只读权限,不授予销售整车客户及司机资料读取权限。 */
|
||||
@GetMapping("/capacity-demands")
|
||||
@PreAuthorize("hasAnyAuthority('platform:fleet:capacity-offer:list','platform:fleet:capacity-offer:manage')")
|
||||
public ApiResponse<PageResult<com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Demand>> capacityDemands(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Query query) {
|
||||
requirePlatform(user); return ApiResponse.success(capacityDemands.page(query));
|
||||
}
|
||||
|
||||
@GetMapping("/{dispatchNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
|
||||
@Operation(summary = "Get dispatch details and all pallet destinations")
|
||||
|
||||
+2
@@ -9,6 +9,8 @@ public class DispatchAggregate {
|
||||
private Long id;
|
||||
private String dispatchNo;
|
||||
private String taskType;
|
||||
/** 是否向内部散板需求池发布本车余位。 */
|
||||
private Boolean publishLooseboardDemand = true;
|
||||
private Long tripId;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseCode;
|
||||
|
||||
+14
-3
@@ -40,12 +40,12 @@ public interface DispatchAggregateMapper {
|
||||
|
||||
/** 首次创建整车所有权及初始版本,不写customer_id。 */
|
||||
@Insert("""
|
||||
INSERT INTO ll_dispatch_order(id,dispatch_no,task_type,trip_id,overseas_warehouse_id,overseas_warehouse_code,
|
||||
INSERT INTO ll_dispatch_order(id,dispatch_no,task_type,publish_looseboard_demand,trip_id,overseas_warehouse_id,overseas_warehouse_code,
|
||||
overseas_warehouse_name,status,stage,record_version,cargo_revision,cargo_hash,review_state,review_round,
|
||||
dispatch_review_json,loading_review_json,resources_json,vehicle_plate,route,planned_departure_at,
|
||||
estimated_arrival_at,item_count,pallet_count,package_count,picked_package_count,shipped_package_count,
|
||||
remark,created_by,created_at,updated_at)
|
||||
VALUES(#{r.id},#{r.dispatchNo},COALESCE(#{r.taskType},'DELIVERY'),#{r.tripId},#{r.overseasWarehouseId},#{r.overseasWarehouseCode},
|
||||
VALUES(#{r.id},#{r.dispatchNo},COALESCE(#{r.taskType},'DELIVERY'),#{r.publishLooseboardDemand},#{r.tripId},#{r.overseasWarehouseId},#{r.overseasWarehouseCode},
|
||||
#{r.overseasWarehouseName},#{r.status},#{r.stage},#{r.recordVersion},#{r.cargoRevision},#{r.cargoHash},
|
||||
#{r.reviewState},#{r.reviewRound},#{r.dispatchReviewJson},#{r.loadingReviewJson},#{r.resourcesJson},
|
||||
#{r.vehiclePlate},#{r.route},#{r.plannedDepartureAt},#{r.estimatedArrivalAt},#{r.itemCount},#{r.palletCount},
|
||||
@@ -66,6 +66,17 @@ public interface DispatchAggregateMapper {
|
||||
+ " ORDER BY d.created_at DESC,d.id DESC LIMIT #{q.limit} OFFSET #{q.offset}</script>")
|
||||
List<DispatchAggregate> page(@Param("q") Search query);
|
||||
|
||||
/** 只读余位来源仅含勾选发布、未发车的有效派送排车,不混入取货和本土卡车。 */
|
||||
@Select("""
|
||||
<script>SELECT d.* FROM ll_dispatch_order d
|
||||
WHERE d.publish_looseboard_demand=TRUE AND d.task_type='DELIVERY' AND d.status='ACTIVE'
|
||||
AND d.departed_at IS NULL AND d.stage IN ('BEFORE_LOADING','LOADING','AFTER_LOADING')
|
||||
<if test='q.origin != null'>AND d.overseas_warehouse_name LIKE CONCAT('%',#{q.origin},'%')</if>
|
||||
<if test='q.destination != null'>AND d.route LIKE CONCAT('%',#{q.destination},'%')</if>
|
||||
ORDER BY d.planned_departure_at,d.created_at DESC,d.id DESC</script>
|
||||
""")
|
||||
List<DispatchAggregate> capacityDemands(@Param("q") com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Query query);
|
||||
|
||||
/** 分页总数复用完全相同筛选,不能用当前页长度替代。 */
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_dispatch_order d WHERE 1=1 " + BASE_FILTER + CURRENT_FILTER + "</script>")
|
||||
long count(@Param("q") Search query);
|
||||
@@ -91,7 +102,7 @@ public interface DispatchAggregateMapper {
|
||||
|
||||
/** 父锁内按公共版本保存状态及审核依据,显式清空旧依据而不依赖实体非空更新策略。 */
|
||||
@Update("""
|
||||
UPDATE ll_dispatch_order SET status=#{r.status},stage=#{r.stage},record_version=#{r.recordVersion},
|
||||
UPDATE ll_dispatch_order SET status=#{r.status},stage=#{r.stage},record_version=#{r.recordVersion},publish_looseboard_demand=#{r.publishLooseboardDemand},
|
||||
review_state=#{r.reviewState},review_round=#{r.reviewRound},review_basis_hash=#{r.reviewBasisHash},
|
||||
dispatch_review_json=#{r.dispatchReviewJson},loading_review_json=#{r.loadingReviewJson},
|
||||
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},
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.dispatch.port;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
|
||||
/** 内部余位池仅公开本车运输摘要,不包含客户货物或司机私人资料。 */
|
||||
public final class DispatchCapacityDemands {
|
||||
/** 只承载只读查询模型。 */
|
||||
private DispatchCapacityDemands() { }
|
||||
public record Query(Integer page, Integer size, String origin, String destination) {
|
||||
/** 查询范围有界,默认每页二十条。 */
|
||||
public Query {
|
||||
page = page == null ? 1 : page; size = size == null ? 20 : size;
|
||||
if (page < 1 || size < 1 || size > 100) throw new IllegalArgumentException("余位需求分页参数无效");
|
||||
origin = origin == null || origin.isBlank() ? null : origin.trim();
|
||||
destination = destination == null || destination.isBlank() ? null : destination.trim();
|
||||
}
|
||||
}
|
||||
public record Demand(String dispatchNo, String warehouseName, String route, Instant plannedDepartureAt,
|
||||
Integer vehiclePalletSpaces, Integer scheduledPalletCount, Integer remainingPalletSpaces,
|
||||
BigDecimal remainingWeightKg, BigDecimal remainingVolumeCbm, boolean capacityKnown, String capacityReason) { }
|
||||
}
|
||||
+6
-3
@@ -23,13 +23,16 @@ public final class DispatchCommands {
|
||||
/** 空cargo表示先建空单;实际来源及客户必须由服务端解析。 */
|
||||
public record Create(String idempotencyKey, String warehouseId, Vehicle vehicle, Driver driver,
|
||||
Instant plannedDepartureAt, Instant estimatedArrivalAt, String remark,
|
||||
List<CargoSelection> cargo, List<BatchAllocation> batchAllocations) { }
|
||||
List<CargoSelection> cargo, List<BatchAllocation> batchAllocations, Boolean publishLooseboardDemand) {
|
||||
/** 新建排车默认发布内部余位,显式取消时保持不发布。 */
|
||||
public Create { publishLooseboardDemand = publishLooseboardDemand == null ? true : publishLooseboardDemand; }
|
||||
}
|
||||
|
||||
public record Update(Long recordVersion, String idempotencyKey, Vehicle vehicle, Driver driver,
|
||||
Instant plannedDepartureAt, Instant estimatedArrivalAt,
|
||||
String remark) {
|
||||
String remark, Boolean publishLooseboardDemand) {
|
||||
/** 缺版本与有效版本零必须区分。 */
|
||||
public Update { recordVersion = requireVersion(recordVersion); }
|
||||
public Update { recordVersion = requireVersion(recordVersion); if (publishLooseboardDemand == null) throw new IllegalArgumentException("请明确是否发布散板需求"); }
|
||||
}
|
||||
|
||||
public record AddCargo(Long recordVersion, String idempotencyKey, List<CargoSelection> cargo,
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class DispatchViews {
|
||||
ReviewState reviewState, Reviews reviews, Counts counts,
|
||||
boolean pickingReconciled, Map<Action, Gate> actions,
|
||||
VehicleInfo vehicleInfo, DispatchInfo dispatchInfo, Documents documents,
|
||||
List<OrderDetail> orderDetails) { }
|
||||
List<OrderDetail> orderDetails, boolean publishLooseboardDemand) { }
|
||||
|
||||
public record PdaTask(String taskNo, String dispatchNo, String warehouseId, Status status,
|
||||
Stage stage, long recordVersion, Reviews reviews, Resources resources, Counts counts,
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ public class DispatchAggregateRepository {
|
||||
public DispatchAggregate lock(String number) { return mapper.lock(number); }
|
||||
/** 整车分页。 */
|
||||
public List<DispatchAggregate> page(DispatchAggregateMapper.Search query) { return mapper.page(query); }
|
||||
/** 一次读取当前有效的内部余位来源,服务计算满载筛选后再分页。 */
|
||||
public List<DispatchAggregate> capacityDemands(com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Query query) { return mapper.capacityDemands(query); }
|
||||
/** 整车分页总数。 */
|
||||
public long count(DispatchAggregateMapper.Search query) { return mapper.count(query); }
|
||||
/** 全搜索基集的阶段和审核分布。 */
|
||||
|
||||
+3
-1
@@ -305,6 +305,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
row.setId(id); row.setDispatchNo(number); row.setTripId(shell.tripId());
|
||||
row.setOverseasWarehouseId(shell.warehouseId()); row.setOverseasWarehouseCode(shell.warehouseCode()); row.setOverseasWarehouseName(shell.warehouseName());
|
||||
row.setStatus(Status.ACTIVE.name()); row.setStage(Stage.BEFORE_LOADING.name()); row.setRecordVersion(0L);
|
||||
row.setPublishLooseboardDemand(request.publishLooseboardDemand());
|
||||
row.setCargoRevision(0L); row.setCargoHash(hashes.source(List.of())); row.setReviewState(ReviewState.DRAFT.name()); row.setReviewRound(0L);
|
||||
row.setDispatchReviewJson(write(pending(0))); row.setLoadingReviewJson(write(pending(0)));
|
||||
var photos=request.driver()==null || request.driver().evidence()==null?List.<Long>of():request.driver().evidence().photoFileIds();
|
||||
@@ -393,6 +394,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var actual=fleet.dispatchResources(number,fleetVehicle(request.vehicle()),fleetDriver(request.driver()));
|
||||
row.setResourcesJson(write(resources(actual,photos)));row.setVehiclePlate(actual.vehicle()==null?null:actual.vehicle().plate());
|
||||
row.setPlannedDepartureAt(utc(request.plannedDepartureAt()));row.setEstimatedArrivalAt(utc(request.estimatedArrivalAt()));row.setRemark(request.remark());
|
||||
row.setPublishLooseboardDemand(request.publishLooseboardDemand());
|
||||
if(!Objects.equals(before,basis(row)))invalidate(row,"车辆、司机或计划信息已修改");
|
||||
fleet.saveDispatchResources(number,actor.operatorId(),actual,request.plannedDepartureAt(),request.remark());
|
||||
return saveCommand(row,actor,identity,request.remark());
|
||||
@@ -754,7 +756,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
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,exceptionRecords(row)),
|
||||
documents,orderDetails(cargo));
|
||||
documents,orderDetails(cargo),Boolean.TRUE.equals(row.getPublishLooseboardDemand()));
|
||||
}
|
||||
|
||||
/** 恢复动作结果前重新核验当前单据范围和原动作权限。 */
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchAggregate;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Objects;
|
||||
|
||||
/** 从日常排车实时投影内部余位,不创建第二套车次、发布单或占位。 */
|
||||
@Service
|
||||
public class DispatchCapacityDemandService {
|
||||
private final DispatchAggregateRepository repository;
|
||||
private final ObjectMapper json;
|
||||
/** 复用排车仓储和正式装载快照解析。 */
|
||||
public DispatchCapacityDemandService(DispatchAggregateRepository repository, ObjectMapper json) {
|
||||
this.repository = repository; this.json = json;
|
||||
}
|
||||
/** 当前候选一次查询;已知满载先过滤,随后分页,保证总数与结果一致。 */
|
||||
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ)
|
||||
public PageResult<DispatchCapacityDemands.Demand> page(DispatchCapacityDemands.Query query) {
|
||||
var values = repository.capacityDemands(query).stream().map(this::project).filter(Objects::nonNull).toList();
|
||||
long start = Math.min((long)(query.page()-1)*query.size(), values.size());
|
||||
int end = (int)Math.min(start+query.size(), values.size());
|
||||
return new PageResult<>(values.subList((int)start,end), query.page(), query.size(), values.size(), (values.size()+query.size()-1)/query.size());
|
||||
}
|
||||
/** 实托数不等于标准托位;缺少可靠装载证据时明确未知,不能拿三十托或零值推算。 */
|
||||
private DispatchCapacityDemands.Demand project(DispatchAggregate row) {
|
||||
Integer slots = null, remaining = null; BigDecimal weight = null, volume = null;
|
||||
String reason = "占用标准托位、重量及体积待确认";
|
||||
try {
|
||||
var resources = row.getResourcesJson() == null ? null : json.readValue(row.getResourcesJson(), DispatchContracts.Resources.class);
|
||||
var cap = resources == null || resources.vehicle() == null ? null : resources.vehicle().capacity();
|
||||
if (cap != null) slots = cap.palletSpaces();
|
||||
var load = row.getLoadingConfirmationJson() == null ? null : json.readValue(row.getLoadingConfirmationJson(), DispatchContracts.ConfirmedLoad.class);
|
||||
if (cap == null || slots == null || slots <= 0 || !positive(cap.maxLoadKg()) || !positive(cap.maxVolumeCbm())) reason = "车辆真实容量待确认";
|
||||
if (cap != null && load != null && load.pallets() != null && !load.pallets().isEmpty()
|
||||
&& load.pallets().stream().allMatch(p -> p.warehousePalletId()!=null && p.palletSpaces()>0)
|
||||
&& load.pallets().stream().map(DispatchContracts.LoadingMeasurement::warehousePalletId).distinct().count()==load.pallets().size()) {
|
||||
// 首版均在起运仓上车,后续仅卸货;任一已知维度满载都退出需求池。
|
||||
long occupied = load.pallets().stream().mapToLong(DispatchContracts.LoadingMeasurement::palletSpaces).sum();
|
||||
if (slots != null && slots > 0) { if (occupied >= slots) return null; remaining = Math.toIntExact(slots-occupied); }
|
||||
if (positive(cap.maxLoadKg()) && load.pallets().stream().allMatch(p -> positive(p.weightKg()))) {
|
||||
var used = load.pallets().stream().map(DispatchContracts.LoadingMeasurement::weightKg).reduce(BigDecimal.ZERO,BigDecimal::add);
|
||||
if (used.compareTo(cap.maxLoadKg())>=0) return null; weight=cap.maxLoadKg().subtract(used);
|
||||
}
|
||||
if (positive(cap.maxVolumeCbm()) && load.pallets().stream().allMatch(p -> positive(p.volumeCbm()))) {
|
||||
var used = load.pallets().stream().map(DispatchContracts.LoadingMeasurement::volumeCbm).reduce(BigDecimal.ZERO,BigDecimal::add);
|
||||
if (used.compareTo(cap.maxVolumeCbm())>=0) return null; volume=cap.maxVolumeCbm().subtract(used);
|
||||
}
|
||||
if (remaining!=null && weight!=null && volume!=null) reason=null;
|
||||
}
|
||||
} catch (JsonProcessingException invalid) { reason="车辆或装载资料待核对"; }
|
||||
return new DispatchCapacityDemands.Demand(row.getDispatchNo(),row.getOverseasWarehouseName(),row.getRoute(),
|
||||
row.getPlannedDepartureAt()==null?null:row.getPlannedDepartureAt().toInstant(ZoneOffset.UTC),slots,row.getPalletCount(),remaining,weight,volume,reason==null,reason);
|
||||
}
|
||||
/** 三维容量均须有真实正值。 */
|
||||
private static boolean positive(BigDecimal value) { return value!=null && value.signum()>0; }
|
||||
}
|
||||
Reference in New Issue
Block a user