SDD-020 — Registry trường chuyên: quản lý & trang công khai
SDD-014 §2 đã thiết kế registry trường chuyên toàn quốc từ 2026-08-14 nhưng chưa có dòng code nào — target_schools vẫn đúng 5 cột của migration 0004 và specialized_programs chưa tồn tại. Tài liệu này thi hành phần đó, rồi dựng thêm hai lớp mà SDD-014 không cần nhưng SRC-203 yêu cầu: người của trường (§4) và thành tích/số liệu theo năm học (§5), cùng ba trang công khai (§6-7).
Phân vai giữa hai tài liệu, để về sau không ai phải đoán:
| SDD-014 | SDD-020 (tài liệu này) | |
|---|---|---|
| Quan tâm | trường là nguồn đề thi | trường là nơi con sẽ thi vào |
| Đọc bởi | Coral (staff), pipeline crawl | Dolphin (quản lý), nemo12.com (công khai) |
| Sở hữu cột | source, provenance crawl | slug, status, mô tả, ảnh, thành tích |
Cả hai đọc cùng một bảng target_schools.
1. Nguyên tắc
- Một danh sách trường duy nhất.
exams.target_school_idđã trỏ vàotarget_schoolstừ migration 0004. Dựng bảngschoolsmới cho "phần giới thiệu" là tạo ra hai danh sách trường — rồi tới ngày một đề thi gắn vào trường ở danh sách A trong khi trang giới thiệu đọc danh sách B, và không ai biết bên nào đúng. Nên §2 mở rộng tại chỗ. - Con số phải có năm học và nguồn. Một tỉ lệ chọi không kèm năm là một con số vô nghĩa; kèm năm mà không kèm nguồn là một con số không kiểm chứng được. Cả hai đều là cột
NOT NULL(§5). - Thiếu thì để trống, cấm điền 0. Đúng luật đã chốt ở Dashboard (SRC-199):
0nghĩa là "đo được và bằng không", khác hẳn "không có dữ liệu". Trên bảng so sánh, ô trống nói thật; ô0nói dối theo hướng bất lợi cho trường đó. - Nemo12 không xếp hạng trường (Q-137). Trang so sánh đặt các số cạnh nhau và để người đọc tự kết luận; không có cột "điểm tổng", không có thứ tự "tốt nhất".
- Chỉ nêu tên người đã đồng ý (§4).
2. Trường (REQ-SCH-15) — mở rộng target_schools
target_schools (0004): id · name · province · kind(chuyen|chon|thuong) · source
target_schools (0047 thêm):
slug UNIQUE? -- URL công khai /specialist-schools/{slug}
short_name? -- "Ams", "KHTN" — tên hiện trên thẻ và bảng so sánh
managed_by? -- so | dai_hoc (SDD-014 §2)
website? · established_year?
summary? -- 1-2 câu, hiện trên thẻ
description? -- phần dài, hiện ở trang trường
logo_media_id? · hero_media_id? → media(id) (SDD-019 §2)
status -- draft | published | archived, MẶC ĐỊNH 'draft'
display_order · updated_atstatus mặc định draft dù bảng đã có sẵn dữ liệu từ trước: các dòng cũ do pipeline đề thi sinh ra chỉ có tên + tỉnh, chưa ai rà soát, và một trang công khai hiện ra tên trường trống trơn còn tệ hơn không có trang. Ai muốn trường lên sóng thì mở #/truong bổ sung thông tin rồi publish — một hành động có chủ ý.
Cổng publish: cần slug + province + summary. slug sinh tự động từ tên (bỏ dấu tiếng Việt) nhưng sửa được; đổi slug của trường đã publish là gãy link cũ nên cổng quản lý cảnh báo.
specialized_programs: id · school_id → target_schools(id) · subject_code
· name_vi -- "Chuyên Toán"
· intake_size? -- chỉ tiêu thường niên
· notes? · status · display_ordersubject_code cố ý không khoá ngoại vào subjects của knowledge graph: lớp chuyên có Sinh, Sử, Địa, Nga, Nhật… trong khi graph mới có Toán/Văn/Anh/Lý/Hoá/Tin. Bắt khoá ngoại nghĩa là không nhập nổi lớp Chuyên Sinh cho tới ngày có content môn Sinh — tức lấy giới hạn nội dung của mình áp lên sự thật ngoài đời.
3. Quản lý trong Dolphin (REQ-SCH-15)
dolphin #/truong — danh sách theo tỉnh, thêm/sửa trường, quản lý lớp chuyên. Cùng cổng vai requireMentor (mentor · staff · admin) với phần còn lại của Dolphin, và cùng nằm sau Cloudflare Access.
Sửa một trường không ảnh hưởng đề thi đã gắn vào nó — quan hệ là khoá ngoại theo id, không theo tên. Xoá trường thì cấm nếu còn đề hoặc còn người trỏ vào; thay bằng status='archived'.
4. Người của trường (REQ-SCH-16)
school_students: id · school_id → target_schools(id)
· full_name · program_id? → specialized_programs(id) · program_label?
· cohort_year? -- khoá vào (2018)
· grad_year? -- năm tốt nghiệp
· kind -- alumni | current, MẶC ĐỊNH 'alumni'
· highlight? -- một dòng: "HCV Olympic Tin học quốc tế 2023"
· achievements? -- nhiều dòng
· current_org? -- đang học/làm ở đâu
· photo_media_id? → media(id)
· mentor_profile_id? → mentor_profiles(id) -- cùng một người
· sources_json · verification_status(unverified|verified|disputed)
· verification_note? · verified_at? · verified_by? -- ghi chú là NỘI BỘ
· consent · consent_note?
· featured · status(draft|published|archived) · display_order · timestampsHai cổng chặn publish, cả hai thi hành trong service chứ không chỉ nhắc trên UI:
status='published'chỉ đặt được khiconsent=1vàverification_status='verified'.
Cổng thứ hai đến từ chỉ đạo chủ dự án 2026-08-17 ("Mọi thông tin đều cần được kiểm chứng và rà soát lại, rồi mới đưa thông tin vào hệ thống") khi gửi danh sách cựu học sinh đầu tiên. Nó đáng có mặt trong schema chứ không phải trong quy trình làm việc, vì đây là lời khẳng định về người có thật: một dòng sai năm sinh hay sai huy chương trên trang công khai không chỉ là dữ liệu xấu mà là nói sai về một cá nhân. sources_json giữ danh sách nguồn đã đối chiếu; verification_status='disputed' dành cho mảnh thông tin mà các nguồn nói khác nhau — giữ lại và đánh dấu, không im lặng chọn một bên.
Ba quyết định đáng ghi lại:
- "Học sinh của trường" ở đây là người được nêu tên công khai, không phải sổ điểm danh (Q-136). Chỉ đạo nói "thêm hoặc sửa thông tin của các student của từng trường", còn phần công khai nói "danh sách các cựu học sinh tiêu biểu" — nên bảng này phục vụ đúng cái thứ hai. Bê danh sách ~1.500 học sinh đang học của một trường chuyên vào D1 rồi đẩy lên trang công khai là hồ sơ trẻ vị thành niên, thuộc REQ-SEC-04, không phải một tính năng marketing.
kind='current'giữ lại cho trường hợp một em đang học được nêu tên (đoạt giải), và em đó vẫn phải qua cổngconsentnhư mọi người khác. consentlà cổng publish, không phải ghi chú. Nêu tên và thành tích một người có thật lên trang công khai mà không hỏi họ là việc không sửa lại được sau khi trang đã lên.mentor_profile_idnối hai chiều với SDD-019. Một người vừa là cựu học sinh Ams vừa là mentor của Nemo12 thì chỉ có một hàng sự thật cho mỗi vai, và trang trường hiện huy hiệu "Mentor của Nemo12" mở sang hồ sơ. Đây chính là chỗ hai chỉ đạo SRC-201 và SRC-203 gặp nhau: câu "mentor đều là cựu học sinh trường chuyên" đọc từmentor_profiles.alumni_school_id→target_schools, cùng bảng mà trang trường đang đọc.
5. Thành tích & số liệu theo năm học (REQ-SCH-17)
school_achievements: id · school_id → target_schools(id)
· school_year -- "2024-2025", BẮT BUỘC
· category -- hsg_quoc_gia | hsg_tinh | olympic_quoc_te | dai_hoc | khac
· title · detail? · quantity?
· source_url? · source_note?
· status · display_order · created_at
school_year_stats: (school_id, school_year, program_id?) PK
· applicants? · seats? · cutoff_score? · ratio? -- đều cho phép NULL
· source_url? · source_note? · updated_atschool_yearlà chuỗi"YYYY-YYYY", không phải số. Năm học Việt Nam vắt qua hai năm dương lịch; ép về một số nguyên là buộc mọi chỗ đọc phải tự nhớ quy ước "2024 nghĩa là 2024-2025", và sớm muộn có chỗ nhớ khác.ratio(tỉ lệ chọi) lưu thẳng chứ không luôn tính từapplicants/seats, vì nguồn công bố thường chỉ đưa tỉ lệ. Khi có đủ cả hai, service tính lại và cảnh báo nếu lệch thay vì im lặng ghi đè — hai nguồn nói khác nhau là thông tin, không phải lỗi.- Không cột nào ở đây
NOT NULLngoài khoá vàschool_year: một trường có thể công bố điểm chuẩn mà không công bố số dự thi. Ô trống là câu trả lời hợp lệ (nguyên tắc §1.3).
6. Trang công khai (REQ-BRD-09)
| Route | Nội dung |
|---|---|
/specialist-schools | mọi trường published, lọc theo tỉnh và loại; mỗi thẻ: logo · tên ngắn · tỉnh · số lớp chuyên · một dòng |
/specialist-schools/{slug} | giới thiệu · lớp chuyên · thành tích nhóm theo năm học, mới nhất trước · cựu học sinh tiêu biểu (mentor Nemo12 có huy hiệu riêng) · số liệu tuyển sinh |
/specialist-schools/so-sanh?ids=a,b,c | 2-4 trường trên cùng bộ chỉ số |
Bộ chỉ số của trang so sánh — cố định, ít, và mỗi ô kèm năm học:
tỉnh · loại · số lớp chuyên · chỉ tiêu (năm gần nhất) · tỉ lệ chọi (năm gần nhất) · điểm chuẩn cao nhất trong các lớp chuyên (năm gần nhất) · số thành tích quốc gia/quốc tế trong 3 năm gần nhất · số cựu học sinh tiêu biểu đã publish.
Hai chỉ số cuối cố ý là "số bản ghi Nemo12 đang có", không phải "thành tích thật của trường", và trang nói rõ điều đó ngay dưới bảng. Một trường ít bản ghi trong hệ có thể chỉ là trường chưa ai nhập, và để người đọc hiểu nhầm con số đó thành chất lượng trường là cách tạo ra một bảng xếp hạng giả mà §1.4 vừa từ chối làm.
7. API
| Method | Path | Quyền |
|---|---|---|
| GET | /v1/public/schools?province=&kind= | công khai |
| GET | /v1/public/schools/{slug} | công khai |
| GET | /v1/public/schools/compare?ids= | công khai (2-4 id) |
| GET · POST | /v1/showcase/schools | 🔐 role mentor/staff/admin |
| PATCH · DELETE | /v1/showcase/schools/{id} | 🔐 như trên |
| POST | /v1/showcase/schools/{id}/programs · PATCH · DELETE /v1/showcase/programs/{id} | 🔐 như trên |
| POST | /v1/showcase/schools/{id}/students · PATCH · DELETE /v1/showcase/students/{id} | 🔐 như trên |
| POST | /v1/showcase/schools/{id}/achievements · DELETE /v1/showcase/achievements/{id} | 🔐 như trên |
| PUT | /v1/showcase/schools/{id}/stats | 🔐 như trên |
Nhánh công khai chỉ trả dòng status='published', và với người thì thêm consent=1. consent_note cùng source_note là ghi chú nội bộ — không ra API công khai, cùng lý do với alumni_note ở SDD-019 §4.
8. Cái chưa làm
| Việc | Vì sao để lại |
|---|---|
| Seed registry toàn quốc bằng AI | SDD-014 §2 đã đặc tả (crawl + cross-check ≥2 nguồn); đợt này dựng bảng và cổng nhập tay trước, seed là việc của pipeline |
| Xếp hạng / điểm tổng trường | Cố ý không làm (Q-137) |
| Danh sách học sinh đang theo học | Child-privacy, ngoài phạm vi (Q-136) |
Nối specialized_programs ↔ learner_exam_targets | Mục tiêu thi của learner hiện lưu tên trường dạng chuỗi; nối lại là một đợt migrate dữ liệu riêng |
Trace
REQ-EXAM-11 · REQ-SCH-15→§2-3 · REQ-SCH-16→§4 · REQ-SCH-17→§5 · REQ-BRD-09→§6-7. US-88→§3 · US-89→§4-5 · US-90→§6 · US-91→§6. Quyết định ✍️: Q-135 (mở rộng target_schools thay vì bảng mới) · Q-136 (phạm vi "student") · Q-137 (không xếp hạng trường).