资讯动态

Java+Vue动漫周边商城系统拆解:从SKU建模到订单库存管理

发布时间:2026/9/15 10:17:50 来源:尧图企业网站定制
最近在整理手头这套基于 Java Vue 的动漫周边商城系统从数据库脚本、核心源码到配套毕业设计文档一行一行重新过了一遍。市面上的商城项目很多但动漫周边这个垂直方向上能把商品SKU建模IP属性区分订单状态机库存扣减这些要点讲清楚的完整项目其实并不多。这篇博文就把这套系统的技术拆解、业务实现和踩坑记录全部摊开讲讲适合正在做毕设的在校生、想积累前后端分离项目经验的自学者也适合想拿现成源码二次开发的朋友参考。先说清楚这套系统是什么后端用 Spring Boot 提供接口前端用 Vue Element UI 构建页面MySQL 负责数据持久化Redis 承担登录状态和缓存整个项目做到了前后端分离。源码包含完整的商品浏览、用户注册登录、购物车、下单支付、订单管理、后台商品维护、分类管理、轮播图管理等功能数据库脚本里预置了分类、商品、用户、订单等测试数据文档部分则覆盖需求分析、数据库设计、接口说明和部署手册。接下来我按实际开发的顺序把每个环节的关键设计思路和实操细节展开讲。1. 技术选型复盘为什么偏偏是 Java Vue而不是其他方案1.1 前后端分离不是赶时髦是这套系统的硬需求很多初学者在做商城类系统时会纠结一个问题是不是用 Spring Boot 写后端再顺手用 Thymeleaf 渲染页面就够了毕竟这样项目结构简单一个应用直接跑起来。但从我做完整套系统的体验来看动漫周边商城这类带丰富交互的电商项目前后端分离是刚性需求原因有两点。先说开发体验。商城前端的业务复杂度不低首页轮播、商品瀑布流、购物车角标实时刷新、订单多状态展示这些逻辑天然适合用组件化方式组织。Vue 把页面拆成一个个组件后维护成本比在模板里堆条件判断低得多。我在写商品筛选区时用 Vue 的 computed 做价格区间和动漫作品双重筛选直接在内存里计算不需要频繁请求后端接口页面响应速度比传统 Session 同步方案明显快一截。再说部署层面。前后端分离之后Vue 打包产物是一堆纯静态文件丢到 Nginx 里就能跑后端接口独立部署在另一台服务器或端口上两者通过 API 通信。这意味着将来商城流量上来了可以单独给后端扩容、给静态资源加 CDN不用改动任何业务代码。对于毕设答辩来说这也能直接回答老师你的系统有什么优点这个问题——前后端解耦、可水平扩展。1.2 为什么不直接套若依框架而要手搭一套精简结构搜过源码的朋友应该知道现在网上大量 Java 管理后台项目都基于若依RuoYi这类快速开发平台。这类框架确实方便——代码生成器一开CRUD 后端接口和 Vue 页面直接就出来了。但我个人强烈建议商城类项目不要无脑套若依至少我整理这套动漫周边商城时选择了基于 Spring Boot 原生结构手搭原因很实际。若依这类框架自带权限管理、部门管理、代码生成等一堆模块这些对商城前台用户来说根本用不上反而拖累了项目的清晰度。商城系统的核心是用户、商品、购物车、订单这四条线把无关的后台管理模板删干净代码量反而更少被老师提问的时候也更容易解释每一行代码的作用。另一个原因是若依的前端技术栈比较重它整合了权限指令、字典管理、多标签页等设计新手进去第一周可能都在学框架本身的约定而不是在学业务。当然我也不是否定快速开发平台如果你做的是企业内部信息管理系统用若依绝对省事。但商城属于典型的电商业务面向外部用户业务流程的完整性远比后台模板重要所以手搭一个精简的 Spring Boot 结构更合适。1.3 这套源码里你能掌握的核心技能点把我整理的这套源码跑通并吃透你得到的不只是一个看起来能用的项目而是一串可以直接写到简历上的技能点Spring Boot 三层架构Controller、Service、Mapper的规范分层JWT Redis 实现的用户认证体系包含登出、Token 刷新MyBatis / MyBatis-Plus 的动态 SQL 和分页查询商品 SKU 属性建模与多条件筛选 SQL 编写购物车的 Redis 临时存储与用户登录后的购物车合并订单状态机设计与库存扣减的乐观锁方案Vue Router 路由守卫、Axios 拦截器、Element UI 组件二次封装Nginx 部署前后端分离项目数据库初始化脚本导入这些技能点组合在一起就是一个标准的企业级电商单体应用雏形应付课程设计和面试项目经验描述都绰绰有余。2. 数据库建模动漫周边商城最需要想清楚的几张表2.1 数据表总览十三张表各司其职这套商城的数据库一共设计了十三张核心表划分为用户体系、商品体系、交易体系、运营体系四组先看总览表表名归属职责说明user用户体系前台用户账号含用户名、密码BCrypt 加密存储、头像、状态cart_item用户体系购物车条目关联用户、商品 SKU、数量user_address用户体系收货地址簿支持多地址含默认地址标记category商品体系商品分类这里做了二级分类支持动漫作品维度product商品体系商品 SPU即一件周边商品的基础信息product_sku商品体系商品 SKU即具体规格库存如手办-普通版-1/7比例product_image商品体系商品图片表支持一个商品多张轮播图product_review商品体系商品评价关联订单项和用户orders交易体系订单主表存订单号、总金额、状态、地址快照order_item交易体系订单明细表记录下单时商品的快照信息payment_log交易体系支付流水表记录模拟支付或真实支付的流水banner运营体系首页轮播图配置admin_user运营体系后台管理员账号与前台用户完全隔离之所以把表和前台后台的用户拆开是因为商城业务中前台用户注册登录走普通接口后台管理员需要在后台模块中维护商品和订单两者的权限边界完全不同。合并成一张用户表再用角色区分也可以但拆开之后代码实现更简单前台用户表不用存管理员字段前后台查询都少一层判断。2.2 动漫周边商品的属性建模SPU 与 SKU 的正确打开方式商品建模是整个数据库设计中最重要的环节也是很多二手源码里做得很糊的地方。如果简单地建一张 product 表把商品名、价格、库存都塞进去看起来没什么问题实际运行起来就会很痛苦。比如一款动漫手办同一个角色可能有普通版和豪华版两个规格豪华版多配一个特典底座价格贵一百库存也独立计算。如果你只在 product 表里存库存那用户下单时到底扣哪个库存根本说不清楚。所以我在这套系统里严格按照 SPUStandard Product Unit标准产品单元和 SKUStock Keeping Unit库存量单位两层建模。product 表存的是商品共性信息像标题、封面图、所属作品、系列、分类 ID、上下架状态、销量product_sku 表存的是具体规格信息包括规格名称普通版/豪华版、价格、库存、SKU 编码。这样设计之后商品详情页展示的是 SPU 公共信息用户选择规格后前端拿到对应 SKU ID请求的库存和价格都是精确到单一规格的。动漫周边还有一个不同于普通商品的地方就是 IP 属性很强。用户搜索鬼灭之刃时期望的结果包含手办、抱枕、徽章、毛绒玩具等不同品类这些商品在分类表里可能属于不同分类。为了支持按作品维度聚合我在 product 表里单独加了 work_name作品名称和 character_name角色名称两个字段同时在 classification 表里预留了 category_code方便二次开发时扩展。查询侧则做了一个名称为按作品筛选的接口实际执行的 SQL 就是对 work_name 字段做 GROUP BY 分组聚合再按销量倒序。这是纯靠数据库三范式很难设计出来的业务字段属于典型的电商反规范化经验实际开发中很有用。2.3 订单状态机与库存扣减方案交易体系最怕的就是状态混乱。我在这套系统里把订单状态定义成了明确的整数枚举后端和前端共用同一套状态值状态值含义触发动作0待付款用户确认下单冻结库存1待发货用户完成支付后通知卖家2已发货卖家填写物流单号3已完成用户确认收货后订单终态4已取消用户主动取消释放库存5超时关闭超时未支付自动关单并释放库存这套状态机的关键在于库存的冻结与释放。我在创建订单时不会直接扣减库存数量而是把 product_sku 表里的库存拆成可用库存和冻结库存两个字段。用户下单后可用库存减一冻结库存加一用户支付成功后冻结库存真正扣减但如果用户超时未支付系统就要把冻结库存释放回可用库存。为什么不能在下单时直接扣库存因为电商场景必须有超时关单机制订单创建到支付之间有一个时间窗口如果直接扣库存用户迟迟不付款库存就一直被无效占用真正想买的人反而买不到。库存扣减的并发问题我采用了乐观锁方案。在 product_sku 表里加了一个 version 字段执行扣减 SQL 时带上版本号条件UPDATE product_sku SET stock stock - 1, version version 1 WHERE id #{skuId} AND version #{version}如果 update 影响行数为 0说明这个 SKU 已经被其他请求改过了需要重新查询库存并继续尝试。在高并发场景下这种方式比直接对整行加悲观锁的效率高因为大多数情况下并发冲突并不存在没有必要让请求排队等待。3. 后端核心链路认证、购物车、下单与库存扣减的完整实现3.1 后端工程结构从入口到 Mapper 的分层规范后端工程我按标准的 Spring Boot 结构组织包名我习惯用 com.anime.mall 作为主包下面再拆 controller、service、mapper、entity、dto、vo、config、common 这些子包。entity 对应数据库表结构dto 是接收前端参数的传输对象vo 是返回给前端的数据对象这三者分开写虽然麻烦但能防止数据库字段直接暴露在前端接口里。common 包里我重点实现了两个东西统一结果返回对象 Result 和全局异常处理器 GlobalExceptionHandler。这里我强烈建议所有后端接口都返回同一个数据结构格式类似{ code: 200, message: success, data: { } }前端 Axios 响应拦截器只需要判断 code 是否为 200是就取 data不是就弹错误提示。如果后端接口有时返回 Map、有时返回 List、有时又直接返回 String前端写接口的人会疯掉。全局异常处理器也很关键所有业务异常统一抛出 BizException由全局捕获后封装成上述结构返回而不需要每个 Controller 里都写 try-catch代码干净很多。3.2 JWT Redis 双保险的登录态方案这套系统的用户认证我一开始想过简单的 Session 方案毕竟 Tomcat 天然支持 Session实现成本最低。但 Session 方案有两个致命短板第一前后端分离之后前端页面和后端接口通常不在同一个域名下Session 的 Cookie 跨域处理很麻烦要改一堆配置第二 Session 存在服务器内存里后端将来如果横向扩展成多实例用户登录态就丢了还得引入 Session 共享。所以最终采用了 JWTJSON Web Token Redis 的方案。用户在登录接口提交用户名密码后端校验通过后用 secretKey 签发一个 token包含 userId、username、expireTime 这些信息同时把 token 以 userId 为 key 存进 Redis设置过期时间为 24 小时。前端拿到 token 后放在请求头的 Authorization 字段里每次请求后端通过拦截器解析 token拿到 userId 后放到 ThreadLocal 里供后续业务直接获取当前登录用户。这套方案有个细节很多教程不会提到JWT 本身是有无状态性的服务端如果不保存状态用户主动退出后 token 在过期前依然有效这会造成安全问题。所以我额外加了一步登出逻辑用户调用登出接口时后端把 Redis 里对应的 key 删掉。同时在拦截器里先查 Redis如果 key 不存在说明用户已退出或 token 已失效直接返回 401。相当于用 Redis 给无状态的 JWT 加了一层可控的会话状态两全其美。3.3 购物车合并与商品选择结算购物车模块在二手源码里很容易被做成只有增删改查。但真正跑业务时你会发现购物车有非常多的状态细节我在整理这套系统时重点处理了两个未登录时的临时购物车和已登录后的购物车合并。因为商城允许游客先浏览加购再跳转登录结算。游客加购的商品我存在浏览器的 localStorage 里数据结构是数组每个元素包含 skuId 和 quantity。用户登录成功后前端先请求后端购物车列表再把 localStorage 里那份临时购物车合并进去合并规则是如果后端购物车已有同一个 skuId就把数量相加并更新如果没有就新增一条。合并完成后清空 localStorage 数据。这个流程完全由后端提供合并接口保证数据一致性。购物车结算时还要处理勾选商品的问题。我设计的 cart_item 表里有一个 selected 字段前端购物车页面每个商品前有复选框用户勾选后立刻调接口更新该字段。点击结算时前端只把选中商品的 skuId 列表传给后端生成订单这样就不用临时维护一个待结算商品列表的中间状态了很省事。3.4 下单流程的事务边界与超时关单下单接口是整套系统最核心、也是事务最容易出错的地方。它的完整流程为接收购物车 skuId 列表和收货地址 ID - 遍历 SKU 查询商品信息和当前库存 - 计算总金额含运费规则- 生成订单主表和订单明细表 - 执行乐观锁扣减冻结库存 - 返回订单号。这整个过程我放在了同一个事务方法里方法上标注 Transactional任何一个环节抛出异常数据库自动回滚保证不会出现订单生成了但库存没冻结这种脏数据。订单超时关单我用了 Spring 的 Scheduled 定时任务每隔一分钟扫描一次 orders 表把超过 30 分钟未支付且状态还是待付款的订单批量标记为超时关闭同时释放对应的冻结库存。这里有一个坑如果定时任务直接更新订单状态为关闭但释放库存失败就会造成订单关了、库存少了的问题所以必须保证这两个操作在同一个事务里执行。4. Vue 前端实现从页面路由到接口对接的完整流程4.1 Vue 项目目录与路由守卫设计前端工程基于 Vue CLI 创建技术栈是 Vue 2 Vuex Vue Router Element UI Axios目录结构如下src ├── api # 接口请求模块按业务拆分 │ ├── product.js │ ├── cart.js │ ├── order.js │ └── user.js ├── assets # 静态资源 ├── components # 公共组件 │ ├── SkuSelector.vue # 商品规格选择器 │ ├── FooterNav.vue │ └── HeaderNav.vue ├── router # 路由配置 │ └── index.js ├── store # Vuex 状态管理 │ ├── modules │ │ ├── cart.js │ │ └── user.js │ └── index.js ├── utils │ └── request.js # Axios 二次封装 ├── views # 页面组件 │ ├── Home.vue │ ├── ProductDetail.vue │ ├── Cart.vue │ ├── Checkout.vue │ ├── OrderList.vue │ ├── Login.vue │ └── Search.vue └── App.vue路由守卫方面我用 Vue Router 的 beforeEach 钩子统一处理页面访问权限。判断逻辑很简单读取 Vuex 中 user 模块的 token如果是空字符串并且目标路由的 meta 中配置了 requiresAuth 为 true就跳转到登录页并带上 redirect 参数登录成功后回跳原页面。这样一来购物车、结算、订单列表这些页面都受保护前台商品浏览则完全放开。4.2 核心页面拆解首页、商品详情与购物车首页我实现的常见布局是顶部导航栏、首页轮播图 Banner、商品分类快捷入口、新品首发和热销榜单几个区域。Banner 数据从后端 banner 表动态获取商品列表则调分页接口拿商品的前几条。这里有个体验细节商品卡片下方展示的销量数据是从 product 表的 sales 字段读取的不需要实时联表聚合性能上更友好。商品详情页是我花费最多精力的页面。上半部分是商品图片轮播和基本信息下半部分是规格选择和详细图文。规格选择这一块我封装成了 SkuSelector 组件后端返回当前 SPU 下的所有 SKU 列表前端按规格维度分组展示。比如一个手办有普通版 / 豪华版两个规格组件会根据 SKU 库存量把库存为 0 的规格置灰不可选中。用户选择完规格后组件通过 Vuex 的 mutation 把选中 SKU 信息和 skuId 传到购物车加购流程。这个交互逻辑初看不复杂但真正实现时需要考虑规格名是否相同默认选中第一个有货规格切换规格后价格和库存联动刷新这些细节这套系统把这个组件单独抽出来复用后续如果扩展成颜色 尺寸 版本的多级规格也只需要改这一个组件。购物车页面的重点在于数据响应和总价计算。我用了 Vuex 管理购物车列表数据页面上每次增加数量或勾选商品都会先调后端接口再在 mutation 里更新本地数据这样就保证了 Vue 的响应式系统能实时驱动总价变化。总价的计算放在一个 getter 里遍历购物车商品只累加 selected 为 true 的条目乘法为单件价格乘以数量。这个小逻辑很简单但能把前端计算一致性这个问题讲透面试被问到购物车时就可以直接拿这个例子说明。4.3 Axios 封装与 Token 携带细节前端接口请求我统一封装在 utils/request.js 里。Axios 实例化时配置了 baseURL 和 timeoutbaseURL 我设置为 /api这样后续通过 Vite 代理转发到后端可以绕开跨域问题下面会详细讲。请求拦截器里做两件事如果 localStorage 里有 token放在请求头的 Authorization 字段如果请求方式是 FormData设置 Content-Type 为 multipart/form-data。响应拦截器里则统一处理返回结构code 为 200 直接返回 datacode 为 401 时跳转登录页并清空本地 tokencode 为其他值时用 Element UI 的 Message 组件弹出错误提示。这里有第二个细节400 系列的 HTTP 状态码和业务异常码要区分开。我的后端约定HTTP 状态码始终返回 200业务是否成功由响应体里的 code 字段决定。这样做的好处是 Axios 默认不会把非 2xx 状态当成异常处理前端逻辑更统一。如果后端遇到没有捕获的异常全局异常处理器统一封装成 code 为 500 的返回体返回前端也能拿到相对友好的错误消息。4.4 Vite 代理配置前端说跨域后端说委屈前后端联调时跨域问题十有八九会出现。我在这套系统里采用的方案是前端开发服务器代理。Vite 配置文件里做如下设置server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样操作后前端所有以 /api 开头的请求都会被 Vite 开发服务器转发到 http://localhost:8080 这个后端地址。由于浏览器的请求目标是当前域名下的 /api 路径不存在跨域问题而后端接收到的又是正常请求也不需要配置 CORS。这是开发环境最推荐的方案。不过要注意部署生产环境后Vite 代理就不生效了。这时候需要靠 Nginx 来做反向代理具体配置我会在部署章节里详细展开。如果你非要在后端配 CORS 解决跨域也不是不行但要注意处理预检请求并且在生产环境里把允许的域名收窄到你的前端域名避免任意站点都能调用你的接口。5. 三个极易翻车环节文件上传、模拟支付与参数校验5.1 图片上传本地存储还是对象存储商城系统离不开图片上传商品图、轮播图、用户头像都要传文件。网上很多教程一上来就推荐阿里云 OSS 或腾讯云 COS这当然是最专业的方案但作为课程设计和源码交付场景要求使用者先买一个云存储桶学习成本和费用成本都偏高。我在这套系统里采用的方案是本地存储。后端接收 MultipartFile 文件后先校验文件类型和大小限制在 5MB 以内只允许 jpg、png、jpeg、webp 格式然后按日期生成存储路径比如/upload/2025/06/14/uuid_originalFileName.jpg绝对路径保存到服务器本地的 upload 目录同时把相对路径写入数据库。前端拿到相对路径后拼上访问前缀就能展示。为了让图片能被外部访问我在后端写了一个简单的静态资源映射配置把 /upload/** 路径映射到本地的 upload 目录这样浏览器直接访问 http://localhost:8080/upload/2025/06/14/xxx.jpg 就能看到图片。这个方案的缺点是图片会占服务器磁盘且不适合大规模分发。但作为毕设和课程设计完全够用。如果将来要转成真实项目只需要改上传逻辑和访问前缀把文件传到 OSS返回的 URL 换成 OSS 地址即可其余业务代码不用动。5.2 模拟支付如何把支付成功这个状态可靠地写回去绝大多数毕设项目不会真的接入支付宝或微信支付因为需要企业资质和商户号。市面上常见的做法是模拟支付我也采用了这种方案。用户确认订单后进入一个模拟支付页面页面显示应支付金额和一个模拟付款按钮点击后前端调后端支付回调接口传入订单号、支付金额、支付方式。后端支付接口的关键逻辑是先查询订单状态只有状态为待付款的订单才能执行支付然后把订单状态更新为待发货同时生成一条支付流水记录到 payment_log 表。这里有一个很多人忽略的问题支付回调接口必须是幂等的。如果用户连续点击两次模拟付款或者前端网络重试导致请求发送了两遍后端不应该因为第二遍请求就把订单金额累积两次或者把状态从待发货异常改到别的状态。我的实现方式是先查订单状态如果已经是待发货直接返回重复支付提示不再更新任何数据。幂等性在真实支付场景中更重要因为支付平台本身会有回调重发机制接真实支付之前先把这个思维建立起来后面能省很多麻烦。5.3 是参数校验不是可选项——前端信任要有底线写前端页面时每个输入框我都做了校验规则像是必填、邮箱格式、手机号格式这些。但很多新手会忽略后端校验觉得前端已经限制了就不会出问题。这是一个非常危险的认知。攻击者可以直接用 Postman 绕过前端向后端接口发请求如果后端不做参数校验就可能出现用户名超长导致数据库报错密码为空也能注册成功商品数量传一个负数这类问题。我在后端统一使用 Spring Validation 注解来做参数校验。在接收参数的 DTO 类上添加 NotBlank、NotNull、Min、Max 这些注解Controller 方法参数前面加 Validated。校验不通过时框架会抛出 MethodArgumentNotValidException被全局异常处理器捕获后返回前端一条友好提示。这部分的代码量很少但却是整个系统健壮性的关键一环建议所有做商城项目的同学都要加上。6. 从源码到交付数据库脚本、打包部署与文档整理6.1 数据库初始化一份脚本把表和测试数据都搞定这套系统的交付文档里我专门整理了一份 init.sql 初始化脚本按照如下顺序执行即可创建数据库CREATE DATABASE anime_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci执行表结构创建语句按用户表、商品表、交易表的顺序建表执行测试数据插入语句这里要特别强调项目使用 MySQL 5.7 及以上版本数据库字符集必须用 utf8mb4而不是 utf8。原因很简单utf8mb4 是真正的四字节 UTF-8 编码支持存储 Emoji 表情和生僻字。如果你的商品标题里包含动漫角色名的特殊符号用 utf8 字符集很容易出现插入数据时报错或者查询时乱码。这个细节也是我实际交付时被问得最多的问题之一。测试数据我预置了大约 30 件动漫周边商品覆盖手办、抱枕、徽章、文具、毛绒玩具等常见品类并且分属 4 个不同动漫作品这样一跑起来首页立即能看到效果。用户方面预置了一个测试账号 admin / 123456方便答辩时直接演示登录和下单。6.2 前后端打包与 Nginx 部署步骤后端打包比较简单在项目根目录执行 Maven 命令 mvn clean package -DskipTests生成 target 目录下的 jar 包然后通过 java -jar anime-mall.jar 启动即可。启动时要记得外部传入数据库连接参数我一般习惯用 application-prod.yml 作为生产环境配置把数据库地址、Redis 地址、JWT secretKey 等参数从代码中解耦出来用环境变量覆盖这样换一台服务器部署时只需要改环境变量不用重新打包。前端打包同样简单在 vue 项目目录执行 npm run build生成 dist 静态目录。部署时我用 Nginx 同时托管前端静态资源和反向代理后端接口核心配置如下server { listen 80; server_name localhost; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传的图片目录 location /upload/ { alias /opt/anime-mall/upload/; } }try_files 那一行很重要它保证了 Vue Router 在 history 模式下刷新页面时不会出现 404。比如你访问 /cart 这个前端路由浏览器会向服务器请求 /cart如果没有 try_files 配置Nginx 会返回 404加上 try_files 后Nginx 会把所有未知路径都回退到 index.html由 Vue Router 接管路由。6.3 文档怎么写才能在答辩时加分源码配套的文档我分了五部分需求分析、数据库设计、接口说明、部署手册和核心业务流程图。写文档的时候有一个核心原则不要写流水账而要写为什么这样设计。比如在数据库设计这一章不要只贴建表语句而要画出 ER 图说明 SPU/SKU 为什么分两张表、订单和订单项为什么要单独拆开、订单状态为什么用整数枚举。这些设计理由才是答辩老师最想听到的东西。接口说明部分我按模块列出所有接口的请求方式、请求路径、请求参数和返回数据示例。文档里附上 Postman 的导出文件会更好老师拿到后可以一键导入 Postman直接调试每个接口比对着文档一个个复制路径方便得多。7. 实测排查过的 Bug 清单与避坑经验7.1 库存超卖问题加了一行 SQL 条件就解决测试过程中我发现一个典型的并发 Bug当多个用户同时购买同一件商品的最后一件库存时会出现超卖。因为最初的扣库存 SQL 是这样的UPDATE product_sku SET stock stock - 1 WHERE id #{skuId}在高并发下两个请求同时读到 stock 1都去执行 update库存先被扣到 0第二个请求又把库存扣成了 -1。解决方式就是我前面说的乐观锁在 update 条件里加上 stock 0 和 version 校验。这样即使两个请求同时到达也只有一个 update 能成功更新行数另一个 update 影响行数为 0在 Service 层判断影响行数后抛出库存不足异常。这个坑非常经典几乎每个商城项目都会遇到我建议你在自测时直接创造一个多线程并发下订单的场景验证修复前后效果差别。7.2 日期格式化与时区的坑数据库时间会比北京时间晚 8 小时项目部署到 Linux 服务器后发现订单创建时间 insert_time 总是比北京晚 8 个小时。排查过程也很典型先看代码里 new Date() 是否正确再查数据库时间最后定位到 MySQL 连接参数缺了 serverTimezone。在 jdbc 连接串里加上 serverTimezoneAsia/Shanghai 后时间就正常了。如果你用 MyBatis-Plus 的自动填充功能同样要在配置里设置时间区域否则自动填充的 createTime 也会跟着出错。7.3 前端报 404 和 405 的排查思路下单接口联调时前端一直报 404我以为是路劲写错了反复检查 Controller 里 RequestMapping 和前端 request 方法的 URL发现完全一致。后来才想到Vite 代理配置里后端 target 是 http://localhost:8080但如果后端实际端口是 8081因为 8080 被占了代理转发就会失败返回 404。解决方式是统一修改后端端口配置或者改 Vite proxy 的 target 端口。还有一次报 405原因是前端用 GET 请求调了一个 POST 接口后端的 PostMapping 校验很严格路径对但方法不允许就返回 405。遇到这类状态码先别急着看代码业务逻辑先用 Postman 直接调后端接口如果 Postman 正常问题就出在前端代理或请求方式如果 Postman 也报错问题才在接口本身。这个排查顺序能省大量时间。7.4 图片上传成功后前端访问 404文件上传接口返回成功数据库也存了路径但前端图片就是显示不出来控制台看到图片请求 404。这个问题的根源在于我把图片保存到了后端项目的 upload 目录但后端以 jar 包方式运行后项目内部的相对路径和打包之前是完全不同的。jar 包运行环境下的文件写入路径可能是临时目录重启就丢或者直接没有权限写入。后来我把存储路径改为配置项统一指向 /opt/anime-mall/upload 这样的外部绝对路径再配合 Nginx 的 alias 映射这个问题才彻底解决。从这里得到的经验是文件上传保存路径一定要用绝对路径配置不要依赖 jar 包内部的相对路径。7.5 中文乱码的根源永远只有一个遇到一次商品名称在数据库中显示正常但前端通过接口返回后乱码的情况。排查时发现是后端接口返回的 Content-Type 里缺少 charsetutf-8。Spring Boot 默认的响应编码在个别环境下可能是 ISO-8859-1只要在配置文件中设置server: servlet: encoding: charset: UTF-8 enabled: true force: true强制所有 HTTP 响应使用 UTF-8 编码问题立刻消失。如果是 Tomcat 直接部署 war 包还要注意数据库连接串的 characterEncoding 参数同样要显式指定为 utf8。中文乱码基本都是编码链路某一环缺了 charset 导致的顺着前端页面 - 后端响应 - 数据存储这条链路逐个排查一定能找到。走到这一步整套系统从最初的选题、数据库建模、后端业务闭环、前端交互实现到最后的打包部署和文档交付全链路就完整了。如果只是拿来当毕设参考我把核心代码框架、数据库脚本和文档结构都摆在了桌面上你可以一边对照一边改造。如果有想法把它做成一个能真实运营的小店那么可以按接入真实支付 - 把图片存储切到对象存储 - 增加后台运营统计报表这个先后顺序去迭代。过程中如果有没讲透的细节欢迎一起讨论这类系统越打磨越有意思。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价