在线练习:动态加载和断点续续这两个核心问题

95 阅读4分钟

非常好!您已经有了一个清晰的流程,现在需要解决的是动态加载断点续续这两个核心问题。这是一个非常经典的设计,我们可以称之为 “无限滚动刷题”“分页加载练习” 模式。

下面我为您提供一个完整、可落地的技术方案。


一、架构思路

核心在于维护两个状态:

  1. 当前练习会话(Session):记录用户正在做哪套题、进度如何。
  2. 题目分页加载:初始加载N题,滚动到底部时自动加载下一页。

我们将通过数据库设计和API配合来实现这一切。


二、数据库设计(关键)

您需要至少三张表来实现这个功能:

1. user_exercise_sessions (用户练习会话表)

这张表是解决“断点续续”问题的核心。它记录了用户每一次练习的全局状态。

字段类型描述
idBIGINT (PK)会话ID
user_idBIGINT用户ID
course_idBIGINT课程ID
current_question_indexINT当前做到第几题了 (例如:85,表示正在做第85题)
is_completedBOOLEAN会话是否已完成
created_atTIMESTAMP创建时间
updated_atTIMESTAMP最后更新时间

2. user_question_answers (用户答题记录表)

这张表记录每一道题的具体答案。

字段类型描述
idBIGINT (PK)记录ID
session_idBIGINT关联到 user_exercise_sessions.id
question_idBIGINT题目ID
user_answerTEXT用户提交的答案 (JSON字符串,兼容单选、多选、填空)
is_correctBOOLEAN是否正确 (可选)
created_atTIMESTAMP创建时间

3. questions (题目表)

您应该已经有的题目表。

字段类型描述
idBIGINT (PK)题目ID
course_idBIGINT课程ID
question_contentTEXT题目内容
correct_answerTEXT正确答案
......其他字段(题型、分值、解析等)

三、后端API设计

1. 初始化或恢复练习会话 GET /api/exercise-session/start

功能:用户点击“开始练习”或登录后检查未完成的会话。 逻辑

  1. 检查该用户 (user_id) 和课程 (course_id) 下是否存在 is_completed = false 的会话。
  2. 如果存在(断点续续):返回该会话的所有信息,特别是 current_question_index
    {
      "sessionId": 12345,
      "courseId": 1,
      "currentQuestionIndex": 85,
      "message": "发现您有一个未完成的练习,是否继续?"
    }
    
  3. 如果不存在(新会话)
    • user_exercise_sessions 中创建一条新记录,current_question_index 设为 0。
    • 返回新的 session_id

2. 获取题目 GET /api/questions

功能:根据当前进度,分页加载题目。 参数

  • courseId: 课程ID
  • sessionId: 会话ID
  • offset: 偏移量 (例如:0, 100, 200...)
  • limit: 每页数量 (固定为100)

逻辑

  1. 前端根据 current_question_index (例如85) 计算出 offset (计算方式:Math.floor(85 / 100) * 100 = 0,即第一页)。
  2. 执行SQL:SELECT * FROM questions WHERE course_id = ? ORDER BY id LIMIT ? OFFSET ?;
  3. 同时,为了渲染题号状态,前端可能需要之前所有题的答题记录。可以再提供一个API:GET /api/session/{sessionId}/answers 来获取本次会话的所有答题记录,用于在题号列表上显示“已答”、“未答”状态。

3. 提交答案并更新进度 POST /api/answer

功能:用户答题后,保存答案并更新会话进度。 参数 (RequestBody):

{
  "sessionId": 12345,
  "questionId": 888,
  "userAnswer": "A" // 或 ["A", "C"]
}

逻辑

  1. user_question_answers 表中插入或更新一条记录。
  2. 关键步骤:更新 user_exercise_sessions 表,将 current_question_index 设置为当前题号(例如 current_question_index = 86)。
  3. (可选)触发自动加载检查:可以在响应体中返回当前总进度,前端据此判断是否需要加载下一页。
    {
      "saved": true,
      "currentIndex": 86,
      "totalAnswered": 86 // 本次会话总共答了多少题
    }
    

四、前端逻辑实现

前端是协调这一切的核心,流程如下:

flowchart TD
    A[用户进入课程练习页] --> B[调用 /api/exercise-session/start<br>检查未完成会话]
    B -- 发现未完成会话 --> C[弹出提示:“是否继续?”]
    C -- 用户点击“继续” --> D[获取current_question_index<br>(例如85)]
    B -- 无会话 --> E[创建新会话<br>current_question_index=0]
    D --> F
    E --> F
    
    subgraph F [初始化加载]
        direction LR
        F1[计算偏移量offset<br>floor(85/100)*100=0]
        F2[调用 GET /api/questions<br>加载第0-99题]
        F1 --> F2
    end
    
    F --> G[渲染题目<br>并定位到第85题]
    
    G --> H
    subgraph H [用户答题循环]
        direction TB
        H1[用户答题]
        H2[点击“下一题”<br>调用 POST /api/answer 保存]
        H3[更新本地进度状态]
        H4[检查是否需要加载更多<br>例如:当前第98题 > 阈值95]
        H4 -- 是 --> H5[异步加载下一批100题<br>(offset=100)]
        H4 -- 否 --> H1
        H5 --> H1
        H1 --> H2 --> H3 --> H4
    end

自动加载下一页的检测条件

// 假设当前正在做第 currentIndex 题
const loadMoreThreshold = 95; // 距离底部5题时开始加载
const currentPage = Math.floor(currentIndex / 100); // 当前在第几页(0-indexed)
const nextPageOffset = (currentPage + 1) * 100; // 下一页的偏移量

if (currentIndex >= (currentPage * 100) + loadMoreThreshold) {
  // 调用API加载 nextPageOffset 开始的100题
  // 将新加载的题目追加到现有的题目列表后面
  fetchQuestions(nextPageOffset, 100);
}

五、用户界面(UI)流程

  1. 登录后进入课程

    • 如果检测到未完成会话,弹出优雅的提示框:“发现您于【2023-10-27 15:30】有一个未完成的练习,共完成了85题,是否继续?”
    • 按钮:【继续练习】、【重新开始】(注意:选择重新开始需要将旧会话标记为is_completed=true,然后创建新会话)。
  2. 练习过程中

    • 用户无感知地做题,系统在后台保存答案和进度。
    • 当做到第95题左右时,前端静默加载第100-199题,用户滑动时体验流畅,没有等待感。
  3. 重新登录

    • 直接应用上述流程,自动定位到上次的题目。

这个方案将为您提供一个非常流畅、专业且用户友好的在线练习体验。