作为一名长期从事Java全栈开发的工程师,我最近完成了一个基于SpringBoot+Vue的大学生班级管理系统。这个项目最初是为某高校辅导员定制的,后来经过多次迭代,逐渐发展成为一个功能完善的管理平台。从实际使用反馈来看,系统显著提升了班级管理效率,特别是在处理学生信息、课程安排和活动组织等方面。
这个系统特别适合作为毕业设计或课程设计的选题,因为它涵盖了现代Web开发的完整技术栈:后端使用SpringBoot+MyBatis,前端采用Vue+Element UI,数据库使用MySQL,实现了典型的前后端分离架构。对于计算机专业的学生来说,通过这个项目可以系统性地掌握企业级应用开发的全流程。
选择SpringBoot作为后端框架主要基于以下几个考虑:
数据库选用MySQL 8.0,主要因为:
Vue.js作为前端框架的优势在于:
Element UI的选择理由:
系统采用经典的三层架构:
code复制┌─────────────────┐
│ 前端 │ Vue.js + Element UI
└────────┬────────┘
│ HTTP/JSON
┌────────▼────────┐
│ 后端 │ SpringBoot + MyBatis
└────────┬────────┘
│ JDBC
┌────────▼────────┐
│ 数据库 │ MySQL 8.0
└─────────────────┘
前后端通过RESTful API交互,数据格式为JSON。这种架构的优点是:
sql复制CREATE TABLE `student_info` (
`stu_id` VARCHAR(20) NOT NULL COMMENT '学号',
`stu_name` VARCHAR(50) NOT NULL COMMENT '姓名',
`stu_gender` CHAR(1) NOT NULL COMMENT '性别(M/F)',
`stu_phone` VARCHAR(15) DEFAULT NULL COMMENT '手机号',
`stu_email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
`stu_class` VARCHAR(20) NOT NULL COMMENT '班级',
`register_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
PRIMARY KEY (`stu_id`),
KEY `idx_class` (`stu_class`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生基本信息';
设计要点:
sql复制CREATE TABLE `class_activity` (
`activity_id` INT NOT NULL AUTO_INCREMENT COMMENT '活动ID',
`activity_name` VARCHAR(100) NOT NULL COMMENT '活动名称',
`activity_time` DATETIME NOT NULL COMMENT '活动时间',
`activity_place` VARCHAR(100) NOT NULL COMMENT '活动地点',
`organizer` VARCHAR(50) NOT NULL COMMENT '组织者',
`participant_num` INT DEFAULT '0' COMMENT '参与人数',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`description` TEXT COMMENT '活动描述',
PRIMARY KEY (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级活动';
优化考虑:
sql复制CREATE TABLE `course_score` (
`record_id` INT NOT NULL AUTO_INCREMENT COMMENT '记录ID',
`stu_id` VARCHAR(20) NOT NULL COMMENT '学号',
`course_name` VARCHAR(50) NOT NULL COMMENT '课程名称',
`course_score` DECIMAL(5,2) NOT NULL COMMENT '成绩',
`semester` VARCHAR(20) NOT NULL COMMENT '学期',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`record_id`),
KEY `idx_stu_id` (`stu_id`),
CONSTRAINT `fk_student` FOREIGN KEY (`stu_id`) REFERENCES `student_info` (`stu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程成绩';
关键设计:
在实际开发中,我们针对性能做了以下优化:
sql复制-- 不好的写法
SELECT * FROM student_info WHERE stu_class = 'CS1801';
-- 优化后的写法
SELECT stu_id, stu_name, stu_gender FROM student_info
WHERE stu_class = 'CS1801'
ORDER BY stu_id LIMIT 20;
优化点:
sql复制-- 添加复合索引
ALTER TABLE course_score ADD INDEX idx_semester_course (semester, course_name);
-- 分析索引使用情况
EXPLAIN SELECT AVG(course_score) FROM course_score
WHERE semester = '2023-2024-1' AND course_name = '数据结构';
sql复制-- 使用INNER JOIN替代WHERE连接
SELECT s.stu_name, c.course_name, c.course_score
FROM student_info s
INNER JOIN course_score c ON s.stu_id = c.stu_id
WHERE c.semester = '2023-2024-1';
典型的application.yml配置示例:
yaml复制server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:mysql://localhost:3306/class_management?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
关键配置说明:
java复制@Data
@TableName("student_info")
public class Student {
@TableId(value = "stu_id", type = IdType.INPUT)
private String stuId;
private String stuName;
private String stuGender;
private String stuPhone;
private String stuEmail;
private String stuClass;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime registerTime;
}
java复制@RestController
@RequestMapping("/student")
@Api(tags = "学生管理")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/list")
@ApiOperation("分页查询学生列表")
public Result<Page<Student>> listStudents(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String className) {
Page<Student> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Student> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(className)) {
queryWrapper.eq(Student::getStuClass, className);
}
return Result.success(studentService.page(page, queryWrapper));
}
@PostMapping("/add")
@ApiOperation("添加学生")
public Result<String> addStudent(@RequestBody @Valid Student student) {
if (studentService.save(student)) {
return Result.success("添加成功");
}
return Result.error("添加失败");
}
}
java复制public interface StudentService extends IService<Student> {
/**
* 批量导入学生信息
*/
Result<String> importStudents(MultipartFile file);
}
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student>
implements StudentService {
@Override
public Result<String> importStudents(MultipartFile file) {
// 实现Excel导入逻辑
// ...
}
}
使用Spring Security + JWT实现认证授权:
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/student/**").hasAnyRole("TEACHER", "ADMIN")
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
java复制@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration}")
private int jwtExpiration;
public 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() + jwtExpiration * 1000L))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
log.error("JWT验证失败: {}", e.getMessage());
}
return false;
}
}
code复制src/
├── api/ # API请求封装
├── assets/ # 静态资源
├── components/ # 公共组件
├── router/ # 路由配置
├── store/ # Vuex状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── student/ # 学生管理
│ ├── course/ # 课程管理
│ └── activity/ # 活动管理
└── App.vue # 根组件
vue复制<template>
<div class="student-container">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>学生列表</span>
<el-button type="primary" @click="showImportDialog" style="float: right">
导入学生
</el-button>
</div>
<el-table
:data="tableData"
border
style="width: 100%"
v-loading="loading">
<el-table-column prop="stuId" label="学号" width="180" />
<el-table-column prop="stuName" label="姓名" width="120" />
<el-table-column prop="stuGender" label="性别" width="80">
<template #default="{row}">
{{ row.stuGender === 'M' ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="stuClass" label="班级" />
<el-table-column label="操作" width="180">
<template #default="{row}">
<el-button size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.current"
:page-sizes="[10, 20, 50, 100]"
:page-size="pagination.size"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</el-card>
</div>
</template>
<script>
import { getStudentList, deleteStudent } from '@/api/student'
export default {
data() {
return {
tableData: [],
loading: false,
pagination: {
current: 1,
size: 10,
total: 0
},
queryParams: {}
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
this.loading = true
try {
const { data } = await getStudentList({
pageNum: this.pagination.current,
pageSize: this.pagination.size,
...this.queryParams
})
this.tableData = data.records
this.pagination.total = data.total
} finally {
this.loading = false
}
},
handleSizeChange(val) {
this.pagination.size = val
this.fetchData()
},
handleCurrentChange(val) {
this.pagination.current = val
this.fetchData()
},
handleEdit(row) {
this.$router.push(`/student/edit/${row.stuId}`)
},
async handleDelete(row) {
try {
await this.$confirm('确认删除该学生吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteStudent(row.stuId)
this.$message.success('删除成功')
this.fetchData()
} catch (e) {
console.error(e)
}
}
}
}
</script>
使用ECharts实现成绩统计分析:
vue复制<template>
<div class="chart-container">
<el-card shadow="hover">
<div slot="header">
<span>班级成绩分布</span>
<el-select
v-model="selectedSemester"
placeholder="请选择学期"
@change="fetchChartData"
style="width: 200px; margin-left: 20px">
<el-option
v-for="item in semesterOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</div>
<div ref="chart" style="width: 100%; height: 400px;"></div>
</el-card>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getScoreDistribution } from '@/api/course'
export default {
data() {
return {
chart: null,
selectedSemester: '',
semesterOptions: [
{ value: '2023-2024-1', label: '2023-2024学年第一学期' },
{ value: '2022-2023-2', label: '2022-2023学年第二学期' }
]
}
},
mounted() {
this.initChart()
this.selectedSemester = this.semesterOptions[0].value
this.fetchChartData()
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart)
window.addEventListener('resize', this.handleResize)
},
async fetchChartData() {
const { data } = await getScoreDistribution({
semester: this.selectedSemester
})
const option = {
title: {
text: '成绩分布统计',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['90-100', '80-89', '70-79', '60-69', '0-59']
},
series: [
{
name: '成绩分布',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: data.excellent, name: '90-100' },
{ value: data.good, name: '80-89' },
{ value: data.medium, name: '70-79' },
{ value: data.pass, name: '60-69' },
{ value: data.fail, name: '0-59' }
]
}
]
}
this.chart.setOption(option)
},
handleResize() {
this.chart && this.chart.resize()
}
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
this.chart && this.chart.dispose()
}
}
</script>
使用Docker部署SpringBoot应用:
dockerfile复制# 基础镜像
FROM openjdk:8-jdk-alpine
# 设置工作目录
WORKDIR /app
# 复制构建好的jar包
COPY target/class-management-0.0.1-SNAPSHOT.jar app.jar
# 暴露端口
EXPOSE 8080
# 启动命令
ENTRYPOINT ["java","-jar","app.jar"]
bash复制# 构建镜像
docker build -t class-management .
# 运行容器
docker run -d -p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-server:3306/class_management \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=yourpassword \
--name class-management \
class-management
使用Nginx部署Vue项目:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
dockerfile复制FROM nginx:alpine
COPY dist/ /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
设置MySQL自动备份:
bash复制# 每日备份脚本
#!/bin/bash
DATE=$(date +%Y%m%d)
BACKUP_DIR="/data/backups"
MYSQL_USER="root"
MYSQL_PASSWORD="yourpassword"
DATABASE="class_management"
mysqldump -u$MYSQL_USER -p$MYSQL_PASSWORD $DATABASE > $BACKUP_DIR/$DATABASE-$DATE.sql
# 保留最近7天备份
find $BACKUP_DIR -type f -name "*.sql" -mtime +7 -exec rm {} \;
设置cron任务:
bash复制0 2 * * * /path/to/backup_script.sh
解决方案:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
javascript复制const StudentList = () => import('./views/student/List.vue')
vue复制<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
vue复制<el-image :src="imgUrl" lazy></el-image>
java复制@PostMapping("/update")
public Result<String> updateStudent(@RequestBody @Valid StudentDTO dto) {
// 自动验证DTO中的约束
// ...
}
@Data
public class StudentDTO {
@NotBlank(message = "学号不能为空")
@Size(min = 10, max = 20, message = "学号长度应在10-20之间")
private String stuId;
@NotBlank(message = "姓名不能为空")
private String stuName;
@Pattern(regexp = "^[MF]$", message = "性别只能是M或F")
private String stuGender;
}
javascript复制import VueSanitize from "vue-sanitize";
Vue.use(VueSanitize);
对于想要基于此项目做毕业设计的同学,可以考虑以下方向:
在实际开发这个项目的过程中,我发现班级管理系统虽然看似简单,但要真正做好需要考虑很多细节。比如权限控制要细致到每个操作按钮,数据校验要考虑各种边界情况,用户体验要兼顾不同角色的需求。建议开发者在实现基础功能后,多从实际使用场景出发,思考如何让系统更加智能、高效。