Update dependencies and enhance WebSocket server logging: Add 'chrono' and 'android_system_properties' to Cargo.lock, improve error handling and logging in websocket_server.rs, and streamline character creation notifications in worker modules for better clarity and maintainability.

This commit is contained in:
Torsten Schulz (local)
2026-01-28 14:21:28 +01:00
parent 2ac474fe0c
commit c9e0781b61
14 changed files with 1174 additions and 1814 deletions

View File

@@ -4,6 +4,7 @@ use std::collections::HashSet;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use chrono::{Local, Timelike};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
@@ -22,6 +23,15 @@ use crate::worker::sql::{
QUERY_GET_USERS_WITH_FILLED_OFFICES,
QUERY_PROCESS_ELECTIONS,
QUERY_TRIM_EXCESS_OFFICES_GLOBAL,
QUERY_FIND_AVAILABLE_CHURCH_OFFICES,
QUERY_FIND_CHURCH_SUPERVISOR,
QUERY_GET_CHURCH_OFFICE_REQUIREMENTS,
QUERY_GET_PENDING_CHURCH_APPLICATIONS,
QUERY_CHECK_CHARACTER_ELIGIBILITY,
QUERY_APPROVE_CHURCH_APPLICATION,
QUERY_REJECT_CHURCH_APPLICATION,
QUERY_CREATE_CHURCH_APPLICATION_JOB,
QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE,
};
pub struct PoliticsWorker {
@@ -57,6 +67,32 @@ struct Office {
region_id: i32,
}
#[derive(Debug, Clone)]
struct AvailableChurchOffice {
office_type_id: i32,
seats_per_region: i32,
region_id: i32,
occupied_seats: i32,
}
#[derive(Debug, Clone)]
struct ChurchSupervisor {
supervisor_character_id: i32,
}
#[derive(Debug, Clone)]
struct ChurchOfficeRequirement {
prerequisite_office_type_id: Option<i32>,
min_title_level: Option<i32>,
}
#[derive(Debug, Clone)]
struct ChurchApplication {
application_id: i32,
office_type_id: i32,
applicant_character_id: i32,
}
// --- SQL-Konstanten (1:1 aus politics_worker.h übernommen) ------------------
@@ -69,6 +105,7 @@ impl PoliticsWorker {
fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc<WorkerState>) {
let mut last_execution: Option<Instant> = None;
let mut last_church_office_run: Option<Instant> = None;
while state.running_worker.load(Ordering::Relaxed) {
let now = Instant::now();
@@ -84,6 +121,25 @@ impl PoliticsWorker {
last_execution = Some(now);
}
// Church Office Job um 13 Uhr
if Self::is_time_13_00() {
let should_run_church = match last_church_office_run {
None => true,
Some(prev) => {
// Prüfe ob seit letztem Lauf mindestens 23 Stunden vergangen sind
// (um sicherzustellen, dass es nur einmal pro Tag läuft)
now.saturating_duration_since(prev) >= Duration::from_secs(23 * 3600)
}
};
if should_run_church {
if let Err(err) = Self::perform_church_office_task(&pool, &broker) {
eprintln!("[PoliticsWorker] Fehler bei performChurchOfficeTask: {err}");
}
last_church_office_run = Some(now);
}
}
// Entspricht ungefähr der 5-Sekunden-Schleife im C++-Code
for _ in 0..5 {
if !state.running_worker.load(Ordering::Relaxed) {
@@ -94,6 +150,14 @@ impl PoliticsWorker {
}
}
/// Prüft ob die aktuelle Uhrzeit 13:00 ist (mit Toleranz von ±1 Minute)
fn is_time_13_00() -> bool {
let now = Local::now();
let hour = now.hour();
let minute = now.minute();
hour == 13 && minute <= 1
}
fn perform_daily_politics_task(
pool: &ConnectionPool,
broker: &MessageBroker,
@@ -553,6 +617,415 @@ impl PoliticsWorker {
.filter_map(map_row_to_office)
.collect())
}
/// Verarbeitet Church Office Jobs um 13 Uhr:
/// - Findet verfügbare Positionen
/// - Verarbeitet bestehende Bewerbungen
/// - Erstellt neue Bewerbungen falls nötig
fn perform_church_office_task(
pool: &ConnectionPool,
broker: &MessageBroker,
) -> Result<(), DbError> {
eprintln!("[PoliticsWorker] Starte Church Office Task um 13 Uhr");
// 1) Verfügbare Church Office Positionen finden
let available_offices = Self::find_available_church_offices(pool)?;
eprintln!(
"[PoliticsWorker] Gefunden: {} verfügbare Church Office Positionen",
available_offices.len()
);
// 2) Für jede verfügbare Position Bewerbungen verarbeiten
for office in &available_offices {
// Supervisor finden
if let Some(supervisor) = Self::find_church_supervisor(pool, office.region_id, office.office_type_id)? {
// Bestehende Bewerbungen für diesen Supervisor verarbeiten
Self::process_church_applications(pool, broker, supervisor.supervisor_character_id)?;
// Falls noch Plätze frei sind, neue Bewerbungen erstellen
let remaining_seats = office.seats_per_region - office.occupied_seats;
if remaining_seats > 0 {
Self::create_church_application_jobs(
pool,
office.office_type_id,
office.region_id,
supervisor.supervisor_character_id,
remaining_seats,
)?;
}
} else {
eprintln!(
"[PoliticsWorker] Kein Supervisor gefunden für office_type_id={}, region_id={}",
office.office_type_id, office.region_id
);
}
}
eprintln!("[PoliticsWorker] Church Office Task abgeschlossen");
Ok(())
}
fn find_available_church_offices(
pool: &ConnectionPool,
) -> Result<Vec<AvailableChurchOffice>, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare(
"find_available_church_offices",
QUERY_FIND_AVAILABLE_CHURCH_OFFICES,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare find_available_church_offices: {e}"
))
})?;
let rows = conn
.execute("find_available_church_offices", &[])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec find_available_church_offices: {e}"
))
})?;
let mut offices = Vec::new();
for row in rows {
let office_type_id = parse_i32(&row, "office_type_id", -1);
let region_id = parse_i32(&row, "region_id", -1);
let seats_per_region = parse_i32(&row, "seats_per_region", 0);
let occupied_seats = parse_i32(&row, "occupied_seats", 0);
if office_type_id >= 0 && region_id >= 0 {
offices.push(AvailableChurchOffice {
office_type_id,
seats_per_region,
region_id,
occupied_seats,
});
}
}
Ok(offices)
}
fn find_church_supervisor(
pool: &ConnectionPool,
region_id: i32,
office_type_id: i32,
) -> Result<Option<ChurchSupervisor>, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("find_church_supervisor", QUERY_FIND_CHURCH_SUPERVISOR)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare find_church_supervisor: {e}"
))
})?;
let rows = conn
.execute("find_church_supervisor", &[&region_id, &office_type_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec find_church_supervisor: {e}"
))
})?;
for row in rows {
let supervisor_character_id = parse_i32(&row, "supervisor_character_id", -1);
if supervisor_character_id >= 0 {
return Ok(Some(ChurchSupervisor {
supervisor_character_id,
}));
}
}
Ok(None)
}
fn process_church_applications(
pool: &ConnectionPool,
broker: &MessageBroker,
supervisor_id: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Bewerbungen für diesen Supervisor abrufen
conn.prepare(
"get_pending_church_applications",
QUERY_GET_PENDING_CHURCH_APPLICATIONS,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare get_pending_church_applications: {e}"
))
})?;
let rows = conn
.execute("get_pending_church_applications", &[&supervisor_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_pending_church_applications: {e}"
))
})?;
let mut applications = Vec::new();
for row in rows {
let application_id = parse_i32(&row, "application_id", -1);
let office_type_id = parse_i32(&row, "office_type_id", -1);
let applicant_character_id = parse_i32(&row, "applicant_character_id", -1);
if application_id >= 0 {
applications.push(ChurchApplication {
application_id,
office_type_id,
applicant_character_id,
});
}
}
// Voraussetzungen prüfen und Bewerbungen verarbeiten
conn.prepare(
"get_church_office_requirements",
QUERY_GET_CHURCH_OFFICE_REQUIREMENTS,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare get_church_office_requirements: {e}"
))
})?;
conn.prepare(
"check_character_eligibility",
QUERY_CHECK_CHARACTER_ELIGIBILITY,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare check_character_eligibility: {e}"
))
})?;
conn.prepare("approve_church_application", QUERY_APPROVE_CHURCH_APPLICATION)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare approve_church_application: {e}"
))
})?;
conn.prepare("reject_church_application", QUERY_REJECT_CHURCH_APPLICATION)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare reject_church_application: {e}"
))
})?;
for app in &applications {
// Voraussetzungen für dieses Amt abrufen
let req_rows = conn
.execute("get_church_office_requirements", &[&app.office_type_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_church_office_requirements: {e}"
))
})?;
let mut requirements = Vec::new();
for req_row in req_rows {
let prerequisite_office_type_id = req_row
.get("prerequisite_office_type_id")
.and_then(|v| v.parse::<i32>().ok());
let min_title_level = req_row
.get("min_title_level")
.and_then(|v| v.parse::<i32>().ok());
requirements.push(ChurchOfficeRequirement {
prerequisite_office_type_id,
min_title_level,
});
}
// Prüfe ob Character die Voraussetzungen erfüllt
let mut eligible = true;
for req in &requirements {
let elig_rows = conn
.execute(
"check_character_eligibility",
&[
&app.applicant_character_id,
&req.prerequisite_office_type_id,
&req.min_title_level,
],
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec check_character_eligibility: {e}"
))
})?;
for elig_row in elig_rows {
let has_prerequisite: bool = elig_row
.get("has_prerequisite")
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false);
let meets_title_requirement: bool = elig_row
.get("meets_title_requirement")
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(false);
if !has_prerequisite || !meets_title_requirement {
eligible = false;
break;
}
}
}
// Bewerbung genehmigen oder ablehnen
if eligible {
let approve_rows = conn
.execute("approve_church_application", &[&app.application_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec approve_church_application: {e}"
))
})?;
if !approve_rows.is_empty() {
eprintln!(
"[PoliticsWorker] Church Application {} genehmigt (office_type_id={}, character_id={})",
app.application_id, app.office_type_id, app.applicant_character_id
);
// Benachrichtigung senden
if let Some(user_id) = Self::get_user_id_for_character(pool, app.applicant_character_id)? {
let msg = format!(
r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#,
user_id
);
broker.publish(msg);
}
}
} else {
conn.execute("reject_church_application", &[&app.application_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec reject_church_application: {e}"
))
})?;
eprintln!(
"[PoliticsWorker] Church Application {} abgelehnt (Voraussetzungen nicht erfüllt)",
app.application_id
);
}
}
Ok(())
}
fn create_church_application_jobs(
pool: &ConnectionPool,
office_type_id: i32,
region_id: i32,
supervisor_id: i32,
count: i32,
) -> Result<(), DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Charaktere für diese Region finden
conn.prepare(
"get_characters_for_church_office",
QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare get_characters_for_church_office: {e}"
))
})?;
let rows = conn
.execute("get_characters_for_church_office", &[&region_id, &count])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_characters_for_church_office: {e}"
))
})?;
conn.prepare(
"create_church_application_job",
QUERY_CREATE_CHURCH_APPLICATION_JOB,
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] prepare create_church_application_job: {e}"
))
})?;
let mut created = 0;
for row in rows {
if let Some(character_id) = row
.get("character_id")
.and_then(|v| v.parse::<i32>().ok())
{
let app_rows = conn
.execute(
"create_church_application_job",
&[&office_type_id, &character_id, &region_id, &supervisor_id],
)
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec create_church_application_job: {e}"
))
})?;
if !app_rows.is_empty() {
created += 1;
eprintln!(
"[PoliticsWorker] Church Application Job erstellt: office_type_id={}, character_id={}, region_id={}",
office_type_id, character_id, region_id
);
}
}
}
eprintln!(
"[PoliticsWorker] {} Church Application Jobs erstellt für office_type_id={}, region_id={}",
created, office_type_id, region_id
);
Ok(())
}
fn get_user_id_for_character(
pool: &ConnectionPool,
character_id: i32,
) -> Result<Option<i32>, DbError> {
let mut conn = pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
let query = "SELECT user_id FROM falukant_data.character WHERE id = $1";
conn.prepare("get_user_id_for_character", query)
.map_err(|e| DbError::new(format!("[PoliticsWorker] prepare get_user_id_for_character: {e}")))?;
let rows = conn
.execute("get_user_id_for_character", &[&character_id])
.map_err(|e| {
DbError::new(format!(
"[PoliticsWorker] exec get_user_id_for_character: {e}"
))
})?;
for row in rows {
if let Some(user_id) = row.get("user_id").and_then(|v| v.parse::<i32>().ok()) {
return Ok(Some(user_id));
}
}
Ok(None)
}
}
impl Worker for PoliticsWorker {