SpringBoot+Vue+MyBatis+MySQL:宠物咖啡馆管理系统全栈实战拆解
2026/9/24 23:33:44 网站建设 项目流程

宠物咖啡馆这几年在一二线城市真的很火,但真正把“咖啡点单”和“宠物互动预约”打通的管理系统其实并不多。我手上这套基于SpringBoot+Vue+MyBatis+MySQL的宠物咖啡馆平台管理系统,就是专门来解决这种线下融合业态的管理痛点:用户端能在线看菜单、下单、预约撸猫时段,管理端能管宠物档案、库存、会员和订单报表。整条链路从前端页面到后端接口全部打通,适合正在做全栈练习的开发者,也适合想给实体咖啡馆做信息化改造的团队参考。这篇文章我会从技术选型、数据库设计、核心代码实现到部署排查一条龙拆解,不绕弯子,全部是能直接落地的东西。

1. 项目整体设计与技术选型拆解

1.1 为什么选择“SpringBoot+Vue+MyBatis+MySQL”这套组合

先说后端。SpringBoot现在是Java后端的绝对主流,它对第三方组件的整合能力极强,内嵌Tomcat之后连部署都省心。做这种中小型管理系统,要求的是快速迭代、稳定可靠,SpringBoot的自动装配机制和生态成熟度天然匹配。再加上RESTful API的开发模式,前端后端分离以后各改各的,互不干扰。

MyBatis的选择有一层现实考量:宠物咖啡馆这种业务涉及的查询逻辑不复杂但变化频繁,比如不同时段的预约人数统计、咖啡销量排行、会员储值明细,每种需求对应的SQL都不太一样。MyBatis把SQL写在XML里,改起来不用重新编译Java代码,这个灵活性在业务调整频繁的场景里特别有用。而且它不屏蔽SQL本身,开发者对每一条查询都心里有数,排查性能问题也更直接。

数据库用MySQL就不用多解释了,开源免费、文档丰富、国内使用面最广。宠物咖啡馆的数据量级别是中小型的,单库单表完全扛得住,根本不需要过早引入分布式中间件来增加复杂度。整套技术栈的逻辑是:用最稳妥的组合覆盖80%的业务场景,剩下20%的个性化需求通过MyBatis的SQL可控性和Vue的组件化来兜底。

1.2 核心业务模块与功能边界划分

整个系统拆成用户端和管理端两条线,端口分开、权限分开、数据通过接口互通。

用户端围绕的是一个普通顾客进店前后的完整动线:

  • 菜单浏览与咖啡点单:展示咖啡、甜点、宠物零食等商品,支持加入购物车、统一下单。
  • 宠物互动预约:选择时间段预约与店内的猫狗互动,核心是时段名额控制,防止某个时段人太多,影响宠物状态。
  • 会员中心:注册登录、储值、消费记录查询、会员等级展示。
  • 订单管理:查看历史订单、订单状态跟踪(待付款、已付款、进行中、已完成)。

管理端对应的是门店运营人员的日常操作界面:

  • 宠物档案管理:每只宠物的名字、品种、年龄、疫苗状态、注意事项,支持图片上传。
  • 商品管理:上下架咖啡饮品、宠物零食、周边商品,维护库存和价格。
  • 预约管理:查看所有时段的预约情况,支持手动取消或改期。
  • 会员管理:客户列表、储值记录、消费频次标签。
  • 订单管理:订单列表、发货/核销操作、退款处理。
  • 数据看板:当日营业额、热门商品、宠物接客频次统计。

模块划分的时候有一个关键原则:用户端页面要轻、管理端要全、两端共用同一套后端接口,但通过角色权限做隔离。这样将来如果想给用户端做小程序版本,后端接口基本不用动,只需要重新写一套视图层。

1.3 业务流程梳理与状态机设计

业务流程是最容易写出问题的地方,尤其是订单和预约这两个核心链路。

订单流程我设计成五个状态:待付款、待制作、已完成、已取消、已退款。用户下单后系统自动生成订单号,前端倒计时15分钟未付款自动关闭。支付成功后状态变为待制作,后台操作员点击开始制作可以加一个“制作中”的中间状态,但对接到咖啡机等IoT设备时再扩展也不迟。咖啡做好后进行核销操作,状态置为已完成,这时才真正扣减库存。

预约流程会稍微复杂一点,因为涉及到人与动物的互动。我设计了“预约提交-待确认-已确认-已完成-已取消”五个状态。用户提交预约后不直接占用名额,需要门店手工确认,确认时系统检查当前时段剩余名额,名额足够才置为已确认。这里有个细节:MySQL的事务隔离级别默认是可重复读,如果并发情况下两个用户同时预约同一个时段,可能会导致超卖,所以在确认预约时用了SELECT FOR UPDATE来锁定时段记录,确保同一时刻只有一个请求能通过名额校验。

这样做的好处是状态流转路径清晰,前后端联调时对接口的预期一致,后续接消息通知(比如微信模板消息)也能在状态变更点挂上钩子。

2. 功能落地:用户端与管理端的关键实现

2.1 用户点单与购物车的接口设计

先说购物车。购物车本质上是一个临时数据容器,可以用前端状态管理(Vuex/Pinia)实现,也可以用后端表存起来。我的建议是登录用户购物车存后端,游客购物车存前端。原因很简单:如果用户换设备登录,后端购物车能直接同步过来;如果是游客,后端存了也无法识别身份,反而留下脏数据。

后端购物车表核心字段就这些:cart_id、user_id、product_id、quantity、checked。接口设计成四个就够了:

@RestController @RequestMapping("/api/cart") public class CartController { @GetMapping("/list") public Result<List<CartVO>> getCartList(@RequestParam Long userId) { return Result.success(cartService.getCartList(userId)); } @PostMapping("/add") public Result<Void> addToCart(@RequestBody CartAddDTO dto) { cartService.addToCart(dto); return Result.success(); } @PutMapping("/update") public Result<Void> updateCart(@RequestBody CartUpdateDTO dto) { cartService.updateQuantity(dto.getCartId(), dto.getQuantity()); return Result.success(); } @DeleteMapping("/remove/{cartId}") public Result<Void> removeFromCart(@PathVariable Long cartId) { cartService.removeFromCart(cartId); return Result.success(); } }

这里面的一个细节是商品价格不能直接从前端传,否则用户可以改价格下单。正确做法是前端只传product_id和quantity,后端实时从数据库读当前价格,再计算小计和总价。我见过不少项目在这里偷懒,结果被薅羊毛,属于典型的低级事故。

点单提交时还要做一个库存预扣,防止用户下单了但咖啡豆不够用。预扣的意思是下单时不真正扣库存,只锁定一定数量,订单超时未支付再释放。我加了一个定时任务,每分钟扫描一次超过15分钟未支付的订单,把预扣库存加回来,保证库存数据的准确性。

2.2 宠物互动预约的时段与名额控制

宠物互动预约是整个系统里最需要细心设计的模块,它直接关系到店铺的接待能力和宠物福利。

我设计的预约规则:每天分成若干个时段(10:00-11:30、13:00-14:30、15:00-16:30等),每个时段每只宠物最多接待3位顾客。这个数字纯粹是从运营角度定的,猫狗接客太多会烦躁,少了大不赚钱。

核心表结构是pet_appointment_slots,记录每个时段的基础信息,每天提前生成未来7天的时段记录:

CREATE TABLE pet_appointment_slots ( slot_id BIGINT PRIMARY KEY AUTO_INCREMENT, pet_id BIGINT NOT NULL COMMENT '宠物ID', slot_date DATE NOT NULL COMMENT '可预约日期', start_time TIME NOT NULL COMMENT '开始时间', end_time TIME NOT NULL COMMENT '结束时间', total_quota INT NOT NULL DEFAULT 3 COMMENT '总名额', booked_count INT NOT NULL DEFAULT 0 COMMENT '已预约人数', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-可预约 0-已关闭', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', UNIQUE KEY uk_pet_slot (pet_id, slot_date, start_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

预约接口我用了乐观锁来防并发超卖,SQL是这样写的:

<update id="bookSlot"> UPDATE pet_appointment_slots SET booked_count = booked_count + 1, version = version + 1 WHERE slot_id = #{slotId} AND booked_count &lt; total_quota AND version = #{version} </update>

执行后判断影响行数,如果为0说明名额已满或者version不匹配,直接返回“该时段已被约满”。这里使用version字段做乐观锁,配合更新条件里的booked_count < total_quota,双保险。实测下来高并发场景下偶尔会有几个请求被拒绝,但不会有超卖,符合实际业务预期。

2.3 管理端订单处理与库存联动

管理端订单列表我用了分页加多条件筛选的组合,支持按订单号、用户手机号、订单状态、日期区间查询。前端用一个搜索表单,后端对应一个动态SQL查询。

核心的订单确认接口要做三件事:更新订单状态、扣减实际库存、生成操作日志。这三件事必须在同一个事务里完成,否则就会出现库存扣了但订单状态没更新,或者订单显示已完成但库存对不上的情况。

@Transactional(rollbackFor = Exception.class) public void confirmOrder(Long orderId) { Order order = orderMapper.selectById(orderId); if (order == null || !"待制作".equals(order.getStatus())) { throw new BusinessException("订单状态异常,无法确认"); } // 扣减库存 List<OrderItem> items = orderItemMapper.selectByOrderId(orderId); for (OrderItem item : items) { int affected = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected == 0) { throw new BusinessException("商品库存不足:" + item.getProductName()); } } // 更新订单状态 orderMapper.updateStatus(orderId, "制作中"); // 记录日志 operationLogMapper.insert(new OperationLog("确认订单", orderId)); }

事务回滚的时候要特别小心:业务异常必须抛出RuntimeException或者自定义异常,rollbackFor要设置为Exception.class,否则Spring默认只在RuntimeException时回滚。我现在写代码基本都习惯在@Transactional注解上显式指定rollbackFor,减少踩坑的概率。

3. 数据库设计与MyBatis实战细节

3.1 核心表结构设计思路

数据库设计在这个项目里是生命线。我建了这些核心表:用户表、会员卡表、商品表、购物车表、订单表、订单明细表、宠物表、预约时段表、预约记录表、库存流水表、操作日志表。

订单表和订单明细表的关系是一对多,设计上必须分开,因为一张订单可能包含多杯咖啡、几份甜点。订单表存的是订单级别的汇总数据(订单号、总金额、状态、支付时间),订单明细表存的是每个商品项的数量、单价、小计。

会员与用户表我选择分开设计。user表解决的是登录认证,member表解决的是会员业务(储值余额、积分、等级)。拆开的理由:并不是所有注册用户都会充值成为会员,混在一张表里会有一堆冗余空字段。member表通过user_id外键关联,没有会员身份的用户查询时left join出来直接就是null。

宠物表的设计要考虑运营维度,除了基本信息(名字、品种、年龄)外,我加了health_status(健康状况)、temperament(性格特点)、daily_max_visitors(每日最大接待人数)这些字段。认真做的话还能加上feeding_record(喂食记录)、vaccine_expiry_date(疫苗到期日)这种养护类字段,能提升系统的专业感。

3.2 MyBatis分页插件与缓存配置

分页是老生常谈的问题,但这个项目里一定要用对。我集成的是PageHelper,理由很简单:使用成本极低,侵入性小。

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>

调用方式是在Mapper查询方法前一行设置分页参数:

public PageResult<OrderVO> getOrderPage(OrderQueryDTO query, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<OrderVO> list = orderMapper.selectOrderList(query); PageInfo<OrderVO> pageInfo = new PageInfo<>(list); return PageResult.of(pageInfo); }

分页插件有几个容易犯的错,我把自己的经验写出来:

  • PageHelper.startPage只对紧接着的下一条查询生效,查询完要立即获取结果,中间不能穿插其他SQL操作,否则分页会失效或者分页到错误的查询上。
  • 如果Mapper方法里有foreach批量插入或者多个select语句,分页可能被意外吞掉,建议这种复杂查询自己手写LIMIT。
  • 查询结果要用PageInfo来包装,它能拿到total、pageNum、pageSize、pages这些分页元数据,前端的分页组件直接对接即可。

MyBatis的缓存我建议把二级缓存默认关掉,理由跟很多资深开发者的观点一致:缓存失效条件不好控制,尤其是有联表查询的场景,一张表更新后关联表缓存无法自动清除,很容易查出脏数据。一级缓存是SQLSession级别的,在Spring管理下每次请求都会新建SQLSession,所以一级缓存的意义也不大。这个项目里的缓存主要靠Redis解决,页面上频繁查询且变化不频繁的数据(比如热门商品列表)直接Cache Aside模式写入Redis。

3.3 高频SQL的性能优化与慢查询排查

开发阶段我开了MySQL的慢查询日志,标准设置为超过1秒的记录,同时配合MyBatis的日志打印,把所有SQL输出到控制台,开发时用眼睛扫就能发现很多写得很难看的SQL。

# application.yml 中的 MyBatis 日志配置 mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

慢查询的典型案例是订单列表联表查询。订单表、订单明细表、用户表、商品表四表联查,如果没有索引,数据量一旦过万就会明显变慢。我给所有外键字段都建了索引:order_id、product_id、user_id、pet_id、slot_id。还有一个容易被忽略的点:状态和时间范围查询一定要建联合索引,比如(status, create_time),这样按状态过滤后再按时间排序才能用上索引。

之前我遇到过一个UPDATE执行慢的问题,排查发现是更新条件里的字段没有索引,导致全表扫描。UPDATE虽然不产生结果集,但WHERE子句的匹配一样要扫描,加索引后执行时间从300ms降到了5ms以内。还有一点,批量更新的时候不要用循环一条条UPDATE,而是用CASE WHEN的方式拼一条SQL批量更新,虽然SQL看起来长一些,但数据库只需要解析一次,性能和日志量都会好很多。

4. 前端Vue实现与环境部署细节

4.1 Vue3环境搭建与依赖管理

前端我选的是Vue3 + Vite + Element Plus的组合。Vite启动速度快,开发体验比Webpack时代舒服太多了。

环境搭建的几个核心步骤:

# 使用 Vite 初始化项目 npm create vite@latest pet-cafe-web -- --template vue # 进入项目目录并安装依赖 cd pet-cafe-web npm install # 安装路由、状态管理、UI组件库、HTTP库 npm install vue-router@4 pinia element-plus axios

这里提醒一句:Node版本不能太低,我用的是Node 18,Vite 5要求Node版本18+。如果你还在用Node 14,建议直接升级到18或20长期支持版本,否则装依赖的时候会报一堆engine提示,甚至直接安装失败。

Vite的代理配置要特别留意,开发环境下前端和后端端口不同,跨域问题靠Vite的server.proxy解决,而不是后端开启CORS。开发时配置代理,生产环境用Nginx做反向代理,前后端都部署在同一个域名下,从根上规避跨域:

// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

4.2 页面组件拆分与路由配置

Vue项目最忌讳一个页面塞几百行代码。我把页面粒度拆到“一个业务功能一个页面”,比如订单模块拆成订单列表页、订单详情页、订单创建页,每个页面里的弹窗表单再拆成子组件。

路由配置用了懒加载,按需加载页面资源,避免首屏一次性加载所有体积过大的JS文件:

const routes = [ { path: '/', component: () => import('@/layouts/MainLayout.vue'), children: [ { path: '/dashboard', component: () => import('@/views/dashboard/index.vue') }, { path: '/orders', component: () => import('@/views/order/OrderList.vue') }, { path: '/pets', component: () => import('@/views/pet/PetList.vue') }, { path: '/login', component: () => import('@/views/login/index.vue') } ] } ]

路由守卫里要做两件事:未登录的用户统一跳到登录页;已经登录但访问了无权限页面时,跳转404页。判断逻辑写在router.beforeEach里,从Pinia或localStorage中读取token,没有token直接next('/login')。

状态管理用Pinia代替了Vuex,接口请求我统一封装成request工具类,基于axios实例创建,设置baseURL、超时时间,加请求拦截器带token,响应拦截器统一处理错误码。用户登录后token保存在localStorage,每次请求自动携带在Authorization的请求头里。

4.3 数据可视化与视频流播放的落地

管理端的数据看板我用了ECharts实现,画了当日营业额趋势折线图、商品销量排行柱状图、各时段预约人数热力图。ECharts和Vue的集成方式很简单,npm安装echarts,组件里用ref绑定DOM节点,在onMounted里初始化图表。

需要注意的一个坑是:ECharts实例在组件销毁时一定要调用dispose()方法释放资源,否则页面频繁切换会出现内存泄漏,页面越来越卡。另外图表数据最好在接口返回后统一format成ECharts需要的格式,不要在option里做复杂转换,可维护性差。

宠物展示页的视频播放是个加分项。咖啡馆可以在店里装摄像头,把宠物的实时画面传到系统里给用户看。在Web端播放m3u8格式的视频流,我用的方案是hls.js库,如果你不想额外引入库,新版Safari浏览器原生支持m3u8播放,但Chrome和Firefox都必须借助hls.js。

<template> <video ref="videoRef" controls autoplay muted></video> </template> <script setup> import { ref, onMounted } from 'vue'; import Hls from 'hls.js'; const videoRef = ref(null); onMounted(() => { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource('https://your-stream-url/live/pet-room.m3u8'); hls.attachMedia(videoRef.value); hls.on(Hls.Events.MANIFEST_PARSED, () => { videoRef.value.play(); }); } }); </script>

实测下来hls.js的播放延迟大约在3-8秒,用于店内的宠物实时展示完全够用。如果需要低延迟可以换WebRTC方案,但服务器配置和技术复杂度会成倍增加,不是这种管理系统的重点。

5. 部署上线与问题排查实录

5.1 前后端分离的生产环境部署流程

部署这块我自己走了一遍完整的流程,从裸机到能访问大概半小时。服务器配置是2核4G的云主机,装的CentOS 7.9,MySQL和Nginx用包管理器安装,Java环境用OpenJDK 17。

后端打包和启动:

# 跳过测试打包 mvn clean package -DskipTests # 启动 jar 包,指定生产环境配置 nohup java -jar pet-cafe-server.jar --spring.profiles.active=prod > logs/app.log 2>&1 &

生产环境的配置文件里我做了几项调整:数据库连接加了时区参数serverTimezone=Asia/Shanghai;连接池最大连接数设置为20;Redis如果有的话也配置上;日志级别调整为INFO,避免生产环境打印大量DEBUG日志拖慢性能。

前端构建:

npm run build

构建产物在dist目录下,我直接把它上传到服务器的/usr/share/nginx/html目录,然后在Nginx配置里加反向代理和前端路由的重写规则:

server { listen 80; server_name pet-cafe.example.com; root /usr/share/nginx/html; index index.html; # 前端路由 history 模式配置 location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存策略 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 7d; add_header Cache-Control "public, max-age=604800"; } }

这里最关键的配置是try_files $uri $uri/ /index.html;。因为Vue Router使用history模式,用户在订单详情页刷新时,Nginx直接按文件路径找肯定找不到对应的物理文件,必须把所有路径都重写到index.html,再由前端路由接管。

5.2 上线后遇到的典型问题与排查方法

我把上线后这段时间踩过的比较有代表性的坑整理成了一张速查表,方便你遇到同类问题时直接对照:

问题表现排查思路解决方案
页面能打开但接口全部报404检查Nginx的location /api代理规则是否生效确认proxy_pass的target地址和后端端口是否一致,用curl直接访问后端接口验证
数据库连接超时,报Too many connections连接池配置过大或连接未释放减小连接池大小,加Druid/ HikariCP的泄漏检测参数
预约时段名额超卖并发校验逻辑有竞态条件加乐观锁或SELECT FOR UPDATE,重测并发场景
前端页面刷新后白屏路由模式与Nginx配置不匹配检查try_files配置,history模式必须重写到index.html
上传宠物图片后访问403图片文件权限不对检查静态资源目录的读写权限,或用Nginx的alias指定图片目录
用户支付成功但订单状态未更新支付回调接口被防火墙拒绝在Nginx中放行支付平台回调的URL,不要用全局鉴权拦截

还有一个非常容易坑人的点:生产环境用HTTPS后,如果页面里加载的接口还是http协议,浏览器会直接拦截混合内容。统一把API地址改成https或者采用相对路径发起请求,让浏览器自动继承页面协议。

另外我在生产环境开启了Sentry前端错误监控,用户端报错的时候能第一时间看到堆栈信息,而不是等用户打电话来反馈“页面打不开”。后端日志用logback滚动策略,每天一个文件,保留30天,磁盘占用也不会太大。

5.3 初始化脚本与快速复现指南

最后分享一个让整套系统快速跑起来的方法。我在发布时把初始化的SQL脚本整理成init.sql,包含了建库、建表、基础数据插入、测试账号创建。拿到的同学只需要三件事就能跑起来:

# 1. 初始化数据库:执行 init.sql(含建库语句和测试数据) mysql -uroot -p < init.sql # 2. 启动后端:确认 application-prod.yml 中数据库账号密码改为自己的 mvn spring-boot:run -Dspring-boot.run.profiles=prod # 3. 启动前端:确认 .env.production 中的接口地址正确 npm run dev

测试账号我预置了两个:一个管理员admin/admin123,一个普通用户test/test123。前端登录后根据角色自动渲染对应菜单,管理端能看到完整的管理功能,普通用户只能看自己的点单和预约记录。

数据库的初始化脚本里我还加了索引优化和少量视图。比如订单金额统计的报表可以用视图实现,但考虑到MySQL视图的物化性能和调试难度,最终报表还是用临时表+聚合查询直接做的,逻辑更透明,出了问题也好排查。

写在最后的几句实在话

开发这套系统前前后后花了大概三周,踩过的坑比我原以为的多不少。从最初的需求设计到数据库建表,再到前后端联调和部署上线,每一个环节都有值得复盘的地方。如果你是想学习全栈开发、做毕业设计或者给线下咖啡馆做信息化改造,这套系统的技术栈和业务复杂程度刚好合适——既有普通增删改查,又有预约并发控制、订单状态机、报表统计这类稍有挑战性的点。

个人最大的感受是:很多项目不是难在技术本身,而是难在业务细节的考量和数据边界的约束。比如预约机制里人和宠物的双向保护,订单和库存的一致性维护,这些东西如果开发前没有想清楚,写代码的时候就会反复推翻重来。把这个项目的业务逻辑吃透,比单纯背十个框架面试题有用得多。

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

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

立即咨询