首页 / 资讯中心 / 文章详情

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

基于Spring Boot的网上蛋糕销售系统设计与实现 ★ FEATURED ARTICLE
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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这样做的用意是在商品列表页只需要查主表在商品详情页再通过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 MapInteger, SetInteger 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;那个唯一索引非常关键。它保证了同一个用户对同一个商品的同一个规格只能有一条记录。前端每次加购时后端先尝试插入如果因为唯一索引冲突就转为更新数量加1。这种写法比先select再判断update更简洁也避免并发下重复插入。订单生成的链路是系统里事务最复杂的地方。我把它拆成几步放在一个事务方法里从请求参数拿到购物车条目ID列表查出对应的购物车项。校验购物车项属于当前用户防止水平越权我测试时专门测了这个点。根据购物车项的商品ID和规格ID查出最新价格和库存。计算总金额减去用户可用优惠券金额得到实际应付金额。生成订单主表记录状态设为待支付。生成订单明细表记录。批量删除已下单的购物车项。锁定库存。第4步的金额计算是前端和后端各算一遍的。前端展示用自己算的金额后端必须重新算一遍后端永远不信任前端传的数据。用户提交订单时传一个totalAmount但这个值我只是拿来对齐校验真正的payAmount以后端计算为准。我在代码里用了一个专门的OrderCalculator类来负责金额计算避免在Service里堆一堆临时变量。3.3 下单时库存与金额的校验细节下单时最容易踩的坑是价格被篡改。攻击者完全可以改前端代码直接把totalAmount改成0.01提交。我在开发时做了两个保险第一后端金额全部根据数据库中的价格实时计算前端传的金额只做一致性比对第二支付回调时再次核对订单金额与支付金额是否相等不一致则标记为异常订单。库存校验的顺序也有讲究。正确的顺序是查商品 - 查库存 - 扣库存 - 生成订单。不能先生成订单再扣库存因为生成订单之后如果扣库存失败订单就成了无效订单还需要额外处理回滚。我把扣库存放在生成订单之前用事务包裹任何一步失败都整体回滚。Transactional(rollbackFor Exception.class) public Order createOrder(CreateOrderRequest request) { Long userId request.getUserId(); ListCartItem 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来承载里面有一个嵌套的ListSpecRequest列表。商品上下架我使用了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数据量大了之后性能有问题。我的做法是当用户筛选条件较少时例如只按分类分页参数设置为不分页而是走缓存只有当用户主动搜索关键词或做复杂筛选时才走数据库分页。效果还凑合。数据库连接池我用的HikariCPSpring Boot 2.x默认就是它。配置上需要注意几个参数maximum-pool-size单机部署我设置为20配合minimum-idle5避免频繁创建连接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.activeprod启动。为了让进程在后台运行并且宕机自动重启我写了systemd服务[Unit] DescriptionCake Shop Application Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/cake-shop ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar cake-shop.jar --spring.profiles.activeprod Restarton-failure RestartSec5 [Install] WantedBymulti-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实现或用SchedulerLock6.2 排查思路与避坑细节遇到线上问题时我的排查顺序是先看日志再看数据最后看代码。日志方面我项目中统一使用SLF4J的占位符风格log.info(order created, orderNo{}, userId{}, payAmount{}, orderNo, userId, payAmount);不要用字符串拼接写日志。占位符风格在日志级别不启用时不会构建字符串性能更好而且日志更整齐。有一个特别值得讲的问题用户付款成功但订单显示待支付。这个问题的根源是支付回调接口和前端轮询接口的时序竞争。用户点击支付成功后前端一边调起支付回调一边轮询订单状态。如果回调处理比轮询慢就会出现短时间的状态不一致。解决方法是前端轮询到几秒内下单时间创建的订单还处于待支付时不要立刻提示支付失败而是多等几秒再问一次后端。数据库层面我遇到过Deadlock found when trying to get lock的问题。原因是我在事务里先更新商品表扣库存再更新订单表插入记录多个并发事务成环等待。解决方法是固定每个事务里更新多张表的顺序先扣库存再插订单最后插明细。统一顺序后死锁基本消失。这是MySQL Innodb下很经典的经验。6.3 上线前建议补齐的几个点支付沙箱测试一定要完整走一遍包括支付成功回调、手动取消支付、重复回调通知等异常场景。很多项目上了Demo支付就完事结果正式支付接入时发现回调验签没做钱收了订单状态却不对。日志保留策略要设置好。蛋糕店业务量不大但日志要留至少30天方便追溯售后问题。我按天滚动日志同时用logback的SizeAndTimeBasedRollingPolicy限制单个文件大小防止磁盘写爆。数据库备份不能省。我写了一个半夜3点的定时脚本用mysqldump全量备份加BINLOG增量备份文件保留7天。对小型项目来说这个策略已经足够应付日常故障。敏感配置不要放代码仓库。数据库密码、Redis密码、支付密钥这些我用application-prod.yml单独写一个配置文件只部署在服务器上不提交到Git。开发环境用另一个配置里面是弱密码和沙箱密钥。写在最后的一些体会这些天把这个网上蛋糕销售系统从空目录写到能上线运行最大的感触是Spring Boot只是一个起点真正决定项目成色的是业务细节想得够不够透彻。比如蛋糕规格要不要独立库存、订单状态能不能乱跳、支付回调怎么保持幂等这些点每一个都能写一篇长文。我个人的经验是做这类系统不要急着写代码先花一晚上把表结构和状态流转画清楚把每个按钮对应哪个状态迁移列出来后面写的每一行代码都会顺畅很多。如果时间允许建议把下单核心链路单独抽出来做一套单元测试至少覆盖库存不足、优惠券不可用、重复提交订单、越权访问这几个场景。这套测试在后续迭代里能帮你挡住大半改动引发的回归问题。最后再分享一个小技巧在开发阶段就接入Swagger并且把每个接口的参数说明、返回值结构都写清楚。这个动作前期会多花五分钟但到了前后端联调那天你会发现自己少接了无数个这个接口传什么参数的电话。项目做到这里功能是完整了但还有很多可以扩展的方向比如门店自提、生日贺卡定制、积分商城、配送路线规划。这些都可以在这个基础上长出来。先把地基打好后面怎么盖楼都不慌。
阅读完成 · 觉得有帮助?
咨询建站