Implement daily political salary management: Introduced a new function run_daily_political_salary to calculate and distribute daily salaries for players with active political offices, utilizing configured values or fallback based on office rank. Updated SQL queries to support this functionality, including checks for the readiness of the necessary database column. Enhanced the PoliticsWorker to trigger daily salary processing, ensuring timely updates for users. Improved documentation for clarity on the new salary management features and their integration into the existing political benefits system.
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 2m51s
All checks were successful
Deploy yourpart (blue-green) / deploy (push) Successful in 2m51s
This commit is contained in:
6
migrations/013_falukant_political_daily_salary.sql
Normal file
6
migrations/013_falukant_political_daily_salary.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Tägliches politisches Gehalt (Daemon): Idempotenz pro Kalendertag (UTC)
|
||||
ALTER TABLE falukant_data.falukant_user
|
||||
ADD COLUMN IF NOT EXISTS last_political_daily_salary_on DATE;
|
||||
|
||||
COMMENT ON COLUMN falukant_data.falukant_user.last_political_daily_salary_on IS
|
||||
'Letzter Tag, an dem political daily salary gutgeschrieben wurde (YpDaemon political_benefits::run_daily_political_salary).';
|
||||
@@ -29,3 +29,7 @@ Tabellen **`political_benefit_last_tick`** und optional **`political_appointment
|
||||
- **Ernennungen**: Daemon setzt nur `pending` → `expired`, wenn `expires_at` überschritten (Anlage durch Backend-API).
|
||||
|
||||
Die Join-Spalte auf `political_office_benefit` heißt im Repo **`political_office_type_id`** — falls das Sequelize-Modell abweicht, SQL in `src/worker/sql.rs` anpassen.
|
||||
|
||||
## `013_falukant_political_daily_salary.sql`
|
||||
|
||||
Spalte **`falukant_data.falukant_user.last_political_daily_salary_on`** (Datum): Idempotenz für **`political_benefits::run_daily_political_salary`** — einmal pro Tag Gutschrift; Beträge aus JSON-Feld **`daily_salary`** (`tr`/`benefitType` = `daily_salary`) oder gestufter Daemon-Fallback nach Amts-Rang.
|
||||
|
||||
@@ -290,6 +290,11 @@ fn political_name_to_rank(name: &str) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Rang 1–5 für eine Amtsbezeichnung (Politik-Tagesgehalt, Zertifikat-Heuristik). Leer → 0.
|
||||
pub fn political_office_name_rank(name: &str) -> i32 {
|
||||
political_name_to_rank(name)
|
||||
}
|
||||
|
||||
fn max_political_rank_from_names(agg: &str) -> i32 {
|
||||
if agg.is_empty() {
|
||||
return 0;
|
||||
|
||||
@@ -34,7 +34,8 @@ use crate::db::{ConnectionPool, DbError};
|
||||
use crate::message_broker::MessageBroker;
|
||||
|
||||
const DAILY_INTERVAL: Duration = Duration::from_secs(24 * 3600);
|
||||
const MONTHLY_INTERVAL: Duration = Duration::from_secs(30 * 24 * 3600);
|
||||
/// Wie `DAILY_INTERVAL`: 1 Spieljahr = 1 Kalendertag — Monats-Stempel/„monthly“-Batch pro Spieltag, nicht alle 30 echten Tage.
|
||||
const MONTHLY_INTERVAL: Duration = DAILY_INTERVAL;
|
||||
/// 12 Monatsticke pro Spieltag (24 h = 1 Spieljahr); 2 h = 1 Spielmonat (Liebschaft + Dienerschaft).
|
||||
const GAME_MONTH_SLICE_INTERVAL: Duration = Duration::from_secs(2 * 3600);
|
||||
|
||||
@@ -298,8 +299,11 @@ impl FalukantFamilyWorker {
|
||||
}
|
||||
|
||||
conn.prepare("mark_daily", QUERY_MARK_LOVER_DAILY_DONE)?;
|
||||
conn.prepare("mark_monthly", QUERY_MARK_LOVER_MONTHLY_DONE)?;
|
||||
for l in &lovers {
|
||||
conn.execute("mark_daily", &[&l.rel_id])?;
|
||||
// Gleicher Spieltag wie Tageslauf: Geburts-/Schwangerschafts-Logik darf nicht 30 echte Tage warten.
|
||||
conn.execute("mark_monthly", &[&l.rel_id])?;
|
||||
}
|
||||
|
||||
let mut marriage_socket_users: HashSet<i32> = HashSet::new();
|
||||
@@ -664,7 +668,7 @@ impl FalukantFamilyWorker {
|
||||
self.publish_falukant_update_family_batch(¬ify, "daily");
|
||||
|
||||
drop(conn);
|
||||
// Liebschafts-Geburt: früher nur alle ~30 Tage in process_monthly — zu selten für kurze Testphasen.
|
||||
// Liebschafts-Geburt: täglich (1 Spieljahr); Monats-Stempel erfolgt oben mit dem Tageslauf.
|
||||
if let Err(e) = self.process_lover_births() {
|
||||
eprintln!("[FalukantFamilyWorker] process_lover_births: {e}");
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
//! Politische Amtsvorteile: `reputation_periodic` (täglich), Ablauf offener Ernennungen,
|
||||
//! Politische Amtsvorteile: `reputation_periodic` (täglich), **tägliches Politik-Gehalt** (pro Amt,
|
||||
//! gestaffelt nach Amts-Rang oder `daily_salary` im Benefit-JSON), Ablauf offener Ernennungen,
|
||||
//! Hilfsabfrage `free_lover_slots` für Liebschafts-Monatstick.
|
||||
//!
|
||||
//! Voraussetzungen: Migration `012_falukant_political_benefits_daemon.sql`, Backend-Seeds
|
||||
//! Voraussetzungen: Migrationen `012_falukant_political_benefits_daemon.sql`,
|
||||
//! `013_falukant_political_daily_salary.sql`, Backend-Seeds
|
||||
//! `falukant_predefine.political_office_benefit` mit JSON (`tr` oder `benefitType`, `gain`, `intervalDays`, …).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::{ConnectionPool, DbError, Row};
|
||||
use crate::message_broker::MessageBroker;
|
||||
|
||||
use crate::worker::base::BaseWorker;
|
||||
use crate::worker::falukant_certificate::political_office_name_rank;
|
||||
use crate::worker::sql::{
|
||||
QUERY_POLITICAL_APPOINTMENT_EXPIRE_PENDING, QUERY_POLITICAL_APPOINTMENT_SCHEMA_READY,
|
||||
QUERY_POLITICAL_BENEFIT_DAEMON_SCHEMA_READY, QUERY_POLITICAL_REPUTATION_APPLY_GAIN,
|
||||
QUERY_POLITICAL_BENEFIT_DAEMON_SCHEMA_READY, QUERY_POLITICAL_DAILY_SALARY_OFFICE_ROWS,
|
||||
QUERY_POLITICAL_DAILY_SALARY_USER_COLUMN_READY, QUERY_POLITICAL_REPUTATION_APPLY_GAIN,
|
||||
QUERY_POLITICAL_REPUTATION_TICK_ROWS, QUERY_POLITICAL_REPUTATION_TICK_UPSERT,
|
||||
QUERY_SUM_FREE_LOVER_SLOTS_FOR_CHARACTER,
|
||||
QUERY_SUM_FREE_LOVER_SLOTS_FOR_CHARACTER, QUERY_UPDATE_LAST_POLITICAL_DAILY_SALARY_ON,
|
||||
};
|
||||
|
||||
fn parse_i32(row: &Row, key: &str, default: i32) -> i32 {
|
||||
@@ -47,6 +54,40 @@ pub fn daemon_schema_ready(pool: &ConnectionPool) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn salary_user_column_ready(pool: &ConnectionPool) -> bool {
|
||||
let mut conn = match pool.get() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if conn
|
||||
.prepare("pds_col", QUERY_POLITICAL_DAILY_SALARY_USER_COLUMN_READY)
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let rows = match conn.execute("pds_col", &[]) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return false,
|
||||
};
|
||||
rows.first()
|
||||
.and_then(|r| r.get("ready"))
|
||||
.map(|v| v == "true" || v == "t")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Fallback-Tageseinkommen (ohne konfiguriertes `daily_salary`), gestaffelt nach Rang 1–5 (niedrig → hoch).
|
||||
fn fallback_daily_salary_for_political_rank(rank: i32) -> f64 {
|
||||
let r = if rank <= 0 { 1 } else { rank.min(5) };
|
||||
match r {
|
||||
1 => 14.0,
|
||||
2 => 28.0,
|
||||
3 => 45.0,
|
||||
4 => 65.0,
|
||||
5 => 95.0,
|
||||
_ => 14.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn appointment_schema_ready(pool: &ConnectionPool) -> bool {
|
||||
let mut conn = match pool.get() {
|
||||
Ok(c) => c,
|
||||
@@ -130,6 +171,85 @@ pub fn run_reputation_periodic_ticks(pool: &ConnectionPool, broker: &MessageBrok
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Täglich: einmal pro Kalendertag Gutschrift für Spieler mit aktivem politischen Amt.
|
||||
/// Betrag pro Amt: Summe `daily_salary` aus `political_office_benefit`, sonst Fallback nach Amtsbezeichnung (`political_office_name_rank`).
|
||||
pub fn run_daily_political_salary(pool: &ConnectionPool, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
if !salary_user_column_ready(pool) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
if conn
|
||||
.prepare("pds_rows", QUERY_POLITICAL_DAILY_SALARY_OFFICE_ROWS)
|
||||
.is_err()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let rows = conn
|
||||
.execute("pds_rows", &[])
|
||||
.map_err(|e| DbError::new(format!("[PoliticalBenefits] exec pds_rows: {e}")))?;
|
||||
|
||||
let mut totals: HashMap<i32, f64> = HashMap::new();
|
||||
|
||||
for row in rows {
|
||||
let uid = parse_i32(&row, "falukant_user_id", -1);
|
||||
if uid < 1 {
|
||||
continue;
|
||||
}
|
||||
let configured = parse_f64_from_row(&row, "configured_daily_salary");
|
||||
let office_name = row.get("office_name").map(|s| s.as_str()).unwrap_or("");
|
||||
let piece = if configured > 0.0 {
|
||||
configured
|
||||
} else {
|
||||
let r = political_office_name_rank(office_name);
|
||||
fallback_daily_salary_for_political_rank(r)
|
||||
};
|
||||
if piece <= 0.0 || !piece.is_finite() {
|
||||
continue;
|
||||
}
|
||||
*totals.entry(uid).or_insert(0.0) += piece;
|
||||
}
|
||||
|
||||
if totals.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
conn.prepare("pds_upd_date", QUERY_UPDATE_LAST_POLITICAL_DAILY_SALARY_ON)
|
||||
.map_err(|e| DbError::new(format!("[PoliticalBenefits] prepare pds_upd_date: {e}")))?;
|
||||
|
||||
let base = BaseWorker::new("PoliticalBenefits", pool.clone(), broker.clone());
|
||||
|
||||
for (uid, total) in totals {
|
||||
if total <= 0.0 || !total.is_finite() {
|
||||
continue;
|
||||
}
|
||||
|
||||
base.change_falukant_user_money(uid, total, "political daily salary")
|
||||
.map_err(|e| {
|
||||
DbError::new(format!(
|
||||
"[PoliticalBenefits] change_falukant_user_money uid={uid} total={total}: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
conn.execute("pds_upd_date", &[&uid])
|
||||
.map_err(|e| DbError::new(format!("[PoliticalBenefits] pds_upd_date uid={uid}: {e}")))?;
|
||||
|
||||
eprintln!(
|
||||
"[PoliticalBenefits] falukantUserId={} action=political_daily_salary amount={:.2}",
|
||||
uid, total
|
||||
);
|
||||
|
||||
let msg = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, uid);
|
||||
broker.publish(msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Täglich: `pending` → `expired`, wenn `expires_at` überschritten.
|
||||
pub fn expire_political_appointments(pool: &ConnectionPool) -> Result<(), DbError> {
|
||||
if !appointment_schema_ready(pool) {
|
||||
|
||||
@@ -228,6 +228,9 @@ impl PoliticsWorker {
|
||||
if let Err(e) = super::political_benefits::expire_political_appointments(pool) {
|
||||
eprintln!("[PoliticsWorker] expire_political_appointments: {e}");
|
||||
}
|
||||
if let Err(e) = super::political_benefits::run_daily_political_salary(pool, broker) {
|
||||
eprintln!("[PoliticsWorker] run_daily_political_salary: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1370,6 +1370,46 @@ pub const QUERY_POLITICAL_APPOINTMENT_EXPIRE_PENDING: &str = r#"
|
||||
AND expires_at < NOW();
|
||||
"#;
|
||||
|
||||
/// Spalte `last_political_daily_salary_on` (Migration `013_falukant_political_daily_salary.sql`).
|
||||
pub const QUERY_POLITICAL_DAILY_SALARY_USER_COLUMN_READY: &str = r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'falukant_data'
|
||||
AND table_name = 'falukant_user'
|
||||
AND column_name = 'last_political_daily_salary_on'
|
||||
) AS ready;
|
||||
"#;
|
||||
|
||||
/// Aktive Ämter mit Spieler-Charakter, noch **kein** Gehalt heute (UTC).
|
||||
/// `configured_daily_salary`: Summe aus `political_office_benefit` mit `daily_salary` im JSON; sonst 0 → Fallback im Daemon nach Amts-Rang.
|
||||
pub const QUERY_POLITICAL_DAILY_SALARY_OFFICE_ROWS: &str = r#"
|
||||
SELECT
|
||||
ch.user_id AS falukant_user_id,
|
||||
COALESCE(pot.name, '') AS office_name,
|
||||
COALESCE((
|
||||
SELECT SUM(GREATEST(0, COALESCE(NULLIF(TRIM(sb.value::jsonb->>'daily_salary'), '')::numeric, 0)))
|
||||
FROM falukant_predefine.political_office_benefit sb
|
||||
WHERE sb.political_office_type_id = po.office_type_id
|
||||
AND COALESCE(sb.value::jsonb->>'tr', sb.value::jsonb->>'benefitType') = 'daily_salary'
|
||||
), 0::numeric) AS configured_daily_salary
|
||||
FROM falukant_data.political_office po
|
||||
JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id
|
||||
JOIN falukant_data.character ch ON ch.id = po.character_id
|
||||
JOIN falukant_data.falukant_user fu ON fu.id = ch.user_id
|
||||
WHERE ch.user_id IS NOT NULL
|
||||
AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW()
|
||||
AND (fu.last_political_daily_salary_on IS NULL OR fu.last_political_daily_salary_on < CURRENT_DATE);
|
||||
"#;
|
||||
|
||||
pub const QUERY_UPDATE_LAST_POLITICAL_DAILY_SALARY_ON: &str = r#"
|
||||
UPDATE falukant_data.falukant_user
|
||||
SET last_political_daily_salary_on = CURRENT_DATE,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1::int
|
||||
AND (last_political_daily_salary_on IS NULL OR last_political_daily_salary_on < CURRENT_DATE);
|
||||
"#;
|
||||
|
||||
/// Summe `count` aus `free_lover_slots`-Benefits (JSON `tr`/`benefitType`), gedeckelt.
|
||||
pub const QUERY_SUM_FREE_LOVER_SLOTS_FOR_CHARACTER: &str = r#"
|
||||
SELECT LEAST(
|
||||
@@ -3330,7 +3370,7 @@ pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY: &str = r#"
|
||||
WHERE rs.active = true
|
||||
AND (
|
||||
rs.last_monthly_processed_at IS NULL
|
||||
OR date_trunc('month', rs.last_monthly_processed_at) < date_trunc('month', CURRENT_TIMESTAMP)
|
||||
OR rs.last_monthly_processed_at::date < CURRENT_DATE
|
||||
);
|
||||
"#;
|
||||
|
||||
@@ -3575,6 +3615,7 @@ pub const QUERY_GET_LOVER_PREGNANCY_CANDIDATES: &str = r#"
|
||||
OR (c1.gender = 'male' AND c2.gender = 'female')
|
||||
AND rs.affection >= 45
|
||||
AND rs.maintenance_level >= 30
|
||||
-- `last_monthly_processed_at` wird im Familien-Tageslauf mitgeführt (1 Spieljahr = 1 Kalendertag).
|
||||
AND rs.last_monthly_processed_at IS NOT NULL
|
||||
AND rs.last_monthly_processed_at >= NOW() - INTERVAL '50 days'
|
||||
AND NOT EXISTS (
|
||||
@@ -3582,7 +3623,7 @@ pub const QUERY_GET_LOVER_PREGNANCY_CANDIDATES: &str = r#"
|
||||
FROM falukant_data.child_relation cr
|
||||
WHERE cr.father_character_id = (CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END)
|
||||
AND cr.mother_character_id = (CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END)
|
||||
AND cr.created_at >= date_trunc('month', CURRENT_TIMESTAMP)
|
||||
AND cr.created_at::date >= CURRENT_DATE
|
||||
)
|
||||
AND (CURRENT_DATE - c_female.birthdate::date) >= 4380
|
||||
AND (CURRENT_DATE - c_female.birthdate::date) < 18993
|
||||
|
||||
Reference in New Issue
Block a user