这个基于SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0技术栈的选课系统,是我在指导大学生毕业设计时反复打磨的一个教学案例。不同于简单的CRUD示例,它完整模拟了高校教务管理中选课业务的真实场景,包含学生选课、教师开课、管理员排课等核心功能模块。
系统采用前后端分离架构,后端基于SpringBoot2构建RESTful API,前端使用Vue3组合式API开发管理界面,MyBatis-Plus简化了数据层操作,MySQL8.0则提供了可靠的数据存储。整套代码经过多次教学实践迭代,在功能完整性和技术新颖性之间取得了较好平衡。
SpringBoot2.7.x作为基础框架,相比旧版本在启动速度和内存占用上有明显优化。特别配置了spring-boot-starter-validation进行参数校验,避免在Controller中写大量if-else判断。例如课程添加接口的DTO类会这样定义:
java复制@Data
public class CourseDTO {
@NotBlank(message = "课程名称不能为空")
@Size(max = 50, message = "名称长度超过限制")
private String courseName;
@NotNull
@Range(min = 1, max = 5, message = "学分范围1-5")
private Integer credit;
}
MyBatis-Plus 3.5.x版本提供了强大的单表CRUD能力,其Lambda表达式查询方式让代码更易维护:
java复制// 查询某教师开设的课程
LambdaQueryWrapper<Course> query = new LambdaQueryWrapper<>();
query.eq(Course::getTeacherId, teacherId)
.between(Course::getStartTime, startDate, endDate);
List<Course> courses = courseMapper.selectList(query);
Vue3组合式API配合TypeScript,使得前端代码组织更清晰。例如选课列表组件:
typescript复制const { courseList, loading, error } = useCourseSelection();
// 在composable中的逻辑封装
function useCourseSelection() {
const courseList = ref<Course[]>([]);
const loading = ref(false);
const fetchCourses = async () => {
loading.value = true;
try {
const res = await api.get('/courses');
courseList.value = res.data;
} catch (err) {
// 错误处理
} finally {
loading.value = false;
}
};
return { courseList, loading, fetchCourses };
}
Element Plus作为UI组件库,其表格组件非常适合展示课程数据,配合虚拟滚动优化了大列表性能。
MySQL8.0充分利用了窗口函数等新特性。核心表结构设计考虑到了选课系统的业务特点:
sql复制CREATE TABLE `course_selection` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`student_id` BIGINT NOT NULL COMMENT '学号',
`course_id` BIGINT NOT NULL COMMENT '课程ID',
`selection_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-有效 0-退选',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_student_course` (`student_id`, `course_id`),
KEY `idx_course` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
选课过程需要处理多种约束条件,代码中使用了策略模式来组织这些规则校验:
java复制public interface SelectionRule {
boolean validate(SelectionContext context);
}
// 具体实现示例:时间冲突检测
public class TimeConflictRule implements SelectionRule {
@Override
public boolean validate(SelectionContext ctx) {
List<Course> selected = ctx.getSelectedCourses();
Course newCourse = ctx.getNewCourse();
return selected.stream().noneMatch(c ->
!(c.getEndTime().isBefore(newCourse.getStartTime()) ||
c.getStartTime().isAfter(newCourse.getEndTime()))
);
}
}
// 在服务层应用规则
public SelectionResult selectCourse(Long studentId, Long courseId) {
SelectionContext context = buildContext(studentId, courseId);
for (SelectionRule rule : rules) {
if (!rule.validate(context)) {
return SelectionResult.fail(rule.getMessage());
}
}
// 通过所有规则后执行选课
return doSelection(studentId, courseId);
}
选课开放时往往面临高并发场景,系统采用多级缓存和乐观锁应对:
java复制@Transactional
public boolean deductQuota(Long courseId) {
Course course = courseMapper.selectById(courseId);
if (course.getRemainQuota() <= 0) {
return false;
}
int updated = courseMapper.updateQuota(
courseId,
course.getRemainQuota() - 1,
course.getVersion()
);
return updated > 0;
}
typescript复制const handleSelect = useDebounceFn(async (courseId) => {
await submitSelection(courseId);
}, 1000);
采用Docker Compose编排服务,示例配置:
yaml复制version: '3'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- redis
- mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: course_selection
volumes:
- mysql_data:/var/lib/mysql
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
Vue项目通过Nginx部署,优化配置包括:
nginx复制server {
listen 80;
server_name course.example.com;
gzip on;
gzip_types text/plain application/javascript application/x-javascript text/css;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
}
}
java复制@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
java复制public class R<T> {
private int code;
private String msg;
private T data;
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.setCode(200);
r.setData(data);
return r;
}
}
java复制@Cacheable(value = "courses", key = "#id")
public Course getById(Long id) {
return courseMapper.selectById(id);
}
java复制// 批量插入选课记录
List<Selection> selections = ...;
selectionService.saveBatch(selections, 1000); // 每批1000条
vue复制<el-table-v2
:columns="columns"
:data="courses"
:width="800"
:height="600"
:row-height="60"
fixed
/>
开发环境常见跨域错误,解决方案:
java复制@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*");
}
};
}
js复制export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
正确写法示例:
java复制@Transactional(rollbackFor = Exception.class)
public void createCourse(CourseDTO dto) {
try {
Course course = convertToEntity(dto);
courseMapper.insert(course);
// 其他数据库操作
} catch (Exception e) {
log.error("创建课程失败", e);
throw new RuntimeException("系统异常");
}
}
对于教学用途,建议引导学生先理解核心业务流程,再逐步扩展这些高级功能。系统目前的代码结构清晰划分了controller、service、mapper层,非常适合作为SpringBoot+Vue全栈开发的入门案例。