[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.Optional;
|
||||||
import java.util.UUID;
|
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);
|
List<VehicleDispatch> findByVedDispatchDate(LocalDate vedDispatchDate);
|
||||||
|
|
||||||
Page<VehicleDispatch> findAll(Pageable pageable);
|
Page<VehicleDispatch> findAll(Pageable pageable);
|
||||||
|
|
||||||
Optional<VehicleDispatch> findByVedUuid(UUID vedUuid);
|
Optional<VehicleDispatch> findByVedUuid(UUID vedUuid);
|
||||||
|
|
||||||
//
|
/* =====================================================
|
||||||
|
* STATUS BASED
|
||||||
|
* ===================================================== */
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT d
|
SELECT d
|
||||||
FROM VehicleDispatch d
|
FROM VehicleDispatch d
|
||||||
|
|
@ -31,41 +47,55 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
||||||
AND d.vedDispatchDate = :dispatchDate
|
AND d.vedDispatchDate = :dispatchDate
|
||||||
AND d.vedStatus <> 'C'
|
AND d.vedStatus <> 'C'
|
||||||
""")
|
""")
|
||||||
Optional<VehicleDispatch> findOpenDispatchByVehId(Long vehId, LocalDate dispatchDate);
|
Optional<VehicleDispatch> findOpenDispatchByVehId(
|
||||||
|
@Param("vehId") Long vehId,
|
||||||
|
@Param("dispatchDate") LocalDate dispatchDate
|
||||||
|
);
|
||||||
|
|
||||||
//
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT d
|
SELECT d
|
||||||
FROM VehicleDispatch d
|
FROM VehicleDispatch d
|
||||||
WHERE d.vedDispatchDate = :dispatchDate
|
WHERE d.vedDispatchDate = :dispatchDate
|
||||||
AND d.vedStatus <> 'C'
|
AND d.vedStatus <> 'C'
|
||||||
""")
|
""")
|
||||||
List<VehicleDispatch> findAllNotClosed(LocalDate dispatchDate);
|
List<VehicleDispatch> findAllNotClosed(
|
||||||
|
@Param("dispatchDate") LocalDate dispatchDate
|
||||||
|
);
|
||||||
|
|
||||||
//
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT d
|
SELECT d
|
||||||
FROM VehicleDispatch d
|
FROM VehicleDispatch d
|
||||||
WHERE d.vedStatus = 'P'
|
WHERE d.vedStatus = 'P'
|
||||||
AND d.vedPausedAt <= :threshold
|
AND d.vedPausedAt <= :threshold
|
||||||
""")
|
""")
|
||||||
List<VehicleDispatch> findPausedBefore(LocalDateTime threshold);
|
List<VehicleDispatch> findPausedBefore(
|
||||||
|
@Param("threshold") LocalDateTime threshold
|
||||||
|
);
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
* SHIFT
|
||||||
|
* ===================================================== */
|
||||||
|
|
||||||
//
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT COALESCE(MAX(d.vedShift), -1)
|
SELECT COALESCE(MAX(d.vedShift), -1)
|
||||||
FROM VehicleDispatch d
|
FROM VehicleDispatch d
|
||||||
WHERE d.vedVehId = :vehId
|
WHERE d.vedVehId = :vehId
|
||||||
AND d.vedDispatchDate = :date
|
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) {
|
default Integer findNextShift(Long vehId, LocalDate date) {
|
||||||
Integer max = findMaxShift(vehId, date);
|
Integer max = findMaxShift(vehId, date);
|
||||||
return max == null ? 0 : max + 1;
|
return max == null ? 0 : max + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =====================================================
|
||||||
|
* REOPEN
|
||||||
|
* ===================================================== */
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
SELECT d
|
SELECT d
|
||||||
FROM VehicleDispatch d
|
FROM VehicleDispatch d
|
||||||
|
|
@ -80,11 +110,63 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
||||||
AND x.vedDispatchDate = :dispatchDate
|
AND x.vedDispatchDate = :dispatchDate
|
||||||
AND x.vedStatus <> 'C'
|
AND x.vedStatus <> 'C'
|
||||||
)
|
)
|
||||||
|
|
||||||
""")
|
""")
|
||||||
List<VehicleDispatch> findAutoClosedCandidatesForReopen(
|
List<VehicleDispatch> findAutoClosedCandidatesForReopen(
|
||||||
@Param("dispatchDate") LocalDate dispatchDate,
|
@Param("dispatchDate") LocalDate dispatchDate,
|
||||||
@Param("closeReason") String closeReason,
|
@Param("closeReason") String closeReason,
|
||||||
@Param("closedAfter") LocalDateTime closedAfter
|
@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 com.goi.erp.entity.VehicleExternalMap;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -22,4 +23,16 @@ public interface VehicleExternalMapRepository extends JpaRepository<VehicleExter
|
||||||
String vexStatus
|
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