SRDsExercise AIDo-Test APITổng quan & Common API
DraftDT-4662Exercise AISRDAPIREST

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àiTrangEndpoint riêng
SPEAK_A_SENTENCE, IMPROVE_A_SENTENCETurn-based/answer, /turn/{turnId}, marking S2S
LINEAR_TOOLLinear Tool/canvas, /speech-correction, /coaching, /snapshot/clone, DELETE /snapshot
GUIDED_PRACTICEGuided Practice— (chỉ Common API)

Quy ước

  • progressId luô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: POST cho hành động tạo/chuyển trạng thái; PATCH cho 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 & pathBodyResponseMô tả
POST /dotest/startStartDoTestRequestDTO (userResourceId, force?, teacherLanguageType?)DocumentIdOnlyBắ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 maxTestAttemptsMAX_TEST_ATTEMPTS_REACHED.
POST /dotest/demo-startDemoStartAiExerciseRequestDTO (resourceId, subClassKey?, studentRegistrationKey?, teacherLanguageType?)DocumentIdOnlyTạo phiên demo/preview qua fake user-resource (disposable), forced demo mode.
PATCH /progress/{progressId}/skipSkipQuestionRequestDTO (questionId)DoTestStateDTOBỏ qua câu → SKIPPED, rồi advance (câu cuối → bài COMPLETED).
POST /progress/{progressId}/nextDoTestStateDTOĐẩy con trỏ sang câu kế (clamp ở câu cuối); không hoàn tất bài.
POST /progress/{progressId}/submitSubmitQuestionRequestDTO (spendTimeInSeconds)DocumentIdOnlyNộ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 & pathResponseMô tả
GET /progress/{progressId}AiUserExerciseProgressDTOState đầ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

FieldKiểuBắt buộcÝ nghĩa
userResourceIdStringId user-resource (bài đã gán cho học viên qua course); BE resolve → AiExercise published mới nhất.
forcebooleantrue = làm lại từ đầu, đóng phiên IN_PROGRESS cũ; false = resume nếu còn phiên dở.
teacherLanguageTypeTeacherLanguageTypeNgôn ngữ giáo viên/feedback (VIETNAMESE mặc định | ENGLISH).

DemoStartAiExerciseRequestDTO

FieldKiểuBắt buộcÝ nghĩa
resourceIdStringId AiExercise đã publish để preview.
subClassKeyStringKhóa lớp giả; null → tự sinh demo-<uuid>.
studentRegistrationKeyStringKhóa học viên giả; null → tự sinh demo-<uuid>.
teacherLanguageTypeTeacherLanguageTypeNhư 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êm spendTimeInSeconds (long, giây tiêu tốn cho hành động đó).

AiUserExerciseProgressDTO (response GET /{progressId}, /canvas, /speech-correction)

FieldKiểuÝ nghĩa
idStringId progress.
userResourceIdStringUser-resource gắn phiên.
exerciseIdStringId AiExercise đang làm.
exerciseTypeAiExerciseTypeDạng bài — discriminator cho content (Jackson EXTERNAL_PROPERTY).
teacherLanguageTypeTeacherLanguageTypeNgôn ngữ feedback.
startModeUserExerciseStartModeREAL_TEST | DEMO_TEST.
statusUserExerciseStatusIN_PROGRESS | COMPLETED.
currentIndexintChỉ số câu đang active.
totalQuestionintTổng số câu.
contentBaseAiExerciseProgressContentDTONội dung đa hình theo exerciseType — xem trang từng dạng.
createdByUserInfoDTONgười tạo phiên.
createdAtZonedDateTimeThời điểm tạo (+07).

DoTestStateDTO (response next / skip)

FieldKiểuÝ nghĩa
progressIdStringId progress.
statusUserExerciseStatusTrạng thái phiên.
teacherLanguageTypeTeacherLanguageTypeNgôn ngữ feedback.
currentIndexintCâu đang active.
currentQuestionIdStringId câu đang active.
totalQuestionintTổng số câu.
totalAnswered / totalSkipped / totalNotAnswerintĐếm câu theo trạng thái (tổng kết).
averageCorrectPercentagedoubleĐiểm phát âm trung bình.
questionsBaseQuestionProgressDTO[]Danh sách câu (đa hình theo dạng).

total* / averageCorrectPercentage hiện chỉ fill đầy đủ cho LINEAR_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)

FieldKiểuÝ nghĩa
questionIdStringId câu.
orderintThứ tự câu trong bài.
stateQuestionStateNOT_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)

EnumGiá trịÝ nghĩa
AiExerciseTypeSPEAK_A_SENTENCE, IMPROVE_A_SENTENCE, LINEAR_TOOL, GUIDED_PRACTICEDạng bài (discriminator content).
UserExerciseStatusIN_PROGRESS, COMPLETEDVòng đời phiên (COMPLETED = terminal, chặn mọi mutate).
UserExerciseStartModeREAL_TEST, DEMO_TESTPhiên thật (tính attempt) vs demo/preview.
QuestionStateNOT_ANSWER, ANSWERING, ANSWERED, SKIPPEDTrạng thái từng câu; chỉ câu 0 khởi động ANSWERING, advance mở câu kế.
TeacherLanguageTypeVIETNAMESE, ENGLISHNgôn ngữ feedback/coaching.
SpeechVerdictGOOD (≥70%), NEED_IMPROVEVerdict chấm phát âm (per-câu/per-card/per-snapshot).

Error codes (AiExerciseDoTestErrorCode)

CodeEnumKhi nào
EAID001PROGRESS_NOT_FOUNDprogressId không tồn tại
EAID002QUESTION_NOT_FOUNDquestionId không có / sai dạng
EAID003QUESTION_NOT_ANSWERABLEcâu không ở ANSWERING hoặc không phải turn-based
EAID004NO_ATTEMPTS_LEFThết lượt trả lời (turn-based)
EAID005TURN_NOT_FOUNDturnId không tồn tại
EAID006MAX_TEST_ATTEMPTS_REACHEDretake vượt maxTestAttempts
EAID007CARD_NOT_FOUNDcardId không có trong snapshot (linear)
EAID008PROGRESS_ALREADY_COMPLETEDmutate sau khi submit
EAID009SELECTED_CARD_REQUIREDcoaching MENU thiếu selectedCardId (linear)
EAID010COACHING_FAILEDAI coaching null/parse fail, hoặc GOOD nhưng thiếu conceptualizedText (linear)
EAID011SNAPSHOT_NOT_FOUNDsnapshotId không tồn tại (linear)
EAID012SNAPSHOT_DELETE_NOT_ALLOWEDxoá snapshot không ở EDITING (linear)

Đọc tiếp