Initial commit: Rust YpDaemon
This commit is contained in:
584
src/worker/director.rs
Normal file
584
src/worker/director.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user