☰
Spring Boot+Vue旗袍商城系统:从架构设计到上线避坑指南
2026/10/10 9:37:19 网站建设 项目流程

这套“Java基于Spring Boot+Vue的旗袍商城系统”,说直白点就是用Spring Boot做后端接口、Vue做前端页面,把一个垂直品类电商商城完整搭起来。旗袍这个类目跟普通服装不太一样——定制尺寸、面料工艺、款式细节这些信息量很大,不是放几张图填个价格就能卖的。系统里既要考虑普通用户的浏览、选购、下单,也要照顾管理员对商品、订单、用户的管理,还牵扯到库存、支付、物流状态这些电商基础链路。

我自己做过类似的垂直电商项目,旗袍商城的难点其实不在“商城”两个字,而在“旗袍”这两个字带来的特殊业务形态。这篇文章会把整个系统的架构选型、数据库设计、后端接口实现、前端页面搭建以及上线阶段容易踩的坑全部过一遍,适合正在做毕业设计、课设,或者想入门全栈开发但需要一个完整业务场景的朋友参考,Java后端和Vue前端两边都会涉及,但不需要你有很深的基础,跟着思路走就能明白。

1. 项目拆解:旗袍商城到底在解决什么问题

1.1 旗袍类目的特殊性

很多人在做商城系统的时候,习惯直接套用通用的商品模型——商品名称、商品图片、价格、库存、分类,然后就没然后了。但旗袍这个品类非常特殊,它的商品信息结构比普通服装复杂得多。

旗袍有三个核心痛点。第一是尺寸,旗袍讲究合体,很多顾客买旗袍是奔着定制去的,需要填写身高、胸围、腰围、臀围、袖长甚至个人体态备注,这些信息如果不能结构化,下单之后客服还要靠聊天记录追问,体验很差。第二是面料和工艺,真丝、香云纱、棉麻、刺绣、盘扣、滚边,这些属性既影响价格,也影响顾客的决策,需要在商品详情页里清晰展示或筛选。第三是款式分类,改良旗袍、传统旗袍、民国风、日常款、婚礼款,不同分类下用户搜索和浏览的路径完全不一样。

所以这系统里,商品表不能简简单单一张搞定。我的做法是拆成商品主表、商品规格表、商品属性表三层。主表存基础信息,规格表存价格和库存的SKU级别数据,属性表存面料、工艺、特殊说明等扩展字段。这样做的直接好处是,后台发布商品时可以灵活配置不同款式和价格,前台筛选时也能基于真实业务标签去过滤,而不是靠标题搜索硬撑。

1.2 为什么是Spring Boot加Vue这套组合

市面上能选的组合很多,比如单体用Thymeleaf直接渲染、用Flask加Jinja2、或者PHP那套老搭档,但Spring Boot加Vue能成为主流不是没有原因的。

从后端角度看,Spring Boot最大的价值是“约定大于配置”带来的开发效率。做一个商城系统,要处理的模块量很大——用户、商品、订单、购物车、支付回调、后台管理,如果从零去配置Spring MVC、事务、连接池,光环境搭建就能耗掉一两天。Spring Boot通过starter机制把这些事全部自动化了,引入一个依赖就自带一套默认配置,开发者只需要关心业务代码。

从前后端协作角度看,Vue负责把页面拆成组件,每个组件只关心自己那一块界面的逻辑,后端只提供JSON数据接口。这种分离模式的好处是,前端页面调整不需要动后端代码,后端接口升级也不影响页面渲染逻辑。实际开发中我甚至可以让一个人同时负责两边,但代码维护起来依然清晰,不会像传统JSP那样前端后端代码揉在一起。

还有一层考虑是生态成熟度。Element UI这套组件库可以让后台管理界面的开发速度翻倍,Pinia做购物车这种需要跨页面共享状态的管理也非常顺手。Spring Boot这边MyBatis-Plus直接省掉了大量单表CRUD的重复代码,翻页查询一行分页插件就搞定。两个生态都是各自领域里资料最丰富、踩坑成本最低的,对一个商城系统来说,这不叫从众,叫稳妥。

1.3 前后端职责划分与数据流

项目规范上我推荐遵循一套清晰的边界。后端只负责三件事:数据存取、业务规则、权限校验。前端只负责四件事:页面渲染、用户交互、数据展示、调用接口。中间通过统一的RESTful接口通信,数据格式统一用JSON。

以用户登录为例,前端拿到用户名密码后调用后端的/api/auth/login接口,后端验证通过后返回一个JWT令牌,前端把令牌存到本地,在后续的请求头里带上这个令牌,后端再用拦截器校验。整个链路里前端不需要关心密码怎么加密、令牌怎么生成,后端也不需要关心登录表单长什么样,各自专注自己那一层,出了问题排查起来定位也快。

数据流上用一张图就能看明白:用户在前端页面操作,触发Vue组件里的方法,方法调用axios请求,请求带着参数打到Spring Boot的Controller,Controller调用Service层处理业务,Service层调用Mapper操作数据库,数据再原路返回渲染到页面。这套流程看起来简单,但真正做到规范,前后端联调时能少吵很多架。

2. 数据库与核心业务模型设计

2.1 商品模型:不只是一件衣服

数据库设计是整个系统中最不能糊弄的部分,表结构一旦定下来,后面业务逻辑都围着它转。旗袍商城我设计了十几张核心表,这里挑最关键的几张展开。

商品相关的表分了三层。第一层是category分类表,字段包括id、父级id、分类名称、排序值、是否显示。注意分类表要有层级概念,因为旗袍既有“传统旗袍”“改良旗袍”这种大类,也有“日常款”“婚礼款”这种标签式分类,用一张扁平表是撑不住的。

第二层是goods商品主表,存商品名称、副标题、主图、详情介绍、品牌、分类id、状态(上架/下架/售罄)、创建时间、更新时间。第三层是goods_sku表,一个商品对应多个SKU,每个SKU对应具体的款式组合,比如“香云纱面料+紫色+160码”,不同SKU可以有不同价格、不同库存。第四层是goods_attr属性表,存商品维度的扩展属性,用key-value的方式比较灵活,面料、工艺、风格、适用场景都能塞进去。

我实际开发中用到的建表语句示意如下:

CREATE TABLE `goods` ( `id` bigint NOT NULL AUTO_INCREMENT, `category_id` bigint DEFAULT NULL COMMENT '分类id', `name` varchar(200) NOT NULL COMMENT '商品名称', `subtitle` varchar(500) DEFAULT NULL COMMENT '副标题', `main_image` varchar(500) DEFAULT NULL COMMENT '主图', `detail` text COMMENT '商品详情', `status` tinyint DEFAULT '1' COMMENT '状态 1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='旗袍商品表';

为什么一定要拆三层而不是直接一张表存所有字段?因为旗袍的款式组合是笛卡尔积。三个面料乘以四个颜色乘以五个尺码就是六十个SKU,如果每个SKU都在商品表里独立成一行,商品列表查出来会重复王麻子,一个旗袍展示出几十条记录谁受得了。拆成SKU表之后,商品列表只查主表,进入详情页才查SKU列表,分页查询性能也更可控。

2.2 订单、库存与定制量体的联动

订单表我划成order主表和order_item子表。主表保存订单编号、用户id、总金额、支付状态、支付时间、收货人、收货地址、订单状态等,子表保存订单里每一个商品的快照信息——商品名称、商品图片、SKU描述、单价、数量、小计金额。

这里有个容易忽略的细节:订单子表一定要存商品名称和SKU描述的冗余副本,而不是只存商品id。为什么?因为商品信息是会变的,商家修改了商品名称或者下架了商品,历史订单不应该跟着变。冗余一份快照进去,哪怕商品删了,用户查历史订单看到的还是当初买的那件衣服的信息。这个原则电商系统里叫“订单快照”,我在实际开发中见过很多新手直接join商品表查订单,结果商品一删订单就显示异常。

再说定制量体。旗袍定制不是标品售卖,不能选了SKU直接下单就完事。我的设计方案是订单表加一个custom_info字段,类型用JSON字符串,里面存身高、三围、体态备注等信息。下单时前端把量体数据作为订单备注提交,后端在创建订单时原样保存。为什么用JSON而不是专门建一张量体表?因为量体字段可能随着业务调整增加或减少,JSON字段可以随时扩展,不需要改表结构。查询的时候用JSON_EXTRACT也能按需取到具体字段,对中小型系统来说完全够用。

规格库存的扣减放在SKU表上,下单时针对每个SKU做库存校验和扣减。这个设计里最怕的是并发问题,两个用户同时下单最后一件商品,如果校验和扣减不控制好,就会出现卖超的情况。解决思路我放到后面专门讲,这里先说表结构层面的准备——库存字段必须放在SKU表里,不能放在商品主表,因为库存是SKU级别的概念,不同花色码数的库存量通常不一样。

2.3 用户权限与后台管理模型

用户体系拆成两张表。user表存普通用户和后台管理员共用的基础资料——用户名、加密后的密码、手机号、头像、角色类型、注册时间。角色类型用枚举区分,1代表普通用户,2代表管理员。为什么不用多张表分别存用户和管理员?因为两者在登录认证、密码存储、基础资料上是高度重合的,共用一张表加角色字段就够了,权限控制通过后端拦截器判断角色类型即可。

权限控制我不建议在这个项目里做得太重。后台几个模块按管理员和超级管理员两级区分就够了,比如普通管理员能管商品和订单,超级管理员能管管理员账号。用@RequireRole注解配合拦截器实现,简单直接。真要做成Spring Security加RBAC五张表的完整权限体系,对一个小型商城来说反而是过度设计,写起来费劲,维护也费劲。

购物车表结构也很简单,cart_item表存用户id、SKU id、数量、勾选状态、创建时间。每个用户多个SKU,每个SKU一条记录。这里数量上限要做控制,单个SKU最多99件,防止用户误操作刷出超大数量导致库存扣减异常。

3. 后端接口实现与关键细节

3.1 项目初始化与依赖选型

Spring Boot项目的初始化,我用的是Spring Initializr生成基础骨架,然后通过Maven引入所需依赖。核心依赖大概这么几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

MyBatis-Plus在这里的价值很大,Model类加几个注解,Mapper接口继承BaseMapper,单表的增删改查就全有了,分页查询用Page对象传入selectPage方法就能拿到结果,根本不用手写SQL。这在商品列表、订单列表这类全是单表CRUD的场景里,省的时间非常可观。

Redis我主要用来做两件事。一个是存储验证码,注册和修改密码时要发短信或邮箱验证码,存储时设置过期时间,校验后删除;另一个是存临时数据,比如支付二维码的会话ID、后台某些统计的缓存结果。对于订单这类核心数据,我还是老老实实存MySQL,Redis只做辅助,不敢在主链路里过度依赖。

3.2 商品列表与详情接口开发

商品列表接口是所有电商系统的门面,设计上要兼顾功能完整和查询性能。整体思路是:接收分类id、筛选项、分页参数,底层用MyBatis-Plus构造条件查询,返回统一格式的结果集给前端。

我写了一个统一响应类ApiResult,结构是code、message、data三件套,所有接口都返回这个格式。

@RestController @RequestMapping("/api/goods") public class GoodsController { @Autowired private GoodsService goodsService; @GetMapping("/list") public ApiResult list(@RequestParam Long categoryId, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "12") Integer pageSize, @RequestParam(required = false) String fabric, @RequestParam(required = false) Integer minPrice, @RequestParam(required = false) Integer maxPrice) { Page<Goods> page = goodsService.queryPage(categoryId, fabric, minPrice, maxPrice, pageNum, pageSize); return ApiResult.success(page); } }

Service层里面的查询逻辑,有几个点值得展开。第一是分类筛选要带上子分类,用户点到“旗袍”大类,应该看到底下所有子分类的商品,所以查询前先根据categoryId查子分类列表,把子分类id拼成IN条件。第二是价格区间查询直接比较整数价格,但数据库里价格不能存成decimal带小数,我一直建议价格字段统一存“分”这个单位,也就是整数,避免浮点运算的精度问题。第三是面料、工艺这类筛选项,先查属性表拿到符合条件的商品id集合,再通过IN条件回到商品表做分页。

商品详情接口的逻辑简单一些,按id查商品信息、查SKU列表、查属性列表、查详情富文本,拼装成一个VO返回给前端。性能上不用做什么优化,商品量在几百到几千这个量级时,四五个单表查询打出去,响应时间依然在几十毫秒内,完全够用。

3.3 下单事务与库存扣减的正确写法

下单接口是整个系统里业务最密集、最容易写错的地方。流程上要同时完成几件事:校验商品上架状态、校验库存充足、扣减库存、创建订单主表记录、创建订单子表记录、清空购物车对应条目。任何一步失败,整个操作都应该回滚,否则就会出现库存扣了订单没生成、或者订单生成了库存没扣的脏数据。

我用的方案是@Transactional注解加同步锁。方法内部按照“锁库存->扣库存->写订单->清购物车”的顺序执行,任一步异常就抛运行时异常触发回滚。先看下单核心代码:

@Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 查询用户购物车选中的条目 List<CartItem> cartItems = cartItemMapper.selectList( new LambdaQueryWrapper<CartItem>() .eq(CartItem::getUserId, dto.getUserId()) .eq(CartItem::getSelected, true)); if (CollectionUtils.isEmpty(cartItems)) { throw new BizException("购物车中未选中任何商品"); } // 2. 预扣库存并生成订单明细 BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> orderItems = new ArrayList<>(); for (CartItem item : cartItems) { GoodsSku sku = goodsSkuMapper.selectById(item.getSkuId()); if (sku == null || sku.getStatus() != 1) { throw new BizException("商品已下架"); } // 乐观锁扣库存:通过版本号防止并发覆盖 int rows = goodsSkuMapper.deductStock(sku.getId(), item.getQuantity(), sku.getVersion()); if (rows == 0) { throw new BizException("库存不足"); } // 组装订单项 OrderItem orderItem = new OrderItem(); orderItem.setGoodsId(sku.getGoodsId()); orderItem.setGoodsName(sku.getGoodsName()); orderItem.setSkuDesc(sku.getSkuDesc()); orderItem.setPrice(sku.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItems.add(orderItem); totalAmount = totalAmount.add(sku.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 保存订单主表和子表 Order order = new Order(); order.setOrderNo(genOrderNo()); order.setUserId(dto.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.CREATED); order.setCustomInfo(dto.getCustomInfo()); orderMapper.insert(order); orderItems.forEach(item -> { item.setOrderId(order.getId()); orderItemMapper.insert(item); }); // 4. 删除购物车已下单商品 cartItemMapper.deleteBatchIds( cartItems.stream().map(CartItem::getId).collect(Collectors.toList()) ); return order.getId(); }

这段代码里有几个细节很关键。扣减库存不是简单执行update,而是用SQL的update ... where stock >= quantity and version = #{version}做一个乐观锁控制,影响行数为0就说明库存已被别人抢先扣掉或者库存不足,直接抛异常回滚。事务开启后,整个流程要么全部成功提交,要么全部回滚,不会出现中间态数据。

订单号生成也值得单独说,我用的是“时间戳加用户id加四位随机数”的拼接方案,同时保证唯一性和不可预测性。真正的电商系统可能用分布式ID或者雪花算法,但在这个项目里,同步控制下的本地生成已经足够,不引入额外的中间件反而更稳。

4. 前端Vue端的落地实践

4.1 项目结构与路由规划

Vue前端我推荐用Vue3加Vite构建,配合Vue Router和Pinia。项目结构上按功能模块划分目录,api目录统一放接口请求,views目录放页面组件,components目录放公共组件,store目录放全局状态,router目录放路由配置,utils放axios封装和工具方法。

路由规划上要把用户端和管理后台拆开。用户端路由包括首页、商品列表、商品详情、购物车、结算、订单列表、订单详情、个人中心、登录注册。管理后台路由包括商品管理、订单管理、用户管理、分类管理、数据概览。两个区域通过路由嵌套和权限控制分开——普通用户访问管理后台的路由时,会触发路由守卫,发现token里的角色不是管理员,直接重定向回首页。

const routes = [ { path: '/', component: () => import('@/layout/HomeLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/home/Home.vue') }, { path: 'goods/list/:categoryId', name: 'GoodsList', component: () => import('@/views/goods/GoodsList.vue') }, { path: 'goods/detail/:id', name: 'GoodsDetail', component: () => import('@/views/goods/GoodsDetail.vue') } ] }, { path: '/admin', component: () => import('@/layout/AdminLayout.vue'), meta: { requiresAuth: true, role: 'ADMIN' }, children: [ { path: 'goods', name: 'AdminGoods', component: () => import('@/views/admin/GoodsManage.vue') }, { path: 'orders', name: 'AdminOrders', component: () => import('@/views/admin/OrderManage.vue') } ] } ]

路由守卫是个容易被忽略但很重要的一环。我们在router.beforeEach里做三件事:判断页面是否需要登录,判断当前用户的角色是否有权限访问,判断登录是否过期。Vue Router4的路由守卫拿meta字段非常方便,推荐每人养成给路由加meta属性的习惯,权限写在路由表里比写在页面里清晰得多。

4.2 商城核心页面实现要点

商城首页是整个系统给用户的第一印象,布局上我用了四块结构:顶部导航、轮播图、分类导航、商品瀑布流。轮播图和商品数据都来自后端接口,分类导航从category表动态加载,商品瀑布流首屏加载前12条,滚动到底部自动加载下一页。

这里分享一个经验:商品列表页不要再自己写一套分页逻辑了,直接用Element Plus的Pagination组件,后端传total和pages回来,前端绑定事件就能完成翻页。我在实际开发中用element-plus的el-pagination配后端的Page对象,十分钟搞定列表分页,比自己写上一堆currentPage和pageSize手动切片清爽得多。

商品详情页是转化率的核心,我做了三个值得提的设计。第一个是SKU联动选择,页面加载后把该商品所有SKU数据拉下来,用户点击不同的面料、颜色、尺码组合,页面实时判断当前组合是否有库存并切换价格展示。第二个是量体信息折叠面板,标准尺码和定制两种模式下,量体表单的显示与隐藏动态切换。第三个是详情图片走马灯,大图展示工艺细节,支持缩放,这对旗袍这种需要看面料纹理的商品特别重要。

购物车页面我用Pinia做状态管理。购物车数据从后端拉取后存进store,用户加减数量时先更新store,然后调用后端接口同步。为什么中间要加一层store而不是直接改后端数据?因为购物车的勾选状态、合计金额计算、角标数量展示这些UI状态,需要全局共享,打开其他页面再回来时还要保持状态。

export const useCartStore = defineStore('cart', { state: () => ({ items: [], selectedIds: [] }), getters: { totalPrice: (state) => state.items .filter(item => state.selectedIds.includes(item.id)) .reduce((sum, item) => sum + item.price * item.quantity, 0) }, actions: { async fetchCart() { const res = await getCartApi() this.items = res.data }, async addItem(skuId, quantity) { await addCartApi({ skuId, quantity }) this.fetchCart() } } })

4.3 管理后台的开发细节

管理后台我用了Vue3加Element Plus的组合,整体是左侧菜单栏加右侧内容区的布局。商品管理、订单管理、用户管理三个模块是核心。

商品管理页最重要的功能是商品上架编辑。表单字段非常多——基本信息、分类、SKU列表、属性列表、详情富文本。SKU列表我实现成动态表格,可以添加和删除行,每一行填写面料、颜色、尺码、价格、库存。这里要注意一个Vue的双向绑定细节:动态表格里处理嵌套数组时,建议用row.id作为表格行的key,增删行时用index定位form.skuList里的对象,避免Vue响应式追踪丢失导致表格数据更新异常。

订单管理页的核心操作是订单状态流转。未付款->已付款->已发货->已完成,每一步都由管理员点击按钮触发,后端接口里校验当前状态是否允许目标状态跳转。这里我额外做了一个操作日志表,每次修改订单状态都记录操作人和操作时间。虽然看起来多了一张表,但真遇到用户投诉订单状态被动了,查日志能省很多口舌。

图片上传是后台里最容易被忽视的模块。商品图片、轮播图、富文本图片,都要走上传接口。前端用el-upload组件,请求方式multipart/form-data,后端接口接收MultipartFile然后保存到本地磁盘。保存路径我建议按日期分目录,比如/2025/06/,避免几千张图片堆在一个文件目录下,系统访问缓慢还不好备份。文件名用UUID重命名,防止用户上传重复文件名覆盖。上传成功返回一个可以访问的图片URL,前端直接用这个URL展示。

5. 上线前必须解决的常见坑

5.1 跨域与代理配置

前后端分离开发模式下,跨域问题几乎每个人都会碰到。表现形式是浏览器控制台报Access-Control-Allow-Origin错误,前端请求发出去了但读不到响应。

问题根源在于前端的开发服务地址是localhost:5173,后端接口跑在localhost:8080,两个不同端口之间属于跨域请求,浏览器默认拦截了。解决办法有两种。开发阶段,我建议直接在Vite的vite.config.js配置proxy代理:

export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

前端请求路径写成/api/goods/list,开发服务器接收到后自动转发到http://localhost:8080/api/goods/list。这个方案的好处是浏览器认为请求没跨域,不需要后端参与,联调效率高。生产部署阶段,前端构建产物由Nginx托管,Nginx里再配置一层反向代理指向后端服务,同样可以规避跨域,而且顺带把静态资源的访问效率提升了。

如果确实需要后端开启CORS,Spring Boot里写一个配置类,注册CorsRegistry,设置允许的来源、方法、请求头。但我的个人建议是能不用CORS就不用,用反向代理的方式一劳永逸,多少流量、多少环境都适用。

5.2 图片上传与静态资源映射

图片上传在后端保存到磁盘后,前端要能通过URL访问到。这一步经常出问题的地方是Spring Boot默认的静态资源映射路径不包含我们上传的文件目录。

我的处理方式是在application.yml里配置自定义静态资源映射,也可以写一个WebMvcConfigurer实现类:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }

上传文件时,文件写到配置的uploadPath下,返回给前端的URL就是http://服务器地址/upload/2025/06/xxx.jpg。注意Windows环境路径末尾要加斜杠,Linux环境也最好统一配置成绝对路径,虽然麻烦一点但部署到服务器后不会因为相对路径问题疯狂排查。

还有一个坑是富文本编辑器里的图片。商品详情的富文本提交时会把图片转成base64内嵌在HTML里,一个详情几十张图全base64会导致提交的数据量爆炸,后端报文直接超限。处理办法是富文本编辑器的图片上传走单独的接口,编辑器拿到图片URL后插入内容,详情里存的始终是图片链接而不是base64。这个细节不做,上线之后商品详情稍微长一点就会出现保存失败,而且MySQL的TEXT字段也扛不住几条就爆。

5.3 并发场景下的库存一致性

商城系统最容易出问题的地方就是库存。我自己在压测阶段模拟过50个用户同时抢一件库存只剩5的旗袍,如果不加控制,最终下单成功的人可能远超5个。原因在于每个用户的下单请求几乎同时到达,都在检查库存时看到了剩余5件,然后一起执行扣减,把库存扣成了负数。

解决方案分成三个层次。最底层的是数据库层面的乐观锁,上一段代码里的deductStock方法就是,在SQL更新语句里加上where stock >= quantity条件,影响行数为0时说明库存已经没了,直接报错。这一层兜底能保证永远不会卖超,是最关键的防线。

第二层是业务层面的动态库存预热。拿Redis把热门商品的库存缓存一份,下单接口先走Redis的decrement操作,判断剩余量是否足够,如果不够就直接拒绝,扛住大量请求时性能比直接查数据库好得多。但要注意Redis和数据库的库存同步逻辑,下单成功扣Redis,事务提交后再扣数据库,定期做对账。

第三层是单机场景下的JVM锁。用synchronized (skuId.intern())锁住同一个SKU的下单操作,这样相同商品的并发请求会排队执行,天然避免了同时读到相同库存的问题。这层锁在单机部署下是有效的,但一旦做负载均衡多实例部署就失效了,需要换成分布式锁。对这个项目来说,单机部署场景下,乐观锁和事务已经足够解决问题,压测数据也很健康。

5.4 前后端联调时的接口规范问题

前后端联调消耗的时间往往比想象的多,大部分问题出在接口约定不明确。时间字段格式不统一、分页参数命名不一致、错误码混乱,这些小事叠加起来能磨掉一大半开发时间。我整理了一张接口约定速查表,在项目启动时发给团队或自己参考:

约定项统一规则
接口前缀统一以/api开头,后台管理再加/admin
数据格式统一{code, message, data},成功时code=200,data为对应数据
分页参数默认pageNum和pageSize,返回体里带total字段
时间格式统一为yyyy-MM-dd HH:mm:ss,通过@JsonFormat注解控制
金额字段后端以“分”为单位的整数,前端展示时再除以100
错误码业务异常统一抛BizException,全局捕获后返回对应code

实际操作中,接口先定义好再开写的好处非常明显。以前端商品详情页为例,页面需要商品基本信息、SKU列表、属性列表、详情富文本四块数据,我推荐直接提供聚合接口/api/goods/{id}一次性返回完整VO,而不是让前端调四个接口自己拼。少几次网络请求,前端代码也简洁。如果分成四个接口,前端加载完还要挨个处理loading状态,体验和代码都好不了。

还有一个小建议:所有接口的日志一定要打完整。请求参数、返回结果、耗时,都要记录。线上出问题时,日志是最快的定位手段。Spring Boot里用@Slf4j加AOP统一打印接口日志,一次配置全程生效,比在业务代码里手动log舒服得多。日志级别生产环境调到INFO,调试问题单独开DEBUG,磁盘和排查效率都能兼顾。

最后再分享两个小技巧。第一个是数据库字段命名从一开始就统一用下划线风格,Java实体用驼峰,MyBatis-Plus开启map-underscore-to-camel-case自动映射,全局配置一次,后面再也不用写繁琐的字段映射。第二个是后端写接口时,Controller层保持薄,只做参数接收和结果返回,真正业务逻辑下沉到Service层,这样单元测试时只需要mock Service,接口层面的修改也不容易影响核心逻辑。这些习惯一开始坚持好,项目越大越能感受到好处。

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

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

立即咨询