Integrate series-service microservice with API gateway routing, implement Series domain and point aggregation logic, and update frontend with SeriesViewModel, SeriesScreen, and dynamic state handling.
This commit is contained in:
+24
-3
@@ -1,9 +1,10 @@
|
||||
package at.mocode.results.service
|
||||
|
||||
import at.mocode.results.service.application.ResultsService
|
||||
import at.mocode.results.service.domain.Ergebnis
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@SpringBootApplication
|
||||
class ResultsServiceApplication
|
||||
@@ -13,7 +14,27 @@ fun main(args: Array<String>) {
|
||||
}
|
||||
|
||||
@RestController
|
||||
class ResultsController {
|
||||
@RequestMapping("/api/v1/results")
|
||||
class ResultsController(
|
||||
private val service: ResultsService
|
||||
) {
|
||||
@GetMapping("/")
|
||||
fun health(): String = "Results Service is running"
|
||||
|
||||
@GetMapping("/bewerb/{bewerbId}")
|
||||
fun getForBewerb(@PathVariable bewerbId: String): List<Ergebnis> = service.getByBewerbId(bewerbId)
|
||||
|
||||
@PostMapping
|
||||
fun save(@RequestBody ergebnis: Ergebnis): Ergebnis = service.saveErgebnis(ergebnis)
|
||||
|
||||
@PutMapping("/{id}")
|
||||
fun update(@PathVariable id: String, @RequestBody ergebnis: Ergebnis): Ergebnis {
|
||||
return service.saveErgebnis(ergebnis.copy(id = id))
|
||||
}
|
||||
|
||||
@PostMapping("/bewerb/{bewerbId}/calculate")
|
||||
fun calculate(@PathVariable bewerbId: String): List<Ergebnis> = service.calculatePlatzierung(bewerbId)
|
||||
|
||||
@GetMapping("/bewerb/{bewerbId}/pdf")
|
||||
fun exportPdf(@PathVariable bewerbId: String): ByteArray = service.generatePdf(bewerbId)
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package at.mocode.results.service.application
|
||||
|
||||
import at.mocode.results.service.domain.Ergebnis
|
||||
import at.mocode.results.service.domain.ErgebnisStatus
|
||||
import at.mocode.results.service.persistence.JpaErgebnisRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class ResultsService(
|
||||
private val repository: JpaErgebnisRepository
|
||||
) {
|
||||
|
||||
fun getByBewerbId(bewerbId: String): List<Ergebnis> = repository.findByBewerbId(bewerbId)
|
||||
|
||||
@Transactional
|
||||
fun saveErgebnis(ergebnis: Ergebnis): Ergebnis = repository.save(ergebnis)
|
||||
|
||||
@Transactional
|
||||
fun calculatePlatzierung(bewerbId: String): List<Ergebnis> {
|
||||
val results = repository.findByBewerbId(bewerbId)
|
||||
.filter { it.status == ErgebnisStatus.OK }
|
||||
|
||||
// Einfache Platzierungs-Logik: Höchste Wertnote zuerst (Standard Dressur/Stil)
|
||||
// Bei gleicher Wertnote: Zeit als Tie-Breaker (Placeholder)
|
||||
val sortedResults = results.sortedWith(
|
||||
compareByDescending<Ergebnis> { it.wertnote ?: 0.0 }
|
||||
.thenBy { it.zeit ?: 0.0 }
|
||||
.thenByDescending { it.fehler ?: 0.0 }
|
||||
)
|
||||
|
||||
val updatedResults = sortedResults.mapIndexed { index, ergebnis ->
|
||||
ergebnis.copy(platzierung = index + 1)
|
||||
}
|
||||
|
||||
return repository.saveAll(updatedResults)
|
||||
}
|
||||
|
||||
fun generatePdf(bewerbId: String): ByteArray {
|
||||
// Placeholder für PDF-Generierung
|
||||
return "PDF Content for Bewerb $bewerbId".toByteArray()
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package at.mocode.results.service.domain
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "ergebnisse")
|
||||
data class Ergebnis(
|
||||
@Id
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
|
||||
@Column(nullable = false)
|
||||
val nennungId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
val bewerbId: String,
|
||||
|
||||
@Column
|
||||
val wertnote: Double? = null,
|
||||
|
||||
@Column
|
||||
val zeit: Double? = null,
|
||||
|
||||
@Column
|
||||
val fehler: Double? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
val status: ErgebnisStatus = ErgebnisStatus.OK,
|
||||
|
||||
@Column
|
||||
val platzierung: Int? = null
|
||||
)
|
||||
|
||||
enum class ErgebnisStatus {
|
||||
OK, AUSGESCHIEDEN, VERZICHTET, DISQUALIFIZIERT, NICHT_GESTARTET
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package at.mocode.results.service.persistence
|
||||
|
||||
import at.mocode.results.service.domain.Ergebnis
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface JpaErgebnisRepository : JpaRepository<Ergebnis, String> {
|
||||
fun findByBewerbId(bewerbId: String): List<Ergebnis>
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
ARG GRADLE_VERSION
|
||||
ARG JAVA_VERSION
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
|
||||
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
|
||||
|
||||
LABEL stage=builder \
|
||||
service=series-service \
|
||||
maintainer="Meldestelle Development Team" \
|
||||
version="${VERSION}" \
|
||||
build.date="${BUILD_DATE}"
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||
-Dorg.gradle.daemon=false \
|
||||
-Dorg.gradle.parallel=true \
|
||||
-Dorg.gradle.workers.max=2 \
|
||||
-Dorg.gradle.jvmargs=-Xmx2g \
|
||||
-XX:+UseParallelGC \
|
||||
-XX:MaxMetaspaceSize=512m"
|
||||
|
||||
ENV GRADLE_USER_HOME=/home/gradle/.gradle
|
||||
|
||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||
COPY gradle/ gradle/
|
||||
RUN chmod +x gradlew
|
||||
COPY platform/ platform/
|
||||
COPY frontend/ frontend/
|
||||
COPY core/ core/
|
||||
COPY backend/ backend/
|
||||
COPY docs/ docs/
|
||||
COPY build.gradle.kts ./
|
||||
|
||||
# Copy series modules
|
||||
COPY backend/services/results/series-service/ backend/services/results/series-service/
|
||||
|
||||
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||
./gradlew :backend:services:results:series-service:dependencies --no-daemon --info
|
||||
|
||||
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||
./gradlew :backend:services:results:series-service:bootJar --no-daemon --info
|
||||
|
||||
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
ARG JAVA_VERSION
|
||||
|
||||
ENV JAVA_VERSION=${JAVA_VERSION} \
|
||||
VERSION=${VERSION} \
|
||||
BUILD_DATE=${BUILD_DATE}
|
||||
|
||||
LABEL service="series-service" \
|
||||
version="${VERSION}" \
|
||||
description="Microservice for Series Management" \
|
||||
maintainer="Meldestelle Development Team" \
|
||||
java.version="${JAVA_VERSION}" \
|
||||
build.date="${BUILD_DATE}"
|
||||
|
||||
ARG APP_USER=appuser
|
||||
ARG APP_GROUP=appgroup
|
||||
ARG APP_UID=1009
|
||||
ARG APP_GID=1009
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --no-cache curl tzdata tini && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
addgroup -g ${APP_GID} -S ${APP_GROUP} && \
|
||||
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh && \
|
||||
mkdir -p /app/logs /app/tmp /app/config && \
|
||||
chown -R ${APP_USER}:${APP_GROUP} /app && \
|
||||
chmod -R 750 /app
|
||||
|
||||
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} \
|
||||
/workspace/backend/services/results/series-service/build/libs/*.jar app.jar
|
||||
|
||||
USER ${APP_USER}
|
||||
|
||||
EXPOSE 8089 5011
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS --max-time 2 http://localhost:8089/actuator/health/readiness || exit 1
|
||||
|
||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||
-XX:+UseG1GC \
|
||||
-XX:+UseStringDeduplication \
|
||||
-XX:+UseContainerSupport \
|
||||
-XX:G1HeapRegionSize=16m \
|
||||
-XX:G1ReservePercent=25 \
|
||||
-XX:InitiatingHeapOccupancyPercent=30 \
|
||||
-XX:+AlwaysPreTouch \
|
||||
-XX:+DisableExplicitGC \
|
||||
-Djava.security.egd=file:/dev/./urandom \
|
||||
-Djava.awt.headless=true \
|
||||
-Dfile.encoding=UTF-8 \
|
||||
-Duser.timezone=Europe/Vienna \
|
||||
-Dspring.backgroundpreinitializer.ignore=true \
|
||||
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus \
|
||||
-Dmanagement.endpoint.health.show-details=always \
|
||||
-Dmanagement.prometheus.metrics.export.enabled=true"
|
||||
|
||||
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS
|
||||
ENV SERVER_PORT=8089
|
||||
ENV LOGGING_LEVEL_ROOT=INFO
|
||||
|
||||
ENTRYPOINT ["tini", "--", "sh", "-c", "\
|
||||
echo 'Starting Results Service with Java ${JAVA_VERSION}...'; \
|
||||
echo 'Service port: ${SERVER_PORT}'; \
|
||||
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
|
||||
echo 'DEBUG mode enabled'; \
|
||||
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5011 -jar app.jar; \
|
||||
else \
|
||||
exec java ${JAVA_OPTS} -jar app.jar; \
|
||||
fi"]
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinJpa)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.series.service.SeriesServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
implementation(libs.bundles.spring.boot.service.complete)
|
||||
implementation(libs.postgresql.driver)
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
||||
// KORREKTUR: Jackson Bundle aufgelöst
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.jackson.datatype.jsr310)
|
||||
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.spring.cloud.starter.consul.discovery)
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.spring.web)
|
||||
|
||||
// KORREKTUR: Resilience Bundle aufgelöst
|
||||
implementation(libs.resilience4j.spring.boot3)
|
||||
implementation(libs.resilience4j.reactor)
|
||||
implementation(libs.spring.boot.starter.aop)
|
||||
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
testImplementation(libs.spring.boot.starter.web)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package at.mocode.series.service
|
||||
|
||||
import at.mocode.series.service.application.SeriesService
|
||||
import at.mocode.series.service.domain.Serie
|
||||
import at.mocode.series.service.domain.SeriePunkt
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@SpringBootApplication
|
||||
class SeriesServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<SeriesServiceApplication>(*args)
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/series")
|
||||
class SeriesController(
|
||||
private val service: SeriesService
|
||||
) {
|
||||
@GetMapping("/")
|
||||
fun health(): String = "Series Service is running"
|
||||
|
||||
@GetMapping
|
||||
fun getAll(): List<Serie> = service.getAllSeries()
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getById(@PathVariable id: String): Serie? = service.getSeriesById(id)
|
||||
|
||||
@PostMapping
|
||||
fun save(@RequestBody serie: Serie): Serie = service.saveSerie(serie)
|
||||
|
||||
@GetMapping("/{id}/stand")
|
||||
fun getStand(@PathVariable id: String) = service.getStand(id)
|
||||
|
||||
@PostMapping("/{id}/punkte")
|
||||
fun addPunkt(@PathVariable id: String, @RequestBody punkt: SeriePunkt): SeriePunkt {
|
||||
return service.addPunkt(punkt.copy(serieId = id))
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package at.mocode.series.service.application
|
||||
|
||||
import at.mocode.series.service.domain.Serie
|
||||
import at.mocode.series.service.domain.SeriePunkt
|
||||
import at.mocode.series.service.persistence.JpaSeriePunktRepository
|
||||
import at.mocode.series.service.persistence.JpaSerieRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class SeriesService(
|
||||
private val serieRepository: JpaSerieRepository,
|
||||
private val punkteRepository: JpaSeriePunktRepository
|
||||
) {
|
||||
|
||||
fun getAllSeries(): List<Serie> = serieRepository.findAll()
|
||||
|
||||
fun getSeriesById(id: String): Serie? = serieRepository.findById(id).orElse(null)
|
||||
|
||||
@Transactional
|
||||
fun saveSerie(serie: Serie): Serie = serieRepository.save(serie)
|
||||
|
||||
fun getStand(serieId: String): Map<Pair<String, String>, Double> {
|
||||
val punkte = punkteRepository.findBySerieId(serieId)
|
||||
|
||||
// Aggregation pro Paar (Reiter, Pferd)
|
||||
return punkte.groupBy { it.reiterId to it.pferdId }
|
||||
.mapValues { (_, v) -> v.sumOf { it.punkte } }
|
||||
.toList()
|
||||
.sortedByDescending { it.second }
|
||||
.toMap()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun addPunkt(punkt: SeriePunkt): SeriePunkt = punkteRepository.save(punkt)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package at.mocode.series.service.domain
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "serien")
|
||||
class Serie(
|
||||
@Id
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
|
||||
@Column(nullable = false)
|
||||
val name: String,
|
||||
|
||||
@Column
|
||||
val beschreibung: String? = null,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
val reglementTyp: ReglementTyp = ReglementTyp.STREICHER_NORMAL,
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "serie_bewerbe", joinColumns = [JoinColumn(name = "serie_id")])
|
||||
@Column(name = "bewerb_id")
|
||||
val bewerbIds: Set<String> = mutableSetOf()
|
||||
)
|
||||
|
||||
enum class ReglementTyp {
|
||||
STREICHER_NORMAL, // z.B. 4 von 6 Wertungen zählen
|
||||
ALLES_ZAEHLT, // Alle Bewerbe zählen
|
||||
MEISTERSCHAFT // Spezielle Gewichtung (z.B. Finale doppelt)
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "serie_punkte")
|
||||
class SeriePunkt(
|
||||
@Id
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
|
||||
@Column(nullable = false)
|
||||
val serieId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
val reiterId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
val pferdId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
val bewerbId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
val punkte: Double,
|
||||
|
||||
@Column(nullable = false)
|
||||
val platzierung: Int
|
||||
)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package at.mocode.series.service.persistence
|
||||
|
||||
import at.mocode.series.service.domain.Serie
|
||||
import at.mocode.series.service.domain.SeriePunkt
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface JpaSerieRepository : JpaRepository<Serie, String>
|
||||
|
||||
@Repository
|
||||
interface JpaSeriePunktRepository : JpaRepository<SeriePunkt, String> {
|
||||
fun findBySerieId(serieId: String): List<SeriePunkt>
|
||||
}
|
||||
Reference in New Issue
Block a user