Initial commit: Rust YpDaemon

This commit is contained in:
Torsten Schulz (local)
2025-11-21 23:05:34 +01:00
commit d0ec363f09
21 changed files with 8067 additions and 0 deletions

584
src/worker/director.rs Normal file
View File

@@ -0,0 +1,584 @@
use crate::db::{DbConnection, DbError, Row};
use crate::message_broker::MessageBroker;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::db::ConnectionPool;
use super::base::{BaseWorker, Worker, WorkerState};
#[derive(Debug, Clone)]
struct Director {
id: i32,
may_produce: bool,
may_sell: bool,
may_start_transport: bool,
}
#[derive(Debug, Clone)]
struct ProductionPlan {
falukant_user_id: i32,
money: i32,
certificate: i32,
branch_id: i32,
product_id: i32,
stock_size: i32,
used_in_stock: i32,
running_productions: i32,
}
#[derive(Debug, Clone)]
struct InventoryItem {
id: i32,
product_id: i32,
quantity: i32,
quality: i32,
sell_cost: f64,
user_id: i32,
region_id: i32,
branch_id: i32,
}
#[derive(Debug, Clone)]
struct SalaryItem {
id: i32,
employer_user_id: i32,
income: i32,
}
pub struct DirectorWorker {
base: BaseWorker,
last_run: Option<Instant>,
}
// SQL-Queries (1:1 aus director_worker.h)
const QUERY_GET_DIRECTORS: &str = r#"
SELECT
d.may_produce,
d.may_sell,
d.may_start_transport,
b.id AS branch_id,
fu.id AS falukantUserId,
d.id
FROM falukant_data.director d
JOIN falukant_data.falukant_user fu
ON fu.id = d.employer_user_id
JOIN falukant_data.character c
ON c.id = d.director_character_id
JOIN falukant_data.branch b
ON b.region_id = c.region_id
AND b.falukant_user_id = fu.id
WHERE current_time BETWEEN '08:00:00' AND '17:00:00';
"#;
const QUERY_GET_BEST_PRODUCTION: &str = r#"
SELECT
fdu.id falukant_user_id,
fdu.money,
fdu.certificate,
ftp.id product_id,
ftp.label_tr,
(
SELECT SUM(quantity)
FROM falukant_data.stock fds
WHERE fds.branch_id = fdb.id
) AS stock_size,
COALESCE((
SELECT SUM(COALESCE(fdi.quantity, 0))
FROM falukant_data.stock fds
JOIN falukant_data.inventory fdi
ON fdi.stock_id = fds.id
WHERE fds.branch_id = fdb.id
), 0) AS used_in_stock,
(ftp.sell_cost * (fdtpw.worth_percent + (fdk_character.knowledge * 2 + fdk_director.knowledge) / 3) / 100 - 6 * ftp.category)
/ (300.0 * ftp.production_time) AS worth,
fdb.id AS branch_id,
(
SELECT COUNT(id)
FROM falukant_data.production
WHERE branch_id = fdb.id
) AS running_productions,
COALESCE((
SELECT SUM(COALESCE(fdp.quantity, 0)) quantity
FROM falukant_data.production fdp
WHERE fdp.branch_id = fdb.id
), 0) AS running_productions_quantity
FROM falukant_data.director fdd
JOIN falukant_data.character fdc
ON fdc.id = fdd.director_character_id
JOIN falukant_data.falukant_user fdu
ON fdd.employer_user_id = fdu.id
JOIN falukant_data.character user_character
ON user_character.user_id = fdu.id
JOIN falukant_data.branch fdb
ON fdb.falukant_user_id = fdu.id
AND fdb.region_id = fdc.region_id
JOIN falukant_data.town_product_worth fdtpw
ON fdtpw.region_id = fdb.region_id
JOIN falukant_data.knowledge fdk_character
ON fdk_character.product_id = fdtpw.product_id
AND fdk_character.character_id = user_character.id
JOIN falukant_data.knowledge fdk_director
ON fdk_director.product_id = fdtpw.product_id
AND fdk_director.character_id = fdd.director_character_id
JOIN falukant_type.product ftp
ON ftp.id = fdtpw.product_id
AND ftp.category <= fdu.certificate
WHERE fdd.id = $1
ORDER BY worth DESC
LIMIT 1;
"#;
const QUERY_INSERT_PRODUCTION: &str = r#"
INSERT INTO falukant_data.production (branch_id, product_id, quantity)
VALUES ($1, $2, $3);
"#;
const QUERY_GET_INVENTORY: &str = r#"
SELECT
i.id,
i.product_id,
i.quantity,
i.quality,
p.sell_cost,
fu.id AS user_id,
b.region_id,
b.id AS branch_id
FROM falukant_data.inventory i
JOIN falukant_data.stock s
ON s.id = i.stock_id
JOIN falukant_data.branch b
ON b.id = s.branch_id
JOIN falukant_data.falukant_user fu
ON fu.id = b.falukant_user_id
JOIN falukant_data.director d
ON d.employer_user_id = fu.id
JOIN falukant_type.product p
ON p.id = i.product_id
WHERE d.id = $1;
"#;
const QUERY_REMOVE_INVENTORY: &str = r#"
DELETE FROM falukant_data.inventory
WHERE id = $1;
"#;
const QUERY_ADD_SELL_LOG: &str = r#"
INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id)
VALUES ($1, $2, $3, $4)
ON CONFLICT (region_id, product_id, seller_id)
DO UPDATE
SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity;
"#;
const QUERY_GET_SALARY_TO_PAY: &str = r#"
SELECT d.id, d.employer_user_id, d.income
FROM falukant_data.director d
WHERE DATE(d.last_salary_payout) < DATE(NOW());
"#;
const QUERY_SET_SALARY_PAYED: &str = r#"
UPDATE falukant_data.director
SET last_salary_payout = NOW()
WHERE id = $1;
"#;
const QUERY_UPDATE_SATISFACTION: &str = r#"
WITH new_sats AS (
SELECT
d.id,
ROUND(
d.income::numeric
/
(
c.title_of_nobility
* POWER(1.231, AVG(k.knowledge) / 1.5)
)
* 100
) AS new_satisfaction
FROM falukant_data.director d
JOIN falukant_data.knowledge k
ON d.director_character_id = k.character_id
JOIN falukant_data.character c
ON c.id = d.director_character_id
GROUP BY d.id, c.title_of_nobility, d.income
)
UPDATE falukant_data.director dir
SET satisfaction = ns.new_satisfaction
FROM new_sats ns
WHERE dir.id = ns.id
AND dir.satisfaction IS DISTINCT FROM ns.new_satisfaction
RETURNING dir.employer_user_id;
"#;
impl DirectorWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self {
base: BaseWorker::new("DirectorWorker", pool, broker),
last_run: None,
}
}
fn run_iteration(&mut self, state: &WorkerState) {
self.base.set_current_step("DirectorWorker iteration");
let now = Instant::now();
let should_run = match self.last_run {
None => true,
Some(last) => now.saturating_duration_since(last) >= Duration::from_secs(60),
};
if should_run {
if let Err(err) = self.perform_all_tasks() {
eprintln!("[DirectorWorker] Fehler beim Ausführen der Aufgabe: {err}");
}
self.last_run = Some(now);
}
std::thread::sleep(Duration::from_secs(1));
if !state.running_worker.load(Ordering::Relaxed) {
return;
}
}
fn perform_all_tasks(&mut self) -> Result<(), DbError> {
self.perform_task()?;
self.pay_salary()?;
self.calculate_satisfaction()?;
Ok(())
}
fn perform_task(&mut self) -> Result<(), DbError> {
self.base
.set_current_step("Get director actions from DB");
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("get_directors", QUERY_GET_DIRECTORS)?;
let directors_rows = conn.execute("get_directors", &[])?;
let directors: Vec<Director> = directors_rows
.into_iter()
.filter_map(Self::map_row_to_director)
.collect();
for director in directors {
if director.may_produce {
self.start_productions(&director)?;
}
if director.may_start_transport {
self.start_transports_stub(&director);
}
if director.may_sell {
self.start_sellings(&director)?;
}
}
Ok(())
}
fn map_row_to_director(row: Row) -> Option<Director> {
Some(Director {
id: row.get("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),
})
}
fn start_productions(&mut self, director: &Director) -> Result<(), DbError> {
self.base
.set_current_step("DirectorWorker: start_productions");
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("get_to_produce", QUERY_GET_BEST_PRODUCTION)?;
let rows = conn.execute("get_to_produce", &[&director.id])?;
if rows.is_empty() {
return Ok(());
}
let plan = match Self::map_row_to_production_plan(&rows[0]) {
Some(p) => p,
None => return Ok(()),
};
self.create_production_batches(&mut conn, &plan)?;
Ok(())
}
fn map_row_to_production_plan(row: &Row) -> Option<ProductionPlan> {
Some(ProductionPlan {
falukant_user_id: row.get("falukant_user_id")?.parse().ok()?,
money: row.get("money")?.parse().ok()?,
certificate: row.get("certificate")?.parse().ok()?,
branch_id: row.get("branch_id")?.parse().ok()?,
product_id: row.get("product_id")?.parse().ok()?,
stock_size: row.get("stock_size")?.parse().ok()?,
used_in_stock: row.get("used_in_stock")?.parse().ok()?,
running_productions: row.get("running_productions")?.parse().ok()?,
})
}
fn create_production_batches(
&mut self,
conn: &mut DbConnection,
plan: &ProductionPlan,
) -> Result<(), DbError> {
let running = plan.running_productions;
if running >= 2 {
return Ok(());
}
let free_capacity =
plan.stock_size - plan.used_in_stock - plan.running_productions;
let one_piece_cost = plan.certificate * 6;
let max_money_production = if one_piece_cost > 0 {
plan.money / one_piece_cost
} else {
0
};
let to_produce = free_capacity
.min(max_money_production)
.min(300)
.max(0);
if to_produce < 1 {
return Ok(());
}
let production_cost = to_produce * one_piece_cost;
if let Err(err) = self.base.change_falukant_user_money(
plan.falukant_user_id,
-(production_cost as f64),
"director starts production",
) {
eprintln!(
"[DirectorWorker] Fehler bei change_falukant_user_money: {err}"
);
}
conn.prepare("insert_production", QUERY_INSERT_PRODUCTION)?;
let mut remaining = to_produce;
while remaining > 0 {
let batch = remaining.min(100);
conn.execute(
"insert_production",
&[&plan.branch_id, &plan.product_id, &batch],
)?;
remaining -= batch;
}
let message = format!(
r#"{{"event":"production_started","branch_id":{}}}"#,
plan.branch_id
);
self.base.broker.publish(message);
Ok(())
}
fn start_transports_stub(&self, _director: &Director) {
// TODO: Transportlogik bei Bedarf aus dem C++-Code nachziehen.
}
fn start_sellings(&mut self, director: &Director) -> Result<(), DbError> {
self.base
.set_current_step("DirectorWorker: start_sellings");
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("get_to_sell", QUERY_GET_INVENTORY)?;
let rows = conn.execute("get_to_sell", &[&director.id])?;
let mut items: Vec<InventoryItem> =
rows.into_iter().filter_map(Self::map_row_to_inventory_item).collect();
conn.prepare("remove_inventory", QUERY_REMOVE_INVENTORY)?;
conn.prepare("add_sell_log", QUERY_ADD_SELL_LOG)?;
for item in items.drain(..) {
self.sell_single_inventory_item(&mut conn, &item)?;
}
Ok(())
}
fn map_row_to_inventory_item(row: Row) -> Option<InventoryItem> {
Some(InventoryItem {
id: row.get("id")?.parse().ok()?,
product_id: row.get("product_id")?.parse().ok()?,
quantity: row.get("quantity")?.parse().ok()?,
quality: row.get("quality")?.parse().ok()?,
sell_cost: row.get("sell_cost")?.parse().ok()?,
user_id: row.get("user_id")?.parse().ok()?,
region_id: row.get("region_id")?.parse().ok()?,
branch_id: row.get("branch_id")?.parse().ok()?,
})
}
fn sell_single_inventory_item(
&mut self,
conn: &mut DbConnection,
item: &InventoryItem,
) -> Result<(), DbError> {
if item.quantity <= 0 {
conn.execute("remove_inventory", &[&item.id])?;
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);
let sell_price = piece_sell_price * item.quantity as f64;
if let Err(err) = self.base.change_falukant_user_money(
item.user_id,
sell_price,
"sell products",
) {
eprintln!(
"[DirectorWorker] Fehler bei change_falukant_user_money (sell products): {err}"
);
}
conn.execute(
"add_sell_log",
&[
&item.region_id,
&item.product_id,
&item.quantity,
&item.user_id,
],
)?;
conn.execute("remove_inventory", &[&item.id])?;
let message = format!(
r#"{{"event":"selled_items","branch_id":{}}}"#,
item.branch_id
);
self.base.broker.publish(message);
Ok(())
}
fn pay_salary(&mut self) -> Result<(), DbError> {
self.base.set_current_step("DirectorWorker: pay_salary");
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("get_salary_to_pay", QUERY_GET_SALARY_TO_PAY)?;
conn.prepare("set_salary_payed", QUERY_SET_SALARY_PAYED)?;
let rows = conn.execute("get_salary_to_pay", &[])?;
let salaries: Vec<SalaryItem> =
rows.into_iter().filter_map(Self::map_row_to_salary_item).collect();
for item in salaries {
if let Err(err) = self.base.change_falukant_user_money(
item.employer_user_id,
-(item.income as f64),
"director payed out",
) {
eprintln!(
"[DirectorWorker] Fehler bei change_falukant_user_money (director payed out): {err}"
);
}
conn.execute("set_salary_payed", &[&item.id])?;
let message =
format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, item.employer_user_id);
self.base.broker.publish(message);
}
Ok(())
}
fn map_row_to_salary_item(row: Row) -> Option<SalaryItem> {
Some(SalaryItem {
id: row.get("id")?.parse().ok()?,
employer_user_id: row.get("employer_user_id")?.parse().ok()?,
income: row.get("income")?.parse().ok()?,
})
}
fn calculate_satisfaction(&mut self) -> Result<(), DbError> {
self.base
.set_current_step("DirectorWorker: calculate_satisfaction");
let mut conn = self
.base
.pool
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("update_satisfaction", QUERY_UPDATE_SATISFACTION)?;
let rows = conn.execute("update_satisfaction", &[])?;
for row in rows {
if let Some(employer_id) = row
.get("employer_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
let message = format!(
r#"{{"event":"directorchanged","user_id":{}}}"#,
employer_id
);
self.base.broker.publish(message);
}
}
Ok(())
}
}
impl Worker for DirectorWorker {
fn start_worker_thread(&mut self) {
let pool = self.base.pool.clone();
let broker = self.base.broker.clone();
self.base
.start_worker_with_loop(move |state: Arc<WorkerState>| {
let mut worker = DirectorWorker::new(pool.clone(), broker.clone());
while state.running_worker.load(Ordering::Relaxed) {
worker.run_iteration(&state);
}
});
}
fn stop_worker_thread(&mut self) {
self.base.stop_worker();
}
fn enable_watchdog(&mut self) {
self.base.start_watchdog();
}
}