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:
2026-04-12 16:58:22 +02:00
parent 4ad9b274e8
commit 6e99bc97fd
20 changed files with 711 additions and 25 deletions
@@ -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)
}
@@ -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))
}
}
@@ -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)
}
@@ -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
)
@@ -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>
}