From e2ad7857b421ef0891da7e280ee89cfd733837da Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 21 Oct 2026 18:50:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(dispatch):=20=E4=BB=8E=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E6=8E=92=E8=BD=A6=E6=8F=90=E4=BE=9B=E5=86=85=E9=83=A8=E6=95=A3?= =?UTF-8?q?=E6=9D=BF=E9=9C=80=E6=B1=82=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/README.md | 2 + deploy/sql/海外区域统一增量-MySQL8.sql | 1 + lastleg-service/AGENTS.md | 1 + .../controller/DispatchQueryController.java | 9 +++ .../dispatch/entity/DispatchAggregate.java | 2 + .../mapper/DispatchAggregateMapper.java | 17 +++- .../port/DispatchCapacityDemands.java | 22 ++++++ .../dispatch/port/DispatchCommands.java | 9 ++- .../lastleg/dispatch/port/DispatchViews.java | 2 +- .../DispatchAggregateRepository.java | 2 + .../service/DispatchAggregateService.java | 4 +- .../DispatchCapacityDemandService.java | 66 ++++++++++++++++ .../resources/db/dispatch-capacity-demand.sql | 3 + .../src/main/resources/db/lastleg.sql | 1 + .../openapi/overseas-business-v1.yaml | 60 ++++++++++++++ .../PlatformDispatchOutboundContractTest.java | 2 +- .../dispatch/DispatchSchemaContractTest.java | 2 +- .../DispatchQueryControllerTest.java | 15 ++++ .../port/DispatchPublicJsonContract.java | 2 +- .../DispatchAggregatePersistenceTest.java | 10 +-- .../DispatchAppointmentAllocationsTest.java | 2 +- ...DispatchCapacityDemandPersistenceTest.java | 78 +++++++++++++++++++ .../service/DispatchDraftPersistenceTest.java | 6 +- .../service/DispatchRepairWorkflowTest.java | 2 +- ...DispatchResourcePhotosPersistenceTest.java | 2 +- ...DispatchReviewWorkflowPersistenceTest.java | 2 +- .../OwnedBusinessScopePersistenceTest.java | 2 +- 27 files changed, 302 insertions(+), 24 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCapacityDemands.java create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandService.java create mode 100644 lastleg-service/src/main/resources/db/dispatch-capacity-demand.sql create mode 100644 lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandPersistenceTest.java diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 4da54370c..5697a4360 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -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 与部署须逐项授权。 diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 347bc2820..c9c1fe2a0 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.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 '起运海外仓编码快照', diff --git a/lastleg-service/AGENTS.md b/lastleg-service/AGENTS.md index 6b429627a..155910180 100644 --- a/lastleg-service/AGENTS.md +++ b/lastleg-service/AGENTS.md @@ -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` | diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java index fbaa18cec..f2ab02744 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java @@ -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> 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") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchAggregate.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchAggregate.java index 0fa048fba..337013c1d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchAggregate.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchAggregate.java @@ -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; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchAggregateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchAggregateMapper.java index a8a1e3745..e53ef8f34 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchAggregateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchAggregateMapper.java @@ -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}") List page(@Param("q") Search query); + /** 只读余位来源仅含勾选发布、未发车的有效派送排车,不混入取货和本土卡车。 */ + @Select(""" + + """) + List capacityDemands(@Param("q") com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Query query); + /** 分页总数复用完全相同筛选,不能用当前页长度替代。 */ @Select("") 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}, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCapacityDemands.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCapacityDemands.java new file mode 100644 index 000000000..9ac6ce756 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCapacityDemands.java @@ -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) { } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java index 3d6b87df9..937664907 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java @@ -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 cargo, List batchAllocations) { } + List cargo, List 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 cargo, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java index 2d662b366..3cbaacdc3 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java @@ -64,7 +64,7 @@ public final class DispatchViews { ReviewState reviewState, Reviews reviews, Counts counts, boolean pickingReconciled, Map actions, VehicleInfo vehicleInfo, DispatchInfo dispatchInfo, Documents documents, - List orderDetails) { } + List orderDetails, boolean publishLooseboardDemand) { } public record PdaTask(String taskNo, String dispatchNo, String warehouseId, Status status, Stage stage, long recordVersion, Reviews reviews, Resources resources, Counts counts, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchAggregateRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchAggregateRepository.java index c0da1705a..f98e11c32 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchAggregateRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchAggregateRepository.java @@ -34,6 +34,8 @@ public class DispatchAggregateRepository { public DispatchAggregate lock(String number) { return mapper.lock(number); } /** 整车分页。 */ public List page(DispatchAggregateMapper.Search query) { return mapper.page(query); } + /** 一次读取当前有效的内部余位来源,服务计算满载筛选后再分页。 */ + public List capacityDemands(com.baiyue.lastleg.dispatch.port.DispatchCapacityDemands.Query query) { return mapper.capacityDemands(query); } /** 整车分页总数。 */ public long count(DispatchAggregateMapper.Search query) { return mapper.count(query); } /** 全搜索基集的阶段和审核分布。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java index 9acc8c401..f63a97c40 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java @@ -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.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())); } /** 恢复动作结果前重新核验当前单据范围和原动作权限。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandService.java new file mode 100644 index 000000000..cba0ab965 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandService.java @@ -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 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; } +} diff --git a/lastleg-service/src/main/resources/db/dispatch-capacity-demand.sql b/lastleg-service/src/main/resources/db/dispatch-capacity-demand.sql new file mode 100644 index 000000000..6a5f798cc --- /dev/null +++ b/lastleg-service/src/main/resources/db/dispatch-capacity-demand.sql @@ -0,0 +1,3 @@ +-- 本次仅新增排车内部余位发布选项;执行需单独授权,不回填、清理或重置业务数据。 +ALTER TABLE ll_dispatch_order + ADD COLUMN publish_looseboard_demand TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否向内部散板需求池发布余位' AFTER task_type; diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index aa3604c51..72a1c4fa0 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -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 '起运海外仓编码快照', diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index 111d3406e..f895b4d25 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -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 diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java index d5a79c7f3..1fbb530f4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java @@ -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"), diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchSchemaContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchSchemaContractTest.java index 90fd2d6ff..9dd91818a 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchSchemaContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchSchemaContractTest.java @@ -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(); - 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();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"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java index a192dc45f..940454818 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java @@ -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()); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java index 0928101c6..af9f501d9 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java @@ -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, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java index fd2bf8209..c18f12569 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java @@ -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 selections) { return new DispatchCommands.Create(key,"9",null,null,null,null,null,selections,List.of()); } + public DispatchCommands.Create create(String key,List selections) { return new DispatchCommands.Create(key,"9",null,null,null,null,null,selections,List.of(), true); } /** 临时资源不创建主档;文件提供者在此夹具隔离,归属HTTP网关另行验证。 */ public DispatchCommands.Create configured(String key,List 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)"; } } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java index bd34fb202..1cd4a33c4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java @@ -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 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); } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandPersistenceTest.java new file mode 100644 index 000000000..4af5a43be --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCapacityDemandPersistenceTest.java @@ -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); } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java index 83efefece..12f6d1774 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java @@ -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();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);} /** 测试独立命令键不依赖执行顺序或系统时间。 */ diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java index 1d5bdc914..68502950b 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java @@ -397,7 +397,7 @@ class DispatchRepairWorkflowTest { } /** 使用当前公开整车创建DTO,不恢复旧客户预排接口。 */ private DispatchCommands.Create create(List 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) { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchResourcePhotosPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchResourcePhotosPersistenceTest.java index 48bbcca4c..8a3f4cdba 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchResourcePhotosPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchResourcePhotosPersistenceTest.java @@ -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); } /** 只替代外部附件提供者,资源、命令和事务仍走真实服务。 */ diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchReviewWorkflowPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchReviewWorkflowPersistenceTest.java index 1fea3557a..f8705f263 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchReviewWorkflowPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchReviewWorkflowPersistenceTest.java @@ -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); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessScopePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessScopePersistenceTest.java index 5f3f64359..b95f2a579 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessScopePersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/ownedbusiness/OwnedBusinessScopePersistenceTest.java @@ -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));