☰
SpringBoot+Android打造美容美发预约系统:全栈开发与避坑指南
2026/10/13 4:09:43 网站建设 项目流程

1. 为什么我会选"美容美发服务预约"当练手项目

这两年接到的项目请求里,美容美发类的管理系统App出现频率一直很高。很多同学找到我时给的题目描述都差不多:SpringBoot后端、Android客户端、在线预约、服务项目管理、登录注册,最好再带个评价功能。说实话,最初我也有点疑惑——一个美发美容预约系统,不就是一个普通的CRUD业务吗?为什么每年都有人选它,而且做完之后普遍反映"比想象中麻烦"?

真正动手之后我才明白,这个题目其实是练全栈能力的黄金样本。它业务不复杂,但覆盖了你以后工作中大概率会遇到的一整条链路:用户端注册登录、分类展示服务项目、按门店和技师维度查档期、提交预约、处理订单状态、发表评价,再到后台管理所有基础数据。你不需要像电商系统那样跟支付库存死磕,也不必像社交App那样考虑消息推送和好友关系,核心逻辑刚好卡在"一个人人都看得懂、但处处需要设计取舍"的中间地带。

再加上它天然带O2O属性——线上选服务、下单预约、线下到店核销——这让前端和后端的交互变得真实可感。你写的每一个预约接口,都对应着现实里"顾客拿起手机约了个发型师"的动作。这种有场景感的项目,拿来练手比空泛的"图书管理系统"有意思得多,写文档的时候也更容易讲清楚业务价值。

从技术堆叠来看,这个题目的性价比也很高:

  • 后端SpringBoot一套下来,Controller、Service、Mapper三层结构清清楚楚;
  • 权限和状态流转比纯演示项目深一档,比如"待接单→已确认→已完成→已评价"这种订单生命周期;
  • 时间冲突检测(同一个技师同一个时段不能同时接两个顾客)涉及到一点并发思维;
  • Android端要处理列表、表单、弹窗、状态刷新这些日常交互,难度也适中。

所以如果正在纠结选什么题目做全栈练手,或者需要做毕设但不想选太俗套的库存管理系统,这类垂直行业服务预约系统是很好的方向。下文我会把这个项目从表结构到Android端到联调踩坑,按自己实操过的顺序完整拆一遍,希望对你有实际帮助。

2. 后端SpringBoot的架构设计与数据模型拆解

2.1 技术栈与目录结构怎么组织才不别扭

我的做法是后端以SpringBoot 2.x为基底,Java版本用8(稳,不用折腾新旧兼容),ORM用的MyBatis-Plus而不是裸MyBatis——原因是它自带分页插件、条件构造器和逻辑删除,能把代码量压掉一大截,尤其适合这种业务量不复杂的系统。数据库用MySQL 8.x,本地开发用Navicat看数据,Redis不是必须的,但如果想给token加个有效期管理,也可以顺手接上,后面我会讲怎么接最省事。

后端模块划分不建议按"包名随便起"的默认习惯来,我建议拆成:

com.example.beauty ├── config // 跨域、拦截器、WebMvc配置 ├── controller // 接口层,只收参数、调service、返回统一结果 ├── service // 业务逻辑层,事务、状态判断都在这里 ├── mapper // DAO层,MyBatis-Plus的BaseMapper接口 ├── entity // 数据库实体 ├── dto // 前端入参/出参的封装对象 ├── vo // 给前端展示用的视图对象 ├── utils // JWT工具、时间工具、上传工具 └── exception // 全局异常定义

这样分的核心逻辑是:让Controller变薄,让Service变厚。很多人做这个项目会顺手把业务判断写在Controller里,比如校验用户是否登录、判断预约时间是否冲突,短期看没问题,但一旦你后面加角色权限、加状态机流转,Controller就会越来越肿,调试时到处找逻辑。我把所有"判断"都下沉到Service层,Controller只负责接收参数和返回数据,这样接口文档看起来清爽,单元测试也好写。

2.2 数据库表:7张表怎么体现出业务闭环

表结构是这种项目最容易出彩也最容易翻车的地方。我最终保留了7张核心表,几乎每一张都能对上一条业务线:

表名核心字段业务意义
userid, phone, password, nickname, avatar, role用户表,role区分普通用户/技师/管理员
technicianid, user_id, name, shop_id, service_items, intro, avatar技师表,关联用户,冗余了擅长项目
service_itemid, name, category, price, duration, cover, status服务项目,比如剪发、染发、护理
shopid, name, address, phone, business_hours门店表,如果业务简单也可以先不做
reservation_orderid, order_no, user_id, tech_id, item_id, appoint_time, status, remark预约订单主表,全系统最核心
order_commentid, order_id, user_id, content, score, create_time评价表
banner/noticeid, img_url, link, title首页轮播或公告,填充界面用

这里要注意一个容易犯的错:不要把技师跟普通用户拆成两张完全隔离的表。因为技师本身也能用App登录、查看自己的日程,从权限模型上看,技师只是用户的一种角色。所以user表里放role字段,1是普通顾客,2是技师,3是管理员,然后technician表存技师的基础资料和服务能力,两者通过user_id关联。这样做登录逻辑统一走一张用户表,不用为技师单独写一套登录验证。

服务项目表和预约订单表的关系也要想清楚。一个订单里到底能选几个服务?实际门店场景里,顾客做头发可能同时做剪发+护理,如果按"一单一项目"设计,用户就得连续下两单,体验很差。我当时的做法是引入一个中间概念:预约订单主表只记录顾客、技师、到店时间和总状态,服务明细另建一张order_item子表,跟service_item关联,存价格快照。这样既支持一单多服务,又能留一份"当时下单时的价格",避免商家后面改价导致历史订单金额对不上。

2.3 预约时间冲突校验:一个逼你想清楚并发的地方

整个项目里技术含量相对最高的不是CRUD,而是预约时段冲突判断。用户选技师A、选今天14:00、做剪发项目(时长假设60分钟),系统必须保证技师A在14:00到15:00之间没有其他已确认订单。

我第一版写得很粗暴:查询技师在该时段的订单,直接遍历比对。

// 第一版:先查出技师当天所有订单,代码里做区间重叠判断 List<ReservationOrder> orders = reservationOrderMapper.selectList( new LambdaQueryWrapper<ReservationOrder>() .eq(ReservationOrder::getTechId, techId) .eq(ReservationOrder::getAppointDate, date) );

逻辑是对的,但这个方式有个隐患:查询和插入之间隔了时间,如果两个用户同时提交,理论上可能都通过冲突检查然后一起插进数据库。数据量小的时候基本遇不到,但这类反映"并发一致性"的点,恰恰是面试官和评审老师最爱追问的。

所以我最终改成了数据库唯一索引 + 状态限制的保守方案。核心思路是给预约表加一个组合唯一索引,用"技师ID + 预约日期"做约束会让同一天只能一单,不够灵活;更好的做法是引入一个预约时段维度,比如把整天按小时切块,预约时插入一条"占用记录",靠唯一索引保证不会重复占用。如果觉得这样设计太重,最实用的小技巧是:用事务 + 数据库行锁。在事务里先执行一条SELECT * FROM reservation_order WHERE tech_id=? AND appoint_time=? FOR UPDATE,再判断是否有重叠订单,然后插入。虽然效率不是最优,但对学生项目、小规模业务来说,正确性和简单性都比极致的并发性能重要得多。

顺手讲下订单状态设计,我保留了五个状态:

  1. 待确认:顾客提交后,等待技师/门店确认
  2. 已确认:门店接受预约
  3. 已完成:顾客到店服务结束后标记
  4. 已取消:用户或门店取消
  5. 已评价:完成且评价过的终态

状态流转我在Service层统一做了一个updateOrderStatus(orderNo, fromStatus, toStatus)方法,每一步都传期望的旧状态,防止用户重复点击导致订单从"已确认"错跳成"已取消"。

2.4 登录鉴权与统一返回结果

登录用的是JWT,结构不复杂:用户输手机号+密码,后端校验通过后生成token返回,Android端把token存到SharedPreferences里,之后所有请求都在Header里带Authorization: Bearer xxx。后端用一个拦截器,对非登录接口直接放行,其他接口取出token并解析用户信息,塞进ThreadLocal里方便Service层取当前用户ID。

这里特别想提醒一个点:JWT里别放敏感信息。有人习惯把手机号、密码状态直接塞进token的claim里,虽然JWT本身有签名防篡改,但Payload是Base64编码,任何人拿到都能解码看到内容。我一般只放userId和role两个字段,验签通过后去Redis或数据库取最新的用户状态。

统一返回结果的类我推荐至少包含这些字段:

{ "code": 200, "message": "操作成功", "data": {} }

接口层全部返回这个结构,错误码用整数区分:200成功,401未登录或token过期,403无权限,500业务异常。Android端封装网络层时,会根据code统一做拦截,code=401就跳转登录页,避免每个Activity里都写一遍错误处理。

3. Android端从UI到通信的完整实现思路

3.1 Android端技术选型与工程结构

Android端是原生AndroidStudio开发,我用的是Java(如果你熟悉Kotlin,换成Kotlin也完全没问题)。网络层用的Retrofit 2 + OkHttp 3,JSON解析Gson,图片加载Glide。这三个是Android开发的黄金组合,网上资料多、稳定,跑Demo不容易出幺蛾子。

项目结构同样按业务模块分包:

com.example.beautyapp ├── activity // 每个页面的Activity ├── adapter // RecyclerView适配器 ├── fragment // 底部导航对应的Fragment ├── model // 数据实体 ├── api // Retrofit接口定义 ├── utils // SharedPreferences工具、日期工具 └── widget // 自定义组件,比如状态标签

界面用底部导航栏做主导航,四个Tab:首页、项目、订单、我的。首页展示门店信息或者轮播图、推荐服务;项目页展示所有服务项目,点击进入详情;订单页按状态分类,可以查看或取消预约;我的页面管理个人信息和技师设置。

3.2 核心页面与数据交互逻辑

首页和项目列表本质上是从后端拉数据填充ListView/RecyclerView。需要特别注意Glide加载服务器图片时,URL地址要能直接访问。很多同学把图片存在服务器某个本地目录,Android端用拼接URL访问,结果调试时死活加载不出来。原因往往是后端没有做静态资源映射——SpringBoot默认不把/upload/**路径映射到磁盘目录。解决方式很直接:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadDir); } }

预约页面是交互逻辑最重的页面。需要选技师、选项目、选到店时间。技师的列表要根据门店查,而技师可约的时间又得实时查询——前端不能随便给一个时间让用户乱选。我当时的做法是:用户选定技师后,Android端单独发一个请求查"该技师的已占用时间段",然后把可用时间段展示成选择chip,已占用时段置灰不可点。这个交互做出来,整个App的使用体验立刻不一样,答辩的时候也很有展示效果。

订单状态流转需要前端配合刷新。用户在订单列表页点"确认预约"或"取消",操作成功后不能只静态提示一下,最好用如下流程:

  1. 调用接口成功
  2. 关闭操作弹窗
  3. 重新请求当前订单列表
  4. 更新Adapter数据并刷新页面

不要只改本地列表里的某个对象状态,否则一旦后端拒绝了操作,前端状态就对不上了。

3.3 网络层封装:Retrofit接口与统一拦截

Retrofit接口定义我不建议一个Activity写一个接口类,而是统一放在ApiService里。比如:

public interface ApiService { @POST("user/login") Call<ApiResult<UserVO>> login(@Body LoginRequest request); @GET("service/list") Call<ApiResult<List<ServiceItemVO>>> getServiceList(@Query("category") String category); @GET("reservation/availableTime") Call<ApiResult<List<String>>> getAvailableTime(@Query("techId") int techId, @Query("date") String date); @POST("reservation/submit") Call<ApiResult<ReservationDetailVO>> submitReservation(@Body ReservationRequest request); }

统一OkHttp拦截器里做三件事:

  1. 每次请求自动在Header里加上token
  2. 打印请求和响应日志,方便调试
  3. 检查响应里的code,如果返回401,发一个广播让App跳转登录页

这里也遇到过很经典的坑,我单独放进后面的联调章节讲。

4. 联调阶段最值得记录的三个真实踩坑现场

很多项目做完一遍能跑,但真正折磨人的是后端的"接口通了但前端拿不到数据"那一堆破事。这里我把自己踩过的三个坑完整复盘一遍,希望你能省下至少两天时间。

4.1 时间参数传参:LocalDateTime与字符串的互相折磨

Android端传给后端一个预约时间,第一版我直接传"2025-04-12 14:30:00"字符串,后端用@RequestParam String appointTime接收,再手动用LocalDateTime.parse()转。问题是这个格式跟前端用DatePicker和TimePicker拼出来的字符串经常对不上——比如月份少了补零、空格变成了T,解析一失败就报错。

更稳的做法是:

  • Android端统一用"yyyy-MM-dd HH:mm:ss"格式生成字符串
  • 后端实体里用LocalDateTime类型,配合Jackson的全局格式化配置:
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8

以及SpringBoot请求参数自动绑定时,要用@DateTimeFormat注解显式声明格式:

public ApiResult<Void> submit(@RequestBody ReservationRequest request) { // request.appointTime 是 LocalDateTime 类型 // Jackson 会根据项目配置自动解析字符串 }

做完这一步,时间格式问题基本绝迹。另外,建议前端只负责传,后端干脆收到后统一做规范化处理——存库时精确到分钟,毫秒全部清掉,避免同一分钟出现多个时间取值不一致。

4.2 Android模拟器请求后端:10分钟排查的http明文问题

这个坑99%的人都会遇到。后端接口写好了,用浏览器访问完全正常,但Android里一调用就报错,错误信息类似CLEARTEXT communication to 10.0.2.2 not permitted by network security policy。原因很简单:从Android 9(API 28)开始,系统默认禁止明文的HTTP网络请求,只允许HTTPS。

模拟器访问本机后端时用的地址是10.0.2.2,而不是localhost或127.0.0.1,这点也要注意。解决方案有两种:

  1. 临时调试方案:在AndroidManifest.xml的application标签里加上android:usesCleartextTraffic="true"
  2. 规范方案:在res/xml里创建network_security_config.xml,配置只允许特定域名/IP走明文
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">10.0.2.2</domain> </domain-config> </network-security-config>

然后用android:networkSecurityConfig="@xml/network_security_config"引用。真机调试时,手机和电脑连同一个局域网,后端地址改成电脑的局域网IP,同样的配置思路可以套用。

4.3 Long类型精度丢失:订单号后端是对的前端变成了一串乱数

订单号我用的是雪花ID生成的一串Long,比如1650123456789012345。Android端用Long接收后,往页面上一放,发现最后几位全变成了0——数字变样了。一开始以为接口数据错了,后来发现是JSON解析的经典问题。

JavaScript里数字精度只有53位(JSON解析时如果转成JS Number会丢精度),但在Android原生端理论上Long是64位不会丢。问题真正的根源其实是Gson在解析超长数字时,如果目标字段类型是long,通常没问题;但如果字段类型是double或者默认被解析成Double,精度立刻丢失。

我的解决方式很简单:

  1. 后端返回订单号给前端时,不以数字Long返回,转成字符串
  2. DTO/VO里写private String orderNo;,把Long转String的工作放在Service层
  3. Android端全部按String接收,页面上展示的就是原始字符串,不会丢

这个改动很小,但能避免排查时怀疑人生。如果你做的是Web管理后台,同理,Long主键转JSON时建议都序列化为String,省得到处踩精度坑。

5. 联调通过后,项目交付的文档和演示经验

做完代码只算完成了60%,这个标题里专门提到"文档+讲解",我把这两块的经验也讲透。

5.1 项目说明书/设计文档该怎么写

文档质量的标志不是页数多,而是逻辑链条完整。我见过很多同学把需求分析、数据库设计、模块设计几大章一贴,但评审老师随便问一句"为什么这个表要冗余字段"就答不上来。

建议文档主线按下面思路走:

  1. 项目背景:阐述目前美容院预约靠电话/微信,效率低、容易撞档期,因此需要一个线上系统让顾客随时查看、预约、评价
  2. 需求分析:画出用户角色图(顾客、技师、管理员)和核心业务流程
  3. 系统设计:后端架构图、Android端模块图
  4. 数据库设计:每张表写清楚字段含义、外键关系、为什么这样设计
  5. 核心功能实现:挑2-3个有深度的地方详细写,最建议写预约冲突检测、订单状态机、JWT鉴权
  6. 系统测试:列功能测试用例,比如"两个用户同时预约同一技师同一时段,只有一个成功"
  7. 总结与展望:可以写"后续可加入支付、会员卡、消息推送等功能"

注意:文档里涉及的表结构、接口地址、字段名,必须跟代码里完全一致。我见过有人文档里写technician表,代码里叫barber,答辩时PPT一翻页就露馅了。

5.2 "调试+讲解"在实际交付中意味着什么

如果你是在帮别人做这个项目,或者准备答辩演示,"讲解"能力甚至比代码能力更影响最终评价。我总结了一套演示顺序,照着讲基本不冷场:

第一步:打开App,注册一个顾客账号,演示注册登录流程; 第二步:进入项目列表,选服务,选技师,看可用时段,提交预约; 第三步:切换技师账号登录,看到新订单提醒,点击"确认"; 第四步:切回顾客账号,看到订单状态变成"已确认",到店核销后标记完成; 第五步:完成订单后去评价,评价完订单变成终态"已评价"。

这五个步骤把系统所有业务闭环展示完了,每一步都对应着之前文档里的核心逻辑,老师如果中途追问,你要能顺手指到对应的代码位置。所以建议演示前把关键代码的类名、方法名记熟,尽量做到"问哪点开哪点"。

调试方面,给几个实际建议:

  • 后端接口用Swagger(springfox或springdoc)自动生成文档,调试时直接在Swagger页面试参数,不用反复在Android端打包测试
  • Android端日志分级打印,关键请求响应用Log.i,错误用Log.e,且打上固定TAG(比如"BeautyApp"),Logcat里过滤后一眼看清网络链路
  • 后端全局异常处理要捕获所有Exception,把堆栈打到控制台,返回给前端时用统一错误结构。这样联调时前端拿着一句"系统异常"来找你,你真能五秒定位,而不是让前端把整个日志翻给你看

5.3 项目复盘:如果重做一次,我会在哪三处改方案

复盘的时候觉得这个题目看似简单,其实有几个地方值得再优化:

第一个,支付功能。当前只做了线上预约,没有押金或者预约金。如果加一个简单的支付宝/微信模拟支付,系统的业务完整度和复杂度都能上一个台阶,答辩时也更有亮点。不接真实支付SDK的话,可以做成"在线支付(模拟)"的订单状态节点,演示时点一下跳转模拟支付成功页。

第二个,消息通知。技师确认订单的环节,目前需要技师主动刷新App才能看到新订单。如果要做得更真实,可以用WebSocket或者轮询做新订单提醒。WebSocket在这个项目里的接入成本不高,SpringBoot原生支持,Android端用OkHttp的WebSocket也顺手,展示效果很加分。

第三个,管理后台。如果时间充裕,建议做一个网页版的管理后台(可以用简单的Vue或纯Thymeleaf),让管理员可以管理服务项目、查看所有预约、取消违规订单。如果只做App,管理员功能只能硬塞在某个隐藏入口里,演示起来很别扭。

6. 写在最后:一些操作层面的小提醒

按我自己的完整开发周期估算,这个项目从零到能演示,大概需要15到20天。后端占5到6天,Android端占7到8天,联调和修修补补占3到4天,剩下2天整理文档和准备演示。如果你是边学边做,时间翻倍也正常,不用焦虑。

最后分享几个我习惯性的检查清单,每次跑项目之前先过一遍,能避免很多低级翻车:

  1. 后端启动前确认MySQL连接串、用户名密码、数据库名都对,服务端口没有被占用
  2. Android端Build之前确认build.gradle里的依赖版本都下载完了,没有版本冲突(常见的是Glide和AndroidX不兼容)
  3. 真机调试时,手机和电脑必须同一局域网,注意手机浏览器先访问一次后端接口,确认网络通
  4. 每次改完后端代码,重启服务的同时看控制台有没有报错,不要只在Android端看到404才回来查
  5. 数据库里的测试数据尽量模拟真实门店,比如"洗剪吹""烫染""皮肤护理"都配上图片,演示效果好很多
  6. 预约演示之前,自己先在测试账号里重复走一遍完整流程,防止当场出现"时间已占用"之类的逻辑小bug

这个项目能带给你的不是"一个能交差的作业",而是一条完整的全栈实操链路:你会更清楚接口怎么设计、前端怎么对接、数据表怎么规划、遇到联调问题怎么快速定位,这些经验在以后做任何业务系统时都能复用。如果后面有兴趣,也可以把支付、消息、会员积分这些模块一个个加起来,每加一个模块,你对系统设计边界和业务复杂度的理解就深一截。希望这篇拆解能帮你少走点弯路,也欢迎带着问题来交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询