1. 这不是又一个“学生毕设系统”而是一套能扛住真实订单压力的蛋糕电商骨架我做Java后端开发十年带过二十多个电商类项目从社区团购到区域烘焙连锁最常被低估的就是“小而美”的垂直品类系统——比如蛋糕。很多人看到“网上蛋糕销售系统”第一反应是哦Spring Boot练手项目。但真正在一线跑过的人都知道蛋糕这行当表面看是甜品背后全是时间敏感型供应链逻辑订单必须30分钟内锁定库存配送时效误差不能超15分钟生日蛋糕得提前72小时预约但允许48小时内改期奶油类商品库存要按“小时”刷新……这些业务规则根本不是CRUD堆出来的。这个系统用Spring Boot打底不是因为它“简单好上手”而是它能把复杂业务逻辑拆解得足够干净用Spring Security管会员等级和优惠券权限用Spring Validation做蛋糕尺寸、口味、配送时间的强校验用Spring Cache缓存热门款式和门店库存用Spring Task调度凌晨自动归档过期订单。MySQL不是随便建几张表就完事——主库分库策略按城市切分订单表加了复合索引status, create_time, delivery_time库存表用了乐观锁版本号防超卖连蛋糕图片的URL都做了CDN前缀预置。Tomcat没用默认配置线程池调到了200禁用了HTTP/1.0启用了gzip压缩Vue.js前端也不是套个Element UI就交差购物车加了本地持久化服务端双写支付回调做了幂等性校验连“立即购买”按钮点击后都加了300ms防抖——因为测试时发现用户手滑连点三次真会生成三笔重复订单。关键词里反复出现的“spring boot”“mysql”“tomcat”“vue.js”不是技术栈罗列而是四个咬合齿轮Spring Boot是传动轴MySQL是底盘悬架Tomcat是引擎舱Vue.js是驾驶舱界面。缺一不可错配一个整辆车就跑偏。所以这篇内容不讲“怎么装Tomcat”而是告诉你为什么在蛋糕系统里Tomcat的maxThreads必须设为200而不是默认200为什么MySQL的innodb_buffer_pool_size要设为物理内存的70%而不是50%为什么Vue路由守卫里要拦截“未登录访问订单页”却放行“首页浏览”这些细节才是真实项目和课程Demo的本质区别。适合谁看如果你正用Spring Boot做毕业设计这篇能帮你避开90%的答辩雷区如果你在中小烘焙企业做IT支持这篇能直接抄作业上线如果你准备Java面试里面提到的“库存扣减的三种实现方式对比”“分布式事务在蛋糕改期场景下的取舍”“Vue组件通信在多地址选择器里的实际应用”全是高频考点。别急着敲代码先搞懂为什么这么设计——这才是十年老司机真正想塞给你的东西。2. 系统整体架构与技术选型背后的硬逻辑2.1 为什么选Spring Boot而不是Spring MVC原生框架十年前我用ServletJSP写过第一版蛋糕站那时候光是处理一个“用户选了巧克力蛋糕草莓夹心蜡烛文字”这种组合就要在Controller里手动解析JSON、校验字段长度、拼接SQL、处理事务回滚……现在回头看那不是开发是受刑。Spring Boot的价值从来不是“自动配置”而是把业务复杂度和基础设施复杂度彻底解耦。举个具体例子蛋糕的“定制化属性”字段。用户下单时可能填“生日快乐小熊图案无糖”也可能只选“经典款”。如果用原生Spring MVC你得在每个Controller方法里写if-else判断字段是否存在、是否为空、是否符合正则。而Spring Boot的Validated注解配合自定义ConstraintValidator能直接把校验逻辑抽成独立类。我写的CakeCustomizationValidator里会检查“蜡烛文字”是否含敏感词比如“寿终正寝”这种乌鸦嘴、“图案描述”是否超过20字避免打印模糊、“无糖选项”是否与奶油类型冲突植物奶油本就不能加糖。这些规则一旦写进Validator所有用到CakeDTO的地方自动生效Controller只剩一行service.createOrder()。更关键的是启动效率。Tomcat默认启动要12秒而Spring Boot内嵌TomcatDevTools热部署改完一行代码3秒内生效。这对迭代速度有多重要上周我们帮一家连锁店上线“节日限定款”设计师凌晨发来新图运营下午就要上架开发团队从接到需求到全网发布只用了6小时——靠的就是Spring Boot的快速反馈闭环。这不是炫技是商业节奏倒逼的技术选择。2.2 MySQL为什么必须分库一张订单表撑不住的真实数据量很多人觉得“小系统用单库就行”直到第一次遇到生日季峰值。去年某城市店庆日单日订单破8000单其中62%集中在晚上7-9点。当时没分库所有订单挤在一张order表里MySQL的QPS瞬间飙到1200慢查询日志里全是“SELECT * FROM order WHERE user_id ? AND status paid”——这个查询没走索引因为status字段基数太低90%都是paidMySQL优化器直接放弃索引走全表扫描。解决方案不是加索引而是按业务维度切分数据。我们把订单库按城市拆成shanghai_order、beijing_order、guangzhou_order三个库每个库再按月分表order_202408, order_202409。这样做的好处有三第一写入压力分散。上海用户下单只写shanghai_order库不会和北京的写操作争抢InnoDB的row lock 第二查询天然分区。查上海用户历史订单直接路由到shanghai_order库不用扫全量数据 第三备份恢复可控。单个城市数据出问题只需恢复对应库不影响其他区域。分库不是靠MyCat或ShardingSphere硬上而是用Spring Boot的AbstractRoutingDataSource动态切换数据源。核心代码就三行public class DynamicDataSourceContextHolder { private static final ThreadLocalString contextHolder new ThreadLocal(); public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } public static String getDataSource() { return contextHolder.get(); } }在Service层根据用户归属城市调用DynamicDataSourceContextHolder.setDataSource(shanghai)后续所有DAO操作自动走对应库。比中间件轻量比硬编码灵活这才是中小团队该有的分库姿势。2.3 Tomcat调优不是“改几个参数”而是匹配蛋糕业务的时间特性Tomcat默认配置是为通用Web应用设计的但蛋糕系统有鲜明的时间特征工作日午休11:30-13:30和晚间18:00-21:00是流量高峰凌晨2-5点是库存同步和报表生成时段。如果用默认配置高峰时段线程池耗尽用户点击“提交订单”按钮后页面转圈30秒——这在餐饮行业等于直接流失客户。我们做了三处关键调整第一线程池扩容。server.xml里把maxThreads从200提到300minSpareThreads从10提到50。计算依据很实在按单城市日均5000单平均每单请求链路耗时800ms含库存校验、优惠计算、支付跳转理论并发峰值5000/(24*3600/800)≈46但必须预留200%冗余应对突发流量所以300是安全值。第二连接超时收紧。connectionTimeout从20000ms20秒降到5000ms。理由很简单用户等待5秒还没响应大概率已关闭页面或切换APP继续维持连接只是浪费线程资源。实测下来超时拒绝率从0.3%降到0.02%服务器负载反而更平稳。第三静态资源分离。把蛋糕图片、JS/CSS文件全扔到NginxTomcat只处理动态请求。这里有个坑Vue打包后的index.html里引用的js路径是相对路径/static/js/app.xxx.js如果Nginx没配好root会404。我们的解法是在Nginx配置里加一句location /static { alias /opt/www/static/; }并确保Vue的public目录文件同步到该路径。这些调整不是凭空而来而是基于APM工具我们用SkyWalking连续一周的监控数据线程池使用率峰值、HTTP响应时间P95、数据库连接池等待数。没有监控数据支撑的调优都是玄学。2.4 Vue.js选型为什么不用React或小程序直击烘焙行业的终端现状搜索热词里“vue.js脚本下载”高居前列说明很多开发者还在手动引入CDN版Vue。但真实项目里我们坚持用Vue CLIWebpack构建原因很现实烘焙店主普遍年龄偏大他们用的安卓手机型号老旧华为Mate8、小米Note3这类微信版本停留在6.7.x很多小程序API根本不支持。而Vue SPA只要兼容IE11以上就能覆盖99%的终端。更重要的是运营灵活性。蛋糕店经常要临时加活动“买蛋糕送咖啡券”“满199减50”这些活动页如果用小程序每次都要提审等3天而Vue做的H5页面运营后台上传HTML片段CDN刷新5分钟就全网生效。上周某店搞“教师节特惠”从策划到上线只用了2小时——靠的就是Vue的动态路由异步组件加载。我们没用Vuex做全局状态管理而是用Composition API的provide/inject。比如购物车数据父组件App.vue通过provide注入cartStore所有子组件用inject获取既避免Vuex的样板代码又保证响应式更新。实测下来100个商品加入购物车渲染耗时比Vuex方案快120ms——对移动端来说这120ms就是用户是否愿意等下去的分界线。还有一个隐藏优势Vue的v-model双向绑定让“蛋糕定制表单”开发效率极高。用户选尺寸radio、选口味checkbox、填祝福语textarea所有数据自动同步到data对象提交时直接JSON.stringify发送。如果用React光是写onChange事件处理器就得写半屏代码。对快速迭代的垂直电商开发效率就是生命线。3. 核心模块实现与关键细节拆解3.1 库存管理如何防止“最后一块蛋糕被抢光”引发的客诉蛋糕库存不是简单的数字减1而是涉及多维度锁定时效释放跨渠道同步。我们曾因库存逻辑缺陷导致同一块“黑森林蛋糕”被美团、自有APP、电话订单同时卖出最后不得不赔顾客三份蛋糕。痛定思痛重构了整套库存体系。核心设计是“三级库存模型”总库存total_stock仓库实际物理数量只由采购系统修改可用库存available_stock扣除已售未发货、冻结中订单后的可售数量预占库存reserved_stock用户加入购物车但未支付时锁定的数量有效期30分钟。关键难点在于“预占库存”的原子性操作。最初用MySQL UPDATE语句UPDATE cake_stock SET reserved_stock reserved_stock 1 WHERE cake_id ? AND available_stock 1;但高并发下仍会出现超卖——因为SELECT available_stock和UPDATE是两个操作中间有时间差。最终采用RedisLua脚本方案-- stock_lock.lua local available redis.call(HGET, KEYS[1], available_stock) local reserved redis.call(HGET, KEYS[1], reserved_stock) if tonumber(available) - tonumber(reserved) tonumber(ARGV[1]) then redis.call(HINCRBY, KEYS[1], reserved_stock, ARGV[1]) return 1 else return 0 endJava端调用Long result redisTemplate.execute(redisScript, Collections.singletonList(cake:1001), 1); if (result 0) { throw new BusinessException(库存不足); }这个脚本在Redis服务端执行全程原子性。实测QPS从800提升到3500超卖率为0。更绝的是“库存释放机制”。用户加购后30分钟未支付Redis里对应的key自动过期用EXPIRE命令但MySQL的reserved_stock不会自动回滚。所以我们加了个定时任务每5分钟扫描Redis里已过期但MySQL未释放的记录执行补偿操作。这个任务用Spring Scheduled实现但加了分布式锁防止集群重复执行——用Redis的SETNX命令key为stock_release_lockvalue为当前时间戳过期时间设为30秒。3.2 订单创建从点击到支付成功的七步原子化流程用户点“立即购买”不是简单插入一条order记录而是跨越支付、库存、通知的七步事务链。任何一步失败都要精准回滚且不能影响其他订单。我们没用Seata这类重型分布式事务框架而是用“本地消息表定时补偿”轻量方案。流程如下预占库存调用前述Redis Lua脚本成功则进入下一步生成订单号用雪花算法Snowflake生成唯一order_no格式为202408152210000001年月日时分秒序列号插入订单主表status初始为createdpayment_status为unpaid插入订单明细表关联蛋糕ID、数量、单价写入本地消息表在同一个MySQL事务里插入一条message_recordcontent为{orderNo:202408152210000001,type:pay_notify}status为pending调用支付接口支付宝/微信SDK发起预支付获取pay_url返回前端跳转把pay_url传给Vue页面重定向到支付页。重点在第5步的本地消息表。它和订单表在同一个库、同一个事务里保证“订单创建成功消息必然写入”。后续由独立的消息消费服务用Spring Boot Actuator健康检查保障服务存活定时扫描message_record表statuspending的记录调用支付平台查询支付结果成功则更新订单payment_statuspaid失败则触发库存回滚。这个设计的好处是支付回调丢失也不怕消息表会兜底支付平台超时也不影响主流程用户看到“请稍后查看订单状态”即可。我们把消息消费频率设为10秒一次实测从支付成功到订单状态更新平均延迟2.3秒完全满足业务要求。3.3 优惠券系统为什么“满199减50”不能简单用if-else实现蛋糕优惠券看似简单实则暗藏陷阱。比如“满199减50”和“第二件半价”叠加时系统要自动计算最优组合而不是简单叠加。更麻烦的是“限指定蛋糕使用”比如“仅限生日蛋糕可用”但用户购物车里既有生日蛋糕又有常温蛋糕优惠券只能抵扣生日蛋糕部分。我们用责任链模式Chain of Responsibility解耦计算逻辑public interface DiscountCalculator { BigDecimal calculate(Order order, Coupon coupon); boolean supports(CouponType type); } Component public class FullReductionCalculator implements DiscountCalculator { Override public BigDecimal calculate(Order order, Coupon coupon) { BigDecimal total order.getItems().stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); if (total.compareTo(coupon.getThreshold()) 0) { return coupon.getDiscount(); } return BigDecimal.ZERO; } Override public boolean supports(CouponType type) { return type CouponType.FULL_REDUCTION; } }所有计算器实现类自动注册到Spring容器计算时遍历调用public BigDecimal calculateTotalDiscount(Order order, ListCoupon coupons) { return coupons.stream() .map(coupon - discountCalculators.stream() .filter(calculator - calculator.supports(coupon.getType())) .findFirst() .map(calculator - calculator.calculate(order, coupon)) .orElse(BigDecimal.ZERO)) .reduce(BigDecimal.ZERO, BigDecimal::add); }这样新增“积分抵扣”“会员等级折扣”时只需写新Calculator实现类无需改动原有代码。上周加“企业团购折上折”两天就上线没动一行旧逻辑。3.4 Vue前端关键交互购物车本地持久化与服务端双写Vue购物车如果只存在内存里用户刷新页面就清空体验极差。但我们没用localStorage因为localStorage容量小5MB、无过期机制、跨域不共享。最终方案是“内存localStorage服务端三副本”。初始化时// store/modules/cart.js const state { items: JSON.parse(localStorage.getItem(cart_items) || []), syncTime: localStorage.getItem(cart_sync_time) || 0 } // 组件mounted时拉取服务端最新购物车 dispatch(cart/fetchFromServer) // 每次添加/删除先更新内存再写localStorage再发请求同步服务端 actions: { addItem({ commit, state }, item) { commit(ADD_ITEM, item) localStorage.setItem(cart_items, JSON.stringify(state.items)) // 防抖1秒内多次操作只发一次同步请求 clearTimeout(this.syncTimer) this.syncTimer setTimeout(() { api.cartSync(state.items) }, 1000) } }服务端同步接口做了幂等设计接收cartItems数组用MD5(item.id item.quantity)作为唯一键只更新变化项。这样即使网络波动导致重复请求数据也不会错乱。最妙的是“离线优先”策略。当网络断开时add/remove操作仍能执行内存localStorage并标记syncStatuspending。网络恢复后自动批量提交pending操作。实测地铁隧道里加购5个蛋糕出站后2秒内全部同步成功——这才是真实用户需要的体验。4. 实操过程与避坑指南从零搭建的完整步骤4.1 开发环境准备IDEA配置Tomcat的六个致命细节网上教程教“File→Settings→Build→Deployment→Application Servers”但漏掉了六个关键点导致90%的新手卡在这里第一JDK版本必须匹配。Spring Boot 3.x要求JDK 17但Tomcat 9.x只支持JDK 8-16。我们用Tomcat 10.1.26支持JDK 17下载地址是https://tomcat.apache.org/download.cgi千万别下错版本。验证方法解压后bin/catalina.sh里grep JAVA_HOME确认支持JDK 17。第二IDEA的Artifact必须选对。新建项目时右键项目→Open Module Settings→ArtifactsType选“Web application: Archive”Output directory指向target/xxx.war而不是默认的out/artifacts。否则打包后WAR包里缺classes目录。第三Tomcat配置里的Deployment路径。在IDEA的Tomcat配置页Deployment标签下点击“”添加ArtifactName填“ROOT”Application context留空不是/这样才能访问http://localhost:8080直接进入首页。如果填了/shop就必须访问http://localhost:8080/shop。第四VM options必须加-Dfile.encodingUTF-8。否则中文日志乱码控制台输出“??????”。这个参数在Tomcat配置页的“Configuration”→“VM options”里填写。第五热部署要关掉“On Update action”里的“Update classes and resources”。Spring Boot DevTools自己管热部署IDEA再插一脚会导致类加载冲突报错“ClassCastException: X cannot be cast to X”。第六启动前务必勾选“After launch”里的“Open browser”URL填http://localhost:8080。很多新手启动成功却没打开页面以为失败其实服务已在运行。做完这六步启动日志里看到“Tomcat started on port(s): 8080 (http)”才算真正成功。我见过太多人卡在第五步反复重装IDEA其实就差一个勾选。4.2 MySQL安装与建库绕过官网下载的三个替代方案MySQL官网下载慢、注册烦、版本杂我们用三种更高效的方式方案一Docker一键部署推荐给Mac/Linuxdocker run -d \ --name mysql-cake \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEcake_shop \ -v /opt/mysql/data:/var/lib/mysql \ -v /opt/mysql/conf:/etc/mysql/conf.d \ mysql:8.0.33注意-v挂载的宿主机路径必须存在且权限为755。conf目录下放my.cnf[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci innodb_buffer_pool_size2G # 物理内存的70%方案二HomebrewMac专属brew install mysql8.0 brew services start mysql8.0 mysql -uroot -p123456 -e CREATE DATABASE cake_shop CHARACTER SET utf8mb4;方案三Windows免安装版绿色解压即用去https://dev.mysql.com/downloads/mysql/ 下载“Windows (x86, 64-bit), ZIP Archive”解压到C:\mysql然后cd C:\mysql\bin mysqld --initialize --console # 记住生成的临时密码 mysqld --install net start mysql mysql -uroot -p # 输入临时密码 ALTER USER rootlocalhost IDENTIFIED BY 123456; CREATE DATABASE cake_shop CHARACTER SET utf8mb4;无论哪种方案建库后必须执行-- 关键设置时区否则Java读取的时间戳会差8小时 SET GLOBAL time_zone 8:00; -- 创建用户并授权 CREATE USER cake_user% IDENTIFIED BY Cake2024; GRANT ALL PRIVILEGES ON cake_shop.* TO cake_user%; FLUSH PRIVILEGES;4.3 Vue项目初始化跳过vue-cli的三个更快方式Vue CLI已成历史我们用ViteVue3组合初始化只要10秒npm create vitelatest cake-frontend -- --template vue cd cake-frontend npm install npm install axios pinia element-plus unplugin-auto-import unplugin-vue-components -D关键配置在vite.config.tsimport { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite export default defineConfig({ plugins: [ vue(), AutoImport({ imports: [vue, vue-router, pinia], dts: true, }), Components({ dirs: [src/components], dts: true, }), ], server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } } })这个配置实现了自动导入ref、computed等API不用写import自动注册components目录下的组件不用Vue.component代理/api请求到后端。比vue-cli少写80%的样板代码。4.4 数据库表结构设计被忽略的三个业务字段网上教程的订单表只有order_id、user_id、total_price但在蛋糕系统里这三个字段救命delivery_time预计送达时间类型DATETIMENOT NULL。不是“下单时间”而是用户选择的“今晚7点送到”。这个字段要参与库存锁定比如7点前的蛋糕必须提前备货也是配送调度的核心依据。cake_customization定制化描述类型JSON允许NULL。存用户填写的“生日快乐小熊图案无糖”而不是拆成多个VARCHAR字段。MySQL 5.7原生支持JSON查询用JSON_CONTAINS函数比LIKE模糊查询快10倍。source_channel来源渠道类型VARCHAR(20)NOT NULL默认web。值为web、wechat、phone。为什么重要因为不同渠道的佣金比例不同微信支付扣0.6%电话订单扣1.2%财务对账时必须区分。建表SQL示例CREATE TABLE order_master ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint NOT NULL, delivery_time datetime NOT NULL COMMENT 预计送达时间, cake_customization json DEFAULT NULL COMMENT 定制化描述, source_channel varchar(20) NOT NULL DEFAULT web, total_amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付,1已支付,2已发货..., PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_delivery_time (delivery_time), KEY idx_user_status (user_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引设计有讲究idx_delivery_time用于查询“今日待配送订单”idx_user_status用于查询“某用户的历史订单”都是高频查询路径。5. 常见问题与排查技巧实录5.1 “Tomcat启动后访问404”的九种可能及速查表现象可能原因排查命令解决方案http://localhost:8080/ 返回404WAR包没部署成功ls $TOMCAT_HOME/webapps/确认ROOT.war存在且解压出ROOT目录http://localhost:8080/api/login 返回404Spring Boot没扫描到Controllergrep Mapped catalina.out检查Controller类是否在SpringBootApplication同包或子包http://localhost:8080/static/js/app.js 返回404Vue静态资源路径错误curl -I http://localhost:8080/static/js/确认nginx配置location /static指向正确路径http://localhost:8080/ 返回空白页Vue路由mode: history导致404浏览器F12看Network是否加载了index.html但js 404在nginx加try_files $uri $uri/ /index.html;http://localhost:8080/api/login 返回500MySQL连接失败tail -f $TOMCAT_HOME/logs/catalina.out | grep SQLException检查application.yml里spring.datasource.url是否正确http://localhost:8080/ 加载极慢Gzip未启用curl -I http://localhost:8080/ | grep Content-Encoding在application.yml加server.compression.enabledtruehttp://localhost:8080/ 中文乱码字符集未设置curl -I http://localhost:8080/ | grep charset在application.yml加server.servlet.context-parameters.encodingUTF-8http://localhost:8080/ 登录后跳转首页失败Vue路由守卫拦截浏览器Console看是否有router.push错误检查router.beforeEach里next()是否被遗漏http://localhost:8080/ 支付回调收不到外网无法访问内网IPcurl http://your-domain.com/api/pay/callback用ngrok暴露本地端口或部署到云服务器我踩过的最深的坑是第九条本地调试用微信支付回调地址填localhost:8080结果微信服务器根本访问不到。解决方案是用natapp国内版ngrok免费隧道能用配置一行命令natapp -authtokenxxxxxx -subdomaincake-dev然后回调地址填https://cake-dev.natapp1.cc/api/pay/callback。这个坑让团队折腾了两天记住所有涉及第三方回调的接口必须外网可达。5.2 MySQL连接池“等待数飙升”的实战诊断法现象系统运行几小时后Tomcat线程池满MySQL连接数卡在100maxActive但show processlist看到只有20个活跃连接。这是典型的连接泄漏。诊断三步法看Druid监控页访问http://localhost:8080/druid看“ActiveCount”和“PoolingCount”是否长期不降查慢查询日志show variables like slow_query_log;开启后tail -f /var/log/mysql/slow.log找执行时间1s的SQL代码审计重点检查有没有手动new Connection()没close或者try-with-resources没写对。我们发现的问题是某个报表导出接口用JDBC原生写法Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); // 忘了ps.close()和conn.close()改成try-with-resourcestry (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // ... } // 自动close连接等待数从50降到0。记住Spring Boot的JdbcTemplate、MyBatis都自动管理连接但凡自己写JDBC必须用try-with-resources。5.3 Vue打包后静态资源404Nginx配置的五个必填项Vue Router用history模式打包后index.html里引用的js路径是/static/js/app.xxx.js但Nginx默认不识别/static路径。正确配置如下server { listen 80; server_name cake-shop.com; location / { root /opt/www/cake-frontend/dist; try_files $uri $uri/ /index.html; # 关键解决history路由404 } location /static { alias /opt/www/cake-frontend/dist/static; # 关键映射static目录 expires 1y; # 缓存一年 add_header Cache-Control public, immutable; } location /api { proxy_pass http://localhost:8080; # 代理到后端 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control public, immutable; } # 防止直接访问敏感文件 location ~* \.(txt|log)$ { deny all; } }漏掉任意一项都会导致404。特别是try_files $uri $uri/ /index.html;没有这行用户分享链接如https://cake-shop.com/order/123刷新页面就会404。5.4 Spring Boot启动慢的四大元凶及提速方案启动时间从32秒优化到8秒的过程元凶一Actuator健康检查。默认检查Redis、DB、Mail等但蛋糕系统不用Mail。解决方案management.endpoint.health.show-detailsnever或management.health.mail.enabledfalse元凶二MyBatis Mapper扫描。MapperScan(com.cake.mapper)扫描整个包包含测试类。解决方案精确到MapperScan(com.cake.mapper.order)元凶三Lombok注解处理器。IDEA里Lombok插件和编译器冲突。解决方案File→Settings→Build→Compiler→Annotation Processors关掉“Enable annotation processing”元凶四DevTools热部署。生产环境必须排除。解决方案scopeprovided/scope在pom.xml里声明DevTools依赖。最终启动日志里看到“Started Application in 8.234 seconds (JVM running for 8.789)”才算达标。记住启动慢不是性能问题是开发体验毒药——每次改代码等10秒一天就浪费2小时。6. 系统上线后的运维要点与扩展建议系统上线不是终点而是运维的起点。我们给客户交付时附赠了一份《蛋糕系统运维手册》核心就三点第一每日必检三件事查Tomcat日志grep ERROR $TOMCAT_HOME/logs/catalina.out | tail -20看是否有未捕获异常查MySQL慢查询mysqldumpslow -s t -t 10 /var/log/mysql/slow.logtop10慢SQL必须优化查Redis内存redis-cli info memory \| grep used_memory_human超过80%触发告警。第二每周必做两件事清理过期订单DELETE FROM order_master WHERE status created AND create_time DATE_SUB(NOW(), INTERVAL 2 HOUR);未支付订单2小时后自动取消备份数据库用mysqldump导出gzip压缩保留最近7天mysqldump -u cake_user -pCake2024 cake_shop | gzip /backup/cake_shop_$(date %Y%m%d).sql.gz find /backup -name cake_shop_*.sql.gz -mtime 7 -delete第三每月必审一次配置检查Tomcat线程池jstat -gc $(pgrep -f tomcat)看YGC次数是否异常高100次/分钟说明内存不足检查MySQL连接池show status like Threads_connected;超过max_connections的80%需扩容检查Vue静态资源