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

【Webman+MySQL答题系统八】成绩统计与错题本接口

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

本篇目标

三个统计接口:我的成绩、试卷维度统计(平均分/及格率)、个人错题本。全部基于 record 与 record_answer 聚合。

第八篇封面

一、数据关系

成绩统计与错题本关系图

二、我的成绩列表

app/controller/StatController.php:

<?php
namespace app\controller;

use support\Request;
use support\Db;

class StatController
{
    public function myRecords(Request $request)
    {
        $userId = (int)$request->get('user_id', 1);   // 第九篇换成登录态
        $items  = Db::table('record as r')
            ->join('paper as p', 'p.id', '=', 'r.paper_id')
            ->where('r.user_id', $userId)
            ->orderByDesc('r.id')
            ->limit(20)
            ->get(['r.id', 'p.title', 'r.score', 'p.total_score',
                     'r.is_pass', 'r.use_seconds', 'r.create_time']);

        return success(['items' => $items]);
    }
}

三、试卷统计

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

        $stat = Db::table('record')
            ->where('paper_id', $paperId)
            ->selectRaw(
                'count(*) as join_count, ' .
                'round(avg(score), 1) as avg_score, ' .
                'max(score) as max_score, ' .
                'sum(is_pass) as pass_count'
            )
            ->first();
        if (!$stat || $stat->join_count == 0) {
            return error('该试卷暂无答题记录');
        }

        return success([
            'join_count' => $stat->join_count,
            'avg_score'  => $stat->avg_score,
            'max_score'  => $stat->max_score,
            'pass_rate'  => round($stat->pass_count * 100 / $stat->join_count, 1) . '%',
        ]);
    }

一条 SQL 聚合四个指标,PHP 端只算及格率——聚合下推数据库是统计接口的第一选择,数据量大时再考虑缓存表。

四、错题本

核心思路:用户全部 record_answer 里 is_right=0 的行,按 question_id 聚合计错次,带出题干与解析:

    public function wrongBook(Request $request)
    {
        $userId = (int)$request->get('user_id', 1);

        $items = Db::table('record_answer as ra')
            ->join('record as r', 'r.id', '=', 'ra.record_id')
            ->join('question as q', 'q.id', '=', 'ra.question_id')
            ->where('r.user_id', $userId)
            ->where('ra.is_right', 0)
            ->groupBy('q.id', 'q.title', 'q.type', 'q.options', 'q.answer', 'q.analysis')
            ->orderByDesc('wrong_count')
            ->limit(50)
            ->selectRaw(
                'q.id, q.title, q.type, q.options, q.answer, q.analysis, ' .
                'count(*) as wrong_count'
            )
            ->get();

        // options 手动 decode + 计算我最近一次的答案
        $items = array_map(function ($row) {
            $row->options = json_decode($row->options, true);
            $last = Db::table('record_answer as ra')
                ->join('record as r', 'r.id', '=', 'ra.record_id')
                ->where('r.user_id', (int)request()->get('user_id', 1))
                ->where('ra.question_id', $row->id)
                ->orderByDesc('ra.id')
                ->value('ra.answer');
            $row->my_last_answer = $last ?: '';
            return $row;
        }, $items);

        return success(['items' => $items]);
    }

两个细节:

  • 错题本此时才返回 answer 和 analysis——复盘场景本来就要给学员看正确答案,与第六篇的防泄题不冲突;
  • groupBy 把 select 里所有非聚合列都列上,兼容 MySQL 5.7 的 only_full_group_by。

五、路由与验证

Route::group('/api/stat', function () {
    Route::get('/my_records', [StatController::class, 'myRecords']);
    Route::get('/paper_stat', [StatController::class, 'paperStat']);
    Route::get('/wrong_book', [StatController::class, 'wrongBook']);
});

curl "http://127.0.0.1:8787/api/stat/wrong_book?user_id=1"
curl "http://127.0.0.1:8787/api/stat/paper_stat?paper_id=1"

用第七篇的半对题验证:wrong_book 应包含第 2 题,wrong_count 至少为 1,my_last_answer 是 "A,D"。

小结

统计三接口完成。业务功能全齐了,下一篇补上登录鉴权,把"写死 user_id=1"换掉。