From 4a2e8148039cafe0e985e22a7da1ce1e84c0fe5e Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 2 Dec 2025 08:55:01 +0100 Subject: [PATCH] Enhance pricing logic in DirectorWorker and implement hourly price recalculation: Added a new `worth_percent` field to the `InventoryItem` struct and updated SQL queries to incorporate this value in price calculations. Refactored price computation logic to use a base price derived from `worth_percent`. Introduced a new hourly price recalculation mechanism that adjusts prices based on sales data from the last hour, ensuring dynamic pricing adjustments. Enhanced logging for better monitoring of price updates. --- src/worker/director.rs | 60 +++++++++++++----- src/worker/value_recalculation.rs | 102 ++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 15 deletions(-) diff --git a/src/worker/director.rs b/src/worker/director.rs index 73d5c1b..fe0081c 100644 --- a/src/worker/director.rs +++ b/src/worker/director.rs @@ -40,6 +40,7 @@ struct InventoryItem { user_id: i32, region_id: i32, branch_id: i32, + worth_percent: f64, // Regionaler worth_percent-Wert für die Preisberechnung } #[derive(Debug, Clone)] @@ -186,7 +187,8 @@ const QUERY_GET_INVENTORY: &str = r#" p.sell_cost, fu.id AS user_id, b.region_id, - b.id AS branch_id + b.id AS branch_id, + COALESCE(tpw.worth_percent, 100.0) AS worth_percent FROM falukant_data.inventory i JOIN falukant_data.stock s ON s.id = i.stock_id @@ -198,6 +200,9 @@ const QUERY_GET_INVENTORY: &str = r#" ON d.employer_user_id = fu.id JOIN falukant_type.product p ON p.id = i.product_id + LEFT JOIN falukant_data.town_product_worth tpw + ON tpw.region_id = b.region_id + AND tpw.product_id = i.product_id WHERE d.id = $1 AND b.id = $2; "#; @@ -728,6 +733,10 @@ impl DirectorWorker { user_id: row.get("user_id")?.parse().ok()?, region_id: row.get("region_id")?.parse().ok()?, branch_id: row.get("branch_id")?.parse().ok()?, + worth_percent: row + .get("worth_percent") + .and_then(|v| v.parse::().ok()) + .unwrap_or(100.0), }) } @@ -741,9 +750,19 @@ impl DirectorWorker { return Ok(()); } - let min_price = item.sell_cost * 0.6; - let piece_sell_price = - min_price + (item.sell_cost - min_price) * (item.quality as f64 / 100.0); + // Neue Preisberechnung gemäß Spezifikation: + // 1. Basispreis = product.sellCost * (worthPercent / 100) + let base_price = item.sell_cost * (item.worth_percent / 100.0); + + // 2. min = basePrice * 0.6, max = basePrice + let min_price = base_price * 0.6; + let max_price = base_price; + + // 3. price = min + (max - min) * (knowledgeFactor / 100) + // knowledgeFactor ist hier item.quality + let knowledge_factor = item.quality as f64; + let piece_sell_price = min_price + (max_price - min_price) * (knowledge_factor / 100.0); + let sell_price = piece_sell_price * item.quantity as f64; if let Err(err) = self.base.change_falukant_user_money( @@ -836,17 +855,22 @@ impl DirectorWorker { return Ok(0); } - // Lokalen Stückpreis berechnen + // Lokalen Stückpreis berechnen (neue Preisberechnung) let local_percent = worth_by_region .get(&item.region_id) .copied() .unwrap_or(100.0); - let local_sell_cost = item.sell_cost * local_percent / 100.0; - let local_min_price = local_sell_cost * 0.6; - let quality_factor = item.quality as f64 / 100.0; - let local_piece_price = - local_min_price + (local_sell_cost - local_min_price) * quality_factor; + // 1. Basispreis = product.sellCost * (worthPercent / 100) + let local_base_price = item.sell_cost * (local_percent / 100.0); + + // 2. min = basePrice * 0.6, max = basePrice + let local_min_price = local_base_price * 0.6; + let local_max_price = local_base_price; + + // 3. price = min + (max - min) * (knowledgeFactor / 100) + let knowledge_factor = item.quality as f64; + let local_piece_price = local_min_price + (local_max_price - local_min_price) * (knowledge_factor / 100.0); let mut best_target_region: Option = None; let mut best_quantity: i32 = 0; @@ -859,11 +883,17 @@ impl DirectorWorker { continue; } - // Remote-Stückpreis - let remote_sell_cost = item.sell_cost * remote_percent / 100.0; - let remote_min_price = remote_sell_cost * 0.6; - let remote_piece_price = - remote_min_price + (remote_sell_cost - remote_min_price) * quality_factor; + // Remote-Stückpreis berechnen (neue Preisberechnung) + // 1. Basispreis = product.sellCost * (worthPercent / 100) + let remote_base_price = item.sell_cost * (remote_percent / 100.0); + + // 2. min = basePrice * 0.6, max = basePrice + let remote_min_price = remote_base_price * 0.6; + let remote_max_price = remote_base_price; + + // 3. price = min + (max - min) * (knowledgeFactor / 100) + let knowledge_factor = item.quality as f64; + let remote_piece_price = remote_min_price + (remote_max_price - remote_min_price) * (knowledge_factor / 100.0); let delta_per_unit = remote_piece_price - local_piece_price; if delta_per_unit <= 0.0 { diff --git a/src/worker/value_recalculation.rs b/src/worker/value_recalculation.rs index 33756a7..e300d7f 100644 --- a/src/worker/value_recalculation.rs +++ b/src/worker/value_recalculation.rs @@ -1,5 +1,6 @@ use crate::db::{ConnectionPool, DbError, Row}; use crate::message_broker::MessageBroker; +use std::collections::HashSet; use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -75,6 +76,53 @@ const QUERY_GET_SELL_REGIONS: &str = r#" GROUP BY region_id; "#; +// Stündliche Preisneuberechnung basierend auf Verkäufen der letzten Stunde +// Für jedes Produkt in jeder Stadt: Wenn überdurchschnittlich viel verkauft wurde, +// sinkt der Preis um 10% (begrenzt auf 0-100) +const QUERY_HOURLY_PRICE_RECALCULATION: &str = r#" + WITH city_sales AS ( + SELECT + s.region_id, + s.product_id, + SUM(s.quantity) AS total_sold + FROM falukant_log.sell s + WHERE s.sell_timestamp >= NOW() - INTERVAL '1 hour' + GROUP BY s.region_id, s.product_id + ), + avg_sales AS ( + SELECT + product_id, + AVG(total_sold) AS avg_sold + FROM city_sales + GROUP BY product_id + ), + price_updates AS ( + SELECT + cs.region_id, + cs.product_id, + cs.total_sold, + COALESCE(av.avg_sold, 0) AS avg_sold, + tpw.worth_percent AS current_price + FROM city_sales cs + JOIN avg_sales av ON av.product_id = cs.product_id + JOIN falukant_data.town_product_worth tpw + ON tpw.region_id = cs.region_id + AND tpw.product_id = cs.product_id + WHERE cs.total_sold > COALESCE(av.avg_sold, 0) + ) + UPDATE falukant_data.town_product_worth tpw + SET worth_percent = GREATEST( + 0, + LEAST( + 100, + pu.current_price * 0.9 -- 10% Preisreduktion + ) + ) + FROM price_updates pu + WHERE tpw.region_id = pu.region_id + AND tpw.product_id = pu.product_id; +"#; + // Ehen / Beziehungen const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#" WITH updated_relations AS ( @@ -168,6 +216,7 @@ impl ValueRecalculationWorker { // statt exakte Uhrzeiten nachzubilden – Verhalten ist funktional ähnlich. let mut last_product = None; let mut last_sell_price = None; + let mut last_hourly_price_recalc = None; loop { if !state.running_worker.load(Ordering::Relaxed) { @@ -192,6 +241,14 @@ impl ValueRecalculationWorker { last_sell_price = Some(now); } + // Stündliche Preisneuberechnung basierend auf Verkäufen der letzten Stunde + if should_run_interval(last_hourly_price_recalc, now, Duration::from_secs(3600)) { + if let Err(err) = Self::calculate_hourly_price_recalculation_inner(&pool, &broker) { + eprintln!("[ValueRecalculationWorker] Fehler in calculateHourlyPriceRecalculation: {err}"); + } + last_hourly_price_recalc = Some(now); + } + // Ehen & Studium bei jedem Durchlauf if let Err(err) = Self::calculate_marriages_inner(&pool, &broker) { eprintln!("[ValueRecalculationWorker] Fehler in calculateMarriages: {err}"); @@ -275,6 +332,51 @@ impl ValueRecalculationWorker { Ok(()) } + fn calculate_hourly_price_recalculation_inner( + pool: &ConnectionPool, + broker: &MessageBroker, + ) -> Result<(), DbError> { + let mut conn = pool + .get() + .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; + + conn.prepare("hourly_price_recalculation", QUERY_HOURLY_PRICE_RECALCULATION)?; + let updated_rows = conn.execute("hourly_price_recalculation", &[])?; + + // Sammle alle betroffenen Regionen für Event-Benachrichtigungen + let mut affected_regions = std::collections::HashSet::new(); + + // Da die Query bereits die Updates durchführt, müssen wir die betroffenen Regionen + // separat abfragen. Alternativ können wir auch einfach alle Regionen benachrichtigen, + // die in der letzten Stunde Verkäufe hatten. + conn.prepare("get_sell_regions_hourly", r#" + SELECT DISTINCT region_id + FROM falukant_log.sell + WHERE sell_timestamp >= NOW() - INTERVAL '1 hour' + "#)?; + let regions = conn.execute("get_sell_regions_hourly", &[])?; + + for row in regions { + if let Some(region_id) = row.get("region_id").and_then(|v| v.parse::().ok()) { + affected_regions.insert(region_id); + } + } + + // Benachrichtige alle betroffenen Regionen über Preisänderungen + for region_id in affected_regions { + let message = + format!(r#"{{"event":"price_update","region_id":{}}}"#, region_id); + broker.publish(message); + } + + eprintln!( + "[ValueRecalculationWorker] Stündliche Preisneuberechnung abgeschlossen. {} Regionen aktualisiert.", + affected_regions.len() + ); + + Ok(()) + } + fn calculate_marriages_inner( pool: &ConnectionPool, broker: &MessageBroker,