软件工程课程作业:基于原生技术栈的简易在线考试系统全栈开发实践
作者:2452120、2452114
项目定位:基于JavaSpringBoot与原生交互设计的轻量化全栈教育平台
第一章:引言
在软件工程专业的学习过程中,我们接触并使用了大量的大型教务系统与在线考试平台。不可否认它们功能强大,但往往伴随着令人头疼的通病:前端组件库过度堆砌导致的极慢加载速度、极其复杂的中间件依赖以及高昂的学习与部署成本。
本项目——简易在线考试系统(OnlineExamSystem)的诞生,源于我们对“轻量化”与“高内聚”的追求。我们决定不依赖Vue/React等重量级前端框架,也不引入MyBatis-Plus的过度封装。2452120同学主导了底层数据库的精细化建模与SpringBoot后端核心业务流的构建;2452114同学则利用原生JavaScript与CSS状态机,徒手搭建了媲美单页面应用(SPA)的流畅交互体验,并攻克了ECharts图表在复杂DOM树中的渲染塌陷难题。
第二章:系统底层架构与数据库设计剖析
一个健壮的系统必须建立在严谨的数据库之上。2452120同学在系统初期,围绕“双角色(Teacher/Student)”与“核心业务流(题库-试卷-错题本)”构建了高内聚的E-R模型。
2.1双角色权限隔离(DualRoleSystem)
系统严格区分了教师与学生权限,这种隔离不仅体现在前端的路由分发上,更深植于后端的拦截器(Interceptor)中。
登录界面布局结构:
标题行:醒目的“简易在线考试系统”系统标识。
输入行1:学号/教工号输入框,内置防抖动的正则校验提示。
输入行2:密码输入框(密文显示)。
操作行:包含“登录”主按钮与“点击注册”文字超链接。


2.2核心表结构设计(SchemaDesign)
为了实现“动态随机组卷”与“错题本动态瘦身”,我们抛弃了将整张试卷作为JSON大字段存储的取巧方案,而是采用了正规的关系型表结构。以下是核心数据表的建表SQL,展示了我们的外键约束与索引设计思考。
点击折叠/展开:系统核心数据库建表SQL(含优化索引)
-- 1. 用户表:存储双角色信息
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名/学号',
password VARCHAR(255) NOT NULL COMMENT '明文密码(简化版)',
role ENUM('TEACHER', 'STUDENT') NOT NULL COMMENT '双角色隔离',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 题库表:支持三种题型与难度梯度,为随机查询建立复合索引
CREATE TABLE questions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(20) NOT NULL COMMENT '题型: SINGLE, MULTIPLE, JUDGE',
content TEXT NOT NULL COMMENT '题干富文本',
options JSON COMMENT '选项列表,利用MySQL8.0的JSON特性',
correct_answer VARCHAR(255) NOT NULL,
score INT NOT NULL DEFAULT 5,
difficulty VARCHAR(20) DEFAULT 'MEDIUM' COMMENT '难度: EASY, MEDIUM, HARD',
knowledge_point VARCHAR(100) COMMENT '知识点标签,用于ECharts图表统计',
INDEX idx_type_diff (type, difficulty) -- 优化随机组卷的查询速度
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 试卷主表
CREATE TABLE papers (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
total_score INT NOT NULL,
time_limit INT NOT NULL COMMENT '考试限时(分钟)',
creator_id BIGINT NOT NULL COMMENT '出卷教师ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 试卷-题目中间表(多对多)
CREATE TABLE paper_questions (
paper_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
question_order INT NOT NULL COMMENT '题目在试卷中的序号',
PRIMARY KEY (paper_id, question_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 5. 动态错题本表
CREATE TABLE mistake_records (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
question_id BIGINT NOT NULL,
error_count INT DEFAULT 1 COMMENT '错题累计次数',
last_error_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_question (user_id, question_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计思考:在questions表中,我们将选项设计为JSON类型,这极大方便了前端原生JS的解析,同时建立idx_type_diff复合索引,为后续学生端的“随机组卷”功能提供了底层查询速度保障。
第三章:教师端全景看板与业务闭环实现
宏观数据看板作为教师登录后的默认首页,我们将死板的数据转化为直观的图形。
主体图表行(ECharts渲染区):
左半区(饼图):展示“知识点分布占比”与“题型比例(单选/多选/判断)”。教师可借此判断题库建设是否偏科。
右半区(柱状图):展示“题目难度梯度分布(易/中/难)”,帮助教师把控整体考核难度。

题库管理矩阵是教师使用频率最高的模块,我们将其打造成了类似Excel的沉浸式数据表。
顶部操作工具条(Toolbar):
右侧放置“+录入新题”主色调按钮与“批量删除”按钮。
数据矩阵表(核心骨架):
表头行(共7列):[序号] | [题型] | [知识点标签] | [难度级别] | [题干截断预览] | [分值] | [操作]。
行内操作按钮:每行的最后一列提供微型的“编辑”与“删除”图标按钮,支持单点精准操作。
丝滑交互逻辑:2452114同学利用CSS伪类:checked实现了点击行首复选框,整行背景立刻变为浅蓝色并带有位移动画的高亮效果。无需JS干预,性能极佳。



3.1前端攻坚:ECharts生命周期塌陷Bug的彻底解法
在2452114同学开发数据看板时,遇到了经典的前端渲染Bug。由于系统采用单页面(SPA)模拟架构,未激活的Tab面板处于display:none状态。当ECharts在隐藏元素中初始化时,由于获取不到DOM的实际宽高,会导致渲染出来的图表宽度仅有100px(俗称“幽灵图表”)。
解决原理:我们没有引入庞大的Vue生命周期钩子,而是利用JavaScript的事件循环(EventLoop)机制,配合宏任务(MacroTask)与ResizeObserver完美解决了这个问题。
点击折叠/展开:原生JS防塌陷图表渲染代码
const DashboardManager = {
charts: {},
initCharts: function() {
const pieDom = document.getElementById('knowledgePieChart');
const barDom = document.getElementById('difficultyBarChart');
// 销毁旧实例防止内存泄漏
if(this.charts.pie) this.charts.pie.dispose();
if(this.charts.bar) this.charts.bar.dispose();
this.charts.pie = echarts.init(pieDom);
this.charts.bar = echarts.init(barDom);
// 模拟从后端拉取的全量看板数据
const pieOption = {
title: { text: '知识点分布占比', left: 'center' },
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: '60%',
data: [
{value: 35, name: '数据结构'},
{value: 20, name: '计算机网络'},
{value: 45, name: '操作系统'}
],
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.5)' } }
}]
};
this.charts.pie.setOption(pieOption);
// ...柱状图配置省略
},
// Tab切换拦截器
switchTab: function(tabId) {
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
document.getElementById(tabId).style.display = 'block';
if (tabId === 'dashboardTab') {
// 核心修复逻辑:宏任务延迟,等待浏览器CSSOM树计算完成
setTimeout(() => {
this.initCharts();
// 强制重绘
this.charts.pie.resize();
this.charts.bar.resize();
}, 50);
}
}
};
// 监听窗口大小变化实现真正的自适应
window.addEventListener('resize', () => {
if(document.getElementById('dashboardTab').style.display === 'block'){
DashboardManager.charts.pie?.resize();
DashboardManager.charts.bar?.resize();
}
});
3.2智能组卷与动态总分计算(后端逻辑)
教师在题库中勾选多道题目后,系统需要自动计算总分并生成试卷记录。2452120同学在后端利用Java8的StreamAPI实现了极简的聚合计算。
配置区:包含两个输入框——“试卷名称”与“考试限时(分钟)”。
底部右侧:固定在底部的“取消”与“确认并发布试卷”按钮。


点击折叠/展开:SpringBoot组卷业务实现代码
@RestController
@RequestMapping("/api/papers")
public class PaperController {
@Autowired
private PaperRepository paperRepository;
@Autowired
private QuestionRepository questionRepository;
/**
* 核心业务:接收教师勾选的题目ID,生成完整试卷并计算总分
*/
@Transactional(rollbackFor = Exception.class)
@PostMapping("/generate")
public Result<Paper> generatePaper(@RequestBody PaperDto dto) {
// 1. 根据传入的ID列表批量查询题目实体
List<Question> selectedQuestions = questionRepository.findAllById(dto.getQuestionIds());
if (selectedQuestions.isEmpty()) {
return Result.error("题库中未找到对应的题目资源!");
}
// 2. 利用Stream流式编程,极速规约(Reduce)计算总分
int autoCalculatedTotalScore = selectedQuestions.stream()
.mapToInt(Question::getScore)
.sum();
// 3. 构建试卷元数据
Paper newPaper = new Paper();
newPaper.setTitle(dto.getTitle());
newPaper.setTimeLimit(dto.getTimeLimit());
newPaper.setCreatorId(dto.getTeacherId());
newPaper.setTotalScore(autoCalculatedTotalScore);
// 4. 保存主表并级联保存多对多中间表(JPA实现省略)
Paper savedPaper = paperRepository.save(newPaper);
return Result.success("试卷生成成功!总分:" + autoCalculatedTotalScore, savedPaper);
}
}
3.3交互巧思:原生CSS状态机实现批量删除
在题目管理模块,2452114同学没有使用耗费性能的JS去逐个监听复选框状态,而是巧用CSS伪类:checked实现了类似Excel的批量高亮交互。结合后端的批量删除接口,做到了如丝般顺滑的体验。


点击折叠/展开:基于CSS状态机的高性能批量交互方案
/* CSS状态机核心思路:摒弃JS,纯靠CSS控制选中态UI */
/* 隐藏原生复选框,但保留其状态功能 */
.question-row input[type="checkbox"] {
display: none;
}
/* 默认行样式 */
.question-row {
background-color: #ffffff;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
border-left: 4px solid transparent;
}
/* 状态机触发:当复选框被选中时,利用兄弟选择器(+)精准改变整行样式 */
.question-row input[type="checkbox"]:checked + .row-content {
background-color: #f0f7ff; /* 浅蓝色选中背景 */
border-left: 4px solid #1890ff; /* 左侧高亮指示条 */
transform: translateX(5px); /* 微弱的位移动画提升打击感 */
box-shadow: 0 4px 12px rgba(24,144,255,0.1);
}
第四章:学生端闭环学习引擎与核心算法
如果说教师端是“管理中心”,那么学生端就是真正的“核心算力区”。2452120同学为学生端设计了严密的考试逻辑闭环。
列表结构:展示已生成的套卷,包含列为[试卷编号] | [试卷名称] | [限时]| [总分] | [操作] 。


4.1自定义随机训练算法
为了满足学生自主刷题的需求,系统允许学生指定题目数量与限时。这就要求后端具备真正的“随机抽题”能力。
参数配置弹窗:弹窗包含——“抽取数量”、“考试用时”。
点击开始后,触发后端的ORDER BY RAND() LIMIT ?机制,毫秒级生成专属练习卷。
点击折叠/展开:MySQL随机抽题算法考量与代码
/**
* Repository层核心接口
* 思考:对于万级以下的中小型题库,ORDER BY RAND()是最优雅的原生方案。
* 如果未来数据量达到百万级,我们会将这部分逻辑迁移至Redis并通过离散ID哈希算法实现。
*/
public interface QuestionRepository extends JpaRepository<Question, Long> {
@Query(nativeQuery = true,
value = "SELECT * FROM questions WHERE type = :type ORDER BY RAND() LIMIT :limit")
List<Question> findRandomQuestionsByType(
@Param("type") String type,
@Param("limit") int limit
);
}

沉浸式答题视图
顶部悬浮区(Fixed Header):巨大醒目的倒计时模块。
中央答题区:每道题作为一个独立卡片。
第一行:题目序号、题型标签、分值、粗体题干。
后续行:纵向排列的A、B、C、D选项(原生<label>包裹<input type="radio/checkbox">,实现点击整行均可选中,扩大点击热区)。
- 底部操作区:悬浮的“交卷并查看成绩”按钮。

4.2极速判卷引擎与动态错题本系统
这是2452120同学设计的完美闭环体系。交卷瞬间完成以下视图的转换:
顶部出分通告:弹出横幅“XX总得分/满分:XX分”。
错题高亮机制:答错的选项底色被渲染为刺眼的红色,而正确的选项则高亮为绿色,并附带详细答案解析。
错题本自动化:所有判错的题目ID会被系统异步写入mistake_records数据表。
动态瘦身逻辑:在独立的“我的错题本”页面,学生重做错题一旦正确,系统会提供一个“移除错题”按钮,彻底清空该记录。



点击折叠/展开:判分引擎与错题本联动核心代码
@Service
public class ExamEngineService {
@Autowired
private QuestionRepository questionRepository;
@Autowired
private MistakeRecordRepository mistakeRepository;
/**
* 核心算法:实时判卷并更新错题本
* @param userId 答题学生ID
* @param userAnswers 学生提交的答案Map (Key: 题目ID, Value: 学生选择)
*/
@Transactional
public ExamResult evaluateAndRecord(Long userId, Map<Long, String> userAnswers) {
int finalScore = 0;
List<Long> currentMistakeIds = new ArrayList<>();
// 1. 获取本次涉及的所有题目实体
List<Question> questions = questionRepository.findAllById(userAnswers.keySet());
for (Question q : questions) {
String studentChoice = userAnswers.get(q.getId());
// 2. 严格比对:考虑大小写与多选题的排序问题
if (q.getCorrectAnswer().equalsIgnoreCase(studentChoice)) {
finalScore += q.getScore();
} else {
// 3. 记录错题流水线
currentMistakeIds.add(q.getId());
recordMistake(userId, q.getId());
}
}
return new ExamResult(finalScore, currentMistakeIds);
}
// 辅助方法:动态错题本的插入或更新(利用了MySQL的ON DUPLICATE KEY特性思维)
private void recordMistake(Long userId, Long questionId) {
MistakeRecord record = mistakeRepository.findByUserIdAndQuestionId(userId, questionId);
if (record == null) {
record = new MistakeRecord();
record.setUserId(userId);
record.setQuestionId(questionId);
record.setErrorCount(1);
} else {
record.setErrorCount(record.getErrorCount() + 1);
}
mistakeRepository.save(record);
}
}
第五章:工程化部署——脱机打包与局域网联机体验
作为一款实用型工具,我们考虑到了最严苛的校园网络环境(如无公网环境的实验室机房)。2452114同学利用Maven生命周期插件,将静态HTML/CSS/JS直接打包进SpringBoot的resources/static目录下。
通过Java获取本机的局域网IPv4地址,只需一台电脑运行java -jar exam-system.jar,全班同学连接同一路由器即可通过局域网IP直接访问该系统进行集中考试,彻底摆脱了复杂的Tomcat与Nginx配置。
点击折叠/展开:局域网联机部署与Maven构建配置
<!-- pom.xml中的核心构建逻辑 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 将所有依赖以及前端静态资源打入一个Fat JAR包中 -->
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
// SpringBoot启动完成回调:自动打印局域网IP,方便学生扫码或输入网址接入
@Component
public class NetworkStartupLogger implements ApplicationListener<WebServerInitializedEvent> {
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
try {
String hostAddress = InetAddress.getLocalHost().getHostAddress();
int port = event.getWebServer().getPort();
System.out.println("==================================================");
System.out.println("考试系统已启动,脱机局域网联机地址:");
System.out.println("http://" + hostAddress + ":" + port);
System.out.println("==================================================");
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
第六章:总结与反思
从最初杂乱无章的对话.txt需求草图,到最终跑通整个考试流向的工程交付,这段全栈开发之旅让我们对软件工程有了全新的认知。
项目核心复盘:
- 前后端解耦的红利:2452120同学专注于后端的RESTful接口防腐,2452114同学专注于前端渲染层,双方基于统一的JSON契约进行通信,极大缩短了调试时间。
- 拒绝过度设计的克制:我们没有引入Redis,而是榨干了MySQL与原生JS的性能,证明了在并发量不夸张的高校场景下,“原生轻量化”才是最优雅的解法。
未来迭代展望:
在这个1.0版本跑通后,如果我们要将其商业化或推广至全校使用,接下来的架构演进方向将是:
- 缓存层引入:利用Redis缓存热门试卷结构,抵御开考瞬间的高并发查询。
- 实时反作弊通道:基于WebSocket建立全双工通信,教师端能够实时监控学生的切屏行为与答题进度。
- AI智能命题:接入大模型API,实现通过上传课件PDF一键抽取知识点并自动生成客观题矩阵。

浙公网安备 33010602011771号