SpringBoot2+Vue3+MySQL8.0爱心商城系统全栈开发与部署指南
发布时间:2026/9/26 16:55:04来源:尧图网络
如果把 Java Web 项目分成“能跑”和“能给别人看”两档爱心商城系统大概属于后者。这个项目用的是SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这一套目前很主流的全栈组合前后端分离代码里带了完整的数据库脚本和部署文档不是那种只写了一堆 CRUD 就来凑数的示例工程。我拿到这套源码之后先按文档把项目跑通然后把订单、购物车、商品管理、爱心积分这几条核心链路都过了一遍又把 MySQL8.0 在不同环境下的安装方式重新验证了一次。这篇文章不打算逐行念代码而是把技术选型、数据库设计、后端脚手架搭建、Vue3 商城联调、MySQL8.0 环境部署这条完整路线捋一遍。想拿这套系统练手、做课程设计或者准备改造成自己作品集的人按这个顺序走基本不会迷路。1. 为什么我把爱心商城定在 SpringBoot2 Vue3 这一套组合上1.1 从“爱心商城”的定位反推技术选型一个商城系统表面上看无非是商品、购物车、订单、支付、用户这些模块但它和普通后台管理系统最大的区别在于前端用户要持续浏览、下单、查看状态后端要处理复杂的订单逻辑和统计查询。这种场景下页面渲染和服务端接口的边界必须清晰所以第一版就不要考虑用 JSP 直接渲染页面了。爱心商城在普通商城基础上多了一层“爱心”属性比如爱心义卖商品、公益项目展示、捐赠记录、爱心积分。这就要求业务表设计里能区分普通商品和义卖商品也能把每一笔捐赠和订单关联起来。反过来看这种带有状态流转、金额计算、积分回写的系统正好适合用 SpringBoot2 做后端 API用 Vue3 做前端 SPA前后端通过 JSON 交互。技术选型不是看哪个新就选哪个而是看这个项目的生命周期和维护成本。SpringBoot2 到现在生态已经很稳定避开了 SpringBoot3 升级过程中那些兼容性折腾Vue3 的 Composition API 写商城页面的状态管理非常顺手MyBatis-Plus 能把单表 CRUD 压缩到极简MySQL8.0 在性能和 SQL 能力上都比 5.7 时代强不少。这一套组合放在一个中型项目里属于“下限很高、上限够用”的配置。1.2 和老牌 SSM JSP 方案比我为什么坚持前后端分离很多人在学习平台练过“Java Web”做出来的东西还是 Servlet JSP Tomcat 那一路。这种方案对入门很友好因为整个请求链路简单浏览器发请求服务端渲染 HTML再返回给浏览器。可一旦项目复杂度上来JSP 里混着 Java 代码和 HTML改个页面布局都可能牵动后端逻辑。我给爱心商城选型时做过一张对比表对比维度SSM JSP 单体渲染SpringBoot2 Vue3 前后端分离开发分工前端页面和后端逻辑耦合两边独立开发只认接口部署方式WAR 包扔 Tomcat前端静态资源 后端 Jar页面交互体验每次操作都可能整页刷新局部更新、组件化交互学习门槛入门低维护偏高起步要多学一点但长期收益高适合场景小型功能站商城、平台型系统、作品集项目选前后端分离还有一个现实理由如果你打算把这个项目放进简历面试官大概率会追问“Vue3 里怎么做路由守卫”“前端怎么处理 token 过期”“接口跨域你怎么解决”。这些经验在单体 JSP 项目里根本接触不到。1.3 MyBatis-Plus 和 MySQL8.0 在这一套里的位置有人可能会问既然都用上 SpringBoot2 了持久层为什么不用 Spring Data JPA区别很简单JPA 强在领域建模实体关系映射很省事但要写出复杂统计 SQL或者对某条慢查询做精细调优时你就得先去理解 Hibernate 的底层解读规则。MyBatis-Plus 是在 MyBatis 基础上做的增强单表 CRUD 不用写 SQL复杂查询继续写在 XML 里SQL 始终是你自己控制的出了问题很好排查。商城系统里订单统计、销售报表这种查询非常多用 MyBatis-Plus 心里更踏实。MySQL8.0 在这一套里的贡献也不小。默认字符集直接就是 utf8mb4支持窗口函数JSON 查询也更方便。如果你还在用 MySQL5.7建议尽早换后面会少很多字符集和 SQL 语法上的小问题。2. 爱心商城的业务模块拆解与数据库设计2.1 核心业务模块除了商品订单爱心属性落在哪我按最常见的爱心商城形态来拆模块拿到源码之后可以对照着找对应包路径用户端注册登录、商品分类浏览、商品详情、购物车、下单、订单列表、爱心积分查看、捐赠记录。管理端商品管理、分类管理、订单管理、爱心项目发布、公益文章发布、会员管理。“爱心”这两个字不是摆设。我在设计里是这样落地的商品表里加一个goods_type字段区分普通商品和爱心义卖商品义卖商品成交之后系统按商品设置的比例自动生成一条捐赠记录同时给用户增加对应的爱心积分。用户可以在个人中心看到自己贡献的积分和捐赠明细形成一条完整的业务闭环。这种设计比单纯把项目命名为“爱心商城”要有说服力得多。2.2 数据库表结构与 MySQL8.0 下的建表细节整个系统建议拆成这些核心表表名用途关键字段user用户表id、username、password、nickname、avatar、pointscategory商品分类id、name、sort、statusgoods商品表id、category_id、title、cover、price、stock、goods_typecart_item购物车项id、user_id、goods_id、quantity、checkedorder_info订单主表id、order_no、user_id、total_amount、statusorder_item订单明细id、order_id、goods_id、goods_title、price、quantitydonation_record捐赠记录id、order_id、user_id、amount、type、create_timearticle爱心文章/项目id、title、content、cover、status这里有个新手很容易踩的坑不要直接把 MySQL 的保留字当表名或字段名。最典型的就是orderMySQL 里它是关键字直接拿来当订单表名会遇到一堆语法问题。所以我一般用order_info、order_item这种名字。字段上类似desc、status如果非要用注意加上反引号但最稳妥的还是避开。建表时统一用这段规则CREATE TABLE goods ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, category_id bigint NOT NULL DEFAULT 0 COMMENT 分类ID, title varchar(100) NOT NULL COMMENT 商品标题, cover varchar(255) DEFAULT COMMENT 封面图, price decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 售价, stock int NOT NULL DEFAULT 0 COMMENT 库存, goods_type tinyint NOT NULL DEFAULT 0 COMMENT 0普通商品 1爱心义卖, deleted tinyint NOT NULL DEFAULT 0 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT商品表;MySQL8.0 建表有两个细节值得注意。一个是utf8mb4_0900_ai_ci是 8.0 的默认排序规则比 5.7 时代的utf8mb4_general_ci排序更准确另一个是 decimal 字段做金额不要用 float不然金额精度会出问题。时间字段建议统一用 datetime配合DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP这样 MyBatis-Plus 里都不用手动维护创建时间。2.3 订单状态和爱心积分流转的设计思路订单状态我建议用一组数字常量维护而不是直接存中文0 待付款、1 已付款待发货、2 已发货、3 已完成、4 已取消、5 退款中。服务端定义一个OrderStatus常量类所有判断走常量前端展示时再映射成文字。这样数据库里不存中文统计和筛选都方便。爱心积分的流转要注意“幂等”。比如一笔订单只能生成一次捐赠记录用户取消订单后对应积分要能回滚或作废。实现方式是在正则donation_record表里让order_id加唯一索引这样即使接口被重复调用数据库层面也不会产生重复数据。3. 后端落地SpringBoot2 工程结构与 MyBatis-Plus 的省力写法3.1 工程目录和统一返回结果后端工程可以按这个目录组织heart-mall-server/ ├── src/main/java/com/example/hearts │ ├── common/ // Result 统一返回、常量、异常 │ ├── config/ // WebConfig、MybatisPlusConfig、CORS 配置 │ ├── controller/ // 接口层 │ ├── service/ // 业务层 │ ├── mapper/ // MyBatis-Plus Mapper 接口 │ ├── entity/ // 数据库实体 │ └── HeartMallApplication.java ├── src/main/resources/ │ ├── mapper/ // 自定义 XML │ └── application.yml └── pom.xml接口返回值一定要统一。我习惯写一个ResultT包含code、message、data三个字段。前端 axios 里直接判断code 200即可不需要每个接口单独处理异常状态。全局异常处理可以用RestControllerAdvice把业务异常和系统异常分开处理这样返回给前端的错误信息才是人话而不是一堆堆栈。3.2 MyBatis-Plus 分页插件与条件构造器MyBatis-Plus 真正省时间的地方在于继承BaseMapperT之后selectById、insert、updateById、deleteById全都有了。但在 SpringBoot2 下使用分页必须配置分页插件否则Page返回的数据是假的所有记录都会被查出来。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }条件构造器是它的另一个核心能力。商城商品列表通常要根据关键词、分类、状态筛选用 LambdaQueryWrapper 可以写得很清晰LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 1) .eq(StringUtils.hasText(categoryId), Goods::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Goods::getTitle, keyword) .orderByDesc(Goods::getCreateTime); PageGoods page goodsMapper.selectPage(new Page(pageNum, pageSize), wrapper);这里的like前面加了条件参数StringUtils.hasText(keyword)意思是只有当 keyword 不为空时才拼接这个条件。这样的写法可以避免到处写if。3.3 Mapper XML 与 Mapper 接口同包的配置细节MyBatis-Plus 虽然能省很多 SQL但像订单报表、多表关联这类场景还是得写 XML。默认做法是把 XML 放在resources/mapper目录下然后在application.yml里配置mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true如果你非要让 XML 和 Mapper 接口放在同一个 Java 包目录下也能做到但需要先在pom.xml里把src/main/java下的 XML 文件作为资源打包build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build这样配置完SpringBoot 打包时才会把 Java 目录里的 XML 一起带到 classpath。但我个人仍建议放resources/mapper因为统一管理更直观IDE 默认也会把 resources 目录打进 classpath不会因为打包配置漏掉而出现“明明 XML 存在却报找不到 statement”的诡异问题。3.4 后端联调前的两个提醒第一逻辑删除字段和数据库唯一索引容易冲突。如果goods表里对某个业务字段加了唯一索引用户删除一条记录后又新增一条相同数据的记录逻辑删只是把deleted置为 1数据库里仍然存在旧记录唯一索引一样会拦截。解决思路是要么唯一索引里把deleted字段带进去要么删除时单独做状态判断。第二物理删除和逻辑删除不要混用既然开了全局逻辑删除写手写 SQL 时也要记得带上deleted 0条件建议所有自定义 SQL 里都补上。4. 前端 Vue3 商城实战从页面到接口联调要过的几条坎4.1 Vite 初始化与商城目录规划前端我用 Vite 来初始化npm create vitelatest heart-mall-web -- --template vue cd heart-mall-web npm install npm install axios vue-router4 pinia element-plusVite 的开发服务器启动速度比 Webpack 快很多这个项目里完全够用。拿到源码后先看src目录下有没有这几个模块views、components、api、router、store、utils。没有的话建议自己补上因为商城页面上“首页-商品列表-商品详情-购物车-订单结算-个人中心”这个链路没有清晰的目录划分会越写越乱。4.2 请求封装、接口地址和跨域处理Vue3 里连接后端最大的坑就是跨域。开发环境下最简单的办法是在vite.config.js里配 proxy让前端开发服务器把/api开头的请求转发到后端import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这里需要和后端约定接口前缀。如果后端所有接口都挂在/api下面前端就不做 rewrite如果后端没有/api就用上面这个写法把前缀剥掉。axios 封装建议统一放在utils/request.js里import axios from axios import { useRouter } from vue-router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) service.interceptors.response.use( res { const data res.data if (data.code ! 200) { // 提示错误信息 return Promise.reject(data) } return data }, err { if (err.response err.response.status 401) { // 跳转登录页 } return Promise.reject(err) } )这里有两个细节很多人会踩一是接口返回的 data 不要直接给页面塞一个巨大的 axios response 对象而是上面这样在拦截器里把res.data吐出去二是 401 处理一定要做否则 token 过期后页面只会静默失败用户完全不知道发生了什么。4.3 购物车状态管理Pinia 比 Vuex 更适合这个场景Vue3 里我强烈建议用 Pinia。它没有 mutations 概念store 里直接写 action开发体验很直白。商城购物车这种状态核心诉求是“跨页面共享”并且要在刷新后仍然存在所以要配合 localStorage 做持久化。import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: JSON.parse(localStorage.getItem(cartItems) || []) }), getters: { totalCount: (state) state.items.reduce((sum, i) sum i.quantity, 0), totalAmount: (state) state.items.reduce((sum, i) sum i.quantity * i.price, 0) }, actions: { addToCart(goods) { const found this.items.find(i i.goodsId goods.id) if (found) { found.quantity } else { this.items.push({ goodsId: goods.id, title: goods.title, price: goods.price, quantity: 1 }) } this.save() }, save() { localStorage.setItem(cartItems, JSON.stringify(this.items)) } } })注意一点从 localStorage 恢复数组时如果只做JSON.parse然后直接赋给 stateVue3 的响应式会把这个引用类型做响应式转换。但如果数据里有 undefined 或不合法的结构计算属性会出问题。建议在恢复时做一次数据清洗保证每条 item 都包含goodsId、title、price、quantity这几个字段。4.4 Vue3 组件和样式上常见的几个细节如果你在项目里用了 Element Plus 这类组件库自定义主题色或修改 Tabs 标签页样式时直接写 CSS 经常不生效因为组件样式用了 scoped。正确做法是用:deep()穿透.heart-tabs :deep(.el-tabs__item) { font-size: 16px; font-weight: 600; }另外Vue3 里v-model的机制和 Vue2 有区别组件自定义v-model:title这种多修饰符写法很常用。面试题里经常出现的computed也是商城项目里的高频点比如“购物车总价”就应该用computed而不是每次进页面重新计算一遍。自己在页面组件里也要注意不要在computed里做赋值操作否则会触发警告。5. MySQL8.0 环境搭建与项目跑通全过程5.1 Windows 下从零安装 MySQL8.0很多项目跑不起来不是代码问题是 MySQL8.0 压根没装对。Windows 下最简单的路径是下载 ZIP 包手动安装。第一步把mysql-8.0.x-winx64.zip解压到D:/mysql8。第二步在根目录新建my.ini[mysqld] basedirD:/mysql8 datadirD:/mysql8/data port3306 character-set-serverutf8mb4 default-authentication-pluginmysql_native_password第三步用管理员权限打开 CMD执行初始化命令mysqld --initialize-insecure注意--initialize-insecure会生成一个 root 空密码账号方便第一次进入。如果用了--initialize系统会生成一个随机临时密码藏在D:/mysql8/data下的.err日志文件里很多人没留意到结果卡在登录那一步。接着安装 Windows 服务并启动mysqld --install MySQL8 net start MySQL8 mysql -u root -p进入 MySQL 后改密码ALTER USER rootlocalhost IDENTIFIED BY 你的密码; FLUSH PRIVILEGES;5.2 Linux 和 Docker 场景下的 MySQL8.0 快速启动Linux 上如果不想手动下载 rpm/deb用 Docker 是最省事的方案docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEheart_mall \ -v /data/mysql8:/var/lib/mysql \ mysql:8.0这里有个关键点-v挂载目录一定要提前给好权限否则 MySQL 容器会因为/var/lib/mysql目录权限问题启动失败。执行前先mkdir -p /data/mysql8再给chmod 777或让 MySQL 镜像里的 mysql 用户能访问。容器起来之后要导入项目里的 SQL 脚本docker exec -i mysql8 mysql -uroot -p123456 heart_mall heart_mall.sql如果是服务器上部署还要记得在云安全组里放通 3306 端口否则本机 Navicat 永远连不上。5.3 连接 MySQL8.0 的两个经典报错用 Navicat 连接 MySQL8.0 时最常见的报错是Client does not support authentication protocol requested by server。原因是 MySQL8.0 默认认证插件改成了caching_sha2_password老版本 Navicat 或驱动不认识。解决办法有两个升级 Navicat 到新版或者把 root 账号改回兼容模式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;另一个问题是时区。JDBC 连接串里如果不带serverTimezone大概率会抛Server returns invalid timezone。建议在连接串里明确写jdbc:mysql://localhost:3306/heart_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai5.4 前后端跑通的完整顺序后端先启动会比较顺。确认数据库建好、SQL 导入成功后在heart-mall-server目录下执行mvn clean package -Dmaven.test.skiptrue java -jar target/heart-mall-server.jar前端执行npm install npm run dev浏览器打开前端地址接口能出数据说明整条链路已经通了。如果接口 504 或者看不到数据优先检查后端启动日志里有没有 MySQL 连接异常其次看浏览器 Network 面板里的请求有没有走到 Vite proxy 规则。6. 源码文档怎么读以及爱心商城还能往哪里扩展6.1 文档内容与源码阅读顺序这套项目带文档很多人拿到手后的第一步就是迷茫东西太多不知道从哪看。我的建议是“先看库再看接口最后看页面”。第一优先看 README 和部署文档确认 JDK、Maven、Node 版本要求。第二看 SQL 文件把所有表结构过一遍理解订单、商品、用户之间的关系。第三看后端application.yml确认数据库名、账号密码、端口是否都能对得上。然后从HeartMallApplication启动类出发按entity → mapper → service → controller看一遍商品和订单两条线。最后看前端views目录从首页组件开始沿着路由走到购物车和订单结算页。这样做的好处是你在看页面组件时已经知道后端接口大概长什么样遇到 axios 请求不会发懵。6.2 现有代码还能补强的几个方向我对这套源码最认可的是核心链路比较完整但要真正做到“作品集级别”还能在几个地方继续强化接口权限目前如果只靠拦截器校验登录状态管理端接口会偏弱。建议引入 Spring Security 或自己写一个简单的RequireRole注解把普通用户和管理员分开。参数校验后端入参尽量使用ValidatedNotBlank避免前端漏传参数时后端报一堆数据库异常。日志把操作日志中间件加上记录谁在什么时候改了商品价格这在商城项目里是硬需求。全文搜索商品表数量大了之后LIKE %关键词%会拖慢查询。初版可以先用 MySQL 的全文索引后面如果商品数据量再大再考虑接入 Elasticsearch 做商品搜索。6.3 从“商城”到“爱心社区”的扩展设想爱心主题其实有很大的延伸空间。比如加一个“公益项目募捐进度”页面每个项目关联一个目标金额用户可以用爱心积分或微信支付参与捐款加一个“爱心排行榜”按用户累计积分排名能有效带动平台活跃度也可以加一个志愿者活动报名模块把单纯的商品交易平台扩成公益社区。技术层面上的扩展也不复杂公益项目表可以复用article表结构加上目标金额、已筹金额、截止时间这几个字段募捐进度可以用定时任务统计。这样项目从“功能演示”变成“有真实业务深度的平台”参考价值会高很多。最后再分享一个我整理这套源码的个人体会真正决定项目完成度的往往不是某个炫技功能而是文档和代码的一致性。你拿到这套项目时建议先不要急着改功能而是把数据库脚本重新执行一遍再按文档把前后端跑通确认整条链路没有断点之后再开始改业务。这个习惯在项目里越早养成后面踩的坑就越少。
网站建设高端定制企业官网