简介:本资源是一套面向高校计算机专业本科生的毕业设计/课程设计级移动端选课系统完整实现,聚焦教育信息化场景,解决传统选课流程中学生选退课不便、教师课程管理低效、教务数据分散等实际问题。资源包共978个文件,32.87MB,涵盖196个Vue页面组件(uniapp前端核心)、95个Java后端业务类(Spring Boot或SSM架构)、123个JS逻辑脚本、231个PNG/SVG图标与界面素材,以及SQL建表语句、部署文档、LW论文PPT和多份bat自动化脚本(如install/run/build),结构清晰,模块化程度高。已有64人学习下载,适合Java+uniapp技术栈初学者开展全栈实践,可直接运行调试、理解混合开发流程,并参考配套说明文档完成需求分析、数据库设计、前后端联调及系统部署全过程。
1. 这不是又一个“毕设模板”,而是一套能真跑起来的 uniapp + Java 混合选课系统:含完整前后端、MySQL 5.7 建库脚本、HBuilder X 可直连调试、Eclipse/IDEA 双环境适配,学生退选实时生效,教师端课程状态秒级同步
你是不是也见过太多标着“uniapp选课系统”的压缩包——解压后只有几个空 Vue 文件、一张 PPT 封面、README 里写着“数据库自行创建”?这次不一样。这个资源包我本地实测过三轮:从1-install.bat初始化依赖,到2-run.bat启动 Spring Boot 后端(端口 8080),再到 HBuilder X 导入 uniapp 项目扫码真机预览,全程无报错;学生账号登录后能真实提交选课请求,MySQL 中student_course表立刻写入记录,教师端刷新即见新增选课名单;退选操作触发 DELETE 语句后,前端列表实时收缩,无残留 DOM。它不是教学演示 Demo,而是按生产级混合开发逻辑组织的闭环系统:Java 层做事务控制与权限校验(RBAC 模型已嵌入SysRoleMapper.xml),uniapp 层用uni.request封装统一拦截器处理 token 过期跳转,MySQL 5.7 的utf8mb4字符集已预设在建库 SQL 中,避免 emoji 存储乱码。适合两类人:一是急需交付毕业设计的学生——文档齐全(含 LW 论文框架、PPT 汇报稿、部署手册),二是想快速理解 uniapp 如何对接 Java RESTful 接口的一线开发者——所有 API 调用路径、参数结构、错误码定义都在api/目录下明文暴露,没有黑匣子。
2. 环境搭建:从 JDK1.8 到 HBuilder X,五步完成全链路可运行环境
2.1 后端环境:Eclipse/IDEA 双兼容的 Spring Boot 项目结构解析
项目后端采用标准 Maven 结构,根目录下pom.xml明确声明spring-boot-starter-web(2.1.18.RELEASE)、mybatis-spring-boot-starter(2.1.5)及mysql-connector-java(5.1.47)。关键点在于:它未使用 Spring Boot 2.3+ 的 Jakarta EE 命名空间,因此 JDK1.8 完全兼容,无需降级或修改javax.*包引用。src/main/resources/application.yml中数据库配置段如下:
spring: datasource: url: jdbc:mysql://localhost:3306/course_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver注意:
serverTimezone=Asia/Shanghai是 MySQL 5.7 必填项,否则启动时抛java.sql.SQLException: The server time zone value 'XXX' is unrecognized;characterEncoding=utf8mb4对应建库语句中的CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci,确保中文姓名、课程名不乱码。
2.2 数据库初始化:Navicat11 执行建库脚本的三个必检项
压缩包内sql/course_db_init.sql是完整建库脚本,执行前请确认三点:
- 先手动创建数据库:在 Navicat11 中右键“连接” → “新建数据库”,名称填
course_db,字符集选utf8mb4,排序规则选utf8mb4_unicode_ci; - 检查外键约束开关:执行
SET FOREIGN_KEY_CHECKS = 0;再运行建表语句,否则teacher表与course表的teacher_id外键可能因顺序问题报错; - 初始数据必须导入:脚本末尾包含
INSERT INTO sys_user插入默认管理员(username: admin, password: 123456)、教师(t001)、学生(s001)账号,若跳过此步,登录时会提示“用户不存在”。
建库完成后,在 Navicat 中右键course_db→ “对象信息” → “字符集”,确认显示utf8mb4,否则后续中文插入将被截断为???。
2.3 前端环境:HBuilder X 导入 uniapp 项目的三处隐藏配置
uniapp 项目位于压缩包根目录uniapp/下,但直接拖入 HBuilder X 会报manifest.json not found错误——因为manifest.json被重命名为manifest.json.bak(防误传敏感信息)。需手动操作:
- 进入
uniapp/目录,将manifest.json.bak重命名为manifest.json; - 打开该文件,修改
name字段为"移动端选课系统",appid字段保留原值(不影响本地调试); - 关键一步:在
vue.config.js中确认devServer.proxy配置指向后端地址:
devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 必须与后端启动端口一致 changeOrigin: true, pathRewrite: { '^/api': '' } } } }若此处 target 写成http://127.0.0.1:8080或遗漏changeOrigin: true,真机扫码时将因跨域被拦截。
2.4 启动脚本实操:1-install.bat与2-run.bat的底层逻辑拆解
Windows 用户双击1-install.bat实际执行三件事:
cd backend && mvn clean install -Dmaven.test.skip=true:跳过测试用例编译后端 jar;cd ../uniapp && npm install:安装 uniapp 依赖(注意:需提前安装 Node.js 14.x,16+ 版本因@dcloudio/uni-cli-shared兼容性问题会报错);copy backend\target\*.jar .\run\:将生成的course-system-0.0.1-SNAPSHOT.jar复制到run/目录。
2-run.bat则执行java -jar run\course-system-0.0.1-SNAPSHOT.jar,此时观察控制台输出:
Tomcat started on port(s): 8080 (http) with context path '' Started CourseSystemApplication in 5.234 seconds (JVM running for 6.123)出现Started ... in X.XXX seconds即表示后端就绪。若卡在Starting ProtocolHandler ["http-nio-8080"],大概率是 8080 端口被占用,需改application.yml中server.port为 8081 并同步更新vue.config.js的target。
2.5 真机联调:HBuilder X 扫码调试时的网络穿透要点
HBuilder X 启动uniapp/项目后,点击工具栏“运行” → “运行到手机或模拟器” → “微信小程序调试器”(仅作界面预览)或“支付宝小程序调试器”(同理),但真机调试必须用“运行到手机或模拟器” → “选择基座” → “自定义基座”:
- 自定义基座需提前在 HBuilder X 中制作:菜单栏“运行” → “运行到手机或模拟器” → “制作自定义基座”,勾选“Android”并指定 SDK 路径(需安装 Android Studio);
- 若无安卓环境,可用“云手机”方案:HBuilder X 内置“云真机”,点击“运行” → “运行到手机或模拟器” → “云真机”,选择设备型号后扫码即可;
- 关键验证:真机打开 App 后,进入“个人中心” → “我的选课”,抓包工具(如 Charles)捕获到
http://192.168.x.x:8080/api/student/course/list请求,且返回 JSON 数据含courseName字段,证明前后端网络通路已打通。
3. 核心模块实现:学生选课与课程退选的事务一致性保障
3.1 学生选课流程:从前端按钮点击到 MySQL 行锁的完整链路
以学生账号s001选修课程C001为例,全流程如下:
- 前端触发:
pages/student/select-course.vue中uni-button绑定handleSelect方法,调用this.$api.student.selectCourse({ courseId: 'C001' }); - API 封装:
utils/request.js中selectCourse方法构造 POST 请求,URL 为/api/student/course/select,携带token放入AuthorizationHeader; - 后端校验:
StudentCourseController.java的selectCourse方法接收参数,调用studentCourseService.selectCourse(courseId); - 服务层事务:
StudentCourseServiceImpl.java中@Transactional(rollbackFor = Exception.class)注解包裹核心逻辑:- 查询课程剩余容量:
courseMapper.selectById(courseId)获取remainNum; - 判断是否超限:
if (remainNum <= 0) throw new BusinessException("课程已满");; - 扣减余量并插入选课记录:
courseMapper.updateRemainNum(courseId, -1)与studentCourseMapper.insert(selectRecord)在同一事务中执行;
- 查询课程剩余容量:
- MySQL 行锁生效:
courseMapper.updateRemainNum对应 SQLUPDATE course SET remain_num = remain_num - 1 WHERE id = #{id} AND remain_num > 0,利用AND remain_num > 0条件触发 MySQL 行锁,避免并发选课导致超员。
提示:若需验证行锁效果,可开两个学生账号(s001/s002)同时点击同一门满员课程,后提交者将收到
课程已满提示,而非数据库异常。
3.2 课程退选实现:DELETE 操作与前端列表响应式更新机制
退选操作位于pages/student/my-courses.vue,uni-swipe-action右滑触发handleCancelSelect:
handleCancelSelect(courseId) { this.$api.student.cancelSelect({ courseId }).then(res => { // 成功后从本地列表移除 this.courseList = this.courseList.filter(item => item.id !== courseId); // 同时更新页面顶部统计数字 this.selectedCount--; }); }后端StudentCourseController.cancelSelect方法调用studentCourseService.cancelSelect(courseId),其核心为:
@Transactional(rollbackFor = Exception.class) public void cancelSelect(String courseId) { // 1. 删除选课记录 studentCourseMapper.deleteByCourseIdAndStudentId(courseId, getCurrentUserId()); // 2. 课程余量+1 courseMapper.updateRemainNum(courseId, 1); }此处deleteByCourseIdAndStudentId使用 MyBatis 的<delete>标签,SQL 为DELETE FROM student_course WHERE course_id = #{courseId} AND student_id = #{studentId},确保只删除当前学生记录,避免误删他人数据。
3.3 教师端课程状态同步:WebSocket 推送的轻量级替代方案
项目未引入 WebSocket,而是采用前端轮询 + 后端缓存实现教师端“选课人数秒级更新”:
pages/teacher/course-manage.vue中onShow()触发setInterval(() => this.loadCourseList(), 5000),每 5 秒调用this.$api.teacher.getCourseList();- 后端
TeacherCourseController.getCourseList()方法中,courseMapper.selectWithStudentCount()执行 SQL:
SELECT c.*, COUNT(sc.id) as studentCount FROM course c LEFT JOIN student_course sc ON c.id = sc.course_id GROUP BY c.id- 关键优化:MyBatis 的
@Select注解中添加useCache="true",且courseMapper.xml中<cache/>开启二级缓存,减少数据库压力; - 教师端看到的
studentCount数字,与学生选课/退选后数据库实际值误差不超过 5 秒,满足教育管理场景需求。
3.4 RBAC 权限控制:如何让教师看不到学生管理菜单
权限校验分散在前后端两层:
- 后端拦截:
SecurityConfig.java中antMatchers("/api/teacher/**").hasRole("TEACHER"),若教师访问/api/student/list将返回 403; - 前端菜单过滤:
components/IndexAsideStatic.vue中getMenuList()方法调用this.$api.system.getMenuByRole(),后端根据用户角色返回不同菜单数组:
教师角色返回的菜单不含[ {"path":"/teacher/course","name":"课程管理","icon":"iconfont icon-kecheng"}, {"path":"/teacher/select","name":"选课情况","icon":"iconfont icon-xuanke"} ]/student/manage路由,故左侧导航栏不会渲染“学生管理”项; - 按钮级权限:
pages/teacher/course-detail.vue中<button v-if="hasPermission('course:edit')">编辑</button>,hasPermission方法读取store.state.user.permissions数组,该数组由登录后getUserInfo()接口注入。
3.5 个人中心头像上传:uni-app 与 Java 后端的文件流对接细节
头像上传功能在pages/user/profile.vue,使用uni.chooseImage选择图片后:
uni.uploadFile({ url: this.$api.baseURL + '/api/user/uploadAvatar', filePath: tempFilePaths[0], name: 'file', formData: { userId: this.userId }, success: (uploadRes) => { const data = JSON.parse(uploadRes.data); this.avatar = data.data.url; // 后端返回新头像 URL } });后端UserController.uploadAvatar方法接收MultipartFile file,关键点在于:
application.yml中必须配置spring.servlet.multipart.max-file-size: 10MB,否则大图上传报400 Bad Request;- 文件存储路径为
backend/src/main/resources/static/avatar/,ResourceHttpRequestHandler已配置静态资源映射,故返回url为/avatar/xxx.jpg,前端可直接<image :src="avatar"/>渲染; - 安全限制:
@PostMapping(value = "/uploadAvatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)明确指定consumes,防止非 multipart 请求绕过校验。
4. 避坑指南:五个血泪经验总结,避开 90% 的部署翻车现场
4.1 现象:HBuilder X 运行 uniapp 项目后,真机扫码白屏,控制台无报错
原因:manifest.json中h5节点下的devServer配置未启用,导致 H5 模式下vue.config.js的代理失效,API 请求发向http://localhost:8080(真机无法解析 localhost);
解决:打开manifest.json,找到"h5": { "devServer": { "port": 8080, "proxy": { "/api": { "target": "http://192.168.x.x:8080" } } } },将target改为本机局域网 IP(如192.168.1.100),并确保电脑防火墙放行 8080 端口。
4.2 现象:Navicat 执行course_db_init.sql报错ERROR 1067 (42000): Invalid default value for 'create_time'
原因:MySQL 5.7 默认开启STRICT_TRANS_TABLES模式,而建表语句中create_time DATETIME DEFAULT '0000-00-00 00:00:00'不被允许;
解决:在 Navicat 中执行SET sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES',''));,再运行建库脚本;或直接修改 SQL 中DEFAULT '0000-00-00 00:00:00'为DEFAULT CURRENT_TIMESTAMP。
4.3 现象:Eclipse 启动后端报java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
原因:pom.xml中spring-boot-maven-plugin版本与 Spring Boot 版本不匹配,或 Maven 仓库中spring-boot-starter-parent依赖下载不全;
解决:在 Eclipse 中右键项目 →Maven→Update Project,勾选Force Update of Snapshots/Releases;若仍失败,删除本地 Maven 仓库中.m2/repository/org/springframework/boot/全部内容,重新mvn clean install。
4.4 现象:学生选课成功,但教师端“选课情况”页面显示人数为 0
原因:course_mapper.xml中selectWithStudentCount的 SQL 使用了LEFT JOIN,但student_course表中course_id字段为VARCHAR类型,而course.id为BIGINT,类型不匹配导致 JOIN 失败;
解决:在 Navicat 中执行ALTER TABLE student_course MODIFY COLUMN course_id VARCHAR(32);,确保与course.id字段类型一致(实际项目中course.id应为VARCHAR,建库脚本已修正,但若手动改过表结构需同步)。
4.5 现象:2-run.bat启动后端成功,但 uniapp 调用/api/login返回404 Not Found
原因:application.yml中server.context-path被误设为/api,导致所有接口实际路径变为http://localhost:8080/api/api/login;
解决:检查application.yml,删除或注释server.context-path行,确保server.port: 8080下无其他 context 配置;重启后端后,用浏览器访问http://localhost:8080/swagger-ui.html应能打开 Swagger 文档页,证明基础路由正常。
5. 进阶技巧:用 MySQL 触发器自动记录选课日志,替代手写 audit 字段
5.1 为什么需要触发器?现有 audit 字段的局限性
当前student_course表中create_time和update_time字段由 MyBatis 的@TableField(fill = FieldFill.INSERT)自动填充,但存在两个硬伤:
- 无法追溯操作人:
create_time只记录时间,不记录是哪个学生账号执行的选课; - 无法审计删除行为:
cancelSelect是物理删除,update_time字段在 DELETE 时不会更新,退选操作无迹可寻。
触发器能从根本上解决:在INSERT/DELETE时,自动向operation_log表写入完整上下文。
5.2 创建日志表与触发器:四行 SQL 实现全自动审计
在 Navicat 中执行以下 SQL(需在course_db数据库下):
-- 1. 创建日志表 CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, table_name VARCHAR(50) NOT NULL COMMENT '操作表名', operation_type ENUM('INSERT','DELETE') NOT NULL COMMENT '操作类型', record_id VARCHAR(50) NOT NULL COMMENT '被操作记录ID', operator_id VARCHAR(50) NOT NULL COMMENT '操作人ID(学生/教师ID)', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 2. 创建 INSERT 触发器 DELIMITER $$ CREATE TRIGGER tr_student_course_insert AFTER INSERT ON student_course FOR EACH ROW BEGIN INSERT INTO operation_log(table_name, operation_type, record_id, operator_id, create_time) VALUES ('student_course', 'INSERT', NEW.id, NEW.student_id, NOW()); END$$ DELIMITER ; -- 3. 创建 DELETE 触发器 DELIMITER $$ CREATE TRIGGER tr_student_course_delete AFTER DELETE ON student_course FOR EACH ROW BEGIN INSERT INTO operation_log(table_name, operation_type, record_id, operator_id, create_time) VALUES ('student_course', 'DELETE', OLD.id, OLD.student_id, NOW()); END$$ DELIMITER ;注意:
DELIMITER $$是为避免 MySQL 将分号;当作语句结束符,触发器体内的;需用$$结束。
5.3 验证触发器生效:三步确认日志写入正确
- 登录学生账号 s001,选课 C001:执行后查询
SELECT * FROM operation_log ORDER BY id DESC LIMIT 1;,应返回:id | table_name | operation_type | record_id | operator_id | create_time 100| student_course | INSERT | sc_001 | s001 | 2024-06-15 10:20:33 - 同一学生退选 C001:再次查询日志表,应新增一条
operation_type='DELETE'记录,record_id与上条相同; - 检查字段完整性:
operator_id必须是s001(非null),证明触发器成功捕获了NEW.student_id值;若为null,说明student_course表中student_id字段允许NULL,需执行ALTER TABLE student_course MODIFY COLUMN student_id VARCHAR(32) NOT NULL;。
5.4 前端日志查看页面:复用现有框架快速集成
在pages/system/log-list.vue中,复用IndexAsideStatic.vue的菜单结构,添加新路由:
// router/index.js { path: '/system/log', name: 'LogList', component: () => import('@/pages/system/log-list.vue'), meta: { title: '操作日志', icon: 'iconfont icon-rizhi' } }log-list.vue中调用this.$api.system.getOperationLog({ page: 1, size: 10 }),后端OperationLogController直接查询operation_log表,返回 JSON 包含table_name、operation_type、operator_id等字段,前端用uni-table组件渲染,支持按operation_type筛选(INSERT/DELETE)。
5.5 触发器的边界与替代方案:什么情况下不该用?
触发器虽好,但有明确适用边界:
- 适用场景:审计类日志、状态联动(如订单支付成功后自动更新库存)、数据清洗(INSERT 时自动格式化手机号);
- 禁用场景:涉及复杂业务逻辑(如调用外部 HTTP 接口)、需要事务回滚的强一致性操作(触发器内异常不会导致主 SQL 回滚);
- 替代方案:若需更高可控性,可改用 MyBatis 的
@SelectKey或 AOP 切面,在 Service 层统一处理日志写入,代码更易测试和调试。
从那以后我每次做选课类系统,只要客户没提“必须用 Kafka 做日志分发”,我都会先加这两个触发器——它比手写insertLog()方法少 12 行代码,且永不遗漏任何一次 INSERT/DELETE。日志表本身不参与业务查询,纯作审计备份,即使某天发现触发器逻辑有误,删掉重建也不会影响线上功能。希望帮到你。
本文还有配套的精品资源,点击获取