简介:这是一个面向高校与培训机构的教育信息化实践项目,学生选课系统完整涵盖学生选课、教师录入并打印成绩、管理员维护成员信息等核心业务,结合人工智能算法实现热门课程预测与个性化推荐,并辅助管理员识别异常选课行为。系统基于Java技术栈开发,后端采用Spring Boot,前端交互界面可配合Vue.js或React使用,同时包含系统分析与设计思路,适合Java学习者及课程设计参考。压缩包共61个文件,其中包含27个Java源程序、27个编译后的class文件、4个XML工程配置以及MySQL驱动JAR包等,整体大小仅2.16MB,目录结构清晰,便于直接导入开发环境或在本地部署运行。目前已有79人学习下载,资源附带完整源码与依赖,可对照研究数据库设计、并发查询优化、AI功能集成等关键环节,是快速上手信息管理系统开发的实用资料。
1. 学生选课系统,先想清楚这三点再写代码
刚接到“学生选课系统”这类需求时,很容易一头扎进前端页面和后端接口,把学生选课、老师打印成绩、管理员管理成员信息三个模块分别实现一遍,联调时才发现权限边界互相冲突、成绩单格式改不动、选课并发把表锁死。一个能真正交付给教务处用的选课系统,核心不是“有没有这个功能”,而是三个约束:选课规则可配置、成绩导出格式可定制、成员权限可审计。本文按管理系统常见的开发路径,从数据模型、接口设计到命令实现,给出可直接落地的方案。适合正在做课程设计、毕业设计或接手校内信息系统改造的开发者,也适合想把自己零散的增删改查脚本整理成规范项目的人。下面这套设计不做过度抽象,每一层都能在本地跑通。
2. 学生选课系统的数据模型与权限边界设计
2.1 三个角色,四张核心表的 ER 关系不能乱
学生选课系统涉及的主体非常明确:学生、教师、管理员,外加课程和选课记录。常见做法是拆成四张核心表:user(统一成员表)、course(课程表)、course_selection(选课记录表)、score(成绩表)。为什么不把学生和教师分成两张独立表?因为学生毕业后可能变成助教,教师也可能兼任选课管理员,统一成员表加role字段,后续扩展辅导员、教务员角色的成本最低。
CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password_hash` VARCHAR(255) NOT NULL, `real_name` VARCHAR(50) NOT NULL, `role` ENUM('admin','teacher','student') NOT NULL DEFAULT 'student', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `course` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `course_no` VARCHAR(20) NOT NULL UNIQUE, `name` VARCHAR(100) NOT NULL, `teacher_id` INT NOT NULL, `max_students` INT NOT NULL DEFAULT 50, `selected_count` INT NOT NULL DEFAULT 0, `semester` VARCHAR(20) NOT NULL, `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1可选 0不可选', FOREIGN KEY (`teacher_id`) REFERENCES `user`(`id`) );成绩不应该挂在选课记录上,而应该有独立的score表。原因很简单:选课记录的生命周期到退课就结束,但成绩需要保留存档;而且成绩可能有平时分、期末分、补考分,一张选课表装不下这些扩展字段。课程表里的selected_count是冗余字段,用来做选课校验,虽然破坏了一点第三范式,但对高并发选课场景很有必要。
2.2 权限校验不要只写在按钮上,要让接口无状态可校验
很多学生项目把权限判断写死在前端按钮v-if="role === 'admin'",接口层不加拦截,结果用 Postman 直接调删除接口就能删库。正确做法是后端在每次请求时解析登录令牌中的角色,再用一个简单的中间件统一拦截。以 Python Flask 为例:
from functools import wraps from flask import request, jsonify import jwt def role_required(*roles): def decorator(f): @wraps(f) def wrapper(*args, **kwargs): token = request.headers.get("Authorization", "").replace("Bearer ", "") try: payload = jwt.decode(token, app.config["SECRET_KEY"], algorithms=["HS256"]) except jwt.InvalidTokenError: return jsonify({"error": "登录状态无效"}), 401 if payload.get("role") not in roles: return jsonify({"error": "权限不足"}), 403 request.user = payload return f(*args, **kwargs) return wrapper return decorator这段代码的要点:角色校验发生在业务逻辑之前,任何视图函数只要加上@role_required("teacher")就自动完成身份验证和授权。request.user把当前登录人信息挂到请求对象上,后续取request.user["id"]就能知道是哪个老师在操作。注意 JWT 的过期时间要设短一点,课程设计里习惯设 24 小时,真在校园网环境里建议 2 小时,配合前端定时刷新令牌。
2.3 成员信息管理里的“软删除”比物理删除更实用
管理员管理成员信息时,常见操作是删除学生或教师账号。如果直接执行DELETE FROM user WHERE id = ?,历史上该学生选过的课程、考过的成绩外键会报错或留下孤儿数据。更稳妥的做法是给user表增加status字段,用状态位标识账号是否可用。
ALTER TABLE `user` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用';管理员界面的“删除”按钮改为“禁用”,只更新状态字段。查询课程列表时自动过滤掉禁用教师的课程;学生登录时检查状态,被禁用则无法选课。这样所有历史记录都保留,也解决了成绩单追溯时找不到教师姓名的问题。如果需求里明确要求物理删除,则需要保证删除前先处理关联表,按顺序删score、course_selection、course中涉及该用户的记录,再删user,这一步必须放在事务里执行。
3. 学生选课核心功能:事务、锁与容量校验
3.1 选课时最容易被忽略的并发超卖问题
学生选课接口如果写成“先查剩余容量再插入选课记录”,高并发场景下同一门课的最后几个名额会被多人同时抢到。校园选课系统峰值流量虽然比不上电商秒杀,但热门公选课开抢时几百人同时点选同一门课是常态。解决办法是用数据库行锁或乐观锁,把“检查容量”和“写入选课记录”放在同一个事务里,并对课程行加锁。
START TRANSACTION; SELECT `max_students`, `selected_count` FROM `course` WHERE `id` = 1 FOR UPDATE; -- 业务层判断 selected_count < max_students UPDATE `course` SET `selected_count` = `selected_count` + 1 WHERE `id` = 1; INSERT INTO `course_selection` (`student_id`, `course_id`, `selected_at`) VALUES (1001, 1, NOW()); COMMIT;FOR UPDATE是对课程行加排他锁,事务未提交前,其他事务对这个课程的选课操作会阻塞等待,直到当前事务提交或回滚。这样就不会出现“两边都查到剩余 1 个名额”的情况。但这套方案对热点课程的并发吞吐有限制,如果学校选课规模大到几千人同时抢一门课,建议改为 Redis 预热课程剩余名额,用 Lua 脚本做原子扣减,再异步落库。对于普通系统,数据库行锁足够。
3.2 防重复选课的唯一索引与友好提示
即使前端做了“已选课程”按钮置灰,也不能保证用户通过多个设备同时操作不重复提交。数据库层面一定要给course_selection表加联合唯一索引:
CREATE TABLE `course_selection` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `student_id` INT NOT NULL, `course_id` INT NOT NULL, `selected_at` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_student_course` (`student_id`, `course_id`) );这样当同一个学生重复提交时,第二次插入会触发1062 Duplicate entry错误。业务层捕获这个异常后,不要直接抛出 500,而是返回“您已选择该课程,请勿重复操作”。Python 侧可以在事务中执行插入,出错后回滚并返回友好提示。此外,选课系统还需要校验课程是否可选、是否在当前选课时间段内,这些判断放在插入之前统一封装成函数。
3.3 退课时的容量回补与事务一致性
退课和选课是对称操作,但很多实现里只做了删除记录,忘了回补course.selected_count,结果课程明明没人选了,容量却显示满员。退课接口必须在一个事务里完成“删除选课记录 + 减少课程已选数量”。
@bp.route("/course/<int:course_id>/drop", methods=["POST"]) @role_required("student") def drop_course(course_id): student_id = request.user["id"] try: db.begin() # 先删除选课记录,若删除影响行数为0说明没选过 result = db.execute( "DELETE FROM course_selection WHERE student_id = ? AND course_id = ?", (student_id, course_id) ) if result.rowcount == 0: db.rollback() return jsonify({"error": "您未选择该课程"}), 404 db.execute( "UPDATE course SET selected_count = selected_count - 1 WHERE id = ?", (course_id,) ) db.commit() except Exception: db.rollback() return jsonify({"error": "退课失败"}), 500 return jsonify({"message": "退课成功"})这里result.rowcount的判空很关键,它利用数据库影响行数来识别“从未选过课”的情况,避免先查询再删除的竞态。退课的时间窗口也要限制:教务处通常规定开课后两周内可退,逾期退课只能走人工审批,这个规则应该配置在系统参数表里而不是写死在代码里。
4. 老师打印成绩:从查询到 Excel 导出的完整链路
4.1 成绩录入与修改的审批留痕
老师打印成绩的前提是成绩已录入完成。成绩录入接口通常设计为按课程批量提交,例如一个 Excel 上传或逐个学生填写。这里要留一个审计字段:记录每个成绩的录入人、录入时间、最后修改时间。教育场景里成绩改错是敏感问题,建议在score表增加updated_by和updated_at字段。
CREATE TABLE `score` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `course_id` INT NOT NULL, `student_id` INT NOT NULL, `score_value` DECIMAL(5,2), `grade_point` DECIMAL(3,1), `entered_by` INT NOT NULL COMMENT '录入人id', `updated_by` INT DEFAULT NULL COMMENT '最后修改人id', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_course_student` (`course_id`, `student_id`) );录入成绩时,教师只被允许录自己负责的课程,这个权限校验需要关联course.teacher_id,所以接口里不能只依赖role_required("teacher"),还需要加一层课程归属判断:
def ensure_teacher_in_course(course_id): teacher_id = request.user["id"] row = db.execute( "SELECT id FROM course WHERE id = ? AND teacher_id = ?", (course_id, teacher_id) ).fetchone() if not row: raise PermissionError("只能管理自己负责的课程")4.2 按课导出带表头的 Excel 成绩单
打印成绩单的核心诉求是格式可控:标题、课程名、授课教师、成绩列、班级列、签字栏。直接调用数据库驱动导出 CSV 虽然简单,但 CSV 用 Excel 打开后中文可能乱码,且无法设置列宽和合并单元格。常见做法是服务端生成真正的.xlsx文件,用 Python 的openpyxl库。
import openpyxl from openpyxl.styles import Font, Alignment from flask import send_file def export_course_scores(course_id): course = db.execute( "SELECT course_no, name, teacher_id FROM course WHERE id = ?", (course_id,) ).fetchone() students = db.execute( """ SELECT u.username, u.real_name, s.score_value FROM score s JOIN user u ON s.student_id = u.id WHERE s.course_id = ? ORDER BY u.username """, (course_id,) ).fetchall() wb = openpyxl.Workbook() ws = wb.active ws.title = "成绩单" ws.append(["课程号", course["course_no"], "课程名", course["name"]]) ws.append(["序号", "学号", "姓名", "成绩"]) for idx, stu in enumerate(students, start=1): ws.append([idx, stu["username"], stu["real_name"], stu["score_value"]]) ws.column_dimensions["A"].width = 8 ws.column_dimensions["B"].width = 16 ws.column_dimensions["C"].width = 12 ws.column_dimensions["D"].width = 10 for row in ws.iter_rows(min_row=1, max_row=len(students) + 2, max_col=4): for cell in row: cell.alignment = Alignment(horizontal="center") # 第一行标题加粗 for cell in ws[1]: cell.font = Font(bold=True) file_path = f"/tmp/score_{course_id}_{int(time.time())}.xlsx" wb.save(file_path) return send_file(file_path, as_attachment=True, download_name=f"{course['name']}成绩单.xlsx")这个导出逻辑里,台账数据查询一次完成,避免循环里逐条查学生姓名。设置列宽和对齐方式是用 openpyxl 调整打印效果的常见手段,比在 Excel 里手工改省事得多。如果学校要求 A4 纸每页固定行数,可以在打印前设置ws.page_setup.orientation = 'portrait'和ws.print_area。
4.3 打印前统计与异常成绩标记
老师打印成绩单前,通常需要看这门课的基本统计信息:应考人数、实考人数、平均分、不及格人数。这些数据可以写在一个统计查询里:
SELECT COUNT(*) AS total, SUM(CASE WHEN score_value IS NULL THEN 1 ELSE 0 END) AS missing, AVG(score_value) AS avg_score, SUM(CASE WHEN score_value < 60 THEN 1 ELSE 0 END) AS failed FROM score WHERE course_id = ?统计结果可以展示在网页端,也可以作为附加信息合并进 Excel 的底部。异常成绩包括缺考(成绩为 NULL)、作弊标记(需要单独字段)、缓考(未参与本次成绩评定),这些不应该混在正常成绩列里。常见做法是增设score_status字段:1正常、2缺考、3作弊、4缓考,导出时正常成绩显示数值,非正常状态显示“缺考”“作弊”文字。
5. 管理员管理成员信息:批量导入、重置密码与操作日志
5.1 用 CSV 批量创建学生账号的正确姿势
管理员逐条手工创建学生账号效率太低,真实系统通常提供批量导入。前端传一个 CSV 或 Excel 文件,后端逐行校验并入库。CSV 导入比 Excel 简单,且教务处导出的学籍表通常可以另存为 CSV。批量导入要考虑两件事:账号重复处理、密码初始设置。
import csv import hashlib import random import string def import_students_from_csv(file_stream): reader = csv.DictReader(file_stream) success_count = 0 errors = [] default_password = "123456" for line_no, row in enumerate(reader, start=2): username = row.get("学号", "").strip() real_name = row.get("姓名", "").strip() if not username or not real_name: errors.append(f"第{line_no}行缺少学号或姓名") continue # 检查是否已存在 exists = db.execute( "SELECT id FROM user WHERE username = ?", (username,) ).fetchone() if exists: errors.append(f"第{line_no}行学号 {username} 已存在") continue password_hash = hashlib.sha256( (default_password + username).encode() ).hexdigest() db.execute( "INSERT INTO user (username, password_hash, real_name, role) VALUES (?, ?, ?, 'student')", (username, password_hash, real_name) ) success_count += 1 db.commit() return success_count, errors注意这里的密码处理,明文“123456”直接存库是绝对不行的。常见做法是用 SHA-256 加盐,这里的盐取学号,虽然简单但不建议生产环境使用,至少要用werkzeug.security的generate_password_hash或 Python 的bcrypt。提示:批量导入结果要返回给前端下载错误报告,让管理员知道哪些行失败、失败原因,而不是只在页面上弹一个“导入完成”。
5.2 重置密码要让管理员不能看到原密码
管理员管理成员信息里,“重置密码”功能很容易被做成“查看密码”。正确做法是管理员只能将密码重置为系统默认值或随机值,重置后用户需要立即修改。随机密码生成逻辑:
import secrets import string def generate_random_password(length=8): alphabet = string.ascii_letters + string.digits return ''.join(secrets.choice(alphabet) for _ in range(length))用secrets模块而不是random,因为random的算法是伪随机且可预测,secrets适合密码这类安全场景。重置后,管理员需要将新密码单独发给学生,系统里不留可查看的明文痕迹。在界面设计上,管理员点击“重置”按钮后,弹窗显示一次性密码,同时记录操作日志。
5.3 操作日志是“管理员管理成员信息”的安全底线
管理员删除用户、禁用账号、重置密码、修改角色,这些都属于敏感操作。如果系统里没有日志,出问题时无法追溯是谁在什么时候干了什么。日志表不需要太复杂,但至少要覆盖operator_id、action、target_user_id、detail、created_at五列。
CREATE TABLE `admin_operation_log` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `operator_id` INT NOT NULL, `action` VARCHAR(50) NOT NULL, `target_user_id` INT NOT NULL, `detail` VARCHAR(500), `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP );每个管理员操作的接口里,在业务成功之后写一条日志,用db.execute插入,和业务操作放在同一事务里,防止业务成功但日志丢失。日志查询界面只对超级管理员开放,普通管理员不能删除或修改日志。真正生产级的系统还会将日志同时发送到外部日志中心,但课程设计或内部系统做到这里已经足够。
6. 让选课系统更像真实系统的几个收尾技巧
如果只是应付验收,增删改查跑通就够了。但要想系统经得起老师反复操作,有几个细节值得补上。第一个是选课时间段控制:很多学生项目根本不校验选课时间,导致凌晨三点也能选课。在course表增加select_start_time和select_end_time两个字段,选课接口首先比较当前时间,不在区间内直接返回“不在选课时间段内”。注意使用数据库服务器时间还是应用服务器时间,需要统一,建议全部用应用服务器的时间。
第二个技巧是成绩单打印时自动合并同课程的不同教学班。高校里同一门课可能由多名老师分别开班,老师打印成绩时应该只看到自己班级的学生。course表里的teacher_id已经天然隔离了教学班,但导出时的文件名建议加上课程号和班级信息,例如“CS101-张三-成绩单.xlsx”,避免老师同时带两个班时下载文件重名。
第三个技巧是验证选课结果的数据一致性。系统上线前写一个简单的一致性检查脚本:
SELECT c.id, c.selected_count, COUNT(cs.id) AS actual_count FROM course c LEFT JOIN course_selection cs ON c.id = cs.course_id GROUP BY c.id, c.selected_count HAVING c.selected_count != actual_count;正常运行这个查询应该返回 0 行。如果返回了数据,说明有接口在某处漏了更新selected_count。把这个检查命令加入定时任务,每天凌晨跑一次,有问题发告警给管理员。这种自查脚本比人的记忆可靠得多,也能帮你快速发现并发回滚场景下不一致的角落。最后再补一个常用命令:清理过期选课页面缓存,如果是用 Nginx 缓存静态资源,选课期间最好关闭,否则学生看到的是缓存里的老数据,容易误以为课程名额没变。
本文还有配套的精品资源,点击获取