新闻详情

新闻详情

首页 / 资讯中心 / 详情

SpringBoot+Vue+MyBatis+MySQL:水果商城订单链路实战

发布时间:2026/9/30 8:42:59来源:尧图网络
SpringBoot+Vue+MyBatis+MySQL:水果商城订单链路实战
最近我又把一个“飘香水果购物网站管理系统”完整跑通了标题里的技术栈非常典型SpringBoot Vue MyBatis MySQL几乎是前后端分离项目最常用的一套组合。标题虽然挂着“2025最新”但说实话版本号不是重点重点是这套组合怎么把水果商城从数据库到页面完整地串起来。这篇文章我以这套系统为例子把项目结构、数据库设计、后端订单链路、前端联调、以及实际跑通时的踩坑经历一次说清楚适合正在做课程设计、毕业设计或者刚开始学前后端分离项目的朋友按图索骥。很多人拿到这类源码第一反应是先点运行结果前端报错、后端起不来、数据库连不上还没看到登录页就放弃了。其实搞清楚系统边界和业务模块再顺着流程去读代码比盲目运行效率高得多。下面直接进入正题。1. 先拆需求和模块边界水果商城和后台管理是两副面孔1.1 不要被“管理系统”三个字带偏它前端是商城后端是运营后台这套系统名字叫“购物网站管理系统”很多新手会误以为只有一个后台管理页面。实际拆开看是标准的双端结构这也是前后端分离项目最常见的形态用户端前台商城面向普通注册用户核心是一整套购物流程首页轮播图、热门水果推荐、公告通知商品浏览分类列表、搜索、商品详情价格、库存、销量、图文描述购物车加购、修改数量、勾选结算订单中心创建订单、模拟支付一般跳转一个支付成功页、查看订单状态和历史订单个人中心注册登录、收货地址、个人信息修改管理后台面向运营管理员核心是数据维护和订单流转仪表盘统计商品总数、订单数、用户数、销售金额商品管理新增、编辑、上下架、库存调整、图片上传分类管理水果分类的增删改订单管理查看未支付/待发货/已发货/已完成订单执行发货操作用户管理查看注册用户、禁用/启用账号两套前端界面共用同一个后端接口但接口权限完全不同。后台接口需要管理员身份用户端接口只需要登录身份。所以看代码时一定要注意Interceptor或者网关层搞清楚哪些接口需要放行比如首页数据、商品列表哪些必须校验Token。1.2 技术栈为什么是“SpringBoot Vue MyBatis MySQL”的组合这个组合看似老生常谈但每一个选型都有明确理由不是随便拼的组件作用选型理由SpringBoot后端框架自动配置极大减少XML配置内嵌Tomcat一键启动适合中小型管理系统快速交付MyBatisORM层相比JPASQL完全可控商城类项目有大量多表联查和统计报表SQL写在XML里方便优化MySQL数据库稳定、免费、事务支持好订单金额和库存这类数据必须依靠关系型数据库的ACID特性Vue前端框架组件化开发页面复杂度可控生态成熟Element UI/Element Plus可以直接搭出后台界面如果你之前只写过单体JSP项目换成这套组合后最大的感受是前端不再负责业务逻辑后端也不再拼HTML职责边界清楚排错容易得多。当然选择MyBatis也意味着你要能写SQL多表联查、分组统计、分页这些基本功不能丢。1.3 拿到源码第一步理清目录不要先跑我强烈建议拿到源码后先做三件事而不是急着启动找到初始化SQL脚本看清楚数据库要建几张表。找到后端配置文件application.yml确认数据库连接、端口号、文件上传路径。找到前端目录区分哪个是用户端、哪个是管理后台、各自的后端接口地址配置在哪里。把这三点理清你已经比一半的人走得远了。很多所谓“跑不起来”根本不是代码问题是端口冲突、数据库密码不对、SQL没导入这三件事。2. 核心表设计一张订单主表怎么吃下整个水果交易链路2.1 数据库表到底要建几张一套完整的飘香水果购物系统核心表一般在6到8张。我按最典型的拆法整理一下category水果分类表fruit_info商品表存水果名称、价格、库存、销量、图片、上下架状态user用户表cart购物车表order_master订单主表order_item订单明细表address收货地址表有的系统直接冗余在订单主表里不单独建表banner或notice轮播图和公告表看项目需要很多新手搞不清order_master和order_item为什么要拆两张表我打个比方你去超市一次性买苹果和香蕉收银小票上会有一个订单号下面列着苹果一行、香蕉一行。一个订单编号对应多条商品记录这就是“一主多明细”结构。订单主表存一次消费的整体信息比如总金额、收货人、订单状态明细表存每一项商品的名称、单价、数量。如果只建一张表一个订单里买五样水果就要插五条一模一样的收货地址记录数据冗余而且订单状态无法统一更新。2.2 订单主表的关键字段设计这里我给一个简化但不失真实性的建表示例直接拿来参考CREATE TABLE order_master ( id bigint(20) NOT NULL AUTO_INCREMENT, order_sn varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, pay_amount decimal(10,2) DEFAULT 0.00 COMMENT 实付金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单状态0待支付1待发货2待收货3已完成4已取消, receiver_name varchar(32) NOT NULL COMMENT 收货人姓名, receiver_phone varchar(20) NOT NULL COMMENT 收货人电话, receiver_address varchar(255) NOT NULL COMMENT 收货地址, remark varchar(255) DEFAULT NULL COMMENT 订单备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_sn (order_sn), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;几个容易被忽略但是很关键的细节金额字段一定用decimal(10,2)不要用float。FLOAT是近似值做金额运算时会出现0.10.2不等于0.3这种经典问题。买东西的账算错了放在真实项目里是要出事的。order_sn要有唯一索引因为它是面向用户的订单号且后续对接第三方支付时往往需要用它做预支付单号。status用tinyint存数字不要直接存中文。状态含义写在代码常量或枚举里页面上再翻译成“待支付”“待发货”等文字。收货人、电话、地址要冗余进订单表不能下单后去查地址表。因为用户之后改了地址历史订单上的收货信息不应该跟着变。2.3 商品表、库存和销量如何协作商品表fruit_info里除了基本的price、stock一般还要设计sales销量字段。销量不是下单成功时随便加一而是要有策略模拟支付的系统通常在用户点击“立即支付”并调用支付成功回调时增加销量如果只是本地演示常见做法是下单成功就加销量。库存字段的唯一标准是“不能出现负数”。这就要靠后端下单事务里的扣库存逻辑用一条带条件更新的SQL保证。另外分类表和商品表的关系是一对多。fruit_info里的category_id一般建一个普通索引。查询某个分类下的水果列表就是WHERE category_id ? AND status 1再按销量或创建时间排序。索引不是越多越好像这种小系统用户表加订单表各加一两个高频索引就足够。3. 后端订单核心链路事务、防超卖和MyBatis映射3.1 Controller-Service-Mapper 三层到底怎么分现在的SpringBoot实战项目基本默认是controller→service→mapper三层。很多同学看源码时容易被“三层”这个概念绕晕其实记住一条原则就行Controller只负责收参数和回结果Service写业务规则Mapper只管SQL。以订单接口为例Controller接收前端传来的购物车勾选商品ID列表、收货地址ID封装成DTO交给Service。Service先校验商品是否存在、库存是否充足然后计算金额执行扣库存、插订单表、插明细表、清购物车。Mapper只提供具体的方法比如selectById、updateStock、insertOrder。源码里如果发现Controller里写了一堆业务代码那说明设计得不够好拿到手后可以自己抽到Service层。3.2 创建订单的Service层伪代码每一行背后都有原因这里我写一个创建订单的核心流程注释里直接解释为什么Transactional(rollbackFor Exception.class) public OrderVO createOrder(CreateOrderDTO dto) { // 1. 从数据库查出当前用户的购物车选中项 // 注意购物车数量、商品ID必须以后端查询结果为准不能信任前端传过来的price ListCart checkedCarts cartMapper.selectCheckedByUserId(dto.getUserId()); // 2. 遍历购物车逐个查出商品最新价格和库存 // 为什么不用前端传的单价因为用户可以改请求参数后端必须以数据库为准 for (Cart cart : checkedCarts) { FruitInfo fruit fruitMapper.selectById(cart.getFruitId()); if (fruit null || fruit.getStatus() ! 1) { throw new BizException(商品不存在或已下架 cart.getFruitId()); } if (fruit.getStock() cart.getQuantity()) { throw new BizException(库存不足只剩 fruit.getStock() 件); } } // 3. 后端重新计算总金额 BigDecimal totalAmount BigDecimal.ZERO; for (Cart cart : checkedCarts) { FruitInfo fruit fruitMapper.selectById(cart.getFruitId()); totalAmount totalAmount.add(fruit.getPrice().multiply(new BigDecimal(cart.getQuantity()))); } // 4. 扣减库存使用带条件的UPDATE防止超卖 for (Cart cart : checkedCarts) { int count fruitMapper.deductStock(cart.getFruitId(), cart.getQuantity()); if (count 0) { throw new BizException(库存扣减失败); } } // 5. 生成订单号时间戳 随机数避免并发重复 String orderSn FR System.currentTimeMillis() RandomUtil.randomNumbers(6); // 6. 插入订单主表和订单明细表明细表冗余商品快照信息 // 7. 删除购物车中已结算的商品 // 8. 返回订单号给前端 }这段代码我特别想强调第4步。扣库存不能用“先查询库存再在Java代码里减一最后UPDATE”的方式因为两个人同时下单同一个水果可能都查出库存只剩1个都以为自己能买结果超卖。正确的做法是把判断库存和扣减合并成一条SQL让数据库帮我们守住库存底线。3.3 防超卖的核心SQL就一行UPDATE fruit_info SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}这条SQL的意思是只有在当前库存大于等于购买数量时才执行扣减。MySQL执行时会锁住这行记录第二个请求来了之后库存已经变小如果小于购买数量影响行数为0Java代码里的count 0判断就会让事务回滚并抛异常。这也是为什么我前面强调订单接口必须加Transactional。因为下单涉及四个写操作扣库存、插订单主表、插订单明细、删购物车任何一个失败系统都不能留下“库存扣了但订单没生成”的半截数据。事务就是保证这四个操作要么全部成功要么全部回滚。需要注意一个事务细节Transactional(rollbackFor Exception.class)如果不写rollbackForSpring默认只在抛出RuntimeException时才回滚自定义的业务异常如果不继承RuntimeException那事务不会回滚这是很经典的坑。3.4 MyBatis的resultMap和驼峰映射决定你少写多少SQL后端连接MySQL时数据库字段一般用下划线风格比如order_sn、create_time而Java实体类属性是驼峰风格比如orderSn、createTime。在不做任何配置时MyBatis查询结果不会自动把order_sn映射到orderSn你大概率查出来全是null。解决办法有两种我更推荐在application.yml里开全局驼峰转换mybatis: configuration: map-underscore-to-camel-case: true这样order_sn会自动映射到orderSn不需要写一堆resultMap。但如果遇到多表联查比如查询订单同时带出商品名称、商品图片推荐在XML里手写resultMap或用特定的VO类接收。resultMap idOrderDetailMap typecom.fruit.entity.vo.OrderDetailVO id columnid propertyid/ result columnorder_sn propertyorderSn/ result columntotal_amount propertytotalAmount/ result columnfruit_name propertyfruitName/ result columnfruit_image propertyfruitImage/ /resultMap这块的排查思路很简单如果页面某个字段显示null先去看数据库字段名和实体类属性名是否对得上再去看map-underscore-to-camel-case有没有生效。百分之八十的null问题出在这里。3.5 分页和统一返回后端不能漏掉的两个武器商城系统首页、后台订单列表都需要分页。MyBatis里最常用的做法是PageHelper但有一个使用纪律必须遵守PageHelper.startPage()后面必须紧跟一条Mapper查询只对紧挨着的那条查询生效中间不能插入其他查询否则分页会错乱。写接口时习惯上用统一返回结构ResultT包含code、msg、data三个字段。前端统一用Response拦截器判断code 200不需要每个页面重复处理成功失败逻辑。全局异常处理用RestControllerAdvice业务异常返回code500参数校验失败返回code400这样接口错误信息才能准确传到前端页面。4. 前端Vue模块从页面搭起来到接口联调的关键细节4.1 一套源码里通常有两个前端工程别搞混这种购物系统源码一般会有两个前端目录一个用户端一个管理后台。用户端走的是商城购物风格管理后台走的是表格表单风格通常基于Element UI或Element Plus搭建。不管用Vue2还是Vue3核心目录结构都差不多src/api接口请求定义每个业务模块一个JS文件src/router路由配置区分需要登录的页面和公开页面src/store或src/pinia全局状态管理存用户信息、Token、购物车数量src/utils/request.js封装axios实例统一处理Token和错误码src/views页面组件拿到源码后先进request.js看看baseURL配的是什么。很多本地跑不起来的项目就是因为baseURL指向了某个已经不存在的线上接口地址或者本地后端端口和配置对不上。4.2 axios拦截器Token统一携带登录过期统一处理购物系统里除了首页和商品列表其余接口基本都要登录。前端实现的核心就是请求拦截器// request.js 核心逻辑 const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器每次请求自动带上Token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理后端返回 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.msg || 请求失败)) }, error { return Promise.reject(error) } )这个封装换成一句话就是别在每一个页面重复写“如果没登录就跳登录页”。登录是你的事情Token是你的事情跳转也是你的事情页面只负责拿到数据。4.3 路由history还是hash本地跑先别纠结部署才重要用户端路由一般长这样/home首页、/product/:id详情、/cart购物车、/order/confirm确认订单、/order/list订单列表。管理后台走/dashboard、/product/list、/product/edit/:id、/order/list。注意这里有个区分前台路由很多页面需要登录后才能访问可以在前置守卫里判断Tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })如果部署时用history模式服务器必须把所有请求都重写到index.html否则用户刷新详情页会404。本地调试阶段直接用hash模式最省心这也是我在本地跑源码时建议的做法。4.4 购物车交互前端只负责视觉和临时改数后端才是权威用户端的购物车是改动频率很高的模块加购、改数量、勾选、删除都要立刻刷新页面总价。很多初学者喜欢在前端维护一个全局数组加购、改数量只改数组结果一刷新页面就全没了。正确思路是保证前端每一次操作都调用后端接口页面展示的数据来自后端返回结果。加购后重新拉购物车列表修改数量后重新计算总价不要自己在前端写一套“伪购物车”逻辑。这样做的另一个好处是用户换一个设备登录购物车数据依然在。购物车表本身只是用户和商品之间的临时关系表它的存在是为了缓存用户的选购意图。真正影响库存和金额的永远是订单创建时的后端重算。4.5 图片上传本地存储路径别写死不然重启就丢文件管理后台添加商品时要用到图片上传。Element UI的el-upload组件把action指向后端接口后端用MultipartFile接收并保存到本地磁盘返回可访问的URL。这一步最常见的坑是源码把上传目录写死在SpringBoot项目内部比如src/main/resources/static/upload/。项目重启时运行目录被Maven clean掉图片全部消失。比较稳妥的做法是把上传路径配置到外部比如D:/fruit-mall/upload/或者配置一个OSS存储。修改上传路径后记得同步配置一个静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file:D:/fruit-mall/upload/); } }前端展示图片时注意接口返回的URL如果相对路径要跟后端域名拼接很多页面图片裂开不是上传问题是拼接问题。5. 我实际跑通这套系统时踩过的坑以及源码调试建议5.1 数据库脚本导入和驱动匹配先于一切代码问题这套系统的数据库初始化脚本一般叫fruit_mall.sql或者db.sql。导入前先确认一件事脚本是给MySQL5.7还是8.0写的。MySQL8.0之后驱动类名变成了com.mysql.cj.jdbc.Driver连接串里通常要加上serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。如果连接串里没有allowPublicKeyRetrievaltrue部分环境启动时会报Public Key Retrieval is not allowed这个问题不是源码坏了是MySQL8默认认证插件导致的。让我头疼过的另一个问题是时区数据库排查和前端时间不一致基本就是连接串没写serverTimezone。这里放一个能用于本地开发环境的配置模板直接照抄也行server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/fruit_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.fruit.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl file: upload-dir: D:/fruit-mall/upload/5.2 前后端联调的跨域问题本地开发建议用代理而不是到处加CrossOrigin用户端在localhost:8081后端在localhost:8080端口不同就产生跨域。很多源码为了省事直接在Controller上写CrossOrigin这样做本地跑通了但后端接口等于对全网开放生产环境肯定不行。我个人的习惯是本地联调时通过前端开发服务器的代理转发请求// vue.config.js module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这样前端代码里请求写/api/user/login代理会把它转发到后端http://localhost:8080/user/login前端视角完全没有跨域问题。如果后端接口本身没有/api前缀就用pathRewrite去掉。5.3 启动顺序MySQL先起再后端再前端别乱跑这套系统的启动顺序建议是启动MySQL确认数据库脚本导入成功。可以在Navicat或命令行执行SHOW TABLES;验证。启动后端SpringBoot看控制台日志最后一行出现Started Application才算成功。启动前端npm install之后再npm run serve或npm run dev。如果后端没有任何报错但一直卡住不动大概率是数据库连接没成功MySQL没启动或者密码不对。如果前端请求显示404先看后端接口有没有/api前缀再看代理转发配置是否和接口路径匹配。这个排查顺序能解决百分之九十的“源码跑不起来”。5.4 前端依赖安装的版本坑node-sass永远是最痛的那个Vue2项目常见的node-sass安装失败核心是Node版本和node-sass版本不兼容。比如Node 18配node-sass 4.14.1基本上必失败。处理办法是换源或者换依赖版本。我建议直接改用sass也就是dart-sassnpm install sass1.70.0 sass-loader8.0.2 --save-dev如果项目引用了大量老式deep语法可能还要调整。另外npm install报权限错误时用管理员终端或者给项目目录设置写权限别用--force硬装。如果网络不稳定换镜像源npm install --registryhttps://registry.npmmirror.com这一套组合拳下来前端依赖基本能顺利装完。5.5 修改扩展建议答辩亮点可以从这三个方向加如果这个项目是你毕业设计或课程设计只是在原始源码上跑通缝合答辩其实很吃力。我建议在吃透源码后挑一个方向做增量开发工程量不大但效果很直观给商品模块加“收藏”功能用户端展示我的收藏列表后台能统计收藏次数。给订单加“取消订单”和“订单倒计时关闭”演示定时任务或延迟消息的用法。给首页加“热销水果排行榜”练习复杂SQL和Redis缓存。这些方向都能基于现有表结构做小改动老师一看就知道你不是只会抄代码。把飘香水果改成零食、鲜花、图书也是同样的套路换汤不换药。说回这套“飘香水果购物网站管理系统”它最大的价值不是“2025最新”这个标签而是把前后端分离项目的完整闭环跑通了一遍。从商品上架到用户下单从购物车到订单明细从后台发货到前台查单整个业务流在代码里能清晰找到对应位置。想真正吃透它不要只看演示视频要动手把SQL脚本导进去然后拿一个水果从商品详情页开始下单一点点看后端日志你会比看十遍教程收获都大。最后再提醒一句源码里的数据库密码、Token密钥、上传路径这些默认配置自己用之前一定要改掉别图省事留在那里。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Claude Code 终于能帮你剪视频了:开源 BeatDesign 的本地画布与时间线 2026/9/30 16:34:01

Claude Code 终于能帮你剪视频了:开源 BeatDesign 的本地画布与时间线

Claude Code 终于能帮你剪视频了:开源平替版 Higgsfield,本地画布 时间线一条龙 你的 AI Agent 能帮你写完一整个 App,却剪不了一条 30 秒的视频。 它看不到你的画布,也碰不到你的时间线。于是你又回到老路:提示词写…

阅读更多 →
深度学习入门指南:从神经网络原理到CNN实战与避坑手册 2026/9/30 16:34:01

深度学习入门指南:从神经网络原理到CNN实战与避坑手册

算起来,我接触神经网络和深度学习也有不少年头了。还记得最早的时候,很多人觉得这是个玄学,调参像炼丹,模型跑起来像黑盒。这几年深度学习已经完全不一样了,它渗透到了图像识别、语音合成、工业检测、流体仿真、量化交…

阅读更多 →
2026 企业 AI 办公工具选型指南:怎么匹配业务场景 2026/9/30 16:34:01

2026 企业 AI 办公工具选型指南:怎么匹配业务场景

一、企业选AI办公工具,为什么不能只看功能列表 很多企业在启动AI办公工具选型工作时,第一反应是拉取不同产品的公开功能清单,逐条比对条目数量,把功能覆盖的广度当成选型的核心判断标准。也有不少团队会直接参考公开的价格排序&am…

阅读更多 →
Jev平替GPT?三天接入踩坑复盘与分级处理降本策略 2026/9/30 16:33:53

Jev平替GPT?三天接入踩坑复盘与分级处理降本策略

1. 为什么我鬼迷心窍,非要把 Jev 当成“穷人版 GPT”接进来 先交代一下背景。当时项目组里正好在做一个内部工单助手,主要场景是让模型帮我们解析用户反馈、抽取关键信息、顺带给客服回话打个草稿。按当时的算力预算和成本模型,如果直接上 GP…

阅读更多 →
AI信息图生成实战:Qwen-Image-2.1提示词模板与本地部署 2026/9/30 16:33:53

AI信息图生成实战:Qwen-Image-2.1提示词模板与本地部署

信息图生成,一直是图像生成模型绕不开的硬骨头。文字渲染、版式布局、数据对齐,哪一样要求都不低,很多模型画风景画人物很顺手,一到带文字的信息图就露馅。Qwen-Image-2.1出来之后我实测了很久,发现这个模型在中文文字…

阅读更多 →
如何为手机AI应用构建测试体系:Off Grid AI三平台(Jest+JUnit+XCTest)测试矩阵实践 2026/9/30 16:33:53

如何为手机AI应用构建测试体系:Off Grid AI三平台(Jest+JUnit+XCTest)测试矩阵实践

如何为手机AI应用构建测试体系:Off Grid AI三平台(JestJUnitXCTest)测试矩阵实践 【免费下载链接】OGAM The Swiss Army Knife of Offline AI. Chat, see, speak, and generate images on your phone or Mac — GGUF LLMs, vision, Whisper s…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉