简介针对高校教务选课与成绩管理场景安卓Android教务选课成绩管理系统以AndroidJava为客户端技术栈结合Apache后台服务围绕学生选课、成绩查询、课程信息展示、教师成绩录入等核心业务展开适合移动应用开发学习者与教务系统设计人员作为完整项目参考。压缩包共收录200个文件大小仅3.56MB其中以91个Java源码文件与64个XML布局/配置资源为主辅以10个frm数据表结构文件、1个SQL脚本及JAR依赖、properties配置等构成一套便于导入工程前梳理的移动教务项目骨架。该项目已吸引571人浏览学习对于如此精简的包体而言具备较高参考价值。包内含完整Java业务逻辑、XML界面布局、SQL数据库脚本及课程、学院、班级、学生、教师、成绩、选课等数据表设计可帮助读者快速看清Android教务系统的分层结构与前后端交互方式也能直接用于课程设计、毕业设计或二次开发。1. 安卓Android教务选课成绩管理系统到底在解决谁的什么问题拿到名字里带(2)的压缩包多数是期末大作业的归档版本。教务选课成绩管理系统是安卓开发课里出现频率最高的综合题学生登录后浏览课程、在容量内选课、按学期查成绩教师维护课程、录入分数。整套流程用本地 SQLite 就能完整演示不需要部署任何后端服务。难点不在控件而在教务两个字同一张课程表学生端看剩余名额管理端看维护入口操作的是同一份数据。安卓开发新手拿它练 Activity 流转和列表刷新有经验的工程师能借它审视事务、并发和版本迁移上的习惯。下文按先表结构、再界面代码的顺序拆解最后落到一条验证命令。2. 数据层先行安卓教务系统的 SQLite 表结构与外键关联2.1 本地 SQLite 够用的边界在哪把系统拆开看教务选课成绩管理的核心就是五张表之间的插入、更新与联查。单机演示场景下SQLite 是默认选择随 APK 打包、零配置、SQL 语法和 MySQL 接近答辩时能说清楚模型。只有出现多台设备同时改同一份数据的需求才需要换成后端数据库。三档选型可以直接当判断标准场景存储方案理由期末大作业 / 单机演示SQLite随应用打包事务、索引、外键都支持局域网多客户端选课后端服务 MySQL容量扣减需要集中控制想省样板代码Room 封装 SQLite编译期校验 SQL少写 Cursor 转换对于标题里这种典型课设项目SQLite 是性价比最高的起点。Android Studio 自带的 Database Inspector 能直接查看模拟器上的库内容比反复拉文件省事真机则需要配合开发者模式调试。对安卓开发来说先把数据模型定住后面写界面才不会反复返工。2.2 五张核心表的建表 SQL 与字段释义常见做法是先建 student、teacher、course 三张基础表再建 select_record 中间表和 grade 成绩表。选课记录表承载学生和课程的多对多关系成绩表单独存放分数避免每次联查时把选课状态和成绩混在一起。建表语句如下-- 学生表sno 作为登录账号密码给默认值便于演示 CREATE TABLE IF NOT EXISTS student ( sid INTEGER PRIMARY KEY AUTOINCREMENT, sno TEXT NOT NULL UNIQUE, sname TEXT NOT NULL, password TEXT NOT NULL DEFAULT 123456, major TEXT ); -- 教师表单独建表区分登录角色 CREATE TABLE IF NOT EXISTS teacher ( tid INTEGER PRIMARY KEY AUTOINCREMENT, tno TEXT NOT NULL UNIQUE, tname TEXT NOT NULL, password TEXT NOT NULL DEFAULT 123456 ); -- 课程表容量上限与已选人数分开存 CREATE TABLE IF NOT EXISTS course ( cid INTEGER PRIMARY KEY AUTOINCREMENT, cname TEXT NOT NULL, credit REAL DEFAULT 2.0, teacher_name TEXT, capacity INTEGER DEFAULT 30, selected_count INTEGER DEFAULT 0 ); -- 选课记录表唯一约束是防重复选课的底线 CREATE TABLE IF NOT EXISTS select_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, sid INTEGER NOT NULL, cid INTEGER NOT NULL, select_time TEXT DEFAULT (datetime(now,localtime)), UNIQUE(sid, cid) ); -- 成绩表一门课一条记录重复录入用覆盖 CREATE TABLE IF NOT EXISTS grade ( gid INTEGER PRIMARY KEY AUTOINCREMENT, sid INTEGER NOT NULL, cid INTEGER NOT NULL, score REAL, remark TEXT, UNIQUE(sid, cid) );这段 SQL 里三个细节值得较真。第一UNIQUE(sid, cid) 是防重复选课的数据库级底线即使 Java 层漏判插入时也会抛约束异常程序里 catch 住即可。第二course 表的 selected_count 是冗余字段好处是查列表不用对 select_record 做 count(*)坏处是每次选课必须和插入选课记录放在同一事务里更新否则余量会漂移。第三成绩被误删后重新录入的场景用 INSERT OR REPLACE 最顺手先查再改反而多一次查询窗口期。2.3 外键约束默认关闭一个必须显式打开的开关SQLite 为兼容旧库外键约束默认是关闭的。Android 里通过 SQLiteOpenHelper 获取连接时需要在 onConfigure 里显式打开Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.execSQL(PRAGMA foreign_keys ON); }打开之后删除还有选课记录在案的课程时数据库会抛外键冲突。程序里 catch SQLiteConstraintException返回该课程已有学生选课不可删除比在业务层自己数 count 可靠。这个开关不开删除操作在演示到一半时爆出来的概率非常高。3. 选课与成绩查询的安卓端实现从登录态到列表刷新3.1 登录模块与 SharedPreferences 会话保持教务系统的登录做两件事校验账号密码、记录当前身份。校验走 SQLite 查询身份用 SharedPreferences 保存App 重启不用重新登录。DbHelper 继承 SQLiteOpenHelper把第 2 章的建表语句放进 onCreate查询方法如下public long checkLogin(String sno, String pwd, String role) { SQLiteDatabase db getReadableDatabase(); String table teacher.equals(role) ? teacher : student; String idCol teacher.equals(role) ? tid : sid; Cursor cursor db.rawQuery( SELECT idCol FROM table WHERE sno? AND password?, new String[]{sno, pwd}); long id -1; if (cursor.moveToFirst()) { id cursor.getLong(0); } cursor.close(); return id; }角色通过参数传进来一个方法同时服务学生端和教师端避免写两套重复代码。密码默认值统一为123456演示时不必逐个建账号。登录成功后把用户 id、角色写进 SharedPreferencessp.edit().putLong(userId, id).putString(role, role).apply()即可。apply() 是异步落盘commit() 同步等待登录这种低频操作用哪个都行但模式必须 MODE_PRIVATE全局可读模式从 API 17 起已废弃跑在旧版本模拟器上的项目尤其要注意。3.2 课程列表的余量计算与刷新方式列表页的核心是一条带余量计算的 SQL。剩余名额直接由 capacity 减 selected_count不必关联选课记录表做子查询只有当前学生是否已选该课才需要 join select_recordSELECT c.cid, c.cname, c.credit, c.teacher_name, c.capacity, c.selected_count, c.capacity - c.selected_count AS remain, EXISTS(SELECT 1 FROM select_record r WHERE r.cid c.cid AND r.sid ?) AS selected FROM course c ORDER BY c.cid;这里把当前学生是否已选用 EXISTS 子查询带出来Adapter 里根据 selected 字段决定按钮显示退选还是选课余量小于等于 0 的课直接置灰按钮。刷新时机上有一个常被忽略的细节操作完成后回到列表页必须重新执行查询而不是在本地集合里改一个字段。列表数据一旦被多个界面持有本地副本很容易和数据库不一致。刷新方式的选择参考这张表场景刷新方式原因选课/退选成功后重新查库数据库才是一致性来源管理端改完容量回列表重新查库列表页不持有写权限本地筛选、排序notifyDataSetChanged数据未落库无需重查几十条课程的量级下直接 notifyDataSetChanged() 就够了不需要上 DiffUtil。3.3 选课写入容量校验与记录插入绑成一个事务选课写入涉及两步course.selected_count 加一select_record 插入一行。两步必须在一个事务里否则出现名额扣了但记录没写的半截状态。容量校验最稳的写法是把条件写进 UPDATE 的 WHERE交给数据库行锁裁决而不是先 SELECT 再在 Java 里比较public boolean selectCourse(long sid, long cid) { SQLiteDatabase db getWritableDatabase(); db.beginTransaction(); try { // 容量条件放进 UPDATE只有 selected_count capacity 时才真正改行 SQLiteStatement stmt db.compileStatement( UPDATE course SET selected_count selected_count 1 WHERE cid ? AND selected_count capacity); stmt.bindLong(1, cid); int updated stmt.executeUpdateDelete(); // 返回受影响行数 if (updated ! 1) { return false; // 容量不足或课程不存在事务回滚 } ContentValues rec new ContentValues(); rec.put(sid, sid); rec.put(cid, cid); long rid db.insertWithOnConflict(select_record, null, rec, SQLiteDatabase.CONFLICT_IGNORE); if (rid -1) { return false; // 重复选课回滚会撤销 capacity 的扣减 } db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } }关键点在两个返回值。executeUpdateDelete() 返回受影响行数结果为 0 说明余量不足finally 里的 endTransaction 走回滚capacity 不会被改动。insertWithOnConflict 命中 UNIQUE(sid, cid) 时返回 -1同样触发回滚于是前一步的容量扣减自动撤销——这两处 return false 都是靠事务回滚保证数据一致的绝不能提前调用 setTransactionSuccessful。成绩查询是典型的两表 join学生端显示课程名、学分、分数。SQL 里的别名不要省Cursor 按列名取值时最不容易写错SELECT c.cname, c.credit, g.score, g.remark FROM grade g JOIN course c ON g.cid c.cid WHERE g.sid ? ORDER BY c.cid;4. 事务、并发与版本迁移安卓选课系统最容易翻车的 3 个参数4.1 先查再写有窗口期容量校验要写进 UPDATE第 3 章的写法能成立是因为容量判断和扣减合并成一条 UPDATE由数据库自己保证原子性。很多人的第一版是先 SELECT capacity、selected_count再在 Java 里比大小最后 UPDATE。两个线程同时读到 remain1两边都认为可以选结果选课记录插了两条容量被扣成负数。问题不在 UPDATE 本身SQLite 是单写者写锁会串行化问题在先读后写的窗口期里判断依据已经过期。课设里常见的补救是给所有数据库操作套一个单线程 Executor 顺序执行确实能挡住并发但也掩盖了问题。更硬核的方案是条件 UPDATE 配合受影响行数判断容量边界由数据库背书。答辩被问到多人同时选最后一门课怎么办时能讲出这两层说明真的理解并发写。4.2 WAL 模式与单写线程的取舍安卓开发里数据库读写放主线程会卡顿几十毫秒的写操作在列表滚动时就能被感知。常见做法是自定义单线程 ExecutorService所有 DB 操作排队执行private final ExecutorService dbExecutor Executors.newSingleThreadExecutor();再配合 WAL 模式让读不阻塞写dbHelper.setWriteAheadLoggingEnabled(true);WAL 的两个代价要提前知道。第一数据库目录下会多出 -wal 和 -shm 两个文件备份数据必须三个文件一起拷只拷 .db 会看到数据丢了。第二多线程并发写时即使底层是同一个 SQLiteDatabase 实例也可能触发 SQLiteDatabaseLockedException所以单写线程的 Executor 依然是课设项目最稳的调度方式。提示WAL 模式下从 /data/data/包名/databases/ 拉取数据做备份时记得连同 .db、-wal、-shm 三个文件一起处理否则可能拿到旧版本内容。4.3 DB_VERSION 不是装饰onUpgrade 只做增量迁移SQLiteOpenHelper 构造函数里的版本号是最容易被无视的参数。表结构加一列时只改 onCreate 里的建表语句对已安装的旧库完全不生效因为数据库文件已经存在不会重新执行 onCreate。正确做法是版本号加 1在 onUpgrade 里写增量迁移private static final int DB_VERSION 3; // 从 1 逐级升上来 Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { // 第一次升级给课程表加学期字段 db.execSQL(ALTER TABLE course ADD COLUMN semester TEXT DEFAULT 2025-2026-1); } if (oldVersion 3) { // 第二次升级给成绩表加查询索引 db.execSQL(CREATE INDEX idx_grade_sid ON grade(sid)); } }每加一次结构变更版本号加一、onUpgrade 加一个 if 分支老用户升级时从 oldVersion 逐级执行到 newVersion。最常见的错误写法是 DROP TABLE 后重建数据全丢演示升级时当场穿帮。答辩被问升级后学生选课记录还在不在答用 ALTER TABLE 做增量迁移就是得分点。4.4 四个参数速查参数设置位置作用常见误用foreign_keysonConfigure开启外键约束不写 PRAGMA删除冲突在运行期才暴露WALsetWriteAheadLoggingEnabled读写并发备份只拷 .db 不带 wal 文件DB_VERSIONSQLiteOpenHelper 构造参数触发 onUpgrade永远写 1改表后老版本崩溃CONFLICT_IGNOREinsertWithOnConflict防重复选课try-catch 吞掉 SQLiteConstraintException5. 打包前用一条指令验证数据落盘与选课主流程界面显示正常不代表数据写进了数据库。开发期验证落盘最直接的办法是 adb 进 shell 查库文件。数据库在 /data/data/包名/databases/ 下debug 版可以用 run-as 直接读# 列出数据库文件 adb shell run-as com.example.eduadmin ls databases/ # 直接查课程表的真实数据 adb shell run-as com.example.eduadmin sqlite3 \ databases/edu_admin.db \ SELECT cid, cname, capacity, selected_count FROM course;模拟器自带 sqlite3真机大部分没有这个二进制。真机调试时改用 exec-out 把库拉到本地再查命令改成adb exec-out run-as com.example.eduadmin cat databases/edu_admin.db /tmp/edu_admin.db然后用本机 sqlite3 打开。注意 run-as 只对 debuggable 的包生效打正式签名包后会被拒绝这是正常的。验证顺序跟着主流程走学生账号登录 → 选一门课 → select_record 多出一条 → course 的 selected_count 加一 → 重复选同一门确认插入被忽略 → 管理端录成绩 → 切回学生账号查成绩一致。五步全部符合预期再在 Android Studio 里执行 Build Build APK(s) 出包。最后一招是初始化数据的正确姿势。空数据库演示很尴尬常见做法是在 onCreate 末尾调用 seedData()插 10 门课程和几个带默认密码的学生账号。注意 onCreate 只在数据库首次创建时执行库文件一旦存在改了种子数据也不会生效。开发期想反复重置一条命令删库最快adb shell run-as com.example.eduadmin rm databases/edu_admin.db下次启动 App 时 SQLiteOpenHelper 发现数据库文件不存在会自动重走 onCreate种子数据全部回来不需要卸载重装也不会丢 Android Studio 的调试设置。在改表结构、调种子数据、查重复选课的开发循环里这条命令比设置里清数据快得多。本文还有配套的精品资源点击获取