[Scheduler] Implemented inspection, gps, odometer, safety-event
This commit is contained in:
parent
6a7882a028
commit
17164458a9
|
|
@ -0,0 +1,58 @@
|
|||
package com.goi.erp.client;
|
||||
|
||||
import com.goi.erp.dto.ExtSamsaraEngineSecondsFetchCommand;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SamsaraStatEngineSecondsClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public SamsaraStatEngineSecondsClient(
|
||||
WebClient.Builder webClientBuilder,
|
||||
@Value("${integration.api.base-url}") String integrationBaseUrl
|
||||
) {
|
||||
this.webClient = webClientBuilder
|
||||
.baseUrl(integrationBaseUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* integration-service 호출
|
||||
* POST /vehicle/odometer/fetch
|
||||
*/
|
||||
public ExtSamsaraEngineSecondsFetchCommand fetchEngineSeconds(
|
||||
ScheduleWorkerRequestDto request
|
||||
) {
|
||||
|
||||
try {
|
||||
return webClient.post()
|
||||
.uri("/vehicle/stat/engine-seconds/fetch")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
resp -> resp.bodyToMono(String.class)
|
||||
.map(body ->
|
||||
new RuntimeException(
|
||||
"INTEGRATION_ERROR: " + body
|
||||
)
|
||||
)
|
||||
)
|
||||
.bodyToMono(ExtSamsaraEngineSecondsFetchCommand.class)
|
||||
.block();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 이 메시지가 OPR 서비스 레벨까지 올라감
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.goi.erp.client;
|
||||
|
||||
import com.goi.erp.dto.ExtSamsaraOdometerFetchCommand;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SamsaraStatOdometerClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public SamsaraStatOdometerClient(
|
||||
WebClient.Builder webClientBuilder,
|
||||
@Value("${integration.api.base-url}") String integrationBaseUrl
|
||||
) {
|
||||
this.webClient = webClientBuilder
|
||||
.baseUrl(integrationBaseUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* integration-service 호출
|
||||
* POST /vehicle/odometer/fetch
|
||||
*/
|
||||
public ExtSamsaraOdometerFetchCommand fetchOdometer(
|
||||
ScheduleWorkerRequestDto request
|
||||
) {
|
||||
|
||||
try {
|
||||
return webClient.post()
|
||||
.uri("/vehicle/stat/odometer/fetch")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
resp -> resp.bodyToMono(String.class)
|
||||
.map(body ->
|
||||
new RuntimeException(
|
||||
"INTEGRATION_ERROR: " + body
|
||||
)
|
||||
)
|
||||
)
|
||||
.bodyToMono(ExtSamsaraOdometerFetchCommand.class)
|
||||
.block();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 이 메시지가 OPR 서비스 레벨까지 올라감
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.goi.erp.client;
|
||||
|
||||
import com.goi.erp.dto.ExtSamsaraSafetyEventFetchCommand;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SamsaraStatSafetyEventClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public SamsaraStatSafetyEventClient(
|
||||
WebClient.Builder webClientBuilder,
|
||||
@Value("${integration.api.base-url}") String integrationBaseUrl
|
||||
) {
|
||||
this.webClient = webClientBuilder
|
||||
.baseUrl(integrationBaseUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* integration-service 호출
|
||||
* POST /vehicle/safety-event/fetch
|
||||
*/
|
||||
public ExtSamsaraSafetyEventFetchCommand fetchSafetyEvent(
|
||||
ScheduleWorkerRequestDto request
|
||||
) {
|
||||
|
||||
try {
|
||||
return webClient.post()
|
||||
.uri("/vehicle/stat/safety-event/fetch")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
resp -> resp.bodyToMono(String.class)
|
||||
.map(body ->
|
||||
new RuntimeException(
|
||||
"INTEGRATION_ERROR: " + body
|
||||
)
|
||||
)
|
||||
)
|
||||
.bodyToMono(ExtSamsaraSafetyEventFetchCommand.class)
|
||||
.block();
|
||||
|
||||
} catch (Exception e) {
|
||||
// 이 메시지가 OPR 서비스 레벨까지 올라감
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package com.goi.erp.controller;
|
||||
|
||||
import com.goi.erp.dto.ProcessResultDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.service.ExtSamsaraInspectionProcessor;
|
||||
import com.goi.erp.service.SchedulerEngineSecondsProcessor;
|
||||
import com.goi.erp.service.SchedulerEngineSecondsRawOrchestrator;
|
||||
import com.goi.erp.service.SchedulerOdometerProcessor;
|
||||
import com.goi.erp.service.SchedulerOdometerRawOrchestrator;
|
||||
import com.goi.erp.service.SchedulerSafetyEventProcessor;
|
||||
import com.goi.erp.service.SchedulerSafetyEventRawOrchestrator;
|
||||
import com.goi.erp.service.VehicleDispatchAutoCloseService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/scheduler/worker")
|
||||
@RequiredArgsConstructor
|
||||
public class SchedulerWorkerController {
|
||||
|
||||
private final ExtSamsaraInspectionProcessor inspectionProcessor;
|
||||
private final VehicleDispatchAutoCloseService autoCloseService;
|
||||
private final SchedulerOdometerRawOrchestrator odometerOrchestrator;
|
||||
private final SchedulerOdometerProcessor odometerProcessor;
|
||||
private final SchedulerEngineSecondsRawOrchestrator enginesecondsOrchestrator;
|
||||
private final SchedulerEngineSecondsProcessor engineSecondsProcessor;
|
||||
private final SchedulerSafetyEventRawOrchestrator safetyEventOrchestrator;
|
||||
private final SchedulerSafetyEventProcessor safetyEventProcessor;
|
||||
|
||||
|
||||
/**
|
||||
* SAMSARA_INSPECTION_PROCESS
|
||||
*/
|
||||
@PostMapping("/samsara/inspection/process")
|
||||
public ScheduleWorkerResponseDto processSamsaraInspection(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
ProcessResultDto result = inspectionProcessor.processUnprocessed();
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(result.getProcessed() + result.getFailed())
|
||||
.successCount(result.getProcessed())
|
||||
.failCount(result.getFailed())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 배차 자동 종료 테스트 (오늘 날짜 기준)
|
||||
*/
|
||||
@PostMapping("/samsara/auto-close/process")
|
||||
public ScheduleWorkerResponseDto processAutoClose(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
ProcessResultDto result = autoCloseService.processAutoClose(request);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(result.getProcessed() + result.getFailed())
|
||||
.successCount(result.getProcessed())
|
||||
.failCount(result.getFailed())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 odometer raw 입력
|
||||
*/
|
||||
@PostMapping("/samsara/odometer/ingest")
|
||||
public ScheduleWorkerResponseDto ingestOdometer(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
return odometerOrchestrator.run(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 odometer process
|
||||
*/
|
||||
@PostMapping("/samsara/odometer/process")
|
||||
public ScheduleWorkerResponseDto processOdometer(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
return odometerProcessor.run(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 engine seconds raw 입력
|
||||
*/
|
||||
@PostMapping("/samsara/engine-seconds/ingest")
|
||||
public ScheduleWorkerResponseDto ingestEngineSeconds(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
return enginesecondsOrchestrator.run(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 odometer process
|
||||
*/
|
||||
@PostMapping("/samsara/engine-seconds/process")
|
||||
public ScheduleWorkerResponseDto processEngineSeconds(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
return engineSecondsProcessor.run(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 safety event raw 입력
|
||||
*/
|
||||
@PostMapping("/samsara/safety-event/ingest")
|
||||
public ScheduleWorkerResponseDto ingestSafetyEvent(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
return safetyEventOrchestrator.run(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 safety event process
|
||||
*/
|
||||
@PostMapping("/samsara/safety-event/process")
|
||||
public ScheduleWorkerResponseDto processSafetyEvent(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
return safetyEventProcessor.run(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 운행정보 입력 process
|
||||
*/
|
||||
@PostMapping("/samsara/stat/process")
|
||||
public ScheduleWorkerResponseDto processStat(
|
||||
@RequestBody ScheduleWorkerRequestDto request
|
||||
) {
|
||||
ProcessResultDto result = autoCloseService.processAutoClose(request);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(result.getProcessed() + result.getFailed())
|
||||
.successCount(result.getProcessed())
|
||||
.failCount(result.getFailed())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.goi.erp.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 ExtSamsaraEngineSecondsFetchCommand {
|
||||
private String source; // SAMSARA
|
||||
private LocalDateTime fetchedAt;
|
||||
private List<ExtSamsaraEngineSecondsRecordDto> records;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.goi.erp.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 ExtSamsaraEngineSecondsRecordDto {
|
||||
private String vehicleExternalId;
|
||||
private Instant sampleTime;
|
||||
private Long engineSeconds;
|
||||
private String payloadHash;
|
||||
private JsonNode payloadJson;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.goi.erp.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 ExtSamsaraOdometerFetchCommand {
|
||||
private String source;
|
||||
private LocalDateTime fetchedAt;
|
||||
private List<ExtSamsaraOdometerRecordDto> records;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.goi.erp.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 ExtSamsaraOdometerRecordDto {
|
||||
private String vehicleExternalId;
|
||||
private Instant sampleTime;
|
||||
private String odometerType;
|
||||
private Long odometerMeters;
|
||||
private String payloadHash;
|
||||
private JsonNode payloadJson;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.goi.erp.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 ExtSamsaraSafetyEventFetchCommand {
|
||||
private String source; // SAMSARA
|
||||
private LocalDateTime fetchedAt;
|
||||
private List<ExtSamsaraSafetyEventRecordDto> records;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.goi.erp.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 ExtSamsaraSafetyEventRecordDto {
|
||||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class ProcessResultDto {
|
||||
private int processed;
|
||||
private int failed;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ScheduleWorkerRequestDto {
|
||||
|
||||
private String jobCode;
|
||||
private LocalDateTime from;
|
||||
private LocalDateTime to;
|
||||
private Integer maxRecords;
|
||||
private Map<String, Map<String, String>> config;
|
||||
|
||||
private List<String> vehicleExternalIds;
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ScheduleWorkerResponseDto {
|
||||
|
||||
private boolean success;
|
||||
|
||||
private int processedCount;
|
||||
private int successCount;
|
||||
private int failCount;
|
||||
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 성공했지만 처리할 대상이 없음
|
||||
*/
|
||||
public static ScheduleWorkerResponseDto successEmpty() {
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(0)
|
||||
.successCount(0)
|
||||
.failCount(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 + 수치
|
||||
*/
|
||||
public static ScheduleWorkerResponseDto success(
|
||||
int processed,
|
||||
int successCount,
|
||||
int failCount
|
||||
) {
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(processed)
|
||||
.successCount(successCount)
|
||||
.failCount(failCount)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 응답
|
||||
*/
|
||||
public static ScheduleWorkerResponseDto failure(
|
||||
String errorCode,
|
||||
String errorMessage
|
||||
) {
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(false)
|
||||
.errorCode(errorCode)
|
||||
.errorMessage(errorMessage)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@Entity
|
||||
@Table(name = "ext_samsara_raw_engine_seconds")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ExtSamsaraRawEngineSeconds {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "esre_id")
|
||||
private Long esreId;
|
||||
|
||||
@Column(name = "esre_source", nullable = false, length = 20)
|
||||
private String esreSource;
|
||||
|
||||
@Column(name = "esre_vehicle_ext_id", nullable = false, length = 50)
|
||||
private String esreVehicleExtId;
|
||||
|
||||
@Column(name = "esre_sample_time", nullable = false)
|
||||
private Instant esreSampleTime;
|
||||
|
||||
@Column(name = "esre_engine_seconds", nullable = false)
|
||||
private Long esreEngineSeconds;
|
||||
|
||||
@Column(name = "esre_hash", nullable = false, length = 64)
|
||||
private String esreHash;
|
||||
|
||||
@Column(name = "esre_fetched_at", nullable = false)
|
||||
private LocalDateTime esreFetchedAt;
|
||||
|
||||
@Column(name = "esre_fetched_date", insertable = false, updatable = false)
|
||||
private LocalDate esreFetchedDate;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "esre_payload", columnDefinition = "jsonb", nullable = false)
|
||||
private JsonNode esrePayload;
|
||||
|
||||
@Column(name = "esre_processed")
|
||||
private Boolean esreProcessed;
|
||||
|
||||
@Column(name = "esre_processed_at")
|
||||
private LocalDateTime esreProcessedAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "ext_samsara_raw_odometer",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "uk_ext_samsara_raw_odometer_hash",
|
||||
columnNames = {"esro_hash"}
|
||||
)
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ExtSamsaraRawOdometer {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "esro_id")
|
||||
private Long esroId;
|
||||
|
||||
@Column(name = "esro_source", nullable = false, length = 20)
|
||||
private String esroSource;
|
||||
|
||||
@Column(name = "esro_vehicle_ext_id", nullable = false, length = 50)
|
||||
private String esroVehicleExtId;
|
||||
|
||||
@Column(name = "esro_sample_time", nullable = false)
|
||||
private Instant esroSampleTime;
|
||||
|
||||
@Column(name = "esro_odometer_type", nullable = false, length = 10)
|
||||
private String esroOdometerType;
|
||||
|
||||
@Column(name = "esro_odometer_meters")
|
||||
private Long esroOdometerMeters;
|
||||
|
||||
@Column(name = "esro_hash", nullable = false, length = 64)
|
||||
private String esroHash;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "esro_payload", columnDefinition = "jsonb", nullable = false)
|
||||
private JsonNode esroPayload;
|
||||
|
||||
@Column(name = "esro_fetched_at", nullable = false)
|
||||
private LocalDateTime esroFetchedAt;
|
||||
|
||||
@Column(
|
||||
name = "esro_fetched_date",
|
||||
insertable = false,
|
||||
updatable = false
|
||||
)
|
||||
private LocalDate esroFetchedDate;
|
||||
|
||||
@Column(name = "esro_processed")
|
||||
private Boolean esroProcessed;
|
||||
|
||||
@Column(name = "esro_processed_at")
|
||||
private LocalDateTime esroProcessedAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@Entity
|
||||
@Table(name = "ext_samsara_raw_safety_event")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ExtSamsaraRawSafetyEvent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "esrs_id")
|
||||
private Long esrsId;
|
||||
|
||||
@Column(name = "esrs_event_id", nullable = false, length = 100)
|
||||
private String esrsEventId;
|
||||
|
||||
@Column(name = "esrs_vehicle_id", nullable = false, length = 100)
|
||||
private String esrsVehicleId;
|
||||
|
||||
@Column(name = "esrs_driver_id", length = 100)
|
||||
private String esrsDriverId;
|
||||
|
||||
@Column(name = "esrs_event_time", nullable = false)
|
||||
private Instant esrsEventTime;
|
||||
|
||||
@Column(name = "esrs_event_date", nullable = false)
|
||||
private LocalDate esrsEventDate;
|
||||
|
||||
@Column(name = "esrs_coaching_state", length = 30)
|
||||
private String esrsCoachingState;
|
||||
|
||||
@Column(name = "esrs_max_accel_g", precision = 9, scale = 6)
|
||||
private BigDecimal esrsMaxAccelG;
|
||||
|
||||
@Column(name = "esrs_latitude", precision = 9, scale = 6)
|
||||
private BigDecimal esrsLatitude;
|
||||
|
||||
@Column(name = "esrs_longitude", precision = 9, scale = 6)
|
||||
private BigDecimal esrsLongitude;
|
||||
|
||||
@Column(name = "esrs_video_forward_url")
|
||||
private String esrsVideoForwardUrl;
|
||||
|
||||
@Column(name = "esrs_video_inward_url")
|
||||
private String esrsVideoInwardUrl;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "esrs_behavior_labels", columnDefinition = "jsonb")
|
||||
private JsonNode esrsBehaviorLabels;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "esrs_raw_payload", columnDefinition = "jsonb", nullable = false)
|
||||
private JsonNode esrsRawPayload;
|
||||
|
||||
@Column(name = "esrs_processed")
|
||||
private Boolean esrsProcessed;
|
||||
|
||||
@Column(name = "esrs_processed_at")
|
||||
private LocalDateTime esrsProcessedAt;
|
||||
|
||||
@Column(name = "esrs_vehicle_safety_event_id")
|
||||
private Long esrsVehicleSafetyEventId;
|
||||
|
||||
@Column(name = "esrs_collected_at")
|
||||
private LocalDateTime esrsCollectedAt;
|
||||
|
||||
@Column(name = "esrs_created_at")
|
||||
private LocalDateTime esrsCreatedAt;
|
||||
|
||||
@Column(name = "esrs_updated_at")
|
||||
private LocalDateTime esrsUpdatedAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "vehicle_dispatch_daily_stat",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "uk_vehicle_dispatch_daily_stat_dispatch_date",
|
||||
columnNames = {"vdds_dispatch_id", "vdds_date"}
|
||||
)
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class VehicleDispatchDailyStat {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "vdds_id")
|
||||
private Long vddsId;
|
||||
|
||||
@Column(name = "vdds_dispatch_id", nullable = false)
|
||||
private Long vddsDispatchId;
|
||||
|
||||
@Column(name = "vdds_date", nullable = false)
|
||||
private LocalDate vddsDate;
|
||||
|
||||
@Column(name = "vdds_odometer_total", precision = 12, scale = 2)
|
||||
private BigDecimal vddsOdometerTotal;
|
||||
|
||||
@Column(name = "vdds_odometer_increment", precision = 12, scale = 2)
|
||||
private BigDecimal vddsOdometerIncrement;
|
||||
|
||||
@Column(name = "vdds_odometer_source", length = 20)
|
||||
private String vddsOdometerSource;
|
||||
|
||||
@Column(name = "vdds_engine_seconds_total")
|
||||
private Integer vddsEngineSecondsTotal;
|
||||
|
||||
@Column(name = "vdds_engine_seconds_increment")
|
||||
private Integer vddsEngineSecondsIncrement;
|
||||
|
||||
@Column(name = "vdds_safety_event_count")
|
||||
private Integer vddsSafetyEventCount;
|
||||
|
||||
@Column(name = "vdds_collected_at")
|
||||
private LocalDateTime vddsCollectedAt;
|
||||
|
||||
@Column(name = "vdds_created_at")
|
||||
private LocalDateTime vddsCreatedAt;
|
||||
|
||||
@Column(name = "vdds_created_by", length = 50)
|
||||
private String vddsCreatedBy;
|
||||
|
||||
@Column(name = "vdds_updated_at")
|
||||
private LocalDateTime vddsUpdatedAt;
|
||||
|
||||
@Column(name = "vdds_updated_by", length = 50)
|
||||
private String vddsUpdatedBy;
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@Entity
|
||||
@Table(name = "vehicle_safety_event")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class VehicleSafetyEvent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "vse_id")
|
||||
private Long vseId;
|
||||
|
||||
@Column(name = "vse_source", nullable = false, length = 20)
|
||||
private String vseSource; // SAMSARA / MANUAL / ETC
|
||||
|
||||
@Column(name = "vse_behavior_summary", length = 200)
|
||||
private String vseBehaviorSummary;
|
||||
|
||||
@Column(name = "vse_dispatch_id", nullable = false)
|
||||
private Long vseDispatchId;
|
||||
|
||||
@Column(name = "vse_vehicle_id", nullable = false)
|
||||
private Long vseVehicleId;
|
||||
|
||||
@Column(name = "vse_driver_id")
|
||||
private Long vseDriverId;
|
||||
|
||||
@Column(name = "vse_event_time", nullable = false)
|
||||
private Instant vseEventTime;
|
||||
|
||||
@Column(name = "vse_event_date", nullable = false)
|
||||
private LocalDate vseEventDate;
|
||||
|
||||
@Column(name = "vse_coaching_state", length = 30)
|
||||
private String vseCoachingState;
|
||||
|
||||
@Column(name = "vse_max_accel_g", precision = 9, scale = 6)
|
||||
private BigDecimal vseMaxAccelG;
|
||||
|
||||
@Column(name = "vse_latitude", precision = 9, scale = 6)
|
||||
private BigDecimal vseLatitude;
|
||||
|
||||
@Column(name = "vse_longitude", precision = 9, scale = 6)
|
||||
private BigDecimal vseLongitude;
|
||||
|
||||
@Column(name = "vse_video_forward_url")
|
||||
private String vseVideoForwardUrl;
|
||||
|
||||
@Column(name = "vse_video_inward_url")
|
||||
private String vseVideoInwardUrl;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "vse_behavior_labels", columnDefinition = "jsonb")
|
||||
private JsonNode vseBehaviorLabels;
|
||||
|
||||
@Column(name = "vse_created_at")
|
||||
private LocalDateTime vseCreatedAt;
|
||||
|
||||
@Column(name = "vse_created_by", length = 50)
|
||||
private String vseCreatedBy;
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.ExtSamsaraRawEngineSeconds;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ExtSamsaraRawEngineSecondsRepository
|
||||
extends JpaRepository<ExtSamsaraRawEngineSeconds, Long> {
|
||||
|
||||
/**
|
||||
* raw engine seconds insert (중복 hash 무시)
|
||||
*/
|
||||
@Transactional
|
||||
@Modifying
|
||||
@Query(
|
||||
value = """
|
||||
INSERT INTO ext_samsara_raw_engine_seconds (
|
||||
esre_source,
|
||||
esre_vehicle_ext_id,
|
||||
esre_sample_time,
|
||||
esre_engine_seconds,
|
||||
esre_hash,
|
||||
esre_fetched_at,
|
||||
esre_payload,
|
||||
esre_processed
|
||||
) VALUES (
|
||||
:source,
|
||||
:vehicleExtId,
|
||||
:sampleTime,
|
||||
:engineSeconds,
|
||||
:hash,
|
||||
:fetchedAt,
|
||||
CAST(:payload AS jsonb),
|
||||
FALSE
|
||||
)
|
||||
ON CONFLICT (esre_hash) DO NOTHING
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
int insertIgnore(
|
||||
@Param("source") String source,
|
||||
@Param("vehicleExtId") String vehicleExtId,
|
||||
@Param("sampleTime") Instant sampleTime,
|
||||
@Param("engineSeconds") Long engineSeconds,
|
||||
@Param("hash") String hash,
|
||||
@Param("fetchedAt") LocalDateTime fetchedAt,
|
||||
@Param("payload") String payload
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 vehicle + 날짜 기준, 아직 처리되지 않은 raw engine seconds
|
||||
* daily stat process 용
|
||||
*/
|
||||
List<ExtSamsaraRawEngineSeconds>
|
||||
findByEsreVehicleExtIdAndEsreFetchedDateAndEsreProcessedFalse(
|
||||
String esreVehicleExtId,
|
||||
LocalDate esreFetchedDate
|
||||
);
|
||||
|
||||
/**
|
||||
* 날짜 기준, 아직 처리되지 않은 vehicleExternalIds
|
||||
*/
|
||||
@Query("""
|
||||
select distinct r.esreVehicleExtId
|
||||
from ExtSamsaraRawEngineSeconds r
|
||||
where r.esreFetchedDate = :date
|
||||
and r.esreProcessed = false
|
||||
""")
|
||||
List<String> findDistinctVehicleExtIdByDateAndUnprocessed(
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
/**
|
||||
* 아직 처리되지 않은 raw engine seconds (날짜 기준)
|
||||
* stat process scheduler 용
|
||||
*/
|
||||
List<ExtSamsaraRawEngineSeconds>
|
||||
findByEsreProcessedFalseAndEsreFetchedDate(
|
||||
LocalDate esreFetchedDate
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 차량의 engine seconds snapshot 이력 (디버깅/분석용)
|
||||
*/
|
||||
List<ExtSamsaraRawEngineSeconds>
|
||||
findByEsreVehicleExtIdOrderByEsreSampleTimeAsc(
|
||||
String esreVehicleExtId
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 차량의 가장 최근 engine seconds snapshot
|
||||
* (리셋 감지 / 기준값 확인용)
|
||||
*/
|
||||
Optional<ExtSamsaraRawEngineSeconds>
|
||||
findTopByEsreVehicleExtIdOrderByEsreSampleTimeDesc(
|
||||
String esreVehicleExtId
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.ExtSamsaraRawOdometer;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ExtSamsaraRawOdometerRepository
|
||||
extends JpaRepository<ExtSamsaraRawOdometer, Long> {
|
||||
|
||||
/* =====================================================
|
||||
* INGEST (idempotent insert)
|
||||
* ===================================================== */
|
||||
@Transactional
|
||||
@Modifying
|
||||
@Query(
|
||||
value = """
|
||||
INSERT INTO ext_samsara_raw_odometer (
|
||||
esro_source,
|
||||
esro_vehicle_ext_id,
|
||||
esro_sample_time,
|
||||
esro_odometer_type,
|
||||
esro_odometer_meters,
|
||||
esro_hash,
|
||||
esro_fetched_at,
|
||||
esro_payload,
|
||||
esro_processed
|
||||
) VALUES (
|
||||
:source,
|
||||
:vehicleExtId,
|
||||
:sampleTime,
|
||||
:odometerType,
|
||||
:odometerMeters,
|
||||
:hash,
|
||||
:fetchedAt,
|
||||
CAST(:payload AS jsonb),
|
||||
FALSE
|
||||
)
|
||||
ON CONFLICT (esro_hash) DO NOTHING
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
int insertIgnore(
|
||||
@Param("source") String source,
|
||||
@Param("vehicleExtId") String vehicleExtId,
|
||||
@Param("sampleTime") Instant sampleTime,
|
||||
@Param("odometerType") String odometerType,
|
||||
@Param("odometerMeters") Long odometerMeters,
|
||||
@Param("hash") String hash,
|
||||
@Param("fetchedAt") LocalDateTime fetchedAt,
|
||||
@Param("payload") String payload
|
||||
);
|
||||
|
||||
/* =====================================================
|
||||
* PROCESS (stat scheduler)
|
||||
* ===================================================== */
|
||||
|
||||
/**
|
||||
* 날짜 기준, 아직 처리되지 않은 raw가 존재하는 vehicleExternalId 목록
|
||||
*/
|
||||
@Query("""
|
||||
SELECT DISTINCT r.esroVehicleExtId
|
||||
FROM ExtSamsaraRawOdometer r
|
||||
WHERE r.esroProcessed = false
|
||||
AND r.esroFetchedDate = :date
|
||||
""")
|
||||
List<String> findDistinctVehicleExtIdByDateAndUnprocessed(
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 날짜 + vehicleExternalIds 기준 미처리 raw 조회
|
||||
* (dispatch 단위 grouping은 service 레벨에서 수행)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT r
|
||||
FROM ExtSamsaraRawOdometer r
|
||||
WHERE r.esroProcessed = false
|
||||
AND r.esroFetchedDate = :date
|
||||
AND r.esroVehicleExtId IN :vehicleExtIds
|
||||
""")
|
||||
List<ExtSamsaraRawOdometer> findUnprocessedByVehicleExtIdsAndDate(
|
||||
@Param("vehicleExtIds") List<String> vehicleExtIds,
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
/* =====================================================
|
||||
* DEBUG / ANALYSIS
|
||||
* ===================================================== */
|
||||
|
||||
List<ExtSamsaraRawOdometer>
|
||||
findByEsroVehicleExtIdOrderByEsroSampleTimeAsc(
|
||||
String esroVehicleExtId
|
||||
);
|
||||
|
||||
Optional<ExtSamsaraRawOdometer>
|
||||
findTopByEsroVehicleExtIdOrderByEsroSampleTimeDesc(
|
||||
String esroVehicleExtId
|
||||
);
|
||||
|
||||
@Query("""
|
||||
SELECT r
|
||||
FROM ExtSamsaraRawOdometer r
|
||||
WHERE r.esroVehicleExtId = :vehicleExtId
|
||||
AND r.esroProcessed = false
|
||||
AND r.esroFetchedDate = :date
|
||||
""")
|
||||
List<ExtSamsaraRawOdometer> findByVehicleExtIdAndDateAndUnprocessed(
|
||||
@Param("vehicleExtId") String vehicleExtId,
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.ExtSamsaraRawSafetyEvent;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ExtSamsaraRawSafetyEventRepository
|
||||
extends JpaRepository<ExtSamsaraRawSafetyEvent, Long> {
|
||||
|
||||
/**
|
||||
* raw safety event insert (중복 event_id 무시)
|
||||
*/
|
||||
@Transactional
|
||||
@Modifying
|
||||
@Query(
|
||||
value = """
|
||||
INSERT INTO ext_samsara_raw_safety_event (
|
||||
esrs_event_id,
|
||||
esrs_vehicle_id,
|
||||
esrs_driver_id,
|
||||
esrs_event_time,
|
||||
esrs_event_date,
|
||||
esrs_coaching_state,
|
||||
esrs_max_accel_g,
|
||||
esrs_latitude,
|
||||
esrs_longitude,
|
||||
esrs_video_forward_url,
|
||||
esrs_video_inward_url,
|
||||
esrs_behavior_labels,
|
||||
esrs_raw_payload,
|
||||
esrs_processed,
|
||||
esrs_collected_at
|
||||
) VALUES (
|
||||
:eventId,
|
||||
:vehicleId,
|
||||
:driverId,
|
||||
:eventTime,
|
||||
:eventDate,
|
||||
:coachingState,
|
||||
:maxAccelG,
|
||||
:latitude,
|
||||
:longitude,
|
||||
:videoForwardUrl,
|
||||
:videoInwardUrl,
|
||||
CAST(:behaviorLabels AS jsonb),
|
||||
CAST(:rawPayload AS jsonb),
|
||||
FALSE,
|
||||
:collectedAt
|
||||
)
|
||||
ON CONFLICT (esrs_event_id) DO NOTHING
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
int insertIgnore(
|
||||
@Param("eventId") String eventId,
|
||||
@Param("vehicleId") String vehicleId,
|
||||
@Param("driverId") String driverId,
|
||||
@Param("eventTime") Instant eventTime,
|
||||
@Param("eventDate") LocalDate eventDate,
|
||||
@Param("coachingState") String coachingState,
|
||||
@Param("maxAccelG") Double maxAccelG,
|
||||
@Param("latitude") Double latitude,
|
||||
@Param("longitude") Double longitude,
|
||||
@Param("videoForwardUrl") String videoForwardUrl,
|
||||
@Param("videoInwardUrl") String videoInwardUrl,
|
||||
@Param("behaviorLabels") String behaviorLabels,
|
||||
@Param("rawPayload") String rawPayload,
|
||||
@Param("collectedAt") LocalDateTime collectedAt
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 vehicle + 날짜 기준, 아직 처리되지 않은 safety events
|
||||
* daily stat / process 용
|
||||
*/
|
||||
List<ExtSamsaraRawSafetyEvent>
|
||||
findByEsrsVehicleIdAndEsrsEventDateAndEsrsProcessedFalse(
|
||||
String esrsVehicleId,
|
||||
LocalDate esrsEventDate
|
||||
);
|
||||
|
||||
/**
|
||||
* 날짜 기준, 아직 처리되지 않은 vehicleIds
|
||||
*/
|
||||
@Query("""
|
||||
select distinct r.esrsVehicleId
|
||||
from ExtSamsaraRawSafetyEvent r
|
||||
where r.esrsEventDate = :date
|
||||
and r.esrsProcessed = false
|
||||
""")
|
||||
List<String> findDistinctVehicleIdByDateAndUnprocessed(
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
/**
|
||||
* 날짜 기준, 아직 처리되지 않은 safety events
|
||||
* process scheduler 용
|
||||
*/
|
||||
List<ExtSamsaraRawSafetyEvent>
|
||||
findByEsrsProcessedFalseAndEsrsEventDate(
|
||||
LocalDate esrsEventDate
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 차량의 가장 최근 safety event
|
||||
*/
|
||||
Optional<ExtSamsaraRawSafetyEvent>
|
||||
findTopByEsrsVehicleIdOrderByEsrsEventTimeDesc(
|
||||
String esrsVehicleId
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.VehicleDispatchDailyStat;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface VehicleDispatchDailyStatRepository
|
||||
extends JpaRepository<VehicleDispatchDailyStat, Long> {
|
||||
|
||||
Optional<VehicleDispatchDailyStat>
|
||||
findByVddsDispatchIdAndVddsDate(
|
||||
Long vddsDispatchId,
|
||||
LocalDate vddsDate
|
||||
);
|
||||
}
|
||||
|
|
@ -14,16 +14,32 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch, Long> {
|
||||
public interface VehicleDispatchRepository
|
||||
extends JpaRepository<VehicleDispatch, Long> {
|
||||
|
||||
/* =====================================================
|
||||
* BASIC
|
||||
* ===================================================== */
|
||||
List<VehicleDispatch> findByVedVehIdAndVedDispatchDate(
|
||||
Long vedVehId,
|
||||
LocalDate vedDispatchDate
|
||||
);
|
||||
|
||||
List<VehicleDispatch> findByVedDriverIdAndVedDispatchDate(
|
||||
Long vedDriverId,
|
||||
LocalDate vedDispatchDate
|
||||
);
|
||||
|
||||
List<VehicleDispatch> findByVedVehIdAndVedDispatchDate(Long vedVehId, LocalDate vedDispatchDate);
|
||||
List<VehicleDispatch> findByVedDriverIdAndVedDispatchDate(Long vedDriverId, LocalDate vedDispatchDate);
|
||||
List<VehicleDispatch> findByVedDispatchDate(LocalDate vedDispatchDate);
|
||||
|
||||
Page<VehicleDispatch> findAll(Pageable pageable);
|
||||
|
||||
Optional<VehicleDispatch> findByVedUuid(UUID vedUuid);
|
||||
|
||||
//
|
||||
/* =====================================================
|
||||
* STATUS BASED
|
||||
* ===================================================== */
|
||||
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
|
|
@ -31,41 +47,55 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
|||
AND d.vedDispatchDate = :dispatchDate
|
||||
AND d.vedStatus <> 'C'
|
||||
""")
|
||||
Optional<VehicleDispatch> findOpenDispatchByVehId(Long vehId, LocalDate dispatchDate);
|
||||
Optional<VehicleDispatch> findOpenDispatchByVehId(
|
||||
@Param("vehId") Long vehId,
|
||||
@Param("dispatchDate") LocalDate dispatchDate
|
||||
);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedDispatchDate = :dispatchDate
|
||||
AND d.vedStatus <> 'C'
|
||||
""")
|
||||
List<VehicleDispatch> findAllNotClosed(LocalDate dispatchDate);
|
||||
List<VehicleDispatch> findAllNotClosed(
|
||||
@Param("dispatchDate") LocalDate dispatchDate
|
||||
);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedStatus = 'P'
|
||||
AND d.vedPausedAt <= :threshold
|
||||
""")
|
||||
List<VehicleDispatch> findPausedBefore(LocalDateTime threshold);
|
||||
List<VehicleDispatch> findPausedBefore(
|
||||
@Param("threshold") LocalDateTime threshold
|
||||
);
|
||||
|
||||
/* =====================================================
|
||||
* SHIFT
|
||||
* ===================================================== */
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT COALESCE(MAX(d.vedShift), -1)
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedVehId = :vehId
|
||||
AND d.vedDispatchDate = :date
|
||||
""")
|
||||
Integer findMaxShift(Long vehId, LocalDate date);
|
||||
Integer findMaxShift(
|
||||
@Param("vehId") Long vehId,
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
//
|
||||
default Integer findNextShift(Long vehId, LocalDate date) {
|
||||
Integer max = findMaxShift(vehId, date);
|
||||
return max == null ? 0 : max + 1;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* REOPEN
|
||||
* ===================================================== */
|
||||
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
|
|
@ -80,11 +110,63 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
|||
AND x.vedDispatchDate = :dispatchDate
|
||||
AND x.vedStatus <> 'C'
|
||||
)
|
||||
|
||||
""")
|
||||
List<VehicleDispatch> findAutoClosedCandidatesForReopen(
|
||||
@Param("dispatchDate") LocalDate dispatchDate,
|
||||
@Param("closeReason") String closeReason,
|
||||
@Param("closedAfter") LocalDateTime closedAfter
|
||||
);
|
||||
|
||||
/* =====================================================
|
||||
* ODOMETER / STAT PROCESS SUPPORT
|
||||
* ===================================================== */
|
||||
|
||||
/**
|
||||
* vehicleId 목록 → dispatchId 목록 (stat processor 핵심)
|
||||
*/
|
||||
@Query("""
|
||||
SELECT DISTINCT d.vedId
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedVehId IN :vehicleIds
|
||||
AND d.vedDispatchDate = :date
|
||||
""")
|
||||
List<Long> findDispatchIdsByVehicleIds(
|
||||
@Param("vehicleIds") List<Long> vehicleIds,
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
/**
|
||||
* 단건: externalId → dispatchId
|
||||
* (디버깅 / 보조용, bulk 로직에는 사용 안 함)
|
||||
*/
|
||||
@Query(
|
||||
value = """
|
||||
SELECT vd.ved_id
|
||||
FROM vehicle_dispatch vd
|
||||
JOIN vehicle_external_map vex
|
||||
ON vex.vex_vehicle_id = vd.ved_veh_id
|
||||
WHERE vex.vex_solution_type = 'SAMSARA'
|
||||
AND vex.vex_external_id = :vehicleExtId
|
||||
AND vex.vex_status = 'A'
|
||||
AND vd.ved_dispatch_date = :dispatchDate
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
Optional<Long> findDispatchId(
|
||||
@Param("vehicleExtId") String vehicleExtId,
|
||||
@Param("dispatchDate") LocalDate dispatchDate
|
||||
);
|
||||
|
||||
@Query("""
|
||||
SELECT vex.vexExternalId
|
||||
FROM VehicleDispatch d
|
||||
JOIN VehicleExternalMap vex
|
||||
ON vex.vexVehicleId = d.vedVehId
|
||||
WHERE d.vedId = :dispatchId
|
||||
AND vex.vexSolutionType = 'SAMSARA'
|
||||
AND vex.vexStatus = 'A'
|
||||
""")
|
||||
Optional<String> findVehicleExternalIdByDispatchId(
|
||||
@Param("dispatchId") Long dispatchId
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.goi.erp.repository;
|
|||
|
||||
import com.goi.erp.entity.VehicleExternalMap;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -22,4 +23,16 @@ public interface VehicleExternalMapRepository extends JpaRepository<VehicleExter
|
|||
String vexStatus
|
||||
);
|
||||
|
||||
@Query("""
|
||||
SELECT m.vexVehicleId
|
||||
FROM VehicleExternalMap m
|
||||
WHERE m.vexSolutionType = :solutionType
|
||||
AND m.vexExternalId IN :externalIds
|
||||
AND m.vexStatus = 'A'
|
||||
""")
|
||||
List<Long> findVehicleIdsByExternalIds(
|
||||
String solutionType,
|
||||
List<String> externalIds
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.VehicleSafetyEvent;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface VehicleSafetyEventRepository
|
||||
extends JpaRepository<VehicleSafetyEvent, Long> {
|
||||
|
||||
/**
|
||||
* 특정 dispatch + 날짜 기준 safety event 목록
|
||||
* (daily stat 재계산 / 디버깅용)
|
||||
*/
|
||||
List<VehicleSafetyEvent> findByVseDispatchIdAndVseEventDate(Long vseDispatchId, LocalDate vseEventDate);
|
||||
|
||||
/**
|
||||
* 특정 dispatch 의 safety event 전체
|
||||
*/
|
||||
List<VehicleSafetyEvent> findByVseDispatchId(Long vseDispatchId);
|
||||
|
||||
/**
|
||||
* 특정 차량 + 날짜 기준 safety event
|
||||
* (dispatch 매핑 검증 / 분석용)
|
||||
*/
|
||||
List<VehicleSafetyEvent> findByVseVehicleIdAndVseEventDate(Long vseVehicleId, LocalDate vseEventDate);
|
||||
|
||||
/**
|
||||
* 특정 source 의 safety event
|
||||
* (SAMSARA / MANUAL 구분용)
|
||||
*/
|
||||
List<VehicleSafetyEvent> findByVseSource(String vseSource);
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goi.erp.dto.ExtSamsaraEngineSecondsFetchCommand;
|
||||
import com.goi.erp.dto.ExtSamsaraEngineSecondsRecordDto;
|
||||
import com.goi.erp.repository.ExtSamsaraRawEngineSecondsRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExtSamsaraEngineSecondsIngestService {
|
||||
|
||||
private final ExtSamsaraRawEngineSecondsRepository repository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Raw engine seconds ingest
|
||||
* - append-only
|
||||
* - idempotent by esre_hash
|
||||
* - obdEngineSeconds only
|
||||
*/
|
||||
@Transactional
|
||||
public int ingest(ExtSamsaraEngineSecondsFetchCommand command) {
|
||||
|
||||
int inserted = 0;
|
||||
|
||||
for (ExtSamsaraEngineSecondsRecordDto record : command.getRecords()) {
|
||||
|
||||
int result = repository.insertIgnore(
|
||||
command.getSource(), // SAMSARA
|
||||
record.getVehicleExternalId(), // samsara vehicle id
|
||||
record.getSampleTime(), // Instant
|
||||
record.getEngineSeconds(), // Long
|
||||
record.getPayloadHash(), // vehicle + time + value
|
||||
command.getFetchedAt(), // LocalDateTime
|
||||
toJson(record.getPayloadJson()) // ✅ payload 저장
|
||||
);
|
||||
|
||||
inserted += result; // 1 or 0
|
||||
}
|
||||
|
||||
return inserted;
|
||||
}
|
||||
|
||||
private String toJson(JsonNode node) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(node);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize payloadJson", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goi.erp.dto.ExtSamsaraOdometerFetchCommand;
|
||||
import com.goi.erp.dto.ExtSamsaraOdometerRecordDto;
|
||||
import com.goi.erp.repository.ExtSamsaraRawOdometerRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExtSamsaraOdometerIngestService {
|
||||
|
||||
private final ExtSamsaraRawOdometerRepository repository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Raw odometer ingest (A안)
|
||||
* - append-only
|
||||
* - idempotent by esro_hash
|
||||
*/
|
||||
@Transactional
|
||||
public int ingest(ExtSamsaraOdometerFetchCommand command) {
|
||||
|
||||
int inserted = 0;
|
||||
|
||||
for (ExtSamsaraOdometerRecordDto record : command.getRecords()) {
|
||||
|
||||
int result = repository.insertIgnore(
|
||||
command.getSource(),
|
||||
record.getVehicleExternalId(),
|
||||
record.getSampleTime(), // Instant
|
||||
record.getOdometerType(),
|
||||
record.getOdometerMeters(),
|
||||
record.getPayloadHash(),
|
||||
command.getFetchedAt(), // LocalDateTime
|
||||
toJson(record.getPayloadJson())
|
||||
);
|
||||
|
||||
inserted += result; // 1 or 0
|
||||
}
|
||||
|
||||
return inserted;
|
||||
}
|
||||
|
||||
private String toJson(JsonNode node) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(node);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize payloadJson", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goi.erp.dto.ExtSamsaraSafetyEventFetchCommand;
|
||||
import com.goi.erp.dto.ExtSamsaraSafetyEventRecordDto;
|
||||
import com.goi.erp.repository.ExtSamsaraRawSafetyEventRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExtSamsaraSafetyEventIngestService {
|
||||
|
||||
private final ExtSamsaraRawSafetyEventRepository repository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Raw safety event ingest
|
||||
*
|
||||
* rules:
|
||||
* - append-only
|
||||
* - idempotent by esrs_event_id
|
||||
* - samsara safety-events API
|
||||
*/
|
||||
@Transactional
|
||||
public int ingest(ExtSamsaraSafetyEventFetchCommand command) {
|
||||
|
||||
int inserted = 0;
|
||||
|
||||
for (ExtSamsaraSafetyEventRecordDto record : command.getRecords()) {
|
||||
|
||||
int result = repository.insertIgnore(
|
||||
record.getEventId(), // samsara event id
|
||||
record.getVehicleId(), // samsara vehicle id
|
||||
record.getDriverId(), // nullable
|
||||
record.getEventTime(), // Instant
|
||||
record.getEventDate(), // LocalDate
|
||||
record.getCoachingState(),
|
||||
record.getMaxAccelerationG(),
|
||||
record.getLatitude(),
|
||||
record.getLongitude(),
|
||||
record.getVideoForwardUrl(),
|
||||
record.getVideoInwardUrl(),
|
||||
toJson(record.getBehaviorLabels()), // jsonb
|
||||
toJson(record.getRawPayload()), // jsonb
|
||||
command.getFetchedAt() // LocalDateTime
|
||||
);
|
||||
|
||||
inserted += result; // 1 or 0
|
||||
}
|
||||
|
||||
return inserted;
|
||||
}
|
||||
|
||||
private String toJson(JsonNode node) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(node);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to serialize JsonNode", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.repository.ExtSamsaraRawEngineSecondsRepository;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
import com.goi.erp.repository.VehicleExternalMapRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SchedulerEngineSecondsProcessor {
|
||||
|
||||
private final ExtSamsaraRawEngineSecondsRepository rawRepository;
|
||||
private final VehicleExternalMapRepository vehicleExternalMapRepository;
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleDispatchDailyStatService dailyStatService;
|
||||
|
||||
public ScheduleWorkerResponseDto run(ScheduleWorkerRequestDto request) {
|
||||
|
||||
LocalDate statDate = request.getFrom().toLocalDate();
|
||||
|
||||
int processed = 0;
|
||||
int success = 0;
|
||||
int failed = 0;
|
||||
|
||||
/* =====================================================
|
||||
* 1. 해당 날짜에 unprocessed raw가 있는 vehicleExternalIds
|
||||
* ===================================================== */
|
||||
List<String> vehicleExtIds =
|
||||
rawRepository.findDistinctVehicleExtIdByDateAndUnprocessed(statDate);
|
||||
|
||||
if (vehicleExtIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_PROCESS] no raw to process date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 2. externalId → vehicleId
|
||||
* ===================================================== */
|
||||
List<Long> vehicleIds =
|
||||
vehicleExternalMapRepository.findVehicleIdsByExternalIds(
|
||||
"SAMSARA",
|
||||
vehicleExtIds
|
||||
);
|
||||
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_PROCESS] no vehicle mapping date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 3. vehicleId → dispatchId
|
||||
* ===================================================== */
|
||||
List<Long> dispatchIds =
|
||||
vehicleDispatchRepository.findDispatchIdsByVehicleIds(
|
||||
vehicleIds,
|
||||
statDate
|
||||
);
|
||||
|
||||
if (dispatchIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_PROCESS] no dispatch date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 4. dispatch 단위 stat 처리
|
||||
* ===================================================== */
|
||||
for (Long dispatchId : dispatchIds) {
|
||||
processed++;
|
||||
try {
|
||||
dailyStatService.processEngineSecondsDaily(dispatchId, statDate);
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
log.error(
|
||||
"[ENGINE_SECONDS_PROCESS_FAIL] dispatchId={} date={}",
|
||||
dispatchId,
|
||||
statDate,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(failed == 0)
|
||||
.processedCount(processed)
|
||||
.successCount(success)
|
||||
.failCount(failed)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.client.SamsaraStatEngineSecondsClient;
|
||||
import com.goi.erp.dto.ExtSamsaraEngineSecondsFetchCommand;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SchedulerEngineSecondsRawOrchestrator {
|
||||
|
||||
private final SamsaraStatEngineSecondsClient statEngineSecondsClient;
|
||||
private final ExtSamsaraEngineSecondsIngestService ingestService;
|
||||
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleExternalMapService vehicleExternalMapService;
|
||||
|
||||
public ScheduleWorkerResponseDto run(ScheduleWorkerRequestDto request) {
|
||||
|
||||
try {
|
||||
LocalDate dispatchDate = request.getFrom().toLocalDate();
|
||||
|
||||
// 1. 대상 dispatch 조회 (NOT CLOSED)
|
||||
List<VehicleDispatch> targets =
|
||||
vehicleDispatchRepository.findAllNotClosed(dispatchDate);
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_RAW] no dispatch to process");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 2. internal vehicleId 수집
|
||||
List<Long> vehicleIds = targets.stream()
|
||||
.map(VehicleDispatch::getVedVehId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_RAW] no vehicles to process");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 3. internal → external (Samsara) 매핑
|
||||
Map<Long, String> vehicleIdToExternalId =
|
||||
vehicleExternalMapService.findExternalIdsByVehicleIds(
|
||||
"SAMSARA",
|
||||
vehicleIds
|
||||
);
|
||||
|
||||
List<String> externalIds = vehicleIdToExternalId.values().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (externalIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_RAW] no external vehicle ids");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 4. request 에 externalIds 세팅
|
||||
request.setVehicleExternalIds(externalIds);
|
||||
|
||||
// 5. integration-service fetch (obdEngineSeconds)
|
||||
ExtSamsaraEngineSecondsFetchCommand fetchResult =
|
||||
statEngineSecondsClient.fetchEngineSeconds(request);
|
||||
|
||||
if (fetchResult == null || fetchResult.getRecords() == null) {
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 6. raw ingest
|
||||
int inserted = ingestService.ingest(fetchResult);
|
||||
|
||||
log.info(
|
||||
"[ENGINE_SECONDS_RAW][DONE] fetched={} inserted={}",
|
||||
fetchResult.getRecords().size(),
|
||||
inserted
|
||||
);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(fetchResult.getRecords().size())
|
||||
.successCount(inserted)
|
||||
.failCount(fetchResult.getRecords().size() - inserted)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[ENGINE_SECONDS_RAW][RUN_FAIL]", e);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(false)
|
||||
.errorCode("ENGINE_SECONDS_RAW_INGEST_ERROR")
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.repository.ExtSamsaraRawOdometerRepository;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
import com.goi.erp.repository.VehicleExternalMapRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SchedulerOdometerProcessor {
|
||||
|
||||
private final ExtSamsaraRawOdometerRepository rawRepository;
|
||||
private final VehicleExternalMapRepository vehicleExternalMapRepository;
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleDispatchDailyStatService dailyStatService;
|
||||
|
||||
public ScheduleWorkerResponseDto run(ScheduleWorkerRequestDto request) {
|
||||
|
||||
LocalDate statDate = request.getFrom().toLocalDate();
|
||||
|
||||
int processed = 0;
|
||||
int success = 0;
|
||||
int failed = 0;
|
||||
|
||||
/* =====================================================
|
||||
* 1. 해당 날짜에 unprocessed raw가 있는 vehicleExternalIds
|
||||
* ===================================================== */
|
||||
List<String> vehicleExtIds =
|
||||
rawRepository.findDistinctVehicleExtIdByDateAndUnprocessed(statDate);
|
||||
|
||||
if (vehicleExtIds.isEmpty()) {
|
||||
log.info("[ODOMETER_PROCESS] no raw to process date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 2. externalId → vehicleId
|
||||
* ===================================================== */
|
||||
List<Long> vehicleIds =
|
||||
vehicleExternalMapRepository.findVehicleIdsByExternalIds(
|
||||
"SAMSARA",
|
||||
vehicleExtIds
|
||||
);
|
||||
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("[ODOMETER_PROCESS] no vehicle mapping date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 3. vehicleId → dispatchId
|
||||
* ===================================================== */
|
||||
List<Long> dispatchIds =
|
||||
vehicleDispatchRepository.findDispatchIdsByVehicleIds(
|
||||
vehicleIds,
|
||||
statDate
|
||||
);
|
||||
|
||||
if (dispatchIds.isEmpty()) {
|
||||
log.info("[ODOMETER_PROCESS] no dispatch date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 4. dispatch 단위 stat 처리
|
||||
* ===================================================== */
|
||||
for (Long dispatchId : dispatchIds) {
|
||||
processed++;
|
||||
try {
|
||||
dailyStatService.processOdometerDaily(dispatchId, statDate);
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
log.error(
|
||||
"[ODOMETER_PROCESS_FAIL] dispatchId={} date={}",
|
||||
dispatchId,
|
||||
statDate,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(failed == 0)
|
||||
.processedCount(processed)
|
||||
.successCount(success)
|
||||
.failCount(failed)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.client.SamsaraStatOdometerClient;
|
||||
import com.goi.erp.dto.ExtSamsaraOdometerFetchCommand;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SchedulerOdometerRawOrchestrator {
|
||||
|
||||
private final SamsaraStatOdometerClient statOdometerClient;
|
||||
private final ExtSamsaraOdometerIngestService ingestService;
|
||||
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleExternalMapService vehicleExternalMapService;
|
||||
|
||||
public ScheduleWorkerResponseDto run(ScheduleWorkerRequestDto request) {
|
||||
|
||||
try {
|
||||
LocalDate dispatchDate = request.getFrom().toLocalDate();
|
||||
|
||||
// 1. 대상 dispatch 조회 (NOT CLOSED)
|
||||
List<VehicleDispatch> targets = vehicleDispatchRepository.findAllNotClosed(dispatchDate);
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
log.info("[ODOMETER_RAW] no dispatch to process");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 2. internal vehicleId 수집
|
||||
List<Long> vehicleIds = targets.stream()
|
||||
.map(VehicleDispatch::getVedVehId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("[ODOMETER_RAW] no vehicles to process");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 3. internal → external (Samsara) 매핑
|
||||
Map<Long, String> vehicleIdToExternalId =
|
||||
vehicleExternalMapService.findExternalIdsByVehicleIds(
|
||||
"SAMSARA",
|
||||
vehicleIds
|
||||
);
|
||||
|
||||
List<String> externalIds = vehicleIdToExternalId.values().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (externalIds.isEmpty()) {
|
||||
log.info("[ODOMETER_RAW] no external vehicle ids");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 4. ★ request 에 externalIds 세팅 (중요)
|
||||
request.setVehicleExternalIds(externalIds);
|
||||
|
||||
// 5. integration-service fetch
|
||||
ExtSamsaraOdometerFetchCommand fetchResult =
|
||||
statOdometerClient.fetchOdometer(request);
|
||||
|
||||
if (fetchResult == null || fetchResult.getRecords() == null) {
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 6. raw ingest
|
||||
int inserted = ingestService.ingest(fetchResult);
|
||||
|
||||
log.info(
|
||||
"[ODOMETER_RAW][DONE] fetched={} inserted={}",
|
||||
fetchResult.getRecords().size(),
|
||||
inserted
|
||||
);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(fetchResult.getRecords().size())
|
||||
.successCount(inserted)
|
||||
.failCount(fetchResult.getRecords().size() - inserted)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[ODOMETER_RAW][RUN_FAIL]", e);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(false)
|
||||
.errorCode("ODOMETER_RAW_INGEST_ERROR")
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.entity.ExtSamsaraRawSafetyEvent;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.entity.VehicleSafetyEvent;
|
||||
import com.goi.erp.repository.ExtSamsaraRawSafetyEventRepository;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
import com.goi.erp.repository.VehicleExternalMapRepository;
|
||||
import com.goi.erp.repository.VehicleSafetyEventRepository;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.goi.erp.client.HcmEmployeeClient;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SchedulerSafetyEventProcessor {
|
||||
|
||||
private final ExtSamsaraRawSafetyEventRepository rawRepository;
|
||||
private final VehicleExternalMapRepository vehicleExternalMapRepository;
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleSafetyEventRepository safetyEventRepository;
|
||||
private final HcmEmployeeClient hcmEmployeeClient;
|
||||
|
||||
@Transactional
|
||||
public ScheduleWorkerResponseDto run(ScheduleWorkerRequestDto request) {
|
||||
|
||||
LocalDate statDate = request.getFrom().toLocalDate();
|
||||
|
||||
int processed = 0;
|
||||
int success = 0;
|
||||
int failed = 0;
|
||||
|
||||
/* =====================================================
|
||||
* 1. 날짜 기준 unprocessed raw 가 있는 samsaraVehicleIds
|
||||
* ===================================================== */
|
||||
List<String> samsaraVehicleIds =
|
||||
rawRepository.findDistinctVehicleIdByDateAndUnprocessed(statDate);
|
||||
|
||||
if (samsaraVehicleIds.isEmpty()) {
|
||||
log.info("[SAFETY_EVENT_PROCESS] no raw to process date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 2. externalId → internal vehicleId 매핑
|
||||
* ===================================================== */
|
||||
List<Long> vehicleIds =
|
||||
vehicleExternalMapRepository.findVehicleIdsByExternalIds(
|
||||
"SAMSARA",
|
||||
samsaraVehicleIds
|
||||
);
|
||||
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("[SAFETY_EVENT_PROCESS] no vehicle mapping date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 3. vehicleId → dispatchId
|
||||
* ===================================================== */
|
||||
List<Long> dispatchIds =
|
||||
vehicleDispatchRepository.findDispatchIdsByVehicleIds(
|
||||
vehicleIds,
|
||||
statDate
|
||||
);
|
||||
|
||||
if (dispatchIds.isEmpty()) {
|
||||
log.info("[SAFETY_EVENT_PROCESS] no dispatch date={}", statDate);
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* 4. dispatch 단위 safety event 처리
|
||||
* ===================================================== */
|
||||
for (Long dispatchId : dispatchIds) {
|
||||
processed++;
|
||||
try {
|
||||
success += processDispatch(dispatchId, statDate);
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
log.error(
|
||||
"[SAFETY_EVENT_PROCESS_FAIL] dispatchId={} date={}",
|
||||
dispatchId,
|
||||
statDate,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(failed == 0)
|
||||
.processedCount(processed)
|
||||
.successCount(success)
|
||||
.failCount(failed)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* dispatch 1건에 대한 safety event 처리
|
||||
*/
|
||||
private int processDispatch(Long dispatchId, LocalDate statDate) {
|
||||
|
||||
VehicleDispatch dispatch =
|
||||
vehicleDispatchRepository.findById(dispatchId).orElseThrow();
|
||||
|
||||
/* =====================================================
|
||||
* internal vehicleId → external samsaraVehicleId
|
||||
* ===================================================== */
|
||||
String samsaraVehicleId =
|
||||
vehicleExternalMapRepository
|
||||
.findAllByVexSolutionTypeAndVexVehicleIdInAndVexStatus(
|
||||
"SAMSARA",
|
||||
List.of(dispatch.getVedVehId()),
|
||||
"A"
|
||||
)
|
||||
.stream()
|
||||
.findFirst()
|
||||
.map(m -> m.getVexExternalId())
|
||||
.orElse(null);
|
||||
|
||||
if (samsaraVehicleId == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* raw 조회 (external id 기준)
|
||||
* ===================================================== */
|
||||
List<ExtSamsaraRawSafetyEvent> raws =
|
||||
rawRepository.findByEsrsVehicleIdAndEsrsEventDateAndEsrsProcessedFalse(
|
||||
samsaraVehicleId,
|
||||
statDate
|
||||
);
|
||||
|
||||
int inserted = 0;
|
||||
|
||||
for (ExtSamsaraRawSafetyEvent raw : raws) {
|
||||
try {
|
||||
if (processSingle(raw, dispatch)) {
|
||||
inserted++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"[SAFETY_EVENT_RAW_PROCESS_FAIL] rawId={} dispatchId={}",
|
||||
raw.getEsrsId(),
|
||||
dispatchId,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return inserted;
|
||||
}
|
||||
|
||||
/**
|
||||
* raw 1건 → vehicle_safety_event
|
||||
*/
|
||||
private boolean processSingle(
|
||||
ExtSamsaraRawSafetyEvent raw,
|
||||
VehicleDispatch dispatch
|
||||
) {
|
||||
// driver resolve
|
||||
Long driverId = resolveDriverId(raw);
|
||||
// behavior summary 생성
|
||||
String behaviorSummary = buildBehaviorSummary(raw.getEsrsBehaviorLabels());
|
||||
|
||||
VehicleSafetyEvent event =
|
||||
VehicleSafetyEvent.builder()
|
||||
.vseSource("SAMSARA")
|
||||
.vseBehaviorSummary(behaviorSummary)
|
||||
.vseDispatchId(dispatch.getVedId())
|
||||
.vseVehicleId(dispatch.getVedVehId())
|
||||
.vseDriverId(driverId)
|
||||
.vseEventTime(raw.getEsrsEventTime())
|
||||
.vseEventDate(raw.getEsrsEventDate())
|
||||
.vseCoachingState(raw.getEsrsCoachingState())
|
||||
.vseMaxAccelG(raw.getEsrsMaxAccelG())
|
||||
.vseLatitude(raw.getEsrsLatitude())
|
||||
.vseLongitude(raw.getEsrsLongitude())
|
||||
.vseVideoForwardUrl(raw.getEsrsVideoForwardUrl())
|
||||
.vseVideoInwardUrl(raw.getEsrsVideoInwardUrl())
|
||||
.vseBehaviorLabels(raw.getEsrsBehaviorLabels())
|
||||
.build();
|
||||
|
||||
VehicleSafetyEvent saved = safetyEventRepository.save(event);
|
||||
|
||||
raw.setEsrsProcessed(true);
|
||||
raw.setEsrsProcessedAt(LocalDateTime.now());
|
||||
raw.setEsrsVehicleSafetyEventId(saved.getVseId());
|
||||
|
||||
rawRepository.save(raw);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private String buildBehaviorSummary(JsonNode behaviorLabels) {
|
||||
|
||||
if (behaviorLabels == null || !behaviorLabels.isArray() || behaviorLabels.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// name 기준 추출
|
||||
List<String> names = new ArrayList<>();
|
||||
|
||||
for (JsonNode label : behaviorLabels) {
|
||||
String name = label.path("name").asText(null);
|
||||
if (name != null) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (names.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 여러 개면 ", "로 join
|
||||
return String.join(", ", names);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Samsara driver id → internal emp id
|
||||
*/
|
||||
private Long resolveDriverId(ExtSamsaraRawSafetyEvent raw) {
|
||||
|
||||
if (raw.getEsrsDriverId() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return hcmEmployeeClient.getEmpIdFromExternalId(
|
||||
"SAMSARA",
|
||||
String.valueOf(raw.getEsrsDriverId())
|
||||
);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.client.SamsaraStatSafetyEventClient;
|
||||
import com.goi.erp.dto.ExtSamsaraSafetyEventFetchCommand;
|
||||
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SchedulerSafetyEventRawOrchestrator {
|
||||
|
||||
private final SamsaraStatSafetyEventClient statSafetyEventClient;
|
||||
private final ExtSamsaraSafetyEventIngestService ingestService;
|
||||
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleExternalMapService vehicleExternalMapService;
|
||||
|
||||
public ScheduleWorkerResponseDto run(ScheduleWorkerRequestDto request) {
|
||||
|
||||
try {
|
||||
LocalDate dispatchDate = request.getFrom().toLocalDate();
|
||||
|
||||
// 1. 대상 dispatch 조회 (NOT CLOSED)
|
||||
List<VehicleDispatch> targets =
|
||||
vehicleDispatchRepository.findAllNotClosed(dispatchDate);
|
||||
|
||||
if (targets.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_RAW] no dispatch to process");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 2. internal vehicleId 수집
|
||||
List<Long> vehicleIds = targets.stream()
|
||||
.map(VehicleDispatch::getVedVehId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (vehicleIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_RAW] no vehicles to process");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 3. internal → external (Samsara) 매핑
|
||||
Map<Long, String> vehicleIdToExternalId =
|
||||
vehicleExternalMapService.findExternalIdsByVehicleIds(
|
||||
"SAMSARA",
|
||||
vehicleIds
|
||||
);
|
||||
|
||||
List<String> externalIds = vehicleIdToExternalId.values().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (externalIds.isEmpty()) {
|
||||
log.info("[ENGINE_SECONDS_RAW] no external vehicle ids");
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 4. request 에 externalIds 세팅
|
||||
request.setVehicleExternalIds(externalIds);
|
||||
|
||||
// 5. integration-service fetch (obdEngineSeconds)
|
||||
ExtSamsaraSafetyEventFetchCommand fetchResult =
|
||||
statSafetyEventClient.fetchSafetyEvent(request);
|
||||
|
||||
if (fetchResult == null || fetchResult.getRecords() == null) {
|
||||
return ScheduleWorkerResponseDto.successEmpty();
|
||||
}
|
||||
|
||||
// 6. raw ingest
|
||||
int inserted = ingestService.ingest(fetchResult);
|
||||
|
||||
log.info(
|
||||
"[ENGINE_SECONDS_RAW][DONE] fetched={} inserted={}",
|
||||
fetchResult.getRecords().size(),
|
||||
inserted
|
||||
);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(true)
|
||||
.processedCount(fetchResult.getRecords().size())
|
||||
.successCount(inserted)
|
||||
.failCount(fetchResult.getRecords().size() - inserted)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[ENGINE_SECONDS_RAW][RUN_FAIL]", e);
|
||||
|
||||
return ScheduleWorkerResponseDto.builder()
|
||||
.success(false)
|
||||
.errorCode("ENGINE_SECONDS_RAW_INGEST_ERROR")
|
||||
.errorMessage(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.entity.ExtSamsaraRawEngineSeconds;
|
||||
import com.goi.erp.entity.ExtSamsaraRawOdometer;
|
||||
import com.goi.erp.entity.VehicleDispatchDailyStat;
|
||||
import com.goi.erp.repository.ExtSamsaraRawEngineSecondsRepository;
|
||||
import com.goi.erp.repository.ExtSamsaraRawOdometerRepository;
|
||||
import com.goi.erp.repository.VehicleDispatchDailyStatRepository;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleDispatchDailyStatService {
|
||||
|
||||
private final VehicleDispatchDailyStatRepository repository;
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final ExtSamsaraRawOdometerRepository rawOdometerRepository;
|
||||
private final ExtSamsaraRawEngineSecondsRepository rawEngineSecondsRepository;
|
||||
|
||||
public Optional<VehicleDispatchDailyStat> getDailyStat(Long dispatchId, LocalDate date) {
|
||||
|
||||
return repository.findByVddsDispatchIdAndVddsDate(dispatchId, date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Odometer 업데이트
|
||||
*
|
||||
* rule:
|
||||
* - 이전 값보다 작아지면 (리셋)
|
||||
* → increment 유지
|
||||
* → total 만 새 값으로 교체
|
||||
*/
|
||||
public VehicleDispatchDailyStat updateOdometerStat(
|
||||
Long dispatchId,
|
||||
LocalDate date,
|
||||
BigDecimal newTotalOdometer,
|
||||
String newSource
|
||||
) {
|
||||
|
||||
VehicleDispatchDailyStat stat = repository
|
||||
.findByVddsDispatchIdAndVddsDate(dispatchId, date)
|
||||
.orElseThrow(() ->
|
||||
new IllegalStateException(
|
||||
"Daily stat not found for dispatchId=" + dispatchId
|
||||
)
|
||||
);
|
||||
|
||||
BigDecimal prevTotal = stat.getVddsOdometerTotal();
|
||||
BigDecimal prevIncrement = stat.getVddsOdometerIncrement();
|
||||
String prevSource = stat.getVddsOdometerSource();
|
||||
|
||||
// 최초
|
||||
if (prevTotal == null) {
|
||||
stat.setVddsOdometerTotal(newTotalOdometer);
|
||||
stat.setVddsOdometerIncrement(BigDecimal.ZERO);
|
||||
stat.setVddsOdometerSource(newSource);
|
||||
stat.setVddsCollectedAt(LocalDateTime.now());
|
||||
return repository.save(stat);
|
||||
}
|
||||
|
||||
// source 변경
|
||||
if (prevSource != null && !prevSource.equals(newSource)) {
|
||||
// total만 교체, increment 유지
|
||||
stat.setVddsOdometerTotal(newTotalOdometer);
|
||||
stat.setVddsOdometerSource(newSource);
|
||||
stat.setVddsCollectedAt(LocalDateTime.now());
|
||||
return repository.save(stat);
|
||||
}
|
||||
|
||||
// 같은 source
|
||||
if (newTotalOdometer != null) {
|
||||
if (newTotalOdometer.compareTo(prevTotal) >= 0) {
|
||||
BigDecimal delta = newTotalOdometer.subtract(prevTotal);
|
||||
stat.setVddsOdometerIncrement(
|
||||
prevIncrement == null ? delta : prevIncrement.add(delta)
|
||||
);
|
||||
}
|
||||
}
|
||||
// 감소(리셋)면 increment 유지
|
||||
stat.setVddsOdometerTotal(newTotalOdometer);
|
||||
stat.setVddsOdometerSource(newSource);
|
||||
stat.setVddsCollectedAt(LocalDateTime.now());
|
||||
return repository.save(stat);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void processOdometerDaily(Long dispatchId, LocalDate statDate) {
|
||||
|
||||
// 1. dispatch → vehicleExternalId 조회
|
||||
String vehicleExtId =
|
||||
vehicleDispatchRepository
|
||||
.findVehicleExternalIdByDispatchId(dispatchId)
|
||||
.orElseThrow(() ->
|
||||
new IllegalStateException(
|
||||
"Vehicle externalId not found. dispatchId=" + dispatchId
|
||||
)
|
||||
);
|
||||
|
||||
// 2. 해당 날짜의 미처리 raw 조회
|
||||
List<ExtSamsaraRawOdometer> raws =
|
||||
rawOdometerRepository.findByVehicleExtIdAndDateAndUnprocessed(
|
||||
vehicleExtId,
|
||||
statDate
|
||||
);
|
||||
|
||||
if (raws.isEmpty()) {
|
||||
log.info(
|
||||
"[ODOMETER_STAT] no raw to process dispatchId={} date={}",
|
||||
dispatchId,
|
||||
statDate
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 타입별 분리
|
||||
var obdSamples = raws.stream()
|
||||
.filter(r -> "OBD".equals(r.getEsroOdometerType()))
|
||||
.sorted(Comparator.comparing(ExtSamsaraRawOdometer::getEsroSampleTime))
|
||||
.toList();
|
||||
|
||||
var gpsSamples = raws.stream()
|
||||
.filter(r -> "GPS".equals(r.getEsroOdometerType()))
|
||||
.sorted(Comparator.comparing(ExtSamsaraRawOdometer::getEsroSampleTime))
|
||||
.toList();
|
||||
|
||||
// 4. 사용할 샘플 결정
|
||||
var samplesToUse = !obdSamples.isEmpty() ? obdSamples : gpsSamples;
|
||||
if (samplesToUse.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. last odometer
|
||||
BigDecimal last =
|
||||
BigDecimal.valueOf(
|
||||
samplesToUse.get(samplesToUse.size() - 1).getEsroOdometerMeters()
|
||||
);
|
||||
|
||||
updateOdometerStat(
|
||||
dispatchId,
|
||||
statDate,
|
||||
last,
|
||||
"SAMSARA_" + samplesToUse.get(0).getEsroOdometerType()
|
||||
);
|
||||
|
||||
// 6. raw 처리 완료
|
||||
raws.forEach(r -> {
|
||||
r.setEsroProcessed(true);
|
||||
r.setEsroProcessedAt(LocalDateTime.now());
|
||||
});
|
||||
|
||||
rawOdometerRepository.saveAll(raws);
|
||||
|
||||
log.info(
|
||||
"[ODOMETER_STAT] done dispatchId={} date={} samples={} type={}",
|
||||
dispatchId,
|
||||
statDate,
|
||||
samplesToUse.size(),
|
||||
samplesToUse.get(0).getEsroOdometerType()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Engine seconds 업데이트
|
||||
*
|
||||
* rule:
|
||||
* - total 이 null 이면 최초 집계 → increment = 0
|
||||
* - 이전 값보다 작아지면 (리셋)
|
||||
* → increment 유지
|
||||
* → total 만 새 값으로 교체
|
||||
*/
|
||||
public VehicleDispatchDailyStat updateEngineSecondsStat(
|
||||
Long dispatchId,
|
||||
LocalDate date,
|
||||
Integer newTotalEngineSeconds
|
||||
) {
|
||||
|
||||
VehicleDispatchDailyStat stat = repository
|
||||
.findByVddsDispatchIdAndVddsDate(dispatchId, date)
|
||||
.orElseThrow(() ->
|
||||
new IllegalStateException(
|
||||
"Daily stat not found for dispatchId=" + dispatchId
|
||||
)
|
||||
);
|
||||
|
||||
Integer prevTotal = stat.getVddsEngineSecondsTotal();
|
||||
Integer prevIncrement = stat.getVddsEngineSecondsIncrement();
|
||||
|
||||
if (prevTotal == null) {
|
||||
// 최초 집계
|
||||
stat.setVddsEngineSecondsTotal(newTotalEngineSeconds);
|
||||
stat.setVddsEngineSecondsIncrement(0);
|
||||
} else if (newTotalEngineSeconds != null) {
|
||||
|
||||
if (newTotalEngineSeconds >= prevTotal) {
|
||||
int delta = newTotalEngineSeconds - prevTotal;
|
||||
|
||||
stat.setVddsEngineSecondsIncrement(
|
||||
prevIncrement == null
|
||||
? delta
|
||||
: prevIncrement + delta
|
||||
);
|
||||
}
|
||||
// else: 리셋 → increment 유지
|
||||
stat.setVddsEngineSecondsTotal(newTotalEngineSeconds);
|
||||
}
|
||||
|
||||
stat.setVddsCollectedAt(LocalDateTime.now());
|
||||
return repository.save(stat);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void processEngineSecondsDaily(Long dispatchId, LocalDate statDate) {
|
||||
|
||||
// 1. dispatch → vehicleExternalId
|
||||
String vehicleExtId =
|
||||
vehicleDispatchRepository
|
||||
.findVehicleExternalIdByDispatchId(dispatchId)
|
||||
.orElseThrow(() ->
|
||||
new IllegalStateException(
|
||||
"Vehicle externalId not found. dispatchId=" + dispatchId
|
||||
)
|
||||
);
|
||||
|
||||
// 2. 해당 날짜의 미처리 raw 조회
|
||||
List<ExtSamsaraRawEngineSeconds> raws =
|
||||
rawEngineSecondsRepository.findByEsreVehicleExtIdAndEsreFetchedDateAndEsreProcessedFalse(
|
||||
vehicleExtId,
|
||||
statDate
|
||||
);
|
||||
|
||||
if (raws.isEmpty()) {
|
||||
log.info(
|
||||
"[ENGINE_SECONDS_STAT] no raw to process dispatchId={} date={}",
|
||||
dispatchId,
|
||||
statDate
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 시간순 정렬
|
||||
raws.sort(
|
||||
Comparator.comparing(ExtSamsaraRawEngineSeconds::getEsreSampleTime)
|
||||
);
|
||||
|
||||
// 4. last engine seconds
|
||||
Integer last =
|
||||
raws.get(raws.size() - 1).getEsreEngineSeconds().intValue();
|
||||
|
||||
updateEngineSecondsStat(
|
||||
dispatchId,
|
||||
statDate,
|
||||
last
|
||||
);
|
||||
|
||||
// 5. raw 처리 완료
|
||||
raws.forEach(r -> {
|
||||
r.setEsreProcessed(true);
|
||||
r.setEsreProcessedAt(LocalDateTime.now());
|
||||
});
|
||||
|
||||
rawEngineSecondsRepository.saveAll(raws);
|
||||
|
||||
log.info(
|
||||
"[ENGINE_SECONDS_STAT] done dispatchId={} date={} samples={}",
|
||||
dispatchId,
|
||||
statDate,
|
||||
raws.size()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.goi.erp.token;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SystemTokenProvider {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${auth.api.base-url}")
|
||||
private String authBaseUrl;
|
||||
|
||||
@Value("${application.security.system.client-id}")
|
||||
private String clientId;
|
||||
|
||||
@Value("${application.security.system.client-secret}")
|
||||
private String clientSecret;
|
||||
|
||||
private volatile String cachedToken;
|
||||
private volatile long expiresAt;
|
||||
|
||||
public String getToken() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (cachedToken != null && now < expiresAt - 30_000) {
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (cachedToken != null && now < expiresAt - 30_000) {
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
Map<String, String> body = Map.of(
|
||||
"clientId", clientId,
|
||||
"clientSecret", clientSecret
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> response =
|
||||
restTemplate.postForObject(
|
||||
authBaseUrl + "/auth/authenticate/system",
|
||||
body,
|
||||
Map.class
|
||||
);
|
||||
|
||||
cachedToken = (String) response.get("access_token");
|
||||
// expiresAt는 jwt exp 파싱하거나, 고정 TTL 쓰면 됨
|
||||
expiresAt = now + 10 * 60 * 1000;
|
||||
|
||||
return cachedToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue