Android考试管理系统课设:Gradle配平、Room建表与组卷判分
简介这是一份面向高校移动应用开发课程与毕业设计场景的考试管理系统课程设计报告选题紧贴线上教学背景下的考试管理需求适合正在准备 Android 课程设计、实训作业或毕业论文的学生与指导教师参考。报告围绕教师发布试卷、学生限时答题、系统自动判分、成绩记录查询与账号管理等核心功能展开重点解决传统纸质考试在线上环境中监考困难、批改繁琐、成绩统计耗时的问题。资源共 1 个 doc 文档约 3.31MB内容含项目背景、Java 开发技术与 Android Studio 开发环境说明、MVC 三层架构与 SQLite 数据库设计、详细功能设计、运行演示及心得体会全文约 14805 字且图文并茂。读者可据此理解 model 实体类、view 布局文件与 controller 控制层的划分思路借鉴数据库表结构与页面流程设计并参考成文的报告框架与写作表达用于快速搭建自己的课程设计文档与答辩材料。目前已有 322 人学习下载。1. Android 考试管理系统课设全景真正卡人的环节在哪选题表上写「Android 考试管理系统」功能列了登录、组卷、答题、判分、成绩查询五大块代码写到判分就卡住截图攒了三十张14805 字的论文却凑不到第四章——这是每年课设季最常见的场面。真正卡住大多数人的不是判分算法而是三件事开发环境版本对不齐导致 Gradle 同步反复失败、数据库表没有试卷快照概念导致改题后历史成绩失真、报告里的详细设计与代码对不上号。这套系统的完整链路应该是「环境配平 → 表结构与模块拆分 → 组卷答题判分运行 → 打包答辩」。判分本身只有几百行代码把随机抽题、计时校准、历史成绩冻结这几个边界讲清楚才是拉开分数的地方。正在做 Android 课程设计、需要交图文并茂论文的同学能直接照着复现想用一个完整小项目补齐 Kotlin Room ViewModel 现代开发栈的从业者也能从表设计和 Gradle 配置里拿到能迁移到生产项目的做法。2. Android 考试管理系统开发环境搭建JDK、SDK 与 Gradle 一次配平开发环境这一步的返工率最高原因几乎都一样——照着某份教程装完 Android StudioJDK 版本、Gradle 插件版本、compileSdk 三者互相不认。先把版本关系理清再动 Gradle 仓库配置最后跑真机顺序不能反。2.1 JDK 与 Android Studio 的版本对齐Android Studio 从 Giraffe 版本起把内置 JDK 换成了 17Gradle 8.x 也要求 JDK 17 起步。常见坑是系统里先装了 JDK 8 做 Java 课设环境变量 JAVA_HOME 指向 8Android Studio 启动时能自动用内置 JDK但命令行执行./gradlew时会直接报Unsupported class file major version。最省事的做法是不改系统 JAVA_HOME让 IDE 和命令行都用 Studio 自带的运行时。在 Settings → Build, Execution, Deployment → Build Tools → Gradle 里把 Gradle JDK 显式选成jbr-17JetBrains Runtime或者本机安装的 JDK 17命令行侧则在项目根目录建一个gradle.properties写上org.gradle.java.home指到同一个路径。这样 IDE 同步和 CI 命令行用的是同一个 JVM能省掉一大半「IDE 能跑、命令行报错」的排查时间。平台版本的选择上课设不要贪新。compileSdk 选 34、targetSdk 选 34、minSdk 选 24 是比较稳的组合24 覆盖了绝大多数还在用的老设备34 又能用上 Android 14 的行为变更答辩时被问到兼容性也有话说。Android Studio 自带的中文语言包可以在 Settings → Plugins 里搜索 Chinese 安装安装后重启即可切换界面语言但注意插件版本要和 Studio 主版本匹配否则重启会提示不兼容。组件推荐版本说明JDK17Gradle 8.x 与 AGP 8.x 的硬性下限Android Studio最新稳定版内置 jbr-17无需额外配 JDKGradle8.6与 AGP 8.3 搭配稳定AGP8.3.xAndroid Gradle Plugin写在版本目录里compileSdk / targetSdk34兼顾新特性与教程覆盖度minSdk24兼容老设备答辩可讲兼容策略Build-Tools34.0.0与 compileSdk 对齐避免 aapt2 报错2.2 SDK 组件与 Gradle 仓库镜像配置SDK 组件的下载在首次安装时最容易卡住因为默认仓库在境外几百兆的 Platform 和 Build-Tools 经常下到一半超时。SDK Manager 里必须勾选的只有四项SDK Platform 34、Android SDK Build-Tools 34.0.0、Android SDK Platform-Tools里面含 adb、Android SDK Command-line Tools。模拟器镜像按需如果打算全程真机调试可以完全不装 Emulator省下好几个 G 的空间。依赖下载慢的问题在 Gradle 层解决更彻底。在settings.gradle.kts里把仓库顺序调成国内镜像优先Gradle 会按声明顺序逐个尝试命中即停不会白白超时重试// settings.gradle.kts pluginManagement { repositories { // 国内镜像优先命中后不再往下走 maven(https://maven.aliyun.com/repository/gradle-plugin) maven(https://maven.aliyun.com/repository/google) maven(https://maven.aliyun.com/repository/public) google() mavenCentral() } } dependencyResolutionManagement { // 禁止子模块自己声明仓库避免仓库来源混乱 repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven(https://maven.aliyun.com/repository/google) maven(https://maven.aliyun.com/repository/public) google() mavenCentral() } } rootProject.name ExamSystem include(:app)repositoriesMode设成FAIL_ON_PROJECT_REPOS是个容易被忽略但很值钱的配置子模块如果偷偷声明了自己的仓库构建会直接失败而不是静默从别处拉包能避免依赖版本在多人协作时漂移。三个镜像地址分别对应 Gradle 插件、Google 系AndroidX、Material和公共库Kotlin、Retrofit 等顺序别调换Google 系的包在 public 仓库里没有。gradle.properties里同时建议打开构建缓存和并行编译课设机器配置一般这两个开关能把二次构建时间压掉一半以上# gradle.properties org.gradle.jvmargs-Xmx2048m -Dfile.encodingUTF-8 org.gradle.paralleltrue org.gradle.cachingtrue android.useAndroidXtrue android.nonTransitiveRClasstrue kotlin.code.styleofficialnonTransitiveRClasstrue会让 R 类只包含本模块资源带来的直接好处是编译更快代价是从此不能再用com.other.module.R跨模块引用资源。课设单模块工程无所谓但如果后面要拆模块这个开关开着比关着更接近生产实践。2.3 骨架工程跑通与 adb 真机调试环境配完不要急着写业务先建一个空 Activity 工程点 Run确认BUILD SUCCESSFUL且能装到设备上。真机调试的前提是打开开发者选项里的 USB 调试连上后执行# 查看已连接设备unauthorized 表示还没在手机上点允许调试 adb devices # 安装 debug 包-r 表示覆盖安装保留数据-t 允许测试包 adb install -r -t app/build/outputs/apk/debug/app-debug.apk # 只看自己应用的日志D 表示 Debug 级别及以上 adb logcat -s ExamSystem:D AndroidRuntime:E三条命令分别解决三个问题adb devices里出现unauthorized说明授权弹窗没点出现offline通常是数据线只供电不传数据adb install报INSTALL_FAILED_UPDATE_INCOMPATIBLE是签名变了先adb uninstall 包名再装adb logcat -s里的 tag 要和代码里Log.d(ExamSystem, ...)保持一致否则筛出来是空的。AndroidRuntime 这个 tag 一定要带上崩溃堆栈全在里面答辩前用真机点一遍所有页面把崩溃日志清干净。注意adb 的-t参数只对android:testOnlytrue的包有效。如果从 IDE 直接 Run 出来的调试包装不上真机先确认设备系统的「通过 USB 安装应用」开关是否打开部分国产系统默认关闭。3. Android 考试管理系统详细设计四张表撑起组卷与判分详细设计章节是论文里最容易被评委追问的部分也是代码量最集中的地方。把需求收敛成四张表、三个模块剩下的界面逻辑都是体力活。这一章先讲表为什么这么分再给 Room 的可编译代码。3.1 需求剪裁把论文目标收敛成四张表课设的功能列表往往写得很满实际上判分正确性和历史成绩可追溯这两点做到位就足以支撑一个完整设计。四张表的分工是question存题库paper存一次组卷的元信息科目、总分、时长、创建时间paper_question存试卷与题目的关联并冻结题目关键字段exam_record存某个用户某次考试的成绩与作答时间。关键在第三张表。很多人图省事在exam_record里只存题目 ID 和用户答案判分时回查题库。一旦老师在考试后修改了某道题的正确答案所有历史成绩会集体变化这是设计缺陷而不是小 bug。正确做法是在组卷时把题干、选项、答案、分值整体快照进paper_question历史试卷从此与题库解耦。这一点在论文的「详细设计」小节里单独写一段是很实在的加分项。表名主要字段设计意图questionid, stem, optionA-D, answer, score, difficulty, knowledgePoint题库主表按知识点和难度建索引paperid, subject, totalScore, durationMin, createTime一次组卷的元信息paper_questionpaperId, questionId, stem, options, answer, score, orderNo组卷时对题目内容做快照exam_recordid, paperId, userId, score, costSec, submitTime, answersJson一次作答的结果记录knowledgePoint上要建普通索引paper_question上建(paperId, orderNo)的复合索引。课设数据量小索引带来的性能差异看不出来但论文里写出索引设计意图说明作者理解「组卷要按知识点抽题」这个查询模式比单纯堆字段更有说服力。3.2 Room 实体与 DAO 的详细设计Room 的写法在 Kotlin 下很直白实体类用Entity标注主键用PrimaryKey(autoGenerate true)字段名和列名默认一致。真正需要留意的是Index注解要写在Entity里而不是分散在字段上否则 Room 编译期校验会给出警告。Entity( tableName question, indices [Index(knowledgePoint), Index(difficulty)] ) data class Question( PrimaryKey(autoGenerate true) val id: Long 0, val stem: String, // 题干 val optionA: String, val optionB: String, val optionC: String, val optionD: String, val answer: String, // 正确答案取值 A/B/C/D val score: Int 2, // 该题分值 val difficulty: Int 1, // 1 易 2 中 3 难 val knowledgePoint: String )DAO 里最核心的方法是随机抽题。SQLite 的ORDER BY RANDOM()在几千条数据规模下完全够用不需要为了「性能」提前引入权重算法Dao interface QuestionDao { // 按知识点与难度随机抽 count 道题 Query( SELECT * FROM question WHERE knowledgePoint :kp AND difficulty :level ORDER BY RANDOM() LIMIT :count ) suspend fun randomPick(kp: String, level: Int, count: Int): ListQuestion // 兜底抽不满时降级到只按知识点抽 Query( SELECT * FROM question WHERE knowledgePoint :kp ORDER BY RANDOM() LIMIT :count ) suspend fun randomPickByKp(kp: String, count: Int): ListQuestion Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertAll(list: ListQuestion) Query(SELECT COUNT(*) FROM question) suspend fun count(): Int }两个抽题方法的差别在第 4 章会用到先按「知识点 难度」抽如果返回条数不足试卷要求就降级用只按知识点抽的版本补齐两条 SQL 的WHERE条件不同命中数量自然不同。所有 DAO 方法都用suspend修饰Room 会在编译期检查调用点是否在协程作用域内比allowMainThreadQueries()那种开关安全得多——后者一旦打开主线程查库造成卡顿问题极难定位。数据库单例用Room.databaseBuilder建课设阶段不需要加密和迁移但fallbackToDestructiveMigration()要慎用它在版本号变更时直接清库演示前一晚改表结构会把录入的题库全冲掉。稳妥做法是版本号不动新增字段用ColumnInfo(defaultValue ...)或者老老实实写Migration。3.3 判分逻辑与 Repository、ViewModel 的职责拆分三个模块的分工要明确Repository 只负责和 DAO 打交道ViewModel 负责业务编排和状态暴露Compose 或 Activity 只负责渲染状态。判分属于纯函数放在 domain 层单独一个文件方便写单元测试。// ExamViewModel.kt 片段 fun submit(paper: ListPaperQuestion, userAnswers: MapLong, String) { viewModelScope.launch { val score paper.sumOf { q - // 答案为空按 0 分计注意不要把 null 当成错误答案之外的处理 if (userAnswers[q.questionId] q.answer) q.score else 0 } val costSec ((SystemClock.elapsedRealtime() - startAt) / 1000).toInt() val record ExamRecord( paperId paperId, userId currentUserId, score score, costSec costSec, submitTime System.currentTimeMillis(), answersJson gson.toJson(userAnswers) ) repo.saveRecord(record) // 写库 _state.update { it.copy(finished true, finalScore score) } } }sumOf一次遍历完成判分比filter加map链式调用少一轮集合创建。用SystemClock.elapsedRealtime()而不是System.currentTimeMillis()算耗时是因为前者基于开机时间单调递增用户手动改系统时间不会影响作答时长统计——这个细节写进论文的心得部分属于能看出实践痕迹的那种。answersJson用 Gson 序列化成一个字符串字段避免再建一张作答明细表是课设规模下合理的取舍。4. Android 考试管理系统运行演示组卷、答题、判分的完整链路环境就绪、表结构落地之后运行链路是串起来的三段选题组卷、限时答题、交卷判分并查成绩。这一段既是论文「运行演示」章节的截图来源也是最容易在答辩现场被要求现场操作的部分每一步都要能稳定复现。4.1 组卷随机抽题与试卷快照落库组卷的输入是「科目 各难度题目数量」输出是一张试卷和它的题目快照。抽题顺序建议从难到易先用严格条件抽抽不满再降级这样试卷难度分布更可控suspend fun buildPaper(subject: String, plan: MapInt, Int): Long { val picked mutableListOfQuestion() plan.forEach { (level, count) - // 第一优先知识点 难度精确匹配 var part questionDao.randomPick(subject, level, count) // 数量不足降级到只按知识点抽再从中剔除已选题目 if (part.size count) { val pickedIds picked.map { it.id }.toSet() val fallback questionDao.randomPickByKp(subject, count * 2) .filter { it.id !in pickedIds } .take(count - part.size) part part fallback } picked part } // 事务内写 paper 与 paper_question保证一组卷要么全成要么全不成 return paperDao.insertPaperWithQuestions(subject, picked) }insertPaperWithQuestions用Transaction标注内部先插paper拿到自增 ID再逐条把题目内容复制进paper_question并写入orderNo。事务的意义在于如果复制到一半进程被杀不会留下一条没有题目的空试卷用户看到「试卷为空」的诡异界面。降级抽题时先取count * 2条再过滤是为了给「已选题目去重」留出余量直接取count条在极端情况下会补不满。试卷快照表里的字段要和question表冗余一份不要偷懒只存 ID。冗余的代价是存储收益是历史成绩永远可复现课设场景下这个取舍毫无疑问。4.2 答题计时为什么不能用简单的循环 delay倒计时最常见的写法是起一个协程每秒delay(1000)然后减一。这个写法在真机上有两个问题应用切到后台后协程可能被系统挂起回到前台时长已经不准delay本身不保证精确到毫秒累计误差在十分钟的考试里能差出好几秒。正确的做法是记录一个绝对截止时刻每次刷新都用当前时间反算剩余让显示值始终以真实时间为准private var deadline 0L fun startTimer(totalSec: Int) { // 用单调时钟用户改系统时间不影响作答时长 deadline SystemClock.elapsedRealtime() totalSec * 1000L viewModelScope.launch { while (isActive) { val remain ((deadline - SystemClock.elapsedRealtime()) / 1000).toInt() if (remain 0) { _state.update { it.copy(remainSec 0) } autoSubmit() // 时间到自动交卷 break } _state.update { it.copy(remainSec remain) } delay(500) // 刷新频率高于 1 秒界面读秒更跟手 } } }刷新周期设成 500 毫秒界面上的秒数变化会更接近真实跳秒用户体感上不会出现「卡一秒」。自动交卷要调用和手动交卷完全相同的方法保证判分路径唯一否则时间到提交的记录可能缺少某些字段。返回前台后不需要做特殊处理因为剩余时间是从deadline反算的界面自动就对齐了。答题界面的进度条可以直接绑remainSec / totalSec用户对时间流逝有直观感知如果是多题一页的布局协调布局配顶部标题栏和底部题号导航能避免长题干把按钮挤出屏幕。4.3 交卷判分与成绩查询的演示路径判分在上一章已经写过这里补运行时的完整行为和验证方法。交卷后写库成功再跳转成绩页不要先跳转再异步写库——用户看到分数后立刻杀掉应用记录就丢了。成绩查询用 Room 的Flow返回值数据一变界面自动刷新Query( SELECT r.id, r.score, r.costSec, r.submitTime, p.subject, p.totalScore FROM exam_record r JOIN paper p ON r.paperId p.id WHERE r.userId :userId ORDER BY r.submitTime DESC ) fun observeRecords(userId: Long): FlowListRecordItem返回Flow而不是List配合collectAsState或LiveData交卷写库后成绩列表会自己更新不需要手动refresh()。演示时建议按这个顺序走一遍进入首页 → 选择题库科目 → 开始考试 → 故意答错两题 → 手动交卷 → 查看分数与耗时 → 返回成绩列表确认新增记录 → 退出账号重新登录确认记录还在。这一串截图正好覆盖论文「运行演示」章节需要的图每张图下面写清楚输入和预期输出比堆十张相似界面图有用。注意导出成绩单或分享截图时会用到 FileProviderAndroidManifest.xml里必须声明 provider 并配置file_paths.xml否则在 Android 7.0 以上会直接抛FileUriExposedException。分享前用FileProvider.getUriForFile拿 Uri不要直接传file://路径。5. 答辩前的打包验证与报告图文组织代码能跑不等于能答辩。最后一章讲两件在评分离散度最高的事Release 包能不能一次签好装到评委手机上以及论文里的图和心得怎么写才不像模板。5.1 签名打包与真机安装验证Debug 包用调试签名换台机器装会提示签名不一致所以答辩当天要带 Release 包。先生成密钥库再配signingConfigs最后走assembleRelease# 生成密钥库有效期 3650 天alias 建议和项目名一致 keytool -genkeypair -v -keystore exam.jks -keyalg RSA -keysize 2048 \ -validity 3650 -alias exam # 构建发布包 ./gradlew clean assembleRelease # 校验签名确认 v1/v2 签名方案都已启用 apksigner verify --verbose app/build/outputs/apk/release/app-release.apkkeytool生成时会让填姓名、组织、地区课设随便填但密码要记住密钥库文件丢了后面就没法给同一个应用发更新。assembleRelease前记得把minifyEnabled打开并配好proguard-rules.proRoom 和 Gson 用的实体类需要 keep 规则否则混淆后查库会报字段找不到# 保留 Room 生成的实现类和实体字段名 -keep class * extends androidx.room.RoomDatabase -keep androidx.room.Entity class * { *; } # Gson 反射用到的实体 -keep class com.example.exam.data.model.** { *; }混淆后一定要装到真机上把查库、判分、成绩列表三条路径各点一遍。apksigner verify输出里要看到Verified using v2 scheme: true只有 v1 签名在部分新系统上装不上。5.2 报告里的详细设计与心得部分怎么写详细设计章节的图和代码要对得上。表结构画 E-R 图字段名和 Room 实体完全一致组卷流程画活动图分支节点要和buildPaper里的降级逻辑一一对应。评委顺着图找代码找得到就是合格的设计章节。每张运行截图下面用两行字写清「输入是什么、观察到什么」比在图旁边标「运行界面」四个字强得多。心得部分最容易写成套话。可写的具体素材其实很多为什么用elapsedRealtime而不是currentTimeMillis、为什么试卷要做快照、为什么 DAO 全部用suspend、组卷降级策略是在什么情况下发现的。每一条都是「先遇到问题、再查资料、最后比较方案」的结构五六百字写三四条就够不用凑。验证顺序也建议固定下来adb install装 Release 包 → 断网打开应用确认不依赖网络 → 新增题库题目 → 组卷 → 限时作答 → 交卷 → 杀进程重进查成绩 → 卸载重装确认数据清空符合预期。这套流程走完基本不会再出现答辩现场翻车的场面。本文还有配套的精品资源点击获取