文章目录[隐藏]
一步步教你,集成在线考试与题库管理系统到WordPress,通过WordPress程序的代码二次开发实现常用互联网小工具功能
引言:为什么要在WordPress中集成在线考试系统?
在当今数字化教育与企业培训蓬勃发展的时代,在线考试与评估已成为教育机构、企业培训部门乃至知识付费创作者不可或缺的功能。WordPress作为全球最受欢迎的内容管理系统(CMS),其强大的可扩展性使其远不止是一个博客平台。通过代码二次开发,我们可以将专业的在线考试与题库管理系统无缝集成到WordPress中,从而打造一个功能全面、用户体验良好的学习与评估平台。
传统的独立考试系统往往存在与主网站风格不一、用户数据割裂、维护成本高等问题。而在WordPress中集成此类系统,则能实现品牌统一、数据集中管理、用户单点登录等诸多优势。更重要的是,通过深度集成,我们还能利用WordPress的生态,结合其他插件与功能,创造出更丰富的互联网小工具应用场景。
本文将详细解析如何通过代码二次开发,在WordPress中从零开始构建一个功能完善的在线考试与题库管理系统,并探讨如何扩展其作为常用互联网小工具的潜力。无论你是WordPress开发者、教育科技创业者,还是企业培训负责人,都能从中获得切实可行的技术方案与灵感。
第一章:项目规划与系统架构设计
1.1 需求分析与功能规划
在开始编码之前,明确的需求分析是成功的关键。一个完整的在线考试与题库管理系统应包含以下核心模块:
- 题库管理模块:支持单选题、多选题、判断题、填空题、问答题等多种题型;支持试题分类、标签、难度分级;支持试题导入导出(Word、Excel、JSON格式)。
- 试卷管理模块:支持手动组卷(按分类、难度随机抽题)和固定试卷;设置考试时间、及格分数、考试次数限制等参数。
- 考试执行模块:全屏考试防作弊、实时计时、自动保存答案、中途恢复功能。
- 成绩与统计分析模块:个人成绩报告、错题集、整体考试统计分析、成绩导出。
- 用户与权限管理:与WordPress用户系统集成,区分考生、教师、管理员角色。
1.2 技术选型与架构设计
考虑到系统的复杂性与WordPress的特性,我们采用分层架构:
- 数据层:在WordPress默认数据库基础上,创建自定义数据表来存储试题、试卷、考试记录等结构化数据。利用WordPress的
$wpdb类进行安全的数据操作。 - 业务逻辑层:开发自定义插件,封装所有考试相关的业务逻辑。采用面向对象编程(OOP)设计,提高代码可维护性。
- 表现层:结合WordPress主题模板和短代码(Shortcode)或Gutenberg块,实现前端界面的灵活嵌入。使用AJAX提升考试过程的交互体验。
数据库设计示例:
wp_exam_questions:试题表(ID、题型、题干、选项、答案、解析、分类、难度、创建时间)wp_exam_papers:试卷表(ID、试卷名称、组卷规则、设置参数)wp_exam_records:考试记录表(ID、用户ID、试卷ID、开始时间、结束时间、得分、答案详情)wp_exam_user_meta:用户扩展表(错题集、收藏试题等)
第二章:开发环境搭建与基础插件创建
2.1 本地开发环境配置
推荐使用Local by Flywheel或XAMPP搭建本地WordPress环境。确保环境支持PHP 7.4+、MySQL 5.6+。安装代码编辑器(如VS Code)并配置调试工具(如Xdebug)。
2.2 创建自定义插件骨架
在wp-content/plugins/目录下创建新文件夹exam-system,并创建主插件文件exam-system.php:
<?php
/**
* Plugin Name: WordPress在线考试与题库管理系统
* Description: 在WordPress中集成完整的在线考试与题库管理功能
* Version: 1.0.0
* Author: 你的名字
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('EXAM_SYSTEM_VERSION', '1.0.0');
define('EXAM_SYSTEM_PATH', plugin_dir_path(__FILE__));
define('EXAM_SYSTEM_URL', plugin_dir_url(__FILE__));
// 核心类加载
require_once EXAM_SYSTEM_PATH . 'includes/class-database.php';
require_once EXAM_SYSTEM_PATH . 'includes/class-question.php';
require_once EXAM_SYSTEM_PATH . 'includes/class-exam.php';
require_once EXAM_SYSTEM_PATH . 'includes/class-shortcodes.php';
require_once EXAM_SYSTEM_PATH . 'includes/class-admin.php';
// 初始化插件
class Exam_System_Plugin {
private static $instance = null;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
// 激活/停用钩子
register_activation_hook(__FILE__, array('Exam_Database', 'create_tables'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
// 初始化各模块
add_action('plugins_loaded', array($this, 'init_modules'));
}
public function init_modules() {
Exam_Database::init();
Exam_Question::init();
Exam_Manager::init();
Exam_Shortcodes::init();
if (is_admin()) {
Exam_Admin::init();
}
}
public function deactivate() {
// 清理临时数据,但不删除考试记录
flush_rewrite_rules();
}
}
// 启动插件
Exam_System_Plugin::get_instance();
2.3 数据库表创建
在includes/class-database.php中实现数据表的创建与升级:
class Exam_Database {
public static function init() {
// 数据库版本控制
add_action('plugins_loaded', array(__CLASS__, 'check_db_version'));
}
public static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_prefix = $wpdb->prefix . 'exam_';
// 试题表
$questions_table = $table_prefix . 'questions';
$sql1 = "CREATE TABLE IF NOT EXISTS $questions_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
question_type varchar(50) NOT NULL DEFAULT 'single_choice',
title text NOT NULL,
options longtext,
correct_answer longtext NOT NULL,
analysis text,
category_id bigint(20) DEFAULT 0,
difficulty tinyint(1) DEFAULT 1,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY category_id (category_id),
KEY difficulty (difficulty)
) $charset_collate;";
// 试卷表
$papers_table = $table_prefix . 'papers';
$sql2 = "CREATE TABLE IF NOT EXISTS $papers_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
paper_name varchar(255) NOT NULL,
question_ids text,
settings longtext,
total_score int(11) DEFAULT 100,
time_limit int(11) DEFAULT 60,
status tinyint(1) DEFAULT 1,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// 考试记录表
$records_table = $table_prefix . 'records';
$sql3 = "CREATE TABLE IF NOT EXISTS $records_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
paper_id bigint(20) NOT NULL,
start_time datetime,
end_time datetime,
score decimal(5,2) DEFAULT 0,
answers longtext,
is_passed tinyint(1) DEFAULT 0,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY paper_id (paper_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql1);
dbDelta($sql2);
dbDelta($sql3);
// 存储数据库版本
add_option('exam_system_db_version', '1.0.0');
}
public static function check_db_version() {
if (get_option('exam_system_db_version') !== '1.0.0') {
self::create_tables();
}
}
}
第三章:核心功能模块开发
3.1 题库管理模块实现
试题管理类 (includes/class-question.php):
class Exam_Question {
private static $question_types = array(
'single_choice' => '单选题',
'multi_choice' => '多选题',
'true_false' => '判断题',
'fill_blank' => '填空题',
'short_answer' => '简答题'
);
public static function init() {
add_action('admin_menu', array(__CLASS__, 'add_admin_menu'));
add_action('wp_ajax_save_question', array(__CLASS__, 'ajax_save_question'));
add_action('wp_ajax_delete_question', array(__CLASS__, 'ajax_delete_question'));
}
// 添加管理菜单
public static function add_admin_menu() {
add_menu_page(
'题库管理',
'考试系统',
'manage_options',
'exam-system',
array(__CLASS__, 'render_questions_page'),
'dashicons-welcome-learn-more',
30
);
add_submenu_page(
'exam-system',
'试题管理',
'试题管理',
'manage_options',
'exam-questions',
array(__CLASS__, 'render_questions_page')
);
}
// 渲染试题管理页面
public static function render_questions_page() {
?>
<div class="wrap">
<h1 class="wp-heading-inline">试题管理</h1>
<button id="add-new-question" class="page-title-action">添加新试题</button>
<hr class="wp-header-end">
<!-- 试题列表表格 -->
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th width="5%">ID</th>
<th width="40%">试题内容</th>
<th width="10%">题型</th>
<th width="10%">难度</th>
<th width="15%">分类</th>
<th width="20%">操作</th>
</tr>
</thead>
<tbody id="questions-list">
<!-- 通过AJAX动态加载 -->
</tbody>
</table>
<!-- 添加/编辑试题模态框 -->
<div id="question-modal" class="modal" style="display:none;">
<div class="modal-content">
<span class="close">×</span>
<h2>编辑试题</h2>
<form id="question-form">
<input type="hidden" id="question_id" name="question_id" value="0">
<p>
<label for="question_type">题型:</label>
<select id="question_type" name="question_type">
<?php foreach(self::$question_types as $key => $label): ?>
<option value="<?php echo esc_attr($key); ?>"><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="question_title">题干:</label><br>
<textarea id="question_title" name="question_title" rows="3" cols="80" required></textarea>
</p>
<div id="options-container">
<!-- 选项内容动态生成 -->
</div>
<p>
<label for="correct_answer">正确答案:</label><br>
<textarea id="correct_answer" name="correct_answer" rows="2" cols="80" required></textarea>
</p>
<p>
<label for="analysis">试题解析:</label><br>
<textarea id="analysis" name="analysis" rows="3" cols="80"></textarea>
</p>
<p>
<button type="submit" class="button button-primary">保存试题</button>
</p>
</form>
</div>
</div>
</div>
<style>
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 800px;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
</style>
<script>
jQuery(document).ready(function($) {
// 加载试题列表
function loadQuestions() {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'get_questions',
nonce: '<?php echo wp_create_nonce('exam_nonce'); ?>'
},
success: function(response) {
$('#questions-list').html(response.data);
}
});
}
// 打开模态框
$('#add-new-question').click(function() {
$('#question-modal').show();
$('#question-form')[0].reset();
$('#question_id').val('0');
});
// 关闭模态框
$('.close').click(function() {
$('#question-modal').hide();
});
// 保存试题
$('#question-form').submit(function(e) {
e.preventDefault();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'save_question',
nonce: '<?php echo wp_create_nonce('exam_nonce'); ?>',
form_data: $(this).serialize()
},
success: function(response) {
if (response.success) {
alert('保存成功!');
$('#question-modal').hide();
loadQuestions();
} else {
alert('保存失败:' + response.data);
}
}
});
});
// 初始加载
loadQuestions();
});
</script>
<?php
}
// AJAX保存试题
public static function ajax_save_question() {
check_ajax_referer('exam_nonce', 'nonce');
global $wpdb;
$table_name = $wpdb->prefix . 'exam_questions';
parse_str($_POST['form_data'], $form_data);
$data = array(
'question_type' => sanitize_text_field($form_data['question_type']),
'title' => wp_kses_post($form_data['question_title']),
'correct_answer' => sanitize_textarea_field($form_data['correct_answer']),
'analysis' => sanitize_textarea_field($form_data['analysis']),
'updated_at' => current_time('mysql')
);
// 处理选项(针对选择题)
if (in_array($form_data['question_type'], array('single_choice', 'multi_choice'))) {
$options = array();
if (isset($form_data['option'])) {
foreach ($form_data['option'] as $key => $value) {
if (!empty(trim($value))) {
$options[$key] = sanitize_text_field($value);
}
}
}
$data['options'] = maybe_serialize($options);
}
$question_id = intval($form_data['question_id']);
if ($question_id > 0) {
// 更新
$result = $wpdb->update($table_name, $data, array('id' => $question_id));
} else {
// 新增
$data['created_at'] = current_time('mysql');
$result = $wpdb->insert($table_name, $data);
$question_id = $wpdb->insert_id;
}
if ($result !== false) {
wp_send_json_success('试题保存成功');
} else {
wp_send_json_error('保存失败:' . $wpdb->last_error);
}
}
}
3.2 试卷管理与组卷功能
试卷管理类 (includes/class-exam.php):
class Exam_Manager {
public static function init() {
add_action('admin_menu', array(__CLASS__, 'add_exam_menu'));
add_action('wp_ajax_generate_paper', array(__CLASS__, 'ajax_generate_paper'));
add_action('wp_ajax_start_exam', array(__CLASS__, 'ajax_start_exam'));
add_action('wp_ajax_submit_exam', array(__CLASS__, 'ajax_submit_exam'));
// 注册短代码
add_shortcode('exam_list', array(__CLASS__, 'shortcode_exam_list'));
add_shortcode('take_exam', array(__CLASS__, 'shortcode_take_exam'));
add_shortcode('exam_results', array(__CLASS__, 'shortcode_exam_results'));
}
// 手动组卷功能
public static function render_paper_generator() {
global $wpdb;
$categories = $wpdb->get_results("SELECT DISTINCT category_id FROM {$wpdb->prefix}exam_questions");
?>
<div class="wrap">
<h1>试卷生成器</h1>
<form id="paper-generator-form">
<table class="form-table">
<tr>
<th><label for="paper_name">试卷名称</label></th>
id="paper_name" name="paper_name" class="regular-text" required></td>
</tr>
<tr>
<th><label>组卷方式</label></th>
<td>
<label><input type="radio" name="generate_type" value="manual" checked> 手动选题</label>
<label><input type="radio" name="generate_type" value="auto"> 自动组卷</label>
</td>
</tr>
<tbody id="manual-selection">
<tr>
<th><label>选择试题</label></th>
<td>
<div style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
<?php
$questions = $wpdb->get_results("SELECT id, title, question_type FROM {$wpdb->prefix}exam_questions ORDER BY id DESC");
foreach ($questions as $q) {
$type_labels = Exam_Question::$question_types;
$type = isset($type_labels[$q->question_type]) ? $type_labels[$q->question_type] : $q->question_type;
echo '<label style="display: block; margin-bottom: 5px;">';
echo '<input type="checkbox" name="selected_questions[]" value="' . $q->id . '"> ';
echo wp_trim_words(strip_tags($q->title), 10) . ' <small>(' . $type . ')</small>';
echo '</label>';
}
?>
</div>
</td>
</tr>
</tbody>
<tbody id="auto-selection" style="display:none;">
<tr>
<th><label>按分类组卷</label></th>
<td>
<div id="category-rules">
<div class="rule-row">
<select name="category_rules[0][category_id]" class="category-select">
<option value="0">所有分类</option>
<?php foreach($categories as $cat): ?>
<option value="<?php echo $cat->category_id; ?>">分类 <?php echo $cat->category_id; ?></option>
<?php endforeach; ?>
</select>
<select name="category_rules[0][question_type]">
<option value="all">所有题型</option>
<?php foreach(Exam_Question::$question_types as $key => $label): ?>
<option value="<?php echo $key; ?>"><?php echo $label; ?></option>
<?php endforeach; ?>
</select>
<input type="number" name="category_rules[0][count]" min="1" max="50" value="5" style="width: 60px;"> 题
<select name="category_rules[0][difficulty]">
<option value="0">所有难度</option>
<option value="1">简单</option>
<option value="2">中等</option>
<option value="3">困难</option>
</select>
<button type="button" class="button remove-rule">删除</button>
</div>
</div>
<button type="button" id="add-rule" class="button">添加规则</button>
</td>
</tr>
</tbody>
<tr>
<th><label for="time_limit">考试时间(分钟)</label></th>
<td><input type="number" id="time_limit" name="time_limit" min="1" max="300" value="60"></td>
</tr>
<tr>
<th><label for="passing_score">及格分数</label></th>
<td><input type="number" id="passing_score" name="passing_score" min="0" max="100" value="60"></td>
</tr>
<tr>
<th><label for="attempts_allowed">考试次数限制</label></th>
<td><input type="number" id="attempts_allowed" name="attempts_allowed" min="0" value="0"> (0表示无限制)</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary">生成试卷</button>
</p>
</form>
<div id="generated-paper" style="display:none; margin-top: 30px; padding: 20px; background: #f5f5f5; border: 1px solid #ddd;">
<h3>试卷生成成功!</h3>
<div id="paper-preview"></div>
<p>
<button id="save-paper" class="button button-primary">保存试卷</button>
<button id="regenerate-paper" class="button">重新生成</button>
</p>
</div>
</div>
<script>
jQuery(document).ready(function($) {
// 切换组卷方式
$('input[name="generate_type"]').change(function() {
if ($(this).val() === 'manual') {
$('#manual-selection').show();
$('#auto-selection').hide();
} else {
$('#manual-selection').hide();
$('#auto-selection').show();
}
});
// 添加规则行
var ruleIndex = 1;
$('#add-rule').click(function() {
var newRow = $('.rule-row:first').clone();
newRow.find('select, input').each(function() {
var name = $(this).attr('name');
$(this).attr('name', name.replace('[0]', '[' + ruleIndex + ']'));
});
newRow.appendTo('#category-rules');
ruleIndex++;
});
// 删除规则行
$(document).on('click', '.remove-rule', function() {
if ($('.rule-row').length > 1) {
$(this).closest('.rule-row').remove();
}
});
// 提交生成试卷
$('#paper-generator-form').submit(function(e) {
e.preventDefault();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'generate_paper',
nonce: '<?php echo wp_create_nonce("exam_nonce"); ?>',
form_data: $(this).serialize()
},
success: function(response) {
if (response.success) {
$('#paper-preview').html(response.data.preview);
$('#generated-paper').show().data('paper-data', response.data.paper_data);
$('html, body').animate({
scrollTop: $('#generated-paper').offset().top
}, 500);
} else {
alert('生成失败:' + response.data);
}
}
});
});
// 保存试卷
$('#save-paper').click(function() {
var paperData = $('#generated-paper').data('paper-data');
paperData.paper_name = $('#paper_name').val();
paperData.time_limit = $('#time_limit').val();
paperData.passing_score = $('#passing_score').val();
paperData.attempts_allowed = $('#attempts_allowed').val();
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'save_paper',
nonce: '<?php echo wp_create_nonce("exam_nonce"); ?>',
paper_data: paperData
},
success: function(response) {
if (response.success) {
alert('试卷保存成功!');
window.location.href = '<?php echo admin_url("admin.php?page=exam-papers"); ?>';
} else {
alert('保存失败:' + response.data);
}
}
});
});
});
</script>
<?php
}
// AJAX生成试卷
public static function ajax_generate_paper() {
check_ajax_referer('exam_nonce', 'nonce');
global $wpdb;
parse_str($_POST['form_data'], $form_data);
$question_ids = array();
if ($form_data['generate_type'] === 'manual') {
// 手动选题
if (!empty($form_data['selected_questions'])) {
$question_ids = array_map('intval', $form_data['selected_questions']);
}
} else {
// 自动组卷
foreach ($form_data['category_rules'] as $rule) {
$where = array('1=1');
$params = array();
if (!empty($rule['category_id']) && $rule['category_id'] != '0') {
$where[] = 'category_id = %d';
$params[] = intval($rule['category_id']);
}
if (!empty($rule['question_type']) && $rule['question_type'] != 'all') {
$where[] = 'question_type = %s';
$params[] = sanitize_text_field($rule['question_type']);
}
if (!empty($rule['difficulty']) && $rule['difficulty'] != '0') {
$where[] = 'difficulty = %d';
$params[] = intval($rule['difficulty']);
}
$sql = "SELECT id FROM {$wpdb->prefix}exam_questions WHERE " . implode(' AND ', $where) . " ORDER BY RAND() LIMIT %d";
$params[] = intval($rule['count']);
if (!empty($params)) {
$results = $wpdb->get_col($wpdb->prepare($sql, $params));
$question_ids = array_merge($question_ids, $results);
}
}
}
if (empty($question_ids)) {
wp_send_json_error('没有选择任何试题');
}
// 生成预览
$preview = '<h4>试卷包含 ' . count($question_ids) . ' 道试题:</h4><ol>';
foreach ($question_ids as $qid) {
$question = $wpdb->get_row($wpdb->prepare("SELECT title, question_type FROM {$wpdb->prefix}exam_questions WHERE id = %d", $qid));
if ($question) {
$type_labels = Exam_Question::$question_types;
$type = isset($type_labels[$question->question_type]) ? $type_labels[$question->question_type] : $question->question_type;
$preview .= '<li>' . wp_trim_words(strip_tags($question->title), 15) . ' <em>(' . $type . ')</em></li>';
}
}
$preview .= '</ol>';
wp_send_json_success(array(
'preview' => $preview,
'paper_data' => array(
'question_ids' => $question_ids,
'total_questions' => count($question_ids)
)
));
}
}
第四章:前端考试界面与交互实现
4.1 考试短代码与页面模板
短代码类 (includes/class-shortcodes.php):
class Exam_Shortcodes {
public static function init() {
// 短代码已在Exam_Manager中注册
add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_frontend_scripts'));
}
public static function enqueue_frontend_scripts() {
wp_enqueue_style('exam-frontend', EXAM_SYSTEM_URL . 'assets/css/frontend.css', array(), EXAM_SYSTEM_VERSION);
wp_enqueue_script('exam-frontend', EXAM_SYSTEM_URL . 'assets/js/frontend.js', array('jquery'), EXAM_SYSTEM_VERSION, true);
wp_localize_script('exam-frontend', 'exam_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('exam_frontend_nonce')
));
}
// 考试列表短代码
public static function shortcode_exam_list($atts) {
if (!is_user_logged_in()) {
return '<div class="exam-notice">请先登录后参加考试</div>';
}
global $wpdb;
$papers = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}exam_papers WHERE status = 1 ORDER BY id DESC");
$output = '<div class="exam-list-container">';
$output .= '<h2>可用考试</h2>';
if (empty($papers)) {
$output .= '<p>暂无可用考试</p>';
} else {
$output .= '<div class="exam-grid">';
foreach ($papers as $paper) {
$settings = maybe_unserialize($paper->settings);
$attempts_allowed = isset($settings['attempts_allowed']) ? $settings['attempts_allowed'] : 0;
// 检查已考次数
$attempts = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d",
get_current_user_id(),
$paper->id
));
$can_take = ($attempts_allowed == 0 || $attempts < $attempts_allowed);
$output .= '<div class="exam-card">';
$output .= '<h3>' . esc_html($paper->paper_name) . '</h3>';
$output .= '<ul class="exam-meta">';
$output .= '<li>题数: ' . $paper->total_questions . '</li>';
$output .= '<li>时间: ' . $paper->time_limit . '分钟</li>';
$output .= '<li>及格分: ' . $paper->passing_score . '</li>';
$output .= '<li>已考次数: ' . $attempts . ($attempts_allowed > 0 ? '/' . $attempts_allowed : '') . '</li>';
$output .= '</ul>';
if ($can_take) {
$output .= '<a href="?exam_id=' . $paper->id . '" class="button start-exam-btn">开始考试</a>';
} else {
$output .= '<button class="button disabled" disabled>考试次数已用完</button>';
}
// 查看历史成绩
$best_score = $wpdb->get_var($wpdb->prepare(
"SELECT MAX(score) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d",
get_current_user_id(),
$paper->id
));
if ($best_score !== null) {
$output .= '<div class="best-score">最佳成绩: ' . floatval($best_score) . '分</div>';
}
$output .= '</div>';
}
$output .= '</div>';
}
$output .= '</div>';
return $output;
}
// 参加考试短代码
public static function shortcode_take_exam($atts) {
if (!is_user_logged_in()) {
return '<div class="exam-notice">请先登录后参加考试</div>';
}
if (!isset($_GET['exam_id'])) {
return '<div class="exam-notice">请选择要参加的考试</div>';
}
$paper_id = intval($_GET['exam_id']);
$user_id = get_current_user_id();
global $wpdb;
// 检查考试次数限制
$paper = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}exam_papers WHERE id = %d AND status = 1",
$paper_id
));
if (!$paper) {
return '<div class="exam-error">考试不存在或已关闭</div>';
}
$settings = maybe_unserialize($paper->settings);
$attempts_allowed = isset($settings['attempts_allowed']) ? $settings['attempts_allowed'] : 0;
$attempts = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d",
$user_id,
$paper_id
));
if ($attempts_allowed > 0 && $attempts >= $attempts_allowed) {
return '<div class="exam-error">您已达到该考试的最大尝试次数</div>';
}
// 检查是否有未完成的考试
$unfinished = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d AND end_time IS NULL",
$user_id,
$paper_id
));
if ($unfinished) {
// 恢复未完成的考试
$record_id = $unfinished->id;
$remaining_time = $paper->time_limit * 60 - (time() - strtotime($unfinished->start_time));
$remaining_time = max(0, $remaining_time);
} else {
// 创建新的考试记录
$wpdb->insert(
$wpdb->prefix . 'exam_records',
array(
'user_id' => $user_id,
'paper_id' => $paper_id,
'start_time' => current_time('mysql'),
'answers' => maybe_serialize(array())
)
);
$record_id = $wpdb->insert_id;
$remaining_time = $paper->time_limit * 60;
}
// 获取试题
$question_ids = maybe_unserialize($paper->question_ids);
if (empty($question_ids)) {
return '<div class="exam-error">试卷试题配置错误</div>';
}
$questions = array();
foreach ($question_ids as $qid) {
$question = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}exam_questions WHERE id = %d",
$qid
));
if ($question) {
$question->options = maybe_unserialize($question->options);
$questions[] = $question;
}
}
// 输出考试界面
