Add transport planning functionality in DirectorWorker: Introduced logic for planning transports for inventory items, including new SQL queries for fetching regional worth and available transport vehicles. Enhanced inventory management by updating quantities based on transport outcomes and improved logging for transport planning decisions.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use crate::db::{DbConnection, DbError, Row};
|
||||
use std::collections::HashMap;
|
||||
use crate::message_broker::MessageBroker;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
@@ -46,6 +47,12 @@ struct SalaryItem {
|
||||
income: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TransportVehicle {
|
||||
id: i32,
|
||||
capacity: i32,
|
||||
}
|
||||
|
||||
pub struct DirectorWorker {
|
||||
base: BaseWorker,
|
||||
last_run: Option<Instant>,
|
||||
@@ -171,6 +178,46 @@ const QUERY_ADD_SELL_LOG: &str = r#"
|
||||
SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity;
|
||||
"#;
|
||||
|
||||
// Regionale Verkaufswürdigkeit pro Produkt/Region für alle Branches eines Users
|
||||
const QUERY_GET_REGION_WORTH_FOR_PRODUCT: &str = r#"
|
||||
SELECT
|
||||
tpw.region_id,
|
||||
tpw.product_id,
|
||||
tpw.worth_percent
|
||||
FROM falukant_data.town_product_worth tpw
|
||||
JOIN falukant_data.branch b
|
||||
ON b.region_id = tpw.region_id
|
||||
WHERE b.falukant_user_id = $1
|
||||
AND tpw.product_id = $2;
|
||||
"#;
|
||||
|
||||
// Verfügbare Transportmittel für eine Route (source_region -> target_region)
|
||||
const QUERY_GET_TRANSPORT_VEHICLES_FOR_ROUTE: &str = r#"
|
||||
SELECT
|
||||
v.id AS vehicle_id,
|
||||
vt.capacity AS capacity
|
||||
FROM falukant_data.vehicle v
|
||||
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)
|
||||
WHERE v.falukant_user_id = $1
|
||||
AND v.region_id = $2
|
||||
AND v.condition > 0
|
||||
AND v.available_from <= NOW();
|
||||
"#;
|
||||
|
||||
// Transport-Eintrag anlegen
|
||||
const QUERY_INSERT_TRANSPORT: &str = r#"
|
||||
INSERT INTO falukant_data.transport
|
||||
(source_region_id, target_region_id, product_id, size, vehicle_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW(), NOW());
|
||||
"#;
|
||||
|
||||
const QUERY_GET_SALARY_TO_PAY: &str = r#"
|
||||
SELECT d.id, d.employer_user_id, d.income
|
||||
FROM falukant_data.director d
|
||||
@@ -417,8 +464,48 @@ impl DirectorWorker {
|
||||
conn.prepare("remove_inventory", QUERY_REMOVE_INVENTORY)?;
|
||||
conn.prepare("add_sell_log", QUERY_ADD_SELL_LOG)?;
|
||||
|
||||
// Falls es nichts zu verkaufen gibt, können wir sofort zurückkehren.
|
||||
if items.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Für alle Items dieses Directors sollten die user_id-Felder identisch
|
||||
// sein (Arbeitgeber des Directors).
|
||||
let falukant_user_id = items[0].user_id;
|
||||
|
||||
// Vor dem eigentlichen Verkauf versucht der Director, lohnende
|
||||
// Transporte zu planen. Dabei werden:
|
||||
// - ggf. Transport-Einträge erzeugt
|
||||
// - Inventar-Mengen reduziert
|
||||
// Die zurückgegebenen Mengen werden dann lokal verkauft.
|
||||
for item in items.iter_mut() {
|
||||
let shipped = self.plan_transports_for_item(
|
||||
&mut conn,
|
||||
falukant_user_id,
|
||||
item,
|
||||
)?;
|
||||
|
||||
if shipped > 0 {
|
||||
if shipped >= item.quantity {
|
||||
// Alles wurde in Transporte umgewandelt, lokal nichts mehr zu verkaufen.
|
||||
item.quantity = 0;
|
||||
} else {
|
||||
// Inventar-Menge in der DB reduzieren und im Item anpassen.
|
||||
let remaining = item.quantity - shipped;
|
||||
Self::update_inventory_quantity(&mut conn, item.id, remaining)?;
|
||||
item.quantity = remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anschließend lokale Verkäufe für die verbleibenden Mengen durchführen.
|
||||
for item in items.drain(..) {
|
||||
self.sell_single_inventory_item(&mut conn, &item)?;
|
||||
if item.quantity > 0 {
|
||||
self.sell_single_inventory_item(&mut conn, &item)?;
|
||||
} else {
|
||||
// Falls die Menge auf 0 gesetzt wurde, das Inventar ggf. aufräumen.
|
||||
conn.execute("remove_inventory", &[&item.id])?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -483,6 +570,262 @@ impl DirectorWorker {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Plant ggf. Transporte für ein einzelnes Inventar-Item und gibt die
|
||||
/// Menge zurück, die tatsächlich in Transporte umgewandelt wurde.
|
||||
///
|
||||
/// Logik:
|
||||
/// - Ermittle regionale "worth_percent"-Werte für das Produkt in allen
|
||||
/// Branch-Regionen des Users.
|
||||
/// - Berechne lokalen Stückpreis (inkl. Qualität) und für jede andere
|
||||
/// Region einen potentiellen Stückpreis.
|
||||
/// - Prüfe für jede Zielregion:
|
||||
/// * Gibt es verfügbare Transportmittel für diese Route?
|
||||
/// * Ist der Mehrerlös (deltaPrice * Menge) größer als die
|
||||
/// Transportkosten (max(1, totalValue * 0.01))?
|
||||
/// - Wähle die Zielregion mit dem größten positiven Nettogewinn und
|
||||
/// erzeuge entsprechende Transporte (begrenzt durch Fahrzeugkapazität).
|
||||
fn plan_transports_for_item(
|
||||
&mut self,
|
||||
conn: &mut DbConnection,
|
||||
falukant_user_id: i32,
|
||||
item: &mut InventoryItem,
|
||||
) -> Result<i32, DbError> {
|
||||
// Sicherheitscheck
|
||||
if item.quantity <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Regionale worth_percent-Werte für dieses Produkt laden
|
||||
conn.prepare(
|
||||
"get_region_worth_for_product",
|
||||
QUERY_GET_REGION_WORTH_FOR_PRODUCT,
|
||||
)?;
|
||||
let rows = conn.execute(
|
||||
"get_region_worth_for_product",
|
||||
&[&falukant_user_id, &item.product_id],
|
||||
)?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut worth_by_region: HashMap<i32, f64> = HashMap::new();
|
||||
for row in rows {
|
||||
let region_id = row
|
||||
.get("region_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1);
|
||||
let percent = row
|
||||
.get("worth_percent")
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(100.0);
|
||||
|
||||
if region_id >= 0 {
|
||||
worth_by_region.insert(region_id, percent);
|
||||
}
|
||||
}
|
||||
|
||||
if worth_by_region.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Lokalen Stückpreis berechnen
|
||||
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;
|
||||
|
||||
let mut best_target_region: Option<i32> = None;
|
||||
let mut best_quantity: i32 = 0;
|
||||
let mut best_remote_piece_price: f64 = 0.0;
|
||||
let mut best_gain: f64 = 0.0;
|
||||
|
||||
// Für jede andere Region prüfen, ob sich ein Transport lohnt.
|
||||
for (®ion_id, &remote_percent) in &worth_by_region {
|
||||
if region_id == item.region_id {
|
||||
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;
|
||||
|
||||
let delta_per_unit = remote_piece_price - local_piece_price;
|
||||
if delta_per_unit <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verfügbare Transportmittel für diese Route abfragen
|
||||
let vehicles = Self::get_transport_vehicles_for_route(
|
||||
conn,
|
||||
falukant_user_id,
|
||||
item.region_id,
|
||||
region_id,
|
||||
)?;
|
||||
|
||||
if vehicles.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Maximale transportierbare Menge anhand der Kapazität ermitteln
|
||||
let mut max_capacity: i32 = 0;
|
||||
for v in &vehicles {
|
||||
max_capacity = max_capacity.saturating_add(v.capacity);
|
||||
}
|
||||
|
||||
if max_capacity <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let qty = std::cmp::min(item.quantity, max_capacity);
|
||||
if qty <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let extra_revenue = delta_per_unit * qty as f64;
|
||||
let total_value = remote_piece_price * qty as f64;
|
||||
let transport_cost = total_value * 0.01_f64;
|
||||
// Kostenformel: max(0.01, totalValue * 0.01)
|
||||
let transport_cost = if transport_cost < 0.01 {
|
||||
0.01
|
||||
} else {
|
||||
transport_cost
|
||||
};
|
||||
|
||||
let net_gain = extra_revenue - transport_cost;
|
||||
if net_gain <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if net_gain > best_gain {
|
||||
best_gain = net_gain;
|
||||
best_target_region = Some(region_id);
|
||||
best_quantity = qty;
|
||||
best_remote_piece_price = remote_piece_price;
|
||||
}
|
||||
}
|
||||
|
||||
// Kein lohnender Transport gefunden
|
||||
let target_region = match best_target_region {
|
||||
Some(r) => r,
|
||||
None => return Ok(0),
|
||||
};
|
||||
|
||||
if best_quantity <= 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Nochmals verfügbare Transportmittel für die gewählte Route laden
|
||||
let vehicles = Self::get_transport_vehicles_for_route(
|
||||
conn,
|
||||
falukant_user_id,
|
||||
item.region_id,
|
||||
target_region,
|
||||
)?;
|
||||
|
||||
if vehicles.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Transporte anlegen, begrenzt durch best_quantity und Kapazitäten
|
||||
conn.prepare("insert_transport", QUERY_INSERT_TRANSPORT)?;
|
||||
|
||||
let mut remaining = best_quantity;
|
||||
for v in &vehicles {
|
||||
if remaining <= 0 {
|
||||
break;
|
||||
}
|
||||
let size = std::cmp::min(remaining, v.capacity);
|
||||
if size <= 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"insert_transport",
|
||||
&[
|
||||
&item.region_id,
|
||||
&target_region,
|
||||
&item.product_id,
|
||||
&size,
|
||||
&v.id,
|
||||
],
|
||||
)?;
|
||||
|
||||
remaining -= size;
|
||||
}
|
||||
|
||||
let shipped = best_quantity - remaining.max(0);
|
||||
|
||||
// Optional: Logging zur Nachvollziehbarkeit
|
||||
if shipped > 0 {
|
||||
eprintln!(
|
||||
"[DirectorWorker] Transport geplant: {} Einheiten von Produkt {} von Region {} nach Region {} (Stückpreis lokal {:.2}, remote {:.2})",
|
||||
shipped, item.product_id, item.region_id, target_region, local_piece_price, best_remote_piece_price
|
||||
);
|
||||
}
|
||||
|
||||
Ok(shipped)
|
||||
}
|
||||
|
||||
fn get_transport_vehicles_for_route(
|
||||
conn: &mut DbConnection,
|
||||
falukant_user_id: i32,
|
||||
source_region: i32,
|
||||
target_region: i32,
|
||||
) -> Result<Vec<TransportVehicle>, DbError> {
|
||||
conn.prepare(
|
||||
"get_transport_vehicles_for_route",
|
||||
QUERY_GET_TRANSPORT_VEHICLES_FOR_ROUTE,
|
||||
)?;
|
||||
let rows = conn.execute(
|
||||
"get_transport_vehicles_for_route",
|
||||
&[&falukant_user_id, &source_region, &target_region],
|
||||
)?;
|
||||
|
||||
let mut result = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let id = row
|
||||
.get("vehicle_id")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(-1);
|
||||
let capacity = row
|
||||
.get("capacity")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
if id >= 0 && capacity > 0 {
|
||||
result.push(TransportVehicle { id, capacity });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn update_inventory_quantity(
|
||||
conn: &mut DbConnection,
|
||||
inventory_id: i32,
|
||||
new_quantity: i32,
|
||||
) -> Result<(), DbError> {
|
||||
const QUERY_UPDATE_INVENTORY_QTY: &str = r#"
|
||||
UPDATE falukant_data.inventory
|
||||
SET quantity = $2
|
||||
WHERE id = $1;
|
||||
"#;
|
||||
|
||||
conn.prepare("update_inventory_qty", QUERY_UPDATE_INVENTORY_QTY)?;
|
||||
conn.execute("update_inventory_qty", &[&inventory_id, &new_quantity])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pay_salary(&mut self) -> Result<(), DbError> {
|
||||
self.base.set_current_step("DirectorWorker: pay_salary");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user