简介:基于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": "stu@example.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,三个参数都可选 List<Course> 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有一个容易忽略的坑:如果不加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.Adapter<CourseAdapter.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,参数以key=value形式拼接,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,email |
course | 课程表 | course_id,course_name,teacher_id,semester,max_count,selected_count |
select_course | 选课记录表 | id,student_id,course_id,select_time |
score | 成绩表 | id,student_id,course_id,score |
notice | 公告表 | 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() { List<Map<String, 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.host=smtp.qq.com mail.smtp.port=465 [mail.smtp.auth](http://mail.smtp.auth)=true mail.smtp.ssl.enable=true mail.username=your_account@qq.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?useSSL=false&characterEncoding=utf8"; 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/";真机调试则要改成电脑的局域网 IP:
private 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 |
| 邮件发送失败 535 | SMTP 授权码错误 | 改用邮箱授权码,不能用登录密码 |
| 成绩提交后无反应 | 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 List<Course> 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之前先同步 Cookie:
CookieManager 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;如果第一条或第三条查询返回空结果,说明选课和成绩的约束正确。第二条如果有不一致,就要检查选课接口中的事务是否真的提交了 —— 这是判断系统是否可靠的最快路径。以上这些点,结合源码自带的配置视频和讲解视频,两天内把环境跑通、把角色流程各演示一遍,是可以做到的。
本文还有配套的精品资源,点击获取