我见过太多SpringBoot毕设项目,界面做得花里胡哨,功能却经不起一句追问:“预约时间冲突你是在哪一层处理的?”“会员等级折扣和积分抵现一起用的时候,金额到底按什么顺序算?”多数人当场卡壳。这个美容店服务管理系统,核心是把医美预约和会员管理平台这两件事真正跑通,而你用SpringBoot框架把它做成一套能演示、能解释、能扩展的美容会所数字化运营系统,其实就是在磨Java后端开发里最常用的那套组合拳:REST接口、JWT鉴权、事务控制、缓存、定时任务、报表统计。这个题目不新,但想做得扎实并不容易。这篇内容我不会只贴代码,而是把从需求拆解到数据库设计、从核心实现到答辩准备的完整链路讲一遍,适合正在做毕业设计、或者想拿一个完整项目练手的人参考。
1. 先想清楚美容店到底要什么:需求拆解决定项目成败
很多同学拿到题目就开始建表写接口,这是最大的坑。美容店服务管理系统看起来只是“预约+会员”四个字,但不同角色的操作路径完全不一样,你如果不先理清角色和用例,写出来的系统要么功能冗余,要么核心场景缺失,论文里的用例图都不知道怎么画。
1.1 角色与核心用例
这套系统的使用者至少有四类:
- 顾客(普通用户/会员):注册登录、浏览服务项目、查看技师排班、在线预约、取消预约、查看订单、消费后评价、查询积分和余额。
- 前台/店员:处理线下到店顾客的预约登记、核销预约单、代客充值、办理会员卡、登记消费。
- 美容师/技师:查看自己的排班和预约列表、标记服务完成。
- 老板/管理员:管理员工信息、服务项目管理、会员等级规则、查看经营统计报表、处理预约冲突和异常订单。
这四个角色就是你的用例图基本盘。很多时候大家只做了顾客端和管理员端,把美容师角色砍掉了,其实美容师的小功能(查看当天预约、调整状态)非常适合体现“你不是在写增删改查,而是在模拟真实业务协作”,答辩时很加分。
1.2 预约流程里的状态流转
预约是这个系统的命脉。我建议把预约单设计成一条清晰的状态机:
code复制待确认 -> 已确认 -> 已完成
^ |
| v
+------ 已取消/爽约
流程是:顾客选择服务项目和技师,系统展示可预约时间段,提交后生成“待确认”订单;商家后台确认后变成“已确认”;顾客到店后前台核销,服务完成技师标记“已完成”;如果顾客预约了没来,可以标记“爽约”,也可以设置超时自动取消。
为什么要刻意设计状态而不是用一个“status”字段随便填?因为状态机决定了你后续所有统计SQL的写法,比如“本月到店转化率 = 已完成数 / 已确认数”“预约爽约率 = 爽约数 / 已确认数”。面试官或答辩老师问“这个系统哪里体现了业务逻辑”,你直接把状态流转图和统计口径甩出来,比任何技术名词都更有说服力。
1.3 会员体系的分层逻辑
会员模块不能只做一张“会员表加个等级字段”。真实的美容会所通常是“储值卡 + 等级折扣 + 积分”三套东西叠加:
- 储值卡:顾客预充值一笔钱,消费时从余额扣,充值时有赠送金额。
- 等级折扣:按累计充值或累计消费金额划分等级,比如普通卡无折扣、银卡95折、金卡9折、钻石卡85折。
- 积分:消费一元积一分,积分可以兑换项目或抵扣现金。
这三套规则必须同时生效,而且结算顺序要事先定好。我建议的规则是:先算会员折扣价,再判断是否使用积分抵现,最后从储值余额或微信支付完成支付。顺序不定义清楚,就会出现“9折之后再被积分抵掉一部分,那储值余额里到底扣多少”这种算不清账的情况。
说白了,需求拆解阶段你花两天把角色、状态机、规则理清楚,后面代码阶段能节省至少一周的返工量。别嫌麻烦。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型不是越花哨越好:从Spring Boot核心出发的组件清单
技术选型是毕设答辩的高危区。老师经常问“你为什么要用这个技术?”,你答不上来,前面演示再好也白搭。记住一个原则:每个组件都要有一个非它不可的理由。
2.1 后端主框架与持久层选择
主框架毫无疑问是Spring Boot。但版本选择要注意,我建议毕业设计用 Spring Boot 2.7.x + JDK 1.8,而不是Spring Boot 3.x。原因很实际:百度出来的教程、网上开源的MyBatis-Plus配置、Springfox的Swagger文档、大部分教学楼的实验环境,默认都是Boot 2.x。Spring Boot 3.x原生要求JDK 17起,而且javax包改成jakarta,很多老依赖直接找不到类,连springfox文档核心包都不兼容。你如果非要折腾新版,光是环境问题就能消耗两三天。
持久层我推荐 MyBatis-Plus,理由是它可以让你少写大量重复的CRUD,自带分页插件、条件构造器、逻辑删除。这些能力在答辩时也容易解释:分页插件走的是MyBatis的Interceptor机制,通过拦截Executor在执行SQL前追加Page分页参数。这一句话就能体现你对框架底层有理解。
2.2 缓存、鉴权与接口文档
缓存必须用 Redis,不要只在配置里挂个依赖就完事。至少要让Redis在三个地方真实发挥作用:
- 缓存服务项目和技师列表,减少数据库查询压力;
- 存储JWT黑名单(用户注销后token失效)或者当前登录用户信息;
- 预约时间段的短时锁,防止并发下同一个技师被同时约走。
鉴权方面,如果只用拦截器加JWT,代码量小,容易讲。但Spring Security是面试高频点,建议至少了解两者区别:拦截器只是Servlet层面的简单过滤,Spring Security是完整的认证授权框架,支持方法级权限控制(@PreAuthorize)。对于美容店系统,用户角色就管理员、店员、顾客三类,我用拦截器+自定义注解也能做,但答辩时主动补充“如果想引入更细粒度的权限控制,可以替换为Spring Security”,会显得你有全局视野。
接口文档建议用 springfox-swagger2(Boot 2.x对应版本)或者 springdoc-openapi(Boot 3.x)。另外可以加一个Knife4j增强UI,演示时直接把接口文档页面打开,让老师看到你并非只写了接口,还规范了接口注释。
2.3 前端技术栈与文件存储
前端建议 Vue3 + Element Plus,做成前后端分离。学生端如果有余力,可以再做一个简单的H5或微信小程序,但毕设最核心的演示场景是后台管理界面,用户端一般用浏览器访问即可。前端打包后的dist目录,可以通过Spring Boot的静态资源映射加载,这样部署时只需要一个Java进程,不需要单独启动Nginx,演示更省事。
文件上传(头像、项目图片、技师资质照片)优先做本地磁盘存储,再配置虚拟路径映射。不要一上来就接阿里云OSS,不是不能用,而是你还要解释“AccessKey怎么保管”“对象存储和本地磁盘区别是什么”,平白增加答辩风险。本地存储配合一个图片访问接口,足够支撑演示。
选型清单确定之后,你写论文的“技术介绍”章节时就有了明确脉络,每个技术都能对应解决系统里的一个具体问题。这比从百度百科抄一段“Spring Boot是什么”强一百倍。
3. 数据库设计:会员、预约、库存与流水的建模思路
数据库设计是整个项目的隐藏评分项。很多评委不看你的界面,直接让你打开数据库表结构,看有没有主外键关系、有没有逻辑删字段、有没有冗余字段的设计考量。我建议核心表控制在10张左右,太少显得简单,太多写不完。
3.1 核心表设计
直接给一个可落地的表清单:
| 表名 | 说明 | 关键字段 |
|---|---|---|
| user | 用户/会员 | id, phone, password, nickname, member_level_id, points, balance, avatar, status |
| member_level | 会员等级 | id, level_name, discount_rate, min_expense, remark |
| service_item | 服务项目 | id, name, cover_image, price, duration_minutes, description, status |
| employee | 美容师 | id, name, avatar, title, service_years, status |
| employee_service | 技师与项目多对多 | id, employee_id, service_item_id |
| appointment | 预约单 | id, appointment_no, user_id, employee_id, service_item_id, appoint_date, start_time, end_time, status, cancel_reason |
| order_info | 消费订单 | id, order_no, appointment_id, user_id, total_amount, discount_amount, points_deduct, actual_amount, pay_status, create_time |
| recharge_record | 充值记录 | id, user_id, amount, gift_amount, balance_before, balance_after, create_time |
| points_log | 积分流水 | id, user_id, change_points, type, remark, create_time |
| evaluation | 评价 | id, appointment_id, user_id, rating, content, create_time |
user表里冗余了member_level_id和points,是因为这两个字段在会员列表页、预约页、结算页高频查询,如果每次回表查等级规则,会非常啰嗦。这种有意识的冗余是允许的,但答辩时要说清楚“我做了冗余,并会在等级变更时同步更新”。
3.2 服务项目与技师的时间槽设计
美容店预约最核心的问题是:怎么知道技师在某个时间是不是有空。存在两种设计方案:
第一种是提前生成时间槽表,把每个技师每天按30分钟粒度切成多个槽位并存入数据库,预约时占用对应槽位。优点是不需要实时计算,锁定一行即可;缺点是数据量大,而且服务项目时长不同会出现碎片槽。
第二种是区间判断法,只在appointment表里存开始时间和结束时间,通过SQL查询判断某个技师在目标时间段是否有重叠预约。我推荐第二种,因为它更接近真实业务,而且实现量小。判断重叠的SQL长这样:
sql复制SELECT COUNT(*) FROM appointment
WHERE employee_id = #{employeeId}
AND appoint_date = #{date}
AND status IN (0, 1) -- 待确认和已确认占时间
AND start_time < #{endTime}
AND end_time > #{startTime}
注意是 start_time < 新结束时间 且 end_time > 新开始时间,这是判断两个左闭右开区间是否重叠的通用写法,两个条件都要有,漏一个都会出问题。
3.3 订单与流水的账目设计
我特别想提醒一个细节:订单表里一定要保存业务快照。什么叫快照?就是顾客下单那一刻的服务项目名称、单价、折扣、积分抵扣等信息,原样保存到order_info表中,不能只存一个service_item_id然后靠关联查询去算价格。原因很简单:美容店项目价格经常调价,如果一个月后你统计营收时再去关联价格表,算出来的历史和顾客实际支付金额对不上,这就是数据事故。快照字段建议这样设计:
sql复制order_no VARCHAR(32) NOT NULL COMMENT '订单编号',
service_name VARCHAR(50) COMMENT '服务项目名称快照',
unit_price DECIMAL(8,2) COMMENT '原价快照',
discount_rate DECIMAL(3,2) COMMENT '折扣快照',
discount_amount DECIMAL(8,2) COMMENT '折扣金额',
points_deduct DECIMAL(8,2) COMMENT '积分抵扣金额',
actual_amount DECIMAL(8,2) COMMENT '实付金额'
至于积分流水、储值余额变动,一律要记录before和after。这是对账的基本功,哪怕你的毕设不接入真实支付,这个意识也能让你在答辩时显得非常专业。
4. 核心功能的代码实现:登录、预约防冲突、会员折扣与报表
进入代码阶段。我不会把每个模块都贴一遍,那样篇幅爆炸。这套系统里真正值得深入讲的是四个点:JWT登录与权限、预约防冲突、会员折扣结算、统计报表。这四个点也是答辩时最容易被拿出来深挖的地方。
4.1 基于JWT的登录与权限校验
登录流程:用户提交手机号密码,校验通过后生成JWT返回前端,前端在请求头带Authorization: Bearer token,后端拦截器解析token并放行。
JWT工具类核心代码:
java复制@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private Long expire; // 毫秒,建议 7200000
public String createToken(Long userId, String role) {
return Jwts.builder()
.claim("userId", userId)
.claim("role", role)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Claims parseToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
}
拦截器里校验token,解析出的用户信息存到ThreadLocal,这样Controller里直接通过UserContext.getUserId()取当前用户,不用每个接口都传一个userId参数。记得到finally里调UserContext.remove(),不清理的话Tomcat线程池复用会导致用户数据串号,这是个隐藏bug。
自定义一个@RequireRole("ADMIN")注解打在管理员接口上,拦截器里通过反射读取注解并比对角色,代码不高深但很实用。答辩时你可以主动说“这就是注解+AOP/拦截器的经典组合”。
4.2 预约下单与防冲突
预约下单的Service要做三件事:
- 校验服务项目、技师、用户状态是否正常;
- 计算并指定时间区间(开始时间 + 项目时长);
- 检查该技师在目标时间段是否已被预约,没有则插入预约单。
防冲突不能只做“先查再插”,因为并发请求同时到达时,两个事务都查不到记录,然后都插进去了,就冲突了。解决方式有几种,从简到繁:
- 数据库层面:在appointment表对
employee_id, appoint_date, start_time, end_time建唯一约束,但区间重叠无法用唯一约束直接表达,只能让程序保证同一开始时间不重复,换句话说粒度不够细。 - 同步锁:在Service方法内对
employeeId.toString().intern()加锁,JVM内同一时间只有一个线程能执行查询+插入。毕设演示环境够用,但集群部署无效。 - Redis分布式锁:用
SETNX对“employeeId:date:startTime”设锁,设置过期时间,抢到锁才允许插入。这是目前开源项目最常用的方案。
我建议毕设至少做到“数据库唯一约束 + 同步块重查”,如果论文想写亮点,可以升级为Redis分布式锁。但要理解锁的本质:锁不是数据库的东西,而是业务并发的保护机制,你要能画出一个并发时序图,解释为什么先查再插会出问题,这个比背概念强得多。
4.3 会员等级折扣与积分结算
结算逻辑是另一个能体现设计能力的点。如果你把所有if-else堆在OrderServiceImpl里,代码会越来越乱。我建议做策略模式:
先定义一个策略接口:
java复制public interface DiscountStrategy {
BigDecimal calculate(BigDecimal originalPrice, MemberLevel level, Integer usePoints);
}
再实现两个策略:等级折扣策略、积分抵现策略。结算时通过一个PriceCalculator组合调用:
java复制BigDecimal discountPrice = originalPrice.multiply(level.getDiscountRate());
BigDecimal pointsDeduction = BigDecimal.valueOf(usePoints / 100.0); // 100积分抵1元
BigDecimal actualAmount = discountPrice.subtract(pointsDeduction).max(BigDecimal.ZERO);
注意两个细节:一是discountRate在数据库里存的是0.95这种小数,不要存整数95,计算时容易错;二是所有金额用BigDecimal,不要用Double,浮点数算钱必出精度问题。这两点是财务系统的基本常识,也是答辩老师非常喜欢挖的细节。
4.4 运营统计报表SQL
管理员的首页统计是展示系统价值的关键。至少实现这几块:今日营收、今日预约数、本月新增会员、热门服务项目、技师工作量排行。
对应SQL很典型:
sql复制-- 今日营收(已完成订单)
SELECT IFNULL(SUM(actual_amount), 0)
FROM order_info
WHERE DATE(create_time) = CURDATE()
AND pay_status = 1;
-- 热门项目 Top5
SELECT si.name, COUNT(oi.id) AS order_count
FROM order_info oi
JOIN service_item si ON si.id = oi.service_item_id
WHERE DATE(oi.create_time) BETWEEN ? AND ?
GROUP BY si.id
ORDER BY order_count DESC
LIMIT 5;
统计不外乎GROUP BY、日期函数、聚合函数,不要为了显得厉害去用复杂的窗口函数,先把基础写对。图表前端用ECharts拉一下就行,数据接口返回List,前端组装成xAxis和series就能出柱状图。这一块效果好,做起来也不难,强烈建议放在演示页最显眼的位置。
5. 我在这个项目里踩过的坑:版本、事务、自动配置与部署
接下来这些坑不是编的,是实打实容易在SpringBoot项目里遇到的高频问题。每一个都可能导致你浪费一整天甚至影响进度。
5.1 Spring Boot版本过高导致AOP切面失效
现在很多教程还是基于Spring Boot 2.x写的,你如果图新鲜下载了Spring Boot 3.x甚至预览版,大概率会遇到“自定义注解切面不生效”的问题。原因不只是包名从javax变成了jakarta,更麻烦的是Spring Boot 3.x中spring-boot-starter-aop的aspectjweaver版本与部分黑客代码不兼容,网上很多“自动拦截日志注解”的示例在3.x下直接失效。
我个人的建议:不要追求Spring Boot最新版。毕设求稳。如果非要上Boot 3.x,请确认所有依赖都是兼容版本,MyBatis-Plus要用3.5.3+,springdoc-openapi用2.x,JDK必须是17以上。如果你在本地只有JDK 1.8,老老实实Boot 2.7.x。
5.2 事务注解失效的几种情况
预约模块里“插入预约单 + 扣减会员积分 + 记录流水”应该在同一个事务里。很多人写了@Transactional却还是出现数据不一致,多半是踩了这几个坑:
- 同类内部调用:
this.createAppointment(),事务注解的本质是Spring AOP生成代理对象,this调用不会经过代理,事务直接失效。解决方法是注入自身代理,或者把事务方法放到另一个Service里。 - 异常被catch住:
try { ... } catch(Exception e) { log.error(...) },异常被吞了,事务感知不到回滚条件。要么异常往外抛,要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。 - 非public方法:
@Transactional默认只对public方法生效,在protected或private上不报错但也不生效。 - MySQL表引擎不是InnoDB:MyISAM不支持事务。建表时确认引擎。
5.3 循环依赖
如果你的UserService要调用OrderService,OrderService又调用UserService,构造器注入时Spring会直接报错。Spring Boot 2.8开始默认不允许循环依赖,网上还有老项目在靠@Lazy打补丁。解决思路是打破依赖环:把公共逻辑抽到独立Service,或者用事件监听解耦。比如下单成功后要通知用户积分变动,完全可以用ApplicationEvent发布一个事件,而不是OrderService直接调UserService。
5.4 Docker部署时的JDK版本问题
把SpringBoot项目打包到Docker Desktop时要注意镜像的JDK版本。如果你本机是JDK 8,但Dockerfile里写的镜像是openjdk:17-jdk,运行时会报不支持类文件版本错误。最简单的处理是使用多阶段构建,或者直接用eclipse-temurin:8-jdk。Dockerfile参考:
dockerfile复制FROM maven:3.8.6-openjdk-8 AS builder
COPY . /app
WORKDIR /app
RUN mvn clean package -DskipTests
FROM openjdk:8-jre
COPY --from=builder /app/target/beauty-admin.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
你还可以在启动命令里加--spring.profiles.active=prod区分开发和生产配置。能说到这一步,说明你是真的部署过,而不只是会点IDE里的绿色启动按钮。
5.5 文件上传与资源映射
上传头像到/upload/avatar/xxx.jpg后,前端访问localhost:8080/upload/avatar/xxx.jpg报404,这是没配置静态资源映射导致的。需要在配置类里加:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceHandler("file:" + uploadPath + "/");
}
}
addResourceHandler定义的是URL路径前缀,addResourceLocations定义的是磁盘物理路径。写错这个,文件上传功能演示时必翻车。
6. 从能跑到能答辩:演示数据准备与高频追问
代码写完只是第一步,真正拉开差距的是演示质量和答辩表现。我看到过太多项目代码没问题,但演示时页面空空如也,或者被老师随口一问就答非所问。
6.1 准备一套能讲故事的演示数据
你不可能现场注册十个用户再预约三次。提前准备一条完整业务链:一个老会员,余额充足,曾经有过几次储值充值记录;一个技师的排班表紧张且部分时间被预约;当天有几条待确认预约、几条已完成订单;首页统计数据有数字。这样演示路径才清晰:从登录开始,演示会员列表、项目价格、预约流程、订单结算(看到折扣和积分抵扣)、后台统计图表变化。
建议写一个data.sql或者通过ApplicationRunner在启动时初始化管理员账号和基础数据。这样不论在谁的电脑上运行,都能一键还原演示环境。
6.2 高频追问及参考答案
我把这些年被问到最多的几个问题列一下,供你提前准备:
- Spring Boot自动装配原理是什么? 核心是@EnableAutoConfiguration,通过AutoConfigurationImportSelector加载META-INF/spring.factories里的配置类,再配合@ConditionalOnClass等条件注解按需装配。你不用背源码,把流程说清楚就行。
- MyBatis-Plus分页插件底层原理? 通过MyBatis的Interceptor插件,拦截Executor的query方法,拿到要执行的SQL后拼上limit分页语句,再执行Page查询并封装总数。
- Redis缓存穿透怎么解决? 查一个不存在的id,每次都打到数据库,可以在缓存里存空值并设置短过期时间,或者用布隆过滤器在请求前过滤掉不存在的id。项目里缓存项目列表时也适用。
- 预约并发了怎么办? 先讲业务判断逻辑:查重叠预约->插入。再讲并发保护:Redis锁或者数据库约束,强调“先查再插”不原子。
- 为什么不用单表user把所有角色都写了? 因为管理员、店员、顾客的操作权限和字段差异较大,统一在一张表里字段冗余多,鉴权逻辑也会混乱,所以user表用role字段区分,但按角色查询用联合索引优化。
6.3 可以继续扩展的方向
如果你的项目还想往上加分,有几个方向成本低、见效快:给预约模块加一个Quartz定时任务,提醒“明天有预约”的客户短信通知;给接口层补充单元测试,用MockMvc跑通登录和预约接口,体现工程质量意识;把前端从Vue2升级到Vue3组合式API,响应式封装得更规范。这些都会在论文的“展望”章节里显得言之有物,而不是空喊“未来可以引入大数据、人工智能”。
我自己做这个题目的最直接感受是:毕业设计不是写一个玩具而是证明你有独立完成业务系统的能力。美容店预约与会员管理虽然行业不复杂,但里面涉及的账户、快照、并发、状态流转都是真实商业系统绕不开的问题。你把这个Spring Boot项目真正吃透,能解释清楚每一个模块为什么这么设计,答辩基本不会差。论文和代码都是外在,你脑子里那套“从需求到实现”的完整逻辑,才是这半年里最值钱的东西。
