简介这是一份面向Android初学者与高校课程设计学习者的学生选课系统完整工程源码基于Android Studio开发可用于课程实践、毕业设计参考或Android基础技能练习。压缩包共620个文件约56.75MB以flat、json、xml、dex、class等构建与编译产物为主同时包含java源码、gradle构建脚本、properties配置、png与jpg界面素材及jar依赖完整呈现了一个Android项目的目录结构与构建流程。资源涉及Gradle构建配置、app模块组织、RecyclerView课程列表展示、SQLite选课数据存储及网络通信等关键技术点读者可据此理解Android应用的UI设计、数据持久化与项目配置方式。目前已有2086人学习下载适合希望借助真实项目熟悉Android Studio开发流程、积累工程经验的学习者参考。1. 学生选课系统在 Android Studio 里到底怎么跑起来很多同学拿到一份“学生选课系统”的 Android Studio 源码包第一反应是双击打开、点绿色三角然后被 Gradle 同步卡住、被 SDK 版本劝退、被模拟器黑屏三连击。这份资源本质上是一个基于 Android Studio 的移动端选课 Demo核心解决的是“学生端登录、浏览课程、选课退课、查看已选”这条主链路适合课程设计、毕业设计起步阶段也适合想练手 Android 原生开发Java/Kotlin SQLite 或本地模拟后端的从业者。它不是什么高并发教务系统别拿生产环境的标准去要求它但作为一套能跑通、能改、能讲清楚逻辑的工程骨架价值是实打实的。下面按“先立住原理、再动手复现、最后排坑”的顺序拆开讲。2. 工程结构与技术选型为什么是 SQLite 而不是远程接口2.1 先看清目录里有什么拿到源码包别急着点运行。先在 Android Studio 里用 Project 视图展开重点看这几个位置app/src/main/java下按包名分层的 Activity/Fragment 与数据访问类app/src/main/res/layout下的 XML 布局app/src/main/res/values下的strings.xml和colors.xml以及app/build.gradle里的compileSdk、minSdk、依赖列表。一个典型的选课系统 Demo 通常会有登录页、课程列表页、课程详情页、已选课程页四个主要界面对应四到五个 Activity数据层用一个DatabaseHelper继承SQLiteOpenHelper来建库建表。判断这份工程能不能直接跑先看build.gradle里的 SDK 版本和你本机装的是否匹配。常见翻车点是源码写的是compileSdk 33你本机只装了 34同步时提示找不到平台。解决方式不是硬改源码而是在 SDK Manager 里补装对应版本或者把compileSdk和targetSdk统一改成你已安装的版本同时确认buildToolsVersion不冲突。2.2 本地 SQLite 方案的选择理由这类课程设计型选课系统绝大多数采用本地 SQLite 而不是远程接口原因很实际不需要部署服务器、不需要处理网络权限和接口鉴权、答辩时断网也能演示。代价是数据不共享多个学生端之间看不到彼此的选课结果容量上限也受单机限制。如果你后续想升级成“多端同步”就得把数据层从SQLiteOpenHelper换成 Retrofit 后端接口这个改造量不小但结构上是清晰的把 DAO 层抽成接口本地实现和远程实现各写一套。建表逻辑一般长这样放在DatabaseHelper.onCreate里// 建三张表学生、课程、选课关系 Override public void onCreate(SQLiteDatabase db) { // 学生表学号做主键密码明文仅用于教学演示 db.execSQL(CREATE TABLE student ( stu_id TEXT PRIMARY KEY, name TEXT NOT NULL, password TEXT NOT NULL)); // 课程表容量字段用于判断是否还能选 db.execSQL(CREATE TABLE course ( course_id TEXT PRIMARY KEY, course_name TEXT NOT NULL, teacher TEXT, credit REAL, capacity INTEGER DEFAULT 50, selected INTEGER DEFAULT 0)); // 选课关系表联合主键防止同一学生重复选同一门课 db.execSQL(CREATE TABLE enrollment ( stu_id TEXT, course_id TEXT, select_time TEXT, PRIMARY KEY (stu_id, course_id))); }这段 SQL 的关键点在enrollment表的联合主键。很多初学者只写自增 id 做主键结果同一个学生能把同一门课选两次已选列表里出现重复条目。联合主键(stu_id, course_id)从数据库层面就堵住了重复插入比在 Java 层用if判断更可靠。course表里的selected字段是冗余计数选课时selected 1退课时selected - 1配合capacity做容量校验。注意这个计数在并发场景下会有竞态但单机 Demo 里够用真要严谨就用事务包起来。2.3 选课与退课的事务写法选课不是单纯插一条记录它要同时更新课程已选人数两步必须原子完成。常见做法是用beginTransaction包住// 选课先查容量再插关系表最后更新计数全程一个事务 public boolean enroll(String stuId, String courseId) { SQLiteDatabase db getWritableDatabase(); db.beginTransaction(); try { // 查当前已选人数和容量 Cursor c db.rawQuery( SELECT selected, capacity FROM course WHERE course_id ?, new String[]{courseId}); if (!c.moveToFirst()) { c.close(); return false; } int selected c.getInt(0); int capacity c.getInt(1); c.close(); if (selected capacity) return false; // 满员直接退出 // 插入选课关系联合主键冲突会抛异常 db.execSQL(INSERT INTO enrollment (stu_id, course_id, select_time) VALUES (?, ?, datetime(now,localtime)), new Object[]{stuId, courseId}); // 更新已选人数 db.execSQL(UPDATE course SET selected selected 1 WHERE course_id ?, new Object[]{courseId}); db.setTransactionSuccessful(); return true; } catch (Exception e) { return false; // 主键冲突或其它异常事务回滚 } finally { db.endTransaction(); } }参数说明stuId是学号courseId是课程编号两者都来自界面传参。datetime(now,localtime)用本地时间记录选课时刻方便已选列表按时间排序。注意return false出现在try块里时finally仍会执行endTransaction但因为没有调用setTransactionSuccessful事务会回滚这是正确的行为。退课逻辑对称删关系表记录、selected - 1同样包事务。3. 从登录到选课四个界面的落地步骤3.1 登录页与 SharedPreferences 会话保持登录页的核心是校验学号和密码校验通过后把学号存进SharedPreferences后续页面直接读取避免每个界面都传 Intent 参数。写法如下// 登录成功后写入会话key 用常量避免拼写错误 SharedPreferences sp getSharedPreferences(session, MODE_PRIVATE); sp.edit().putString(current_stu_id, stuId).apply(); // 其它页面读取当前登录学号 String stuId getSharedPreferences(session, MODE_PRIVATE) .getString(current_stu_id, null); if (stuId null) { startActivity(new Intent(this, LoginActivity.class)); finish(); // 未登录直接踢回登录页 }apply()是异步写commit()是同步写登录场景用apply()足够。注意在onCreate里做登录态判断时要在setContentView之前或之后都行但finish()必须调用否则用户按返回键还能回到这个页面。密码校验在 Demo 里通常是明文比对这在教学场景可接受但如果要写进简历或答辩材料建议至少加一层 MD5 或 SHA-256 哈希并在说明里写清楚“生产环境应使用加盐哈希 HTTPS”。3.2 课程列表用 RecyclerView 而不是 ListView课程列表页建议用RecyclerView虽然ListView写起来更快但RecyclerView的 ViewHolder 复用机制更规范答辩时也更好讲。适配器里每个 item 展示课程名、教师、学分、已选/容量点击 item 跳详情或直接弹选课确认框。// 适配器 onBindViewHolder 里绑定数据并处理点击 Override public void onBindViewHolder(ViewHolder holder, int position) { Course c list.get(position); holder.tvName.setText(c.getName()); holder.tvTeacher.setText(c.getTeacher()); // 容量展示成 已选 32/50 的形式 holder.tvCapacity.setText(已选 c.getSelected() / c.getCapacity()); // 满员时按钮置灰避免无效点击 holder.btnEnroll.setEnabled(c.getSelected() c.getCapacity()); holder.btnEnroll.setOnClickListener(v - { if (listener ! null) listener.onEnroll(c); }); }参数上注意setEnabled的判断要和数据库里的容量校验保持一致界面置灰只是第一道防线真正的拦截在enroll()方法里。两者都做是因为用户可能快速连点界面状态来不及刷新。列表数据刷新用notifyDataSetChanged()或更精细的notifyItemChanged(position)后者性能更好但要注意 position 的准确性。3.3 已选课程页与退课确认已选页按当前学号查enrollment联表course展示课程名和选课时间。退课要加一个AlertDialog二次确认这是血泪经验没有确认框时用户误触退课按钮直接把课退了课程设计演示现场很尴尬。// 退课二次确认确认后才调数据库 new AlertDialog.Builder(this) .setTitle(确认退课) .setMessage(确定要退选《 courseName 》吗) .setPositiveButton(确定, (d, w) - { boolean ok dbHelper.drop(stuId, courseId); if (ok) { Toast.makeText(this, 退课成功, Toast.LENGTH_SHORT).show(); reloadList(); // 重新查询刷新列表 } else { Toast.makeText(this, 退课失败, Toast.LENGTH_SHORT).show(); } }) .setNegativeButton(取消, null) .show();drop()方法内部同样是事务删关系记录、selected - 1。注意退课成功后必须重新查询列表不能只从内存 list 里 remove否则和数据库状态不一致下次进入页面又会出现。reloadList()里重新执行查询并notifyDataSetChanged()。3.4 界面跳转与数据传递的两种方式Activity 之间传课程对象常见两种做法一是把课程 id 放进 Intent目标页再查一次数据库二是实现Serializable或Parcelable直接传对象。前者更稳因为数据以数据库为准不会出现传过去的对象和库里不一致后者写起来省事但容易埋坑。我一般选前者Intent 里只放course_id字符串详情页onCreate里查库填充。// 列表页跳详情只传 id Intent intent new Intent(this, CourseDetailActivity.class); intent.putExtra(course_id, course.getCourseId()); startActivity(intent); // 详情页接收并查库 String courseId getIntent().getStringExtra(course_id); Course course dbHelper.findCourseById(courseId);这样做的额外好处是详情页从后台恢复时数据仍然正确不会因为序列化对象过期而显示旧数据。4. 避坑与排查五个高频翻车现场4.1 Gradle 同步失败提示找不到 SDK 平台现象打开工程后底部一直转圈最后报Failed to find target with hash string android-33。原因源码的compileSdk写的是 33本机 SDK 目录里没有这个平台。解决打开 SDK Manager 勾选对应 API 级别安装或者把app/build.gradle里的compileSdk、targetSdk改成已安装的版本改完点 Sync Now。注意不要只改一处compileSdk和targetSdk不一致时某些 API 行为会有差异。4.2 模拟器启动后应用闪退Logcat 报 SQLiteException现象点运行应用图标一闪就退回桌面Logcat 里出现no such table: course。原因数据库版本号没变但表结构改了或者onCreate根本没执行常见于先建了库文件又改了建表语句。解决把DATABASE_VERSION加 1在onUpgrade里DROP TABLE再重建或者直接卸载应用重装让数据库重新创建。开发阶段最省事的做法是每次改表结构就卸载重装别在onUpgrade里纠结迁移逻辑。4.3 选课成功但列表不刷新现象点了选课Toast 提示成功但课程列表里的“已选人数”没变。原因数据库更新了但界面用的还是旧的内存 list没有重新查询。解决选课回调里调reloadList()重新查库并刷新适配器。另一个隐蔽原因是RecyclerView的 adapter 数据源和查询结果不是同一个引用notifyDataSetChanged刷了个寂寞。检查 adapter 构造时传入的 list 是否就是查询返回的那个。4.4 中文乱码或 strings.xml 报错现象界面上的中文显示成问号或者strings.xml里直接写中文导致编译警告。原因文件编码不是 UTF-8或者硬编码字符串没抽到资源文件。解决Android Studio 右下角确认文件编码为 UTF-8所有界面文案抽到res/values/strings.xml代码里用getString(R.string.xxx)引用。硬编码中文在 Demo 里能跑但一旦要做多语言或改文案就得满工程搜得不偿失。4.5 真机调试连不上现象模拟器能跑插上真机后 Android Studio 设备列表里看不到。原因手机没开开发者选项和 USB 调试或者数据线只供电不传数据。解决设置里连续点版本号七次开启开发者选项打开 USB 调试换一根能传数据的线弹授权框时点允许。部分机型还需要在开发者选项里打开“USB 安装”。这一步没有技术含量但卡住的人最多排查顺序就是先换线、再换口、再看授权。5. 进阶改造把本地 Demo 升级成可演示的完整方案5.1 用 Room 替换裸 SQLite如果想让工程看起来更“现代”可以把SQLiteOpenHelper换成 Room。Room 在编译期校验 SQL实体类用注解映射表结构DAO 用接口声明查询少写很多 Cursor 样板代码。改造步骤在build.gradle加androidx.room:room-runtime和注解处理器把三张表写成三个Entity类写一个Dao接口声明Insert、Query、Update再用Database抽象类串起来。选课事务用Transaction注解方法即可。代价是引入注解处理器后编译变慢且 Room 的版本要和 Kotlin/Java 版本匹配升级时容易踩依赖冲突。5.2 加一个简单的数据校验层Demo 最容易缺的是输入校验。登录页学号为空、课程容量为负数、学分超出范围这些都应该在入库前拦掉。我一般会在 DAO 之上加一个Validator类集中做非空、长度、数值范围检查返回错误码而不是抛异常界面根据错误码提示。这样答辩时被问到“异常输入怎么处理”有东西可讲也避免脏数据把列表搞崩。5.3 验证改造是否成功的三个检查点改完之后别只看能不能跑按这三个点验一遍第一选同一门课两次第二次应被联合主键拦下并提示“已选过”第二把某门课容量改成 1两个学生先后选第二个应提示满员且selected不超过capacity第三退课后重新进入已选页该课程应消失且课程列表的已选人数减一。这三点过了数据一致性基本就稳了。检查项操作预期结果重复选课同一学号对同一课程点两次选课第二次提示已选关系表无重复记录容量边界容量设为 1两个学号先后选第二个提示满员selected 不超过 1退课一致性退课后切到已选页再切回已选列表无该课课程列表计数减一从那以后我每次拿到这类课程设计源码都先跑一遍上面这张表再动业务代码因为数据层的坑往往在演示前十分钟才暴露提前验一遍能省掉现场冒汗的后悔药。希望帮到你。本文还有配套的精品资源点击获取