# Question bank — MySQL schema & API (EmsNew)

Tài liệu này mô tả **schema đang chạy trong codebase** (`App\Models\EmsNew`), cách **payload FE** (dạng `@Untitled-1`) map vào DB, và **ví dụ curl** cho `POST` / `GET` / `PATCH` / `DELETE`.

> **Lưu ý:** Phần **cuối tài liệu** có gợi ý thiết kế mở rộng (`question_types`, `question_options` tách bảng) — **chưa** implement trong migration hiện tại; options đang nằm trong JSON `answer_scheme`.

---

## 1. Mục tiêu

- Một bảng **`questions`** cho cả **câu gốc** (passage / reading) và **câu con** (`parent_id`).
- **Nội dung & đáp án** linh hoạt qua JSON: `content`, `answer_scheme` (FE có thể gửi `answer_data` — API merge sang `answer_scheme`).
- Tương thích MySQL 8: cột `JSON`, index, foreign key.
- API REST: `App\Http\Controllers\EmsNew\QuestionController`, route prefix mặc định Laravel: **`/api/questions`**.

---

## 2. Bảng liên quan (đang có migration)

| Bảng | Vai trò |
|------|---------|
| `questions` | Câu hỏi gốc + con; `parent_id` self-FK, soft delete |
| `exams` | Đề / kỳ thi |
| `exam_sections` | Phần trong đề (sau migration enhance) |
| `exam_question_items` | Nối `exam_id` ↔ `question_id`, `order`, `weight`, `section_id` |
| `submissions` | Bài nộp theo user + exam |
| `submission_details` | Chi tiết theo `question_id` + `user_answer` (JSON) |

**Không có** bảng `question_types` hay `question_options` trong code hiện tại — loại câu lưu ở cột `questions.type` (string) và phần chi tiết trong `answer_scheme`.

---

## 3. Bảng `questions` (schema thực tế)

Cấu trúc tổng hợp từ migration `2026_04_03_100000_*` và `2026_04_03_150000_*`:

```sql
CREATE TABLE questions (
    id              BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

    parent_id       BIGINT UNSIGNED NULL,
    CONSTRAINT fk_questions_parent
        FOREIGN KEY (parent_id) REFERENCES questions(id) ON DELETE CASCADE,

    type            VARCHAR(50) NOT NULL,          -- passage, single_choice, multi_choice, true_false, matching, ...

    content         JSON NOT NULL,                 -- stem: text, media, ...
    metadata        JSON NULL,                     -- tag, skill, ... (sau migration enhance)

    answer_scheme   JSON NULL,                     -- đáp án / config theo type (trước đây tên cột: answer_data)

    `order`         INT NOT NULL DEFAULT 0,       -- thứ tự trong cùng parent
    is_active       TINYINT(1) NOT NULL DEFAULT 1,
    created_by      BIGINT UNSIGNED NULL,
    created_at      TIMESTAMP NULL,
    updated_at      TIMESTAMP NULL,
    deleted_at      TIMESTAMP NULL,                -- soft delete

    INDEX idx_questions_parent (parent_id)
);
```

### 3.1. Giá trị `type` (cột DB) sau khi API chuẩn hoá

FE có thể gửi `type: "question"` + `answer_data.type` — backend map sang cột `type`:

| `answer_data.type` (hoặc tương đương) | Cột `questions.type` |
|--------------------------------------|----------------------|
| `none` | `passage` (gốc không đáp án) |
| `true_false` | `true_false` |
| `select_single` | `single_choice` |
| `select_multi` | `multi_choice` |
| `matching` | `matching` |
| … | … |

Passage gốc thường: `"type": "passage"` + `answer_data.type: "none"`.

### 3.2. Lưu `content` và `answer_scheme`

- **`content`**: ví dụ `{ "text": "<h2>...</h2>", "media": { "audio", "image", "video" } }` cho passage; `{ "text": "..." }` cho câu con.
- **`answer_scheme`**: toàn bộ object đáp án (sau khi gộp từ `answer_data`); backend có thể **bổ sung `id` UUID** cho từng `option` và chuẩn hoá `matching` (left/right/correct_mapping).

---

## 4. Đầu vào FE khi tạo (mẫu chuẩn)

Payload ban đầu giống file ví dụ — **bắt buộc có key `children`** nếu muốn lưu **cả cây** trong một lần `POST`:

```json
{
  "type": "passage",
  "content": {
    "text": "<h2>The Rise of Renewable Energy</h2><p>Solar and wind power are becoming the primary sources of energy globally...</p>",
    "media": { "audio": null, "image": "https://cdn.example.com/energy.jpg", "video": null }
  },
  "answer_data": {
    "type": "none"
  },
  "children": [
    {
      "type": "question",
      "content": { "text": "Solar energy is a renewable source of power." },
      "answer_data": {
        "type": "true_false",
        "options": [
          { "value": "True", "is_correct": true },
          { "value": "False", "is_correct": false }
        ],
        "explain": "Đoạn văn khẳng định năng lượng mặt trời là nguồn tài nguyên tái tạo."
      }
    },
    {
      "type": "question",
      "content": { "text": "Which country leads in wind energy production?" },
      "answer_data": {
        "type": "select_single",
        "options": [
          { "value": "China", "is_correct": true },
          { "value": "Vietnam", "is_correct": false },
          { "value": "Germany", "is_correct": false }
        ],
        "explain": "Theo số liệu mới nhất, Trung Quốc hiện đứng đầu về sản lượng điện gió."
      }
    },
    {
      "type": "question",
      "content": { "text": "Select ALL benefits of solar energy mentioned in the text:" },
      "answer_data": {
        "type": "select_multi",
        "options": [
          { "value": "Low maintenance", "is_correct": true },
          { "value": "Zero emissions", "is_correct": true },
          { "value": "Works at night", "is_correct": false }
        ],
        "explain": "Văn bản đề cập đến chi phí bảo trì thấp và không phát thải, nhưng năng lượng mặt trời không hoạt động vào ban đêm."
      }
    },
    {
      "type": "question",
      "content": { "text": "Match the energy source with its characteristic:" },
      "answer_data": {
        "type": "matching",
        "left": ["Solar", "Wind"],
        "right": ["Photovoltaic cells", "Turbines"],
        "correct_mapping": [
          { "left": "Solar", "right": "Photovoltaic cells" },
          { "left": "Wind", "right": "Turbines" }
        ],
        "explain": "Pin mặt trời (Photovoltaic) dùng cho điện mặt trời, và Tuabin dùng cho điện gió."
      }
    }
  ]
}
```

**Mapping vào DB:**

- **1 dòng** passage: `parent_id = NULL`, `type = passage`, `content` = phần `content` gốc, `answer_scheme` = `{ "type": "none" }` (sau chuẩn hoá).
- **4 dòng** câu con: `parent_id = id passage`, `order` = 0..3, mỗi dòng `type`/`answer_scheme` theo bảng map ở §3.1.

---

## 5. API routes (tham chiếu)

File: `routes/api.php`

| Method | Path | Mô tả |
|--------|------|--------|
| `POST` | `/api/questions` | Tạo 1 câu hoặc **cả cây** (có `children`) |
| `GET` | `/api/questions/{id}` | Chi tiết; `?with_children=1`, `?include_answer_scheme=1` |
| `PATCH` / `PUT` | `/api/questions/{id}` | Cập nhật; có `children` = đồng bộ cây con |
| `DELETE` | `/api/questions/{id}` | Soft delete node + hậu duệ |

Implementation: `App\Http\Controllers\EmsNew\QuestionController`, models `App\Models\EmsNew\Question`, services `App\Services\EmsNew\*`.

---

## 6. Ví dụ curl

Thay `BASE` bằng origin thật (ví dụ `https://lmsnew-test.hocmai.net`). Thêm header auth nếu API yêu cầu (ví dụ `Authorization: Bearer ...`).

### 6.1. Tạo cả passage + 4 câu con (body như §4)

```bash
curl -sS -X POST "${BASE}/api/questions" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d @- <<'EOF'
{
  "type": "passage",
  "content": {
    "text": "<h2>The Rise of Renewable Energy</h2><p>Solar and wind power are becoming the primary sources of energy globally...</p>",
    "media": { "audio": null, "image": "https://cdn.example.com/energy.jpg", "video": null }
  },
  "answer_data": { "type": "none" },
  "children": [
    {
      "type": "question",
      "content": { "text": "Solar energy is a renewable source of power." },
      "answer_data": {
        "type": "true_false",
        "options": [
          { "value": "True", "is_correct": true },
          { "value": "False", "is_correct": false }
        ],
        "explain": "Đoạn văn khẳng định năng lượng mặt trời là nguồn tài nguyên tái tạo."
      }
    },
    {
      "type": "question",
      "content": { "text": "Which country leads in wind energy production?" },
      "answer_data": {
        "type": "select_single",
        "options": [
          { "value": "China", "is_correct": true },
          { "value": "Vietnam", "is_correct": false },
          { "value": "Germany", "is_correct": false }
        ],
        "explain": "Theo số liệu mới nhất, Trung Quốc hiện đứng đầu về sản lượng điện gió."
      }
    },
    {
      "type": "question",
      "content": { "text": "Select ALL benefits of solar energy mentioned in the text:" },
      "answer_data": {
        "type": "select_multi",
        "options": [
          { "value": "Low maintenance", "is_correct": true },
          { "value": "Zero emissions", "is_correct": true },
          { "value": "Works at night", "is_correct": false }
        ],
        "explain": "Văn bản đề cập đến chi phí bảo trì thấp và không phát thải, nhưng năng lượng mặt trời không hoạt động vào ban đêm."
      }
    },
    {
      "type": "question",
      "content": { "text": "Match the energy source with its characteristic:" },
      "answer_data": {
        "type": "matching",
        "left": ["Solar", "Wind"],
        "right": ["Photovoltaic cells", "Turbines"],
        "correct_mapping": [
          { "left": "Solar", "right": "Photovoltaic cells" },
          { "left": "Wind", "right": "Turbines" }
        ],
        "explain": "Pin mặt trời (Photovoltaic) dùng cho điện mặt trời, và Tuabin dùng cho điện gió."
      }
    }
  ]
}
EOF
```

Response trả `data` dạng cây, `answer_scheme` có thể đã có `id` cho từng option (UUID).

### 6.2. Đọc passage kèm cây + đáp án (CMS)

Thay `PASSAGE_ID` bằng `id` passage sau khi tạo.

```bash
curl -sS "${BASE}/api/questions/PASSAGE_ID?with_children=1&include_answer_scheme=1" \
  -H "Accept: application/json"
```

### 6.3. Cập nhật chỉ nội dung passage (không đổi `children`)

```bash
curl -sS -X PATCH "${BASE}/api/questions/PASSAGE_ID" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"content":{"text":"<h2>Updated title</h2><p>...</p>","media":{"audio":null,"image":"https://cdn.example.com/energy.jpg","video":null}}}'
```

### 6.4. Đồng bộ `children` (thay / thêm / bỏ câu con)

Gửi **đủ** danh sách câu con mong muốn: phần tử **có `id`** = cập nhật; **không `id`** = tạo mới; id **có trong DB nhưng không còn trong mảng** = soft-delete.

```bash
curl -sS -X PATCH "${BASE}/api/questions/PASSAGE_ID" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"children":[{"id":CHILD_ID_1,"type":"true_false","content":{"text":"..."},"answer_scheme":{"type":"true_false","options":[]}}]}'
```

### 6.5. Xóa mềm một nhánh câu hỏi

```bash
curl -sS -X DELETE "${BASE}/api/questions/QUESTION_ID" \
  -H "Accept: application/json"
```

---

## 7. Bảng `exams`, `exam_question_items`, `submissions`

- Gắn câu hỏi vào đề: dùng `exam_question_items` (`exam_id`, `question_id`, `order`, `weight`, `section_id` tuỳ migration).
- Làm bài / chấm: `submissions` + `submission_details` (`question_id` FK tới `questions.id`, `user_answer` JSON).

Model EmsNew: `App\Models\EmsNew\Question`, `Submission`, `SubmissionDetail`; `App\Models\Exam`, `ExamQuestionItem`, …

---

## 8. Phụ lục — hướng mở rộng (chưa implement)

Nếu sau này cần **chuẩn hoá type** và **tách option** ra bảng riêng (giống thiết kế tài liệu cũ), có thể cân nhắc:

- **`question_types`**: `code` (READING, MCQ_SINGLE, …), `config_schema` JSON.
- **`questions`**: thêm `type_id` FK → `question_types`, cột `content_json` / `stem_text` tách bạch hơn `content`.
- **`question_options`**: `question_id`, `content`, `is_correct`, `order` — phục vụ query SQL trên từng đáp án.

Hiện tại **toàn bộ option** (MCQ / true_false / …) nằm trong **`answer_scheme`** JSON; migration không tạo `question_types` / `question_options`.

---

## 9. Tóm tắt

| Thành phần | Thực tế trong project |
|------------|------------------------|
| Lưu passage + câu con | Một bảng `questions`, quan hệ `parent_id` |
| Loại câu | Cột `type` (string), map từ `answer_data.type` khi FE gửi `type: "question"` |
| Đáp án / option | JSON `answer_scheme` (có thể sinh `id` UUID khi lưu) |
| Tạo cây từ FE | `POST /api/questions` với body §4 |
| Đọc / sửa / xóa | `GET` / `PATCH` (kèm `children` nếu sync) / `DELETE` như §6 |
