
简介一份基于安卓的大学课程电子管理平台系统设计与实现项目面向计算机相关专业正在完成毕业设计或期末大作业的学生也适合需要实战练习的开发者。压缩包整体约19.76MB核心内容包括论文、开发文档、数据文档与源码其中源码均已通过本地编译与严格调试可直接运行。论文部分详细涵盖系统背景、需求分析与架构设计开发文档按模块说明了Android端从界面到逻辑的实现过程数据文档提供了数据库表结构与初始化数据源码工程则便于导入Android Studio后对照学习或二次开发。整套方案经导师指导并认可难度适中覆盖设计、实现到测试的完整流程代码结构与注释也比较清晰。目前已有32人学习下载适合用作项目模板、答辩参考或功能扩展的起点。1. 「基于Android的大学课程电子管理平台」是毕业设计里看着普通、实际五脏俱全的题「基于Android的大学课程电子管理平台」放到毕业设计里属于那种「一眼看不出亮点、做完才发现工作量刚好卡在及格线以上」的稳妥选择。它要求的不是做一个能点击的演示壳而是把学生选课、课表查询、成绩录入、教师端发布课程、管理员维护数据这一条完整业务线跑通。真正拉开分数的地方不是界面好看而是数据模型怎么设计、选课事务怎么不出错、升级数据库时怎么不闪退——这些恰好是评审老师最爱追问的部分。这个题适合没独立做过完整 App 的学生也适合想用 Android Studio 把一个业务闭环从零落地的人参考。下面这套方案是我做类似课设反复验证过的组合照着重做一遍比临时换题稳得多。2. 技术栈选型Android Studio Java SQLite 这套组合为什么最不容易翻车做这种「管理系统」类题目最怕的不是功能做不完而是选了一个自己不熟悉的技术栈被一个环境问题卡住一整天。我见过太多人一上来就用协程、Jetpack Compose、MVVM 全家桶结果答辩前还在跟 Gradle 依赖冲突搏斗。课设的第一目标是「稳定交付」不是展示新潮。2.1 语言选 Java资料多、老师熟、检索成本低Java 还是 Kotlin如果这是你的第一个完整项目我建议用 Java。不是说 Kotlin 不好而是课设场景下的「踩坑—修复」速度更重要。Android 开发的大多数历史问题、老师手上的旧代码、博客里的报错解决方案全是 Java 写的。你遇到一个 ClassNotFoundExceptionJava 的解决方案一搜一大把Kotlin 的答案经常要附带协程和空安全的上下文新手看起来更吃力。另一个现实因素是答辩。指导老师和评审专家里老一辈对 Java 的接受度普遍高于 Kotlin。你讲「用 Gradle 配置 Kotlin 扩展」和讲「基于 JVM 的面向对象编程配合 SQLite 持久化」后者更容易被理解。Kotlin 的空安全确实能减少崩溃但这个项目的数据量小、逻辑直接Java 写起来也不会有多危险。2.2 数据存储选 Room底层还是 SQLite但帮你省掉一大半模板代码「电子管理平台」的核心是数据所以存储方案是选型的重中之重。Android 上持久化方案对比下来最适合这个题目的是 Room。我做一个表格把三个常见方案摆一起看方案建表方式版本迁移线程约束答辩被追问的难度原生 SQLiteOpenHelper手写 SQL手写 onUpgrade无强制约束中Room注解实体类自动生成建表Migration 机制默认不允许主线程查询低直接存 JSON/文件无表结构无无高老师会质疑为什么不选数据库Room 是官方推荐的 SQLite 封装它做的事情很简单把Entity注解的类自动映射成表把Dao接口里的方法编译时检查 SQL 语法。你在 Logcat 里看到的还是 SQLite底层没有任何魔法。我用三个最小文件给你看骨架这是整个项目的地基Entity(tableName user) public class User { PrimaryKey(autoGenerate true) private int userId; ColumnInfo(name account) private String account; ColumnInfo(name password) private String password; ColumnInfo(name role) private int role; // getter 和 setter 按下述逐字段补全 }Dao public interface UserDao { Query(SELECT * FROM user WHERE account :account) User findByAccount(String account); }Database(entities {User.class}, version 1, exportSchema false) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); }参数说明Entity(tableName user)声明这个类对应 user 表PrimaryKey(autoGenerate true)让自增主键由数据库管理ColumnInfo(name account)指定字段名Query(SELECT * FROM user WHERE account :account)是编译期校验的 SQL:account会被安全绑定成查询参数没有拼接注入问题Database(version 1)是数据库版本号后面升级表结构时必须手动加 1。2.3 架构用 MVC 就够了别在这个题目上搬全家桶很多文章会推荐你上 MVVM DataBinding 协程理由是「现代 Android 开发都这么写」。但如果你的目标是顺利毕业、稳定演示这套组合反而是风险源。原因很简单每多一个框架依赖就多一个可能翻车的地方。DataBinding 的注解处理器版本不匹配会导致编译失败协程一旦出现未捕获异常崩溃栈对新手很不友好MVVM 的 LiveData 观察者链写多了代码量不仅没减少反而要解释好几个概念。这个项目的业务流是登录 → 查课表 → 选课 → 查看成绩。数据量小、界面层级浅用 MVC 足够。Activity 当控制器负责接收点击事件和跳转Adapter 当视图层负责把数据画到列表上Room 的 DAO 当模型层负责读写数据。三个人各管一段答辩时也能讲得清清楚楚。3. 数据建模四张表撑起整个「管理」逻辑重点是选课关系怎么设计管理系统和高保真界面的本质区别在于数据之间的关系。界面再漂亮如果学生选了课之后在成绩表里查不到对应记录这个系统就是不合格的。所以动手写 Activity 之前先把数据库的表结构定下来。这门课我一般先画一张草稿哪些实体之间有「一对多」关系哪些是「多对多」关系。课程管理系统的核心就两类关系用户和课程之间的归属关系学生和课程之间的选课关系。3.1 用户表学生、教师、管理员用一张表还是三张表这个题目里通常有学生、教师、管理员三种角色。常见做法是拆成 student、teacher、admin 三张表字段各自独立。我不建议这么做。原因有两点。第一登录时你只拿到一个账号和密码如果三张表分开你得先去判断这个账号属于哪张表或者建一张独立的 account 表做映射这凭空多了一层查询。第二课设规模的数据量通常只有几百条拆表的收益体现不出来反而让权限判断变复杂。用一张 user 表加 role 字段最直接。role 用整数区分0 学生、1 教师、2 管理员。建表语句CREATE TABLE user ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role INTEGER NOT NULL DEFAULT 0, real_name TEXT NOT NULL );这条 SQL 里UNIQUE约束很关键。如果不加同一学号能被注册两次选课关系里学生的身份就乱了。role INTEGER NOT NULL DEFAULT 0的默认值保证即使注册页面某个分支忘了传角色也不会产生一个无身份用户。3.2 课程表上课时间和课程基本信息必须拆开存接下来是课程的信息结构。很多第一次做的人会把课程名、教师、上课时间、教室全部塞进一张 course 表遇到一门课一周有两节在不同教室的情况就只能在字段里填逗号拼接或额外加两个冗余字段。这是设计缺陷。正解是拆成两张表course 存课程的基本信息course_schedule 存上课时间地点。一门课一周可以有多条 schedule 记录。CREATE TABLE course ( course_id INTEGER PRIMARY KEY AUTOINCREMENT, course_name TEXT NOT NULL, teacher_id INTEGER, credit REAL NOT NULL DEFAULT 2.0, capacity INTEGER NOT NULL DEFAULT 60, selected_count INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(teacher_id) REFERENCES user(user_id) );CREATE TABLE course_schedule ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_id INTEGER NOT NULL, day_of_week INTEGER NOT NULL, start_section INTEGER NOT NULL, end_section INTEGER NOT NULL, week_list TEXT NOT NULL DEFAULT 1-16, classroom TEXT NOT NULL, FOREIGN KEY(course_id) REFERENCES course(course_id) );这里重点说week_list。它存的是一个字符串表示这门课在哪些周上课。格式我用两种「1-16」表示第 1 到第 16 周每周都上「1-8,10-16」表示前八周和第十到十六周上第九周停一次。解析时用正则或 split 展开成 List 交给课程表去判断某一天是否有课。这么做比存两种布尔字段单周、双周灵活也不会出现单双周字段无法描述「前八周上」的尴尬。3.3 选课关系中间表、唯一约束和冗余字段一起上学生和课程之间是多对多关系必须有中间表。这张表同时承担选课记录和成绩记录两个职责CREATE TABLE student_course ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, score REAL, create_time TEXT NOT NULL DEFAULT (datetime(now, localtime)), UNIQUE(student_id, course_id), FOREIGN KEY(student_id) REFERENCES user(user_id), FOREIGN KEY(course_id) REFERENCES course(course_id) );UNIQUE(student_id, course_id)是防止重复选课的最后一道防线。就算应用层漏判了一个「是否已选」数据库也会在第二次插入时抛约束异常你可以捕获后提示用户。selected_count放在 course 表里是刻意的冗余。每次选课成功后执行UPDATE course SET selected_count selected_count 1查询课表详情时直接读这个字段判断是否已满不用COUNT(*)去扫中间表。数据量小的时候两者性能差距不大但这个字段让「剩余名额」的展示变得非常直接而且给后面的事务设计留了抓手。3.4 数据库升级的后手版本号和 onUpgrade 是「后悔药」写管理系统时表结构经常做到一半想加字段比如课程表要加一个「上课地点备注」。如果你直接改建表语句而无视 version老用户装有数据的设备启动时会直接闪退。原因是 App 目录里的旧数据库没有你要的新字段SQL 执行到那些列时报no such column。这段话值得记住数据库版本号是给程序认的不是给人看的。我用 SQLiteOpenHelper 举例Room 的思路完全一致private static final int DB_VERSION 2; Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE course ADD COLUMN remark TEXT DEFAULT ); } }这段代码的逻辑是把 version 从 1 改成 2触发onUpgrade然后执行 ALTER TABLE 补充字段。关键点在于oldVersion 2的判断保证这个迁移动作只执行一次。以后从版本 1 直接升到 3 的设备也会先走这个分支——这是数据库升级最容易被忽视的边界条件。4. Android 端实现顺序登录、课程表、选课三个模块怎么落地数据表定完App 端的实现顺序我建议是先把登录打通再看课程表最后处理选课事务。这个顺序的唯一标准是「每个阶段结束时都有一个能看的成果」。登录通了你有了一个跑得起来的闭环课程表通了你有了可视化选课通了你有了业务价值。4.1 登录模块查询用户表 SharedPreferences 保存会话登录页面的核心逻辑很简单读取输入框内容查 user 表匹配 account 和 password。这里要注意两个细节。第一个是查询条件里同时带上两个字段不要先按 account 查出来再在 Java 里比对 password那样会把密码比对逻辑暴露在代码里不优雅。第二个是登录成功后只把 user_id 和 role 存到 SharedPreferences不要存密码。核心代码String sql SELECT * FROM user WHERE account ? AND password ?; Cursor cursor db.rawQuery(sql, new String[]{account, password}); if (cursor.moveToFirst()) { int userId cursor.getInt(cursor.getColumnIndexOrThrow(user_id)); int role cursor.getInt(cursor.getColumnIndexOrThrow(role)); getSharedPreferences(session, MODE_PRIVATE) .edit() .putInt(user_id, userId) .putInt(role, role) .putString(account, account) .apply(); // 根据 role 跳转到学生主页、教师主页或管理端 } else { runOnUiThread(() - Toast.makeText(this, 账号或密码错误, Toast.LENGTH_SHORT).show()); } cursor.close();rawQuery的第二个参数是String[]里面的每个?占位符按顺序绑定。这是原生 SQLite 防注入的标准写法不要手动拼字符串。getColumnIndexOrThrow会在列名不存在时直接抛异常能尽早暴露表结构和代码不一致的问题。登录按钮的点击事件里还要做一件事验证期间把按钮置为不可用并显示一个 ProgressBar。这个项目查表很快但按钮防重复点击是任何时候都该有的习惯——用户连续点两次会产生两条登录请求后面再做跳转就乱了。提示登录失败的 Toast 统一提示「账号或密码错误」不要区分「账号不存在」和「密码错误」。区分会给枚举工具提供线索答辩时老师也更容易问倒你。4.2 课程表模块GridView 按周一到周日排布课程表的难点不在数据在布局。我的做法是GridView列数设为 7第一行固定显示星期一到星期日第二节以后放课程。一门课如果从第 1 节上到第 2 节就要占两行高度。课程数据从course_schedule查出来之后不能直接往外层 List 里塞要先转换成一个带「行、列、跨行数」的对象。核心计算/** * 把课程安排换算为网格位置 * return int[]{row, col, rowSpan} */ private int[] positionOf(CourseSchedule schedule) { int row schedule.getStartSection() 1; // 第 0 行是星期表头 int col schedule.getDayOfWeek() - 1; // 周一在第 0 列 int span schedule.getEndSection() - schedule.getStartSection() 1; return new int[]{row, col, span}; }拿到rowSpan后在 Adapter 的getView里把对应 item 的 LayoutParams 高度按行数乘以一个固定行高同时把课程名和教室拼进同一行文本LayoutParams param new LayoutParams( LayoutParams.MATCH_PARENT, itemHeight * rowSpan); textView.setText(courseName \n classroom);itemHeight我用 48dp一屏能显示 6 到 7 节。你可以改成48dp到60dp太高小屏幕放不下太低密集到看不清。课程名太长时用android:maxLines2截断不要让它把格子撑变形。这节的周次判断要复用 3.2 里说的week_list解析结果。查询某天的课时不能只看day_of_week还要把当前周次拿出来走一遍week_list判断是否命中。漏掉这一步的典型症状是「学生对不上课课表上却有课」。4.3 选课模块事务、容量校验、防重复三件事一次做完选课是整个系统里业务约束最集中的地方也是答辩时最好演示的亮点。逻辑拆开是三步当前用户没选过这门课这门课的selected_count没超过capacity插入中间表并刷新已选人数。这三步必须包在同一个事务里。否则第一个人选完最后一门课第二个人同时提交两个人都读到剩余名额为 1然后都插入成功课程就超员了。我用SQLiteDatabase手动事务写这一段因为它的结构最清楚database.beginTransaction(); try { SQLiteStatement check database.compileStatement( SELECT COUNT(*) FROM student_course WHERE student_id ? AND course_id ?); check.bindLong(1, studentId); check.bindLong(2, courseId); long existed check.simpleQueryForLong(); check.close(); if (existed 0) { throw new RuntimeException(duplicate enroll); } Cursor cursor database.rawQuery( SELECT capacity, selected_count FROM course WHERE course_id ?, new String[]{String.valueOf(courseId)}); cursor.moveToFirst(); int capacity cursor.getInt(0); int selected cursor.getInt(1); cursor.close(); if (selected capacity) { throw new RuntimeException(course full); } database.execSQL( INSERT INTO student_course(student_id, course_id) VALUES(?, ?), new Object[]{studentId, courseId}); database.execSQL( UPDATE course SET selected_count selected_count 1 WHERE course_id ?, new Object[]{courseId}); database.setTransactionSuccessful(); } catch (RuntimeException e) { runOnUiThread(() - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show()); } finally { database.endTransaction(); }这段代码有两个必须解释的点。第一beginTransaction到setTransactionSuccessful之间的所有 SQL 是原子的任何一个崩掉endTransaction会全部回滚不会出现「插入了选课记录但人数没加」的中间状态。第二两个execSQL的参数都用了Object[]占位和我前面登录取参方式一致避免自行拼接字符串。判断「已选」专门用一个compileStatement预编译语句而不是rawQuery。区别在于这条语句会被重复执行学生可能连续选多门课预编译后每次只换参数比rawQuery多次解析 SQL 快一些。课设规模下性能差异不明显但你答辩时能说出这个取舍比简单说「够用」强得多。5. 避坑指南模拟器能跑真机白屏、数据库升级闪退等 5 个高频翻车点这类课设项目有五个坑几乎每年都有人栽进去。我把现象、原因、解决一条条写出来你遇到时能少走半天弯路。5.1 现象模拟器一切正常真机一点就闪退原因八成不是代码而是 Android 系统权限。Android 6.0API 23开始存储、通讯录这些危险权限需要在运行时动态申请Android 13 之后通知权限也默认关闭。模拟器通常默认授予所有权限真机不会。解决在onCreate里对用到的权限做一轮申请常见是读外部存储和写外部存储。动态申请代码几行就能写完if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE }, 1001); }Build.VERSION.SDK_INT Build.VERSION_CODES.M这个判断保证旧设备不受影响新设备弹窗请求。申请完记得在onRequestPermissionsResult里处理拒绝分支拒绝后给出引导提示而不是假装没发生。5.2 现象Android Studio 里跑得好好的打包的 APK 发给别人安装提示「应用未安装」原因你用的是 Studio 默认的 debug 签名打包或者 testOnly 属性没有关闭。debug 签名的证书有效期短且和正式签名不是同一个覆盖安装会冲突。解决用 Android Studio 的 Build Generate Signed Bundle / APK 走正式签名流程。生成 keystore 后把签名配置写进build.gradleandroid { signingConfigs { release { storeFile file(release.keystore) storePassword 你的密码 keyAlias androidkey keyPassword 你的密码 } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled false testOnly false } } }这里给一个额外提醒密码直接写在build.gradle里课设可以接受但如果将来这个项目要上仓库一定要把release.keystore和密码从版本控制里移除那是你应用的私钥丢了等于失去了更新权。5.3 现象老设备升级 App 后打开某个页面直接闪退Logcat 报no such column原因数据库表结构在代码里改了但数据库版本号没变onUpgrade没有被触发旧数据库还是老结构。解决把Database(version 1)改成version 2。如果用 SQLiteOpenHelper手动把常量改成 2。然后在迁移代码里用 3.4 节写的if (oldVersion 2)包裹 ALTER TABLE 语句。最严格的做法是每个版本对应一个 if 分支不要合并否则以后版本 1 升到 3 时会把版本 2 的改动也一并执行。5.4 现象明明是自己的 App却找不到它创建的数据库文件原因数据库默认存在/data/data/包名/databases/非 root 设备里这个目录对普通用户不可见。Windows 上用文件管理器翻到对应路径只能看到一个空目录或「找不到文件」。解决Android Studio 自带调试工具能直接抓包内文件。打开 Device File ExplorerStudio 右下角侧边栏切到/data/data/你的包名/databases/就能看到.db文件可以直接右键 Save As 下载到本地用 SQLite 工具打开。如果要做成给老师演示的「导出功能」就不能只读这个目录要把它复制到应用的外部存储目录再发送出去。我在下一章给你完整的备份代码。这条经验是血泪换来的第一版系统做完想导数据库给老师检查愣是找不到文件最后只能截屏。5.5 现象课表数据一多滑动或者进入选课页就开始卡顿原因你把数据库查询直接写在了主线程跑。SQLite 在小数据量下响应很快但查询结果要组装成 Java 对象列表再塞进 Adapter这中间对象创建和触发的重新测量布局叠加起来主线程就要忙几秒。解决把所有数据库读写挪到子线程runOnUiThread回来再刷新 UI。如果你用 Room可以在 Dao 方法上加协程或者用AsyncTask。课设项目不一定要引入 RxJava一个简单的线程池就够ExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(() - { ListCourse list repo.queryCourses(); runOnUiThread(() - adapter.setData(list)); });newSingleThreadExecutor保证所有数据库操作在一个队列里串行执行避免读写并发时 SQLite 抛database is locked。这是我建议的默认实现代码量小答辩也好解释。顺便提一句Android Studio 汉化后某些菜单名是中文但 Logcat 里报错还是英文。不要因为界面汉化了就忽略报错信息搜报错的时候把 CtrlF 复制出来的整段英文拿去搜比你自己翻译成中文再搜可靠。6. 答辩前最后一公里数据库备份、课表导出 PDF 和一条不依赖网络的演示路径留到最后的往往是给评委留下印象的东西。功能做完了还要让整个系统看起来是「可交付」的。我的习惯是做三个收尾动作数据库可导出、课表可成 PDF、演示路径不依赖网络。6.1 一键备份把 SQLite 数据库复制到外部存储并用 FileProvider 提供给老师备份功能的核心动作就是把数据库文件从一个目录复制到另一个目录File dbFile new File(getApplicationContext().getDatabasePath(school.db).getPath()); File targetDir getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS); File targetFile new File(targetDir, school_backup_ System.currentTimeMillis() .db); try (FileInputStream in new FileInputStream(dbFile); FileOutputStream out new FileOutputStream(targetFile)) { byte[] buf new byte[1024]; int len; while ((len in.read(buf)) 0) { out.write(buf, 0, len); } } catch (IOException e) { Log.e(Backup, backup failed, e); }getApplicationContext().getDatabasePath(school.db)返回的是数据库在内部存储的真实路径getExternalFilesDir指向应用专属的外部存储目录不需要申请任何运行时权限。备份文件生成后再通过 FileProvider 把它以content://的形式分享出去老师就可以直接用微信或文件管理器接收这个.db文件。FileProvider的配置需要在AndroidManifest.xml里注册 provider并在res/xml/file_paths.xml里声明对应路径。这个导出流程等于给了老师一个「检查数据库里到底有没有数据」的正规入口比口头解释有说服力。6.2 课表导出 PDF用 PdfDocument 画一张简单的表格导出 PDF 功能在这个题目里属于「亮眼加分项」。Android 自带的PdfDocument足够用PdfDocument document new PdfDocument(); PdfDocument.PageInfo pageInfo new PdfDocument.PageInfo.Builder(595, 842, 1).create(); PdfDocument.Page page document.startPage(pageInfo); Canvas canvas page.getCanvas(); Paint paint new Paint(); paint.setTextSize(14); // 逐行绘制课表文本 for (int row 0; row scheduleRows.size(); row) { canvas.drawText(scheduleRows.get(row), 40, 60 row * 28, paint); } document.finishPage(page);595 x 842是 A4 纸的点数尺寸canvas.drawText把每一行文本画到对应的 y 坐标行距 28 点是预留字高。生成完通过 FileProvider 分享出去和数据库备份共用同一个 provider 配置。如果你不想引入 iText 之类的第三方库这个原生方案在课设规模下是足够的。6.3 演示脚本一条不依赖网络的完整路径我最后调试系统时总会给自己写一个演示路径按这个顺序讲逻辑最顺学生端登录 → 查看本周课表 → 进入选课页选一门有剩余名额的课 → 回到课程表确认新增的课程出现了 → 切到教师端登录 → 进入这门课看到学生已加入 → 给这名学生录一个成绩 → 切到学生端查成绩 → 看到分数已更新。每一步之间要有「验证节点」。比如选课前先截图课表选完再截一张两张对比证明数据落库了杀进程重进 App确认登录态还在证明 SharedPreferences 生效清应用数据后再从备份文件恢复数据库证明备份功能是真的能恢复而不是只复制了个文件。这三个动作里有数据库层面的验证、有 UI 层面的验证、也有数据恢复层面的验证覆盖了评审老师最常问的「数据会不会丢」「登录状态怎么保持」「数据库能不能迁移」。答辩之前把这条路径完整走三遍我自己的习惯是第一遍看功能第二遍故意做错操作看会不会崩第三遍只看关键页面截图用来兜底设备突发情况。课设做到这个程度就已经比大多数同类题目往前多走了一步。希望帮到你。本文还有配套的精品资源点击获取