
简介基于微信小程序与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.10.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不用写XMLBaseMapper提供现成的selectById、updateById省下的时间够你多写两个业务接口。工程目录按功能分包而不是按技术分层com.example.beauty ├── controller # REST接口层 ├── service # 业务逻辑层 ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── config # 拦截器、跨域、全局异常 └── common # 统一返回结果、枚举、工具类按功能分包比如把appointment相关的controller、service、mapper放一个包在这个项目里不推荐因为美容院业务表之间关联紧密按层分包代码定位更直观出了问题找文件也快。application.yml里有两个配置容易踩坑。第一个是spring.datasource.url需要带useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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组件的modedate和modeselector分别绑定日期和时间段数据源从后端接口拉取当天可约时段。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_typeauthorization_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传个userId2就能看到用户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 doesnt exist优先检查数据库有没有执行SQL脚本、401 Unauthorized优先检查是不是没带token。把这些排查路径记熟了比多写一百行CRUD都有价值。希望这篇笔记能帮你的答辩少一点惊吓多一点底气。本文还有配套的精品资源点击获取