1. 项目背景与核心需求
在高校教务管理中,选课系统一直是连接学生与课程资源的核心枢纽。传统的人工选课方式存在效率低下、易出错、资源分配不均等问题。我去年参与某师范院校教务系统升级时,亲眼目睹了教务处老师用Excel表格手工处理3000多名学生的选课数据,不仅耗时长达两周,还出现了几十处课程冲突需要人工核对。
这正是我们开发这套基于Java+SSM+Django的选课管理系统的初衷。系统需要实现三大核心模块:
-
课程管理中枢:支持课程信息的CRUD操作,包括课程代码、名称、学分、授课教师等20余个字段的维护,特别需要处理课程时间冲突检测(比如同一教室同一时段不能安排两门课)
-
智能选课引擎:学生端需要实现:
- 课程检索(按学分/教师/时间等多维度筛选)
- 选课冲突检测(与已选课程时间冲突提示)
- 热门课程排队机制(当选择人数超过容量时)
-
数据可视化看板:管理员需要实时掌握:
- 各课程选课人数分布
- 专业选课倾向分析
- 教室利用率热力图
关键设计原则:系统采用"松耦合+模块化"架构,前端用Bootstrap保证响应式布局,后端SSM处理核心业务逻辑,Django Admin快速搭建管理后台。这种混合架构在保证系统稳定性的同时,大幅降低了开发成本。
2. 技术栈选型与架构设计
2.1 为什么选择SSM+Django组合
在技术选型阶段,我们对比了三种方案:
| 方案 | 开发效率 | 性能 | 可维护性 | 学习成本 |
|---|---|---|---|---|
| 纯Java EE | 低 | 高 | 中 | 高 |
| Spring Boot+Vue | 中 | 高 | 高 | 中 |
| SSM+Django(当前方案) | 高 | 中 | 高 | 低 |
最终选择SSM+Django主要基于以下考虑:
- 快速原型开发:Django的admin模块能在2小时内搭建出功能完善的后台管理系统,而用Java实现同等功能需要2天
- ORM优势互补:MyBatis适合复杂SQL查询,Django ORM则简化了基础CRUD操作
- 权限控制:Spring Security+Django Auth组成双重权限校验,学生/教师/管理员权限分离更彻底
2.2 系统分层架构
code复制┌───────────────────────────────────────┐
│ 表现层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Bootstrap前端│ ←→ │ Django Admin│ │
│ └─────────────┘ └─────────────┘ │
└───────────────┬───────────────────────┘
↓ (REST API)
┌───────────────────────────────────────┐
│ 业务逻辑层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Spring MVC │ ←→ │ Django Views│ │
│ └─────────────┘ └─────────────┘ │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 数据持久层 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ MyBatis │ ←→ │ Django ORM │ │
│ └─────────────┘ └─────────────┘ │
└───────────────┬───────────────────────┘
↓
┌───────────────────────────────────────┐
│ 数据库层 │
│ MySQL 5.7 │
└───────────────────────────────────────┘
2.3 数据库关键设计
课程表的核心字段设计示例:
sql复制CREATE TABLE `course` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course_code` varchar(20) NOT NULL COMMENT '课程代码',
`name` varchar(100) NOT NULL COMMENT '课程名称',
`credit` tinyint(4) NOT NULL DEFAULT '2' COMMENT '学分',
`teacher_id` int(11) NOT NULL COMMENT '授课教师',
`max_student` smallint(6) NOT NULL DEFAULT '100' COMMENT '最大选课人数',
`current_student` smallint(6) NOT NULL DEFAULT '0' COMMENT '当前选课人数',
`schedule_json` json DEFAULT NULL COMMENT '排课时间(JSON格式)',
`classroom_id` int(11) DEFAULT NULL COMMENT '教室ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`course_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
踩坑提醒:schedule_json字段存储如
{"weekday":3,"start_section":5,"end_section":7}的时间信息,这种设计比拆分多表关联查询效率提升40%,但要注意JSON字段的索引问题。
3. 核心功能实现细节
3.1 选课并发控制
高并发选课场景下,我们采用"乐观锁+Redis限流"双重保障:
java复制// 选课核心代码片段
@Transactional
public Map<String, Object> chooseCourse(Integer courseId, String username) {
// 1. Redis限流(每秒50个请求)
String redisKey = "course:" + courseId;
Long count = redisTemplate.opsForValue().increment(redisKey);
if (count != null && count > 50) {
throw new RateLimitException("选课请求过于频繁");
}
// 2. 乐观锁更新
int affected = courseMapper.updateCourseStock(
courseId,
"current_student < max_student",
LocalDateTime.now()
);
if (affected == 0) {
throw new CourseFullException("课程已满");
}
// 3. 记录选课关系
UserCourse uc = new UserCourse();
uc.setUserId(getUserId(username));
uc.setCourseId(courseId);
userCourseMapper.insert(uc);
return Map.of("success", true, "message", "选课成功");
}
3.2 课程冲突检测算法
时间冲突检测是选课系统的关键功能,我们采用位运算提升检测效率:
python复制# Django中的冲突检测实现
def check_schedule_conflict(user_id, new_course):
# 获取用户已选课程的时间位图
selected = UserCourse.objects.filter(user_id=user_id).select_related('course')
user_bitmap = 0b0
for uc in selected:
course = uc.course
for time_slot in course.schedule_json:
day = time_slot['weekday'] # 1-7表示周一到周日
start = time_slot['start_section'] # 1-12节课
end = time_slot['end_section']
for s in range(start, end+1):
user_bitmap |= 1 << (day*24 + s) # 按位标记
# 检查新课程时间
for time_slot in new_course.schedule_json:
day = time_slot['weekday']
start = time_slot['start_section']
end = time_slot['end_section']
for s in range(start, end+1):
if user_bitmap & (1 << (day*24 + s)):
return True # 存在冲突
return False
3.3 管理端Excel导入导出
教务人员常需要批量操作课程数据,我们使用Apache POI实现:
java复制// Excel导出示例
@GetMapping("/export")
public void exportCourses(HttpServletResponse response) throws IOException {
List<Course> courses = courseService.getAll();
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("课程列表");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("课程代码");
headerRow.createCell(1).setCellValue("课程名称");
// 其他表头...
// 填充数据
int rowNum = 1;
for (Course course : courses) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(course.getCourseCode());
row.createCell(1).setCellValue(course.getName());
// 其他字段...
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=courses.xlsx");
workbook.write(response.getOutputStream());
workbook.close();
}
4. 部署与优化实践
4.1 混合环境部署方案
系统采用Docker Compose实现一键部署:
yaml复制version: '3'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: course_db
ports:
- "3306:3306"
volumes:
- ./mysql_data:/var/lib/mysql
redis:
image: redis:alpine
ports:
- "6379:6379"
java_app:
build:
context: ./ssm
dockerfile: Dockerfile
ports:
- "8080:8080"
depends_on:
- mysql
- redis
django_admin:
build:
context: ./django
dockerfile: Dockerfile
ports:
- "8000:8000"
depends_on:
- mysql
4.2 性能优化技巧
- 查询优化:对课程列表接口添加二级缓存
java复制@Cacheable(value = "courses", key = "#page+'_'+#size")
public PageInfo<Course> getCourseList(int page, int size) {
PageHelper.startPage(page, size);
List<Course> list = courseMapper.selectAll();
return new PageInfo<>(list);
}
- 前端优化:使用DataTables实现服务端分页
javascript复制$('#course-table').DataTable({
"processing": true,
"serverSide": true,
"ajax": {
"url": "/api/courses",
"type": "POST"
},
"columns": [
{"data": "courseCode"},
{"data": "name"},
// 其他列...
]
});
- 安全加固:防止SQL注入和XSS攻击
java复制// MyBatis使用#{}防止注入
@Select("SELECT * FROM course WHERE name LIKE CONCAT('%',#{name},'%')")
List<Course> searchByName(String name);
// 前端转义HTML
<div th:text="${course.description}"></div>
5. 扩展功能与二次开发
5.1 微信小程序集成
为方便学生移动端选课,我们扩展了微信小程序接口:
java复制@RestController
@RequestMapping("/mini")
public class MiniProgramController {
@GetMapping("/courses")
public JsonResult getCourses(@RequestParam String openid) {
User user = userService.getByOpenid(openid);
List<CourseVO> list = courseService.getForUser(user.getId());
return JsonResult.success(list);
}
@PostMapping("/choose")
public JsonResult chooseCourse(
@RequestParam String openid,
@RequestParam Integer courseId) {
// ...选课逻辑
}
}
5.2 智能推荐算法
基于协同过滤的课程推荐:
python复制# Django中实现推荐逻辑
def recommend_courses(user_id):
from sklearn.metrics.pairwise import cosine_similarity
# 获取用户-课程矩阵
user_courses = UserCourse.objects.all()
data = pd.DataFrame(list(user_courses.values('user_id', 'course_id')))
matrix = pd.pivot_table(data, index='user_id', columns='course_id', aggfunc=len, fill_value=0)
# 计算相似度
similarities = cosine_similarity(matrix)
user_idx = matrix.index.get_loc(user_id)
similar_users = np.argsort(-similarities[user_idx])[1:6] # 取前5个相似用户
# 推荐课程
rec_courses = set()
for sim_user in similar_users:
sim_user_id = matrix.index[sim_user]
taken = set(data[data['user_id']==sim_user_id]['course_id'])
mine = set(data[data['user_id']==user_id]['course_id'])
rec_courses.update(taken - mine)
return Course.objects.filter(id__in=list(rec_courses)[:10])
6. 项目演进与经验总结
在实际部署过程中,我们遇到了几个典型问题及解决方案:
-
选课峰值期的系统崩溃:
- 现象:开学选课期间,每秒200+的请求导致数据库连接耗尽
- 解决方案:
- 引入Redis缓存课程余量信息
- 使用Nginx做限流(limit_req模块)
- 前端添加排队进度条
-
跨校区课程时间同步:
- 问题:不同校区存在时区差异导致课程时间显示错误
- 解决:统一使用UTC时间存储,前端按用户时区转换
java复制@JsonFormat(timezone = "UTC", pattern = "yyyy-MM-dd HH:mm") private Date startTime; -
数据迁移的兼容性问题:
- 旧系统使用SQL Server,新系统用MySQL
- 开发了专门的ETL工具处理:
- 数据类型转换(如nvarchar → utf8mb4)
- 自增ID冲突解决
- 存储过程重写
这个项目给我的深刻体会是:混合技术栈虽然能发挥各语言优势,但需要特别注意:
- 接口数据格式的统一(我们采用JSON Schema校验)
- 事务跨框架处理(最终使用分布式事务Seata)
- 监控体系的整合(Prometheus+Grafana统一监控)
对于教学机构的信息化建设,我有两个建议:一是前期做好容量规划,至少按在校生数量的3倍设计系统容量;二是建立完善的数据备份机制,我们采用xtrabackup每天全备+binlog增量备份,曾多次挽救误操作导致的数据丢失。
