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

@@ -3,7 +3,6 @@ use std::collections::HashMap;
use crate::message_broker::MessageBroker;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::db::ConnectionPool;
@@ -25,6 +24,7 @@ use crate::worker::sql::{
QUERY_GET_SALARY_TO_PAY,
QUERY_SET_SALARY_PAYED,
QUERY_UPDATE_SATISFACTION,
QUERY_GET_DIRECTOR_USER,
QUERY_COUNT_VEHICLES_IN_BRANCH_REGION,
QUERY_COUNT_VEHICLES_IN_REGION,
QUERY_CHECK_ROUTE,
@@ -35,20 +35,15 @@ use crate::worker::sql::{
QUERY_GET_USER_OFFICES,
QUERY_CUMULATIVE_TAX_NO_EXEMPT,
QUERY_CUMULATIVE_TAX_WITH_EXEMPT,
QUERY_GET_VEHICLES_TO_REPAIR_IN_REGION,
QUERY_REPAIR_VEHICLE,
};
use crate::worker::publish_update_status;
#[derive(Debug, Clone)]
struct Director {
id: i32,
branch_id: i32,
falukant_user_id: i32,
may_produce: bool,
may_sell: bool,
may_start_transport: bool,
may_repair_vehicles: bool,
}
#[derive(Debug, Clone)]
@@ -162,14 +157,6 @@ impl DirectorWorker {
}
for director in directors {
if director.may_repair_vehicles {
if let Err(err) = self.repair_vehicles(&director) {
eprintln!(
"[DirectorWorker] Fehler bei repair_vehicles für Director {}: {err}",
director.id
);
}
}
if director.may_produce {
eprintln!(
"[DirectorWorker] Starte Produktionsprüfung für Director {} (branch_id={})",
@@ -205,106 +192,15 @@ impl DirectorWorker {
Some(Director {
id: row.get("id")?.parse().ok()?,
branch_id: row.get("branch_id")?.parse().ok()?,
falukant_user_id: row.get("falukant_user_id")?.parse().ok()?,
may_produce: row.get("may_produce").map(|v| v == "t" || v == "true").unwrap_or(false),
may_sell: row.get("may_sell").map(|v| v == "t" || v == "true").unwrap_or(false),
may_start_transport: row
.get("may_start_transport")
.map(|v| v == "t" || v == "true")
.unwrap_or(false),
may_repair_vehicles: row
.get("may_repair_vehicles")
.map(|v| v == "t" || v == "true")
.unwrap_or(false),
})
}
fn repair_vehicles(&mut self, director: &Director) -> Result<(), DbError> {
self.base
.set_current_step("DirectorWorker: repair_vehicles");
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Region des Directors/Branches bestimmen
conn.prepare("get_branch_region_for_repair", QUERY_GET_BRANCH_REGION)?;
let rows = conn.execute("get_branch_region_for_repair", &[&director.branch_id])?;
let region_id: i32 = rows
.first()
.and_then(|r| r.get("region_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
if region_id < 0 {
return Ok(());
}
conn.prepare(
"get_vehicles_to_repair",
QUERY_GET_VEHICLES_TO_REPAIR_IN_REGION,
)?;
conn.prepare("repair_vehicle", QUERY_REPAIR_VEHICLE)?;
let candidates =
conn.execute("get_vehicles_to_repair", &[&director.falukant_user_id, &region_id])?;
if candidates.is_empty() {
return Ok(());
}
let mut repaired = 0usize;
for row in candidates {
let vehicle_id = row
.get("vehicle_id")
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
let condition = row
.get("condition")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(100.0);
let type_cost = row
.get("cost")
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);
if vehicle_id < 0 || type_cost <= 0.0 {
continue;
}
// repairCost = round(type.cost * 0.8 * (100 - condition) / 100)
let repair_cost = (type_cost * 0.8 * (100.0 - condition) / 100.0).round();
if repair_cost <= 0.0 {
continue;
}
// Preconditions (wie Backend):
// - sufficientFunds: wir versuchen abzubuchen und überspringen bei Fehler
if let Err(_err) = self.base.change_falukant_user_money(
director.falukant_user_id,
-repair_cost,
&format!("repair vehicle {}", vehicle_id),
) {
continue;
}
// Fahrzeug auf 100 setzen + available_from in Zukunft (build_time_minutes)
let _ = conn.execute("repair_vehicle", &[&vehicle_id])?;
repaired += 1;
}
if repaired > 0 {
eprintln!(
"[DirectorWorker] {} Fahrzeug(e) automatisch zur Reparatur gestartet (director_id={}, region_id={})",
repaired, director.id, region_id
);
// Frontend: Branches/Status neu laden
publish_update_status(&self.base.broker, director.falukant_user_id);
}
Ok(())
}
fn start_productions(&mut self, director: &Director) -> Result<(), DbError> {
self.base
.set_current_step("DirectorWorker: start_productions");
@@ -319,36 +215,10 @@ impl DirectorWorker {
conn.prepare("get_to_produce", QUERY_GET_BEST_PRODUCTION)?;
let rows = conn.execute("get_to_produce", &[&director.id, &director.branch_id])?;
if rows.is_empty() {
// Debug: SQL-Vorschau nur gedrosselt loggen, damit wir die Query testen können
// ohne Log-Flut.
static LAST_EMPTY_PROD_LOG: Mutex<Option<Instant>> = Mutex::new(None);
let mut last = LAST_EMPTY_PROD_LOG.lock().unwrap();
let should_log = last
.map(|t| t.elapsed().as_secs() >= 60)
.unwrap_or(true);
if should_log {
// SQL ggf. kürzen, um Log-Flut zu vermeiden
let mut sql_preview = QUERY_GET_BEST_PRODUCTION.to_string();
const MAX_SQL_PREVIEW: usize = 1200;
if sql_preview.len() > MAX_SQL_PREVIEW {
sql_preview.truncate(MAX_SQL_PREVIEW);
sql_preview.push_str("");
}
eprintln!(
"[DirectorWorker] Keine Produktionskandidaten für Director {} (branch_id={}). Query (get_to_produce): {} | params: director_id={}, branch_id={}",
director.id,
director.branch_id,
sql_preview,
director.id,
director.branch_id
);
*last = Some(Instant::now());
} else {
eprintln!(
"[DirectorWorker] Keine Produktionskandidaten für Director {} gefunden.",
director.id
);
}
eprintln!(
"[DirectorWorker] Keine Produktionskandidaten für Director {} gefunden.",
director.id
);
return Ok(());
}
@@ -432,32 +302,12 @@ impl DirectorWorker {
base_plan.running_productions_quantity = running_productions_quantity;
// Eine neue Produktion starten (max. 100 Stück)
let produced_quantity = match self.create_single_production(&mut conn, &base_plan) {
Ok(qty) => qty,
Err(err) => {
eprintln!(
"[DirectorWorker] Fehler beim Starten einer Produktion: {err}"
);
break;
}
};
// WICHTIG: Wenn wir die gesamte verfügbare Kapazität verwendet haben, breche ab.
// Sonst könnte die nächste Iteration fälschlicherweise noch Platz sehen, wenn
// die gerade gestartete Produktion noch nicht in running_productions_quantity enthalten ist.
if produced_quantity >= free_capacity {
if let Err(err) = self.create_single_production(&mut conn, &base_plan) {
eprintln!(
"[DirectorWorker] Produktion mit {} Stück gestartet, was die gesamte freie Kapazität ({}) ausnutzt. Breche ab.",
produced_quantity, free_capacity
"[DirectorWorker] Fehler beim Starten einer Produktion: {err}"
);
break;
}
// Wenn wir weniger als 10% der freien Kapazität produziert haben, könnte es sein,
// dass wir noch mehr Platz haben. Aber sicherheitshalber brechen wir nach einer
// Produktion ab, um Race Conditions zu vermeiden.
// Die nächste Iteration (beim nächsten Director-Check) wird dann wieder prüfen.
break;
}
Ok(())
@@ -519,15 +369,12 @@ impl DirectorWorker {
&mut self,
conn: &mut DbConnection,
plan: &ProductionPlan,
) -> Result<i32, DbError> {
// WICHTIG: Kapazität direkt aus dem Plan berechnen (wurde gerade in der Schleife aktualisiert)
) -> Result<(), DbError> {
let free_capacity = Self::calc_free_capacity(plan);
let one_piece_cost = Self::calc_one_piece_cost(plan);
let max_money_production = Self::calc_max_money_production(plan, one_piece_cost);
// to_produce darf NIE größer sein als free_capacity, sonst passt es nicht ins Lager
// Zusätzlich: max. 100 Stück pro Produktion, und max. was das Geld erlaubt
let to_produce = free_capacity.min(max_money_production).min(100).max(0);
let to_produce = (free_capacity.min(max_money_production)).clamp(0, 100);
eprintln!(
"[DirectorWorker] Produktionsberechnung: free_capacity={}, one_piece_cost={}, max_money_production={}, to_produce={}, running_productions={}",
@@ -546,16 +393,7 @@ impl DirectorWorker {
plan.running_productions,
plan.running_productions_quantity
);
return Ok(0);
}
// Sicherheitsprüfung: to_produce darf niemals größer sein als free_capacity
if to_produce > free_capacity {
eprintln!(
"[DirectorWorker] FEHLER: to_produce ({}) > free_capacity ({})! Das sollte nicht passieren. Breche Produktion ab.",
to_produce, free_capacity
);
return Ok(0);
return Ok(());
}
let production_cost = to_produce as f64 * one_piece_cost;
@@ -596,8 +434,7 @@ impl DirectorWorker {
);
self.base.broker.publish(message);
// Rückgabe der produzierten Menge, damit die Schleife entscheiden kann, ob sie weiterläuft
Ok(to_produce)
Ok(())
}
fn calc_free_capacity(plan: &ProductionPlan) -> i32 {
@@ -648,8 +485,14 @@ impl DirectorWorker {
// Für alle Items dieses Directors sollten die user_id-Felder identisch
// sein (Arbeitgeber des Directors).
let falukant_user_id = if items.is_empty() {
// User-ID ist bereits über QUERY_GET_DIRECTORS geladen.
director.falukant_user_id
// Wenn keine Items vorhanden sind, müssen wir die user_id anders ermitteln
conn.prepare("get_director_user", QUERY_GET_DIRECTOR_USER)?;
let user_rows = conn.execute("get_director_user", &[&director.id])?;
user_rows
.into_iter()
.next()
.and_then(|row| row.get("employer_user_id").and_then(|v| v.parse::<i32>().ok()))
.ok_or_else(|| DbError::new("Konnte employer_user_id nicht ermitteln"))?
} else {
items[0].user_id
};
@@ -665,7 +508,7 @@ impl DirectorWorker {
let vehicles_in_branch = vehicle_count_rows
.into_iter()
.next()
.and_then(|row| row.get("cnt").and_then(|v| v.parse::<i32>().ok()))
.and_then(|row| row.get("count").and_then(|v| v.parse::<i32>().ok()))
.unwrap_or(0);
// Falls es nichts zu transportieren gibt, prüfe auf leere Transporte
@@ -718,7 +561,7 @@ impl DirectorWorker {
let vehicles_in_branch_after = vehicle_count_rows_after
.into_iter()
.next()
.and_then(|row| row.get("cnt").and_then(|v| v.parse::<i32>().ok()))
.and_then(|row| row.get("count").and_then(|v| v.parse::<i32>().ok()))
.unwrap_or(0);
if vehicles_in_branch_after == 0 {
@@ -768,7 +611,7 @@ impl DirectorWorker {
let vehicles_in_branch_final = vehicle_count_rows_final
.into_iter()
.next()
.and_then(|row| row.get("cnt").and_then(|v| v.parse::<i32>().ok()))
.and_then(|row| row.get("count").and_then(|v| v.parse::<i32>().ok()))
.unwrap_or(0);
if vehicles_in_branch_final == 0 {
@@ -963,38 +806,24 @@ impl DirectorWorker {
return Ok(());
}
// compute piece price and full sell price
let piece_price = Self::compute_piece_sell_price(item);
let revenue = piece_price * item.quantity as f64;
// compute piece price and full sell price
let piece_price = Self::compute_piece_sell_price(item);
let sell_price = piece_price * item.quantity as f64;
let one_piece_cost = Self::resolve_one_piece_cost(conn, item.product_id, item.sell_cost)?;
let cumulative_tax_percent =
Self::get_cumulative_tax_percent(conn, item.branch_id, item.user_id)?;
let cumulative_tax_percent = Self::get_cumulative_tax_percent(conn, item.branch_id, item.user_id)?;
let revenue_cents = (revenue * 100.0).round() as i64;
let cost = one_piece_cost * item.quantity as f64;
let cost_cents = (cost * 100.0).round() as i64;
let revenue_cents = (sell_price * 100.0).round() as i64;
let cost_cents = (one_piece_cost * item.quantity as f64 * 100.0).round() as i64;
let profit_cents = (revenue_cents - cost_cents).max(0);
// Steuer wird vom Gewinn abgezogen (nicht „zugerechnet“)
let tax_cents = ((profit_cents as f64) * cumulative_tax_percent / 100.0).round() as i64;
let payout_cents = revenue_cents - tax_cents;
eprintln!("[DirectorWorker] sell: revenue={:.2}, cost={:.2}, profit_cents={}, tax%={:.2}, tax_cents={}, payout_cents={}", revenue, cost, profit_cents, cumulative_tax_percent, tax_cents, payout_cents);
// Treasury-User-ID optional per ENV, fallback auf DEFAULT_TREASURY_USER_ID
let treasury_user_id: i32 = std::env::var("TREASURY_FALUKANT_USER_ID")
.ok()
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(DEFAULT_TREASURY_USER_ID);
eprintln!("[DirectorWorker] sell: revenue={:.2}, cost={:.2}, profit_cents={}, tax%={:.2}, tax_cents={}, payout_cents={}", sell_price, one_piece_cost * item.quantity as f64, profit_cents, cumulative_tax_percent, tax_cents, payout_cents);
if tax_cents > 0 {
let tax_amount = (tax_cents as f64) / 100.0;
if let Err(err) = self.base.change_falukant_user_money(
treasury_user_id,
tax_amount,
&format!("tax from sale product {}", item.product_id),
) {
if let Err(err) = self.base.change_falukant_user_money(DEFAULT_TREASURY_USER_ID, tax_amount, &format!("tax from sale product {}", item.product_id)) {
eprintln!("[DirectorWorker] Fehler bei change_falukant_user_money (tax): {err}");
}
}
@@ -1008,7 +837,7 @@ impl DirectorWorker {
eprintln!(
"[DirectorWorker] sell: user_id={}, revenue={:.2}, tax={:.2}, payout={:.2}, product_id={}",
item.user_id,
revenue,
sell_price,
(tax_cents as f64) / 100.0,
payout_amount,
item.product_id
@@ -1221,7 +1050,7 @@ impl DirectorWorker {
let vehicle_count = vehicle_count_rows
.into_iter()
.next()
.and_then(|row| row.get("cnt").and_then(|v| v.parse::<i32>().ok()))
.and_then(|row| row.get("count").and_then(|v| v.parse::<i32>().ok()))
.unwrap_or(0);
eprintln!(
@@ -1239,7 +1068,7 @@ impl DirectorWorker {
let route_exists = route_rows
.into_iter()
.next()
.and_then(|row| row.get("1").and_then(|v| v.parse::<i32>().ok()))
.and_then(|row| row.get("count").and_then(|v| v.parse::<i32>().ok()))
.unwrap_or(0) > 0;
eprintln!(