Refactor SQL queries into a dedicated module

- Moved SQL queries from multiple worker files into `src/worker/sql.rs` for better organization and maintainability.
- Updated references in `stockage_manager.rs`, `transport.rs`, `underground.rs`, `user_character.rs`, and `value_recalculation.rs` to use the new centralized SQL queries.
- Improved code readability by replacing `.get(0)` with `.first()` for better clarity when retrieving the first row from query results.
- Cleaned up unnecessary comments and consolidated related SQL queries.
This commit is contained in:
Torsten Schulz (local)
2025-12-13 11:57:28 +01:00
parent a9d490ce38
commit 10bc1e5a52
14 changed files with 1955 additions and 2213 deletions

View File

@@ -86,15 +86,7 @@ async fn append_ws_log(
.map(|s| s.to_string());
let target_user = json
.get("user_id")
.and_then(|v| {
if let Some(s) = v.as_str() {
Some(s.to_string())
} else if let Some(n) = v.as_i64() {
Some(n.to_string())
} else {
None
}
});
.and_then(|v| v.as_str().map(|s| s.to_string()).or_else(|| v.as_i64().map(|n| n.to_string())));
(event, target_user)
} else {
(None, None)
@@ -510,10 +502,8 @@ async fn handle_connection<S>(
.and_then(|v| {
if let Some(s) = v.as_str() {
s.parse::<i64>().ok()
} else if let Some(n) = v.as_i64() {
Some(n)
} else {
None
v.as_i64()
}
})
.map(|v| v == numeric_uid)

View File

@@ -166,7 +166,7 @@ impl BaseWorker {
let rows = conn.execute("get_money_for_clamp", &[&falukant_user_id])?;
let current_money: f64 = rows
.get(0)
.first()
.and_then(|r| r.get("money"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0);

View File

@@ -10,6 +10,19 @@ use std::thread;
use std::time::Duration;
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED,
QUERY_GET_TOWN_REGION_IDS,
QUERY_LOAD_FIRST_NAMES,
QUERY_LOAD_LAST_NAMES,
QUERY_INSERT_CHARACTER,
QUERY_GET_ELIGIBLE_NPC_FOR_DEATH,
QUERY_DELETE_DIRECTOR,
QUERY_DELETE_RELATIONSHIP,
QUERY_DELETE_CHILD_RELATION,
QUERY_INSERT_NOTIFICATION,
QUERY_MARK_CHARACTER_DECEASED,
};
pub struct CharacterCreationWorker {
pub(crate) base: BaseWorker,
@@ -21,145 +34,7 @@ pub struct CharacterCreationWorker {
death_thread: Option<thread::JoinHandle<()>>,
}
// SQL-Queries analog zur C++-Implementierung
const QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED: &str = r#"
SELECT created_at
FROM falukant_data."character"
WHERE user_id IS NULL
AND created_at::date = CURRENT_DATE
ORDER BY created_at DESC
LIMIT 1;
"#;
const QUERY_GET_TOWN_REGION_IDS: &str = r#"
SELECT fdr.id
FROM falukant_data.region fdr
JOIN falukant_type.region ftr ON fdr.region_type_id = ftr.id
WHERE ftr.label_tr = 'city';
"#;
const QUERY_LOAD_FIRST_NAMES: &str = r#"
SELECT id, gender
FROM falukant_predefine.firstname;
"#;
const QUERY_LOAD_LAST_NAMES: &str = r#"
SELECT id
FROM falukant_predefine.lastname;
"#;
const QUERY_INSERT_CHARACTER: &str = r#"
INSERT INTO falukant_data.character(
user_id,
region_id,
first_name,
last_name,
birthdate,
gender,
created_at,
updated_at,
title_of_nobility
) VALUES (
NULL,
$1,
$2,
$3,
NOW(),
$4,
NOW(),
NOW(),
$5
);
"#;
const QUERY_GET_ELIGIBLE_NPC_FOR_DEATH: &str = r#"
WITH aged AS (
SELECT
c.id,
(current_date - c.birthdate::date) AS age,
c.user_id
FROM
falukant_data.character c
WHERE
c.user_id IS NULL
AND (current_date - c.birthdate::date) > 60
),
always_sel AS (
SELECT *
FROM aged
WHERE age > 85
),
random_sel AS (
SELECT *
FROM aged
WHERE age <= 85
ORDER BY random()
LIMIT 10
)
SELECT *
FROM always_sel
UNION ALL
SELECT *
FROM random_sel;
"#;
const QUERY_DELETE_DIRECTOR: &str = r#"
DELETE FROM falukant_data.director
WHERE director_character_id = $1
RETURNING employer_user_id;
"#;
const QUERY_DELETE_RELATIONSHIP: &str = r#"
WITH deleted AS (
DELETE FROM falukant_data.relationship
WHERE character1_id = $1
OR character2_id = $1
RETURNING
CASE
WHEN character1_id = $1 THEN character2_id
ELSE character1_id
END AS related_character_id,
relationship_type_id
)
SELECT
c.user_id AS related_user_id
FROM deleted d
JOIN falukant_data.character c
ON c.id = d.related_character_id;
"#;
const QUERY_DELETE_CHILD_RELATION: &str = r#"
WITH deleted AS (
DELETE FROM falukant_data.child_relation
WHERE child_character_id = $1
RETURNING
father_character_id,
mother_character_id
)
SELECT
cf.user_id AS father_user_id,
cm.user_id AS mother_user_id
FROM deleted d
JOIN falukant_data.character cf
ON cf.id = d.father_character_id
JOIN falukant_data.character cm
ON cm.id = d.mother_character_id;
"#;
const QUERY_INSERT_NOTIFICATION: &str = r#"
INSERT INTO falukant_log.notification (
user_id,
tr,
shown,
created_at,
updated_at
) VALUES ($1, 'director_death', FALSE, NOW(), NOW());
"#;
const QUERY_MARK_CHARACTER_DECEASED: &str = r#"
DELETE FROM falukant_data.character
WHERE id = $1;
"#;
impl CharacterCreationWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
@@ -329,10 +204,10 @@ impl CharacterCreationWorker {
}
fn load_names(&mut self) {
if self.first_name_cache.is_empty() || self.last_name_cache.is_empty() {
if let Err(err) = self.load_first_and_last_names() {
eprintln!("[CharacterCreationWorker] Fehler in loadNames: {err}");
}
if (self.first_name_cache.is_empty() || self.last_name_cache.is_empty())
&& let Err(err) = self.load_first_and_last_names()
{
eprintln!("[CharacterCreationWorker] Fehler in loadNames: {err}");
}
}
@@ -491,12 +366,12 @@ impl CharacterCreationWorker {
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0);
if character_id > 0 && Self::calculate_death_probability(age) {
if let Err(err) = Self::handle_character_death(pool, broker, character_id) {
eprintln!(
"[CharacterCreationWorker] Fehler beim Bearbeiten des NPC-Todes (id={character_id}): {err}"
);
}
if character_id > 0 && Self::calculate_death_probability(age)
&& let Err(err) = Self::handle_character_death(pool, broker, character_id)
{
eprintln!(
"[CharacterCreationWorker] Fehler beim Bearbeiten des NPC-Todes (id={character_id}): {err}"
);
}
}
@@ -531,13 +406,12 @@ impl CharacterCreationWorker {
// 1) Director löschen und User benachrichtigen
conn.prepare("delete_director", QUERY_DELETE_DIRECTOR)?;
let dir_result = conn.execute("delete_director", &[&character_id])?;
if let Some(row) = dir_result.get(0) {
if let Some(user_id) = row
if let Some(row) = dir_result.first()
&& let Some(user_id) = row
.get("employer_user_id")
.and_then(|v| v.parse::<i32>().ok())
{
Self::notify_user(pool, broker, user_id, "director_death")?;
}
{
Self::notify_user(pool, broker, user_id, "director_death")?;
}
// 2) Relationships löschen und betroffene User benachrichtigen

View File

@@ -31,6 +31,10 @@ use crate::worker::sql::{
QUERY_GET_BRANCH_REGION,
QUERY_GET_AVERAGE_WORTH,
QUERY_UPDATE_INVENTORY_QTY,
QUERY_GET_PRODUCT_COST,
QUERY_GET_USER_OFFICES,
QUERY_CUMULATIVE_TAX_NO_EXEMPT,
QUERY_CUMULATIVE_TAX_WITH_EXEMPT,
};
#[derive(Debug, Clone)]
@@ -103,7 +107,7 @@ impl DirectorWorker {
}
}
fn run_iteration(&mut self, state: &WorkerState) {
fn run_iteration(&mut self, _state: &WorkerState) {
self.base.set_current_step("DirectorWorker iteration");
let now = Instant::now();
@@ -119,11 +123,7 @@ impl DirectorWorker {
self.last_run = Some(now);
}
std::thread::sleep(Duration::from_secs(1));
if !state.running_worker.load(Ordering::Relaxed) {
return;
}
std::thread::sleep(Duration::from_secs(1));
}
fn perform_all_tasks(&mut self) -> Result<(), DbError> {
@@ -222,7 +222,7 @@ impl DirectorWorker {
return Ok(());
}
let mut base_plan = match Self::map_row_to_production_plan(&rows[0]) {
let mut base_plan = match rows.first().and_then(Self::map_row_to_production_plan) {
Some(p) => p,
None => {
eprintln!(
@@ -374,7 +374,7 @@ impl DirectorWorker {
let one_piece_cost = Self::calc_one_piece_cost(plan);
let max_money_production = Self::calc_max_money_production(plan, one_piece_cost);
let to_produce = free_capacity.min(max_money_production).min(100).max(0);
let to_produce = (free_capacity.min(max_money_production)).clamp(0, 100);
eprintln!(
"[DirectorWorker] Produktionsberechnung: free_capacity={}, one_piece_cost={}, max_money_production={}, to_produce={}, running_productions={}",
@@ -718,15 +718,19 @@ impl DirectorWorker {
// Helper: get one_piece_cost from DB row fallback logic
fn resolve_one_piece_cost(conn: &mut DbConnection, product_id: i32, fallback: f64) -> Result<f64, DbError> {
conn.prepare("get_product_cost", "SELECT original_sell_cost, sell_cost FROM falukant_type.product WHERE id = $1")?;
conn.prepare("get_product_cost", QUERY_GET_PRODUCT_COST)?;
let rows = conn.execute("get_product_cost", &[&product_id])?;
if let Some(row) = rows.get(0) {
if let Some(osc) = row.get("original_sell_cost") {
if let Ok(v) = osc.parse::<f64>() { return Ok(v); }
}
if let Some(sc) = row.get("sell_cost") {
if let Ok(v) = sc.parse::<f64>() { return Ok(v); }
}
if let Some(row) = rows.first()
&& let Some(osc) = row.get("original_sell_cost")
&& let Ok(v) = osc.parse::<f64>()
{
return Ok(v);
}
if let Some(row) = rows.first()
&& let Some(sc) = row.get("sell_cost")
&& let Ok(v) = sc.parse::<f64>()
{
return Ok(v);
}
Ok(fallback)
}
@@ -736,15 +740,12 @@ impl DirectorWorker {
// Default
let mut cumulative_tax_percent = DEFAULT_TAX_PERCENT;
conn.prepare("get_branch_region", "SELECT region_id FROM falukant_data.branch WHERE id = $1;")?;
conn.prepare("get_branch_region", QUERY_GET_BRANCH_REGION)?;
let branch_rows = conn.execute("get_branch_region", &[&branch_id])?;
let branch_region_id: Option<i32> = branch_rows.get(0).and_then(|r| r.get("region_id")).and_then(|v| v.parse().ok());
let branch_region_id: Option<i32> = branch_rows.first().and_then(|r| r.get("region_id")).and_then(|v| v.parse().ok());
if let Some(region_id) = branch_region_id {
conn.prepare(
"get_user_offices",
"SELECT po.id AS office_id, pot.name AS office_name, po.region_id, rt.label_tr AS region_type FROM falukant_data.political_office po JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id JOIN falukant_data.region r ON r.id = po.region_id JOIN falukant_type.region_type rt ON rt.id = r.region_type_id WHERE po.holder_id = $1 AND (po.end_date IS NULL OR po.end_date > NOW());",
)?;
conn.prepare("get_user_offices", QUERY_GET_USER_OFFICES)?;
let offices = conn.execute("get_user_offices", &[&user_id])?;
let mut exempt_types: Vec<String> = Vec::new();
@@ -767,27 +768,19 @@ impl DirectorWorker {
}
if exempt_types.is_empty() {
conn.prepare(
"cumulative_tax_no_exempt",
"WITH RECURSIVE ancestors AS (SELECT id, parent_id, COALESCE(tax_percent,0.0) AS tax_percent FROM falukant_data.region WHERE id = $1 UNION ALL SELECT r.id, r.parent_id, COALESCE(r.tax_percent,0.0) FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id) SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors;",
)?;
conn.prepare("cumulative_tax_no_exempt", QUERY_CUMULATIVE_TAX_NO_EXEMPT)?;
let res = conn.execute("cumulative_tax_no_exempt", &[&region_id])?;
if let Some(row) = res.get(0) {
if let Some(tp) = row.get("total_percent") {
cumulative_tax_percent = tp.parse::<f64>().unwrap_or(DEFAULT_TAX_PERCENT);
}
if let Some(row) = res.first()
&& let Some(tp) = row.get("total_percent")
{
cumulative_tax_percent = tp.parse::<f64>().unwrap_or(DEFAULT_TAX_PERCENT);
}
} else {
conn.prepare(
"cumulative_tax_with_exempt",
"WITH RECURSIVE ancestors AS (SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END AS tax_percent FROM falukant_data.region r JOIN falukant_type.region_type rt ON rt.id = r.region_type_id WHERE r.id = $1 UNION ALL SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END FROM falukant_data.region r JOIN falukant_type.region_type rt ON rt.id = r.region_type_id JOIN ancestors a ON r.id = a.parent_id) SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors;",
)?;
conn.prepare("cumulative_tax_with_exempt", QUERY_CUMULATIVE_TAX_WITH_EXEMPT)?;
let exempt_array: Vec<&str> = exempt_types.iter().map(|s| s.as_str()).collect();
let res = conn.execute("cumulative_tax_with_exempt", &[&region_id, &exempt_array])?;
if let Some(row) = res.get(0) {
if let Some(tp) = row.get("total_percent") {
cumulative_tax_percent = tp.parse::<f64>().unwrap_or(DEFAULT_TAX_PERCENT);
}
if let Some(row) = res.first() && let Some(tp) = row.get("total_percent") {
cumulative_tax_percent = tp.parse::<f64>().unwrap_or(DEFAULT_TAX_PERCENT);
}
}
}
@@ -828,10 +821,8 @@ impl DirectorWorker {
}
let payout_amount = (payout_cents as f64) / 100.0;
if payout_cents != 0 {
if let Err(err) = self.base.change_falukant_user_money(item.user_id, payout_amount, "sell products") {
eprintln!("[DirectorWorker] Fehler bei change_falukant_user_money (sell products): {err}");
}
if payout_cents != 0 && let Err(err) = self.base.change_falukant_user_money(item.user_id, payout_amount, "sell products") {
eprintln!("[DirectorWorker] Fehler bei change_falukant_user_money (sell products): {err}");
}
// Debug: Log vor dem DB-Aufruf

File diff suppressed because it is too large Load Diff

View File

@@ -5,48 +5,17 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_GET_NEW_HOUSE_DATA,
QUERY_ADD_NEW_BUYABLE_HOUSE,
QUERY_UPDATE_BUYABLE_HOUSE_STATE,
QUERY_UPDATE_USER_HOUSE_STATE,
};
pub struct HouseWorker {
base: BaseWorker,
}
// SQL-Queries analog zu `houseworker.h`
const QUERY_GET_NEW_HOUSE_DATA: &str = r#"
SELECT
h.id AS house_id
FROM
falukant_type.house AS h
WHERE
random() < 0.0001
AND label_tr <> 'under_bridge';
"#;
const QUERY_ADD_NEW_BUYABLE_HOUSE: &str = r#"
INSERT INTO falukant_data.buyable_house (house_type_id)
VALUES ($1);
"#;
const QUERY_UPDATE_BUYABLE_HOUSE_STATE: &str = r#"
UPDATE falukant_data.buyable_house
SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)),
floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)),
wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)),
window_condition = ROUND(window_condition - random() * (3 + 0 * id));
"#;
const QUERY_UPDATE_USER_HOUSE_STATE: &str = r#"
UPDATE falukant_data.user_house
SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)),
floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)),
wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)),
window_condition = ROUND(window_condition - random() * (3 + 0 * id))
WHERE house_type_id NOT IN (
SELECT id
FROM falukant_type.house h
WHERE h.label_tr = 'under_bridge'
);
"#;
impl HouseWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self {

View File

@@ -6,6 +6,23 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_COUNT_OFFICES_PER_REGION,
QUERY_FIND_OFFICE_GAPS,
QUERY_SELECT_NEEDED_ELECTIONS,
QUERY_INSERT_CANDIDATES,
QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES,
QUERY_PROCESS_EXPIRED_AND_FILL,
QUERY_USERS_IN_CITIES_OF_REGIONS,
QUERY_NOTIFY_OFFICE_EXPIRATION,
QUERY_NOTIFY_ELECTION_CREATED,
QUERY_NOTIFY_OFFICE_FILLED,
QUERY_GET_USERS_WITH_EXPIRING_OFFICES,
QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS,
QUERY_GET_USERS_WITH_FILLED_OFFICES,
QUERY_PROCESS_ELECTIONS,
QUERY_TRIM_EXCESS_OFFICES_GLOBAL,
};
pub struct PoliticsWorker {
base: BaseWorker,
@@ -42,466 +59,6 @@ struct Office {
// --- SQL-Konstanten (1:1 aus politics_worker.h übernommen) ------------------
const QUERY_COUNT_OFFICES_PER_REGION: &str = r#"
WITH
seats_per_region AS (
SELECT
pot.id AS office_type_id,
rt.id AS region_id,
pot.seats_per_region AS seats_total
FROM falukant_type.political_office_type AS pot
JOIN falukant_type.region AS rt
ON pot.region_type = rt.label_tr
),
occupied AS (
SELECT
po.office_type_id,
po.region_id,
COUNT(*) AS occupied_count
FROM falukant_data.political_office AS po
GROUP BY po.office_type_id, po.region_id
),
combined AS (
SELECT
spr.region_id,
spr.seats_total AS required_count,
COALESCE(o.occupied_count, 0) AS occupied_count
FROM seats_per_region AS spr
LEFT JOIN occupied AS o
ON spr.office_type_id = o.office_type_id
AND spr.region_id = o.region_id
)
SELECT
region_id,
SUM(required_count) AS required_count,
SUM(occupied_count) AS occupied_count
FROM combined
GROUP BY region_id;
"#;
/// Findet alle Kombinationen aus Amtstyp und Region, für die laut
/// `seats_per_region` mehr Sitze existieren sollten, als aktuell in
/// `falukant_data.political_office` belegt sind. Es werden ausschließlich
/// positive Differenzen (Gaps) zurückgegeben wenn `occupied > required`
/// (z.B. nach Reduktion der Sitzzahl), wird **nichts** gelöscht und die
/// Kombination erscheint hier nicht.
const QUERY_FIND_OFFICE_GAPS: &str = r#"
WITH
seats AS (
SELECT
pot.id AS office_type_id,
rt.id AS region_id,
pot.seats_per_region AS seats_total
FROM falukant_type.political_office_type AS pot
JOIN falukant_type.region AS rt
ON pot.region_type = rt.label_tr
),
occupied AS (
SELECT
po.office_type_id,
po.region_id,
COUNT(*) AS occupied_count
FROM falukant_data.political_office AS po
GROUP BY po.office_type_id, po.region_id
)
SELECT
s.office_type_id,
s.region_id,
(s.seats_total - COALESCE(o.occupied_count, 0)) AS gaps
FROM seats AS s
LEFT JOIN occupied AS o
ON s.office_type_id = o.office_type_id
AND s.region_id = o.region_id
WHERE (s.seats_total - COALESCE(o.occupied_count, 0)) > 0;
"#;
const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#"
WITH
target_date AS (
SELECT NOW()::date AS election_date
),
expired_today AS (
DELETE FROM falukant_data.political_office AS po
USING falukant_type.political_office_type AS pot
WHERE po.office_type_id = pot.id
AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date
= (SELECT election_date FROM target_date)
RETURNING
pot.id AS office_type_id,
po.region_id AS region_id
),
gaps_per_region AS (
SELECT
office_type_id,
region_id,
COUNT(*) AS gaps
FROM expired_today
GROUP BY office_type_id, region_id
),
to_schedule AS (
SELECT
g.office_type_id,
g.region_id,
g.gaps,
td.election_date
FROM gaps_per_region AS g
CROSS JOIN target_date AS td
WHERE NOT EXISTS (
SELECT 1
FROM falukant_data.election AS e
WHERE e.office_type_id = g.office_type_id
AND e.region_id = g.region_id
AND e.date::date = td.election_date
)
),
new_elections AS (
INSERT INTO falukant_data.election
(office_type_id, date, posts_to_fill, created_at, updated_at, region_id)
SELECT
ts.office_type_id,
ts.election_date,
ts.gaps,
NOW(),
NOW(),
ts.region_id
FROM to_schedule AS ts
RETURNING
id AS election_id,
region_id,
posts_to_fill
)
SELECT
ne.election_id,
ne.region_id,
ne.posts_to_fill
FROM new_elections AS ne
ORDER BY ne.region_id, ne.election_id;
"#;
const QUERY_INSERT_CANDIDATES: &str = r#"
INSERT INTO falukant_data.candidate
(election_id, character_id, created_at, updated_at)
SELECT
$1 AS election_id,
sub.id AS character_id,
NOW() AS created_at,
NOW() AS updated_at
FROM (
WITH RECURSIVE region_tree AS (
SELECT r.id
FROM falukant_data.region AS r
WHERE r.id = $2
UNION ALL
SELECT r2.id
FROM falukant_data.region AS r2
JOIN region_tree AS rt
ON r2.parent_id = rt.id
)
SELECT ch.id
FROM falukant_data.character AS ch
JOIN region_tree AS rt2
ON ch.region_id = rt2.id
WHERE ch.user_id IS NULL
AND ch.birthdate <= NOW() - INTERVAL '21 days'
AND ch.title_of_nobility IN (
SELECT id
FROM falukant_type.title
WHERE label_tr != 'noncivil'
)
ORDER BY RANDOM()
LIMIT ($3 * 2)
) AS sub(id);
"#;
/// Wahlen finden, für die noch keine Kandidaten existieren.
/// Das umfasst sowohl frisch eingetragene Wahlen als auch manuell
/// angelegte Wahlen, solange:
/// - das Wahl-Datum heute oder in der Zukunft liegt und
/// - noch kein Eintrag in `falukant_data.candidate` existiert.
const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#"
SELECT
e.id AS election_id,
e.region_id AS region_id,
e.posts_to_fill
FROM falukant_data.election AS e
WHERE e.region_id IS NOT NULL
AND e.posts_to_fill > 0
AND e.date::date >= CURRENT_DATE
AND NOT EXISTS (
SELECT 1
FROM falukant_data.candidate AS c
WHERE c.election_id = e.id
);
"#;
const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#"
WITH
expired_offices AS (
DELETE FROM falukant_data.political_office AS po
USING falukant_type.political_office_type AS pot
WHERE po.office_type_id = pot.id
AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW()
RETURNING
pot.id AS office_type_id,
po.region_id AS region_id
),
distinct_types AS (
SELECT DISTINCT office_type_id, region_id FROM expired_offices
),
votes_per_candidate AS (
SELECT
dt.office_type_id,
dt.region_id,
c.character_id,
COUNT(v.id) AS vote_count
FROM distinct_types AS dt
JOIN falukant_data.election AS e
ON e.office_type_id = dt.office_type_id
JOIN falukant_data.vote AS v
ON v.election_id = e.id
JOIN falukant_data.candidate AS c
ON c.election_id = e.id
AND c.id = v.candidate_id
WHERE e.date >= (NOW() - INTERVAL '30 days')
GROUP BY dt.office_type_id, dt.region_id, c.character_id
),
ranked_winners AS (
SELECT
vpc.office_type_id,
vpc.region_id,
vpc.character_id,
ROW_NUMBER() OVER (
PARTITION BY vpc.office_type_id, vpc.region_id
ORDER BY vpc.vote_count DESC
) AS rn
FROM votes_per_candidate AS vpc
),
selected_winners AS (
SELECT
rw.office_type_id,
rw.region_id,
rw.character_id
FROM ranked_winners AS rw
JOIN falukant_type.political_office_type AS pot
ON pot.id = rw.office_type_id
WHERE rw.rn <= pot.seats_per_region
),
insert_winners AS (
INSERT INTO falukant_data.political_office
(office_type_id, character_id, created_at, updated_at, region_id)
SELECT
sw.office_type_id,
sw.character_id,
NOW(),
NOW(),
sw.region_id
FROM selected_winners AS sw
RETURNING id AS new_office_id, office_type_id, character_id, region_id
),
count_inserted AS (
SELECT
office_type_id,
region_id,
COUNT(*) AS inserted_count
FROM insert_winners
GROUP BY office_type_id, region_id
),
needed_to_fill AS (
SELECT
dt.office_type_id,
dt.region_id,
(pot.seats_per_region - COALESCE(ci.inserted_count, 0)) AS gaps
FROM distinct_types AS dt
JOIN falukant_type.political_office_type AS pot
ON pot.id = dt.office_type_id
LEFT JOIN count_inserted AS ci
ON ci.office_type_id = dt.office_type_id
AND ci.region_id = dt.region_id
WHERE (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) > 0
),
random_candidates AS (
SELECT
rtf.office_type_id,
rtf.region_id,
ch.id AS character_id,
ROW_NUMBER() OVER (
PARTITION BY rtf.office_type_id, rtf.region_id
ORDER BY RANDOM()
) AS rn
FROM needed_to_fill AS rtf
JOIN falukant_data.character AS ch
ON ch.region_id = rtf.region_id
AND ch.user_id IS NULL
AND ch.birthdate <= NOW() - INTERVAL '21 days'
AND ch.title_of_nobility IN (
SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil'
)
AND NOT EXISTS (
SELECT 1
FROM falukant_data.political_office AS po2
JOIN falukant_type.political_office_type AS pot2
ON pot2.id = po2.office_type_id
WHERE po2.character_id = ch.id
AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) >
NOW() + INTERVAL '2 days'
)
),
insert_random AS (
INSERT INTO falukant_data.political_office
(office_type_id, character_id, created_at, updated_at, region_id)
SELECT
rc.office_type_id,
rc.character_id,
NOW(),
NOW(),
rc.region_id
FROM random_candidates AS rc
JOIN needed_to_fill AS rtf
ON rtf.office_type_id = rc.office_type_id
AND rtf.region_id = rc.region_id
WHERE rc.rn <= rtf.gaps
RETURNING id AS new_office_id, office_type_id, character_id, region_id
)
SELECT
new_office_id AS office_id,
office_type_id,
character_id,
region_id
FROM insert_winners
UNION ALL
SELECT
new_office_id AS office_id,
office_type_id,
character_id,
region_id
FROM insert_random;
"#;
const QUERY_USERS_IN_CITIES_OF_REGIONS: &str = r#"
WITH RECURSIVE region_tree AS (
SELECT id
FROM falukant_data.region
WHERE id = $1
UNION ALL
SELECT r2.id
FROM falukant_data.region AS r2
JOIN region_tree AS rt
ON r2.parent_id = rt.id
)
SELECT DISTINCT ch.user_id
FROM falukant_data.character AS ch
JOIN region_tree AS rt2
ON ch.region_id = rt2.id
WHERE ch.user_id IS NOT NULL;
"#;
const QUERY_NOTIFY_OFFICE_EXPIRATION: &str = r#"
INSERT INTO falukant_log.notification
(user_id, tr, created_at, updated_at)
SELECT
po.character_id,
'notify_office_expiring',
NOW(),
NOW()
FROM falukant_data.political_office AS po
JOIN falukant_type.political_office_type AS pot
ON po.office_type_id = pot.id
WHERE (po.created_at + (pot.term_length * INTERVAL '1 day'))
BETWEEN (NOW() + INTERVAL '2 days')
AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second');
"#;
const QUERY_NOTIFY_ELECTION_CREATED: &str = r#"
INSERT INTO falukant_log.notification
(user_id, tr, created_at, updated_at)
VALUES
($1, 'notify_election_created', NOW(), NOW());
"#;
const QUERY_NOTIFY_OFFICE_FILLED: &str = r#"
INSERT INTO falukant_log.notification
(user_id, tr, created_at, updated_at)
VALUES
($1, 'notify_office_filled', NOW(), NOW());
"#;
const QUERY_GET_USERS_WITH_EXPIRING_OFFICES: &str = r#"
SELECT DISTINCT ch.user_id
FROM falukant_data.political_office AS po
JOIN falukant_type.political_office_type AS pot
ON po.office_type_id = pot.id
JOIN falukant_data.character AS ch
ON po.character_id = ch.id
WHERE ch.user_id IS NOT NULL
AND (po.created_at + (pot.term_length * INTERVAL '1 day'))
BETWEEN (NOW() + INTERVAL '2 days')
AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second');
"#;
const QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS: &str = r#"
SELECT DISTINCT ch.user_id
FROM falukant_data.election AS e
JOIN falukant_data.character AS ch
ON ch.region_id = e.region_id
WHERE ch.user_id IS NOT NULL
AND e.date >= NOW() - INTERVAL '1 day';
"#;
const QUERY_GET_USERS_WITH_FILLED_OFFICES: &str = r#"
SELECT DISTINCT ch.user_id
FROM falukant_data.political_office AS po
JOIN falukant_data.character AS ch
ON po.character_id = ch.id
WHERE ch.user_id IS NOT NULL
AND po.created_at >= NOW() - INTERVAL '1 minute';
"#;
const QUERY_PROCESS_ELECTIONS: &str = r#"
SELECT office_id, office_type_id, character_id, region_id
FROM falukant_data.process_elections();
"#;
/// Schneidet für alle Amtstyp/Region-Kombinationen überzählige Einträge in
/// `falukant_data.political_office` ab, so dass höchstens
/// `seats_per_region` Ämter pro Kombination übrig bleiben.
///
/// Die Auswahl, welche Ämter entfernt werden, erfolgt deterministisch über
/// `created_at DESC`: die **neuesten** Ämter bleiben bevorzugt im Amt,
/// ältere Einträge werden zuerst entfernt. Damit lässt sich das Verhalten
/// später leicht anpassen (z.B. nach bestimmten Prioritäten).
const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#"
WITH seats AS (
SELECT
pot.id AS office_type_id,
rt.id AS region_id,
pot.seats_per_region AS seats_total
FROM falukant_type.political_office_type AS pot
JOIN falukant_type.region AS rt
ON pot.region_type = rt.label_tr
),
ranked AS (
SELECT
po.id,
po.office_type_id,
po.region_id,
s.seats_total,
ROW_NUMBER() OVER (
PARTITION BY po.office_type_id, po.region_id
ORDER BY po.created_at DESC
) AS rn
FROM falukant_data.political_office AS po
JOIN seats AS s
ON s.office_type_id = po.office_type_id
AND s.region_id = po.region_id
),
to_delete AS (
SELECT id
FROM ranked
WHERE rn > seats_total
)
DELETE FROM falukant_data.political_office
WHERE id IN (SELECT id FROM to_delete);
"#;
impl PoliticsWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
@@ -542,11 +99,11 @@ impl PoliticsWorker {
broker: &MessageBroker,
) -> Result<(), DbError> {
// 1) Optional: Positionen evaluieren (aktuell nur Logging/Struktur)
let _ = Self::evaluate_political_positions(pool)?;
Self::evaluate_political_positions(pool)?;
// 2) Schema-Änderungen abgleichen: neue / zusätzliche Ämter anlegen,
// ohne bestehende Amtsinhaber bei Reduktion zu entfernen.
let _ = Self::sync_offices_with_types(pool)?;
Self::sync_offices_with_types(pool)?;
// 3) Ämter, die bald auslaufen, benachrichtigen
Self::notify_office_expirations(pool, broker)?;

View File

@@ -7,6 +7,14 @@ use std::time::{Duration, Instant};
use crate::db::ConnectionPool;
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_GET_FINISHED_PRODUCTIONS,
QUERY_GET_AVAILABLE_STOCKS,
QUERY_DELETE_PRODUCTION,
QUERY_INSERT_INVENTORY,
QUERY_INSERT_UPDATE_PRODUCTION_LOG,
QUERY_ADD_OVERPRODUCTION_NOTIFICATION,
};
/// Abbildet eine abgeschlossene Produktion aus der Datenbank.
#[derive(Debug, Clone)]
@@ -28,128 +36,6 @@ struct StockInfo {
filled: i32,
}
// SQL-Queries analog zur C++-Implementierung
// Wichtig: Pro `production.id` darf hier **genau eine Zeile** zurückkommen.
// Durch die Joins auf Director/Knowledge/Wetter kann es sonst zu Mehrfachzeilen mit
// unterschiedlicher berechneter Qualität kommen. Deshalb wird die Qualität
// über MAX() aggregiert und nach `production_id` gruppiert.
const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#"
SELECT
p.id AS production_id,
p.branch_id,
p.product_id,
p.quantity,
p.start_timestamp,
pr.production_time,
-- Aggregierte Qualitätsbewertung pro Produktion inkl. Wettereinfluss
MAX(
GREATEST(
0,
LEAST(
100,
ROUND(
(
CASE
WHEN k2.id IS NOT NULL
THEN (k.knowledge * 2 + k2.knowledge) / 3
ELSE k.knowledge
END
)::numeric
+ COALESCE(pwe.quality_effect, 0) * 2.5
)
)
)::int
) AS quality,
br.region_id,
br.falukant_user_id AS user_id
FROM falukant_data.production p
JOIN falukant_type.product pr
ON p.product_id = pr.id
JOIN falukant_data.branch br
ON p.branch_id = br.id
JOIN falukant_data.character c
ON c.user_id = br.falukant_user_id
JOIN falukant_data.knowledge k
ON p.product_id = k.product_id
AND k.character_id = c.id
JOIN falukant_data.stock s
ON s.branch_id = br.id
-- Optionaler Wettereinfluss: pro (Produkt, Wetter) genau ein Datensatz
LEFT JOIN falukant_type.product_weather_effect pwe
ON pwe.product_id = p.product_id
AND pwe.weather_type_id = p.weather_type_id
LEFT JOIN falukant_data.director d
ON d.employer_user_id = c.user_id
LEFT JOIN falukant_data.knowledge k2
ON k2.character_id = d.director_character_id
AND k2.product_id = p.product_id
WHERE p.start_timestamp + INTERVAL '1 minute' * pr.production_time <= NOW()
GROUP BY
p.id,
p.branch_id,
p.product_id,
p.quantity,
p.start_timestamp,
pr.production_time,
br.region_id,
br.falukant_user_id
ORDER BY p.start_timestamp;
"#;
const QUERY_GET_AVAILABLE_STOCKS: &str = r#"
SELECT
stock.id,
stock.quantity AS total_capacity,
(
SELECT COALESCE(SUM(inventory.quantity), 0)
FROM falukant_data.inventory
WHERE inventory.stock_id = stock.id
) AS filled,
stock.branch_id
FROM falukant_data.stock stock
JOIN falukant_data.branch branch
ON stock.branch_id = branch.id
WHERE branch.id = $1
ORDER BY total_capacity DESC;
"#;
const QUERY_DELETE_PRODUCTION: &str = r#"
DELETE FROM falukant_data.production
WHERE id = $1;
"#;
const QUERY_INSERT_INVENTORY: &str = r#"
INSERT INTO falukant_data.inventory (
stock_id,
product_id,
quantity,
quality,
produced_at
) VALUES ($1, $2, $3, $4, NOW());
"#;
const QUERY_INSERT_UPDATE_PRODUCTION_LOG: &str = r#"
INSERT INTO falukant_log.production (
region_id,
product_id,
quantity,
producer_id,
production_date
) VALUES ($1, $2, $3, $4, CURRENT_DATE)
ON CONFLICT (producer_id, product_id, region_id, production_date)
DO UPDATE
SET quantity = falukant_log.production.quantity + EXCLUDED.quantity;
"#;
const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#"
INSERT INTO falukant_log.notification (
user_id,
tr,
shown,
created_at,
updated_at
) VALUES ($1, $2, FALSE, NOW(), NOW());
"#;
pub struct ProduceWorker {
base: BaseWorker,

File diff suppressed because it is too large Load Diff

View File

@@ -8,49 +8,17 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_GET_TOWNS,
QUERY_INSERT_STOCK,
QUERY_CLEANUP_STOCK,
QUERY_GET_REGION_USERS,
};
pub struct StockageManager {
base: BaseWorker,
}
// SQL-Queries analog zu `stockagemanager.h`
const QUERY_GET_TOWNS: &str = r#"
SELECT fdr.id
FROM falukant_data.region fdr
JOIN falukant_type.region ftr
ON ftr.id = fdr.region_type_id
WHERE ftr.label_tr = 'city';
"#;
const QUERY_INSERT_STOCK: &str = r#"
INSERT INTO falukant_data.buyable_stock (region_id, stock_type_id, quantity)
SELECT
$1 AS region_id,
s.id AS stock_type_id,
GREATEST(1, ROUND(RANDOM() * 5 * COUNT(br.id))) AS quantity
FROM falukant_data.branch AS br
CROSS JOIN falukant_type.stock AS s
WHERE br.region_id = $1
GROUP BY s.id
ORDER BY RANDOM()
LIMIT GREATEST(
ROUND(RANDOM() * (SELECT COUNT(id) FROM falukant_type.stock)),
1
);
"#;
const QUERY_CLEANUP_STOCK: &str = r#"
DELETE FROM falukant_data.buyable_stock
WHERE quantity <= 0;
"#;
const QUERY_GET_REGION_USERS: &str = r#"
SELECT c.user_id
FROM falukant_data.character c
WHERE c.region_id = $1
AND c.user_id IS NOT NULL;
"#;
impl StockageManager {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
Self {

View File

@@ -6,7 +6,16 @@ use std::sync::Arc;
use std::time::Duration;
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_GET_ARRIVED_TRANSPORTS,
QUERY_GET_AVAILABLE_STOCKS,
QUERY_INSERT_INVENTORY,
QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT,
QUERY_DELETE_TRANSPORT,
QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION,
QUERY_GET_BRANCH_REGION,
QUERY_UPDATE_TRANSPORT_SIZE,
};
#[derive(Debug, Clone)]
struct ArrivedTransport {
id: i32,
@@ -26,98 +35,6 @@ struct StockInfo {
filled: i32,
}
// Ermittelt alle Transporte, die gemäß Distanz und Fahrzeuggeschwindigkeit bereits
// angekommen sein sollten. Die Reisezeit wird hier vereinfacht als
// travel_minutes = distance / speed
// interpretiert, d.h. `speed` gibt die Einheiten "Distanz pro Minute" an.
const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#"
SELECT
t.id,
t.product_id,
t.size,
t.vehicle_id,
t.source_region_id,
t.target_region_id,
b_target.id AS target_branch_id,
b_source.id AS source_branch_id,
rd.distance AS distance,
v.falukant_user_id AS user_id
FROM falukant_data.transport AS t
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_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)
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
WHERE vt.speed > 0
AND t.created_at
+ (rd.distance / vt.speed::double precision) * INTERVAL '1 minute'
<= NOW();
"#;
// Verfügbare Lagerplätze in einem Branch, analog zur Logik im ProduceWorker.
const QUERY_GET_AVAILABLE_STOCKS: &str = r#"
SELECT
stock.id,
stock.quantity AS total_capacity,
(
SELECT COALESCE(SUM(inventory.quantity), 0)
FROM falukant_data.inventory
WHERE inventory.stock_id = stock.id
) AS filled
FROM falukant_data.stock stock
JOIN falukant_data.branch branch
ON stock.branch_id = branch.id
WHERE branch.id = $1
ORDER BY total_capacity DESC;
"#;
const QUERY_INSERT_INVENTORY: &str = r#"
INSERT INTO falukant_data.inventory (
stock_id,
product_id,
quantity,
quality,
produced_at
) VALUES ($1, $2, $3, $4, NOW());
"#;
const QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT: &str = r#"
UPDATE falukant_data.vehicle
SET region_id = $2,
condition = GREATEST(0, condition - $3::int),
available_from = NOW(),
updated_at = NOW()
WHERE id = $1;
"#;
const QUERY_DELETE_TRANSPORT: &str = r#"
DELETE FROM falukant_data.transport
WHERE id = $1;
"#;
/// Notification-Eintrag, analog zur Overproduction-Notification im ProduceWorker.
/// `tr` wird hier als JSON-String mit Übersetzungs-Key und Werten gespeichert.
const QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION: &str = r#"
INSERT INTO falukant_log.notification (
user_id,
tr,
shown,
created_at,
updated_at
) VALUES ($1, $2, FALSE, NOW(), NOW());
"#;
pub struct TransportWorker {
base: BaseWorker,
}
@@ -313,17 +230,16 @@ impl TransportWorker {
}
// Nutzer informieren, dass Ware noch im Transportmittel liegt.
if t.user_id > 0 {
if let Err(err) = Self::insert_transport_waiting_notification(
pool,
t.user_id,
product_id,
remaining_quantity,
) {
eprintln!(
"[TransportWorker] Fehler beim Schreiben der Transport-Waiting-Notification: {err}"
);
}
if t.user_id > 0 && let Err(err) = Self::insert_transport_waiting_notification(
pool,
t.user_id,
product_id,
remaining_quantity,
)
{
eprintln!(
"[TransportWorker] Fehler beim Schreiben der Transport-Waiting-Notification: {err}"
);
}
}
@@ -420,19 +336,12 @@ impl TransportWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
// Region des Branches abrufen
const QUERY_GET_BRANCH_REGION: &str = r#"
SELECT region_id
FROM falukant_data.branch
WHERE id = $1
LIMIT 1;
"#;
conn.prepare("get_branch_region", QUERY_GET_BRANCH_REGION)?;
// Region des Branches abrufen
conn.prepare("get_branch_region", QUERY_GET_BRANCH_REGION)?;
let rows = conn.execute("get_branch_region", &[&target_branch_id])?;
let region_id = rows
.get(0)
.first()
.and_then(|r| r.get("region_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
@@ -479,14 +388,7 @@ impl TransportWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
const QUERY_UPDATE_TRANSPORT_SIZE: &str = r#"
UPDATE falukant_data.transport
SET size = $2,
updated_at = NOW()
WHERE id = $1;
"#;
conn.prepare("update_transport_size", QUERY_UPDATE_TRANSPORT_SIZE)?;
conn.prepare("update_transport_size", QUERY_UPDATE_TRANSPORT_SIZE)?;
conn.execute("update_transport_size", &[&transport_id, &new_size])?;
Ok(())

View File

@@ -190,7 +190,7 @@ impl UndergroundWorker {
let task_type = r.get("underground_type").cloned().unwrap_or_default();
let params = r.get("parameters").cloned().unwrap_or_else(|| "{}".into());
Ok(Self::handle_task(pool, &task_type, performer_id, victim_id, &params)?)
Self::handle_task(pool, &task_type, performer_id, victim_id, &params)
}
fn handle_task(
@@ -348,7 +348,7 @@ impl UndergroundWorker {
let rows = conn.execute("ug_select_char_user", &[&character_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("user_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1))
@@ -513,10 +513,10 @@ impl UndergroundWorker {
let mut out = Vec::new();
for r in rows {
if let Some(t) = r.get("stock_type_id").and_then(|v| v.parse::<i32>().ok()) {
if allowed.contains(&t) {
out.push(r.clone());
}
if let Some(t) = r.get("stock_type_id").and_then(|v| v.parse::<i32>().ok())
&& allowed.contains(&t)
{
out.push(r.clone());
}
}
out

View File

@@ -8,6 +8,41 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_GET_USERS_TO_UPDATE,
QUERY_UPDATE_CHARACTERS_HEALTH,
QUERY_UPDATE_MOOD,
QUERY_UPDATE_GET_ITEMS_TO_UPDATE,
QUERY_UPDATE_GET_CHARACTER_IDS,
QUERY_UPDATE_KNOWLEDGE,
QUERY_DELETE_LOG_ENTRY,
QUERY_GET_OPEN_CREDITS,
QUERY_UPDATE_CREDIT,
QUERY_CLEANUP_CREDITS,
QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM,
QUERY_GET_CURRENT_MONEY,
QUERY_GET_HOUSE_VALUE,
QUERY_GET_SETTLEMENT_VALUE,
QUERY_GET_INVENTORY_VALUE,
QUERY_GET_CREDIT_DEBT,
QUERY_COUNT_CHILDREN,
QUERY_GET_HEIR,
QUERY_RANDOM_HEIR,
QUERY_SET_CHARACTER_USER,
QUERY_UPDATE_USER_MONEY,
QUERY_GET_FALUKANT_USER_ID,
QUERY_AUTOBATISM,
QUERY_GET_PREGNANCY_CANDIDATES,
QUERY_INSERT_CHILD,
QUERY_INSERT_CHILD_RELATION,
QUERY_DELETE_DIRECTOR,
QUERY_DELETE_RELATIONSHIP,
QUERY_DELETE_CHILD_RELATION,
QUERY_DELETE_KNOWLEDGE,
QUERY_DELETE_DEBTORS_PRISM,
QUERY_DELETE_POLITICAL_OFFICE,
QUERY_DELETE_ELECTION_CANDIDATE,
};
/// Vereinfachtes Abbild eines Characters aus `QUERY_GET_USERS_TO_UPDATE`.
#[derive(Debug, Clone)]
@@ -26,418 +61,7 @@ pub struct UserCharacterWorker {
last_mood_run: Option<Instant>,
}
// SQL-Queries (1:1 aus der C++-Implementierung übernommen, gruppiert nach Themen)
const QUERY_GET_USERS_TO_UPDATE: &str = r#"
SELECT id, CURRENT_DATE - birthdate::date AS age, health
FROM falukant_data."character"
WHERE user_id IS NOT NULL;
"#;
const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#"
UPDATE falukant_data."character"
SET health = $1
WHERE id = $2;
"#;
// Mood-Update mit zufälliger Auswahl pro Charakter:
// Jeder lebende Charakter hat pro Aufruf (ca. 1x pro Minute)
// eine kleine Chance auf Mood-Wechsel. Die Bedingung `random() < 1.0 / 50.0`
// ergibt im Erwartungswert ca. alle 50 Minuten einen Wechsel, verteilt
// individuell und zufällig.
const QUERY_UPDATE_MOOD: &str = r#"
UPDATE falukant_data."character" AS c
SET mood_id = falukant_data.get_random_mood_id()
WHERE c.health > 0
AND random() < (1.0 / 50.0);
"#;
const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#"
SELECT id, product_id, producer_id, quantity
FROM falukant_log.production p
WHERE p.production_timestamp::date < current_date;
"#;
const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#"
SELECT fu.id AS user_id,
c.id AS character_id,
c2.id AS director_id
FROM falukant_data.falukant_user fu
JOIN falukant_data.character c
ON c.user_id = fu.id
LEFT JOIN falukant_data.director d
ON d.employer_user_id = fu.id
LEFT JOIN falukant_data.character c2
ON c2.id = d.director_character_id
WHERE fu.id = $1;
"#;
const QUERY_UPDATE_KNOWLEDGE: &str = r#"
UPDATE falukant_data.knowledge
SET knowledge = LEAST(knowledge + $3, 100)
WHERE character_id = $1
AND product_id = $2;
"#;
const QUERY_DELETE_LOG_ENTRY: &str = r#"
DELETE FROM falukant_log.production
WHERE id = $1;
"#;
// Kredit- und Vermögens-Queries
const QUERY_GET_OPEN_CREDITS: &str = r#"
SELECT
c.id AS credit_id,
c.amount,
c.remaining_amount,
c.interest_rate,
fu.id AS user_id,
fu.money,
c2.id AS character_id,
dp.created_at AS debitor_prism_start,
dp.created_at::date < current_date AS prism_started_previously
FROM falukant_data.credit c
JOIN falukant_data.falukant_user fu
ON fu.id = c.id
JOIN falukant_data.character c2
ON c2.user_id = c.falukant_user_id
LEFT JOIN falukant_data.debtors_prism dp
ON dp.character_id = c2.id
WHERE c.remaining_amount > 0
AND c.updated_at::date < current_date;
"#;
const QUERY_UPDATE_CREDIT: &str = r#"
UPDATE falukant_data.credit c
SET remaining_amount = $1
WHERE falukant_user_id = $2;
"#;
const QUERY_CLEANUP_CREDITS: &str = r#"
DELETE FROM falukant_data.credit
WHERE remaining_amount >= 0.01;
"#;
const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#"
INSERT INTO falukant_data.debtors_prism (character_id)
VALUES ($1);
"#;
const QUERY_GET_CURRENT_MONEY: &str = r#"
SELECT COALESCE(money, 0) AS sum
FROM falukant_data.falukant_user
WHERE user_id = $1;
"#;
const QUERY_HOUSE_VALUE: &str = r#"
SELECT COALESCE(SUM(h.cost), 0) AS sum
FROM falukant_data.user_house AS uh
JOIN falukant_type.house AS h
ON uh.house_type_id = h.id
WHERE uh.user_id = $1;
"#;
const QUERY_SETTLEMENT_VALUE: &str = r#"
SELECT COALESCE(SUM(b.base_cost), 0) AS sum
FROM falukant_data.branch AS br
JOIN falukant_type.branch AS b
ON br.branch_type_id = b.id
WHERE br.falukant_user_id = $1;
"#;
const QUERY_INVENTORY_VALUE: &str = r#"
SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum
FROM falukant_data.inventory AS i
JOIN falukant_type.product AS p
ON i.product_id = p.id
JOIN falukant_data.branch AS br
ON i.stock_id = br.id
WHERE br.falukant_user_id = $1;
"#;
const QUERY_CREDIT_DEBT: &str = r#"
SELECT COALESCE(SUM(remaining_amount), 0) AS sum
FROM falukant_data.credit
WHERE falukant_user_id = $1;
"#;
const QUERY_COUNT_CHILDREN: &str = r#"
SELECT COUNT(*) AS cnt
FROM falukant_data.child_relation
WHERE father_character_id = $1
OR mother_character_id = $1;
"#;
// Vererbungs-Queries
const QUERY_GET_HEIR: &str = r#"
SELECT child_character_id
FROM falukant_data.child_relation
WHERE father_character_id = $1
OR mother_character_id = $1
ORDER BY (is_heir IS TRUE) DESC,
updated_at DESC
LIMIT 1;
"#;
const QUERY_RANDOM_HEIR: &str = r#"
WITH chosen AS (
SELECT
cr.id AS relation_id,
cr.child_character_id
FROM
falukant_data.child_relation AS cr
JOIN
falukant_data.character AS ch
ON ch.id = cr.child_character_id
WHERE
(cr.father_character_id = $1 OR cr.mother_character_id = $1)
AND ch.region_id = (
SELECT region_id
FROM falukant_data.character
WHERE id = $1
)
AND ch.birthdate >= NOW() - INTERVAL '10 days'
AND ch.title_of_nobility = (
SELECT id
FROM falukant_type.title
WHERE label_tr = 'noncivil'
)
ORDER BY RANDOM()
LIMIT 1
)
UPDATE
falukant_data.child_relation AS cr2
SET
is_heir = TRUE,
updated_at = NOW()
FROM
chosen
WHERE
cr2.id = chosen.relation_id
RETURNING
chosen.child_character_id;
"#;
const QUERY_SET_CHARACTER_USER: &str = r#"
UPDATE falukant_data.character
SET user_id = $1,
updated_at = NOW()
WHERE id = $2;
"#;
const QUERY_UPDATE_USER_MONEY: &str = r#"
UPDATE falukant_data.falukant_user
SET money = $1,
updated_at = NOW()
WHERE user_id = $2;
"#;
const QUERY_GET_FALUKANT_USER_ID: &str = r#"
SELECT user_id
FROM falukant_data.character
WHERE id = $1
LIMIT 1;
"#;
// Schwangerschafts-Queries
const QUERY_AUTOBATISM: &str = r#"
UPDATE falukant_data.child_relation
SET name_set = TRUE
WHERE id IN (
SELECT cr.id
FROM falukant_data.child_relation cr
JOIN falukant_data.character c
ON c.id = cr.child_character_id
WHERE cr.name_set = FALSE
AND c.birthdate < current_date - INTERVAL '5 days'
);
"#;
const QUERY_GET_PREGNANCY_CANDIDATES: &str = r#"
SELECT
r.character1_id AS father_cid,
r.character2_id AS mother_cid,
c1.title_of_nobility,
c1.last_name,
c1.region_id,
fu1.id AS father_uid,
fu2.id AS mother_uid,
((CURRENT_DATE - c1.birthdate::date)
+ (CURRENT_DATE - c2.birthdate::date)) / 2 AS avg_age_days,
100.0 /
(1 + EXP(
0.0647 * (
((CURRENT_DATE - c1.birthdate::date)
+ (CURRENT_DATE - c2.birthdate::date)) / 2
) - 0.0591
)) AS prob_pct
FROM falukant_data.relationship r
JOIN falukant_type.relationship r2
ON r2.id = r.relationship_type_id
AND r2.tr = 'married'
JOIN falukant_data.character c1
ON c1.id = r.character1_id
JOIN falukant_data.character c2
ON c2.id = r.character2_id
LEFT JOIN falukant_data.falukant_user fu1
ON fu1.id = c1.user_id
LEFT JOIN falukant_data.falukant_user fu2
ON fu2.id = c2.user_id
WHERE random() * 100 < (
100.0 /
(1 + EXP(
0.11166347 * (
((CURRENT_DATE - c1.birthdate::date)
+ (CURRENT_DATE - c2.birthdate::date)) / 2
) - 2.638267
))
) / 2;
-- Hinweis: Der Divisor `/ 2` halbiert die Wahrscheinlichkeit und ist Teil der
-- ursprünglichen Formel. Wurde vorübergehend entfernt, um die Geburtenrate zu erhöhen,
-- wurde aber wiederhergestellt, um die mathematische Korrektheit der Formel zu gewährleisten.
-- Um die Geburtenrate anzupassen, sollte stattdessen die Formel selbst angepasst werden.
"#;
const QUERY_INSERT_CHILD: &str = r#"
INSERT INTO falukant_data.character (
user_id,
region_id,
first_name,
last_name,
birthdate,
gender,
title_of_nobility,
mood_id,
created_at,
updated_at
) VALUES (
NULL,
$1::int,
(
SELECT id
FROM falukant_predefine.firstname
WHERE gender = $2
ORDER BY RANDOM()
LIMIT 1
),
$3::int,
NOW(),
$2::varchar,
$4::int,
(
SELECT id
FROM falukant_type.mood
ORDER BY RANDOM()
LIMIT 1
),
NOW(),
NOW()
)
RETURNING id AS child_cid;
"#;
const QUERY_INSERT_CHILD_RELATION: &str = r#"
INSERT INTO falukant_data.child_relation (
father_character_id,
mother_character_id,
child_character_id,
name_set,
created_at,
updated_at
)
VALUES (
$1::int,
$2::int,
$3::int,
FALSE,
NOW(), NOW()
);
"#;
// Aufräum-Queries beim Tod eines Characters
const QUERY_DELETE_DIRECTOR: &str = r#"
DELETE FROM falukant_data.director
WHERE director_character_id = $1;
"#;
const QUERY_DELETE_RELATIONSHIP: &str = r#"
DELETE FROM falukant_data.relationship
WHERE character1_id = $1
OR character2_id = $1;
"#;
const QUERY_DELETE_CHILD_RELATION: &str = r#"
DELETE FROM falukant_data.child_relation
WHERE child_character_id = $1
OR father_character_id = $1
OR mother_character_id = $1;
"#;
const QUERY_DELETE_KNOWLEDGE: &str = r#"
DELETE FROM falukant_data.knowledge
WHERE character_id = $1;
"#;
const QUERY_DELETE_DEBTORS_PRISM: &str = r#"
DELETE FROM falukant_data.debtors_prism
WHERE character_id = $1;
"#;
/// Löscht alle Ämter eines Charakters und stellt anschließend sicher, dass
/// für die betroffenen Amtstyp/Region-Kombinationen nicht mehr Ämter
/// besetzt sind als durch `seats_per_region` vorgesehen.
///
/// Die überzähligen Ämter werden deterministisch nach `created_at DESC`
/// gekappt, d.h. neuere Amtsinhaber bleiben bevorzugt im Amt.
const QUERY_DELETE_POLITICAL_OFFICE: &str = r#"
WITH removed AS (
DELETE FROM falukant_data.political_office
WHERE character_id = $1
RETURNING office_type_id, region_id
),
affected AS (
SELECT DISTINCT office_type_id, region_id
FROM removed
),
seats AS (
SELECT
pot.id AS office_type_id,
rt.id AS region_id,
pot.seats_per_region AS seats_total
FROM falukant_type.political_office_type AS pot
JOIN falukant_type.region AS rt
ON pot.region_type = rt.label_tr
JOIN affected AS a
ON a.office_type_id = pot.id
AND a.region_id = rt.id
),
ranked AS (
SELECT
po.id,
po.office_type_id,
po.region_id,
s.seats_total,
ROW_NUMBER() OVER (
PARTITION BY po.office_type_id, po.region_id
ORDER BY po.created_at DESC
) AS rn
FROM falukant_data.political_office AS po
JOIN seats AS s
ON s.office_type_id = po.office_type_id
AND s.region_id = po.region_id
),
to_delete AS (
SELECT id
FROM ranked
WHERE rn > seats_total
)
DELETE FROM falukant_data.political_office
WHERE id IN (SELECT id FROM to_delete);
"#;
const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#"
DELETE FROM falukant_data.election_candidate
WHERE character_id = $1;
"#;
// SQL moved to `src/worker/sql.rs`
impl UserCharacterWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
@@ -470,7 +94,7 @@ impl UserCharacterWorker {
}
if !state.running_worker.load(Ordering::Relaxed) {
return;
// worker stopping
}
}
@@ -893,8 +517,8 @@ impl UserCharacterWorker {
let inserted =
conn.execute("insert_child", &[&region_id, &gender, &last_name, &title_of_nobility])?;
let child_cid = inserted
.get(0)
let child_cid = inserted
.first()
.and_then(|r| r.get("child_cid"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1);
@@ -1002,7 +626,7 @@ impl UserCharacterWorker {
let rows = conn.execute("get_falukant_user_id", &[&character_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("user_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1))
@@ -1019,7 +643,7 @@ impl UserCharacterWorker {
let rows = conn.execute("get_heir", &[&deceased_character_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("child_character_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1))
@@ -1036,7 +660,7 @@ impl UserCharacterWorker {
let rows = conn.execute("random_heir", &[&deceased_character_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("child_character_id"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(-1))
@@ -1084,7 +708,7 @@ impl UserCharacterWorker {
let rows = conn.execute("get_current_money", &[&falukant_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("sum"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0))
@@ -1097,11 +721,11 @@ impl UserCharacterWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("house_value", QUERY_HOUSE_VALUE)?;
conn.prepare("house_value", QUERY_GET_HOUSE_VALUE)?;
let rows = conn.execute("house_value", &[&falukant_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("sum"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0))
@@ -1114,11 +738,11 @@ impl UserCharacterWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("settlement_value", QUERY_SETTLEMENT_VALUE)?;
conn.prepare("settlement_value", QUERY_GET_SETTLEMENT_VALUE)?;
let rows = conn.execute("settlement_value", &[&falukant_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("sum"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0))
@@ -1131,11 +755,11 @@ impl UserCharacterWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("inventory_value", QUERY_INVENTORY_VALUE)?;
conn.prepare("inventory_value", QUERY_GET_INVENTORY_VALUE)?;
let rows = conn.execute("inventory_value", &[&falukant_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("sum"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0))
@@ -1148,11 +772,11 @@ impl UserCharacterWorker {
.get()
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
conn.prepare("credit_debt", QUERY_CREDIT_DEBT)?;
conn.prepare("credit_debt", QUERY_GET_CREDIT_DEBT)?;
let rows = conn.execute("credit_debt", &[&falukant_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("sum"))
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0))
@@ -1169,7 +793,7 @@ impl UserCharacterWorker {
let rows = conn.execute("count_children", &[&deceased_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("cnt"))
.and_then(|v| v.parse::<i32>().ok())
.unwrap_or(0))

View File

@@ -6,291 +6,27 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use super::base::{BaseWorker, Worker, WorkerState};
use crate::worker::sql::{
QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER,
QUERY_DELETE_OLD_PRODUCTIONS,
QUERY_GET_PRODUCERS_LAST_DAY,
QUERY_UPDATE_REGION_SELL_PRICE,
QUERY_DELETE_REGION_SELL_PRICE,
QUERY_GET_SELL_REGIONS,
QUERY_HOURLY_PRICE_RECALCULATION,
QUERY_SET_MARRIAGES_BY_PARTY,
QUERY_GET_STUDYINGS_TO_EXECUTE,
QUERY_GET_OWN_CHARACTER_ID,
QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE,
QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE,
QUERY_SET_LEARNING_DONE,
};
pub struct ValueRecalculationWorker {
base: BaseWorker,
}
// Produktwissen / Produktions-Logs
const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#"
UPDATE falukant_data.knowledge k
SET knowledge = LEAST(100, k.knowledge + 1)
FROM falukant_data.character c
JOIN falukant_log.production p
ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
WHERE c.id = k.character_id
AND c.user_id = 18
AND k.product_id = 10;
"#;
const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#"
DELETE FROM falukant_log.production flp
WHERE DATE(flp.production_timestamp) < CURRENT_DATE;
"#;
const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#"
SELECT p.producer_id
FROM falukant_log.production p
WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day'
GROUP BY producer_id;
"#;
// Regionale Verkaufspreise
const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#"
UPDATE falukant_data.town_product_worth tpw
SET worth_percent =
GREATEST(
0,
LEAST(
CASE
WHEN s.quantity > avg_sells THEN tpw.worth_percent - 1
WHEN s.quantity < avg_sells THEN tpw.worth_percent + 1
ELSE tpw.worth_percent
END,
100
)
)
FROM (
SELECT region_id,
product_id,
quantity,
(SELECT AVG(quantity)
FROM falukant_log.sell avs
WHERE avs.product_id = s.product_id) AS avg_sells
FROM falukant_log.sell s
WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day'
) s
WHERE tpw.region_id = s.region_id
AND tpw.product_id = s.product_id;
"#;
const QUERY_DELETE_REGION_SELL_PRICE: &str = r#"
DELETE FROM falukant_log.sell s
WHERE DATE(s.sell_timestamp) < CURRENT_DATE;
"#;
const QUERY_GET_SELL_REGIONS: &str = r#"
SELECT s.region_id
FROM falukant_log.sell s
WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day'
GROUP BY region_id;
"#;
// Stündliche Preisneuberechnung basierend auf Verkäufen der letzten Stunde
// Zwei Ebenen der Preisberechnung:
// 1. Weltweit: Vergleich Stadt-Verkäufe vs. weltweiter Durchschnitt
// - ±5% Toleranz: Preis bleibt gleich
// - Mehr Verkäufe (>5% über Durchschnitt): Preis +10%
// - Weniger Verkäufe (<5% unter Durchschnitt): Preis -10%
// 2. Parent-Region: Vergleich Stadt-Verkäufe vs. Durchschnitt der parent-region
// - ±5% Toleranz: Preis bleibt gleich
// - Abweichung >±5%: Preis ±5%
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
),
world_avg_sales AS (
SELECT
product_id,
AVG(total_sold) AS avg_sold
FROM city_sales
GROUP BY product_id
),
parent_region_sales AS (
SELECT
r.parent_region_id,
cs.product_id,
AVG(cs.total_sold) AS avg_sold
FROM city_sales cs
JOIN falukant_data.region r ON r.id = cs.region_id
WHERE r.parent_region_id IS NOT NULL
GROUP BY r.parent_region_id, cs.product_id
),
price_updates_world AS (
SELECT
cs.region_id,
cs.product_id,
cs.total_sold,
COALESCE(wa.avg_sold, 0) AS world_avg,
tpw.worth_percent AS current_price,
CASE
-- Mehr als 5% über dem weltweiten Durchschnitt: 10% teurer
WHEN cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05
THEN tpw.worth_percent * 1.1
-- Weniger als 5% unter dem weltweiten Durchschnitt: 10% billiger
WHEN cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95
THEN tpw.worth_percent * 0.9
-- Innerhalb ±5%: Preis bleibt gleich
ELSE tpw.worth_percent
END AS price_after_world
FROM city_sales cs
JOIN world_avg_sales wa ON wa.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
-- Nur updaten wenn es eine Änderung gibt (außerhalb der ±5% Toleranz)
WHERE cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05
OR cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95
),
all_cities_with_prices AS (
SELECT
cs.region_id,
cs.product_id,
cs.total_sold,
r.parent_region_id,
tpw.worth_percent AS original_price,
COALESCE(puw.price_after_world, tpw.worth_percent) AS price_after_world
FROM city_sales cs
JOIN falukant_data.region r ON r.id = cs.region_id
JOIN falukant_data.town_product_worth tpw
ON tpw.region_id = cs.region_id
AND tpw.product_id = cs.product_id
LEFT JOIN price_updates_world puw
ON puw.region_id = cs.region_id
AND puw.product_id = cs.product_id
),
price_updates_parent AS (
SELECT
acwp.region_id,
acwp.product_id,
acwp.total_sold,
acwp.parent_region_id,
COALESCE(prs.avg_sold, 0) AS parent_avg,
acwp.price_after_world AS current_price,
CASE
-- Mehr als 5% über dem parent-region Durchschnitt: 5% teurer
WHEN acwp.total_sold > COALESCE(prs.avg_sold, 0) * 1.05
THEN acwp.price_after_world * 1.05
-- Weniger als 5% unter dem parent-region Durchschnitt: 5% billiger
WHEN acwp.total_sold < COALESCE(prs.avg_sold, 0) * 0.95
THEN acwp.price_after_world * 0.95
-- Innerhalb ±5%: Preis bleibt gleich (vom world-update)
ELSE acwp.price_after_world
END AS new_price
FROM all_cities_with_prices acwp
LEFT JOIN parent_region_sales prs
ON prs.parent_region_id = acwp.parent_region_id
AND prs.product_id = acwp.product_id
WHERE acwp.parent_region_id IS NOT NULL
AND (
acwp.total_sold > COALESCE(prs.avg_sold, 0) * 1.05
OR acwp.total_sold < COALESCE(prs.avg_sold, 0) * 0.95
)
),
final_price_updates AS (
SELECT
COALESCE(pup.region_id, puw.region_id) AS region_id,
COALESCE(pup.product_id, puw.product_id) AS product_id,
COALESCE(pup.new_price, puw.price_after_world, acwp.original_price) AS final_price
FROM all_cities_with_prices acwp
LEFT JOIN price_updates_world puw
ON puw.region_id = acwp.region_id
AND puw.product_id = acwp.product_id
LEFT JOIN price_updates_parent pup
ON pup.region_id = acwp.region_id
AND pup.product_id = acwp.product_id
WHERE puw.region_id IS NOT NULL
OR pup.region_id IS NOT NULL
)
UPDATE falukant_data.town_product_worth tpw
SET worth_percent = GREATEST(
0,
LEAST(
100,
fpu.final_price
)
)
FROM final_price_updates fpu
WHERE tpw.region_id = fpu.region_id
AND tpw.product_id = fpu.product_id;
"#;
// Ehen / Beziehungen
const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#"
WITH updated_relations AS (
UPDATE falukant_data.relationship AS rel
SET relationship_type_id = (
SELECT id
FROM falukant_type.relationship AS rt
WHERE rt.tr = 'married'
)
WHERE rel.id IN (
SELECT rel2.id
FROM falukant_data.party AS p
JOIN falukant_type.party AS pt
ON pt.id = p.party_type_id
AND pt.tr = 'wedding'
JOIN falukant_data.falukant_user AS fu
ON fu.id = p.falukant_user_id
JOIN falukant_data.character AS c
ON c.user_id = fu.id
JOIN falukant_data.relationship AS rel2
ON rel2.character1_id = c.id
OR rel2.character2_id = c.id
JOIN falukant_type.relationship AS rt2
ON rt2.id = rel2.relationship_type_id
AND rt2.tr = 'engaged'
WHERE p.created_at <= NOW() - INTERVAL '1 day'
)
RETURNING character1_id, character2_id
)
SELECT
c1.user_id AS character1_user,
c2.user_id AS character2_user
FROM updated_relations AS ur
JOIN falukant_data.character AS c1
ON c1.id = ur.character1_id
JOIN falukant_data.character AS c2
ON c2.id = ur.character2_id;
"#;
// Lernen / Studium
const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#"
SELECT
l.id,
l.associated_falukant_user_id,
l.associated_learning_character_id,
l.learn_all_products,
l.learning_recipient_id,
l.product_id,
lr.tr
FROM falukant_data.learning l
JOIN falukant_type.learn_recipient lr
ON lr.id = l.learning_recipient_id
WHERE l.learning_is_executed = FALSE
AND l.created_at + INTERVAL '1 day' < NOW();
"#;
const QUERY_GET_OWN_CHARACTER_ID: &str = r#"
SELECT id
FROM falukant_data.character c
WHERE c.user_id = $1;
"#;
const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#"
UPDATE falukant_data.knowledge k
SET knowledge = LEAST(100, k.knowledge + $1)
WHERE k.character_id = $2
AND k.product_id = $3;
"#;
const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#"
UPDATE falukant_data.knowledge k
SET knowledge = LEAST(100, k.knowledge + $1)
WHERE k.character_id = $2;
"#;
const QUERY_SET_LEARNING_DONE: &str = r#"
UPDATE falukant_data.learning
SET learning_is_executed = TRUE
WHERE id = $1;
"#;
impl ValueRecalculationWorker {
pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self {
@@ -601,7 +337,7 @@ impl ValueRecalculationWorker {
let rows = conn.execute("get_own_character_id", &[&falukant_user_id])?;
Ok(rows
.get(0)
.first()
.and_then(|r| r.get("id"))
.and_then(|v| v.parse::<i32>().ok()))
}