基于Android的校园网络选课系统:从角色权限到数据并发控制
简介基于Android平台的网络选课系统毕业设计源码面向高校计算机相关专业学生完整实现超级管理员、教务工作人员、教师与学生四类角色权限划分涵盖学生按学期/任课教师/课程名查找并选修课程、在线查看成绩教师查看选课学生名单和录入课程成绩教务查看选课人数并修改选课公告与停课通知管理员对所有基本信息及用户进行增删改查等功能是一套业务闭环清晰的教学管理项目。资源为ZIP压缩包约81.76MB内含2000个文件以PNG图片、XML布局、Java源码及JAR依赖库为主同时提供SQL数据库脚本、Gradle配置和项目结构说明文档便于导入Android Studio后直接理解工程架构。目前已有169人学习下载。压缩包内除源代码与MySQL数据库脚本外还附带一套完整配置视频流程讲解、项目结构说明文档、代码讲解视频以及软件配置好之后每次打开运行的流程视频可帮助从环境搭建、数据导入到代码调试全程参考适合毕业设计选题、二次开发或课程设计复用。1. 从教务排课到移动端选课这套 Android 网络选课系统到底解决了什么做过教务系统的人都知道选课这件事远不是“做一个列表加一个按钮”那么简单。学生要按学期、教师、课程名多条件检索教师要看自己课程下的选课名单并录入成绩教务要盯每门课的选课人数还要发停课通知超级管理员则要兜底所有用户和基础信息的增删改查。这套基于 Android 的网络选课系统把上述角色完整落地到了移动端后端用 Java Web 提供接口管理端和客户端共用 MySQL 数据库客户端通过 HTTP 与服务器交互。适合正在做毕业设计、需要一套“角色划分清晰 业务闭环完整 可演示可答辩”的 Android 项目的同学也适合想快速理解移动端选课系统权限模型和成绩流水的开发者。它不是那种只有一个登录页的 demo而是把选课、成绩录入、公告管理、邮箱找回密码这些真实教务场景串了起来拿去做课程设计或毕设二次开发省去从零搭表结构的时间。2. 角色权限模型与前后端交互先搞清楚数据流再谈界面2.1 四种角色的权限边界与菜单设计系统把用户划分为超级管理员、教务工作人员、教师、学生四类登录成功后返回的不只是用户名和密码校验结果还带着一个角色标识。我在拆这套源码时第一件事就是去看它的用户表设计通常是一张user表字段包含user_id、username、password、role、email等role用整数或字符串区分四种身份。常见的做法是登录接口返回 JSON格式大致如下{ code: 200, message: 登录成功, data: { userId: 1001, username: stu_2021, role: student, email: stuexample.com } }Android 端拿到role后用SharedPreferences缓存然后决定首页展示哪些菜单入口。比如学生端显示“课程查询”“我的选课”“成绩查询”教师端显示“我的课程”“成绩录入”教务端显示“选课统计”“公告管理”管理员端则是全部权限。这种“服务端校验 客户端控制展示”的方式在毕设里足够用但如果你要做得更严谨每个接口还需要在服务端再校验一次角色权限防止有人绕过客户端直接调接口。参数说明role字段值建议用super_admin、admin、teacher、student这样的字符串比数字 0/1/2/3 可读性强也方便在服务端写拦截器时做枚举匹配。2.2 课程检索与选课请求的状态流转学生端的核心操作是选课。源码里课程检索接口通常长这样GetMapping(/course/query) public Result queryCourse(RequestParam(required false) String semester, RequestParam(required false) String teacherName, RequestParam(required false) String courseName) { // 拼接动态 SQL三个参数都可选 ListCourse list courseMapper.selectByCondition(semester, teacherName, courseName); return Result.success(list); }Android 端通过Retrofit或Volley发起 GET 请求携带用户输入的查询条件。选课接口则是 POST参数为studentId和courseId服务端需要做三重校验课程是否存在、是否已选过、选课人数是否已满。-- 选课表 CREATE TABLE select_course ( id int(11) NOT NULL AUTO_INCREMENT, student_id int(11) NOT NULL, course_id int(11) NOT NULL, select_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stu_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个容易忽略的坑如果不加UNIQUE KEY同一学生重复提交选课请求时会产生重复记录必须在数据库层面兜底。我看到源码的建表 SQL 里是有这层约束的这也是我推荐直接使用它的原因之一。2.3 成绩录入与公告发布的数据流教师录入成绩是在课程详情页进入的接口大致为PostMapping(/score/update) public Result updateScore(RequestParam Integer studentId, RequestParam Integer courseId, RequestParam Double score) { // 校验分数范围 0-100可包含一位小数 scoreMapper.update(studentId, courseId, score); return Result.success(); }Android 端用一个EditText接收分数点击提交后调接口。这里要注意的是成绩表的主键设计一个学生一门课只能有一条成绩记录所以要么用student_id course_id做联合主键要么在score表上也加唯一约束。公告模块相对独立管理员和教务可以发公告所有用户登录后首页展示公告列表。数据结构就是notice表字段有title、content、publish_time、publisher客户端在登录成功后调一次GET /notice/list拉取最新几条即可。3. Android 客户端实现细节选课列表、状态刷新与本地缓存3.1 课程列表适配器与多条件查询课程列表页面用RecyclerView展示适配器里需要根据课程状态显示不同按钮文字。比如“已经选课”显示灰色“已选”“人数已满”显示“满员”“可选”显示红色“选课”。public class CourseAdapter extends RecyclerView.AdapterCourseAdapter.ViewHolder { Override public void onBindViewHolder(ViewHolder holder, int position) { CourseItem item list.get(position); holder.tvCourseName.setText(item.getCourseName()); holder.tvTeacher.setText(item.getTeacherName()); int status item.getStatus(); // 0可选 1已选 2满员 if (status 0) { holder.btnSelect.setEnabled(true); holder.btnSelect.setText(选课); holder.btnSelect.setTextColor(Color.RED); } else if (status 1) { holder.btnSelect.setEnabled(false); holder.btnSelect.setText(已选); } else { holder.btnSelect.setEnabled(false); holder.btnSelect.setText(满员); } } }逻辑说明onBindViewHolder中根据status字段动态更新按钮的可点击状态和文字。这里有一个细节 —— 适配器里的status来自服务器返回的 JSON它是由服务端在查询课程时通过子查询或关联查询算出来的SELECT c.*, CASE WHEN sc.id IS NOT NULL THEN 1 WHEN c.selected_count c.max_count THEN 2 ELSE 0 END AS status FROM course c LEFT JOIN select_course sc ON c.id sc.course_id AND sc.student_id #{studentId}注意这个LEFT JOIN的ON条件里带了sc.student_id #{studentId}只匹配当前学生的选课记录不会因为其他学生的选课而产生笛卡尔积膨胀。如果去掉这个条件会出现严重的性能问题。3.2 选课操作的异步处理与结果反馈点击“选课”按钮后不能直接在主线程发请求使用AsyncTask或Handler是毕设里的常规做法。new Thread(() - { String result HttpUtil.postForm(/course/select, studentId studentId courseId courseId); runOnUiThread(() - { JSONObject obj new JSONObject(result); if (obj.getInt(code) 200) { Toast.makeText(this, 选课成功, Toast.LENGTH_SHORT).show(); // 刷新当前列表或跳转到我的选课页 loadCourseList(); } else { Toast.makeText(this, obj.getString(message), Toast.LENGTH_SHORT).show(); } }); }).start();参数说明HttpUtil.postForm是源码里封装的一个工具类内部使用HttpURLConnection参数以keyvalue形式拼接Content-Type为application/x-www-form-urlencoded。这把网络请求的样板代码收敛到了一个类里便于替换为OkHttp。选课成功后刷新列表是因为服务端返回的课程状态已经变化。这里我建议本地不要缓存课程列表而是每次重新拉取保证人数和状态是最新的。3.3 我的课程与成绩查询页面“我的课程”页面调用的接口是GetMapping(/course/mine) public Result myCourses(RequestParam Integer studentId, RequestParam(required false) String semester) { // 根据学期筛选不传则查全部 return Result.success(courseMapper.selectByStudent(studentId, semester)); }Android 端用一个Spinner或下拉框让用户选择学期选择后重新请求接口并更新RecyclerView。成绩查询同理接口返回课程名、学分、分数、是否及格用一个单独的成绩列表适配器展示。这里有一个典型的做题思路用HashMap维护学期名和对应的semesterId当用户切换下拉选项时取选中的semesterId作为请求参数。代码不复杂但要保证在onItemSelected中判断position ! 0否则会重复触发请求。4. 服务端与数据库关键设计从 SQL 文件到业务接口4.1 数据库表结构与外键关系解读源码附带的是 MySQL 的 SQL 文件导入后可以看到以下核心表表名用途关键字段user用户表user_id,username,password,role,emailcourse课程表course_id,course_name,teacher_id,semester,max_count,selected_countselect_course选课记录表id,student_id,course_id,select_timescore成绩表id,student_id,course_id,scorenotice公告表id,title,content,publish_time注意course表里有个selected_count字段它是在选课成功时通过事务更新加 1。这里推荐用“数据库乐观锁 应用层判断”的方式处理并发选课超选问题-- 选课时先尝试更新课程人数如果当前人数小于上限才更新成功 UPDATE course SET selected_count selected_count 1 WHERE course_id #{courseId} AND selected_count max_count;然后用UPDATE的返回行数判断是否选课成功如果返回 0 说明已经满员。这个写法比先SELECT再UPDATE更适合并发场景也是这套源码里值得摘出来讲的知识点。4.2 教务端选课统计接口教务工作人员需要查看每一门课程的选课人数。接口实现思路是GetMapping(/admin/course/statistics) public Result courseStatistics() { ListMapString, Object list courseMapper.selectWithCount(); return Result.success(list); }对应的 SQL 语句如下SELECT c.course_id, c.course_name, c.teacher_name, c.max_count, COUNT(sc.id) AS actual_count FROM course c LEFT JOIN select_course sc ON c.course_id sc.course_id GROUP BY c.course_id ORDER BY actual_count DESC;这里用LEFT JOIN是因为有些课程可能还没有人选COUNT(sc.id)统计选课记录数不会因为LEFT JOIN产生 NULL 而漏掉课程。Android 端拿到数据后用MPAndroidChart绘制柱状图是加分项如果不想引入第三方库直接展示RecyclerView列表也完全能交差。4.3 邮箱找回密码的流程忘记密码通过邮箱验证设置新密码这个功能在毕设里经常被做成“发验证码但发不出去”因为很多同学没有配置邮件服务器。源码里用的是JavaMail通过 QQ 邮箱或网易邮箱的 SMTP 发送验证码。# mail.properties mail.smtp.hostsmtp.qq.com mail.smtp.port465 [mail.smtp.auth](http://mail.smtp.auth)true mail.smtp.ssl.enabletrue mail.usernameyour_accountqq.com mail.password你的授权码注意mail.password不是 QQ 邮箱的登录密码而是 SMTP 授权码需要在邮箱设置里生成。这个细节如果没注意邮件永远发送失败报 535 错误。Android 端的“忘记密码”页面用户输入邮箱后点击发送验证码服务端生成 6 位随机数并存储到 Redis 或内存 Map设置 5 分钟过期然后校验验证码后允许修改密码。如果你们的毕设环境没有 Redis用 ConcurrentHashMap 存储也可以但要加过期清理逻辑。5. 从下载源码到跑通演示导入步骤、常见报错与踩坑清单5.1 项目导入与数据库初始化拿到源码包后目录里应该有 Android 客户端工程、服务端工程、SQL 文件以及配置视频。先导入数据库mysql -u root -p select_course_system.sql然后用 Android Studio 打开客户端工程等待 Gradle 同步完成。服务端是 Java Web 项目推荐用 IntelliJ IDEA Tomcat 8.5 运行。关键配置在数据库连接文件里private static final String DB_URL jdbc:mysql://localhost:3306/select_course?useSSLfalsecharacterEncodingutf8; private static final String DB_USER root; private static final String DB_PASSWORD 123456;注意这里有个高发问题MySQL 8.x 的驱动包名是com.mysql.cj.jdbc.Driver而 MySQL 5.x 用的是com.mysql.jdbc.Driver如果版本不匹配启动时直接ClassNotFoundException。解决办法是换成对应版本的驱动 jar或者修改DB_URL和驱动类名。5.2 Android 模拟器访问本机服务的地址配置Android 模拟器里访问本机的localhost是无效的因为模拟器自己有一个回环地址。正确写法是// 10.0.2.2 是 Android 模拟器访问宿主机的固定地址 private static final String BASE_URL http://10.0.2.2:8080/select_course/;真机调试则要改成电脑的局域网 IPprivate static final String BASE_URL http://192.168.1.100:8080/select_course/;同时确保手机和电脑在同一个 WiFi 下并且服务端防火墙放行 8080 端口。我见过不少同学在这里卡住 —— 模拟器里能跑换真机就超时大多是 IP 没改或防火墙拦截。5.3 常见错误对照表错误现象原因处理方法登录后列表空白接口地址中项目名与 Tomcat 部署名不一致核对BASE_URL里的select_course是否与 Web 应用发布名一致选课提示“服务器忙”服务端事务回滚或 SQL 异常查看 Tomcat 控制台堆栈检查selected_count字段类型是否为 int邮件发送失败 535SMTP 授权码错误改用邮箱授权码不能用登录密码成绩提交后无反应HTTP 方法不对POST 写成了 GET检查 Retrofit 或 HttpUtil 的请求方式图片或样式加载不出来静态资源路径问题Android WebView 加载时需设置WebViewClient并允许混合内容操作步骤打开服务端控制台看日志把异常信息复制到搜索引擎基本能定位大部分问题。这套源码整体结构不复杂新手重点排查数据库连接和 IP 配置两项即可。6. 进阶玩法把选课系统从“能答辩”升级为“能上线”6.1 并发选课优化乐观锁 唯一索引双保险毕业设计演示时没有人会同时点选课按钮但如果你想把项目写到简历上并发控制必须提。前面提到用UPDATE course SET selected_count selected_count 1 WHERE selected_count max_count实现原子操作这一步已经能挡住大部分超选。但还有一种情况同一学生同时点多个设备的选课按钮产生两条select_course记录。解决办法是在建表时加唯一索引ALTER TABLE select_course ADD UNIQUE INDEX uk_student_course (student_id, course_id);然后在插入的代码里捕获DuplicateKeyException返回“你已经选过这门课”。这样即使应用层漏判数据库也会兜底。6.2 课程查找加模糊搜索和分页源码里的查询是精确匹配但实际使用中“按课程名查找”更合理的体验是模糊搜索。把 Service 里的参数解析改为public ListCourse selectByCondition(String semester, String teacherName, String courseName) { // 使用 LIKE 拼接注意 SQL 注入防护 return courseMapper.selectByCondition( semester, % teacherName %, % courseName % ); }同时给课程表加上分页GetMapping(/course/query) public Result queryCourse(RequestParam int page, RequestParam int pageSize) { // page 从 1 开始 int offset (page - 1) * pageSize; return courseMapper.selectByPage(offset, pageSize); }Android 端用无限滚动监听在onScrolled判断最后一个可见项是否接近底部是则加载下一页。这是我个人比较推荐的扩展点代码量不大但能显著提升项目的完整度。6.3 用 WebView 包装后台管理页面如果你觉得 Android 原生写超级管理员的增删改查页面太繁琐可以借用服务端已有的 JSP 页面。做法是在客户端加一个隐藏入口比如连续点击版本号 5 次打开WebView加载管理后台地址WebView webView new WebView(this); WebSettings settings webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); webView.setWebViewClient(new WebViewClient()); webView.loadUrl(BASE_URL admin/login.jsp);这样管理员的管理操作走浏览器模式学生和教师端保留原生体验。不过要注意WebView 的 Cookie 与原生请求的 Session 不共享需要在loadUrl之前先同步 CookieCookieManager cookieManager CookieManager.getInstance(); cookieManager.setAcceptCookie(true); cookieManager.setCookie(BASE_URL, JSESSIONID sessionId);这是一个很实用的技巧能把复杂的 CRUD 界面快速复用又不破坏移动端的轻量化交互。6.4 验证系统完整性的自查命令跑完所有流程后可以用下面几条 SQL 检查数据一致性-- 检查是否有重复选课 SELECT student_id, course_id, COUNT(*) FROM select_course GROUP BY student_id, course_id HAVING COUNT(*) 1; -- 检查课程实际选课人数与统计字段是否一致 SELECT c.course_id, c.selected_count, COUNT(sc.id) AS real_count FROM course c LEFT JOIN select_course sc ON c.course_id sc.course_id GROUP BY c.course_id HAVING c.selected_count ! real_count; -- 检查成绩表是否有多余记录 SELECT sc.student_id, sc.course_id, COUNT(*) FROM score sc GROUP BY sc.student_id, sc.course_id HAVING COUNT(*) 1;如果第一条或第三条查询返回空结果说明选课和成绩的约束正确。第二条如果有不一致就要检查选课接口中的事务是否真的提交了 —— 这是判断系统是否可靠的最快路径。以上这些点结合源码自带的配置视频和讲解视频两天内把环境跑通、把角色流程各演示一遍是可以做到的。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →