☰
Android点名系统源码解析:SQLite全流程实战与避坑指南
2026/10/22 2:56:40 网站建设 项目流程

简介:基于Android平台的点名考勤系统完整项目,定位Android毕业设计/课程设计场景,面向需要快速实现课堂考勤、出勤统计一体化管理的学生开发者。客户端功能覆盖老师登录、班级信息查看、点名签到、统计信息、个人信息及收藏;后台管理员端则包含学生信息、老师信息、班级信息的添加与查看,前后台权限清晰,适合作为移动端管理类毕设的参考蓝本。资源包共1505个文件,容量约69.73MB,内含Android Studio工程所需的Java源码(224个)、XML界面布局(251个)、class编译文件(366个)与jar依赖库(170个),同时提供APK安装包、SQL数据库脚本、多类型图片与JSP/HTML辅助页面,可直接导入运行,也可安装到手机上演示点名流程。压缩包目录结构完整,便于按模块查找登录、班级、签到、统计等关键代码,节省从零搭建的时间。目前已有184人学习下载,适合需要快速搭建考勤系统、理解Android客户端业务逻辑,并希望获得可演示成果的毕业设计作者。

1. 为什么点名系统是Android课程设计的稳妥选题:一套能跑通全流程的源码

做Android Studio课程设计或安卓毕业设计时,最怕的不是不会写代码,而是选题太泛、功能做不闭环。点名系统这个题目正好卡在中间:数据库就三张表,界面不复杂,但登录、列表、表单、统计这几件事全都能练到,老师也爱看。这套资源把“老师点名”和“后台管理”拆成两端:老师端管登录、班级、点名签到、出勤统计和个人信息,管理端管学生、老师、班级信息的增删改查,功能链是完整的。拿到手先跑一遍主流程,再按自己的界面风格改,能省掉不少选题和搭框架的时间。下面从客户端功能说起,把每个模块的实现路径、参数和坑一次性讲清楚。

2. 客户端功能拆解:登录、班级列表、点名、统计在SQLite里怎么闭环

客户端要回答的核心问题只有一个:老师打开App之后,从“我是谁”到“今天谁没来”需要几步。这条链路是:老师登录 → 进入班级列表 → 选择一个班 → 打开学生名单 → 逐个标记出勤状态 → 保存到本地数据库 → 在统计页按日期查结果。每一步对应一个Activity,数据统一存在本地SQLite里,没有网络请求,所以演示时不会因为连不上WiFi而陷入尴尬。

2.1 登录模块:本地SQLite校验账号的完整写法

课程设计阶段的点名系统,账号校验用本地SQLite就够了,没必要上服务端。常见做法是管理端先把老师账号写进teacher表,客户端登录时做一次等值查询。选SQLite而不是文件存储的原因很简单:文件存储你得自己处理并发和格式,SQLite帮你把事务、索引、查询都封装好了,代码量少一半。

建表语句放在SQLiteOpenHelper的onCreate里,点名系统涉及的四张表一次建完:

public class DBHelper extends SQLiteOpenHelper { public DBHelper(Context context) { super(context, "sign_db.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { // 老师表:账号唯一,密码明文存储(课程设计要求够用) db.execSQL("CREATE TABLE teacher (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "username TEXT UNIQUE," + "password TEXT," + "real_name TEXT)"); // 班级表 db.execSQL("CREATE TABLE class_info (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "class_name TEXT," + "grade TEXT)"); // 学生表:class_id关联班级表 db.execSQL("CREATE TABLE student (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "student_no TEXT UNIQUE," + "student_name TEXT," + "class_id INTEGER)"); // 出勤表:status 0缺勤 1出勤 2迟到 3请假 db.execSQL("CREATE TABLE attendance (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "student_id INTEGER," + "class_id INTEGER," + "status INTEGER DEFAULT 0," + "date TEXT)"); } }

参数说明:数据库名sign_db.db,version设为1;后续如果要加字段,把version改成2并实现onUpgrade,否则旧设备上不会执行新的建表语句。username加了UNIQUE约束,重复账号插入时会抛异常,管理端添加老师前要先做查重。

登录校验的方法长这样:

public boolean checkLogin(String username, String password) { SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.rawQuery( "SELECT * FROM teacher WHERE username=? AND password=?", new String[]{username, password}); boolean success = cursor.moveToFirst(); cursor.close(); db.close(); return success; }

逻辑说明:用占位符?而不是字符串拼接,是为了防止SQL注入,这个习惯在答辩时能加分。密码是明文比较,真实项目里要加盐哈希,但教学演示项目里明文的好处是数据流一目了然,老师问起来你能快速讲清楚。

登录成功后用SharedPreferences记住登录态,避免每次冷启动都重新输入账号:

SharedPreferences sp = getSharedPreferences("login_info", MODE_PRIVATE); sp.edit().putBoolean("is_login", true) .putInt("teacher_id", teacherId) .apply();

这里有个容易被问到的点:为什么用MODE_PRIVATE?因为登录态只属于当前App,不需要跟其他应用共享,MODE_PRIVATE是Android默认且最安全的文件访问模式。退出登录时把is_login置false就行,逻辑对称,好讲。

2.2 点名签到:从班级Spinner到出勤记录的状态流转

点名的交互流程是:老师先选班级,学生列表跟着刷新,然后对每个学生标记出勤状态,最后点保存批量写入出勤表。这里用RecyclerView承载学生列表,每个item右边放一个下拉框或一组单选按钮,不建议逐个弹AlertDialog,数据量大时操作效率太低。

保存点名结果的核心代码:

public void saveAttendance(ArrayList<StudentStatus> list, int classId, String date) { SQLiteDatabase db = dbHelper.getWritableDatabase(); db.beginTransaction(); try { for (StudentStatus item : list) { ContentValues values = new ContentValues(); values.put("student_id", item.studentId); values.put("class_id", classId); values.put("status", item.status); // 0缺勤 1出勤 2迟到 3请假 values.put("date", date); // 格式必须是 yyyy-MM-dd db.insert("attendance", null, values); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } }

逻辑说明:一个事务包住批量写入,中途任何一条失败都会回滚,数据库里不会出现半截数据。这个细节在课程设计答辩时是加分项。status用int而不是String,是因为后面统计时要用CASE WHEN做聚合,int比较和分组比字符串快,也不容易拼错。

日期参数date的格式必须全局统一,我一般用SimpleDateFormat强制转成yyyy-MM-dd:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); String today = sdf.format(new Date());

如果一处存了2025-03-04,另一处存了2025/03/04,统计模块GROUP BY会按两个维度分组,出勤率直接算错。这个问题在第4章的坑里还会再遇到。

2.3 统计信息:按日期聚合出勤率,SQL写法决定结果对错

统计页要回答的问题是:某一天某个班,应到几人、实到几人、缺勤几人。最直接的做法是把出勤表和学生表做关联查询,按班级和日期聚合。核心SQL如下:

String sql = "SELECT s.class_id, " + "COUNT(s.id) AS total_students, " + "SUM(CASE WHEN a.status = 1 THEN 1 ELSE 0 END) AS present_count " + "FROM student s " + "LEFT JOIN attendance a ON s.id = a.student_id AND a.date = ? " + "WHERE s.class_id = ? " + "GROUP BY s.class_id"; Cursor cursor = db.rawQuery(sql, new String[]{date, classId});

这里最关键的坑是LEFT JOIN的过滤条件位置。a.date = ?这个条件必须放在ON子句里,不能放到WHERE子句。如果写成WHERE a.date = ?,SQL会先做LEFT JOIN再把没有出勤记录的行过滤掉,效果等于INNER JOIN,统计出的应到人数会比全班实际人数少。演示时数据量小不容易暴露,但老师一旦手动往数据库里插一条没有出勤记录的学生,当场就能看出数字对不上。

统计结果的展示不建议引入第三方图表库。课程设计阶段用自绘View画一个横向条形图就够,界面干净,还少一个依赖冲突的风险源。自绘方案在第5章会给示例代码。

3. 后台管理端:学生、老师、班级三张表的增删改查怎么落地

管理端和客户端跑在同一个App里,靠不同的入口进入。管理端的核心职责是维护基础数据:老师账号、学生名单、班级信息。理解这一点后再看数据库结构,就清楚为什么表之间的关系是这样设计的了。

3.1 数据表设计与四张表之间的引用关系

整个点名系统一共四张核心表,关系如下:

表名关键字段作用
teacherid, username, password, real_name老师登录账号,管理端添加
class_infoid, class_name, grade班级基础信息
studentid, student_no, student_name, class_id学生属于哪个班级
attendanceid, student_id, class_id, status, date每天的点名记录

student表通过class_id关联class_info表,attendance表同时冗余了student_id和class_id。为什么不只存student_id?因为统计时经常要按班级维度查一整天的数据,如果只有student_id,每次都要先JOIN出班级再聚合,SQL写起来绕,数据量大时性能也会差。课程设计阶段几千条数据,冗余这个字段带来的查询便利远大于存储成本。

3.2 管理端添加学生:查重、插入、刷新三步走

管理端最常见的操作是添加学生。流程是:输入学号、姓名、选班级 → 校验学号是否重复 → 插入student表 → 刷新列表。代码实现:

public long addStudent(String studentNo, String name, int classId) { SQLiteDatabase db = dbHelper.getWritableDatabase(); // 第一步:查重,学号不能重复 Cursor cursor = db.rawQuery( "SELECT id FROM student WHERE student_no = ?", new String[]{studentNo}); if (cursor.moveToFirst()) { cursor.close(); return -1; // 返回-1表示学号已存在 } cursor.close(); // 第二步:插入新学生 ContentValues values = new ContentValues(); values.put("student_no", studentNo); values.put("student_name", name); values.put("class_id", classId); return db.insert("student", null, values); }

逻辑说明:返回-1表示学号重复,Activity里根据返回值决定是弹Toast提示还是跳转页面。这里先查再插的做法在并发场景下不是绝对安全,但管理端只有一个人操作,完全够用。如果想把逻辑收得更严谨,可以把student_no的UNIQUE约束利用起来,用insertWithOnConflict方法配合CONFLICT_IGNORE,省掉一次查询。

还有一个细节:删除班级时要考虑级联问题。如果某个班还有学生,直接删班级会导致学生表里的class_id变成悬空引用,点名时查不到班级名称。常见做法是删除前先统计该班学生数量,大于0就提示“请先清空该班学生”,而不是直接执行DELETE。这个防御性判断写起来不难,但对数据完整性很重要。

3.3 列表刷新机制:onResume重查是最简单可靠的方案

管理端在添加或编辑完一条数据后,列表要立刻刷新。最常见的写法是在onResume里重新查询数据库并调用notifyDataSetChanged,而不是用接口回调去通知上一个页面。回调链写多了容易晕,而且生命周期处理不好会出现空指针。

@Override protected void onResume() { super.onResume(); loadStudentList(); adapter.notifyDataSetChanged(); }

逻辑说明:onResume在每次回到这个页面时都会触发,包括从添加页面返回、从详情页返回、从后台切回前台。代价是每次都重新查一遍数据库,但几千条数据毫秒级完成,课程设计完全能接受。如果数据量到几万条甚至更多,可以换成DiffUtil做增量更新,那不是这个项目需要的复杂度。

班级列表和学生列表联动是管理端一个常见的交互要求。实现方式是用Spinner承载班级列表,onItemSelectedListener里重新查询该班学生并刷新适配器。注意一个隐蔽问题:Spinner初始化时会默认选中第0项,会触发一次onItemSelected回调,此时如果学生适配器还没准备好,容易空指针。解决办法是在回调里判断适配器是否已经初始化,或者用一个boolean标志位跳过第一次回调。

4. 复现与排错避坑:从项目导入到跑起来的五个拦路虎

拿到这套源码后,第一件事不是打开代码看逻辑,而是先把环境对齐。课程设计项目的创建年份往往跟当前开发环境差两三个大版本,Gradle、SDK、JDK任何一个对不上,编译都会卡在第一关。

4.1 环境版本匹配:Gradle与SDK的隐性门槛

如果直接用当前版本的Android Studio打开旧项目,Studio会提示升级Gradle插件版本。通常选择自动升级就能过,但国内网络环境经常导致Gradle发行包下载超时,进度条卡在下载阶段不动。

这时候的替代方案是修改gradle-wrapper.properties里distributionUrl,手动指定一个你已经下载过或者镜像站能快速拉取的版本:

distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip

参数说明:8.7对应Android Gradle Plugin 8.x系列。如果项目用的AGP版本太低,而SDK Platform装的是最新版,还会报AAPT2相关错误,日志里会出现类似“Failed to find target with hash string”的提示。解决办法是打开SDK Manager,把项目build.gradle里targetSdkVersion对应的SDK Platform勾选安装。

还有一个小提示:项目包里那个resources.ap_文件是Android构建过程中生成的资源中间文件,不是APK损坏,也不要手动改名成APK安装,直接忽略它就好。真正要装的是项目构建出来的app-debug.apk。

4.2 四个高频踩坑:从闪退到数据对不上

下面四条都是这个项目复现过程中最可能遇到的问题,每一条我都按现象、原因、解决的顺序写。

坑一:App安装后打开就闪退,日志报no such table

现象:首次安装点击图标,App闪退,Logcat里明确提示SQLiteException: no such table: teacher。 原因:数据库的onCreate没有执行成功。常见原因有两个,一是DBHelper构造函数传了不同的数据库名,导致建表语句写在A文件里,查询却在查B文件;二是主界面在DBHelper初始化之前就执行了查询,而查询SQL里引用了还没建出来的表。 解决:先卸载App再重新安装,让onCreate从头执行;检查DBHelper构造函数的数据库名是否全局一致;在onCreate里加一行Log.d打印建表完成日志,确认建表流程走到了哪一步。

坑二:管理端添加了老师,客户端登录却提示账号不存在

现象:管理端提示老师添加成功,切到客户端登录界面输入同样的账号密码,提示用户名或密码错误。 原因:两个端虽然界面不同,但共用同一个数据库文件,数据大概率是写进去了,问题出在登录SQL的字段名写错,最常见的是把username写成了user_name,字段名对不上就查不到结果。 解决:用Android Studio自带的Database Inspector打开项目数据库文件,直接查看teacher表的真实字段名,把登录SQL里的字段名改正确。这类问题光看代码很难发现,看表结构最直接。

坑三:统计页的应到人数比实际少

现象:班级里明明有10个人,统计页显示应到8人,缺勤人数一直是0。 原因:查询用了INNER JOIN,没有出勤记录的学生被过滤掉了,分母变成了“有记录的人数”而不是“全班人数”。另一个常见原因是status没有设置默认值,部分记录里status为NULL,SUM(CASE WHEN status = 1)的计算结果把NULL忽略掉了。 解决:把统计查询改成LEFT JOIN,确保没有出勤记录的学生也保留在结果集里;建表时给status字段加DEFAULT 0约束,从根源上避免NULL值。

坑四:真机调试报INSTALL_FAILED_OLDER_SDK

现象:手机连上USB后点击Run,安装阶段报INSTALL_FAILED_OLDER_SDK。 原因:项目里设置的minSdkVersion高于手机的系统版本。课程设计项目经常把minSdk设到26甚至更高,而演示用的手机可能是Android 9甚至更低。 解决:在build.gradle里把minSdkVersion降到21或19,重新编译安装。这个改动不影响功能逻辑,只是降低了安装门槛。

4.3 模拟器还是真机:答辩演示场景怎么选

如果答辩要在教室大屏上投影,模拟器是更稳妥的选择,窗口缩放方便、画面稳定、演示时不会有手机息屏的尴尬。但模拟器也有一个隐蔽问题:如果项目依赖了只提供arm64-v8a原生库的第三方组件,而模拟器镜像是x86_64,运行时就会报“Unable to load library”之类的错误。

我的建议是优先真机,其次模拟器。模拟器一定要选API级别和项目targetSdkVersion一致的镜像,不要用最新的预览版。涉及通知栏、系统相册这类功能的调用,模拟器上的表现经常和真机不一致,不是代码问题,是模拟器本身的差距,答辩前一定在真机上完整过一遍流程。

5. 扩展方向与验证方法:把点名系统从“能用”变成“好讲”

答辩最怕的就是“这个功能是怎么实现的”答不上来。代码不是你写的没关系,但数据流必须梳理清楚。我拿到这套源码后做的第一件事,不是改界面,而是在纸上画一条数据流:管理端添加老师 → teacher表 → 老师登录校验 → 进入班级列表 → 选择班级点名 → 写入attendance表 → 统计页聚合展示。能把这六步讲清楚,基础分就拿到了。

第二步是验证边界情况。比如同一天对同一个班点了两次名,attendance表里会出现重复记录,统计页的实到人数会翻倍。处理方式是在表结构上给(student_id, date)加UNIQUE约束,或者保存前先查一次是否已存在。这类细节不需要写进代码里,但答辩时主动提出来,效果会好很多。

如果想做得更出彩,三个方向可以选一个:

  • 把密码从明文改成MD5加盐。代码改动很小,但涉及安全话题,答辩时能展开讲“为什么不能存明文密码”。
  • 把统计页从列表改成自绘柱状图。不需要第三方库,自己写一个View绘制每日出勤率,工作量约40行代码。
  • 加入二维码签到。老师端生成当堂课二维码,学生端扫码后标记出勤。工作量偏大,适合毕业设计,普通课程设计不建议。

以自绘柱状图为例,核心逻辑在onDraw里按比例画矩形:

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (data == null || data.isEmpty()) return; float barWidth = (getWidth() - 20f) / data.size(); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); for (int i = 0; i < data.size(); i++) { float barHeight = getHeight() * data.get(i).rate; // rate是出勤率,范围0~1 float left = 10 + i * barWidth; paint.setColor(Color.parseColor("#4CAF50")); canvas.drawRect(left, getHeight() - barHeight, left + barWidth - 10, getHeight(), paint); } }

参数说明:data是出勤率列表,每个元素包含一个rate字段;每根柱子的宽度是总宽度减20像素后的等分值,柱子之间留10像素间隔,视觉上就是一根根独立的柱状条。Paint在循环里创建不是最优写法,但数据量只有几条时无所谓,保持代码短更容易讲。

最后说一个我自己的习惯:每次拿到别人的课程设计源码,绝不先改功能,而是先在模拟器上完整跑一遍主流程——从管理端添加账号,到客户端登录,到完成一次点名,再到统计页核对数字。全部通过之后才开始动代码。从那以后我每次复现项目都强制走这个流程,基本没在答辩前翻过车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询