Android选课系统源码复现指南:从解压到跑通的实战教程

发布时间:2026/10/13 19:39:34

Android选课系统源码复现指南:从解压到跑通的实战教程 简介一份基于Android Studio开发的选课系统完整工程面向正在学习Android开发的高校学生、毕业设计者以及需要快速搭建移动端选课原型的开发者。工程覆盖登录界面设计、用户名与密码记住功能、密码隐藏/显示切换并通过SharedPreferences与网络请求完成数据持久化与登录校验课程模块采用RecyclerView展示课程列表支持选课确认与已选课程管理涉及Android Studio环境配置、依赖库导入、异步网络请求及安全存储等常见开发环节。资源共含1486个文件其中611个xml布局与配置、472个png界面资源、150个class编译产物、27个dex及23个jar依赖同时包含11个java源码、gradle构建脚本和可直接安装的app-debug.apk压缩包整体约25.47MB工程结构完整导入Android Studio即可查看运行效果。目前已有2504人学习下载适合作为Android课设、实训项目或入门选课系统的参考范本。1. 拿到一份 Android 选课系统源码先别急着解压不少读者下载过类似“Android设计选课系统.rar”这样的课程设计压缩包解压后面对几十个文件、几百行代码第一反应是直接拖进 Android Studio 准备跑起来结果 Sync 就报错一片最后只能放弃。这个标题背后的东西并不神秘它是一套基于 Android 原生开发的选课 App常见形态是学生端加管理员端数据存在 SQLite 或远程 MySQL用 ListView 或 RecyclerView 展示课表通过 AlertDialog 或 Activity 完成选课、退课操作。它解决的核心问题是在课多、时间冲突、容量有限的情况下让学生能用手机完成选课而不是去机房排队。适合的人群也很明确——Android 初学者、做课程设计的学生以及想快速搭一套 CRUD 应用来练手的人。这篇文章会把整个方案的原理、复现步骤和踩坑点讲清楚保证你拿到源码后能真正跑起来而不是让它在压缩包里吃灰。2. 选课系统的架构与技术选型为什么课程设计都这么写2.1 功能模块拆解先看懂它由哪几个页面组成一份典型的 Android 选课系统无论 .rar 里的代码写了多少个类最终跑起来后界面和交互都不会偏离这套功能集合。我拿到源码的第一步从来不是看代码而是先梳理它的功能模块搞清楚每个页面对应哪个 Activity 或 Fragment这样后面改起来才不迷路。最基础的学生端功能有三块登录与注册、浏览课程列表、选课与退课。登录和注册通常共用一个 LoginActivity注册后数据插入本地 SQLite 或远程数据库课程列表一般用 ListView 或 RecyclerView 展示课程编号、课程名、教师、学分、容量和已选人数选课与退课通过点击列表项弹出 Dialog 或跳转详情页来操作。管理员端一般多一块课程管理和学生管理能增删改课程、查看选课名单这类页面大多直接用 ListView 加自定义 Adapter 实现没有引入复杂的架构组件。技术栈为什么是这个样子得从课程设计的目标反推它要展示学生对 Android 四大组件、布局、事件处理、数据存储的掌握程度所以不会用 Jetpack Compose 这类声明式 UI也不会引入 MVVM 这样的架构分层。常见写法是一个 Activity 对应一个页面自己写 SQLiteOpenHelper 管理数据库用 Intent 做页面跳转用 SimpleAdapter 或 BaseAdapter 绑定数据。这套做法虽然“土”但知识点覆盖面够广答辩时老师问起来也答得上。2.2 为什么选 SQLite 而不是 MySQL课程设计与生产实践的分界线选课系统这类小型应用到底该用本地 SQLite 还是远程 MySQL是很多初学者拿到源码后第一反应想改的地方。我建议先不要改原因有三。第一原源码的数据库表结构和增删改查方法都是围绕本地数据库写的改成远程库意味着所有 SQL 要重写成远程调用工作量大且容易把课设做成“半成品”。第二答辩演示在教室或实验室进行网络环境不稳定本地数据库永远不会连不上演示成功率接近百分之百。第三课程设计评分看重的不是你用了多先进的技术而是你能否讲清楚“为什么这么设计”和“每个类的职责”SQLite 恰恰很好讲。但这里有一个非常值钱的认知本地 SQLite 和远程 MySQL 在表结构设计上是相通的。你完全可以先按本地库的表结构理解数据模型将来接 MySQL 时只需把 SQLiteOpenHelper 的 CRUD 代码换成 OkHttp 或 Volley 的 POST 请求。也就是说这份源码的数据库层是理解 Android 数据持久化的绝佳蓝本别因为它“土”就小看它。以最常见的表结构为例学生表 student 包含学号、姓名、密码、专业等字段课程表 course 包含课程号、课程名、教师、学分、容量、已选人数选课表 sc 记录学号、课程号和选课时间。这三张表已经可以支撑一张前三章介绍的完整功能闭环。再看关键代码新建一个类继承 SQLiteOpenHelperpublic class DBHelper extends SQLiteOpenHelper { public static final String DB_NAME course_selection.db; public static final int DB_VERSION 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE student ( sno TEXT PRIMARY KEY, sname TEXT NOT NULL, spassword TEXT NOT NULL, smajor TEXT)); db.execSQL(CREATE TABLE course ( cno TEXT PRIMARY KEY, cname TEXT NOT NULL, teacher TEXT, credit REAL, capacity INTEGER, selected INTEGER DEFAULT 0)); db.execSQL(CREATE TABLE sc ( sno TEXT, cno TEXT, select_time TEXT, PRIMARY KEY(sno, cno))); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 课程设计阶段通常直接删表重建生产环境绝不能这么做 db.execSQL(DROP TABLE IF EXISTS sc); db.execSQL(DROP TABLE IF EXISTS course); db.execSQL(DROP TABLE IF EXISTS student); onCreate(db); } }这段代码的逻辑说清楚。三个建表语句分别对应三个实体sc 表用联合主键防止同一学生重复选同一门课selected 字段用整型而不是布尔型因为选课人数要参与容量比较布尔型装不下。一个关键设计是容量和已选人数都放在 course 表里而不是在查询时用 COUNT 去数选课表这样读写都很快但代价是并发下数据可能不一致课程设计不需要考虑并发这个取舍是正确的。2.3 页面跳转与数据传递Intent 和 Bundle 怎么串起整个 App理解完数据库再看页面之间是怎么串起来的。Android 原生页面跳转靠 Intent传参数靠 Bundle 或直接把数据封装成对象实现 Parcelable 接口。选课系统这种小型应用最常见的做法是 Intent 携带主键值目标页面用主键到数据库里重新查询完整记录而不是把整个对象传过去因为 Android 的 Bundle 传 Parcelable 对象性能并不像很多人想象得那么便宜而且改动类定义后还要同步改序列化逻辑。学生选课的过程可以拆成四步。第一步在课程列表页面监听列表项点击事件读到课程号第二步 new 一个 Intent把课程号通过 putExtra 传给学生操作页面或直接传给学生详情页第三步详情页从 Intent 中取出课程号调用 DBHelper 的查询方法拿到课程完整信息并显示第四步用户点击选课按钮时详情页先检查已选人数是否小于容量再检查该学生是否已经选过两个都通过才执行插入语句。这两重校验一个都不能少少了就会读到“选课成功但是实际上没选上”这种看起来匪夷所思的 bug。数据传递的边界问题也值得说。很多课设源码里喜欢把用户名、学号存在全局变量或用静态变量比如写一个 UserManager 类存放当前登录学生的学号任何页面都能直接访问。这种方式在小应用里确实省事但容易埋坑——当系统杀掉了 Activity 而不是整个进程时静态变量还在没问题可如果你用 SharedPreferences 只存了登录状态而没存学号Activity 重建后学号就丢了。我见过不止一个课设项目死在“应用退到后台第二天再打开选课页就崩了”这个场景上。3. 复现这个 Android 选课系统从解压到跑通的完整步骤3.1 拿到压缩包后的第一件事检查项目结构和文件清单从“Android设计选课系统.rar”这个名字能确定的是它是个压缩包但里面有完整可运行的 Android Studio 项目还是只有一个 app 模块的工程需要解压后实际检查。我拿到任何 Android 压缩包源码都会先做三步检查解压后看根目录有没有 build.gradle 和 settings.gradle 这两个文件有它们才能确认这是 Gradle 工程看 app/src/main/java 目录下包名路径里有没有 MainActivity.java 或 MainActivity.kt看有没有项目根目录的 local.properties 文件这个文件记录本机 SDK 路径通常是别人电脑上的路径所以一般需要重新生成。一个完整的项目压缩包里应该能看到的文件类型如下这里用表格列清楚文件或目录作用通常需要修改吗build.gradle根目录定义仓库和插件版本通常需要settings.gradle声明包含哪些模块一般不改gradle/wrapper/gradle-wrapper.properties指定 Gradle 版本可能需要app/build.gradle编译配置、SDK 版本、依赖几乎必改app/src/main/AndroidManifest.xml声明权限和组件可能需改app/src/main/java/包名/Java 源码若报错则需修改app/src/main/res/布局、图片、字符串资源一般不改local.properties本机 SDK 路径必改Android Studio 会重新生成这里要特别注意一个文件gradle-wrapper.properties。多数课设源码的 Gradle 版本和当前电脑上的 Android Studio 能兼容的版本对不上最常见的现象是 Sync 时卡在下载 Gradle 或报“Failed to notify project evaluation listener”。我一般会直接查看这个文件里的 distributionUrl 字段比如它是 6.5 或 4.4然后判断本机 Android Studio 版本能否兼容不兼容就直接改成本机能用的版本而不是傻等它下载。检查这一步别急着双击 .apk 文件——课程设计源码一般不带 .apk往往只带工程文件。你的目标是把工程导入 Android Studio 后用模拟器或真机重新编译安装而不是在原地找现成安装包。3.2 Android Studio 导入与 Gradle 同步四个关键修改点打开 Android Studio 后选择“Open an existing project”定位到解压后的目录。这里有个很多人会踩的坑Android Studio 的 Open 对话框选目录时要选中包含 settings.gradle 的那一层不是选 app 目录也不是选 .idea 目录。选错的话 Android Studio 会把整个目录当作普通文件夹打开界面右下角出现进度条转了几秒钟后提示“未找到 Gradle 项目”这个错误信息给人的迷惑性很强因为目录看起来是工程目录实际上识别失败。正常导入后 Android Studio 会触发一次 Gradle Sync在这个阶段会报一堆错要稳定复现这个项目必须依次处理好这四类问题。第一SDK 编译版本不匹配。打开 app/build.gradle看 compileSdkVersion、minSdkVersion、targetSdkVersion 这三个值。老课设通常写 compileSdkVersion 25 或 26而当前 Android Studio 可能已经预装到 SDK 34 或更高。Gradle Sync 会报错找不到 compileSdk 版本解决方法是把 compileSdkVersion 改成当前环境已有的版本比如 33 或 34把 targetSdkVersion 也同步调高minSdkVersion 可以保持不变它只决定最低能装哪些版本的 Android 系统。第二Gradle 插件版本与 Gradle 版本不匹配。根目录 build.gradle 中 classpath com.android.tools.build:gradle:3.x.x 与 gradle-wrapper.properties 里的 Gradle 版本必须满足兼容矩阵。比如使用 gradle:4.1.0 就要求 Gradle 6.5 或更高使用 gradle:7.0.0 则要求 Gradle 7.0 以上。最简单的做法是直接 upgrade 到当前 Android Studio 默认支持的插件版本与 Gradle 版本组合但有些老项目用了过时的 API升级插件后代码编译会开始报错权衡后我建议如果代码本身是基于旧 API 写的优先保持版本不动安装配套版本的 Android Studio 或手动下载 Gradle如果代码本身比较规范直接升到新版本。第三依赖库版本过老或仓库访问不了。老项目里常见 implementation com.android.support:appcompat-v7:26.1.0 这类写法新环境强烈建议迁移到 AndroidX把 appcompat-v7 改成 androidx.appcompat:appcompat:1.x.x同时把所有 import 语句中 com.android.support 开头的包替换成 androidx 对应的包并在 gradle.properties 里加上 android.useAndroidXtrue。这个工作看起来工作量不大但当你打开源码发现几十个 Java 文件都有 import 需要替换时手动改不现实推荐用 Android Studio 菜单中的 Refactor 菜单里的 Migrate to AndroidX它会自动处理大部分替换。第四local.properties 缺失或 SDK 路径错误。Android Studio 通常会自动生成这个文件但如果 .rar 里自带的 local.properties 指向的是原作者的 sdk.dir而你的 SDK 在另一个路径Sync 就会报“SDK location not found”。直接删除这个文件让 Android Studio 重新生成这是最简单、最不容易出错的方案。随手补充一个在 Android 开发社区流传很广的细节如果控制台报了“Unable to resolve dependency for :appdebug/compileClasspath”九成是网络问题或依赖仓库配置不完整。先检查根目录 build.gradle 的仓库配置确保 google() 和 mavenCentral() 都写了然后检查网络代理是否被开启——如果开着代理Gradle 可能连不上仓库或下载到一半失败。这个问题在校园网环境下尤其常见。3.3 跑起来之前的检查AndroidManifest.xml 和资源文件核对Gradle Sync 通过后先别急着点 Run先花两分钟检查 AndroidManifest.xml 和 res 目录。有的课设源码压缩包里只带了 Java 文件和布局文件漏了图片资源或 strings.xml有的漏了权限声明。AndroidManifest.xml 里至少要有下面这些静态检查项所有在用到的 Activity 都要在 application 节点内用 activity 标签注册漏注册直接导致应用安装成功后点图标闪退Logcat 报“ActivityNotFoundException”INTERNET 权限是否有声明若应用要访问远程数据库或加载网络图片就必须有应用是否声明了某个 Activity 为 MAIN 和 LAUNCHER没有声明则安装后没有启动入口资源文件方面重点检查 res/drawable 目录下的图片是否缺失、res/values/strings.xml 是否被改了名字或删掉了默认字符串。老课设里面特别喜欢用自定义标题栏或自定义按钮背景如果 drawable 下有几个 .xml 文件引用了其他 drawable 文件而后者漏了Gradle 编译阶段就会报“Resource linking failed”连安装包都出不来。这类问题针对性很强报错信息里会明确告诉你是哪个文件找不到资源照着路径补进去或注释掉对应引用即可没有太多玄学空间。还有一类极常见的问题Android 12 及更高版本对导出的 Activity 组件有显式声明要求。如果你的 compileSdkVersion 和 targetSdkVersion 被调高到了 31 以上那么 manifest 中所有带 intent-filter 的 Activity 必须显式声明 android:exportedtrue 或 false否则编译直接失败。老课设源码里几乎不会写这个属性所以凡是升级到新 SDK 的这一步不能省。4. 手把手实现三个核心模块登录、课程列表、选课操作4.1 登录模块的实现从 XML 布局到数据库校验选课系统的入口是登录页绝大多数源码用一个 LoginActivity 承载输入框、按钮和提示文案。布局用 LinearLayout 竖向排列顶部放一个 ImageView 或 TextView 当 Logo中间放两个 EditText下面放一个 Button。密码框要设置 android:inputTypetextPassword避免明文显示EditText 的 hint 属性要写好提示文案比如“请输入学号”。这些布局细节看起来不起眼但答辩演示时输入错位、密码可见这类问题会给人留下非常差的印象。登录按钮的点击事件核心逻辑是检查学号和密码是否为空两者都不为空时才去数据库查询查到记录且密码匹配则跳转到主界面否则用 Toast 弹出“学号或密码错误”。这里有两个容易被忽略的点。一是用户名校验。很多课设源码把学号当字符串处理直接用 equals 比较这没错但如果你从 EditText 拿到的字符串里带了空格equals 就会失败。所以在做校验前先调用 trim() 去掉首尾空白这是一个在 Android 开发里讲起来很小但初学者经常翻车的点。二是登录状态的保存。常见做法是登录成功后用 SharedPreferences 保存一个布尔值和学号然后跳转页面下次打开 App 时先检查 SharedPreferences 里有没有登录标记有就直接进主界面没有则停留在登录页。这个机制做完你的课设就能多讲一个“持久化登录状态”的知识点很加分。验证逻辑关键是这段代码private void doLogin() { String sno etSno.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (TextUtils.isEmpty(sno) || TextUtils.isEmpty(password)) { Toast.makeText(this, 学号和密码不能为空, Toast.LENGTH_SHORT).show(); return; } DBHelper helper new DBHelper(this); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT * FROM student WHERE sno? AND spassword?, new String[]{sno, password}); if (cursor.moveToFirst()) { SharedPreferences sp getSharedPreferences(login_info, MODE_PRIVATE); sp.edit().putBoolean(is_login, true).putString(sno, sno).apply(); startActivity(new Intent(this, MainActivity.class)); finish(); } else { Toast.makeText(this, 学号或密码错误, Toast.LENGTH_SHORT).show(); } cursor.close(); db.close(); }这段代码的逻辑说明先用 trim 处理输入然后检查空值这是所有表单类应用的通用套路查询时用?占位符而不是字符串拼接可以防止 SQL 注入同时代码读起来也比加号拼接干净得多查询成功后把登录状态写入 SharedPreferences通过 apply() 异步提交不阻塞主线程最后做页面跳转并调用 finish()这样用户按返回键时不会回到登录页这是一个非常基础但很多人会漏掉的小细节。cursor 和 db 的关闭很多人都忘了写不写不报错但长时间使用会提示数据库连接数超限而且老师问到“资源释放”这个问题时你得答得出来。4.2 课程列表展示用 RecyclerView 还是 ListView老课设源码里清一色是 ListView因为 ListView 在 Android 早期就是列表的代名词而且 ListView 配合 SimpleAdapter 写起来确实快。但如果你想把这份源码做得更“现代”一点把 ListView 换成 RecyclerView 是性价比最高的一次重构代码量增加不多但能体现出你对 Android 列表组件的理解已经到了新的层次。这里把两者的关键差异搞清楚也是答辩时老师高频追问的地方。ListView 的适配器继承 BaseAdaptergetView 方法里要自己写好 convertView 的复用逻辑RecyclerView 的适配器继承 RecyclerView.Adapter 配合 ViewHolder 强制要求复用。ListView 默认只支持纵向滚动RecyclerView 通过 LayoutManager 可以切换成网格或瀑布流。ListView 的点击事件用 setOnItemClickListenerRecyclerView 要在适配器内部写接口回调或直接在 onBindViewHolder 里对每个 item 根部设置点击监听。下面是一小段用 RecyclerView 展示课程列表的适配器核心代码public class CourseAdapter extends RecyclerView.AdapterCourseAdapter.ViewHolder { private ListCourse courseList; private OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(int position); } public CourseAdapter(ListCourse list, OnItemClickListener listener) { this.courseList list; this.listener listener; } Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_course, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(ViewHolder holder, int position) { Course course courseList.get(position); holder.tvName.setText(course.getName()); holder.tvTeacher.setText(course.getTeacher()); holder.tvCredit.setText(String.valueOf(course.getCredit())); holder.itemView.setOnClickListener(v - listener.onItemClick(position)); } Override public int getItemCount() { return courseList null ? 0 : courseList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvName, tvTeacher, tvCredit; ViewHolder(View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_name); tvTeacher itemView.findViewById(R.id.tv_teacher); tvCredit itemView.findViewById(R.id.tv_credit); } } }这里的两个关键设计值得琢磨。第一点击事件通过自定义接口回调暴露给外部不在适配器里直接处理业务逻辑这样适配器可以只负责绑定数据业务层放在 Activity 或 Fragment 中写职责更清晰。第二onCreateViewHolder 里执行 inflateonBindViewHolder 里只做数据绑定强制复用 ViewHolder所以即使数据量几百条滑动列表时也不会像 ListView 不写复用那么卡。4.3 选课与退课的数据库事务容量和重复性的双重校验选课操作是核心业务逻辑上必须处理两个问题选课人数已满时不能选重复选课时不能选。这两个问题的对应解法是查容量和查重复但要把它写好光这两步还不够。看下面的选课代码它完整展示了选课时要做的事public boolean selectCourse(SQLiteDatabase db, String sno, String cno) { db.beginTransaction(); try { Cursor cursor db.rawQuery( SELECT capacity, selected FROM course WHERE cno?, new String[]{cno}); if (!cursor.moveToFirst()) { cursor.close(); return false; // 课程不存在 } int capacity cursor.getInt(0); int selected cursor.getInt(1); cursor.close(); if (selected capacity) { return false; // 容量已满 } Cursor dupCursor db.rawQuery( SELECT 1 FROM sc WHERE sno? AND cno?, new String[]{sno, cno}); boolean duplicated dupCursor.moveToFirst(); dupCursor.close(); if (duplicated) { return false; // 已选过该课 } db.execSQL(UPDATE course SET selectedselected1 WHERE cno?, new String[]{cno}); db.execSQL(INSERT INTO sc(sno, cno, select_time) VALUES(?, ?, datetime(now, localtime)), new String[]{sno, cno}); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } }这段代码的核心在于把两步数据库写操作放在同一个事务里update 把已选人数加 1insert 向选课表写入记录。如果不用事务两步之间一旦出现异常或进程被杀死就会留下“选了课但人数没加”或“人数加了但选课记录不存在”这种脏数据。课程设计阶段数据库里就几十条数据看不出问题但这恰恰是一个能体现你专业度的设计细节——老师看到 beginTransaction 和 setTransactionSuccessful 时基本就不会在这个点上继续追问了。另一个值得说清楚的是返回值的语义返回 false 时调用方需要区分“课程不存在”“名额已满”“重复选课”这三种失败原因。简单返回布尔值会让用户只看到“选课失败”而不知道具体原因体验很差。改进方式是返回 int 或定义的枚举0 表示成功1 表示课程不存在2 表示已满3 表示重复选课Activity 端根据返回值弹不同的 Toast。这在源代码里的实现可能只是几行 if 判断但写出来以后答辩时面试官看你的眼光会不一样。5. 复现中的高频踩坑与排查清单从同步失败到选课崩溃5.1 编译期报错的三种典型场景与处理顺序.rar 里的课设代码几乎不可能顺利编译原因主要是旧代码与新工具链不兼容。按照重要性和出现频率排序编译期报错通常集中在 SDK 版本、依赖冲突、代码 API 过时三个层面。SDK 版本问题是 Sync 阶段就挂掉的那种报错关键词为“Failed to find target with hash string android-XX”或“compileSdkVersion XX”。解决办法是把 app/build.gradle 里的 compileSdkVersion 改成你本机已安装的版本。同时要检查 buildToolsVersion 是否指定了旧版本如果指定了通常会报找不到对应版本直接删掉该行让 Android Studio 用默认工具版本就行。依赖冲突的典型报错是“Program type already present”或“Duplicate class”。这个几乎都是因为同一个库被不同方式引了两次比如旧项目同时引了 support-v4 和 appcompat-v7而 appcompat 内部又会间接依赖 v4版本不一致时就会冲突。处理办法是找到 app/build.gradle 的 dependencies 块删掉其中多余的那个或者统一用 AndroidX 替代整个 support 库再执行一次 Clean 和 Rebuild。代码 API 过时的表现就不一样了Sync 能通过但一编译就报方法找不到或类已经废弃。最常见的几个包括 getResource 被替换成 getResources、getColor(int) 在新版本中必须传入 Theme 参数、AsyncTask 被标记为过时不过仍可运行。这些错误大多好修关键是不要手抖把整个文件重写按照编译器的提示一行行改就行。5.2 安装与运行阶段的三类崩溃闪退、白屏、数据库锁定编译通过只走了全程的一半装到模拟器或真机后才是真正的考验。最常见的现象有三类我按实际工作中遇到过的概率从高到低排。第一位是“启动即闪退”Logcat 中通常能看到“ FATAL EXCEPTION ”和 Caused by 行。根据我的经验闪退的根因在新手源码里高频集中在三种第一某个 Activity 找不到报 ActivityNotFoundException第二布局文件里 findViewById 拿到了 null报 NullPointerException第三数据库查询的表字段在代码里写错了名字报 android.database.sqlite.SQLiteException。排查手法很简单打开 Android Studio 的 Logcat 窗口把级别调到 Error筛选当前包名先看 Caused by 那行它直接指到是哪一行的代码崩了。不要看最上面那一大段红色的堆栈那里面大量信息是无用的系统底层调用。第二位是“白屏但进程没有退出”。这往往是布局加载成功后某个网络请求或数据库查询耗时太长主线程卡住系统 ANR 弹窗又刚好没弹出来。解决方法是先在代码里搜索是否存在主线程上执行网络请求的写法——老课设里直接在主线程 new 一个 Thread 做 MySQL 连接或者用 HttpURLConnection 同步请求这在 targetSdkVersion 低的时候能跑但 Android 9 及以上直接抛 NetworkOnMainThreadException。处理办法是引入 Handler 加子线程或者更省事地用 AsyncTask课程设计阶段不需要上协程。第三位是“提示 database is locked”。真机上跑课设经常遇到数据库被锁原因是多个线程同时操作同一个 SQLiteOpenHelper 实例或者某处代码打开了数据库却没有关闭。SQLite 的锁机制对单线程访问是透明的但多线程同时写库就会触发 BUSY 信号。最简单的解决方式是把 DBHelper 写成单例确保所有操作都走同一个实例同时把写入操作按上面 4.3 节的方法放到事务里。如果症状还在检查数据库连接有没有关闭每一条 Cursor 用完必须调 close()。5.3 把数据库文件拉出来检查模拟器调试的必会技巧课程设计阶段运行逻辑出了问题只看 Toast 内容往往不够因为 Toast 只能告诉你“不成功”但数据库里实际是什么状态无法从界面判断。这时候把数据库文件从模拟器或真机上拉出来用可视化工具查看是效率最高的排查方式。常见做法是用 adb 命令具体流程是先确定应用包名和数据库名然后执行下面的命令把数据库从设备上复制到电脑adb shell run-as com.example.courseselection cat databases/course_selection.db course_selection.db这句命令的本质是app 的数据库文件默认是私有数据直接 adb pull 会提示 Permission deniedrun-as 可以以应用身份读取该文件。如果应用是 Debug 签名dev 包run-as 就能直接用。有些源码应用的包名在 build.gradle 里写的 applicationId 跟模拟器上的实际不是同一个先用 adb shell pm list packages 查一下真实包名再跑上面这条命令以免拉错文件或者报找不到包。数据库文件拉到电脑后可以用 DB Browser for SQLite 打开直接看 student、course、sc 三张表的字段和记录。你会发现选课失败时其实不是代码逻辑错了而是库里 course 表的 selected 字段已经等于 capacity这就是 5.2 节说的“界面判断不了的隐藏状态”。把数据库拉出来对比一下就能快速区分是逻辑问题还是数据问题。5.4 时间与日期字段的坑SQLite 的 datetime 函数和 Java 格式不一致选课系统会记录选课时间大多数课设代码在往 sc 表插入记录时直接用 SQLite 的 datetime(now, localtime) 函数拿到的是一个如 “2024-05-20 14:30:00” 的字符串。看起来正常但如果 Java 端读这个时间后要计算“距选课结束还有多久”的逻辑就要用 SimpleDateFormat 解析格式串必须是 yyyy-MM-dd HH:mm:ss秒以下是不能有的否则解析抛 ParseException程序直接崩溃。这是典型的“数据库层格式和 Java 层格式不一致”导致的边界问题。另外一个容易被忽略的坑某些源码里插入时间用的是 Java 的 System.currentTimeMillis()即一个 long 型时间戳而 datetime(now, localtime) 返回的是字符串。两种时间格式混在一个表里之后按时间排序或筛选某日期区间时排序结果会完全乱套。打好基础的办法是统一要么全部用 SQLite 的 datetime 字符串要么全部用 long 时间戳别在同一个表里混着用。这个细节看起来不大但一旦出问题排查起来非常费时间。6. 把课设从“能跑”升级到“能讲”三个高性价比的进阶改造当你把这套选课系统完整跑通以后精力应该放在最后一个阶段如何让它从“跑通了”变成“值得讲”。选课系统的功能本身是固定的但一道加分项在于你有没有往里面增加一些能体现设计思考的东西。我推荐三个方向按收益比从高到低排列。第一个方向是引入 Fragment 做单 Activity 多页面架构。老课设里每跳转一个页面就 startActivity 一次返回到课表页要按返回键一页页回退体验差。把主界面改成一个 MainActivity 加底部导航栏内部放三个 Fragment——课程列表、已选课程、我的——你的 App 瞬间从“课设风”变成了“商业 App 风”。Fragment 的生命周期和事务管理本身就值得展开讲两分钟。第二个方向是在退课逻辑里加上退课截止时间判断。这是从业务角度做的改进如果当前时间晚于退课截止时间退课按钮置灰或点击后弹 Toast 提示“已过退课期”。实现只需在数据库表里加一个退课截止字段并在退课按钮的点击事件里做一次时间比较。这个功能很多商业系统都有但课堂作业里极少有人做属于投入极小、讲起来很体面的改动。第三个方向是这个项目天然适合写自动化测试。Android 本地单元测试可以测 DBHelper 中 insert、select、update 的逻辑UI 测试可以用 Espresso 模拟点击事件测试登录、选课、退课三条流程。课设不要求写测试但只要写了老师立刻知道你理解工程化的价值。测试代码量不大贴一段最核心的测试方法RunWith(AndroidJUnit4.class) public class CourseSelectionTest { Test public void testSelectCourse_withFullCapacity_shouldReturnFalse() { Context context ApplicationProvider.getApplicationContext(); DBHelper helper new DBHelper(context); SQLiteDatabase db helper.getWritableDatabase(); // 先造一条容量为1、已选人数为1的课程数据 db.execSQL(INSERT INTO course(cno, cname, teacher, credit, capacity, selected) VALUES(C001, Android开发, 张老师, 2.0, 1, 1)); // 再选一次应该返回false boolean result new CourseService(helper).selectCourse(db, 2024001, C001); assertFalse(result); db.close(); } }这段测试的核心思想在于数据从代码里造而不是依赖界面上的输入这样测试可以重复跑每次跑完数据互不干扰。写完这样一条断言你就能在答辩时自信地讲清楚“这个方法保证了容量已满时选课不会写入脏数据”。对比绝大多数课设只能演示“从界面上点一遍”这已经形成了代差。做一个项目最怕的是功能跑通了但别人问“你这里为什么这么写”时回答不上来。我自己的习惯是每完成一个课设项目都把所有类打印出来在类名旁边手写三行注释——这个类负责什么、依赖谁、被谁依赖。选课系统这种体量的代码这样梳理一遍不超过半小时但对逻辑的理解效果比看十遍视频都强。希望这篇文章中的复现步骤和踩坑记录能帮到你哪怕你只按第 5 章的排查清单修好了一个闪退也算值了。如果你在这个基础上把 Fragment 改造和测试补上了那这份“Android设计选课系统”就可以从压缩包里的老古董变成你简历上真正拿得出手的 Android 项目。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/13 19:34:32

二维正态分布中ρ为何等于相关系数?从几何直观到数学证明

1. 这不是数学证明题,而是一次对统计直觉的重建“证明二维正态分布中的参数ρ为相关系数”——看到这个标题,很多人第一反应是翻出《概率论与数理统计》教材,找到那个带积分、协方差定义和联合密度函数的推导过程,抄一遍公式&…

2026/10/13 19:34:32

FastAPI实战:异步Web开发、依赖注入与自动文档

聊到 fastapi,很多人第一反应是“又一个 Python Web 框架”。说实话,我以前也这么想,毕竟 Flask 在中小型项目里已经够用,Django 在重型项目里也站稳了脚跟。真正让我改变看法的是一次内部工具重构:那套系统要定时抓取…

2026/10/13 19:34:32

微电网综合能源优化调度:Matlab+YALMIP+CPLEX建模实战解析

1. 先说清楚这项目到底要解决什么问题1.1 微电网、综合能源系统与优化调度三者到底怎么咬合拿到这个题目的时候,我第一反应是“又一套经典组合拳”。微电网、综合能源系统、Matlab、CPLEX,凡是做过电力系统或能源方向课题的人,几乎都能在这几…

2026/10/13 20:54:53

Tool Calling实战指南:从原理到生产环境避坑

1. 从一次“人工智障”的对话说起前阵子帮一个做电商的朋友看他们的客服机器人。用户问“我上周买的那个蓝色杯子发货了吗”,机器人回复了一大段关于杯子材质的介绍,还贴心地推荐了同款杯垫。朋友很无奈,说这机器人“知识渊博但听不懂人话”。…

2026/10/13 20:54:53

C语言文件操作避坑指南:FILE、fopen、fgets、feof与EOF详解

1. 为什么文件操作是C语言里最容易翻车的地方刚接触C语言文件操作的人,十有八九会觉得这块内容"看起来很简单"——不就是打开文件、读一读、写一写、关掉吗?fopen、fgets、feof、EOF这几个函数和宏,翻来覆去就那么点东西。但真正在…

2026/10/13 20:54:53

Python爬虫实战:通过JSON接口采集电影短评并落地CSV

简介:以豆瓣短评抓取为实战场景的Python爬虫与数据分析资料包,面向希望系统练习网络请求、页面解析、数据清洗与可视化的初中级学习者。项目以病毒和疾病题材电影短评为对象,完整演示了构建爬虫获取HTML内容,利用解析库提取短评正…

2026/10/13 20:54:53

Tool Calling 实战指南:从原理到落地的工具调用设计

1. 从一次接口对接的深夜加班说起前阵子帮一个做智能客服的朋友看他们新版本的设计方案,会上有人抛出一个问题:“用户问‘帮我查一下上周三的订单发货了没’,模型怎么知道要去调订单系统的接口,而不是自己编一个‘已经发货了’的答…

2026/10/13 20:54:53

时间复杂度不是考试题,而是程序员的性能直觉

1. 这不是“刷题考试”,而是你写每行代码时都在用的底层直觉 很多人第一次看到“时间复杂度”四个字,脑子里自动弹出的画面是:大学算法课上黑板上密密麻麻的O(n)、O(log n),老师念着“大O表示法”,台下有人偷偷刷手机&…

2026/10/13 20:49:52

构建Agent技能库:从工具调用到可复用执行技能的工程实践

先说个我最近深有体会的事:现在的对话式AI模型,单聊知识问答已经没什么门槛了,真正拉开差距的,是它能不能替你把一件事完整办完。你让它查资料,它能答;你让它订会议室、汇总邮件、跑一遍数据处理脚本&#…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/13 0:04:41

基于Triton部署YOLO目标检测:从训练脚本到线上服务

简介:这份资源面向希望将目标检测模型落地到生产环境的深度学习开发者与算法工程师,聚焦于用 Triton Inference Server 部署 YOLOv9 的完整实战方案,解决从训练权重到线上推理服务之间的工程化难题。压缩包共 16 个文件,约 886KB&…

2026/10/13 0:04:41

手把手教你云端部署Qwen-Image-2.1:从GPU选型到在线文生图服务

Qwen-Image-2.1 这个开源的图像生成模型,最近在内容创作和 AI 应用圈子里热度相当高。它属于通义 Qwen 系列里的视觉生成分支,让我眼前一亮的是它对中文长文本的理解能力——以前用那些老方案生成带中文文字的图,十个里九个是“乱码印刷体”&…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑