SpringBoot+Vue3电商系统实战:从数据库设计到部署上线全解析
写这套“SpringBoot Vue3 MyBatis MySQL”前后端分离电商系统是很多 Java 学习者迈入企业级项目的一道门槛。它既不像简单的 CRUD 那样浮于表面也不像高并发大厂架构那样遥不可及刚好卡在“能跑通完整业务”和“有真实工程味道”之间的位置。这篇文章我会按照一套实际可落地的电商应用系统为线索把项目拆解、技术选型、数据库设计、鉴权流程、订单与库存处理、前后端交互乃至部署上线整个链路讲清楚。无论你是毕业设计要交差还是准备拿一个像样的项目写进简历亦或是中小团队想快速搭一版电商业务原型这套东西都有直接的参考价值。1. 项目定位与整体设计思路1.1 电商系统到底包含哪些核心业务很多人一提“电商系统”就想着把某宝某东的功能全塞进去结果项目做到一半直接烂尾。真实的电商应用系统功能边界应该是清晰的管理端管货、管单、管数据用户端看货、下单、付款。基于这个边界我把功能模块拆成两条主线和一条支撑线。用户端前台商城注册登录、商品分类浏览、商品检索、商品详情、购物车管理、订单提交、在线支付、个人中心、订单查询、收货地址管理。管理端后台管理管理员登录、商品分类维护、商品上下架与库存管理、订单处理发货、关闭、轮播图/公告配置、用户管理、销售数据统计。公共支撑统一认证鉴权、全局异常处理、统一响应封装、文件上传与静态资源映射。这个划分就是把系统做成两个独立前端项目加一个聚合后端。管理端不走商城页面用户端不接触管理接口两边共用同一套用户体系但角色权限不同。这样划分还有一个好处分工清晰哪怕你是自己一个人写也不会写着写着把代码揉成一团。1.2 为什么选前后端分离架构这套系统之所以采用前后端分离不是赶时髦而是因为这个模式在真实业务里确实更舒服。前端通过 HTTP 接口消费数据后端只负责业务逻辑与数据持久化两者通过 JSON 进行交互。部署时各自独立前端构建产物扔到 Nginx后端打成 Jar 包跑在服务器上互不阻塞。前后端分离带来的直接收益有三个。第一团队协作不必互相等前端可以基于接口文档先 Mock 数据开发后端专注业务接口第二接口可以被多个端复用管理后台和用户端都能访问同一套商品服务第三项目结构清晰每个模块的边界由接口定义后续扩展小程序端只是“再加一个前端项目”的事。但分离也有代价最典型的就是跨域和鉴权。跨域在开发环境下通过后端配置 CORS 解决生产环境用 Nginx 反向代理同域部署鉴权通过 JWT 令牌机制完成后端负责签发和校验前端负责存储和携带。这两个问题我会在后面的章节专门展开。1.3 这一套组合拳的优势在哪里SpringBoot 负责把后端的各种配置自动化处理掉内嵌 Tomcat项目启动只跑一个 Main 方法Vue3 负责前端组件化开发配合组合式 API 和路由懒加载页面再多也不怕首屏卡顿MyBatis 负责持久层灵活 SQL复杂查询不绕弯MySQL 承担数据存储稳定且成本低。这个组合说不上“多先进”但胜在生态成熟、文档齐全、踩坑的人多任何报错几乎一搜就有答案。我见过不少项目一上来就上微服务、上 Elasticsearch、上消息队列结果部署环境资源吃紧代码复杂到连作者自己都理不清。这套电商系统不需要那些重型武器的原因是单机 MySQL 扛住万级用户没有问题图片存储用本地目录或者云存储都行订单量上来了再考虑 Redis 缓存和 MQ 削峰也不迟。技术选型要匹配业务阶段这才是务实的做法。2. 核心技术栈与工程结构2.1 后端主要依赖与版本选择这套系统的核心依赖我建议以稳定为首要原则不用盲目追求最新版本。依赖项版本建议说明JDK8 或 11兼容性最好大多数生产服务器仍以这两个版本为主SpringBoot2.7.x稳定且资料多避免直接用 3.x 带来的 javax 到 jakarta 迁移问题MyBatis3.5.x配合 spring-boot-starter 使用MyBatis-Plus3.5.x个人项目强烈建议引入节省大量单表 CRUD 时间MySQL5.7 或 8.0生产环境 5.7 稳定8.0 功能更新根据服务器内存选择JWTjjwt 0.9.x轻量无状态认证方案Lombok最新稳定版减少 Getter/Setter 样板代码SpringBoot 2.7 配合 JDK 8 是我认为最稳的搭配。很多旧教程里的 Maven 仓库依赖、配置文件写法都能直接复用遇到问题也好排查。数据库连接池用默认的 HikariCP 就行不需要额外引入 Druid 除非你特别需要监控面板。MyBatis-Plus 在这个项目里属于“用了就回不去”的工具。它提供的 BaseMapper 让单表 CRUD 无需写一行 SQL分页插件、条件构造器、代码生成器又大幅提升了开发速度。但要注意一点复杂多表关联查询仍然需要手写 XML不能偷懒全部依赖 MP否则迟早被多表 JOIN 教育。2.2 前端依赖与构建工具前端基于 Vue3 生态核心依赖需要明确。构建工具选择 Vite开发时冷启动速度极快改动保存到浏览器刷新几乎感知不到延迟。Vue Router 负责路由Pinia 取代 Vuex 成为状态管理方案Axios 统一处理 HTTP 请求Element Plus 提供后台管理界面组件移动端适配则可以结合自己的需求引入 Vant。这里要说一个实际体会前端包体积膨胀很快Element Plus 如果全量引入会让首屏加载变慢。建议按需引入组件并用 Vite 的代码分包策略让路由级组件懒加载。商城首页这种需要快速打开的场景首屏资源越小越好。管理端和用户端是独立的 Vue 项目还是集成在一个项目里两种方案都可行。我的做法是直接建两个独立工程shop-admin和shop-front后端接口前缀也已经区分了/api/admin/**和/api/user/**。这样做防止前后端权限逻辑耦合也给团队成员分配开发任务时更自然。2.3 后端工程结构参考不搞复杂的微服务拆分就用经典的单体分层架构但包结构要清晰包名要体现职责。com.example.mall ├── common # 通用类统一响应、异常处理、工具类 │ ├── result │ ├── exception │ └── utils ├── config # 配置类跨域、MyBatis-Plus、WebMvc、JWT拦截器 ├── controller # 控制层请求入口 │ ├── admin │ └── user ├── service # 业务层核心逻辑 │ ├── impl ├── mapper # 数据访问层接口 ├── entity # 数据库实体 ├── dto # 数据传输对象接收前端参数 ├── vo # 视图对象返回给前端的数据 └── interceptor # 拦截器JWT 认证Controller 层只做参数接收、调用 Service、返回统一结果所有业务判断下沉到 Service。Mapper 层只负责数据操作。DTO 和 VO 分开是很多新手容易忽略的点直接把 Entity 返回给前端会泄露密码、状态字段等内部信息还容易导致接口数据结构和数据库表改造成联动灾难。2.4 前端工程结构参考用户端和管理端都按 Vue3 的标准目录组织核心差异在页面和路由守卫。src/ ├── api/ # 接口封装按业务模块拆分文件 ├── assets/ ├── components/ # 公共组件 ├── router/ # 路由配置 前置守卫 ├── store/ # Pinia 状态存储 ├── views/ # 页面组件 │ ├── home/ │ ├── product/ │ ├── cart/ │ ├── order/ │ └── user/ ├── utils/ # 请求封装、token 管理 ├── App.vue └── main.jsapi/目录按照后端接口模块一一对应比如product.js里打包商品的列表、详情、搜索接口。utils/request.js封装 Axios 实例统一设置 BaseURL、请求头携带 Token、统一处理响应码和超时提示。路由守卫检查本地是否有 Token未登录跳转到登录页。这些约定一旦定好新增页面只需要照着既有模式复制粘贴改一改开发效率提升非常明显。3. 数据库设计与核心表结构3.1 核心表清单与关系梳理电商系统数据库设计是整个项目的根基表设计合理后面写业务逻辑就是顺畅的表设计稀烂查一个订单可能要把五六个表 JOIN 起来性能惨不忍睹。我在这套系统里设计了这些核心表表名用途核心字段user用户表id、username、password、nickname、avatar、phone、statusadmin_user管理员表id、username、password、rolecategory商品分类表id、name、parent_id、sort、iconproduct商品表id、category_id、name、sub_title、main_image、detail、price、stock、sales、statuscart_item购物车表id、user_id、product_id、quantity、checkedorder订单表id、order_no、user_id、total_amount、status、address_snapshot、create_time、pay_timeorder_item订单项表id、order_id、product_id、product_name、product_image、price、quantity、total_priceshipping_address收货地址表id、user_id、receiver_name、receiver_phone、province、city、detail_addr、is_defaultpayment_record支付流水表id、order_no、pay_no、pay_amount、pay_status、pay_timebanner轮播图表id、image_url、link_url、sort、status订单表和订单项表必须分成两张表。订单表负责记录订单的整体信息比如总金额、状态、支付时间订单项表记录具体买了哪些商品、什么价格、多少个。为什么不能合并成一张表假如一个订单买了三种商品合并的话就要拆成三条记录每一条都要重复存储用户 id、订单号、总金额、地址快照数据冗余严重统计订单数还会被重复计算。3.2 为什么订单表要冗余一份商品快照一个很容易被新手忽略的设计细节是地址快照和商品快照。地址快照在order表里用一个字段存下单时的完整地址字符串商品快照在order_item表里冗余了product_name、product_image、price这些字段。为什么要这么做因为商品会改价格会调地址会换。用户在 6 月 1 日下单订单金额 199 元结果 6 月 2 日商家把价格改成了 149 元总不能要求历史订单的价格跟着变吧所以订单一旦生成就把当时的商品名称、图片、价格、收货人信息全部冗余固化到订单相关表里。查询历史订单时读到的是快照数据而不是去联查当时可能已经下架的商品表。这是电商系统设计的基本共识。3.3 关键索引与字段约束建议表结构设计不仅仅是字段对索引也是性能的生命线。订单表最常见的查询条件是按用户查看订单列表所以(user_id, create_time)组合索引是必须的订单号order_no要建立唯一索引它既是业务标识也是支付回调定位订单的关键。商品表按分类浏览是高频场景category_id建普通索引商品列表按销量排序是高频需求(sales, status)组合索引保证排序查询不慢。库存字段stock定义成 int 而不是 bigint因为电商单品的库存量撑死几万件。价格字段用 decimal(10,2) 而不是 float/double这一点极其重要。浮点数在 MySQL 中计算会出现精度丢失比如 0.1 0.2 不等于 0.3涉及金额的字段用浮点类型等于给财务埋雷。逻辑删除的设计也值得说一下。商品、分类这类数据我建议加deleted字段用逻辑删除而不是物理删除。理由是商品删除后用户的历史订单还引用了商品名称和图片快照虽然订单表里已经有冗余快照但如果运营误删商品导致管理后台查询不到关联信息排查起来非常麻烦。MyBatis-Plus 提供了TableLogic注解配置之后调用删除接口实际执行的是更新deleted字段查询自动过滤完全无感知。4. 后端核心模块实现与重难点拆解4.1 统一响应结构与全局异常处理前后端交互的第一件事是定通信协议。如果每个接口返回结构不统一前端 Axios 拦截器就没法统一处理。我这里直接给出一套标准结构{ code: 200, message: 操作成功, data: { ... } }code是业务状态码200代表成功401代表未登录或 Token 失效500代表服务器内部错误业务参数错误可以自定义如1001。message是人可读的提示信息data是真正的业务数据。后端写一个Result类提供静态方法Result.ok(data)和Result.fail(code, message)所有 Controller 返回类型都是Result。然后写一个RestControllerAdvice全局异常处理器捕获业务异常、参数校验异常、兜底异常统一封装为Result返回。这样做的好处是前端拿到非 200 码直接弹出错误提示不用每个接口单独处理异常状态。4.2 JWT 认证与登录鉴权流程用户和管理员共用一套 JWT 机制。用户登录成功后后端生成 Token 返回给前端前端存在本地存储后续每个请求在 Header 里携带Authorization: Bearer token。后端通过拦截器解析 Token确认用户身份后放行。为什么不用 Session因为前后端分离后前端可能部署在 A 域名后端部署在 B 域名Session 依赖 Cookie跨域场景下 Cookie 的管理变得很麻烦。JWT 是自包含的服务器不需要保存会话状态天然适合接口鉴权。JWT 令牌的典型载荷包含用户 id、用户名、角色、过期时间。登录接口签发 Token 时我会把用户 id 放进 claims后续获取当前登录用户时从 Token 解析出 id再去数据库查最新用户信息。注意不要在 Token 里塞密码、手机号等敏感信息因为 JWT 的 payload 只是 Base64 编码不是加密的任何人都能解开看。拦截器实现上需要区分哪些接口需要登录、哪些接口放行。用 SpringBoot 拦截器注册时可以直接指定排除路径比如用户端的/api/user/login/、商品列表/api/user/product/**放行管理端的/api/admin/login/放行其余的/api/admin/**必须校验管理员 Token。代码层面关键点在拦截器里public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { return reject(response, 未登录); } String token authHeader.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { return reject(response, 登录状态已失效); } }4.3 商品管理端的实现要点管理端商品管理是典型的 CRUD 页面但有几个细节值得注意。商品新增和编辑走同一个页面后端用一个ProductSaveDTO接收参数通过 id 是否为空判断是新增还是更新。商品主图上传走文件上传接口文件保存到服务器本地目录然后返回可访问的 URL。这里要配置虚拟路径映射否则前端拿到/upload/xx.jpg根本访问不到文件。SpringBoot 里用 WebMvcConfigurer 的addResourceHandlers把磁盘目录映射为/upload/**访问路径。商品列表页要注意分页和关键字搜索。搜索条件是商品名称模糊查询和分类筛选分页用 MyBatis-Plus 的分页插件。这里我建议列表接口返回的数据封装成ProductAdminVO只包含管理端需要的字段比如 id、名称、价格、库存、销量、上下架状态。详情接口再返回完整信息避免列表页一次性查询大字段比如商品详情的富文本大段文字拖垮前端渲染。上下架操作是一个独立接口只修改status字段。库存调整也是独立接口方便运营在后台手动修正库存。把状态变更从编辑接口分离出来是为了前端操作更简洁后台业务日志也好记录。4.4 商品分类与树形结构处理商品分类是树形结构比如“手机数码”下面有“手机通讯”“摄影摄像”再往下可以继续细分。数据库表通过parent_id字段实现父子关系顶层分类的parent_id为 0。前端展示分类时通常需要一次性拿到完整的分类树。后端的做法是从数据库查出全部有效分类按parent_id组装成树形结构返回。代码不难核心是递归或者两层循环配合 Map 组装但要注意两个细节一是排好序同类目下兄弟节点的顺序根据sort字段决定二是只组装状态正常的分类下架的分类连叶子都不要出现否则前端会渲染出空白节点。4.5 用户端商品列表、详情与搜索用户端商品列表接口要考虑三个参数分类 id、关键字、分页。分类 id 存在时需要查询该分类及其所有子分类下的商品。SQL 上可以使用子查询先把子分类 id 集合求出来SELECT * FROM product WHERE category_id IN ( SELECT id FROM category WHERE id #{categoryId} OR parent_id #{categoryId} ) AND status 1 ORDER BY sales DESC商品详情页除了基础信息还需要展示轮播图。轮播图如果直接存在商品表的一个字段里用逗号分隔多个图片地址后端返回时再拆分处理简单直接。商品详情接口还需要增加浏览量的自增逻辑但这里要注意刷量问题可以不做过多防御给一个简单views views 1的 SQL 更新即可。商品搜索建议直接用数据库的LIKE模糊查询电商项目数据量在百万以内完全够用。更复杂的搜索需求可以留到项目后期引入搜索服务但当前阶段不要过度设计。4.6 购物车模块的实现方案购物车有两种设计一种是基于 Redis 存储性能好但实现复杂一种是存 MySQL 表简单可靠。这套系统我采用 MySQL 表方案。购物车表cart_item记录用户、商品、数量和选中状态。加购接口的逻辑是先查用户是否已经对该商品加过购物车如果存在则数量累加否则新增一条记录。前端页面展示购物车时后端接口要联查商品表返回商品的实时价格、封面图、库存和上下架状态。这样设计的好处是购物车列表直接展示商品最新信息不需要在购物车表里冗余商品价格价格以商品表为准。购物车还有一个批量删除和修改数量的接口。修改数量时后端要校验不能超过库存上限并且数量最小为 1最大可以设一个限制比如 99防止客户端异常数据。购物车选中状态用checked字段标识下单时后端根据选中的购物车记录生成订单。注意这里有个坑前端把选中的购物车 id 列表传给后端后端要根据这些 id 查出记录再校验归属权。不能只传商品 id 就下单否则用户可以在前端伪造请求用别人的购物车记录给自己下单虽然实际场景下别人不买账但归属校验是基本安全要求。4.7 下单流程与订单状态机下单是整个系统最核心的流程涉及事务和状态流转不能有一点马虎。我的下单流程是这样设计的用户传入选中的购物车记录 id 列表和收货地址 id。后端校验购物车记录归属当前用户查出所有商品信息。遍历校验商品状态是否上架、库存是否足够。校验库存时要查实时库存而不是前端传入的库存。订单金额计算单价乘以数量累加这是服务端权威计算不能信任前端的任何金额数据。生成订单号和订单项扣减库存清空选中购物车记录这一步要走数据库事务。返回订单号前端跳转到支付页面。订单状态我定义了五个待支付、已支付/待发货、已发货、已完成、已取消。状态流转不能让用户任意跳后端要校验状态的前置条件。比如只有待支付订单能取消只有已支付订单能发货发货后用户才能确认收货。这些规则在 Service 层通过枚举或常量判断即可。数据库事务用Transactional标注下单方法。一个典型的事务问题库存扣减失败时前面插入的订单数据必须回滚。试想一下订单已经生成了但库存没扣成功用户付款后实际没货可发这绝对属于严重 bug。4.8 库存扣减与超卖问题电商项目面试必问题如何避免库存超卖。数据库层面最简单的方案是扣库存 SQL 带上库存条件UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}返回受影响行数为 0说明库存不足业务层抛异常回滚事务。这种方式利用数据库行锁保证了原子性单机 MySQL 下完全够用。为什么不先查库存再 update因为并发场景下两个请求同时读到库存是 5 件各扣 5 件最后库存变成 0 而不是负数看起来没问题但如果库存是 3 件呢两个请求都读到 3 件都认为有货各扣 3 件最终库存就是 -3这就是超卖。带条件的原子更新从原理上杜绝了这个问题。4.9 支付模块与回调处理支付模块真实的落地方式一般对接第三方支付平台的网关接口。前端拿到后端生成的支付参数后弹起支付支付平台完成扣款后通过异步通知接口回调后端后端根据支付结果更新订单状态。如果只是学习和演示可以做一个模拟支付的接口用户点击确认支付后端直接扣订单金额并更新订单状态为已支付。演示场景不需要真实资金流动但支付回调的“处理顺序”要理解清楚支付回调更新订单状态时必须校验支付的订单号是否存在、金额是否匹配、订单是否处于待支付状态然后更新支付时间和订单状态记录支付流水。这三个动作放在同一个事务里防止回调重复到达导致重复更新。订单支付成功后的处理逻辑中还要注意一件事支付回调更新状态是幂等操作。也就是说同一条支付通知来了两次甚至三次后端的处理结果是一样的不能出现支付成功后又把订单改成待支付这种活见鬼的状态。5. 前端核心交互与接口对接5.1 请求封装与拦截器设计前端所有请求通过 Axios 发出封装一个request.js是标准操作。实例创建时设置基础路径和超时时间请求拦截器统一加 Token响应拦截器统一处理业务码。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录状态已过期请重新登录) } ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )这里的核心思想是成功的业务请求直接返回data部分业务失败统一弹提示并 reject。页面里调用接口就不需要再写一堆 if 判断错误分支。5.2 路由与页面权限控制路由配置要区分静态路由和动态路由。登录页、注册页、商品列表页、详情页等公开页面直接静态配置个人中心、订单管理等需要登录的页面用路由守卫控制。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })管理端路由额外加一层角色判断本地存储里存一个用户角色字段不是管理员角色直接跳转到 403 页面。路由懒加载写法component: () import(/views/admin/ProductManage.vue)切到哪个页面才加载哪个页面的代码。5.3 商品列表页的筛选交互与状态管理商品列表页一般需要支持分类导航、关键字搜索、排序切换和分页加载。用户点击左边分类、顶部搜索框输入、切换“最新/销量/价格”排序时都会重新请求列表接口。为了不在多个组件之间传参传得头大用 Pinia 定义一个productQueryStore保存当前查询条件列表组件和分类组件都从 store 里读取改动时统一触发列表刷新。商品列表数据结构定好了前端页面不会乱{ list: [ { id: 1, name: 无线蓝牙耳机, mainImage: http://..., price: 199.00, sales: 1200, stock: 88 } ], total: 45, pageNum: 1, pageSize: 10 }价格格式化在前端做用toFixed(2)保证显示两位小数。商品卡片组件只负责展示不负责拉数据这样列表页可以继续用同一套组件复用。5.4 购物车与订单结算页的状态流转购物车页面勾选商品选中状态存储在购物车表里也可以通过本地状态暂存下单时把选中的购物车 id 传给后端。结算页会显示商品清单、总金额、收货地址。进入结算页时前端最好先调用一个接口获取购物车提交预览数据包含商品列表和服务端计算的总金额。拿到后端数据渲染总金额永远以服务端返回为准。提交订单之后跳转收银台页面。如果当前系统对接了真实支付就展示支付二维码或拉起支付插件模拟支付就直接调支付模拟接口支付成功后跳转订单详情页。订单列表页要支持按状态筛选标签全部、待支付、待收货、已完成后端接口接受status参数自动过滤。5.5 管理端数据统计页面的实现管理端通常需要一块数据看板展示今日订单数、今日销售额、商品总数、总用户数。用 SQL 查询即可订单数和销售额用COUNT和SUM查当天订单商品和用户数直接COUNT(*)。如果还需要近七天的销售趋势写一条 group by 日期的查询返回日期和金额列表前端用图表库折线图展示。这类统计接口不追求毫秒级性能数据量小时查询速度可以忽略。但要注意 SQL 的日期过滤写法别不小心把昨天数据也算进来。用DATE_FORMAT(create_time, %Y-%m-%d) CURDATE()直接按当天过滤清楚可靠。6. 前后端联调中的接口规范与关键细节6.1 接口文档先行联调事半功倍前后端分离项目最大的沟通成本在于接口约定谁先动工谁吃亏。在实际开发流程里我强烈建议先定接口文档哪怕只是用 Markdown 或在线接口文档工具记录下来约定好路径、请求参数、返回结构再分头开发。接口路径命名要有规律。管理端走/api/admin/product/list用户端走/api/user/product/list模块名放在中间动词明确表示动作。分页参数统一叫pageNum和pageSize返回格式统一携带total字段。命名一旦随意前端对接的时候每个接口都要猜效率极低。6.2 参数校验不能只靠前端前端做表单校验只是用户体验层面的保障后端必须独立做参数校验。登录接口校验用户名密码非空注册接口校验手机号格式下单接口校验地址 id 存在的。SpringBoot 可以用Validated加注解校验 DTO 字段也可以直接手写判断抛业务异常。个人习惯手写判断多一些因为能灵活控制错误提示文案但项目里为了统一也可以用校验注解。后端不校验参数会带来严重后果用户传一个负数作为购买数量后端不加判断直接扣库存实际是给库存加回去订单金额也可能变成负数。虽然正常用户不会这么干但接口一旦暴露谁都能构造任意请求。安全的第一道防线就是后端参数校验。6.3 时间、金额、枚举值的传输约定数据库 datetime 类型返回给前端Jackson 默认序列化成本地时间格式2025-01-01 12:00:00字符串是可以直接显示的。建议在配置里统一全局时间格式避免前端拿到时间戳还要自己做转换。金额类型前端展示用字符串或数字均可但要保证不丢失精度。如果后端返回 decimal 类型Jackson 默认可能转成数字前端运算时可能出现精度问题。项目里把金额字段在 VO 中直接定义为 String 类型返回显示就走字符串运算时后端完成前端只负责展示从根上规避精度问题。用户角色的枚举值不要直接返回英文或数字就完事。比如订单状态0 1 2 3 4前端拿到 0 也不知道是哪个状态。后端返回订单状态码时同时返回一个状态描述文字前端直接展示描述排序筛选用状态码。7. 部署上线与环境搭建7.1 后端打包与配置分离后端打包执行mvn clean package -DskipTests生成可执行 Jar。生产环境配置通过application-prod.yml读取环境变量里的数据库口令、文件上传路径不能把数据库密码硬编码传到代码仓库里。可以用 SpringBoot 的${MYSQL_PASSWORD}占位符部署时在系统环境变量里配置。Jar 包放服务器上后用nohup java -jar mall.jar --spring.profiles.activeprod mall.log 21 启动。如果需要开机自启写一个 systemd 服务文件比直接扔rc.local规范得多。7.2 前端构建与 Nginx 部署前端两个项目分别执行npm run build生成 dist 目录。Nginx 配置两个 server 块分别代理用户端和管理端同时将/api/路径反向代理到后端服务。Nginx 部署的核心配置片段server { listen 80; server_name shop.example.com; location / { root /var/www/shop-front/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files配置必须写否则 Vue Router 使用 history 模式时用户刷新/product/1这种页面会 404。此配置让刷新请求回退到 index.html由前端路由接管。管理端单独配置一个 server 块监听不同端口或不同域名两者都指向同一个后端/api/跨域问题在生产环境因为同源部署而彻底消失。图片上传后生成的 URL 需要是浏览器能直接访问的路径如果图片存在后端服务器本地需要给 Nginx 也配一个/upload/的静态资源代理。7.3 MySQL 初始化与数据导入数据库初始化用项目里的db/mall.sql文件在服务器上执行source mall.sql完成建库建表。生产数据库账号要单独创建不给 root 权限账号只需要该库的增删改查权限即可。MySQL 5.7 默认是区分表名大小写的Linux 下如果建表用的Product查询写product会报表不存在所以统一用小写表名避免环境差异踩坑。7.4 服务器部署清单速查服务器环节我总结一份自查清单每一条都是真实踩过的坑后端端口确认未被占用防火墙放行。MySQL 允许远程连接时注意不直接用 root单独建账号并授权库级权限。图片上传目录要存在并且应用有写权限用chmod授权不该默认给 7770755 配合目录属主管理更安全。前端request.js的 baseURL 在构建时要用相对路径/api不要写局域网 IP部署环境变化时不用重新构建。Vite 构建产物如果出现资源 404检查base配置是否设置为/。修改前端代码后重新构建部署浏览器缓存不更新时Nginx 配置添加Cache-Control: no-store或者给静态资源加 hash 文件名。8. 常见问题与排查技巧实录8.1 接口 404 排查路径后端接口前端访问 404排查顺序是先确认后端服务启动成功再确认 Nginx 代理地址配置正确然后查看后端控制台有没有拦截器把请求拦掉最后用 Postman 直接请求后端接口测试。多数 404 的原因是前端请求路径和后端类名上的RequestMapping拼写不一致或者拦截器注册时把接口路径排除错了一批。优先级最高的定位手段是 Postman 直连后端接口能通说明后端没问题问题在前端地址或者代理。8.2 JWT 过期后前端没有跳转登录Axios 响应拦截器里判断了code 401但实际运行时发现过期后页面没有跳转。原因多半是后端返回的 HTTP 状态码不是 200而是在拦截器里直接设置了 401 的 HTTP 状态码。Ajax 进入的是响应拦截器的 error 分支而不是成功分支后端返回 JSON 但前端 error 分支里没有处理业务码。解决方案统一策略让后端鉴权失败时返回 HTTP 200 但业务码为 401前端在成功分支统一处理或者前端在 error 分支里读取返回体判断 code。采用前一种方案最简单后端返回结构保持一致性前端只用一个拦截器逻辑。8.3 事务不生效的常见原因下单方法加了Transactional但库存扣减异常时订单数据没有回滚。排查点主要有三个方法是否被同类内部调用Spring 事务代理基于 AOP同类内部this调用不会经过代理事务失效方法是否是 private事务方法是否在 Controller 层直接加注解而没有经过 Service。项目中统一规则就是事务只加载 Service 实现类的 public 方法上外部调用只走接口排查成本立刻降低。8.4 库存扣减为负数的场景复现测试环境出现过即使写了stock #{quantity}的更新条件库存还是变成负数的情况。复盘后发现问题不在 SQL而在业务代码执行顺序不对代码先执行了商品库存查询判断又执行了库存更新但两者之间没有事务包裹。两个并发请求都通过第一步查询判断库存足够再执行更新时条件无法阻止库存扣成负数吗其实可以阻止但前提是 SQL 更新直接条件拦截。如果更新语句写成先查出当前库存再在 Java 里计算出新值再更新就会发生并发覆盖。根本解决办法就是那个带条件的原子更新 SQL别把计算逻辑拆到 Java 层。8.5 前端打包后页面白屏npm run build构建成功Nginx 打开首页一片空白。控制台报 JavaScript 资源 404原因基本可以锁定为打包后静态资源路径不对Vite 构建的默认base是/如果项目部署在子路径下就要配置成子路径。多数场景部署在根路径把 Nginx 的 server 块location /指向 dist 目录即可解决。还有第二种可能前端代码里某个直接引用的动态图片路径在打包后被解析成了 undefined这种通常是代码问题不是配置问题重点查图片引用路径以/开头还是相对路径。8.6 常见问题速查表问题现象可能原因处理建议登录后请求接口 401Token 没过期但拦截器解析失败检查 JWT 生成的密文与解析密钥是否一致商品列表能打开详情 404详情接口地址与网关代理规则冲突核对 Vue 路由路径和后端接口地址排除/结尾差异下单成功但库存没变化事务未提交或执行了缓存库存检查 Service 方法是否有Transactional库存更新是否作用于正确商品图片上传成功但页面打不开静态资源映射未配置或 Nginx 未代理上传目录检查本地访问http://ip:8080/upload/xx.jpg是否返回文件管理端接口对外开放拦截器放行路径配置过宽确认/api/admin/**全部走认证拦截不能只依赖前端路由隐藏8.7 一个排查数据库连接池异常的经验上线初期遇到过数据库偶尔连接超时后台日志出现Connection is not available, request timed out。图省事的办法是把连接池最大连接数调大但根源是代码里有地方忘记释放连接。HikariCP 默认自动回收确实不会彻底让连接泄漏但高频调用下最终撑满连接池。后来逐个检查 DAO 操作发现某处 XML 里写的是select * from product where id #{id}这个不算泄漏但真正的问题出在有人把大文件上传写进了业务事务里上传耗时十几秒占用了事务连接。解决办法是事务方法保持轻量文件上传放到 Controller 层先完成再进入事务调用连接池配置连接最大生命周期定期回收陈旧连接。9. 项目扩展方向与后续演进建议这套系统跑通之后往下的扩展路线可以踩很多真实的业务痛点。我个人体会是与其把初始版本堆得无比复杂不如先把闭环打通、把质量做稳再根据真实使用反馈逐层扩展。第一个建议方向是引入 Redis 承担缓存。商品详情页和热门商品列表是典型的读多写少场景首次查询后缓存到 Redis设置合理的过期时间能够显著减轻数据库压力。购物车数据如果想做到跨设备同步也可以迁移到 Redis 结构化存储中。此时要注意缓存一致性更新商品时先更新数据库再删除缓存下次查询再回填坚持后台线程或定时任务做补偿。第二个方向是引入消息队列处理订单状态变更。当高并发下单场景出现时订单创建、支付回调、短信通知、发货通知这些流程可以通过消息队列异步化。支付回调先写消息订单服务异步消费修改状态库存扣减也可以在 Redis 中先预扣再异步落库这样单体应用变成“可演进”的架构。第三个方向是引入定时任务处理超时订单。订单生成但用户没有支付常规做法是定时扫描超过 30 分钟未支付的订单自动取消并回补库存。Spring 的Scheduled就可以实现但要注意任务执行加锁集群部署时多台机器同时跑定时任务会导致重复回补真实做法是用 Redis 分布式锁保证同一时刻只有一台机器执行。第四个方向是权限框架替换。管理端的角色权限目前靠拦截器和注解简单控制如果需要更细粒度的菜单权限、数据权限可以引入成熟的权限框架。这样管理员只能看到自己有权限的菜单业务数据的范围也能通过数据权限配置约束。10. 个人实操体会总结从零到一把这套电商应用系统完整写下来我最大的感受是真正难的不是写代码而是做决策。技术选型克制、模块边界清晰、接口规范统一、数据库设计严谨这些看似“软”的功夫决定项目的下限。代码写得多快多花哨只能决定上限。有几个细节我建议所有做同类项目的朋友一定不要省。第一个是全局异常处理没有它前端收到的错误信息五花八门联调效率低到让人想砸键盘。第二个是统一响应结构前端拦截器只要写一次就能应对所有接口后期加接口零成本。第三个是订单金额与库存操作的服务端权威计算绝不信任前端传参。这三个点看着基础但在实际业务中每次出问题都和它们脱不了干系。如果还有余力建议把系统的日志做得再完善一些。操作日志记录谁在什么时间修改了什么商品、调整了多少库存业务表里加create_time和update_time字段是底线。排查线上问题没日志就像黑灯瞎火走夜路摔跟头都找不到原因。最后说一句可能会被当成“保守”的话参考别人的优秀开源项目没问题但每个模块一定要自己动手敲一遍。只有自己踩过购物车归属校验的坑、调过跨域头的错、体会过事务回滚的救场时刻这套技术栈才真正变成你自己的东西。拿一份源码改改名字交差看起来省了时间亏掉的是对系统全貌的把控能力。这套电商系统最大的价值不在源码本身而在它逼你把前端、后端、数据库、部署整个链路串起来的完整过程。把这套链路吃透了以后不管换成什么业务骨架都是通的。