
做毕设和课程设计这些年我接过不少类似的题其中“美容美发服务系统App”这种“springboot Android”的组合真的属于长盛不衰的经典款。乍一看这题目好像没什么新意但仔细拆一下前端要跑安卓App、后端要撑一套预约下单流程、中间还要处理图片、订单状态、会员余额这些麻烦事业务链路完整度其实相当高。很多同学拿到的源码、文档、讲解视频都是配套好的但真到了自己部署和答辩的时候要么卡在环境上要么对代码一知半解最后只能硬背PPT。这篇文章我不打算照着项目文档再念一遍功能清单而是从一个“拿到这套代码之后该怎么入手”的角度把springboot后端、Android客户端、数据库设计、调试过程和常见坑都捋一遍。内容会覆盖整包部署、核心模块解析、接口对接思路、真机联调技巧还有答辩时容易被追问的细节。不管你是打算二次开发还是只想顺利跑通然后写论文按这个顺序走一遍心里基本就有底了。1. 项目整体设计与思路拆解1.1 这个选题“稳”在哪里一个毕业设计或课程设计的好坏不只看技术新不新更要看业务逻辑能不能自圆其说。美容美发服务系统这个题目天然把用户、技师、门店、订单、支付、评价这些要素串在了一起几乎覆盖了一个小型交易系统的主干流程。对于需要快速出活的学生来说它不像“电商秒杀系统”那样对并发有要求也不像“推荐系统”那样需要跑算法难度曲线平缓但完整度高非常容易在答辩时把业务讲清楚。更重要的是这类题目能自然地划分出三种角色普通用户、门店/技师端、平台管理员。有了角色划分权限控制和接口设计就有话可讲论文里的用例图、时序图、ER图也都好画。评委和导师见过的题目很多对这种“麻雀虽小五脏俱全”的系统接受度很高不会追问太多高深的底层原理但会重视业务流程的闭合性。比如你预约了一个发型师订单状态怎么流转、取消后库存时段怎么释放、充值余额有没有消费流水这些比“我用到了Redis缓存”更能体现实际设计能力。1.2 技术选型背后的真实逻辑技术栈选的是springboot Android原生这个组合在众多选题里出现频率极高原因很实在springboot已经成为Java后端开发的事实标准哪怕不写复杂微服务单机就足够支撑这类预约系统Android端用原生语言开发不需要额外的跨端框架对学生来说可控性强打包也好演示。后端部分常规配置就是springboot 2.x MyBatis-Plus MySQL。MyBatis-Plus最近的几个版本对单表CRUD的简化非常明显BaseMapper自带增删改查省掉了一堆XML映射文件。这个项目如果是多表关联比如订单关联服务和技师还是需要用注解或XML写join。这里要提醒一下拿到源码后先看pom.xml确认数据库方言、分页插件、以及是否引入了druid连接池。很多同学启动失败都是因为默认的HikariCP和MySQL驱动版本不兼容或者时区配置没写。Android端请求框架选型常见的有三种HttpURLConnection、OkHttp、Retrofit。正规一点的源码都会封装Retrofit配合Gson解析统一返回体。如果不是很熟悉Retrofit至少要把OkHttp的拦截器看一遍因为登录token的添加就是在拦截器里完成的。图片加载十有八九是GlideGlide的缓存策略、占位图设置都是面试和答辩的加分点。整体来看这套技术选型没有一项是“炫技”但每一项都是当前Android开发的主流答案写成论文也不会被质疑。1.3 核心模块与数据库表怎么拆系统本质上围绕“预约”展开我在看这类项目源码时习惯先找订单表从订单表出发反推所有关联表。一个合理的预约流程是用户浏览门店和服务项目选定技师选择时间生成带状态的订单到店后由技师或管理员确认核销最后用户评价。对应到数据库最少要有这几张表表名核心职责关键字段user用户信息与身份id, phone, password, nickname, avatar, role, balancestore门店基本信息id, name, address, phone, cover, business_hoursservice_item服务项目id, store_id, name, price, duration, cover, descriptionbarber技师档案id, store_id, name, avatar, skill, level, introappointment预约订单id, user_id, barber_id, item_id, appoint_time, status, remarkcomment评价记录id, appointment_id, user_id, content, score, create_timerecharge_record充值/消费流水id, user_id, amount, type, balance_after, create_time拿到源码后先打开数据库脚本对照这套表结构理解一遍。如果发现缺表或者字段对不上多半是版本不一致要以最新sql文件为准。特别要注意status字段的取值比如0待接单、1已确认、2已完成、3已取消、4已评价这类状态定义一般写在代码常量类里改动时前后端要同步。2. 核心功能细节解析与实现要点2.1 注册登录与Token会话保持用户端登录是整个App的第一道门槛很多源码在这块的处理方式都不一样。简单项目会用手机号加密码直接查表登录成功后在内存或本地保存一个userId规范一些的项目会引入JWT后端签发tokenAndroid端收到后存储到SharedPreferences后续请求都带上这个token。后者的好处是后端接口无状态适合答辩时讲“会话保持”和“接口安全”。密码加密方面明文存储是大忌至少要使用MD5加盐或BCrypt。如果源码里直接compare(password, row.getPassword())且数据库存的是明文你可以在论文里强调自己做了改进改用Spring Security提供的BCryptPasswordEncoder加密。这个改进不需要改动太多代码只需要加密工具类和注册登录两处调用却能成为答辩亮点。如果要深挖可以顺带说明BCrypt自动加盐、计算成本可调的原理但点到为止就好。接口层面的权限控制往往通过一个HandlerInterceptor实现。preHandle里取出请求头中的token调用JwtUtil解析如果有效就放行无效就返回401。这里需要注意在线程内无法直接从拦截器取到用户对象很多项目会通过ThreadLocal传递用户信息拦截器解析完token后set进去Controller里用UserHolder.get()获取。这是实际工程里非常常见的写法如果你发现自己的源码没有这层设计可以考虑补上也顺带解决“订单记录里如何知道当前用户是谁”的问题。2.2 预约下单与时段防冲突预约是这个系统的核心业务也是最容易被评委追问有没有“认真考虑”的地方。很多简单实现里用户提交预约时只做一次“当前时间是否空闲”的判断然后直接插入订单。问题在于如果两个用户同时在选同一个技师的同一时段服务端没有加锁或唯一索引就可能出现超卖式的冲突。处理方案并不复杂核心是给预约增加“防重约束”。比如在appointment表上为(barber_id, appoint_time, status)建立一个唯一逻辑或者在创建订单前对指定技师和时段执行一次带条件的count查询并配合事务来控制。更稳妥的做法是先执行“UPDATE预约状态 SET status已确认 WHERE id? AND status待确认”通过affected rows数量判断当前订单是否被其他操作抢先类似乐观锁的思路。把这条逻辑写在论文的“并发控制”小节里说服力会强很多。订单状态是另一种容易出问题的地方。我见过不少源码用户取消订单只是改变本地状态但没有处理后续的时段释放。严谨的流程里取消操作要唤醒对应的时段状态并且如果项目做了技师排班还要释放排班占用。这部分要结合代码逐行看状态机流转是否完整取消订单时是否回调了releaseBarberSlot之类的方法前端是否有对应的按钮置灰逻辑。如果能把这个闭环讲明白答辩基本就稳了。2.3 会员卡与余额流水美容美发业务里会员卡充值几乎是标配也因此这类系统十有八九带余额支付和充值功能。余额字段放在user表里是很直接的设计但只有余额没有流水会在对账时说不清楚。合理做法是单独建一张recharge_record表type字段区分充值、消费、退款、赠送每次余额变动就插入一条流水余额本身可以用balance字段冗余存储查询时以流水表为准做对账。这里有一个工程细节扣减余额的接口必须是事务性的。如果先更新余额再插入流水中途抛异常会导致两边不一致反之先插流水再更新余额也存在更新失败的风险。正确做法是使用Transactional注解把余额扣减和流水记录放在同一方法内并确保异常能触发回滚。如果源码里没有事务控制这也是一个值得写进“系统改进”章节的点。Android端支付通常不会接真实微信支付宝而是模拟支付。大多数项目就是充值页输入金额后直接调用后端的recharge接口。注意讲解时要把这个说清楚这是教学演示项目不是商用的支付系统。答辩时如果被问“真的对接微信支付怎么做”你可以简单说需要申请商户号、生成支付参数、回调验签并补充说自己查阅过相关文档比直接说“没做过”要好得多。2.4 管理端该有的样子管理端在不少项目里会被简化成一个藏在App里的管理员标签页或者干脆用电脑浏览器访问一个简单的Web管理页面。从技术上看springboot天然支持同时输出REST接口和页面模板所以管理端做成Web方式并不难。如果是纯App内管理那就要考虑登录角色区分管理员登录后跳到一个不同的Fragment集这部分代码通常写在MainActivity的role判断里。管理功能主要有四块门店管理、服务项目管理、技师管理、订单管理。门店和服务项目一般就是增删改查配一个图片上传订单管理重点关注按状态筛选和手动确认核销技师管理里可能会包含排班表排班表如果做得简单就用日期字符串格式存储。拿到源码后我建议先把管理端接口梳理一遍画一张“接口-功能”对照表方便论文里画系统功能结构图也方便理解后端Controller层到底写了哪些接口。3. 实操过程与核心环节实现3.1 环境准备与项目结构速览做这类项目最怕的就是环境不一致。建议先统一版本JDK用1.8或11Maven用3.6以上MySQL用5.7或8.0Android Studio建议用较新的稳定版SDK版本根据项目minSdk和targetSdk配置。整个项目是前后端分离的后端是springboot工程前端是Android工程两者要分开导入IDE。后端导入时用IDEA打开pom.xml所在目录等待依赖下载如果公司或学校网络慢可以配置阿里云镜像。Android工程用Android Studio导入这里要注意Gradle版本与JDK版本的匹配Gradle 7.x需要JDK11如果你本机是JDK8启动时会有报错。项目根目录下一般有README或者部署文档先阅读这部分确认数据库脚本位置和默认端口。一个典型的springboot项目目录结构是这样的src/main/java ├─ com.example.beauty │ ├─ controller // 接口层 │ ├─ service // 业务层 │ ├─ mapper // 数据访问层 │ ├─ entity // 实体类 │ ├─ config // 配置类 │ ├─ interceptor // 拦截器 │ └─ utils // 工具类 src/main/resources ├─ application.yml ├─ mapper // MyBatis XML └─ db.sql要注意的是不同源码的包名不太一样有的叫com.example.demo有的叫com.beauty.app。不统一没关系按功能模块去识别即可。Android端目录则按Activity、Adapter、Fragment、utils、api包区分先看api包里的RetrofitService或OkHttpUtil你就能找到所有后端接口的调用点。3.2 后端接口落地示例以“查询门店列表”为例一个标准接口从下到上是这样的。Controller层RestController RequestMapping(/api/store) public class StoreController { Autowired private StoreService storeService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { return Result.success(storeService.pageStore(page, size)); } }Service层Service public class StoreService { Autowired private StoreMapper storeMapper; public IPageStore pageStore(Integer page, Integer size) { PageStore p new Page(page, size); return storeMapper.selectPage(p, null); } }如果你用的是MyBatis-PlusMapper层几乎不用写任何SQLMapper public interface StoreMapper extends BaseMapperStore { }这里的Result是统一返回体一般包含code、message、data三个字段。建议你把Result类单独看一遍因为Android端解析数据的格式完全取决于这个类。有的项目用code200表示成功有的用code0还有的直接返回boolean解析前一定要对应好否则会出现“列表加载不出来但后台日志没有报错”的情况。3.3 Android端的请求封装Android端请求封装的教科书写法是这样的定义API接口加Retrofit注解然后通过单例获取服务对象。举个例子public interface ApiService { GET(api/store/list) CallResultListStore getStoreList(Query(page) int page, Query(size) int size); POST(api/user/login) CallResultLoginResponse login(Body LoginRequest request); }Retrofit单例的创建public class ApiClient { private static final String BASE_URL http://10.0.2.2:8080/; private static ApiService apiService; public static ApiService getInstance() { if (apiService null) { synchronized (ApiClient.class) { if (apiService null) { Retrofit retrofit new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); apiService retrofit.create(ApiService.class); } } } return apiService; } }注意BASE_URL这个地方如果用的是模拟器后端跑在本机要用10.0.2.2指向宿主机如果用的是真机要改成电脑在局域网里的IP比如192.168.1.100并且要保证手机和电脑连接的是同一个Wi-Fi。很多同学第一次调试时后端接口在浏览器里能打开App里却总是超时多半就是IP写错了。Gson在解析时如果实体类的字段名和后端返回的JSON不一致可以使用SerializedName注解做映射。这个细节在项目里很常见因为后端习惯用下划线命名而Android端习惯用驼峰比如mobile和userMobile。当发现某个字段解析出来是null时先检查是不是忘记加注解了。3.4 图片上传与静态资源访问美容美发系统的门店相册、服务项目图片、用户头像都涉及图片上传。后端的经典做法是把文件保存到服务器某个目录同时把访问路径存入数据库。在application.yml里配置静态资源映射比如spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MBConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: System.getProperty(user.dir) /upload/); } }Android端用Glide加载图片时要注意URL拼接。如果后端返回的是相对路径“/upload/1.jpg”前端需要拼上完整的服务器地址。比较规范的做法是在统一Response解析时对图片字段做处理或者干脆在后端直接返回完整URL。调试时常见的问题是图片加载404原因要么是相对路径没拼对要么是磁盘路径跟代码里的路径不一致。3.5 后端与Android端的调试方法调试是整个项目最耗时间的环节但也是有套路可循的。后端调试优先使用断点在IDEA里点击方法左侧行号打断点以debug模式启动请求进来后会停在断点处可以逐行查看变量。如果项目里使用了MyBatis-Plus可以在application.yml中打开SQL日志mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会打印每一条执行的SQL排查“查不到数据”“字段对不上”这类问题非常高效。Android端调试首先看Logcat在代码里使用Log.d(TAG, message)打日志。数据解析失败时把原始JSON字符串先打印出来确认后端到底返回了什么。也可以用断点调试Android代码比如在onResponse回调里打断点直接看response.body()的原始内容。接口请求层面的问题我习惯在电脑上用抓包工具或者直接在OkHttp中加日志拦截器。OkHttp的拦截器非常方便HttpLoggingInterceptor logging new HttpLoggingInterceptor(); logging.setLevel(HttpLoggingInterceptor.Level.BODY);加上以后每个请求的URL、请求头、请求体、响应体都会完整打印在Logcat里90%的接口问题靠它能定位到。4. 常见问题与排查技巧实录4.1 端口被占用与启动失败springboot默认端口是8080如果本机已经有服务占用了8080就会启动失败。报错一般长这样Port 8080 was already in use。解决办法有几种直接改application.yml里的server.port换成8081、8082等或者在终端里查一下是哪个进程占用了端口。Windows下用netstat -ano | findstr 8080 taskkill /pid 对应PID /F还有一类启动失败跟数据库连接有关报错包含Communications link failure或者Access denied。前者检查MySQL服务是否启动、端口是否是默认的3306、地址是否写对后者检查账号密码和授权MySQL 8.0用户需要确认是否开启了mysql_native_password插件。很多macOS版本的MySQL默认只允许root在本地登录连接时记得在URL里加上useSSLfalse和serverTimezoneAsia/Shanghai否则时区报错会让人一头雾水。4.2 Android网络权限与明文流量问题App调接口报错时千万不要无脑怀疑代码先检查AndroidManifest.xml里有没有加网络权限uses-permission android:nameandroid.permission.INTERNET /新版Android对明文HTTP的限制非常严格targetSdkVersion如果大于等于28使用http://地址会被系统直接拦掉报错一般是Cleartext HTTP traffic not permitted。最简单的处理方式是在AndroidManifest.xml的application节点上加上android:usesCleartextTraffictrue更优雅的方式是配置networkSecurityConfig允许特定域名的明文流量。答辩时建议说后者因为能体现你对Android 9网络安全的了解。另一个容易被忽略的点是如果App里用到了定位或者读取相册还要检查对应权限和动态权限申请的代码。如果源码里写的是targetSdkVersion 28以下的逻辑在高版本手机上要单独适配运行时权限。4.3 模拟器与真机访问后端差异模拟器访问宿主机用10.0.2.2但10.0.2.2只对Android Studio自带的模拟器有效。如果你用第三方模拟器比如雷神模拟器或夜神模拟器它们通常把宿主机地址映射成了10.0.3.2或者别的地址配置方式都不一样网上很多教程在10.0.2.2这点上说得很笼统容易踩坑。最省事的办法还是用真机。手机和电脑连同一个Wi-Fi后端启动后把BASE_URL改成电脑的局域网IP手机浏览器先访问一下如果能看到JSON返回说明网络通。但注意笔记本或台式机如果开了防火墙手机可能无法访问8080端口需要在防火墙入站规则里放行对应端口。如果实在不方便真机也可以不依赖模拟器地址映射而是使用USB连接后执行adb reverse tcp:8080 tcp:8080然后再把BASE_URL改为http://localhost:8080/这样手机请求就会通过USB隧道转发到电脑的8080端口速度比Wi-Fi还快。4.4 数据库乱码与字段对不上中文乱码绝大多数问题出在数据库编码。建库的时候要指定utf8mb4CREATE DATABASE beauty CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;同时检查connection URL里有没有characterEncodingutf8。另外MySQL 8.0与5.7的驱动类名也不一样5.7用com.mysql.jdbc.Driver8.0用com.mysql.cj.jdbc.Driver。如果项目里用的是旧驱动换成新驱动后DataSource会自动识别但最好手动确认一下。“字段对不上”一般是实体类属性与表字段命名不一致导致的。用MyBatis-Plus时默认开启驼峰转下划线所以实体类里写storeName对应数据库字段store_name才能正确映射。如果看到某个字段怎么查都是null要么是resultMap没配要么是表里没这个字段先拿SQL在Navicat里跑一遍再回代码里找问题。4.5 启动后页面能打开但接口404后端能启动但Android端请求一直404这种现象很常见。先分清是Controller路径问题还是请求方式问题。Postman或浏览器里直接访问接口确认接口本身是好的。如果浏览器通了App不通检查Retrofit里的GET、POST注解与后端RequestMapping路径是否完全一致大小写、斜杠都要严格对应。另一个原因是前端请求带了特殊参数比如Content-Type。如果Android端用Body传对象Retrofit默认是application/json如果后端使用RequestParam接收就需要用Query或form表单方式。接口签名对不上后端甚至不会进入Controller方法就直接返回404和500。遇到这类问题用OkHttp日志拦截器打印请求体和后端接收的参数名逐一比对一般几分钟就能定位。5. 文档与讲解视频的使用思路5.1 把项目文档改造成自己的拿到配套的Word文档或PDF不建议直接改个名字就提交。导师通常看得多一眼就能认出是不是模板。但也没必要全盘重写我的经验是框架和图表可以借鉴业务流程、数据库设计、代码说明、测试结果这四部分必须结合源码重新组织。先把系统划分为用户模块、门店模块、预约模块、支付模块、评论模块然后逐块从源码中找到对应的Controller和Service截图关键代码并配上自己的注释。论文里的功能结构图不要全平台照搬至少要把图表重画一遍配色可以换掉模块命名也能微调。需求分析部分要结合自己部署时遇到的问题补充非功能性需求比如“系统应支持同一时段预约互斥处理”“图片上传后支持在线预览”。测试截图用自己运行起来后的项目跑一遍把App里实际的效果图替换进文档这样整体可信度会高很多。5.2 讲解视频与答辩准备讲解类的视频一般有两类一类是项目演示另一类是代码讲解。演示视频里会操作App完成注册、登录、预约、支付、查看订单等流程这部分先看两遍然后自己在模拟器或真机上重复做几遍确保能顺手演示。代码讲解部分视频里往往会跳到某个类或者某个方法去解释建议按照视频的思路把对应代码位置标注出来形成一套自问自答的讲稿。答辩时最常被问的几个问题提前准备就不会慌常见问题推荐回答思路为什么选择springboot简化配置、内嵌Tomcat、微服务社区成熟适合中小型项目快速开发。用户登录怎么保证安全密码BCrypt加密登录后签发JWT请求在拦截器中校验token。如何防止同一技师同一时段重复预约事务控制 预约状态乐观锁更新同时保证更新后释放时段的原子性。余额充值是怎么设计的事务中同时完成余额更新和流水插入查询时以流水表为对账依据。Android端如何向后端传token在OkHttp拦截器中添加请求头Authorization: Bearer {token}。这里多说一句回答问题不用讲得特别长但要把原理和实现细节带到。比如问JWT时顺便说说为什么不把用户信息全部放token里因为token一旦泄露内容就能被解出来所以只保存userId和过期时间需要用户信息时再查库。这种细节会让人觉得你真的动手改过代码而不是只会读PPT。5.3 二次开发可以从哪下手如果你想在原有基础上做一些差异化功能我建议优先从以下三个方向入手。第一是在预约模块加上简单的时间段选择控件后端生成当天可预约的时段列表前端用RecyclerView展示看起来就比固定时间输入高级很多。第二是数据可视化后端增加统计接口统计最近一周、一个月各门店的预约量前端用MPAndroidChart画柱状图或折线图这也是很多论文加分的点。第三是消息推送用极光推送或Firebase Cloud Messaging实现预约成功后的通知虽然接入成本稍高但效果非常直观。这三个方向都不需要推翻现有架构只是在接口和页面层面做加法。改完了以后配合Scrum的方式记录迭代过程把每次提交后面的动机写清楚放在论文“系统实现”章节里这样整篇论文会有真实的努力痕迹比堆砌技术名词扎实得多。个人实操中的后话这套项目我从拿到手到跑通花了大概一个周末。第一次启动报错多半是MySQL版本和驱动不一致Android端用第三方模拟器卡在IP地址上调好以后整个流程跑下来也就半小时。如果你也是刚拿到这类源码我的建议是别急着改代码先把环境跑通对照数据库表把核心流程的代码读一遍再想想要加什么功能。项目本身不难但它是一套能完整走起来的前后端工程把每一层打通的感觉比任何教程都直观得多。后面不管是继续加功能还是拿去面试这台地基都很值得花时间打牢。