Android SQLite校园签到系统:离线本地数据库实战
2026/9/17 8:36:36 网站建设 项目流程

简介:本资源是一套完整的Android课程设计项目——校园签到系统,面向高校计算机/软件工程专业学生及Android初学者,解决课堂考勤数字化管理的实际需求。系统采用Android Studio开发,基于SQLite本地数据库实现数据持久化,涵盖管理员后台与学生移动端双角色功能:管理员可进行学生信息维护、签到任务发布与打卡记录统计;学生端支持注册登录、签到查看、实时打卡、历史记录查询及个人信息管理。资源包共511个文件,含115个Java业务逻辑代码、215个XML界面布局文件、152个PNG图标资源,辅以Gradle构建脚本、JAR依赖库(如jsoup、universal-image-loader等)及配置文件,整体2.67MB,结构规范、模块清晰,便于理解Android MVC架构与SQLite CRUD操作实践。目前已有214人学习下载,适合课程设计参考、毕业设计原型开发或Android基础项目实战训练。

1. 一个能跑通的校园签到系统:不是Demo,是带完整业务闭环的Android课设实战项目

这不是一个只画了三个Activity、点一下跳转就完事的“课设充数工程”。它用 Android Studio 搭建起从学生注册登录、教师发布签到、实时打卡、本地记录归档到后台管理查看的全链路闭环——所有数据落地在 SQLite,不依赖网络请求、不调用远程API,纯离线本地数据库驱动。整个流程覆盖了 Android 开发中高频且易出错的典型场景:多表关联(学生表、签到表、打卡记录表)、事务控制(发布签到+生成默认打卡状态)、UI 状态同步(登录后自动刷新签到列表)、密码明文存储风险规避(虽未加盐哈希但已预留字段)、以及最关键的——SQLiteOpenHelper 的升级路径设计。适合大三下学期刚学完《移动应用开发》和《数据库原理》的学生直接拉下来编译运行、调试修改、写进课程报告;也适合想快速验证 SQLite 在 Android 中真实承载能力的开发者,作为轻量级本地业务系统的最小可行原型(MVP)来拆解。


2. 从 build.gradle 到 DatabaseHelper:构建可演进的本地数据库架构

2.1 项目结构与 Gradle 配置解析:为什么依赖项里混着 jsoup 和 commons-net?

项目根目录下的build.gradle和模块级build.gradle共同定义了整个工程的构建契约。关键点不在版本号堆砌,而在于依赖意图的显式表达

// app/build.gradle 片段 dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // 【核心】SQLite 封装层:Room 不在此列,说明本项目采用原生 SQLiteDatabase + Cursor 手动操作 // 这意味着你需要自己写 create table、insert、query、beginTransaction 等逻辑 implementation 'androidx.sqlite:sqlite-framework:2.4.0' // 【线索】jsoup-1.13.1.jar 和 commons-net-3.0.1.jar 的存在,暗示未来可能扩展网络功能 // 例如:教师端通过 HTTP POST 向简易后端同步签到数据,或学生端拉取学校公告(用 jsoup 解析 HTML) // 当前未启用,但 jar 包已预埋,避免后期引入时版本冲突 implementation files('libs/jsoup-1.13.1.jar') implementation files('libs/commons-net-3.0.1.jar') // 【兼容性】universal-image-loader-1.9.3.jar 表明 UI 层需加载头像等本地资源 // 虽未见图片上传逻辑,但学生个人信息页大概率含头像显示控件 implementation files('libs/universal-image-loader-1.9.3.jar') }

注意settings.gradle中若包含include ':app'以外的模块(如':library'),说明该项目已预留插件化或模块化扩展空间;而gradlew.batgradlew的存在,则确保 Windows/macOS/Linux 下均可统一执行./gradlew assembleDebug构建 APK,规避本地 Gradle 版本差异导致的编译失败。

2.2 DatabaseHelper 设计:三张表如何支撑“发布-打卡-查记录”主干流程?

SQLiteOpenHelper 子类(通常命名为DBHelper.javaMyDatabaseHelper.java)是本项目的中枢。它不止创建表,更定义了数据演化契约。以下是根据项目功能反推的建表逻辑(实际代码需在onCreate()onUpgrade()中实现):

2.2.1 学生表(student):基础身份锚点
CREATE TABLE student ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, -- 登录账号,唯一约束防重名 password TEXT NOT NULL, -- 密码字段存在,但课设中常为明文(生产环境必须加密) real_name TEXT, -- 真实姓名,用于签到记录展示 class_name TEXT, -- 所属班级,便于教师按班筛选 phone TEXT, -- 联系方式,注册时采集 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
2.2.2 签到任务表(sign_task):教师发布的签到事件
CREATE TABLE sign_task ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, -- 签到标题,如“2024春《数据库》第3次课” description TEXT, -- 补充说明,如“教室:A305,时间:14:00-14:45” start_time TEXT NOT NULL, -- ISO8601 格式字符串,如 "2024-05-20T14:00:00" end_time TEXT NOT NULL, -- 签到截止时间 status INTEGER DEFAULT 1, -- 1=进行中,0=已结束,-1=已取消 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
2.2.3 打卡记录表(sign_record):学生行为的原子存证
CREATE TABLE sign_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, -- 外键关联 student.id task_id INTEGER NOT NULL, -- 外键关联 sign_task.id sign_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 实际打卡时间戳 location TEXT, -- 可选:GPS坐标或手动填写位置(课设中常为空) status INTEGER DEFAULT 1, -- 1=已签到,0=缺勤(教师端可手动修改) FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY (task_id) REFERENCES sign_task(id) ON DELETE CASCADE );

提示ON DELETE CASCADE是关键设计。当教师删除某次签到任务时,对应的所有打卡记录自动清除,避免孤儿数据。但需在onUpgrade()中谨慎处理——SQLite 不支持ALTER TABLE ... ADD CONSTRAINT,外键需在建表时声明,并通过PRAGMA foreign_keys = ON;在每次获取SQLiteDatabase实例后启用。

2.3 数据库升级策略:从 v1 到 v2 的安全迁移路径

假设初始版本DATABASE_VERSION = 1,后续需新增“学生头像路径”字段。不能直接ALTER TABLE student ADD COLUMN avatar_path TEXT;—— 因 Android 低版本 SQLite 不支持此语法。正确做法是在onUpgrade()中:

@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2 && newVersion >= 2) { // 步骤1:创建新表(含 avatar_path) db.execSQL("CREATE TABLE student_new (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "username TEXT NOT NULL UNIQUE," + "password TEXT NOT NULL," + "real_name TEXT," + "class_name TEXT," + "phone TEXT," + "avatar_path TEXT," + // 新增字段 "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ");"); // 步骤2:迁移旧数据(显式列出字段,避免顺序错乱) db.execSQL("INSERT INTO student_new (id, username, password, real_name, class_name, phone, created_at) " + "SELECT id, username, password, real_name, class_name, phone, created_at FROM student;"); // 步骤3:删除旧表,重命名新表 db.execSQL("DROP TABLE student;"); db.execSQL("ALTER TABLE student_new RENAME TO student;"); } }

逻辑说明:此方案兼容所有 Android 版本。oldVersion < 2 && newVersion >= 2判断确保仅执行一次迁移;INSERT ... SELECT显式指定字段名,防止因表结构调整导致列错位;DROP前无BEGIN TRANSACTION是因 SQLite 自动事务,但若涉及多表联动更新,务必包裹db.beginTransaction()/db.setTransactionSuccessful()/db.endTransaction()


3. 学生端核心功能实现:注册、登录、签到打卡的代码落地方案

3.1 注册模块:表单校验与数据库插入的原子性保障

学生注册流程需同时写入student表并返回成功标识。关键点在于空值校验前置唯一性冲突捕获

// RegisterActivity.java private void performRegister() { String username = etUsername.getText().toString().trim(); String password = etPassword.getText().toString().trim(); String realName = etRealName.getText().toString().trim(); // 【前端校验】避免无效请求 if (username.isEmpty() || password.isEmpty() || realName.isEmpty()) { Toast.makeText(this, "请填写完整信息", Toast.LENGTH_SHORT).show(); return; } // 【数据库插入】使用 insert() 方法,返回 -1 表示失败 ContentValues values = new ContentValues(); values.put("username", username); values.put("password", password); // 注意:生产环境应调用 PasswordUtil.hash(password) values.put("real_name", realName); values.put("class_name", etClass.getText().toString().trim()); values.put("phone", etPhone.getText().toString().trim()); long result = db.insert("student", null, values); if (result == -1) { // 【唯一约束触发】捕获 SQLiteConstraintException(需 try-catch) Toast.makeText(this, "用户名已存在,请更换", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show(); startActivity(new Intent(this, LoginActivity.class)); finish(); } }

参数说明db.insert("student", null, values)中第二个参数nullColumnHack设为null表示当所有字段均为null时仍允许插入(此处无需);values必须包含NOT NULL字段,否则插入失败。Toast提示需在主线程,故直接调用。

3.2 登录模块:凭证匹配与会话状态持久化

登录成功后,需将用户 ID 或用户名存入SharedPreferences,供后续 Activity 获取当前上下文:

// LoginActivity.java private void performLogin() { String username = etUsername.getText().toString().trim(); String password = etPassword.getText().toString().trim(); // 【查询语句】使用 ? 占位符防 SQL 注入 String selection = "username = ? AND password = ?"; String[] selectionArgs = {username, password}; // 明文比对,课设常见,非生产方案 Cursor cursor = db.query("student", new String[]{"id", "real_name", "class_name"}, selection, selectionArgs, null, null, null); if (cursor.moveToFirst()) { // 【持久化用户状态】 SharedPreferences sp = getSharedPreferences("user_session", MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putInt("student_id", cursor.getInt(0)); // id 列索引为 0 editor.putString("real_name", cursor.getString(1)); editor.putString("class_name", cursor.getString(2)); editor.apply(); // 异步提交,比 commit() 更轻量 startActivity(new Intent(this, MainActivity.class)); finish(); } else { Toast.makeText(this, "用户名或密码错误", Toast.LENGTH_SHORT).show(); } cursor.close(); }

逻辑说明db.query()返回Cursor,需手动close()防内存泄漏;getSharedPreferences("user_session", MODE_PRIVATE)创建私有配置文件,其他 Activity 可通过相同 key 读取;apply()无返回值,commit()返回布尔值表示是否成功,课设中apply()足够。

3.3 签到打卡功能:事务保证“发布任务”与“生成默认记录”的一致性

教师端发布签到时,需同时插入sign_task并为所有已注册学生生成待打卡记录(status=0)。这必须在一个事务中完成:

// TeacherActivity.java(伪代码) private void publishSignTask(String title, String desc, String startTime, String endTime) { db.beginTransaction(); try { // 步骤1:插入签到任务 ContentValues taskValues = new ContentValues(); taskValues.put("title", title); taskValues.put("description", desc); taskValues.put("start_time", startTime); taskValues.put("end_time", endTime); long taskId = db.insert("sign_task", null, taskValues); if (taskId == -1) throw new SQLException("任务插入失败"); // 步骤2:查询所有学生ID Cursor studentCursor = db.query("student", new String[]{"id"}, null, null, null, null, null); while (studentCursor.moveToNext()) { int studentId = studentCursor.getInt(0); // 为每个学生插入一条初始记录(status=0 表示未打卡) ContentValues recordValues = new ContentValues(); recordValues.put("student_id", studentId); recordValues.put("task_id", taskId); recordValues.put("status", 0); db.insert("sign_record", null, recordValues); } studentCursor.close(); db.setTransactionSuccessful(); } finally { db.endTransaction(); } }

参数说明beginTransaction()启动事务;setTransactionSuccessful()标记成功;endTransaction()提交或回滚。若中间任意一步抛异常,事务自动回滚,确保不会出现“任务发布了但没人能打卡”的脏数据。


4. 教师端管理功能:多条件查询与状态批量更新的实战技巧

4.1 学生管理:模糊搜索与分页加载的 Cursor 优化

教师需在列表中快速定位学生,LIKE查询配合LIMIT/OFFSET实现分页:

// StudentManagerActivity.java private Cursor queryStudents(String keyword, int offset, int limit) { String selection = "real_name LIKE ? OR username LIKE ?"; String[] selectionArgs = {"%" + keyword + "%", "%" + keyword + "%"}; String orderBy = "created_at DESC"; String limitClause = limit + " OFFSET " + offset; return db.query("student", new String[]{"id", "username", "real_name", "class_name", "phone"}, selection, selectionArgs, null, null, orderBy, limitClause); } // 使用示例:加载前20条 Cursor cursor = queryStudents("", 0, 20); SimpleCursorAdapter adapter = new SimpleCursorAdapter( this, android.R.layout.simple_list_item_2, cursor, new String[]{"real_name", "class_name"}, new int[]{android.R.id.text1, android.R.id.text2}, 0 ); listView.setAdapter(adapter);

提示SimpleCursorAdapter已废弃,但课设中仍广泛使用;若用RecyclerView,需自定义CursorRecyclerViewAdapter并在onBindViewHolder()中调用cursor.getString(cursor.getColumnIndex("real_name"))OFFSET在大数据量时性能下降,课设数据量小可忽略。

4.2 签到记录查看:三表 JOIN 查询的字段映射与空值处理

要显示“张三 | 2024春《数据库》第3次课 | 已签到 | 2024-05-20 14:02:15”,需联查三张表:

// SignRecordActivity.java private Cursor querySignRecords(int taskId) { String sql = "SELECT " + "s.real_name AS student_name, " + "t.title AS task_title, " + "r.sign_time, " + "CASE r.status WHEN 1 THEN '已签到' ELSE '未签到' END AS status_desc " + "FROM sign_record r " + "JOIN student s ON r.student_id = s.id " + "JOIN sign_task t ON r.task_id = t.id " + "WHERE t.id = ? " + "ORDER BY r.sign_time DESC"; return db.rawQuery(sql, new String[]{String.valueOf(taskId)}); }

逻辑说明rawQuery()直接执行 SQL,CASE WHEN生成中文状态;JOIN顺序影响性能,小表(sign_task)放后;AS别名确保CursorgetColumnIndex("student_name")可准确获取字段。若某学生无打卡记录,LEFT JOIN可保留任务信息,但此处需求为“有记录才显示”,故用INNER JOIN

4.3 批量修改打卡状态:IN 子句与事务的协同应用

教师可勾选多个学生,一键标记为“已签到”:

// 批量更新 SQL(安全写法) String updateSql = "UPDATE sign_record SET status = 1 WHERE task_id = ? AND student_id IN (" + TextUtils.join(",", Collections.nCopies(studentIds.size(), "?")) + ")"; String[] args = new String[studentIds.size() + 1]; args[0] = String.valueOf(taskId); for (int i = 0; i < studentIds.size(); i++) { args[i + 1] = String.valueOf(studentIds.get(i)); } db.execSQL(updateSql, args);

参数说明Collections.nCopies(n, "?")动态生成 n 个?占位符,避免 SQL 拼接注入;args数组首元素为taskId,后续为学生 ID 列表。此写法兼容任意长度 ID 列表,且execSQL()不返回结果,适合 DML 操作。


5. SQLite 调试与验证:用 DB Browser for SQLite 定位课设常见数据问题

5.1 导出 APK 中的数据库文件:adb 命令精准提取

课设中最常见的问题是“代码写了但数据没进去”。直接查看 APK 内部数据库是最高效验证方式:

# 步骤1:确认设备已连接且开启USB调试 adb devices # 步骤2:查找应用数据目录(包名需替换为实际值,如 com.example.campuscheckin) adb shell "run-as com.example.campuscheckin ls /data/data/com.example.campuscheckin/databases/" # 步骤3:复制数据库文件到电脑(注意:run-as 仅对 debug 版 APK 有效) adb shell "run-as com.example.campuscheckin cp /data/data/com.example.campuscheckin/databases/mydb.db /sdcard/mydb.db" adb pull /sdcard/mydb.db ./mydb.db # 步骤4:用 DB Browser for SQLite 打开 mydb.db,直观检查表结构与数据

提示:若run-as报错Permission denied,说明 APK 为 release 版或未签名 debug;此时需在AndroidManifest.xml中临时添加android:debuggable="true"并重新构建。

5.2 常见数据异常的快速诊断表

现象可能原因验证方法修复建议
注册后登录失败username字段未设UNIQUE,或插入时未 commit在 DB Browser 中查看student表,确认用户名是否存在检查onCreate()CREATE TABLE语句是否含UNIQUE关键字
签到列表为空sign_task.status != 1sign_record.task_id关联错误查看sign_taskstatus值;检查sign_recordtask_id是否匹配sign_task.id发布任务时显式设置status=1;插入记录前Log.d()打印taskId
修改密码后无法登录password字段被UPDATE但未刷新SharedPreferences登录时Log.d()打印查询到的密码与输入密码的hashCode()登录成功后,若密码变更,需同步更新SharedPreferences中的凭证缓存
中文乱码(如“张三”显示为“???”)数据库创建时未指定编码,或ContentValues插入时字符集错误在 DB Browser 中直接查看字段值是否为乱码Android SQLite 默认 UTF-8,乱码多因 Java 字符串构造错误,检查new String(byte[], "UTF-8")是否遗漏

5.3 性能边界测试:1000 条打卡记录下的查询响应

课设虽数据量小,但需理解 SQLite 的性能拐点。用以下命令模拟压力:

# 在 DB Browser 中执行(或通过 adb shell sqlite3) -- 插入1000条测试记录 WITH RECURSIVE cnt(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM cnt LIMIT 1000) INSERT INTO sign_record (student_id, task_id, status) SELECT (x % 50) + 1, 1, 1 FROM cnt;

然后在代码中测量查询耗时:

long start = System.currentTimeMillis(); Cursor cursor = db.query("sign_record", null, "task_id = ?", new String[]{"1"}, null, null, null); Log.d("Perf", "1000 records query time: " + (System.currentTimeMillis() - start) + "ms"); cursor.close();

结论:在中端 Android 设备上,1000 条记录的query()通常 < 10ms,证明 SQLite 完全胜任课设规模。若超 50ms,需检查是否缺失task_id索引——执行CREATE INDEX idx_task_id ON sign_record(task_id);可提升 5 倍以上速度。

adb shell sqlite3进入数据库后,执行.schema查看建表语句,.indices sign_record查看现有索引,是课设调试最硬核却最有效的手段。

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

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

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

立即咨询