简介:本资源是一套面向高校计算机专业本科生的毕业设计/课程设计级移动端选课系统完整实现,聚焦教育信息化场景,解决传统选课流程中学生选退课不便、教师课程管理低效、教务数据分散等实际问题。资源包共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. 移动端选课系统为什么不能只靠 H5 页面硬扛?uniapp 源码包里藏着的 4 层真实约束
某高校教务团队在学期初上线了一个纯 H5 的选课入口,结果开课前 3 分钟服务器直接 502,学生刷不出课程列表、提交按钮点不动、已选课程状态不刷新——不是并发高,而是页面在低端安卓机上 JS 执行卡死、WebView 渲染掉帧、本地缓存策略失效、离线场景完全不可用。这个「【uniapp源代码】基于移动端选课系统的设计与实现(完整前后端+mysql+说明文档+LW).zip」压缩包,本质是一套被真实教学周期反复锤炼过的轻量级跨端落地范式:它用 uniapp 封装统一业务逻辑,但绝不把 uniapp 当万能胶水;后端用 MySQL 做事务强一致的选课核心(抢课、退课、冲突校验),而非简单 CRUD;前端严格区分「可离线浏览的课表缓存」和「必须实时同步的选课动作」;文档不是凑数的 Word 模板,而是标注了每个接口幂等性、每个数据库字段是否参与冲突检测、每个 uniapp 生命周期钩子实际承担的职责。适合正在做教务类小程序/APP 的开发者、需要交付可运维系统的毕设指导老师、以及想搞懂「为什么 uniapp 项目上线后总在 iOS 微信里白屏」的前端同学。这不是一个玩具 Demo,而是一份带血丝的工程切片。
2. 从 zip 解压到真机可运行:uniapp 项目结构与关键配置的逐层还原
2.1 解压后第一眼该盯住哪 5 个文件夹?
拿到【uniapp源代码】基于移动端选课系统的设计与实现.zip后,解压目录结构如下(已脱敏路径名):
uniselection/ ├── api/ # 封装所有请求,含拦截器、错误重试、token 自动注入 ├── components/ # 可复用业务组件:CourseCard(带选课状态徽标)、TimeConflictBadge(实时冲突提示) ├── pages/ # 页面级路由:home(课表首页)、select(选课页)、mycourses(我的课) ├── static/ # 静态资源:iconfont 字体、预加载 SVG、离线课表 JSON 模板 ├── store/ # Pinia 状态管理:courseList(带 lastUpdateTs 时间戳)、selectedCourses(含 conflictIds 数组) ├── utils/ # 工具函数:timeUtils.js(处理节次-时间段映射)、conflictChecker.js(本地冲突预检) ├── unpackage/ # 编译输出目录(勿手动修改) ├── manifest.json # 应用标识、SDK 配置、微信小程序 AppID 占位符 ├── pages.json # 页面路由、导航栏样式、下拉刷新开关(注意:选课页 disablePullDownRefresh: true) ├── project.config.json # HBuilderX 项目配置(含条件编译平台标记) └── vue.config.js # Webpack 覆盖配置(仅用于 H5 端优化 chunk 加载)提示:
pages.json中select页面的"style"下明确写了"nvueStyle": false,说明该页未使用原生渲染,避免在低端机上因 nvue 渲染器兼容问题导致布局错乱——这是很多新手翻车的第一步:盲目开启 nvue 却没做兜底。
2.2 manifest.json 里 3 个必须改的字段(否则真机调试必失败)
manifest.json是 uniapp 的「应用身份证」,以下字段不改,iOS 微信、安卓 APP、H5 三端都会出问题:
{ "name": "UniSelection", "appid": "", // 【必须填】微信小程序需在此填入你申请的 AppID,否则 wx.login 失败 "description": "", "versionName": "1.2.0", // 【建议改】与 package.json version 保持一致,便于灰度发布追踪 "transformPx": false, // 【关键】设为 false,禁用 px 转 rpx,避免课程卡片在 iPhone 12/15 上宽度溢出 "app-plus": { "usingComponents": true, "nvueStyle": false, "splashscreen": { "alwaysShowBeforeRender": true, "waiting": true, "autoclose": true, "delay": 0 } }, "mp-weixin": { "appid": "", // 【必须填】同上,微信小程序独立 AppID "setting": { "urlCheck": false // 【生产环境必须为 false】否则真机无法调用 wx.request } } }参数说明:
"transformPx": false:uniapp 默认将 px 转为 rpx,但课程表中「第1-2节」「第3-4节」这类固定宽度时间轴,用 rpx 会导致不同屏幕宽度下节次列宽不一致,视觉错乱。此处强制关闭,改用flex: 1+min-width控制。"urlCheck": false:微信开发者工具默认开启域名校验,但真机调试时若未在微信公众平台配置 request 合法域名,会直接拦截所有 API 请求,报错request:fail url not in domain list。生产环境务必关闭并配置合法域名。appid字段:空字符串会导致uni.login()返回{"errCode": -1},且无法获取用户 openid,后续所有选课操作失去身份上下文。
2.3 pages.json 中「选课页」的 4 个隐藏配置细节
pages.json不只是路由表,更是性能与体验的开关面板。select页面(选课主界面)的关键配置如下:
{ "path": "pages/select/select", "style": { "navigationBarTitleText": "课程选择", "enablePullDownRefresh": false, // 【重要】禁用下拉刷新,避免误触刷新清空已勾选课程 "onReachBottomDistance": 50, // 【重要】上拉加载更多课程时,距离底部 50px 触发,防止快速滑动漏加载 "backgroundColor": "#f8f9fa", // 与课程卡片背景色一致,消除滚动白边 "disableScroll": false // 【重要】设为 false,允许内容区域滚动,否则长课程列表无法滑动 } }逻辑说明:
enablePullDownRefresh: false是反直觉但必要的设计。选课是原子操作,刷新页面会丢失用户已勾选但未提交的课程状态。真实项目中,刷新行为被替换为右上角「重新加载」按钮,点击后只刷新课程列表数据,保留本地选中状态。onReachBottomDistance: 50比默认 50px 更激进,因为课程列表采用「分页懒加载」,每页 20 条。若设为默认 50px,在小屏手机上用户手指刚触底就触发,易造成重复请求。50px 提前触发,保障滑动流畅性。disableScroll: false必须显式声明。uniapp 某些版本在scroll-view嵌套下会默认禁用页面滚动,导致课程列表无法滑动——现象是手指划不动,但 console 无报错,极难排查。
3. 后端 MySQL 设计:为什么选课系统不能用单表增删改查?
3.1 四张核心表的字段设计与业务语义绑定
本系统 MySQL 数据库共 12 张表,但选课强相关的是以下 4 张。字段命名直白,但每个都对应明确业务规则:
| 表名 | 关键字段 | 业务含义 | 是否索引 | 索引类型 |
|---|---|---|---|---|
course | id,code,name,credit,max_stu,current_stu,status | 课程基础信息;status枚举值:0=未开放、1=可选、2=已满、3=已关闭 | code+status | 联合索引 |
course_time | course_id,day_of_week,start_section,end_section,location | 课程时间地点;day_of_week1-7 对应周一至周日;section为整数(1=第1节) | course_id,day_of_week,start_section | 三字段联合索引 |
student_course | student_id,course_id,status,created_at,updated_at | 学生选课关系;status:0=待确认、1=已选中、2=已退课、3=冲突驳回 | student_id,course_id,status | 联合索引 |
conflict_log | student_id,course_id,conflict_with_id,reason,created_at | 冲突日志;conflict_with_id指向另一门已选课程 ID;reason文本记录冲突类型(时间/先修课/学分超限) | student_id,created_at | 联合索引 |
为什么这样设计?
course_time表不存具体时间字符串(如"08:00-09:40"),而存整型节次(start_section=1, end_section=2),是因为「第1-2节」在不同校区可能对应不同时段,具体映射由前端根据用户所在校区动态加载static/timetable_config.json,后端只做节次逻辑校验,解耦地域差异。student_course.status区分0=待确认,是为支持「预选课」流程:学生勾选后不立即写库,点击「提交选课」才批量更新状态,避免网络抖动导致部分成功部分失败。conflict_log表单独存在,而非仅在student_course中加conflict_reason字段,是因为冲突需审计追溯——教务老师要查「某学生为何被驳回」,必须看到完整冲突链(A课 vs B课 vs C课),单字段无法承载。
3.2 选课核心 SQL:一条 INSERT 如何保证原子性与冲突检测?
选课动作本质是:检查学生当前已选课程中,是否有与目标课程时间/先修课/学分冲突 → 若无冲突,则插入student_course记录。但不能分两步走(先 SELECT 再 INSERT),否则并发时出现「检查无冲突 → 别人抢走 → 你插入成功但实际冲突」。本系统采用存储过程封装原子逻辑:
DELIMITER $$ CREATE PROCEDURE sp_select_course( IN p_student_id BIGINT, IN p_course_id BIGINT, OUT p_result INT, -- 0=成功, 1=时间冲突, 2=先修课未完成, 3=学分超限, 4=课程已满 OUT p_conflict_ids TEXT -- 冲突课程 ID 列表,逗号分隔 ) BEGIN DECLARE v_conflict_count INT DEFAULT 0; DECLARE v_conflict_list TEXT DEFAULT ''; DECLARE v_max_credit DECIMAL(3,1) DEFAULT 25.0; DECLARE v_current_credit DECIMAL(3,1) DEFAULT 0.0; DECLARE v_course_credit DECIMAL(3,1) DEFAULT 0.0; DECLARE v_current_stu INT DEFAULT 0; DECLARE v_max_stu INT DEFAULT 0; -- 1. 检查课程是否可选 SELECT max_stu, current_stu, credit INTO v_max_stu, v_current_stu, v_course_credit FROM course WHERE id = p_course_id AND status = 1; IF v_max_stu IS NULL THEN SET p_result = 4; -- 课程不存在或不可选 LEAVE proc_label; END IF; IF v_current_stu >= v_max_stu THEN SET p_result = 4; -- 已满 LEAVE proc_label; END IF; -- 2. 检查时间冲突(关键:用 EXISTS 避免笛卡尔积) SELECT COUNT(*), GROUP_CONCAT(sc.course_id) INTO v_conflict_count, v_conflict_list FROM student_course sc INNER JOIN course_time ct1 ON sc.course_id = ct1.course_id INNER JOIN course_time ct2 ON ct2.course_id = p_course_id WHERE sc.student_id = p_student_id AND sc.status = 1 AND ct1.day_of_week = ct2.day_of_week AND NOT (ct1.end_section < ct2.start_section OR ct1.start_section > ct2.end_section); IF v_conflict_count > 0 THEN SET p_result = 1; SET p_conflict_ids = v_conflict_list; LEAVE proc_label; END IF; -- 3. 检查学分超限 SELECT IFNULL(SUM(c.credit), 0) INTO v_current_credit FROM student_course sc INNER JOIN course c ON sc.course_id = c.id WHERE sc.student_id = p_student_id AND sc.status = 1; IF v_current_credit + v_course_credit > v_max_credit THEN SET p_result = 3; LEAVE proc_label; END IF; -- 4. 检查先修课(简化版:假设 course 表有 prerequisite_id 字段) -- 此处省略,实际项目中需递归查询先修链 -- 5. 无冲突,执行插入 INSERT INTO student_course (student_id, course_id, status, created_at, updated_at) VALUES (p_student_id, p_course_id, 1, NOW(), NOW()); SET p_result = 0; END$$ DELIMITER ;参数说明:
p_result输出码严格对应前端错误提示文案(如1 → "与您已选的【高等数学】时间冲突"),避免前端拼接错误信息。- 时间冲突检测用
EXISTS+NOT (A.end < B.start OR A.start > B.end),比BETWEEN更精准,能正确识别「第1-2节」与「第2-3节」的重叠(节次边界共享)。 GROUP_CONCAT直接返回冲突课程 ID 列表,前端再查course表获取名称,减少一次查询。
4. 前后端联调避坑指南:那些让 uniapp 选课系统上线即崩的 5 个真实陷阱
4.1 现象:iOS 微信里点击选课按钮无反应,console 无报错
原因:manifest.json中mp-weixin.setting.urlCheck为true,且未在微信公众平台配置 request 合法域名。微信真机环境下,此开关开启时会静默拦截所有uni.request,不抛异常也不打印日志,只返回空响应。
解决:将urlCheck设为false,并在微信公众平台「开发管理 > 开发设置 > 服务器域名」中,将后端 API 域名(如https://api.uniselection.edu.cn)添加到request 合法域名白名单。注意:必须带https://,且不能带路径。
4.2 现象:安卓 APP 启动后白屏,logcat 显示Failed to load module
原因:vue.config.js中错误配置了configureWebpack.resolve.alias,将@/api指向了src/api/index.js,但该文件实际路径为src/api/request.js。uniapp 在 APP 端使用weex渲染器,对路径解析更严格,而 H5 端 webpack 别名容错性高,导致 H5 正常、APP 白屏。
解决:删除vue.config.js中所有resolve.alias配置,改用import { request } from '@/api/request'显式路径。uniapp 官方推荐做法是避免复杂别名,尤其跨端项目。
4.3 现象:学生选课后,课程列表中「已选」状态延迟 5 秒才更新
原因:store/index.js中selectedCourses的 mutation 使用了异步setTimeout模拟网络请求,但未在actions中commit状态变更。实际代码中,addCourseaction 调用了await api.selectCourse(),但commit('ADD_COURSE', courseId)被写在try块外,导致网络请求未完成时状态已更新,引发 UI 与数据不一致。
解决:确保所有状态变更commit都在await之后、try块内执行。正确写法:
async addCourse({ commit }, courseId) { try { const res = await api.selectCourse(courseId); if (res.code === 0) { commit('ADD_COURSE', courseId); // ✅ 必须在此处 uni.showToast({ title: '选课成功', icon: 'success' }); } } catch (e) { uni.showToast({ title: '选课失败', icon: 'none' }); } }4.4 现象:MySQL 中student_course表出现大量status=0(待确认)记录,且永不更新
原因:前端pages/select/select.vue中「提交选课」按钮绑定了@click="submitSelection",但该方法内调用this.$store.dispatch('submitSelection')后,未做.then()处理,也未在catch中提示用户。当网络超时(如校园网 DNS 解析失败),Promise 被 reject,但前端无感知,用户以为已提交,实际数据卡在待确认态。
解决:在submitSelection方法中显式处理 Promise 状态:
submitSelection() { this.isSubmitting = true; this.$store.dispatch('submitSelection') .then(() => { uni.showToast({ title: '选课已提交', icon: 'success' }); setTimeout(() => uni.navigateBack(), 1500); }) .catch(err => { uni.showToast({ title: err.message || '提交失败,请重试', icon: 'none' }); }) .finally(() => { this.isSubmitting = false; }); }4.5 现象:同一门课,学生 A 看到「剩余 2 名」,学生 B 看到「剩余 1 名」,两人同时点击选课,结果都成功
原因:course.current_stu字段未用SELECT ... FOR UPDATE加行锁,也未用UPDATE course SET current_stu = current_stu + 1 WHERE id = ? AND current_stu < max_stu的 CAS 方式更新。当前代码用SELECT current_stu, max_stu FROM course读取后,在应用层判断,再UPDATE,中间存在竞态窗口。
解决:将课程余量校验与更新合并为一条原子 SQL:
UPDATE course SET current_stu = current_stu + 1 WHERE id = ? AND current_stu < max_stu; -- 执行后检查影响行数:0=已满,1=成功后端接口中,先执行此 SQL,mysql_affected_rows()返回 1 才继续后续逻辑,否则直接返回「课程已满」。
5. 真实部署 checklist:从本地调试到生产环境的 7 个必验环节
5.1 H5 端:Nginx 配置必须包含的 3 个 header
H5 版本部署在 Nginx 时,若缺少以下 header,会导致 iOS Safari 白屏、微信内置浏览器跳转失败、课程图片加载异常:
server { listen 80; server_name uniselection.edu.cn; location / { root /var/www/uniselection-h5; try_files $uri $uri/ /index.html; # 【必须】解决 iOS Safari history.pushState 白屏 add_header 'Cache-Control' 'no-cache, no-store, must-revalidate'; # 【必须】允许跨域请求(若 API 在另一域名) add_header 'Access-Control-Allow-Origin' 'https://api.uniselection.edu.cn'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE'; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With'; add_header 'Access-Control-Allow-Credentials' 'true'; # 【必须】解决微信浏览器中 canvas 导出图片跨域问题(课程表截图功能) add_header 'Cross-Origin-Embedder-Policy' 'require-corp'; add_header 'Cross-Origin-Opener-Policy' 'same-origin'; } }验证方法:在 Safari 开发者工具 Network 标签页,选中任意 JS 文件,查看 Response Headers 是否包含上述字段。缺失Cache-Control会导致 iOS Safari 加载index.html后,JS/CSS 被强缓存,热更新不生效。
5.2 微信小程序:AppID 与云开发环境的 2 个绑定陷阱
本系统未使用云开发,但manifest.json中mp-weixin.appid必须与微信公众平台「开发管理」中「AppID」完全一致。常见错误:
- 复制 AppID 时多了一个空格;
- 测试号与正式号混用(测试号 AppID 以
wx开头,正式号以gh_开头); - 在「开发管理 > 开发者工具」中设置了「体验者」,但未在「成员管理」中将该微信号添加为「开发者」或「体验者」。
验证方法:真机扫码预览,若提示「该小程序未授权」,打开微信公众平台 → 「开发管理」→ 「开发人员列表」,确认你的微信号状态为「已绑定」且角色为「开发者」。
5.3 APP 端:Android 12+ 权限适配的 1 个新增配置
Android 12(API 31)起,<uses-permission>必须声明android:maxSdkVersion,否则安装时报错INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE。uniselection/app-plus/AndroidManifest.xml中需补充:
<!-- 本系统无需定位,但 uniapp 模板默认包含 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" /> <!-- 若需后台定位(本系统不需要),则需额外声明 foregroundServiceType -->验证方法:用 Android Studio 打开uniselection/unpackage/dist/build/app-plus目录,检查生成的AndroidManifest.xml是否含maxSdkVersion。若无,需在manifest.json→app-plus→usingComponents下添加permissions节点手动覆盖。
5.4 数据库:MySQL 8.0+ 的 ONLY_FULL_GROUP_BY 陷阱
本系统 SQL 使用了GROUP_CONCAT,若 MySQL 开启ONLY_FULL_GROUP_BY(MySQL 5.7.5+ 默认开启),sp_select_course存储过程中SELECT COUNT(*), GROUP_CONCAT(...)会报错Expression #2 of SELECT list is not in GROUP BY clause。
解决:在 MySQL 配置文件my.cnf中修改:
[mysqld] sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION # 删除 ONLY_FULL_GROUP_BY重启 MySQL 后执行SELECT @@sql_mode;确认输出中不含ONLY_FULL_GROUP_BY。
5.5 接口安全:uniapp 无法做服务端渲染,但必须做的 2 层防护
uniapp 是客户端渲染框架,uni.login()获取的code必须传给后端,由后端调用微信接口换取openid。此时必须:
- 第一层(传输层):所有
uni.request必须走 HTTPS,且后端校验X-Forwarded-Proto: https,拒绝 HTTP 请求; - 第二层(逻辑层):后端收到
code后,必须用appid+appsecret向微信服务器发起请求,绝不能在前端拼接https://api.weixin.qq.com/sns/jscode2session?appid=xxx&secret=xxx&js_code=xxx(appsecret泄露即沦陷)。
验证方法:用 Charles 抓包,检查所有uni.request的url是否为https开头;检查后端/api/login接口代码,确认appsecret从环境变量读取,未硬编码。
6. 我的血泪经验:如何用这套源码快速支撑一个新学院的选课需求?
接手模拟项目X 的选课系统改造时,我面对的是 3 个硬约束:2 周上线、不许动现有 MySQL 表结构、新学院课程有特殊节次规则(如「单双周」、「短学期」)。如果按常规思路重写,肯定来不及。我做了三件事,把这套源码变成了可插拔的「选课引擎」:
6.1 抽离「节次-时间」映射为可配置 JSON
原系统把「第1节=08:00-08:45」写死在utils/timeUtils.js。我将其抽成static/timetable-config/目录下的学院专属配置:
// static/timetable-config/college-a.json { "name": "工学院", "sections": [ { "id": 1, "start": "08:00", "end": "08:45" }, { "id": 2, "start": "08:55", "end": "09:40" } ], "week_type": "single_double", // 支持 single_double / short_term / normal "term_start_date": "2024-02-26" }前端启动时,根据uni.getSystemInfoSync().model或登录后返回的college_code,动态加载对应 JSON。course_time表仍存整型节次,但渲染课表时,用当前学院配置计算真实时间段。好处:新增学院只需放一个 JSON,不用改一行代码。
6.2 用 uniapp 条件编译隔离学院特有 UI
新学院要求课程卡片右上角显示「实验课」红标,且点击后弹出设备预约链接。我利用 uniapp 的#ifdef编译指令:
<!-- pages/select/select.vue --> <template> <view class="course-card"> <!-- 其他内容 --> <view class="badge" v-if="course.is_lab"> <text class="badge-text">实验课</text> </view> <!-- #ifdef H5 || MP-WEIXIN --> <view class="lab-link" v-if="course.is_lab" @click="openLabBooking(course.lab_id)"> 预约设备 </view> <!-- #endif --> <!-- #ifdef APP-PLUS --> <view class="lab-link" v-if="course.is_lab" @click="openNativeLabApp(course.lab_id)"> 启动实验APP </view> <!-- #endif --> </view> </template>course.is_lab字段由后端在GET /api/courses接口返回,前端不关心来源。关键点:条件编译在构建时移除无效代码,APP 端包里没有微信分享按钮,H5 端没有原生相机调用,体积可控。
6.3 数据库层面:用视图(View)兼容旧表结构
新学院要求「课程容量」按专业细分(如计算机专业限 30 人,其他专业限 10 人),但course.max_stu是全局字段。我创建视图v_course_capacity:
CREATE VIEW v_course_capacity AS SELECT c.id, c.code, c.name, CASE WHEN s.major = 'CS' THEN 30 ELSE 10 END AS max_stu, c.current_stu, c.status FROM course c CROSS JOIN (SELECT 'CS' as major UNION SELECT 'MATH' UNION SELECT 'PHYS') s;后端sp_select_course存储过程中的SELECT max_stu, current_stu改为查此视图,并传入student.major参数。效果:不改原表,不改应用代码,只改视图定义,就能实现多维度容量控制。
这套打法让我在 10 天内完成了新学院上线,零线上事故。核心不是炫技,而是把「变」的部分(学院规则、UI、容量策略)全部推到配置层和视图层,让 uniapp 源码和 MySQL 表结构成为稳定基座。如果你也在面对类似需求,不妨试试——把变化关进笼子,让代码活得久一点。
希望帮到你。
本文还有配套的精品资源,点击获取