当前位置: 首页 > news >正文

软件工程课程作业:基于原生技术栈的简易在线考试系统全栈开发实践

作者: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)中。

image

image

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复合索引,为后续学生端的“随机组卷”功能提供了底层查询速度保障。

第三章:教师端全景看板与业务闭环实现

教师端的核心诉求是“掌控力”。我们需要提供宏观的数据看板,以及细颗粒度的题目增删查改。

image

3.1前端攻坚:ECharts生命周期塌陷Bug的彻底解法

在2452114同学开发数据看板时,遇到了经典的前端渲染Bug。由于系统采用单页面(SPA)模拟架构,未激活的Tab面板处于display:none状态。当ECharts在隐藏元素中初始化时,由于获取不到DOM的实际宽高,会导致渲染出来的图表宽度仅有100px(俗称“幽灵图表”)。

解决原理:我们没有引入庞大的Vue生命周期钩子,而是利用JavaScript的事件循环(EventLoop)机制,配合宏任务(MacroTask)与ResizeObserver完美解决了这个问题。

image

点击折叠/展开:原生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实现了极简的聚合计算。

image

image

点击折叠/展开:SpringBoot组卷业务实现代码
@RestController
@RequestMapping("/api/papers")
public class PaperController {@Autowiredprivate PaperRepository paperRepository;@Autowiredprivate 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的批量高亮交互。结合后端的批量删除接口,做到了如丝般顺滑的体验。

image

image

点击折叠/展开:基于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自定义随机训练算法

为了满足学生自主刷题的需求,系统允许学生指定题目数量与限时。这就要求后端具备真正的“随机抽题”能力。

点击折叠/展开: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);
}

4.2极速判卷引擎与动态错题本系统

这是本系统最引以为傲的“闭环体系”。交卷瞬间,系统前端立即冻结交互,后端高速比对标准答案。

不仅如此,判卷引擎在发现错题时,会异步将题目ID写入mistake_records表。学生在后续可以在错题本中重刷这些题目,一旦答对,点击“移除”,真正实现了错题本的动态瘦身

点击折叠/展开:判分引擎与错题本联动核心代码
    @Servicepublic class ExamEngineService {@Autowiredprivate QuestionRepository questionRepository;@Autowiredprivate MistakeRecordRepository mistakeRepository;/*** 核心算法:实时判卷并更新错题本* @param userId 答题学生ID* @param userAnswers 学生提交的答案Map (Key: 题目ID, Value: 学生选择)*/@Transactionalpublic 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> {@Overridepublic 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需求草图,到最终跑通整个考试流向的工程交付,这段全栈开发之旅让我们对软件工程有了全新的认知。

项目核心复盘

  1. 前后端解耦的红利:2452120同学专注于后端的RESTful接口防腐,2452114同学专注于前端渲染层,双方基于统一的JSON契约进行通信,极大缩短了调试时间。
  2. 拒绝过度设计的克制:我们没有引入Redis,而是榨干了MySQL与原生JS的性能,证明了在并发量不夸张的高校场景下,“原生轻量化”才是最优雅的解法。

未来迭代展望

在这个1.0版本跑通后,如果我们要将其商业化或推广至全校使用,接下来的架构演进方向将是:

  1. 缓存层引入:利用Redis缓存热门试卷结构,抵御开考瞬间的高并发查询。
  2. 实时反作弊通道:基于WebSocket建立全双工通信,教师端能够实时监控学生的切屏行为与答题进度。
  3. AI智能命题:接入大模型API,实现通过上传课件PDF一键抽取知识点并自动生成客观题矩阵。
http://www.jsqmd.com/news/667671/

相关文章:

  • 实战指南:利用Application Verifier与WinDbg精准捕获Windows应用内存泄漏与堆损坏
  • 深入ZYNQ数据通路:AXI DMA如何成为PS与PL之间的‘高速公路’?
  • LaTeX表格总是不听话?用[h]参数让它乖乖待在原地(附完整代码示例)
  • 【AI面试八股文 Vol.1.1 | 专题3:State Schema 设计】State Schema设计:TypedDict / Pydantic类型约束
  • 从GL_INVALID_FRAMEBUFFER到内存溢出:OpenGL ES移动端开发中glGetError的7个典型错误排查实录
  • FPGA系统健康守护者:深入解读Xilinx SYSMON的报警机制与电源管理实战
  • ROS2导航实战:从TF_OLD_DATA警告到Gazebo插件配置的避坑指南
  • AMD锐龙笔记本用VMware装macOS避坑指南:拯救者R7 4800H + Win11实测
  • 用程序员思维理解GLM:当统计学遇上面向对象编程
  • Nginx 0day漏洞应急响应:两种升级策略的实战对比与选择
  • HS2-HF_Patch:Honey Select 2终极汉化与优化补丁完整指南
  • 2、IntelliJ IDEA 之下载与安装
  • Barrier终极指南:一套键鼠控制Windows、macOS、Linux三系统,免费开源KVM软件让你效率翻倍![特殊字符]
  • OpenMV传感器配置避坑指南:从sensor.reset()到find_blobs()的完整流程
  • RT-Thread SPI Flash驱动调试避坑指南:从ENV配置到CubeMX引脚,解决‘unknown flash’错误
  • 汇编语言从零到一:手把手构建你的第一个可执行程序
  • 手把手教你用ROS camera_calibration完成工业相机内参标定
  • Android JNI开发避坑:手把手教你定位并解决SIGABRT信号导致的Native崩溃
  • RTK差分定位实战:如何配置RTKLIB连接香港CORS的NTRIP服务获取实时数据流
  • 保护公司核心测试资产:CANoe CAPL脚本的3种加密方法与硬件绑定实战指南
  • 从零到一:HuggingFace生态全景与实战入门指南
  • 别再死记硬背CNN和RNN了!聊聊‘归纳偏置’这个让模型变聪明的‘潜规则’
  • 华硕枪神6/6Plus超竞版 G733C 原厂Win11 21H2系统-宇程系统站
  • DDR4内存初始化全流程解析:从复位到预充电的底层细节
  • 为什么93%的数学家还没用上AGI工具?,SITS2026披露阻碍落地的5个认知盲区与迁移路线图
  • F3D三维查看器:为什么这款轻量级工具正在颠覆3D预览体验?
  • 从一次‘背锅’经历讲起:我是如何用VRRP+静态路由搞定小型企业网络冗余的
  • 如何全面修复Windows运行时问题:专业级Visual C++ Redistributable系统优化方案
  • 华硕枪神6/6plus G533Z G733Z 原厂Win11 21H2系统-宇程系统站
  • 从字符流到语义单元:深入理解编译原理中的Token化过程