This commit is contained in:
Дмитрий Соловьев
2026-05-25 14:23:52 +03:00
parent b3a6012ebb
commit d48ddd2657
1066 changed files with 104601 additions and 3 deletions
@@ -0,0 +1,11 @@
package space.nstart.pcp.pcp_stations_service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class PcpStationsServiceApplication
fun main(args: Array<String>) {
runApplication<PcpStationsServiceApplication>(*args)
}
@@ -0,0 +1,46 @@
package space.nstart.pcp.pcp_stations_service.configuration
import org.springframework.http.ResponseEntity
import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.support.WebExchangeBindException
import reactor.core.publisher.Mono
class CustomValidationException(message: String) : RuntimeException(message)
class CustomErrorException(message: String) : RuntimeException(message)
@ControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(CustomValidationException::class)
fun handleValidation(ex: CustomValidationException): ResponseEntity<Map<String, String>> {
return ResponseEntity.badRequest()
.body(mapOf("error" to ex.message!!))
}
@ExceptionHandler(CustomErrorException::class)
fun handleError(ex: CustomErrorException): ResponseEntity<Map<String, String>> {
return ResponseEntity.internalServerError()
.body(mapOf("error" to ex.message!!))
}
@ExceptionHandler(WebExchangeBindException::class)
fun handleValidationExceptions(ex: WebExchangeBindException): Mono<ResponseEntity<Map<String, String>>> {
val errors = mutableMapOf<String, String>()
ex.bindingResult.fieldErrors.forEach { error ->
errors[error.field] = error.defaultMessage ?: "Validation error"
}
return Mono.just(ResponseEntity.badRequest().body(errors))
}
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationExceptions(ex: MethodArgumentNotValidException): ResponseEntity<Map<String, String>> {
val errors = mutableMapOf<String, String>()
ex.bindingResult.allErrors.forEach { error ->
val fieldName = (error as FieldError).field
val errorMessage = error.defaultMessage
errors[fieldName] = errorMessage ?: "Validation error"
}
return ResponseEntity.badRequest().body(errors)
}
}
@@ -0,0 +1,14 @@
package space.nstart.pcp.pcp_stations_service.configuration
import jakarta.validation.Validation
import jakarta.validation.Validator
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class ValidationConfig {
@Bean
fun validator(): Validator {
return Validation.buildDefaultValidatorFactory().validator
}
}
@@ -0,0 +1,34 @@
package space.nstart.pcp.pcp_stations_service.controller
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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
import space.nstart.pcp.pcp_stations_service.service.StationService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.util.UUID
@RestController
@RequestMapping("api/stations")
class StationController {
@Autowired
private lateinit var stationService: StationService
@GetMapping()
fun all() = stationService.all()
@GetMapping("/{station_id}")
fun byId(@PathVariable("station_id") id : UUID): StationDTO = stationService.byId(id)
@PostMapping()
fun add(@RequestBody body : StationDTO): StationDTO = stationService.add(body)
@DeleteMapping("{station_id}")
fun delete(@PathVariable("station_id") id : UUID): Int = stationService.delete(id)
}
@@ -0,0 +1,53 @@
package space.nstart.pcp.pcp_stations_service.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 space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.util.UUID
@Entity
@Table(name = "stations")
class StationEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val stationId : UUID? = null,
@Column(nullable = false)
val number : Int = 0,
@Column(nullable = false)
val name : String = "",
@Column(nullable = false)
val longitude : Double = 0.0,
@Column(nullable = false)
val latitude : Double = 0.0,
@Column(nullable = false)
val height : Double = 0.0,
@Column(name = "elevation_min", nullable = false)
val elevationMin : Double = 0.0,
@Column(name = "elevation_max", nullable = false)
val elevationMax : Double = 0.0,
){
constructor(s : StationDTO) : this(
s.id,
s.number,
s.name,
s.position.long,
s.position.lat,
s.position.height,
s.elevationMin,
s.elevationMax
)
fun toDTO() = StationDTO(
stationId,
number,
name,
PositionDTO(latitude, longitude, height),
elevationMin,
elevationMax
)
}
@@ -0,0 +1,23 @@
package space.nstart.pcp.pcp_stations_service.repository
import jakarta.transaction.Transactional
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 space.nstart.pcp.pcp_stations_service.entity.StationEntity
import java.util.Optional
import java.util.UUID
interface StationRepository : JpaRepository<StationEntity, UUID> {
fun findByStationId(id : UUID) : Optional<StationEntity>
@Modifying
@Transactional
@Query("DELETE FROM stations WHERE station_id =:id", nativeQuery = true)
fun deleteByStationId(@Param("id") id : UUID) : Int
}
@@ -0,0 +1,30 @@
package space.nstart.pcp.pcp_stations_service.service
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.repository.StationRepository
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.util.UUID
@Service
class StationService {
@Autowired
private lateinit var stationRepository: StationRepository
fun all() = stationRepository.findAll().map { it.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)
}
@@ -0,0 +1,13 @@
spring:
application:
name: pcp-stations-service
profiles:
default: local
config:
import: "configserver:"
cloud:
config:
uri: ${CONFIG_SERVER_URI:http://localhost:8888}
fail-fast: ${CONFIG_SERVER_FAIL_FAST:true}
profile: ${SPRING_CLOUD_CONFIG_PROFILE:${SPRING_PROFILES_ACTIVE:${spring.profiles.default}}}
label: ${SPRING_CLOUD_CONFIG_LABEL:master}
@@ -0,0 +1,14 @@
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE IF NOT EXISTS stations (
station_id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
number INT NOT NULL,
name VARCHAR(55) NOT NULL,
latitude DOUBLE PRECISION NOT NULL,
longitude DOUBLE PRECISION NOT NULL,
height DOUBLE PRECISION NOT NULL,
elevation_min DOUBLE PRECISION NOT NULL,
elevation_max DOUBLE PRECISION NOT NULL
);
CREATE INDEX idx_stations_number ON stations(number);
@@ -0,0 +1,13 @@
package space.nstart.pcp.pcp_stations_service
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class PcpStationsServiceApplicationTests {
@Test
fun contextLoads() {
}
}
@@ -0,0 +1,102 @@
package space.nstart.pcp.pcp_stations_service.controller
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.verify
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.MediaType
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.web.reactive.function.client.WebClient
import space.nstart.pcp.pcp_stations_service.service.StationService
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.util.UUID
import kotlin.test.assertEquals
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
"spring.cloud.config.enabled=false",
"spring.cloud.config.import-check.enabled=false",
"spring.boot.admin.client.enabled=false"
]
)
class StationControllerTest {
@LocalServerPort
private var port: Int = 0
@MockitoBean
private lateinit var stationService: StationService
@Test
fun `station save endpoint returns dto with position`() {
val station = sampleStation()
doReturn(station).`when`(stationService).add(anyStation())
val actual = WebClient.create("http://localhost:$port")
.post()
.uri("/api/stations")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(station)
.retrieve()
.bodyToMono(StationDTO::class.java)
.block()!!
assertEquals(station.id, actual.id)
assertEquals(station.position.lat, actual.position.lat)
assertEquals(station.position.long, actual.position.long)
verify(stationService).add(anyStation())
}
@Test
fun `station get by id endpoint returns dto without optional wrapper`() {
val station = sampleStation()
val stationId = station.id!!
doReturn(station).`when`(stationService).byId(stationId)
val actual = WebClient.create("http://localhost:$port")
.get()
.uri("/api/stations/$stationId")
.retrieve()
.bodyToMono(StationDTO::class.java)
.block()!!
assertEquals(stationId, actual.id)
assertEquals(station.name, actual.name)
verify(stationService).byId(stationId)
}
@Test
fun `station delete endpoint returns deleted rows count`() {
val stationId = UUID.fromString("3ad079f2-625f-47bc-a2b8-4e0c5f7d5c16")
doReturn(1).`when`(stationService).delete(stationId)
val actual = WebClient.create("http://localhost:$port")
.delete()
.uri("/api/stations/$stationId")
.retrieve()
.bodyToMono(Int::class.java)
.block()!!
assertEquals(1, actual)
verify(stationService).delete(stationId)
}
private fun sampleStation() = StationDTO(
id = UUID.fromString("2f1d1d8b-588d-4d4e-a456-80a5c93e09dd"),
number = 7,
name = "Station 7",
position = PositionDTO(
lat = 55.75,
long = 37.62,
height = 180.0
),
elevationMin = 5.0,
elevationMax = 88.0
)
private fun anyStation(): StationDTO = any(StationDTO::class.java) ?: StationDTO()
}
@@ -0,0 +1,19 @@
spring:
config:
import: "optional:configserver:"
cloud:
config:
enabled: false
datasource:
url: jdbc:h2:mem:pcp_stations;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
flyway:
enabled: false