资讯动态

SpringBoot+Vue+MyBatis实战:美发门店管理系统开发全解析

发布时间:2026/9/9 20:04:56 来源:尧图企业网站定制
我接手过一个朋友的美发门店管理需求当时店里用的还是纸质登记本加微信群接龙。预约经常撞车会员卡余额要翻聊天记录员工提成月底对账能对到半夜。后来我帮他做了一套基于SpringBootVue的管理系统用MyBatis操作MySQL存储业务数据前后端分离部署整个流程才彻底理顺。先给这套系统一个定位它不是那种大而全的ERP而是专门针对美发门店日常运营场景设计的轻量级管理系统。核心功能包括预约排班、会员储值、订单收银、员工提成、产品库存和基础数据统计。技术栈就是标题里那套经典组合——SpringBoot负责后端接口和业务逻辑Vue负责前端页面交互MyBatis作为ORM框架操作MySQL数据库。如果你手头刚好有类似的门店管理需求不管是用作毕业设计、接私活还是想给自家或者朋友的店做一套内部工具这套系统的架构和实现思路都可以直接拿来参考。我不打算把源码从头到尾贴一遍那样又长又没意义。我更想把这套系统从需求拆解、数据库设计、后端接口实现到前端联调、上线部署的完整链路讲清楚把我在实际开发中踩过的坑和权衡过的取舍也一并交代。这样你拿到源码时能看得懂每一张表为什么这么建、每一个接口为什么这么写而不仅仅是能跑起来。1. 门店管理的业务需求拆解光记流水账远远不够美发门店的业务看起来简单——剪发、烫染、卖卡、卖产品。但真正做系统的时候你会发现理发店的业务形态其实相当有代表性。它既有服务行业典型的预约履约流程又有零售行业的商品销售和库存管理还有人员管理上的提成计算和排班需求。一套系统要同时处理好这三条线才算真正满足门店的需求。1.1 三类核心角色的差异化诉求第一个角色是顾客。顾客关心的东西很直接能不能快速约到合适的发型师到店之后不用等太久会员卡里的余额和消费记录清清楚楚充值的时候有清晰的价格和优惠规则。顾客对系统的感知基本集中在小程序或前端页面上他们不会关心后端用的是SpringBoot还是别的框架但你的接口响应速度、预约冲突处理逻辑直接影响他们的体验。第二个角色是门店员工包括前台、发型师和店长。前台需要处理预约登记、到店核销、收银开单这些日常操作他们要的是一个简单快速的录入界面。发型师关心的是自己的排班表、预约客户和提成明细。店长则需要看到营业数据——今天的流水是多少、哪个项目卖得好、哪个发型师贡献最高、库存还够不够。这些诉求对应到系统里就是预约管理、订单管理、员工管理和统计报表四大模块。第三个角色是系统管理员。这个角色往往被忽略但实际使用中特别重要。门店的员工流动性比较大今天这个人还在前台下个月可能就换人了。所以系统的权限管理不能太复杂但又要保证员工只能看到自己该看的数据。我的做法是设计了简单的角色区分管理员拥有全部权限店长可以看到门店级数据普通员工只能操作自己的工作台。权限控制不做到按钮级别只到菜单和接口层面这个粒度对一个小型门店系统来说已经足够了。1.2 预约、会员、库存三条业务线的流转关系梳理清楚角色之后下一步是把业务流程串起来。不管门店的业务多复杂核心的业务流只有一条顾客发起预约 → 前台或顾客自行安排到店时间 → 发型师提供服务 → 收银结账消费或扣卡 → 同步更新会员余额、员工提成、产品库存。这条主链路里藏着三个容易被忽略的细节我在设计时花了不少精力处理。第一预约和排班的绑定关系。一个发型师同一天不能接无限个单否则就撞车了。所以后端在创建预约时必须做并发校验——同一时间段内同一发型师的预约数量不能超过一个阈值一般是1因为一个发型师同时只能服务一个顾客。第二会员余额和消费订单的事务一致性。顾客用储值卡结账时系统要先判断余额是否充足然后创建一个消费订单再扣减余额还要生成一条余额流水方便后续对账。这四个操作必须在一个数据库事务里完成任何一个步骤失败都要回滚。我实际开发中遇到过扣款成功但订单创建失败的情况排查下来就是事务边界没控制好。第三服务项目与产品库存的联动。烫染项目会消耗药水、染膏等产品。每次完成一个服务订单系统就需要根据订单关联的项目自动扣减对应产品的库存。这个动作如果靠人工在后台操作很容易漏所以要做成自动的。但自动扣减又带来一个问题——如果发型师实际用了两盒染膏但系统只扣了一盒的库存账面就不准了。所以我在设计时留了一个库存调整的接口允许员工手动修正库存数据用途备注清楚就行。2. 技术选型SpringBoot Vue MyBatis MySQL为什么这套组合最省心技术选型是整套系统的地基选不好后面全是坑。我一开始也纠结过是不是要用更热门的技术栈但最终坚定地选了SpringBoot Vue MyBatis MySQL这套组合。原因很现实稳定、生态成熟、资料多、团队上手快而且对服务器配置要求不高。2.1 后端为什么锁定SpringBootSpringBoot不只是简化了Spring的配置它给中小型管理系统带来的最大价值是自动装配和起步依赖。你引入一个spring-boot-starter-web内置的Tomcat、Spring MVC、Jackson这些组件就全部就位了不需要像早期的SSH框架那样写一堆XML配置文件。对门店管理系统这种典型CRUD占大头的业务来说这种开箱即用的体验能省掉大量搭建时间。SpringBoot还有一个好处是部署方便。它内置了Tomcat打包成JAR文件之后服务器上只要装了JDK就能直接跑起来不需要单独装和配置Tomcat。我部署这套系统的时候后端就是一个java -jar命令的事。这一点在给真实门店部署时尤其重要因为门店的服务器或云主机配置往往不高运维能力也有限越简单的部署方式越不容易出问题。如果你是在面试中聊SpringBoot可以多关注自动配置的源码逻辑——SpringBootApplication这个组合注解背后EnableAutoConfiguration是通过AutoConfigurationImportSelector加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中的配置类来实现自动装配的。理解了这个机制遇到为什么引入了某个starter之后相关组件就能直接用这类问题时就不会只停留在背结论的层面。2.2 MyBatis和MyBatis-Plus到底该选谁在ORM框架的选择上我和不少人讨论过MyBatis和MyBatis-Plus的区别。简单说MyBatis-Plus是MyBatis的增强工具它把单表的增删改查做成了通用的BaseMapper你不用写SQL就能直接调用insert、selectById、updateById这些方法。而MyBatis本身需要你在XML或注解里手写SQL。我的建议是如果这套系统是你自己从零开发且对SQL掌控力还不错直接用MyBatis就够了灵活可控。但如果项目工期紧、或者团队里有人不太擅长手写SQL用MyBatis-Plus能显著提升开发效率。这套门店管理系统我最终用的是纯MyBatis——因为业务虽然看似简单但订单、提成、报表这些查询的SQL复杂度并不低手写SQL能让我精确控制每一条查询逻辑也方便后续调优。先说一个我在配置MyBatis时一定会做的设置在application.yml里打开驼峰命名映射让数据库字段create_time自动映射到Java属性createTime。这个配置值得单独说是因为很多新手在遇到明明字段对得上但查出来是null的时候往往排查很久才发现是没开驼峰映射。mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.shop.entity configuration: map-underscore-to-camel-case: true # 控制台打印SQL联调阶段强烈建议开启 log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations指定了Mapper XML文件的路径type-aliases-package让XML里写resultType的时候可以直接用别名。而log-impl这一项我建议在开发环境打开它能直接在控制台打印出MyBatis执行的SQL语句和参数排查问题会方便太多。上线前记得关掉或者改成日志级别更高的实现否则每个SQL都打到控制台攒多了对性能有影响也不安全。2.3 MyBatis缓存机制别看是小事踩坑是真耽误时间很多人在用MyBatis的时候对缓存不太上心但门店管理系统这种读多写少的场景缓存机制如果没弄清楚很容易出现数据看着不对的诡异问题。MyBatis有一级缓存和二级缓存。一级缓存是SqlSession级别的同一个SqlSession内执行两次相同的查询第二次会直接走缓存不查数据库。二级缓存是Mapper级别的跨SqlSession共享需要在Mapper XML里配置cache/标签才会启用。实际开发中一级缓存在Spring集成环境下会带来一个隐蔽的问题——Spring管理的Mapper Bean底层SqlSession的生命周期和事务绑定。如果你在一个事务方法里先查询一个会员的余额然后在同一个事务里别的操作更新了这个会员的余额再查询一次同样的数据你拿到的可能还是第一次查询的旧值因为一级缓存没失效。要避免这个坑最简单的办法是把查询和更新的操作拆到不同的事务方法里或者更新操作后手动调用sqlSession.clearCache()。我在这套系统里就明确规定了一个事务方法中避免出现先查后改再查同一对象的写法。二级缓存我在这套系统里是关闭的。因为门店系统的数据实时性要求高会员余额、预约状态这些信息不能容忍脏读。而MyBatis的二级缓存默认粒度比较粗缓存失效策略在复杂查询下不好控制与其提心吊胆不如不用。做技术选型的时候不用什么和用什么一样重要。2.4 Vue版本选型与前端工程化准备前端用Vue是顺理成章的事。不过市面上Vue 2和Vue 3并存的局面确实让不少人纠结。这套系统用的是Vue 3。原因不复杂Vue 3的Composition API在组件逻辑复用上比Vue 2的Options API清晰得多而且生态已经成熟Element Plus这类组件库对Vue 3的支持也很好。在Vue的环境配置上这里强调几个容易踩坑的点。Node.js版本不能太老。Vue 3的构建工具Vite要求Node.js 14.18或16建议直接用最新的LTS版本装完NPM直接一条龙。安装依赖的时候如果网络不稳定导致安装缓慢或者失败可以考虑给NPM配置国内镜像源npm config set registry https://registry.npmmirror.com能省下大量等待时间。创建Vue项目的时候我习惯用Vite而不是Vue CLI。Vite的开发服务器启动速度是以毫秒计的热更新体验比Webpack时代的Vue CLI好太多。命令也不复杂npm create vitelatest hair-salon-admin -- --template vue这里有个小坑npm create vite拉取模板的时候如果Node版本过低会直接报错所以还是那句话先把Node版本升上去。Vue项目建好之后还要装路由和状态管理。路由用Vue Router状态管理用Pinia。这两样是Vue前端工程化的标配。尤其是路由在管理后台里负责页面跳转和权限控制。处理路由参数的时候有个细节美发门店系统里预约详情的跳转经常需要携带预约ID、会员ID这些参数。用this.$route.query或useRoute().query拿参数的时候刷新页面参数不会丢失因为参数在URL上。但如果用$router.push传params参数刷新后参数就没了因为params参数不体现在URL里。涉及详情页跳转的一律用query传ID这是我做前端联调时总结出来的经验。3. 数据库设计门店管理系统的表结构是地基中的地基数据库设计是整个系统成败的关键。我见过不少项目代码写得没什么问题但数据库表结构设计得乱七八糟导致后面写SQL的时候各种别扭。这一节把核心表结构和设计理由讲清楚。3.1 用户、会员与员工表的设计要点用户体系涉及三类表系统用户表、会员表、员工表。系统用户表(sys_user)主要用于登录后台管理界面。字段包括id、username、password我使用的是BCrypt加密存储、real_name、role、status、create_time。其中role字段我直接用字符串存了ADMIN、MANAGER、STAFF三种角色。这种设计比单独建一张角色表和权限表简单得多对小型系统来说够用且好理解。如果你后续要扩展到更复杂的权限体系再拆成RBAC模型也不迟。会员表(member)我单独建因为它和系统用户是两种完全不同的实体。会员表的核心字段是id、name、phone、level、balance、total_consumption、create_time。这里面有两个字段特别值得说。balance存的是会员卡余额我直接用DECIMAL(10,2)类型不用FLOAT或DOUBLE因为浮点数在计算金额时天生有精度误差存钱的事必须用定点数。total_consumption是累计消费金额这个字段的意义在于计算会员等级和后期做营销分析不要等到需要的时候再临时去订单表里SUM那样随着数据量增长会越来越慢不如在每次消费时顺手维护一个累计值。员工表(staff)和系统用户表是关联关系。门店员工的档案信息姓名、电话、职位、入职时间、服务项目技能在员工表里存着他们的登录账号则挂在系统用户表下通过user_id关联。之所以这么做是因为员工和登录用户并不是总是一一对应的——比如一个前台可能不直接服务顾客他登录系统主要是帮顾客操作预约和收银他本身并不是发型师这个业务实体。业务实体和账号分开建模这套系统在应对真实门店的人员变动时会更灵活。3.2 预约、订单与库存表的关系设计预约表(appointment)、订单表(order)和库存表(stock)构成了业务的核心数据流设计这三张表时我花的心思最多。预约表的结构大概是这样的CREATE TABLE appointment ( id bigint(20) NOT NULL AUTO_INCREMENT, member_id bigint(20) DEFAULT NULL COMMENT 会员ID非会员可为空, customer_name varchar(50) NOT NULL COMMENT 顾客姓名, customer_phone varchar(20) DEFAULT NULL COMMENT 联系电话, staff_id bigint(20) NOT NULL COMMENT 发型师ID, service_item_id bigint(20) NOT NULL COMMENT 服务项目ID, appointment_time datetime NOT NULL COMMENT 预约到店时间, service_duration int(11) DEFAULT 60 COMMENT 服务时长(分钟), status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待服务 1已到店 2已完成 3已取消 4已爽约, remark varchar(255) DEFAULT NULL, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_staff_time (staff_id, appointment_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约表;注意这个联合索引idx_staff_time。判断发型师某个时间段是否空闲的查询条件是staff_id ? AND appointment_time BETWEEN ? AND ?这个联合索引能让查询走索引而不是全表扫。虽然门店系统的数据量大概率撑不起索引优化的必要性但这是一个良好的设计习惯而且如果未来接入小程序线上预约数据量增长后这个索引就是性能保障。订单表(orders)记录每一次服务产生的结算信息。关键字段是order_no、member_id、staff_id、total_amount、discount_amount、pay_amount、pay_type现金/微信/支付宝/会员卡、status、create_time。其中order_no我用时间戳加随机数生成作为业务流水号对外展示。同时设计了订单明细表order_item一个订单对应多个明细项每项指向一个服务项目SPU或者一个商品SKU方便后续统计各项目的销售情况。库存表(stock)相对简单核心字段是product_id、stock_count、safety_stock。safety_stock是安全库存阈值当库存数量低于这个值时系统在报表页给出补货提示。这个功能不复杂但门店店长反馈说这是他们最喜欢的功能之一因为以前总是等药水用完了才想起来采购。4. 后端核心接口预约防冲突、事务管理、MyBatis动态SQL实战后端接口是整个系统的中枢。这一章挑三个最有代表性的模块来讲预约的并发防冲突处理、订单收银中的事务管理、报表查询中的MyBatis动态SQL。4.1 预约接口的防冲突设计前后端双重校验预约撞车是美发门店最常遇到的问题也是系统最需要解决的核心问题。接口设计上我没有选择在数据库层面加锁而是采用应用层乐观校验数据库唯一约束兜底的双保险方案。后端校验的逻辑是当收到一个新预约请求时查询同一个staff_id下status为待服务或已到店状态且时间有重叠的记录数量。如果大于等于1就拒绝新的预约。这里的时间重叠判断逻辑是预约开始时间 新预约结束时间 AND 预约结束时间 新预约开始时间。public synchronized boolean checkAppointmentConflict(Long staffId, LocalDateTime startTime, LocalDateTime endTime) { LambdaQueryWrapperAppointment wrapper new LambdaQueryWrapper(); wrapper.eq(Appointment::getStaffId, staffId); wrapper.in(Appointment::getStatus, Arrays.asList(0, 1)); wrapper.lt(Appointment::getAppointmentTime, endTime); wrapper.gt(Appointment::getServiceDuration, 0); // 这里用现有时间和时长计算结束时间再比较 // SQL: WHERE staff_id ? AND status IN (0,1) // AND appointment_time ? // AND DATE_ADD(appointment_time, INTERVAL service_duration MINUTE) ? return count(wrapper) 0; }这里还有一个关键问题并发场景下如果两个请求同时进来都查到当前没有冲突预约然后同时插入还是会撞车。所以我在预约表上加了uk_staff_appointment这个唯一约束用staff_id appointment_time生成唯一键数据库层面的约束做最后的兜底。插入时捕获DuplicateKeyException返回友好的错误提示该时间段已被预约请选择其他时间。双保险双保险说的就是这个。前端也要做一层校验。预约表单里选择时间时提前把该发型师当天的已预约时间段拉下来在日期时间选择器里禁用掉这些时段。前端拦截能提升用户体验后端校验兜底保证数据正确性两者互为补充缺一不可。4.2 订单收银与余额扣减事务边界就是生命线订单收银是系统里对数据一致性要求最高的操作。顾客选择用会员卡结账时的完整操作链路是创建主订单状态为已完成创建订单明细扣减会员卡余额生成一条资金流水根据服务项目明细扣减产品库存计算并记录发型师的提成这六步必须全部成功只要任何一步失败整个操作就要回滚。否则就会出现钱扣了但订单没生成或者订单生成了但库存没扣减的严重问题。SpringBoot的Transactional注解是控制事务的利器。但这里有个关键点事务方法不能通过同类内部的this调用否则事务注解会失效。原因是Spring的事务是基于AOP代理实现的this调用不会经过代理。我见过太多次这个坑代码里this.xxx()调来调去事务静悄悄失效数据错了还排查不到原因。正确的做法是把收银逻辑写在一个方法里由Controller调用这个被Spring代理的Bean方法Transactional(rollbackFor Exception.class) public Order createOrderWithCardPay(OrderCreateDTO dto) { // 1. 创建订单 Order order createOrder(dto); // 2. 扣减会员余额 memberService.deductBalance(dto.getMemberId(), dto.getPayAmount()); // 3. 生成余额流水 balanceLogService.createLog(dto.getMemberId(), -dto.getPayAmount(), 会员卡消费); // 4. 扣减库存 stockService.deductStockByOrder(dto.getItems()); // 5. 计算提成 commissionService.calculateCommission(order); return order; }rollbackFor Exception.class这个属性建议显式写上。默认情况下Transactional只在遇到RuntimeException时才回滚如果业务代码抛出一个自定义的受检异常事务是不会自动回滚的。这个细节在面试中经常考在实际开发中也确实坑过人。4.3 报表查询中的MyBatis动态SQL一个标签解决所有组合查询门店管理系统的报表模块最大的特点是查询条件不固定。今天想看某个时间段内所有发型师的业绩明天想看某个会员的全部消费记录后天想看某类服务项目的趋势。如果为每一种组合都写一个固定的SQLMapper接口会膨胀得没法看。这时候MyBatis的动态SQL就派上了用场。where标签和if标签组合使用是处理动态查询条件的经典方案select idselectOrderReport resultTypemap SELECT DATE(o.create_time) AS biz_date, s.name AS staff_name, COUNT(DISTINCT o.id) AS order_count, SUM(o.pay_amount) AS total_amount FROM orders o LEFT JOIN staff s ON o.staff_id s.id where if teststartTime ! null and startTime ! AND o.create_time #{startTime} /if if testendTime ! null and endTime ! AND o.create_time lt; #{endTime} /if if teststaffId ! null AND o.staff_id #{staffId} /if if testmemberId ! null AND o.member_id #{memberId} /if /where GROUP BY DATE(o.create_time), s.id ORDER BY biz_date DESC /selectwhere标签会自动处理掉第一个条件前面的AND关键字这个特性让SQL拼接变得极其安全。如果你不用where而是手动拼WHERE 11虽然也能实现同样效果但SQL里无端多出一个11看起来总觉得草率了。MyBatis的trim标签也能做到类似效果但我个人最推荐的还是where因为它语义最清晰。这里有个小细节逻辑删除和状态过滤容易写漏。比如订单查询里如果订单有取消状态默认报表查询就应该过滤掉已取消的订单。我的做法是在公共SQL片段用sql标签定义一个基础过滤条件所有统计类查询都引用它这样就不会出现有的报表包含了取消订单有的不包含这种口径不一致的问题。5. 前端Vue工程落地页面拆解与接口联调经验后端的接口设计得再合理前端展示不顺畅这套系统在门店里也用不起来。美发店的前台和发型师对系统的要求就是快、清晰、不容易点错。这一章聊聊前端工程化落地过程中比较关键的部分。5.1 管理后台的页面架构与路由设计管理后台我采用的是经典侧边栏加顶栏布局Element Plus的el-container组件一套就能搭出来。整个后台的页面结构包括仪表盘首页、预约管理、会员管理、订单管理、员工管理、库存管理、报表统计、系统设置八个一级模块。路由设计上要注意模块组织和懒加载const routes [ { path: /, component: Layout, redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 营业看板, icon: Odometer } }, { path: appointment, name: Appointment, component: () import(/views/appointment/index.vue), meta: { title: 预约管理, icon: Calendar } } // 其他模块... ] } ]component: () import(...)这种写法实现了路由级代码分割首屏只加载必要组件切换页面时才按需加载对应模块。门店后台部署在低配服务器上的时候首屏加载速度尤其重要懒加载能明显改善体验。路由守卫是权限管理的前端实现。我在router.beforeEach里做登录态校验未登录一律跳转到登录页。角色权限方面前端根据用户角色动态生成可见菜单管理员和店长能看到报表统计普通员工则没有这个菜单入口。前端菜单隐藏只是体验优化真正的权限控制还是要靠后端的接口鉴权前端隐藏菜单不代表接口就可以裸奔这个安全意识一定得有。5.2 预约看板和收银台的交互设计细节预约管理页面是整个系统里最高频使用的页面前台的日常工作基本都在这里完成。我给它的定位是一个看板默认显示当天的所有预约按时间轴排列每个预约卡片上展示顾客姓名、服务项目、发型师、到店时间、状态不同状态用不同颜色区分待服务蓝色、已到店绿色、已完成灰色、已取消黄色。这个页面有两个交互设计是跟店长反复沟通后确定的。第一已到店的预约卡片要能一键切换成已完成触发收银操作。前台的点击路径要短不能让她先点进详情页再点按钮。第二预约列表的筛选要支持按发型师、按时间段、按状态三个维度自由组合所以这个页面我留了三个筛选器而不是一个搜索框。收银台页面是另一个高频页面交互设计逻辑是先选人再选项目最后选支付方式。选会员时会自动带出会员卡余额并显示余额充足/余额不足的提示。选项目时支持多选右侧实时计算总价和优惠后的应付金额。整个收银操作控制在三步以内因为前台高峰期时同时接待多个顾客操作步骤越长越容易出错。5.3 接口联调中的跨域问题和接口错误码统一处理前后端联调阶段最常遇到的就是跨域问题。前端开发环境跑在http://localhost:5173后端接口跑在http://localhost:8080两个端口不同浏览器会拦截跨域请求。我的解决方案是在后端加CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns(*)和allowCredentials(true)要配合使用如果只用allowedOrigins(*)再配合allowCredentials(true)在某些版本下会报错因为通配符和携带Cookie的凭证模式互斥。接口联调还有一个每个团队都应该做的事统一接口返回结构。我的做法是所有的接口统一返回ResultT对象结构是{ code: 200, message: success, data: ... }。前端在axios拦截器里统一处理这个结构code 200时直接取出data返回给页面其他code弹出统一的错误提示。这样页面代码里就不用每个接口都写一遍错误处理逻辑代码干净很多。axios拦截器的常见写法service.interceptors.response.use( (response) { const res response.data if (res.code 200) { return res.data } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, (error) { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )统一错误码还有个额外的好处后端如果抛出业务异常比如预约时间冲突余额不足预约不存在前端都能拿到清晰的中文提示而不是一个干巴巴的HTTP 500状态码。6. 从开发到上线环境配置、部署细节与常见问题排查源码能在本地跑起来只是第一步真正把系统部署到门店的服务器上还有一些环境配置和部署细节需要注意。这也是我在这套系统上花的时间最多、踩的坑也最多的部分。6.1 环境准备MySQL 8.0安装与连接配置中的几个关键坑MySQL的安装教程网上非常多这里不赘述完整步骤重点说三个我在配置MySQL 8.0时反复遇到的坑。第一个坑是认证插件兼容性问题。MySQL 8.0默认的认证插件是caching_sha2_password而一些老版本的工具和驱动使用的还是mysql_native_password。如果后端连不上数据库报错信息里出现Authentication plugin caching_sha2_password cannot be loaded解决办法是创建一个使用mysql_native_password插件的用户或者升级数据库驱动到最新版本。我更推荐后者因为这是从根上解决问题。第二个坑是时区问题。MySQL 8.0的连接URL里如果没有设置serverTimezone你可能会发现数据库时间和本地时间差好几个小时。这个问题在部署到云服务器时尤其明显因为云服务器默认时区可能是UTC。我的做法是在JDBC连接串里显式设置serverTimezoneAsia/Shanghai同时在MySQL配置文件my.cnf里设置default-time-zone 08:00双保险确保时间一致。spring: datasource: url: jdbc:mysql://localhost:3306/hair_salon?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这个参数也值得解释一下。使用caching_sha2_password认证时如果连接走的是非SSL通道首次连接需要从服务器获取公钥来加密密码没有这个参数就会报错。虽然是开发环境的常见配置但在生产环境使用前要确认网络环境和安全策略允许。第三个坑是数据库字符集。建库的时候一定要用utf8mb4而不是utf8。MySQL的utf8实际上是utf8mb3只能存基本的多语言字符存不了emoji表情比如会员昵称里带个笑脸符号就会报错。而且utf8mb4是utf8的超集向下兼容不存在不用的理由。建库语句直接写成CREATE DATABASE hair_salon DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;一劳永逸。6.2 SpringBoot项目打包与服务器部署实践后端打包用的是Maven。在项目根目录执行mvn clean package -DskipTests打出来的JAR包在target目录下。这一步有个细节——如果用了多环境配置文件application-dev.yml和application-prod.yml打包时要指定用哪个环境mvn clean package -DskipTests -Dspring.profiles.activeprod部署时我习惯用nohup让JAR包在后台运行nohup java -jar hair-salon-server.jar --spring.profiles.activeprod app.log 21 日志重定向到app.log文件排查问题的时候直接tail -f app.log看日志输出。如果修改了配置需要重启先ps -ef | grep hair-salon查进程ID然后kill再重新启动。前端打包是Vite构建命令npm run build构建产物在dist目录下。部署的时候把dist目录里的静态文件放到Nginx的根目录再配置Nginx反向代理把/api前缀的请求转发到后端服务。Nginx的配置大概是这样的server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; 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; } }try_files $uri $uri/ /index.html;这一行很关键。Vue Router用history模式时刷新页面会向服务器发起真实请求如果服务器上没有对应的路由文件路径就会出现404。这行配置把所有不存在的路径都重写回index.html由前端路由接管刷新就不会404了。6.3 上线前必查清单与实际运营中的二次优化最后整理一份上线前必查清单这些都是我部署完被临时叫起来排查问题时积累出来的第一检查数据库连接池配置。SpringBoot默认的HikariCP连接池很好用但要确认maximum-pool-size设置合理。门店系统这个规模核心服务maximum-pool-size设10就够了minimum-idle设5。如果参数不匹配比如多个服务实例共享同一个数据库连接数设置太大反而会把数据库连接耗尽。第二定时任务和软删除的坑。如果项目里用Scheduled做定时统计比如每天凌晨汇总前一天的营业数据注意定时任务所在的服务要只有一个实例在执行。如果部署了两个实例定时任务就会重复执行数据翻倍。我当时犯过这个错误后来在配置中心加了任务执行开关只有主节点开启定时任务。第三生产环境要做的安全配置。首当其冲的是MySQL密码不要硬编码在application.yml里。我至少做了两件事一是把配置文件放在JAR包外部部署时通过--spring.config.location指定外部配置这样修改配置不用重新打包二是生产环境数据库密码通过环境变量注入配置里只用${DB_PASSWORD}占位符。另外一个很容易被忽视的点是生产环境一定记得把MyBatis的SQL日志输出关掉否则SQL全打到日志文件里日志文件会膨胀得非常快而且SQL里往往带着查询参数长期看也是一种数据暴露风险。第四数据备份。门店的营业数据丢了是没法挽回的。我给服务器配了一个每天凌晨的MySQL自动备份任务备份文件保留最近7天。具体做法是写一个cron定时任务执行mysqldump把整个hair_salon库导成SQL文件再按日期命名归档。这套机制不复杂但真发生误删数据的时候能救命。系统上线运行一段时间之后店长反馈最多的需求居然是能不能加一个会员生日提醒和能不能在收银页面显示顾客最近一次来的消费记录。这两个需求本质上都指向同一个方向帮门店员工更好地服务老客户。我在会员列表页加了生日字段和到期提醒标签在收银台页面加了最近消费抽屉栏这两个小改动让系统的实用性提升了一个台阶。很多时候客户提出的需求背后真正想要的并不是复杂的功能而是能让自己工作更省心、让顾客体验更好的小细节。设计系统的时候别总想着做大做全先解决最痛的点再根据实际使用反馈迭代这条路通常是对的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价