[Scheduler] Implemented inspection, gps, odometer, safety-event
This commit is contained in:
parent
dc10f8b1d7
commit
e1d17b7af7
|
|
@ -1,7 +1,8 @@
|
||||||
package com.goi.integration.samsara.client;
|
package com.goi.integration.samsara.client;
|
||||||
|
|
||||||
import com.goi.integration.common.dto.ExtIngestResult;
|
import com.goi.integration.common.dto.ExtIngestResult;
|
||||||
import com.goi.integration.samsara.dto.ExtInspectionIngestCommand;
|
import com.goi.integration.samsara.dto.VehicleInspectionIngestCommand;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatOdometerCommand;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -27,11 +28,7 @@ public class OprIngestClient {
|
||||||
// @Autowired
|
// @Autowired
|
||||||
// private ObjectMapper objectMapper;
|
// private ObjectMapper objectMapper;
|
||||||
|
|
||||||
public ExtIngestResult ingestInspection(ExtInspectionIngestCommand command) {
|
public ExtIngestResult ingestInspection(VehicleInspectionIngestCommand command) {
|
||||||
// String json = objectMapper
|
|
||||||
// .writerWithDefaultPrettyPrinter()
|
|
||||||
// .writeValueAsString(command);
|
|
||||||
//
|
|
||||||
log.info(
|
log.info(
|
||||||
"[OPR_INGEST][REQUEST] source={}, type={}, fetchedAt={}",
|
"[OPR_INGEST][REQUEST] source={}, type={}, fetchedAt={}",
|
||||||
command.getSource(),
|
command.getSource(),
|
||||||
|
|
@ -39,19 +36,6 @@ public class OprIngestClient {
|
||||||
command.getFetchedAt()
|
command.getFetchedAt()
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
|
||||||
try {
|
|
||||||
log.debug(
|
|
||||||
"[OPR_INGEST][REQUEST_BODY]\n{}",
|
|
||||||
new com.fasterxml.jackson.databind.ObjectMapper()
|
|
||||||
.writerWithDefaultPrettyPrinter()
|
|
||||||
.writeValueAsString(command)
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("[OPR_INGEST][REQUEST_BODY] failed to serialize", e);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ExtIngestResult result = oprWebClient.post()
|
ExtIngestResult result = oprWebClient.post()
|
||||||
.uri("/ext/samsara/inspections/ingest")
|
.uri("/ext/samsara/inspections/ingest")
|
||||||
|
|
@ -97,4 +81,63 @@ public class OprIngestClient {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* Odometer ingest
|
||||||
|
* ========================= */
|
||||||
|
public ExtIngestResult ingestVehicleOdometer(
|
||||||
|
VehicleStatOdometerCommand command
|
||||||
|
) {
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[OPR_INGEST][ODOMETER][REQUEST] source={}, records={}, fetchedAt={}",
|
||||||
|
command.getSource(),
|
||||||
|
command.getRecords() != null ? command.getRecords().size() : 0,
|
||||||
|
command.getFetchedAt()
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ExtIngestResult result = oprWebClient.post()
|
||||||
|
.uri("/ext/samsara/odometer/ingest")
|
||||||
|
.header(INTERNAL_TOKEN_HEADER, token)
|
||||||
|
.bodyValue(command)
|
||||||
|
.retrieve()
|
||||||
|
.onStatus(
|
||||||
|
status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||||
|
resp -> resp.bodyToMono(String.class)
|
||||||
|
.map(body -> {
|
||||||
|
if (resp.statusCode().value() == 403) {
|
||||||
|
return new IllegalStateException(
|
||||||
|
"OPR_AUTH_FAILED: " + body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new RuntimeException(
|
||||||
|
"OPR_INGEST_HTTP_ERROR: " +
|
||||||
|
resp.statusCode() + " body=" + body
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.bodyToMono(ExtIngestResult.class)
|
||||||
|
.block();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[OPR_INGEST][ODOMETER][SUCCESS] received={}, inserted={}, updated={}, skipped={}",
|
||||||
|
result.getReceived(),
|
||||||
|
result.getInserted(),
|
||||||
|
result.getUpdated(),
|
||||||
|
result.getSkipped()
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(
|
||||||
|
"[OPR_INGEST][ODOMETER][FAILED] source={}, error={}",
|
||||||
|
command.getSource(),
|
||||||
|
e.getMessage(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,32 @@ public class SamsaraClient {
|
||||||
.queryParam("vehicleIds", vehicleIdsParam)
|
.queryParam("vehicleIds", vehicleIdsParam)
|
||||||
.queryParam("startTime", startTime.toString())
|
.queryParam("startTime", startTime.toString())
|
||||||
.queryParam("endTime", endTime.toString())
|
.queryParam("endTime", endTime.toString())
|
||||||
.queryParam("types", "obdOdometerMeters")
|
.queryParam("types", "obdOdometerMeters,gpsOdometerMeters")
|
||||||
|
.build())
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(String.class)
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vehicle odometer history (OBD odometer meters)
|
||||||
|
* Raw JSON 반환
|
||||||
|
*/
|
||||||
|
public String getVehicleEngineSeconds(
|
||||||
|
List<String> vehicleExternalIds,
|
||||||
|
Instant startTime,
|
||||||
|
Instant endTime
|
||||||
|
) {
|
||||||
|
|
||||||
|
String vehicleIdsParam = String.join(",", vehicleExternalIds);
|
||||||
|
|
||||||
|
return webClient.get()
|
||||||
|
.uri(uriBuilder -> uriBuilder
|
||||||
|
.path("/fleet/vehicles/stats/history")
|
||||||
|
.queryParam("vehicleIds", vehicleIdsParam)
|
||||||
|
.queryParam("startTime", startTime.toString())
|
||||||
|
.queryParam("endTime", endTime.toString())
|
||||||
|
.queryParam("types", "obdEngineSeconds")
|
||||||
.build())
|
.build())
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(String.class)
|
.bodyToMono(String.class)
|
||||||
|
|
@ -89,7 +114,7 @@ public class SamsaraClient {
|
||||||
* Raw JSON 반환
|
* Raw JSON 반환
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public String getVehicleSafetyEvents(Instant startTime, Instant endTime, List<String> vehicleExternalIds) {
|
public String getVehicleSafetyEvents(List<String> vehicleExternalIds, Instant startTime, Instant endTime) {
|
||||||
|
|
||||||
String vehicleIdsParam = String.join(",", vehicleExternalIds);
|
String vehicleIdsParam = String.join(",", vehicleExternalIds);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto;
|
import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto;
|
||||||
import com.goi.integration.samsara.dto.ScheduleWorkerResponseDto;
|
import com.goi.integration.samsara.dto.ScheduleWorkerResponseDto;
|
||||||
import com.goi.integration.samsara.service.InspectionIngestWorker;
|
import com.goi.integration.samsara.service.VehicleInspectionIngestWorker;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -20,7 +20,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class InspectionWorkerController {
|
public class InspectionWorkerController {
|
||||||
|
|
||||||
private final InspectionIngestWorker inspectionIngestWorker;
|
private final VehicleInspectionIngestWorker inspectionIngestWorker;
|
||||||
|
|
||||||
@PostMapping("/worker")
|
@PostMapping("/worker")
|
||||||
public ScheduleWorkerResponseDto runDvir(
|
public ScheduleWorkerResponseDto runDvir(
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package com.goi.integration.samsara.controller;
|
package com.goi.integration.samsara.controller;
|
||||||
|
|
||||||
import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto;
|
import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto;
|
||||||
import com.goi.integration.samsara.dto.VehicleGpsResponseDto;
|
import com.goi.integration.samsara.dto.VehicleGpsResponseDto;
|
||||||
import com.goi.integration.samsara.dto.VehicleOdometerCommand;
|
import com.goi.integration.samsara.dto.VehicleStatEngineSecondsCommand;
|
||||||
import com.goi.integration.samsara.service.VehicleOdometerHistoryService;
|
import com.goi.integration.samsara.dto.VehicleStatOdometerCommand;
|
||||||
import com.goi.integration.samsara.service.VehicleOdometerService;
|
import com.goi.integration.samsara.dto.VehicleStatSafetyEventCommand;
|
||||||
import com.goi.integration.samsara.service.VehicleGpsService;
|
import com.goi.integration.samsara.service.VehicleGpsService;
|
||||||
|
import com.goi.integration.samsara.service.VehicleStatEngineSecondsService;
|
||||||
|
import com.goi.integration.samsara.service.VehicleStatOdometerService;
|
||||||
|
import com.goi.integration.samsara.service.VehicleStatSafetyEventService;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
@ -20,8 +22,9 @@ import org.springframework.web.bind.annotation.*;
|
||||||
public class VehicleController {
|
public class VehicleController {
|
||||||
|
|
||||||
private final VehicleGpsService vehicleStatService;
|
private final VehicleGpsService vehicleStatService;
|
||||||
private final VehicleOdometerHistoryService vehicleOdometerHistoryService;
|
private final VehicleStatOdometerService statOdometerService;
|
||||||
private final VehicleOdometerService vehicleOdometerService;
|
private final VehicleStatEngineSecondsService statEngineSecondsService;
|
||||||
|
private final VehicleStatSafetyEventService statSafetyEventService;
|
||||||
|
|
||||||
@GetMapping("/stat/gps")
|
@GetMapping("/stat/gps")
|
||||||
public List<VehicleGpsResponseDto> getGps(
|
public List<VehicleGpsResponseDto> getGps(
|
||||||
|
|
@ -30,42 +33,34 @@ public class VehicleController {
|
||||||
return vehicleStatService.getVehicleGps(vehicleIds);
|
return vehicleStatService.getVehicleGps(vehicleIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vehicle odometer history summary (window-based)
|
* odometer fetch 후 리턴
|
||||||
*/
|
*/
|
||||||
@GetMapping("/stat/odometer/history")
|
@PostMapping("/stat/odometer/fetch")
|
||||||
public List<VehicleOdometerHistoryResponseDto> getOdometerHistory(
|
public VehicleStatOdometerCommand fetchStatOdometer(
|
||||||
@RequestParam List<String> vehicleIds,
|
@RequestBody ScheduleWorkerRequestDto request
|
||||||
@RequestParam Instant startTime,
|
|
||||||
@RequestParam Instant endTime
|
|
||||||
) {
|
) {
|
||||||
return vehicleOdometerHistoryService.getOdometerHistory(
|
return statOdometerService.fetchCommand(request);
|
||||||
vehicleIds, startTime, endTime
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vehicle odometer raw fetch (for opr-rest-api ingest)
|
* odometer fetch 후 리턴
|
||||||
*/
|
*/
|
||||||
@PostMapping("/odometer/fetch")
|
@PostMapping("/stat/engine-seconds/fetch")
|
||||||
public VehicleOdometerCommand fetchOdometer(
|
public VehicleStatEngineSecondsCommand fetchStatEngineSeconds(
|
||||||
@RequestParam List<String> vehicleIds,
|
@RequestBody ScheduleWorkerRequestDto request
|
||||||
@RequestParam Instant startTime,
|
|
||||||
@RequestParam Instant endTime
|
|
||||||
) {
|
) {
|
||||||
return vehicleOdometerService.fetchOdometerCommand(
|
return statEngineSecondsService.fetchCommand(request);
|
||||||
vehicleIds,
|
|
||||||
startTime,
|
|
||||||
endTime
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @GetMapping("/safety/events")
|
/**
|
||||||
// public List<VehicleSafetyEventResponseDto> getSafetyEvents(
|
* odometer fetch 후 리턴
|
||||||
// @RequestParam Instant startTime,
|
*/
|
||||||
// @RequestParam Instant endTime,
|
@PostMapping("/stat/safety-event/fetch")
|
||||||
// @RequestParam List<String> vehicleIds
|
public VehicleStatSafetyEventCommand fetchStatSafetyEvent(
|
||||||
// ) {
|
@RequestBody ScheduleWorkerRequestDto request
|
||||||
// return vehicleSafetyEventService.getSafetyEvents(vehicleIds, startTime, endTime);
|
) {
|
||||||
// }
|
return statSafetyEventService.fetchCommand(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|
@ -19,4 +20,6 @@ public class ScheduleWorkerRequestDto {
|
||||||
private LocalDateTime to;
|
private LocalDateTime to;
|
||||||
private Integer maxRecords;
|
private Integer maxRecords;
|
||||||
private Map<String, Map<String, String>> config;
|
private Map<String, Map<String, String>> config;
|
||||||
|
|
||||||
|
private List<String> vehicleExternalIds;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import java.util.List;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
public class VehicleOdometerCommand {
|
public class VehicleInspectionIngestCommand {
|
||||||
private String source;
|
private String source;
|
||||||
private LocalDateTime fetchedAt;
|
private LocalDateTime fetchedAt;
|
||||||
private List<VehicleOdometerRecordDto> records;
|
private List<VehicleInspectionRecordDto> records;
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
public class ExtInspectionRecordDto {
|
public class VehicleInspectionRecordDto {
|
||||||
private String externalId; // inspection id
|
private String externalId; // inspection id
|
||||||
private String vehicleExternalId; // vehicle.id
|
private String vehicleExternalId; // vehicle.id
|
||||||
private String driverExternalId; // signatoryUser.id
|
private String driverExternalId; // signatoryUser.id
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package com.goi.integration.samsara.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
|
||||||
public class VehicleOdometerHistoryResponseDto {
|
|
||||||
private String vohVehicleExternalId;
|
|
||||||
private Instant vohFirstSampleTime;
|
|
||||||
private Instant vohLastSampleTime;
|
|
||||||
private Long vohFirstOdometerMeters;
|
|
||||||
private Long vohLastOdometerMeters;
|
|
||||||
private Integer vohSampleCount;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.goi.integration.samsara.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class VehicleStatEngineSecondsCommand {
|
||||||
|
private String source;
|
||||||
|
private LocalDateTime fetchedAt;
|
||||||
|
private List<VehicleStatEngineSecondsRecordDto> records;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.goi.integration.samsara.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class VehicleStatEngineSecondsRecordDto {
|
||||||
|
private String vehicleExternalId;
|
||||||
|
private Instant sampleTime;
|
||||||
|
private Long engineSeconds;
|
||||||
|
private String payloadHash;
|
||||||
|
private JsonNode payloadJson;
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,8 @@ import java.util.List;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
public class ExtInspectionIngestCommand {
|
public class VehicleStatOdometerCommand {
|
||||||
private String source;
|
private String source;
|
||||||
private LocalDateTime fetchedAt;
|
private LocalDateTime fetchedAt;
|
||||||
private List<ExtInspectionRecordDto> records;
|
private List<VehicleStatOdometerRecordDto> records;
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.Instant;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
|
@ -13,10 +13,10 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
public class VehicleOdometerRecordDto {
|
public class VehicleStatOdometerRecordDto {
|
||||||
private String source; // SAMSARA
|
|
||||||
private String vehicleExternalId; // samsara vehicle.id
|
private String vehicleExternalId; // samsara vehicle.id
|
||||||
private LocalDateTime sampleTime; // snapshot time (UTC)
|
private Instant sampleTime; // snapshot time (UTC)
|
||||||
|
private String odometerType; // "OBD" | "GPS"
|
||||||
private Long odometerMeters; // cumulative meters
|
private Long odometerMeters; // cumulative meters
|
||||||
private String payloadHash; // idempotent hash
|
private String payloadHash; // idempotent hash
|
||||||
private JsonNode payloadJson; // minimal raw payload
|
private JsonNode payloadJson; // minimal raw payload
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.goi.integration.samsara.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class VehicleStatSafetyEventCommand {
|
||||||
|
private String source;
|
||||||
|
private LocalDateTime fetchedAt;
|
||||||
|
private List<VehicleStatSafetyEventRecordDto> records;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.goi.integration.samsara.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class VehicleStatSafetyEventRecordDto {
|
||||||
|
private String eventId;
|
||||||
|
private String vehicleId;
|
||||||
|
private String driverId;
|
||||||
|
private Instant eventTime;
|
||||||
|
private LocalDate eventDate;
|
||||||
|
private String coachingState;
|
||||||
|
private Double maxAccelerationG;
|
||||||
|
private Double latitude;
|
||||||
|
private Double longitude;
|
||||||
|
private String videoForwardUrl;
|
||||||
|
private String videoInwardUrl;
|
||||||
|
private JsonNode behaviorLabels;
|
||||||
|
private JsonNode rawPayload;
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@ import com.goi.integration.common.dto.ExtIngestResult;
|
||||||
import com.goi.integration.common.util.DateTimeUtil;
|
import com.goi.integration.common.util.DateTimeUtil;
|
||||||
import com.goi.integration.common.util.ExtPayloadHashUtil;
|
import com.goi.integration.common.util.ExtPayloadHashUtil;
|
||||||
import com.goi.integration.samsara.client.OprIngestClient;
|
import com.goi.integration.samsara.client.OprIngestClient;
|
||||||
import com.goi.integration.samsara.dto.ExtInspectionIngestCommand;
|
import com.goi.integration.samsara.dto.VehicleInspectionIngestCommand;
|
||||||
import com.goi.integration.samsara.dto.ExtInspectionRecordDto;
|
import com.goi.integration.samsara.dto.VehicleInspectionRecordDto;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -19,7 +19,7 @@ import java.util.List;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InspectionIngestService {
|
public class VehicleInspectionIngestService {
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final OprIngestClient oprIngestClient;
|
private final OprIngestClient oprIngestClient;
|
||||||
|
|
@ -44,7 +44,7 @@ public class InspectionIngestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
List<ExtInspectionRecordDto> records = new ArrayList<>();
|
List<VehicleInspectionRecordDto> records = new ArrayList<>();
|
||||||
log.info("inspection data size={}", data.size());
|
log.info("inspection data size={}", data.size());
|
||||||
|
|
||||||
// data[] 각 node 가 하나의 inspection (preTrip / postTrip)
|
// data[] 각 node 가 하나의 inspection (preTrip / postTrip)
|
||||||
|
|
@ -67,7 +67,7 @@ public class InspectionIngestService {
|
||||||
|
|
||||||
// record DTO
|
// record DTO
|
||||||
records.add(
|
records.add(
|
||||||
ExtInspectionRecordDto.builder()
|
VehicleInspectionRecordDto.builder()
|
||||||
.externalId(externalId)
|
.externalId(externalId)
|
||||||
.vehicleExternalId(vehicleExtId)
|
.vehicleExternalId(vehicleExtId)
|
||||||
.driverExternalId(driverExtId)
|
.driverExternalId(driverExtId)
|
||||||
|
|
@ -82,8 +82,8 @@ public class InspectionIngestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ingest command 생성
|
// ingest command 생성
|
||||||
ExtInspectionIngestCommand command =
|
VehicleInspectionIngestCommand command =
|
||||||
ExtInspectionIngestCommand.builder()
|
VehicleInspectionIngestCommand.builder()
|
||||||
.source("SAMSARA")
|
.source("SAMSARA")
|
||||||
.fetchedAt(LocalDateTime.now())
|
.fetchedAt(LocalDateTime.now())
|
||||||
.records(records)
|
.records(records)
|
||||||
|
|
@ -15,10 +15,10 @@ import java.time.ZoneOffset;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InspectionIngestWorker {
|
public class VehicleInspectionIngestWorker {
|
||||||
|
|
||||||
private final SamsaraClient samsaraClient;
|
private final SamsaraClient samsaraClient;
|
||||||
private final InspectionIngestService ingestService;
|
private final VehicleInspectionIngestService ingestService;
|
||||||
|
|
||||||
public ScheduleWorkerResponseDto execute(ScheduleWorkerRequestDto request) {
|
public ScheduleWorkerResponseDto execute(ScheduleWorkerRequestDto request) {
|
||||||
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
package com.goi.integration.samsara.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.goi.integration.samsara.client.SamsaraClient;
|
|
||||||
import com.goi.integration.samsara.dto.VehicleOdometerHistoryResponseDto;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class VehicleOdometerHistoryService {
|
|
||||||
|
|
||||||
private final SamsaraClient samsaraClient;
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
public List<VehicleOdometerHistoryResponseDto> getOdometerHistory(
|
|
||||||
List<String> vehicleExternalIds,
|
|
||||||
Instant startTime,
|
|
||||||
Instant endTime
|
|
||||||
) {
|
|
||||||
// api 호출
|
|
||||||
String rawJson = samsaraClient.getVehicleOdometerHistory(vehicleExternalIds, startTime, endTime);
|
|
||||||
|
|
||||||
return parseOdometerHistory(rawJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<VehicleOdometerHistoryResponseDto> parseOdometerHistory(String rawJson) {
|
|
||||||
|
|
||||||
List<VehicleOdometerHistoryResponseDto> results = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
JsonNode root = objectMapper.readTree(rawJson);
|
|
||||||
JsonNode dataArray = root.path("data");
|
|
||||||
|
|
||||||
for (JsonNode vehicleNode : dataArray) {
|
|
||||||
|
|
||||||
String vehicleId = vehicleNode.path("id").asText();
|
|
||||||
JsonNode odoArray = vehicleNode.path("obdOdometerMeters");
|
|
||||||
|
|
||||||
if (!odoArray.isArray() || odoArray.isEmpty()) {
|
|
||||||
// 데이터 없음
|
|
||||||
results.add(
|
|
||||||
VehicleOdometerHistoryResponseDto.builder()
|
|
||||||
.vohVehicleExternalId(vehicleId)
|
|
||||||
.vohSampleCount(0)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<JsonNode> it = odoArray.elements();
|
|
||||||
|
|
||||||
JsonNode first = it.next();
|
|
||||||
JsonNode last = first;
|
|
||||||
|
|
||||||
int count = 1;
|
|
||||||
|
|
||||||
while (it.hasNext()) {
|
|
||||||
last = it.next();
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 거리 계산은 opr 에서. 여기선 이전 누적치를 모름.
|
|
||||||
long firstValue = first.path("value").asLong();
|
|
||||||
long lastValue = last.path("value").asLong();
|
|
||||||
|
|
||||||
Instant firstTime = Instant.parse(first.path("time").asText());
|
|
||||||
Instant lastTime = Instant.parse(last.path("time").asText());
|
|
||||||
|
|
||||||
results.add(
|
|
||||||
VehicleOdometerHistoryResponseDto.builder()
|
|
||||||
.vohVehicleExternalId(vehicleId)
|
|
||||||
.vohFirstSampleTime(firstTime)
|
|
||||||
.vohLastSampleTime(lastTime)
|
|
||||||
.vohFirstOdometerMeters(firstValue)
|
|
||||||
.vohLastOdometerMeters(lastValue)
|
|
||||||
.vohSampleCount(count)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[ODOMETER][PARSE_FAIL]", e);
|
|
||||||
throw new RuntimeException("Failed to parse vehicle odometer history", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
package com.goi.integration.samsara.service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.goi.integration.common.util.DateTimeUtil;
|
|
||||||
import com.goi.integration.common.util.ExtPayloadHashUtil;
|
|
||||||
import com.goi.integration.samsara.client.SamsaraClient;
|
|
||||||
import com.goi.integration.samsara.dto.VehicleOdometerCommand;
|
|
||||||
import com.goi.integration.samsara.dto.VehicleOdometerRecordDto;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class VehicleOdometerService {
|
|
||||||
|
|
||||||
private final SamsaraClient samsaraClient;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch samsara odometer data and build command
|
|
||||||
*/
|
|
||||||
public VehicleOdometerCommand fetchOdometerCommand(
|
|
||||||
List<String> vehicleExternalIds,
|
|
||||||
Instant startTime,
|
|
||||||
Instant endTime
|
|
||||||
) {
|
|
||||||
|
|
||||||
String rawJson =
|
|
||||||
samsaraClient.getVehicleOdometerHistory(
|
|
||||||
vehicleExternalIds,
|
|
||||||
startTime,
|
|
||||||
endTime
|
|
||||||
);
|
|
||||||
|
|
||||||
List<VehicleOdometerRecordDto> records =
|
|
||||||
parseRawOdometer(rawJson);
|
|
||||||
|
|
||||||
return VehicleOdometerCommand.builder()
|
|
||||||
.source("SAMSARA")
|
|
||||||
.fetchedAt(LocalDateTime.now())
|
|
||||||
.records(records)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse samsara raw JSON → odometer snapshot records
|
|
||||||
*/
|
|
||||||
private List<VehicleOdometerRecordDto> parseRawOdometer(String rawJson) {
|
|
||||||
|
|
||||||
List<VehicleOdometerRecordDto> records = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
JsonNode root = objectMapper.readTree(rawJson);
|
|
||||||
JsonNode dataArray = root.path("data");
|
|
||||||
|
|
||||||
if (!dataArray.isArray()) {
|
|
||||||
log.warn("[ODOMETER] no data[] in samsara response");
|
|
||||||
return records;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (JsonNode vehicleNode : dataArray) {
|
|
||||||
|
|
||||||
String vehicleExtId =
|
|
||||||
vehicleNode.path("id").asText(null);
|
|
||||||
|
|
||||||
JsonNode odoArray =
|
|
||||||
vehicleNode.path("obdOdometerMeters");
|
|
||||||
|
|
||||||
if (!odoArray.isArray() || odoArray.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (JsonNode sample : odoArray) {
|
|
||||||
|
|
||||||
String timeStr =
|
|
||||||
sample.path("time").asText(null);
|
|
||||||
|
|
||||||
if (timeStr == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDateTime sampleTime =
|
|
||||||
DateTimeUtil.parseToUTC(timeStr);
|
|
||||||
|
|
||||||
long meters =
|
|
||||||
sample.path("value").asLong();
|
|
||||||
|
|
||||||
String hash =
|
|
||||||
ExtPayloadHashUtil.sha256FromStrings(
|
|
||||||
vehicleExtId,
|
|
||||||
timeStr,
|
|
||||||
String.valueOf(meters)
|
|
||||||
);
|
|
||||||
|
|
||||||
records.add(
|
|
||||||
VehicleOdometerRecordDto.builder()
|
|
||||||
.source("SAMSARA")
|
|
||||||
.vehicleExternalId(vehicleExtId)
|
|
||||||
.sampleTime(sampleTime)
|
|
||||||
.odometerMeters(meters)
|
|
||||||
.payloadHash(hash)
|
|
||||||
.payloadJson(sample)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[ODOMETER][PARSE_FAIL]", e);
|
|
||||||
throw new RuntimeException("Failed to parse samsara odometer payload", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return records;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
package com.goi.integration.samsara.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.goi.integration.common.util.ExtPayloadHashUtil;
|
||||||
|
import com.goi.integration.samsara.client.SamsaraClient;
|
||||||
|
import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatEngineSecondsCommand;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatEngineSecondsRecordDto;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Samsara API 호출
|
||||||
|
* - raw JSON 파싱
|
||||||
|
* - "사실 데이터"를 record 단위로 변환
|
||||||
|
*
|
||||||
|
* 판단 / 선택 / 병합 없음
|
||||||
|
* obdEngineSeconds 단일 타입
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class VehicleStatEngineSecondsService {
|
||||||
|
|
||||||
|
private final SamsaraClient samsaraClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller 진입점
|
||||||
|
*/
|
||||||
|
public VehicleStatEngineSecondsCommand fetchCommand(ScheduleWorkerRequestDto request) {
|
||||||
|
|
||||||
|
if (request.getVehicleExternalIds() == null || request.getVehicleExternalIds().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("vehicleExternalIds is required");
|
||||||
|
}
|
||||||
|
if (request.getFrom() == null || request.getTo() == null) {
|
||||||
|
throw new IllegalArgumentException("from/to is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant start = request.getFrom().toInstant(ZoneOffset.UTC);
|
||||||
|
Instant end = request.getTo().toInstant(ZoneOffset.UTC);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[{}][ENGINE_SECONDS][FETCH] start={} end={} vehicles={}",
|
||||||
|
request.getJobCode(), start, end, request.getVehicleExternalIds().size()
|
||||||
|
);
|
||||||
|
|
||||||
|
String rawJson = samsaraClient.getVehicleEngineSeconds(
|
||||||
|
request.getVehicleExternalIds(),
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
VehicleStatEngineSecondsCommand command = buildCommandFromRawJson(rawJson);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[{}][ENGINE_SECONDS][FETCH_OK] records={} fetchedAt={}",
|
||||||
|
request.getJobCode(),
|
||||||
|
command.getRecords().size(),
|
||||||
|
command.getFetchedAt()
|
||||||
|
);
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* raw JSON → records → command
|
||||||
|
*
|
||||||
|
* response sample:
|
||||||
|
* {
|
||||||
|
* "data": [
|
||||||
|
* {
|
||||||
|
* "id": "...",
|
||||||
|
* "obdEngineSeconds": [{"time":"...Z","value":123}, ...]
|
||||||
|
* }, ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private VehicleStatEngineSecondsCommand buildCommandFromRawJson(String rawJson) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(rawJson);
|
||||||
|
JsonNode dataArray = root.path("data");
|
||||||
|
|
||||||
|
if (!dataArray.isArray()) {
|
||||||
|
log.warn("[ENGINE_SECONDS] samsara response has no data[]");
|
||||||
|
return VehicleStatEngineSecondsCommand.builder()
|
||||||
|
.source("SAMSARA")
|
||||||
|
.fetchedAt(LocalDateTime.now())
|
||||||
|
.records(List.of())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<VehicleStatEngineSecondsRecordDto> records = new ArrayList<>();
|
||||||
|
|
||||||
|
for (JsonNode vehicleNode : dataArray) {
|
||||||
|
|
||||||
|
String vehicleExtId = vehicleNode.path("id").asText(null);
|
||||||
|
if (vehicleExtId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// obdEngineSeconds samples only
|
||||||
|
parseSamples(
|
||||||
|
records,
|
||||||
|
vehicleExtId,
|
||||||
|
vehicleNode.path("obdEngineSeconds")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return VehicleStatEngineSecondsCommand.builder()
|
||||||
|
.source("SAMSARA")
|
||||||
|
.fetchedAt(LocalDateTime.now())
|
||||||
|
.records(records)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[ENGINE_SECONDS][PARSE_FAIL]", e);
|
||||||
|
throw new RuntimeException("ENGINE_SECONDS_FETCH_PARSE_FAIL", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* samplesArray: [{"time": "...Z", "value": 123}, ...]
|
||||||
|
*/
|
||||||
|
private void parseSamples(
|
||||||
|
List<VehicleStatEngineSecondsRecordDto> records,
|
||||||
|
String vehicleExtId,
|
||||||
|
JsonNode arrayNode
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (arrayNode == null || !arrayNode.isArray() || arrayNode.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (JsonNode sample : arrayNode) {
|
||||||
|
|
||||||
|
String timeStr = sample.path("time").asText(null);
|
||||||
|
if (timeStr == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant sampleTime;
|
||||||
|
try {
|
||||||
|
sampleTime = Instant.parse(timeStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long engineSeconds = sample.path("value").asLong();
|
||||||
|
|
||||||
|
// 단일 타입이지만 해시 혹시 모를 확장을 위해 "obdEngineSeconds" 고정 문자열 포함 권장
|
||||||
|
String hash = ExtPayloadHashUtil.sha256FromStrings(
|
||||||
|
vehicleExtId,
|
||||||
|
timeStr,
|
||||||
|
"obdEngineSeconds",
|
||||||
|
String.valueOf(engineSeconds)
|
||||||
|
);
|
||||||
|
|
||||||
|
records.add(
|
||||||
|
VehicleStatEngineSecondsRecordDto.builder()
|
||||||
|
.vehicleExternalId(vehicleExtId)
|
||||||
|
.sampleTime(sampleTime)
|
||||||
|
.engineSeconds(engineSeconds)
|
||||||
|
.payloadHash(hash)
|
||||||
|
.payloadJson(sample) // 원본 sample 그대로
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
package com.goi.integration.samsara.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.goi.integration.common.util.ExtPayloadHashUtil;
|
||||||
|
import com.goi.integration.samsara.client.SamsaraClient;
|
||||||
|
import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatOdometerCommand;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatOdometerRecordDto;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* integration-service 책임:
|
||||||
|
* - Samsara API 호출
|
||||||
|
* - raw JSON 파싱
|
||||||
|
* - "사실 데이터"를 record 단위로 변환
|
||||||
|
*
|
||||||
|
* 판단 / 선택 / 병합 없음
|
||||||
|
* OBD / GPS 는 type 으로 구분된 독립 record
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class VehicleStatOdometerService {
|
||||||
|
|
||||||
|
private final SamsaraClient samsaraClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller 진입점
|
||||||
|
*/
|
||||||
|
public VehicleStatOdometerCommand fetchCommand(ScheduleWorkerRequestDto request) {
|
||||||
|
|
||||||
|
if (request.getVehicleExternalIds() == null || request.getVehicleExternalIds().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("vehicleExternalIds is required");
|
||||||
|
}
|
||||||
|
if (request.getFrom() == null || request.getTo() == null) {
|
||||||
|
throw new IllegalArgumentException("from/to is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant start = request.getFrom().toInstant(ZoneOffset.UTC);
|
||||||
|
Instant end = request.getTo().toInstant(ZoneOffset.UTC);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[{}][ODOMETER][FETCH] start={} end={} vehicles={}",
|
||||||
|
request.getJobCode(), start, end, request.getVehicleExternalIds().size()
|
||||||
|
);
|
||||||
|
|
||||||
|
String rawJson = samsaraClient.getVehicleOdometerHistory(
|
||||||
|
request.getVehicleExternalIds(),
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
VehicleStatOdometerCommand command = buildCommandFromRawJson(rawJson);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[{}][ODOMETER][FETCH_OK] records={} fetchedAt={}",
|
||||||
|
request.getJobCode(),
|
||||||
|
command.getRecords().size(),
|
||||||
|
command.getFetchedAt()
|
||||||
|
);
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* raw JSON → records → command
|
||||||
|
*
|
||||||
|
* - OBD / GPS 각각 독립 record
|
||||||
|
* - time 이 달라도 정상 처리
|
||||||
|
* - hash 에 type 포함
|
||||||
|
*/
|
||||||
|
private VehicleStatOdometerCommand buildCommandFromRawJson(String rawJson) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(rawJson);
|
||||||
|
JsonNode dataArray = root.path("data");
|
||||||
|
|
||||||
|
if (!dataArray.isArray()) {
|
||||||
|
log.warn("[ODOMETER] samsara response has no data[]");
|
||||||
|
return VehicleStatOdometerCommand.builder()
|
||||||
|
.source("SAMSARA")
|
||||||
|
.fetchedAt(LocalDateTime.now())
|
||||||
|
.records(List.of())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<VehicleStatOdometerRecordDto> records = new ArrayList<>();
|
||||||
|
|
||||||
|
for (JsonNode vehicleNode : dataArray) {
|
||||||
|
|
||||||
|
String vehicleExtId = vehicleNode.path("id").asText(null);
|
||||||
|
if (vehicleExtId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OBD samples
|
||||||
|
parseSamples(
|
||||||
|
records,
|
||||||
|
vehicleExtId,
|
||||||
|
vehicleNode.path("obdOdometerMeters"),
|
||||||
|
"OBD"
|
||||||
|
);
|
||||||
|
|
||||||
|
// GPS samples
|
||||||
|
parseSamples(
|
||||||
|
records,
|
||||||
|
vehicleExtId,
|
||||||
|
vehicleNode.path("gpsOdometerMeters"),
|
||||||
|
"GPS"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return VehicleStatOdometerCommand.builder()
|
||||||
|
.source("SAMSARA")
|
||||||
|
.fetchedAt(LocalDateTime.now())
|
||||||
|
.records(records)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[ODOMETER][PARSE_FAIL]", e);
|
||||||
|
throw new RuntimeException("ODOMETER_FETCH_PARSE_FAIL", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* samplesArray: [{"time": "...Z", "value": 123}, ...]
|
||||||
|
* type: "OBD" | "GPS"
|
||||||
|
*/
|
||||||
|
private void parseSamples(
|
||||||
|
List<VehicleStatOdometerRecordDto> records,
|
||||||
|
String vehicleExtId,
|
||||||
|
JsonNode arrayNode,
|
||||||
|
String type
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (arrayNode == null || !arrayNode.isArray() || arrayNode.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (JsonNode sample : arrayNode) {
|
||||||
|
|
||||||
|
String timeStr = sample.path("time").asText(null);
|
||||||
|
if (timeStr == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant sampleTime;
|
||||||
|
try {
|
||||||
|
sampleTime = Instant.parse(timeStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// invalid timestamp → skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long meters = sample.path("value").asLong();
|
||||||
|
|
||||||
|
// ✅ type 포함 hash (idempotent)
|
||||||
|
String hash = ExtPayloadHashUtil.sha256FromStrings(
|
||||||
|
vehicleExtId,
|
||||||
|
timeStr,
|
||||||
|
type,
|
||||||
|
String.valueOf(meters)
|
||||||
|
);
|
||||||
|
|
||||||
|
records.add(
|
||||||
|
VehicleStatOdometerRecordDto.builder()
|
||||||
|
.vehicleExternalId(vehicleExtId)
|
||||||
|
.sampleTime(sampleTime)
|
||||||
|
.odometerType(type) // "OBD" or "GPS"
|
||||||
|
.odometerMeters(meters)
|
||||||
|
.payloadHash(hash)
|
||||||
|
.payloadJson(sample) // 원본 sample 그대로
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
package com.goi.integration.samsara.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.goi.integration.samsara.client.SamsaraClient;
|
||||||
|
import com.goi.integration.samsara.dto.ScheduleWorkerRequestDto;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatSafetyEventCommand;
|
||||||
|
import com.goi.integration.samsara.dto.VehicleStatSafetyEventRecordDto;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Samsara /fleet/safety-events API 호출
|
||||||
|
* - raw JSON 파싱
|
||||||
|
* - safety event 단위 record 생성
|
||||||
|
*
|
||||||
|
* ❌ 판단 / 병합 / dispatch 매핑 없음
|
||||||
|
* ✅ 사실 데이터 그대로 전달
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class VehicleStatSafetyEventService {
|
||||||
|
|
||||||
|
private final SamsaraClient samsaraClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller 진입점
|
||||||
|
*/
|
||||||
|
public VehicleStatSafetyEventCommand fetchCommand(ScheduleWorkerRequestDto request) {
|
||||||
|
|
||||||
|
if (request.getVehicleExternalIds() == null || request.getVehicleExternalIds().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("vehicleExternalIds is required");
|
||||||
|
}
|
||||||
|
if (request.getFrom() == null || request.getTo() == null) {
|
||||||
|
throw new IllegalArgumentException("from/to is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant start = request.getFrom().toInstant(ZoneOffset.UTC);
|
||||||
|
Instant end = request.getTo().toInstant(ZoneOffset.UTC);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[{}][SAFETY_EVENT][FETCH] start={} end={} vehicles={}",
|
||||||
|
request.getJobCode(), start, end, request.getVehicleExternalIds().size()
|
||||||
|
);
|
||||||
|
|
||||||
|
String rawJson = samsaraClient.getVehicleSafetyEvents(
|
||||||
|
request.getVehicleExternalIds(),
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
VehicleStatSafetyEventCommand command = buildCommandFromRawJson(rawJson);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"[{}][SAFETY_EVENT][FETCH_OK] records={} fetchedAt={}",
|
||||||
|
request.getJobCode(),
|
||||||
|
command.getRecords().size(),
|
||||||
|
command.getFetchedAt()
|
||||||
|
);
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* raw JSON → records → command
|
||||||
|
*
|
||||||
|
* response sample:
|
||||||
|
* {
|
||||||
|
* "data": [
|
||||||
|
* {
|
||||||
|
* "id": "...",
|
||||||
|
* "vehicle": { "id": "..." },
|
||||||
|
* "driver": { "id": "..." },
|
||||||
|
* "time": "...Z",
|
||||||
|
* "location": { "latitude": 0, "longitude": 0 },
|
||||||
|
* "coachingState": "...",
|
||||||
|
* "maxAccelerationGForce": 0.58,
|
||||||
|
* "behaviorLabels": [...]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private VehicleStatSafetyEventCommand buildCommandFromRawJson(String rawJson) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(rawJson);
|
||||||
|
JsonNode dataArray = root.path("data");
|
||||||
|
|
||||||
|
if (!dataArray.isArray()) {
|
||||||
|
log.warn("[SAFETY_EVENT] samsara response has no data[]");
|
||||||
|
return VehicleStatSafetyEventCommand.builder()
|
||||||
|
.source("SAMSARA")
|
||||||
|
.fetchedAt(LocalDateTime.now())
|
||||||
|
.records(List.of())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<VehicleStatSafetyEventRecordDto> records = new ArrayList<>();
|
||||||
|
|
||||||
|
for (JsonNode eventNode : dataArray) {
|
||||||
|
|
||||||
|
String eventId = eventNode.path("id").asText(null);
|
||||||
|
if (eventId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode vehicleNode = eventNode.path("vehicle");
|
||||||
|
String vehicleId = vehicleNode.path("id").asText(null);
|
||||||
|
if (vehicleId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode driverNode = eventNode.path("driver");
|
||||||
|
String driverId = driverNode.isMissingNode()
|
||||||
|
? null
|
||||||
|
: driverNode.path("id").asText(null);
|
||||||
|
|
||||||
|
String timeStr = eventNode.path("time").asText(null);
|
||||||
|
if (timeStr == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant eventTime;
|
||||||
|
try {
|
||||||
|
eventTime = Instant.parse(timeStr);
|
||||||
|
} catch (Exception e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode locationNode = eventNode.path("location");
|
||||||
|
|
||||||
|
records.add(
|
||||||
|
VehicleStatSafetyEventRecordDto.builder()
|
||||||
|
.eventId(eventId)
|
||||||
|
.vehicleId(vehicleId)
|
||||||
|
.driverId(driverId)
|
||||||
|
.eventTime(eventTime)
|
||||||
|
.eventDate(eventTime.atZone(ZoneOffset.UTC).toLocalDate())
|
||||||
|
.coachingState(eventNode.path("coachingState").asText(null))
|
||||||
|
.maxAccelerationG(
|
||||||
|
eventNode.hasNonNull("maxAccelerationGForce")
|
||||||
|
? eventNode.get("maxAccelerationGForce").asDouble()
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
.latitude(
|
||||||
|
locationNode.hasNonNull("latitude")
|
||||||
|
? locationNode.get("latitude").asDouble()
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
.longitude(
|
||||||
|
locationNode.hasNonNull("longitude")
|
||||||
|
? locationNode.get("longitude").asDouble()
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
.videoForwardUrl(
|
||||||
|
eventNode.path("downloadForwardVideoUrl").asText(null)
|
||||||
|
)
|
||||||
|
.videoInwardUrl(
|
||||||
|
eventNode.path("downloadInwardVideoUrl").asText(null)
|
||||||
|
)
|
||||||
|
.behaviorLabels(eventNode.path("behaviorLabels"))
|
||||||
|
.rawPayload(eventNode)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return VehicleStatSafetyEventCommand.builder()
|
||||||
|
.source("SAMSARA")
|
||||||
|
.fetchedAt(LocalDateTime.now())
|
||||||
|
.records(records)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[SAFETY_EVENT][PARSE_FAIL]", e);
|
||||||
|
throw new RuntimeException("SAFETY_EVENT_FETCH_PARSE_FAIL", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue