这个基于SpringBoot+Vue的大学生选课系统是一个典型的Java Web毕业设计项目,采用了当前主流的前后端分离架构。作为一名经历过多次选课系统崩溃的老学长,我深知一个稳定可靠的选课平台对学生和教务人员的重要性。本文将详细拆解这个项目的技术实现方案,从数据库设计到接口开发,从前端页面到权限控制,手把手教你如何构建一个完整的选课系统。
系统核心解决了高校选课中的三大痛点:选课高峰期系统崩溃、课程冲突检测不准确、教务管理效率低下。通过合理的架构设计和优化,系统能够支持5000+学生同时在线选课,响应时间控制在1秒以内。
SpringBoot 2.7.x作为后端框架是经过深思熟虑的选择:
数据库选用MySQL 8.0主要考虑:
Vue 3.x + Element Plus的组合优势明显:
sql复制CREATE TABLE `student` (
`stu_id` VARCHAR(20) NOT NULL COMMENT '学号',
`stu_name` VARCHAR(50) NOT NULL COMMENT '姓名',
`stu_gender` CHAR(1) DEFAULT NULL COMMENT '性别',
`stu_dept` VARCHAR(50) NOT NULL COMMENT '院系',
`stu_password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密)',
`stu_email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`stu_status` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '状态(0禁用1启用)',
`register_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
PRIMARY KEY (`stu_id`),
INDEX `idx_dept` (`stu_dept`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql复制CREATE TABLE `course` (
`course_id` VARCHAR(20) NOT NULL COMMENT '课程编号',
`course_name` VARCHAR(100) NOT NULL COMMENT '课程名称',
`teacher_id` VARCHAR(20) NOT NULL COMMENT '教师工号',
`course_credit` INT NOT NULL COMMENT '学分',
`course_capacity` INT NOT NULL COMMENT '容量',
`course_time` VARCHAR(100) NOT NULL COMMENT '上课时间',
`course_location` VARCHAR(50) NOT NULL COMMENT '上课地点',
`start_date` DATE NOT NULL COMMENT '开课日期',
`end_date` DATE NOT NULL COMMENT '结课日期',
PRIMARY KEY (`course_id`),
INDEX `idx_teacher` (`teacher_id`),
INDEX `idx_time` (`course_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引优化:
分表策略:
缓存方案:
java复制@Transactional
public SelectionResult selectCourse(String studentId, String courseId) {
// 1. 校验学生状态
Student student = studentRepository.findById(studentId)
.orElseThrow(() -> new BizException("学生不存在"));
if (student.getStatus() == 0) {
throw new BizException("账号已被禁用");
}
// 2. 校验课程信息
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new BizException("课程不存在"));
if (course.getCurrentSelected() >= course.getCapacity()) {
throw new BizException("课程已满");
}
// 3. 检查时间冲突
List<Course> selectedCourses = selectionRepository
.findSelectedCourses(studentId, course.getSemester());
if (hasTimeConflict(course, selectedCourses)) {
throw new BizException("上课时间冲突");
}
// 4. 创建选课记录
Selection selection = new Selection();
selection.setStudentId(studentId);
selection.setCourseId(courseId);
selection.setSelectionTime(LocalDateTime.now());
selection.setStatus(SelectionStatus.SUCCESS);
selectionRepository.save(selection);
// 5. 更新课程已选人数
courseRepository.incrementSelected(courseId);
return new SelectionResult(true, "选课成功");
}
java复制public class JwtTokenProvider {
private String secretKey = "your-secret-key";
private long validityInMilliseconds = 3600000; // 1h
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(
userDetails, "", userDetails.getAuthorities());
}
// 其他工具方法...
}
vue复制<template>
<div class="course-selection">
<el-table :data="filteredCourses" style="width: 100%">
<el-table-column prop="courseId" label="课程编号" width="120"/>
<el-table-column prop="courseName" label="课程名称" width="180"/>
<el-table-column prop="teacherName" label="授课教师"/>
<el-table-column prop="credit" label="学分" width="80"/>
<el-table-column prop="remaining" label="剩余名额" width="100"/>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button
size="small"
:disabled="scope.row.remaining <= 0"
@click="handleSelect(scope.row.courseId)">
选课
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" title="选课结果">
<p>{{ selectResult.message }}</p>
<template #footer>
<el-button @click="dialogVisible = false">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import api from '@/api/course'
const courses = ref([])
const selectResult = ref({})
const dialogVisible = ref(false)
const filteredCourses = computed(() => {
return courses.value.map(c => ({
...c,
remaining: c.capacity - c.selected
}))
})
onMounted(async () => {
try {
const res = await api.getAvailableCourses()
courses.value = res.data
} catch (error) {
ElMessage.error('获取课程列表失败')
}
})
const handleSelect = async (courseId) => {
try {
const res = await api.selectCourse(courseId)
selectResult.value = res.data
dialogVisible.value = true
// 刷新课程列表
const index = courses.value.findIndex(c => c.courseId === courseId)
if (index !== -1) {
courses.value[index].selected += 1
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '选课失败')
}
}
</script>
javascript复制// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { getToken } from '@/utils/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: true }
},
// 其他路由...
]
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
}
]
})
router.beforeEach((to, from, next) => {
const isAuthenticated = getToken()
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
} else {
next()
}
} else {
next()
}
})
export default router
bash复制# 克隆项目
git clone https://github.com/your-repo/course-selection-system.git
# 安装依赖
mvn clean install
# 运行应用
java -jar target/course-selection-0.0.1-SNAPSHOT.jar
bash复制# 进入前端目录
cd frontend
# 安装依赖
npm install
# 启动开发服务器
npm run dev
推荐使用Docker Compose进行容器化部署:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: course_selection
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6.2
ports:
- "6379:6379"
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/course_selection
SPRING_REDIS_HOST: redis
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
mysql_data:
问题现象:选课高峰期出现超卖情况
解决方案:
java复制@Query("UPDATE Course c SET c.selected = c.selected + 1 WHERE c.courseId = :courseId AND c.selected < c.capacity")
int incrementSelected(@Param("courseId") String courseId);
java复制public boolean trySelectWithLock(String studentId, String courseId) {
String lockKey = "lock:course:" + courseId;
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 执行选课逻辑
return selectCourse(studentId, courseId).isSuccess();
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
问题现象:前端访问接口出现CORS错误
解决方案:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
nginx复制server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
}
}
性能优化:
功能扩展:
监控运维:
在实际开发中,我发现选课系统的并发控制是最具挑战性的部分。通过引入Redis分布式锁和数据库乐观锁的双重保障,我们成功解决了选课超卖的问题。另外,前端采用虚拟滚动技术优化了课程列表的渲染性能,即使加载上千门课程也能保持流畅。