Exercise AI — Do-Test API
Hợp đồng REST của runtime làm bài. Base path: /api/v1/exercise-ai.
Trang này gom quy ước chung + các Common API (dùng cho mọi dạng bài). Phần endpoint riêng theo dạng tách ra từng trang để dễ update độc lập:
| Dạng bài | Trang | Endpoint riêng |
|---|---|---|
SPEAK_A_SENTENCE, IMPROVE_A_SENTENCE | Turn-based | /answer, /turn/{turnId}, marking S2S |
LINEAR_TOOL | Linear Tool | /canvas, /speech-correction, /coaching, /snapshot/clone, DELETE /snapshot |
GUIDED_PRACTICE | Guided Practice | — (chỉ Common API) |
Quy ước
progressIdluôn là path variable cho mọi endpoint mutate progress đang tồn tại — không nằm trong body. Pattern:{HTTP} /{progressId}/<action>.- Verb:
POSTcho hành động tạo/chuyển trạng thái;PATCHcho cập nhật một phần câu/canvas. - Lỗi nghiệp vụ ném
BusinessLogicException(mãAiExerciseDoTestErrorCode); global handler render envelope. - Mọi mutation bị chặn khi progress đã
COMPLETED(PROGRESS_ALREADY_COMPLETED). - Auth JWT (học viên/guest); riêng nhóm
marking/*là server-to-server (không validator).
Common API — mọi dạng bài
Command (mutate)
| Method & path | Body | Response | Mô tả |
|---|---|---|---|
POST /dotest/start | StartDoTestRequestDTO (userResourceId, force?, teacherLanguageType?) | DocumentIdOnly | Bắt đầu / resume phiên cho userResourceId. force = làm lại từ đầu (đóng phiên IN_PROGRESS cũ). Resolve bài → bản published mới nhất. Khóa theo (userResourceId, userId) chống tạo trùng; REAL_TEST vượt maxTestAttempts → MAX_TEST_ATTEMPTS_REACHED. |
POST /dotest/demo-start | DemoStartAiExerciseRequestDTO (resourceId, subClassKey?, studentRegistrationKey?, teacherLanguageType?) | DocumentIdOnly | Tạo phiên demo/preview qua fake user-resource (disposable), forced demo mode. |
PATCH /progress/{progressId}/skip | SkipQuestionRequestDTO (questionId) | DoTestStateDTO | Bỏ qua câu → SKIPPED, rồi advance (câu cuối → bài COMPLETED). |
POST /progress/{progressId}/next | — | DoTestStateDTO | Đẩy con trỏ sang câu kế (clamp ở câu cuối); không hoàn tất bài. |
POST /progress/{progressId}/submit | SubmitQuestionRequestDTO (spendTimeInSeconds) | DocumentIdOnly | Nộp bài: tính tổng kết per-type (computeSubmitSummary), set COMPLETED + completedAt. Trả id thôi — FE reload qua GET /{progressId}. |
Query (read)
| Method & path | Response | Mô tả |
|---|---|---|
GET /progress/{progressId} | AiUserExerciseProgressDTO | State đầy đủ (content + questions đa hình theo exerciseType). Dùng để resume/re-sync. |
GET /turn/{turnId}là query nhưng chỉ dùng cho dạng turn-based → xem Turn-based.
Ví dụ — Common API
POST /dotest/start
// Request
{
"userResourceId": "665f0a2b3c4d5e6f7a8b9c0d",
"force": false,
"teacherLanguageType": "VIETNAMESE" // optional, default VIETNAMESE
}// Response 200 — DocumentIdOnly
{ "id": "6700a1b2c3d4e5f60718293a" }POST /dotest/demo-start
// Request
{
"resourceId": "665fae11b2c3d4e5f6071827", // id AiExercise đã publish
"subClassKey": null, // optional — tự sinh "demo-<uuid>"
"studentRegistrationKey": null, // optional — tự sinh "demo-<uuid>"
"teacherLanguageType": "ENGLISH"
}// Response 200 — DocumentIdOnly
{ "id": "6700a1b2c3d4e5f60718293b" }PATCH /progress/{progressId}/skip
// Request
{ "questionId": "665fae11b2c3d4e5f6071900" }// Response 200 — DoTestStateDTO
{
"progressId": "6700a1b2c3d4e5f60718293a",
"status": "IN_PROGRESS",
"teacherLanguageType": "VIETNAMESE",
"currentIndex": 2,
"currentQuestionId": "665fae11b2c3d4e5f6071901",
"totalQuestion": 10,
"totalAnswered": 0,
"totalSkipped": 0,
"totalNotAnswer": 0,
"averageCorrectPercentage": 0.0,
"questions": []
}POST /progress/{progressId}/next
// Request — không có body// Response 200 — DoTestStateDTO (xem cấu trúc ở skip)
{
"progressId": "6700a1b2c3d4e5f60718293a",
"status": "IN_PROGRESS",
"currentIndex": 3,
"currentQuestionId": "665fae11b2c3d4e5f6071902",
"totalQuestion": 10
}POST /progress/{progressId}/submit
// Request — SubmitQuestionRequestDTO
{ "spendTimeInSeconds": 540 }// Response 200 — DocumentIdOnly (FE reload state qua GET /{progressId})
{ "id": "6700a1b2c3d4e5f60718293a" }GET /progress/{progressId}
Trả AiUserExerciseProgressDTO. Khung chung (phần content đa hình theo dạng — xem trang từng type):
{
"id": "6700a1b2c3d4e5f60718293a",
"userResourceId": "665f0a2b3c4d5e6f7a8b9c0d",
"exerciseId": "665fae11b2c3d4e5f6071827",
"exerciseType": "SPEAK_A_SENTENCE",
"teacherLanguageType": "VIETNAMESE",
"startMode": "REAL_TEST",
"status": "IN_PROGRESS",
"currentIndex": 0,
"totalQuestion": 10,
"content": { /* polymorphic — xem trang từng dạng */ },
"createdBy": { "id": "u_123", "fullName": "Nguyễn Văn A" },
"createdAt": "2026-06-30T10:00:00+07:00"
}Shared shapes — ý nghĩa field
Request DTOs (Common)
StartDoTestRequestDTO
| Field | Kiểu | Bắt buộc | Ý nghĩa |
|---|---|---|---|
userResourceId | String | ✅ | Id user-resource (bài đã gán cho học viên qua course); BE resolve → AiExercise published mới nhất. |
force | boolean | true = làm lại từ đầu, đóng phiên IN_PROGRESS cũ; false = resume nếu còn phiên dở. | |
teacherLanguageType | TeacherLanguageType | Ngôn ngữ giáo viên/feedback (VIETNAMESE mặc định | ENGLISH). |
DemoStartAiExerciseRequestDTO
| Field | Kiểu | Bắt buộc | Ý nghĩa |
|---|---|---|---|
resourceId | String | ✅ | Id AiExercise đã publish để preview. |
subClassKey | String | Khóa lớp giả; null → tự sinh demo-<uuid>. | |
studentRegistrationKey | String | Khóa học viên giả; null → tự sinh demo-<uuid>. | |
teacherLanguageType | TeacherLanguageType | Như trên. |
SkipQuestionRequestDTO: questionId (String, ✅) — câu cần bỏ qua.
SubmitQuestionRequestDTO: spendTimeInSeconds (long) — thời gian làm bài (từ BaseCommonProgressRequestDTO).
DocumentIdOnly: id (String) — id progress vừa tạo/nộp; FE reload state qua GET /{progressId}.
Các request DTO mutate một câu/canvas kế thừa
BaseCommonProgressRequestDTO→ luôn có thêmspendTimeInSeconds(long, giây tiêu tốn cho hành động đó).
AiUserExerciseProgressDTO (response GET /{progressId}, /canvas, /speech-correction)
| Field | Kiểu | Ý nghĩa |
|---|---|---|
id | String | Id progress. |
userResourceId | String | User-resource gắn phiên. |
exerciseId | String | Id AiExercise đang làm. |
exerciseType | AiExerciseType | Dạng bài — discriminator cho content (Jackson EXTERNAL_PROPERTY). |
teacherLanguageType | TeacherLanguageType | Ngôn ngữ feedback. |
startMode | UserExerciseStartMode | REAL_TEST | DEMO_TEST. |
status | UserExerciseStatus | IN_PROGRESS | COMPLETED. |
currentIndex | int | Chỉ số câu đang active. |
totalQuestion | int | Tổng số câu. |
content | BaseAiExerciseProgressContentDTO | Nội dung đa hình theo exerciseType — xem trang từng dạng. |
createdBy | UserInfoDTO | Người tạo phiên. |
createdAt | ZonedDateTime | Thời điểm tạo (+07). |
DoTestStateDTO (response next / skip)
| Field | Kiểu | Ý nghĩa |
|---|---|---|
progressId | String | Id progress. |
status | UserExerciseStatus | Trạng thái phiên. |
teacherLanguageType | TeacherLanguageType | Ngôn ngữ feedback. |
currentIndex | int | Câu đang active. |
currentQuestionId | String | Id câu đang active. |
totalQuestion | int | Tổng số câu. |
totalAnswered / totalSkipped / totalNotAnswer | int | Đếm câu theo trạng thái (tổng kết). |
averageCorrectPercentage | double | Điểm phát âm trung bình. |
questions | BaseQuestionProgressDTO[] | Danh sách câu (đa hình theo dạng). |
total*/averageCorrectPercentagehiện chỉ fill đầy đủ choLINEAR_TOOL(từLinearProgressContent); dạng khác = 0 cho tới khi có tổng kết riêng.
BaseQuestionProgressDTO (base mọi câu)
| Field | Kiểu | Ý nghĩa |
|---|---|---|
questionId | String | Id câu. |
order | int | Thứ tự câu trong bài. |
state | QuestionState | NOT_ANSWER | ANSWERING | ANSWERED | SKIPPED. |
Subtype (field riêng theo dạng): Turn-based thêm attemptsLeft, turns[]; Linear thêm snapshots[], currentSnapshotId, totalDoCoachingMenu, totalDoCoachingLive; Guided không thêm gì (scaffolded). Content-DTO: LinearProgressContentDTO thêm totalAnswered/Skipped/NotAnswer/averageCorrectPercentage; Turn/Guided chỉ có questions[].
Enum tham chiếu (shared)
| Enum | Giá trị | Ý nghĩa |
|---|---|---|
AiExerciseType | SPEAK_A_SENTENCE, IMPROVE_A_SENTENCE, LINEAR_TOOL, GUIDED_PRACTICE | Dạng bài (discriminator content). |
UserExerciseStatus | IN_PROGRESS, COMPLETED | Vòng đời phiên (COMPLETED = terminal, chặn mọi mutate). |
UserExerciseStartMode | REAL_TEST, DEMO_TEST | Phiên thật (tính attempt) vs demo/preview. |
QuestionState | NOT_ANSWER, ANSWERING, ANSWERED, SKIPPED | Trạng thái từng câu; chỉ câu 0 khởi động ANSWERING, advance mở câu kế. |
TeacherLanguageType | VIETNAMESE, ENGLISH | Ngôn ngữ feedback/coaching. |
SpeechVerdict | GOOD (≥70%), NEED_IMPROVE | Verdict chấm phát âm (per-câu/per-card/per-snapshot). |
Error codes (AiExerciseDoTestErrorCode)
| Code | Enum | Khi nào |
|---|---|---|
| EAID001 | PROGRESS_NOT_FOUND | progressId không tồn tại |
| EAID002 | QUESTION_NOT_FOUND | questionId không có / sai dạng |
| EAID003 | QUESTION_NOT_ANSWERABLE | câu không ở ANSWERING hoặc không phải turn-based |
| EAID004 | NO_ATTEMPTS_LEFT | hết lượt trả lời (turn-based) |
| EAID005 | TURN_NOT_FOUND | turnId không tồn tại |
| EAID006 | MAX_TEST_ATTEMPTS_REACHED | retake vượt maxTestAttempts |
| EAID007 | CARD_NOT_FOUND | cardId không có trong snapshot (linear) |
| EAID008 | PROGRESS_ALREADY_COMPLETED | mutate sau khi submit |
| EAID009 | SELECTED_CARD_REQUIRED | coaching MENU thiếu selectedCardId (linear) |
| EAID010 | COACHING_FAILED | AI coaching null/parse fail, hoặc GOOD nhưng thiếu conceptualizedText (linear) |
| EAID011 | SNAPSHOT_NOT_FOUND | snapshotId không tồn tại (linear) |
| EAID012 | SNAPSHOT_DELETE_NOT_ALLOWED | xoá snapshot không ở EDITING (linear) |
Đọc tiếp
- Domain Model & State — entity, enum, state machine.
- Turn-based · Linear Tool · Guided.