
把 springbootvue 的校园餐厅商铺管理系统做完并部署上线之后我最想说的其实是这类系统真正难的不是页面多好看、功能多齐全而是把“商家入驻 → 学生点餐 → 订单流转 → 数据统计”这条业务链路用一套清晰的权限边界和状态机串起来。如果你正准备做类似的 Web 项目不管是毕业设计、课程设计还是真实需求这篇文章会帮你少走很多弯路。下面我会从技术选型、数据库设计、前后端核心实现到部署上线的完整过程把我的做法和踩过的坑一次性讲清楚。1. 校园餐厅商铺管理系统的定位三个角色一张订单链1.1 校园餐饮独有的管理痛点先说一个很现实的问题为什么校园餐厅需要一套独立的商铺管理系统而不是直接套用美团、饿了么那套商业平台的逻辑校园餐厅和商业综合体最大的区别在于“封闭场景 固定客流”。一个大学校区里通常有多个食堂每个食堂又分若干档口档口的经营者流动性很大可能一个学期就换一批。学生用餐高峰集中在中午和傍晚订单量瞬间打满人工记录很容易出差错。同时学校后勤需要掌握每个商铺的经营状况、卫生评价、用户投诉过去靠纸质表格和微信群接龙很难沉淀数据。这套系统的核心目标就是把“商铺入驻审核 → 学生浏览点餐 → 订单履约 → 评价反馈 → 经营统计”这条完整链路搬到 Web 上。它服务的对象不是平台运营方而是校园内部的三类人学生、商铺档口经营者、管理员后勤或食堂负责人。1.2 系统最终要交付什么三个端的功能边界我在做需求拆解时没有急着建表而是先画了一张职责边界表。这个习惯很重要它能避免后续开发中“接口该给谁开、字段该谁维护”的混乱。角色主要功能核心诉求学生浏览商铺和菜品、加入购物车、下单、模拟支付、查看订单、评价流程顺畅下单快商家菜品增删改查与上下架、接单/完成订单、查看营业数据操作简单数据清楚管理员商铺入驻审核、用户管理、全局数据看板监管直观统计准确很多同类项目的失败点在于把学生端做得很花哨但商家端几乎没有工具属性。实际上菜品上下架和接单操作才是商家每天都会用的功能。我在设计时把商家端定位成“工具”尽量减少页面跳转层级菜品管理、订单处理都在一个页面上完成。1.3 为什么说这是一个“麻雀虽小五脏俱全”的 Web 项目如果你想找一个能覆盖 Web 开发核心技能的实战项目这个系统是非常合适的。它几乎囊括了企业级应用最常见的几个模块登录鉴权JWT Spring Security、数据权限角色控制、文件上传菜品图片、定时任务超时订单关闭、数据聚合经营统计、前后端分离联调、Nginx 部署。技术栈不算新但胜在覆盖面完整。正因为它没有特别冷门的技术你踩过的每一个坑网上基本都能找到对应的解决方案这对学习和排查问题都很友好。2. 技术选型SpringBoot 2.7 Vue 3 这套组合的取舍逻辑2.1 后端SpringBoot MyBatis-Plus为什么不选 JPA后端我选了 SpringBoot 2.7.18 MyBatis-Plus 3.5.x没有选 JPA也没有选 SpringBoot 3.x。原因有几个。第一个是版本稳定性的问题。SpringBoot 2.7 是 2.x 系列的最后一个大版本兼容 JDK 8而 JDK 8 目前依然是很多学校和企业服务器的主流环境。SpringBoot 3.x 虽然新的但要求 JDK 17部分老的第三方依赖如果没有及时适配很容易出现版本冲突。第二个是查询可控性的问题。餐厅商铺管理系统涉及订单、统计这类数据SQL 往往需要多表关联和条件聚合。MyBatis-Plus 在保留 MyBatis 灵活 SQL 能力的基础上提供了 BaseMapper 和 LambdaQueryWrapper单表 CRUD 不用写 XML复杂统计还是可以手写 SQL两头兼顾。JPA 虽然开发效率高但复杂查询的 SQL 调优和拼接逻辑对新手来说相对难掌控。再说说 SpringBoot 自动装配的原理。初次接触 MyBatis-Plus 时你会发现引入一个依赖再配上数据源就能直接注入 Mapper 使用了这就是依赖包下的spring.factories或AutoConfiguration.imports文件在起作用。SpringBoot 启动时会扫描这些自动配置类帮你完成 SqlSessionFactory、MapperScanner 等组件的初始化。理解这个机制对排查“为什么项目启动报找不到 Bean”这类问题特别有帮助。2.2 前端Vue 3 Vite Element Plus Pinia前端选型我直接锁定了 Vue 3 Vite Element Plus Pinia。Vite 相比 Webpack 最大的优势是开发环境启动速度快它利用浏览器原生 ES Module省掉了打包这一步。校园管理系统这种中后台项目页面数量不算特别多但组件化拆分之后Vite 的热更新体验还是明显优于 Webpack。状态管理用了 Pinia。Vue 2 时代常用 VuexVue 3 之后 Pinia 更轻量去掉了 mutations直接在 store 里同步修改 state写起来简单很多。购物车、用户登录信息这种跨组件共享的数据放到 Pinia 里管理非常合适。组件库选了 Element Plus。它的表格、表单、弹窗、上传组件都很齐全尤其适合管理系统这种“增删改查 筛选”的典型页面形态。需要注意的是 Element Plus 只兼容 Vue 3如果你项目还是 Vue 2就只能用 Element UI。2.3 环境与基础设施规划JDK、MySQL、Redis、文件存储开发环境我建议统一版本避免“在我机器上能跑”的尴尬JDK 8Maven 3.6MySQL 8.0Redis 6.x用于短信类登录验证码、Token 黑名单、首页缓存Node.js 16 以上MySQL 8.0 的驱动类名和 5.x 不同配置用的是com.mysql.cj.jdbc.Driver时区也建议显式指定为Asia/Shanghai不然很容易出现数据库时间和本地时间相差 8 小时的问题。Redis 在这个项目里不是必须的但我还是加上了。主要用途是缓存商铺列表和菜品列表减轻数据库压力。另一个用途是模拟支付时的防重复提交 Token当然这个也可以用 JVM 级锁解决。如果你想把项目精简一些不用 Redis 也能跑通用本地缓存或者直接查库都行。文件存储方面菜品图片最开始直接用本地磁盘路径保存简单但我建议在表中只存相对路径比如/images/dish/1690000000001.jpg服务器上单独建一个静态资源目录然后通过 Spring 的静态资源映射暴露出去。后续如果要迁移到云存储只需要把上传逻辑改一下前端不用动。3. 从需求到模块学生端、商家端、管理端的具体功能拆解3.1 学生端从找店到评价的完整闭环学生端的核心流程是首页浏览商铺 → 进入商铺查看菜品 → 加入购物车 → 结算下单 → 支付 → 查看订单状态 → 收货后评价。首页我设计了两级入口。第一级是商铺列表展示商铺名称、评分、营业状态第二级是商铺详情重点展示菜品分类和菜品卡片。学生可以按食堂分区筛选商铺也可以搜索菜品名称。购物车部分没有让用户注册强制完善资料而是在下单时填写送餐地址比如宿舍楼栋加房号和备注。校园场景下“到店自取”和“送餐到楼”是两种常见履约方式我在订单表里用一个delivery_type字段区分。虽然只是一个字段但实际开发中商家接单逻辑会因此不同自取订单只需要备餐送餐订单还要留意配送地址。订单结束后学生可以对商铺评分1~5 星并填写评语。评分会实时更新到商铺表里的平均分字段。这里要注意平均分字段应该在同一个事务里更新避免出现商铺显示 5 星实际评价记录里全是 1 星这种数据不一致。3.2 商家端菜品管理与接单履约的工具属性商家端我做了三个页面菜品管理、订单管理、营业统计。菜品管理页是商家使用频率最高的页面所以表格里除了展示菜品名称、价格、分类还直接放了一个 switch 组件来控制上下架不用进入编辑页。修改状态时调用一个专门的下架接口后端会校验商铺状态避免“已停业商铺的菜品还在首页显示”。订单管理页按状态分 Tab 展示比如“待接单”“制作中”“已完成”。商家点击接单后订单状态从“已支付”变成“制作中”备餐完成点“出餐”订单变成“已完成”。整个过程中商家只能看到自己商铺的订单这是通过数据行级权限实现的后端查询时必须带上shop_id条件。这个接口如果删掉了shop_id过滤学生端就能看到别人的订单这是非常严重的越权漏洞。营业统计页用 ECharts 展示近 7 日订单量和销售额柱状图以及热门菜品 Top10 列表。商家不一定每天看数据但月底核对收入时很有用。3.3 管理端入驻审核与经营分析的宏观视角管理端是容易被忽略但很重要的部分。商铺入驻时商家账号先注册然后提交商铺入驻申请管理员审核通过后商家才能登录商家端进行菜品管理。这是一个典型的“注册 入驻审核”流程。管理端页面包括用户管理、商铺审核、商铺管理、分类管理、数据看板。用户管理可以禁用某个违规账号商铺管理可以强制将某个商铺停业数据看板展示总用户数、商铺数、今日订单数、今日销售额以及各食堂营收占比。这里有一个细节管理员操作“强制停业”时除了修改商铺状态还必须把该商铺的菜品全部下架已支付的未完成订单要触发退款通知。这个操作建议放在一个事务方法里完成如果用两个独立接口中间万一出错数据就处于“商铺已停业但菜品还在卖”的状态。3.4 权限模型角色散落在前端路由和后端接口里权限模型是这套系统最核心的骨架。我用的是经典的 RBAC 简化版本用户表里直接存一个role字段三个值分别是STUDENT、MERCHANT、ADMIN。学生可以访问/user/**商家可以访问/merchant/**管理员可以访问/admin/**其他角色访问一律返回 403。后端的做法是写一个 Spring Security 配置类用antMatchers按路径前缀做拦截再配合 JWT 里的角色信息做校验。前端路由同样按角色做了动态渲染登录成功后根据用户角色动态添加对应路由。菜单是从后端返回的menuList渲染的不让前端写死这样后续加新页面时不需要重新发布前端版本。4. 数据库设计商铺、菜品、订单、评价的表结构逻辑4.1 核心表清单和关键字段我建了 6 张核心业务表外加 1 张用户表。下面直接列出主要字段理解表结构比背建表语句更重要。user 表id、username、passwordBCrypt 加密后的密文、real_name、phone、roleSTUDENT/MERCHANT/ADMINavatar、status0 禁用 / 1 正常、create_timeshop 表id、user_id商家账号 id、name、logo、description、noticeaddress、business_hours、status0 待审核 / 1 营业中 / 2 已停业score平均评分、score_count评分数、create_timedish 表id、shop_id、name、category分类如“热菜”“凉菜”、price、imagedescription、status0 下架 / 1 上架、sales累计销量、create_timeorders 表id、order_no唯一订单号、user_id、shop_idtotal_amount、status0 待支付 / 1 已支付 / 2 制作中 / 3 已完成 / 4 已取消 / 5 已退款delivery_type0 到店自取 / 1 送餐到楼、address、remark以及 create_time、pay_time、complete_time 等时间字段order_item 表id、order_id、dish_id、dish_name、price、quantityrating 表id、user_id、shop_id、order_id、score、content、create_time为什么订单明细要单独建表而不是在订单表里存一个 JSON道理很简单订单表和订单明细是一对多关系用户下单多个菜品统计商家销量时直接查order_item表按dish_id分组即可。如果存成 JSON后面统计会非常痛苦。4.2 订单状态机从待支付到已完成的流转规则订单状态是整个系统的业务中枢我在这里单独设计了一个状态转换规则避免任意状态互跳导致数据混乱。0 待支付初始状态。用户提交订单后生成超过 15 分钟未支付由定时任务自动改为 4 已取消。1 已支付模拟支付成功可以立即出餐。此时商家端待接单列表里能看到该订单。2 制作中商家点击接单后进入此状态。3 已完成商家出餐后进入此状态。学生端可进行评价。4 已取消用户主动取消或超时未支付。5 已退款商家未接单时用户申请退款或管理员强制退款。状态转换我只允许以下几种0→1、0→4、0→5、1→2、1→5、2→3。其他跳转全部拦截比如已完成订单不能直接取消。这个逻辑用一个枚举类统一管理避免在 Controller 里散落各种 if 判断。4.3 索引、金额精度和软删除这些容易忽略的细节金额字段必须用decimal(10,2)不要用 float 或 double。数据库浮点数在累加计算时会有精度问题订单金额算错 0.01 元在财务对账时就会非常麻烦。Java 侧对应的类型用BigDecimal。高频查询字段要建索引。我这里重点给orders表的user_id、shop_id、status建了联合索引给order_item表的dish_id建了索引。尤其是商家端查询“某商铺待接单订单”SQL 会带上shop_id ? and status ?这两个条件的联合索引命中后查询速度非常快。软删除也是必选项。商铺和菜品删除时我用的不是物理 DELETE而是加一个deleted字段默认0查询时默认过滤deleted 0。因为历史订单里可能关联了已经删除的菜品或商铺如果物理删掉用户查看历史订单时关联查询就会出错。而订单本身不做软删除订单是交易数据需要永久保留。5. 后端核心实现登录鉴权、订单事务与统计接口5.1 JWT 登录鉴权实现登录接口的流程是接收用户名密码 → 查出用户 → 用 BCrypt 校验密码 → 生成 JWT → 返回 Token。密码加密用的是 Spring Security 自带的BCryptPasswordEncoder绝不允许明文存库。Service public class AuthServiceImpl implements AuthService { Autowired private UserMapper userMapper; Override public String login(LoginRequest request) { User user userMapper.selectByUsername(request.getUsername()); if (user null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } if (user.getStatus() 0) { throw new BusinessException(账号已被禁用); } // 生成 JWT角色信息写入 token String token JwtUtil.createToken(user.getId(), user.getRole()); return token; } }生成 Token 时我会把userId和role放进去同时设置两小时的过期时间。后续接口通过一个JwtAuthenticationFilter拦截请求从 Header 的Authorization字段里解析 Token再取出用户信息放入SecurityContext和ThreadLocal这样 Controller 里就能直接拿到当前用户。Spring Security 的配置核心如下Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/login, /api/auth/register, /api/shop/list, /api/dish/list).permitAll() .antMatchers(/api/merchant/**).hasRole(MERCHANT) .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }注意permitAll的路径要精确否则会出现“未登录也能访问管理员接口”的问题。这个问题我见过不少。5.2 订单提交的事务与库存扣减下单接口是整个系统里事务最复杂的操作涉及以下动作查商铺营业状态停业商铺直接拒绝下单。查询订单中的每个菜品校验是否上架、价格是否有变更。计算订单总金额。生成订单号和订单明细。扣减菜品的sales销量注意这里不是真正的库存扣减校园档口售罄通常靠商家手动下架。插入订单与订单明细。整个过程必须加Transactional任何一个环节失败都要回滚。我踩过的典型坑是往订单明细里插入菜品时用了前端传来的price而不是数据库里的最新价格。如果商家修改了价格前端传的旧价格就会导致订单金额错误。正确做法是后端重新查菜品表用数据库里的价格重新计算前端传的price字段直接忽略。订单号生成也需要注意。我用的是时间戳加随机数的方式但并发高时可能重复。比较稳妥的做法是yyyyMMddHHmmss 用户ID后四位 四位随机数再对order_no字段建唯一索引。万一真的撞号数据库会报唯一约束异常代码里捕获后重新生成即可。5.3 统计接口的 SQL 与定时关单商家端的营业统计需要按日期聚合数据核心 SQL 如下SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS date, COUNT(*) AS order_count, SUM(total_amount) AS total_amount FROM orders WHERE shop_id #{shopId} AND status IN (2, 3) AND create_time #{startDate} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY date这里要注意状态过滤。待支付订单和已取消订单都不应该算入营业额所以我只统计状态为制作中和已完成的订单这样比直接查所有订单更准确。超时未支付订单的关闭我用了 Spring 的Scheduled定时任务每分钟扫描一次Component public class OrderCloseTask { Scheduled(cron 0 */1 * * * ?) public void closeExpiredOrders() { // 查询创建时间早于当前时间15分钟且状态为待支付的订单 // 批量修改状态为已取消 } }定时任务在单实例部署下没问题但如果以后做了多实例部署要加分布式锁避免重复执行。项目初期不需要过度设计但心里要有这个数。5.4 单元测试与接口调试的建议SpringBoot 项目的单元测试我建议至少覆盖三层Service 层的业务规则、Mapper 层的 SQL、Controller 层的鉴权。订单状态机的测试尤其值得写。用SpringBootTest把整个事务跑起来断言从“待支付”到“已支付”再到“制作中”的每一步状态是否按预期流转。这种测试比手动用 Postman 验证要高效得多回归测试时能直接暴露状态跳转 bug。接口调试方面Swagger / Knife4j 强烈建议集成。Knife4j 是 Swagger 的增强版界面更友好。集成后老板或客户可以直接在浏览器里查看和调用所有接口不用你逐个截图。6. 前端关键实现请求封装、购物车响应式与路由守卫6.1 Axios 拦截器与接口统一封装前端所有请求都走统一的 Axios 实例。我在src/utils/request.js里创建了一个配置了baseURL的实例然后给请求和响应分别加了拦截器。// src/utils/request.js import axios from axios import { ElMessage } from element-plus import { useUserStore } from /store/user import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动带 token request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) // 响应拦截器统一处理业务码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response?.status 401) { ElMessage.error(登录已过期请重新登录) router.push(/login) } else { ElMessage.error(error.response?.data?.message || 网络异常) } return Promise.reject(error) } ) export default request统一封装的好处是登录过期、服务端报错、网络异常这些情况都在拦截器里处理了业务代码里只需要写request.get(/order/list)再拿数据不需要每个页面都写一遍错误提示。6.2 购物车状态管理与 computed 计算购物车我放在 Pinia 里管理同时持久化到 localStorage。用户刷新页面后购物车不会丢。// src/store/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: JSON.parse(localStorage.getItem(cart) || []), currentShopId: null }), getters: { totalCount: state state.items.reduce((sum, item) sum item.quantity, 0), totalPrice: state state.items.reduce((sum, item) sum item.price * item.quantity, 0) }, actions: { addItem(dish) { const existing this.items.find(item item.dishId dish.id) if (existing) { existing.quantity } else { this.items.push({ dishId: dish.id, name: dish.name, price: dish.price, quantity: 1, image: dish.image }) } localStorage.setItem(cart, JSON.stringify(this.items)) }, clear() { this.items [] localStorage.removeItem(cart) } } })购物车结算总价用到了getters这在 Vue 里对应的是computed计算属性。它和普通方法的区别在于计算属性会基于响应式依赖自动缓存items没变化时不会重新计算。这在购物车这种频繁操作对象的场景下性能优势非常明显。需要注意一个问题同一个购物车只允许属于一个商铺。如果学生从前一个商铺加购又跑到另一个商铺加购系统需要判断当前购物车是否为空或者属于同一商铺否则提示“需先清空购物车或结算”。这个限制我在前端和后端都做了校验防止用户提交一个包含多个商铺菜品的订单。6.3 路由守卫与动态权限菜单前端路由分了两块静态路由登录、注册、首页和动态路由学生页、商家页、管理页。在全局前置守卫里加判断// src/router/index.js router.beforeEach((to, from, next) { const userStore useUserStore() if (to.path /login) { next() return } if (!userStore.token) { next(/login) return } if (userStore.role ADMIN to.path.startsWith(/merchant)) { next(/403) return } next() })动态菜单的渲染思路是登录成功后从后端/api/user/menus接口拿菜单列表前端根据菜单的path和component字段动态注册路由组件。这里有个比较隐蔽的坑动态路由必须用addRoute注册而且用户退出登录时要removeRoute清理干净否则切换账号时旧权限路由还会存在导致学生账号能看到商家页面。6.4 ECharts 数据看板的对接管理端和商家端的数据看板都用了 ECharts。我的做法是在后端把聚合好的数据返回给前端前端直接注入到图表配置里。// 管理端数据看板 const res await getOverviewData() // res.data: { totalUsers, totalShops, todayOrders, todayAmount, weekOrders: [{date, count, amount}] } const chart echarts.init(document.getElementById(orderChart)) chart.setOption({ xAxis: { type: category, data: res.weekOrders.map(item item.date) }, yAxis: { type: value }, series: [ { name: 订单数, type: bar, data: res.weekOrders.map(item item.count) }, { name: 销售额, type: line, data: res.weekOrders.map(item item.amount) } ] })折线图和柱状图配合展示既能看订单量的波动也能看销售额变化比较直观。ECharts 图表在数据更新后要调用setOption而不是重新init否则会生成多个 canvas 实例导致页面变卡。管理端切换 Tab 时如果图表没有及时dispose也会出现内存占用越来越高的问题。7. 前后端联调与部署上线的完整记录7.1 开发环境的跨域处理SpringBoot 后端默认端口是 8080Vite 开发服务器默认是 5173前后端直接请求会产生跨域。我在前端用了 Vite 的 proxy 代理而不是在后端开启 CORS。// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样配置之后前端请求/api/xxx会由 Vite 代理转发到后端 8080浏览器看到的是同源请求不需要后端设置Access-Control-Allow-Origin。生产环境部署时同样的代理逻辑由 Nginx 实现所以整个项目在后端不需要额外处理跨域。7.2 文件上传的静态资源映射菜品图片上传到本地磁盘后前端访问图片会 404因为默认情况下 SpringBoot 不会把磁盘上的任意目录映射为静态资源。需要在配置类里添加资源处理器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceHandler(file: uploadPath /); } }同时要在application.yml里配置上传文件大小限制spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB如果不限制恶意用户上传超大文件会拖垮服务器。图片上传接口建议加一个文件类型校验只允许 jpg、png、webp 等常见图片格式而且不要信任前端传的Content-Type要读取文件头做二次校验。这里踩坑的教训是有人直接上传了一个伪装成图片的 JSP 文件如果服务器没有拦截 JSP 执行就会留下安全隐患。所以上传目录一定要放在 Web 应用的可执行代码目录之外。7.3 SpringBoot jar Nginx 部署部署方案是后端打 jar 包用java -jar启动前端构建后的 dist 目录交给 Nginx 托管Nginx 把/api开头的请求反向代理到 8080 端口。后端打包mvn clean package -DskipTests scp target/restaurant-system-1.0.0.jar root服务器:/opt/app/ java -jar restaurant-system-1.0.0.jar --spring.profiles.activeprod前端构建npm run build # dist 目录拷贝到服务器 /usr/share/nginx/restaurant-web/Nginx 配置如下server { listen 80; server_name your-domain.com; root /usr/share/nginx/restaurant-web; index index.html; location / { 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里最关键的配置是try_files $uri $uri/ /index.html;。前端使用 Vue Router 的 history 模式时刷新某个子页面路径比如/merchant/orderNginx 会先尝试找该路径对应的文件找不到就回退到index.html由前端路由接管。如果忘记这行配置刷新页面就会得到 404。生产环境建议用systemd托管后端进程这样程序崩溃后能自动重启。大致思路是写一个.service文件ExecStart指向java -jar命令Restartalways。7.4 各种环境坑的汇总最后把这些年做这类项目踩过的坑集中列一下都是实打实的问题。坑一Node 版本和 Vite 版本不匹配。太低版本的 Node 跑不起新版 Vite而学习用的机器上往往装的是旧版 Node。建议先执行node -v看版本小于 16 的先升级。Vite 5 以上要求的 Node 版本更高如果不能升级 Node就固定使用 Vite 4 版本。坑二SpringBoot 版本太高导致依赖冲突。有些教程直接用 SpringBoot 3.x你在网上搜到的很多老教程基于 2.x依赖坐标和配置方式有差异。我建议按本项目的版本定SpringBoot 2.7.18既能正常跑也方便找资料。坑三MySQL 时区问题。连接 MySQL 8.0 时URL 里一定要加serverTimezoneAsia/Shanghai否则数据库时间比本地时间早 8 小时订单的create_time对不上。坑四前后端联调时看接口返回不报错但数据是 null。大多是 JSON 序列化循环引用的问题或者实体类里多表关联的字段没有加JsonIgnore。尤其是订单和用户、订单和商铺之间的互相引用容易造成无限递归。坑五上传的图片刷新后不显示。检查前端访问图片的地址和后端静态资源映射的路径是否对得上注意路径前缀有没有经过 Nginx 转发时被吃掉。坑六管理端页面加载慢。我在项目时发现首页商铺列表每次刷新都全量查询数据库而且商铺图片用原图传输页面体积很大。后来加了首页缓存餐品图片做了压缩前端用懒加载体感好了很多。对这类中小型系统前端懒加载和数据缓存带来的性能提升非常明显。我个人在实际项目里最深的体会是这类管理系统功能表看起来很简单但真正的功夫都花在“边界条件”上。角色边界、状态边界、数据边界每一个边界都是一道隐藏的需求题。你把订单状态、店铺状态、用户状态这些临界情况都过了整个系统才能称得上完整。希望这篇拆解能让你少走一些弯路不管是自己开发还是带着团队做类似的 SpringBoot Vue 前后端分离项目都能有一个更清晰的全局视野。