stations readable from MDM (raw). get by id ok
This commit is contained in:
@@ -88,3 +88,6 @@ spring:
|
||||
max-in-memory-size: 20MB
|
||||
server:
|
||||
port: ${pcp.ports.stations:8080}
|
||||
|
||||
classifier:
|
||||
stations-url: ${CLASSIFIER_STATIONS_URL:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records}
|
||||
|
||||
@@ -64,7 +64,7 @@ services:
|
||||
volumes:
|
||||
- kafka-data:/var/lib/kafka/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1"]
|
||||
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
@@ -192,7 +192,7 @@ wait_for_postgres() {
|
||||
wait_for_kafka() {
|
||||
local attempt
|
||||
for attempt in {1..60}; do
|
||||
if compose exec -T kafka kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then
|
||||
if compose exec -T kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --list >/dev/null 2>&1; then
|
||||
echo "Kafka is ready."
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -50,6 +50,11 @@ dependencies {
|
||||
|
||||
implementation("org.locationtech.jts:jts-core:1.19.0")
|
||||
|
||||
// in‑memory cache с поддержкой TTL, позволяет задать TTL и автоматически удалять устаревшие записи.
|
||||
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||
|
||||
// обёртка для «circuit breaker», «retry» и т.п. защищает от падения при ошибках сети (circuit‑breaker + fallback).
|
||||
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
|
||||
|
||||
testImplementation("junit:junit")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package space.nstart.pcp.pcp_stations_service.configuration
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
|
||||
@Configuration
|
||||
class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
fun webClientBuilder(): WebClient.Builder = WebClient.builder()
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package space.nstart.pcp.pcp_stations_service.integration
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.WebClientException
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
|
||||
import java.time.Duration
|
||||
import java.util.UUID
|
||||
|
||||
@Component
|
||||
class NsiStationClient(
|
||||
webClientBuilder: WebClient.Builder,
|
||||
|
||||
@Value("\${classifier.stations-url:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records}")
|
||||
private val stationsUrl: String
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private val webClient: WebClient = webClientBuilder.build()
|
||||
|
||||
fun fetchStations(): List<StationDTO> {
|
||||
val records = try {
|
||||
webClient.get()
|
||||
.uri(stationsUrl)
|
||||
.retrieve()
|
||||
.bodyToMono(Array<NsiStationRecord>::class.java)
|
||||
.block(Duration.ofSeconds(15))
|
||||
?.toList()
|
||||
?: emptyList()
|
||||
} catch (ex: WebClientResponseException) {
|
||||
log.error(
|
||||
"NSI stations request failed. url={}, status={}, body={}",
|
||||
stationsUrl,
|
||||
ex.statusCode,
|
||||
ex.responseBodyAsString,
|
||||
ex
|
||||
)
|
||||
throw NsiStationsUnavailableException("NSI stations request failed: $stationsUrl", ex)
|
||||
} catch (ex: WebClientException) {
|
||||
log.error("NSI stations request failed. url={}", stationsUrl, ex)
|
||||
throw NsiStationsUnavailableException("NSI stations request failed: $stationsUrl", ex)
|
||||
}
|
||||
|
||||
log.info("Loaded {} station records from NSI. url={}", records.size, stationsUrl)
|
||||
|
||||
return records.mapIndexed { index, record ->
|
||||
record.toStationDTO(number = index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun NsiStationRecord.toStationDTO(number: Int): StationDTO {
|
||||
return StationDTO(
|
||||
id = id.toUuidOrNull(),
|
||||
number = number,
|
||||
name = data?.name ?: businessKey ?: "",
|
||||
position = PositionDTO(
|
||||
lat = data?.lat ?: 0.0,
|
||||
long = data?.lon ?: 0.0,
|
||||
height = data?.elevationM ?: 0.0
|
||||
),
|
||||
elevationMin = data?.elevationMinDeg ?: 0.0,
|
||||
elevationMax = 90.0
|
||||
)
|
||||
}
|
||||
|
||||
private fun String?.toUuidOrNull(): UUID? =
|
||||
this?.let {
|
||||
runCatching { UUID.fromString(it) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
class NsiStationsUnavailableException(
|
||||
message: String,
|
||||
cause: Throwable
|
||||
) : RuntimeException(message, cause)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class NsiStationRecord(
|
||||
val id: String? = null,
|
||||
val businessKey: String? = null,
|
||||
val data: NsiStationData? = null
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class NsiStationData(
|
||||
val lat: Double? = null,
|
||||
val lon: Double? = null,
|
||||
val code: String? = null,
|
||||
val name: String? = null,
|
||||
val status: String? = null,
|
||||
|
||||
@JsonProperty("elevation_m")
|
||||
val elevationM: Double? = null,
|
||||
|
||||
@JsonProperty("elevation_min_deg")
|
||||
val elevationMinDeg: Double? = null
|
||||
)
|
||||
+10
-8
@@ -4,27 +4,29 @@ import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Service
|
||||
import space.nstart.pcp.pcp_stations_service.configuration.CustomErrorException
|
||||
import space.nstart.pcp.pcp_stations_service.entity.StationEntity
|
||||
import space.nstart.pcp.pcp_stations_service.integration.NsiStationClient
|
||||
import space.nstart.pcp.pcp_stations_service.repository.StationRepository
|
||||
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class StationService {
|
||||
|
||||
class StationService (
|
||||
){
|
||||
|
||||
@Autowired
|
||||
private lateinit var stationRepository: StationRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var nsiStationClient: NsiStationClient
|
||||
|
||||
fun all(): List<StationDTO> = nsiStationClient.fetchStations()
|
||||
|
||||
fun all() = stationRepository.findAll().map { it.toDTO() }
|
||||
fun byId(id: UUID): StationDTO =
|
||||
nsiStationClient.fetchStations()
|
||||
.firstOrNull { it.id == id }
|
||||
?: throw CustomErrorException("Станция $id не найдена")
|
||||
|
||||
fun add(station : StationDTO): StationDTO = stationRepository.save(StationEntity(station)).toDTO()
|
||||
|
||||
fun byId(id : UUID): StationDTO =
|
||||
stationRepository.findByStationId(id)
|
||||
.map { it.toDTO() }
|
||||
.orElseThrow { CustomErrorException("Станция $id не найдена") }
|
||||
|
||||
fun delete(id : UUID): Int = stationRepository.deleteByStationId(id)
|
||||
}
|
||||
|
||||
+6
@@ -51,6 +51,12 @@ class StationControllerTest {
|
||||
verify(stationService).add(anyStation())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get all stations`() {
|
||||
val stations = stationService.getStations();
|
||||
assertEquals(8, stations.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `station get by id endpoint returns dto without optional wrapper`() {
|
||||
val station = sampleStation()
|
||||
|
||||
Reference in New Issue
Block a user