news 2026/10/14 22:54:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

库存字段我特意放在了 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 class="compare-table"> <table> <thead> <tr> <th>参数项</th> <th v-for="item in compareList" :key="item.id">{{ item.name }}</th> </tr> </thead> <tbody> <tr v-for="field in fieldConfigs" :key="field.key"> <td>{{ field.label }}</td> <td v-for="item in compareList" :key="item.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 Result<Void> handleBusinessException(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public Result<Void> handleValidException(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldError().getDefaultMessage(); return Result.fail(400, message); } @ExceptionHandler(Exception.class) public Result<Void> 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;charset=UTF-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 列表从数据库查询实际价格,与前端提交总价比对 List<SkuInfo> 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

租赁的库存与销售库存是分开管理的。同一个 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 小时前后端时区不一致后端统一 GMT+8,JSON 序列化时格式化时间

跨域问题值得多说一句。我最早在前端配了 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 支付回调,这样前后端联调时完全不需要依赖外部环境。

项目本身到这里已经交付,如果想要在它的基础上做二次开发,建议优先补上支付安全加固和后台数据统计两块,这两个模块对实际运营的价值最大。最后提醒一句,涉及支付的场景一定要做好幂等和回调防重,别等上线出问题再来补漏洞,那样付出的成本会高得多。

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

LLM-notebook实战:从零构建AI驱动的智能笔记系统

1. 为什么我们需要一个“AI 时代的笔记本”1.1 从“记录工具”到“思考伙伴”的转变过去十几年&#xff0c;笔记软件的核心逻辑一直是“存储与检索”。你写下什么&#xff0c;它就存什么&#xff1b;你需要什么&#xff0c;就去搜什么。Evernote、Notion、Obsidian 这些工具本质…

作者头像 李华
网站建设 2026/10/14 22:54:02

LaTeX图片排版全攻略:从单图插入到浮动体机制

用过模板的人可能都有这种经历&#xff1a;以为自己只是往 LaTeX 里“贴”一张图片&#xff0c;结果图片一会儿跑到下一章开头&#xff0c;一会儿跟正文叠在一起&#xff0c;位置永远不在你心里预期的位置。过去几年里我都在用 LaTeX 写论文、做课程报告、整理技术文档&#xf…

作者头像 李华
网站建设 2026/10/14 22:53:52

AI辅助论文写作实战:从文献地图到去AI化的七步协作流程

1. 这套写作模式到底在解决什么问题第一次接触“AI辅助论文写作”这个概念的人&#xff0c;脑子里冒出来的画面通常是&#xff1a;把题目丢给模型&#xff0c;等三十秒&#xff0c;一篇八千字的论文就整整齐齐躺在屏幕上。我当初也是这么想的&#xff0c;直到自己真拿几个题目去…

作者头像 李华
网站建设 2026/10/14 22:53:31

LaTeX图片插入完全指南:从浮动体机制到排版实战

LaTeX排版经验总结这个系列&#xff0c;我第一篇就想写图片插入。原因无他——几乎每个刚上手LaTeX的人&#xff0c;都会在插图这一步碰一鼻子灰&#xff1a;明明按教程写了\includegraphics&#xff0c;编译却报"File not found"&#xff1b;图片放得好好的&#xf…

作者头像 李华
网站建设 2026/10/14 22:53:05

Qt Creator 4.11.2源码编译与定制实践:从构建到避坑

简介&#xff1a;Qt Creator 4.11.2 开源源代码包&#xff0c;主要面向需要在龙芯处理器平台上进行源码编译、功能定制或二次开发的 C/Qt 工程师与学习者&#xff0c;尤其是需要处理平台适配与交叉编译难题的开发者。包内收纳集成开发环境的完整头文件、实现源码、构建脚本与少…

作者头像 李华
网站建设 2026/10/14 22:52:54

Scratch安装避坑指南:从下载到首次运行全流程详解

先说个有点反直觉的现象&#xff1a;越是看起来“零基础友好”的软件&#xff0c;越容易在第一步就把人劝退。Scratch 就是这样。很多朋友以为装个少儿编程软件能有多难&#xff0c;结果一搜“下载”两个字&#xff0c;先被各种下载站的花花界面绕晕&#xff0c;下载完要么是打…

作者头像 李华