基于Android的课堂点名系统:SQLite+Room本地数据方案实战
发布时间:2026/10/12 3:22:46 锦皓数字建站

简介这份资源是面向高校计算机与移动开发学习者的Android课堂点名系统完整项目源码基于Java语言与Eclipse开发环境构建适合作为课程设计、毕业设计或Android入门到进阶的实战参考。压缩包共1374个文件约29.15MB涵盖168个java源文件、244个class编译文件、84个jsp页面、54个xml配置、97个jar依赖包以及大量png、gif、jpg界面素材与sql数据库脚本完整呈现客户端与服务端双端结构。项目实现快速点名、实时同步、出勤统计、后台管理与安全通信等核心功能客户端负责签到交互与Material Design界面服务端承担数据存储与权限验证。已有1723人学习下载读者可借此掌握Android网络通信、数据库持久化与前后端协作的完整开发流程并参考其目录组织与代码分层思路进行二次开发或功能扩展。1. 点名这件小事为什么值得用 Android 重做一遍带过课的人都知道课堂点名最烦的不是点是点完之后那一堆烂账。纸质名单勾勾画画期末要统计出勤率得一张张翻用 Excel 记手机和电脑之间来回传版本一多就乱用现成的问卷工具又没法跟课程、学号、请假状态绑在一起。基于 Android 的课堂点名系统解决的正是这个场景老师拿一台安卓手机或平板学生扫码或报学号出勤数据实时落到本地数据库课后一键导出。它适合两类人——一类是高校里被考勤统计折磨的一线教师另一类是正在做 Android 课程设计、想找一个麻雀虽小五脏俱全的练手项目的学生。这个系统的技术骨架其实很清晰SQLite 存数据、RecyclerView 列名单、扫码或输入做签到入口、后台线程处理批量写入。把这四块吃透你得到的不只是一个点名工具而是一套能复用到任何名单状态场景的 Android 本地数据方案。2. 先把数据模型定死三张表撑起整个点名系统很多人一上来就写 Activity写到一半发现这个学生这学期请了几次假算不出来回头改表结构前面的代码全废。血泪经验是Android 本地应用的数据模型必须在动手写界面之前定死因为 SQLite 的 ALTER TABLE 能力很弱改字段基本等于重建表。2.1 课程、学生、考勤记录的最小三表设计一个能用的点名系统核心就三张表。课程表存课程名和上课时间学生表存学号和姓名考勤记录表是核心它把某学生某节课的状态记成一行。状态用整数枚举别用中文串否则查询和统计时你会后悔。-- 课程表一门课一行 CREATE TABLE course ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, -- 课程名如移动应用开发 class_no TEXT, -- 教学班号用于区分同一门课的不同班 created_at INTEGER NOT NULL -- 创建时间戳便于排序 ); -- 学生表一个学生一行学号唯一 CREATE TABLE student ( id INTEGER PRIMARY KEY AUTOINCREMENT, sno TEXT NOT NULL UNIQUE, -- 学号唯一约束防止重复导入 name TEXT NOT NULL, course_id INTEGER NOT NULL, -- 外键归属哪门课 FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE ); -- 考勤记录表一个学生一节课一行 CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, session_date TEXT NOT NULL, -- 上课日期格式 yyyy-MM-dd status INTEGER NOT NULL DEFAULT 0, -- 0出勤 1迟到 2请假 3缺勤 marked_at INTEGER NOT NULL, -- 标记时间戳 UNIQUE(student_id, course_id, session_date) -- 同一人同一天只能有一条 );逻辑说明attendance表上的UNIQUE(student_id, course_id, session_date)是整套系统的防重核心。老师手滑点两次签到、学生重复扫码都会被这个约束挡在数据库层而不是靠界面判断——界面判断永远有漏网之鱼。status用整数而非文本是因为统计出勤率时SUM(CASE WHEN status0 THEN 1 ELSE 0 END)比字符串比较快得多也省空间。参数说明session_date用yyyy-MM-dd字符串而不是时间戳是为了让按天分组的 SQL 写起来直观GROUP BY session_date直接可用如果存时间戳每次分组都要做日期函数转换索引也用不上。ON DELETE CASCADE保证删课程时学生和考勤记录一起清掉避免脏数据。2.2 用 Room 还是裸 SQLiteOpenHelper这是选型第一个岔路口。裸SQLiteOpenHelper灵活但你要自己写 Cursor 解析、自己管关闭、自己防注入Room 是官方 ORM编译期校验 SQL返回 LiveData 或 Flow界面能自动刷新。我的建议是课程设计级别、表结构稳定直接上 Room省下的样板代码够你多写两个功能。// build.gradle.kts 里加依赖版本按你项目实际填 // implementation(androidx.room:room-runtime:2.x.x) // ksp(androidx.room:room-compiler:2.x.x) Entity(tableName attendance, indices [Index(value [student_id,course_id,session_date], unique true)]) data class Attendance( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name student_id) val studentId: Long, ColumnInfo(name course_id) val courseId: Long, ColumnInfo(name session_date) val sessionDate: String, val status: Int 0, ColumnInfo(name marked_at) val markedAt: Long System.currentTimeMillis() ) Dao interface AttendanceDao { // 用 REPLACE 策略重复签到直接覆盖配合唯一索引天然幂等 Insert(onConflict OnConflictStrategy.REPLACE) suspend fun mark(a: Attendance) // 统计某课程某天的出勤人数 Query(SELECT COUNT(*) FROM attendance WHERE course_id:cid AND session_date:date AND status0) suspend fun presentCount(cid: Long, date: String): Int }逻辑说明OnConflictStrategy.REPLACE配合唯一索引让重复签到这个动作变成幂等操作——点一次和点十次结果一样这是移动端弱网、手抖场景下最省心的设计。suspend函数保证数据库操作不跑在主线程避免 ANR。参数说明indices里的唯一索引和前面 SQL 的UNIQUE约束等价Room 会在建表时生成。markedAt默认取当前时间戳调用方不用手动传。注意 Room 的实体类字段名和列名不一致时要用ColumnInfo显式映射否则编译期就报错这反而是好事——错误提前暴露。3. 签到入口怎么做扫码、学号输入与批量标记数据层稳了接下来是老师真正天天用的部分。签到入口的设计直接决定这个系统是能用还是好用。我一般会做三个入口并存扫码学生出示二维码、学号快速输入学生报号老师敲、整班批量标记默认全出勤再改个别状态。第三个入口最容易被忽略但它才是真实课堂里用得最多的——大部分课大部分人都在老师只需要处理少数异常。3.1 扫码签到用 ZXing 还是 CameraX 自绘扫码方案常见两类集成成熟的扫码库或者用 CameraX 加图像分析自己接识别。课程设计级别我强烈建议用成熟库把精力留给业务逻辑。下面是一个基于主流扫码库的最小接入思路不同库 API 有差异核心是拿到解码字符串后交给业务层。// 扫码结果回调里拿到的是学生二维码里的学号字符串 private fun onScanResult(raw: String) { // 1. 先做格式校验别把任意二维码都当学号 val sno raw.trim() if (!sno.matches(Regex(^\\d{6,12}$))) { toast(二维码内容不是有效学号) return } // 2. 丢到后台线程查库并写入避免阻塞扫码预览 lifecycleScope.launch(Dispatchers.IO) { val student studentDao.findBySno(sno) if (student null) { withContext(Dispatchers.Main) { toast(学号 $sno 不在本班名单) } returnlaunch } attendanceDao.mark( Attendance( studentId student.id, courseId currentCourseId, sessionDate today(), status 0 ) ) withContext(Dispatchers.Main) { toast(${student.name} 签到成功) } } }逻辑说明扫码回调运行在相机线程绝不能在里面直接查库或弹 Toast否则预览会卡顿甚至崩。这里用lifecycleScope加Dispatchers.IO把查库和写入挪到后台结果再切回主线程提示。正则校验是防呆——学生可能拿错二维码比如付款码、别的课的表单码格式校验能挡掉大部分误扫。参数说明正则^\\d{6,12}$按你们学校学号长度调整太宽松会误判太严格会漏掉带字母的学号。today()是封装好的日期函数返回yyyy-MM-dd必须和数据库里session_date的格式完全一致否则唯一约束失效、统计也对不上。3.2 批量标记默认全勤再改异常的正确姿势真实课堂里老师更常见的操作是先全勾上再把缺勤的几个人点掉。这个交互如果实现成逐个点签到效率极低。正确做法是一次性给全班插入出勤记录再对个别学生改状态。Dao interface AttendanceDao { // 批量插入冲突时忽略已存在的记录不动 Insert(onConflict OnConflictStrategy.IGNORE) suspend fun markAll(list: ListAttendance) // 单独改某人状态 Query(UPDATE attendance SET status:status, marked_at:ts WHERE student_id:sid AND course_id:cid AND session_date:date) suspend fun updateStatus(sid: Long, cid: Long, date: String, status: Int, ts: Long) } // 调用侧一键全勤 suspend fun markAllPresent(courseId: Long, date: String) { val students studentDao.listByCourse(courseId) val records students.map { Attendance(studentId it.id, courseId courseId, sessionDate date, status 0) } attendanceDao.markAll(records) // 已签到的不会被覆盖 }逻辑说明批量插入用IGNORE而不是REPLACE是因为一键全勤不应该覆盖老师已经手动标记的请假、迟到记录。如果用了REPLACE老师先标了三个请假再点全勤这三个请假就被冲掉了——这是很隐蔽的翻车点。updateStatus单独走 UPDATE只动状态字段不动其他。参数说明markAll的列表长度等于班级人数几十到一两百条一次事务插入完全没问题如果班级上千人建议分批每批 500 条避免单次事务过大。updateStatus的四个条件缺一不可少一个course_id就可能改到别的班的学生。4. 出勤统计与导出把烂账变成一张能交差的表点名系统做完签到只是完成一半另一半是期末能导出。老师要的不是数据库里的一堆行而是一张能直接贴进教学检查材料的表横轴是日期纵轴是学生格子里是状态。这个透视表在 SQL 里用条件聚合就能生成不用把数据拉到内存里算。4.1 用一条 SQL 生成考勤透视表-- 生成学生 × 日期的考勤矩阵P出勤 L迟到 A请假 X缺勤 SELECT s.sno, s.name, MAX(CASE WHEN a.session_date 2025-03-01 THEN CASE a.status WHEN 0 THEN P WHEN 1 THEN L WHEN 2 THEN A ELSE X END END) AS d01, MAX(CASE WHEN a.session_date 2025-03-08 THEN CASE a.status WHEN 0 THEN P WHEN 1 THEN L WHEN 2 THEN A ELSE X END END) AS d08, -- 出勤率出勤迟到算到课 ROUND(100.0 * SUM(CASE WHEN a.status IN (0,1) THEN 1 ELSE 0 END) / COUNT(a.id), 1) AS rate FROM student s LEFT JOIN attendance a ON a.student_id s.id AND a.course_id s.course_id WHERE s.course_id :cid GROUP BY s.id ORDER BY s.sno;逻辑说明LEFT JOIN保证没被点过名的学生也出现在结果里否则缺勤的人反而消失了这是统计里最坑的错。MAX(CASE WHEN ...)是透视表的标准写法每个日期一列。出勤率把迟到也算到课是因为多数学校的口径如此具体按你们规定调整status IN (0,1)里的值。参数说明日期列是硬编码的实际项目里应该先查一次本课程有哪些上课日期再动态拼 SQL 的列部分。ROUND(...,1)保留一位小数100.0里的.0不能省否则整数除法会把出勤率算成 0 或 1。4.2 导出 CSV 并处理中文乱码导出最容易被忽略的是编码。Android 默认写 UTF-8但 Excel 打开 UTF-8 的 CSV 经常乱码因为 Excel 默认按本地编码解析。解决办法是在文件开头写 UTF-8 BOM。suspend fun exportCsv(courseId: Long, out: OutputStream) withContext(Dispatchers.IO) { // 关键先写 BOMExcel 才认 UTF-8 out.write(byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte())) val writer BufferedWriter(OutputStreamWriter(out, Charsets.UTF_8)) writer.write(学号,姓名,出勤率\n) // rows 来自上面的透视查询 rows.forEach { r - writer.write(${r.sno},${r.name},${r.rate}%\n) } writer.flush() }逻辑说明BOM 那三个字节是给 Excel 看的这是 UTF-8的信号不加就是乱码加了记事本和现代编辑器也正常。写文件走Dispatchers.IO大班级导出时不会卡界面。参数说明Charsets.UTF_8显式指定别依赖平台默认。如果导出后字段里有逗号比如姓名里带逗号少见但存在要用双引号包裹字段否则列会错位。5. 避坑与排查那些让点名系统当场翻车的细节这一章是我踩过的坑合集每条都按现象→原因→解决写照着排查能省你几个通宵。现象扫码签到偶尔提示成功但统计里查不到。原因扫码回调里用了异步写库但 Activity 在写入完成前被销毁lifecycleScope随生命周期取消写入被中断。解决把写库逻辑放到applicationScope或前台 Service 里或者用NonCancellable包住关键写入确保签到这种不可丢的操作不被生命周期取消。现象同一学生同一天出现两条记录。原因唯一索引没建成功或者建在了错误的字段组合上。解决用PRAGMA index_list(attendance)确认索引存在检查 Room 实体的indices注解是否和表结构一致如果之前用旧版本建过表要升版本号触发迁移或重建。现象期末导出出勤率全是 0。原因整数除法。SUM(...)/COUNT(...)两边都是整数结果被截断。解决分子乘100.0或CAST(... AS REAL)前面 SQL 里的100.0就是干这个的。现象删了一门课学生名单还在但考勤统计报错。原因外键约束没生效SQLite 默认不开外键。解决在onOpen或 Room 的RoomDatabase.Callback里执行PRAGMA foreign_keysON否则ON DELETE CASCADE是摆设。现象批量全勤后之前标记的请假全没了。原因批量插入用了REPLACE策略。解决改成IGNORE只补不覆盖前面 3.2 的代码就是正确写法。6. 进阶把点名系统做成能长期用的工具做到这里一个能交课程设计的点名系统已经完整了。但如果你想让它在真实课堂里活过一个学期还有两件事值得做。第一件是加历史课程复用新学期开同一门课不该重新导入名单而应该从上一期的student表按class_no复制一份考勤记录清空但学生保留。第二件是加异常提醒连续三次缺勤的学生在点名界面上标红这个用一条 SQL 就能算出来。-- 找出连续缺勤达到阈值的学生简化版统计缺勤次数 SELECT s.sno, s.name, COUNT(*) AS absent_times FROM student s JOIN attendance a ON a.student_id s.id WHERE a.course_id :cid AND a.status 3 GROUP BY s.id HAVING absent_times 3 ORDER BY absent_times DESC;逻辑说明HAVING过滤聚合后的结果WHERE过滤聚合前的行两者不能混。这个查询返回的就是需要重点关注的学生界面层拿到后把对应行标红即可。参数说明阈值 3 按你们学校规定调整status 3对应缺勤和前面枚举保持一致。如果要做真正的连续判断得按日期排序后用窗口函数LAG比较SQLite 3.25 以上支持但复杂度陡增课程设计级别用累计次数已经够用。最后说个我自己的习惯每次改完表结构我一定先写一个造 50 条假数据的测试方法跑一遍签到、统计、导出全流程确认没崩再动界面。这个习惯帮我挡掉了至少三次改字段忘了改查询的低级翻车。Android 本地应用没有服务端兜底数据库就是唯一的真相来源对它客气点它才对你客气。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。