news 2026/10/20 18:38:56

基于Spring Boot的网上蛋糕销售系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot的网上蛋糕销售系统设计与实现

1. 从需求到系统:网上蛋糕销售系统的定位与整体拆解

先把这个项目的本质说清楚。网上蛋糕销售系统,表面看是一个电商系统,但它的业务特性决定了它不能照搬通用商城的那套设计。蛋糕不是标品,它有尺寸、口味、奶油类型、配送时间这些维度,而且订单往往是"提前预订"而不是"即时发货"。这意味着从表结构设计到订单状态机,都要围绕"预订+配送"这两个关键词来展开。

我实际做这个系统时,首先做的是把标题里的需求翻译成功能清单。用户端需要:注册登录、浏览商品、按分类筛选、加入购物车、提交订单、在线支付或货到付款、查看订单状态。管理员端需要:商品管理、分类管理、订单管理、会员管理、轮播图管理、基础参数配置。这些功能看起来就是标准的两个端,但落地时真正的复杂度集中在几个点:商品规格如何处理、库存什么时候扣、订单状态如何流转、支付回调怎么兼容。

Spring Boot在这个项目里的定位,不是"框架选型"那么简单。它决定了整个项目的分层方式、依赖管理方式、以及后续扩展的边界。我见过太多项目把Controller写成"上帝类",一个类里放了几十个方法,业务代码和SQL混在一起,最后连加一个字段都要全局搜索。所以这个系统从一开始就坚持了清晰的分层:Controller只做参数接收和响应封装,Service层写业务规则,Mapper层只管数据访问。这样做的代价是前期多写一些接口和转换代码,但到了后期加功能、改bug的时候,你会庆幸当时没有图省事。

1.1 为什么选Spring Boot做蛋糕电商后端

选Spring Boot,不是因为它是"默认选项",而是因为它在这个场景下的几个优势恰好对得上。

第一,内置Tomcat和自动化配置,开发体验好。蛋糕销售系统这种项目,业务逻辑不复杂,核心价值在业务流程的完整性上。Spring Boot允许你用一个spring-boot-starter-web就把Web环境拉起来,不需要像传统SSM那样去做一堆XML配置。我实际开发时,从零搭建到能跑通用户注册接口,大概只花了一个多小时,其中大部分时间还是花在想接口的参数上,而不是在配置上。

第二,生态成熟,社区方案丰富。做这个系统要用的东西——MyBatis或MyBatis-Plus、Redis、JWT、Swagger、支付宝沙箱支付——全部有Spring Boot的Starter或者成熟的整合方案。这意味着遇到问题时,搜索到的解决方案几乎都是针对Spring Boot的,排查成本低很多。

第三,和前端分离开发的配合好。现在做这种项目,前端基本是Vue或React的SPA应用,后端只需要提供JSON接口。Spring Boot天然的RESTful支持,配合注解式参数校验,让前后端联调变得很顺畅。我在开发时是前后端并行推进的,后端只要把接口文档定义清楚(我用的是Swagger注释),前端就可以模拟数据先开发,等后端接口就绪后再替换成真实请求。

有一个容易忽视的点:版本选择。开发时我用的Spring Boot版本是2.7.x,而不是3.x。原因是3.x基于Jakarta命名空间,很多老版本的依赖(尤其是MyBatis相关)需要额外适配。如果是做课设或者快速上线,2.7.x是目前最稳的选择,资料多、坑少。当然如果是从头学,直接上3.x也没问题,但要做好踩新坑的心理准备。

1.2 核心功能模块与数据流梳理

在动手写代码之前,我画了一遍业务数据流,这比写代码本身重要得多。整个系统的核心数据流是这样的:

用户在小程序或Web端浏览蛋糕商品,商品信息从数据库读取并经过Redis缓存加速;用户将商品加入购物车,购物车数据可以存Redis也可以存数据库,我最后选择了数据库存储,理由后面会讲;用户确认订单时,系统要校验用户登录态、校验商品库存、计算订单金额(叠加优惠券和会员折扣)、生成订单记录和订单明细;用户付款后,系统收到支付回调,更新订单状态,同时扣减库存;管理员在后台看到订单,执行制作、配送、完成等操作,整个状态流转被严格约束。

这个流程里最需要想清楚的是"库存扣减的时机"。蛋糕不同于普通商品,它往往是按订单制作的,所以有两条路线:一种是在下单时就扣库存,适合现货型蛋糕(比如小蛋糕、杯子蛋糕);另一种是接单后再准备原料,适合定制蛋糕。我的做法是折中:普通蛋糕用"下单锁库存、支付确认扣库存"的策略,定制蛋糕走单独的表单和审核流程,不占用常规库存。这样既避免了超卖,又不会因为用户下单不付款而长期占用库存。

另外,订单状态机也是需要提前定义清楚的。我设计了这样一组状态:待支付、已支付待制作、制作中、配送中、已完成、已取消。这里的关键是,哪些状态允许用户操作,哪些状态只有管理员能操作,必须画成一张状态流转表,避免代码里出现"从已完成跳到待支付"这种荒谬情况。

2. 数据库设计与核心模型:把"蛋糕生意"翻译成表结构

数据库设计决定这个系统的天花板。我见过很多项目,代码写得挺热闹,一到数据库就露怯:商品表和订单表没有关联索引、金额字段用浮点型、状态字段用中文存、没有逻辑删除字段……这些坑一个接一个,到后期全变成性能问题和数据质量问题。

我的核心表设计思路是"按业务域划分,主表精简,明细独立"。整个系统核心表有:用户表、商品表、商品分类表、购物车表、订单表、订单明细表、收货地址表、优惠券表、会员等级表、轮播图表。加上一些辅助表如管理员表、操作日志表。

2.1 订单、商品、用户三张核心表的设计思路

用户表:我保留了最基础的字段,id,username,password,nickname,avatar,phone,email,status,create_time,update_time。密码字段我用BCrypt加密存储,不用MD5。对于会员等级,我单独建了会员等级表,通过level_id关联,而不是在用户表里塞一堆level_name,discount_rate这样冗余字段。

商品表:这里要仔细讲,因为蛋糕商品的属性比较特殊。我在商品表里存了通用信息:id,category_id,name,subtitle,main_image,detail,price,stock,status,sales,create_time。但蛋糕的"口味" "尺寸" "甜度"这些规格属性,不适合直接堆在主表里。

我的方案是建立商品规格表,用spec_name和spec_value两个字段区分,例如:

CREATE TABLE `cake_spec` ( `id` int(11) NOT NULL AUTO_INCREMENT, `product_id` int(11) NOT NULL COMMENT '商品ID', `spec_name` varchar(50) NOT NULL COMMENT '规格名:尺寸/口味/甜度', `spec_value` varchar(100) NOT NULL COMMENT '规格值:6寸/10寸', `price_delta` decimal(10,2) DEFAULT '0.00' COMMENT '相对于基础价格的加价', `stock` int(11) DEFAULT '0' COMMENT '该规格独立库存', PRIMARY KEY (`id`), KEY `idx_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这样做的用意是:在商品列表页只需要查主表,在商品详情页再通过product_id把规格列表查出来,前端用规格联动组件展示。不同尺寸的蛋糕加价不同,价格通过price_delta计算。规格还支持独立库存,避免"主表一个库存数字无法区分6寸和10寸各剩多少"的尴尬。

订单表和订单明细表是整个系统的核心中的核心。订单主表记录一次交易的整体信息:order_no,user_id,total_amount,pay_amount,pay_type,status,receiver_name,receiver_phone,receiver_address,remark,delivery_time,create_time,pay_time。订单明细表记录每个商品项的独立信息:order_id,product_id,product_name,product_image,spec_desc,price,quantity,subtotal。

我特别强调一点:订单明细表里一定要冗余保存商品名称、图片、规格描述、单价这些快照字段。为什么?因为商品表中的价格和名称是会变的,如果只存product_id,后续查历史订单时看到的可能已经不是用户下单时的商品信息了。这在售后服务、对账时会非常麻烦。

2.2 库存扣减与订单状态流转的关键陷阱

库存扣减是这个系统里最容易出问题的地方。我最初的做法是在下单逻辑里"先查库存,再update ... set stock = stock - 1",但这在高并发下会超卖。后来改成了两种方式结合:

简单场景用乐观锁:

UPDATE cake_product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

用stock >= quantity条件,如果更新影响行数为0,说明库存不足,直接返回"库存不足"提示。这种方式适合单机部署、并发量不高的蛋糕店场景,实现简单且可靠。

复杂场景或者需要支持秒杀活动时,用Redis预扣库存加异步写库。但蛋糕系统基本不需要走到这一步,所以我只把方案写在设计文档里,实际代码用的还是乐观锁方案。

订单状态流转我专门做了一个状态机工具类,定义好状态转换规则:

public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付待制作"), MAKING(2, "制作中"), DELIVERING(3, "配送中"), COMPLETED(4, "已完成"), CANCELLED(5, "已取消"); // 允许的状态迁移集合 private static final Map<Integer, Set<Integer>> allowedTransitions = new HashMap<>(); static { allowedTransitions.put(0, Set.of(1, 5)); // 待支付 -> 已支付 / 取消 allowedTransitions.put(1, Set.of(2, 5)); // 已支付 -> 制作中 / 取消 allowedTransitions.put(2, Set.of(3)); // 制作中 -> 配送中 allowedTransitions.put(3, Set.of(4)); // 配送中 -> 已完成 } }

这样做的好处是状态流转逻辑集中管理,不会散落在各个Service方法里。我遇到过项目里直接在多个方法里写了if (order.getStatus() == 1) order.setStatus(2),最后状态根本不可控。用状态机以后,非法流转直接抛异常,问题都暴露在开发期而不是上线后。

还有个细节:金额字段类型。decimal(10,2)是标准做法,但要注意Java侧用BigDecimal接收,不要用Double或Float。浮点数在十进制计算里会丢精度,轻则对账不平,重则线上事故。这个算老生常谈,但总有人踩。

3. 核心功能实现:登录鉴权、商品浏览、购物车与下单

这一部分是整个系统的主干,也是工作量最大的部分。我按"从入口到出口"的顺序来讲,顺便把路上遇到的坑都说清楚。

3.1 登录鉴权的两种主流方案取舍

登录鉴权我用的是JWT方案。传统的Session方案在单体应用里其实完全够用,而且实现更简单:登录成功后把用户信息放进Session,后续请求从Session里取。但Session方案在前后端分离的场景下有一系列蛋疼的问题:跨域需要处理Cookie携带、移动端没有Cookie机制、服务端需要维护Session存储(默认内存实现重启即丢失)。

JWT方案的基本流程是:用户提交用户名密码,后端验证通过后生成一个带签名和过期时间的Token,返回给前端。前端之后每次请求在Authorization头带上这个Token,后端通过拦截器解析Token得到用户信息。Token本身是无状态的,不需要在服务端存储,天然适合前后端分离。

我直接使用Spring Security来实现登录认证,因为手动解析Token虽然也能做,但Spring Security提供了:AuthenticationManager、:UserDetailsService、安全过滤器链等一整套机制,代码更规范,安全性也更好。

// 密码校验(Spring Security BCrypt) BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String rawPassword = user.getPassword(); if (!encoder.matches(rawPassword, userInDb.getPassword())) { throw new BusinessException("用户名或密码错误"); }

JWT生成我用了io.jsonwebtoken:jjwt库,设置过期时间位2小时。这里有个小坑:Token过期时间定得太短,用户频繁重新登录体验差;定得太长,Token泄露风险高。我的做法是Access Token有效期2小时,配合一个简单的"无操作踢出机制"——用户在Redis里维护一个最后活跃时间,超过一定时间就强制重新登录。

3.2 购物车与订单生成的完整链路

购物车我最终选择了数据库存储,而不是Redis。虽然Redis的读写性能更好,但蛋糕系统的购物车有它的特殊性:用户可能几天前加了一堆东西,几天后再来下单,如果Redis缓存过期,购物车就清空了,这种体验很不好。数据库存储的成本在低并发场景下完全可以接受,同时天然持久化。

购物车表结构:

CREATE TABLE `cart_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `product_id` int(11) NOT NULL, `spec_id` int(11) DEFAULT NULL COMMENT '规格ID(如6寸/10寸)', `quantity` int(11) NOT NULL DEFAULT '1', `is_selected` tinyint(1) NOT NULL DEFAULT '1', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_product_spec` (`user_id`, `product_id`, `spec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

那个唯一索引非常关键。它保证了同一个用户对同一个商品的同一个规格只能有一条记录。前端每次加购时,后端先尝试插入,如果因为唯一索引冲突,就转为更新数量加1。这种写法比"先select再判断update"更简洁,也避免并发下重复插入。

订单生成的链路是系统里事务最复杂的地方。我把它拆成几步,放在一个事务方法里:

  1. 从请求参数拿到购物车条目ID列表,查出对应的购物车项。
  2. 校验购物车项属于当前用户,防止水平越权(我测试时专门测了这个点)。
  3. 根据购物车项的商品ID和规格ID,查出最新价格和库存。
  4. 计算总金额,减去用户可用优惠券金额,得到实际应付金额。
  5. 生成订单主表记录,状态设为待支付。
  6. 生成订单明细表记录。
  7. 批量删除已下单的购物车项。
  8. 锁定库存。

第4步的金额计算是前端和后端各算一遍的。前端展示用自己算的金额,后端必须重新算一遍,后端永远不信任前端传的数据。用户提交订单时传一个totalAmount,但这个值我只是拿来对齐校验,真正的payAmount以后端计算为准。我在代码里用了一个专门的OrderCalculator类来负责金额计算,避免在Service里堆一堆临时变量。

3.3 下单时库存与金额的校验细节

下单时最容易踩的坑是"价格被篡改"。攻击者完全可以改前端代码,直接把totalAmount改成0.01提交。我在开发时做了两个保险:第一,后端金额全部根据数据库中的价格实时计算,前端传的金额只做一致性比对;第二,支付回调时再次核对订单金额与支付金额是否相等,不一致则标记为异常订单。

库存校验的顺序也有讲究。正确的顺序是:查商品 -> 查库存 -> 扣库存 -> 生成订单。不能"先生成订单再扣库存",因为生成订单之后如果扣库存失败,订单就成了无效订单,还需要额外处理回滚。我把扣库存放在生成订单之前,用事务包裹,任何一步失败都整体回滚。

@Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderRequest request) { Long userId = request.getUserId(); List<CartItem> cartItems = cartItemMapper.selectByIds(request.getCartItemIds()); // 1. 校验归属 for (CartItem item : cartItems) { if (!item.getUserId().equals(userId)) { throw new BusinessException("购物车数据异常,请刷新后重试"); } } // 2. 计算金额 BigDecimal totalAmount = orderCalculator.calculateTotalAmount(cartItems); BigDecimal payAmount = orderCalculator.applyDiscount(totalAmount, request.getCouponId()); // 3. 锁定库存(乐观锁) for (CartItem item : cartItems) { int rows = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new BusinessException(item.getProductName() + " 库存不足"); } } // 4. 生成订单主表 + 明细表 // ... 省略具体字段赋值 }

关于事务,我用了rollbackFor = Exception.class,这个细节很多人会漏。Spring默认只对RuntimeException回滚,如果业务方法里抛的是受检异常,默认是不回滚的。我在代码规范里强制所有业务异常继承RuntimeException,这样rollbackFor就始终生效。

还有一个配送时间的问题。蛋糕订单需要用户选择送达时间,我给订单表加了delivery_time字段,并做了这样一个校验逻辑:送达时间必须晚于当前时间至少1小时(给门店留制作时间),且早于当天23点。这个校验放在后端,避免用户选择已过期的时间。

4. 管理端与营销功能:后台不是简单的CRUD

管理端是很多同类项目的重灾区。不少人在后台随便写几个表格、几个弹窗就交差了,结果上线后发现运营根本没法用。我做管理端时的原则是:每一个操作按钮都要对应当前场景下"合理的业务动作",而不是暴露一堆裸的CRUD接口。

4.1 商品上下架与分类管理

商品管理的表单其实是一个"复合表单"。前端需要上传图片、填写基本信息、动态添加规格(尺寸、口味、价格增量、独立库存)。后端接收时,我用一个ProductSaveRequest来承载,里面有一个嵌套的List<SpecRequest>列表。

商品上下架我使用了status字段,同时做了时间维度:on_sale_time和off_sale_time。这样做的好处是运营可以提前编辑好活动商品,到点自动上架。我加了定时任务,每分钟扫描一次到点需要上下架的商品。这个功能虽然不是主需求,但加上的效果非常加分。

分类管理相对简单,但要注意层级。蛋糕分类可能有"生日蛋糕""慕斯蛋糕""下午茶甜点"等,我设计为单层扁平分类,没有做无限级树。原因是蛋糕店的品类有限,单层分类搭配图标和排序字段就够了,做树形反而增加后端接口和前端组件的复杂度。如果非要支持二级分类,也可以,但需要引入parent_id字段并做递归查询,我建议根据实际店铺规模来决定,别一开始就设计过度。

4.2 订单管理:列表查询、状态操作与打印小票

订单列表是整个后台最复杂的页面。运营需要快速找出"今天待配送的订单""用户取消的订单""已付款未制作的订单",所以查询条件必须丰富:订单号模糊查询、用户手机号查询、状态筛选、时间范围筛选、配送时间段筛选。这些条件组合起来的SQL,如果不加索引,数据量一上来就会慢。

我建的索引组合是:

ALTER TABLE `order_info` ADD INDEX `idx_status_create_time` (`status`, `create_time`); ALTER TABLE `order_info` ADD INDEX `idx_user_id` (`user_id`); ALTER TABLE `order_info` ADD INDEX `idx_delivery_time` (`delivery_time`);

这三个索引覆盖了绝大多数查询场景。特别注意(status, create_time)这种联合索引,它让"按状态筛选并且按时间倒序"的查询直接用索引完成排序,不需要临时文件排序。

订单状态操作员界面上就是几个按钮:确认收款、开始制作、开始配送、完成订单。这些按钮在后端都有对应的状态迁移校验(就是前面说的状态机)。我看过有些项目后台管理直接暴露了updateStatus接口,参数传什么就改成什么状态,这等于把数据库改成了后花园。我的做法是每个操作单独定义接口,例如confirmPaid(orderId)、startMaking(orderId)、startDelivering(orderId),语义明确且状态迁移逻辑内聚在各自的方法里。

打印小票这个功能经常被忽略。蛋糕店接单后需要把订单信息打出来贴到订单上,我集成了一个简单的打印模板,用HTML渲染订单信息,后台浏览器调用打印。这个功能不需要后端做什么,主要是前端控制打印区域,但要注意打印样式跟屏幕样式分离,我用的是CSS媒体查询@media print来控制。

4.3 优惠券与会员等级:常见营销玩法的落地

优惠券是整个系统里最容易出现"四舍五入导致金额不对"的功能。我做的券有两种:满减券和折扣券。满减券逻辑简单,订单金额达到阈值,减去固定金额。折扣券则是按折扣率计算,例如9折券,实际支付金额 = 订单金额 * 0.9。

这里的坑是折扣后的金额可能无限小数,比如计算出来是99.99元优惠9.99元,实际应该保留两位小数。我在设计时定了规则:券的折扣金额在计算时取ROUND_DOWN(向下取整,优惠给得保守一点),而满减金额固定是整数,不会产生小数问题。优惠分摊到订单明细时,用最后一个明细项兜底的方式处理误差。

会员等级我做了三档:普通会员(默认)、银卡会员(累计消费满500)、金卡会员(累计消费满2000)。会员的折扣是在优惠券之后叠加的,但注意不能叠加到包邮门槛之外去,容易把利润压得很薄。我当时的做法是:会员折扣只作用于非活动商品,活动商品不参与会员折扣。这个规则虽然简单,但避免了促销和会员权益冲突。

用户下单成功后,累计消费金额要更新。我直接在支付回调里做累计,同时把用户的level_id升级。更新用户等级时需要小心:不能每次订单都全表扫描用户消费记录,我是在用户表里冗余了一个total_consumption字段,每次支付回调累加,这样判断等级只需要查这一个字段。

5. 性能、安全与部署实操

这块看着不是蛋糕销售系统的核心,但真到上线阶段,这些问题比业务功能还让人头大。我把一个单体Spring Boot应用稳稳定定跑起来的几个关键点讲一遍。

5.1 缓存、分页与数据库连接池

缓存方面我用Redis做商品详情缓存和分类缓存。为什么只缓存这两个?因为它们是高频访问但低频修改的数据。商品详情页是用户浏览最多的页面,每次从MySQL查一次详情再关联规格查询,耗时大约在几十毫秒到上百毫秒,加上网络传输,前端体感会卡。用Redis缓存商品详情后,响应时间能压到几毫秒。

缓存更新的策略是"先更新数据库,再删除缓存",而不是"先更新缓存"。因为更新数据库后直接删除缓存,下一次请求重新加载,能避免并发下旧数据写回缓存的问题。我加了一个简单的定时任务做兜底——每五分钟扫描一次可能存在的缓存不一致,但这个兜底其实很少触发。

分页查询我用的是MyBatis-Plus的Page对象,但有一个小坑:默认的Page在count查询时是全表count,数据量大了之后性能有问题。我的做法是当用户筛选条件较少时(例如只按分类),分页参数设置为不分页,而是走缓存;只有当用户主动搜索关键词或做复杂筛选时,才走数据库分页。效果还凑合。

数据库连接池我用的HikariCP,Spring Boot 2.x默认就是它。配置上需要注意几个参数:maximum-pool-size,单机部署我设置为20,配合minimum-idle=5,避免频繁创建连接;connection-timeout设为30000毫秒,防止突发流量下等待连接超时;max-lifetime设为1800000毫秒(30分钟),跟MySQL的wait_timeout配合,防止连接被数据库端断开后客户端还在用。

5.2 安全防护:从接口防刷到SQL注入

安全这块必须认真对待,因为电商系统天然涉及用户数据和资金。

首先是登录接口的防刷。我加了一个基于IP的限流:同一个IP一小时内最多登录失败5次,超过就锁定该IP一小时。实现就是用Redis计数:

String key = "login:fail:" + ipAddr; Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, 1, TimeUnit.HOURS); } if (count > 5) { throw new BusinessException("登录失败次数过多,请一小时后重试"); }

其次是SQL注入防护。MyBatis的#{}预编译已经防住了大部分注入,但要注意:如果用了${}拼接,必须做白名单校验。我在做排序功能时用到了${}拼接排序字段,这里做了严格校验,排序字段必须在一个枚举列表里,否则报错。

XSS防护用的是全局过滤器,对输入参数做HTML标签转义。这套方案对"商详页输入评论"这种用户生成内容很有必要,否则攻击者可以构造<script>标签窃取Cookie。

最后是越权问题。我在每个需要鉴权的接口里都校验了当前用户ID与操作对象的所有者ID是否匹配。比如用户访问/order/detail/1001,这个订单的user_id必须是当前登录用户,否则直接拒绝。这个逻辑很简单,但非常重要。

5.3 部署方案:从本地运行到服务器上线

部署方案我用了最经典也最稳的组合:Nginx + Spring Boot JAR包 + MySQL + Redis,运行在一台2核4G的云服务器上。

Spring Boot应用打成JAR包后,用java -jar cake-shop.jar --spring.profiles.active=prod启动。为了让进程在后台运行并且宕机自动重启,我写了systemd服务:

[Unit] Description=Cake Shop Application After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/cake-shop ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar cake-shop.jar --spring.profiles.active=prod Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target

这里-Xms512m -Xmx1024m要根据服务器内存来调,堆内存设置太大会导致整台服务器OOM,太小会导致应用频繁GC卡顿。2G内存的服务器,我给JVM最大堆设为1G,给操作系统和MySQL、Redis留出余量。

Nginx配置上,前端静态文件(Vue打包后的dist目录)直接交给Nginx处理,/api开头的请求反向代理到本地8080端口。还需要配置gzip压缩,把一些文本类的响应体积压掉60%以上,对蛋糕图片多的商品列表页虽然帮助有限,但接口JSON部分效果明显。

server { listen 80; server_name your-domain.com; root /opt/cake-web/dist; index 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; } }

部署时的另一个重点是数据库的初始化脚本。我用了Spring Boot的schema.sql和data.sql自动建表,但这个在生产环境很危险——如果重启时加载了错误版本的schema.sql,可能把已有数据表结构搞乱。我的实际方案是:生产环境数据库用迁移脚本(如Flyway或Liquibase)管理,开发环境可以用自动初始化。把环境和脚本管理分开,是上线前必须做的决策。

6. 常见问题与排查实录

最后这部分,我整理一下开发、测试、上线过程中真实遇到的问题。这些坑很多不在教科书里,但实际做项目时几乎一定会碰到。

6.1 典型问题速查表

问题原因解决方案
前端提交订单返回"购物车数据异常"用户购物车商品被后台下架或删除,查不到记录下单时先过滤掉失效商品,提示"部分商品已失效,请重新选择"
支付回调后订单状态未更新回调接口没做幂等,重复报文导致状态回跳回调处理前先判断当前状态,只有待支付才允许更新
商品详情缓存与数据库不一致更新商品后未清除缓存统一封装缓存删除工具方法,所有写操作都调用
后台订单列表页响应慢缺少联合索引,状态筛选走了全表扫描按业务查询维度补联合索引,见5.1节
两台服务器部署后Session共享失效JWT本身是无状态的,但如果有老的Session代码在跑排查代码,确保所有鉴权都走JWT,不使用HttpSession
图片上传到本地服务器重启后丢失Tomcat目录或临时目录被清空上传目录配置为独立磁盘路径,关闭直接重启时的目录重置
定时任务重复执行(多实例部署时)Cron被每个实例各跑一遍引入分布式锁(Redis实现),或用@SchedulerLock

6.2 排查思路与避坑细节

遇到线上问题时,我的排查顺序是:先看日志,再看数据,最后看代码。日志方面,我项目中统一使用SLF4J的占位符风格:

log.info("order created, orderNo={}, userId={}, payAmount={}", orderNo, userId, payAmount);

不要用字符串拼接写日志。占位符风格在日志级别不启用时不会构建字符串,性能更好,而且日志更整齐。

有一个特别值得讲的问题:用户付款成功但订单显示"待支付"。这个问题的根源是支付回调接口和前端轮询接口的时序竞争。用户点击支付成功后,前端一边调起支付回调,一边轮询订单状态。如果回调处理比轮询慢,就会出现短时间的状态不一致。解决方法是:前端轮询到几秒内下单时间创建的订单还处于待支付时,不要立刻提示"支付失败",而是多等几秒再问一次后端。

数据库层面,我遇到过Deadlock found when trying to get lock的问题。原因是我在事务里先更新商品表扣库存,再更新订单表插入记录,多个并发事务成环等待。解决方法是固定每个事务里更新多张表的顺序:先扣库存,再插订单,最后插明细。统一顺序后,死锁基本消失。这是MySQL Innodb下很经典的经验。

6.3 上线前建议补齐的几个点

  1. 支付沙箱测试一定要完整走一遍,包括支付成功回调、手动取消支付、重复回调通知等异常场景。很多项目上了Demo支付就完事,结果正式支付接入时发现回调验签没做,钱收了订单状态却不对。

  2. 日志保留策略要设置好。蛋糕店业务量不大,但日志要留至少30天,方便追溯售后问题。我按天滚动日志,同时用logback的SizeAndTimeBasedRollingPolicy限制单个文件大小,防止磁盘写爆。

  3. 数据库备份不能省。我写了一个半夜3点的定时脚本,用mysqldump全量备份加BINLOG增量,备份文件保留7天。对小型项目来说,这个策略已经足够应付日常故障。

  4. 敏感配置不要放代码仓库。数据库密码、Redis密码、支付密钥这些,我用application-prod.yml单独写一个配置文件,只部署在服务器上,不提交到Git。开发环境用另一个配置,里面是弱密码和沙箱密钥。

写在最后的一些体会

这些天把这个网上蛋糕销售系统从空目录写到能上线运行,最大的感触是:Spring Boot只是一个起点,真正决定项目成色的,是业务细节想得够不够透彻。比如蛋糕规格要不要独立库存、订单状态能不能乱跳、支付回调怎么保持幂等,这些点每一个都能写一篇长文。

我个人的经验是,做这类系统不要急着写代码,先花一晚上把表结构和状态流转画清楚,把"每个按钮对应哪个状态迁移"列出来,后面写的每一行代码都会顺畅很多。如果时间允许,建议把下单核心链路单独抽出来做一套单元测试,至少覆盖库存不足、优惠券不可用、重复提交订单、越权访问这几个场景。这套测试在后续迭代里能帮你挡住大半改动引发的回归问题。

最后再分享一个小技巧:在开发阶段就接入Swagger,并且把每个接口的参数说明、返回值结构都写清楚。这个动作前期会多花五分钟,但到了前后端联调那天,你会发现自己少接了无数个"这个接口传什么参数"的电话。

项目做到这里,功能是完整了,但还有很多可以扩展的方向,比如门店自提、生日贺卡定制、积分商城、配送路线规划。这些都可以在这个基础上长出来。先把地基打好,后面怎么盖楼都不慌。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 18:38:35

pdfium静态库集成实战:从验证到渲染的避坑指南

简介&#xff1a;这份资源是已编译完成的PDFium静态库&#xff0c;面向需要在C/C项目中集成PDF解析、渲染与打印能力的开发者&#xff0c;尤其适合不愿从源码编译、希望快速接入PDF处理功能的中高级技术人员。压缩包共45个文件&#xff0c;约2.92MB&#xff0c;以24个h头文件为…

作者头像 李华
网站建设 2026/10/20 18:37:59

FX3U超音波三边封制袋机调试要点与PLC控制解析

做包装设备这些年&#xff0c;调试过的制袋机不算少&#xff0c;但FX3U超音波型三边封制袋机这个组合&#xff0c;确实值得单独拿出来聊一聊。三边封制袋机本身是包装行业里非常成熟的机型&#xff0c;加上三菱FX3U这颗老而弥坚的PLC做控制核心&#xff0c;再配上超音波焊接系统…

作者头像 李华
网站建设 2026/10/20 18:37:52

IMX335接RV1126调试全流程:从点亮到图像质量调优

刚把 imx335 接到 RV1126 开发板上的那一天&#xff0c;我就知道这不会是一次“改改 dts 就能出图”的活。RV1126 这颗 SoC 在中小型视频方案里很常见&#xff0c;imx335 又是一颗配套频率很高的 sensor&#xff0c;理论上系里的参考代码应该已经很完整&#xff0c;但实际调试时…

作者头像 李华
网站建设 2026/10/20 18:36:57

内部排序与外部排序实战:从算法选型到多路归并工程落地

1. 排序这件事&#xff0c;远没有课本上讲的那么简单很多人第一次接触排序&#xff0c;都是在数据结构课上。老师讲冒泡、插入、选择&#xff0c;然后快排、归并、堆排&#xff0c;最后来一句“实际工程中直接用库函数就行”。这话对&#xff0c;也不对。对的是&#xff0c;日常…

作者头像 李华
网站建设 2026/10/20 18:36:35

Masked Generative Transformer:少步数高质量图像生成新范式

1. 从"堆叠式生成"到"掩码式生成"&#xff1a;这篇论文到底在解决什么问题文本到图像生成这个方向&#xff0c;过去几年几乎被扩散模型统治。你输入一句"一只戴着墨镜的柴犬在冲浪"&#xff0c;模型就开始一步步去噪&#xff0c;从纯噪声里慢慢&…

作者头像 李华
网站建设 2026/10/20 18:36:34

掩码生成式Transformer:文生图效率新路线与并行解码实践

1. 从标题拆解这项工作的核心价值 1.1 这个标题到底在说什么 先把标题拆开看。Masked Generative Transformers&#xff0c;直译过来是“掩码生成式Transformer”&#xff0c;文本到图像的SOTA&#xff0c;意思是在“文字生成图片”这个任务上做到了当时的最好效果&#xff0c…

作者头像 李华