☰
跑通SpringBoot+Vue+MyBatis美容院管理系统:从源码到部署的全解析
2026/9/26 4:41:22 网站建设 项目流程

把一套基于 SpringBoot + Vue + MyBatis 架构的美容院管理系统从源码跑通到什么程度,才算真正吃透了?我最近刚好完整地把这套带 MySQL 数据库的企业级源码翻了一遍,不只是启动起来看看页面,而是把后端接口、前端交互、数据库表结构和线上部署全都走了一遍。从实际体验来说,它不是一个只停留在空壳的模板,而是把会员档案、预约排班、订单收银、套餐卡次、库存预警、员工提成和营收报表这些美容院日常经营里绕不开的模块都串起来了。如果你是做 Java 课程设计、毕业设计,或者正准备接手一个中小门店的信息化项目,这篇文章应该能帮你省掉读源码和踩坑的不少时间。

下面我就按照实际开发中会遇到的顺序,把这套系统的设计思路、核心代码、数据库细节、部署问题和二次开发方向拆开讲一遍。文章里所有配置和代码示例,都是我在跑通这套源码时真正用到的写法,你照着操作基本不会卡壳。

1. 项目定位与技术选型:为什么是这套组合

1.1 这套系统到底解决了什么问题

美容院的线下业务链条其实比表面看起来要长。顾客进门不是简单买一次服务,而是要经历建档、预约、到店、划卡、购买产品、续充、评价这么一串过程。很多小门店还在用 Excel 手记,不仅容易漏,也根本没法统计哪个项目卖得好、哪个员工做了多少业绩。这套系统做的事情,就是把这些散落的业务节点全部结构化。

从功能上看,会员模块要能管理档案、余额、等级、累计消费;预约模块要能按项目和员工排班,并且自动挡住时间冲突;订单模块要能把服务项目、产品、卡次消耗都记清楚;报表模块要能按日、按周、按月统计营收和客单价。这些不是独立功能,而是靠一条完整的数据流串起来。源码里最大的优点,就是预约、订单、会员余额这三块不是各写各的,而是通过外键和状态字段形成闭环。

很多学生在做类似题目时,容易把重心放在“能增删改查”就行。但真正拿这套系统去用,你会发现事务、时区、并发扣款、分页查询、登录失效这些细节才是决定项目能不能落地的关键。这套源码好就好在,它把工程化的套路带进来了,不是纯 CRUD 堆砌。

1.2 为什么选 SpringBoot + Vue + MyBatis + MySQL 这套技术组合

有人会问我,做这种管理系统用 Python 或者 PHP 不是更快吗?但放在国内大部分课程设计、毕业设计和中小企业的实际情况里,SpringBoot 就是 Java 后端最稳妥的选择。它把 Tomcat、Spring 容器、自动配置全打包好了,写一个 Application 类就能启动,部署时一个 jar 文件搞定,不用像传统 SSM 那样配置一堆 XML。

Vue 的好处是组件化清晰,页面上的会员表格、预约日历、订单弹窗都能拆成独立组件维护。美容院系统的交互不算复杂,但信息量大,一个页面要同时展示很多状态,Vue 的数据绑定比 jQuery 手工操作 DOM 省心太多。Element UI 组件库也能直接弥补后台管理系统对表格、表单、弹窗、表格树的需求,开发速度非常快。

MyBatis 在这一点上有不可替代的优势:多条件动态查询。美容院的报表和筛选页经常出现“会员等级 + 服务项目 + 消费日期”这种任意组合,MyBatis 的<if>标签可以很优雅地拼 SQL。如果你用 JPA/Hibernate,反而需要花很多精力处理动态 Query 逻辑。MySQL 更不用说,它稳定、轻量、社区资料丰富,足够支撑这套系统未来很长一段时间的业务规模。

1.3 前后端分离的工程结构与模块划分

拿到源码后,不要急着扔到 IDEA 里跑。先看目录,这套源码大概率是分成 backend 和 frontend 两个独立工程。后端是标准的 Spring Boot Maven 结构,controller、service、mapper、entity、common 分得很清楚;前端是 Vue CLI 创建的工程,views 下面按业务模块建文件夹,api 目录里放所有接口请求方法。

开发环境时,前端 dev server 默认跑在 8081 端口,后端跑在 8080 端口。这里有一个容易踩坑的地方:如果前端直接用 axios 请求localhost:8080,浏览器会因为端口不同产生跨域。解决方式是在 vue.config.js 里配置代理,把/api前缀的请求转发到后端。很多同学以为跨域就得在后端写@CrossOrigin,其实开发阶段用 devServer.proxy 更干净,后端也不容易被全局跨域配置污染。

2. 数据库设计与核心业务模型

2.1 核心表结构逐张拆解

数据库是这套系统的地基,我建议你花一天时间把所有建表 SQL 看一遍。下面这张表把核心业务表的功能列出来,对应关系一目了然:

表名核心作用关键字段
member会员档案与余额phone 唯一、balance、level、total_consume
employee员工基础信息name、position、status、commission_rate
service_item服务项目定义name、price、duration、commission_rate
appointment预约记录member_id、employee_id、service_item_id、start_time、end_time、status
orders订单主表order_no、member_id、amount、pay_type、appointment_id
order_item订单明细order_id、type、item_id、price
account_log余额流水member_id、change_amount、balance_after、reason
inventory产品库存product_name、stock、warning_line

这些表不是随随便便设计的。比如 member 表里,手机号必须加唯一索引,因为它是会员卡号和登录账号;balance 用 decimal(10,2) 而不是 float,金额计算最怕精度丢失。service_item 表里的 duration 一定要存分钟数,这个字段后面在预约冲突判断里会用到。

建表语句里,表名和字段名尽量都用下划线风格,并且写 COMMENT。不要小看注释,后面自己维护时会非常感激当时写了这些的人。下面这段我稍微精简过的 member 建表语句,可以作为新建项目的参考模板:

CREATE TABLE `member` ( `id` bigint NOT NULL AUTO_INCREMENT, `phone` varchar(20) NOT NULL COMMENT '手机号,登录凭证', `name` varchar(50) DEFAULT '' COMMENT '会员姓名', `level` tinyint DEFAULT 0 COMMENT '等级:0普通 1银卡 2金卡 3钻石', `balance` decimal(10,2) DEFAULT 0.00 COMMENT '账户余额', `total_consume` decimal(10,2) DEFAULT 0.00 COMMENT '累计消费金额', `created_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员表';

注意字符集一定要用 utf8mb4。很多人只写 utf8,结果用户输入一个生僻字或者 emoji 表情,存储就直接报错或者乱码。数据库编码问题是最容易在后期爆雷的。

2.2 表关联关系与数据一致性要点

表与表之间的关系并不复杂。member 是主数据,orders 通过 member_id 关联;orders 通过 order_item 关联具体服务或产品;appointment 同时关联 member、employee、service_item 三方,是典型的多对一关系。逻辑上,一次到店消费的完整链路是:先建预约,到店后核销预约并创建订单,订单完成后再扣减会员卡次或余额。

这里最重要的设计思想是:预约和订单是两个环节,千万别混在一张表里。预约解决的是“谁、什么时间、找哪位员工、做什么项目”的排班问题;订单解决的是“消费了什么、应收多少、怎么支付”的交易问题。把这两个概念拆开,后续做日程表、排班表、员工业绩统计都会轻松很多。

数据一致性方面,我强烈建议你在二次开发时补上 account_log 流水表。因为每次充值、消费、退款都会改变 member.balance,如果只更新余额字段,以后查账会发现对不上。只要余额变动就插入一条流水,记录变动前后的值,这样出问题可以直接回溯。源码里如果本身已经有这张表,那更好;如果没有,自己加一张也很简单,只是需要让所有修改余额的地方都走同一个 service 方法,不要到处直接 update member。

2.3 初始化数据与索引设计实战

初始化脚本也是项目的一部分。管理员的密码不能以明文形式写在 SQL 里,必须是 BCrypt 加密后的字符串。哪怕是演示项目,我也建议你从第一天开始保持这个习惯,不然代码泄露出去,后台分分钟被登录。

索引不用建太多,但要建在刀刃上。手机号字段建唯一索引,用于登录和防重复建档;预约表的 appointment_time 建普通索引,因为日报表每天都要查当天预约;orders 的 create_time 建索引,营业额统计按天分组时才不会全表扫描;order_item 的 order_id 建索引,查订单明细会非常快。用 MySQL 的EXPLAIN SELECT ...可以直观看到查询是否走了索引,这是调优最基础的手段。

3. SpringBoot 后端实现关键点

3.1 依赖清单与代码分层

后端 pom.xml 里应该包含这些核心依赖:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、pagehelper-spring-boot-starter、lombok、java-jwt 或 jjwt。有些项目还会引入 spring-boot-starter-validation 来做参数校验。这里我建议你核实一下版本号,尽量用 Spring Boot 2.7.x 和对应的 mybatis starter,不要盲目上新版本,否则插件配置写法和源码里的演示代码不一致。

代码分层上,这套源码是标准的 Controller-Service-Mapper 三层。Controller 只负责接收参数和返回统一 Result 对象;Service 写真正的业务逻辑;Mapper 只做数据库访问。我见过不少项目把业务逻辑写在 Controller 里,改动一次就要动接口层,非常痛苦。分层最核心的意义是让每个类只做一类事,后面无论是改 SQL 还是换前端页面,影响范围都能控制住。

业务逻辑里,凡是涉及多张表写入的方法,必须在 Service 方法上加@Transactional。举个例子,会员充值时要做两件事:更新 member 表的 balance,插入 account_log 流水。如果第二步失败,第一步的余额变更就得回滚,否则账就平不了。事务默认只回滚 RuntimeException,所以不要在业务方法里随意 catch 异常后吞掉,这样事务会失效。

3.2 MyBatis 动态 SQL 与 PageHelper 分页配置

MyBatis 在这里最常被用到的玩法是动态 SQL。会员列表页一般有搜索条件:手机号、等级、注册时间。如果用字符串拼接 SQL 很容易碰到引号问题,而用 mapper XML 的<if>标签就非常安全。下面这段 SQL 就是典型的写法:

<select id="selectMemberByCondition" resultType="com.example.entity.Member"> SELECT * FROM member <where> <if test="phone != null and phone != ''"> AND phone LIKE CONCAT('%', #{phone}, '%') </if> <if test="level != null"> AND level = #{level} </if> </where> ORDER BY id DESC </select>

<where>标签会自动去掉第一个多出来的 AND,这个细节比手写WHERE 1=1要好一截。分页推荐使用 PageHelper。在 Spring Boot 中,如果已经引用了 pagehelper-spring-boot-starter,yml 里可以这么配:

pagehelper: helper-dialect: mysql reasonable: true

reasonable: true的作用是当页码小于 1 时自动显示第一页,大于总页数时自动显示最后一页,避免出现空白列表。代码里使用分页时要注意一个硬性规则:PageHelper.startPage 后必须紧跟着下一条查询语句才会被拦截生效。如果中间插入了其他查询,分页会错乱。这是新手最常见的使用错误。

3.3 JWT 登录鉴权与全局异常处理

前后端分离项目的登录状态不能靠 Session,因为 Vue 页面和后端接口通常不在一块。源码里一般用 JWT 生成 token,后端通过拦截器校验。我实际配置的时候会写一个 JwtInterceptor 类,实现 HandlerInterceptor 的 preHandle 方法,从请求头 Authorization 里取 token,解析成功后把 userId 放到 request attribute 里供后续使用。没带 token 或者 token 过期,就返回 401 和一段 JSON。

登录接口本身和静态资源要放行,所以拦截器注册时需要排除/api/auth/login、/error等路径。这个如果漏掉,会出现自己人却被自己人拦截的尴尬情况。我建议把放行路径集中写在配置类里,方便后面维护。

全局异常处理用@RestControllerAdvice。推荐拆三个方法:一个处理自定义业务异常,比如“余额不足”“预约时间冲突”;一个处理参数校验异常;一个处理兜底的 Exception。返回体统一是 Result 对象,包含 code、message、data。这样前端就能统一识别错误并弹出提示。千万不要让系统默认的 500 异常页直接暴露在用户面前,那样既丑又泄露内部细节。

3.4 预约冲突判断与会员充值事务

预约模块是整个系统的难点,也是最容易被做成“假功能”的地方。判断员工是否空闲,不能只看有没有同一个时间点的记录,而要看时间段是否重叠。假设预约从 start_time 开始,持续 duration 分钟,那么 end_time 等于 start_time 加 duration。新预约与已有预约冲突的条件是:新开始的 time 小于已有预约的结束时间,并且新结束的 time 大于已有预约的开始时间。对应 SQL 写法:

SELECT COUNT(*) FROM appointment WHERE employee_id = #{employeeId} AND status IN (0, 1) AND #{endTime} > start_time AND start_time < #{endTime}

这段 SQL 如果查出来 count 大于 0,就不能插入新预约。注意这里 status 要过滤,因为已取消、已完成的预约不应该参与冲突判断。Time 字段建议用 datetime,不要只存日期,否则做不了精确的排班判断。

会员充值接口要展示事务的典型用法。我先用原子的 SQL 更新余额,再插入流水:

@Transactional(rollbackFor = Exception.class) public void recharge(Long memberId, BigDecimal amount) { int count = memberMapper.increaseBalance(memberId, amount); if (count == 0) { throw new BusinessException("会员不存在"); } accountLogMapper.insert(memberId, amount, BalanceChangeType.RECHARGE); }

increaseBalance 对应的 SQL 是UPDATE member SET balance = balance + #{amount} WHERE id = #{memberId},这种写法天然避免“先查后改”的并发问题。如果不写事务,第二步插入流水失败,钱就凭空多了。

4. Vue 前端实现与交互细节

4.1 路由守卫与页面权限控制

前端第一个要处理的是登录跳转逻辑。vue-router 的全局前置守卫里写一段判断,没有 token 就强制跳到登录页。这是后台管理系统最基础的拦截。路由定义时可以在 meta 里存放角色信息,比如管理员才能访问员工管理页。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else { next() } })

这段代码非常简单,但已经能解决大部分页面白屏问题。真正企业级项目会把角色、菜单权限都做成动态渲染,就是从后端接口拿当前用户能访问的菜单列表,再动态生成路由。这套源码如果只做了静态路由,你可以在二次开发时慢慢改成动态的,不用一步到位。

4.2 Axios 拦截器与统一错误处理

前端请求层直接决定体验。我建议所有接口请求都走同一个 axios 实例,而不是每个页面单独 import axios。原因是能在拦截器里统一做三件事:加 token、解析业务错误码、处理 401 跳登录。

service.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.clear() router.push('/login') } Message.error(error.message) return Promise.reject(error) })

有人会问,为什么 code 不等于 200 还要 reject,而不是直接把数据返回?因为绝大多数页面只需要成功的数据,失败情况统一抛到调用方,再由调用方决定是否需要 special 处理。这套写法能减少很多if (data.code === 200) ... else ...的重复代码。

4.3 ECharts 报表展示与表单校验实战

管理系统里的报表页一定要做,不然到答辩时没有亮点。ECharts 图标库接入很简单,后端接口返回一个数组,前端 foreach 推进 option 里即可。比较常见的是年度营业额折线图和项目消费排行条形图。写报表组件时要注意一个真实存在的坑:容器 div 必须显式设置宽度和高度,否则 ECharts 初始化时拿不到尺寸,图表不显示。如果图表是在弹窗里渲染,必须用this.$nextTick等弹窗内容挂载后再 init。

表单校验方面,Element UI 的 rules 很好用。手机号校验用正则/^1[3-9]\d{9}$/,金额校验要限制两位小数。同时提交按钮要绑定this.$refs.form.validate(),否则即便填错了也能提交成功。还有一个容易忽略的点:前端精度和金额不要用 JavaScript 浮点数直接计算,例如 0.1 + 0.2 会得到 0.30000000000000004。前端只是展示,真正的金额计算与合法校验必须在后端完成。

5. 部署上线与常见问题排查

5.1 从源码到生产环境的完整打包步骤

本地跑通后,离上线就差打包这一步。后端正向操作很简单,在 pom.xml 所在目录执行:

mvn clean package -DskipTests

然后把 target 目录下生成的 jar 传到服务器,准备 application-prod.yml 配置好生产数据库地址,启动时指定 profile:

java -jar beauty-admin.jar --spring.profiles.active=prod

前端先修改.env.production文件中的VUE_APP_BASE_API,比如设为/api,再执行:

npm run build

构建完成后会生成 dist 目录,把这个目录放到 Nginx 配置的 web 根路径下,再配置反向代理,将/api的请求转发给后端的 8080 端口。Nginx 配置片段如下:

location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

这里最核心的思路是尽量减少跨域,前端请求的地址看起来和页面域名一致,Nginx 在服务端转发。生产环境尽量不要让后端暴露 CORS 全局允许,风险太大。

5.2 上线后最容易踩的五个坑

第一个坑是时区。MySQL 连接串里必须带上serverTimezone=Asia/Shanghai,否则新版驱动会报链路错误。如果部署后发现所有时间都差了 8 小时,先查 JVM 时区,启动命令加-Duser.timezone=Asia/Shanghai。

第二个坑是数据库编码。建库时就用utf8mb4,否则汉字没问题,用户输入 emoji 就会报错。

第三个坑是前端路由 history 模式刷新 404。Nginx 需要追加一行:

try_files $uri $uri/ /index.html;

不加这一行,用户只要按一下 F5,页面就是 404。

第四个坑是数据库连接池空闲超时。如果系统隔了一夜没人访问,第二天第一次请求可能报“Communications link failure”。解决办法是在 Hikari 配置里加connection-test-query: SELECT 1,并合理设置空闲超时时间。

第五个坑是账号密码等敏感信息硬编码在 yml 里。虽然这个项目是教学用途,但你一旦上线,就要把密码挪到环境变量或配置中心。安全习惯越早养成越好。

5.3 数据库备份与系统安全加固

上线后的数据是无价的,每天至少要自动备份一次。可以用 crontab 执行定时任务:

0 2 * * * mysqldump -u beauty_user -p'密码' beauty > /backup/beauty_$(date +\%Y\%m\%d).sql

注意这里%在 crontab 里要写成\%,否则不会按天生成文件名。建议保留最近七天,加一行清理命令删除过期备份。

安全方面我一直坚持几个底线:后台登录必须加图形验证码,防止暴力破解;数据库账号不要用 root,新建一个只授权业务库的账号;密码不存明文,统一用 BCrypt 加密;文件上传接口必须校验文件类型和文件头,防止有人传恶意脚本。这套系统涉及客户信息和资金交易,安全不是可选项,是必须做的事。

6. 二次开发与扩展方向

6.1 小程序端与移动端能力扩展

这套系统的后端接口已经按 JSON 格式提供,天然可以给微信小程序复用。你只需要在小程序里配置合法的域名,使用wx.request调用后端/api接口,登录时把后端返回的 token 存入wx.setStorageSync。小程序端可以做一个顾客入口,让会员查看余额、卡次、预约记录,并在线提交预约申请。改造点在于小程序没有浏览器 localStorage,接口请求需要重新封装一个 request 函数,但逻辑和前端拦截器完全一致。

如果要做微信登录,还需要增加一个根据 openid 查找或创建会员的接口,并在 member 表新增 openid 字段。这个扩展不难,工作量主要集中在小程序的界面适配。

6.2 营销活动与消息通知集成

美容院最关心的其实是复购和到店率。可以在这套系统上增加两个增量功能:一是短信提醒,预约前一天自动给会员发提醒消息;二是储值赠送规则,比如“充 1000 送 100”,在充值接口里判断规则表后追加赠送金额。这两个功能都离不开定时任务。Spring Boot 可以引入 spring-boot-starter-quartz,每天固定时间扫描 appointment 表,把 start_time 在第二天的预约查出来,批量调用短信服务商接口。

营销活动千万不要在原有代码里到处写 if else,推荐单独建一张 promotion_rule 表,配置好满减条件,只改配置不动代码,这样以后非技术人员也能在后台维护活动。

我个人在实际操作里最大的体会是:拿到这套源码别急着改界面,一定要先把数据库表关系画出来,再顺着一次“预约→到店→下单→扣款”的流程把代码走一遍。三次下来,你对 SpringBoot、Vue、MyBatis、MySQL 这整套组合的把握,比单独看十个教程都强。遇到问题优先看启动日志和控制台报错,不要靠猜。这套项目的下限是能跑通,上限完全取决于你在表结构和接口规范上愿意投入多少心思。

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

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

立即咨询