最近在整理手头这套基于 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 表里存库存,那用户下单时到底扣哪个库存?根本说不清楚。
所以我在这套系统里严格按照 SPU(Standard Product Unit,标准产品单元)和 SKU(Stock 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 共享。
所以最终采用了 JWT(JSON 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 和 timeout,baseURL 我设置为 /api,这样后续通过 Vite 代理转发到后端,可以绕开跨域问题(下面会详细讲)。请求拦截器里做两件事:如果 localStorage 里有 token,放在请求头的 Authorization 字段;如果请求方式是 FormData,设置 Content-Type 为 multipart/form-data。响应拦截器里则统一处理返回结构,code 为 200 直接返回 data,code 为 401 时跳转登录页并清空本地 token,code 为其他值时用 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 连接串里加上 serverTimezone=Asia/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 里缺少 charset=utf-8。Spring Boot 默认的响应编码在个别环境下可能是 ISO-8859-1,只要在配置文件中设置:
server: servlet: encoding: charset: UTF-8 enabled: true force: true强制所有 HTTP 响应使用 UTF-8 编码,问题立刻消失。如果是 Tomcat 直接部署 war 包,还要注意数据库连接串的 characterEncoding 参数,同样要显式指定为 utf8。中文乱码基本都是编码链路某一环缺了 charset 导致的,顺着"前端页面 -> 后端响应 -> 数据存储"这条链路逐个排查,一定能找到。
走到这一步,整套系统从最初的选题、数据库建模、后端业务闭环、前端交互实现,到最后的打包部署和文档交付,全链路就完整了。如果只是拿来当毕设参考,我把核心代码框架、数据库脚本和文档结构都摆在了桌面上,你可以一边对照一边改造。如果有想法把它做成一个能真实运营的小店,那么可以按"接入真实支付 -> 把图片存储切到对象存储 -> 增加后台运营统计报表"这个先后顺序去迭代。过程中如果有没讲透的细节,欢迎一起讨论,这类系统越打磨越有意思。