feat(fleet): enforce reviewed trips and original fee coverage for dispatch
This commit is contained in:
+7
-1
@@ -48,6 +48,9 @@ public class VehiclePersistenceAdapter implements VehicleRepository {
|
||||
record.setId(IdWorker.getId());
|
||||
record.setPlateNumber(vehicle.plateNumber());
|
||||
record.setModel(vehicle.model());
|
||||
record.setMaxPalletSpaces(vehicle.maxPalletSpaces());
|
||||
record.setMaxLoadKg(vehicle.maxLoadKg());
|
||||
record.setMaxVolumeCbm(vehicle.maxVolumeCbm());
|
||||
record.setFuelConsumptionPer100Km(vehicle.fuelConsumptionPer100Km());
|
||||
record.setMileageKm(vehicle.mileageKm());
|
||||
record.setDriverOrDispatchCompany(vehicle.driverOrDispatchCompany());
|
||||
@@ -76,6 +79,9 @@ public class VehiclePersistenceAdapter implements VehicleRepository {
|
||||
.eq(VehicleRecord::getId, id).eq(VehicleRecord::getRecordVersion, expectedVersion)
|
||||
.set(VehicleRecord::getPlateNumber, changes.plateNumber())
|
||||
.set(VehicleRecord::getModel, changes.model())
|
||||
.set(VehicleRecord::getMaxPalletSpaces, changes.maxPalletSpaces())
|
||||
.set(VehicleRecord::getMaxLoadKg, changes.maxLoadKg())
|
||||
.set(VehicleRecord::getMaxVolumeCbm, changes.maxVolumeCbm())
|
||||
.set(VehicleRecord::getFuelConsumptionPer100Km, changes.fuelConsumptionPer100Km())
|
||||
.set(VehicleRecord::getMileageKm, changes.mileageKm())
|
||||
.set(VehicleRecord::getDriverOrDispatchCompany, changes.driverOrDispatchCompany())
|
||||
@@ -112,7 +118,7 @@ public class VehiclePersistenceAdapter implements VehicleRepository {
|
||||
return new VehicleSnapshot(value.getId(), value.getPlateNumber(), value.getModel(),
|
||||
value.getFuelConsumptionPer100Km(), value.getMileageKm(), value.getDriverOrDispatchCompany(), value.getPhone(),
|
||||
value.getRegion(), value.getOwnership(), value.getStatus(), value.getRemark(), version(value.getRecordVersion()),
|
||||
value.getUpdatedBy(), value.getUpdatedAt());
|
||||
value.getUpdatedBy(), value.getUpdatedAt(), value.getMaxPalletSpaces(), value.getMaxLoadKg(), value.getMaxVolumeCbm());
|
||||
}
|
||||
|
||||
private static long version(Long value) {
|
||||
|
||||
+3
@@ -14,6 +14,9 @@ public class VehicleRecord {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String plateNumber;
|
||||
private String model;
|
||||
private Integer maxPalletSpaces;
|
||||
private BigDecimal maxLoadKg;
|
||||
private BigDecimal maxVolumeCbm;
|
||||
private BigDecimal fuelConsumptionPer100Km;
|
||||
private Long mileageKm;
|
||||
private String driverOrDispatchCompany;
|
||||
|
||||
+32
-6
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.masterdata.vehicle.application;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.model.VehicleModels;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleAssignmentGuard;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.DuplicatePlateException;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.NewVehicle;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.SearchCriteria;
|
||||
@@ -24,9 +25,11 @@ public class VehicleApplicationService {
|
||||
private static final Set<String> STATUSES = Set.of("IN_SERVICE", "DISPATCHED", "MAINTENANCE", "INACTIVE", "RETURNED");
|
||||
|
||||
private final VehicleRepository vehicles;
|
||||
private final VehicleAssignmentGuard assignments;
|
||||
|
||||
public VehicleApplicationService(VehicleRepository vehicles) {
|
||||
public VehicleApplicationService(VehicleRepository vehicles, VehicleAssignmentGuard assignments) {
|
||||
this.vehicles = vehicles;
|
||||
this.assignments = assignments;
|
||||
}
|
||||
|
||||
public PageResult<VehicleModels.Response> page(VehicleModels.Query query) {
|
||||
@@ -41,6 +44,7 @@ public class VehicleApplicationService {
|
||||
|
||||
@Transactional
|
||||
public VehicleModels.Response create(String operator, VehicleModels.Create request) {
|
||||
validateCapacity(request.maxPalletSpaces(), request.maxLoadKg(), request.maxVolumeCbm());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long id;
|
||||
try {
|
||||
@@ -48,7 +52,8 @@ public class VehicleApplicationService {
|
||||
requiredText(request.plateNumber(), "车牌号").toUpperCase(Locale.ROOT),
|
||||
requiredText(request.model(), "车型"), request.fuelConsumptionPer100Km(), request.mileageKm(),
|
||||
trim(request.driverOrDispatchCompany()), trim(request.phone()), region(request.region()),
|
||||
ownership(request.ownership()), status(request.status()), trim(request.remark()), operator, now));
|
||||
ownership(request.ownership()), status(request.status()), trim(request.remark()), operator, now,
|
||||
request.maxPalletSpaces(), request.maxLoadKg(), request.maxVolumeCbm()));
|
||||
} catch (DuplicatePlateException exception) {
|
||||
throw conflict("车牌号已存在");
|
||||
}
|
||||
@@ -57,16 +62,25 @@ public class VehicleApplicationService {
|
||||
|
||||
@Transactional
|
||||
public VehicleModels.Response update(long id, String operator, VehicleModels.Update request) {
|
||||
validateCapacity(request.maxPalletSpaces(), request.maxLoadKg(), request.maxVolumeCbm());
|
||||
VehicleSnapshot current = locked(id);
|
||||
requireVersion(current, request.recordVersion());
|
||||
if (request.mileageKm() < current.mileageKm()) throw badRequest("当前公里码不能小于已有公里码");
|
||||
if (!current.plateNumber().equals(requiredText(request.plateNumber(), "车牌号").toUpperCase(Locale.ROOT))
|
||||
|| !current.model().equals(requiredText(request.model(), "车型")) || !current.region().equals(region(request.region()))
|
||||
|| !current.ownership().equals(ownership(request.ownership()))
|
||||
|| !java.util.Objects.equals(current.maxPalletSpaces(), request.maxPalletSpaces())
|
||||
|| !sameDecimal(current.maxLoadKg(), request.maxLoadKg()) || !sameDecimal(current.maxVolumeCbm(), request.maxVolumeCbm())) {
|
||||
assignments.requireUnassigned(id);
|
||||
}
|
||||
boolean updated;
|
||||
try {
|
||||
updated = vehicles.update(id, request.recordVersion(), new VehicleChanges(
|
||||
requiredText(request.plateNumber(), "车牌号").toUpperCase(Locale.ROOT),
|
||||
requiredText(request.model(), "车型"), request.fuelConsumptionPer100Km(), request.mileageKm(),
|
||||
trim(request.driverOrDispatchCompany()), trim(request.phone()), region(request.region()),
|
||||
ownership(request.ownership()), trim(request.remark()), operator, LocalDateTime.now()));
|
||||
ownership(request.ownership()), trim(request.remark()), operator, LocalDateTime.now(),
|
||||
request.maxPalletSpaces(), request.maxLoadKg(), request.maxVolumeCbm()));
|
||||
} catch (DuplicatePlateException exception) {
|
||||
throw conflict("车牌号已存在");
|
||||
}
|
||||
@@ -80,6 +94,7 @@ public class VehicleApplicationService {
|
||||
requireVersion(current, request.recordVersion());
|
||||
String status = status(request.status());
|
||||
if (status.equals(current.status())) return response(current);
|
||||
assignments.requireUnassigned(id);
|
||||
boolean updated = vehicles.updateStatus(id, request.recordVersion(), status, operator, LocalDateTime.now());
|
||||
if (!updated) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
return detail(id);
|
||||
@@ -87,11 +102,12 @@ public class VehicleApplicationService {
|
||||
|
||||
public byte[] exportCsv(VehicleModels.Query query) {
|
||||
var rows = vehicles.list(criteria(query)).stream().map(this::response).toList();
|
||||
StringBuilder csv = new StringBuilder("\uFEFF车辆ID,车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,更新时间\r\n");
|
||||
for (var row : rows) csv.append(row.id()).append(',').append(cell(row.plateNumber())).append(',')
|
||||
StringBuilder csv = new StringBuilder("\uFEFF车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,最大托位,最大载重(kg),可用容积(m³),更新时间\r\n");
|
||||
for (var row : rows) csv.append(cell(row.plateNumber())).append(',')
|
||||
.append(cell(row.model())).append(',').append(row.fuelConsumptionPer100Km()).append(',').append(row.mileageKm()).append(',')
|
||||
.append(cell(row.driverOrDispatchCompany())).append(',').append(cell(row.phone())).append(',')
|
||||
.append(row.region()).append(',').append(row.ownership()).append(',').append(row.status()).append(',')
|
||||
.append(cell(row.maxPalletSpaces())).append(',').append(cell(row.maxLoadKg())).append(',').append(cell(row.maxVolumeCbm())).append(',')
|
||||
.append(row.updatedAt()).append("\r\n");
|
||||
return csv.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
@@ -115,7 +131,17 @@ public class VehicleApplicationService {
|
||||
return new VehicleModels.Response(String.valueOf(value.id()), value.plateNumber(), value.model(),
|
||||
value.fuelConsumptionPer100Km(), value.mileageKm(), value.driverOrDispatchCompany(), value.phone(),
|
||||
value.region(), value.ownership(), value.status(), value.remark(), value.recordVersion(),
|
||||
value.updatedBy(), value.updatedAt());
|
||||
value.updatedBy(), value.updatedAt(), value.maxPalletSpaces(), value.maxLoadKg(), value.maxVolumeCbm());
|
||||
}
|
||||
|
||||
private static void validateCapacity(Integer pallets, java.math.BigDecimal weight, java.math.BigDecimal volume) {
|
||||
if (pallets == null && weight == null && volume == null) return;
|
||||
if (pallets == null || weight == null || volume == null || pallets < 1 || pallets > 10000
|
||||
|| weight.signum() <= 0 || volume.signum() <= 0) throw badRequest("车辆托位、载重和容积须同时填写且大于零,或全部留空待确认");
|
||||
}
|
||||
|
||||
private static boolean sameDecimal(java.math.BigDecimal first, java.math.BigDecimal second) {
|
||||
return first == null ? second == null : second != null && first.compareTo(second) == 0;
|
||||
}
|
||||
|
||||
private void requireVersion(VehicleSnapshot value, long expected) {
|
||||
|
||||
+10
-3
@@ -26,7 +26,10 @@ public final class VehicleModels {
|
||||
@Size(max = 128) String driverOrDispatchCompany,
|
||||
@Size(max = 64) String phone,
|
||||
@NotBlank String region, @NotBlank String ownership,
|
||||
@NotBlank String status, @Size(max = 1000) String remark) { }
|
||||
@NotBlank String status, @Size(max = 1000) String remark,
|
||||
@Min(1) @Max(10000) Integer maxPalletSpaces,
|
||||
@DecimalMin(value = "0", inclusive = false) @Digits(integer = 9, fraction = 3) BigDecimal maxLoadKg,
|
||||
@DecimalMin(value = "0", inclusive = false) @Digits(integer = 9, fraction = 3) BigDecimal maxVolumeCbm) { }
|
||||
|
||||
public record Update(@NotNull @Min(0) Long recordVersion,
|
||||
@NotBlank @Size(max = 64) String plateNumber,
|
||||
@@ -36,12 +39,16 @@ public final class VehicleModels {
|
||||
@Size(max = 128) String driverOrDispatchCompany,
|
||||
@Size(max = 64) String phone,
|
||||
@NotBlank String region, @NotBlank String ownership,
|
||||
@Size(max = 1000) String remark) { }
|
||||
@Size(max = 1000) String remark,
|
||||
@Min(1) @Max(10000) Integer maxPalletSpaces,
|
||||
@DecimalMin(value = "0", inclusive = false) @Digits(integer = 9, fraction = 3) BigDecimal maxLoadKg,
|
||||
@DecimalMin(value = "0", inclusive = false) @Digits(integer = 9, fraction = 3) BigDecimal maxVolumeCbm) { }
|
||||
|
||||
public record ChangeStatus(@NotBlank String status, @NotNull @Min(0) Long recordVersion) { }
|
||||
|
||||
public record Response(String id, String plateNumber, String model, BigDecimal fuelConsumptionPer100Km,
|
||||
long mileageKm, String driverOrDispatchCompany, String phone, String region,
|
||||
String ownership, String status, String remark, long recordVersion,
|
||||
String updatedBy, LocalDateTime updatedAt) { }
|
||||
String updatedBy, LocalDateTime updatedAt,
|
||||
Integer maxPalletSpaces, BigDecimal maxLoadKg, BigDecimal maxVolumeCbm) { }
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.application.port.out;
|
||||
|
||||
/** Called while the vehicle row is locked; fleet owns the active assignment. */
|
||||
public interface VehicleAssignmentGuard {
|
||||
void requireUnassigned(long vehicleId);
|
||||
}
|
||||
+6
-3
@@ -26,16 +26,19 @@ public interface VehicleRepository {
|
||||
|
||||
record NewVehicle(String plateNumber, String model, BigDecimal fuelConsumptionPer100Km, long mileageKm,
|
||||
String driverOrDispatchCompany, String phone, String region, String ownership, String status,
|
||||
String remark, String operator, LocalDateTime createdAt) { }
|
||||
String remark, String operator, LocalDateTime createdAt,
|
||||
Integer maxPalletSpaces, BigDecimal maxLoadKg, BigDecimal maxVolumeCbm) { }
|
||||
|
||||
record VehicleChanges(String plateNumber, String model, BigDecimal fuelConsumptionPer100Km, long mileageKm,
|
||||
String driverOrDispatchCompany, String phone, String region, String ownership, String remark,
|
||||
String operator, LocalDateTime updatedAt) { }
|
||||
String operator, LocalDateTime updatedAt,
|
||||
Integer maxPalletSpaces, BigDecimal maxLoadKg, BigDecimal maxVolumeCbm) { }
|
||||
|
||||
record VehicleSnapshot(long id, String plateNumber, String model, BigDecimal fuelConsumptionPer100Km,
|
||||
long mileageKm, String driverOrDispatchCompany, String phone, String region,
|
||||
String ownership, String status, String remark, long recordVersion,
|
||||
String updatedBy, LocalDateTime updatedAt) { }
|
||||
String updatedBy, LocalDateTime updatedAt,
|
||||
Integer maxPalletSpaces, BigDecimal maxLoadKg, BigDecimal maxVolumeCbm) { }
|
||||
|
||||
final class DuplicatePlateException extends RuntimeException {
|
||||
public DuplicatePlateException(Throwable cause) {
|
||||
|
||||
Reference in New Issue
Block a user