ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android Studio学生选课系统开发全解析:从数据库设计到事务与并发控制

Android Studio学生选课系统开发全解析:从数据库设计到事务与并发控制 简介这是一份基于Android Studio开发的学生选课系统项目资源适合正在学习Android开发、希望从完整项目中掌握工程结构与常见技术要点的学习者。压缩包共620个文件体积约56.75MB涵盖java源代码、xml界面与资源文件、json配置、class与dex编译产物、apk安装包以及gradle构建脚本等类型既有可直接阅读的源码也有可供运行的构建结果便于对照学习。内容围绕选课场景展开涉及RecyclerView课程列表、SQLite数据持久化、网络通信、Loader与LiveData等知识点能够帮助读者理解Android应用的UI设计、数据存储与异步处理流程。目前已有2084人学习下载对于需要项目实战参考或课程设计借鉴的开发者来说是一份可落地的参考资料。1. 项目全貌先搞清楚你要做的是一个什么系统先说个题外话。“androidstdio”这个拼写我见的频率相当高实际上正确写法是Android Studio不过这并不影响我们理解这个题目的本质——它是计算机软工类课程设计里出现率极高的一道题一个跑在 Android 手机上的学生选课系统。很多读者第一次看到这个题目时脑子里的第一反应是“我要做一个 APP 给全校学生选课用”然后开始幻想登录、选课、退课、课表、消息推送、管理后台全套上齐。真这么做大概率会在中期检查时翻车。冷静下来拆解一下这个项目的核心其实很聚焦学生端是主战场日常操作无非是登录、浏览课程、选课、退课、查看已选课程管理端即便要做也不是重点能有一个简单地添加课程、查看选课人数的入口就够了。整体数据量级也就是“几百个学生、几十门课程”的水准完全不需要分布式架构不需要消息队列更不需要微服务。想清楚这一点你选择的每一项技术都是为了“快速、稳定、能演示、能答辩”而不是为了炫技。我在指导这类项目时经常和学生说一句话选课系统这事本质上就是一个“带约束条件的购物车”。用户浏览商品课程把商品加入购物车选课但购物车有规则——库存不能超课程容量同一时间段不能加两件“同类型冲突商品”课程时间冲突。一旦你把这个类比放在脑子里整个系统的逻辑边界就非常清晰了写代码的时候也不会东一榔头西一棒槌。这篇博文适合正在做课程设计、毕业设计或者想拿一个完整 Android 项目练手的朋友。我会从需求、技术选型、数据库设计、核心代码实现到排坑经验按一个过来人的视角讲透。文章中涉及的具体实现方案是我基于大量同类项目总结出的最稳妥路线你完全可以照着落地。2. 核心设计把数据库和核心流程先理顺2.1 三张表打天下学生、课程、选课记录数据库设计是这个项目最不该偷懒的模块。我见过太多人上来就建了七八张表角色表、权限表、院系表、教师表……最后发现用到的就三张。实际上一个典型的学生选课系统三张表足够覆盖所有核心场景。第一张是student学生表字段基本就是学号、姓名、密码、班级。学号建议设为主键既是登录账号又是唯一标识这样登录时你可以直接用“学号 密码”作为查询条件少一次关联查询。第二张是course课程表字段包括课程编号、课程名称、授课教师、上课时间、上课地点、学分、容量、已选人数。这里的“上课时间”建议用字符串直接存比如“周一 3-4 节”而不是拆成星期几和第几节两个字段——为什么因为你做的是 Android 应用后台不是企业级 ERP简单直白的数据格式能帮你省掉大量解析逻辑。第三张是enrollment选课记录表字段为记录 ID、学号、课程编号学号和课程编号联合起来做唯一约束避免同一个人重复选同一门课。也许你会问要不要把“已选人数”实时算出来而不是存一个字段理论上是但实操中我建议直接在 course 表里维护一个选课人数计数。原因很简单这个项目的数据量级根本不需要考虑并发一致性每次选课成功就把计数加一退课就减一查询课程列表时直接读不用写复杂的 COUNT 语句UI 刷起来还快。这是典型的“用空间换逻辑简化”的思路答辩时你也能讲清楚设计动机。2.2 选课规则怎么落地容量、冲突、重复选课系统的核心不只是“能存数据”而是规则校验要严谨。我把规则拆成了三关每次选课操作都要依次通过这三关才能落库。第一关是重复性校验先查询 enrollment 表里是否已经存在该学生的这门课记录。有就直接提示“该课程已选请勿重复操作”不需要再往下走。第二关是容量校验比较 course 表里的已选人数和容量。已选人数 容量时直接拒绝并且前端要友好提示“课程名额已满”。这里有个细节值得注意——不要只在学生点“选课”按钮时才判断容量课程列表展示时就应该把已满的课程置灰或标注“已满”让规则前置。第三关是时间冲突校验找出这个学生已选的所有课程逐一比对上课时间。如果新选课程的时间段和已选课程有重叠必须拒绝。时间冲突这个逻辑我在第一次做的时候偷懒只做了“星期几”维度的比较结果出了 bug同一周的周二和周五不冲突这没问题但如果一门课是“周一 1-2 节”另一门是“周一 3-4 节”它们星期相同但时间错开了其实是不冲突的。后来我把上课时间解析成“星期 开始节次 结束节次”三个值来做区间重叠判断才算真正解决问题。这块逻辑是选课系统的“灵魂”也是答辩时最容易被老师追问的地方值得多花点心思。public boolean hasTimeConflict(String newTime, ListString selectedTimes) { // 将周一 3-4节解析为 星期几 起止节次 TimeSlot newSlot TimeSlot.parse(newTime); for (String selected : selectedTimes) { TimeSlot slot TimeSlot.parse(selected); // 同一星期才判断区间是否重叠 if (slot.week newSlot.week newSlot.start slot.end slot.start newSlot.end) { return true; } } return false; }3. 实操过程与关键实现从新建项目到跑通选课流程3.1 项目结构与界面层的组织方式确定好数据库设计之后就到了真正动手指建项目的时候。用 Android Studio 新建工程时很多初学者喜欢默认生成一个 MainActivity然后所有代码全堆在里面——Activity 里写数据库操作、写列表适配、写点击事件、写网络请求。这种写法不是不能跑但到了后期每加一个功能都要在几百行的 Activity 里翻找维护成本极其痛苦。更重要的是如果你要拿着这个项目答辩老师看到上千行的 Activity第一印象就会打折。我推荐的方案是按功能分包至少分四层。第一层是activity包放 LoginActivity、MainActivity课程列表、SelectedCoursesActivity已选课程、AdminActivity管理端等界面类。第二层是adapter包放各个列表的 RecyclerView.Adapter。第三层是db包放数据库帮助类和 DAOData Access Object类。第四层是model包放 Student、Course、Enrollment 这种实体类。实体类的作用是让数据在 Java 对象和数据库行之间来回转换典型的三层架构思想代码结构一眼就能看出“界面只管显示、数据层只管存取、模型层统一格式”。为了让数据库操作更顺手我给每个表写一个独立的 DAO 类比如CourseDao就是所有针对 course 表查询、插入、更新的操作集合。这样页面里不会出现任何一行 SQL 语句所有数据操作都是调用方法。比如课程列表页面只需要courseDao.getAllCourses()就能拿到一个ListCourse然后传给 RecyclerView.Adapter 展示。提示如果你对 Android 自带 SQLite 的 Cursor 操作感到繁琐可以引入 Room 库。Room 会在编译期帮你校验 SQL 语句还能自动处理大部分模板代码对初学者非常友好。但有个前提——如果你对 SQL 本身还不熟我建议先用原生 SQLite 跑通一套再换 Room否则出了问题你分不清是 SQL 写错还是框架问题。3.2 登录、课程列表、我的课表三个关键界面登录界面是整个 App 的门面也是老师上手 Demo 时第一个看到的东西。我建议登录方式就用“学号 密码”即可不需要注册功能——预置几个测试账号放进数据库里比如2021001 / 123456这样演示时输入就能进省去现场注册的尴尬和麻烦。登录的核心代码很简单从输入框读取学号和密码去 student 表里查记录匹配成功就跳转主页面并把当前学生的学号通过 Intent 传递过去失败则用 Toast 提示“学号或密码错误”。课程列表主页面我用的 RecyclerView CardView 的方案。每张卡片展示课程名称、教师、时间地点、学分、容量进度比如“已选 45/50”右侧放一个“选课”按钮。这里最关键的体验细节是按钮状态要实时反映课程容量已满就变灰不可点已选就显示“已选”且不可点。实现方法并不复杂Adapter 绑定数据时顺便判断一下该课程是否已被当前学生选过然后动态切换按钮样式。“我的课表”页面就更有意思了。如果把已选课程平铺展示太普通了我见过做得好的同学用一个简单的 GridLayout 模拟出周一到周五、第 1 到第 8 节的网格每门课按时间位置放进对应的格子。这个效果看起来加分不少技术实现难度其实不大——把已选课程按“星期几 第几节”分组往 GridLayout 里对应的位置塞进一个 TextView 就行。这种“最笨但最直观”的课表实现比引入复杂框架划算得多。3.3 选课和退课的操作流程数据库事务要重视选课按钮的点击事件是整个项目最核心的代码路径。每一步都不能马虎我按实际代码顺序给你拆解一下。第一步从按钮所在位置拿到对应的 Course 对象和当前登录学生的学号。第二步调用 EnrollmentDao 的查询方法确认没选过这门课。第三步比较已选人数和容量。第四步查询该学生所有已选课程的时间做时间冲突检测。全部通过才执行插入选课记录和把 course 表的选课人数加一这两步数据库操作。说到这我必须强调一个很多初学者完全意识不到的坑这两步数据库操作必须放在同一个事务里。什么叫事务就是一荣俱荣、一损俱损。如果只插入选课记录成功但更新人数失败了那选课记录和人数就对不上了反过来也一样。SQLite 的 beginTransaction 和 setTransactionSuccessful 这两个方法就是干这个用的。以前我见过一个学生在这里踩坑退课时删除了选课记录但人数没减演示了三次数据一次比一次离谱。后来我让他把数据库操作全部包进事务里问题立刻消失。db.beginTransaction(); try { enrollmentDao.insert(enrollment); // 插入选课记录 courseDao.increaseSelectedCount(courseId); // 选课人数1 db.setTransactionSuccessful(); } finally { db.endTransaction(); }退课的逻辑正好相反先删除选课记录再把选课人数减一同样放在一个事务里。退课时不需要校验容量和时间冲突但是需要判断一条边界情况这门课是不是已经选过了。如果没有选过却点了退课要给出提示而不是报错。一个好的设计是已选列表页只展示真正选过的课程从根源上杜绝“退不存在的课”这种操作。4. 常见问题与排查实录我能想到的坑都在这了4.1 环境与构建模拟器卡、Gradle 下载慢、SDK 版本不匹配先聊构建环境的问题。Android Studio 首次创建项目时Gradle 和依赖库需要从远程仓库下载如果你的网络状况不太理想很容易卡在构建进度条上。我的建议是创建项目之前先到官网把对应版本的 Gradle 压缩包下载好手动放到 Gradle 的本地目录下再修改项目中gradle-wrapper.properties的配置指向本地路径。这个操作听起来麻烦但能让你免去无数次重新构建的等待。模拟器卡顿也是高频问题。如果你的开发机内存不到 16G我建议直接用真机调试用 USB 连上手机打开开发者选项里的 USB 调试比模拟器省心太多。真机调试还有一个好处碰到手机屏幕适配问题能第一时间发现。很多初学者只盯着 Pixel 模拟器调 UI拿到华为、小米真机一跑按钮全部错位原因就是没有考虑屏幕尺寸差异。还有一个很隐蔽的坑compileSdk 和 targetSdk 版本不一致导致的依赖冲突。当你引入 Room 或其他第三方库时有时会提示要求最低 API 级别比你项目配置的要高。解决办法很简单把build.gradle里的 compileSdk 和 targetSdk 统一调高到 34 或 35重新同步即可。这类错误信息通常很长但关键就一句“requires compileSdk 34”学会读报错信息比死记配置重要得多。4.2 数据刷新列表不更新是最常见的问题我接手过不少选修课项目的“售后咨询”其中被问得最多的一句话是“老师我选课成功了但列表里没变。”这个问题百分之九十出在——你把选课操作完成后忘了让列表重新加载数据。RecyclerView 的数据源是内存里的ListCourse数据库变了不等于内存里的列表变了你必须手动重新查询数据库再把新数据设置给 Adapter并调用notifyDataSetChanged()方法通知界面刷新。更优雅一点的方案是改变数据源而不是整体刷新。比如选课成功后你可以在内存中把对应 Course 对象的已选人数加一并记录一个“本课程已被当前学生选过”的标记然后只对这个 item 做局部刷新。这种方法避免了整个列表的闪烁体验好很多。但我坦白讲对于课设级别的项目直接重新查询 全量刷新是最稳的代码不容易出 bug答辩演示也看不出差异。另外一个很容易漏掉的刷新场景从课程列表选完课跳到“我的课表”页面时忘记在onResume里重新加载数据。如果你只在onCreate里加载了一次数据那么你从选课页返回课表页时看到的永远是老数据。我的习惯是凡是列表类页面数据加载逻辑都写在onResume里而不是onCreate里这样每次页面回到前台都会自动刷新从根上解决“返回后数据不更新”的玄学问题。注意如果你用原生 SQLite 而没有用 LiveData 之类的东西在子线程操作数据库后更新 UI 一定要切换到主线程。否则应用会直接崩溃报错信息是Only the original thread that created a view hierarchy can touch its views。简单粗暴的解决办法就是数据量小的时候直接在 UI 线程跑数据库查询虽然不优雅但不会崩。4.3 选课逻辑边界并发、重复点击、时间冲突的隐性 bug选课系统的并发问题在课设答辩时被问到的概率极高。老师最爱问的一句话是“如果两个学生同时选同一门只剩一个名额的课怎么办”你需要明白本地 SQLite 架构下真正的并发并不会发生——你的 App 没有同时被两个人操作的可能。但你要能回答出理论方案如果是服务器数据库可以给课程表加行锁或者在更新人数时使用“原子更新 条件判断”的方式比如UPDATE course SET selected_count selected_count 1 WHERE course_id ? AND selected_count capacity这句 SQL 自带条件判断能够从数据库层面兜底。面试时能说出这种方案说明你思考过并发问题加分项。还有一个非常隐蔽的 bug 来自“重复点击”。选课按钮如果响应很快用户可能连续点了两下。第一次点击已经完成选课第二次点击又触发了一遍逻辑。如果你的重复性校验做在了异步回调里第一次插入还没完成第二次请求已经进来了就会出现重复记录。我的处理方式简单粗暴在按钮点击后立刻设置一个标志位禁止再次点击选课完成后再恢复。这种用户体验上的细节优化成本极低但能有效避免莫名其妙的数据问题。最后说一个容易被忽略的 UI 细节课程时间显示格式。很多课设喜欢把时间存成中文格式比如“周一第3-4节”这样用户看着舒服但你的代码要做时间冲突判断时解析中文会变得非常痛苦。我建议数据表里用“foo”这种简写格式存储展示时再做格式化。宁可多写一个格式化方法也不要在冲突判断时纠结字符串匹配。5. 从课程设计到可扩展的系统还能往哪个方向再进一步跟你分享一个我私心觉得有用的“加分思路”。如果这个项目你打算拿去参加答辩或者作品展示可以考虑加一个**“选课结果导出”**的功能——把当前学生的课表生成一个 PDF 或分享成文本。实现思路是读取已选课程列表用 Android 的 PdfDocument 类或者简单的 Intent 分享文本导出课表文件。这个功能本身不复杂但特别容易打动评委因为它体现了一个“能够站在用户角度考虑问题”的思维而不是“只会实现布置的功能”。再往远一点想你还可以把这个本地单机版改造成客户端-服务器版。服务器端用 Java Web 或 Node.js 写一套 REST APIAndroid 端通过 OkHttp 请求接口获取数据。数据层从本地 SQLite 换成远程 MySQL界面上几乎不用改动。这种演进路径非常自然也是目前企业里常见的前后端分离开发模式。说实话能独立走完这条路的应届生在面试时的竞争力已经比大多数只会调界面的候选人强了。如果你学有余力还可以考虑引入简洁的 MVVM 架构——用 ViewModel 管理界面数据用 Repository 层统一封装数据来源本地数据库或远程接口UI 层仅负责展示和交互。这样做的好处是逻辑更清晰、更容易写单元测试。但记住架构是为可维护性服务的如果你的项目本身只有几个页面强行上 MVVM 反而会把简单问题复杂化。选一条适合自己当前能力水平的路线比什么都重要。我在实操中反复体会到一个道理选课系统这类 CRUD 项目难点从来不在技术而在边界情况是否考虑周全、数据结构是否设计合理、流程是否走通闭环。你能不能在规定时间内把这三件事做到位比用多么时髦的框架重要得多。希望这篇分享能帮你少走一些弯路把精力花在真正能提升项目质量的地方。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进