☰
蛋糕电商系统实战:Spring Boot高并发库存与订单设计
2026/10/2 6:50:31 网站建设 项目流程

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原生框架?

十年前我用Servlet+JSP写过第一版蛋糕站,那时候光是处理一个“用户选了巧克力蛋糕+草莓夹心+蜡烛文字”这种组合,就要在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内嵌Tomcat+DevTools热部署,改完一行代码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%都是paid),MySQL优化器直接放弃索引走全表扫描。

解决方案不是加索引,而是按业务维度切分数据。我们把订单库按城市拆成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 ThreadLocal<String> 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提到300,minSpareThreads从10提到50。计算依据很实在:按单城市日均5000单,平均每单请求链路耗时800ms(含库存校验、优惠计算、支付跳转),理论并发峰值=5000/(24*3600/800)≈46,但必须预留200%冗余应对突发流量,所以300是安全值。

第二,连接超时收紧。connectionTimeout从20000ms(20秒)降到5000ms。理由很简单:用户等待5秒还没响应,大概率已关闭页面或切换APP,继续维持连接只是浪费线程资源。实测下来,超时拒绝率从0.3%降到0.02%,服务器负载反而更平稳。

第三,静态资源分离。把蛋糕图片、JS/CSS文件全扔到Nginx,Tomcat只处理动态请求。这里有个坑: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 CLI+Webpack构建,原因很现实:烘焙店主普遍年龄偏大,他们用的安卓手机型号老旧(华为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是两个操作,中间有时间差。最终采用Redis+Lua脚本方案:

-- 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 end

Java端调用:

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_lock",value为当前时间戳,过期时间设为30秒。

3.2 订单创建:从点击到支付成功的七步原子化流程

用户点“立即购买”不是简单插入一条order记录,而是跨越支付、库存、通知的七步事务链。任何一步失败,都要精准回滚,且不能影响其他订单。我们没用Seata这类重型分布式事务框架,而是用“本地消息表+定时补偿”轻量方案。

流程如下:

  1. 预占库存:调用前述Redis Lua脚本,成功则进入下一步;
  2. 生成订单号:用雪花算法(Snowflake)生成唯一order_no,格式为202408152210000001(年月日时分秒+序列号);
  3. 插入订单主表:status初始为"created",payment_status为"unpaid";
  4. 插入订单明细表:关联蛋糕ID、数量、单价;
  5. 写入本地消息表:在同一个MySQL事务里,插入一条message_record,content为{"orderNo":"202408152210000001","type":"pay_notify"},status为"pending";
  6. 调用支付接口:支付宝/微信SDK发起预支付,获取pay_url;
  7. 返回前端跳转:把pay_url传给Vue,页面重定向到支付页。

重点在第5步的本地消息表。它和订单表在同一个库、同一个事务里,保证“订单创建成功,消息必然写入”。后续由独立的消息消费服务(用Spring Boot Actuator健康检查保障服务存活)定时扫描message_record表,status="pending"的记录,调用支付平台查询支付结果,成功则更新订单payment_status="paid",失败则触发库存回滚。

这个设计的好处是:支付回调丢失也不怕,消息表会兜底;支付平台超时也不影响主流程,用户看到“请稍后查看订单状态”即可。我们把消息消费频率设为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, List<Coupon> 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),并标记syncStatus="pending"。网络恢复后,自动批量提交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→Artifacts,Type选“Web application: Archive”,Output directory指向target/xxx.war,而不是默认的out/artifacts。否则打包后WAR包里缺classes目录。

第三,Tomcat配置里的Deployment路径。在IDEA的Tomcat配置页,Deployment标签下,点击“+”添加Artifact,Name填“ROOT”,Application context留空(不是"/"),这样才能访问http://localhost:8080直接进入首页。如果填了"/shop",就必须访问http://localhost:8080/shop。

第四,VM options必须加-Dfile.encoding=UTF-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/Linux)

docker run -d \ --name mysql-cake \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=cake_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-server=utf8mb4 collation-server=utf8mb4_unicode_ci innodb_buffer_pool_size=2G # 物理内存的70%

方案二:Homebrew(Mac专属)

brew install mysql@8.0 brew services start mysql@8.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 'root'@'localhost' IDENTIFIED BY '123456'; CREATE DATABASE cake_shop CHARACTER SET utf8mb4;

无论哪种方案,建库后必须执行:

-- 关键!设置时区,否则Java读取的时间戳会差8小时 SET GLOBAL time_zone = '+8:00'; -- 创建用户并授权 CREATE USER 'cake_user'@'%' IDENTIFIED BY 'Cake@2024'; GRANT ALL PRIVILEGES ON cake_shop.* TO 'cake_user'@'%'; FLUSH PRIVILEGES;

4.3 Vue项目初始化:跳过vue-cli的三个更快方式

Vue CLI已成历史,我们用Vite+Vue3组合,初始化只要10秒:

npm create vite@latest cake-frontend -- --template vue cd cake-frontend npm install npm install axios pinia element-plus unplugin-auto-import unplugin-vue-components -D

关键配置在vite.config.ts:

import { 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(预计送达时间):类型DATETIME,NOT 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

索引设计有讲究: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.enabled=true
http://localhost:8080/ 中文乱码字符集未设置curl -I http://localhost:8080/ | grep "charset"在application.yml加server.servlet.context-parameters.encoding=UTF-8
http://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 -authtoken=xxxxxx -subdomain=cake-dev

然后回调地址填https://cake-dev.natapp1.cc/api/pay/callback。这个坑让团队折腾了两天,记住:所有涉及第三方回调的接口,必须外网可达。

5.2 MySQL连接池“等待数飙升”的实战诊断法

现象:系统运行几小时后,Tomcat线程池满,MySQL连接数卡在100(maxActive),但show processlist看到只有20个活跃连接。这是典型的连接泄漏。

诊断三步法:

  1. 看Druid监控页:访问http://localhost:8080/druid,看“ActiveCount”和“PoolingCount”是否长期不降;
  2. 查慢查询日志:show variables like 'slow_query_log';,开启后tail -f /var/log/mysql/slow.log,找执行时间>1s的SQL;
  3. 代码审计:重点检查有没有手动new Connection()没close,或者try-with-resources没写对。

我们发现的问题是:某个报表导出接口,用JDBC原生写法:

Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); // 忘了ps.close()和conn.close()

改成try-with-resources:

try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // ... } // 自动close

连接等待数从50+降到0。记住:Spring Boot的JdbcTemplate、MyBatis都自动管理连接,但凡自己写JDBC,必须用try-with-resources。

5.3 Vue打包后静态资源404:Nginx配置的五个必填项

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-details=never,或management.health.mail.enabled=false;
  • 元凶二:MyBatis Mapper扫描。@MapperScan("com.cake.mapper")扫描整个包,包含测试类。解决方案:精确到@MapperScan("com.cake.mapper.order");
  • 元凶三:Lombok注解处理器。IDEA里Lombok插件和编译器冲突。解决方案:File→Settings→Build→Compiler→Annotation Processors,关掉“Enable annotation processing”;
  • 元凶四:DevTools热部署。生产环境必须排除。解决方案:<scope>provided</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.log,top10慢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 -p'Cake@2024' 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静态资源

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

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

立即咨询