这个前后端分离的Web在线考试系统是我去年带队开发的一个教育类项目,目前已经在三所高校的实际教学中投入使用。系统采用SpringBoot+Vue的技术栈,完美解决了传统纸质考试效率低下、阅卷工作量大、成绩统计繁琐等痛点问题。
从技术架构来看,系统前端使用Vue.js实现响应式布局和组件化开发,后端基于SpringBoot提供RESTful API接口,数据持久层采用MyBatis框架,数据库选用MySQL 8.0。这种架构组合既保证了开发效率,又能满足高并发场景下的性能需求。特别值得一提的是,我们通过JWT(JSON Web Token)实现了无状态的认证机制,相比传统的Session方式更适应分布式部署场景。
选择SpringBoot作为后端框架主要基于以下几个考虑:
前端选用Vue.js而非React或Angular,主要因为:
系统采用典型的三层架构:
code复制表现层:Vue.js + Element UI
业务逻辑层:SpringBoot + Spring Security
数据访问层:MyBatis + MySQL
前后端通过RESTful API进行通信,接口设计遵循以下原则:
认证流程采用JWT方案,具体实现如下:
java复制// JWT工具类核心代码
public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final long EXPIRATION_TIME = 864_000_000; // 10天
public static String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public static Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
前端需要在axios拦截器中添加token:
javascript复制// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, error => {
return Promise.reject(error);
});
试题采用分类存储策略,不同类型试题在数据库中的存储方式有所不同:
json复制{
"options": [
{"id": 1, "content": "选项A"},
{"id": 2, "content": "选项B"}
]
}
code复制"answer": "Java|面向对象|跨平台"
code复制"answer": true
阅卷核心算法实现:
java复制public class GradingService {
public ExamResult autoGrade(ExamPaper paper, StudentAnswer answer) {
int totalScore = 0;
int correctCount = 0;
for (Question question : paper.getQuestions()) {
String stdAnswer = question.getAnswer();
String stuAnswer = answer.getAnswer(question.getId());
switch (question.getType()) {
case SINGLE_CHOICE:
case TRUE_FALSE:
if (stdAnswer.equals(stuAnswer)) {
totalScore += question.getScore();
correctCount++;
}
break;
case FILL_BLANK:
String[] stdParts = stdAnswer.split("\\|");
String[] stuParts = stuAnswer.split("\\|");
int blankScore = question.getScore() / stdParts.length;
for (int i = 0; i < stdParts.length; i++) {
if (i < stuParts.length && stdParts[i].equalsIgnoreCase(stuParts[i])) {
totalScore += blankScore;
correctCount++;
}
}
break;
}
}
return new ExamResult(totalScore, correctCount, paper.getQuestions().size());
}
}
用户表(user)设计考虑:
sql复制CREATE TABLE `user` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) NOT NULL,
`role_type` tinyint NOT NULL COMMENT '1学生 2教师 3管理员',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_login_time` datetime DEFAULT NULL,
PRIMARY KEY (`user_id`),
UNIQUE KEY `idx_username` (`username`),
UNIQUE KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
试卷查询的典型SQL优化案例:
sql复制-- 优化前(全表扫描)
SELECT * FROM question WHERE question_type = 1 ORDER BY RAND() LIMIT 20;
-- 优化后(利用索引覆盖)
SELECT q.* FROM question q
JOIN (
SELECT question_id FROM question
WHERE question_type = 1
ORDER BY RAND() LIMIT 20
) t ON q.question_id = t.question_id;
推荐使用Docker Compose进行容器化部署:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: exam_system
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/exam_system
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
SpringBoot Actuator监控端点配置:
properties复制# application.properties
management.endpoints.web.exposure.include=health,info,metrics,prometheus
management.metrics.export.prometheus.enabled=true
management.endpoint.health.show-details=always
配合Grafana仪表板可以实时监控:
开发初期遇到的典型跨域问题及最终解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.allowCredentials(false)
.maxAge(3600);
}
}
同时需要在Vue项目中配置代理:
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
为防止考试作弊,实现了以下控制策略:
实现代码片段:
javascript复制// 考试页面心跳检测
let leaveCount = 0;
window.addEventListener('blur', () => {
leaveCount++;
if (leaveCount > 3) {
this.$confirm('您已多次离开考试页面,系统将自动交卷', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.submitExam();
});
}
});
// 定时保存答案
setInterval(() => {
this.saveAnswers();
}, 30000);
基于现有系统的可扩展功能:
编程题评测的架构设想:
code复制用户提交代码 → 代码沙箱执行 → 测试用例验证 → 返回评测结果
其中代码沙箱可以采用以下安全措施:
这个考试系统从最初版本到现在已经迭代了5个主要版本,最大的体会是教育类系统的开发不仅要考虑技术实现,更要深入理解教学场景的实际需求。比如我们最初设计的自动组卷算法虽然技术很先进,但实际使用中发现教师更希望能手动调整组卷结果,于是后来增加了"智能推荐+人工调整"的混合模式,这才真正得到了用户的认可。