简介:这份资源面向Android初学者与移动开发课程学习者,提供一套以SQLite为数据核心的学生成绩管理完整项目,可用于课程设计参考或CRUD与界面开发的练手场景。压缩包共1641个文件,约18.93MB,以685个class、277个dex等编译产物为主,辅以94个xml布局与配置、40个java源码、53个jar依赖及14个apk安装包,另有json、properties、gradle等构建文件,完整保留了工程结构与可运行产物。目前已有4243人学习下载。项目覆盖SQLiteOpenHelper建表与版本管理、Java业务逻辑、RecyclerView等原生控件布局,并涉及条件查询、ORDER BY排序、CursorLoader异步加载与异常处理等要点,读者可据此理解数据模型设计、增删改查实现与Material Design界面组织方式,适合对照源码梳理Android本地存储应用的开发脉络。
1. 从一张成绩单说起:为什么用 Android + SQLite 做学生成绩管理
带过几届课设之后我发现一个规律:凡是选题写「学生成绩管理系统」的,十有八九最后做成了控制台增删改查,交上去能跑,答辩一问就露馅。问题不在题目老,在于没把「移动端」这个前提当回事。成绩管理真正的使用场景是老师在课堂上用手机录分、班主任在办公室外查排名、学生自己翻成绩单,这些动作都发生在没有电脑的时刻。所以这套方案选 Android 原生 + SQLite 本地库,不是图省事,而是让数据跟着人走:单机可用、断网可用、装完即用,不需要额外部署服务器。它适合两类人:一是要交毕设或课设、需要一套结构完整能讲清楚的作品;二是刚接触 Android 数据持久化、想拿一个真实业务把 SQLite 增删改查、事务、游标吃透的开发者。下面按需求、设计、实现、避坑、验收的顺序,把整套东西拆开讲。
2. 需求与角色拆解:三类用户、四张核心表定住系统边界
动手写第一行代码之前,先把「谁在用、用什么、数据长什么样」定死。这一步偷懒,后面改表结构会改到怀疑人生。
2.1 三类角色的权限边界
学生成绩管理系统听起来简单,但角色一多,权限就容易糊。我一般按最小可用原则划三条线:
| 角色 | 能做什么 | 不能做什么 |
|---|---|---|
| 管理员 | 管理班级、课程、教师账号,导入学生名单 | 不能直接改学生分数 |
| 教师 | 录入/修改所授课程的成绩,查看本课程统计 | 不能跨课程改分,不能删学生 |
| 学生 | 查看本人各科成绩与总分排名 | 不能查看他人成绩 |
这张表的价值在于:它直接决定了后面每个界面的入口和 SQL 的 where 条件。比如学生端查成绩,SQL 里必须带student_id = ?,而不是查全表再在 Java 层过滤——后者是课设里最常见的越权漏洞,答辩老师一问「学生能不能看到别人分数」就答不上来。
2.2 四张核心表的结构设计
数据模型不用搞复杂,四张表足够撑起完整业务:学生表、课程表、成绩表、账号表。成绩表是核心,它把学生和课程连起来,形成多对多关系。
-- 学生表:学号做主键,避免重名问题 CREATE TABLE student ( student_no TEXT PRIMARY KEY, -- 学号,业务主键 name TEXT NOT NULL, class_name TEXT NOT NULL, gender INTEGER DEFAULT 0 -- 0未知 1男 2女 ); -- 课程表:课程编号唯一,学分用于加权 CREATE TABLE course ( course_id TEXT PRIMARY KEY, course_name TEXT NOT NULL, credit REAL NOT NULL DEFAULT 1.0, teacher_id TEXT ); -- 成绩表:联合主键防止同一学生同一课程重复录入 CREATE TABLE score ( student_no TEXT NOT NULL, course_id TEXT NOT NULL, score REAL, exam_time TEXT, PRIMARY KEY (student_no, course_id), FOREIGN KEY (student_no) REFERENCES student(student_no), FOREIGN KEY (course_id) REFERENCES course(course_id) ); -- 账号表:role 区分身份,关联业务主键 CREATE TABLE account ( username TEXT PRIMARY KEY, password TEXT NOT NULL, -- 存哈希,不存明文 role INTEGER NOT NULL, -- 1管理员 2教师 3学生 ref_no TEXT -- 教师工号或学生学号 );几个设计点值得说清楚。成绩表用(student_no, course_id)联合主键,而不是自增 id,好处是数据库层面天然挡住重复录入——老师手滑点两次保存,第二次直接主键冲突,比在代码里查一遍再插更可靠。score允许为 NULL,表示「已选课未出分」,这个状态在统计平均分时要排除,不能当 0 算,否则拉低全班均分。外键约束在 SQLite 里默认是关闭的,必须在onConfigure里手动打开,这一点后面避坑章节会专门讲。
2.3 功能清单与优先级
把功能按「必须有 / 应该有 / 可以有」排一遍,课设时间紧就砍后面两档:
- 必须有:登录鉴权、学生/课程/成绩的增删改查、按学生查成绩、按课程查成绩。
- 应该有:班级平均分与排名、成绩录入的批量保存、修改密码。
- 可以有:成绩导出为 CSV、简单图表展示分数分布。
优先级的意义在于答辩时的取舍。老师问「为什么没做图表」,你答「核心链路是数据准确性和权限隔离,图表属于展示层增强,在有限周期内优先保证前者」,这比硬凑一个半成品图表更有说服力。
3. 总体架构与数据访问层:从 Activity 到 SQLite 的分层落地
架构不用堆名词,但这个项目必须分层,否则 SQL 散落在各个 Activity 里,改一个字段要翻十个文件。
3.1 三层结构:UI、业务、数据访问
整体分三层,职责单一:
- UI 层:Activity / Fragment,只负责界面渲染和用户输入收集,不写 SQL。
- 业务层:Service 或 Manager 类,处理权限判断、分数合法性校验、统计计算。
- 数据层:DAO 类 +
SQLiteOpenHelper子类,唯一接触数据库的地方。
数据流向是单向的:UI 调业务,业务调 DAO,DAO 返回实体对象,逐层回传。这样做的直接收益是测试方便——DAO 可以脱离界面单独验证,业务逻辑也能用假数据跑。很多课设把查询写在按钮点击事件里,界面和 SQL 缠在一起,答辩演示时改个查询条件就得重新编译整个界面,非常被动。
3.2 SQLiteOpenHelper 的建库与升级
数据库的创建和版本管理集中在一个 Helper 里,这是 Android 持久化的标准入口:
public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "school_score.db"; private static final int DB_VERSION = 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.setForeignKeyConstraintsEnabled(true); // 关键:开启外键约束 } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_STUDENT); db.execSQL(CREATE_COURSE); db.execSQL(CREATE_SCORE); db.execSQL(CREATE_ACCOUNT); seedAdmin(db); // 首次建库插入默认管理员 } @Override public void onUpgrade(SQLiteDatabase db, int oldV, int newV) { // 课设阶段简单处理:版本变化重建表,生产环境应做数据迁移 db.execSQL("DROP TABLE IF EXISTS score"); db.execSQL("DROP TABLE IF EXISTS student"); db.execSQL("DROP TABLE IF EXISTS course"); db.execSQL("DROP TABLE IF EXISTS account"); onCreate(db); } }onConfigure里开外键是血泪经验。SQLite 出于兼容性默认不校验外键,你不开,插一条学号不存在的成绩也能成功,数据就脏了。onUpgrade里直接删表重建,在课设阶段可以接受,但要在文档里注明「生产环境需做增量迁移」,答辩时主动说出来是加分项,说明你知道边界在哪。
3.3 DAO 层的增删改查封装
以成绩 DAO 为例,把常用操作封成方法,参数用实体对象传,避免一长串散参数:
public class ScoreDao { private final DBHelper helper; public ScoreDao(Context ctx) { this.helper = new DBHelper(ctx); } // 插入或更新成绩:用 REPLACE 简化「有则改、无则插」 public boolean saveScore(String studentNo, String courseId, double score) { SQLiteDatabase db = helper.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("student_no", studentNo); cv.put("course_id", courseId); cv.put("score", score); cv.put("exam_time", new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA) .format(new Date())); long row = db.insertWithOnConflict("score", null, cv, SQLiteDatabase.CONFLICT_REPLACE); return row != -1; } // 查某学生全部成绩,带课程名,用 JOIN 一次取回 public List<ScoreItem> queryByStudent(String studentNo) { SQLiteDatabase db = helper.getReadableDatabase(); String sql = "SELECT c.course_name, c.credit, s.score " + "FROM score s JOIN course c ON s.course_id = c.course_id " + "WHERE s.student_no = ? ORDER BY c.course_id"; Cursor cur = db.rawQuery(sql, new String[]{studentNo}); List<ScoreItem> list = new ArrayList<>(); try { while (cur.moveToNext()) { list.add(new ScoreItem(cur.getString(0), cur.getDouble(1), cur.getDouble(2))); } } finally { cur.close(); // 游标必须关,否则连接泄漏 } return list; } }insertWithOnConflict配CONFLICT_REPLACE是这套方案里最省事的一招:老师改分和录分走同一个入口,不用先查再判断。rawQuery里用?占位符而不是字符串拼接,这是防 SQL 注入的基本功,虽然本地库风险低,但习惯要正。Cursor放在try-finally里关闭,是 Android 数据访问的硬规矩,忘了关,反复查询后会出现「too many open files」的崩溃,而且这种崩溃在演示时不一定复现,属于典型的玄学问题。
3.4 统计查询:平均分与排名的 SQL 写法
统计功能是答辩的加分点,也是检验 SQL 功力的地方。班级某课程平均分:
SELECT c.course_name, AVG(s.score) AS avg_score, COUNT(s.score) AS cnt FROM score s JOIN course c ON s.course_id = c.course_id JOIN student st ON s.student_no = st.student_no WHERE st.class_name = ? AND s.score IS NOT NULL GROUP BY c.course_id;注意WHERE s.score IS NOT NULL这个条件,未出分的记录不能进平均分分母,否则均分被拉低。学生总分排名用窗口函数(Android 自带的 SQLite 版本较新时支持):
SELECT student_no, SUM(score) AS total, RANK() OVER (ORDER BY SUM(score) DESC) AS rank_no FROM score WHERE score IS NOT NULL GROUP BY student_no;如果目标机型 SQLite 版本偏老不支持窗口函数,就退化成子查询计数:统计「总分比我高的人数 + 1」。两种写法都要会,因为真机碎片化是绕不开的现实。
4. 界面与业务逻辑实现:登录鉴权、成绩录入与统计展示
数据层稳了,界面层就是把它接出来。这一章讲三个最容易被做砸的界面。
4.1 登录与角色跳转
登录界面只做两件事:校验账号密码、按角色跳不同主页。密码存哈希,用MessageDigest做 SHA-256 加盐:
public static String hash(String pwd, String salt) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update((pwd + salt).getBytes(StandardCharsets.UTF_8)); byte[] digest = md.digest(); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); }盐值可以固定存在账号表里,也可以按用户名派生。课设阶段用固定盐加 SHA-256 已经比明文强很多,答辩时能讲清「为什么不存明文、哈希为什么不可逆」就够。登录成功后把role和ref_no存进SharedPreferences,后续界面据此决定显示哪些按钮,而不是每次重新查库。
4.2 成绩录入界面:批量保存与事务
老师录分是批量场景,一个班几十人,逐条提交体验很差。做法是界面上用列表展示学生名单,每行一个输入框,点「保存」时统一提交,并且包在一个事务里:
public boolean saveBatch(List<ScoreItem> items, String courseId) { SQLiteDatabase db = helper.getWritableDatabase(); db.beginTransaction(); try { for (ScoreItem it : items) { if (it.score < 0 || it.score > 100) { throw new IllegalArgumentException("分数越界:" + it.studentNo); } ContentValues cv = new ContentValues(); cv.put("student_no", it.studentNo); cv.put("course_id", courseId); cv.put("score", it.score); db.insertWithOnConflict("score", null, cv, SQLiteDatabase.CONFLICT_REPLACE); } db.setTransactionSuccessful(); return true; } catch (Exception e) { return false; // 任一条失败,整批回滚 } finally { db.endTransaction(); } }事务的意义是「要么全成、要么全不成」。假设录到第 20 条时发现某分数是 120,抛异常回滚,前 19 条也不会写进去,老师改完再整体提交,数据不会出现「一半新一半旧」的中间态。分数范围校验放在业务层而不是数据库约束里,是为了给出明确提示,而不是让用户看到一个冷冰冰的约束冲突。
4.3 统计展示:用 RecyclerView 承载结果
统计结果用RecyclerView展示,Adapter 里绑定课程名、均分、人数。这里有个细节:统计查询可能耗时,虽然本地库通常很快,但班级人数多、联表复杂时仍建议放到子线程,用ExecutorService或AsyncTask(老项目常见)执行,回调主线程更新 UI。直接在onCreate里查库,数据量大时会有轻微卡顿,答辩演示时被看出来就尴尬。
界面层的原则就一条:Activity 里不出现Cursor和 SQL 字符串。所有数据通过 DAO 方法拿实体列表,界面只认实体。这样换数据库、加缓存都不影响界面,也是分层架构真正落地的地方。
5. 避坑与排查:五个最容易翻车的点
这一章是踩坑记录,每条按「现象 → 原因 → 解决」写,照着排查能省不少时间。
5.1 外键约束不生效,脏数据悄悄进库
现象:插入一条学号不存在的成绩,居然成功了,查询时 JOIN 不出来,界面上显示空白行。原因:SQLite 默认关闭外键校验,onCreate里写了FOREIGN KEY也没用。解决:在SQLiteOpenHelper的onConfigure里调用setForeignKeyConstraintsEnabled(true),注意是onConfigure不是onCreate,写错位置不生效。
5.2 游标未关闭导致偶发崩溃
现象:反复进出成绩查询页,几次之后应用闪退,日志里有文件描述符相关报错。原因:Cursor用完没close(),连接持续泄漏。解决:所有rawQuery返回的游标放进try-finally,finally里关闭;或者用try-with-resources(API 16+ 的 Cursor 实现了 Closeable)。这条没有捷径,只能靠代码规范。
5.3 主线程查库造成界面卡顿
现象:进入统计页时界面停顿半秒,列表才刷出来。原因:查询和统计在 UI 线程同步执行。解决:把 DAO 调用包进ExecutorService,结果通过Handler或runOnUiThread回主线程。本地库虽快,但联表加聚合在数据量上来后并不便宜,养成异步习惯。
5.4 分数为 NULL 被当成 0 参与统计
现象:某课程还没出分,平均分却明显偏低。原因:AVG会忽略 NULL,但如果代码里先getDouble拿到 0 再参与计算,NULL 就变成了 0。解决:查询时显式WHERE score IS NOT NULL,实体里用Double包装类型而非double基本类型,让「未出分」有独立状态。
5.5 数据库升级直接删表丢数据
现象:改了一版表结构,装上后原有成绩全没了。原因:onUpgrade里DROP TABLE重建。解决:课设阶段可接受但要写进文档说明;更稳妥的做法是按版本号逐级ALTER TABLE加字段,或先把旧数据导出到临时表再迁移。答辩时能讲清「为什么这么选、生产环境怎么做」比代码本身更重要。
6. 验收与答辩:怎么证明这套系统真的能用
验收不是把 App 打开点一遍就完事,要按「数据正确性、权限隔离、异常处理」三条线走一遍,每条都有可演示的动作。
数据正确性:准备一份已知答案的测试数据,比如 3 个学生、2 门课、6 条成绩,手工算出每门课均分和每人总分,录入后逐项核对。重点演示「改分」和「重复录入」两个动作,证明CONFLICT_REPLACE生效、不会产生重复行。再故意录一个 120 分,展示事务回滚、整批不落库。
权限隔离:用学生账号登录,尝试通过界面入口和直接构造查询两条路径访问他人成绩,证明 where 条件卡死在student_no上。这一条最能体现设计深度,很多课设栽在这里。
异常处理:断网不影响使用(本地库)、杀进程重进数据还在、连续快速点击保存不产生重复记录。这三条演示完,系统的健壮性基本就立住了。
答辩陈述建议按「需求 → 表设计 → 分层 → 一个核心 SQL → 一个避坑点」的顺序讲,控制在五分钟内。老师最爱问的三个问题提前准备好答案:为什么用 SQLite 不用网络数据库(单机场景、零部署、离线可用);密码为什么不能明文(哈希不可逆、防拖库);外键为什么默认不开(兼容性设计,需手动启用)。把这三个答利索,基本就稳了。
我自己的习惯是:每次交付前,拿一份全新的测试数据从建库开始完整走一遍,不跳过任何一步,因为「在我机器上是好的」这句话在答辩现场一文不值。从那以后我每次做带本地库的项目,都强制走一遍「清数据 → 重建 → 全流程录入 → 核对统计」的回归,宁可多花二十分钟,也不在台上翻车。希望这套结构能帮到你,照着搭一遍,剩下的就是往里填业务细节了。
本文还有配套的精品资源,点击获取