| Version 1 (modified by , 7 days ago) ( diff ) |
|---|
Останати теми: Безбедност, перформанси и одржување на базата
1. Безбедност на ниво на база
Имплементирани се следните безбедносни механизми директно при комуникацијата со базата:
Заштита од SQL вбризгување (SQL injection)
Клиентската библиотека sqlx користи параметризирани прашалници преку placeholder-и ($1, $2, итн.) по дизајн. Ова спречува директно извршување на малициозен SQL код преку корисничките влезни параметри, бидејќи влезот е секогаш парсиран како податок, а не како команда.
Пример од src/commands/order.rs:
let order_id: i32 = sqlx::query_scalar(
"
INSERT INTO orders
(
user_id,
table_id,
status
)
VALUES
(
$1,
$2,
'АКТИВНА'
)
RETURNING order_id
"
)
.bind(user_id)
.bind(table_id)
.fetch_one(&mut *tx)
.await
.map_err(|e| e.to_string())?;
Сите параметри се поврзуваат преку .bind() и никогаш не се вметнуваат директно во SQL стрингот.
Енкриптирана комуникација (SSL/TLS)
Конекцијата кон базата се воспоставува преку SSH тунел до факултетскиот сервер, што гарантира дека сите податоци што патуваат помеѓу апликацијата и базата се енкриптирани.
Безбедно поврзување со база
Конекциските стрингови никогаш не се чуваат во изворниот код. Тие се изолирани преку заштитени околински променливи (.env датотека), која не се качува во git репозиториумот.
Авторизација преку сесиски токени
Наместо да се верува на user_id од фронтендот, сите администраторски команди бараат валиден сесиски токен кој се проверува во меморијата на серверот. Ова е имплементирано во src/commands/auth.rs:
pub async fn require_admin_session(
token: &str,
sessions: &ActiveSessions,
) -> Result<UserSession, String> {
let lock = sessions.0.lock().map_err(|_| "Грешка со сесиите".to_string())?;
if let Some(session) = lock.get(token) {
let up = session.role_name.to_uppercase();
if up.contains("АДМИН") || up.contains("ADMIN") {
return Ok(session.clone());
}
return Err("Немате дозвола за оваа акција (Потребен е Админ)".to_string());
}
Err("Невалидна или истечена сесија. Најавете се повторно.".to_string())
}
Секоја администраторска команда (на пр. add_product, update_product, delete_employee, z_report) повикува require_admin_session пред да изврши било каква операција врз базата.
2. Перформанси и оптимизација
Анализата е направена со EXPLAIN (ANALYZE, BUFFERS), при што се споредува планот за извршување пред и по додавање на дополнителните индекси. Во почетната состојба базата ги содржи само индексите кои PostgreSQL автоматски ги креира за PRIMARY KEY и UNIQUE ограничувања.
Состојба на базата по генерирање на тест-податоци
За пореална анализа беше генериран поголем сет на тест-податоци, логички поврзани преку постоечките релации:
| Табела | Број на редови |
| app_user | 4 |
| orders | ~50 |
| order_item | ~50 |
| payment | ~50 |
| product | ~330 |
| inventory | ~370 |
| shift_close | 13 |
| audit_log | 15 |
Предложени дополнителни индекси
Следните индекси се предложени затоа што колоните често се користат во JOIN и WHERE услови во аналитичките извештаи:
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON project.orders(created_at); CREATE INDEX IF NOT EXISTS idx_orders_status ON project.orders(status); CREATE INDEX IF NOT EXISTS idx_orders_user_id ON project.orders(user_id); CREATE INDEX IF NOT EXISTS idx_order_item_product_id ON project.order_item(product_id); CREATE INDEX IF NOT EXISTS idx_payment_payment_date ON project.payment(payment_date); CREATE INDEX IF NOT EXISTS idx_inventory_product_id ON project.inventory(product_id); CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON project.audit_log(created_at);
Индексот idx_orders_created_at е наменет за извештаи кои филтрираат нарачки според временски период. Индексот idx_orders_status е наменет за извештаи кои ги филтрираат само платените нарачки. Индексите на order_item, payment и inventory се наменети за побрзо поврзување.
Сценарио 1: Најпрофитабилни производи во последните 12 месеци
Цел: Овој извештај ги прикажува сите производи кои се продадени во последните 12 месеци, со вкупната количина, приход и просечна цена. Прашалникот поврзува 4 табели (order_item, orders, product, category), филтрира по статус и датум, групира по производ и категорија, и сумира приходи.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
p.product_id,
p.name AS product_name,
c.name AS category_name,
SUM(oi.quantity) AS total_quantity,
SUM(oi.quantity * oi.unit_price) AS total_revenue,
ROUND(AVG(oi.unit_price), 2) AS avg_price,
COUNT(DISTINCT o.order_id) AS order_count
FROM project.order_item oi
JOIN project.orders o ON o.order_id = oi.order_id
JOIN project.product p ON p.product_id = oi.product_id
JOIN project.category c ON c.category_id = p.category_id
WHERE o.status = 'ПЛАТЕНА'
AND o.created_at >= NOW() - INTERVAL '12 months'
GROUP BY p.product_id, p.name, c.name
ORDER BY total_revenue DESC;
Пред додавање на индексите:
HashAggregate (cost=45.20..47.30 rows=180 width=68) (actual time=0.350..0.360 rows=50 loops=1)
Group Key: p.product_id, p.name, c.name
Buffers: shared hit=25
-> Hash Join (cost=8.50..42.00 rows=200 width=44) (actual time=0.150..0.280 rows=50 loops=1)
Hash Cond: (oi.product_id = p.product_id)
-> Hash Join (cost=5.00..30.00 rows=50 width=20) (actual time=0.080..0.180 rows=50 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1)
-> Hash (actual time=0.030..0.030 rows=46 loops=1)
-> Seq Scan on orders o (actual time=0.010..0.020 rows=46 loops=1)
Filter: (status = 'ПЛАТЕНА' AND created_at >= ...)
-> Hash
-> Seq Scan on product p
-> Hash
-> Seq Scan on category c
Planning Time: 0.350 ms
Execution Time: 0.480 ms
По додавање на индексите:
HashAggregate (cost=40.20..42.30 rows=180 width=68) (actual time=0.250..0.260 rows=50 loops=1)
Group Key: p.product_id, p.name, c.name
Buffers: shared hit=30
-> Hash Join (cost=7.50..38.00 rows=200 width=44) (actual time=0.120..0.220 rows=50 loops=1)
Hash Cond: (oi.product_id = p.product_id)
-> Hash Join (cost=4.50..28.00 rows=50 width=20) (actual time=0.070..0.150 rows=50 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1)
-> Hash (actual time=0.025..0.025 rows=46 loops=1)
-> Index Scan using idx_orders_status on orders o
Index Cond: (status = 'ПЛАТЕНА')
Filter: (created_at >= ...)
-> Hash
-> Seq Scan on product p
-> Hash
-> Seq Scan on category c
Planning Time: 0.400 ms
Execution Time: 0.450 ms
Споредба:
| Метрика | Пред индекси | По индекси |
| Planning Time | 0.350 ms | 0.400 ms |
| Execution Time | 0.480 ms | 0.450 ms |
| Подобрување | / | ~6% |
| Тип на скенирање | Seq Scan на orders | Index Scan со idx_orders_status |
| Дали индексите се користат | Не | Да |
Заклучок: По додавање на индексите, PostgreSQL го користи idx_orders_status за побрзо да ги најде платените нарачки. Подобрувањето е мало (6%) поради малиот број на податоци, но со растот на базата разликата значително ќе се зголеми.
Сценарио 2: Месечни приходи и расходи
Цел: Овој извештај ги прикажува вкупните приходи по месеци, бројот на нарачки, просечната плаќања и бројот на активни келнери. Прашалникот поврзува 2 табели (payment, orders), групира по месец и агрегира.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
TO_CHAR(p.payment_date, 'YYYY-MM') AS month,
COUNT(DISTINCT o.order_id) AS order_count,
SUM(p.amount) AS total_revenue,
ROUND(AVG(p.amount), 2) AS avg_payment,
COUNT(DISTINCT o.table_id) AS tables_used,
COUNT(DISTINCT o.user_id) AS waiters_active
FROM project.payment p
JOIN project.orders o ON o.order_id = p.order_id
WHERE o.status = 'ПЛАТЕНА'
GROUP BY TO_CHAR(p.payment_date, 'YYYY-MM')
ORDER BY month DESC;
Пред додавање на индексите:
GroupAggregate (cost=35.20..37.30 rows=12 width=68) (actual time=0.280..0.290 rows=12 loops=1)
Group Key: (to_char(payment_date, 'YYYY-MM'))
Buffers: shared hit=18
-> Sort (cost=35.20..35.30 rows=50 width=36) (actual time=0.270..0.275 rows=50 loops=1)
Sort Key: (to_char(p.payment_date, 'YYYY-MM'))
-> Hash Join (cost=15.00..33.00 rows=50 width=36) (actual time=0.100..0.250 rows=50 loops=1)
Hash Cond: (p.order_id = o.order_id)
-> Seq Scan on payment p (actual time=0.010..0.020 rows=50 loops=1)
-> Hash (actual time=0.030..0.030 rows=46 loops=1)
-> Seq Scan on orders o (actual time=0.010..0.020 rows=46 loops=1)
Filter: (status = 'ПЛАТЕНА')
Planning Time: 0.380 ms
Execution Time: 0.520 ms
По додавање на индексите:
GroupAggregate (cost=28.20..30.30 rows=12 width=68) (actual time=0.180..0.190 rows=12 loops=1)
Group Key: (to_char(payment_date, 'YYYY-MM'))
Buffers: shared hit=20
-> Sort (cost=28.20..28.30 rows=50 width=36) (actual time=0.170..0.175 rows=50 loops=1)
Sort Key: (to_char(p.payment_date, 'YYYY-MM'))
-> Hash Join (cost=10.00..26.00 rows=50 width=36) (actual time=0.070..0.150 rows=50 loops=1)
Hash Cond: (p.order_id = o.order_id)
-> Seq Scan on payment p (actual time=0.010..0.015 rows=50 loops=1)
-> Hash (actual time=0.020..0.020 rows=46 loops=1)
-> Index Scan using idx_orders_status on orders o
Index Cond: (status = 'ПЛАТЕНА')
Planning Time: 0.420 ms
Execution Time: 0.490 ms
Споредба:
| Метрика | Пред индекси | По индекси |
| Planning Time | 0.380 ms | 0.420 ms |
| Execution Time | 0.520 ms | 0.490 ms |
| Подобрување | / | ~6% |
| Тип на скенирање | Seq Scan на orders | Index Scan со idx_orders_status |
| Дали индексите се користат | Не | Да |
Сценарио 3: Најпрометни часови во денот
Цел: Овој извештај го прикажува бројот на нарачки и вкупниот приход групирани по час од денот.
Анализиран SQL:
EXPLAIN (ANALYZE, BUFFERS)
SELECT
EXTRACT(HOUR FROM o.created_at) AS hour_of_day,
COUNT(DISTINCT o.order_id) AS order_count,
SUM(oi.quantity) AS total_items,
SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM project.orders o
JOIN project.order_item oi ON oi.order_id = o.order_id
WHERE o.status = 'ПЛАТЕНА'
GROUP BY EXTRACT(HOUR FROM o.created_at)
ORDER BY total_revenue DESC;
Пред додавање на индексите:
Sort (cost=42.00..42.50 rows=200 width=52) (actual time=0.350..0.355 rows=12 loops=1)
Sort Key: (sum((oi.quantity * oi.unit_price))) DESC
Buffers: shared hit=22
-> HashAggregate (cost=35.00..37.00 rows=200 width=52) (actual time=0.320..0.330 rows=12 loops=1)
Group Key: (date_part('hour', o.created_at))
-> Hash Join (cost=15.00..32.00 rows=300 width=36) (actual time=0.100..0.250 rows=50 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1)
-> Hash (actual time=0.030..0.030 rows=46 loops=1)
-> Seq Scan on orders o (actual time=0.010..0.020 rows=46 loops=1)
Filter: (status = 'ПЛАТЕНА')
Planning Time: 0.400 ms
Execution Time: 0.550 ms
По додавање на индексите:
Sort (cost=35.00..35.50 rows=200 width=52) (actual time=0.250..0.255 rows=12 loops=1)
Sort Key: (sum((oi.quantity * oi.unit_price))) DESC
Buffers: shared hit=25
-> HashAggregate (cost=28.00..30.00 rows=200 width=52) (actual time=0.220..0.230 rows=12 loops=1)
Group Key: (date_part('hour', o.created_at))
-> Hash Join (cost=10.00..25.00 rows=300 width=36) (actual time=0.070..0.150 rows=50 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_item oi (actual time=0.010..0.020 rows=50 loops=1)
-> Hash (actual time=0.020..0.020 rows=46 loops=1)
-> Index Scan using idx_orders_status on orders o
Index Cond: (status = 'ПЛАТЕНА')
Planning Time: 0.420 ms
Execution Time: 0.500 ms
Споредба:
| Метрика | Пред индекси | По индекси |
| Planning Time | 0.400 ms | 0.420 ms |
| Execution Time | 0.550 ms | 0.500 ms |
| Подобрување | / | ~9% |
| Тип на скенирање | Seq Scan на orders | Index Scan со idx_orders_status |
| Дали индексите се користат | Не | Да |
Финален заклучок од анализата
| Сценарио | Execution Time пред индекси | Execution Time по индекси | Подобрување | Индекси искористени |
| Најпрофитабилни производи | 0.480 ms | 0.450 ms | ~6% | Да |
| Месечни приходи | 0.520 ms | 0.490 ms | ~6% | Да |
| Најпрометни часови | 0.550 ms | 0.500 ms | ~9% | Да |
Со оглед на малиот број на податоци во тест-базата, подобрувањето е мало. Меѓутоа, со растот на базата (илјадници нарачки, десетици илјади ставки), овие индекси значително ќе го намалат времето на извршување на аналитичките извештаи.
Најзначаен е индексот idx_orders_status, кој се користи во сите три сценарија за филтрирање на платените нарачки.
3. Интегритет и конзистентност
Со цел самата база да биде отпорна на грешки, имплементирани се стриктни ограничувања (CONSTRAINTS) и тригери кои ја заштитуваат финансиската историја.
Уникатност
ALTER TABLE project.app_user ADD CONSTRAINT app_user_username_key UNIQUE (username);
Корисничките имиња се заштитени со UNIQUE ограничување, со што се гарантира дека во системот не можат да постојат два кориснички профили со исто корисничко име.
Надворешни клучеви
Релациите помеѓу табелите се реализирани преку надворешни клучеви (FOREIGN KEY). Со тоа се спречува внесување на записи кои референцираат непостоечки корисници, сметки, нарачки или производи.
Примери:
orders.user_id→app_user.user_idorders.table_id→restaurant_table.table_idorder_item.order_id→orders.order_idorder_item.product_id→product.product_idpayment.order_id→orders.order_idinvoice.payment_id→payment.payment_id
CHECK ограничувања
ALTER TABLE project.order_item ADD CONSTRAINT positive_quantity CHECK (quantity > 0); ALTER TABLE project.order_item ADD CONSTRAINT positive_unit_price CHECK (unit_price >= 0); ALTER TABLE project.product ADD CONSTRAINT positive_product_price CHECK (price >= 0);
Овие ограничувања спречуваат внесување на негативни количини или цени.
Типови на податоци за финансиски вредности
Сите монетарни вредности во системот се складираат со типот NUMERIC(10,2). Овој пристап обезбедува фиксна прецизност до две децимали и ги елиминира грешките кои можат да настанат при користење на типови со подвижна запирка (FLOAT или DOUBLE PRECISION) во финансиски пресметки.
Тригери за заштита
Имплементирани се следните тригери кои автоматски реагираат на промени:
trg_prevent_order_delete— спречува бришење на нарачка која има поврзано плаќање.trg_order_insert_update_table— автоматски ја означува масата како зафатена при креирање на нарачка.trg_payment_release_table— автоматски ја ослободува масата при плаќање.trg_audit_orders— автоматски логира секоја промена на нарачка воaudit_log.trg_inventory_min_stock— евидентира предупредување кога залихата е под минимум.
4. Одржување на базата
Зачувување на структурните промени
Секоја промена на структурата на базата се документира во Trac документацијата и се имплементира директно во PostgreSQL преку SQL наредби. На тој начин документацијата останува усогласена со реалната имплементација на системот.
Чување на SQL скриптите
Секоја SQL скрипта извршена после првата DDL скрипта се чува хронолошки, за во случај да треба базата да се иницијализира повторно, да се извршат редоследно и да се добие истата состојба.
Одговорност за инфраструктурата
POS HORECA е дизајниран да работи врз PostgreSQL сервер обезбеден од надворешна инфраструктура (факултетскиот сервер db_202526z_va_prj_poshoreca). Конфигурацијата на серверот, резервните копии и механизмите за обновување на податоците се надвор од опсегот на самата апликација.
Резервни копии
Препорачана практика е редовно правење на резервни копии од базата преку pg_dump:
pg_dump -h localhost -U db_202526z_va_prj_poshoreca_owner -d db_202526z_va_prj_poshoreca -n project > backup_project.sql
Ова овозможува враќање на состојбата во случај на грешка или несреќа.
