feat: unterstütze Einzeldatei-Import, verbessere Fortschrittsanzeige und Logging im ZNS-Import

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-16 23:45:18 +02:00
parent 26b3b193ca
commit a1194adeac
8 changed files with 271 additions and 40 deletions
@@ -67,8 +67,8 @@ class ZnsImportService(
if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
// Wir lesen den Stream direkt zeilenweise mit dem korrekten Encoding
val reader = zip.bufferedReader(CP850)
val lines = mutableListOf<String>()
val reader = zip.bufferedReader(CP850)
// WICHTIG: Wir dürfen den Reader NICHT schließen (use), da sonst der ZipInputStream geschlossen wird!
var line = reader.readLine()
@@ -78,17 +78,103 @@ class ZnsImportService(
}
line = reader.readLine()
}
println("[DEBUG_LOG] Datei $fileName extrahiert: ${lines.size} Zeilen")
dateien[fileName] = lines
}
zip.closeEntry()
entry = zip.nextEntry
}
} finally {
// Wir schließen den ZipInputStream NICHT hier, sondern überlassen es dem Aufrufer
} catch (e: Exception) {
println("[DEBUG_LOG] Fehler beim Extrahieren der ZIP (eventuell keine ZIP-Datei?): ${e.message}")
}
return dateien
}
/**
* Importiert ZNS-Daten aus einem Stream. Erkennt automatisch, ob es eine ZIP oder eine DAT ist.
*/
suspend fun importiereStream(
inputStream: InputStream,
fileName: String,
mode: ZnsImportMode = ZnsImportMode.FULL
): ZnsImportResult {
val upperName = fileName.uppercase()
return if (upperName.endsWith(".ZIP")) {
importiereZip(inputStream, mode)
} else if (upperName.endsWith(".DAT")) {
importiereEinzelDatei(inputStream, upperName, mode)
} else {
ZnsImportResult(fehler = listOf("Dateiformat nicht unterstützt: $fileName"))
}
}
private suspend fun importiereEinzelDatei(
inputStream: InputStream,
fileName: String,
mode: ZnsImportMode
): ZnsImportResult {
println("[DEBUG_LOG] Importiere Einzeldatei: $fileName")
val lines = inputStream.bufferedReader(CP850).readLines().filter { it.isNotBlank() }
println("[DEBUG_LOG] Einzeldatei $fileName hat ${lines.size} Zeilen")
val fehler = mutableListOf<String>()
val warnungen = mutableListOf<String>()
var vereineImportiert = 0
var vereineAktualisiert = 0
var reiterImportiert = 0
var reiterAktualisiert = 0
var pferdeImportiert = 0
var pferdeAktualisiert = 0
var richterImportiert = 0
var richterAktualisiert = 0
when (fileName) {
FILE_VEREIN -> {
val (n, u) = importiereVereine(lines, fehler)
vereineImportiert = n
vereineAktualisiert = u
}
FILE_LIZENZ -> {
val (n, u) = importiereReiter(lines, fehler, warnungen)
reiterImportiert = n
reiterAktualisiert = u
}
FILE_PFERDE -> {
if (mode == ZnsImportMode.FULL) {
val (n, u) = importierePferde(lines, fehler)
pferdeImportiert = n
pferdeAktualisiert = u
}
}
FILE_RICHT -> {
if (mode == ZnsImportMode.FULL) {
val (n, u) = importiereFunktionaere(lines, fehler, warnungen)
richterImportiert = n
richterAktualisiert = u
}
}
else -> fehler.add("Unbekannte DAT-Datei: $fileName")
}
return ZnsImportResult(
vereineImportiert = vereineImportiert,
vereineAktualisiert = vereineAktualisiert,
reiterImportiert = reiterImportiert,
reiterAktualisiert = reiterAktualisiert,
pferdeImportiert = pferdeImportiert,
pferdeAktualisiert = pferdeAktualisiert,
richterImportiert = richterImportiert,
richterAktualisiert = richterAktualisiert,
fehler = fehler,
warnungen = warnungen
)
}
/**
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
*
@@ -101,8 +187,8 @@ class ZnsImportService(
mode: ZnsImportMode = ZnsImportMode.FULL
): ZnsImportResult {
val dateien = extrahiereDateien(zipInputStream)
// println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}")
// dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
println("[DEBUG_LOG] Gefundene Dateien im ZIP: ${dateien.keys}")
dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
val fehler = mutableListOf<String>()
val warnungen = mutableListOf<String>()
@@ -151,7 +237,11 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val verein = ZnsVereinParser.parse(zeile) ?: return@forEachIndexed
val verein = ZnsVereinParser.parse(zeile)
if (verein == null) {
if (index < 5) println("[DEBUG_LOG] Parser lieferte null für Zeile ${index + 1}: '$zeile'")
return@forEachIndexed
}
val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer)
if (vorhanden == null) {
vereinRepository.save(verein)
@@ -186,7 +276,11 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val parsed = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed
val parsed = ZnsReiterParser.parse(zeile)
if (parsed == null) {
if (index < 5) println("[DEBUG_LOG] Reiter-Parser lieferte null für Zeile ${index + 1}: '$zeile'")
return@forEachIndexed
}
// Relationen auflösen
val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) }
@@ -31,7 +31,7 @@ class ZnsImportController(
@RequestParam("mode", defaultValue = "FULL") mode: ZnsImportMode
): ResponseEntity<ImportStartResponse> {
val job = jobRegistry.erstelleJob()
orchestrator.starteImport(job.jobId, file.bytes, mode)
orchestrator.starteImport(job.jobId, file.bytes, file.originalFilename ?: "zns_import.zip", mode)
return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId))
}
@@ -13,10 +13,8 @@ import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
@Configuration
@Profile("dev")
class ZnsImportDatabaseConfiguration(
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
@Value("\${spring.datasource.username}") private val username: String,
@@ -26,23 +24,27 @@ class ZnsImportDatabaseConfiguration(
@PostConstruct
fun initializeDatabase() {
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service...")
Database.connect(jdbcUrl, user = username, password = password)
transaction {
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
VereinTable,
ReiterTable,
HorseTable,
FunktionaerTable,
FunktionaersQualifikationenTable,
FunktionaerQualifikationTable,
ReitLizenzenTable,
FahrLizenzenTable,
StartkartenTable,
ReiterLizenzenZuordnungTable
)
statements.forEach { exec(it) }
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)
log.info("Initialisiere Datenbank-Schema für ZNS-Import-Service (JDBC: {})...", jdbcUrl)
try {
Database.connect(jdbcUrl, user = username, password = password)
transaction {
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
VereinTable,
ReiterTable,
HorseTable,
FunktionaerTable,
FunktionaersQualifikationenTable,
FunktionaerQualifikationTable,
ReitLizenzenTable,
FahrLizenzenTable,
StartkartenTable,
ReiterLizenzenZuordnungTable
)
statements.forEach { exec(it) }
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)
}
} catch (e: Exception) {
log.error("Fehler bei der Datenbank-Initialisierung: {}", e.message, e)
}
}
}
@@ -19,16 +19,22 @@ class ZnsImportOrchestrator(
) {
private val scope = CoroutineScope(Dispatchers.IO)
fun starteImport(jobId: String, zipBytes: ByteArray, mode: ZnsImportMode = ZnsImportMode.FULL) {
fun starteImport(jobId: String, bytes: ByteArray, fileName: String, mode: ZnsImportMode = ZnsImportMode.FULL) {
scope.launch {
runCatching {
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5)
println("[DEBUG_LOG] Starte Import Job $jobId (File: $fileName, Size: ${bytes.size} bytes)")
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Bereite Datei vor...", 5)
// Archivierung
archiviereZip(zipBytes)
archiviereDatei(bytes, fileName)
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20)
val result = service.importiereZip(zipBytes.inputStream(), mode)
val result = service.importiereStream(bytes.inputStream(), fileName, mode)
println("[DEBUG_LOG] Import Ergebnis: ${result.zusammenfassung()}")
if (result.fehler.isNotEmpty()) {
println("[DEBUG_LOG] Fehler im Import: ${result.fehler.joinToString()}")
}
jobRegistry.aktualisiereStatus(
jobId, ImportJobStatus.ABGESCHLOSSEN,
@@ -40,20 +46,27 @@ class ZnsImportOrchestrator(
job.warnungen.addAll(result.warnungen)
}
}.onFailure { ex ->
println("[DEBUG_LOG] Kritischer Fehler im ZnsImportOrchestrator: ${ex.message}")
ex.printStackTrace()
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.FEHLER, "Fehler: ${ex.message}")
jobRegistry.findeJob(jobId)?.fehler?.add(ex.message ?: "Unbekannter Fehler")
}
}
}
private fun archiviereZip(bytes: ByteArray) {
private fun archiviereDatei(bytes: ByteArray, originalFileName: String) {
try {
val dir = File(archivePath)
if (!dir.exists()) dir.mkdirs()
if (!dir.exists()) {
val success = dir.mkdirs()
println("[DEBUG_LOG] Archiv-Verzeichnis erstellt ($archivePath): $success")
}
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
val archiveFile = File(dir, "zns_import_$timestamp.zip")
val extension = originalFileName.substringAfterLast(".", "bin")
val archiveFile = File(dir, "zns_import_${timestamp}.$extension")
archiveFile.writeBytes(bytes)
println("[DEBUG_LOG] Datei archiviert: ${archiveFile.absolutePath}")
} catch (e: Exception) {
// Archivierung schlägt fehl -> Loggen aber Import nicht abbrechen
println("[WARN] Archivierung der ZNS-Datei fehlgeschlagen: ${e.message}")