Changes between Initial Version and Version 1 of AdvancedApplicationDevelopment


Ignore:
Timestamp:
09/24/26 02:27:06 (7 days ago)
Author:
201178
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedApplicationDevelopment

    v1 v1  
     1= Напреден апликативен развој (Трансакции, Pooling) =
     2
     3За да се разликуваат нарачките како ентитети во системот и трансакциите како теоретски поим во областа на датабазите, првите ќе ги нарекувам „нарачки", а вторите „трансакции во датабаза" во понатамошните спомнувања.
     4
     5== Pooling на конекции ==
     6
     7Во Rust апликацијата се користи библиотеката '''sqlx''' со нејзиниот `PgPool` механизам за pooling на конекции. Pool-от се креира еднаш при стартување на апликацијата и се споделува низ сите Tauri команди преку `State<'_, PgPool>`.
     8
     9Конфигурацијата е дефинирана во функцијата `create_pool()`:
     10
     11{{{
     12use sqlx::{PgPool, postgres::PgPoolOptions};
     13
     14pub async fn create_pool() -> PgPool {
     15    let database_url =
     16        "postgres://db_202526z_va_prj_poshoreca_owner:4040c5ddf69@localhost:5432/db_202526z_va_prj_poshoreca";
     17
     18    PgPoolOptions::new()
     19        .max_connections(10)
     20        .after_connect(|conn, _meta| {
     21            Box::pin(async move {
     22                sqlx::query("SET search_path TO project, public")
     23                    .execute(conn)
     24                    .await?;
     25                Ok(())
     26            })
     27        })
     28        .connect(database_url)
     29        .await
     30        .expect("Failed to connect to PostgreSQL")
     31}
     32}}}
     33
     34Овој систем одржува до '''10 паралелни конекции''', што драстично ја намалува латенцијата. Дополнително, `after_connect` гарантира дека секоја нова конекција го има поставено `search_path` на `project` шемата.
     35
     36Примери за користење на конекција од pool-от низ апликацијата:
     37
     38 * `create_order()` во `src/commands/order.rs` — користи `pool.begin()` за трансакција
     39 * `get_products()` во `src/commands/product.rs` — користи `pool.inner()` за читање
     40 * `create_payment()` во `src/commands/payment.rs` — користи `pool.inner()` за едноставен INSERT
     41 * `get_products_by_category()` во `src/commands/product.rs` — користи `pool.inner()` за филтрирано читање
     42
     43== Трансакции во датабаза ==
     44
     45За секоја операција што бара повеќекратен упис или проверка пред упис, се користи `pool.begin()` блок со цел да се елиминира ризикот од race conditions и делумни податоци. Ако било кој чекор врати грешка, целиот блок прави rollback.
     46
     47=== Пример 1: Креирање на нарачка ===
     48
     49Сместена во `src/commands/order.rs`, функцијата `create_order` ги извршува следните чекори како една атомична целина:
     50
     51 # Впишува нов запис во табелата `orders` со статус `АКТИВНА`.
     52 # Автоматски ја означува масата како `ЗАФАТЕНА` преку `set_table_status`.
     53 # Ако нешто не успее, целиот блок прави rollback и нарачката не се креира.
     54
     55{{{
     56#[tauri::command]
     57pub async fn create_order(
     58    user_id: i32,
     59    table_id: i32,
     60    pool: State<'_, PgPool>
     61) -> Result<i32, String> {
     62
     63    let mut tx = pool.begin()
     64        .await
     65        .map_err(|e| e.to_string())?;
     66
     67    let order_id: i32 = sqlx::query_scalar(
     68        "
     69        INSERT INTO orders
     70        (
     71            user_id,
     72            table_id,
     73            status
     74        )
     75        VALUES
     76        (
     77            $1,
     78            $2,
     79            'АКТИВНА'
     80        )
     81        RETURNING order_id
     82        "
     83    )
     84        .bind(user_id)
     85        .bind(table_id)
     86        .fetch_one(&mut *tx)
     87        .await
     88        .map_err(|e| e.to_string())?;
     89
     90    set_table_status(table_id, STATUS_OCCUPIED, &mut *tx)
     91        .await?;
     92
     93    tx.commit()
     94        .await
     95        .map_err(|e| e.to_string())?;
     96
     97    Ok(order_id)
     98}
     99}}}
     100
     101'''Зошто е трансакција:''' Ако нарачката се креира, но `set_table_status` не успее, нарачката не смее да остане во базата. Трансакцијата гарантира дека или '''двете''' операции успеваат, или '''ниту една'''.
     102
     103=== Пример 2: Процесирање на плаќање ===
     104
     105Сместена во `src/commands/order.rs`, функцијата `pay_order` ги извршува следните чекори како една трансакција:
     106
     107 # Проверува дали нарачката е веќе платена.
     108 # Го зема `table_id` за подоцна да ја ослободи масата.
     109 # Ја пресметува вкупната сума од `order_item`.
     110 # Ако сумата е нула, ја отфрла трансакцијата.
     111 # Впишува запис во `payment`.
     112 # Ја ажурира состојбата на нарачката на `ПЛАТЕНА`.
     113 # Го намалува залихот на производите преку `update_stock`.
     114 # Ја ослободува масата преку `set_table_status`.
     115 # Генерира запис во `invoice` со број `INV-{order_id}`.
     116 # Ако било кој чекор не успее, целиот блок прави rollback.
     117
     118{{{
     119#[tauri::command]
     120pub async fn pay_order(
     121    order_id: i32,
     122    method: String,
     123    pool: State<'_, PgPool>
     124) -> Result<String, String> {
     125
     126    let mut tx = pool.begin()
     127        .await
     128        .map_err(|e| e.to_string())?;
     129
     130    let existing: Option<i32> = sqlx::query_scalar(
     131        "
     132        SELECT payment_id
     133        FROM payment
     134        WHERE order_id = $1
     135        "
     136    )
     137        .bind(order_id)
     138        .fetch_optional(&mut *tx)
     139        .await
     140        .map_err(|e| e.to_string())?;
     141
     142    if existing.is_some() {
     143        return Err("Нарачката е веќе платена".to_string());
     144    }
     145
     146    let table_id: i32 = sqlx::query_scalar(
     147        "
     148        SELECT table_id
     149        FROM orders
     150        WHERE order_id = $1
     151        "
     152    )
     153        .bind(order_id)
     154        .fetch_one(&mut *tx)
     155        .await
     156        .map_err(|e| e.to_string())?;
     157
     158    let total: Decimal = sqlx::query_scalar(
     159        "
     160        SELECT COALESCE(SUM(quantity * unit_price), 0)
     161        FROM order_item
     162        WHERE order_id = $1
     163        "
     164    )
     165        .bind(order_id)
     166        .fetch_one(&mut *tx)
     167        .await
     168        .map_err(|e| e.to_string())?;
     169
     170    if total == Decimal::ZERO {
     171        return Err("Нарачката е празна".to_string());
     172    }
     173
     174    let payment_id: i32 = sqlx::query_scalar(
     175        "
     176        INSERT INTO payment
     177        (
     178            order_id,
     179            amount,
     180            method
     181        )
     182        VALUES
     183        (
     184            $1,
     185            $2,
     186            $3
     187        )
     188        RETURNING payment_id
     189        "
     190    )
     191        .bind(order_id)
     192        .bind(total)
     193        .bind(method)
     194        .fetch_one(&mut *tx)
     195        .await
     196        .map_err(|e| e.to_string())?;
     197
     198    sqlx::query(
     199        "
     200        UPDATE orders
     201        SET status = 'ПЛАТЕНА'
     202        WHERE order_id = $1
     203        "
     204    )
     205        .bind(order_id)
     206        .execute(&mut *tx)
     207        .await
     208        .map_err(|e| e.to_string())?;
     209
     210    update_stock(order_id, &mut tx)
     211        .await?;
     212
     213    set_table_status(table_id, STATUS_FREE, &mut *tx)
     214        .await?;
     215
     216    let invoice_number = format!("INV-{}", order_id);
     217
     218    sqlx::query(
     219        "
     220        INSERT INTO invoice
     221        (
     222            payment_id,
     223            invoice_number
     224        )
     225        VALUES
     226        (
     227            $1,
     228            $2
     229        )
     230        "
     231    )
     232        .bind(payment_id)
     233        .bind(&invoice_number)
     234        .execute(&mut *tx)
     235        .await
     236        .map_err(|e| e.to_string())?;
     237
     238    tx.commit()
     239        .await
     240        .map_err(|e| e.to_string())?;
     241
     242    Ok(invoice_number)
     243}
     244}}}
     245
     246'''Зошто е трансакција:''' Ова е најкомплексната операција во системот. Вклучува 6+ операции кои мора сите да успеат или ниту една:
     247
     248 * Ако плаќањето успее, но `update_stock` не успее → залихот нема да се намали.
     249 * Ако `update_stock` успее, но `invoice` не успее → фактурата нема да се генерира.
     250 * Ако масата не се ослободи → масата останува зафатена засекогаш.
     251
     252Трансакцијата гарантира '''atomicity''' — сè или ништо.
     253
     254=== Пример 3: Додавање на ставка во нарачка ===
     255
     256Сместена во `src/commands/order_item.rs`, функцијата `add_order_item` користи трансакција за да провери дали производот веќе постои во нарачката пред да го вметне или ажурира.
     257
     258{{{
     259#[tauri::command]
     260pub async fn add_order_item(
     261    token: String,
     262    order_id: i32,
     263    product_id: i32,
     264    quantity: i32,
     265    pool: State<'_, PgPool>,
     266    sessions: State<'_, ActiveSessions>,
     267) -> Result<(), String> {
     268    {
     269        let lock = sessions.0.lock().map_err(|_| "Системска грешка".to_string())?;
     270        if !lock.contains_key(&token) {
     271            return Err("Невалидна сесија. Најавете се повторно.".to_string());
     272        }
     273    }
     274
     275    if quantity <= 0 {
     276        return Err("Количината мора да биде поголема од 0".to_string());
     277    }
     278
     279    let mut tx = pool.begin().await.map_err(|e| e.to_string())?;
     280
     281    let existing: Option<i32> = sqlx::query_scalar(
     282        "
     283        SELECT item_number
     284        FROM order_item
     285        WHERE order_id = $1
     286        AND product_id = $2
     287        "
     288    )
     289        .bind(order_id)
     290        .bind(product_id)
     291        .fetch_optional(&mut *tx)
     292        .await
     293        .map_err(|e| e.to_string())?;
     294
     295    if let Some(item_number) = existing {
     296        sqlx::query(
     297            "
     298            UPDATE order_item
     299            SET quantity = quantity + $3
     300            WHERE order_id = $1
     301            AND item_number = $2
     302            "
     303        )
     304            .bind(order_id)
     305            .bind(item_number)
     306            .bind(quantity)
     307            .execute(&mut *tx)
     308            .await
     309            .map_err(|e| e.to_string())?;
     310    } else {
     311        let next_item_number: i32 = sqlx::query_scalar(
     312            "
     313            SELECT COALESCE(MAX(item_number), 0) + 1
     314            FROM order_item
     315            WHERE order_id = $1
     316            "
     317        )
     318            .bind(order_id)
     319            .fetch_one(&mut *tx)
     320            .await
     321            .map_err(|e| e.to_string())?;
     322
     323        let unit_price: rust_decimal::Decimal = sqlx::query_scalar(
     324            "SELECT price FROM product WHERE product_id = $1"
     325        )
     326            .bind(product_id)
     327            .fetch_one(&mut *tx)
     328            .await
     329            .map_err(|e| e.to_string())?;
     330
     331        sqlx::query(
     332            "
     333            INSERT INTO order_item (order_id, item_number, product_id, quantity, unit_price)
     334            VALUES ($1, $2, $3, $4, $5)
     335            "
     336        )
     337            .bind(order_id)
     338            .bind(next_item_number)
     339            .bind(product_id)
     340            .bind(quantity)
     341            .bind(unit_price)
     342            .execute(&mut *tx)
     343            .await
     344            .map_err(|e| e.to_string())?;
     345    }
     346
     347    tx.commit().await.map_err(|e| e.to_string())?;
     348    Ok(())
     349}
     350}}}
     351
     352=== Пример 4: Затворање на смена (Z-извештај) ===
     353
     354Сместена во `src/commands/report.rs`, функцијата `z_report` користи трансакција за атомично пресметување и впишување на Z-извештајот.
     355
     356{{{
     357#[tauri::command]
     358pub async fn z_report(
     359    token: String,
     360    pool: State<'_, PgPool>,
     361    sessions: State<'_, ActiveSessions>,
     362) -> Result<serde_json::Value, String> {
     363    let session = require_admin_session(&token, sessions.inner()).await?;
     364    let admin_id = session.user_id;
     365
     366    let mut tx = pool.begin()
     367        .await
     368        .map_err(|e| e.to_string())?;
     369
     370    let last_close: Option<chrono::NaiveDateTime> = sqlx::query_scalar(
     371        "SELECT MAX(closed_at) FROM shift_close"
     372    )
     373        .fetch_one(&mut *tx)
     374        .await
     375        .map_err(|e| e.to_string())?;
     376
     377    let since = last_close.unwrap_or_else(|| {
     378        chrono::NaiveDate::from_ymd_opt(2000, 1, 1)
     379            .unwrap()
     380            .and_hms_opt(0, 0, 0)
     381            .unwrap()
     382    });
     383
     384    let row = sqlx::query(
     385        "
     386        SELECT
     387            COALESCE(SUM(amount), 0) as total,
     388            COUNT(*) as order_count
     389        FROM payment
     390        WHERE payment_date > $1
     391        "
     392    )
     393        .bind(since)
     394        .fetch_one(&mut *tx)
     395        .await
     396        .map_err(|e| e.to_string())?;
     397
     398    let total: Decimal = row.get("total");
     399    let order_count: i64 = row.get("order_count");
     400
     401    sqlx::query(
     402        "
     403        INSERT INTO shift_close (closed_by, total, order_count)
     404        VALUES ($1, $2, $3)
     405        "
     406    )
     407        .bind(admin_id)
     408        .bind(total)
     409        .bind(order_count as i32)
     410        .execute(&mut *tx)
     411        .await
     412        .map_err(|e| e.to_string())?;
     413
     414    tx.commit()
     415        .await
     416        .map_err(|e| e.to_string())?;
     417
     418    Ok(serde_json::json!({
     419        "total": total.to_string(),
     420        "order_count": order_count,
     421        "since": since.to_string()
     422    }))
     423}
     424}}}
     425
     426=== Пример 5: Намалување на залиха при продажба ===
     427
     428Сместена во `src/commands/inventory.rs`, функцијата `update_stock` се повикува **во истата трансакција** како и `pay_order`, што гарантира дека залихот е секогаш синхронизиран со продажбата.
     429
     430{{{
     431pub async fn update_stock(
     432    order_id: i32,
     433    conn: &mut sqlx::PgConnection
     434) -> Result<(), String> {
     435    let rows = sqlx::query(
     436        "SELECT product_id, quantity FROM order_item WHERE order_id = $1"
     437    )
     438        .bind(order_id)
     439        .fetch_all(&mut *conn)
     440        .await
     441        .map_err(|e| e.to_string())?;
     442
     443    for row in rows {
     444        let p_id: i32 = row.get("product_id");
     445        let order_qty: i32 = row.get("quantity");
     446
     447        let recipe_row = sqlx::query(
     448            "SELECT recipe_id FROM recipe WHERE product_id = $1"
     449        )
     450            .bind(p_id)
     451            .fetch_optional(&mut *conn)
     452            .await
     453            .map_err(|e| e.to_string())?;
     454
     455        if let Some(r) = recipe_row {
     456            let r_id: i32 = r.get("recipe_id");
     457
     458            let ings = sqlx::query(
     459                "SELECT ingredient_id, quantity_needed FROM recipe_item WHERE recipe_id = $1"
     460            )
     461                .bind(r_id)
     462                .fetch_all(&mut *conn)
     463                .await
     464                .map_err(|e| e.to_string())?;
     465
     466            for ing in ings {
     467                let ing_id: i32 = ing.get("ingredient_id");
     468                let qty_needed: rust_decimal::Decimal = ing.get("quantity_needed");
     469
     470                sqlx::query(
     471                    "UPDATE ingredient SET current_stock = current_stock - ($1 * $2) WHERE ingredient_id = $3"
     472                )
     473                    .bind(qty_needed)
     474                    .bind(order_qty as f64)
     475                    .bind(ing_id)
     476                    .execute(&mut *conn)
     477                    .await
     478                    .map_err(|e| e.to_string())?;
     479            }
     480        } else {
     481            sqlx::query(
     482                "INSERT INTO inventory (product_id, quantity_change, operation_type) VALUES ($1, -$2, 'ПРОДАЖБА')"
     483            )
     484                .bind(p_id)
     485                .bind(order_qty)
     486                .execute(&mut *conn)
     487                .await
     488                .map_err(|e| e.to_string())?;
     489        }
     490    }
     491    Ok(())
     492}
     493}}}
     494
     495=== Заштитен механизам ===
     496
     497Ако било кој чекор во `pool.begin()` блокот врати грешка (на пример: нарачката е веќе платена, сумата е нула, масата не постои), целиот блок прави '''rollback''' и извршувањето се прекинува. Ова значи дека '''ниту еден делумен податок не останува трајно зачуван''' во базата.
     498
     499Овој механизам е критичен за:
     500
     501 * Да се спречи креирање на нарачка без означена маса како зафатена.
     502 * Да се спречи двојно плаќање на иста нарачка (проверка `existing.is_some()`).
     503 * Да се спречи ослободување на маса без успешно плаќање.
     504 * Да се осигура дека залихот е секогаш конзистентен со продажбата.
     505 * Да се осигура дека фактурата е секогаш генерирана со плаќањето.
     506 * Да се осигура дека Z-извештајот е секогаш конзистентен со плаќањата.