简介:这是面向毕业设计、课程设计与项目实战的微信小程序美容院管理系统完整项目包,采用小程序前端与 Java 后端分层实现,围绕美容院日常管理场景设计,适合计算机相关专业学生参考、复现与二次开发。包内共 1404 个文件,压缩后约 15.86MB:Java、Vue、JS 覆盖后端接口与前端业务逻辑,WXML/WXSS 与 PNG/SVG 提供小程序页面结构和界面素材,SQL 脚本用于初始化数据库,还包含说明文档、启动脚本及备份配置,目录划分清晰。压缩包内附带 3 个一键安装、运行、构建的 bat 脚本和 .classpath 等工程配置,可从零开始搭建调试环境,快速验证完整业务流程。已有 318 人学习使用。对于需要完成美容行业管理系统选题、理解前后端联调或搭建可用原型的学习者,这份资源能提供一套可直接落地的参考方案。
1. 从一坨“跑不起来的项目”到能答辩的美容院系统:这份资源到底给了什么
毕业设计最怕的不是不会写代码,而是拿到手的项目缺数据库、缺依赖、前后端对不上。基于微信小程序+java后端的美容院管理系统,把源码、数据库、说明文档三样东西打包在一起,补齐了一条能完整跑通的小程序预约→后端接口→管理端维护的链路。它能解决的是从“有一堆文件”到“能演示、能答辩、能按评委要求改”的整个过程。这套资源适合计算机专业准备毕设的学生,也适合课程设计小组快速拿来做前后端分离项目的底子。下面我从文件结构、启动脚本、常见坑、答辩优化四个角度把它拆开。
2. 系统结构拆解:小程序端、Vue管理端、Java后端各自管哪一块
2.1 从文件命名倒推项目结构
资源包里有一串带 .bak 后缀的文件:IndexMain.vue.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak。从命名就能看出,管理端是 Vue 单页应用,页面被拆成了头部(IndexHeader)、侧边栏(IndexAsideStatic)、面包屑(BreadCrumbs)和主内容区(IndexMain),这套布局骨架在若依一类的后台管理模板里非常常见。.bak 是开发过程中的备份文件,也就是说原工程在改样式或功能时保留了上一版,方便随时回滚。
项目里同时出现1-install.bat、2-run.bat、3-build.bat三个脚本,说明交付方已经把运行顺序安排明白了:先装依赖,再启动后端,然后构建前端。这个顺序和多数 Spring Boot + Vue 的前后端分离项目一致。微信小程序端负责给顾客用:浏览服务项目、预约、查看会员信息;Java 后端负责提供 REST 接口和数据库读写;Vue 管理端给美容院员工用,录入会员、排班、处理预约。
2.2 后端接口链路:从小程序请求到数据库返回
后端主体是 Java,常见做法是 Spring Boot + MyBatis Plus。以会员管理为例,小程序里顾客登录后要查自己的余额和卡项,请求会先落到 Controller,再走到 Service,最后通过 Mapper 操作数据库。下面是一段接口示意:
@RestController @RequestMapping("/api/member") public class MemberController { @GetMapping("/info") public Result<MemberInfo> getMemberInfo(@RequestHeader("token") String token) { MemberInfo member = memberService.getInfoByToken(token); return Result.success(member); } }逻辑说明:@RequestHeader("token")要求请求头必须带上令牌,小程序端在wx.request的header里放token,管理端在 axios 拦截器里放同样的 token。这样一套后端接口可以同时被两端复用,不用各写一套。参数说明:真实项目里token一般取wx.login的 code 换来的 openid 后生成的会话标识,不要直接把 openid 当 token 用,否则泄露到前端会有安全风险。这里返回的是统一封装Result,包含 code、msg、data 三个字段,前端判断 code 是 0 才取 data。
再往下看,MyBatis Plus 的 Mapper 通常长这样:
public interface MemberMapper extends BaseMapper<Member> { @Select("SELECT * FROM member WHERE phone = #{phone}") Member selectByPhone(String phone); }这个BaseMapper已经提供了selectById、selectList、insert等常用方法,只要实体类字段和表字段映射好,大部分增删改查都不用写 SQL。需要注意@TableName标注表名,@TableField标注字段名,比如@TableField("card_expire_time") private LocalDate cardExpireTime;,驼峰转下划线是默认规则,但如果你改了默认配置,就一定要显式标注,否则运行时报字段不存在。参数说明:@Select注解适合简单查询,复杂统计建议写到resource/mapper/MemberMapper.xml里,避免 SQL 字符串拼接。
2.3 数据表设计:美容院系统最少需要这几张表
数据库是 MySQL,初始化脚本里通常会建六张核心表:会员表、员工表、服务项目表、预约表、订单表、卡项套餐表。表格如下:
| 表名 | 关键字段 | 用途 |
|---|---|---|
| member | id, name, phone, balance, level | 会员基础信息和余额 |
| staff | id, name, position, status | 美容师/理疗师档案 |
| service_item | id, name, duration, price, status | 可预约的项目 |
| appointment | id, member_id, staff_id, item_id, start_time, end_time, status | 预约排班记录 |
| order | id, order_no, member_id, amount, pay_type | 消费订单 |
| member_card | id, member_id, card_type, balance, expire_time | 储值卡/套餐卡 |
设计要点:预约表里一定要有start_time和end_time两个字段,不要只存一个日期或时段,否则后面做冲突检测非常痛苦。appointment.status建议取值范围是 0 待确认、1 已确认、2 已完成、3 已取消,这个状态枚举要和小程序端、管理端的下拉框保持一致。
建表脚本示例(SQL):
CREATE TABLE `appointment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `member_id` bigint(20) NOT NULL COMMENT '会员ID', `staff_id` bigint(20) NOT NULL COMMENT '员工ID', `item_id` bigint(20) NOT NULL COMMENT '服务项目ID', `start_time` datetime NOT NULL, `end_time` datetime NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_member` (`member_id`), KEY `idx_staff_time` (`staff_id`, `start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:联合索引(staff_id, start_time)是给冲突检测查询用的,MySQL 会快很多。注意status用tinyint不用varchar,避免枚举值写错。时间字段统一datetime,不要混用timestamp。
2.4 管理端页面与接口怎么对拍
前后端分离项目里,页面显示空数据的原因大多不是没数据,而是字段名对不上。管理端的服务项目表格列绑定的是prop="serviceName",但后端返回的是name,页面就空白。我一般打开浏览器 F12 看 Network 里的响应 JSON,再和表格列的 prop 逐个对。比如前端请求GET /api/service/list,返回值长这样:
{ "code": 0, "data": [ { "id": 1, "serviceName": "深层清洁", "duration": 60, "price": 268 } ] }那后端 VO 里就必须有serviceName这个字段名,否则用name就会翻车。管理端 Vue 页面怎么发请求?在api/service.js里,常见写法是:
import request from '@/utils/request' export function listService(params) { return request({ url: '/service/list', method: 'get', params }) }然后在service.vue的created()里调用listService({ page: 1, size: 10 }),把返回的data.records赋值给表格的tableData。逻辑说明:这里request就是 axios 实例,统一设置 baseURL 和 token。参数说明:分页参数尽量用page和size,和后端 PageHelper 或 MyBatis Plus 的分页参数保持一致,避免管理端页数对不上。
这个对拍过程是联调工作量的主要来源。拿到资源包后,建议优先把服务列表、预约列表、会员列表这三个接口和页面都过一遍,能跑通这套,其他模块基本是一样的套路。
3. 本地环境搭建:install、run、build 三件套怎么落地
3.1 环境检查与初始化
拿到资源包后,我习惯先看1-install.bat干什么。这类项目一般需要 JDK8+、Maven3.6+、Node.js 14+、MySQL 5.7/8.0。先检查:
java -version mvn -v node -v mysql --version如果哪一项提示“不是内部或外部命令”,就去对应的环境变量里补。这里最常见的坑是电脑里装了多个 JDK 版本,JAVA_HOME指向了 JDK17,但项目用的是 JDK8,启动报UnsupportedClassVersionError。解决方式是把JAVA_HOME切到 JDK8 路径,或者在 IDE 里指定 Project SDK。这类问题属于环境问题,不把环境理顺,后面每一步都会被连累。
提示:优先把解压路径和目标文件夹全部改成英文,Maven 和 Node 在中文路径下偶尔会抽风,报错也看不出原因。
3.2 数据库导入:先把地基打好
数据库脚本一般在sql目录或根目录下,名字类似db_beauty.sql。导入命令:
mysql -u root -p -e "source /你的路径/db_beauty.sql"导入后进 MySQL 确认表数量和种子数据:
USE db_beauty; SHOW TABLES; SELECT COUNT(*) FROM member;如果SELECT COUNT(*) FROM member返回 0,说明脚本里只有表结构没有数据,管理端页面会空。我一般会至少造五到十条会员记录,方便后面演示预约流程。这里的坑在后面专章讲,先记住一个判断标准:导入脚本后能查到种子数据,才算这一步做完。
3.3 后端启动:脚本跑还是 IDE 跑
2-run.bat内部大概率是这两条之一:
mvn spring-boot:run java -jar target/beauty-system.jar我一般建议先试1-install.bat,它会对后端执行mvn install把依赖拉下来,再试2-run.bat。如果你在 IDE 里跑,直接在Application类的main方法上点运行。启动成功后,控制台会出现Tomcat started on port(s): 8080,然后访问http://localhost:8080/api/member/info看是否返回 JSON。如果出现Unable to find Spring Boot之类的错误,检查 pom.xml 里的 parent 版本和本地 Maven 仓库是否一致。
后端配置集中在application.yml或application-druid.yml。一个典型的数据库连接配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/db_beauty?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver逻辑说明:serverTimezone=Asia/Shanghai必须加,不加会出现第 4 章那个时区问题。useSSL=false避免本地连 MySQL 报 SSL 警告。参数说明:如果你用 MySQL 5.7,驱动可以换成com.mysql.jdbc.Driver,但 8.x 之后推荐用cj驱动。
3.4 管理端前端跑起来
管理端是 Vue 工程,3-build.bat是打包用的,本地调试不要先 build,而是先装依赖再起 dev server:
npm install npm run dev启动后看http://localhost:8089,如果页面出来了但接口报 401 或跨域,需要在vue.config.js里配代理:
module.exports = { devServer: { port: 8089, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }逻辑说明:devServer.proxy把前端发的/api开头的请求转发到后端 8080,这样浏览器看到的就是同源请求,不会触发 CORS。参数说明:changeOrigin设为 true 很重要,它会把请求的 Host 头改成 target 的域名,否则后端若校验 Host 会拒绝。
3.5 小程序端导入与请求地址
用微信开发者工具导入小程序目录,第一次导入会提示“未注册 AppID”,选择测试号即可。然后打开config.js或utils/request.js,把全局接口地址改成:
const BASE_URL = 'http://localhost:8080'说明:模拟器里 localhost 指向你的电脑,所以能通。真机预览时,手机和电脑要在同一 Wi-Fi,把BASE_URL改成电脑的局域网 IP,比如http://192.168.1.5:8080,同时后端启动时加上--server.address=0.0.0.0,防火墙放行 8080 端口。这一步卡住的人很多,主要原因是只改了前端没改后端绑定地址。
注意还要在微信开发者工具右上角详情里勾选“不校验合法域名”,否则请求接口直接被拦截。这个开关只在开发调试阶段有效,真机预览时同样要开,除非你已经在小程序后台配置了业务域名。
4. 避坑手册:这五个问题最容易让答辩现场翻车
4.1 个人小程序拿不到授权手机号
现象:小程序端点“微信一键登录”,模拟器能弹窗但真机一直报错,或者压根没有获取手机号按钮。
原因:微信官方接口getPhoneNumber要求小程序为企业认证主体,个人开发者无权调用。2023 年后获取手机号还改成了动态验证码模式,必须在用户开通时填验证码。
解决:答辩演示不要依赖这个接口,改成账号密码登录,手机号做成表单里的可编辑字段,后端存进 member 表。如果想要“微信登录”的体验,用wx.login拿 code 去后端换 openid,再拿 openid 关联本地账号。这样既避开了资质限制,也演示了微信登录流程。
4.2 管理端跨域报错,小程序反而没事
现象:Vue 管理端在浏览器里请求后端,Console 报Access-Control-Allow-Origin,而小程序端同样请求后端却很顺畅。
原因:小程序不是浏览器环境,没有同源策略限制;浏览器管理端才需要服务端返回 CORS 头。很多新手在前端修来修去,其实问题在后端。
解决:在后端加一个全局过滤器,统一加三个响应头。参考代码:
@Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, token"); chain.doFilter(req, res); } }逻辑说明:OPTIONS预检请求需要直接放行,避免浏览器拦截 PUT/DELETE。参数说明:Access-Control-Allow-Headers里必须包含你前端实际用到的所有自定义头,最常见的就是token,漏了就报“Request header field token is not allowed”。
4.3 MyBatis Plus 自动建表把数据表搞乱
现象:后端启动后数据库自动生成几十张表,字段和 .sql 脚本里的对不上,原来的数据也查不到了。
原因:项目里开了spring.jpa.hibernate.ddl-auto或 MyBatis Plus 的auto-ddl自动建表配置。自动生成的表结构是实体类推出来的,字段顺序和注释都丢了。
解决:关闭自动建表,手动执行 .sql 脚本。Spring Boot 配置里加一行spring.jpa.hibernate.ddl-auto=none;MyBatis Plus 则在application.yml里去查auto-ddl相关配置。更简单的方式是把 .sql 脚本导出的库名改成另一个库名,确保自动建表不影响手动数据。
4.4 .bak 文件到底是备份还是主文件
现象:改页面时习惯性打开IndexMain.vue.bak,改了半天发现不是生效的那个文件,页面没有变化。
原因:.bak是开发时留的备份,IDE 和编辑器默认不会把它当源码编译。有些交付方会把最终版命名为 .bak,主文件反而被覆盖,造成“打开的能跑,正常文件跑不了”。
解决:先看同目录下是否有不带 .bak 的同名文件,有就以不带 .bak 的为准;如果只有 .bak,复制一份去掉后缀名再打开。第二步是搜索入口文件里import或src引用路径,确认当前真正加载哪个文件。
4.5 预约时间少了 8 小时
现象:管理端看到的预约时间是 14:00,数据库存的却是 06:00,差了一整个白天。
原因:JDBC 连接时区没设置,MySQL 默认用系统时区,Java 进程用北京时区,串了。
解决:在 jdbc url 加serverTimezone=Asia/Shanghai,同时检查 Java 的 Jackson 配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8参数说明:time-zone: GMT+8是固定 +8 小时,比写Asia/Shanghai更直白,不会受系统时区策略影响。改完重启后,用一条 SQL 查SELECT start_time, CONVERT_TZ(start_time, '+00:00', '+08:00')验证是否恢复。
5. 进阶优化:把系统调到能答辩、能加分的水准
5.1 小程序自定义导航栏高度适配
资源包里的小程序可能用了系统默认导航栏,但你拿到手做二次开发时,难免要改成自定义导航栏。自定义顶部导航栏高度在不同机型上不一致,iPhone X 系列的刘海屏和 Android 主流机型相差几十像素。适配代码:
Page({ data: { statusBarHeight: 20, navBarHeight: 44 }, onLoad() { const windowInfo = wx.getWindowInfo() const menuRect = wx.getMenuButtonBoundingClientRect() this.setData({ statusBarHeight: windowInfo.statusBarHeight, navBarHeight: (menuRect.top - windowInfo.statusBarHeight) * 2 + menuRect.height }) } })逻辑说明:wx.getWindowInfo()返回statusBarHeight,wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置,用它计算自定义导航栏高度,能保证右侧按钮不重叠。参数说明:计算方式是胶囊顶部减去状态栏高度,得到导航栏上下内边距,再乘以 2 加上胶囊高度,这是官方社区里最常见的公式。在app.json里同时设置"navigationStyle": "custom",否则页面的胶囊按钮会重复出现。
5.2 预约冲突校验:一个条件查询就够了
预约模块最容易被评委追问“并发怎么办”。哪怕你项目里只有单机,也要把 SQL 写成能防冲突的形状。核心查询:
SELECT COUNT(*) FROM appointment WHERE staff_id = #{staffId} AND status IN (0, 1) AND start_time < #{endTime} AND end_time > #{startTime}逻辑说明:查询同一员工在目标时间段内是否已有状态为待确认或已确认的预约。两个区间交叠的判断是start_time < 新结束时间 AND end_time > 新开始时间,这个条件把包含、交叉、邻接都覆盖了。参数说明:status IN (0,1)排除了已取消和已完成,避免旧记录阻塞新预约。在 Service 里包一层@Transactional,然后先做查询再做插入,防止两个请求同时进来。
还可以给 appointment 表加一个唯一约束,但 MySQL 对范围唯一不好做,面试时能说出“用排它锁或唯一预约号”就算过关。不过最稳妥的还是事务 + SQL 条件查询。
5.3 操作日志:给“毕业设计”加一层管理员的底
管理端录入数据之后,评委很可能问“怎么知道是谁改的”。加一个操作日志比想象中简单。用 Spring AOP 切@Log注解,记录模块、操作类型、操作人、IP、耗时。简化版切面:
@Aspect @Component public class LogAspect { @AfterReturning("@annotation(operLog)") public void saveLog(JoinPoint joinPoint, Log operLog) { String module = operateLog.module(); String username = getCurrentUsername(); logService.save(module, username, LocalDateTime.now()); } }逻辑说明:@AfterReturning定位到被@Log注解标注的方法,方法正常返回后写入一条日志。参数说明:module()是注解里的元数据,比如“会员管理”“预约管理”,这样日志表能按模块过滤。这个切面不用侵入业务代码,只要在 Controller 方法上加@Log(module = "会员管理")就行。
日志表结构大概是id, module, username, action, ip, create_time,字段不用多,够看即可。注意不要记录密码等敏感字段,切面里要对入参做脱敏。另外建议把@Log放在 Controller 层,不要放 Service 层,因为 Controller 能拿到完整的用户上下文。写完这层,系统就有了“谁在什么时间改了哪些数据”的审计链路。答辩时讲到这一节,比单纯重复“增删改查”有说服力得多。
三块都落地后,系统就从小程序的单点演示,变成一个前后端能闭环、能应对追问的完整项目。
6. 交付前的验证清单:五步确认系统没白跑
拿到这套美容院管理系统后,我每次都会强制自己走一遍最小验证流程:第一步,清空数据库重新导入 .sql,避免依赖之前手动插入的临时数据;第二步,用 Postman 把登录、服务列表、创建预约、会员查询四个核心接口跑一遍,确认返回格式统一;第三步,启动 Vue 管理端走一次“登录→新增服务项目→编辑→删除”,重点看 F12 的 Network 有没有红色报错;第四步,在微信开发者工具里走一遍“登录→选项目→选时间→提交预约→在管理端确认”;第五步,把数据库 binlog 关掉,重新从一个干净的 .sql 导出为db_beauty_final.sql交付。
这里最容易被忽略的是第二步和第四步的接口字段一致性。我自己的习惯是拿一个小程序端的请求参数,直接复制到 Postman 里发,发现参数名不一致就立刻改后端 VO 或前端提交体,不要等到联调再改。改完再跑一遍,直到两边完全一致。
另一个后悔药技巧是初始化 Git 版本管理。拿到资源包后,先不要急着改代码,第一步就做git init并提交原始版本。之后每完成一个功能点就 commit 一次。这样后面无论是改崩了 Vue 页面还是把 application.yml 改坏,都能用git checkout回到上一个能跑的版本。我见过太多人把项目改崩后只能求朋友重新发一份压缩包,其实一份 Git 历史就是最好的“后悔药”。
从那以后,我每次接触别人的毕业设计项目,都会先跑通最小验证流程、建立版本基线,再动业务代码。这个习惯让我省下了大量重复排查时间。希望帮到你。
本文还有配套的精品资源,点击获取