feat(dispatch): 从当前排车提供内部散板需求池
This commit is contained in:
@@ -52,6 +52,8 @@
|
||||
|
||||
换标直接出库的实物体积快照在 `ll_outbound_order.volume_cbm`、`ll_outbound_pallet_item.volume_cbm`、`ll_delivery_order.total_volume_cbm` 三列均允许 NULL,表示来源尚未确认;已有实盘尺寸按箱计算,不能用默认 0 代替未知,也不按 SKU 件数重复累计。两份初始化基线同步此定义,已有 TEST 只能在核对结构和授权后精确调整这三列,不重放空库文件或回填历史行。
|
||||
|
||||
排车内部散板需求发布选项由 `lastleg-service/src/main/resources/db/dispatch-capacity-demand.sql` 提供精确结构变更:仅向 `ll_dispatch_order` 新增默认勾选的 `publish_looseboard_demand` 列,Lastleg与海外空库基线同步。该文件不是自动启动脚本;执行须单独授权,不能重放完整空库文件,不新增业务数据回填或清理。实际执行与部署状态仍归测试环境主契约。
|
||||
|
||||
## 唯一入口与区域边界
|
||||
|
||||
V13 收货覆盖由 `lastleg-service/src/main/resources/db/appointment-receiving.sql` 提供四表:独立收货预约、当前/历史客户成员、待确认变化和不可变覆盖版本;同步 Lastleg 与海外空库基线。有效外部约号唯一性归收货预约的目的仓与约号,批次头表不再独立占用约号。部署前必须核对四表及旧头表唯一索引;本批只准备基线,不提供测试存量回填或自动重置。旧 `reset-test-fba-appointments.sql` 已拒绝存在这些新表的环境,不能用于重放 V13 初始化。实际 SQL 与部署须逐项授权。
|
||||
|
||||
@@ -417,6 +417,7 @@ CREATE TABLE ll_dispatch_order (
|
||||
id BIGINT NOT NULL COMMENT '整车排车主单内部主键',
|
||||
dispatch_no VARCHAR(64) NOT NULL COMMENT '真实排车业务号',
|
||||
task_type VARCHAR(32) NOT NULL DEFAULT 'DELIVERY' COMMENT '任务类型:仓库派送、到仓取货、本土卡车',
|
||||
publish_looseboard_demand TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否向内部散板需求池发布余位',
|
||||
trip_id BIGINT NOT NULL COMMENT '本趟唯一车辆资源记录ID',
|
||||
overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '真实起运海外仓ID',
|
||||
overseas_warehouse_code VARCHAR(128) NULL COMMENT '起运海外仓编码快照',
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
| 贴标申请、箱唛/SKU/组合换标与 PDA 完工 | `relabel/` |
|
||||
| PDA 仓内通用扫码与操作历史 | `pda/warehouse/controller/PdaWarehouseController.java` |
|
||||
| 船节点接收与海外 PDA 可收货门禁 | `shipping/service/ContainerShippingNodeConsumer.java` |
|
||||
| 内部散板需求只读来源 | `dispatch/service/DispatchCapacityDemandService.java`、`DispatchQueryController.java`的`/dispatches/capacity-demands`;默认发布字段随原排车创建/编辑保存,当前业务口径见拼车主契约,不恢复旧车次人工发布入口 |
|
||||
| 平台排车查询、整车四块详情、审核与实载 | `dispatch/controller/DispatchQueryController.java`、`DispatchAggregateController.java`;公开契约`dispatch/port/`,业务规则见`../../docs/海外仓业务/排车列表统一作业改造契约.md` |
|
||||
| 草稿、双端装车发运 | `dispatch/controller/SmartDispatchController.java`、`dispatch/pda/controller/DispatchPdaController.java`;平台维护`DispatchController.java`;自动发运编排`dispatch/service/DispatchLoadingDepartureService.java` |
|
||||
| 仓储资源调用 | `inboundplan/pda/gateway/WarehouseResourceGateway.java` |
|
||||
|
||||
+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; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 本次仅新增排车内部余位发布选项;执行需单独授权,不回填、清理或重置业务数据。
|
||||
ALTER TABLE ll_dispatch_order
|
||||
ADD COLUMN publish_looseboard_demand TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否向内部散板需求池发布余位' AFTER task_type;
|
||||
@@ -1895,6 +1895,7 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_order (
|
||||
id BIGINT NOT NULL COMMENT '整车排车主单内部主键',
|
||||
dispatch_no VARCHAR(64) NOT NULL COMMENT '真实排车业务号',
|
||||
task_type VARCHAR(32) NOT NULL DEFAULT 'DELIVERY' COMMENT '任务类型:仓库派送、到仓取货、本土卡车',
|
||||
publish_looseboard_demand TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否向内部散板需求池发布余位',
|
||||
trip_id BIGINT NOT NULL COMMENT '本趟唯一车辆资源记录ID',
|
||||
overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '真实起运海外仓ID',
|
||||
overseas_warehouse_code VARCHAR(128) NULL COMMENT '起运海外仓编码快照',
|
||||
|
||||
@@ -4,6 +4,35 @@ info:
|
||||
description: 派送资料完整契约见同目录 delivery-documents-v1.yaml;派送列表/详情行返回 bolNo、documentSummary(三类状态与x/3),状态仍使用 version 和 allowedActions。
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/api/platform/v1/last-leg/dispatches/capacity-demands:
|
||||
get:
|
||||
summary: 读取有效派送排车的内部散板余位需求
|
||||
description: 仅平台内部capacity-offer:list或manage可读;返回运输摘要,不返回客户、司机私人资料。实时排除未勾选、取货、本土卡车、已发车、取消及已知满载排车;缺测余位明确未知。
|
||||
operationId: listDispatchCapacityDemands
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
|
||||
- { name: origin, in: query, schema: { type: string } }
|
||||
- { name: destination, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: 统一业务响应,data为包含records、page、size、total和pages的分页结果。
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: string }
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
records: { type: array, items: { $ref: '#/components/schemas/DispatchCapacityDemand' } }
|
||||
page: { oneOf: [{ type: string, pattern: '^[0-9]+$' }, { type: integer }] }
|
||||
size: { oneOf: [{ type: string, pattern: '^[0-9]+$' }, { type: integer }] }
|
||||
total: { oneOf: [{ type: string, pattern: '^[0-9]+$' }, { type: integer }] }
|
||||
pages: { oneOf: [{ type: string, pattern: '^[0-9]+$' }, { type: integer }] }
|
||||
|
||||
/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}/increase-forecast-cartons:
|
||||
post:
|
||||
summary: 测量完成前增加预报箱数并补充箱唛
|
||||
@@ -3961,6 +3990,10 @@ components:
|
||||
type: object
|
||||
x-java-record: DispatchCommands.Update
|
||||
properties:
|
||||
publishLooseboardDemand:
|
||||
type: boolean
|
||||
description: 是否向内部散板需求池发布本车余位;新建默认勾选。
|
||||
|
||||
recordVersion:
|
||||
oneOf:
|
||||
- type: string
|
||||
@@ -3990,6 +4023,7 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- publishLooseboardDemand
|
||||
- recordVersion
|
||||
- idempotencyKey
|
||||
DeliveryStatusChange:
|
||||
@@ -5783,10 +5817,31 @@ components:
|
||||
required:
|
||||
- sourceType
|
||||
- sourceVersion
|
||||
DispatchCapacityDemand:
|
||||
type: object
|
||||
x-java-record: DispatchCapacityDemands.Demand
|
||||
properties:
|
||||
dispatchNo: { type: string }
|
||||
warehouseName: { type: string, nullable: true }
|
||||
route: { type: string, nullable: true }
|
||||
plannedDepartureAt: { type: string, format: date-time, nullable: true }
|
||||
vehiclePalletSpaces: { type: integer, nullable: true }
|
||||
scheduledPalletCount: { type: integer, nullable: true }
|
||||
remainingPalletSpaces: { type: integer, nullable: true }
|
||||
remainingWeightKg: { type: number, nullable: true }
|
||||
remainingVolumeCbm: { type: number, nullable: true }
|
||||
capacityKnown: { type: boolean }
|
||||
capacityReason: { type: string, nullable: true }
|
||||
required: [dispatchNo, capacityKnown]
|
||||
DispatchCreate:
|
||||
type: object
|
||||
x-java-record: DispatchCommands.Create
|
||||
properties:
|
||||
publishLooseboardDemand:
|
||||
type: boolean
|
||||
description: 是否向内部散板需求池发布本车余位;新建默认勾选。
|
||||
default: true
|
||||
|
||||
idempotencyKey:
|
||||
type: string
|
||||
warehouseId:
|
||||
@@ -6475,6 +6530,10 @@ components:
|
||||
type: object
|
||||
x-java-record: DispatchViews.Detail
|
||||
properties:
|
||||
publishLooseboardDemand:
|
||||
type: boolean
|
||||
description: 是否向内部散板需求池发布本车余位;新建默认勾选。
|
||||
|
||||
dispatchNo:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -6530,6 +6589,7 @@ components:
|
||||
$ref: '#/components/schemas/DispatchOrderDetail'
|
||||
nullable: true
|
||||
required:
|
||||
- publishLooseboardDemand
|
||||
- dispatchNo
|
||||
- status
|
||||
- stage
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class PlatformDispatchOutboundContractTest {
|
||||
assertTrue(Arrays.stream(DispatchController.class.getDeclaredMethods())
|
||||
.noneMatch(method -> Set.of("depart", "cancel").contains(method.getName())));
|
||||
assertEquals(Set.of("recordVersion", "idempotencyKey", "vehicle", "driver",
|
||||
"plannedDepartureAt", "estimatedArrivalAt", "remark"),
|
||||
"plannedDepartureAt", "estimatedArrivalAt", "remark", "publishLooseboardDemand"),
|
||||
Arrays.stream(DispatchCommands.Update.class.getRecordComponents())
|
||||
.map(component -> component.getName()).collect(java.util.stream.Collectors.toSet()));
|
||||
assertEquals(Set.of("recordVersion", "idempotencyKey", "reason"),
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ class DispatchSchemaContractTest {
|
||||
"ll_dispatch_pda_task",com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask.class);
|
||||
for(var entry:tables.entrySet()) {
|
||||
String body=table(sql,entry.getKey());var columns=new TreeSet<String>();
|
||||
var matcher=Pattern.compile("(?:^|,)\\s*([a-z][a-z0-9_]*)\\s+(?:BIGINT|INT|CHAR|VARCHAR|DATETIME|TIMESTAMP|JSON|TEXT|LONGTEXT|DECIMAL)\\b",Pattern.MULTILINE).matcher(body);
|
||||
var matcher=Pattern.compile("(?:^|,)\\s*([a-z][a-z0-9_]*)\\s+(?:BIGINT|TINYINT|INT|CHAR|VARCHAR|DATETIME|TIMESTAMP|JSON|TEXT|LONGTEXT|DECIMAL)\\b",Pattern.MULTILINE).matcher(body);
|
||||
while(matcher.find())columns.add(matcher.group(1));
|
||||
var fields=new TreeSet<String>();for(var field:entry.getValue().getDeclaredFields())if(!Modifier.isStatic(field.getModifiers()) && !field.isSynthetic())fields.add(field.getName().replaceAll("([a-z0-9])([A-Z])","$1_$2").toLowerCase(Locale.ROOT));
|
||||
assertThat(columns).as(entry.getKey()).isEqualTo(fields).doesNotContain("customer_id");
|
||||
|
||||
+15
@@ -29,6 +29,7 @@ import static org.mockito.Mockito.when;
|
||||
class DispatchQueryControllerTest {
|
||||
@Mock DispatchAggregateService service;
|
||||
@Mock DispatchBolPrintService printService;
|
||||
@Mock com.baiyue.lastleg.dispatch.service.DispatchCapacityDemandService capacityDemands;
|
||||
private DispatchQueryController controller;
|
||||
|
||||
@BeforeEach
|
||||
@@ -36,6 +37,7 @@ class DispatchQueryControllerTest {
|
||||
controller = new DispatchQueryController();
|
||||
ReflectionTestUtils.setField(controller, "aggregateService", service);
|
||||
ReflectionTestUtils.setField(controller, "printService", printService);
|
||||
ReflectionTestUtils.setField(controller, "capacityDemands", capacityDemands);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -72,6 +74,19 @@ class DispatchQueryControllerTest {
|
||||
assertThat(print.value()).isEqualTo("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')");
|
||||
}
|
||||
|
||||
/** 需求池使用独立列表权限且只允许平台身份,不向客户开放摘要来源。 */
|
||||
@Test void capacityDemandPoolUsesItsOwnPermissionAndRejectsMerchantIdentity() throws Exception {
|
||||
var query=new com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Query(1,20,null,null);
|
||||
var permission=DispatchQueryController.class.getMethod("capacityDemands",AuthenticatedUser.class,query.getClass()).getAnnotation(PreAuthorize.class);
|
||||
assertThat(permission.value()).isEqualTo("hasAnyAuthority('platform:fleet:capacity-offer:list','platform:fleet:capacity-offer:manage')");
|
||||
when(capacityDemands.page(query)).thenReturn(new PageResult<>(List.of(),1,20,0,0));
|
||||
assertThat(controller.capacityDemands(platform(),query).data().records()).isEmpty();
|
||||
var merchant=new AuthenticatedUser(7L,9L,9L,8L,"customer","MERCHANT","WEB",Set.of());
|
||||
assertThatThrownBy(()->controller.capacityDemands(merchant,query)).isInstanceOfSatisfying(ResponseStatusException.class,error->assertThat(error.getStatusCode().value()).isEqualTo(403));
|
||||
assertThatThrownBy(()->controller.capacityDemands(null,query)).isInstanceOfSatisfying(ResponseStatusException.class,error->assertThat(error.getStatusCode().value()).isEqualTo(401));
|
||||
verify(capacityDemands).page(query);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser platform() {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB", Set.of());
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public final class DispatchPublicJsonContract {
|
||||
rejectsMissing(requests, request, "{}");
|
||||
rejectsMissing(requests, request, "{\"recordVersion\":null}");
|
||||
rejectsMissing(requests, request, "{\"recordVersion\":-1}");
|
||||
Object zero = requests.readValue("{\"recordVersion\":0}", request);
|
||||
Object zero = requests.readValue(request == DispatchCommands.Update.class ? "{\"recordVersion\":0,\"publishLooseboardDemand\":true}" : "{\"recordVersion\":0}", request);
|
||||
require(Long.valueOf(0).equals(request.getMethod("recordVersion").invoke(zero)), "显式零版本必须合法");
|
||||
}
|
||||
rejectsMissing(requests, DispatchCommands.Update.class,
|
||||
|
||||
+5
-5
@@ -204,7 +204,7 @@ public class DispatchAggregatePersistenceTest {
|
||||
/** 空单编辑保留零货量与版本命令,不借编辑隐式制造可装车来源。 */
|
||||
@Test void emptyShellEditPreservesZeroCargoAndVersionedReplay() throws Exception {
|
||||
var f=new Fixture();var created=f.tx.execute(t->f.service.create(f.actor,f.create("empty-edit",List.of())));
|
||||
var request=new DispatchCommands.Update(0L,"empty-edit-original",null,null,null,null,"空单补充备注");
|
||||
var request=new DispatchCommands.Update(0L,"empty-edit-original",null,null,null,null,"空单补充备注", true);
|
||||
var saved=f.tx.execute(t->f.service.update(f.actor,created.dispatchNo(),request));
|
||||
assertThat(saved.stage()).isEqualTo(Stage.BEFORE_LOADING);assertThat(saved.counts().packageCount()).isZero();
|
||||
assertThat(saved.recordVersion()).isGreaterThan(created.recordVersion());
|
||||
@@ -244,7 +244,7 @@ public class DispatchAggregatePersistenceTest {
|
||||
}
|
||||
@Test void updateSavesCurrentVehiclePlanAndReplayWithoutAnyVehicleAppointmentField() throws Exception {
|
||||
var f=new Fixture();var input=f.configured("update-without-vehicle-appointment",List.of(f.source(1,"101",501,"ONT8",3)));var created=f.tx.execute(t->f.service.create(f.actor,input));
|
||||
var arrival=input.plannedDepartureAt().plusSeconds(3600);var request=new DispatchCommands.Update(created.recordVersion(),"update-current-plan",input.vehicle(),input.driver(),input.plannedDepartureAt(),arrival,"调整本趟计划");
|
||||
var arrival=input.plannedDepartureAt().plusSeconds(3600);var request=new DispatchCommands.Update(created.recordVersion(),"update-current-plan",input.vehicle(),input.driver(),input.plannedDepartureAt(),arrival,"调整本趟计划", true);
|
||||
var saved=f.tx.execute(t->f.service.update(f.actor,created.dispatchNo(),request));assertThat(saved.recordVersion()).isGreaterThan(created.recordVersion());
|
||||
var detail=f.tx.execute(t->f.service.detail(f.actor,created.dispatchNo()));assertThat(detail.dispatchInfo().estimatedArrivalAt()).isEqualTo(arrival);assertThat(detail.documents().remark()).isEqualTo("调整本趟计划");
|
||||
var publicJson=f.json.valueToTree(detail);assertThat(publicJson.path("dispatchInfo").has("vehicleAppointment")).isFalse();assertThat(publicJson.path("dispatchInfo").has("fbaAppointments")).isTrue();
|
||||
@@ -604,7 +604,7 @@ public class DispatchAggregatePersistenceTest {
|
||||
}
|
||||
|
||||
/** 空计划和资源是合法待完善事实,不补假日期或主档。 */
|
||||
public DispatchCommands.Create create(String key,List<DispatchCommands.CargoSelection> selections) { return new DispatchCommands.Create(key,"9",null,null,null,null,null,selections,List.of()); }
|
||||
public DispatchCommands.Create create(String key,List<DispatchCommands.CargoSelection> selections) { return new DispatchCommands.Create(key,"9",null,null,null,null,null,selections,List.of(), true); }
|
||||
|
||||
/** 临时资源不创建主档;文件提供者在此夹具隔离,归属HTTP网关另行验证。 */
|
||||
public DispatchCommands.Create configured(String key,List<DispatchCommands.CargoSelection> sources) {
|
||||
@@ -613,7 +613,7 @@ public class DispatchAggregatePersistenceTest {
|
||||
new DispatchContracts.Capacity(15,new BigDecimal("10000"),new BigDecimal("50")),List.of(101L)),
|
||||
new DispatchCommands.Driver(ResourceMode.EXTERNAL,null,"测试司机","123456789",
|
||||
new DispatchContracts.DriverEvidence("TEST-LICENSE",java.time.LocalDate.of(2099,1,1),List.of(202L),departure.plusSeconds(86400),List.of())),
|
||||
departure,null,null,sources,List.of());
|
||||
departure,null,null,sources,List.of(), true);
|
||||
}
|
||||
|
||||
/** 通过实际两级流程进入装车,测试不直接伪造通过状态。 */
|
||||
@@ -632,6 +632,6 @@ public class DispatchAggregatePersistenceTest {
|
||||
}
|
||||
|
||||
/** 测试建表按实际持久属性类型生成,正式DDL仍由独立主文档集成。 */
|
||||
private static String type(Class<?> type) { return type==Long.class||type==long.class?"BIGINT":type==Integer.class||type==int.class?"INT":type==LocalDateTime.class?"TIMESTAMP(3)":type==BigDecimal.class?"DECIMAL(18,6)":"VARCHAR(20000)"; }
|
||||
private static String type(Class<?> type) { return type==Long.class||type==long.class?"BIGINT":type==Boolean.class||type==boolean.class?"BOOLEAN":type==Integer.class||type==int.class?"INT":type==LocalDateTime.class?"TIMESTAMP(3)":type==BigDecimal.class?"DECIMAL(18,6)":"VARCHAR(20000)"; }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -40,6 +40,6 @@ class DispatchAppointmentAllocationsTest {
|
||||
when(provider.candidateBatches(identities)).thenReturn(Map.of());assertThatThrownBy(()->service.candidates(List.of(a,b))).hasMessageContaining("批读范围不完整");
|
||||
}
|
||||
private static com.baiyue.lastleg.dispatch.port.DispatchCommands.Create create(String key,List<BatchAllocation> allocations) {
|
||||
return new com.baiyue.lastleg.dispatch.port.DispatchCommands.Create(key,"9",null,null,null,null,null,List.of(),allocations);
|
||||
return new com.baiyue.lastleg.dispatch.port.DispatchCommands.Create(key,"9",null,null,null,null,null,List.of(),allocations, true);
|
||||
}
|
||||
}
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.port.*;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class DispatchCapacityDemandPersistenceTest {
|
||||
/** 默认发布和取消发布随原排车命令同事务保存,原请求重放不重复修改。 */
|
||||
@Test void defaultsAndUnpublishesThroughTheExistingVersionedCommand() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
var request=f.json.readValue("{\"idempotencyKey\":\"pool-default\",\"warehouseId\":\"9\",\"cargo\":[],\"batchAllocations\":[]}",DispatchCommands.Create.class);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,request));
|
||||
var service=new DispatchCapacityDemandService(f.repository,f.json);
|
||||
assertThat(service.page(query()).records()).extracting(DispatchCapacityDemands.Demand::dispatchNo).containsExactly(created.dispatchNo());
|
||||
assertThat(f.service.detail(f.actor,created.dispatchNo()).publishLooseboardDemand()).isTrue();
|
||||
var update=new DispatchCommands.Update(created.recordVersion(),"pool-disable",null,null,null,null,null,false);
|
||||
var saved=f.tx.execute(s->f.service.update(f.actor,created.dispatchNo(),update));
|
||||
assertThat(f.service.detail(f.actor,created.dispatchNo()).publishLooseboardDemand()).isFalse();
|
||||
assertThat(service.page(query()).total()).isZero();
|
||||
var replay=f.tx.execute(s->f.service.update(f.actor,created.dispatchNo(),update));
|
||||
assertThat(replay.recordVersion()).isEqualTo(saved.recordVersion());
|
||||
assertThat(service.page(query()).total()).isZero();
|
||||
}
|
||||
/** 余位缺测时保持未知;不能将实托数量当标准托位或使用默认三十托。 */
|
||||
@Test void unknownCapacityIsVisibleAndNonDeliveryOrDepartedRowsAreExcluded() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
var service=new DispatchCapacityDemandService(f.repository,f.json);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.create("pool-unknown",List.of())));
|
||||
var demand=service.page(query()).records().get(0);
|
||||
assertThat(demand.capacityKnown()).isFalse();assertThat(demand.remainingPalletSpaces()).isNull();
|
||||
assertThat(demand.remainingWeightKg()).isNull();assertThat(demand.capacityReason()).isNotBlank();
|
||||
for(var type:List.of("PICKUP","LOCAL_TRUCK")) {
|
||||
f.db.update("UPDATE ll_dispatch_order SET task_type=?",type);assertThat(service.page(query()).total()).isZero();
|
||||
}
|
||||
f.db.update("UPDATE ll_dispatch_order SET task_type='DELIVERY',stage='DELIVERING'");assertThat(service.page(query()).total()).isZero();
|
||||
f.db.update("UPDATE ll_dispatch_order SET stage='BEFORE_LOADING',status='CANCELLED'");assertThat(service.page(query()).total()).isZero();
|
||||
}
|
||||
/** 真实装载去重后计算三维余位,已知满载在分页前排除。 */
|
||||
@Test void computesConfirmedCapacityAndRemovesFullLoadsBeforePagination() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
var service=new DispatchCapacityDemandService(f.repository,f.json);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("pool-known",List.of())));
|
||||
var load=new ConfirmedLoad("test-manifest",List.of(new LoadingMeasurement(101L,3,new BigDecimal("400"),new BigDecimal("5"),1)),"operator-test",Instant.now());
|
||||
f.db.update("UPDATE ll_dispatch_order SET loading_confirmation_json=?,stage='AFTER_LOADING',pallet_count=1",f.json.writeValueAsString(load));
|
||||
var demand=service.page(query()).records().get(0);
|
||||
assertThat(demand.capacityKnown()).isTrue();assertThat(demand.remainingPalletSpaces()).isEqualTo(12);
|
||||
assertThat(demand.remainingWeightKg()).isEqualByComparingTo("9600");assertThat(demand.remainingVolumeCbm()).isEqualByComparingTo("45");
|
||||
// 同一张实托占三个标准托位,不能按实托一件减一。
|
||||
assertThat(demand.scheduledPalletCount()).isOne();
|
||||
var full=new ConfirmedLoad("full",List.of(new LoadingMeasurement(101L,15,new BigDecimal("400"),new BigDecimal("5"),1)),"operator-test",Instant.now());
|
||||
f.db.update("UPDATE ll_dispatch_order SET loading_confirmation_json=?",f.json.writeValueAsString(full));
|
||||
assertThat(service.page(query()).total()).isZero();assertThat(service.page(query()).records()).isEmpty();
|
||||
}
|
||||
/** 重量或体积缺测不掩盖已明确的托位满载。 */
|
||||
@Test void knownFullPalletSpacesHideEvenWithUnknownWeight() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
f.tx.execute(s->f.service.create(f.actor,f.configured("pool-full-slots",List.of())));
|
||||
var load=new ConfirmedLoad("partial",List.of(new LoadingMeasurement(101L,15,null,null,1)),"operator-test",Instant.now());
|
||||
f.db.update("UPDATE ll_dispatch_order SET loading_confirmation_json=?",f.json.writeValueAsString(load));
|
||||
assertThat(new DispatchCapacityDemandService(f.repository,f.json).page(query()).total()).isZero();
|
||||
}
|
||||
/** 新建时可以显式取消默认发布,后续不产生额外发布记录。 */
|
||||
@Test void createCanOptOutAndFiltersUseTheActualWarehouseAndRoute() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();var initial=f.create("pool-opt-out",List.of());
|
||||
var request=new DispatchCommands.Create(initial.idempotencyKey(),initial.warehouseId(),null,null,null,null,null,List.of(),List.of(),false);
|
||||
f.tx.execute(s->f.service.create(f.actor,request));
|
||||
var service=new DispatchCapacityDemandService(f.repository,f.json);assertThat(service.page(query()).total()).isZero();
|
||||
f.db.update("UPDATE ll_dispatch_order SET publish_looseboard_demand=TRUE,overseas_warehouse_name='测试仓',route='测试仓 → ONT8'");
|
||||
assertThat(service.page(new DispatchCapacityDemands.Query(1,20,"测试","ONT8")).total()).isOne();
|
||||
assertThat(service.page(new DispatchCapacityDemands.Query(1,20,"其它仓",null)).total()).isZero();
|
||||
}
|
||||
/** 使用与正式列表相同的分页输入。 */
|
||||
private DispatchCapacityDemands.Query query() { return new DispatchCapacityDemands.Query(1,20,null,null); }
|
||||
}
|
||||
+3
-3
@@ -326,7 +326,7 @@ class DispatchDraftPersistenceTest {
|
||||
@Test void conversionKeepsWholeSharedGroupAndOriginalCommandIdentity(){
|
||||
relabelPair();var state=select(empty(1),0,1);var full=conversion(state,"convert-whole");var vehicle=full.groups().get(0);var create=vehicle.dispatch();
|
||||
var partial=new DispatchCommands.Create("partial-child",create.warehouseId(),create.vehicle(),create.driver(),create.plannedDepartureAt(),
|
||||
create.estimatedArrivalAt(),create.remark(),create.cargo().subList(0,1),create.batchAllocations());
|
||||
create.estimatedArrivalAt(),create.remark(),create.cargo().subList(0,1),create.batchAllocations(), true);
|
||||
assertThatThrownBy(()->service.convert(owner,new ConvertDraft(state.recordVersion(),"partial-convert",state.draftToken(),List.of(new DraftVehicle(vehicle.draftContainerId(),partial)))))
|
||||
.isInstanceOf(BizException.class);
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM draft_created_dispatch",Integer.class)).isZero();
|
||||
@@ -368,7 +368,7 @@ class DispatchDraftPersistenceTest {
|
||||
var state=select(empty(2),0,1);state=select(state,1,2);var original=conversion(state,"convert");
|
||||
assertThatThrownBy(()->service.convert(owner,new ConvertDraft(original.recordVersion(),"partial",original.draftToken(),original.groups().subList(0,1)))).isInstanceOf(BizException.class);
|
||||
var first=original.groups().get(0);var r=first.dispatch();
|
||||
var foreign=new DispatchCommands.Create(r.idempotencyKey(),"93",r.vehicle(),r.driver(),r.plannedDepartureAt(),r.estimatedArrivalAt(),r.remark(),r.cargo(),r.batchAllocations());
|
||||
var foreign=new DispatchCommands.Create(r.idempotencyKey(),"93",r.vehicle(),r.driver(),r.plannedDepartureAt(),r.estimatedArrivalAt(),r.remark(),r.cargo(),r.batchAllocations(), true);
|
||||
assertThatThrownBy(()->service.convert(owner,new ConvertDraft(original.recordVersion(),"foreign-wh",original.draftToken(),List.of(new DraftVehicle(first.draftContainerId(),foreign),original.groups().get(1))))).isInstanceOf(BizException.class);
|
||||
clearInvocations(aggregate);versions.put(1L,"changed");
|
||||
assertThatThrownBy(()->service.convert(owner,original)).isInstanceOf(IllegalStateException.class);
|
||||
@@ -554,7 +554,7 @@ class DispatchDraftPersistenceTest {
|
||||
}
|
||||
/** 每个非空服务器容器生成一组完整创建请求。 */
|
||||
private ConvertDraft conversion(Draft draft,String key){var groups=new ArrayList<DraftVehicle>();int n=0;for(var c:draft.containers())if(!c.sourceKeys().isEmpty())groups.add(new DraftVehicle(c.draftContainerId(),new DispatchCommands.Create("vehicle-"+(++n),draft.warehouseId(),null,null,null,null,null,
|
||||
draft.sources().stream().filter(s->c.sourceKeys().contains(s.sourceKey())).map(Candidate::selection).toList(),List.of())));return new ConvertDraft(draft.recordVersion(),key,draft.draftToken(),groups);}
|
||||
draft.sources().stream().filter(s->c.sourceKeys().contains(s.sourceKey())).map(Candidate::selection).toList(),List.of(), true)));return new ConvertDraft(draft.recordVersion(),key,draft.draftToken(),groups);}
|
||||
/** 主单边界返回冻结的真实公开结果形状。 */
|
||||
private static CommandResult result(String no,String key,int cargo){return new CommandResult(no,Action.CREATE,key,0,Status.ACTIVE,Stage.BEFORE_LOADING,Instant.now(),Effect.APPLIED,new Counts(cargo,cargo,10L*cargo,0,0),new DispatchResults.Saved(no,List.of()),false,false);}
|
||||
/** 测试独立命令键不依赖执行顺序或系统时间。 */
|
||||
|
||||
+1
-1
@@ -397,7 +397,7 @@ class DispatchRepairWorkflowTest {
|
||||
}
|
||||
/** 使用当前公开整车创建DTO,不恢复旧客户预排接口。 */
|
||||
private DispatchCommands.Create create(List<DispatchCommands.CargoSelection> cargo) {
|
||||
return new DispatchCommands.Create(UUID.randomUUID().toString(),"9",null,null,null,null,null,cargo,List.of());
|
||||
return new DispatchCommands.Create(UUID.randomUUID().toString(),"9",null,null,null,null,null,cargo,List.of(), true);
|
||||
}
|
||||
/** 重放固定原版本及原键,取消旧单不会取得或释放新单占用。 */
|
||||
private void cancel(String number) {
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ class DispatchResourcePhotosPersistenceTest {
|
||||
var e = d.evidence();
|
||||
var driver = new DispatchCommands.Driver(d.mode(), d.driverId(), d.name(), d.phone(),
|
||||
new DispatchContracts.DriverEvidence(e.licenseNo(), e.licenseExpiresOn(), e.fileIds(), e.expectedTaskEndAt(), photos));
|
||||
return new DispatchCommands.Update(version, key, input.vehicle(), driver, input.plannedDepartureAt(), input.estimatedArrivalAt(), "维护照片");
|
||||
return new DispatchCommands.Update(version, key, input.vehicle(), driver, input.plannedDepartureAt(), input.estimatedArrivalAt(), "维护照片", true);
|
||||
}
|
||||
|
||||
/** 只替代外部附件提供者,资源、命令和事务仍走真实服务。 */
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ class DispatchReviewWorkflowPersistenceTest {
|
||||
long item=f.db.queryForObject("SELECT id FROM ll_dispatch_item WHERE plan_pallet_id=3",Long.class);
|
||||
var removed=f.tx.execute(s->f.service.removeCargo(f.actor,created.dispatchNo(),item,new DispatchCommands.ReasonCommand(addedAgain.recordVersion(),"remove","调整目的仓")));
|
||||
var resources=f.configured("updated-resources",List.of());
|
||||
var updated=f.tx.execute(s->f.service.update(f.actor,created.dispatchNo(),new DispatchCommands.Update(removed.recordVersion(),"update-vehicle",resources.vehicle(),resources.driver(),resources.plannedDepartureAt().plusSeconds(60),null,"客服更正")));
|
||||
var updated=f.tx.execute(s->f.service.update(f.actor,created.dispatchNo(),new DispatchCommands.Update(removed.recordVersion(),"update-vehicle",resources.vehicle(),resources.driver(),resources.plannedDepartureAt().plusSeconds(60),null,"客服更正", true)));
|
||||
var detail=f.service.detail(f.actor,created.dispatchNo());
|
||||
var retained=loading?detail.reviews().loadingReview():detail.reviews().dispatchReview();
|
||||
assertThat(detail.reviewState()).isEqualTo(ReviewState.DRAFT);
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class OwnedBusinessScopePersistenceTest {
|
||||
when(reader.detail(any(),anyString())).thenAnswer(call->{
|
||||
var s=baseline.repository.source(call.getArgument(1));var r=baseline.codec.read(s.getResourcesJson(),DispatchContracts.Resources.class);
|
||||
return new DispatchViews.Detail(s.getDispatchNo(),"DELIVERY",DispatchTypes.Status.ACTIVE,DispatchTypes.Stage.valueOf(s.getStage()),s.getRecordVersion(),
|
||||
DispatchTypes.ReviewState.APPROVED,null,null,true,Map.of(),new DispatchViews.VehicleInfo(r.vehicle(),r.driver()),null,null,List.of());
|
||||
DispatchTypes.ReviewState.APPROVED,null,null,true,Map.of(),new DispatchViews.VehicleInfo(r.vehicle(),r.driver()),null,null,List.of(),true);
|
||||
});
|
||||
queries=tx(new OwnedBusinessQueryService(baseline.repository,baseline.projection,baseline.codec,reader));
|
||||
var commands=tx(new OwnedBusinessCommandService(baseline.repository,baseline.codec));
|
||||
|
||||
Reference in New Issue
Block a user