feat(fleet): match cargo stops when binding dispatches

This commit is contained in:
2026-10-09 15:04:51 +08:00
parent 2ec5414b39
commit c4a7c2a22d
3 changed files with 51 additions and 1 deletions
@@ -206,11 +206,36 @@ public class FleetTripService {
try { if (bindings.insert(binding) != 1) throw conflict("排车单关联失败"); }
catch (DuplicateKeyException duplicate) { throw conflict("排车单已关联其他车次,请刷新后重试"); }
bookingReservations.bindIncoming(trip, dispatch, dispatches.sourceCargo(dispatch.dispatchNo()), operator);
var route = matchedStops(response(trip).stops(), dispatches.manifest(dispatch.dispatchNo(), false).cargo());
validatePlan(instant(trip.getPlannedDepartureAt()), instant(trip.getExpectedDriverEndAt()), route);
bookingReservations.requireRoute(trip, route);
trip.setStopsJson(write(route));
dispatches.assign(dispatch, assignment(trip));
persist(trip, request.recordVersion(), "BIND_DISPATCH", request.reason(), operator);
return detail(trip);
}
static List<FleetTripModels.Stop> matchedStops(List<FleetTripModels.Stop> existing, List<FleetExecutionModels.Cargo> cargo) {
// Keep established stop indexes/order: confirmed bookings may refer to them.
var result = new ArrayList<>(existing);
boolean onlyPlaceholder = existing.size() == 2 && existing.get(1).destinationKey() == null
&& existing.get(1).name().startsWith("待确定卸货点");
if (onlyPlaceholder) result.remove(1);
var keys = new HashSet<String>();
for (var stop : result) if (stop.destinationKey() != null) keys.add(stop.destinationKey());
for (var item : cargo) {
if (item.destinationKey() == null || item.destinationKey().isBlank())
throw conflict("排车货物目的地未确认,请先完善排车单目的地");
if (keys.add(item.destinationKey())) {
if (item.destinationLabel() == null || item.destinationLabel().isBlank())
throw conflict("排车货物目的地名称缺失,请先完善排车单");
result.add(new FleetTripModels.Stop(item.destinationLabel(), null, item.destinationKey()));
}
}
if (result.size() < 2) throw conflict("排车单没有可用卸货目的地");
return List.copyOf(result);
}
@Transactional
public FleetTripModels.Detail unbindDispatch(String tripNo, String operator, FleetTripModels.BindingCommand request) {
var trip = planned(tripNo, request.recordVersion());
@@ -220,7 +220,7 @@ paths:
parameters: [{$ref: '#/components/parameters/TripNo'}]
post:
summary: 将客户排车单关联到车次
description: '权限:platform:fleet:trip:edit;按业务单号解析客户归属,同仓且未开工;关联和车/司机快照同步原子提交。'
description: '权限:platform:fleet:trip:edit;按业务单号解析客户归属,同仓且未开工;关联、车/司机快照同步及按真实货物目的地标识补齐卸货站原子提交;目的地缺失拒绝关联,已有有效站点顺序与索引保留,初始默认占位站替换为真实目的地。'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/BindingCommand'}}}}
responses: {'200': {$ref: '#/components/responses/Detail'}}
/api/platform/v1/last-leg/fleet/trips/{tripNo}/dispatches/unbind:
@@ -0,0 +1,25 @@
package com.baiyue.lastleg.fleet.trip;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class FleetTripMatchedStopsTest {
private FleetExecutionModels.Cargo cargo(String key, String label) {
return new FleetExecutionModels.Cargo("DP-1", "A", "1", "2", "PLT-1", 1, key, label);
}
@Test void replacesOnlyInitialPlaceholderAndDeduplicatesByIdentity() {
var origin = new FleetTripModels.Stop("起运仓", null, null);
var result = FleetTripService.matchedStops(List.of(origin, new FleetTripModels.Stop("待确定卸货点1", null, null)),
List.of(cargo("FBA:ACY2", "ACY2"), cargo("FBA:ACY2", "ACY2")));
assertThat(result).containsExactly(origin, new FleetTripModels.Stop("ACY2", null, "FBA:ACY2"));
}
@Test void preservesExistingStopIndexesAndAppendsDifferentIdentityEvenWhenNamesMatch() {
var existing = List.of(new FleetTripModels.Stop("起点", null, null), new FleetTripModels.Stop("ACY2", null, "FBA:ACY2"));
var result = FleetTripService.matchedStops(existing, List.of(cargo("PRIVATE:OTHER", "ACY2")));
assertThat(result.subList(0,2)).isEqualTo(existing);
assertThat(result.get(2).destinationKey()).isEqualTo("PRIVATE:OTHER");
}
@Test void rejectsMissingIdentityWithoutUsingDisplayNameAsKey() {
assertThatThrownBy(() -> FleetTripService.matchedStops(List.of(new FleetTripModels.Stop("起点", null, null)), List.of(cargo(null,"ACY2"))))
.hasMessageContaining("目的地未确认");
}
}