feat(fleet): query vehicle delivery and reassignment history
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/api/platform/v1/last-leg/fleet/vehicles/{vehicleId}")
|
||||
public class PlatformVehicleFulfillmentController {
|
||||
private final VehicleFulfillmentService service;
|
||||
public PlatformVehicleFulfillmentController(VehicleFulfillmentService service) { this.service = service; }
|
||||
@GetMapping("/deliveries")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-master-data:vehicle:list') and hasAuthority('platform:overseas-warehouse:delivery:list')")
|
||||
public ApiResponse<PageResult<VehicleFulfillmentModels.Delivery>> deliveries(@PathVariable @Min(1) long vehicleId, @Valid VehicleFulfillmentModels.Query query) {
|
||||
return ApiResponse.success(service.deliveries(vehicleId, query));
|
||||
}
|
||||
@GetMapping("/assignment-history")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-master-data:vehicle:list') and hasAuthority('platform:fleet:trip:list')")
|
||||
public ApiResponse<PageResult<VehicleFulfillmentModels.Change>> changes(@PathVariable @Min(1) long vehicleId, @Valid VehicleFulfillmentModels.Query query) {
|
||||
return ApiResponse.success(service.changes(vehicleId, query));
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
/** Read projection in the Lastleg schema. Never derives vehicle identity from a mutable plate. */
|
||||
@Mapper
|
||||
public interface VehicleFulfillmentMapper {
|
||||
String DEPARTED = "EXISTS (SELECT 1 FROM ll_fleet_trip_history h WHERE h.trip_id=t.id AND h.action='DEPART' "
|
||||
+ "AND JSON_UNQUOTE(JSON_EXTRACT(h.snapshot_json,'$.trip.vehicleId'))=#{vehicleKey})";
|
||||
String DELIVERIES = " FROM ll_delivery_order d JOIN ll_fleet_trip_dispatch b "
|
||||
+ "ON b.dispatch_id=d.dispatch_order_id AND b.customer_id=d.customer_id "
|
||||
+ "JOIN ll_fleet_trip t ON t.id=b.trip_id WHERE ((t.status='PLANNED' AND t.vehicle_id=#{vehicleId}) OR " + DEPARTED + ")";
|
||||
String PREVIOUS_VEHICLE = "JSON_UNQUOTE(JSON_EXTRACT(p.snapshot_json,'$.trip.vehicleId'))";
|
||||
String NEXT_VEHICLE = "JSON_UNQUOTE(JSON_EXTRACT(h.snapshot_json,'$.trip.vehicleId'))";
|
||||
String CHANGES = " FROM ll_fleet_trip_history h JOIN ll_fleet_trip t ON t.id=h.trip_id "
|
||||
+ "JOIN ll_fleet_trip_history p ON p.trip_id=h.trip_id AND p.record_version=h.record_version-1 "
|
||||
+ "WHERE h.action='ASSIGN' AND " + PREVIOUS_VEHICLE + " IS NOT NULL AND " + PREVIOUS_VEHICLE + " <> 'null' "
|
||||
+ "AND (" + PREVIOUS_VEHICLE + "=#{vehicleKey} OR " + NEXT_VEHICLE + "=#{vehicleKey}) "
|
||||
+ "AND (" + PREVIOUS_VEHICLE + "<>" + NEXT_VEHICLE
|
||||
+ " OR JSON_UNQUOTE(JSON_EXTRACT(p.snapshot_json,'$.trip.driverId'))<>JSON_UNQUOTE(JSON_EXTRACT(h.snapshot_json,'$.trip.driverId')))";
|
||||
|
||||
@Select("SELECT COUNT(*)" + DELIVERIES)
|
||||
long countDeliveries(@Param("vehicleId") long vehicleId, @Param("vehicleKey") String vehicleKey);
|
||||
@Select("SELECT d.delivery_no,d.dispatch_no,t.trip_no,d.bol_no,d.customer_name,d.destination_type,"
|
||||
+ "d.destination_warehouse_code,d.destination_warehouse_name,d.delivery_address,d.planned_delivery_at,d.status,"
|
||||
+ "CASE WHEN " + DEPARTED + " THEN 'DEPARTED' ELSE 'PLANNED' END AS relationship" + DELIVERIES
|
||||
+ " ORDER BY d.created_at DESC,d.id DESC LIMIT #{size} OFFSET #{offset}")
|
||||
List<VehicleFulfillmentModels.Delivery> deliveries(@Param("vehicleId") long vehicleId, @Param("vehicleKey") String vehicleKey,
|
||||
@Param("offset") long offset, @Param("size") int size);
|
||||
@Select("SELECT COUNT(*)" + CHANGES)
|
||||
long countChanges(@Param("vehicleKey") String vehicleKey);
|
||||
@Select("SELECT t.trip_no,h.record_version,h.occurred_at,h.reason,p.snapshot_json AS before_snapshot,h.snapshot_json AS after_snapshot"
|
||||
+ CHANGES + " ORDER BY h.occurred_at DESC,h.id DESC LIMIT #{size} OFFSET #{offset}")
|
||||
List<VehicleFulfillmentModels.ChangeRow> changes(@Param("vehicleKey") String vehicleKey, @Param("offset") long offset, @Param("size") int size);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class VehicleFulfillmentModels {
|
||||
private VehicleFulfillmentModels() { }
|
||||
public record Query(@Min(1) Integer page, @Min(1) @Max(100) Integer size) {
|
||||
public Query { page = page == null ? 1 : page; size = size == null ? 10 : size; }
|
||||
public long offset() { return ((long) page - 1) * size; }
|
||||
}
|
||||
public record Delivery(String deliveryNo, String dispatchNo, String tripNo, String bolNo,
|
||||
String customerName, String destinationType, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String deliveryAddress,
|
||||
LocalDateTime plannedDeliveryAt, String status, String relationship) { }
|
||||
public record ChangeRow(String tripNo, long recordVersion, LocalDateTime occurredAt, String reason,
|
||||
String beforeSnapshot, String afterSnapshot) { }
|
||||
public record Change(String tripNo, long recordVersion, Instant occurredAt, String reason,
|
||||
String previousVehiclePlate, String vehiclePlate,
|
||||
String previousDriverName, String driverName, String relationship,
|
||||
List<String> dispatchNos) { }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ)
|
||||
public class VehicleFulfillmentService {
|
||||
private final VehicleRepository vehicles;
|
||||
private final VehicleFulfillmentMapper mapper;
|
||||
private final ObjectMapper json;
|
||||
public VehicleFulfillmentService(VehicleRepository vehicles, VehicleFulfillmentMapper mapper, ObjectMapper json) {
|
||||
this.vehicles = vehicles; this.mapper = mapper; this.json = json;
|
||||
}
|
||||
public PageResult<VehicleFulfillmentModels.Delivery> deliveries(long vehicleId, VehicleFulfillmentModels.Query query) {
|
||||
requireVehicle(vehicleId); String key = Long.toString(vehicleId);
|
||||
long total = mapper.countDeliveries(vehicleId, key);
|
||||
return new PageResult<>(mapper.deliveries(vehicleId, key, query.offset(), query.size()), query.page(), query.size(), total, pages(total, query.size()));
|
||||
}
|
||||
public PageResult<VehicleFulfillmentModels.Change> changes(long vehicleId, VehicleFulfillmentModels.Query query) {
|
||||
requireVehicle(vehicleId); String key = Long.toString(vehicleId);
|
||||
long total = mapper.countChanges(key);
|
||||
var records = mapper.changes(key, query.offset(), query.size()).stream().map(row -> change(key, row)).toList();
|
||||
return new PageResult<>(records, query.page(), query.size(), total, pages(total, query.size()));
|
||||
}
|
||||
private VehicleFulfillmentModels.Change change(String key, VehicleFulfillmentModels.ChangeRow row) {
|
||||
try {
|
||||
JsonNode before = json.readTree(row.beforeSnapshot()).path("trip"), snapshot = json.readTree(row.afterSnapshot()), after = snapshot.path("trip");
|
||||
if (!before.hasNonNull("vehicleId") || !after.hasNonNull("vehicleId")) throw new IllegalStateException("车辆更换历史缺少身份快照");
|
||||
var dispatches = new ArrayList<String>();
|
||||
for (JsonNode binding : snapshot.path("dispatches")) if (binding.hasNonNull("dispatchNo")) dispatches.add(binding.get("dispatchNo").asText());
|
||||
String relationship = before.path("vehicleId").asText().equals(after.path("vehicleId").asText()) ? "DRIVER_CHANGED"
|
||||
: key.equals(before.path("vehicleId").asText()) ? "REPLACED_OUT" : "REPLACED_IN";
|
||||
return new VehicleFulfillmentModels.Change(row.tripNo(), row.recordVersion(), row.occurredAt().toInstant(ZoneOffset.UTC), row.reason(),
|
||||
text(before, "vehiclePlate"), text(after, "vehiclePlate"), text(before, "driverName"), text(after, "driverName"), relationship, java.util.List.copyOf(dispatches));
|
||||
} catch (JsonProcessingException error) { throw new IllegalStateException("车辆更换历史快照无法读取", error); }
|
||||
}
|
||||
private static String text(JsonNode value, String key) { return value.hasNonNull(key) ? value.get(key).asText() : null; }
|
||||
private static long pages(long total, int size) { return total / size + (total % size == 0 ? 0 : 1); }
|
||||
private void requireVehicle(long vehicleId) {
|
||||
vehicles.findById(vehicleId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "车辆不存在"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: 车辆履约与更换历史
|
||||
version: 1.0.0
|
||||
description: >-
|
||||
平台按稳定车辆ID读取Lastleg本库履约事实,不按当前车牌文本反查,不修改单据或历史。
|
||||
计划中的派送归当前分配车辆;实际发车后按不可变DEPART历史中的车辆身份归属。
|
||||
关联排车和派送时同时匹配客户归属,不返回财务、附件、联系人或内部数据库主键。
|
||||
更换记录依据相邻车次版本的ASSIGN快照,首次分配和资源未变化的操作不算更换。
|
||||
旧车和新车均能读取同一更换事实;仅换司机也保留。返回历史车牌/姓名,不用当前主档覆盖。
|
||||
不生成原型示例记录;没有正式车次关联的旧派送不通过车牌猜测归属。
|
||||
security: [{bearerAuth: []}]
|
||||
paths:
|
||||
/api/platform/v1/last-leg/fleet/vehicles/{vehicleId}/deliveries:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/Vehicle'}
|
||||
- {$ref: '#/components/parameters/Page'}
|
||||
- {$ref: '#/components/parameters/Size'}
|
||||
get:
|
||||
summary: 当前车辆关联派送记录
|
||||
description: '同时要求 platform:overseas-master-data:vehicle:list 与 platform:overseas-warehouse:delivery:list。按派送创建时间、内部排序键倒序分页;排序键不返回。'
|
||||
responses:
|
||||
'200':
|
||||
description: '统一ApiResponse,成功code为200,data为分页;车辆不存在返回业务404语义。'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
data:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/Page'}
|
||||
- type: object
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/Delivery'}}
|
||||
/api/platform/v1/last-leg/fleet/vehicles/{vehicleId}/assignment-history:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/Vehicle'}
|
||||
- {$ref: '#/components/parameters/Page'}
|
||||
- {$ref: '#/components/parameters/Size'}
|
||||
get:
|
||||
summary: 当前车辆相关的换车与换司机事实
|
||||
description: '同时要求 platform:overseas-master-data:vehicle:list 与 platform:fleet:trip:list。按事件时间、历史排序键倒序分页,一行一个变更事件;关联排车使用该事件快照,不使用当前关联覆盖历史。'
|
||||
responses:
|
||||
'200':
|
||||
description: '统一ApiResponse,data为分页。历史记录只含稳定业务编号及更换事实,不公开整份客户快照或操作人内部ID。'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
data:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/Page'}
|
||||
- type: object
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/Change'}}
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||
parameters:
|
||||
Vehicle: {name: vehicleId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
Page: {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||
Size: {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 10}}
|
||||
schemas:
|
||||
Page:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
page: {type: integer, minimum: 1}
|
||||
size: {type: integer, minimum: 1, maximum: 100}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
Delivery:
|
||||
type: object
|
||||
required: [deliveryNo, dispatchNo, tripNo, status, relationship]
|
||||
properties:
|
||||
deliveryNo: {type: string}
|
||||
dispatchNo: {type: string}
|
||||
tripNo: {type: string}
|
||||
bolNo: {type: string, nullable: true}
|
||||
customerName: {type: string, nullable: true}
|
||||
destinationType: {type: string}
|
||||
destinationWarehouseCode: {type: string, nullable: true}
|
||||
destinationWarehouseName: {type: string, nullable: true}
|
||||
deliveryAddress: {type: string, nullable: true}
|
||||
plannedDeliveryAt: {type: string, nullable: true, description: 服务端作业时区的本地日期时间}
|
||||
status: {type: string}
|
||||
relationship: {type: string, enum: [PLANNED, DEPARTED]}
|
||||
Change:
|
||||
type: object
|
||||
required: [tripNo, recordVersion, occurredAt, relationship, dispatchNos]
|
||||
properties:
|
||||
tripNo: {type: string}
|
||||
recordVersion: {type: string, pattern: '^[0-9]+$', description: 车次版本字符串,与车次号组成事件标识}
|
||||
occurredAt: {type: string, format: date-time}
|
||||
reason: {type: string, nullable: true}
|
||||
previousVehiclePlate: {type: string, nullable: true}
|
||||
vehiclePlate: {type: string, nullable: true}
|
||||
previousDriverName: {type: string, nullable: true}
|
||||
driverName: {type: string, nullable: true}
|
||||
relationship: {type: string, enum: [REPLACED_OUT, REPLACED_IN, DRIVER_CHANGED]}
|
||||
dispatchNos: {type: array, items: {type: string}}
|
||||
Reference in New Issue
Block a user