☰
SpringBoot+Vue+MySQL实现美发门店管理系统:从表设计到部署避坑
2026/10/5 4:01:02 网站建设 项目流程

我在做管理类系统项目时有个体会:不管你服务的是美发店、洗衣店还是宠物店,本质上都在处理“人、货、场、账”四件事。前阵子我把一个基于SpringBoot+Vue的美发门店管理系统完整整理了一遍,后端用Java+SpringBoot+MyBatis操作MySQL,前端用Vue做管理后台,整套源码包含顾客管理、预约排班、服务消费、会员储值、库存和报表等模块。这篇文章我按当时的真实开发顺序来写,从业务需求拆解到表结构设计,再到后端接口、前端页面、部署打包,每个环节都会讲清楚为什么这么做,以及哪些地方容易踩坑。正在做课程设计、毕业设计,或者准备给线下门店做信息化改造的朋友,可以直接参考里面的思路和代码片段。

1. 立项之前,先想清楚美发门店到底需要管理系统解决什么问题

很多人做这类系统,容易一上来就开项目、建表、写登录注册,最后页面是挺多,但门店老板一用就发现对不上真实场景。所以我建议先别碰代码,把美发店的日常流程走一遍。

1.1 门店老板最头疼的三个点,也是系统的核心价值

我去过不少美发店,观察下来最典型的情况是这样的:

第一,顾客信息极度分散。老客户电话号码写在纸质登记本上,会员消费记录靠店主脑子记,或者微信聊天记录里翻。顾客今天办了张卡,下次来换了另一个店员,余额到底多少完全说不清。这是会员储值模块必须解决的“对账”问题。

第二,预约和排班经常冲突。店里三四个理发师,热线电话一响,前台手写预约本,很容易把同一个时间段约给两个人。或者理发师临时休息,前面约好的客人也不知道,导致客人到店白等。这是预约排班模块要解决的“时间冲突”问题。

第三,经营数据没有沉淀。一个月的营业额多少?哪个项目最赚钱?哪个理发师贡献最大?染发膏进货多少、消耗多少?这些数据如果靠人肉Excel统计,月底对账就是灾难。这是报表和库存模块要解决的“经营分析”问题。

1.2 系统的业务边界,按“进店-服务-离店-复购”这条链来切

基于上面三个痛点,我把系统边界拆成六大模块,而不是做成一个什么都往里塞的大杂烩:

  • 顾客管理:建档、联系方式、消费历史、来源渠道,相当于给门店做客户关系管理。
  • 员工管理:理发师账号、角色权限、服务排班状态,后台管理员和前台收银看到的内容要区分开。
  • 预约管理:顾客或前台发起预约,指定理发师和时间段,状态分待服务、已完成、已取消。
  • 服务项目管理:剪发、染发、烫发、护理等套餐,记录价格和成本价,方便算毛利。
  • 订单与收银:顾客到店完成服务后开单,支持现金、微信、支付宝、储值卡扣款。
  • 会员储值与库存:充值送余额、消费扣余额,过程中必须留流水;洗发水、染发膏、护发素等耗材有出入库记录,低于警戒值要能提醒。

边界确定之后,后续表结构、接口设计、页面菜单就都围绕这六块展开,不会跑偏。

2. 技术选型:SpringBoot+Vue+MySQL+MyBatis为什么是这套组合

选型这一步,很多教程直接告诉你“用什么”,但很少解释“为什么”。我在项目里最终确定SpringBoot+Vue+MySQL+MyBatis,不是因为它热门,而是这个组合最适合做中小型门店管理系统。

2.1 SpringBoot负责后端能力快速落地

SpringBoot最大的价值是简化了Spring项目的搭建和配置。早年的SSM项目,要写一堆XML配置:数据源、事务管理器、MyBatis的SqlSessionFactory、组件扫描,每个都要手动配。SpringBoot用自动配置和starter机制把这些默认配置内嵌到依赖里,我们只管业务代码。

做美发门店管理系统这种项目,后端本质就是一个提供增删改查和简单权限的API服务。SpringBoot的启动类加几行注解就能跑起内嵌Tomcat,不用单独部署Servlet容器。这一点对课程设计、接私活、快速交付都非常重要。

版本上我建议优先用SpringBoot 2.7.x。为什么不是最新的3.x?因为SpringBoot 3.0以后,底层Java EE规范从javax迁移到了jakarta,很多网上旧教程里的代码会直接编译报错。如果你的参考代码、教学视频都是基于2.x的,突然用3.x会遇到一堆包名对不上的问题,这种问题对新手来说排查成本很高。技术旧一点不丢人,能稳定跑通才是重点。

2.2 Vue做管理端页面,交互和开发效率都合适

管理后台的页面有个特点:表格多、表单多、数据联动多。如果还像以前一样用jQuery手写DOM,一个订单编辑页能写哭你。Vue的核心是数据和视图绑定,你只需要维护一份data数据,页面上自动同步更新,不需要手动操作DOM节点。

组件化也是选择Vue的重要原因。顾客列表、预约表格、项目选择下拉框,都是可以复用的组件。前后端可以分开并行开发:我定义好接口文档,前端同事用Mock数据先做页面,后端接口通了再联调。相比传统的模板引擎,这种开发模式更适合以API为中心的管理系统。

版本选择上,新项目建议Vue 3 + Element Plus。Element Plus是Element UI的Vue3版本,表格、表单、弹窗、日期选择器这些现成组件都有,开发效率非常高。部分老项目还在用Vue 2 + Element UI,如果你是看别人的源码复现,那保持源码版本一致就好,别混用。

2.3 MySQL存数据,MyBatis写SQL,各司其职

MySQL在中小型项目里几乎是默认选择:成熟稳定、免费、资料多,InnoDB引擎支持事务和外键约束,适合订单、储值这种需要强一致性的数据。美发门店单店的数据量不大,哪怕几年下来订单也就几十万条,MySQL完全扛得住。

ORM层我用MyBatis而不是JPA/Hibernate,原因很实在。管理系统里大量查询是动态条件——预约列表要按顾客姓名、手机号、状态、日期任意组合筛选;报表模块要按天、按月分组聚合;订单明细要算项目销售占比。这类SQL如果用JPA写,要么改方法名特别长,要么写@Query注解,要么Specification动态拼接,学习成本不低。MyBatis的XML里用<where>、<if>写动态SQL,非常直观,DBA和开发都能看懂。

如果你现在看到很多项目改用了MyBatis-Plus,不用纠结。MyBatis-Plus只是把单表增删改查封装好了,减少简单CRUD代码量,但复杂SQL还是得写XML。本项目的核心价值在于理解表结构设计和业务逻辑,所以按MyBatis来梳理完全没有问题,想用Plus的话在单表操作时可以少写些代码,但是动态查询部分逻辑是一样的。

3. 数据库设计:把业务模型落到MySQL表上

一套系统行不行,看表结构最快。好的表结构能让你后面的代码写得顺畅,不好的表结构会让你在service层堆无数判断逻辑。我设计表的时候,遵循了“先理关系,再定字段,最后补充索引”的顺序。

3.1 核心表和字段,我直接给出可复用的设计

因为输入的原始描述信息有限,以下是我在这类项目中反复验证过的一套核心表结构,字段含义尽量按照实际业务命名。实际落地时大家可以按需增删字段。

顾客表 customer:

字段类型说明
idbigint主键,自增
namevarchar(50)顾客姓名
phonevarchar(20)手机号,建索引,支持模糊搜索
gendertinyint性别,1男2女
birthdaydate生日,做营销提醒用
leveltinyint会员等级,0普通1白银2黄金
sourcevarchar(50)来源渠道:美团、抖音、老客介绍
remarkvarchar(255)备注
create_timedatetime建档时间

员工表 staff:

字段类型说明
idbigint主键
namevarchar(50)姓名
accountvarchar(50)登录账号
passwordvarchar(100)BCrypt加密后的密码
roletinyint1管理员2收银员3理发师
positionvarchar(50)职称:总监、高级技师、实习发型师
work_statustinyint1在班2休息3离职
phonevarchar(20)手机号

服务项目表 service_item:

字段类型说明
idbigint主键
namevarchar(100)项目名:剪发、染发、烫发、护理
categoryvarchar(50)分类,用于报表分组
pricedecimal(10,2)售价
cost_pricedecimal(10,2)成本价,算毛利用
duration_minutesint预计耗时,预约排班用
statustinyint1上架0下架

预约表 appointment:

字段类型说明
idbigint主键
customer_idbigint顾客ID
staff_idbigint理发师ID
service_namesvarchar(255)预约的服务项目名,冗余存储
appointment_timedatetime预约到店时间
statustinyint0待服务1已完成2已取消
remarkvarchar(255)备注
create_timedatetime预约创建时间

这里我把service_names直接冗余成字符串,而不是做一张复杂的预约-服务关联表。原因是预约阶段不需要精确统计金额,主要是让理发师知道客人来做什么;等实际消费结账时,再走订单明细表精确记录项目和价格。这样设计既简单又够用。

订单表 orders:

字段类型说明
idbigint主键
order_novarchar(50)单号,格式如20250112143005001
customer_idbigint顾客ID
staff_idbigint主要服务理发师ID
total_amountdecimal(10,2)订单原价总额
discount_amountdecimal(10,2)优惠金额
paid_amountdecimal(10,2)实收金额
pay_typetinyint0现金1微信2支付宝3储值卡
statustinyint0待支付1已完成2已退款
create_timedatetime下单时间

订单明细表 order_item:

字段类型说明
idbigint主键
order_idbigint订单ID
service_item_idbigint服务项目ID
item_namevarchar(100)项目名称快照,防止项目改名后历史订单不准
pricedecimal(10,2)当时售价
subtotaldecimal(10,2)小计金额

会员储值表 membership:

字段类型说明
idbigint主键
customer_idbigint顾客ID,一个顾客最多一张卡
balancedecimal(10,2)当前余额
leveltinyint会员等级
discountdecimal(3,2)折扣,如0.85
total_rechargedecimal(10,2)累计充值金额

充值流水表 recharge_record:

字段类型说明
idbigint主键
customer_idbigint顾客ID
membership_idbigint会员卡ID
amountdecimal(10,2)本次充值金额
gift_amountdecimal(10,2)赠送金额
balance_afterdecimal(10,2)充值后余额
create_timedatetime充值时间

库存表 inventory:

字段类型说明
idbigint主键
product_namevarchar(100)耗材名,如某品牌染发膏
categoryvarchar(50)分类:染发、烫发、洗护、工具
unitvarchar(20)单位:支、瓶、盒
quantityint当前库存数量
warn_valueint警戒值,低于该值提醒补货
pricedecimal(10,2)采购单价

上面这几张表基本能覆盖单店美发门店的全部核心业务。商品零售如果门店有洗发水售卖,可以再加一张 product 表做实物商品,和 service_item 在订单里通过类型字段区分即可,我这里为了保持系统边界简洁,没有扩展。

3.2 会员储值不能只靠余额字段,流水才是记账底线

会员余额数据表面上一张membership表就够了,但实际最容易出问题的恰恰是余额变动。顾客充值1000送200,余额1200;做项目消费380,余额820;第二次充值2000送500,余额3320。如果每次只是简单更新balance字段,没有任何流水记录,一旦出现纠纷或者程序扣错,根本无从查起。

所以我在设计里要求:余额可以冗余存储在membership表中,但每一笔余额变动都必须写入recharge_record或订单变更记录。充值时写一条充值流水,记录本次充值金额、赠送金额、充值后余额;储值卡消费时,在订单表记录pay_type=3,同时会在代码逻辑里更新余额并写一条余额变动记录。

这样的设计其实不复杂,却能在后续对账、审计、客服查证时省下大量时间。这也是简历上可以写进项目亮点的细节,面试官很吃这一套。

3.3 物理外键能省则省,但索引不要省

很多教材喜欢强调外键约束,实际项目里我一般不建物理外键,而是用逻辑外键维护关系。原因有二:一是物理外键会影响写入性能,每次插入都要检查父表;二是删除数据时容易被外键约束卡住,门店数据比如订单和顾客,业务上本来就不该物理删除。逻辑外键让我们可以用service层控制删除逻辑(比如会员有充值记录时不允许删除顾客,而是置为“已注销”状态)。

但是索引一定要建。美发门店系统的查询场景主要是三种:

  • 按顾客手机号或姓名搜索:customer表phone字段建议建普通索引,因为大量时间会花在“报手机号查会员”上。
  • 按日期查预约和订单:appointment表appointment_time、orders表create_time建普通索引,报表按天统计会走索引,不然随数据量增大,月底统计会越来越慢。
  • 按状态查列表:status这种区分度不高的字段,可以考虑与时间字段建联合索引,比如(create_time, status),避免回表。

4. SpringBoot+MyBatis后端实现:把核心功能逐个落地

表结构确定后,后端开发就变得顺理成章。我采用的还是经典的Controller-Service-Mapper分层。下面挑几个有代表性的部分展开,一是登录鉴权,二是动态查询SQL,三是报表统计SQL。

4.1 项目分层和目录结构

后端的包结构我习惯这样规划:

src/main/java/com/example/barber ├── controller # 前端调用入口,接收参数,返回Result ├── service # 业务逻辑,事务控制 ├── mapper # MyBatis接口,定义方法 ├── entity # 数据库表对应的实体 ├── common # 统一返回结果、异常处理、常量 └── config # JWT拦截器、跨域配置、Web配置 src/main/resources ├── mapper # MyBatis的XML文件 └── application.yml

分层的核心价值是让代码职责单一。Controller只负责参数接收和结果包装,Service负责业务规则(比如下单时校验余额、更新库存),Mapper负责数据库交互。很多同学喜欢在Controller里写一大堆业务逻辑,短时间内跑得通,但订单、储值这种涉及多个表操作的场景,事务管理就会变得很别扭。

4.2 登录与权限:JWT+拦截器怎么配合

管理系统肯定要有登录和角色控制。我采用的方案是:登录成功后生成JWT Token,前端把Token存到localStorage,每次请求在HTTP Header里带上Authorization字段,后端拦截器统一校验。

登录核心思路大概是这样的:

// 1. 根据账号查出员工,比对BCrypt密码 Staff staff = staffMapper.selectByAccount(account); if (staff == null || !BCrypt.checkpw(rawPassword, staff.getPassword())) { throw new BizException("账号或密码错误"); } // 2. 生成JWT,把员工ID和角色放进去 String token = Jwts.builder() .claim("staffId", staff.getId()) .claim("role", staff.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, "你的签名密钥") .compact(); // 3. 返回token和员工基本信息给前端

拦截器的作用是统一校验,不用每个Controller方法里面都判断是否登录。注册拦截器时注意放行登录接口:

// 伪代码 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login");

拦截器中校验Token是否合法,并把当前登录员工ID和角色挂到request里,后续Controller里直接拿,避免重复解析。比如收银员只能操作订单和会员充值,管理员才能看到报表和员工管理,这些就用角色字段做判断。

如果你用的是SpringBoot 3.x,注意JWT库的选择要匹配新规范,建议项目初期就锁定好版本,不然会踩编译期的坑。

4.3 预约列表的动态SQL:查询条件按需拼接

预约管理页面一般有一个筛选区域:输入顾客姓名、手机号,选择状态,选择预约日期,点击查询。如果每个条件都写一个SQL方法,代码冗余到爆炸。MyBatis的<where>加<if>就是为这种场景准备的。

给大家一个可直接抄的XML示例:

<select id="selectAppointmentPage" resultType="com.example.barber.entity.Appointment"> SELECT a.*, c.name AS customerName, c.phone AS customerPhone, s.name AS staffName FROM appointment a LEFT JOIN customer c ON a.customer_id = c.id LEFT JOIN staff s ON a.staff_id = s.id <where> <if test="keyword != null and keyword != ''"> AND (c.name LIKE CONCAT('%', #{keyword}, '%') OR c.phone LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND a.status = #{status} </if> <if test="appointmentDate != null and appointmentDate != ''"> AND DATE(a.appointment_time) = #{appointmentDate} </if> </where> ORDER BY a.appointment_time DESC </select>

注意两点:一是用了<where>标签,它会在第一个条件满足时自动拼上WHERE,并且去掉后面多余的开头AND,比手动写“WHERE 1=1”优雅得多;二是查询条件我用appointmentDate而不直接用date,原因是date在部分数据库里属于保留字,容易出问题,没必要为了省三个字符去踩这个坑。

4.4 报表统计:聚合SQL在数据库里算好,不要在Java里循环

报表模块最忌讳的是把明细数据全部查出来,然后在Java里for循环累加。日期范围一大,内存和接口响应都会很难看。正确的做法是把聚合逻辑交给MySQL。

统计最近七天的营业额:

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS orderCount, SUM(paid_amount) AS revenue, AVG(paid_amount) AS avgOrderAmount FROM orders WHERE status = 1 AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY day ORDER BY day DESC

统计服务项目的销售排行:

SELECT oi.item_name AS name, COUNT(*) AS count, SUM(oi.subtotal) AS amount FROM order_item oi JOIN orders o ON oi.order_id = o.id WHERE o.status = 1 AND o.create_time >= #{startDate} GROUP BY oi.item_name ORDER BY amount DESC LIMIT 10

这类SQL放到Mapper里执行,返回List

List<Map<String, Object>> list = reportMapper.selectServiceTop(startDate, endDate);

前端直接拿这个List渲染ECharts图表。实践下来的体感是:一条聚合SQL替代几十行Java代码,性能还好。数据库本来就应该做它最擅长的事。

5. Vue前端:管理后台的页面结构和关键交互

前端的核心不是炫技,而是让收银员、理发师、老板都能快速上手。下面我从项目结构、请求封装、表格表单、日历图表四条线来讲。

5.1 前端项目结构和路由设计

Vue项目我习惯分成四层:

  • api目录:按模块放接口请求文件,比如api/customer.js、api/order.js。
  • views目录:页面组件,登录、顾客管理、预约管理、订单收银、会员储值、报表。
  • router目录:路由配置,区分公开页面和登录后页面。
  • utils目录:axios实例、token管理、格式化工具。

路由设计上,登录页独立存在,进系统之后外面套一层Layout布局,页面之间用子路由切换。简化后的路由是这样的:

{ path: '/login', component: () => import('@/views/login/index.vue') }, { path: '/', component: () => import('@/layout/index.vue'), redirect: '/customer', children: [ { path: 'customer', component: () => import('@/views/customer/index.vue') }, { path: 'appointment', component: () => import('@/views/appointment/index.vue') }, { path: 'order', component: () => import('@/views/order/index.vue') }, { path: 'report', component: () => import('@/views/report/index.vue') } ] }

路由守卫里加一个判断:没有token就跳转登录页,有token但访问登录页就直接回到首页。很简单,但必不可少。

5.2 axios封装和Token注入

每次请求都手动加Authorization头太蠢,正确做法是封装一个axios实例,用请求拦截器统一注入。

import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } else { ElMessage.error('网络异常') } return Promise.reject(error) } )

这里baseURL用/api,是因为我后面打包时准备直接把前端dist放进SpringBoot的static目录,让后端统一提供/api接口,这样前端请求就是同源,能避开大部分开发环境才有的跨域问题。

5.3 用Element Plus实现标准表格管理页

顾客管理、订单管理这类页面,本质都是“条件筛选表格+弹窗编辑表单”的标准范式。我一般这样组织:

  • 顶部搜索区:el-input、el-select、el-date-picker,点击查询时把条件对象传给后端。
  • 中间表格区:el-table绑定列表数据,支持分页。
  • 右侧弹窗区:el-dialog里放el-form,新增和编辑复用同一个弹窗,只是初始数据不同。

关键点是“新增”和“编辑”两个操作要在表单提交时区分。我通常给dialog一个title变量和一个form对象,新增时清空表单,编辑时把当前行数据浅拷贝进去,注意不要直接把table row对象赋值给form,否则你弹窗还没保存,表格里数据已经被改了,这种坑很隐蔽。

5.4 预约日视图和统计图表怎么选型

预约管理页如果只是普通表格,门店前台使用体验一般。我希望在日历视图上直接看到哪天哪个理发师约满了。这里有两个选型方案:

  • 方案一:用现成的fullcalendar组件。它支持月视图、日视图,可以拖拽事件,社区生态大。缺点是需要额外npm安装,样式定制要花时间。
  • 方案二:用Element Plus自带的el-calendar,自己在日期格子里渲染预约摘要。胜在不用额外依赖,但功能比较轻,不适合做复杂的拖拽调整。

考虑到项目周期,我当时用的方案二做基础版本,把预约数据按日期分组渲染成标签显示在日历格子上,点击某一天再进入当天明细列表。如果后续要支持前台在线选时间段,再升级到fullcalendar也来得及。

统计报表页面我用ECharts,柱状图看每日营业额走势,饼图看项目销售占比。ECharts在前端生态里属于标配,文档丰富,遇到问题搜一下基本都有答案。图表组件建议单独封装成chart.vue,接收option参数,父组件只负责准备数据,这样复用性会好很多。

6. 本地跑通到部署上线,最常遇到的四个坑

这套系统我在本地跑通、打包部署的过程中,遇到过不少实际问题。下面几个坑比较有代表性,我按出现频率从高到低排序。

6.1 MySQL连接配置:时区、SSL、公钥获取

用MySQL 8.0连接SpringBoot,如果直接在application.yml里写最短的url,大概率会报错。我最终稳定使用的配置如下:

spring: datasource: url: jdbc:mysql://localhost:3306/barber_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

三个关键参数再强调一下:serverTimezone=Asia/Shanghai解决日期差8小时的问题;useSSL=false是因为本地开发用的通常是自签名证书,MySQL默认会尝试SSL握手,反而容易报错;allowPublicKeyRetrieval=true是MySQL 8.0连接时获取公钥用的,不加会报Public Key Retrieval is not allowed,很多人卡在这一步。

6.2 MyBatis XML文件没有被识别到

项目一切配置看起来都对,启动也没问题,但一调用Mapper接口就报:

Invalid bound statement (not found): com.example.barber.mapper.CustomerMapper.selectByPage

这个报错十有八九是XML没打进编译后的classpath。Maven默认只把resources目录下的资源打包,但如果你把XML放在java源码目录里,或者IDEA的resource过滤配置不对,target里找不到XML,MyBatis自然就绑定不了。

解决思路是确认两点:第一,XML文件放src/main/resources/mapper目录下;第二,application.yml里配置mapper-locations:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.barber.entity

如果这样还不行,再检查pom.xml的build节点,把src/main/resources显式加入到resources里。这个坑最多,但也最好排查,按上面两步走基本能解决。

6.3 跨域配置和axios的baseURL

我在开发时前端用Vue的8080端口,后端跑8080端口,比如前端请求http://localhost:8080/api/customer/list,这就属于跨域请求了。前端控制台会看到CORS错误。解决方法是给后端加跨域配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

这里特别提醒:如果设了allowCredentials(true),就不能用allowedOrigins(""),因为带凭证的跨域请求不允许通配来源,这是浏览器安全策略。要用allowedOriginPatterns("")来放宽模式匹配。

同时记得放行OPTIONS预检请求。虽然上面的配置里allowedMethods已经包含了OPTIONS,但如果你还配置了登录拦截器,别把OPTIONS请求直接拦截掉了,否则前端会看到“Failed to fetch”或者奇怪的CORS报错。

6.4 部署:前端dist打进SpringBoot还是用Nginx

项目部署有两种主流方案,我两种都试过,给个明确结论。

如果你追求“一个jar包搞定一切”,就把前端构建产物放到后端resources/static下:

cd frontend npm run build cp -r dist/* ../backend/src/main/resources/static/ cd ../backend mvn clean package java -jar target/barber-1.0.0.jar

这样访问http://服务器IP:8080就是前端页面,页面请求/api/**会走到后端的Controller。适合低并发、单机部署的场景,管理后台完全够用。

如果你以后想扩展小程序、客户端等接入,建议正式环境用Nginx:

server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /var/www/barber/dist; try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

Nginx方案的好处是静态文件响应更快,后端服务可以独立重启,不影响前端访问。缺点是多一个服务要维护。项目刚起步阶段,我建议先用方案一,把jar包跑起来再说。

7. 从课程设计到真门店落地,还差这几点实际经验

如果你做这个系统的目的不只是交作业,而是希望门店真的能用起来,那我再分享几个容易忽略、但实际运营中很关键的点。

7.1 小票打印、员工提成、消息通知这些细节别忽略

订单收银后,门店通常要打印小票。我在Vue端用的是浏览器打印方案,把订单详情放进一个隐藏的打印模板里,调用window.print()。简单但有效,没有额外引入打印服务。

员工提成也是一个容易出问题的点。美发店的提成计算不一定是简单的10%,可能是按服务项目分类,染发提成比例和剪发不同,还可能和理发师职称挂钩。这个逻辑我当时直接在订单完成的时候算出提成金额,存到一张员工业绩表中,而不是每次临时计算。这样月底发工资时直接按业绩表汇总,数据更可控。

如果后续要做会员生日提醒、服务到期召回,通常需要短信或微信通知能力。这个不是系统本身能独立解决的,需要接入服务商的API,并且涉及模板审核、费用等问题。我的建议是第一步先把顾客生日、消费间隔这些数据统计出来,利用报表模块做成提醒列表,由前台手动电话联系,等业务跑顺了再考虑自动化通知。

7.2 我对这个项目的复盘和给你的建议

把整个系统从头做一遍,我觉得最值得关注的核心链路就是:顾客进店-登记/预约-分配理发师-服务完成-开单收银-会员余额变动-老板看报表。这条链路任何一个环节断了,系统都会变成摆设。

我也给准备答辩或者面试的朋友几个建议:一是不要只背概念,一定要能说清楚表与表之间的关联,比如为什么订单要有明细表;二是准备一个“踩坑故事”,比如MyBatis XML扫描不到、跨域预检请求拦截这类问题,面试官对这些真实问题非常有兴趣;三是尽量把代码里的命名规范做好,一个清晰的分层结构比多写几个烂功能更能说明你的工程素养。

最后,源码的具体结构我在博客里已经按模块拆开讲过了,不管你是直接照着写还是做二次开发,建议先用真实门店的数据跑一遍业务流程,你会在这一步发现很多设计阶段没考虑到的小细节,而这些细节恰恰是这个项目真正有价值的地方。

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

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

立即咨询