feat(fleet): expose bounded origin-aware trip calendar
This commit is contained in:
+7
@@ -5,10 +5,17 @@ import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = "warehouse-service", contextId = "fleetWarehouseClient", url = "${baiyue.clients.domestic-warehouse.url:}")
|
||||
public interface FleetWarehouseClient {
|
||||
@PostMapping("/internal/warehouses/platform-metadata/batch")
|
||||
ApiResponse<MetadataResult> platformMetadata(@RequestHeader("X-Internal-Service-Token") String token, @RequestBody MetadataRequest request);
|
||||
record MetadataRequest(List<Long> warehouseIds) { }
|
||||
record Metadata(Long id, String warehouseCode, String warehouseName, String warehouseType, String countryCode, String timezone, String status) { }
|
||||
record MetadataResult(List<Metadata> records, List<Long> missingIds) { }
|
||||
@GetMapping("/internal/warehouses/order-catalog")
|
||||
ApiResponse<List<Warehouse>> activeWarehouses(@RequestHeader("X-Internal-Service-Token") String token);
|
||||
@GetMapping("/internal/warehouses/0/{warehouseId}/active")
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import com.baiyue.lastleg.fleet.driver.FleetWarehouseClient;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
|
||||
/** Read-only transport plans. A trip is never evidence of a receiving appointment confirmation. */
|
||||
@Service
|
||||
public class FleetTripCalendarService {
|
||||
public record Entry(String sourceType, String tripNo, String status, String originWarehouseId,
|
||||
String originWarehouseCode, String originWarehouseName, String originCountryCode,
|
||||
String originTimezone, String originWarehouseStatus, Instant plannedDepartureAt,
|
||||
Instant expectedDriverEndAt, Instant departedAt, Instant completedAt,
|
||||
List<FleetTripModels.Stop> stops, List<String> incompleteReasons) { }
|
||||
public record Result(List<Entry> records, List<Entry> unresolvedRecords) { }
|
||||
private static final Set<String> COUNTRIES = Set.of(Locale.getISOCountries());
|
||||
private final FleetTripMapper trips;
|
||||
private final FleetWarehouseClient warehouses;
|
||||
private final ObjectMapper json;
|
||||
private final String token;
|
||||
|
||||
public FleetTripCalendarService(FleetTripMapper trips, FleetWarehouseClient warehouses, ObjectMapper json,
|
||||
@Value("${baiyue.security.internal-service-token:}") String token) {
|
||||
this.trips=trips; this.warehouses=warehouses; this.json=json; this.token=token;
|
||||
}
|
||||
|
||||
@Transactional(readOnly=true)
|
||||
public Result calendar(Instant from, Instant to) {
|
||||
if (from==null || to==null || !to.isAfter(from) || Duration.between(from,to).compareTo(Duration.ofDays(44))>0)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"查询须为不超过44天的UTC时间范围");
|
||||
var rows=trips.calendar(LocalDateTime.ofInstant(from,ZoneOffset.UTC),LocalDateTime.ofInstant(to,ZoneOffset.UTC));
|
||||
if (rows.size()>2000) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"车次过多,请缩小查询日期范围");
|
||||
if (rows.isEmpty()) return new Result(List.of(),List.of());
|
||||
var ids=rows.stream().map(FleetTripEntity::getWarehouseId).filter(Objects::nonNull).filter(id->id>0).distinct().sorted().toList();
|
||||
var metadata=metadata(ids);
|
||||
var records=new ArrayList<Entry>(); var unresolved=new ArrayList<Entry>();
|
||||
for (var row:rows) {
|
||||
var warehouse=row.getWarehouseId()==null?null:metadata.get(row.getWarehouseId());
|
||||
var reasons=new ArrayList<String>();
|
||||
String country=warehouse==null?null:warehouse.countryCode();
|
||||
if (country!=null && COUNTRIES.contains(country) && !"US".equals(country)) continue;
|
||||
if (warehouse==null) reasons.add("ORIGIN_WAREHOUSE_UNKNOWN");
|
||||
if (country==null || !COUNTRIES.contains(country)) reasons.add("ORIGIN_COUNTRY_UNKNOWN");
|
||||
String zone=warehouse==null?null:warehouse.timezone();
|
||||
if (zone==null || !zone.contains("/") || zone.startsWith("Etc/") || zone.startsWith("SystemV/") || !ZoneId.getAvailableZoneIds().contains(zone)) {
|
||||
zone=null; reasons.add("ORIGIN_TIMEZONE_UNKNOWN");
|
||||
}
|
||||
boolean unresolvedOrigin=!reasons.isEmpty();
|
||||
List<FleetTripModels.Stop> stops;
|
||||
try {
|
||||
stops=json.readValue(row.getStopsJson(),new TypeReference<List<FleetTripModels.Stop>>() { });
|
||||
if (stops==null || stops.size()<2 || stops.stream().anyMatch(s->s==null || s.name()==null || s.name().isBlank()))
|
||||
throw new IllegalArgumentException("incomplete route");
|
||||
stops=List.copyOf(stops);
|
||||
} catch (Exception invalid) { stops=List.of(); reasons.add("ROUTE_INCOMPLETE"); }
|
||||
var entry=new Entry("TRIP_PLAN",row.getTripNo(),row.getStatus(),row.getWarehouseId()==null?null:row.getWarehouseId().toString(),
|
||||
warehouse==null?row.getWarehouseCode():warehouse.warehouseCode(),warehouse==null?row.getWarehouseName():warehouse.warehouseName(),
|
||||
country,zone,warehouse==null?null:warehouse.status(),instant(row.getPlannedDepartureAt()),instant(row.getExpectedDriverEndAt()),
|
||||
instant(row.getDepartedAt()),instant(row.getCompletedAt()),stops,List.copyOf(reasons));
|
||||
(unresolvedOrigin?unresolved:records).add(entry);
|
||||
}
|
||||
return new Result(List.copyOf(records),List.copyOf(unresolved));
|
||||
}
|
||||
|
||||
private Map<Long,FleetWarehouseClient.Metadata> metadata(List<Long> ids) {
|
||||
if (ids.isEmpty()) return Map.of();
|
||||
try {
|
||||
var response=warehouses.platformMetadata(token,new FleetWarehouseClient.MetadataRequest(ids));
|
||||
if (response==null || !response.successful() || response.data()==null || response.data().records()==null || response.data().missingIds()==null)
|
||||
throw unavailable();
|
||||
var result=new HashMap<Long,FleetWarehouseClient.Metadata>(); var requested=new HashSet<>(ids);
|
||||
for (var row:response.data().records()) {
|
||||
if (row==null || !requested.contains(row.id()) || result.putIfAbsent(row.id(),row)!=null) throw unavailable();
|
||||
}
|
||||
var missing=new HashSet<>(response.data().missingIds()); var expectedMissing=new HashSet<>(requested); expectedMissing.removeAll(result.keySet());
|
||||
if (missing.size()!=response.data().missingIds().size() || !missing.equals(expectedMissing)) throw unavailable();
|
||||
return result;
|
||||
} catch (RuntimeException failure) { throw unavailable(); }
|
||||
}
|
||||
private static Instant instant(LocalDateTime value) { return value==null?null:value.toInstant(ZoneOffset.UTC); }
|
||||
private static ResponseStatusException unavailable() { return new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"仓库时区资料暂不可用,请重试"); }
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface FleetTripMapper extends BaseMapper<FleetTripEntity> {
|
||||
@Select("SELECT trip_no,status,warehouse_id,warehouse_code,warehouse_name,planned_departure_at,expected_driver_end_at,departed_at,completed_at,stops_json FROM ll_fleet_trip WHERE planned_departure_at>=#{from} AND planned_departure_at<#{to} ORDER BY planned_departure_at,trip_no LIMIT 2001")
|
||||
java.util.List<FleetTripEntity> calendar(@Param("from") java.time.LocalDateTime from,@Param("to") java.time.LocalDateTime to);
|
||||
@Select("SELECT * FROM ll_fleet_trip WHERE trip_no=#{tripNo} FOR UPDATE")
|
||||
FleetTripEntity lockByNo(@Param("tripNo") String tripNo);
|
||||
@Select("SELECT * FROM ll_fleet_trip WHERE trip_no=#{tripNo}")
|
||||
|
||||
+10
@@ -17,6 +17,16 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
@Validated
|
||||
@RequestMapping("/api/platform/v1/last-leg/fleet/trips")
|
||||
public class PlatformFleetTripController {
|
||||
@jakarta.annotation.Resource private FleetTripCalendarService calendar;
|
||||
@GetMapping("/calendar")
|
||||
@PreAuthorize("hasAuthority('platform:fleet:trip:list')")
|
||||
public ApiResponse<FleetTripCalendarService.Result> calendar(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam java.time.Instant from,@RequestParam java.time.Instant to) {
|
||||
operator(user);
|
||||
if (!"PLATFORM".equals(user.appCode()) || !Long.valueOf(0).equals(user.tenantId()))
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,"仅平台主体可读取车次日历");
|
||||
return ApiResponse.success(calendar.calendar(from,to));
|
||||
}
|
||||
@jakarta.annotation.Resource private FleetTripPreparationService preparation;
|
||||
@PostMapping("/prepare")
|
||||
@PreAuthorize("hasAuthority('platform:fleet:trip:edit')")
|
||||
|
||||
Reference in New Issue
Block a user