尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

SpringBoot+Vue电商网站全栈实战:SKU建模、乐观锁与JWT认证

发布时间:2026/9/26 4:41:03

资讯中心
01
ARTICLE

SpringBoot+Vue电商网站全栈实战:SKU建模、乐观锁与JWT认证

SpringBoot+Vue电商网站全栈实战:SKU建模、乐观锁与JWT认证
时间回到我刚做完这个项目的那个晚上脑子里全是订单状态、库存超卖和前后端联调时那几百行报错日志。说实话基于 SpringBootVUE 的综合电商网站设计与实现这个题目在 Java 全栈类项目里几乎是被写烂了的“老三样”之一但正因为烂大街它反而成了一张试金石——同样的题目有的人做出来就是玩具 Demo答辩时讲两句就没词了有的人做出来却是一套能演示、能压测、能讲清楚每一个设计决策的完整系统。这篇文章我会把当初从零搭建到上线部署的完整过程拆开揉碎重点放在那些文档里不会写的部分SKU/SPU 到底怎么建模、下单扣库存为什么不能只靠 if 判断、JWT 令牌过期之后前端怎么续期、支付回调的幂等性怎么保证、以及最后部署到服务器上图片加载不出来的那些破事。不管你是做毕设、找工作写进简历还是纯粹想练手全栈这篇都值得你认真看一遍。1. 这个题目到底在考察什么先说清楚项目的骨架“综合电商网站”听起来很宽泛但落到技术上其实就两条主线前台用户能逛能下单、后台管理员能管商品管订单。绝大多数人把它做成了“商品列表 购物车 下单”三步走然后就去忙别的了。这不能说是错的但如果把这个项目当作展示能力的作品需要面对几个更深层的问题商品数据怎么组织、库存扣减在高并发下会不会出问题、订单的状态流转怎样才能不乱、文件上传后存到哪里、以及前后端分离架构下用户身份怎么认证。1.1 这类项目的标配功能清单与进阶思路一个“能打”的综合电商网站至少需要覆盖下面这些模块模块前台用户侧后台管理侧用户系统注册、登录、JWT 认证、个人资料用户列表、状态管理、角色区分商品系统分类浏览、关键字搜索、商品详情商品 CRUD、上下架、库存维护购物车加入购物车、修改数量、删除、结算无需后台但数据库需完整记录订单系统提交订单、支付、查看历史订单、取消订单列表、发货操作、退款处理支付模块模拟支付沙箱/伪支付支付回调记录、对账营销模块优惠券领取/使用可加分优惠券创建、发放记录物流与评价物流轨迹可模拟、商品评价可加分发货填写单号、评价管理基础功能每个教程都在做真正的加分项在于后面的内容库存扣减的并发安全方案、订单状态机的设计、支付回调幂等处理。这三个点哪怕只做到位一个答辩时都能让你从“用过框架”变成“做过设计”的档位。1.2 开发环境与版本选型为什么这样组合我当时的选型是这样的后端SpringBoot 2.7.xJDK 8 或 11MyBatis-Plus 作为 ORM 框架MySQL 5.7/8.0Redis 用于缓存和库存预扣Maven 做依赖管理。前端Vue 2.x Vue Router Vuex如果不想引入太重的东西可以用 Pinia但配套生态和文档量 Vue 2 更丰富Element UI 做后台管理页面Vue 3 Element Plus 也行看你熟悉哪个选哪个。部署后端打包 jar 直接扔服务器跑前端 npm run build 之后把 dist 目录交给 Nginx 托管。版本上有个很实际的建议——SpringBoot 不要追求最新尤其是 3.x 系列。SpringBoot 3 基于 Jakarta EE 换了包名网上能找到的资料和踩坑经验相对少很多老教程直接跑不通。用 2.7.x 这种稳定版本做项目遇到问题搜一下基本都有解决方案把精力留给业务逻辑才是正事。2. 项目工程结构设计与数据库建模从根上决定上限很多同学喜欢拿到题目就先写 Controller而 Controller 里直接就写业务代码。这种开发方式在小 Demo 里看不出来问题一旦订单流程牵扯到商品、库存、积分、物流、支付回调几个模块互相调用代码马上变成一团乱麻。个人推荐的分层是Controller接口层→ Service业务层→ Mapper数据访问层。实体用entity包DTO 和 VO 要分开建——入参不要直接用实体类去接出参也不要直接把实体类返回给前端。刚接触项目的人会嫌麻烦等到要加一个字段、又要防止用户传某个字段进来把数据改了的时候就会明白这个设计多重要了。2.1 一张典型电商库表的设计思路以product商品表为例最简单的建模通常长这样id、name、category_id、price、stock、image、description、status、create_time、update_time这样设计在只有一个商品级别的时候够用。可一旦涉及到“颜色尺码版本”比如同一款手机有 8G256G 和 12G512G 两个规格价格还可能不一样单表平铺就完全没法处理了。这里就要引入电商系统里极其常见的一对概念SPUStandard Product Unit标准化产品单元和 SKUStock Keeping Unit库存量单位。SPU 描述的是“这是个什么商品”比如“iPhone 15 Pro Max”拥有统一的标题、描述、详情图。SKU 描述的是“具体哪个版本有货”比如“iPhone 15 Pro Max / 黑色 / 256G”库存、价格挂在 SKU 上。对应的表结构调整为spu表存公共信息sku表存具体规格和库存中间再挂一个sku_specification记录规格项的 JSON 数据。JSON 字段在某些场景下如规格组合是合理的妥协不必为此单独建五六张表。2.2 购物车和订单表建表常见的坑购物车这块很多人想当然只存“商品 ID 和数量”这样做在需求简单时确实没问题但用户如果修改了商品价格、商品下架了购物车里的价格和状态就全乱了。稳妥的做法是购物车表存user_id、sku_id、quantity、checked是否选中结算每次拉取购物车列表时实时去查 SKU 最新的价格和库存而不是把价格直接冗余进去。这样用户下次进购物车时能立即看到价格变动做促销活动也方便。订单表则要明确区分“订单主表”和“订单明细表”。一个订单对应多条商品记录主表存订单号、用户 ID、总金额、状态、收货人、手机号、地址明细表存每一条商品的 SKU ID、快照名称、快照价格、购买数量。这里有个关键细节订单里的商品名称和价格必须存快照不能下单之后再去 join 商品表。道理很直白——商品以后改价、改名甚至删除历史订单不能跟着变否则对账和售后的数据全乱套了。3. 商品模块与缓存策略别把数据库扛在肩膀上商品详情页是电商网站访问量最大的页面之一如果不加缓存每次都要查spu、sku、图片表数据库压力会非常大。最简单的梯度方案是热数据放 Redis冷数据回源数据库。3.1 商品列表缓存与缓存穿透的简单解法商品列表页的数据结构可以缓存成一个 JSON 字符串Redis Key 设计为product:list:{categoryId}:{page}:{size}超时时间设在 30 到 60 分钟。这样一来同一页的请求全部打 Redis数据库的查询频率大幅度下降。缓存穿透的问题在于——有人故意查不存在的商品 ID每次都绕过 Redis 打到数据库。解法是在查不到数据时也往 Redis 里写一个短暂的“空缓存”过期时间设短一些比如 2 分钟这样同一 ID 的恶意请求就只会第一次穿透到数据库。这个方法虽然老套但确实简单高效。缓存击穿是指一个热点 Key 刚好失效大量请求瞬间打进数据库。最简单的处理是给热点 Key 设置不主动过期靠后台定时任务刷新或者直接加锁在缓存失效时只放行一个线程去数据库查询其他人等待缓存重建完成。项目里用到锁的情况下分布式锁是个常用工具这里就不展开细节了但思路可以选择本地 JVM 锁或者 Redis 分布式锁取决于你服务的节点数量。3.2 商品上下架与缓存同步比较重要的一条实操经验是写了数据库一定别忘了主动删缓存而不是被动等过期。比如后台管理员上架商品、改价格如果只更新 MySQL 而不同步删掉 Redis 里的旧 JSON用户端刷新一小时还是老数据。常见做法是封装一个CacheService在商品 update/delete 的方法里把涉及该商品的列表缓存、详情缓存全部删除。下次请求到来时缓存未命中自动回源数据库再重建缓存这样就完成了数据同步。我见过有人为了追求“绝对一致”搞实时双写、消息队列通知说实话在单体电商项目里这是杀鸡用牛刀。删除缓存回源重建的延迟只有几十毫秒对用户来说根本无感但代码量少一个数量级。4. 核心交易链路购物车、下单、库存扣减与状态机这是整个项目里最有含金量的部分也是面试官或答辩老师最喜欢深挖的地方。4.1 购物车到订单的完整流程用户从购物车结算到订单生成通常分这么几步前端把勾选的购物车记录 ID 列表传到后端。后端根据 ID 查出购物车明细校验 SKU 是否上架、库存是否充足。按当前最新价格计算总金额前端显示的价格只做展示不能作为后端结算依据。生成订单主表和订单明细表给订单生成唯一的业务订单号。扣减库存这一步是并发控制的核心。清空对应购物车记录。返回订单号和待支付金额到前端。这个流程最关键的点在于第 5 步的库存扣减。很多初学者写的是这样一段代码Sku stock skuMapper.selectById(skuId); if (stock.getStock() quantity) { stock.setStock(stock.getStock() - quantity); skuMapper.updateById(stock); }这在单用户、低并发下演示没问题但只要两个用户同时下单最后一件商品程序读到的库存可能都是 1两个请求都通过了 if 判断最后都把库存减成 0超卖就发生了。这是并发场景下的经典竞争条件。4.2 乐观锁扣库存最简单可靠的方案数据库层面最直接的方案是乐观锁给sku表加一个version字段每次更新时带上版本条件UPDATE sku SET stock stock - #{quantity}, version version 1 WHERE sku_id #{skuId} AND stock #{quantity}注意这里的 SQL 直接用stock quantity作为条件比“先查再改”安全得多。受影响行数如果是 0就说明库存被扣光或者并发冲突了服务端立刻返回“库存不足”前端收到提示后刷新购物车即可。乐观锁在高并发冲突时会导致部分请求失败用户体验会受到影响。常见的更进一步优化是Redis 预扣库存——下单前先用DECR命令在 Redis 里扣库存不足直接拒绝请求等支付成功后异步更新数据库的最终库存。这样能扛住瞬时流量但引入的消息补偿逻辑会复杂不少。对于单体毕设项目或个人作品乐观锁已经够用了如果你想让答辩老师眼前一亮可以在方案说明里主动提到“Redis 预扣 异步对账”的进阶思路并标出自己实现的乐观锁版本是它的简化形态。4.3 订单状态机设计为什么不能随便 if-else订单状态是电商系统里最容易写乱的模块。常见状态有待支付PENDING_PAYMENT、已支付PAID、已发货SHIPPED、已完成COMPLETED、已取消CANCELLED、退款中REFUNDING和已退款REFUNDED。如果每个状态变化都在业务代码里判断“当前状态能不能转成目标状态”后期加需求就会在各个方法里塞一层又一层 ifBug 层出不穷。可靠的办法是建立一个状态机表或枚举映射把合法流转明确写出来当前状态合法目标状态触发动作待支付已支付、已取消支付回调、用户取消已支付已发货、退款中商家发货、用户申请退款已发货已完成、退款中用户确认收货、申请售后已完成无终点状态已取消无终点状态退款中已退款商家同意退款然后在更新状态时只执行一步操作if (orderStateMachine.canTransit(currentStatus, targetStatus)) { order.setStatus(targetStatus); orderMapper.updateById(order); } else { throw new BusinessException(非法的订单状态流转); }这样无论前端传什么状态后端都有统一的校验入口订单状态就不会乱。它看起来只是多写了一张对照表但真的能避免后期数据脏乱差和线上改代码的悲剧。5. 前后端分离下的登录认证与联调JWT 与路由守卫SpringBoot 和 Vue 分属两个服务Session 机制默认不适用更通用的做法是 JWTJSON Web Token。用户登录成功后后端生成一个包含用户 ID、用户名、过期时间的签名 Token 返回给前端前端拿着它放到请求头Authorization里访问所有受保护接口。后端写一个拦截器统一校验。5.1 后端拦截器与 Token 校验拦截器要配置白名单比如/api/user/login、/api/user/register、商品列表和详情查询、图片访问路径。其余接口全部过 Token 校验校验通过后把用户信息放入ThreadLocal或请求上下文方便后续业务代码获取当前登录用户。SpringBoot 里推荐用HandlerInterceptor加WebMvcConfigurer注册滤掉 OPTIONS 预检请求这是前后端分离项目最容易被忽略的地方——浏览器跨域时会先发一个OPTIONS请求探测接口是否允许访问如果你把所有非白名单路径都要求带 Token预检请求拿不到 Token 就被拦了前端看着莫名其妙的网络报错还以为接口坏了。5.2 前端路由守卫与 Token 刷新Vue 侧用户未登录却访问/cart、/order等页面时需要统一重定向到登录页。在router/index.js中配置全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else { next(); } });登录后把 Token 存到localStorageAxios 请求拦截器统一带上请求头。同时响应拦截器里判断401状态码发现 Token 过期就跳转登录页不要把后端抛出的错误信息直接裸露给用户。Token 有效期设多长也是个值得考虑的问题。太短用户频繁掉线太长有安全风险。合理设计是短 Token 刷新 Token访问 Token 2 小时过期刷新 Token 7 天或 30 天访问 Token 失效后用刷新 Token 换新。如果觉得双 Token 机制工作量比较大至少要把 Token 时效设为 24 小时且记住密码自动登录并能讲清楚这个取舍的利弊。5.3 接口联调时的跨域配置开发环境下后端要开启跨域允许最简单的全局配置Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:8081); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这里有个坑allowCredentials(true)时AllowedOrigin不能写*必须明确指定来源地址否则浏览器依然会拦截掉请求。除非你完全不使用 Cookie 和 Credentials但推荐的做法是严格配置前端实际地址。6. 支付模块的模拟实现与回调幂等性设计电商网站不可能真的对接支付宝微信支付完整流程但这不代表“支付”这环应该砍掉。标准的替代方案是模拟支付沙箱在项目中设计一个支付订单表点击“立即支付”后生成支付单跳转一个模拟收银台页面用户确认支付后把支付状态改变再由一个模拟回调接口通知订单系统支付成功。这既还原了真实支付的链路又规避了个人签约第三方支付商户号的繁琐门槛。6.1 支付回调的幂等性处理真实支付回调和模拟回调本质都要求确保逻辑只执行一次。比如支付宝回调同一个通知可能发送多次如果每次回调都直接把订单状态改成已支付后续重复回调就可能导致订单状态被重复推进或关联的加积分操作重复执行。最基本的解决思路是引入回调流水表记录支付回调的payment_id或trade_no处理前先去查这条记录是否已经处理过如果处理过直接返回成功应答不再走重复业务逻辑。同时再配合状态机流转判断——只有待支付状态下的订单才能被置为已支付也能杜绝重复推进的问题。两者叠加使用幂等才有保障。回调接口必须对外部封锁不能让用户自己构造一个请求来模拟支付成功。一个实用的做法是在支付单表里预留一个notify_secret随机值前端发起支付时后端把它返回给支付沙箱保存回调时从回调消息的签名信息里取出来做比对对不上直接拒绝。这个设计放在答辩里杀全场——你把“防伪造回调”这种真实支付的安全机制讲出来明显比“我们调用一个接口改一下状态”有深度。6.2 支付后逻辑的一致性库存、积分、优惠券支付成功后不止改订单状态一件事还牵连着很多关联数据。通常是订单状态变为已支付 → 积分流水新增 → 优惠券标记为已使用 → 如果之前用的是 Redis 预扣库存还要把扣减结果同步到 MySQL。这里多个操作之间的“要么全成功要么全不成功”约束最直观的方案是使用 Spring 的Transactional注解包住整个方法。要记住一个原则事务只能覆盖同步调用链异步操作比如发消息、发邮件出了异常回滚不掉需要单独补偿。单体项目典型异步场景订单超时自动取消可以用延迟消费、定时任务扫描到期未支付订单来兜底不要勉强在支付回调里实现定时任务等待。7. 项目性能优化与上线部署从能跑到能扛一个电商网站如果只满足于本地能跑通那它永远只是个练习项目。要拿得出手至少得做到部署上线、访问顺畅、数据不崩这三条基本线。7.1 数据库层面容易被忽略的索引设计商品表按分类筛选则category_id加索引按关键词搜索数据库层面至少对name字段加普通索引。订单表最核心的是按用户查订单user_id加索引create_time加索引否则用户订单一多全表扫描会越来越慢。订单号有唯一索引。“哪列参与查询哪个列就加索引”这个原则在初期够用。后续如果范围查询和排序字段复杂度上升可以在性能调优阶段逐步优化组合索引。7.2 图片与静态资源的存储方案初学者最常见的做法是把图片上传到项目内部的resources/static/upload目录问题在于后端前后端分离之后图片要么走后端端口要么前端直接把请求打到后端占带宽。更麻烦的是重新部署时本地文件会被清掉。简单的改进是把图片存放目录配置到服务器独立路径如/home/app/images后端用映射方式把上传目录暴露为可访问的 URL。项目上线后我推荐把图片迁移到对象存储服务成本更低访问速度也更快。前端打包出来的dist目录本身是一堆纯静态文件放在 Nginx 下面网址就是http://服务器IP:80用户在浏览器访问的是 Nginx 服务的页面。前端代码通过fetch(/api/xxx)请求时再由 Nginx 反代到后端的localhost:8080这样浏览器访问环境里没有跨域问题比开发环境单独配跨域更顺滑。7.3 一个简单但完整的 Nginx 配置参考server { listen 80; server_name your-domain.com; root /home/app/dist; index index.html; location / { try_files $uri $uri/ /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; } location /upload/ { alias /home/app/images/; } }里面最要紧的是try_files $uri $uri/ /index.html;这一行。Vue 是单页应用路由由前端控制如果用户直接访问http://域名/order/123Nginx 在 dist 目录里找不到对应的物理文件会返回 404。加上这行后所有未知路径都会回退到index.html再由 Vue Router 接管页面展示这就解决了前端 History 模式下刷新白屏的问题。后端打包用mvn clean package -DskipTests nohup java -jar admin-web.jar --server.port8080 app.log 21 nohup保证关闭终端后进程继续跑。上线初期在app.log里实时追踪错误信息是排查线上问题最快的路径。8. 实测踩坑记录这几个坑我替你踩平了项目写过一遍真正留在记忆里的往往是那些折磨过你的细节问题。下面几条是我个人认为最容易被卡住且搜半天才找到答案的点趁现在连同解决思路一并写出来。8.1 前端传时间字符串给后端JSON 解析直接报错前端new Date()出来的时间格式传到后端Jackson 解析时会因为格式不匹配直接抛异常。解决办法是在 SpringBoot 里统一配置时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8后端的LocalDateTime配合 Jackson 的 JSR310 支持就能做到自动转换。时区必须显式设为 GMT8否则服务器在 UTC 时区跑时所有时间都和本地对不上。当时因为时区问题导致订单支付后后台显示时间差 8 小时查了一下午才发现。8.2 金额计算用 double 等于给自己埋雷金额永远不要用double或float这是老生常谈但踩的人还是前赴后继。0.1 0.2 在二进制浮点里不精确计算结果和数据库里的DECIMAL对不上订单总额对账必然出问题。后端使用BigDecimal数据库使用DECIMAL(10,2)前端展示由后端返回字符串。必要情况下统一封装金额运算工具类加一个scale(2)的方法。8.3 Vue 项目首次加载慢白屏时间长打包后的 JS 文件体积过大是控制台里常见提示Chunk-Size警告的来源。最简单的优化是路由懒加载——把组件 import 改成动态导入const Cart () import(../views/Cart.vue);这样每个路由对应的 JS 文件会被拆成独立小块首屏才加载需要的部分整体体积瞬间下降很多。再把 Nginx 开启 gzip 压缩首屏体验会有明显改善。8.4 部署后图片看不到权限、路径、大小三个问题一起查如果用户评论区的图片全挂通常是三个原因图片目录没有读权限、Nginx 映射配置错误、上传时限制了大小。按顺序排查先确认文件是否真的传到了服务器指定目录再确认ls -l查看目录权限有没有www-data或其他 web 用户的权限最后再用浏览器直接访问http://域名/upload/xxx.jpg看反馈——这种逐步缩小范围的排查方式往往比盯着配置反复试更高效。8.5 前端调用报 404先分清“后端没起”还是“路径拼错”前后端分离之后接口 404 有 80% 的情况是前端请求路径api前缀和后端 Controller 的RequestMapping对不上。排查时先打开浏览器开发者工具看 Network看清楚请求的完整 URL 是什么再去后端代码里找到对应的 controllerRequestMapping。如果两者完全一致且 Nginx 配置也没什么问题再检查是否忘了加CrossOrigin或全局跨域配置。9. 写在最后的一些真心话如果你是从零开始做这个项目大概准备三到四周时间就能完整走一遍。第一周把后端接口写好能跑第二周把前端页面联调完第三周处理库存并发、支付模拟、缓存优化这些有深度的部分第四周打磨部署和文档。整个过程里最有收益的往往不是最后跑通的那一瞬间而是那些反复排查 Bug、看日志定位问题的夜晚。我个人实际经验是在论文和答辩环节里你要重点讲清楚的不是“我用了什么框架”而是“某个核心问题我用什么方案解决了为什么这样选暴露什么问题还有什么提升空间”。订单状态机设计、乐观锁扣库存、JWT 认证流程、支付回调幂等随便挑一个讲到这种深度都已经超过八成同题目的作品。最后再分享一个收尾技巧给自己的演示录一段 5 分钟以内的视频按“前台逛店 → 下单 → 支付 → 后台发货 → 用户确认收货”这条线走一遍再配合一两处源码讲解。这套演示放到简历作品链接里比千言万语都有说服力。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。