Vue3+SpringBoot无人机商城平台实战:从架构设计到库存扣减方案

发布时间:2026/10/14 22:54:11
Vue3+SpringBoot无人机商城平台实战:从架构设计到库存扣减方案 这两年做无人机垂直电商平台的项目特别多很多朋友一上来就问“Vue3 SpringBoot 做商城是不是就是普通的增删改查” 说实话如果只是把通用电商那套商品、购物车、订单搬过来确实不难但无人机这个品类很特殊参数维度多、客单价高、决策链路长还牵扯租赁、保险、培训这些衍生业务。我这个项目就是围绕 V3 商城 SpringBoot 后端把无人机销售与服务平台完整跑通的设计与实现从数据库建模到前端组件从订单状态机到库存扣减踩了不少坑也沉淀了一套可以直接照着抄的打法。这篇文章就把整个方案掰开揉碎从技术选型到核心代码到线上问题排查一次性讲清楚。1. 平台整体架构与关键设计决策1.1 技术选型为什么是 Vue3 SpringBoot先交代一下最终的选型结论这也是我在项目起步阶段纠结最久的部分。前端用了 Vue3 Vite Pinia Element Plus后端是 SpringBoot 2.7 MyBatis-Plus MySQL 8.0 Redis部署用 Nginx 做静态资源托管和反向代理后端直接打 jar 包跑在轻量服务器上。这个组合现在来看比较主流但仍值得说说每一项背后的取舍。Vue3 相比 Vue2 最大的优势是组合式 API无人机商品的参数展示、筛选、对比这类复杂交互逻辑复用比选项式 API 清爽很多搭配 Vite 之后冷启动速度肉眼可见地快开发体验好。SpringBoot 则天生适合做这种单体应用快速落地自动配置能省掉大量 XML 配置内嵌 Tomcat 也让部署变得简单。MyBatis-Plus 在单表 CRUD 和分页查询上效率极高对这个项目体量来说完全够用比 JPA 更容易理解和控制 SQL。在微服务和单体之间我也考虑过。无人机商城虽然业务模块看起来多商品、订单、租赁、保险但初期流量有限强行拆微服务只会增加运维负担。我最终选择了模块化的单体架构按业务分包后续如果需要拆分也有清晰的边界可以循着划分。1.2 前后端分离架构的设计思路前后端分离是这个项目的最重要骨架前端负责页面渲染和交互后端只提供 JSON API两边完全通过 HTTP 接口通信。这么做带来的好处首先体现在并行开发上——前端写页面的同时后端可以并行出接口两边只要把接口文档约定清楚就能同时推进。我这次就是先通过 YApi 搭了接口文档前端同学我自己对着文档 mock 数据后端还是我专心写业务逻辑整体开发节奏比原先预期的快了很多。另一个好处是部署灵活。前端构建后就是纯静态文件扔到 Nginx 里就能跑后端只在 /api 前缀下提供服务。这样即使将来要把静态资源放到 OSS 或者 CDN 上前端代码完全不用改。当然分离也带来了新的问题最典型的就是跨域。前端跑在 5173 端口Vite 开发服务器后端跑在 8080浏览器会拦截跨域请求。我的解决方案是在后端加一个全局 CORS 配置类同时前端 axios 的 baseURL 统一配置用环境变量区分开发和生产。这块细节在后面第 5 章会专门讲踩坑过程。1.3 数据库建模无人机商品的 SKU 设计先列一下核心表的划分用户表、商品表、商品 SKU 表、分类表、购物车表、订单表、订单明细表、收货地址表、租赁订单表、保险订单表、售后工单表。为什么要单独拆出 SKU 表因为无人机不是普通标准件同样一个型号可能因为搭载的镜头不同、续航套件不同产生多个不同库存、不同价格的销售单元。比如说某款航拍无人机裸机、单电池版、畅飞套装版本质上是三个 SKU价格差了一大截库存也要分开管理。SKU 表的设计需要考虑属性维度。无人机相关的规格包括飞行时间、图传距离、最大续航里程、避障等级、最大起飞重量、摄像头像素、存储方式、遥控器型号等。我在设计时用了通用属性方案而不是把每个字段都写死到表里——用一张商品属性表存储“属性名 属性值”这样以后加新无人机型号的时候不需要改表结构。CREATE TABLE product_sku ( id bigint NOT NULL COMMENT SKU ID, product_id bigint NOT NULL COMMENT 商品ID, sku_name varchar(128) NOT NULL COMMENT SKU名称, price decimal(10,2) NOT NULL COMMENT 销售价格, stock int NOT NULL COMMENT 库存数量, cover_image varchar(512) DEFAULT NULL COMMENT SKU封面图, spec_json json DEFAULT NULL COMMENT 规格属性JSON, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;库存字段我特意放在了 SKU 表上而不是商品表原因很直接用户下单时锁的是具体某个 SKU 的库存而不是整个商品的库存。如果只设计商品级库存下单时还得再查一次 SKU 库存容易出错性能也更差。后面第 3 章会继续讲库存扣减的并发控制。2. 前端 Vue3 核心模块的实战实现2.1 工程化初始化的关键步骤项目初始化我直接用了 Vite 官方提供的 vue 模板比手动搭 Webpack 省事太多。Vite 创建项目的命令很简单但真正要跑起一个能维护的工程还需要做几件事配路径别名、装路由和状态管理、封装请求工具、统一代码规范。路径别名可以让 import 语句简洁很多不用写一长串 ../../../我在 vite.config.js 里配了 指向 src。ESLint Prettier 在项目一开始就配好虽然前期写代码会觉得有点约束但后面维护时不用花精力在格式打架上收益非常大。这些配置看起来很基础但很多项目就是因为前期没做工程化规范后面接口联调时改个路径要全局替换非常痛苦。前端目录结构我采用了按业务模块划分的方式而不是按文件类型划分src/ api/ -- 接口请求定义 assets/ -- 静态资源 components/ -- 公共组件 router/ -- 路由配置 stores/ -- Pinia 状态管理 utils/ -- 工具函数 views/ home/ -- 首页 product/ -- 商品详情与列表 cart/ -- 购物车 order/ -- 订单 user/ -- 个人中心 admin/ -- 后台管理按业务模块划分的好处是加一个新页面时相关的视图、组件、接口都能在一个文件夹里找到不用在多个目录之间跳来跳去。这一点到后期要调整某个业务模块时体会特别深。2.2 axios 请求封装与登录态管理axios 封装是整个前端请求层的基石也是我花时间比较多的地方。我统一封装了一个 http.js做了三件事请求拦截器加 token、响应拦截器拆出业务数据、统一处理 401 未登录状态。// utils/http.js 核心逻辑 import axios from axios import { useUserStore } from /stores/user import { ElMessage } from element-plus import router from /router const http axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) http.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) http.interceptors.response.use( response { const res response.data // 后端统一返回 { code, message, data } 结构 if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { const userStore useUserStore() userStore.clearLoginInfo() router.push({ path: /login, query: { redirect: router.currentRoute.value.fullPath } }) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )登录态我放在了 Pinia 的 user store 里用 localStorage 做了持久化。整体流程是登录成功收到 token 和用户信息后写入 store 和 localStorage请求时从 store 读取 token 放进 header退出登录或 token 过期时清空并跳转登录页。这里有个细节值得展开token 过期后跳回登录页时我保留了 redirect 参数等用户再次登录成功后可以自动跳回原来的页面这个体验细节虽然小但用户感知很强。响应拦截器里把 code 拆开处理而不是直接返回整个 response 对象是为了让业务代码更干净。组件里只要直接拿数据用不用每次都 res.data.data 这种三层嵌套。2.3 无人机参数组件与商品筛选无人机商品展示不同于普通数码产品用户更关心的是续航、图传距离、避障能力、抗风等级这类参数。我专门做了一个参数对比组件放在商品列表页和详情页支持用户勾选 2~3 款无人机进行参数横向对比。这个功能在通用电商里很少见但直接影响无人机消费者的购买决策。参数对比组件的数据来源是后端返回的商品属性表前端通过配置化的方式渲染。我在组件里定义了一个 attributeFields 配置数组每一项包含字段名、中文名、排序然后循环渲染成表格。新增一种属性时只需要改配置不用改组件模板整个组件可复用性比较高。!-- ProductCompare.vue 核心结构简化 -- template div classcompare-table table thead tr th参数项/th th v-foritem in compareList :keyitem.id{{ item.name }}/th /tr /thead tbody tr v-forfield in fieldConfigs :keyfield.key td{{ field.label }}/td td v-foritem in compareList :keyitem.id {{ item.spec[field.key] || - }} /td /tr /tbody /table /div /template script setup import { ref } from vue const props defineProps({ compareList: { type: Array, default: () [] } }) const fieldConfigs ref([ { key: flightTime, label: 飞行时间分钟 }, { key: transmissionDistance, label: 图传距离公里 }, { key: avoidanceLevel, label: 避障等级 }, { key: maxWindResistance, label: 抗风等级 }, { key: cameraPixels, label: 相机像素万 } ]) /script参数对比组件虽然逻辑不难但做的时候要处理好空值展示有些低配机型没有某项参数直接显示“-”而不是空着视觉上更整齐。我一开始没注意这个细节有用户反馈表格里有大片空白后来统一改成空值显示“-”对比效果好了很多。2.4 购物车、订单与支付的完整前端流程购物车模块是电商前端链路中最容易出问题的一块。我总结了几个核心注意点购物车选中状态要能跨页面保持、价格计算必须以后端实时返回为准、库存变更要能及时拦截。购物车的状态我用 Pinia 管理把 cart 列表和 selectedIds 都放在 store 里。这样商品详情页加购、购物车页改数量、订单确认页展示所有组件拿到的都是同一份数据不会出现不同页面数据不一致的问题。结算时有一个很关键的动作——“验价”用户在前端看到的价格只是展示真正提交订单时后端会重新计算总价防止用户绕过前端篡改金额。所以前端购物车的价格计算我保留了但明确在代码注释里标明“仅供展示下单价格以后端为准”。订单流程前端这边主要拆成几个页面订单确认页、支付页、订单列表页、订单详情页。订单确认页要注意几个回显数据的可靠性地址要用默认地址回显、SKU 信息要展示图片和名称、总价要来自购物车验价接口。支付页我接的是支付宝沙箱环境前端只需要拿到支付表单后自动提交或跳转到支付链接即可。实际业务中这块还要考虑支付回调的状态同步后面第 3 章会详细说后端的处理。3. 后端 SpringBoot 核心业务实现3.1 统一返回体与全局异常处理后端接口层我首先规范了统一返回结构。不管接口成功还是失败返回给前端的 JSON 都是同一个格式code、message、data。成功时 code 为 200业务异常返回自定义错误码系统异常返回 500。这样做前端拦截器才好统一处理。全局异常处理用 RestControllerAdvice 注解实现。我需要处理三类异常业务异常比如库存不足、订单状态非法、参数校验异常Valid 校验失败、系统异常。业务异常我用了一个自定义 BusinessException抛出去时携带错误码和错误消息全局处理器统一捕获后返回。RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } ExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleValidException(MethodArgumentNotValidException e) { String message e.getBindingResult().getFieldError().getDefaultMessage(); return Result.fail(400, message); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { log.error(系统异常, e); return Result.fail(500, 系统繁忙请稍后重试); } }这个设计的重要性体现在联调阶段。如果没有统一异常处理后端报错时前端拿到的是底层异常堆栈既难看又容易泄露内部信息统一之后前端只需要关心 code 和 message联调效率高了一个量级。给新加接口时我只需要在业务代码里抛 BusinessException不用每个接口单独写 try-catch 包装代码量少了很多。3.2 JWT 认证与 RBAC 权限设计无人机商城平台涉及三种角色普通用户、商家、管理员。权限上普通用户能看商品、下单、租赁、预约售后商家能管理自己的商品库存和订单管理员能审核商品、管理全站订单和用户。我采用了 JWT Spring Security 的方案但只用到了 Security 的过滤器链没有用它的 UserDetailsService 那套繁琐配置因为用户表结构和角色声明用自定义的校验逻辑更直观。JWT 的具体流程是用户登录成功后后端生成一个 token里面包含 userId、role、过期时间用 HMAC 密钥签名后返回给前端。前端每次请求在 Authorization header 带上这个 token后端定义一个 JwtAuthenticationFilter在请求进入 Controller 前解析 token、设置当前用户上下文。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); Long userId claims.get(userId, Long.class); String role claims.get(role, String.class); // 将用户信息放入上下文 UserContext.set(new CurrentUser(userId, role)); } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\登录已过期\}); return; } } filterChain.doFilter(request, response); } }RBAC 权限控制在方法上通过 PreAuthorize 注解实现。管理员接口写上 PreAuthorize(hasRole(ADMIN))商家接口写上 hasRole(SELLER)没有权限的请求会被 Spring Security 拦截并返回 403。分层上用户上下文用了 ThreadLocal 存储同一个线程内任何地方都能拿到当前登录用户但要注意请求结束时的清理否则线程池复用会串数据。3.3 订单状态机与事务控制订单是这个平台的心脏我设计了完整的订单状态机待支付、待发货、待收货、已完成、已取消、售后中。状态流转规则集中在 OrderService 里不允许通过直接修改数据库字段的方式跳状态所有变更走统一入口方法。订单创建是最需要谨慎处理的地方。用户从购物车提交订单后端要做的操作包括校验商品是否上架、锁定库存、计算价格、生成订单号和订单明细、扣减 Redis 库存、清空对应购物车记录。这些操作必须在一个事务里完成任何一个步骤失败都要全部回滚。事务控制我用 Transactional 注解同时把锁库存的逻辑单独放到一个方法里。因为自己写乐观锁的话锁库存的 UPDATE 语句和其他业务操作分开的话事务边界不好控制。核心逻辑大致是Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateRequest request) { // 1. 验价根据 skuId 列表从数据库查询实际价格与前端提交总价比对 ListSkuInfo skuInfos skuMapper.selectBatchIds(request.getSkuIds()); BigDecimal totalPrice calculateTotalPrice(skuInfos); // 2. 生成订单号 String orderNo OrderNoGenerator.generate(); // 3. 扣减库存伪代码实际用乐观锁 for (SkuOrderItem item : request.getItems()) { int affected skuMapper.deductStock(item.getSkuId(), item.getQuantity()); if (affected 0) { throw new BusinessException(5001, 商品库存不足); } } // 4. 插入订单表和订单明细表 // 5. 清空购物车 return orderId; }订单号生成用了时间戳 用户 ID 随机数的组合实际项目中可以用雪花算法保证高并发下的唯一性和趋势递增。要考虑的一个点是 MySQL 中订单号建议用 varchar 类型而不是 bigint因为雪花算法生成的 ID 是 19 位数字除以 10 的幂次后在 bigint 范围内但前端 JS 的 Number 精度有限会丢失精度所以统一用字符串处理更安全。支付回调是另一个容易踩坑的地方。支付平台异步通知后端后端需要根据支付结果更新订单状态这里要注意“幂等性”同一笔支付回调可能会被通知多次处理前必须查询订单当前状态如果已经是已支付就直接返回成功不再重复处理。3.4 库存扣减方案乐观锁实战库存扣减在电商项目里是并发问题的重灾区。我踩过一次负数库存的坑测试时用两个账号同时下单同一个 SKU结果库存从 2 变成了 -1。问题出在最初的实现是“先查库存再 UPDATE stock stock - quantity”这两个操作之间没有原子性保证。修复方案我用了乐观锁。在 SKU 表里加了一个 version 字段执行扣减时带上条件“stock quantity AND version 对应值”通过 UPDATE 返回的影响行数判断是否扣减成功。SQL 大致如下UPDATE product_sku SET stock stock - #{quantity}, version version 1 WHERE id #{skuId} AND stock #{quantity} AND version #{version}如果影响行数为 0说明库存不足或版本不对业务代码重新查询后提示用户“手慢了库存不足”。这个方案没有用数据库的悲观锁SELECT FOR UPDATE因为悲观锁在高并发下会阻塞其他事务对订单这种短事务影响明显乐观锁在库存充足时几乎不会发生冲突性能好很多。如果后续库存热点特别集中比如秒杀场景再考虑引入 Redis 预扣库存 异步扣数据库的方案但对商城的正常销售节奏纯数据库乐观锁是够用的。4. 无人机平台特有业务模块的设计4.1 租赁模块为什么做了它以及怎么落地单纯做无人机销售平台的价值比较单薄。无人机的购买决策成本高很多用户第一次接触无人机并不确定要不要花几千块钱买自己心仪的机型更希望先租一台试试手感。所以我在平台里加了租赁模块满足这个场景需求。租赁的核心是两个计算租金计算和押金管理。租金按天计费支持按机型配置日租价、押金额。下单时生成租赁订单支付押金后平台发货用户归还时根据超期天数补收超期租金然后原路退还押金。这套流程在数据库上我设计了独立的租赁订单表字段包括商品 SKU、租期开始日、租期结束日、日租金、押金、实际归还日、超期租金等。CREATE TABLE rental_order ( id bigint NOT NULL, order_no varchar(64) NOT NULL, user_id bigint NOT NULL, sku_id bigint NOT NULL, start_date date NOT NULL COMMENT 租期开始, end_date date NOT NULL COMMENT 租期结束, daily_price decimal(10,2) NOT NULL, deposit decimal(10,2) NOT NULL COMMENT 押金, status tinyint NOT NULL COMMENT 状态机, actual_return_date date DEFAULT NULL, overdue_fee decimal(10,2) DEFAULT 0.00, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;租赁的库存与销售库存是分开管理的。同一个 SKU 物理上可能只有一个库存但逻辑上用于租赁的数量和用于销售的数量需要单独配置。我这里采用了一个字段拆分租赁库存归 SKU 表的一个租赁状态标签管理或者独立一张租赁库存表视业务量大小。我这里采取了简单做法在 SKU 表增加了 sale_stock 和 rental_stock 两个字段分别扣减避免两种业务互相干扰。4.2 保险与售后预约无人机作为一种有碰撞风险的高价值商品用户购买后普遍关心后续维护。平台里我集成了保险服务和售后维修预约。保险模块本质上是一个商品附加服务的销售功能用户可以在购买无人机时选择是否加购一年期的机身险下单时生成一个附加订单跟主订单关联。售后预约功能相对复杂一点涉及预约单的创建、商家接单、物流收件、维修进度上报、返还寄送这几个状态节点。我设计了 service_order 表状态机是待审核、已接单、维修中、已寄回、已完成。用户端只需要提交故障描述并上传故障视频或图片后面的流程由商家端操作用户可以在列表页看到进度变化。模块开发过程中有几个容易忽略的细节售后工单的图片上传要限制大小和格式我做了前端压缩 后端文件类型校验双重防护状态变更给用户推送微信公众号模板消息需要提前申请模板权限实际做的时候要注意权限维修费用的确认需要用户二次确认后才能进入维修中状态避免费用争议。4.3 商家端与管理后台的功能边界平台有商家自定义商品的需求所以我在后台管理端做了商品审核流程。商家发布商品后并不会直接上架而是进入“待审核”状态只有管理员审核通过后才会展示到前台。这里涉及商品表的状态字段草稿、待审核、已上架、已下架、审核驳回。管理后台还有订单管理视图、用户管理视图、售后单管理视图。订单管理视图我做了多维筛选条件按订单号、状态、用户手机号、时间范围考虑到商家要处理售后还要给商家留“填写物流单号”的入口。这些后台功能虽然不复杂但相当占用开发时间因为要处理的业务状态非常多每个状态对应哪些操作按钮、哪些角色可以操作都要严格在页面和接口两侧都做好控制。5. 联调踩坑与性能优化实录5.1 前后端联调最常见的 5 个问题这个项目开发过程中我前后端一起写还会遇到一些联调问题总结成一张速查表放在这里遇到类似情况可以照着排查。现象可能原因解决办法前端请求报 404baseURL 配置错了或后端接口路径没对齐检查 axios baseURL 和后端 controller 的 RequestMapping 是否一致跨域报错浏览器拦截了不同源的请求后端配置 CORS 过滤器允许前端地址跨域请求成功但 data 为 undefined后端返回结构里 data 是 null前端解析层次对不上统一结果封装结构前端拦截器统一拆包页面刷新后登录态丢失前端 store 没有持久化使用 localStorage 保存 token 和用户信息初始化时重新加载时间显示相差 8 小时前后端时区不一致后端统一 GMT8JSON 序列化时格式化时间跨域问题值得多说一句。我最早在前端配了 proxy 代理开发环境能通但打包上线后 Nginx 里没有配对应代理导致接口全都 502。后来我前后端都处理了开发环境用 Vite proxy生产环境后端加 CORS 配置。如果你要复用这个方案可以参考下面这段后端 CORS 配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:5173, http://your-domain.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }5.2 图片上传与资源访问的坑商品图片和售后凭证图在项目里两类上传需求并存。商品图片要求清晰度比较高无人机细节多原图可能 5MB 以上售后凭证则只需要 1~2MB 即可。我做了前端压缩使用 canvas 把图片宽高限制在 2000px 以内质量按 0.8 输出压缩后通常能控制在 500KB 以内上传速度明显改善。后端这边要注意上传文件的接口通常要加大 Spring 的文件大小限制默认 1MB 的配置很容易触发异常。我在 application.yml 里修改了两个参数spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB上传文件的存储路径也要注意。开发的时候存在本地临时目录但打包部署后路径可能变了最好用绝对路径配置项管理。我是把上传后的文件存储在项目外部的一个目录下配合 Nginx 的静态资源映射来访问避免重新部署时把用户上传的文件弄丢。5.3 购物车价格的二次校验购物车是前端传参最容易出问题的一环。用户在前端加购一个 SKU、改数量、勾选多个结算如果后端完全信任前端提交的商品价格那风险太大了——懂技术的用户可以篡改请求参数把价格改成 0.01。所以反向校验是必须的提交订单时前端不传单价只传 skuId 和数量后端自己从数据库查真实单价重新计算总价再和前端传的总价做比对。不一致就直接报错不让订单创建下去。这个校验在联调阶段帮了我大忙。有几次前端版本迭代把参数名字改了后端验价失败接口直接抛出异常比“用户下单价格变成 0”然后到支付环节才暴露问题要好处理得多。所以在接口设计时传价格就意味着把钱交给了不可信的前端该把价格计算搬到后端的还是要搬。5.4 首页性能优化与懒加载商品列表页是用户访问最多的页面也是性能优化收益最明显的地方。我在前端做了图片懒加载通过 Vue 的 directive 实现图片进入视口才开始加载配合 loading 占位图首屏加载速度提升明显。Element Plus 自带懒加载指令但如果自己实现也不复杂。后端这边做了分页查询优化。MyBatis-Plus 的分页插件用起来很方便只需配置一个 PaginationInnerInterceptor然后 Page 对象作为查询参数即可。同时要注意列表接口的返回字段要精简不要返回 spec_json 这种大字段的大而全的列表详情页再单独查询详情这样列表接口的响应体积能减少一半以上。Redis 缓存我用在了两个地方商品详情和首页 Banner 配置。商品详情从数据库查出来后序列化到 Redis设置 30 分钟过期当后台修改商品时主动删除对应缓存保证数据一致性。首页 Banner 这类配置变更频率极低的静态内容缓存更久也没问题。这个策略让我本地用几万条模拟数据压了一下首页接口QPS 从 200 左右提升到了 1200效果非常明显。5.5 未来扩展方向项目做完了但有不少可以继续拓展的方向这里简单记录一下。一是搜索引擎目前商品搜索用 MySQL 的 LIKE 模糊匹配数据量大了之后可以引入 ElasticSearch 提升搜索和筛选体验二是秒杀活动无人机在促销节叠加秒杀场景时可以把库存扣减进一步提升到 Redis Lua 脚本保证原子性三是数据看板后台目前只有订单列表如果能加一个销售趋势图、热门商品排行、用户转化漏斗会更有运营价值。写在最后的一些经验总结这个项目从立项到跑通前后花了不到两个月中间踩的坑确实不少。我个人最大的收获就是不要小看那些看起来简单的模块。像购物车、订单这种看起来“不就 CRUD 嘛”的功能真正做到健壮、并发安全、数据一致要花的时间远超预期。尤其是验价和库存扣减这两个点不亲自遇到负数库存和篡改价格的用户根本不会意识到问题的严重性。另一个感受是前后端分离项目一定要先把接口文档定好。我这次是先写 YApi 文档再写代码表面上多花了一点时间实际联调非常顺畅。如果你正在做类似的商城项目强烈建议至少把订单和商品相关的接口先定清楚后面能省下大量改接口的时间。还有一个小技巧想分享给读者调试订单状态机时可以准备一套现成的 mock 支付工具本地把支付回调模拟出来不用每次都走真的支付流程。我在项目里做了个本地调试开关通过配置文件控制是否启用 mock 支付回调这样前后端联调时完全不需要依赖外部环境。项目本身到这里已经交付如果想要在它的基础上做二次开发建议优先补上支付安全加固和后台数据统计两块这两个模块对实际运营的价值最大。最后提醒一句涉及支付的场景一定要做好幂等和回调防重别等上线出问题再来补漏洞那样付出的成本会高得多。