Füge SQL-Abfragen für politische Ämter hinzu und implementiere Normalisierung bestehender Ämter
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 4m1s

This commit is contained in:
Torsten Schulz (local)
2026-08-28 07:09:24 +02:00
parent fb5c1eb8be
commit 1ff4925b88
2 changed files with 166 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState}; use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::falukant_certificate::political_office_name_rank;
use crate::worker::sql::{ use crate::worker::sql::{
QUERY_COUNT_OFFICES_PER_REGION, QUERY_COUNT_OFFICES_PER_REGION,
QUERY_FIND_OFFICE_GAPS, QUERY_FIND_OFFICE_GAPS,
@@ -22,6 +23,9 @@ use crate::worker::sql::{
QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS, QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS,
QUERY_GET_USERS_WITH_FILLED_OFFICES, QUERY_GET_USERS_WITH_FILLED_OFFICES,
QUERY_PROCESS_ELECTIONS, 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_PLAYER_ELECTION_RESULT_ROWS,
QUERY_INSERT_NOTIFICATION, QUERY_INSERT_NOTIFICATION,
QUERY_TRIM_EXCESS_OFFICES_GLOBAL, QUERY_TRIM_EXCESS_OFFICES_GLOBAL,
@@ -143,6 +147,8 @@ impl PoliticsWorker {
eprintln!("[PoliticsWorker] Fehler bei perform_church_office_task: {err}"); eprintln!("[PoliticsWorker] Fehler bei perform_church_office_task: {err}");
} }
Self::normalize_existing_political_offices(pool)?;
// 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur) // 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur)
Self::evaluate_political_positions(pool)?; Self::evaluate_political_positions(pool)?;
@@ -169,7 +175,11 @@ impl PoliticsWorker {
// 6) Bereits faellige Wahlen auswerten und neu besetzte Aemter melden. // 6) Bereits faellige Wahlen auswerten und neu besetzte Aemter melden.
// Wichtig: Vor dem Anlegen neuer Wahlen ausfuehren, damit frisch // Wichtig: Vor dem Anlegen neuer Wahlen ausfuehren, damit frisch
// angelegte Wahlen nicht im selben Tageslauf wieder verarbeitet werden. // 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) { if let Err(e) = Self::notify_player_election_results(pool, broker) {
eprintln!("[PoliticsWorker] notify_player_election_results: {e}"); eprintln!("[PoliticsWorker] notify_player_election_results: {e}");
} }
@@ -616,6 +626,126 @@ impl PoliticsWorker {
.collect()) .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<i32, Vec<(i32, i32, i32)>> = 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<i32, i32> = 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<Office>,
) -> Result<Vec<Office>, 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> { fn enforce_election_lead_time(pool: &ConnectionPool) -> Result<(), DbError> {
let mut conn = pool let mut conn = pool
.get() .get()

View File

@@ -1412,6 +1412,41 @@ pub const QUERY_PROCESS_ELECTIONS: &str = r#"
FROM falukant_data.process_elections(); 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`). /// 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). /// Läuft nach `process_elections()`; Deduplizierung über bestehende `election_result`-Notifications (`tr` JSON).
pub const QUERY_PLAYER_ELECTION_RESULT_ROWS: &str = r#" pub const QUERY_PLAYER_ELECTION_RESULT_ROWS: &str = r#"