最近被好几个做毕业设计的同学问到同一个项目方向Spring Boot Android的家教管理系统。问的人多了我干脆把整个项目的设计思路和实现过程完整梳理一遍。这个题目乍看是一个后台管理系统 一个App但真正动手才会发现核心难点根本不在CRUD而在于家教业务本身的特殊性——多角色权限、时间匹配、订单状态流转、还要兼顾Android端的离线与弱网场景。这篇文章我会从架构选型、数据库设计、Android端实现、服务端核心逻辑、联调踩坑这几个维度把这个项目完整拆开尽量做到你照着写就能少走弯路。这个项目适合两类人一类是需要完成毕业设计的在校同学可以作为系统设计与实现的参考蓝本另一类是自己想接私活或做开源项目的开发者这套业务模型稍作扩展就能落地到健身私教、乐器陪练、上门美容等预约类服务场景。文章里所有代码都基于Spring Boot 2.x MyBatis Plus MySQL 5.7 Android原生Java这也是目前性价比最高、参考资料最多的组合。1. 选型阶段为什么要用Spring Boot接Android而不是其他组合1.1 技术栈选择的现实逻辑很多同学一上来就纠结后端用SSH还是Spring BootAndroid用原生还是Flutter我的建议很直接——毕业设计优先考虑能讲清楚原理 资料好查 演示效果直观而不是一味追求新潮。Spring Boot Android原生这套组合正好满足这三点。先说后端。Spring Boot对比传统SSHSpring MVC Hibernate Struts最大的优势是简化了配置。SSH时代写一个XML配置要半天Spring Boot用注解 自动配置就能搞定。对于家教管理系统这种业务不算复杂、但涉及多模块交互的项目Spring Boot的起步依赖、内嵌Tomcat、统一异常处理这些特性都很实用。再说Android端。选择原生而不是Flutter或React Native核心原因是毕业设计评审老师更看重你真正理解了移动端开发流程。原生开发能清晰展示Activity/Fragment生命周期、Adapter列表渲染、网络请求封装、本地缓存这些基本功答辩时每个环节你都能讲出设计理由。而且原生Java代码和JS/TS/Go等语言的知识体系更贴近国内课程体系自己排查问题也更容易。1.2 整体架构单体应用还是前后端分离家教管理系统我建议采用**服务端单体 Android原生客户端**的架构模式而不是拆微服务。原因很简单这个项目的核心是业务逻辑闭环验证不是高并发。拆微服务反而会让事务一致性、接口调用链变得复杂对毕业设计来说属于给自己挖坑。实际的架构分三层表现层Android端App负责信息展示、用户操作、本地缓存业务层Spring Boot工程按模块分包controller / service / mapper / entity提供RESTful JSON接口数据层MySQL数据库存储用户、教员、订单、评价、课时记录等核心数据服务端只对Android端暴露JSON接口不做页面渲染。这样设计的好处是Android端和服务端可以并行开发只需要提前约定接口文档调试时可以先用Postman跑通后端再对接App端问题定位非常清晰。有一个细节建议即便不做前后端分离的Web管理后台也可以在服务端顺手加入Spring Security做接口鉴权在后续测试中会非常有用。下面这张表是各层技术职责的参考层级技术选型职责范围Android UIXML布局 RecyclerView列表展示、表单交互、页面跳转Android网络Retrofit OkHttp接口请求、响应拦截、Token注入Android本地SharedPreferences / SQLite登录态缓存、搜索历史记录服务端应用Spring Boot 2.xREST接口、业务逻辑、参数校验ORMMyBatis Plus单表CRUD、条件构造器权限认证JWTJSON Web Token用户登录态签发与校验数据库MySQL 5.7 Navicat业务数据持久化2. 家教业务的数据结构设计从用户角色到课时流转2.1 核心角色模型用户、家长、教员、管理员家教管理系统绝不是简单的用户表 订单表它最核心的特征是多角色模型。家长要找教员教员要接单管理员要审核教员资质和课程信息这三类动作对应的是三种不同的数据视图。设计时我建议用一张user表加上role字段而不是分三张表因为登录和账号体系天然是统一的只是业务操作范围不同。我的用户表设计要点如下id主键自增role角色标识1家长、2教员、3管理员username/password登录凭证密码必须BCrypt加密存储phone手机号作为家长联系教员的重要通道avatar、nickname基础资料status账号状态禁用/启用create_time、update_time审计字段需要特别提醒的是password字段千万别用MD5明文存储答辩时一旦被问到安全设计BCrypt是加分项。理由很简单BCrypt自带随机盐相同密码每次加密结果不同能有效抵抗彩虹表攻击。2.2 订单与课时家教业务的两条核心链路家教业务里最容易想当然的坑是把订单设计成一锤子买卖。实际上家教服务是按课时消耗的家长先购买课时包然后每次上课扣减课时这个模型必须从表设计层面就确定下来否则后续退款、续费、结算会很痛苦。我的核心表结构设计如下teacher_info教员信息表扩展用户表包含教学科目、授课年级、教龄、期望时薪、教资认证状态、自我介绍course课程表教员发布的课程含科目、适合年级、课程名称、课时数、总价、课程封面order订单表家长购买课程的订单含订单号、家长id、课程id、总价、支付状态、订单状态class_hour_record课时记录表每次上课的记录含订单id、教员id、家长id、上课日期、开始时间、时长、状态待确认/已完成/已取消appointment约课表家长和教员约课的时间表含具体时间槽、状态待确认/已确认/已完成/已过期review评价表家长对教员的评价含评分、内容、匿名标识那这个两条链路如何理解第一条是购买链家长浏览课程 - 下单支付 - 系统生成订单 - 订单变成待上课状态。第二条是消耗链家长和教员约课 - 上课 - 课时记录生成 - 状态变更为已完成 - 课时扣减 - 若所有课时用完订单状态变成已完成。清晰区分这两条链路是后面写业务逻辑的基石。很多同学把约课和购买混在一个表里到期末才发现统计每个教员的课时收入和统计家长剩余课时数根本没法算就是因为数据模型没有独立拆分。2.3 数据库索引与唯一性约束的细节表结构设计好了还得注意数据库层的约束。这里强调三个高频扯皮问题第一手机号字段要加唯一索引。用户注册时必须校验手机号是否已存在防止一个手机号注册多个账号。第二约课表要加唯一约束teacher_id course_id start_time防止家长重复预约同一个时间槽。第三课时记录表的状态字段建议用tinyint0待确认 / 1已完成 / 2已取消比varchar省空间且利于索引。到这里数据库层面的核心设计就完成了。我习惯在写代码之前先用Navicat把表全部建好再用MyBatis Plus的代码生成器生成entity/mapper/service基础代码省时省力。3. Android端实现网络层、登录态与家长端主流程3.1 项目分包与MVVM决策Android端的包结构决定了后期扩展的顺畅程度。虽然Java原生开发不强制MVVM但我强烈建议至少把数据层网络、缓存和UI层Activity、Adapter分开否则所有代码堆在Activity里几千行不是梦。一个清晰的包结构示例com.example.tutorapp ├── api/ // Retrofit接口定义与网络Client单例 ├── model/ // 数据模型与后端entity对应 ├── ui/ // Activity Adapter │ ├── login/ │ ├── home/ │ ├── course/ │ ├── order/ │ └── mine/ ├── utils/ // 时间格式化、加密、校验工具 └── cache/ // SharedPreferences封装为什么我不追求完整的MVVM比如ViewModel LiveData因为项目规模就三个核心页面流引入过多架构层反而增加理解成本。毕业设计讲清楚网络层如何封装列表如何加载状态如何刷新就已经能拿分了。3.2 网络层封装Retrofit OkHttp Token注入网络层是Android端最重要的一块因为它是连接App与后端的唯一通道。核心需求有三个BaseUrl管理、Token自动注入、统一数据解析。先用Retrofit定义接口比如用户登录、获取课程列表、创建订单public interface ApiService { POST(api/user/login) CallApiResponseLoginBean login(Body LoginRequest request); GET(api/course/list) CallApiResponseListCourseBean getCourseList( Query(page) int page, Query(size) int size, Query(subject) String subject ); POST(api/order/create) CallApiResponseOrderBean createOrder(Body OrderCreateRequest request); }然后封装一个统一的ApiClient单例public class ApiClient { private static final String BASE_URL http://你的服务器IP:8080/; private static ApiService apiService; public static ApiService getApiService() { if (apiService null) { OkHttpClient client new OkHttpClient.Builder() .addInterceptor(new TokenInterceptor()) // 注入JWT .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); Retrofit retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); apiService retrofit.create(ApiService.class); } return apiService; } }TokenInterceptor是关键它的作用是在每次请求Header中自动加上Authorization字段。这样业务代码只需要关心业务参数不用重复写Tokenpublic class TokenInterceptor implements Interceptor { Override public Response intercept(Chain chain) throws IOException { Request original chain.request(); String token TokenCache.getToken(); if (token ! null !token.isEmpty()) { Request newRequest original.newBuilder() .header(Authorization, Bearer token) .build(); return chain.proceed(newRequest); } return chain.proceed(original); } }注意BASE_URL如果是局域网IP使用Android模拟器访问本机服务端时不能用localhost要用10.0.2.2。真实手机连接时需要保证手机和服务器在同一网段并且服务端要开启防火墙端口。这一条就够踩半天坑的后面章节我会单独说。3.3 登录态管理Token存储、自动登录与退出登录态是移动端体验的命脉。我的实现思路是登录成功后服务端返回JWTAndroid端写入SharedPreferences后续所有网络请求由拦截器自动携带启动App时先检查本地是否存有Token有就直接进主页没有则跳登录页。本地缓存封装建议写成工具类方便全局访问public class TokenCache { private static final String PREFS_NAME tutor_prefs; private static final String KEY_TOKEN token; private static final String KEY_USER_ROLE user_role; public static void saveLoginInfo(Context context, String token, int role) { SharedPreferences sp context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); sp.edit().putString(KEY_TOKEN, token) .putInt(KEY_USER_ROLE, role) .apply(); } public static String getToken(Context context) { SharedPreferences sp context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); return sp.getString(KEY_TOKEN, ); } public static void clear(Context context) { SharedPreferences sp context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); sp.edit().clear().apply(); } }这里有一个细节容易漏当用户角色是教员时登录后进入的首页应该不一样。家长端看到的是找老师课程列表我的课时教员端看到的是我的课程待接单收入统计。所以登录后不能只跳到同一个主页要根据role字段做分支跳转最好再配合SplashActivity做一个统一入口判断。3.4 列表加载RecyclerView 下拉刷新 分页家教App里最常见的页面就是课程列表、教员列表、订单列表这些都是典型的列表页。Android端标准做法是RecyclerView 下拉刷新SwipeRefreshLayout 分页加载。具体实现流程页面进入时请求第一页数据page1, size10渲染到RecyclerView的Adapter中用户上拉到底部时page自增加载下一页若返回数据不足一页则标记没有更多了停止加载有一个很实用的封装技巧Adapter里维护一个ListT dataList再提供一个addAll(ListT newData)方法第一个页面直接setNewData后续页面用addAll追加数据。同时用isLoading标志避免用户快速上滑时重复触发请求。这里不贴完整代码了核心是明白什么时候请求下一页比怎么写Adapter更关键——正确时机是最后一个item出现时// 在onBindViewHolder中判断 if (position dataList.size() - 1) { loadMore(); // 触发分页回调 }4. 服务端核心逻辑家长找老师、约课、结算这条链路4.1 登录注册与JWT鉴权设计服务端第一件事是搭好登录注册与鉴权体系。我用的是JWTJSON Web Token方案无状态、不需要在服务端存Session非常适合移动端场景。核心流程用户提交用户名密码服务端校验密码是否正确在校验通过后用jwt工具类生成Token包含userId和role将Token返回给Android端之后每次请求由拦截器解析Token并放行或拒绝生成Token的典型代码public String generateToken(Integer userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }同时需要一个拦截器或过滤器在请求进入Controller之前校验Token校验失败统一返回401。注意一点JWT的secretKey必须写在配置文件中不能硬编码包括密码加密的BCrypt盐也最好不要全局固定。这不是什么高深技术但很多同学的代码里直接写一串字符串答辩时被问配置分离就卡住了。4.2 家长端核心接口课程搜索与教员筛选家长端最重要的交互是找合适的家教。简单的做法是提供一个课程列表接口支持按科目、年级、价格区间、教员教龄筛选。用MyBatis Plus的条件构造器可以非常方便地实现动态查询Override public PageResultCourseVO searchCourses(int page, int size, String subject, Integer grade) { LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(subject)) { wrapper.eq(Course::getSubject, subject); } if (grade ! null) { wrapper.eq(Course::getGrade, grade); } wrapper.orderByDesc(Course::getCreateTime); PageCourse coursePage courseMapper.selectPage(new Page(page, size), wrapper); // 转VO补充教员昵称、头像、评分等冗余展示字段 ... }字段级查询走LambdaQueryWrapper是MyBatis Plus的核心优势代码简洁且避免SQL注入风险。想挑战复杂查询的话也可以在XML里写resultMap做表关联查询但对这个项目来说能用简单逻辑就不上复杂方案。4.3 订单状态机与课时扣减逻辑在前面我说过订单和课时要分开设计。现在实现订单创建、支付回调、课时扣减这三块业务逻辑。订单创建接口的输入是家长id、课程id输出是订单号。逻辑如下校验课程是否存在且状态为上架校验家长账号状态正常创建订单状态为待支付生成唯一订单号规则可以用时间戳 随机数避免简单自增id暴露单量支付功能在模拟项目里通常不接入真实支付渠道。我的做法是设计一个模拟支付接口前端调用后直接将订单状态置为已支付同时在订单表记录支付时间。这样可以完整演示业务闭环而不需要真实商户号。课时扣减是这里最容易出错的地方。家长购买课时包后每次上课完成应扣减剩余课时数。扣减逻辑必须使用数据库乐观锁或加锁防止并发下超扣。我建议在订单表中设计一个remaining_hours字段更新时用带条件的SQLUPDATE t_order SET remaining_hours remaining_hours - 1 WHERE id #{orderId} AND remaining_hours 0;这样写简单且安全remaining_hours 0这个条件保证了不会扣成负数。如果更新影响行数为0说明课时不足或订单异常服务端直接抛业务异常提示剩余课时不足。4.4 约课时间的冲突校验约课是教务系统里另一个高频难点。一个教员可能同时被多位家长约课数据库层面尽管有唯一约束兜底但业务层面仍要主动校验时间冲突。约课表字段我设计为teacher_id、course_id、appoint_date、start_time、end_time。家长发起约课请求时服务端需要查询int conflictCount appointmentMapper.selectCount( new LambdaQueryWrapperAppointment() .eq(Appointment::getTeacherId, teacherId) .eq(Appointment::getAppointDate, date) .eq(Appointment::getStatus, 1) // 已确认 .and(w - w .le(Appointment::getStartTime, endTime) .ge(Appointment::getEndTime, startTime) ) );这段逻辑本质是判断两个时间区间是否有交集。开始时间小于等于对方结束时间且结束时间大于等于对方开始时间即为冲突。这是日历类业务最常见的逻辑值得理解通透因为家教之外的会议室预约、诊室排班都是一模一样的套路。4.5 教员端课程发布与收入统计教员端的主要操作是发布课程、确认约课、查看自己的课时收入。发布课程接口要写好校验逻辑科目不能为空、课时数必须大于0、价格区间要合理。课时收入统计是答辩时容易被追问的点它不能简单地把所有订单金额加起来而要按已完成课时计算收入 已完成课时数 × 单课时结算价单课时结算价 课程总价 × 平台分成比例 / 总课时数平台分成比例这个逻辑在数据库里用系统配置表存默认值比如0.8表示教员拿80%。这样设计体现你对真实业务场景的理解答辩时能讲出这笔钱不是瞬间到账是跟着课时消耗走的就算过关了。5. 联调阶段实测权限、时间、支付这三个高频翻车点5.1 前后端联调的环境配置到了联调阶段以前写好的代码往往会暴露出一堆问题。先整理一下环境配置Android端模拟器访问电脑本机后端时BaseUrl使用http://10.0.2.2:8080/真机调试时换成电脑局域网IP且要确保电脑防火墙允许8080端口入站。服务端MySQL的时区配置也要注意建议连接URL加上serverTimezoneAsia/Shanghai否则时间字段会差8小时约课功能会莫名其妙出bug。5.2 权限问题接口未鉴权导致的越权操作联调中第一个常见问题是前后端接口对接时发现未登录也能访问部分接口。原因往往是拦截器只拦截了login接口之外的请求但没有排除一些公共接口比如课程列表、教员公开信息。正确做法维护一个白名单列表。公开接口放行其余接口均需校验Token。在Spring Boot里可以用拦截器实现也可以在需要免鉴权的接口上加自定义PassToken注解再在拦截器里读取注解判断。我个人偏好注解方案因为可以精确控制到接口级别。5.3 时间问题前端传时间容易踩的三个坑时间段约课功能很依赖时间传递。联调时最容易遇到的坑有三个。第一个坑是时区不一致。后端接收的日期时间如果带时区偏移与MySQL的datetime类型互转时就可能出现8小时偏差。建议前端统一传yyyy-MM-dd HH:mm:ss字符串后端用DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)接收存入MySQL时保持字符串格式避免使用Date类型反复转来转去。第二个坑是前端展示状态与后端不一致。约课状态为待确认时前端应该展示待教员确认不要让用户看到英文枚举或数字。可以在服务端VO中直接组合好状态文本返回前端只做展示不做翻译。第三个坑是过期约课的处理。设计了约课功能却不处理过期未确认状态会导致用户看到永远待确认的脏数据。解决方案是提供定时任务或懒处理当教员或家长查看约课列表时将当前时间小于约课时间且状态仍为待确认的记录批量更新为已过期。毕业设计场景用后端的Scheduled定时任务每10分钟执行一次代码简单、演示效果也不错。5.4 支付环节模拟支付的回调处理因为没有真实支付渠道模拟支付接口的流程是Android端点击模拟支付按钮 - 服务端创建支付流水记录 - 将订单状态改为已支付 - 通知Android端刷新订单状态。这个过程中的关键点是幂等性同样的支付请求不能重复改变订单状态。我的做法是在订单表增加pay_status字段支付回调里先判断如果已经是已支付状态直接返回成功不再更新。等于用状态位天然实现了幂等。界面上的反馈也要跟上支付成功后Toast弹出支付成功并跳转到我的课时页面让用户直观看到课时数已到账。5.5 专项自测用一个月跑完家长和教员两条完整流程系统是否可交付最终要去跑一遍完整业务流程。我会建议你至少做以下几组专项测试家长注册 - 登录 - 搜索课程 - 查看教员详情 - 下单 - 模拟支付 - 查看我的课时教员发布课程 - 家长约课 - 教员确认约课 - 上课完成 - 扣课时 - 教员查看收入管理员登录 - 审核教员资质 - 下架违规课程 - 查看平台数据统计每组测试过程中用表格记录测试时间、测试步骤、预期结果、实际结果、是否通过。这不仅是为了自测还是毕业设计答辩时的有力佐证材料。用真实数据说话远比你现场演示更强。我在自己的项目里就是这样做的测试用例文档最后成了设计说明书的重要组成部分。6. 复盘与后续扩展这套系统的可复用价值在哪写完这个项目后回头看家教管理系统最大的收获其实不是某个接口的写法而是学会了如何拆解一个多角色业务系统。用户、角色、权限、订单、课时、时间槽这些概念在电商、教育、医疗、健身等很多行业都能通用数据库表设计、状态流转思路可以直接迁移。如果时间允许这个项目有三处很自然的扩展方向。第一个是Web管理后台服务端接口已经是RESTful风格直接用Vue或Thymeleaf套一套就能做出管理后台页面。第二个是消息通知可以在约课确认、上课提醒、课时不足等节点引入WebSocket或极光推送提升用户体验。第三个是评分体系目前评价表只有评分和文本可以扩展为多维评分教学态度、专业水平、沟通能力丰富数据可视化的维度。最后说一个心得很多同学喜欢把项目做得花里胡哨界面加了一堆动画布局也很炫但一到业务逻辑环节就经不起追问。我个人的建议是把80%精力放在业务闭环的严谨性上界面以清晰可用为主。这个项目不管谁来做答辩属于稳的类型。如果正在做类似题目建议先把数据库表建好再按注册登录 - 课程列表 - 下单支付 - 约课上课 - 课时结算这条主链路逐步推进把每一步的输入输出调通再回头补管理员端和辅助功能。主线通了项目真正难的部分就过去了一大半。