From 1ff4925b88b36aa9886240434d1dab9d04c420ac Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 28 Aug 2026 07:09:24 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCge=20SQL-Abfragen=20f=C3=BCr=20politisch?= =?UTF-8?q?e=20=C3=84mter=20hinzu=20und=20implementiere=20Normalisierung?= =?UTF-8?q?=20bestehender=20=C3=84mter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/worker/politics.rs | 132 ++++++++++++++++++++++++++++++++++++++++- src/worker/sql.rs | 35 +++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/src/worker/politics.rs b/src/worker/politics.rs index 78e276c..0f4aa9b 100755 --- a/src/worker/politics.rs +++ b/src/worker/politics.rs @@ -6,6 +6,7 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; +use crate::worker::falukant_certificate::political_office_name_rank; use crate::worker::sql::{ QUERY_COUNT_OFFICES_PER_REGION, QUERY_FIND_OFFICE_GAPS, @@ -22,6 +23,9 @@ use crate::worker::sql::{ QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS, QUERY_GET_USERS_WITH_FILLED_OFFICES, QUERY_PROCESS_ELECTIONS, + QUERY_GET_POLITICAL_OFFICES_FOR_CHARACTER, + QUERY_GET_ALL_POLITICAL_OFFICES, + QUERY_REMOVE_POLITICAL_OFFICE_BY_ID, QUERY_PLAYER_ELECTION_RESULT_ROWS, QUERY_INSERT_NOTIFICATION, QUERY_TRIM_EXCESS_OFFICES_GLOBAL, @@ -143,6 +147,8 @@ impl PoliticsWorker { eprintln!("[PoliticsWorker] Fehler bei perform_church_office_task: {err}"); } + Self::normalize_existing_political_offices(pool)?; + // 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur) Self::evaluate_political_positions(pool)?; @@ -169,7 +175,11 @@ impl PoliticsWorker { // 6) Bereits faellige Wahlen auswerten und neu besetzte Aemter melden. // Wichtig: Vor dem Anlegen neuer Wahlen ausfuehren, damit frisch // angelegte Wahlen nicht im selben Tageslauf wieder verarbeitet werden. - let new_offices_from_elections = Self::process_elections(pool)?; + let new_offices_from_elections = + Self::normalize_elected_political_offices(pool, Self::process_elections(pool)?)?; + // Beförderungen geben das vorherige Amt wieder frei; die Ersatzwahl + // wird noch in diesem Tageslauf angelegt. + Self::sync_offices_with_types(pool)?; if let Err(e) = Self::notify_player_election_results(pool, broker) { eprintln!("[PoliticsWorker] notify_player_election_results: {e}"); } @@ -616,6 +626,126 @@ impl PoliticsWorker { .collect()) } + /// Bereinigt Bestandsdaten nach derselben Regel wie neue Wahlergebnisse. + /// Für mehrere Einträge desselben Amts bleibt der jüngste (größte ID) übrig. + fn normalize_existing_political_offices(pool: &ConnectionPool) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare( + "get_all_political_offices", + QUERY_GET_ALL_POLITICAL_OFFICES, + )?; + conn.prepare( + "remove_political_office_by_id", + QUERY_REMOVE_POLITICAL_OFFICE_BY_ID, + )?; + let rows = conn.execute("get_all_political_offices", &[])?; + + let mut by_character: HashMap> = HashMap::new(); + for row in rows { + let character_id = parse_i32(&row, "character_id", -1); + let office_id = parse_i32(&row, "office_id", -1); + let office_type_id = parse_i32(&row, "office_type_id", -1); + if character_id < 0 || office_id < 0 || office_type_id < 0 { + continue; + } + let rank = political_office_name_rank( + row.get("office_name").map(String::as_str).unwrap_or(""), + ); + by_character + .entry(character_id) + .or_default() + .push((office_id, office_type_id, rank)); + } + + for offices in by_character.values() { + let highest_rank = offices.iter().map(|(_, _, rank)| *rank).max().unwrap_or(0); + let mut newest_by_type: HashMap = HashMap::new(); + for (office_id, office_type_id, _) in offices { + newest_by_type + .entry(*office_type_id) + .and_modify(|current| *current = (*current).max(*office_id)) + .or_insert(*office_id); + } + + for (office_id, office_type_id, rank) in offices { + let duplicate = newest_by_type.get(office_type_id) != Some(office_id); + if *rank < highest_rank || duplicate { + conn.execute("remove_political_office_by_id", &[office_id])?; + } + } + } + + Ok(()) + } + + /// Wendet die Ein-Amts-Regel auf jedes soeben durch eine Wahl erzeugte Amt an. + /// Höhere Ämter ersetzen niedrigere, Wiederwahlen ersetzen das alte gleiche + /// Amt und eine niedrigere Wahl wird nicht als zusätzliches Amt behalten. + fn normalize_elected_political_offices( + pool: &ConnectionPool, + elected_offices: Vec, + ) -> Result, DbError> { + let mut retained = Vec::new(); + + for elected in elected_offices { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + conn.prepare( + "get_character_political_offices", + QUERY_GET_POLITICAL_OFFICES_FOR_CHARACTER, + )?; + conn.prepare( + "remove_political_office_by_id", + QUERY_REMOVE_POLITICAL_OFFICE_BY_ID, + )?; + + let offices = conn.execute("get_character_political_offices", &[&elected.character_id])?; + let Some(new_row) = offices.iter().find(|row| { + parse_i32(row, "office_id", -1) == elected.office_id + }) else { + continue; + }; + let new_rank = political_office_name_rank( + new_row.get("office_name").map(String::as_str).unwrap_or(""), + ); + + let mut remove_new_office = false; + let mut offices_to_remove = Vec::new(); + for existing in &offices { + let existing_id = parse_i32(existing, "office_id", -1); + if existing_id < 0 || existing_id == elected.office_id { + continue; + } + let existing_type_id = parse_i32(existing, "office_type_id", -1); + let existing_rank = political_office_name_rank( + existing.get("office_name").map(String::as_str).unwrap_or(""), + ); + + if existing_type_id == elected.office_type_id || existing_rank < new_rank { + offices_to_remove.push(existing_id); + } else if existing_rank > new_rank { + remove_new_office = true; + break; + } + } + + if remove_new_office { + conn.execute("remove_political_office_by_id", &[&elected.office_id])?; + continue; + } + + for office_id in offices_to_remove { + conn.execute("remove_political_office_by_id", &[&office_id])?; + } + retained.push(elected); + } + + Ok(retained) + } + fn enforce_election_lead_time(pool: &ConnectionPool) -> Result<(), DbError> { let mut conn = pool .get() diff --git a/src/worker/sql.rs b/src/worker/sql.rs index 2c835e2..879d192 100755 --- a/src/worker/sql.rs +++ b/src/worker/sql.rs @@ -1412,6 +1412,41 @@ pub const QUERY_PROCESS_ELECTIONS: &str = r#" FROM falukant_data.process_elections(); "#; +/// Aktuelle politische Ämter eines Charakters; die Rangfolge wird im Daemon +/// einheitlich über `political_office_name_rank` bestimmt. +pub const QUERY_GET_POLITICAL_OFFICES_FOR_CHARACTER: &str = r#" + SELECT po.id AS office_id, + po.office_type_id, + COALESCE(pot.name, '') AS office_name + FROM falukant_data.political_office po + JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id + WHERE po.character_id = $1::int; +"#; + +pub const QUERY_GET_ALL_POLITICAL_OFFICES: &str = r#" + SELECT po.id AS office_id, + po.character_id, + po.office_type_id, + COALESCE(pot.name, '') AS office_name + FROM falukant_data.political_office po + JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id + ORDER BY po.character_id, po.id; +"#; + +/// Entfernt ein einzelnes Amt bei Beförderung bzw. unzulässiger niedriger Wahl +/// und schreibt es wie alle regulären Amtsenden in die Historie. +pub const QUERY_REMOVE_POLITICAL_OFFICE_BY_ID: &str = r#" + WITH removed AS ( + DELETE FROM falukant_data.political_office + WHERE id = $1::int + RETURNING character_id, office_type_id, region_id, created_at + ) + INSERT INTO falukant_log.political_office_history + (character_id, office_type_id, region_id, start_date, end_date, created_at, updated_at) + SELECT character_id, office_type_id, region_id, created_at, NOW(), NOW(), NOW() + FROM removed; +"#; + /// Wahlergebnis für Spieler-Kandidaten (`falukant_data.candidate` + `character.user_id IS NOT NULL`). /// Läuft nach `process_elections()`; Deduplizierung über bestehende `election_result`-Notifications (`tr` JSON). pub const QUERY_PLAYER_ELECTION_RESULT_ROWS: &str = r#"