news 2026/9/28 1:44:57

Spring Boot+小程序美容院管理系统毕业设计全流程落地指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot+小程序美容院管理系统毕业设计全流程落地指南

简介:基于微信小程序与Java后端的美容院管理系统毕业设计,面向计算机相关专业学生,适用于毕业设计、课程设计或项目实战,涵盖小程序前端、Java后端、管理后台及数据库,演示预约、会员、套餐、订单等美容业务场景。压缩包共1404个文件,约15.86MB,包含Java源码、Vue页面、小程序wxml/wxss/js、JSON配置、SQL脚本及说明文档;前端以png、svg、jpg图片及小程序页面为主,后端以Java与vue组件为核心,辅以xml、yml,目录结构清晰便于检索。目前已有318人学习下载,适合需要快速搭建系统原型或借鉴前后端分离架构的开发者。资源内含源码、数据库脚本与说明文件,可帮助理解小程序调用Java接口、操作MySQL数据表的流程,减少调试弯路,对毕业答辩或二次开发有参考价值。

1. 美容院管理系统:这类毕业设计为什么年年有人做,年年有人翻车

每年毕业季,Java Web 方向的选题里,“某某管理系统”是最稳妥也最泛滥的一类。美容院管理系统之所以被反复拿出来做,是因为它的业务边界足够清晰:会员卡、套餐、预约、库存和员工提成,这几张表一建模,一个完整业务闭环就出来了,比图书馆管理系统多一层“线上线下联动”的味道,又比电商系统简单得多,工作量恰好卡在一个本科生能独立完成的范围里。

但正因为题材常见,答辩时老师一眼就能看出你是真做了还是抄的。微信小程序做前端、Java 做后端接口、MySQL 存数据,这个组合本身没毛病,真正的分水岭在于:预约时间冲突怎么处理、会员卡余额和次卡次数怎么扣、支付回调怎么保证幂等。这三个点只要有一个写明白,答辩就能站得住。这篇笔记就按“系统拆解 → 环境搭建 → 核心代码 → 避坑 → 查漏补缺”的顺序,把一套能跑通的美容院管理系统完整落地路径讲清楚。

如果你正准备拿这个题目做毕业设计,或者接手了别人留下的源码包想跑起来,这篇能帮你省下一个星期的瞎折腾时间。

2. 系统拆解:美容院业务里的角色、状态机和“钱”的问题

2.1 三种角色和它们的权限边界

美容院管理系统最常见的角色划分是三端:小程序端给顾客用,管理端给店长和前台用,后端管理后台跑在浏览器里给老板看数据。顾客端的核心操作是注册登录、浏览项目、预约服务、查看余额和消费记录;员工端要能处理签到核销、登记充值和划卡;老板端看营收报表和项目消耗排行。

这三个角色的权限在Spring Boot里用拦截器加注解就能搞定。设计上不要搞太复杂的RBAC表,毕业设计阶段用角色字段加一个枚举就够:ROLE_CUSTOMER、ROLE_STAFF、ROLE_ADMIN,登录时把角色写进JWT的claims里,后端接口用@PreAuthorize("hasRole('ADMIN')")控制。真正需要多一张权限表的场景是员工也有级别区分(美容师/店长/经理),这时候再考虑单独建sys_role和sys_menu两张表,否则徒增工作量。

两位顾客之间不能看彼此的预约记录,员工看不到营收利润,这些用user_id作为数据隔离维度就够了。常见做法是每个业务表都带create_by字段,查询时统一加WHERE create_by = ?,比在SQL里写子查询走一遍sys_user表要清爽得多。

2.2 核心业务表:会员卡、套餐、预约、消耗记录

美容院和普通零售最大的区别在于“预付+消耗”模式。顾客办了一张2000元的储值卡,或者买了一个10次的肩颈套餐,钱在办卡时已经收了,后续服务是逐步扣减的。所以数据库设计里最关键的几张表不是用户表,而是member_card、card_recharge_record、appointment和consume_record。

member_card表建议字段:card_id、user_id、card_type(储值卡/次卡/疗程卡)、balance(余额,单位用分)、remain_count(剩余次数)、valid_start、valid_end、status。注意,余额一定要用整数存分,不要用DOUBLE存元。小数在Java的float里会出现0.1+0.2不等于0.3的问题,虽然可以用BigDecimal规避,但数据库层面用INT存分是最保险的,查询时除以100展示就行。

appointment表要单独说。预约的核心字段是staff_id、service_item_id、appoint_date、time_slot。时间冲突检测的SQL不能只做等值匹配,因为顾客A约了10:00-11:00,顾客B想约10:30就没有空位,你需要存start_time和end_time两个字段,查询时用:

SELECT COUNT(*) FROM appointment WHERE staff_id = #{staffId} AND appoint_date = #{date} AND status != 'CANCELLED' AND start_time < #{endTime} AND end_time > #{startTime}

这条SQL的边界条件很多人会写反,start_time < endTime表示旧预约的开始时间早于新预约的结束时间,end_time > startTime表示旧预约的结束时间晚于新预约的开始时间,两个条件同时成立才叫冲突。只有“A的结束时间等于B的开始时间”这种首尾相接的情况不会被误判为冲突,符合实际业务。

2.3 消费扣减:余额和次卡不能一把梭

扣费逻辑是这套系统的“钱”之所在,也是最容易写出并发问题的地方。储值卡消费就是余额减掉本次金额,次卡消费就是剩余次数减一。看起来简单,但如果顾客在手机端同时发起两笔预约,后台两个线程同时读到余额100元,各自扣除80元,最后数据库里可能只剩20元——还亏了20元。

解决方式有两种。第一种是用数据库的行锁:

@Transactional public boolean consumeByBalance(Long cardId, long amountFen) { MemberCard card = memberCardMapper.selectByCardIdForUpdate(cardId); if (card.getBalance() < amountFen) { return false; } memberCardMapper.deductBalance(cardId, amountFen); return true; }

selectByCardIdForUpdate对应SQL是SELECT * FROM member_card WHERE card_id = ? FOR UPDATE,在事务里锁定这行记录,其他线程必须等这个事务提交才能继续操作。第二种是用乐观锁,在member_card表加version字段,更新时带WHERE version = ?,影响行数为0就重试。

毕业设计用第一种就够了,FOR UPDATE写法简单,逻辑直观,答辩时也容易解释。需要注意的是,FOR UPDATE必须在事务里才生效,所以@Transactional注解不能漏。

3. 项目落地:从空目录到前后端跑通的最小步骤

3.1 后端工程初始化:Spring Boot 3 + MyBatis-Plus

后端工程建议直接用Spring Initializr生成,不要手搭。选Java 17、Spring Boot 3.x、MyBatis-Plus、MySQL驱动、Lombok这几个依赖。用MyBatis-Plus而不是纯MyBatis的原因很简单:单表CRUD不用写XML,BaseMapper提供现成的selectById、updateById,省下的时间够你多写两个业务接口。

工程目录按功能分包而不是按技术分层:

com.example.beauty ├── controller # REST接口层 ├── service # 业务逻辑层 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── config # 拦截器、跨域、全局异常 └── common # 统一返回结果、枚举、工具类

按功能分包(比如把appointment相关的controller、service、mapper放一个包)在这个项目里不推荐,因为美容院业务表之间关联紧密,按层分包代码定位更直观,出了问题找文件也快。

application.yml里有两个配置容易踩坑。第一个是spring.datasource.url需要带useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,不然中文乱码和时区错乱一起找上门。第二个是MyBatis-Plus的逻辑删除配置,在application.yml写:

mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

实体类里对应的deleted字段上加@TableLogic,这样删除操作自动变成UPDATE ... SET deleted = 1,预约记录和会员卡都不会被物理删除,后面想恢复数据或者做报表统计还有后悔药吃。

3.2 小程序端:原生还是 uniapp,建议直接原生

微信小程序前端用原生写还是用uniapp,这个问题在学生项目里反复出现。如果只做微信端,原生是最好的选择,原因很实际:原生开发者工具调试直接、组件报错容易排查、代码量小。uniapp的优势是一套代码编译到多端,但你的项目只需要跑在微信里,没必要引入多一层编译链。更关键的是,原生小程序的wx.request封装起来也就几十行代码,没必要为这个上框架。

小程序端的目录结构按页面划分:

pages/ ├── index/ # 首页,项目展示 ├── booking/ # 预约页 ├── card/ # 我的会员卡 ├── order/ # 预约记录 └── mine/ # 个人中心

每个页面四个文件(.wxml、.wxss、.js、.json),这是微信小程序的固定结构。预约页需要处理好日期选择器和时间段选择器的联动,建议用picker组件的mode="date"和mode="selector"分别绑定日期和时间段,数据源从后端接口拉取当天可约时段。

3.3 数据库初始化:SQL脚本里要包含测试数据

数据库脚本是这个项目的门面,评审老师一定会打开看。一个完整的beauty.sql至少要包含建库语句、建表语句和测试数据三部分。建库语句用CREATE DATABASE IF NOT EXISTS beauty_db DEFAULT CHARACTER SET utf8mb4;,表结构里的created_time和updated_time统一用DATETIME类型并设置默认值CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP。

测试数据非常重要,而且要贴合业务。用户表里至少准备三五个顾客,会员卡表里要有储值卡、次卡、已过期卡各一张,预约表里要有已完成、待服务、已取消三种状态各一两条,项目表里要有面部护理、肩颈按摩、美甲这几个常见美容项目。这样你run起来后端接口一调试,所有列表都有数据展示,页面不会空荡荡。很多人的SQL脚本只有表结构没有数据,前端页面调通后一片空白,截图都没法截。

4. 关键代码实现:预约冲突、扣费事务、登录态这几个硬骨头

4.1 预约接口:事务、锁和状态机一起上

预约功能是前后端交互最多的地方,前端传过来staffId、serviceItemId、date、timeSlot,后端要做的事包括:校验顾客是否登录、校验时间段是否在营业范围内、检查目标美容师是否已被预约、扣减会员卡余额或次数、创建预约记录。这几步必须在一个事务里完成,任何一个环节失败都要整体回滚。

@Transactional(rollbackFor = Exception.class) public Appointment createAppointment(AppointmentRequest req) { // 1. 校验时间段格式,例如 10:00-11:00 TimeSlot slot = TimeSlot.parse(req.getTimeSlot()); // 2. 检查美容师是否已被预约 int conflict = appointmentMapper.countConflict( req.getStaffId(), req.getDate(), slot.getStart(), slot.getEnd()); if (conflict > 0) { throw new BizException(ErrorCode.STAFF_BUSY, "该时段美容师已被预约"); } // 3. 扣减会员卡余额或次数 boolean deducted = cardService.deductForAppointment( req.getUserId(), req.getCardId(), req.getServiceItemId()); if (!deducted) { throw new BizException(ErrorCode.BALANCE_NOT_ENOUGH, "余额不足或次数不足"); } // 4. 创建预约记录 Appointment appointment = new Appointment(); appointment.setUserId(req.getUserId()); appointment.setStaffId(req.getStaffId()); appointment.setServiceItemId(req.getServiceItemId()); appointment.setAppointDate(req.getDate()); appointment.setStartTime(slot.getStart()); appointment.setEndTime(slot.getEnd()); appointment.setStatus("PENDING"); appointmentMapper.insert(appointment); return appointment; }

这里有几个参数要注意。@Transactional(rollbackFor = Exception.class)不能省,Spring默认只在遇到RuntimeException时回滚,如果你抛的是受检异常,事务不会回滚,会出现“提示预约失败但卡里的钱已经扣了”的问题。TimeSlot.parse是把前端传的字符串时间段解析成开始时间和结束时间,前端约定好格式HH:mm-HH:mm,后端解析后存两个日期字符串。deductForAppointment内部先查卡的类型再决定扣余额还是扣次数,这就是前面说的行锁逻辑。

4.2 微信登录:wx.login 拿 code,后端换 openid

小程序端的登录不能像网页那样输入用户名密码,微信生态里的标准流程是wx.login获取临时code,传给后端,后端拿code去微信接口换取openid,然后以openid作为用户唯一标识。

// 小程序端 wx.login({ success: (res) => { wx.request({ url: 'https://yourdomain.com/api/auth/login', method: 'POST', data: { code: res.code }, success: (resp) => { const { token } = resp.data.data; wx.setStorageSync('token', token); } }); } });
// 后端 public String wxLogin(String code) { // 1. 调用微信接口换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code"; String resp = restTemplate.getForObject(url, String.class); JSONObject json = JSONObject.parseObject(resp); String openid = json.getString("openid"); if (openid == null) { throw new BizException(ErrorCode.WX_LOGIN_FAIL, "微信登录失败"); } // 2. 查库,用户不存在则自动注册 User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(0, 6)); user.setRole("ROLE_CUSTOMER"); userMapper.insert(user); } // 3. 签发 JWT String token = JwtUtil.createToken(user.getId(), user.getRole()); return token; }

这段代码里的appId和appSecret要配在小程序后台,后端从配置文件中读取。注意jscode2session接口有调用频率限制,如果某个用户短时间内频繁登录,微信会返回errcode45011,需要在后端做容错。JWT的过期时间建议设7天,过期后前端拦截401响应,自动跳转登录页重新走一遍wx.login,对用户来说是无感的。

4.3 拦截器:token 校验和角色鉴权

后端接口不能裸奔,需要写一个拦截器统一校验Authorization头里的token。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri = request.getRequestURI(); if ("/api/auth/login".equals(uri)) { return true; } // 从 header 获取 token String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BizException(ErrorCode.UNAUTHORIZED, "未登录"); } // 解析 token,拿到用户信息后放入 request 上下文 Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } }

记得在WebMvcConfigurer里注册拦截器,并设置excludePathPatterns放行/api/auth/**和静态资源路径。request.setAttribute这一步很关键,后面的service层直接从request里拿userId用,这样可以保证每个接口操作的都是当前登录用户的数据,不会出现越权。很多毕业设计的越权漏洞就是漏了这一层,用户A传个userId=2就能看到用户B的预约记录。

5. 避坑指南:本地能跑 vs 答辩能过是两回事

5.1 跨域问题:小程序不存在,但接口测试工具会遇到

做小程序开发时,wx.request天然不受浏览器同源策略限制,不需要在后端配CORS。但如果你用Swagger调试接口或者用Postman测试,你会发现接口能通,而用浏览器直接访问前端页面时,控制台报错No 'Access-Control-Allow-Origin' header is present。

解决方式两种:一种是在后端加一个CorsFilter的配置类,允许所有来源访问;另一种是写一个@CrossOrigin注解加在Controller类上。建议用配置类,因为接口多了以后不可能每个Controller都加注解。配置时allowedOriginPatterns("*")不要写成allowedOrigins("*"),后者在Spring Boot 2.4以上版本默认不允许携带凭证。

5.2 图片上传:别直接存本地磁盘

美容项目的宣传图、用户的头像,这些图片上传是躲不掉的功能。最省事的做法是前端把图片传给后端,后端存到服务器磁盘并把访问路径返回。但毕业设计的代码评审老师会看你的存储策略,直接存到/usr/local/upload这种本地路径的问题是:服务器重启后路径可能丢失、多实例部署时文件不同步、答辩现场临时换电脑文件就全没了。

更稳的做法是用云存储的对象存储服务,比如阿里云OSS或者腾讯云COS,后端拿到上传的文件后直接转存到云桶里,返回公网URL给小程序的<image>组件展示。用云存储需要配accessKeyId和accessSecret,这些密钥不要写死在代码里,放在application-dev.yml中用环境变量引用。如果你不想引入云服务依赖,也可以用七牛云,免费额度对毕业设计来说足够用了。

5.3 定时任务:会员卡过期提醒要会用 @Scheduled

会员卡有过期时间,过期后不能继续消费,这个状态变化有两种实现方式。一种是在查询时判断当前时间是否超过valid_end,动态计算状态;另一种是写一个定时任务每天凌晨扫描一次,把过期的卡状态批量更新为EXPIRED。两种都对,但答辩老师通常会问“如果用户在卡过期的前一秒发起消费怎么办”,用动态判断就能答上来——扣费接口里查卡时顺带校验valid_end > NOW(),不需要依赖定时任务保证实时性。

定时任务即使不做状态更新,也要用来做过期提醒。用@Scheduled(cron = "0 0 9 * * ?")每天早上9点扫一次,查出未来7天内过期的卡,给对应的用户发一条订阅消息。小程序订阅消息要用户在小程序里主动授权一次wx.requestSubscribeMessage,一次性订阅只能推一条消息,所以提醒功能的最佳实践是:发现有过期风险的卡时,先推送一条“您的会员卡将于X天后过期”,用户点击后再引导授权下一次订阅。

5.4 数据库连接池:默认配置不够用

Spring Boot 2.3以上默认用HikariCP连接池,默认maximumPoolSize是10。毕业设计并发量不大,10个连接通常够用,但有一个坑是:如果代码里有慢SQL或者事务忘记关闭连接,连接池会被占满,后续请求全部卡在获取连接的状态。表现就是接口偶尔正常偶尔超时,重启项目后又恢复。

建议配置显式放在application.yml里:

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000

另外排查连接泄露的方法是看MySQL的show processlist,如果看到大量Sleep状态的连接,说明有连接没释放。MyBatis-Plus的select操作一般不会出这个问题,手动写的SqlSessionTemplate操作要注意finally里关闭。

5.5 小程序预览和真机调试的差异

开发者工具里一切正常,一上真机就白屏,这种翻车基本是三个原因。第一个是wx.request的域名没配,小程序后台的“服务器域名”需要把https://yourdomain.com加到request合法域名列表里,开发模式下可以在工具里勾选“不校验合法域名”绕过,但真机预览时必须配好。第二个是本地http://localhost接口地址在真机上访问不到,真机访问的是局域网IP,要么把后端部署到云服务器,要么手机和电脑连同一个WiFi,把请求地址改成电脑的局域网IP。第三个是wx.login拿到的code只能用一次,5分钟内有效,如果调试时后端接口报错导致code被重复消费,就会看到errcode40029。

6. 查漏补缺与进阶技巧:做完增删改查之后,答辩还能问什么

当核心功能做完之后,绝大部分人的项目就停在了“能跑但只做完了增删改查”的状态。这个时候再往里加三个小能力,就能从“做了个系统”变成“有设计思想”。

第一个是支付回调的幂等处理。如果项目里接入了微信支付(可能只是模拟),顾客充值后微信服务器会异步通知后端“已支付”。由于网络原因,同一个支付结果可能通知两次,如果你的处理逻辑是“收到回调就把余额加一千”,那么两次回调会导致余额多加了一千元。解决方式是在recharge_record表里用transaction_id建唯一索引,处理回调前先查询这个流水号是否已存在,存在就直接返回成功,不重复入账。

第二个是数据备份和恢复。MySQL的mysqldump定时备份很多人知道,但不会主动写进文档。简历里写了一句“熟练使用MySQL”后,答辩老师问你“如果数据库崩了怎么办”,这个问题比业务逻辑更考验基本功。在后端项目的pom.xml里引入spring-boot-starter-quartz,写一个定时任务每天凌晨调用mysqldump命令导出SQL文件到服务器备份目录,保留最近7天的备份。这个功能做成一个/api/admin/backup接口触发,截图展示备份文件列表,视觉效果比任何流程图都直接。

第三个是接口文档的维护。用Springdoc自动生成OpenAPI文档,引入springdoc-openapi-starter-webmvc-ui依赖后,启动项目访问/swagger-ui.html就能看到所有接口的文档。答辩前把所有接口的注释写清楚,演示时打开这个页面逐个接口点过去调用,比对着Postman演示专业得多。

最后说一个习惯。每年帮人排查毕业设计项目,见的最多的问题是“我按教程敲的代码为什么报错”。排查第一步永远不是看代码,而是看控制台第一行报错。ClassNotFoundException优先检查依赖坐标、Table doesn't exist优先检查数据库有没有执行SQL脚本、401 Unauthorized优先检查是不是没带token。把这些排查路径记熟了,比多写一百行CRUD都有价值。希望这篇笔记能帮你的答辩少一点惊吓,多一点底气。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:41:36

VT-x/AMD-V硬件虚拟化无痕Hook:从VMCS配置到EPT页表操纵实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:41:27

MT4 DLL接口实战:从解压Demo到跨账户跟单系统搭建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:40:49

Keil MDK下STM32 printf重定向:MicroLIB、标准库与SWO三种方案详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:39:52

memtester 4.3.0内存测试实战:从安装到定位DDR故障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:39:31

ESP32控制SG90舵机避坑指南:接线、供电与调试全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华