☰
SpringBoot+Vue全栈实践:美发店管理系统设计与部署全解析
2026/10/6 14:22:14 网站建设 项目流程

“美发店管理系统”这个标题,乍一看真的很像毕业设计或培训机构练手项目,我最初接这个需求的时候也下意识觉得:不就是顾客、服务项目、订单三个模块的增删改查嘛,有什么难的。但真正蹲在门店里观察了两天业务流程之后,我发现完全不是这么回事。一套能被美发店日常真正用起来的管理系统,难点根本不在“管理”这两个字的表面,而在预约调度、会员储值、员工提成这三块既琐碎又互相牵连的业务逻辑上。这篇文章我就完整回溯一下自己基于 SpringBoot+Vue+MyBatis+MySQL 开发美发门店管理系统的全过程,从需求拆解、数据库建模、后端接口设计、前端页面组织,一路讲到部署上线时踩过的坑。如果你正准备接手类似的垂直行业管理系统,或者想拿这个项目作为技术练手、给自己的简历补一个完整的全栈项目,这篇文章应该能帮你少走不少弯路。

1. 需求拆解:一个美发店系统里藏着哪些“隐形”业务

很多人拿到“美发店管理系统”这种题目,第一反应就是做界面、做菜单,但业务方真正会怎么用这套系统,才是决定开发量的核心。我建议你动手前先坐在前台待半天,看看一天里店员都在做什么:接电话、登记到店顾客、安排发型师、开单、收银、充卡、记考勤……这些动作落到系统里,就是一条完整的主数据链。

1.1 四类角色看到的功能完全不同

美发店不是只有一个“管理员”角色,实际运转至少涉及四类人,权限和界面差异很大:

  • 店长/老板:看经营日报、卖卡统计、员工业绩、预约情况、库存进货,一般不下场操作具体的预约和收银。
  • 前台:最核心的使用者,负责登记顾客、安排预约、开消费单、收银、办卡充值、退卡退款。
  • 发型师/技师:查看自己的当日预约、更新服务状态、记录顾客偏好、查看个人业绩。
  • 会员(顾客):虽然很多门店初期只在收银台用系统,但系统必须预留会员端查询入口,如查余额、查积分、查看消费记录、在线预约。

如果把四类角色的菜单一拉,你就会发现功能矩阵比想象中宽:员工管理、排班管理、预约管理、服务项目管理、会员管理、会员卡与储值流水、订单收银、折扣与积分、库存进货、提成结算、经营报表,十几个模块没有一个是可以省略的。我见过不少把系统只做到“订单管理”就算完的项目,那种东西上线一周就会被前台抛弃,因为解决不了她每天最痛的预约冲突问题。

1.2 核心业务主线:预约-到店-消费-结算-提成

美发店和餐饮、零售最大的区别在于:它的商品是“发型师的时间”,而时间是不可复用的资源。所以业务主线一定长这样:

  • 顾客通过电话、到店或线上发起预约,明确要找哪位发型师、做什么项目;
  • 预约到店后前台确认签到,安排到具体工位;
  • 发型师开始服务,过程中可能临时增加烫染项目;
  • 服务结束开单结算,使用余额、会员卡折扣或现金扫码支付;
  • 支付完成后按项目计提成,记录到员工当月业绩中。

这一条线走下来,每一环都会产生跨模块的数据变动。预约会占用排班时间,结算会影响会员余额和会员等级,提成需要按照服务项目和员工级别拆分。所以开发时千万不要按菜单一个一个做,而是要先画清这条主线,再决定表结构和接口顺序。如果一开始就按“用户模块、订单模块、商品模块”这种教科书式结构搭,后期必然面临大量接口重构。

1.3 容易被忽略的隐形需求

除了明面上的功能,还有几个隐性需求是决定系统是否耐用的关键:

  • 爽约与等待机制:顾客迟到、预约未到店怎么办,是否支持预约改签、排队候补。
  • 不同发型师不同定价:同一个剪发项目,总监、高级技师、普通技师的单价不同,提成比例也不同。
  • 会员卡不止一种:有储值卡、次卡、折扣卡,还有充值赠送规则,比如充3000送300,这笔赠送金额如何记账。
  • 服务时长差异:剪发可能30分钟,烫发可能3小时,预约时间片必须按项目时长动态计算,不能简单用固定时段格子。

把这些需求带进设计,系统才不会做出来“像系统但没法用”。我在实际项目里有个习惯:每个模块必须用两句大白话写清使用场景,比如“前台在周一早上打开今日预约页,看到10点王姐要染发,点名找Tony,预计2小时”,然后所有表字段和接口都去满足这类场景。

2. 为什么2025年依然会选SpringBoot+Vue+MyBatis+MySQL这套组合

这套组合在Java圈子里的讨论热度一直没降过,原因很实在:它不追求新奇特,但工程化成熟度极高,招聘市场上的人也好招,出了问题网上一搜全是方案。尤其对这种业务规则复杂的中后台系统,稳定压倒一切。

2.1 前后端版本怎么选最稳妥

我在这个项目里后端用的是 Spring Boot 3.2,JDK 17,前端 Vue 3 + Vite + Element Plus + Pinia,数据库 MySQL 8.0。如果你所在公司服务器环境比较旧,只能跑 JDK 8,那 Spring Boot 2.7.x 也是完全够用的。这里有一个经验:不要盲目追新。Spring Boot 3.x 对 JDK 版本有硬性要求,如果你的生产环境还是 CentOS 7 里装的老 JDK 8,强行升 Boot 3 会带来一堆迁移成本,收益却不大。

前端方面,默认脚手架选 Vite 而非 Vue CLI,创建速度快、构建速度快,配置也直观。组件库我用 Element Plus,表格、表单、弹窗、日期选择器、日历组件都有现成的,对后台管理类页面来说,自己从零写组件的性价比非常低。Pinia 替代 Vuex 管理登录状态和全局信息,更简单,TS 支持也更好。

2.2 为什么在这个项目里坚持用 MyBatis 而不是 JPA

这套系统的主要页面几乎都是多表关联查询,比如“某发型师某天某时段是否可预约”“某会员的储值流水和订单汇总”“某员工当月提成明细”,这些查询天然需要可控的 SQL。MyBatis 最大的好处就是 SQL 完全在你的手里,怎么连表、怎么聚合、怎么加条件,清清楚楚,性能问题也一眼能定位。而 JPA/Hibernate 在复杂查询下很容易生成性能不可控的 SQL,出了问题反而要花大量时间调教。

另外,MyBatis 的 XML 映射文件对团队协作很友好。CRUD 写注解简单,复杂查询写 XML,格式统一、可读性好。哪怕是一个刚毕业的同学接手,看几天 XML 也能快速上手。配合 PageHelper 做分页,手写几个一对一、一对多查询,这套组合对这类项目来讲是“刚刚好”的复杂度。

2.3 工程目录怎么组织才不混乱

我没有采用多模块 Maven 工程,而是用一个仓库包含两个子目录的方案:

  • frontend/:Vue3 前端工程,独立开发和构建;
  • server/:Spring Boot 后端工程,包含 controller、service、mapper、entity、common 等包。

开发时前端通过 Vite 代理把/api转发到后端 8080,生产环境则把前端构建产物交给 Nginx,或者直接用后端 resource 目录托管。两个目录互不干扰,部署灵活,看起来也清爽。不要一上来就把系统拆成微服务,单体应用+前后端分离是这个体量项目最正确、最省心的架构。

3. 数据库建模:一张门店表开始的完整设计思路

数据库是整个系统的地基,这块我不太建议偷懒。美发店系统表不少,但核心就围绕“人”和“时间”两个维度展开。人包括员工、会员、服务项目;时间包括排班、预约、订单流水。下面是我最终整理出的核心表清单,以及每张表承担的角色。

表名作用关键字段示例
shop门店信息,为后续连锁预留id, name, address, phone
sys_user登录账号,关联员工id, username, password, role, employee_id
employee员工档案id, name, title, level, hire_date
service_item服务项目id, name, category, duration, price
employee_service员工与可服务项目关联employee_id, service_id, price
schedule排班表id, employee_id, work_date, start_time, end_time
appointment预约单id, shop_id, employee_id, member_id, service_id, appointment_date, start_time, end_time, status
member会员档案id, name, phone, level, created_at
member_card会员卡账户id, member_id, card_no, balance, total_recharge, status
card_transaction储值/扣费流水id, card_id, type, amount, balance_after, order_id
orders消费订单id, order_no, member_id, employee_id, total_amount, pay_amount, discount_amount, status
order_item订单明细id, order_id, service_id, employee_id, price, commission_rate
commission提成记录id, employee_id, order_id, item_id, commission_amount, settled

3.1 金额字段与状态字段的谨慎设计

金额字段一律用DECIMAL(10,2),不要用FLOAT或DOUBLE,否则浮点误差会在报表汇总时集中爆发。状态字段我用TINYINT加注释,比如预约状态:0待确认、1已确认、2已取消、3已完成、4已爽约。很多人喜欢用字符串存状态,虽然直观,但枚举值和代码里的常量映射没配合好的话,后期改一个词全表都要更新,非常痛苦。

3.2 会员余额为什么不建议直接改数字

会员储值看起来是“余额 = 余额 + 充值金额”,但实际业务里还有充值赠送、消费扣减、退款、后台手工调整、过期清零。如果只更新余额字段,没有任何流水记录,顾客来前台质疑“我上次充的3000哪去了”,你根本没法解释。正确做法是:每个金额变动都写入card_transaction流水表,同时在同一个事务内更新member_card.balance。流水是凭证,余额是结果。查询时默认展示余额,但随时可以拉出完整资金流水。这也是财务审计的基本要求。

3.3 预约时间的存储口径与唯一索引

预约表的时间字段我建议拆成三个:appointment_date日期、start_time开始时间、end_time结束时间。不要把开始结束合并成一个start_at时间戳,因为你后面还要做“今天有哪些预约”这种按天查询,按日期字段分组和索引都更高效。为了防止并发导致同一发型师同一时段被重复预约,我建了组合唯一索引(employee_id, appointment_date, start_time)。不过这个唯一索引只是最后一道防线,真正判断冲突还得靠事务内的查询和锁机制(下文细说)。

4. 后端最容易翻车的三个业务点:预约、排班、会员资金

如果只是做CRUD,后端三天就写完了,但实际开发时间几乎都花在了几个关键业务规则上。我把它们单拎出来讲,因为这些地方最能体现一个后端工程师对业务的理解。

4.1 预约冲突判断:不能只靠一条SQL

我先给一个最容易被新手写出来的伪代码:

@Transactional public Long createAppointment(AppointmentCreateRequest req) { // 查询该发型师该时段是否已有预约 int count = appointmentMapper.countByEmployeeAndTime( req.getEmployeeId(), req.getDate(), req.getStartTime(), req.getEndTime()); if (count > 0) { throw new BizException("该时段已被预约"); } Appointment appointment = new Appointment(...); appointmentMapper.insert(appointment); return appointment.getId(); }

这段代码单线程跑没问题,但一旦前台两台收银机同时操作,或者未来接了小程序在线预约,并发请求下两个线程都可能查到count = 0,然后各自插入成功,就出现了数据层面的冲突。解决方式有两种配合使用:

  • 利用数据库唯一索引兜底,插入时如果违反唯一约束,捕获DuplicateKeyException后返回友好提示;
  • 在事务内对排班记录加锁,比如SELECT ... FOR UPDATE,让同一员工同时段的预约串行化。

更完善的方案还会做一个“重叠区间”的SQL判断:

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

这个判断逻辑要反复测试,比如“上一个预约16:00结束,新预约16:00开始”这种边界情况,必须保证预约不重叠。

4.2 排班与预约的联动:服务时长动态计算

发型师不是机器,每天有上下班时间,也可能请假。所以预约模块必须带着排班一起考虑。我用schedule表存员工的每日工作时间,请假或休息则直接不生成排班记录。用户选项目时,前端传入门店的营业时间和项目时长,后端再结合员工当天已有预约做“可用时间片”计算:

// 获取员工一天的可用时间段列表 List<TimeSlot> availableSlots = scheduleService.getAvailableSlots(employeeId, date); // 过滤掉已被预约的时间段 List<Appointment> existingAppointments = appointmentMapper.findByEmployeeAndDate(employeeId, date); // 根据每个预约的start_time和end_time切分可用时间片 List<TimeSlot> result = SlotSplitter.subtract(availableSlots, existingAppointments);

这里有个细节:烫染类项目动辄两三小时,所以必须用“项目时长”去匹配可用时间片,而不是简单判断某个小时格子是否空闲。为了让前台更直观,前端预约页返回的结果我会设计成“可用开始时间列表”,比如10:00、10:30、13:00,每个时间都满足完整做完一个项目的时长需求。排班模块还要支持周模板复制,比如店长设置一次“周一至周日每天10:00-20:00”,自动生成整周排班,不然手工逐日排班会让后台烦到放弃。

4.3 会员余额扣减:用数据库行锁保证并发安全

会员储值涉及真金白银,这里有一个千万不能犯的错误:先查余额,再判断是否充足,再更新余额。下面这是错误示范:

MemberCard card = cardMapper.selectById(cardId); if (card.getBalance().compareTo(amount) < 0) { throw new BizException("余额不足"); } card.setBalance(card.getBalance().subtract(amount)); cardMapper.updateById(card);

并发场景下,两个请求同时读到余额1000,都判断可以扣900,然后依次更新,最后余额可能变成-800。正确姿势是原子扣减:

UPDATE member_card SET balance = balance - #{amount} WHERE id = #{id} AND balance >= #{amount}

如果受影响行数为0,就说明余额不足或卡不可用,再抛异常回滚事务。这样即使并发100个请求,数据库行锁也会保证只有一个扣减成功。同事务内再插入一条扣费流水card_transaction,记录扣费前后快照、订单号、操作人,形成完整闭环。

5. 前端页面组织:预约日历、角色路由、列表组件的落地做法

后端做得再稳,前台用着不顺手,整个系统一样会失败。美发店前台普遍没时间接受复杂培训,页面必须大、字要清楚、按钮要少、操作要三步以内完成。

5.1 按角色和门店维度控制路由与菜单

前端登录后会拿到当前用户的角色列表和菜单树。我用 Vue Router 的beforeEach全局守卫做路由拦截,路由元信息里标记meta.roles,没有权限的跳转到403页面。菜单则通过meta.title和meta.icon动态渲染,后端只需要返回一个具备层级结构的菜单数组,前端根据路径映射即可,这样新增菜单时不用改前端代码。

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

5.2 预约日历的实现思路

预约页是前台每天打开频率最高的页面,我实现了一个以“日期 + 发型师”为维度的看板:左侧列出员工,顶部按小时划分时间刻度,格子显示预约顾客姓氏和项目,点击格子弹出新建预约弹窗。这里我没有用现成的日历控件,而是直接自己用 CSS Grid + 绝对定位画的时间轴,因为现成控件很难同时展示多员工和时长不等的预约块。服务时长不同的预约在时间轴上表现为宽度相同、高度不同的色块,背景色根据项目类型区分,比如剪发蓝色、烫发红色、染发橙色,前台一眼就能看出今天哪段时间最饱和。数据来源是后端综合排班和已有预约计算出的“时间格”接口,前端只负责渲染。

5.3 列表页封装一套可复用的“表格逻辑”

因为系统里列表页非常多,会员列表、订单列表、流水列表、员工列表,结构基本一致。我把常见的分页、筛选、加载态、刷新逻辑抽成了一个useTable组合式函数,每个页面只需传入接口函数和查询参数,就能自动管理数据列表:

const { tableData, pageInfo, loading, query, fetchData } = useTable({ fetchApi: memberApi.getList, immediate: true });

弹窗操作也做了一层useDialog封装,控制新增、编辑、查看三个模式的开关和表单回填。这样大量页面代码被压缩到几十行,后续改交互也不会所有页面都手忙脚乱。

6. 前后端联调、部署上线之后的那些坑

这个部分我想多说一点,因为很多新手项目做到“本地能跑”就以为完事了,实际上部署和被真实业务用起来,才是所有坑集中爆发的地方。

6.1 Vite 代理和跨域问题

开发环境下,前端跑在5173端口,后端跑在8080端口。如果不做任何处理,所有请求都会因为跨域被浏览器拦截。我用 Vite 代理解决,而不是在后端配 CORS 全放行:

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端代码里看起来调用的就是/api/xxx同源地址,开发环境稳定,也不会留下安全隐患。生产环境我用了 Nginx 反向代理,把/api转发到 Java 服务,前端静态文件直接由 Nginx 托管。前后端分离部署的好处是,将来前端要升级,只替换静态文件即可;后端重启也不影响静态资源服务。

6.2 MyBatis 的哈希缓存和动态SQL问题

我在项目里基本关闭了 MyBatis 的二级缓存。这类管理系统的数据实时性要求很高,预约状态、会员余额随时都在变,二级缓存一旦存在,查询结果就容易是脏数据。一级缓存默认开启但作用范围小,问题不大。在 XML 中写动态 SQL 时有两个坑:

  • 小于号>小于号必须转义,比如end_time < #{endTime}要写成&lt;,或者用<![CDATA[ ... ]]>包裹;
  • <if test="status != null">中,判断数字时0也是有效值,不要写status != '',否则状态为0时会查不到数据。

这两个问题我实际调试时都遇到过,排查半天才发现是符号或判断条件的问题。

6.3 MySQL连接参数、时区与备份

Spring Boot 连接 MySQL 的 JDBC URL 我建议显式配置:

jdbc:mysql://localhost:3306/hair_salon?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

serverTimezone不设置的话,Java 8 以上版本经常会出现时间比实际早8小时的问题。useSSL=false避免本地环境 SSL 握手报错,allowPublicKeyRetrieval=true解决 MySQL 8 的公钥检索异常。上线后我还加了定时任务:每天凌晨用 mysqldump 备份业务库,保留最近7天备份。这套系统的数据都是真金白银,备份是零成本的保险。

6.4 前端打包放进SpringBoot还是独立部署

我看到很多教程喜欢把Vue构建后的dist目录拷进 Spring Boot 的static目录,做成一个 jar 包部署。这种做法确实简单,但开发迭代时前后端耦合严重,前端每次修改都要重新打包后端,很笨拙。我更推荐独立部署:后端 jar 包单独跑,前端 dist 由 Nginx 托管,Nginx 里加一个/api反向代理即可。这样运维可以单独重启接口服务,发布前端也只是覆盖文件,故障面小很多。

7. 最后分享几点我做完这个项目的真实体会

这套系统我断断续续开发了大概两个月,真正让我耗时间的不是代码,而是反复确认业务流程:预约改期了原时段要不要释放、充值时赠送金额是否参与退款、发型师离职后历史提成怎么处理。建议你在编码之前,把这些边界问题全部问清楚,并且落到状态机或规则表里。哪怕只是自己拿Excel模拟一遍“充值1000送200,消费一笔,退款一笔”的全过程,也能帮你提前发现大量设计漏洞。我在开发过程中一个特别有用的习惯是写“业务规则清单”,比如“预约只能修改未来且未开始的预约”“余额扣减必须走流水”“同一员工同一时段只能有一个有效预约”,一条一条过,系统验收时几乎不用返工。希望这篇内容能让你少熬夜,也希望你有兴趣把预约提醒、移动端预约这类延展功能加进去,那会是一个更完整的作品。

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

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

立即咨询