Maktab boshqaruv tizimi arxitekturasi: mahalliy bozor uchun ERP loyihalash saboqlari
So'nggi bir necha yil ichida men M Smart School — maktab boshqaruv tizimini yozdim va hozir uni real sharoitda joriy qilyapman: bitta pilot maktab, 300 dan ortiq o'quvchi. Boshlashdan oldin bu menga "oddiy CRUD" bo'lib tuyulgan edi: o'quvchilar ro'yxati, baholar jurnali, davomat. Amalda esa bu to'laqonli ERP bo'lib chiqdi — o'z domeni, vaqt o'lchovi va eng muhimi, texnikadan uzoq foydalanuvchilari bilan. Quyida shu yo'lda qabul qilgan arxitektura qarorlarim va ulardan olgan saboqlarim — maxfiy detallar emas, umumiy muhandislik xulosalari.
Domen modeli: jadvallar emas, munosabatlar va vaqt
Birinchi qarashda domen oddiy: o'quvchi, sinf, fan, dars jadvali, davomat, baho — olti jadval. Lekin real murakkablik entity'larning o'zida emas, ular orasidagi munosabatlarda va vaqt o'lchovida yashaydi:
- O'quvchi sinfga shunchaki "tegishli" emas — u ma'lum o'quv yilida shu sinfda o'qiydi. Kelasi yil "5-A" sinf "6-A"ga aylanadi, o'quvchi esa boshqa maktabga ko'chib ketishi mumkin.
- Baho "o'quvchi + fan" juftligiga emas, "o'quvchi + fan + chorak" uchligiga bog'lanadi. Choraksiz baho ma'nosiz — u qaysi davr uchun qo'yilganini bilmaysiz.
- Davomat konkret darsga yoziladi, dars esa jadvalning konkret sanaga proyeksiyasi. Jadval o'zgartirilsa, o'tgan davomat tarixi buzilmasligi kerak.
Shu sababli baholar jadvali boshidanoq vaqt kontekstini o'z ichiga oladi:
CREATE TABLE grades (
id BIGSERIAL PRIMARY KEY,
school_id BIGINT NOT NULL, -- tenant
student_id BIGINT NOT NULL,
subject_id BIGINT NOT NULL,
term_id BIGINT NOT NULL, -- chorak: baho vaqt kontekstisiz yashamaydi
lesson_id BIGINT, -- qaysi darsda qo'yilgani (ixtiyoriy)
value SMALLINT NOT NULL CHECK (value BETWEEN 1 AND 5),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_grades_lookup ON grades (school_id, student_id, term_id);Eng katta sabog'im: "o'quvchi — sinf" bog'lanishini to'g'ridan-to'g'ri foreign key qilmang. Buning o'rniga enrollment (a'zolik) jadvali kerak: o'quvchi, sinf, o'quv yili va sana oralig'i. Shunda yil o'rtasida kelgan yoki ketgan o'quvchi, sinfdan sinfga ko'chish — hammasi tarixni buzmasdan ifodalanadi.
Multi-tenant: har maktabga alohida baza yoki umumiy baza
Bir nechta maktabga xizmat qiladigan tizimda bu birinchi katta qaror. Ikkala yo'lning ham o'z narxi bor:
- Alohida baza: kuchli izolyatsiya, bitta maktabni backup/restore qilish oson, ma'lumot sizib chiqish xavfi minimal. Lekin N maktab — N marta migratsiya, N ta monitoring nuqtasi, connection'lar va infra xarajati o'sib boradi.
- Umumiy baza + tenantid: migratsiya bitta, infra arzon, maktablararo statistika oddiy so'rov bilan chiqadi. Asosiy xavf — birgina unutilgan WHERE schoolid boshqa maktabning ma'lumotini ko'rsatib yuborishi mumkin.
Men umumiy baza + tenantid yo'lini tanladim. Mantiq oddiy: jamoa kichik, maktablar soni hozircha oz, va operatsion yuk (N ta bazani boqish) bu bosqichda izolyatsiya foydasidan qimmatroq. Xavfni kamaytirish uchun tenant filtri ixtiyoriy odatdan majburiy qatlamga aylandi: barcha so'rovlar repository qatlamidan o'tadi va u schoolid'siz so'rov qurishga umuman yo'l qo'ymaydi. PostgreSQL'da qo'shimcha to'siq sifatida Row Level Security ham yoqsa bo'ladi. Muhim jihat: sxema bir xil bo'lgani uchun, kelajakda yirik mijoz alohida baza talab qilsa, ko'chirish real va arzon yo'l bo'lib qoladi — bu qaror qaytarilmas emas.
Rollar va ruxsatlar: RBAC'ni oddiy saqlash
To'rtta asosiy rol bor: direktor, o'qituvchi, ota-ona, o'quvchi. Bu yerdagi asosiy vasvasa — "kelajak uchun" universal permission engine yozish. Amalda esa rol → ruxsatlar lug'ati va bitta scope tekshiruvi yetarli bo'ldi:
const PERMISSIONS: Record<Role, string[]> = {
director: ["reports:read", "schedule:write", "users:manage"],
teacher: ["attendance:write", "grades:write", "schedule:read"],
parent: ["grades:read", "attendance:read"],
student: ["grades:read", "schedule:read"],
};
function can(user: User, permission: string): boolean {
return PERMISSIONS[user.role].includes(permission);
}
// Scope alohida tekshiriladi: ota-ona faqat o'z farzandini ko'radi
function inScope(user: User, studentId: number): boolean {
if (user.role === "parent") return user.childIds.includes(studentId);
if (user.role === "student") return user.studentId === studentId;
return true; // direktor va o'qituvchi o'z maktabi doirasida ko'radi
}Ruxsat ("nima qila oladi") va scope ("kim ustida") ajratilgani kodni tekis saqlaydi. Yangi rol — masalan, o'quv ishlari bo'yicha o'rinbosar — lug'atga bitta qator qo'shish bilan hal bo'ladi. Per-object ACL, dinamik rollar kabi murakkabliklar kerak bo'lganda qo'shiladi; pilot davomida kerak bo'lgani yo'q.
Mahalliy sharoit: beqaror internet va past texnik savodxonlik
Bu ikki fakt arxitekturaga har qanday texnologik tanlovdan ko'ra ko'proq ta'sir qildi:
- Sahifalar yengil: server-side render, minimal JavaScript. Har bir sahifa sekin mobil internetda ham ochilishi shart.
- Formalar kichik: davomat kiritish 2-3 bosishda bajariladi, uzun formalar bosqichlarga bo'lingan.
- Saqlash yaqqol tasdiqlanadi: optimistik UI o'rniga aniq "Saqlandi" signali. Foydalanuvchi tizimga ishonishi uchun bu muhim.
- Internet uzilsa, kiritilgan ma'lumot yo'qolmaydi: forma holati lokal saqlanadi va ulanish qaytganda qayta yuboriladi.
To'liq oflayn-first PWA haqida ko'p o'yladim, lekin ataylab qilmadim: sinxronizatsiya konfliktlarini boshqarish kichik jamoa uchun juda qimmat murakkablik. Oddiy retry va draft saqlash muammoning katta qismini yopdi.
Umumiy tamoyil: soddalik funksiya sonidan ustun. Har bir yangi tugma — bu o'qituvchilarni o'qitish xarajati va qo'llab-quvvatlashga keladigan qo'shimcha savollar oqimi. Feature so'ralganda birinchi savolim: "busiz yashab bo'ladimi?"
Hisobotlar: real vaqtda hisoblash yoki kechqurun tayyorlash
Rahbariyatga agregatlar kerak: sinf kesimida davomat foizi, chorak bo'yicha o'zlashtirish dinamikasi, fanlar kesimi. 300 o'quvchida raw jadvallar ustidan real vaqt so'rovlari bemalol ishlaydi — lekin men baribir kunlik agregat jadvallarni tanladim.
Sabablari ikkita. Birinchisi texnik: hisobot so'rovlari kun davomida asosiy yozuv oqimi (davomat, baholar) bilan raqobatlashmasin. Ikkinchisi mahsulotga oid: direktor uchun "kechagi holat" mutlaqo yetarli — maktab hisobotida hech kim soniyalik aniqlikni kutmaydi.
-- Kechki cron: sinf bo'yicha kunlik davomat agregati
INSERT INTO attendance_daily (school_id, class_id, date, present, absent)
SELECT school_id, class_id, date,
COUNT(*) FILTER (WHERE status = 'present'),
COUNT(*) FILTER (WHERE status = 'absent')
FROM attendance
WHERE date = CURRENT_DATE
GROUP BY school_id, class_id, date
ON CONFLICT (school_id, class_id, date) DO UPDATE
SET present = EXCLUDED.present,
absent = EXCLUDED.absent;Bonus samara: agregat jadvaldan o'qiydigan hisobot sahifalari juda tez ochiladi — zaif internetda bu alohida qadrlanadi.
Pilot joriy etishdan saboqlar
Tizimni birdan o'nta maktabga emas, bitta pilot maktabga chiqardik — va bu eng to'g'ri qaror bo'ldi:
- Real ma'lumot sxemani sinaydi. Yil o'rtasida kelgan o'quvchi, bir xil ism-familiyali bolalar, sinfini almashtirganlar — bularning barchasi birinchi haftalarda chiqdi va enrollment modeli o'zini oqladi.
- Trening feature'dan muhim. Eng yaxshi funksiya ham foydalanuvchi uni topa olmasa mavjud emas. Jonli ko'rsatish va qisqa yo'riqnomalar kod yozishdan kam vaqt olmadi.
- Feedback tsikli qisqa bo'lsin. Davomat modulini birinchi haftadayoq qayta ishladik — o'qituvchilar jarayonni biz kutgandan boshqacha tasavvur qilgan ekan. Pilot bo'lmaganida buni o'nta maktabda birdan bilardik.
- Ichki admin asboblarga vaqt ajrating. Ma'lumot importi, xatoni tuzatish, o'quvchini ko'chirish — bularsiz har bir mayda muammo dasturchining qo'liga tushadi.
Xulosa
Maktab ERP'sining qiyin qismi texnologiya emas. Qiyini — domen (vaqt o'lchovli munosabatlar), odamlar (texnikadan uzoq foydalanuvchilar) va sharoit (beqaror internet). Arxitektura shu uchtasini hurmat qilsa, oddiy monolit, umumiy baza, to'rt rolli RBAC va kechki agregatlar bilan ham ishonchli tizim quriladi. Murakkablikni muammo talab qilganda qo'shing — oldindan emas.