学生选课系统是高校教务管理中最基础也最关键的模块之一。十年前我刚入行做校园信息化时,亲眼见过某高校用Excel表格手工处理3000多名学生的选课数据,结果因为公式错误导致近百名学生课程冲突。这种原始操作方式不仅效率低下,还容易引发教学事故。
基于Java的学生选课系统正是为了解决这些痛点而生。它通过标准化的流程设计,实现了从课程发布、学生选课、冲突检测到成绩录入的全流程数字化管理。相比市面上的通用教务系统,自主开发的选课系统更能贴合具体院校的个性化需求,比如特殊的学分计算规则或选课优先级设置。
这个毕设项目的独特之处在于:
后端采用SpringBoot+MyBatis组合主要基于三点考虑:
前端选用Thymeleaf模板引擎而非Vue/React的原因是:
数据库选择MySQL5.7而非新版8.0的考量:
课程表(t_course)的关键字段设计:
sql复制CREATE TABLE `t_course` (
`cid` int NOT NULL AUTO_INCREMENT COMMENT '课程ID',
`cname` varchar(50) NOT NULL COMMENT '课程名称',
`credit` tinyint NOT NULL COMMENT '学分',
`tid` int NOT NULL COMMENT '教师ID',
`capacity` smallint NOT NULL COMMENT '容量',
`selected` smallint DEFAULT '0' COMMENT '已选人数',
`time_slot` varchar(20) NOT NULL COMMENT '时间槽位',
`location` varchar(30) NOT NULL COMMENT '上课地点',
PRIMARY KEY (`cid`),
KEY `idx_teacher` (`tid`),
KEY `idx_time` (`time_slot`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
时间槽位采用"周几-节次"格式存储,如"Mon-1"表示周一第1节。这种设计便于实现冲突检测:
java复制// 冲突检测核心逻辑
public boolean checkConflict(List<String> existSlots, String newSlot) {
String[] parts = newSlot.split("-");
String day = parts[0];
int period = Integer.parseInt(parts[1]);
for (String slot : existSlots) {
if (slot.startsWith(day)) {
int existPeriod = Integer.parseInt(slot.split("-")[1]);
if (Math.abs(existPeriod - period) < 2) { // 相邻节次视为冲突
return true;
}
}
}
return false;
}
选课操作需要保证原子性的三个步骤:
采用Spring声明式事务管理:
java复制@Transactional(rollbackFor = Exception.class)
public boolean selectCourse(int sid, int cid) throws Exception {
// 1. 查询课程信息
Course course = courseMapper.selectById(cid);
if (course.getSelected() >= course.getCapacity()) {
throw new RuntimeException("课程已满");
}
// 2. 获取学生已选课程时间
List<String> existSlots = selectMapper.querySelectedSlots(sid);
if (checkConflict(existSlots, course.getTimeSlot())) {
throw new RuntimeException("时间冲突");
}
// 3. 插入选课记录
Selection record = new Selection();
record.setSid(sid);
record.setCid(cid);
record.setSelectTime(new Date());
if (selectMapper.insert(record) <= 0) {
return false;
}
// 4. 更新课程已选人数
course.setSelected(course.getSelected() + 1);
return courseMapper.updateById(course) > 0;
}
使用FullCalendar库呈现课表视图时,需注意时区问题:
javascript复制document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
timeZone: 'local', // 必须显式设置
initialView: 'timeGridWeek',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'timeGridWeek,timeGridDay'
},
events: function(fetchInfo, successCallback, failureCallback) {
$.get('/course/myTimetable', {
sid: $('#studentId').val()
}, function(data) {
var events = [];
$.each(data, function(index, item) {
events.push({
title: item.cname + '@' + item.location,
start: parseSlotToDate(item.timeSlot),
allDay: false
});
});
successCallback(events);
});
}
});
calendar.render();
});
// 将"Mon-1"转换为具体时间
function parseSlotToDate(slot) {
const dayMap = {'Mon':1, 'Tue':2, 'Wed':3, 'Thu':4, 'Fri':5};
let [day, period] = slot.split('-');
let date = new Date();
date.setDate(date.getDate() + (dayMap[day] - date.getDay()));
date.setHours(8 + (period-1)*2, 0, 0); // 假设每节课2小时
return date;
}
在压力测试时发现,当多个学生同时抢热门课程时会出现超卖现象。解决方案:
java复制@Update("update t_course set selected=selected+1 where cid=#{cid} and selected<capacity")
int incrementSelected(@Param("cid") int cid);
javascript复制function selectCourse(cid) {
$('#submitBtn').prop('disabled', true);
$.ajax({
url: '/selection/select',
type: 'POST',
data: {cid: cid},
success: function(res) {
if(res.code === 200) {
showToast('选课成功');
} else {
setTimeout(function() {
selectCourse(cid); // 失败后自动重试
}, 1000);
}
},
complete: function() {
$('#submitBtn').prop('disabled', false);
}
});
}
原算法将相邻节次视为冲突,但实际存在以下特殊情况需要处理:
改进后的冲突检测逻辑:
java复制public boolean checkConflict(SelectionVO exist, CourseVO newCourse) {
// 在线课程不检查地点
if (!"online".equals(newCourse.getLocationType())) {
if (exist.getLocation().equals(newCourse.getLocation())) {
return true;
}
// 跨校区检查
if (getCampus(exist.getLocation()) != getCampus(newCourse.getLocation())) {
int travelTime = getTravelTime(exist.getLocation(), newCourse.getLocation());
if (Math.abs(exist.getEndTime() - newCourse.getStartTime()) < travelTime) {
return true;
}
}
}
// 时间重叠检查
return exist.getDayOfWeek() == newCourse.getDayOfWeek()
&& exist.getEndTime() > newCourse.getStartTime()
&& newCourse.getEndTime() > exist.getStartTime();
}
通过微信开放平台实现移动端选课:
关键代码示例:
java复制@RestController
@RequestMapping("/wechat")
public class WechatController {
@GetMapping("/login")
public Result<String> wechatLogin(@RequestParam String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={code}&grant_type=authorization_code";
Map<String, String> params = new HashMap<>();
params.put("appid", wechatConfig.getAppId());
params.put("secret", wechatConfig.getSecret());
params.put("code", code);
String response = restTemplate.getForObject(url, String.class, params);
JSONObject json = JSON.parseObject(response);
String openid = json.getString("openid");
// 生成JWT
String token = JwtUtil.generateToken(openid);
return Result.success(token);
}
}
基于历史数据实现个性化推荐:
推荐系统核心逻辑:
python复制# 使用Surprise库实现协同过滤
from surprise import Dataset, KNNBasic
from surprise.model_selection import cross_validate
def train_recommender():
# 加载评分数据 (sid, cid, rating)
data = Dataset.load_from_df(ratings_df, reader)
trainset = data.build_full_trainset()
# 使用物品协同过滤
sim_options = {
'name': 'cosine',
'user_based': False
}
algo = KNNBasic(sim_options=sim_options)
algo.fit(trainset)
# 为指定学生生成推荐
inner_cids = [trainset.to_inner_iid(cid) for cid in candidate_cids]
predictions = [algo.predict(sid, cid) for cid in inner_cids]
return sorted(predictions, key=lambda x: x.est, reverse=True)[:10]
properties复制# 初始连接数
spring.datasource.druid.initial-size=5
# 最大连接数
spring.datasource.druid.max-active=20
# 获取连接超时时间(ms)
spring.datasource.druid.max-wait=60000
# 最小空闲连接
spring.datasource.druid.min-idle=5
properties复制# 最大线程数
server.tomcat.max-threads=200
# 最大连接数
server.tomcat.max-connections=1000
# 连接超时(ms)
server.tomcat.connection-timeout=5000
java复制@Configuration
public class ActuatorConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated();
}
}
xml复制<appender name="ELK" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>192.168.1.100:5000</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app":"course-system","env":"production"}</customFields>
</encoder>
</appender>
<logger name="com.example.course" level="DEBUG" additivity="false">
<appender-ref ref="ELK"/>
<appender-ref ref="CONSOLE"/>
</logger>
sql复制DELIMITER //
CREATE PROCEDURE generate_test_data(IN studentCount INT, IN courseCount INT)
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= studentCount DO
INSERT INTO t_student(sno, sname, gender, major)
VALUES(CONCAT('2023',LPAD(i,4,'0')),
CONCAT('学生',i),
IF(RAND()>0.5,'男','女'),
ELT(FLOOR(1+RAND()*5),'计算机','电子','机械','经管','外语'));
SET i = i + 1;
END WHILE;
SET i = 1;
WHILE i <= courseCount DO
INSERT INTO t_course(cname, credit, tid, capacity, time_slot, location)
VALUES(CONCAT('课程',i),
FLOOR(1+RAND()*4),
FLOOR(1+RAND()*10),
FLOOR(30+RAND()*70),
CONCAT(ELT(FLOOR(1+RAND()*5),'Mon','Tue','Wed','Thu','Fri'),'-',FLOOR(1+RAND()*6)),
CONCAT(ELT(FLOOR(1+RAND()*3),'一教','二教','三教'),FLOOR(100+RAND()*500)));
SET i = i + 1;
END WHILE;
END //
DELIMITER ;
Q:为什么选用MyBatis而不是Hibernate?
A:MyBatis更适合需要精细控制SQL的场景,且学习曲线更平缓。选课系统中有大量复杂查询(如多条件筛选课程),使用MyBatis可以编写优化过的SQL语句。
Q:系统能承受多少并发?
A:在4核8G的测试环境中,使用JMeter模拟测试,系统可以稳定处理500并发选课请求。通过添加Redis缓存课程余量信息,还可以进一步提升并发能力。