1 Commits

Author SHA1 Message Date
5eb98f33c9 Update Rust crate rand to 0.9 2025-12-19 16:14:03 +01:00
6 changed files with 61 additions and 131 deletions

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
rand = "0.8" rand = "0.9"
postgres = "0.19" postgres = "0.19"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@@ -808,29 +808,18 @@ impl DirectorWorker {
// compute piece price and full sell price // compute piece price and full sell price
let piece_price = Self::compute_piece_sell_price(item); 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 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)?;
// Preis-Inflation: Der Preis wird basierend auf der Steuer inflatiert, let revenue_cents = (sell_price * 100.0).round() as i64;
// damit der Netto-Betrag für den Verkäufer gleich bleibt (wie im Frontend) let cost_cents = (one_piece_cost * item.quantity as f64 * 100.0).round() as i64;
let inflation_factor = if cumulative_tax_percent >= 100.0 { let profit_cents = (revenue_cents - cost_cents).max(0);
1.0 let tax_cents = ((profit_cents as f64) * cumulative_tax_percent / 100.0).round() as i64;
} else {
1.0 / (1.0 - cumulative_tax_percent / 100.0)
};
let adjusted_price_per_unit = (piece_price * inflation_factor * 100.0).round() / 100.0;
let revenue = adjusted_price_per_unit * item.quantity as f64;
// Steuer wird auf Revenue berechnet (nicht auf Profit), wie im Frontend
let revenue_cents = (revenue * 100.0).round() as i64;
let tax_cents = ((revenue * cumulative_tax_percent / 100.0) * 100.0).round() as i64;
let payout_cents = revenue_cents - tax_cents; let payout_cents = revenue_cents - tax_cents;
let cost = one_piece_cost * item.quantity as f64; 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);
let profit_cents = revenue_cents - (cost * 100.0).round() as i64;
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);
if tax_cents > 0 { if tax_cents > 0 {
let tax_amount = (tax_cents as f64) / 100.0; let tax_amount = (tax_cents as f64) / 100.0;
@@ -848,7 +837,7 @@ impl DirectorWorker {
eprintln!( eprintln!(
"[DirectorWorker] sell: user_id={}, revenue={:.2}, tax={:.2}, payout={:.2}, product_id={}", "[DirectorWorker] sell: user_id={}, revenue={:.2}, tax={:.2}, payout={:.2}, product_id={}",
item.user_id, item.user_id,
revenue, sell_price,
(tax_cents as f64) / 100.0, (tax_cents as f64) / 100.0,
payout_amount, payout_amount,
item.product_id item.product_id

View File

@@ -432,15 +432,9 @@ impl EventsWorker {
} }
} }
// Globaler Skalierungsfaktor für Ereignisfrequenz. // Globaler Skalierungsfaktor für Ereignisfrequenz (1.0 = unverändert).
// Default: 1.0 (unverändert). Optional per ENV `EVENT_RATE_SCALE` konfigurierbar. // Setze auf 0.05, um Ereignisse auf 1/20 der ursprünglichen Häufigkeit zu reduzieren.
fn event_rate_scale() -> f64 { const EVENT_RATE_SCALE: f64 = 0.05;
std::env::var("EVENT_RATE_SCALE")
.ok()
.and_then(|v| v.parse::<f64>().ok())
.filter(|v| v.is_finite() && *v >= 0.0)
.unwrap_or(1.0)
}
fn check_and_trigger_events_inner( fn check_and_trigger_events_inner(
pool: &ConnectionPool, pool: &ConnectionPool,
@@ -449,12 +443,11 @@ impl EventsWorker {
rng: &mut impl Rng, rng: &mut impl Rng,
events: &[RandomEvent], events: &[RandomEvent],
) -> Result<(), DbError> { ) -> Result<(), DbError> {
let rate_scale = Self::event_rate_scale();
// Prüfe jedes mögliche Ereignis // Prüfe jedes mögliche Ereignis
for event in events { for event in events {
// Zufällige Prüfung basierend auf Wahrscheinlichkeit // Zufällige Prüfung basierend auf Wahrscheinlichkeit
let roll = rng.gen_range(0.0..=1.0); let roll = rng.gen_range(0.0..=1.0);
let effective_prob = event.probability_per_minute * rate_scale; let effective_prob = event.probability_per_minute * Self::EVENT_RATE_SCALE;
if roll < effective_prob { if roll < effective_prob {
eprintln!( eprintln!(
"[EventsWorker] Ereignis '{}' wurde ausgelöst (Wahrscheinlichkeit: {:.4}% -> skaliert {:.4}%)", "[EventsWorker] Ereignis '{}' wurde ausgelöst (Wahrscheinlichkeit: {:.4}% -> skaliert {:.4}%)",

View File

@@ -30,27 +30,11 @@ SELECT DISTINCT b.falukant_user_id AS user_id FROM falukant_data.branch b WHERE
pub const QUERY_UPDATE_WEATHER: &str = r#" pub const QUERY_UPDATE_WEATHER: &str = r#"
WITH all_regions AS ( WITH all_regions AS (
SELECT DISTINCT r.id AS region_id SELECT DISTINCT r.id AS region_id FROM falukant_data.region r JOIN falukant_type.region tr ON r.region_type_id = tr.id WHERE tr.label_tr = 'city'
FROM falukant_data.region r
JOIN falukant_type.region tr ON r.region_type_id = tr.id
WHERE tr.label_tr = 'city'
),
random_weather AS (
SELECT
ar.region_id,
(
SELECT wt.id
FROM falukant_type.weather wt
ORDER BY RANDOM()
LIMIT 1
) AS weather_type_id
FROM all_regions ar
) )
INSERT INTO falukant_data.weather (region_id, weather_type_id) INSERT INTO falukant_data.weather (region_id, weather_type_id)
SELECT rw.region_id, rw.weather_type_id SELECT ar.region_id, (SELECT wt.id FROM falukant_type.weather wt ORDER BY random() + ar.region_id * 0 LIMIT 1) FROM all_regions ar
FROM random_weather rw ON CONFLICT (region_id) DO UPDATE SET weather_type_id = EXCLUDED.weather_type_id;
ON CONFLICT (region_id)
DO UPDATE SET weather_type_id = EXCLUDED.weather_type_id;
"#; "#;
pub const QUERY_INSERT_NOTIFICATION: &str = r#" pub const QUERY_INSERT_NOTIFICATION: &str = r#"
@@ -60,7 +44,7 @@ VALUES ($1, $2, FALSE, NOW(), NOW());
// Product pricing // Product pricing
pub const QUERY_GET_PRODUCT_COST: &str = r#" pub const QUERY_GET_PRODUCT_COST: &str = r#"
SELECT sell_cost, sell_cost AS original_sell_cost FROM falukant_type.product WHERE id = $1; SELECT original_sell_cost, sell_cost FROM falukant_type.product WHERE id = $1;
"#; "#;
pub const QUERY_GET_DIRECTORS: &str = r#" pub const QUERY_GET_DIRECTORS: &str = r#"
@@ -206,12 +190,8 @@ DELETE FROM falukant_data.inventory WHERE id = $1;
"#; "#;
pub const QUERY_ADD_SELL_LOG: &str = r#" pub const QUERY_ADD_SELL_LOG: &str = r#"
INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id, sell_timestamp) INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id) VALUES ($1, $2, $3, $4)
VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (region_id, product_id, seller_id) DO UPDATE SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity;
ON CONFLICT (seller_id, product_id, region_id)
DO UPDATE SET
quantity = falukant_log.sell.quantity + EXCLUDED.quantity,
sell_timestamp = COALESCE(EXCLUDED.sell_timestamp, NOW());
"#; "#;
pub const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#" pub const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#"
SELECT SELECT
@@ -223,37 +203,15 @@ SELECT
t.target_region_id, t.target_region_id,
b_target.id AS target_branch_id, b_target.id AS target_branch_id,
b_source.id AS source_branch_id, b_source.id AS source_branch_id,
COALESCE(rd.distance, 0.0) AS distance, rd.distance AS distance,
v.falukant_user_id AS user_id v.falukant_user_id AS user_id
FROM falukant_data.transport AS t FROM falukant_data.transport AS t
JOIN falukant_data.vehicle AS v ON v.id = t.vehicle_id JOIN falukant_data.vehicle AS v ON v.id = t.vehicle_id
JOIN falukant_type.vehicle AS vt ON vt.id = v.vehicle_type_id JOIN falukant_type.vehicle AS vt ON vt.id = v.vehicle_type_id
LEFT JOIN LATERAL ( JOIN falukant_data.region_distance AS rd ON ((rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id) OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL)
SELECT rd.distance
FROM falukant_data.region_distance AS rd
WHERE (
(rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id)
OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id)
)
AND (
rd.transport_mode = vt.transport_mode
OR rd.transport_mode IS NULL
OR vt.transport_mode IS NULL
)
ORDER BY
(rd.transport_mode = vt.transport_mode) DESC,
(rd.transport_mode IS NULL) DESC
LIMIT 1
) AS rd ON TRUE
LEFT JOIN falukant_data.branch AS b_target ON b_target.region_id = t.target_region_id AND b_target.falukant_user_id = v.falukant_user_id LEFT JOIN falukant_data.branch AS b_target ON b_target.region_id = t.target_region_id AND b_target.falukant_user_id = v.falukant_user_id
LEFT JOIN falukant_data.branch AS b_source ON b_source.region_id = t.source_region_id AND b_source.falukant_user_id = v.falukant_user_id LEFT JOIN falukant_data.branch AS b_source ON b_source.region_id = t.source_region_id AND b_source.falukant_user_id = v.falukant_user_id
WHERE ( WHERE vt.speed > 0 AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' <= NOW();
-- Transport ist angekommen basierend auf Distanz und Geschwindigkeit (wenn speed > 0)
(vt.speed > 0 AND rd.distance IS NOT NULL AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' <= NOW())
OR
-- Fallback: Transport ist länger als 24 Stunden unterwegs (falls keine Distanz gefunden wurde oder speed = 0/NULL)
(t.created_at + INTERVAL '24 hours' <= NOW())
);
"#; "#;
pub const QUERY_GET_AVAILABLE_STOCKS: &str = r#" pub const QUERY_GET_AVAILABLE_STOCKS: &str = r#"
@@ -347,13 +305,7 @@ SELECT v.id AS vehicle_id, vt.capacity AS capacity
FROM falukant_data.vehicle v FROM falukant_data.vehicle v
JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id
JOIN falukant_data.region_distance rd ON ((rd.source_region_id = v.region_id AND rd.target_region_id = $3) OR (rd.source_region_id = $3 AND rd.target_region_id = v.region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) JOIN falukant_data.region_distance rd ON ((rd.source_region_id = v.region_id AND rd.target_region_id = $3) OR (rd.source_region_id = $3 AND rd.target_region_id = v.region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL)
WHERE v.falukant_user_id = $1 WHERE v.falukant_user_id = $1 AND v.region_id = $2;
AND v.region_id = $2
AND v.id NOT IN (
SELECT DISTINCT t.vehicle_id
FROM falukant_data.transport t
WHERE t.vehicle_id IS NOT NULL
);
"#; "#;
pub const QUERY_INSERT_TRANSPORT: &str = r#" pub const QUERY_INSERT_TRANSPORT: &str = r#"
@@ -830,8 +782,7 @@ pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#"
dt.office_type_id, dt.office_type_id,
dt.region_id, dt.region_id,
c.character_id, c.character_id,
COUNT(v.id) AS vote_count, COUNT(v.id) AS vote_count
COALESCE(ch.reputation, 0) AS reputation
FROM distinct_types AS dt FROM distinct_types AS dt
JOIN falukant_data.election AS e JOIN falukant_data.election AS e
ON e.office_type_id = dt.office_type_id ON e.office_type_id = dt.office_type_id
@@ -840,10 +791,8 @@ pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#"
JOIN falukant_data.candidate AS c JOIN falukant_data.candidate AS c
ON c.election_id = e.id ON c.election_id = e.id
AND c.id = v.candidate_id AND c.id = v.candidate_id
JOIN falukant_data."character" AS ch
ON ch.id = c.character_id
WHERE e.date >= (NOW() - INTERVAL '30 days') WHERE e.date >= (NOW() - INTERVAL '30 days')
GROUP BY dt.office_type_id, dt.region_id, c.character_id, ch.reputation GROUP BY dt.office_type_id, dt.region_id, c.character_id
), ),
ranked_winners AS ( ranked_winners AS (
SELECT SELECT
@@ -852,7 +801,7 @@ pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#"
vpc.character_id, vpc.character_id,
ROW_NUMBER() OVER ( ROW_NUMBER() OVER (
PARTITION BY vpc.office_type_id, vpc.region_id PARTITION BY vpc.office_type_id, vpc.region_id
ORDER BY vpc.vote_count DESC, vpc.reputation DESC, vpc.character_id ASC ORDER BY vpc.vote_count DESC
) AS rn ) AS rn
FROM votes_per_candidate AS vpc FROM votes_per_candidate AS vpc
), ),
@@ -1491,11 +1440,10 @@ pub const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#"
INSERT INTO falukant_log.notification ( INSERT INTO falukant_log.notification (
user_id, user_id,
tr, tr,
character_id,
shown, shown,
created_at, created_at,
updated_at updated_at
) VALUES ($1, $2, NULL, FALSE, NOW(), NOW()); ) VALUES ($1, $2, FALSE, NOW(), NOW());
"#; "#;
// Aliases for personal variants (keeps original prepared statement names used in events.worker) // Aliases for personal variants (keeps original prepared statement names used in events.worker)

View File

@@ -2,8 +2,8 @@ use crate::db::{ConnectionPool, DbError};
use crate::message_broker::MessageBroker; use crate::message_broker::MessageBroker;
use std::cmp::min; use std::cmp::min;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::Duration;
use super::base::{BaseWorker, Worker, WorkerState}; use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{ use crate::worker::sql::{
@@ -51,8 +51,8 @@ impl TransportWorker {
eprintln!("[TransportWorker] Fehler in process_arrived_transports: {err}"); eprintln!("[TransportWorker] Fehler in process_arrived_transports: {err}");
} }
// Minütlich prüfen (nicht sekündlich pollen) // Einmal pro Sekunde prüfen
for _ in 0..60 { for _ in 0..1 {
if !state.running_worker.load(Ordering::Relaxed) { if !state.running_worker.load(Ordering::Relaxed) {
break; break;
} }
@@ -67,18 +67,11 @@ impl TransportWorker {
) -> Result<(), DbError> { ) -> Result<(), DbError> {
let transports = Self::load_arrived_transports(pool)?; let transports = Self::load_arrived_transports(pool)?;
if !transports.is_empty() {
eprintln!(
"[TransportWorker] {} angekommene Transport(e) gefunden",
transports.len()
);
}
for t in transports { for t in transports {
if let Err(err) = Self::handle_arrived_transport(pool, broker, &t) { if let Err(err) = Self::handle_arrived_transport(pool, broker, &t) {
eprintln!( eprintln!(
"[TransportWorker] Fehler beim Verarbeiten von Transport {} (vehicle_id={}, product_id={:?}, size={}): {}", "[TransportWorker] Fehler beim Verarbeiten von Transport {}: {err}",
t.id, t.vehicle_id, t.product_id, t.size, err t.id
); );
} }
} }
@@ -94,19 +87,6 @@ impl TransportWorker {
conn.prepare("get_arrived_transports", QUERY_GET_ARRIVED_TRANSPORTS)?; conn.prepare("get_arrived_transports", QUERY_GET_ARRIVED_TRANSPORTS)?;
let rows = conn.execute("get_arrived_transports", &[])?; let rows = conn.execute("get_arrived_transports", &[])?;
if rows.is_empty() {
// Nur alle 60 Sekunden loggen, um Log-Flut zu vermeiden
static LAST_LOG: Mutex<Option<Instant>> = Mutex::new(None);
let mut last_log = LAST_LOG.lock().unwrap();
let should_log = last_log
.map(|t| t.elapsed().as_secs() >= 60)
.unwrap_or(true);
if should_log {
eprintln!("[TransportWorker] Keine angekommenen Transporte gefunden");
*last_log = Some(Instant::now());
}
}
let mut result = Vec::with_capacity(rows.len()); let mut result = Vec::with_capacity(rows.len());
for row in rows { for row in rows {
let id = parse_i32(&row, "id", -1); let id = parse_i32(&row, "id", -1);
@@ -243,13 +223,6 @@ impl TransportWorker {
); );
broker.publish(target_inventory_message); broker.publish(target_inventory_message);
} }
} else {
// Nichts konnte eingelagert werden - Transport bleibt unverändert
// Logge dies, damit wir sehen, dass der Transport wartet
eprintln!(
"[TransportWorker] Transport {} wartet: Kein Lagerplatz verfügbar (branch_id={}, product_id={}, size={})",
t.id, t.target_branch_id, product_id, t.size
);
} }
// Keine Notification für wartende Transporte, um Notification-System zu entlasten. // Keine Notification für wartende Transporte, um Notification-System zu entlasten.

View File

@@ -5,12 +5,39 @@ 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::sql::QUERY_UPDATE_WEATHER;
pub struct WeatherWorker { pub struct WeatherWorker {
base: BaseWorker, base: BaseWorker,
} }
// Query zum Aktualisieren des Wetters für alle Regionen
// Wählt für jede Region ein zufälliges Wetter aus allen verfügbaren Wettertypen aus
// Wichtig: Jede Region bekommt ein individuelles, zufälliges Wetter
const QUERY_UPDATE_WEATHER: &str = r#"
WITH all_regions AS (
SELECT DISTINCT r.id AS region_id
FROM falukant_data.region r
JOIN falukant_type.region tr ON r.region_type_id = tr.id
WHERE tr.label_tr = 'city'
),
random_weather AS (
SELECT
ar.region_id,
(
SELECT wt.id
FROM falukant_type.weather wt
ORDER BY RANDOM()
LIMIT 1
) AS weather_type_id
FROM all_regions ar
)
INSERT INTO falukant_data.weather (region_id, weather_type_id)
SELECT rw.region_id, rw.weather_type_id
FROM random_weather rw
ON CONFLICT (region_id)
DO UPDATE SET weather_type_id = EXCLUDED.weather_type_id;
"#;
impl WeatherWorker { impl WeatherWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self { Self {