新闻详情

新闻详情

首页 / 资讯中心 / 详情

Android前后台分离选课系统实战:Retrofit+Spring Boot完整实现

发布时间:2026/9/16 14:30:45来源:尧图网络
Android前后台分离选课系统实战:Retrofit+Spring Boot完整实现
简介面向Android课程设计的一套选课系统完整源码采用前后台分离思路覆盖学生端与管理员端。学生通过账号密码登录后可查看个人课表、浏览选课列表并提交选课还支持修改个人信息管理员使用固定账号登录后既可管理全部课程信息也能删除或新增课程可修改学生每门课的成绩并能添加新的学生账号。项目中借助SQLiteDatabase完成数据持久化使用底部导航栏组织核心页面配合Activity跳转实现常用流程UI与交互逻辑都比较常规适合安卓初学者作为课设模板或二次开发起点。压缩包大小29.52MB当前详情页显示文件总数为0、文件类型明细暂无实际内容请以解压后为准。该资源已有3921人学习适用于Android Studio 3.6.1及以上版本配置Gradle 5.6.4即可运行。代码带注释便于对照理解选课模块、角色权限和数据表设计。1. 这套选课系统的价值在于把“数据边界”画在手机之外课程设计里“选课系统”出镜率极高但多数交上去的版本是把课程数据直接塞进 SQLite界面和逻辑全挤在一个 Activity 里。能打开、能点按钮、能查几条记录本质上是个单机演示程序。答辩时老师只要问“课程数据换一台手机怎么办”“两个人同时选最后一门课会不会超选”当场就卡住。标题里的“前后台分离”就是把数据与业务逻辑放到独立服务端Android Studio 端只负责展示与交互双方通过 HTTP 接口交换 JSON。这套结构的优势不是“看着高级”而是界面可以单独调试、数据可以单独验证、选课竞争条件可以在服务端统一处理正好覆盖课程设计最看重的几个评分点架构清晰、功能完整、有测试支撑。2. 前后台分离的架构怎么切Android Studio 负责 UI服务端负责数据2.1 前后台分离的分界点接口、协议、状态各管各的前后台分离不是简单地把代码分成两个工程而是要分清三种边界接口边界、协议边界、状态边界。接口边界指客户端只依赖服务端暴露的 REST API不会直接操作数据库协议边界指双方约定 JSON 结构字段名、类型、错误码都统一状态边界指的是“选课是否成功”这类业务状态由服务端判定Android 端只负责把判定结果渲染出来不能自己在本地把剩余名额减一。这个划分对课设尤其关键。很多同学会在 Android 端做一个“本地课程缓存”选课时先改本地数据再同步服务端一旦同步失败两边数据就对不上。前后台分离的写法应该是服务端是唯一事实来源Android 端每次进入页面都重新拉取课程列表选课动作只提交请求界面状态跟着响应刷新。这样排查问题的时候只需要问“服务端返回了什么”而不是“客户端哪里算错了”。2.2 选型依据Retrofit OkHttp JSON 为什么是课设首选实现前后台分离的 Android 端常见方案是 Retrofit 加 OkHttp 加 Gson。Retrofit 负责把接口方法声明成 Kotlin/Java 函数OkHttp 负责真正的网络请求与连接管理Gson 负责 JSON 和实体类的互转。这三样都是 Android Studio 默认工程里可以直接引入的库资料多、报错容易搜、课程设计答辩时也经得起问。技术职责为什么选它常见替代Retrofit接口定义与请求装配注解声明式写法代码量少泛型解析完善Ktor Client、VolleyOkHttpHTTP 连接、超时、日志连接复用好拦截器方便调试稳定HttpURLConnectionGsonJSON 序列化/反序列化与 Retrofit 集成简单字段映射灵活Moshi、kotlinx.serializationSpring Boot服务端接口与事务起步快内置 Tomcat课设环境友好Flask、Node.js Express我在课设里推荐 Spring Boot 做服务端不需要额外配 Tomcat一个 main 方法就能启动MyBatis 可以直接写 SQL对“数据库查询”这个考点的展示非常直观。如果你的电脑跑不动 Spring Boot用 Flask 写同样接口也行Android 端代码完全不受影响这正是前后台分离的好处。2.3 统一接口规范ApiResp 包裹返回数据省掉一半查询调试客户端和服务端约定一个统一响应结构我用一个泛型包装类所有接口都返回这个格式前端只解析一次。{ code: 0, message: success, data: {} }code 为 0 表示成功非 0 表示业务失败message 给用户提示data 放具体数据。有人会直接用 HTTP 状态码表示业务状态我一般不建议HTTP 200 只代表请求到达服务器不代表选课成功。比如“课程已满”是预期内的业务结果HTTP 状态码仍应返回 200由 code 字段区分。code含义客户端处理0成功解析 data1001课程不存在或已满Toast message1002重复选课Toast message 并刷新列表1003参数缺失检查请求参数Android 端对应的 Kotlin 数据类长这样data class ApiRespT( SerializedName(code) val code: Int, SerializedName(message) val message: String, SerializedName(data) val data: T? )注意两个点第一Gson 反序列化时只看SerializedName注解接口字段名和 Kotlin 字段名不一致很容易踩坑后面排错章节会细说第二data 字段的泛型 T 在 Retrofit 声明返回类型时要写具体类型比如ApiRespListCourseDto否则泛型擦除会直接解析失败。3. 服务端选课系统的数据模型与查询接口3.1 数据库三表设计student、course、selected_course 及唯一索引选课系统最经典的库表结构是三张表学生表、课程表、选课关系表。先看建表 SQL这套结构在 MySQL 8 和 MariaDB 上都直接可用。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, teacher VARCHAR(50), credit INT DEFAULT 2, total INT NOT NULL, remaining INT NOT NULL ); CREATE TABLE selected_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) );课程表保留了一个remaining冗余字段专门用来表示剩余名额。这不是数据库设计课说的“反范式错误”而是针对“选课”这种高频读、低频写场景的常见做法列表页直接读 remaining 展示“剩余 X 人”不用实时 count 选课关系表。selected_course上唯一的联合索引uk_student_course是防重复选课的第一道防线即使业务代码漏判断数据库层面也不允许同一个学生对同一门课插入两条记录。这个索引在答辩时一定要主动提它能直接引出第五章的并发验证。3.2 查询接口的 SQL 与实现关联查询的取舍课程列表接口要做的是“查所有课程并标记当前学生是否已选”。常见实现是在 SQL 里用LEFT JOIN一次查出结果避免在 Android 端用循环比对。Select( SELECT c.id, c.name, c.teacher, c.credit, c.total, c.remaining, IF(s.id IS NULL, 0, 1) AS selected FROM course c LEFT JOIN selected_course s ON c.id s.course_id AND s.student_id #{studentId} ORDER BY c.id ) ListMapString, Object listCourses(Long studentId);注意 JOIN 的条件里带上了s.student_id #{studentId}这是关键。如果把这个条件写进 WHERE就变成了内连接没选过的课程会被过滤掉写在 JOIN 条件里LEFT JOIN 才能保留未选课程并通过IF(s.id IS NULL, 0, 1)判断出是否已选。这条 SQL 是课设报告里可以直接截图分析的内容把“为什么放 ON 不放 WHERE”讲清楚数据库查询这一项得分就不会低。接口层暴露为GET /api/courses?studentId1返回的就是之前约定的ApiResp结构。实际调用时MyBatis 返回的 Map 里字段名是下划线风格而客户端用的是驼峰我一般在 SQL 里用别名直接转换比如把student_id写成studentId避免在 Java 里再做一层转换。3.3 选课操作的防超卖处理事务、先查后改与唯一索引的兜底选课是这个系统的核心事务逻辑不能只是“insert 一条记录”。两个学生同时请求最后一门课时如果只做“先查剩余再插入”中间隔着网络延迟两个请求都可能读到 remaining1导致超卖。服务端处理我一般这样写Transactional public void selectCourse(Long studentId, Long courseId) { // 带行锁查询阻塞并发的其他选课请求 Course course courseMapper.selectByIdForUpdate(courseId); if (course null || course.getRemaining() 0) { throw new BusinessException(1001, 课程已满或不存在); } int count selectedMapper.countByStudentAndCourse(studentId, courseId); if (count 0) { throw new BusinessException(1002, 不能重复选课); } selectedMapper.insert(studentId, courseId); courseMapper.decreaseRemaining(courseId); }selectByIdForUpdate对应的 SQL 是SELECT * FROM course WHERE id #{courseId} FOR UPDATE这是悲观锁的写法事务提交前其他事务要更新同一行必须等待。对课设场景选课并发量远没有电商秒杀那么大悲观锁简单可靠、容易解释。如果你想在报告里体现进阶思考可以补一句高并发场景会改用乐观锁或 Redis 原子扣减但课程设计用行锁已经足够。Transactional保证这三步在同一事务里插入成功但扣减失败时整体回滚不会出现“票扣了但没选上”的数据不一致。注意事务要加在 Service 方法上而不是 Controller 方法上加错了位置会导致事务完全不生效。4. Android Studio 客户端实现Retrofit 到 RecyclerView 的完整链路4.1 定义 Retrofit 接口和网络层baseUrl、超时、日志拦截器先在build.gradle里加依赖然后写网络层。依赖版本不用刻意追新我用的是一套非常稳定的组合implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.12.0 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1网络层我习惯用一个单例对象封装避免每个页面都 new 一个 Retrofitobject ApiClient { private const val BASE_URL http://10.0.2.2:8080/ private val okHttpClient OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .addInterceptor(HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BODY }) .build() val courseApi: CourseApi Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() .create(CourseApi::class.java) }baseUrl 有两个坑必须说。第一必须以/结尾否则和GET(api/courses)拼起来会少掉斜杠第二10.0.2.2是 Android 模拟器访问宿主机的专用地址真机调试要改成电脑的局域网 IP。connectTimeout 指建立连接的超时readTimeout 指等待响应的超时课设环境里服务端第一次启动往往比较慢readTimeout 给到 15 秒比较保险。日志拦截器只建议在 debug 环境开输出所有请求和响应体这是排查接口问题最快的工具。接口定义用协程的 suspend 写法比传统 enqueue 回调简洁很多Retrofit 2.6 以上原生支持interface CourseApi { GET(api/courses) suspend fun getCourses(Query(studentId) studentId: Long): ApiRespListCourseDto POST(api/select) suspend fun selectCourse(Body SelectRequest request): ApiRespAny }Query会把 studentId 拼到 URL 上Body会把对象序列化成 JSON 放进请求体。这两个注解是 Retrofit 最常用的两个参数注解考试和面试都爱问。4.2 ViewModel LiveData 管理课程列表与选课状态页面直接用 Activity 发请求也能跑但旋转屏幕时请求会中断、数据会丢。用 ViewModel 把网络请求和界面状态放进生命周期感知的容器里是“优秀课设”和普通课设的明显分界线。class CourseViewModel : ViewModel() { private val api ApiClient.courseApi private val _courseList MutableLiveDataListCourseDto() val courseList: LiveDataListCourseDto _courseList private val _tip MutableLiveDataString() val tip: LiveDataString _tip fun loadCourses(studentId: Long) { viewModelScope.launch { try { val resp api.getCourses(studentId) if (resp.code 0) { _courseList.value resp.data } else { _tip.value resp.message } } catch (e: Exception) { _tip.value 网络异常${e.message} } } } fun selectCourse(studentId: Long, courseId: Long) { viewModelScope.launch { try { val resp api.selectCourse(SelectRequest(studentId, courseId)) _tip.value if (resp.code 0) 选课成功 else resp.message } catch (e: Exception) { _tip.value 请求失败${e.message} } } } }viewModelScope 是 lifecycle-viewmodel-ktx 提供的协程作用域ViewModel 销毁时自动取消协程避免请求还没回来页面已经关了导致的泄漏。LiveData 的value赋值只能在主线程上面代码没有切换线程所以安全如果网络请求被放到了Dispatchers.IO就必须改用postValue这是新手最常掉的坑。界面侧通过observe监听courseList数据到位就刷新 RecyclerView监听tip弹一个 Toast。整个 Activity 里不会出现任何网络代码这就是前后台分离在客户端层面的体现网络层、数据层、界面层各管各的。4.3 RecyclerView 的选课按钮逻辑防重复点击与失败回滚选课按钮的交互逻辑比想象中容易写崩。常见问题是用户手快双击按钮一个学生发出两个选课请求或者请求失败后按钮一直保持禁用用户以为点过了。我一般会维护一个selecting集合记录正在提交中的课程。class CourseAdapter( private val onSelect: (CourseDto) - Unit ) : RecyclerView.AdapterCourseAdapter.VH() { private val selecting mutableSetOfLong() fun setSelecting(courseId: Long, selecting: Boolean) { if (selecting) this.selecting.add(courseId) else this.selecting.remove(courseId) notifyDataSetChanged() } // 在 onBindViewHolder 里 holder.btnSelect.setOnClickListener { val course item if (selecting.contains(course.id)) returnsetOnClickListener if (course.selected 1) { Toast.makeText(context, 已选过这门课, Toast.LENGTH_SHORT).show() } else { setSelecting(course.id, true) onSelect(course) } } }在 ViewModel 返回响应后调用setSelecting(courseId, false)重置按钮状态同时刷新课程列表更新剩余名额和已选标记。这样双击提交被拦截在网络层之前失败也不影响后续重试。选课前再弹一个AlertDialog让用户确认这一条交互细节放到课设演示里会非常显眼属于低成本高印象分的功能。5. 联调排错与并发验证优秀课设的加分项5.1 联调阶段最常见的 5 个报错从 Android Studio 到 JSON 解析前后台分离的项目第一次把两端连起来跑报错几乎是必然的。下面这 5 个是我在课设指导里见到最频繁的问题按检查顺序列出来。现象原因处理Connection refused模拟器用了 localhost指到自己服务端保持启动baseUrl 改成http://10.0.2.2:8080/CLEARTEXT communication not permittedAndroid 9 默认禁止明文 HTTP在 AndroidManifest.xml 的 application 上加android:usesCleartextTraffictrueJSON 解析失败 Expected BEGIN_OBJECT后端 data 字段返回结构不一致用日志拦截器看原始 JSON对照SerializedName逐个字段核对中文乱码服务端和数据库编码不一致MySQL 建库用utf8mb4服务端连接串加characterEncodingUTF-8Gradle 导入慢或失败首次需下载依赖用阿里云镜像仓库或打开 Android Studio 的 Offline Work 模式其中明文 HTTP 的问题只在调试阶段建议打开课程设计不涉及上线场景可以用这个方案如果你的服务端配了 HTTPS就不需要这项配置。5.2 用一个并发脚本证明“不超卖”答辩时直接演示选课系统最重要的验证是并发下不超卖。我把数据库里课程 ID 为 1 的课程余量设为 8然后用一条循环命令模拟 20 个学生同时抢课COURSE_ID1 for i in $(seq 1 20); do curl -s -X POST http://10.0.2.2:8080/api/select \ -H Content-Type: application/json \ -d {\studentId\: $i, \courseId\: $COURSE_ID} done wait让 20 个请求并发执行wait等全部结束。跑完后查询数据库SELECT COUNT(*) FROM selected_course WHERE course_id 1; SELECT remaining FROM course WHERE id 1;正确的预期是COUNT(*) 8remaining 08 个请求成功其余 12 个返回课程已满不允许插入第 9 条记录。如果结果出现大于 8 的记录数说明事务或行锁没有生效回到 3.3 节检查Transactional是否加在了 Service 层。建议把命令和查询结果截图放进课设报告同时把这组 curl 参数改成你自己的课程 ID 和端口跑通后存档这就是整个项目最有力的数据一致性证据。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ASP经典技术实战:GM推广系统v6.0部署与归因开发解析 2026/9/16 15:09:52

ASP经典技术实战:GM推广系统v6.0部署与归因开发解析

简介:这是一套基于ASP技术构建的游戏推广系统源码,面向游戏管理员提供用户管理、推广链接追踪、数据分析、广告投放与奖励发放等一体化后台功能,适合熟悉服务器端脚本开发的运营人员或学习者参考和二次开发。压缩包共四百九十个文件&#xff…

阅读更多 →
Omi Windows 桌面端「What‘s New」更新日志体系解析:从 changelog Fragment 到丙烯酸 Toast 的完整链路 2026/9/16 15:09:52

Omi Windows 桌面端「What‘s New」更新日志体系解析:从 changelog Fragment 到丙烯酸 Toast 的完整链路

Omi Windows 桌面端「Whats New」更新日志体系解析:从 changelog Fragment 到丙烯酸 Toast 的完整链路 【免费下载链接】Friend AI that sees your screen, listens to your conversations and tells you what to do 项目地址: https://gitcode.com/GitHub_Trendi…

阅读更多 →
Litestar 状态码参考:`litestar.status_codes` 模块全量解析与实战用法 2026/9/16 15:09:52

Litestar 状态码参考:`litestar.status_codes` 模块全量解析与实战用法

Litestar 状态码参考:litestar.status_codes 模块全量解析与实战用法 【免费下载链接】litestar Light, flexible and extensible ASGI framework | Built to scale 项目地址: https://gitcode.com/GitHub_Trending/li/litestar 本文是 Litestar 框架内置状态…

阅读更多 →
图片风格迁移系统:VGG19特征提取与Gram矩阵双损失原理详解 2026/9/16 15:09:52

图片风格迁移系统:VGG19特征提取与Gram矩阵双损失原理详解

简介:这是一份基于PyTorch实现的图片风格迁移完整项目,面向深度学习初学者与计算机视觉爱好者,可帮助理解如何利用卷积神经网络(CNN)将内容图像与风格图像融合,生成艺术化作品。资源共28个文件,…

阅读更多 →
STM32裸机实现RNDIS+LwIP USB网络设备 2026/9/16 15:09:52

STM32裸机实现RNDIS+LwIP USB网络设备

简介:本资源是面向嵌入式开发工程师与STM32进阶学习者的RNDIS网络通信实战工程,聚焦于在STM32F429DISCO开发板上基于LwIP协议栈实现USB RNDIS虚拟网卡功能,并采用静态IP配置(不依赖DHCP),适用于USB转以太网…

阅读更多 →
Claude 提示工程交互教程:9 章练习写出高质量提示词的完整指南 2026/9/16 15:06:51

Claude 提示工程交互教程:9 章练习写出高质量提示词的完整指南

Claude 提示工程交互教程:9 章练习写出高质量提示词的完整指南 【免费下载链接】prompt-eng-interactive-tutorial Anthropics Interactive Prompt Engineering Tutorial 项目地址: https://gitcode.com/GitHub_Trending/pr/prompt-eng-interactive-tutorial …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞