| Version 3 (modified by , 10 hours ago) ( diff ) |
|---|
Функции, Процедури и Тригери
Во оваа фаза беа имплементирани три функции, три процедури и три тригери кои ја поддржуваат основната бизнис логика на системот IDontKnow. Тие автоматизираат процеси како пресметка на кредитно-пондериран просек и отворен долг, проверка на слободни места, регистрирање наплата со автоматска алокација кон обврски, упис на студент на курс, сериско генерирање обврски за школарина, како и валидација на оценки, ажурирање на отворената сума и контрола на капацитетот на курсевите преку тригери.
Сите рутини се креирани во базата advdb_202526l_prj_idontknow (PostgreSQL 17.9).
Функции
Функциите се користат за пресметување и враќање на вредности кои се потребни во повеќе делови од системот и може да се вградат директно во прашалник, поглед или друга рутина.
1. fn_prosek_student
CREATE OR REPLACE FUNCTION fn_prosek_student(
p_student_id INTEGER,
p_sistem_ocenuvanje_id INTEGER DEFAULT NULL
)
RETURNS NUMERIC
LANGUAGE sql
STABLE
AS $$
SELECT ROUND(SUM(sov.ocena_vrednost::numeric * ak.krediti)
/ NULLIF(SUM(ak.krediti), 0), 2)
FROM sm_student_ocena_zapis soz
JOIN sm_student_upisan_kurs suk ON soz.student_upisan_kurs_id = suk.id
JOIN sm_student_semestar_upis ssu ON suk.student_semestar_upis_id = ssu.id
JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
JOIN sm_kurs_realizacija kr ON suk.kurs_realizacija_id = kr.id
JOIN sm_akreditacija ak ON kr.akreditacija_id = ak.id
JOIN sm_sistem_ocenuvanje_vrednost sov ON soz.sistem_ocenuvanje_vrednost_id = sov.id
WHERE ss.student_id = p_student_id
AND sov.polozitelna
AND (p_sistem_ocenuvanje_id IS NULL OR sov.sistem_ocenuvanje_id = p_sistem_ocenuvanje_id);
$$;
Функцијата fn_prosek_student го пресметува кредитно-пондерираниот просек од положените оценки на даден студент. Како влезни параметри прима p_student_id и опционално p_sistem_ocenuvanje_id, со кој пресметката може да се ограничи на еден систем на оценување. Поминувајќи низ синџирот на табели од sm_student_ocena_zapis до sm_akreditacija, ја дели сумата на (вредност на оценка × кредити) со вкупните кредити и резултатот го заокружува на две децимали.
Реализирана е како функција бидејќи враќа единечна вредност што директно се вградува во прашалници — на пример листа на студенти со просек над праг, услов за стипендија или уверение (SELECT indeks, fn_prosek_student(s.id) FROM sm_studenti s), што не е возможно со процедура. За масовни извештаи постои погледот vw_studentski_oceni; функцијата е за поединечна, параметризирана употреба во произволен контекст.
2. fn_otvoren_dolg
CREATE OR REPLACE FUNCTION fn_otvoren_dolg(
p_user_id INTEGER,
p_vid_valuta_id INTEGER
)
RETURNS NUMERIC
LANGUAGE sql
STABLE
AS $$
SELECT COALESCE(SUM(fo.otvorena_suma), 0)
FROM fin_finansiska_obvrska fo
WHERE fo.user_id = p_user_id
AND fo.vid_valuta_id = p_vid_valuta_id
AND fo.otvorena_suma > 0
AND fo.status_finansiska_obvrska_id IN
(SELECT id FROM fin_status_finansiska_obvrska WHERE ime IN ('Izdadena', 'Delumno platena'));
$$;
Функцијата fn_otvoren_dolg ја враќа вкупната отворена сума на корисник во дадена валута. Прима p_user_id и p_vid_valuta_id, и собира otvorena_suma само од обврските во статус Izdadena или Delumno platena, враќајќи 0 ако нема такви.
Истата проверка ја користат повеќе места — блокада при упис, опомени и работа на шалтер — па е издвоена како функција со повратна вредност што може да се вгради дури и во услов (WHERE fn_otvoren_dolg(...) > 0). Како процедура би морала да враќа преку OUT-параметар и не би можела да се користи на тој начин.
3. fn_slobodni_mesta
CREATE OR REPLACE FUNCTION fn_slobodni_mesta(p_kurs_realizacija_id INTEGER)
RETURNS INTEGER
LANGUAGE sql
STABLE
AS $$
SELECT kr.kapacitet_studenti
- (SELECT COUNT(*)::int
FROM sm_student_upisan_kurs suk
WHERE suk.kurs_realizacija_id = kr.id
AND suk.status_slushanje = 'UPISAN')
FROM sm_kurs_realizacija kr
WHERE kr.id = p_kurs_realizacija_id;
$$;
Функцијата fn_slobodni_mesta го враќа бројот на слободни места на дадена реализација на курс — kapacitet_studenti намалено за бројот на активни (UPISAN) уписи. Доколку капацитетот е NULL, тоа значи неограничен број места.
Се прикажува при пребарување курсеви и се користи внатре во процедурата pr_zapisi_student_na_kurs, па затоа е функција што враќа вредност. Контролата на капацитетот ја спроведува тригерот trg_kapacitet_kurs; функцијата само информира, додека тригерот е тој што го гарантира ограничувањето.
Процедури
Процедурите се користат за извршување деловни операции врз базата кои опфаќаат повеќе чекори и табели, се повикуваат експлицитно со CALL и единствени смеат да користат COMMIT / ROLLBACK внатре.
1. pr_registriraj_naplata
CREATE OR REPLACE PROCEDURE pr_registriraj_naplata(
p_user_id INTEGER,
p_suma NUMERIC,
p_vid_valuta_id INTEGER,
p_organizaciona_edinica_id INTEGER,
p_referenca VARCHAR DEFAULT NULL,
p_korisnik_naplata_id INTEGER DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
v_naplata_id INTEGER;
v_smetka_id INTEGER;
v_ostatok NUMERIC := p_suma;
v_obvrska RECORD;
v_alocirano NUMERIC;
BEGIN
IF p_suma <= 0 THEN
RAISE EXCEPTION 'Сумата мора да биде позитивна (добиено: %)', p_suma;
END IF;
-- активна трансакциска сметка на организационата единица (приоритет: иста валута)
SELECT oes.transakciska_smetka_id
INTO v_smetka_id
FROM or_organizaciona_edinica_smetki oes
JOIN fin_transakciski_smetki ts ON oes.transakciska_smetka_id = ts.id
WHERE oes.organizaciona_edinica_id = p_organizaciona_edinica_id
AND oes.aktivna
AND ts.aktivna
ORDER BY (ts.vid_valuta_id = p_vid_valuta_id) DESC, ts.id
LIMIT 1;
IF v_smetka_id IS NULL THEN
RAISE EXCEPTION 'Организационата единица % нема активна трансакциска сметка',
p_organizaciona_edinica_id;
END IF;
INSERT INTO fin_naplati (user_id, korisnik_naplata_id, transakciska_smetka_id,
organizaciona_edinica_id, naplatena_suma, vid_valuta_id,
status_transakcija_id, nadvoresna_referenca,
requested_at, confirmed_at, data)
VALUES (p_user_id, COALESCE(p_korisnik_naplata_id, p_user_id), v_smetka_id,
p_organizaciona_edinica_id, p_suma, p_vid_valuta_id,
(SELECT id FROM fin_status_transakcija WHERE ime_status = 'ACCEPTED'),
p_referenca, now(), now(), now())
RETURNING id INTO v_naplata_id;
FOR v_obvrska IN
SELECT fo.id, fo.otvorena_suma
FROM fin_finansiska_obvrska fo
WHERE fo.user_id = p_user_id
AND fo.vid_valuta_id = p_vid_valuta_id
AND fo.otvorena_suma > 0
ORDER BY fo.datum_dospevanje NULLS LAST, fo.id
FOR UPDATE
LOOP
EXIT WHEN v_ostatok <= 0;
v_alocirano := LEAST(v_ostatok, v_obvrska.otvorena_suma);
INSERT INTO fin_naplata_alokacija (naplata_id, finansiska_obvrska_id,
alocirana_suma, created_at)
VALUES (v_naplata_id, v_obvrska.id, v_alocirano, now());
v_ostatok := v_ostatok - v_alocirano;
END LOOP;
IF v_ostatok > 0 THEN
RAISE NOTICE 'Наплата %: % останува неалоцирано (преплата).', v_naplata_id, v_ostatok;
END IF;
END;
$$;
Процедурата pr_registriraj_naplata регистрира наплата и автоматски ја алоцира кон отворените обврски на корисникот. Откако ќе валидира дека сумата е позитивна и ќе ја најде активната трансакциска сметка на организационата единица (со приоритет на иста валута), внесува запис во fin_naplati, па во циклус ги покрива обврските по редослед на најстаро доспевање, внесувајќи редови во fin_naplata_alokacija. Ако остане непокриен дел (преплата), тој останува неалоциран и се пријавува со NOTICE.
Ова е деловна операција составена од еден INSERT во fin_naplati и повеќе INSERT-и во fin_naplata_alokacija, која се извршува атомски и не враќа вредност за прашалник — затоа е процедура, а не функција. Намерно не ја менува otvorena_suma: тоа го прави тригерот trg_azuriraj_otvorena_suma, така што изведената вредност останува конзистентна без оглед дали алокацијата ја внела оваа процедура, друга апликација или рачен SQL. Процедурата одлучува како се распределува наплатата, а тригерот гарантира книговодствена конзистентност.
2. pr_zapisi_student_na_kurs
CREATE OR REPLACE PROCEDURE pr_zapisi_student_na_kurs(
p_student_id INTEGER,
p_kurs_realizacija_id INTEGER
)
LANGUAGE plpgsql
AS $$
DECLARE
v_ssu_id INTEGER;
v_user_id INTEGER;
v_dolg NUMERIC;
v_valuta INTEGER;
v_slobodni INTEGER;
BEGIN
SELECT ssu.id
INTO v_ssu_id
FROM sm_student_semestar_upis ssu
JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
JOIN sm_kurs_realizacija kr ON kr.id = p_kurs_realizacija_id
WHERE ss.student_id = p_student_id
AND ssu.akademski_semestar_id = kr.akademski_semestar_id
ORDER BY ssu.datum_upis DESC
LIMIT 1;
IF v_ssu_id IS NULL THEN
RAISE EXCEPTION 'Студентот % нема упис во семестарот на курсот %',
p_student_id, p_kurs_realizacija_id;
END IF;
IF EXISTS (SELECT 1
FROM sm_student_upisan_kurs
WHERE student_semestar_upis_id = v_ssu_id
AND kurs_realizacija_id = p_kurs_realizacija_id) THEN
RAISE EXCEPTION 'Студентот % е веќе запишан на курсот %',
p_student_id, p_kurs_realizacija_id;
END IF;
SELECT s.user_id
INTO v_user_id
FROM sm_studenti s
WHERE s.id = p_student_id;
FOR v_valuta IN
SELECT DISTINCT vid_valuta_id
FROM fin_finansiska_obvrska
WHERE user_id = v_user_id
AND otvorena_suma > 0
LOOP
v_dolg := fn_otvoren_dolg(v_user_id, v_valuta);
IF v_dolg > 0 THEN
RAISE EXCEPTION 'Студентот % има отворен долг % (валута %) — уписот е блокиран',
p_student_id, v_dolg, v_valuta;
END IF;
END LOOP;
v_slobodni := fn_slobodni_mesta(p_kurs_realizacija_id);
IF v_slobodni IS NOT NULL AND v_slobodni <= 0 THEN
RAISE EXCEPTION 'Нема слободни места на курсот %', p_kurs_realizacija_id;
END IF;
INSERT INTO sm_student_upisan_kurs (student_semestar_upis_id, kurs_realizacija_id, status_slushanje)
VALUES (v_ssu_id, p_kurs_realizacija_id, 'UPISAN');
END;
$$;
Процедурата pr_zapisi_student_na_kurs запишува студент на курс. Го наоѓа семестралниот упис на студентот за семестарот на курсот, одбива дупликат упис, одбива ако студентот има отворен долг (преку fn_otvoren_dolg) и одбива ако нема слободни места (преку fn_slobodni_mesta), а потоа внесува запис во sm_student_upisan_kurs со статус UPISAN.
Повеќечекорната валидациона секвенца со завршно запишување, повикана експлицитно од акција на корисникот, ја прави процедура; таа воедно демонстрира компонирање бидејќи ги повикува функциите fn_otvoren_dolg и fn_slobodni_mesta. Проверката на долг е политика на уписот, а не интегритет на податоците (администратор може по исклучок да запише студент со долг преку директен INSERT), па затоа не е тригер; капацитетот, напротив, е физичко ограничување и е заштитен со тригер.
3. pr_generiraj_obvrski_za_semestar
CREATE OR REPLACE PROCEDURE pr_generiraj_obvrski_za_semestar(p_akademski_semestar_id INTEGER)
LANGUAGE plpgsql
AS $$
DECLARE
v_cenovnik RECORD;
v_oe INTEGER;
v_opis VARCHAR;
v_student RECORD;
v_brojac INTEGER := 0;
BEGIN
SELECT asem.organizaciona_edinica_id,
c.id AS cenovnik_id,
c.iznos,
c.vid_valuta_id,
asem.tip_naplata_organizaciona_edinica_id
INTO v_cenovnik
FROM sm_akademski_semestar asem
JOIN fin_tip_naplati_cenovnik c
ON c.tip_naplata_organizaciona_edinica_id = asem.tip_naplata_organizaciona_edinica_id
AND c.aktivna
WHERE asem.id = p_akademski_semestar_id
ORDER BY c.prioritet
LIMIT 1;
IF v_cenovnik IS NULL THEN
RAISE EXCEPTION 'Нема активен ценовник за семестар %', p_akademski_semestar_id;
END IF;
v_oe := v_cenovnik.organizaciona_edinica_id;
v_opis := 'Школарина семестар ' || p_akademski_semestar_id;
FOR v_student IN
SELECT DISTINCT s.user_id
FROM sm_student_semestar_upis ssu
JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
JOIN sm_studenti s ON ss.student_id = s.id
WHERE ssu.akademski_semestar_id = p_akademski_semestar_id
AND NOT EXISTS (SELECT 1
FROM fin_finansiska_obvrska fo
WHERE fo.user_id = s.user_id
AND fo.opis = 'Школарина семестар ' || p_akademski_semestar_id)
LOOP
INSERT INTO fin_finansiska_obvrska (user_id, organizaciona_edinica_id,
tip_naplata_organizaciona_edinica_id,
tip_naplati_cenovnik_id,
status_finansiska_obvrska_id,
datum_kreiranje, datum_dospevanje,
originalna_suma, otvorena_suma,
vid_valuta_id, opis)
VALUES (v_student.user_id, v_oe, v_cenovnik.tip_naplata_organizaciona_edinica_id,
v_cenovnik.cenovnik_id,
(SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Izdadena'),
now(), CURRENT_DATE + 30, v_cenovnik.iznos, v_cenovnik.iznos,
v_cenovnik.vid_valuta_id, v_opis);
v_brojac := v_brojac + 1;
IF v_brojac % 1000 = 0 THEN
COMMIT; -- дозволено само во процедура
END IF;
END LOOP;
RAISE NOTICE 'Генерирани % обврски за семестар %', v_brojac, p_akademski_semestar_id;
END;
$$;
Процедурата pr_generiraj_obvrski_za_semestar сериски генерира обврски за школарина за сите студенти запишани во даден академски семестар, според активниот ценовник на семестарот. Прескокнува студенти кои веќе имаат таква обврска (идемпотентно според opis), а на секои 1000 внесени редови прави COMMIT.
Ова е единствената рутина што мора да биде процедура: периодичниот COMMIT врз batch од десетици илјади редови е дозволен само во процедура — функциите и тригерите во PostgreSQL не смеат да завршуваат трансакции.
Тригери
Тригерите се користат за автоматско извршување логика при внесување или промена на податоци во табелите. Тие се единствениот механизам кој гарантира дека правилото важи без разлика кој и како пишува во табелата — апликација, процедура или рачен INSERT.
1. trg_validna_ocena
CREATE OR REPLACE FUNCTION trgf_validna_ocena()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_status VARCHAR;
BEGIN
SELECT status_slushanje
INTO v_status
FROM sm_student_upisan_kurs
WHERE id = NEW.student_upisan_kurs_id;
IF v_status IS DISTINCT FROM 'UPISAN' THEN
RAISE EXCEPTION 'Оценка може да се внесе само за активен упис (статус: %)', v_status;
END IF;
IF NEW.osvoeni_poeni IS NOT NULL AND (NEW.osvoeni_poeni < 0 OR NEW.osvoeni_poeni > 100) THEN
RAISE EXCEPTION 'Освоени поени мора да бидат 0–100 (добиено: %)', NEW.osvoeni_poeni;
END IF;
IF NEW.datum_zapis > now() THEN
RAISE EXCEPTION 'Датумот на запис не смее да биде во иднина';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_validna_ocena ON sm_student_ocena_zapis;
CREATE TRIGGER trg_validna_ocena
BEFORE INSERT ON sm_student_ocena_zapis
FOR EACH ROW
EXECUTE FUNCTION trgf_validna_ocena();
Тригерот trg_validna_ocena се активира пред внесување оценка во sm_student_ocena_zapis. Тој одбива оценка ако уписот не е во статус UPISAN, ако освоените поени се надвор од опсегот 0–100, или ако датумот на запис е во иднина.
Реализиран е како тригер бидејќи станува збор за интегритет на податоците — невалидна оценка е невалидна без оглед кој ја внесува (апликација, import или рачен INSERT), што процедура не би го гарантирала. CHECK-ограничување не е доволно бидејќи правилото за статусот бара поглед во друга табела (sm_student_upisan_kurs).
2. trg_azuriraj_otvorena_suma
CREATE OR REPLACE FUNCTION trgf_azuriraj_otvorena_suma()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_nova NUMERIC;
BEGIN
SELECT otvorena_suma - NEW.alocirana_suma
INTO v_nova
FROM fin_finansiska_obvrska
WHERE id = NEW.finansiska_obvrska_id
FOR UPDATE;
IF v_nova < 0 THEN
RAISE EXCEPTION 'Алокацијата (%) ја надминува отворената сума на обврската %',
NEW.alocirana_suma, NEW.finansiska_obvrska_id;
END IF;
UPDATE fin_finansiska_obvrska
SET otvorena_suma = v_nova,
status_finansiska_obvrska_id = CASE
WHEN v_nova = 0 THEN
(SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Platena')
ELSE
(SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Delumno platena')
END
WHERE id = NEW.finansiska_obvrska_id;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_azuriraj_otvorena_suma ON fin_naplata_alokacija;
CREATE TRIGGER trg_azuriraj_otvorena_suma
AFTER INSERT ON fin_naplata_alokacija
FOR EACH ROW
EXECUTE FUNCTION trgf_azuriraj_otvorena_suma();
Тригерот trg_azuriraj_otvorena_suma се активира по внесување алокација во fin_naplata_alokacija. Тој ја намалува otvorena_suma на соодветната обврска за алоцираната сума, одбива алокација што би ја однела сумата под нула, и автоматски го поставува статусот на Platena (кога ќе достигне нула) или Delumno platena.
otvorena_suma е изведена вредност што мора да се ажурира секогаш кога ќе настане алокација, па е врзана за настанот преку тригер, а не за повикувачот. Кога би била дел од процедурата pr_registriraj_naplata, секој иден писател на алокации (на пр. модул за поврат или миграција) би морал да не заборави да ја ажурира. Тригерот е AFTER INSERT затоа што редот прво мора да постои како валиден, а ажурирањето на другата табела е последица.
3. trg_kapacitet_kurs
CREATE OR REPLACE FUNCTION trgf_kapacitet_kurs()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_kapacitet INTEGER;
v_upisani INTEGER;
BEGIN
IF NEW.status_slushanje <> 'UPISAN' THEN
RETURN NEW;
END IF;
SELECT kapacitet_studenti
INTO v_kapacitet
FROM sm_kurs_realizacija
WHERE id = NEW.kurs_realizacija_id
FOR UPDATE; -- сериализира конкурентни уписи на иста реализација
IF v_kapacitet IS NULL THEN
RETURN NEW; -- неограничен капацитет
END IF;
SELECT COUNT(*)
INTO v_upisani
FROM sm_student_upisan_kurs
WHERE kurs_realizacija_id = NEW.kurs_realizacija_id
AND status_slushanje = 'UPISAN';
IF v_upisani >= v_kapacitet THEN
RAISE EXCEPTION 'Курсот % е полн (капацитет %)', NEW.kurs_realizacija_id, v_kapacitet;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_kapacitet_kurs ON sm_student_upisan_kurs;
CREATE TRIGGER trg_kapacitet_kurs
BEFORE INSERT ON sm_student_upisan_kurs
FOR EACH ROW
EXECUTE FUNCTION trgf_kapacitet_kurs();
Тригерот trg_kapacitet_kurs се активира пред внесување упис во sm_student_upisan_kurs. За нов упис со статус UPISAN ја заклучува реализацијата (FOR UPDATE), го брои бројот на активни уписи и одбива ако е достигнат kapacitet_studenti (при што NULL значи неограничено).
Капацитетот мора да издржи и конкурентни уписи — двајца студенти што истовремено го земаат последното место. Проверка само во процедура би била race-condition бидејќи двете трансакции би виделе слободно место; тригерот со заклучување на редот на реализацијата ги сериализира таквите инсерти. Процедурата pr_zapisi_student_na_kurs сепак повикува fn_slobodni_mesta однапред за љубезна порака, но тригерот е последната, непробивна одбрана.
Како се тестирано (сите со ROLLBACK, без траен ефект)
| Тест | Резултат |
fn_prosek_student(1) | 8.00 |
fn_otvoren_dolg(2, 1) | 74925.00 |
fn_slobodni_mesta(1) | 100 |
| П1: наплата 5000 за корисник 2 | 1 алокација, долг 74925→69925, статус → Delumno platena (Т2) |
| П2: студент со долг | блокирано со јасна порака |
| П2: студент без долг | упис креиран (UPISAN) |
| П3: непостоечки семестар | „Нема активен ценовник" |
| Т1: 150 поени / оценка на POLOZENO | двете одбиени |
| Т3: упис над капацитет | одбиен |
Напомена: целосниот тек на П3 не е извршен врз живата база бидејќи содржи COMMIT (не може да се врати со ROLLBACK) и би генерирал ~13.000 реални обврски; валидационата патека е тестирана.
Attachments (1)
- 13_routines.sql (14.7 KB ) - added by 29 hours ago.
Download all attachments as: .zip
