首页 / 资讯中心 / 文章详情

Spring Boot+Vue生鲜商城系统设计与实现:前后端分离实战解析

Spring Boot+Vue生鲜商城系统设计与实现:前后端分离实战解析 ★ FEATURED ARTICLE
做过几个前后端分离的项目之后我对这类商城系统的“套路”已经很熟悉了但拿到“javavue基于springboot框架的生鲜商城系统设计与实现”这个需求时我还是犹豫了一下。生鲜这个品类和卖书、卖衣服的普通商城差别太大了从商品模型到订单流程每一处都在提醒你这不是在做一个CRUD界面而是在做一个需要应对损耗、时效、价格波动的业务系统。这套系统说到底就是给生鲜电商场景做的一套完整解决方案用户端可以浏览分类、搜索商品、加购下单管理端可以维护商品、处理订单、管理库存同时系统层面要保证下单、扣库存、订单状态流转这些环节的数据一致性。适合想做Java全栈项目的人参考无论是用来学习Spring Boot和Vue的配合方式还是作为毕业设计、求职项目都能从中找到可以直接落地的设计思路和代码参考。1. 业务需求拆解生鲜商城系统到底要做什么1.1 商城系统的用户端与管理端职责划分开始动代码之前一定先把角色和边界理清楚。我的习惯是先把系统拆成两个视角一个是普通用户天天要用的前台一个是运营人员每天要打交道的后台。前台部分的核心链路很清晰注册登录、浏览商品、搜索分类、查看详情、加入购物车、提交订单、模拟支付、查看订单状态、取消订单。这些环节一个都不能少少了任何一环用户流程就断了。管理端则要承担完全不同的职责商品管理上架、下架、编辑价格库存、分类管理、订单处理发货、查看订单详情、用户管理、数据统计。商品上架之后用户马上能在前台看到库存改了之后用户加购时立刻感知这种前后台联动的闭环才是这个项目真正的价值所在。1.2 生鲜业务和普通电商的核心差异生鲜电商和普通电商的差异我总结下来有三点这三点直接决定了技术设计第一个是时效性。生鲜商品有保质期下单之后如果半小时不支付库存就应该释放给其他人否则会出现用户锁了一堆菜最后不付款真正想买的人买不到。所以订单超时未支付自动关闭是一个必须做的功能不是锦上添花。第二个是库存的敏感度。普通商品库存多几件少几件问题不大但生鲜库存如果超卖用户下单了却拿不到货客户体验直接崩掉。这就要求扣库存的时候必须处理好并发不能简单地读出来减一就写回去。第三个是商品形态多样。有的按件卖比如一盒草莓有的按重量卖比如一条鱼。体现在数据模型上就是商品表里需要有一个字段标识计费方式下单和结算的逻辑都要根据这个字段做差异化处理。1.3 为什么选择前后端分离架构我在设计这个项目时结构上直接选了前后端分离没有用传统的服务端模板渲染。原因很现实后端只需要把数据通过接口吐出去前端怎么展示完全不需要后端操心。以后想加一个移动端H5同一套接口直接复用不需要重新开发一套后端逻辑。具体来说后端用Spring Boot提供RESTful API前端用Vue单页应用来承载界面。这种方式让开发时可以并行推进我负责接口A同学负责页面约定好接口文档就能各干各的互不阻塞。后面遇到问题排查起来也清晰接口报错还是页面报错一眼就能看明白。2. 技术选型思路与项目初始化配置2.1 核心框架为什么是Spring Boot和Vue后端选择Spring Boot最重要的原因是它把大量的配置工作都自动化了。传统的SSM项目要写一堆XML配置Spring Boot直接用注解加约定就搞定内嵌的Tomcat容器让应用可以一键启动特别适合这种需要快速迭代、快速交付的项目场景。前端选择Vue核心原因是它的学习曲线和数据绑定机制。Vue的双向绑定让表单操作变得极其顺手组件化的开发方式让页面结构清晰配合生态里成熟的路由和状态管理工具做一套多页面的商城前台完全够用。相比原生JavaScript写DOM操作Vue的开发效率高出一大截。2.2 工程依赖清单与项目骨架搭建后端我用Maven管理依赖核心依赖包括这些dependencies !-- Web开发支持内含内嵌Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis框架负责SQL映射和持久层操作 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId /dependency !-- MySQL数据库驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId /dependency !-- Redis缓存客户端 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- JWT令牌处理 -- dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId /dependency !-- Lombok简化实体类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency /dependencies前端这边我通过Vite创建Vue项目安装Vue Router来管理路由Pinia管理全局状态Axios发送HTTP请求Element Plus提供现成的组件库。Element Plus特别好用的一点是表格、表单、弹窗这些后台管理常遇到的组件都是现成的大幅减少自定义样式的成本。后端目录结构我按照职责分了层这是整个Java Web项目的老规矩也是多人协作的基础com.example.freshmall ├── controller // 接口层只接收参数和返回结果 ├── service // 业务层核心逻辑都在这一层 ├── mapper // 数据访问层定义接口方法 ├── entity // 实体类对应数据库表结构 ├── vo // 视图对象用于接口返回数据 ├── common // 通用返回结果、异常处理、工具类 ├── config // 配置类比如跨域、拦截器注册2.3 为什么普遍选择MySQL和Redis组合这个项目的持久化方案我用的是MySQL加Redis的组合。MySQL作为主数据库存储用户、商品、订单这些核心业务数据原因很简单关系型数据库的事务能力、SQL查询能力、数据一致性保障是这类业务系统的底线要求。Redis在项目里主要承担三个职责一是缓存首页推荐商品等读多写少的数据降低数据库压力二是存储验证码这类短生命周期数据自动过期省心三是处理购物车的临时状态虽然我最终把购物车落到了MySQL但缓存方案在频繁改动场景下响应更快。3. 数据库设计与核心表结构详解3.1 核心业务表的建模分析我把这套系统的表拆成了两类基础信息表用户、分类、商品、地址和交易相关表购物车、订单、订单明细。下面是核心表的字段设计说明。用户表包含用户名、密码BCrypt加密存储、昵称、手机号、头像、状态等基本字段。登录名用用户名或者手机号都行密码千万别明文存这是底线。商品分类表存储分类名称、父级分类ID、排序号、是否显示。生鲜的分类层级不会太深两级基本够用一级是蔬菜、水果、肉禽蛋奶、水产二级再往下细分。商品表这是整个系统最复杂的表之一字段包括商品名称、分类ID、主图、详情描述、售价、原价、库存量、单位、销量、是否上架、创建时间。生鲜特有的字段还包括计价方式1表示按件2表示按重量重量商品的库存实际上是一个可售重量的计数。购物车表字段为ID、用户ID、商品ID、购买数量、勾选状态、加入时间。生鲜场景加一个字段标记该商品的快照价格避免用户加购后商品涨价导致结算时扯皮。订单主表订单号、用户ID、订单总金额、实付金额、配送地址快照、订单状态、下单时间、支付时间、发货时间。订单状态我用的是待支付、已支付待发货、已发货、已完成、已取消五态模型。订单明细表关联订单ID、商品ID、商品名称快照、商品图片快照、单价快照、数量、小计金额。快照字段非常重要商品信息后面可能会改但订单里的信息必须保持下单那一刻的样子。3.2 商品表的关键字段与DDL参考商品表是业务的核心我给出实际开发时比较实用的建表语句片段CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT COMMENT 商品ID, category_id bigint NOT NULL COMMENT 分类ID, name varchar(100) NOT NULL COMMENT 商品名称, main_image varchar(255) DEFAULT NULL COMMENT 商品主图URL, detail_html text COMMENT 商品详情富文本, price decimal(10,2) NOT NULL COMMENT 销售单价, original_price decimal(10,2) DEFAULT NULL COMMENT 划线原价, stock int NOT NULL DEFAULT 0 COMMENT 库存数量, unit varchar(10) DEFAULT 件 COMMENT 计价单位, pricing_type tinyint NOT NULL DEFAULT 1 COMMENT 计价方式: 1按件 2按重量, sales int NOT NULL DEFAULT 0 COMMENT 销量, status tinyint NOT NULL DEFAULT 1 COMMENT 状态: 0下架 1上架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_status (category_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;注意两点第一金额字段统一用decimal(10,2)禁止用float或double浮点数算钱会出现0.1加0.2不等于0.3的精度问题第二idx_category_status这个组合索引覆盖了按分类查询上架商品这个最高频的查询场景能明显加快列表页的响应速度。3.3 订单状态设计与库存扣减方案订单状态我设计了五种每种状态之间的转换关系必须严格限定不能出现待支付直接跳已完成这种非法流转。状态机我放在后端枚举里管理前端只要把状态值对应的文案展示出来就行。库存扣减是并发场景下最需要关注的一环。我直接采用乐观锁的方式在update语句里加上库存数大于等于购买数量的条件同时扣减成功的行数大于0才算扣库存成功UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这个方案相比“查询库存再更新”的方式避免了商品超卖问题实现成本低且可靠。实际下单选库存时要考虑一个“预占”概念用户下单但不支付库存要先锁定等支付成功才真正扣减超时未支付则释放回库存。这个逻辑我放在订单服务里统一处理。4. 后端核心模块实现全流程4.1 统一返回结果与全局异常处理搭建接口的返回格式我会在项目一开始就统一好不然后面每写一个接口都返回不同的JSON结构前端光适配就够头疼了。我的方案是一个泛型的结果包装类。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }配合全局异常处理器所有业务异常都被拦截并转成约定的错误码。这样前端看到code不等于200就可以统一弹错误提示不需要每个接口单独处理异常分支。4.2 用户登录认证与JWT无状态鉴权实现用户登录这块我没用传统的Session方案因为Session在前后端分离下需要处理跨域携带Cookie的问题麻烦还不好扩展。我采用JWT令牌的方式登录成功后服务端生成一个包含用户ID的令牌返回给前端前端存储在本地之后每次请求都会带上这个令牌后端通过拦截器验证令牌并解析出当前登录用户。public String createToken(Long userId) { Calendar expire Calendar.getInstance(); expire.add(Calendar.HOUR, 24); return JWT.create() .withClaim(userId, userId) .withExpiresAt(expire.getTime()) .sign(Algorithm.HMAC256(secretKey)); }拦截器解析令牌后把用户ID保存到ThreadLocal里当前请求的Controller或Service中随时可以取到这样用户相关的查询就不需要前端在每次请求里都加用户ID了。需要登录的接口基本上就是购物车、下单、订单列表这类我对它们做了统一的鉴权拦截。4.3 商品展示与购物车模块的接口设计商品模块这部分的逻辑相对直白核心接口是首页推荐商品列表、按分类查询商品、关键字搜索商品、商品详情。搜索查询我用MyBatis的动态SQL来处理根据用户传入的参数动态拼接条件关键字匹配商品名称分类ID存在时就过滤指定分类。购物车我选择存储在MySQL表中而不是Redis。原因很实际购物车数据虽然变动频繁但量级不大加之用户需要随时看到加购的商品数据库存储更容易保证持久化和数据一致性。购物车的增删改查接口就是围绕用户ID和商品ID展开。提交购物车商品到订单时前端会把购物车勾选的多条记录ID传给后端后端逐条读取商品快照信息生成订单主记录和订单明细记录同时清空这些购物车项。4.4 订单流程与模拟支付回调机制订单模块是整个系统业务逻辑最重的地方我从创建订单开始串一下用户携带收货地址ID、商品ID列表和备注信息调用创建订单接口后端先校验商品状态和库存然后计算总价删除对应购物车项生成订单号用时间戳加随机数保证不重复订单状态置为待支付此时Redis里存入一个订单编号到过期时间的映射。模拟支付这里我做了一个简单但有效的方案提供一个支付接口前端点击“立即支付”后调用该接口后端通过定时器和延迟任务扫描待支付订单超过30分钟未支付就自动置为已取消同时回补库存。实际的支付对接后续替换成服务商SDK时把回调处理落在订单状态更新这个环节就行了。库存回补的逻辑必须做成幂等的也就是订单取消操作不管触发几次只能回补一次库存。我的做法是先检查订单状态只有待支付或者已支付状态才允许执行取消和回补同时在SQL更新条件里加状态限定避免重复扣补。4.5 管理端商品管理与订单处理实现管理端这块我的重心放在商品管理和订单处理上。商品管理就是后台的增删改查列表用分页查询编辑商品时可以直接修改价格、库存、上下架状态。上架状态更新时我会把商品ID同步到Redis做标记这样前台列表查询的时候可以直接过滤。订单处理相对简单管理员看到已支付的订单后点击发货按钮订单状态从待发货变为已发货并且记录发货时间。界面用Element Plus的表格加状态标签一眼就能看清当前每个订单走到哪一步。5. 前端页面构建与交互实现细节5.1 页面路由规划与整体布局设计前端页面我按照用户端和管理端两块来规划路由。用户端页面包括首页、分类商品列表页、商品详情页、搜索列表页、购物车页、订单结算页、订单列表页、登录注册页管理端则作为一个独立布局内部包含商品管理、订单管理、分类管理等页面。路由配置上普通页面直接挂载管理端页面我封装了一层路由守卫用户访问管理端路由时先判断本地存储有没有登录令牌没有就直接跳转到登录页防止没登录的人直接打开后台地址。5.2 Axios请求拦截与后端交互规范Axios的封装是前后端衔接的关键一环。我在封装的request.js里统一配置了请求的基本URL和超时时间请求拦截器负责把JWT令牌塞进请求头响应拦截器则统一处理返回结果。service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); service.interceptors.response.use(res { const code res.data.code; if (code 200) { return res.data; } else { ElMessage.error(res.data.message); return Promise.reject(res.data); } }, err { ElMessage.error(网络异常请稍后重试); return Promise.reject(err); });这样处理之后业务代码里调用接口就变得很干净只需要直接拿data使用错误弹窗全部统一处理不需要每个页面自己写一遍提示逻辑。5.3 商品列表与购物车状态管理商品列表页的关键在于筛选和分页。我用了一个搜索栏左边是分类树选择右边是关键字搜索框下方是ElTable展示的列表分页组件同步当前页码和每页条数。每次查询条件变化都重新请求接口并刷新表格数据。购物车内的全局状态用Pinia来管理核心状态包括购物车商品数量和购物车商品详情的列表。用户加购、删除、选中、跳转结算这些操作都会触发对应的Action同步更新本地数据和后端数据。有一个交互细节值得注意用户操作购物车时后端返回了最新购物车数据前端会以这个响应为准整体覆盖本地状态而不是本地自己算加一减一这样做可以避免前端后端数量不一致的尴尬情况。5.4 管理端表格与富文本编辑体验管理端页面的开发我觉得是整套系统里最顺手的部分因为Element Plus的Table组件加Form表单已经解决了大部分重复工作。商品列表页直接用ElTable渲染数据配上状态标签和按钮操作组上架下架用Switch组件切换。编辑弹窗里商品详情用富文本编辑器图片上传先传到本地或对象存储保存时把URL字符串放进表单一起提交。6. 部署启动全流程从本地环境到打包发布6.1 本地开发环境准备与数据初始化拿到项目后启动要先确认环境完备我的顺序是安装JDK 8或11、安装Maven 3.6、安装MySQL 5.7或8.0、安装Redis。数据库建好后执行初始化脚本把表结构和基础测试数据灌进去。如果是项目附带SQL文件直接source即可。后端配置核心就是数据源和Redis地址修改application.yml里面的数据库账号密码、URL连接串、Redis地址端口然后启动服务类。前端执行npm install安装依赖再执行npm run serve启动开发服务器默认端口8080。前端开发模式自带代理把/api前缀的请求转发到后端8080端口实现联调。6.2 前端构建产物与Nginx部署方案本地开发完成后发布上线我一般用Nginx托管前端打包产物同时做接口反向代理。前端执行npm run build把生成的dist目录放到服务器/usr/share/nginx/html下Nginx配置里做两个关键动作一是将前端请求的/api路径反向代理到后端服务地址二是解决Vue的history路由刷新404问题。server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; # 重要非 /api 路径的统一回退到 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; } }后端打包直接用Maven的package命令生成jar包放到服务器执行nohup java -jar xxx.jar 后台运行。生产环境的数据库、Redis配置通过外部配置文件或环境变量覆盖避免把测试环境的连接信息打包进发布包。6.3 本地运行后端连接数据库和Redis的注意点这一步是很多人第一次跑项目最容易翻车的地方数据库连不上或者Redis认证失败。我先说数据库本地MySQL的root账号默认密码五花八门配置里的密码必须改成本地真实密码而且连接串里的serverTimezoneAsia/Shanghai不能丢否则时间字段的读写会出时区偏差。Redis这边我本地的版本默认没有密码但如果你装的是云厂商的单机版或者某些带密码的发行版必须在配置里补上password否则启动即报错。还有一个排查顺序值得记住看到连接异常不要改后端代码先看是不是进程没启动、端口被占用、认证失败再往代码方向查。7. 开发中遇到的典型问题与排查技巧7.1 前后端跨域问题的完整处理方案跨域是我项目联调第一天就碰到的问题浏览器控制台直接报CORS错误前端请求根本发不出去。当时自定义了一个CorsFilter用整批拦截配置允许跨域请求携带认证信息和自定义请求头。仅靠Spring的跨域配置能解决开发阶段的跨域问题到了生产环境用了Nginx同源访问之后跨域就不存在了。所以我的建议是开发阶段用后端配置放开跨域保证前端本地服务能访问上线阶段用Nginx同源部署把后端的跨域配置关掉或收紧安全性和稳定性都会更好。7.2 金额计算引发浮动误差问题这种问题在订单金额计算时最容易爆出来。系统里总价由单价乘数量再累加如果用了Double类型很多价格会出现类似的精度偏差导致前端展示总价为99.99但明细对不上对账时发现少了一分钱排查方向很久找不到。我前面强调过财务相关字段必须用BigDecimal计算时用BigDecimal.valueOf()包装严禁直接用浮点数运算。生产环境的对账接口能精准匹配离不开这些细节。7.3 Vue Router的history模式刷新404问题我在本地用npm run serve一直好好的打包上传到Nginx后访问首页没问题但只要在子路由页面按F5刷新直接看到Nginx的404页面。原因很简单Vue Router的history模式把路由交给前端管理但Nginx默认会根据URL去找对应路径的物理文件找不到就404了。解决方式就是在Nginx配置里加上try_files $uri $uri/ /index.html把不存在的路径请求都回退到首页由前端路由接管。这个问题虽然小但部署上线时几乎每个人都会遇到。7.4 库存超卖场景的并发复现与修复测试阶段我用多个账号同时对同一件商品下单发现有的时候订单创建成功了但库存变成负数。原因在于最初的代码是先查询库存在Service层判断库存大于0再执行更新并发情况下多个请求同时通过了库存查询都执行了扣减操作。修复方案我就的不再重复查询判断而是直接在SQL层做原子扣减通过stock quantity作为更新条件配合MyBatis返回的影响行数判断是否扣减成功。这也解释了为什么项目里商品下单的代码必须在一次数据库操作里完成扣减而不是分两步走。7.5 MyBatis多表关联查询字段映射丢失商品列表需要展示分类名称商品表和分类表做关联查询。我刚开始直接查两张表返回的POJO里分类名称一直是null查了半天发现是MyBatis的自动映射只处理同名字段category_name和POJO里的categoryName对不上。解决方式有两种要么给查询字段起别名跟POJO字段保持一致要么在resultMap里显式配置映射关系。我个人建议用别名方案写起来简单SQL读起来也直观比如SELECT c.name AS categoryName配合MapUnderscoreToCamelCase配置基本上能减少很多映射配置工作。7.6 待支付订单的超时自动关闭实现订单超时关闭我用的是定时任务扫描方案Spring自带的Scheduled注解固定每分钟扫描一次待支付订单把超过30分钟未支付的订单置为取消并回补库存。定时任务写的逻辑注意要加上状态条件再更新不要全部更新成取消状态。后来对可靠性有更高要求后Redis过期监听的方案更好每次创建订单后把订单号存进Redis并设置30分钟过期过期后触发回调执行关闭操作。定时扫描在数据量大时有延迟但胜在简单作为这个项目的实现完全够用。项目做完之后再回头看整个设计最大的收获其实是理解了一件事所谓的系统实现核心不是代码怎么写而是业务规则怎么建模。像生鲜这种时效性极强的业务库存的预占、订单时效的关闭、价格和商品的快照每一个看似简单的设计背后都有一个业务场景在驱动。如果你打算基于这个项目做二次开发我建议优先尝试扩展这几个方向一是加入优惠券模块这会强制你重新思考订单金额的计算流程二是接入真实支付渠道你会对回调机制和幂等性有更深刻的理解三是数据统计报表聚合查询和图表展现会让项目在展示时加分不少。最后再分享一个小建议做这种全栈项目的时候每一个接口都记得写接口文档哪怕只是接口路径和参数说明的表格后面前端联调和项目答辩都会省心很多。
阅读完成 · 觉得有帮助?
咨询建站