
最近一直在折腾一个网购平台的项目从数据库设计到前后端联调踩了不少坑也积累了不少经验。这套系统用的是 Java SpringBoot Vue3 MyBatis典型的前后端分离架构数据库选的 MySQL覆盖了用户、商品、购物车、订单、支付这些电商核心链路。今天就把整个项目的源码结构和核心实现思路整理一遍给正在做类似课设、毕设或者刚入行想了解电商系统怎么搭的同学一个参考。先说一下这套系统能干什么。用户端可以注册登录、浏览商品分类、搜索商品、加购物车、提交订单、模拟支付、查看订单状态后台管理端可以做商品上下架、库存管理、订单发货、分类维护、轮播图设置这些操作。说白了就是一套能跑起来的完整电商闭环不是那种只有 CRUD 的 demo而是每个环节都有业务逻辑兜底能真实感受到前后端分离项目在工程层面是怎么组织的。如果你是第一次接触这类项目建议按我的思路先整体过一遍架构再看数据库设计然后顺着后端接口走到前端页面最后处理部署和排查问题。这样比直接盲扒源码要高效得多。1. 项目整体思路与架构拆解1.1 为什么坚持做前后端分离我见过很多同学做电商项目时习惯用模板引擎后端一个 SpringBoot 应用把页面和控制层全包了。这种方案在简单场景下确实省事但一旦涉及移动端适配、多端复用、前后端并行开发就会很痛苦。这套网购平台我一开始就定了前后端分离的路子后端只提供 RESTful API前端用 Vue3 独立构建两边通过 JSON 数据交互。拆开之后的好处很明显。第一个是后端接口可以单独测试用接口测试工具直接验证业务逻辑不用每次改完代码都要启动一个浏览器去点页面。第二个是前端团队和后端团队可以并行推进后端写完用户模块的接口前端同步开发登录页面互不阻塞。第三个是将来如果要做小程序或者移动端 App后端 API 可以直接复用不需要重写。当然分离也带来了一些额外成本比如跨域处理、接口文档维护、前后端联调时字段对齐问题。这些在项目里我都做了对应处理跨域通过配置拦截器统一解决接口返回统一封装成 Result 对象前端有独立的 request 工具类处理响应。这些细节等会儿会展开讲。1.2 技术栈选型背后的逻辑SpringBoot MyBatis MySQL 这套组合在中小型项目里非常成熟。SpringBoot 的核心价值在于自动配置省掉了一大堆 XML 配置一个启动类就能把应用跑起来这对快速搭建原型很有帮助。MyBatis 虽然要自己写 SQL但电商系统里经常会出现复杂的多表关联查询、动态条件拼接用 MyBatis 的where、if标签反而比 JPA 那种全自动 ORM 更可控。性能上手写 SQL 对索引的利用也更直接查出什么问题一眼就能看到完整语句。前端选 Vue3 而不是 Vue2主要是看中了组合式 API 带来的逻辑复用能力。购物车、订单这类业务代码量都不小用 setup 语法可以把相关的响应式状态和方法聚在一起比 Options API 那种把所有逻辑拆散在 data、methods、watch 里的方式清晰得多。配合 Pinia 做全局状态管理多组件之间的数据同步问题也顺手解决了。再就是 MySQL。电商系统的核心是事务下单这个操作必须保证扣库存、生成订单、更新状态这三个动作要么全成功要么全回滚。这个项目在订单生成方法上加了Transactional注解底层就是靠 MySQL 的 InnoDB 引擎支持事务。选型的时候没考虑用其他数据库主要是因为 MySQL 的资料多、部署简单、生态成熟出了问题好排查。2. 数据库设计与核心表结构2.1 核心业务表与字段规划数据库是一个电商系统的地基表结构设计不好后面写接口的时候会处处难受。这个项目我规划了用户表、商品表、分类表、轮播图表、购物车表、订单表、订单明细表、收货地址表一共 8 张核心表。初期没有引入过于复杂的营销表、优惠券表避免把范围撑得太大先把主干跑通。用户表的设计要注意密码不能存明文项目里用了 MD5 加盐的方式处理。用户名做了唯一索引这是登录时的查询条件没有索引的情况下用户多了以后会很慢。商品表里最关键的是价格字段记住能用DECIMAL(10,2)就不要用FLOAT浮点数在电商金额场景下会出现精度丢失比如 0.1 加 0.2 可能等于 0.30000000000000004这在结算时是致命的。订单表是整个系统的核心我设计了几个重要的状态字段订单编号、用户 ID、订单金额、收货信息快照、订单状态、支付时间、发货时间、创建时间。这里有个容易忽略的细节收货地址不能只存地址 ID因为用户改地址后历史订单要保留下单时的收货信息所以订单表里冗余存储了一份收货人信息快照。这在实际电商系统里是标准做法。订单明细表则是把一次订单里的多个商品拆成单独记录每条包含商品 ID、商品名称快照、商品图片、购买单价、数量、小计。商品名称也做了快照因为商家改商品名后历史订单应该展示当时的名称而不是改完后的名称。2.2 表关系与查询路径设计表之间的关系其实很直观。用户表跟购物车表是一对多用户表跟订单表是一对多订单表跟订单明细表是一对多商品表跟分类表是多对一。在设计外键时我并没有在物理层建外键约束而是保留逻辑关联。原因是外键在插入和更新时会带来额外的锁开销而且这个项目里订单明细和商品表之间是弱关联——商品删了历史订单记录还在如果建了物理外键删商品会被限制住。典型的查询路径是这样的用户在前端选好商品后前端把购物车 ID 列表传给后端后端查购物车表关联商品表算出总价用户下单后后端往订单表和订单明细表插入数据同时扣减商品库存用户查询订单列表时后端查订单表按创建时间倒序返回点进详情页再查对应的订单明细。分页这块用 MyBatis 的 PageHelper 插件商品列表、订单列表、用户列表都走分页接口。这里要注意一个细节pageNum和pageSize一定要做参数校验和上限控制不然用户可以传一个极大值把全表数据一次拉走在生产环境这是一个严重的隐患。我在前端也做了限制pageSize最大 100。3. 后端核心功能实现与业务逻辑3.1 用户认证与 JWT 登录方案登录模块看起来简单但方案选型很关键。这个项目用了 JWT 做无状态认证流程是用户登录成功后后端生成一个包含用户 ID 和用户名的 token 返回给前端前端把 token 存在本地存储里之后每次请求都在请求头上带上Authorization: Bearer token字段后端拦截器解析 token 拿到当前用户。写 JWT 的时候有个容易踩的坑token 里的过期时间不能设置太长项目里设置了 24 小时。过期后前端要能感知到 401 状态码自动跳转登录页。我在前端的 axios 拦截器里做了统一处理响应状态是 401 时清除本地登录状态并跳转/login。而后端拦截器也做了一层白名单/api/user/register、/api/user/login、/api/product/list这类不需要登录就能访问的接口要放行否则用户连登录页面都进不去。密码加密这里我推荐再加一层盐值单纯 MD5 面对彩虹表攻击是无能为力的。实现思路是用户注册时生成一个随机盐值把盐值和密码拼接后做 MD5数据库里同时存储盐值和加密结果。登录校验时取出盐值重新计算比对这样即使用户的密码散列泄露攻击者也无法通过预计算表直接反推出明文密码安全性好很多。我在项目中还做了一个用户注册时的唯一性校验。注册接口里先按用户名查一次库存在则直接返回错误。这里要注意并发场景下可能同时注册同一个用户名单纯的先查再插会有风险严谨的做法是在 user 表的 username 字段上建唯一索引让数据库兜底。3.2 商品模块与订单状态机商品模块的核心是分类管理和商品列表。分类采用两级结构一级分类下面挂二级分类商品表里存的是二级分类的 ID。查询商品时通过分类 ID 关联查询也可以直接按商品名模糊搜索。这个模块用到最多的就是 MyBatis 的if标签比如按分类查、按关键字查、按价格区间查三个条件任意组合写一个动态 SQL 就够了不用为每种组合单独写方法。订单模块是整个项目里业务最重的地方。我实现了一个状态机思路订单状态用整数表示0 待付款1 待发货2 待收货3 已完成4 已取消。每个状态的流转都有明确前提待付款只能通过支付接口转到待发货或已取消待发货只能通过发货接口转到待收货待收货只能通过确认收货转到已完成。在前端页面按钮的显示逻辑跟状态一一对应后端接口在状态变更前也做校验防止跳过中间状态比如直接对已取消的订单执行确认收货这种操作必须被拒绝。下单流程的代码顺序是先查询商品信息并计算总价然后扣库存再生成订单主记录接着批量插入订单明细最后清空该用户的购物车中已下单的商品。这一串操作全部放在一个事务方法里任何一步出错都会整体回滚。我在调试时验证过故意把商品库存扣成负数抛异常订单数据不会残留购物车也不会被清掉。还有一点关于库存扣减库存的 SQL 建议在 WHERE 条件中加入库存大于当前购买数量的判断例如UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}返回受影响行数判断是否扣减成功。这样能防止并发超卖比先查库存再判断要可靠得多。4. 前端 Vue3 工程化实践4.1 项目目录结构与状态管理方案前端项目的目录结构直接影响代码的可维护性。这套系统的前端分了src/api、src/router、src/stores、src/views、src/components、src/utils这几个核心目录。api目录下按业务模块拆分文件比如product.js、order.js、cart.js每个文件导出一个函数对应一个后端接口。这样页面组件里只调用函数不直接写 axios 请求接口地址集中管理后改起来方便。状态管理这块我用了 Pinia。购物车数量、用户登录信息、订单提交后的支付参数这些都是跨页面共享的数据如果靠组件 props 一层层传代码会很混乱。Pinia 的 store 像是一个全局的响应式仓库任何组件里都可以读写。比如用户登录后把用户信息存到userStore个人中心页面和订单结算页面都能直接取不需要重新请求接口。路由设计上项目区分了前端路由和权限控制。用户端路由有首页、商品分类、购物车、订单列表、订单详情、个人中心管理端路由有商品管理、订单管理、分类管理、用户管理、轮播图管理。前端在路由守卫里判断登录状态和角色角色权限后端接口同样有拦截器校验权限两边都做一层不是多余的因为前端控制只是体验优化真正的安全边界必须放在后端。4.2 关键页面逻辑与状态同步购物车页面是前后端配合的典型场景。购物车的加购、数量修改、勾选商品、删除商品每一步操作都会调用后端接口。前端拿到返回的新购物车数据后更新 Pinia 里保存的购物车商品总数。这里要注意的就是状态同步如果只有本地加减而不同步后端刷新页面数据就会回退用户会以为系统出 bug 了。我处理的方式是每次增删改之后都重新拉取购物车列表虽然多打一个请求但数据绝对准确。订单结算页面稍微复杂一点。用户从购物车勾选商品后点击去结算前端把选中的购物车 ID 列表和收货地址 ID 传给后端后端返回商品的实时价格和库存。这里为什么必须查实时价格因为购物车里的价格是加购时的快照这段时间内商品可能调价了下单必须用最新价格否则商家会亏。前端拿到实时价格后再显示应付金额同时要求用户选择地址确认后才能提交订单。提交订单成功后前端跳到收银台风格的支付页面展示订单编号、金额和支付方式选择。这个项目里支付是模拟实现用户点击支付按钮前端调支付接口后端延迟一秒后把订单状态从待付款改成待发货。整个交互流程和真实电商平台差别不大只是省去了接入第三方支付网关的步骤。5. 部署配置与高频故障排查5.1 本地环境搭建三个容易出错的配置文件一个前后端分离项目跑通本地环境基本上是四个步骤导入 SQL、改后端配置、启动 SpringBoot、启动 Vue 开发服务器。听起来简单实际操作中的报错往往集中在配置细节上。我把自己常用的配置框架整理出来照着改基本不会跑偏。后端application.yml里最核心的是数据源和 MyBatis 配置。数据源要确认 MySQL 的地址、库名、账号、密码全都匹配。MyBatis 配置要注意mapper-locations指向classpath:mapper/*.xml如果路径写错启动时会报找不到 Statement 的错误。另外要设置map-underscore-to-camel-case: true否则数据库的下划线字段名映射不到 Java 的驼峰属性查询结果全是 null。前端的.env.development文件里要配置 Vite 的代理转发。开发模式下前端跑在 5173 端口后端跑在 8080 端口两者不同源浏览器会拦截后端返回的跨域响应。我在 Vite 配置里加了代理把所有/api开头的请求转发到http://localhost:8080这样前端代码里写的请求地址都是相对路径不需要关心跨域。生产环境部署时前端构建出的静态文件由 Nginx 托管再把/api反代到后端服务即可。还有一个很多人忽略的文件是 MySQL 的初始化 SQL。如果数据库字符集没设置成 utf8mb4商品描述里有 emoji 或特殊符号时会报字符集相关的错误。导入 SQL 之前最好确认数据库的默认字符集或者在初始化 SQL 开头统一指定DEFAULT CHARSETutf8mb4。5.2 实战中遇到的高频报错与解决记录我把这套系统从零开发到联调完成遇到过的报错类型基本是五类整理成一份速查表几乎每个做类似项目的人都会碰到。第一类是端口冲突。后端端口 8080 被其他程序占用启动时日志直接报Port already in use。排查方法是在命令行里查端口占用找到对应的进程号之后结束掉或者直接改server.port避开。如果你本机同时启动过多个 SpringBoot 项目这种问题会反复出现。第二类是数据库连接失败。报错信息常见的是Access denied for user或者Communications link failure。前者检查账号密码和授权后者基本就是 MySQL 服务没启动或者spring.datasource.url里的端口、主机地址写错了。还有一个隐蔽的点MySQL 8 和之前的版本驱动类名不一样8.x 要写com.mysql.cj.jdbc.Driver而且 URL 里加上serverTimezoneAsia/Shanghai否则时间字段会有 8 小时的偏移。第三类是 MyBatis 的绑定异常报Invalid bound statement (not found)。出现这个错先看 XML 文件的 namespace 是否和 Mapper 接口全限定名一致再看 mapper-locations 路径是否扫到了 XML 文件最后检查 XML 里的 id 是否和接口方法名一致。三处有一处不匹配就会报这个错。第四类是前端跨域问题。浏览器 Console 报CORS policy错误排查时先确认请求是否走了 Vite 代理再看后端拦截器是否放行了 OPTIONS 预检请求。写一个Filter或者实现WebMvcConfigurer的addCorsMappings统一处理跨域避免每个 Controller 都加CrossOrigin。第五类是 Vue3 响应式丢失。最常见的是从接口拿到的数据直接赋值给 reactive 数组后页面不更新。原因是用let list reactive([])定义后用list res.data整体替换了引用响应式丢失了。正确做法是list.splice(0, list.length, ...res.data)或者改用ref([])赋值时给list.value整体赋值就不会有这种问题。6. 源码结构笔记与后续扩展方向这套系统的源码整体上可以分成两个工程后端server目录和前端web目录。后端按三层架构组织controller 只接收参数和返回结果service 里写业务逻辑mapper 负责数据库操作。DTO 和 VO 要跟实体类分开前端需要什么字段就封装成对应的 VO 返回不要把实体类直接暴露给前端这是保护后端数据结构的关键习惯。前端工程按照页面维度组织每个页面的代码控制在几百行以内。如果某个页面的逻辑超了就把公共部分抽到 hooks 里。比如订单列表和订单详情都要处理状态文字映射就可以把订单状态和描述、标签颜色封装到一个独立的模块里两个页面共用。后续想扩展的话优先级我先排好。第一是增加 Redis做验证码存储和购物车缓存Redis 处理这类高频读写比数据库快一个量级。第二是接入真实的第三方支付替换掉模拟支付逻辑同时完善支付回调处理和订单超时关单机制。第三是增加商品规格参数支持多规格选择和对应库存这需要重新设计商品规格表。第四是做一个简单的推荐系统基于用户浏览记录和购买历史做协同过滤这属于锦上添花排在最后就好。我实际做完这套系统后最大的体会是电商项目的难点不在单点技术上而在业务链路的衔接和状态管理上。一个下单操作可能要改十几张表、涉及多个服务的协作任何一环没想清楚联调的时候都要返工。所以给我的话给正在做类似项目的同学一个建议开工之前先拿一天时间画好数据库表关系图把订单状态流转图和购物车交互图理清楚你觉得是浪费时间实际能帮你省掉后面很大一部分调试成本。代码写错了可以改数据模型设计错了返工的代价才叫人头疼。