#08 – Thiết kế API
Giai đoạn: Thiết kế hệ thống · Phiên bản 0.1 – 29/10/2026
Mức tổng quan để chốt hợp đồng giữa frontend và backend. Đặc tả chi tiết sẽ được sinh tự động dạng OpenAPI 3.1 từ mã nguồn (NFR-MAINT-01).
#1. Quy ước chung
| Mục |
Quy ước |
| Kiểu |
REST + JSON, tiền tố /api/v1 |
| Xác thực |
Cookie phiên (web) hoặc Authorization: Bearer <token>; CMS dùng tiền tố /api/v1/admin |
| Tên trường |
camelCase; thời gian ISO 8601 có múi giờ (2026-11-30T09:00:00+07:00) |
| Phân trang |
?cursor=...&limit=20 → { items, nextCursor } |
| Idempotency |
Header Idempotency-Key cho POST /activations, POST /orders, POST /attempts/{id}/submit |
| Lỗi |
{ "error": { "code": "CODE_NOT_FOUND", "message": "...", "details": {...} } } + mã HTTP phù hợp |
| Giới hạn tần suất |
Header RateLimit-Remaining, Retry-After khi 429 |
| Phiên bản |
Thay đổi phá vỡ tương thích → /api/v2 |
#Mã lỗi nghiệp vụ chính
| Mã |
HTTP |
Ý nghĩa |
UNAUTHENTICATED |
401 |
Chưa đăng nhập |
FORBIDDEN |
403 |
Không có vai trò phù hợp |
NOT_ENTITLED |
403 |
Chưa kích hoạt sách / chưa mua gói (kèm bookId để hiện nút kích hoạt) |
CODE_NOT_FOUND |
404 |
ID không tồn tại (kèm suggestions) |
ACTIVATION_INVALID |
422 |
Mã kích hoạt không tồn tại / đã thu hồi |
ACTIVATION_USED |
409 |
Mã đã được tài khoản khác dùng |
ACTIVATION_LOCKED |
429 |
Thử sai quá nhiều lần |
ATTEMPT_CLOSED |
409 |
Lượt đã nộp/hết giờ, không nhận câu trả lời |
ATTEMPT_IN_PROGRESS |
409 |
Đã có lượt đang làm (kèm attemptId) |
VALIDATION_ERROR |
400 |
Dữ liệu không hợp lệ |
#2. API cho học sinh
#2.1 Tài khoản & kích hoạt
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/auth/sso/authorize → /auth/sso/callback |
Đăng nhập Hoclieu |
FR-01.01 |
| POST |
/auth/register, /auth/login, /auth/otp, /auth/password/reset |
Tài khoản riêng |
FR-01.01, 01.08 |
| POST |
/auth/logout |
Đăng xuất (?all=true mọi thiết bị) |
FR-01.08 |
| GET / PATCH |
/me |
Hồ sơ, đồng ý điều khoản |
FR-01.07 |
| POST |
/activations/preview |
Kiểm tra mã, trả thông tin sách (không kích hoạt) |
FR-01.04 |
| POST |
/activations |
Kích hoạt |
FR-01.02–04 |
| GET |
/me/books |
Sách đã kích hoạt |
FR-01.05 |
POST /api/v1/activations
Idempotency-Key: 7c1f...
{ "code": "k7xq-m4pa-r9" }
201 Created
{
"book": { "id": "b-toan-27", "title": "Ôn thi TNTHPT Toán", "edition": "2027", "subject": "MATH" },
"entitlement": { "validTo": "2027-09-30T23:59:59+07:00" }
}
#2.2 Phân giải ID
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/codes/{code}?edition=27 |
Phân giải ID → tài nguyên |
FR-02.02–05 |
| GET |
/me/recent-codes |
ID đã mở gần đây |
FR-02.07 |
GET /api/v1/codes/tv012
200 OK
{ "code": "TV012", "kind": "EXAMPLE", "resourceId": "r-123", "bookId": "b-toan-27",
"path": "/vi-du/r-123", "printedPage": 87 }
403 Forbidden
{ "error": { "code": "NOT_ENTITLED", "details": { "bookId": "b-toan-27", "bookTitle": "Ôn thi TNTHPT Toán 2027" } } }
404 Not Found
{ "error": { "code": "CODE_NOT_FOUND", "details": { "normalized": "TV999", "suggestions": ["TV099"] } } }
#2.3 Học liệu
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/resources/{id} |
Nội dung lý thuyết/ví dụ (rich text, video id, liên kết cùng chuyên đề) |
FR-03.01–02, 02.08 |
| GET |
/videos/{id}/playback |
URL phát HLS ký số + vị trí đã xem |
FR-03.03–04 |
| PUT |
/videos/{id}/progress |
Lưu vị trí xem |
FR-03.04 |
| GET |
/books/{id}/tree |
Cây thư viện kèm trạng thái học |
FR-03.05–06 |
| POST |
/content-reports |
Báo lỗi nội dung |
FR-03.07 |
#2.4 Làm bài & kết quả
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/assessments/{id} |
Thông tin đề, cấu trúc phần, chế độ cho phép, lượt đang làm dở (không có đáp án) |
FR-04.02–05 |
| POST |
/assessments/{id}/attempts |
Bắt đầu lượt { mode, timed, assignmentId? } |
FR-04.05, 04.09 |
| GET |
/attempts/{id} |
Lấy lượt + câu hỏi + câu trả lời đã lưu (khôi phục) |
FR-04.06 |
| PATCH |
/attempts/{id}/answers |
Lưu theo lô các câu trả lời |
FR-04.06 |
| POST |
/attempts/{id}/uploads |
Tải ảnh bài viết tay (URL ký số tải lên) |
FR-04.11 |
| POST |
/attempts/{id}/submit |
Nộp bài → kết quả |
FR-04.08, 05.01 |
| GET |
/attempts/{id}/result |
Kết quả, điểm theo phần, từng câu + lời giải |
FR-05.01–03, 05.07 |
| PUT |
/attempts/{id}/self-grades |
Tự chấm theo rubric |
FR-05.05 |
| GET |
/assessments/{id}/answer-key |
Chế độ tra đáp án (cần entitlement) |
FR-05.06 |
| GET |
/attempts/{id}/recommendations |
Đề xuất củng cố |
FR-06.01–04 |
PATCH /api/v1/attempts/a-789/answers
{
"answers": [
{ "questionId": "q-1", "response": "B", "timeSpentMs": 41200, "clientUpdatedAt": "2026-11-28T20:15:03.120+07:00" },
{ "questionId": "q-14", "response": { "a": true, "b": false, "c": true, "d": true } },
{ "questionId": "q-19", "response": "-1,5", "flagged": true }
]
}
200 OK
{ "savedAt": "2026-11-28T20:15:03.410+07:00", "deadlineAt": "2026-11-28T21:30:00+07:00" }
POST /api/v1/attempts/a-789/submit
200 OK
{
"score": 7.25, "maxScore": 10, "submitType": "MANUAL", "durationSec": 4680,
"sections": [
{ "title": "Phần I", "score": 2.5, "max": 3 },
{ "title": "Phần II", "score": 2.75, "max": 4 },
{ "title": "Phần III", "score": 2.0, "max": 3 }
],
"summary": { "correct": 14, "wrong": 6, "blank": 2 },
"recommendations": [
{ "tag": { "id": "t-nh", "name": "Nguyên hàm – Tích phân" }, "errorRate": 0.67,
"resources": [ { "kind": "THEORY", "code": "TL021", "resourceId": "r-21" },
{ "kind": "EXERCISE_SET", "code": "TB021", "resourceId": "r-77" } ] }
]
}
#2.5 Lịch sử & tiến bộ
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/me/attempts?subject=MATH&kind=MOCK_EXAM |
Lịch sử lượt làm |
FR-07.01 |
| GET |
/me/progress?subject=MATH |
Chuỗi điểm theo thời gian, trung bình |
FR-07.03–04 |
| GET |
/me/weak-topics?subject=MATH |
Điểm yếu tổng hợp |
FR-06.05 |
| GET |
/me/assignments |
Bài được giao |
FR-09.03 |
#2.6 Thương mại (P3)
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/packages?subject= |
Danh sách gói |
FR-08.01 |
| POST |
/orders |
Tạo đơn, trả URL thanh toán |
FR-08.02 |
| GET |
/orders/{id}, /me/orders |
Trạng thái, lịch sử |
FR-08.04 |
| POST |
/payments/{provider}/ipn |
Nhận IPN (không cần đăng nhập, kiểm chữ ký) |
FR-08.03 |
#2.7 Sự kiện
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| POST |
/events |
Gửi theo lô ≤ 50 sự kiện, chấp nhận khách ẩn danh |
FR-11.01 |
#3. API cho giáo viên
| Phương thức |
Đường dẫn |
Mô tả |
FR |
| GET |
/teacher/classes |
Lớp (đồng bộ LMS hoặc tự tạo) |
FR-09.01 |
| POST |
/teacher/classes, /classes/join |
Tạo lớp / học sinh vào bằng mã lớp (dự phòng) |
FR-09.01 |
| GET |
/teacher/assessments?bookId= |
Đề có thể giao |
FR-09.02 |
| POST |
/teacher/classes/{id}/assignments |
Giao bài { assessmentId, openAt, dueAt, modes } |
FR-09.02 |
| GET |
/teacher/assignments/{id}/report |
Tiến độ, phổ điểm, bảng điểm |
FR-09.04–05 |
| GET |
/teacher/assignments/{id}/item-analysis |
Câu và chuyên đề sai nhiều |
FR-09.06 |
| GET |
/teacher/assignments/{id}/export |
Xuất Excel |
FR-09.07 |
| POST |
/teacher/assignments/{id}/reminders |
Nhắc học sinh chưa nộp |
FR-09.04 |
#4. API cho CMS & quản trị (/admin)
| Nhóm |
Đường dẫn chính |
FR |
| Sách & cây nội dung |
/admin/books, /admin/books/{id}/nodes, /admin/resources |
FR-10.01 |
| Câu hỏi |
/admin/questions (CRUD, lọc theo tag/trạng thái), /admin/passages, /admin/rubrics |
FR-10.02, 10.10 |
| Import |
POST /admin/imports (tải file) → GET /admin/imports/{id} (xem trước, lỗi) → POST /admin/imports/{id}/commit |
FR-10.03 |
| Video |
POST /admin/videos (URL tải lên trực tiếp tới dịch vụ video), webhook /admin/videos/webhook |
FR-10.04 |
| Đề |
/admin/assessments, /admin/assessments/{id}/sections |
FR-10.07 |
| ID |
POST /admin/books/{id}/codes/generate, POST /admin/codes/{code}/assign, POST /admin/books/{id}/codes/lock, `GET /admin/books/{id}/codes/export?format=xlsx |
zip` |
| Kiểm tra liên kết |
GET /admin/books/{id}/link-check |
FR-10.14 |
| Ma trận |
/admin/taxonomy, PUT /admin/questions/{id}/tags, /admin/recommendation-settings |
FR-10.06, 10.12 |
| Duyệt |
POST /admin/resources/{id}/submit-review, /approve, /reject, GET /admin/resources/{id}/versions |
FR-10.08 |
| Mã kích hoạt |
POST /admin/activation-batches, GET /admin/activation-batches/{id}/export, GET /admin/activation-codes/lookup, POST /admin/activation-codes/{id}/revoke |
FR-10.09 |
| Người dùng CMS |
/admin/staff, /admin/roles |
FR-10.13 |
| Báo cáo |
/admin/reports/overview, /admin/reports/content, /admin/reports/commerce, ?from=&to=&subject=&format=csv |
FR-11.02–05 |
| Hỗ trợ |
/admin/users?query=, /admin/users/{id}, /admin/users/{id}/lock |
FR-11.06 |
| Gói đề |
/admin/packages |
FR-08.06 |
#5. Tích hợp ngoài (cần xác nhận với đối tác)
| Tích hợp |
Hướng |
Dữ liệu |
Câu hỏi |
| Hoclieu SSO |
SYS → Hoclieu |
OIDC: sub, họ tên, email/SĐT, vai trò |
Q02 |
| LMS Hoclieu |
SYS ↔ LMS |
Lớp, thành viên, bài giao, điểm |
Q12 |
| Dịch vụ video |
SYS ↔ Video |
Tải lên, webhook trạng thái, token phát |
Q13 |
| Cổng thanh toán |
SYS ↔ Cổng |
Tạo giao dịch, IPN, truy vấn trạng thái |
Q11 |
| OTP/Thông báo |
SYS → Nhà cung cấp |
Số điện thoại, nội dung mẫu |
Q02 |