您的当前位置:首页>全部文章>文章详情

【Webman+MySQL答题系统六】用户答题流程:取卷与作答接口

果子发表于:2026-10-05 20:55:45浏览:1次TAG: #PHP #MySql #答题系统

本篇目标

切到学员视角:实现取卷接口。按试卷顺序组装题目,处理好 JSON 选项,并且从返回数据里剥掉答案——这是防泄题的底线。

第六篇封面

一、接口设计

GET /api/answer/paper?paper_id=1

返回:
{
  "paper": {"id":1, "title":"Webman 基础测试卷", "total_score":6, "duration":30},
  "questions": [
    {"id":1, "type":1, "title":"...", "options":{"A":"...","B":"..."}, "score":2}
  ]
}

没有 answer、没有 analysis。前端拿到的只是"题面",判分权完全在服务端(第七篇)。

二、答题流程

整个学员端流程是一条直线:

用户答题流程图

三、控制器实现

app/controller/AnswerController.php:

<?php
namespace app\controller;

use support\Request;
use support\Db;

class AnswerController
{
    public function paper(Request $request)
    {
        $paperId = (int)$request->get('paper_id');

        $paper = Db::table('paper')
            ->where('id', $paperId)
            ->where('status', 1)
            ->first();
        if (!$paper) {
            return error('试卷不存在或已停用', 404);
        }

        // 按组卷时的 sort 顺序取题
        $questions = Db::table('paper_question as pq')
            ->join('question as q', 'q.id', '=', 'pq.question_id')
            ->where('pq.paper_id', $paperId)
            ->where('q.status', 1)
            ->orderBy('pq.sort')
            ->get(['q.id', 'q.type', 'q.title', 'q.options', 'q.score']);

        $items = [];
        foreach ($questions as $q) {
            $items[] = [
                'id'      => $q->id,
                'type'    => $q->type,
                'title'   => $q->title,
                'options' => json_decode($q->options, true), // Db 查询不走模型 casts,手动 decode
                'score'   => $q->score,
                // 注意:这里绝不输出 q.answer 和 q.analysis
            ];
        }

        return success([
            'paper' => [
                'id'          => $paper->id,
                'title'       => $paper->title,
                'total_score' => $paper->total_score,
                'duration'    => $paper->duration,
            ],
            'questions' => $items,
        ]);
    }
}

register 路由:

Route::get('/api/answer/paper', [AnswerController::class, 'paper']);

四、为什么这里用 Db 而不是 Model

  • 查询是"两表 join + 指定列",Db::table 写起来最直接;
  • join 结果没有模型实例,casts 不生效,所以 options 手动 json_decode;
  • 当初在 Question 模型里配 casts 是给管理端单表 CRUD 用的。两套写法并存是正常的:模型管单表,Db 管聚合。

五、前端作答状态

UniApp 侧把 questions 渲染成列表,作答状态存一个对象即可:

// data()
answers: {}   // {questionId: 'A'} 单选/判断;多选存 'A,C'

// 单选点击
this.answers[q.id] = key
// 多选点击:切换选中再拼接
const cur = (this.answers[q.id] || '').split(',').filter(Boolean)
const i = cur.indexOf(key)
i >= 0 ? cur.splice(i, 1) : cur.push(key)
this.answers[q.id] = cur.sort().join(',')

提交时把 answers、paper_id、use_seconds(前端计时)一起 POST 给第七篇的 submit 接口。

六、验证

curl "http://127.0.0.1:8787/api/answer/paper?paper_id=1"

// 检查两点:
// 1. questions 里找不到 answer / analysis 字段
// 2. options 是对象而不是 JSON 字符串

小结

取卷完成:按 sort 组装、手动 decode、答案字段物理剥离。下一篇写整个系统的心脏——提交与自动判分。