stations readable from MDM (raw). get by id ok

This commit is contained in:
Ivan I. Ovchinnikov
2026-07-07 22:17:44 +03:00
parent fe54c095ab
commit b37009a169
8 changed files with 142 additions and 11 deletions
+3
View File
@@ -88,3 +88,6 @@ spring:
max-in-memory-size: 20MB max-in-memory-size: 20MB
server: server:
port: ${pcp.ports.stations:8080} port: ${pcp.ports.stations:8080}
classifier:
stations-url: ${CLASSIFIER_STATIONS_URL:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records}
+1 -1
View File
@@ -64,7 +64,7 @@ services:
volumes: volumes:
- kafka-data:/var/lib/kafka/data - kafka-data:/var/lib/kafka/data
healthcheck: 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 interval: 10s
timeout: 10s timeout: 10s
retries: 30 retries: 30
+1 -1
View File
@@ -192,7 +192,7 @@ wait_for_postgres() {
wait_for_kafka() { wait_for_kafka() {
local attempt local attempt
for attempt in {1..60}; do 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." echo "Kafka is ready."
return 0 return 0
fi fi
@@ -50,6 +50,11 @@ dependencies {
implementation("org.locationtech.jts:jts-core:1.19.0") implementation("org.locationtech.jts:jts-core:1.19.0")
// inmemory cache с поддержкой TTL, позволяет задать TTL и автоматически удалять устаревшие записи.
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
// обёртка для «circuit breaker», «retry» и т.п. защищает от падения при ошибках сети (circuitbreaker + fallback).
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
testImplementation("junit:junit") testImplementation("junit:junit")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
@@ -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()
}
@@ -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
)
@@ -4,27 +4,29 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.nstart.pcp.pcp_stations_service.configuration.CustomErrorException 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.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_stations_service.repository.StationRepository
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.util.UUID import java.util.UUID
@Service @Service
class StationService { class StationService (
){
@Autowired @Autowired
private lateinit var stationRepository: StationRepository 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 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) fun delete(id : UUID): Int = stationRepository.deleteByStationId(id)
} }
@@ -51,6 +51,12 @@ class StationControllerTest {
verify(stationService).add(anyStation()) verify(stationService).add(anyStation())
} }
@Test
fun `get all stations`() {
val stations = stationService.getStations();
assertEquals(8, stations.size)
}
@Test @Test
fun `station get by id endpoint returns dto without optional wrapper`() { fun `station get by id endpoint returns dto without optional wrapper`() {
val station = sampleStation() val station = sampleStation()