优化
This commit is contained in:
+13
-6
@@ -10,12 +10,19 @@ import java.net.http.HttpClient;
|
||||
public class LastlegRestClientFactory {
|
||||
public RestClient build(RestClient.Builder builder, String baseUrl,
|
||||
LastlegRestClientProperties.ClientTimeouts timeouts) {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(timeouts.getConnectTimeout())
|
||||
.build();
|
||||
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient);
|
||||
requestFactory.setReadTimeout(timeouts.getReadTimeout());
|
||||
JdkClientHttpRequestFactory requestFactory = requestFactory(timeouts);
|
||||
return builder.requestFactory(requestFactory).baseUrl(baseUrl).build();
|
||||
}
|
||||
}
|
||||
|
||||
HttpClient httpClient(LastlegRestClientProperties.ClientTimeouts timeouts) {
|
||||
return HttpClient.newBuilder()
|
||||
.connectTimeout(timeouts.getConnectTimeout())
|
||||
.build();
|
||||
}
|
||||
|
||||
JdkClientHttpRequestFactory requestFactory(LastlegRestClientProperties.ClientTimeouts timeouts) {
|
||||
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient(timeouts));
|
||||
requestFactory.setReadTimeout(timeouts.getReadTimeout());
|
||||
return requestFactory;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -50,8 +50,14 @@ public class LastlegOrderController {
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestBody EventEnvelope event) {
|
||||
tokenGuard.verify(token);
|
||||
int consumed = EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED.equals(event.eventType())
|
||||
? shippingNodeConsumer.consume(event) : consumer.consume(event);
|
||||
int consumed;
|
||||
if (EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED.equals(event.eventType())) {
|
||||
consumed = shippingNodeConsumer.consume(event);
|
||||
} else if (EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(event.eventType())) {
|
||||
consumed = consumer.consume(event);
|
||||
} else {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Lastleg 不消费该事件类型: " + event.eventType());
|
||||
}
|
||||
return ApiResponse.success(consumed);
|
||||
}
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.adapter.in.web;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService.VehicleFailure;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.model.VehicleModels;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/master-data/vehicles")
|
||||
@Tag(name = "Vehicle master data / 车辆主数据")
|
||||
public class PlatformVehicleController {
|
||||
private final VehicleApplicationService service;
|
||||
|
||||
public PlatformVehicleController(VehicleApplicationService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_LIST + "')")
|
||||
@Operation(summary = "Page vehicle master data", description = "Required authority: platform:overseas-master-data:vehicle:list")
|
||||
public ApiResponse<PageResult<VehicleModels.Response>> page(@Valid VehicleModels.Query query) {
|
||||
return execute(() -> ApiResponse.success(service.page(query)));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_LIST + "')")
|
||||
@Operation(summary = "Get vehicle master data detail", description = "Required authority: platform:overseas-master-data:vehicle:list; excludes delivery fulfillment history")
|
||||
public ApiResponse<VehicleModels.Response> detail(@PathVariable long id) {
|
||||
return execute(() -> ApiResponse.success(service.detail(id)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_CREATE + "')")
|
||||
public ApiResponse<VehicleModels.Response> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody VehicleModels.Create request) {
|
||||
return execute(() -> ApiResponse.success(service.create(operator(user), request)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_EDIT + "')")
|
||||
public ApiResponse<VehicleModels.Response> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long id,
|
||||
@Valid @RequestBody VehicleModels.Update request) {
|
||||
return execute(() -> ApiResponse.success(service.update(id, operator(user), request)));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/status")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_STATUS + "')")
|
||||
public ApiResponse<VehicleModels.Response> status(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long id,
|
||||
@Valid @RequestBody VehicleModels.ChangeStatus request) {
|
||||
return execute(() -> ApiResponse.success(service.changeStatus(id, operator(user), request)));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_EXPORT + "')")
|
||||
public ResponseEntity<byte[]> export(@Valid VehicleModels.Query query) {
|
||||
return execute(() -> ResponseEntity.ok().contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=vehicles.csv").body(service.exportCsv(query)));
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
if (user == null) return "UNKNOWN";
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
|
||||
private <T> T execute(Supplier<T> action) {
|
||||
try {
|
||||
return action.get();
|
||||
} catch (VehicleFailure failure) {
|
||||
HttpStatus status = switch (failure.kind()) {
|
||||
case BAD_REQUEST -> HttpStatus.BAD_REQUEST;
|
||||
case NOT_FOUND -> HttpStatus.NOT_FOUND;
|
||||
case CONFLICT -> HttpStatus.CONFLICT;
|
||||
};
|
||||
throw new ResponseStatusException(status, failure.getMessage(), failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,13 +1,12 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.mapper;
|
||||
package com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.entity.Vehicle;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface VehicleMapper extends BaseMapper<Vehicle> {
|
||||
public interface VehicleMapper extends BaseMapper<VehicleRecord> {
|
||||
@Select("SELECT * FROM ll_vehicle WHERE id=#{id} LIMIT 1 FOR UPDATE")
|
||||
Vehicle lockById(@Param("id") long id);
|
||||
VehicleRecord lockById(@Param("id") long id);
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class VehiclePersistenceAdapter implements VehicleRepository {
|
||||
private final VehicleMapper vehicles;
|
||||
|
||||
public VehiclePersistenceAdapter(VehicleMapper vehicles) {
|
||||
this.vehicles = vehicles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageSlice page(SearchCriteria criteria) {
|
||||
Page<VehicleRecord> page = vehicles.selectPage(Page.of(criteria.page(), criteria.size()), wrapper(criteria));
|
||||
return new PageSlice(page.getRecords().stream().map(this::snapshot).toList(), page.getCurrent(),
|
||||
page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VehicleSnapshot> list(SearchCriteria criteria) {
|
||||
return vehicles.selectList(wrapper(criteria)).stream().map(this::snapshot).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<VehicleSnapshot> findById(long id) {
|
||||
return Optional.ofNullable(vehicles.selectById(id)).map(this::snapshot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<VehicleSnapshot> lockById(long id) {
|
||||
return Optional.ofNullable(vehicles.lockById(id)).map(this::snapshot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long create(NewVehicle vehicle) {
|
||||
VehicleRecord record = new VehicleRecord();
|
||||
record.setId(IdWorker.getId());
|
||||
record.setPlateNumber(vehicle.plateNumber());
|
||||
record.setModel(vehicle.model());
|
||||
record.setFuelConsumptionPer100Km(vehicle.fuelConsumptionPer100Km());
|
||||
record.setMileageKm(vehicle.mileageKm());
|
||||
record.setDriverOrDispatchCompany(vehicle.driverOrDispatchCompany());
|
||||
record.setPhone(vehicle.phone());
|
||||
record.setRegion(vehicle.region());
|
||||
record.setOwnership(vehicle.ownership());
|
||||
record.setStatus(vehicle.status());
|
||||
record.setRemark(vehicle.remark());
|
||||
record.setRecordVersion(0L);
|
||||
record.setCreatedBy(vehicle.operator());
|
||||
record.setUpdatedBy(vehicle.operator());
|
||||
record.setCreatedAt(vehicle.createdAt());
|
||||
record.setUpdatedAt(vehicle.createdAt());
|
||||
try {
|
||||
vehicles.insert(record);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
throw new DuplicatePlateException(exception);
|
||||
}
|
||||
return record.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(long id, long expectedVersion, VehicleChanges changes) {
|
||||
try {
|
||||
return vehicles.update(null, new LambdaUpdateWrapper<VehicleRecord>()
|
||||
.eq(VehicleRecord::getId, id).eq(VehicleRecord::getRecordVersion, expectedVersion)
|
||||
.set(VehicleRecord::getPlateNumber, changes.plateNumber())
|
||||
.set(VehicleRecord::getModel, changes.model())
|
||||
.set(VehicleRecord::getFuelConsumptionPer100Km, changes.fuelConsumptionPer100Km())
|
||||
.set(VehicleRecord::getMileageKm, changes.mileageKm())
|
||||
.set(VehicleRecord::getDriverOrDispatchCompany, changes.driverOrDispatchCompany())
|
||||
.set(VehicleRecord::getPhone, changes.phone()).set(VehicleRecord::getRegion, changes.region())
|
||||
.set(VehicleRecord::getOwnership, changes.ownership()).set(VehicleRecord::getRemark, changes.remark())
|
||||
.set(VehicleRecord::getUpdatedBy, changes.operator()).set(VehicleRecord::getUpdatedAt, changes.updatedAt())
|
||||
.setSql("record_version = record_version + 1")) == 1;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
throw new DuplicatePlateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateStatus(long id, long expectedVersion, String status, String operator,
|
||||
java.time.LocalDateTime updatedAt) {
|
||||
return vehicles.update(null, new LambdaUpdateWrapper<VehicleRecord>()
|
||||
.eq(VehicleRecord::getId, id).eq(VehicleRecord::getRecordVersion, expectedVersion)
|
||||
.set(VehicleRecord::getStatus, status).set(VehicleRecord::getUpdatedBy, operator)
|
||||
.set(VehicleRecord::getUpdatedAt, updatedAt).setSql("record_version = record_version + 1")) == 1;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<VehicleRecord> wrapper(SearchCriteria criteria) {
|
||||
return new LambdaQueryWrapper<VehicleRecord>()
|
||||
.and(StringUtils.hasText(criteria.keyword()), wrapper -> wrapper.like(VehicleRecord::getPlateNumber, criteria.keyword())
|
||||
.or().like(VehicleRecord::getModel, criteria.keyword())
|
||||
.or().like(VehicleRecord::getDriverOrDispatchCompany, criteria.keyword()))
|
||||
.eq(criteria.ownership() != null, VehicleRecord::getOwnership, criteria.ownership())
|
||||
.eq(criteria.region() != null, VehicleRecord::getRegion, criteria.region())
|
||||
.eq(criteria.status() != null, VehicleRecord::getStatus, criteria.status())
|
||||
.orderByDesc(VehicleRecord::getUpdatedAt).orderByDesc(VehicleRecord::getId);
|
||||
}
|
||||
|
||||
private VehicleSnapshot snapshot(VehicleRecord value) {
|
||||
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());
|
||||
}
|
||||
|
||||
private static long version(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.entity;
|
||||
package com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_vehicle")
|
||||
public class Vehicle {
|
||||
public class VehicleRecord {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String plateNumber;
|
||||
private String model;
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
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.VehicleRepository.DuplicatePlateException;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.NewVehicle;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.SearchCriteria;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.VehicleChanges;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository.VehicleSnapshot;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class VehicleApplicationService {
|
||||
private static final Set<String> OWNERSHIPS = Set.of("OWNED", "DISPATCHED");
|
||||
private static final Set<String> REGIONS = Set.of("DOMESTIC", "OVERSEAS");
|
||||
private static final Set<String> STATUSES = Set.of("IN_SERVICE", "DISPATCHED", "MAINTENANCE", "INACTIVE", "RETURNED");
|
||||
|
||||
private final VehicleRepository vehicles;
|
||||
|
||||
public VehicleApplicationService(VehicleRepository vehicles) {
|
||||
this.vehicles = vehicles;
|
||||
}
|
||||
|
||||
public PageResult<VehicleModels.Response> page(VehicleModels.Query query) {
|
||||
var page = vehicles.page(criteria(query));
|
||||
return new PageResult<>(page.records().stream().map(this::response).toList(), page.page(),
|
||||
page.size(), page.total(), page.pages());
|
||||
}
|
||||
|
||||
public VehicleModels.Response detail(long id) {
|
||||
return response(required(id));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleModels.Response create(String operator, VehicleModels.Create request) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long id;
|
||||
try {
|
||||
id = vehicles.create(new NewVehicle(
|
||||
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));
|
||||
} catch (DuplicatePlateException exception) {
|
||||
throw conflict("车牌号已存在");
|
||||
}
|
||||
return detail(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleModels.Response update(long id, String operator, VehicleModels.Update request) {
|
||||
VehicleSnapshot current = locked(id);
|
||||
requireVersion(current, request.recordVersion());
|
||||
if (request.mileageKm() < current.mileageKm()) throw badRequest("当前公里码不能小于已有公里码");
|
||||
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()));
|
||||
} catch (DuplicatePlateException exception) {
|
||||
throw conflict("车牌号已存在");
|
||||
}
|
||||
if (!updated) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
return detail(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleModels.Response changeStatus(long id, String operator, VehicleModels.ChangeStatus request) {
|
||||
VehicleSnapshot current = locked(id);
|
||||
requireVersion(current, request.recordVersion());
|
||||
String status = status(request.status());
|
||||
if (status.equals(current.status())) return response(current);
|
||||
boolean updated = vehicles.updateStatus(id, request.recordVersion(), status, operator, LocalDateTime.now());
|
||||
if (!updated) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
return detail(id);
|
||||
}
|
||||
|
||||
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(',')
|
||||
.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(row.updatedAt()).append("\r\n");
|
||||
return csv.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private SearchCriteria criteria(VehicleModels.Query query) {
|
||||
return new SearchCriteria(query.page(), query.size(), trim(query.keyword()),
|
||||
optional(query.ownership(), OWNERSHIPS, "车辆归属"),
|
||||
optional(query.region(), REGIONS, "车辆区域"),
|
||||
optional(query.status(), STATUSES, "车辆状态"));
|
||||
}
|
||||
|
||||
private VehicleSnapshot required(long id) {
|
||||
return vehicles.findById(id).orElseThrow(VehicleApplicationService::notFound);
|
||||
}
|
||||
|
||||
private VehicleSnapshot locked(long id) {
|
||||
return vehicles.lockById(id).orElseThrow(VehicleApplicationService::notFound);
|
||||
}
|
||||
|
||||
private VehicleModels.Response response(VehicleSnapshot value) {
|
||||
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());
|
||||
}
|
||||
|
||||
private void requireVersion(VehicleSnapshot value, long expected) {
|
||||
if (value.recordVersion() != expected) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
}
|
||||
|
||||
private static String ownership(String value) { return required(value, OWNERSHIPS, "车辆归属"); }
|
||||
private static String region(String value) { return required(value, REGIONS, "车辆区域"); }
|
||||
private static String status(String value) { return required(value, STATUSES, "车辆状态"); }
|
||||
|
||||
private static String optional(String value, Set<String> allowed, String label) {
|
||||
return StringUtils.hasText(value) ? required(value, allowed, label) : null;
|
||||
}
|
||||
|
||||
private static String required(String value, Set<String> allowed, String label) {
|
||||
if (!StringUtils.hasText(value)) throw badRequest(label + "不能为空");
|
||||
String normalized = value.trim().toUpperCase(Locale.ROOT);
|
||||
if (!allowed.contains(normalized)) throw badRequest(label + "不合法");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String requiredText(String value, String label) {
|
||||
if (!StringUtils.hasText(value)) throw badRequest(label + "不能为空");
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String cell(Object value) { return "\"" + String.valueOf(value == null ? "" : value).replace("\"", "\"\"") + "\""; }
|
||||
private static VehicleFailure notFound() { return new VehicleFailure(FailureKind.NOT_FOUND, "车辆档案不存在"); }
|
||||
private static VehicleFailure badRequest(String message) { return new VehicleFailure(FailureKind.BAD_REQUEST, message); }
|
||||
private static VehicleFailure conflict(String message) { return new VehicleFailure(FailureKind.CONFLICT, message); }
|
||||
|
||||
public enum FailureKind { BAD_REQUEST, NOT_FOUND, CONFLICT }
|
||||
|
||||
public static final class VehicleFailure extends RuntimeException {
|
||||
private final FailureKind kind;
|
||||
|
||||
public VehicleFailure(FailureKind kind, String message) {
|
||||
super(message);
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
public FailureKind kind() {
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.dto;
|
||||
package com.baiyue.lastleg.masterdata.vehicle.application.model;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
@@ -11,8 +11,8 @@ import jakarta.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public final class VehicleDtos {
|
||||
private VehicleDtos() { }
|
||||
public final class VehicleModels {
|
||||
private VehicleModels() { }
|
||||
|
||||
public record Query(@Min(1) Integer page, @Min(1) @Max(200) Integer size,
|
||||
@Size(max = 128) String keyword, String ownership, String region, String status) {
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.application.port.out;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface VehicleRepository {
|
||||
PageSlice page(SearchCriteria criteria);
|
||||
|
||||
List<VehicleSnapshot> list(SearchCriteria criteria);
|
||||
|
||||
Optional<VehicleSnapshot> findById(long id);
|
||||
|
||||
Optional<VehicleSnapshot> lockById(long id);
|
||||
|
||||
long create(NewVehicle vehicle);
|
||||
|
||||
boolean update(long id, long expectedVersion, VehicleChanges changes);
|
||||
|
||||
boolean updateStatus(long id, long expectedVersion, String status, String operator, LocalDateTime updatedAt);
|
||||
|
||||
record SearchCriteria(int page, int size, String keyword, String ownership, String region, String status) { }
|
||||
|
||||
record PageSlice(List<VehicleSnapshot> records, long page, long size, long total, long pages) { }
|
||||
|
||||
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) { }
|
||||
|
||||
record VehicleChanges(String plateNumber, String model, BigDecimal fuelConsumptionPer100Km, long mileageKm,
|
||||
String driverOrDispatchCompany, String phone, String region, String ownership, String remark,
|
||||
String operator, LocalDateTime updatedAt) { }
|
||||
|
||||
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) { }
|
||||
|
||||
final class DuplicatePlateException extends RuntimeException {
|
||||
public DuplicatePlateException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.dto.VehicleDtos;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.service.VehicleService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/master-data/vehicles")
|
||||
@Tag(name = "Vehicle master data / 车辆主数据")
|
||||
public class PlatformVehicleController {
|
||||
@Resource private VehicleService service;
|
||||
|
||||
@GetMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_LIST + "')")
|
||||
@Operation(summary = "Page vehicle master data", description = "Required authority: platform:overseas-master-data:vehicle:list")
|
||||
public ApiResponse<PageResult<VehicleDtos.Response>> page(@Valid VehicleDtos.Query query) { return ApiResponse.success(service.page(query)); }
|
||||
|
||||
@GetMapping("/{id}") @PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_LIST + "')")
|
||||
@Operation(summary = "Get vehicle master data detail", description = "Required authority: platform:overseas-master-data:vehicle:list; excludes delivery fulfillment history")
|
||||
public ApiResponse<VehicleDtos.Response> detail(@PathVariable long id) { return ApiResponse.success(service.detail(id)); }
|
||||
|
||||
@PostMapping @PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_CREATE + "')")
|
||||
public ApiResponse<VehicleDtos.Response> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody VehicleDtos.Create request) { return ApiResponse.success(service.create(operator(user), request)); }
|
||||
|
||||
@PutMapping("/{id}") @PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_EDIT + "')")
|
||||
public ApiResponse<VehicleDtos.Response> update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long id,
|
||||
@Valid @RequestBody VehicleDtos.Update request) { return ApiResponse.success(service.update(id, operator(user), request)); }
|
||||
|
||||
@PatchMapping("/{id}/status") @PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_STATUS + "')")
|
||||
public ApiResponse<VehicleDtos.Response> status(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long id,
|
||||
@Valid @RequestBody VehicleDtos.ChangeStatus request) { return ApiResponse.success(service.changeStatus(id, operator(user), request)); }
|
||||
|
||||
@GetMapping(value = "/export", produces = "text/csv")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.VEHICLE_EXPORT + "')")
|
||||
public ResponseEntity<byte[]> export(@Valid VehicleDtos.Query query) {
|
||||
return ResponseEntity.ok().contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=vehicles.csv").body(service.exportCsv(query));
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
if (user == null) return "UNKNOWN"; return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.dto.VehicleDtos;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.entity.Vehicle;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.mapper.VehicleMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class VehicleService {
|
||||
private static final Set<String> OWNERSHIPS = Set.of("OWNED", "DISPATCHED");
|
||||
private static final Set<String> REGIONS = Set.of("DOMESTIC", "OVERSEAS");
|
||||
private static final Set<String> STATUSES = Set.of("IN_SERVICE", "DISPATCHED", "MAINTENANCE", "INACTIVE", "RETURNED");
|
||||
@Resource private VehicleMapper vehicles;
|
||||
|
||||
public PageResult<VehicleDtos.Response> page(VehicleDtos.Query query) {
|
||||
Page<Vehicle> page = vehicles.selectPage(Page.of(query.page(), query.size()), wrapper(query));
|
||||
return new PageResult<>(page.getRecords().stream().map(this::response).toList(), page.getCurrent(),
|
||||
page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public VehicleDtos.Response detail(long id) { return response(required(id)); }
|
||||
|
||||
@Transactional
|
||||
public VehicleDtos.Response create(String operator, VehicleDtos.Create request) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Vehicle value = new Vehicle(); value.setId(IdWorker.getId());
|
||||
value.setPlateNumber(requiredText(request.plateNumber(), "车牌号").toUpperCase(Locale.ROOT));
|
||||
value.setModel(requiredText(request.model(), "车型")); value.setFuelConsumptionPer100Km(request.fuelConsumptionPer100Km());
|
||||
value.setMileageKm(request.mileageKm()); value.setDriverOrDispatchCompany(trim(request.driverOrDispatchCompany()));
|
||||
value.setPhone(trim(request.phone())); value.setRegion(region(request.region())); value.setOwnership(ownership(request.ownership()));
|
||||
value.setStatus(status(request.status())); value.setRemark(trim(request.remark())); value.setRecordVersion(0L);
|
||||
value.setCreatedBy(operator); value.setUpdatedBy(operator); value.setCreatedAt(now); value.setUpdatedAt(now);
|
||||
try { vehicles.insert(value); } catch (DuplicateKeyException exception) { throw conflict("车牌号已存在"); }
|
||||
return detail(value.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleDtos.Response update(long id, String operator, VehicleDtos.Update request) {
|
||||
Vehicle current = locked(id); requireVersion(current, request.recordVersion());
|
||||
if (request.mileageKm() < current.getMileageKm()) throw badRequest("当前公里码不能小于已有公里码");
|
||||
int updated;
|
||||
try {
|
||||
updated = vehicles.update(null, new LambdaUpdateWrapper<Vehicle>()
|
||||
.eq(Vehicle::getId, id).eq(Vehicle::getRecordVersion, request.recordVersion())
|
||||
.set(Vehicle::getPlateNumber, requiredText(request.plateNumber(), "车牌号").toUpperCase(Locale.ROOT))
|
||||
.set(Vehicle::getModel, requiredText(request.model(), "车型"))
|
||||
.set(Vehicle::getFuelConsumptionPer100Km, request.fuelConsumptionPer100Km())
|
||||
.set(Vehicle::getMileageKm, request.mileageKm())
|
||||
.set(Vehicle::getDriverOrDispatchCompany, trim(request.driverOrDispatchCompany()))
|
||||
.set(Vehicle::getPhone, trim(request.phone())).set(Vehicle::getRegion, region(request.region()))
|
||||
.set(Vehicle::getOwnership, ownership(request.ownership())).set(Vehicle::getRemark, trim(request.remark()))
|
||||
.set(Vehicle::getUpdatedBy, operator).set(Vehicle::getUpdatedAt, LocalDateTime.now())
|
||||
.setSql("record_version = record_version + 1"));
|
||||
} catch (DuplicateKeyException exception) { throw conflict("车牌号已存在"); }
|
||||
if (updated != 1) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
return detail(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VehicleDtos.Response changeStatus(long id, String operator, VehicleDtos.ChangeStatus request) {
|
||||
Vehicle current = locked(id); requireVersion(current, request.recordVersion());
|
||||
String status = status(request.status());
|
||||
if (status.equals(current.getStatus())) return response(current);
|
||||
int updated = vehicles.update(null, new LambdaUpdateWrapper<Vehicle>()
|
||||
.eq(Vehicle::getId, id).eq(Vehicle::getRecordVersion, request.recordVersion())
|
||||
.set(Vehicle::getStatus, status).set(Vehicle::getUpdatedBy, operator)
|
||||
.set(Vehicle::getUpdatedAt, LocalDateTime.now()).setSql("record_version = record_version + 1"));
|
||||
if (updated != 1) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
return detail(id);
|
||||
}
|
||||
|
||||
public byte[] exportCsv(VehicleDtos.Query query) {
|
||||
var rows = vehicles.selectList(wrapper(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(',')
|
||||
.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(row.updatedAt()).append("\r\n");
|
||||
return csv.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<Vehicle> wrapper(VehicleDtos.Query query) {
|
||||
String keyword = trim(query.keyword()); String ownership = optional(query.ownership(), OWNERSHIPS, "车辆归属");
|
||||
String region = optional(query.region(), REGIONS, "车辆区域"); String status = optional(query.status(), STATUSES, "车辆状态");
|
||||
return new LambdaQueryWrapper<Vehicle>()
|
||||
.and(StringUtils.hasText(keyword), w -> w.like(Vehicle::getPlateNumber, keyword)
|
||||
.or().like(Vehicle::getModel, keyword).or().like(Vehicle::getDriverOrDispatchCompany, keyword))
|
||||
.eq(ownership != null, Vehicle::getOwnership, ownership).eq(region != null, Vehicle::getRegion, region)
|
||||
.eq(status != null, Vehicle::getStatus, status).orderByDesc(Vehicle::getUpdatedAt).orderByDesc(Vehicle::getId);
|
||||
}
|
||||
private Vehicle required(long id) {
|
||||
Vehicle value = vehicles.selectById(id); if (value == null) throw notFound(); return value;
|
||||
}
|
||||
private Vehicle locked(long id) {
|
||||
Vehicle value = vehicles.lockById(id); if (value == null) throw notFound(); return value;
|
||||
}
|
||||
private VehicleDtos.Response response(Vehicle value) {
|
||||
return new VehicleDtos.Response(String.valueOf(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());
|
||||
}
|
||||
private void requireVersion(Vehicle value, long expected) {
|
||||
if (version(value.getRecordVersion()) != expected) throw conflict("车辆档案已被其他操作修改,请刷新后重试");
|
||||
}
|
||||
private static String ownership(String value) { return required(value, OWNERSHIPS, "车辆归属"); }
|
||||
private static String region(String value) { return required(value, REGIONS, "车辆区域"); }
|
||||
private static String status(String value) { return required(value, STATUSES, "车辆状态"); }
|
||||
private static String optional(String value, Set<String> allowed, String label) {
|
||||
return StringUtils.hasText(value) ? required(value, allowed, label) : null;
|
||||
}
|
||||
private static String required(String value, Set<String> allowed, String label) {
|
||||
if (!StringUtils.hasText(value)) throw badRequest(label + "不能为空");
|
||||
String normalized = value.trim().toUpperCase(Locale.ROOT);
|
||||
if (!allowed.contains(normalized)) throw badRequest(label + "不合法"); return normalized;
|
||||
}
|
||||
private static String requiredText(String value, String label) {
|
||||
if (!StringUtils.hasText(value)) throw badRequest(label + "不能为空"); return value.trim();
|
||||
}
|
||||
private static long version(Long value) { return value == null ? 0L : value; }
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String cell(Object value) { return "\"" + String.valueOf(value == null ? "" : value).replace("\"", "\"\"") + "\""; }
|
||||
private static ResponseStatusException notFound() { return new ResponseStatusException(HttpStatus.NOT_FOUND, "车辆档案不存在"); }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
+2
@@ -32,6 +32,8 @@ public class UnloadCompletedOutboxRepository {
|
||||
.eq(LastlegEventOutbox::getId, id)
|
||||
.eq(LastlegEventOutbox::getEventType, EventTypes.OVERSEAS_CONTAINER_UNLOADED)
|
||||
.and(status -> status.eq(LastlegEventOutbox::getStatus, "PENDING")
|
||||
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt)
|
||||
.or().le(LastlegEventOutbox::getNextRetryAt, now))
|
||||
.or(stale -> stale.eq(LastlegEventOutbox::getStatus, "PROCESSING")
|
||||
.le(LastlegEventOutbox::getLockedAt, expiredLeaseBefore)))
|
||||
.set(LastlegEventOutbox::getStatus, "PROCESSING")
|
||||
|
||||
@@ -54,6 +54,8 @@ public class LoadingCompletedConsumer {
|
||||
throw new IllegalArgumentException("尾程交接事件区域无效");
|
||||
if (!"LOADING_PLAN".equals(envelope.aggregateType()))
|
||||
throw new IllegalArgumentException("尾程交接事件聚合类型无效");
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()),
|
||||
envelope.payloadHash())) throw new IllegalArgumentException("payloadHash 与尾程事件负载不一致");
|
||||
FirstLegLoadingPlanCompletedV1 payload = parsePayload(envelope);
|
||||
if(payload.containerNo()==null||payload.containerNo().isBlank())
|
||||
throw new IllegalArgumentException("事件没有实际集装箱号 containerNo");
|
||||
@@ -70,6 +72,11 @@ public class LoadingCompletedConsumer {
|
||||
inbox.setEventVersion(envelope.eventVersion()); inbox.setPayloadHash(envelope.payloadHash());
|
||||
inbox.setPayloadJson(envelope.payload().toString()); inbox.setStatus("RECEIVED"); inbox.setReceivedAt(LocalDateTime.now());
|
||||
try { inboxMapper.insert(inbox); } catch(DuplicateKeyException exception) {
|
||||
LastlegEventInbox raced = inboxMapper.selectById(envelope.eventId());
|
||||
if (raced == null || raced.getPayloadHash() == null
|
||||
|| !raced.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("事件摘要冲突", exception);
|
||||
}
|
||||
return countOrders(envelope,payload.containerNo());
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.lastleg.shipping.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.FirstLegContainerShippingNodeRecordedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
@@ -91,6 +93,10 @@ public class ContainerShippingNodeConsumer {
|
||||
|
||||
private void validate(EventEnvelope envelope) {
|
||||
envelope.requireValid();
|
||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(envelope.payload()),
|
||||
envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("payloadHash 与船节点事件负载不一致");
|
||||
}
|
||||
if (!EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED.equals(envelope.eventType())
|
||||
|| envelope.eventVersion() != 1 || envelope.sourceRegion() != RegionCode.CN
|
||||
|| envelope.targetRegion() != RegionCode.OVERSEAS
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.lastleg.config;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class LastlegRestClientFactoryTest {
|
||||
private final LastlegRestClientFactory factory = new LastlegRestClientFactory();
|
||||
|
||||
@Test
|
||||
void defaultsArePositiveAndSeparateFileUploadsFromWarehouseCalls() {
|
||||
LastlegRestClientProperties properties = new LastlegRestClientProperties();
|
||||
|
||||
assertThat(properties.getUnloadPhotoFile().getConnectTimeout()).isEqualTo(Duration.ofSeconds(3));
|
||||
assertThat(properties.getUnloadPhotoFile().getReadTimeout()).isEqualTo(Duration.ofSeconds(30));
|
||||
assertThat(properties.getRelabelPhotoFile().getReadTimeout()).isEqualTo(Duration.ofSeconds(30));
|
||||
assertThat(properties.getWarehouseResource().getReadTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
assertThat(properties.getPalletPhysical().getReadTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnboundedOrNonPositiveTimeouts() {
|
||||
assertThatThrownBy(() -> new LastlegRestClientProperties.ClientTimeouts(null, Duration.ofSeconds(1)))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> new LastlegRestClientProperties.ClientTimeouts(Duration.ZERO, Duration.ofSeconds(1)))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> new LastlegRestClientProperties.ClientTimeouts(Duration.ofSeconds(1), Duration.ZERO))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindsPerClientTimeoutOverridesFromConfiguration() {
|
||||
var source = new MapConfigurationPropertySource(Map.of(
|
||||
"baiyue.http.clients.unload-photo-file.connect-timeout", "125ms",
|
||||
"baiyue.http.clients.unload-photo-file.read-timeout", "9s",
|
||||
"baiyue.http.clients.pallet-physical.connect-timeout", "250ms",
|
||||
"baiyue.http.clients.pallet-physical.read-timeout", "2s"));
|
||||
|
||||
LastlegRestClientProperties properties = new Binder(source)
|
||||
.bind("baiyue.http.clients", Bindable.of(LastlegRestClientProperties.class))
|
||||
.orElseThrow(() -> new AssertionError("Lastleg HTTP client properties were not bound"));
|
||||
|
||||
assertThat(properties.getUnloadPhotoFile().getConnectTimeout()).isEqualTo(Duration.ofMillis(125));
|
||||
assertThat(properties.getUnloadPhotoFile().getReadTimeout()).isEqualTo(Duration.ofSeconds(9));
|
||||
assertThat(properties.getPalletPhysical().getConnectTimeout()).isEqualTo(Duration.ofMillis(250));
|
||||
assertThat(properties.getPalletPhysical().getReadTimeout()).isEqualTo(Duration.ofSeconds(2));
|
||||
assertThat(properties.getWarehouseResource().getReadTimeout()).isEqualTo(Duration.ofSeconds(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesConfiguredConnectAndReadTimeoutsWithoutRetryOrFallback() throws Exception {
|
||||
var timeouts = new LastlegRestClientProperties.ClientTimeouts(
|
||||
Duration.ofMillis(250), Duration.ofMillis(75));
|
||||
assertThat(factory.httpClient(timeouts).connectTimeout()).contains(Duration.ofMillis(250));
|
||||
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/slow", exchange -> {
|
||||
try { Thread.sleep(500); }
|
||||
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); }
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
try {
|
||||
RestClient client = factory.build(RestClient.builder(),
|
||||
"http://127.0.0.1:" + server.getAddress().getPort(), timeouts);
|
||||
assertThatThrownBy(() -> client.get().uri("/slow").retrieve().toBodilessEntity())
|
||||
.isInstanceOf(ResourceAccessException.class);
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -3,6 +3,8 @@ package com.baiyue.lastleg.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderSummary;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
@@ -21,6 +23,8 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LastlegOrderControllerTest {
|
||||
private final LastlegOrderService service = mock(LastlegOrderService.class);
|
||||
@@ -46,6 +50,17 @@ class LastlegOrderControllerTest {
|
||||
verify(service).page(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() {
|
||||
EventEnvelope event = mock(EventEnvelope.class);
|
||||
when(event.eventType()).thenReturn(EventTypes.IAM_USER_STATUS_CHANGED);
|
||||
LoadingCompletedConsumer loading = (LoadingCompletedConsumer) ReflectionTestUtils.getField(controller, "consumer");
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> controller.receive("internal-token", event));
|
||||
|
||||
verify(loading, never()).consume(event);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long customerId, String appCode) {
|
||||
return new AuthenticatedUser(1L, customerId, customerId, 1L, "tester", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.controller;
|
||||
package com.baiyue.lastleg.masterdata.vehicle.adapter.in.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.application;
|
||||
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService.FailureKind;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService.VehicleFailure;
|
||||
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.VehicleRepository.VehicleSnapshot;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class VehicleApplicationServiceTest {
|
||||
private VehicleRepository repository;
|
||||
private VehicleApplicationService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
repository = mock(VehicleRepository.class);
|
||||
service = new VehicleApplicationService(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionBeforeUpdating() {
|
||||
when(repository.lockById(1L)).thenReturn(Optional.of(vehicle(3L, 1000L)));
|
||||
VehicleModels.Update request = update(2L, 1000L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOfSatisfying(VehicleFailure.class,
|
||||
failure -> assertThat(failure.kind()).isEqualTo(FailureKind.CONFLICT));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDecreasingOdometer() {
|
||||
when(repository.lockById(1L)).thenReturn(Optional.of(vehicle(3L, 1000L)));
|
||||
VehicleModels.Update request = update(3L, 999L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOfSatisfying(VehicleFailure.class,
|
||||
failure -> assertThat(failure.kind()).isEqualTo(FailureKind.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownStatus() {
|
||||
when(repository.lockById(1L)).thenReturn(Optional.of(vehicle(3L, 1000L)));
|
||||
assertThatThrownBy(() -> service.changeStatus(1L, "9", new VehicleModels.ChangeStatus("BUSY", 3L)))
|
||||
.isInstanceOfSatisfying(VehicleFailure.class,
|
||||
failure -> assertThat(failure.kind()).isEqualTo(FailureKind.BAD_REQUEST));
|
||||
}
|
||||
|
||||
private static VehicleModels.Update update(long version, long mileage) {
|
||||
return new VehicleModels.Update(version, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", null);
|
||||
}
|
||||
|
||||
private static VehicleSnapshot vehicle(long version, long mileage) {
|
||||
return new VehicleSnapshot(1L, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", "IN_SERVICE", null, version, "9", null);
|
||||
}
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
package com.baiyue.lastleg.masterdata.vehicle.service;
|
||||
|
||||
import com.baiyue.lastleg.masterdata.vehicle.dto.VehicleDtos;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.entity.Vehicle;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.mapper.VehicleMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class VehicleServiceTest {
|
||||
private VehicleMapper mapper;
|
||||
private VehicleService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mapper = mock(VehicleMapper.class);
|
||||
service = new VehicleService();
|
||||
ReflectionTestUtils.setField(service, "vehicles", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionBeforeUpdating() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
VehicleDtos.Update request = update(2L, 1000L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409 CONFLICT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDecreasingOdometer() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
VehicleDtos.Update request = update(3L, 999L);
|
||||
assertThatThrownBy(() -> service.update(1L, "9", request))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400 BAD_REQUEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownStatus() {
|
||||
when(mapper.lockById(1L)).thenReturn(vehicle(3L, 1000L));
|
||||
assertThatThrownBy(() -> service.changeStatus(1L, "9", new VehicleDtos.ChangeStatus("BUSY", 3L)))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("400 BAD_REQUEST");
|
||||
}
|
||||
|
||||
private static VehicleDtos.Update update(long version, long mileage) {
|
||||
return new VehicleDtos.Update(version, "ABC-001", "Box Truck", new BigDecimal("12.50"), mileage,
|
||||
"Driver", "123", "OVERSEAS", "OWNED", null);
|
||||
}
|
||||
|
||||
private static Vehicle vehicle(long version, long mileage) {
|
||||
Vehicle value = new Vehicle(); value.setId(1L); value.setRecordVersion(version); value.setMileageKm(mileage);
|
||||
value.setStatus("IN_SERVICE"); return value;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.lastleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class UnloadCompletedOutboxRepositoryTest {
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void claimRechecksRetryWindowAfterCandidateSelection() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
LastlegEventOutbox.class);
|
||||
LastlegEventOutboxMapper mapper = mock(LastlegEventOutboxMapper.class);
|
||||
UnloadCompletedOutboxRepository repository = new UnloadCompletedOutboxRepository();
|
||||
ReflectionTestUtils.setField(repository, "mapper", mapper);
|
||||
ArgumentCaptor<LambdaUpdateWrapper> wrapper = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
when(mapper.update(isNull(LastlegEventOutbox.class), wrapper.capture())).thenReturn(1);
|
||||
LocalDateTime now = LocalDateTime.of(2026, 8, 25, 12, 0);
|
||||
|
||||
assertThat(repository.claim(1L, "worker", now, now.minusMinutes(1))).isTrue();
|
||||
|
||||
assertThat(wrapper.getValue().getSqlSegment())
|
||||
.contains("next_retry_at", "<=", "status", "locked_at");
|
||||
}
|
||||
}
|
||||
+2
@@ -8,6 +8,8 @@ class LoadingCompletedConsumerTest {
|
||||
@Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion(){ service.consume(event()); verify(shippingGates).initialize("book-1","100","","MSCU1",0L,1L); }
|
||||
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
|
||||
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
||||
@Test void tamperedPayloadHashIsRejectedBeforeInboxMutation(){ EventEnvelope source=event(); EventEnvelope tampered=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),"sha256:"+"0".repeat(64)); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(tampered)); assertTrue(error.getMessage().contains("payloadHash")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); }
|
||||
@Test void concurrentDuplicateKeyWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox raced=new LastlegEventInbox(); raced.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(null,raced); doThrow(new org.springframework.dao.DuplicateKeyException("race")).when(inbox).insert(any(LastlegEventInbox.class)); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(event)); assertTrue(error.getMessage().contains("摘要冲突")); verify(orders,never()).countContainer(anyString(),anyString()); }
|
||||
@Test void inboundSnapshotUsesCustomerIdAndHasNoPhantomTenantColumn() throws Exception {
|
||||
assertThrows(NoSuchFieldException.class,()->LastlegInboundOrder.class.getDeclaredField("tenantId"));
|
||||
String schema=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/db/lastleg.sql"));
|
||||
|
||||
+15
@@ -96,6 +96,21 @@ class ContainerShippingNodeConsumerTest {
|
||||
.hasMessageContaining("摘要冲突");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tamperedPayloadHashIsRejectedBeforeInboxMutation() {
|
||||
EventEnvelope source = event("123e4567-e89b-12d3-a456-426614174105", "PICKUP_READY", 80, 5,
|
||||
Instant.parse("2026-08-21T10:00:00Z"));
|
||||
EventEnvelope tampered = new EventEnvelope(source.eventId(), source.eventType(), source.eventVersion(),
|
||||
source.occurredAt(), source.sourceRegion(), source.targetRegion(), source.tenantId(),
|
||||
source.aggregateType(), source.aggregateId(), source.aggregateVersion(), source.handoverVersion(),
|
||||
source.traceId(), source.correlationId(), source.causationId(), source.payload(),
|
||||
"sha256:" + "0".repeat(64));
|
||||
|
||||
assertThatThrownBy(() -> consumer.consume(tampered)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("payloadHash");
|
||||
verify(inboxes, never()).insert(any(LastlegEventInbox.class));
|
||||
}
|
||||
|
||||
private ContainerShippingGate gate(String status, int sequence, long version, Instant actualAt) {
|
||||
ContainerShippingGate gate = new ContainerShippingGate(); gate.setContainerBookingId("CB-1");
|
||||
gate.setCustomerId("100"); gate.setOceanBookingId("OB-1"); gate.setContainerNo("CONT-1");
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"description": "Development-stage Lastleg slice dependency baseline. Existing edges may be removed; new edges require architecture review.",
|
||||
"allowedSliceEdges": [
|
||||
"dispatch-delivery->inbound-pda",
|
||||
"dispatch-delivery->inventory",
|
||||
"dispatch-delivery->shared-technical",
|
||||
"dispatch-delivery->tailleg-handoff",
|
||||
"dropship->inbound-pda",
|
||||
"dropship->shared-technical",
|
||||
"dropship->tailleg-handoff",
|
||||
"inbound-pda->dropship",
|
||||
"inbound-pda->inventory",
|
||||
"inbound-pda->shared-technical",
|
||||
"inbound-pda->tailleg-handoff",
|
||||
"inventory->inbound-pda",
|
||||
"inventory->shared-technical",
|
||||
"overseas-master-data->shared-technical",
|
||||
"procurement-trucking-looseboard->overseas-master-data",
|
||||
"procurement-trucking-looseboard->shared-technical",
|
||||
"procurement-trucking-looseboard->tailleg-handoff",
|
||||
"relabel-transfer-discard->dropship",
|
||||
"relabel-transfer-discard->inventory",
|
||||
"relabel-transfer-discard->shared-technical",
|
||||
"relabel-transfer-discard->tailleg-handoff",
|
||||
"tailleg-handoff->dropship",
|
||||
"tailleg-handoff->inbound-pda",
|
||||
"tailleg-handoff->inventory",
|
||||
"tailleg-handoff->shared-technical"
|
||||
],
|
||||
"requiredClassLocations": {
|
||||
"com.baiyue.lastleg.masterdata.vehicle.adapter.in.web.PlatformVehicleController": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleController.java",
|
||||
"com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence.VehicleMapper": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehicleMapper.java",
|
||||
"com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence.VehiclePersistenceAdapter": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehiclePersistenceAdapter.java",
|
||||
"com.baiyue.lastleg.masterdata.vehicle.adapter.out.persistence.VehicleRecord": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehicleRecord.java",
|
||||
"com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/VehicleApplicationService.java",
|
||||
"com.baiyue.lastleg.masterdata.vehicle.application.model.VehicleModels": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/model/VehicleModels.java",
|
||||
"com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository": "src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/port/out/VehicleRepository.java"
|
||||
},
|
||||
"applicationDependencyRule": {
|
||||
"packagePrefix": "com.baiyue.lastleg.masterdata.vehicle.application",
|
||||
"allowedInternalPrefix": "com.baiyue.lastleg.masterdata.vehicle.application",
|
||||
"forbiddenImportPrefixes": [
|
||||
"org.springframework.web",
|
||||
"org.apache.ibatis",
|
||||
"com.baomidou.mybatisplus",
|
||||
"com.baiyue.lastleg.client",
|
||||
"com.baiyue.lastleg.config"
|
||||
]
|
||||
},
|
||||
"sliceSpecificRules": {
|
||||
"overseas-master-data": {
|
||||
"allowedOutgoing": ["shared-technical"],
|
||||
"allowedIncoming": ["procurement-trucking-looseboard"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user