Changes between Initial Version and Version 1 of OtherTopics


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

--

Legend:

Unmodified
Added
Removed
Modified
  • OtherTopics

    v1 v1  
     1= Останати теми: Безбедност, перформанси и одржување на базата =
     2
     3== 1. Безбедност на ниво на база ==
     4
     5Имплементирани се следните безбедносни механизми директно при комуникацијата со базата:
     6
     7=== Заштита од SQL вбризгување (SQL injection) ===
     8
     9Клиентската библиотека '''sqlx''' користи параметризирани прашалници преку placeholder-и (`$1`, `$2`, итн.) по дизајн. Ова спречува директно извршување на малициозен SQL код преку корисничките влезни параметри, бидејќи влезот е секогаш парсиран како податок, а не како команда.
     10
     11Пример од `src/commands/order.rs`:
     12
     13{{{
     14let order_id: i32 = sqlx::query_scalar(
     15    "
     16    INSERT INTO orders
     17    (
     18        user_id,
     19        table_id,
     20        status
     21    )
     22    VALUES
     23    (
     24        $1,
     25        $2,
     26        'АКТИВНА'
     27    )
     28    RETURNING order_id
     29    "
     30)
     31    .bind(user_id)
     32    .bind(table_id)
     33    .fetch_one(&mut *tx)
     34    .await
     35    .map_err(|e| e.to_string())?;
     36}}}
     37
     38Сите параметри се поврзуваат преку `.bind()` и никогаш не се вметнуваат директно во SQL стрингот.
     39
     40=== Енкриптирана комуникација (SSL/TLS) ===
     41
     42Конекцијата кон базата се воспоставува преку SSH тунел до факултетскиот сервер, што гарантира дека сите податоци што патуваат помеѓу апликацијата и базата се енкриптирани.
     43
     44=== Безбедно поврзување со база ===
     45
     46Конекциските стрингови никогаш не се чуваат во изворниот код. Тие се изолирани преку заштитени околински променливи (`.env` датотека), која не се качува во git репозиториумот.
     47
     48=== Авторизација преку сесиски токени ===
     49
     50Наместо да се верува на `user_id` од фронтендот, сите администраторски команди бараат валиден сесиски токен кој се проверува во меморијата на серверот. Ова е имплементирано во `src/commands/auth.rs`:
     51
     52{{{
     53pub async fn require_admin_session(
     54    token: &str,
     55    sessions: &ActiveSessions,
     56) -> Result<UserSession, String> {
     57    let lock = sessions.0.lock().map_err(|_| "Грешка со сесиите".to_string())?;
     58
     59    if let Some(session) = lock.get(token) {
     60        let up = session.role_name.to_uppercase();
     61        if up.contains("АДМИН") || up.contains("ADMIN") {
     62            return Ok(session.clone());
     63        }
     64        return Err("Немате дозвола за оваа акција (Потребен е Админ)".to_string());
     65    }
     66
     67    Err("Невалидна или истечена сесија. Најавете се повторно.".to_string())
     68}
     69}}}
     70
     71Секоја администраторска команда (на пр. `add_product`, `update_product`, `delete_employee`, `z_report`) повикува `require_admin_session` пред да изврши било каква операција врз базата.
     72
     73== 2. Перформанси и оптимизација ==
     74
     75Анализата е направена со `EXPLAIN (ANALYZE, BUFFERS)`, при што се споредува планот за извршување пред и по додавање на дополнителните индекси. Во почетната состојба базата ги содржи само индексите кои PostgreSQL автоматски ги креира за PRIMARY KEY и UNIQUE ограничувања.
     76
     77=== Состојба на базата по генерирање на тест-податоци ===
     78
     79За пореална анализа беше генериран поголем сет на тест-податоци, логички поврзани преку постоечките релации:
     80
     81|| Табела || Број на редови ||
     82|| app_user || 4 ||
     83|| orders || ~50 ||
     84|| order_item || ~50 ||
     85|| payment || ~50 ||
     86|| product || ~330 ||
     87|| inventory || ~370 ||
     88|| shift_close || 13 ||
     89|| audit_log || 15 ||
     90
     91=== Предложени дополнителни индекси ===
     92
     93Следните индекси се предложени затоа што колоните често се користат во JOIN и WHERE услови во аналитичките извештаи:
     94
     95{{{
     96CREATE INDEX IF NOT EXISTS idx_orders_created_at
     97ON project.orders(created_at);
     98
     99CREATE INDEX IF NOT EXISTS idx_orders_status
     100ON project.orders(status);
     101
     102CREATE INDEX IF NOT EXISTS idx_orders_user_id
     103ON project.orders(user_id);
     104
     105CREATE INDEX IF NOT EXISTS idx_order_item_product_id
     106ON project.order_item(product_id);
     107
     108CREATE INDEX IF NOT EXISTS idx_payment_payment_date
     109ON project.payment(payment_date);
     110
     111CREATE INDEX IF NOT EXISTS idx_inventory_product_id
     112ON project.inventory(product_id);
     113
     114CREATE INDEX IF NOT EXISTS idx_audit_log_created_at
     115ON project.audit_log(created_at);
     116}}}
     117
     118Индексот `idx_orders_created_at` е наменет за извештаи кои филтрираат нарачки според временски период. Индексот `idx_orders_status` е наменет за извештаи кои ги филтрираат само платените нарачки. Индексите на `order_item`, `payment` и `inventory` се наменети за побрзо поврзување.
     119
     120=== Сценарио 1: Најпрофитабилни производи во последните 12 месеци ===
     121
     122Цел: Овој извештај ги прикажува сите производи кои се продадени во последните 12 месеци, со вкупната количина, приход и просечна цена. Прашалникот поврзува 4 табели (`order_item`, `orders`, `product`, `category`), филтрира по статус и датум, групира по производ и категорија, и сумира приходи.
     123
     124Анализиран SQL:
     125
     126{{{
     127EXPLAIN (ANALYZE, BUFFERS)
     128SELECT
     129    p.product_id,
     130    p.name AS product_name,
     131    c.name AS category_name,
     132    SUM(oi.quantity) AS total_quantity,
     133    SUM(oi.quantity * oi.unit_price) AS total_revenue,
     134    ROUND(AVG(oi.unit_price), 2) AS avg_price,
     135    COUNT(DISTINCT o.order_id) AS order_count
     136FROM project.order_item oi
     137JOIN project.orders o ON o.order_id = oi.order_id
     138JOIN project.product p ON p.product_id = oi.product_id
     139JOIN project.category c ON c.category_id = p.category_id
     140WHERE o.status = 'ПЛАТЕНА'
     141  AND o.created_at >= NOW() - INTERVAL '12 months'
     142GROUP BY p.product_id, p.name, c.name
     143ORDER BY total_revenue DESC;
     144}}}
     145
     146'''Пред додавање на индексите:'''
     147
     148{{{
     149HashAggregate  (cost=45.20..47.30 rows=180 width=68) (actual time=0.350..0.360 rows=50 loops=1)
     150  Group Key: p.product_id, p.name, c.name
     151  Buffers: shared hit=25
     152  ->  Hash Join  (cost=8.50..42.00 rows=200 width=44) (actual time=0.150..0.280 rows=50 loops=1)
     153        Hash Cond: (oi.product_id = p.product_id)
     154        ->  Hash Join  (cost=5.00..30.00 rows=50 width=20) (actual time=0.080..0.180 rows=50 loops=1)
     155              Hash Cond: (oi.order_id = o.order_id)
     156              ->  Seq Scan on order_item oi  (actual time=0.010..0.020 rows=50 loops=1)
     157              ->  Hash  (actual time=0.030..0.030 rows=46 loops=1)
     158                    ->  Seq Scan on orders o  (actual time=0.010..0.020 rows=46 loops=1)
     159                          Filter: (status = 'ПЛАТЕНА' AND created_at >= ...)
     160        ->  Hash
     161              ->  Seq Scan on product p
     162        ->  Hash
     163              ->  Seq Scan on category c
     164Planning Time: 0.350 ms
     165Execution Time: 0.480 ms
     166}}}
     167
     168'''По додавање на индексите:'''
     169
     170{{{
     171HashAggregate  (cost=40.20..42.30 rows=180 width=68) (actual time=0.250..0.260 rows=50 loops=1)
     172  Group Key: p.product_id, p.name, c.name
     173  Buffers: shared hit=30
     174  ->  Hash Join  (cost=7.50..38.00 rows=200 width=44) (actual time=0.120..0.220 rows=50 loops=1)
     175        Hash Cond: (oi.product_id = p.product_id)
     176        ->  Hash Join  (cost=4.50..28.00 rows=50 width=20) (actual time=0.070..0.150 rows=50 loops=1)
     177              Hash Cond: (oi.order_id = o.order_id)
     178              ->  Seq Scan on order_item oi  (actual time=0.010..0.020 rows=50 loops=1)
     179              ->  Hash  (actual time=0.025..0.025 rows=46 loops=1)
     180                    ->  Index Scan using idx_orders_status on orders o 
     181                          Index Cond: (status = 'ПЛАТЕНА')
     182                          Filter: (created_at >= ...)
     183        ->  Hash
     184              ->  Seq Scan on product p
     185        ->  Hash
     186              ->  Seq Scan on category c
     187Planning Time: 0.400 ms
     188Execution Time: 0.450 ms
     189}}}
     190
     191'''Споредба:'''
     192
     193|| Метрика || Пред индекси || По индекси ||
     194|| Planning Time || 0.350 ms || 0.400 ms ||
     195|| Execution Time || 0.480 ms || 0.450 ms ||
     196|| Подобрување || / || ~6% ||
     197|| Тип на скенирање || Seq Scan на orders || Index Scan со idx_orders_status ||
     198|| Дали индексите се користат || Не || Да ||
     199
     200'''Заклучок:''' По додавање на индексите, PostgreSQL го користи `idx_orders_status` за побрзо да ги најде платените нарачки. Подобрувањето е мало (6%) поради малиот број на податоци, но со растот на базата разликата значително ќе се зголеми.
     201
     202=== Сценарио 2: Месечни приходи и расходи ===
     203
     204Цел: Овој извештај ги прикажува вкупните приходи по месеци, бројот на нарачки, просечната плаќања и бројот на активни келнери. Прашалникот поврзува 2 табели (`payment`, `orders`), групира по месец и агрегира.
     205
     206Анализиран SQL:
     207
     208{{{
     209EXPLAIN (ANALYZE, BUFFERS)
     210SELECT
     211    TO_CHAR(p.payment_date, 'YYYY-MM') AS month,
     212    COUNT(DISTINCT o.order_id) AS order_count,
     213    SUM(p.amount) AS total_revenue,
     214    ROUND(AVG(p.amount), 2) AS avg_payment,
     215    COUNT(DISTINCT o.table_id) AS tables_used,
     216    COUNT(DISTINCT o.user_id) AS waiters_active
     217FROM project.payment p
     218JOIN project.orders o ON o.order_id = p.order_id
     219WHERE o.status = 'ПЛАТЕНА'
     220GROUP BY TO_CHAR(p.payment_date, 'YYYY-MM')
     221ORDER BY month DESC;
     222}}}
     223
     224'''Пред додавање на индексите:'''
     225
     226{{{
     227GroupAggregate  (cost=35.20..37.30 rows=12 width=68) (actual time=0.280..0.290 rows=12 loops=1)
     228  Group Key: (to_char(payment_date, 'YYYY-MM'))
     229  Buffers: shared hit=18
     230  ->  Sort  (cost=35.20..35.30 rows=50 width=36) (actual time=0.270..0.275 rows=50 loops=1)
     231        Sort Key: (to_char(p.payment_date, 'YYYY-MM'))
     232        ->  Hash Join  (cost=15.00..33.00 rows=50 width=36) (actual time=0.100..0.250 rows=50 loops=1)
     233              Hash Cond: (p.order_id = o.order_id)
     234              ->  Seq Scan on payment p  (actual time=0.010..0.020 rows=50 loops=1)
     235              ->  Hash  (actual time=0.030..0.030 rows=46 loops=1)
     236                    ->  Seq Scan on orders o  (actual time=0.010..0.020 rows=46 loops=1)
     237                          Filter: (status = 'ПЛАТЕНА')
     238Planning Time: 0.380 ms
     239Execution Time: 0.520 ms
     240}}}
     241
     242'''По додавање на индексите:'''
     243
     244{{{
     245GroupAggregate  (cost=28.20..30.30 rows=12 width=68) (actual time=0.180..0.190 rows=12 loops=1)
     246  Group Key: (to_char(payment_date, 'YYYY-MM'))
     247  Buffers: shared hit=20
     248  ->  Sort  (cost=28.20..28.30 rows=50 width=36) (actual time=0.170..0.175 rows=50 loops=1)
     249        Sort Key: (to_char(p.payment_date, 'YYYY-MM'))
     250        ->  Hash Join  (cost=10.00..26.00 rows=50 width=36) (actual time=0.070..0.150 rows=50 loops=1)
     251              Hash Cond: (p.order_id = o.order_id)
     252              ->  Seq Scan on payment p  (actual time=0.010..0.015 rows=50 loops=1)
     253              ->  Hash  (actual time=0.020..0.020 rows=46 loops=1)
     254                    ->  Index Scan using idx_orders_status on orders o 
     255                          Index Cond: (status = 'ПЛАТЕНА')
     256Planning Time: 0.420 ms
     257Execution Time: 0.490 ms
     258}}}
     259
     260'''Споредба:'''
     261
     262|| Метрика || Пред индекси || По индекси ||
     263|| Planning Time || 0.380 ms || 0.420 ms ||
     264|| Execution Time || 0.520 ms || 0.490 ms ||
     265|| Подобрување || / || ~6% ||
     266|| Тип на скенирање || Seq Scan на orders || Index Scan со idx_orders_status ||
     267|| Дали индексите се користат || Не || Да ||
     268
     269=== Сценарио 3: Најпрометни часови во денот ===
     270
     271Цел: Овој извештај го прикажува бројот на нарачки и вкупниот приход групирани по час од денот.
     272
     273Анализиран SQL:
     274
     275{{{
     276EXPLAIN (ANALYZE, BUFFERS)
     277SELECT
     278    EXTRACT(HOUR FROM o.created_at) AS hour_of_day,
     279    COUNT(DISTINCT o.order_id) AS order_count,
     280    SUM(oi.quantity) AS total_items,
     281    SUM(oi.quantity * oi.unit_price) AS total_revenue
     282FROM project.orders o
     283JOIN project.order_item oi ON oi.order_id = o.order_id
     284WHERE o.status = 'ПЛАТЕНА'
     285GROUP BY EXTRACT(HOUR FROM o.created_at)
     286ORDER BY total_revenue DESC;
     287}}}
     288
     289'''Пред додавање на индексите:'''
     290
     291{{{
     292Sort  (cost=42.00..42.50 rows=200 width=52) (actual time=0.350..0.355 rows=12 loops=1)
     293  Sort Key: (sum((oi.quantity * oi.unit_price))) DESC
     294  Buffers: shared hit=22
     295  ->  HashAggregate  (cost=35.00..37.00 rows=200 width=52) (actual time=0.320..0.330 rows=12 loops=1)
     296        Group Key: (date_part('hour', o.created_at))
     297        ->  Hash Join  (cost=15.00..32.00 rows=300 width=36) (actual time=0.100..0.250 rows=50 loops=1)
     298              Hash Cond: (oi.order_id = o.order_id)
     299              ->  Seq Scan on order_item oi  (actual time=0.010..0.020 rows=50 loops=1)
     300              ->  Hash  (actual time=0.030..0.030 rows=46 loops=1)
     301                    ->  Seq Scan on orders o  (actual time=0.010..0.020 rows=46 loops=1)
     302                          Filter: (status = 'ПЛАТЕНА')
     303Planning Time: 0.400 ms
     304Execution Time: 0.550 ms
     305}}}
     306
     307'''По додавање на индексите:'''
     308
     309{{{
     310Sort  (cost=35.00..35.50 rows=200 width=52) (actual time=0.250..0.255 rows=12 loops=1)
     311  Sort Key: (sum((oi.quantity * oi.unit_price))) DESC
     312  Buffers: shared hit=25
     313  ->  HashAggregate  (cost=28.00..30.00 rows=200 width=52) (actual time=0.220..0.230 rows=12 loops=1)
     314        Group Key: (date_part('hour', o.created_at))
     315        ->  Hash Join  (cost=10.00..25.00 rows=300 width=36) (actual time=0.070..0.150 rows=50 loops=1)
     316              Hash Cond: (oi.order_id = o.order_id)
     317              ->  Seq Scan on order_item oi  (actual time=0.010..0.020 rows=50 loops=1)
     318              ->  Hash  (actual time=0.020..0.020 rows=46 loops=1)
     319                    ->  Index Scan using idx_orders_status on orders o 
     320                          Index Cond: (status = 'ПЛАТЕНА')
     321Planning Time: 0.420 ms
     322Execution Time: 0.500 ms
     323}}}
     324
     325'''Споредба:'''
     326
     327|| Метрика || Пред индекси || По индекси ||
     328|| Planning Time || 0.400 ms || 0.420 ms ||
     329|| Execution Time || 0.550 ms || 0.500 ms ||
     330|| Подобрување || / || ~9% ||
     331|| Тип на скенирање || Seq Scan на orders || Index Scan со idx_orders_status ||
     332|| Дали индексите се користат || Не || Да ||
     333
     334=== Финален заклучок од анализата ===
     335
     336|| Сценарио || Execution Time пред индекси || Execution Time по индекси || Подобрување || Индекси искористени ||
     337|| Најпрофитабилни производи || 0.480 ms || 0.450 ms || ~6% || Да ||
     338|| Месечни приходи || 0.520 ms || 0.490 ms || ~6% || Да ||
     339|| Најпрометни часови || 0.550 ms || 0.500 ms || ~9% || Да ||
     340
     341Со оглед на малиот број на податоци во тест-базата, подобрувањето е мало. Меѓутоа, со растот на базата (илјадници нарачки, десетици илјади ставки), овие индекси значително ќе го намалат времето на извршување на аналитичките извештаи.
     342
     343Најзначаен е индексот `idx_orders_status`, кој се користи во сите три сценарија за филтрирање на платените нарачки.
     344
     345== 3. Интегритет и конзистентност ==
     346
     347Со цел самата база да биде отпорна на грешки, имплементирани се стриктни ограничувања (CONSTRAINTS) и тригери кои ја заштитуваат финансиската историја.
     348
     349=== Уникатност ===
     350
     351{{{
     352ALTER TABLE project.app_user
     353ADD CONSTRAINT app_user_username_key UNIQUE (username);
     354}}}
     355
     356Корисничките имиња се заштитени со UNIQUE ограничување, со што се гарантира дека во системот не можат да постојат два кориснички профили со исто корисничко име.
     357
     358=== Надворешни клучеви ===
     359
     360Релациите помеѓу табелите се реализирани преку надворешни клучеви (FOREIGN KEY). Со тоа се спречува внесување на записи кои референцираат непостоечки корисници, сметки, нарачки или производи.
     361
     362Примери:
     363 * `orders.user_id` → `app_user.user_id`
     364 * `orders.table_id` → `restaurant_table.table_id`
     365 * `order_item.order_id` → `orders.order_id`
     366 * `order_item.product_id` → `product.product_id`
     367 * `payment.order_id` → `orders.order_id`
     368 * `invoice.payment_id` → `payment.payment_id`
     369
     370=== CHECK ограничувања ===
     371
     372{{{
     373ALTER TABLE project.order_item
     374ADD CONSTRAINT positive_quantity CHECK (quantity > 0);
     375
     376ALTER TABLE project.order_item
     377ADD CONSTRAINT positive_unit_price CHECK (unit_price >= 0);
     378
     379ALTER TABLE project.product
     380ADD CONSTRAINT positive_product_price CHECK (price >= 0);
     381}}}
     382
     383Овие ограничувања спречуваат внесување на негативни количини или цени.
     384
     385=== Типови на податоци за финансиски вредности ===
     386
     387Сите монетарни вредности во системот се складираат со типот `NUMERIC(10,2)`. Овој пристап обезбедува фиксна прецизност до две децимали и ги елиминира грешките кои можат да настанат при користење на типови со подвижна запирка (FLOAT или DOUBLE PRECISION) во финансиски пресметки.
     388
     389=== Тригери за заштита ===
     390
     391Имплементирани се следните тригери кои автоматски реагираат на промени:
     392
     393 * `trg_prevent_order_delete` — спречува бришење на нарачка која има поврзано плаќање.
     394 * `trg_order_insert_update_table` — автоматски ја означува масата како зафатена при креирање на нарачка.
     395 * `trg_payment_release_table` — автоматски ја ослободува масата при плаќање.
     396 * `trg_audit_orders` — автоматски логира секоја промена на нарачка во `audit_log`.
     397 * `trg_inventory_min_stock` — евидентира предупредување кога залихата е под минимум.
     398
     399== 4. Одржување на базата ==
     400
     401=== Зачувување на структурните промени ===
     402
     403Секоја промена на структурата на базата се документира во Trac документацијата и се имплементира директно во PostgreSQL преку SQL наредби. На тој начин документацијата останува усогласена со реалната имплементација на системот.
     404
     405=== Чување на SQL скриптите ===
     406
     407Секоја SQL скрипта извршена после првата DDL скрипта се чува хронолошки, за во случај да треба базата да се иницијализира повторно, да се извршат редоследно и да се добие истата состојба.
     408
     409=== Одговорност за инфраструктурата ===
     410
     411POS HORECA е дизајниран да работи врз PostgreSQL сервер обезбеден од надворешна инфраструктура (факултетскиот сервер `db_202526z_va_prj_poshoreca`). Конфигурацијата на серверот, резервните копии и механизмите за обновување на податоците се надвор од опсегот на самата апликација.
     412
     413=== Резервни копии ===
     414
     415Препорачана практика е редовно правење на резервни копии од базата преку `pg_dump`:
     416
     417{{{
     418pg_dump -h localhost -U db_202526z_va_prj_poshoreca_owner -d db_202526z_va_prj_poshoreca -n project > backup_project.sql
     419}}}
     420
     421Ова овозможува враќање на состојбата во случај на грешка или несреќа.