1. 项目整体思路与架构设计
1.1 为什么选Spring Boot + Android这套组合
先说结论:这套技术栈放在计算机毕业设计里,属于性价比非常高的选择。后端用Spring Boot,客户端用Android原生,两个方向都踩在了当前就业市场的主流需求上,评阅老师看着熟悉,你答辩也有东西可讲。
整个系统的定位是"在线家教服务平台",说白了就是做两件事:让家长能发布家教需求、找到合适的教员;让教员能浏览订单、接单、完成授课。围绕这个核心,衍生出用户注册登录、个人信息管理、订单状态流转、评价体系、消息通知等功能。技术上没有特别炫酷的难点,但覆盖了前后端开发的主要环节,非常适合作为毕业设计的选题范围。
我见过不少学生一上来就纠结要不要用微服务、要不要上Redis缓存、要不要搞个消息队列。这里我的建议很直接:毕设项目不要为了技术而技术,架构能清晰表达业务即可。Spring Boot自带内嵌Tomcat,MyBatis做持久层,MySQL存数据,Android端用OkHttp或者Retrofit做网络请求,这套方案足够撑起整个项目,而且每一环都能讲清楚原理,答辩时不会给自己挖坑。
1.2 系统核心需求拆解
在做任何编码之前,把需求梳理清楚是最关键的一步。我习惯用角色驱动的方式去拆功能,也就是先搞清楚系统里有哪几类人,每类人分别能干什么。
这个家教系统里主要有三类角色:
| 角色 | 核心诉求 | 主要功能 |
|---|---|---|
| 家长/学生 | 找到合适的家教老师 | 发布需求、搜索教员、下单、确认授课、评价 |
| 教员 | 获取学生资源、管理授课安排 | 注册教员档案、浏览订单、接单、查看授课记录 |
| 系统管理员 | 维持平台正常运转 | 用户管理、订单监管、审核教员信息 |
基于这三类角色的诉求,系统功能模块可以拆成下面这些:
- 用户模块:注册、登录、个人资料编辑、密码修改
- 教员模块:教员认证(填写教学科目、教学经验、价格)、教员列表展示
- 需求模块:家长发布家教需求,包括科目、年级、上课时间、预期价格
- 订单模块:从家长下单到教员接单再到授课完成的全流程状态管理
- 评价模块:授课结束后家长对教员进行打分和文字评价
- 消息模块:订单状态变更时给对端发送通知(毕设阶段做基础的消息列表即可)
这个需求清单列出来后,基本就能估算出工作量了:后端大概七八张表、二三十个接口;Android端有七八个核心页面。对于一个人独立开发来说,这个量级在三个月内完成从设计到联调再到写论文,时间上是充裕的。
1.3 可行性分析与技术选型考量
每次带学生做类似项目,我都会先帮他分析一遍"为什么这个方案可行",这个思路自己心里有数,答辩时也能应对"你这个某某技术为什么这么选"这类问题。
Spring Boot选择2.x版本就好,比如2.5.x或2.6.x。别上来就追新用Spring Boot 3,那套东西虽然已经发布很久,但部分配套依赖的兼容性对新手不够友好。我们做毕设,稳定压倒一切。Android端建议compileSdk用33或34,minSdk用24左右就行,覆盖绝大多数真机,MVP或MVVM都可以,我实际操作下来觉得MVP配合接口回调的方式更好理解,调试起来逻辑也更清楚,代码量也不算大。
数据库层面,MySQL 5.7或8.0都行,本地开发跑在Navicat或者DataGrip里。表结构设计遵循第三范式,不过有些场景可以适当冗余以提高查询效率,这个我在后面详细讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端核心模块设计与实现要点
2.1 数据库表设计与关系梳理
数据库设计是整个项目的基石。表设计得好,后面写Mapper、改接口都顺畅;表设计有坑,后面越补越乱。我通常会先画一个简单的ER图,再落到具体的建表SQL。这里我直接给出一个经过验证的、可直接参考的表结构清单。
用户表(t_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 用户ID |
| username | varchar(32) 唯一 | 登录账号 |
| password | varchar(128) | 加密存储,使用BCrypt |
| real_name | varchar(32) | 真实姓名 |
| phone | varchar(20) | 手机号 |
| role | tinyint | 1-家长 2-教员 3-管理员 |
| avatar | varchar(255) | 头像地址 |
| created_at | datetime | 注册时间 |
教员信息表(t_teacher)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 教员ID |
| user_id | bigint 外键 | 关联t_user表 |
| subject | varchar(50) | 教学科目,如"高中数学" |
| intro | text | 个人简介 |
| price | decimal(10,2) | 每小时价格 |
| experience | int | 教龄,单位:年 |
| status | tinyint | 0-待审核 1-已通过 2-已拒绝 |
家教需求表(t_demand)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 需求ID |
| user_id | bigint 外键 | 发布者(家长)ID |
| subject | varchar(50) | 辅导科目 |
| grade | varchar(50) | 学生年级 |
| address | varchar(255) | 授课地址 |
| expected_price | decimal(10,2) | 期望价格 |
| class_time | varchar(100) | 上课时间段描述 |
| remark | text | 补充说明 |
| create_time | datetime | 发布时间 |
订单表(t_order)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键自增 | 订单ID |
| demand_id | bigint 外键 | 关联需求表 |
| parent_id | bigint | 家长用户ID |
| teacher_id | bigint | 教员用户ID |
| status | tinyint | 0-待接单 1-已接单 2-授课中 3-已完成 |
| create_time | datetime | 下单时间 |
| finish_time | datetime | 完成时间 |
这边需要拆开说明的是订单状态流转的设计思路。很多学生喜欢用字符串存状态,比如"待接单""已完成"直接写进去,这看是方便,但后面写条件查询、做状态变更,都非常容易出bug。我更推荐用数字枚举状态,在Java代码里定义一个枚举类或者常量类统一管理,Android端同时维护一份相同的定义。这样联调时只要对数字值,各写各的逻辑,就能保证状态完全对齐。
评价表、消息表结构相对简单,就不逐一列字段了,核心就是关联订单ID和用户ID,加上评分和评语。需要注意的是消息表最好加一个is_read字段,用于实现未读数角标,这个功能虽然小,但在演示时非常加分。
关于表关系,我特别要强调一个容易忽视的点:订单表里冗余了parent_id和teacher_id两个用户ID,而不是只存demand_id再关联查询。这样做的好处是,订单列表页加载时不需要回表去查用户信息,一个SQL直接带出所有要展示的数据。毕设阶段查询性能压力不大,但代码里少一次关联join,Mapper写起来也更清爽。
2.2 登录认证与Token机制
登录是几乎所有系统的入口,也是毕设答辩时的重点提问区域。我会让学生想清楚:为什么HTTP是无状态的?移动端应用登录后,服务器怎么识别是同一个用户?
常用的方案有几种:
- 简单Session方式:服务器保存会话,客户端存Cookie
- Token方式:服务器签发一个Token字符串,客户端保存并在后续请求头中携带
- JWT方式:自包含的Token,服务器不保存会话状态
对Android客户端来说,我推荐用JWT或者简单的UUID Token都行。如果追求容易解释,UUID Token配合Redis存储最简单;如果想让论文里有更多技术含量,JWT是更好的选择。
我在实际操作中倾向直接使用JWT,因为它不需要额外的Redis依赖,而且返回的值本身就是一串header.payload.signature,Android端可以不做任何处理,把字符串塞进SharedPreferences就行。后端每次请求从Authorization请求头里取出Token,用拦截器校验。写起来代码也不复杂:
java复制// JWT工具类核心方法
public String generateToken(Long userId, String username, String role) {
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.claim("role", role)
.setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
需要提醒的一点:毕业设计里不要自己实现加密算法、不要写裸的随机Token拼接。用现成的jjwt库就好,版本选择时要找和Spring Boot版本兼容的,不然启动时会遇到NoSuchMethodError之类的问题,这个坑我踩过,后面在问题章节详细说。
2.3 REST API接口设计规范
前后端接口一致性是联调效率的关键。我的习惯是先定接口文档,再写代码。哪怕是毕设项目,也建议你在项目根目录放一个api.md,把每个接口的路径、请求方式、请求参数、响应格式列清楚。别嫌这点工作多余,等Android端开始对接时你就会感谢这份文档了。
统一响应结构
json复制{
"code": 200,
"message": "操作成功",
"data": {
"token": "xxx.xxx.xxx",
"userId": 1
}
}
code用200表示成功,401表示未登录或Token失效,500表示服务器异常。这个结构要写一个通用的Result<T>类,所有Controller返回值都走它。
核心接口清单示例
| 模块 | 接口路径 | 方法 | 说明 |
|---|---|---|---|
| 用户 | /api/user/register | POST | 注册 |
| 用户 | /api/user/login | POST | 登录 |
| 用户 | /api/user/info | GET | 获取当前用户信息 |
| 用户 | /api/user/update | PUT | 更新用户资料 |
| 教员 | /api/teacher/apply | POST | 提交教员认证 |
| 教员 | /api/teacher/list | GET | 教员列表(按科目/价格筛选) |
| 需求 | /api/demand/publish | POST | 发布家教需求 |
| 需求 | /api/demand/list | GET | 需求列表(分页) |
| 订单 | /api/order/create | POST | 家长下单 |
| 订单 | /api/order/accept | POST | 教员接单 |
| 订单 | /api/order/finish | POST | 确认完成 |
| 订单 | /api/order/list | GET | 我的订单列表 |
| 评价 | /api/comment/submit | POST | 提交评价 |
接口命名我刻意保持了名词加动词的风格。有一点要注意:移动端接口的路径前缀建议用/api集中管理,后面加Spring Security或者拦截器时,可以直接用/api/**匹配需要鉴权的路径,静态资源就排除掉,不会误伤。
3. Android客户端核心开发实战
3.1 Android项目结构与技术选型
Android端的项目结构,我建议按模块分包,不要按层次分包。按层次分包的意思是把所有Activity放一个包、所有Adapter放一个包、所有Fragment放一个包。这样做小项目还行,但只要功能一多,找代码就很痛苦。按模块分包更清晰,每个功能模块一个包,里面自带Activity、Adapter、Bean:
code复制cn.edu.school.tutor
├── model/ # 实体类(和后台的User、Order对应)
├── network/ # 网络请求封装
├── ui/login/ # 登录注册模块
├── ui/home/ # 首页/导航模块
├── ui/demand/ # 需求发布与浏览模块
├── ui/order/ # 订单管理模块
├── ui/teacher/ # 教员模块
├── ui/mine/ # 个人中心模块
├── utils/ # 工具类
└── widget/ # 自定义View
技术选型上,网络层用Retrofit + OkHttp是比较主流的方式。Retrofit的注解API非常直观,把接口定义成Java方法即可。配合Gson把后台返回的JSON转成实体对象,省去手动解析JSON的麻烦。有人这时候会问:为什么不用Volley或者HttpURLConnection直接写?都可以,但Retrofit学习成本低、代码量少,而且面试时提到Retrofit本身就是一个加分项。不过需要注意的是,Retrofit 2.6.0之后的版本,suspend函数支持得很好,但如果你还在用Java写Android,那还是老一套call.enqueue回调写法,也够用。
3.2 登录注册与用户信息本地存储
登录注册是整个Android端的起点。页面设计上,就是两个EditText加上一个登录按钮、一个跳转注册的入口。但背后有不少细节值得写出来。
首次打开App时,检查SharedPreferences里有没有存Token,有就直接拉取用户信息进入首页,没有就跳转到登录页。Token失效的情况怎么处理?我是在拦截器里处理的:如果后端返回401,就清空本地Token并强制跳转到登录Activity。
这个逻辑要用一个全局的工具类管理,不要散落在各个Activity里。我实际开发中是这样封装的:
java复制public class UserManager {
private static final String PREF_NAME = "tutor_pref";
private static final String KEY_TOKEN = "token";
private static final String KEY_USER_ID = "user_id";
private static UserManager instance;
private SharedPreferences sp;
public static UserManager getInstance() {
if (instance == null) {
instance = new UserManager();
}
return instance;
}
public void saveLoginInfo(String token, Long userId) {
sp.edit()
.putString(KEY_TOKEN, token)
.putLong(KEY_USER_ID, userId)
.apply();
}
public String getToken() {
return sp.getString(KEY_TOKEN, "");
}
public boolean isLoggedIn() {
return !getToken().isEmpty();
}
public void logout() {
sp.edit().clear().apply();
}
}
密码明文存到SharedPreferences?不行。密码只在用户输入那一刻使用,请求登录后后端返回Token,前端就彻底不需要再保留密码了。设计上要明确:Android端绝不本地存储密码,这是安全性底线,答辩时被问到安全设计,这也能成为你的一个回答亮点。
3.3 核心业务页面的实现思路
这里的核心页面有四个,分别是:需求列表页、需求发布页、教员列表页、订单管理页。逐个讲一下。
需求列表页是最典型的"列表页"模式,用RecyclerView承载。数据来源是从后端拉取的/api/demand/list接口,返回的是分页JSON。列表项Item展示科目、年级、期望价格、发布时间几个关键信息。点击某个Item后,跳转到需求详情页,里面展示完整信息,并提供一个"立即下单"的按钮。
需求发布页是相对复杂的表单页。涉及多个输入项,包括科目、年级、授课地址、期望价格、上课时间描述、备注。表单校验要放在前端做:科目非空、价格大于0、地址非空。这些校验逻辑通过TextWatcher或者提交时统一校验都可以,我习惯用后者,因为代码集中、好维护。
订单管理页分为家长视角和教员视角。家长看到的订单列表是自己发布的,状态可能是待接单、已接单、已完成;教员看到的订单列表是待接单的和自己已接单的。两个视角通过登录用户的role字段来控制展示内容。订单状态变更在UI上的反馈非常直接:待接单的订单在教员端显示"接单"按钮,接单后按钮消失,变成"等待授课完成"。
关于列表页的下拉刷新和上拉加载,毕设阶段建议用Android自带的SwipeRefreshLayout加分页参数就行。分页参数的设计要提前跟后端约定好:请求参数pageNum、pageSize,返回体里要有total和records。这个约定在api.md里写清楚,两个端不会各搞一套。
3.4 Android进度条与加载状态处理
分析关键词时看到"android进度条"搜索热度很高,这里正好扩展讲一下。移动端加载数据时最忌讳的事情是用户点了按钮之后没有反馈,误以为App卡死了。正确处理方式是:
- 页面首次加载数据:显示一个居中的转圈ProgressBar
- 点击"登录""提交"等按钮:按钮内显示一个小型加载动画,同时禁用按钮防止重复提交
- 网络请求失败:显示错误提示和一个"重试"按钮
进度条的三种使用场景分别对应不同的代码写法。首次加载可以用布局里的android:visibility="visible"控制显示隐藏;按钮内的加载动画可以定义一组drawable作为点击后的背景;重试按钮则加一个点击事件重新发起请求。这些小功能,每个单独看都不复杂,但组合起来就是App"体验感"的来源。
4. 关键业务场景与接口联调过程
4.1 完整交易流程的状态机设计
家教业务闭环的核心就是订单状态的流转。我建议把这个状态机画清楚,直接放进毕业论文里,也是一个很好的图表素材:
- 家长发布需求 -> 需求处于待接单状态
- 家长看到需求列表里合适的教员列表,也可以直接浏览教员列表页,主动选择某个教员
- 订单创建后状态为0(待接单)
- 教员接单后状态变为1(已接单)
- 授课完成后家长确认,状态变为2(已完成)
- 完成后触发评价流程
这里有一个业务上的细节要想清楚:家长发布需求后,要不要强制走"等教员抢单"的流程? 在实际设计中,我建议做成双通道:家长可以发布需求等教员主动接单,也可以直接列表里找教员发起下单。两种方式殊途同归,最终都生成一笔订单。这样做的好处是覆盖了线上到线下的完整家教场景,演示时可以展示两条路径,内容更丰富。
状态机各状态对应的权限控制:
| 订单状态 | 家长可操作 | 教员可操作 |
|---|---|---|
| 0-待接单 | 取消订单 | 接单 |
| 1-已接单 | 确认开始授课 | 确认开始授课 |
| 2-授课中 | 确认完成 | 确认完成 |
| 3-已完成 | 评价 | 查看评价 |
同一个字段、角色不同、按钮不同,这个在Android端开发时要用loginUser的role去判断。代码里建议直接用一个Manager判断当前用户角色,然后控制对应按钮的显隐。千万别在每个页面都写"if用户是家长"的判断语句,后面修改排列规则时你会改到怀疑人生。
4.2 前后端联调中的时间与日期处理
联调阶段最常见的Bug来源就是时间字段的序列化和反序列化。后端返回的日期通常是一个ISO格式的字符串,比如"2025-06-10 14:30:00",但Android端Gson默认解析格式可能与这个不一致,解析失败后表现就是订单列表的时间全部变成1970年,或者直接报错。
我的做法是:后端统一返回格式化的字符串,不在JSON里传Date对象。
具体来说,在Spring Boot的实体类上加上注解:
java复制@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
LocalDateTime配合Jackson的jsr310模块,序列化结果就是上面这个字符串。Android端拿到字符串后,如果需要显示就原样展示;如果要做倒计时、排序等逻辑,用SimpleDateFormat解析成Date对象再处理。
code复制/**
- 把后端返回的时间字符串转成友好显示格式
*/
public static String formatTime(String timeStr) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(timeStr);
SimpleDateFormat show = new SimpleDateFormat("MM月dd日 HH:mm");
return show.format(date);
} catch (Exception e) {
return timeStr;
}
}
还有一个容易忽略的点:MySQL的时区问题。如果连接数据库的URL里没有加serverTimezone=Asia/Shanghai,查出的时间可能差8个小时,老版本的MySQL驱动会直接报错。正确写法是:
yaml复制jdbc:mysql://localhost:3306/tutor_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
这一个参数就能解决“为什么列表页显示的时间和数据库实际时间不一样”这种经典问答题。
4.3 图片上传与头像展示
头像上传是毕设系统里的标准功能。Android端的相册选择到文件上传,涉及两个核心技术点:权限申请和文件上传方式。
权限方面,Android 6.0以上的运行时权限需要动态申请,READ_EXTERNAL_STORAGE在Android 13之后变成了READ_MEDIA_IMAGES,这个差异很多新手不知道,直接在AndroidManifest里声明旧权限,结果在真机上拿不到相册图片。建议代码里做版本判断兼容:
java复制if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// 请求 READ_MEDIA_IMAGES
} else {
// 请求 READ_EXTERNAL_STORAGE
}
文件上传本身用OkHttp的Multipart方式实现。后端接收时不要用String接,用MultipartFile:
java复制@PostMapping("/api/user/upload-avatar")
public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
String fileName = UUID.randomUUID().toString().replace("-", "")
+ "." + getExtension(file.getOriginalFilename());
String filePath = "/upload/" + fileName;
// 保存到静态资源目录
file.transferTo(new File(uploadDir + filePath));
return Result.success(filePath);
}
这里有个容易踩的坑:保存路径和访问路径不一致。如果你把图片保存到了项目的target目录或临时目录,重启后静态资源就丢了。最好是配置一个绝对路径作为上传目录,然后通过资源映射把这个路径暴露给HTTP访问:
yaml复制spring:
resources:
static-locations: file:/opt/tutor/upload/
这样的话,上传后返回的/upload/2025/06/abc.jpg,Android端拼上服务器IP就能直接访问出来。
4.4 前后端打包部署与校内演示准备
毕业设计演示环节,最尴尬的情况就是Android连不上后端数据库。为避免这种问题,我在实际带学生时总结了几个后端部署的要点。
后端打包成Jar包,放到服务器或者实验室机器上启动。如果只有一台电脑,直接用java -jar跑本地也行,但Android模拟器的IP地址是10.0.2.2,真机的IP就是电脑的局域网IP。这个问题几乎每届学生都会问,我先写在前面:Android模拟器访问电脑本机服务,地址要用10.0.2.2,不能用localhost或127.0.0.1,因为模拟器里的localhost指向的是模拟器自己。
BaseUrl的配置要单独抽出来,放在一个常量类里,方便切换调试环境:
java复制public class ApiConfig {
// 模拟器环境
public static final String BASE_URL = "http://10.0.2.2:8080/";
// 真机调试时改成电脑局域网IP
// public static final String BASE_URL = "http://192.168.1.101:8080/";
}
另外有个必须注意的点:Android 9.0及以上,默认禁用明文HTTP流量。如果你后端是HTTP而不是HTTPS,需要在AndroidManifest.xml里配置usesCleartextTraffic="true",否则所有请求都会报CLEARTEXT communication not permitted。这个错误白纸黑字写在提示里,但很多人根本看不懂它说的是什么意思,排查半天才发现。
5. 常见问题与联调避坑实录
5.1 后端启动与依赖兼容性问题
整个毕设过程中,后端启动阶段我遇到过的典型问题排前三的是:
第一,MyBatis的Mapper接口注入失败。 启动报错Field userMapper in xxxService required a bean of type xxxMapper。解决方式是在启动类上加@MapperScan("com.tutor.mapper")或者在每个Mapper接口上加@Mapper注解,二选一即可,不要两个都加,否则虽然不报错但扫描重复了。
第二,版本兼容问题。 我前面提到jjwt库,如果你用Spring Boot 2.7.x配jjwt 0.9.1,启动时会遇到Unable to load class 'javax.xml.bind.DatatypeConverter'之类的错误。这是因为新版JDK移除了JAXB模块。处理办法有两个:要么升级到io.jsonwebtoken:jjwt-api:0.11.5配合jjwt-impl和jjwt-jackson两个依赖,要么直接换成com.auth0:java-jwt:3.19.0。我更推荐后者,API更简洁,坑也更少。
第三,接口中文乱码问题。 这通常发生在POST请求的JSON体里面。排查方向是先看数据库表的字符集是不是utf8mb4,再看Spring Boot配置文件里的spring.http.encoding.force-response,最后看Android端有没有显式设置请求体的编码。这三个地方都对了,基本不会再乱。实践下来,最省心的方法是在创建MySQL数据库的时候就指定字符集:
sql复制CREATE DATABASE tutor_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
utf8mb4比utf8多了对表情符号的支持,虽然毕设没人发表情评论,但选了总没错。
5.2 Android端崩溃与数据展示异常
Android端我遇到最多的问题集中在主线程操作和空指针上。
主线程执行网络请求是老生常谈的坑。Android不允许在主线程(UI线程)里做网络耗时操作,你要是直接在Activity的onCreate里同步调Retrofit的execute()方法,会抛NetworkOnMainThreadException。Retrofit的enqueue回调本身就是异步的,所以用enqueue就没事;但如果用了同步式写法,一定要丢到子线程里执行。
RecyclerView的Item点击闪退也是高频错误。重点检查有没有复用Item的ViewHolder并在绑定数据时清空上一个Item的残留状态,比如点击事件里获取不到position的旧数据导致数组越界。习惯在Adapter里单独定义一个接口回调即setOnItemClickListener的内部监听器,把点击事件从Activity里面解耦出去,这样代码逻辑会清爽很多。
图片加载不出来又是一个经典问题。这里建议不要自己写复杂的图片加载逻辑,直接用Glide库就好。Glide的链式调用一行代码解决加载、缓存、占位图、错误图这些全部问题:
java复制Glide.with(context)
.load(avatarUrl)
.placeholder(R.drawable.ic_default_avatar)
.error(R.drawable.ic_default_avatar)
.circleCrop()
.into(imageView);
但用Glide时也要注意一个问题:如果你在Adapter里绑定图片时,Item被复用了,Glide可能显示错图。解决办法是Glide会自动处理这种情况,只要你在onBindViewHolder里对ImageView调用了setImageDrawable(null)或者让Glide的load覆盖上去就行。毕设项目的并发场景不大,这类问题出现的概率也低。
5.3 调试工具的实用技巧分享
使用Postman或Apifox调试后端接口。 我强烈建议即便有Android端,也不要跳过后端接口测试这一步。先确认每个接口在Postman里返回的数据结构和预期一致,再去找Android端的问题。这个习惯能省下大量联调时间里"前后端互相觉得是对方的锅"的无谓扯皮。
这里要分享一个实际经验:Windows上装Postman本身可能很慢,如果下载受阻,可以直接用IDEA自带的HTTP Client功能,新建一个.http文件,在里面写请求头和请求体,点击发送即可看响应。这个功能完全够用,不依赖外部安装。
使用Android Profiler分析卡顿。 Android Studio的自带工具怎么看主线程是否阻塞、内存是否存在泄漏、网络请求是否被阻塞,这些问题在Profiler里一目了然。毕业答辩时的"你做过性能优化吗"这类问题,完全可以用一段Profiler截图和优化前后响应时间对比的数据来回答,比空口讲"我写代码很规范"有说服力得多。
日志打印规范。 后端用Slf4j的Logger打印请求日志和SQL参数,Android端用Log.d加统一Tag。这里有一个建议:做好日志分级。正式演示时不要用Log.v把各种调试信息打满屏幕,什么都不方便看。更好的做法是写一个LogUtil工具,Debug级别时为真,Release打包时自动不输出,避免暴露敏感信息。
6. 拓展方向与个人实操体会
6.1 从毕设到可用产品的拓展思路
这套家教系统做完整后,往可落地方向拓展的空间其实不小。如果时间充裕,两个方向我觉得可以考虑。
消息推送改造。 目前的消息是请求时拉取列表,无法主动通知。接入一个推送服务(比如极光推送或者FCM在国内场景的替代方案)后,订单状态变化就能实时推到用户手机上。这个功能对真实家教平台来说几乎是刚需,因为家长希望第一时间知道有人接单,教员希望第一时间抢到优质需求。
支付与订单结算。 做成纯粹的线上流程,就需要接入支付功能。但毕业设计里我不建议真正接支付平台,因为涉及商户资质校验,非常麻烦。可以自己模拟一个虚拟钱包:家长充值虚拟币、下单时扣减、授课完成后结算给教员,所有逻辑自己实现,既能体现业务完整性,又规避了真实资金的合规问题。
多角色审核流程。 目前管理员对教员的审核只是简单的状态修改。可以升级为上传身份证照片、教学资质证明文件,管理员端进行人工审核。这套流程往线上运营产品靠拢时是核心环节,面试简历上写"设计了一套包含实名认证的教员准入体系",含金量会高很多。
6.2 做这类项目时我坚持的几条原则
说了这么多技术细节,最后聊几句实在的。我做过一个又一个管理系统类的实践项目,在这类“业务管理系统”里,坚持下列几个原则非常重要。
第一,优先保证业务闭环的完整,而不是功能的堆砌。 我见过不少Demo,把用户管理做成能填十几个字段的大表单,但订单核心流程还是断的,这其实是本末倒置的。一个能演示"注册-发布需求-下单-接单-评价"完整闭环的系统,远比一个看起来功能很多但跑不通的系统有价值。
第二,代码规范比代码量重要。 命名不要用拼音缩写,Controller层不要写业务逻辑,SQL不要塞进Activity里。这些规范在毕设评审时一眼就能看出项目是否用心。说白了,答辩老师不一定一行行读你的代码,但如果你表达清晰、结构规范、能准确说出设计理由,高分基本稳了。
第三,做好演练。 演示系统的整个流程要走通三遍以上,确认从登录到订单创建的每一步都正常。演示时手抖点错了、网络断了一下、某个页面加载慢了,这些事情都是在紧张状态下容易发生的。用真机连热点、把服务器提前启动好,这些小细节能在关键时刻救你一命,都是真金白银的教训换来的。
第四,别忘记备份。 开发过程中数据库换个表结构、依赖版本调一下,这些都是常态。本地的MySQL数据文件、Spring Boot的target目录、Android的build目录最好定期打包备份,做到万无一失。很多学生答辩前一天还在为找回数据库文件焦头烂额,非常影响节奏。
这套系统虽然是从毕业设计场景出发的,但如果你把思路理清、代码写规范了,它完全可以作为一块跳板。后续无论是继续深化技术栈、完善业务能力,还是直接把原型思路延伸到自己真正想做的产品上,都会非常顺手。反正做项目的过程中,把一个环节弄懂弄透,学到的都是自己的。
