拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3+MySQL商城源码:从环境搭建到订单落库全链路实战

SpringBoot+Vue3+MySQL商城源码:从环境搭建到订单落库全链路实战

简介:这是一套面向高校计算机专业毕业设计的商城购物网站完整项目源码,基于SpringBoot与Vue3.0技术栈,配合MySQL数据库实现,适合正在准备毕设或需要全栈实战练习的开发者参考。项目分为前台与后台两大模块:前台涵盖用户注册登录、商品查询与购买、订单管理、个人中心(地址与资料管理);后台包含用户管理、商品类型与商品管理、订单发货处理、评论管理及系统设置,功能链路完整,可直接作为毕设选题的落地实现。资源包共314个文件,以120个Java后端源码、30个Vue组件、90个CSS样式、25个JS脚本及14个XML配置为主,另含数据库脚本、图片与字体等静态资源,压缩包约910KB,结构清晰便于按模块阅读。目前已有184人学习下载,读者可据此快速理解前后端分离架构、接口设计与数据库表结构,并在此基础上完成二次开发与论文撰写。

1. 从一份能跑通的商城源码说起:SpringBoot+Vue3+MySQL 到底交付了什么

很多同学做毕业设计卡在同一个地方:需求文档写完了,ER 图画完了,真到敲代码时发现前后端联调、数据库建表、分页查询、订单状态流转这些环节一个都绕不过去。这份基于 SpringBoot + Vue3.0 + MySQL 的商城购物网站源码,解决的正是「从设计到能跑起来」这一段。它包含完整的 Java 后端源码、Vue3 前端工程和数据库脚本,覆盖商品展示、购物车、下单、用户管理等商城核心链路。适合正在做 JavaWeb 方向毕业设计、课程设计,或者想拿一个前后端分离项目练手 SpringBoot 和 Vue3 的开发者。你拿到手之后,重点不是从头读代码,而是先把它跑起来,再顺着一条业务线往下拆。

2. 环境准备与数据库落地:MySQL 建库、SpringBoot 配置、Vue3 启动顺序

2.1 三个组件的版本匹配与安装顺序

这套项目是典型的前后端分离结构,后端 SpringBoot 提供 REST 接口,前端 Vue3 通过 axios 调用,MySQL 存数据。跑起来之前,三个东西的版本要对上,不然会出现「后端能启动但前端 404」或者「数据库连不上」这类玄学问题。

常见做法是先装 JDK,建议 JDK 8 或 JDK 11,SpringBoot 2.x 对这两个版本兼容最好。然后是 MySQL,5.7 和 8.0 都能用,但要注意驱动类名和连接 URL 的差异。最后是 Node.js,Vue3 项目一般需要 Node 14 以上,推荐 16 或 18 的 LTS 版本。

安装顺序建议:JDK → MySQL → Node.js → 后端启动 → 前端启动。这个顺序不是随便定的,因为后端启动时会去连 MySQL,前端启动时会去调后端接口,前面的没起来,后面的必然报错。

提示:如果你之前装过其他版本的 JDK 或 MySQL,先确认环境变量指向的是当前要用的版本,java -version和mysql --version各跑一遍,别嫌麻烦。

2.2 数据库脚本导入与连接配置

数据库脚本一般是一个.sql文件,里面包含建库、建表、插入初始数据三部分。导入方式有两种:命令行和图形化工具。命令行更稳,图形化工具更直观,看你习惯。

用命令行导入的步骤:

# 登录 MySQL,输入密码后进入 mysql -u root -p # 创建数据库(如果脚本里没有 CREATE DATABASE 语句) CREATE DATABASE mall_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 切换到该数据库 USE mall_db; # 导入脚本,注意路径换成你本地的实际路径 source /path/to/your/mall.sql; # 验证表是否建好 SHOW TABLES;

如果你用 Navicat 或者 MySQL Workbench,操作更简单:新建连接 → 新建数据库 → 右键「运行 SQL 文件」→ 选择脚本 → 执行。执行完刷新一下,看到表列表就说明成功了。

导入之后要检查两件事:一是字符集,确保是utf8mb4,不然中文商品名会变问号;二是初始数据,有些脚本会插入管理员账号和测试商品,确认这些数据在不在,后面登录要用。

接下来改后端配置文件。SpringBoot 的数据库配置通常在application.yml或application.properties里,找到spring.datasource这一段:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码

几个参数说明一下。driver-class-name这一行,MySQL 8.0 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver,写错了启动直接报错。url里的serverTimezone必须加,不然 MySQL 8.0 会报时区异常。useSSL=false是关掉 SSL 连接,本地开发没必要开,开了反而容易出连接错误。characterEncoding=utf8保证中文不乱码。

注意:密码不要加引号,除非你的密码里真的有特殊字符。很多人在这里多写了引号,导致连接失败,排查半天。

2.3 后端启动与前端联调

数据库通了之后,启动后端。如果你用 IDEA,找到主启动类,右键 Run 就行。如果用命令行:

# 在项目根目录下,先打包再运行 mvn clean package -DskipTests # 运行 jar 包 java -jar target/mall-0.0.1-SNAPSHOT.jar

启动成功的标志是控制台出现Started Application in x.x seconds,并且没有异常堆栈。如果报Communications link failure,说明数据库没连上,回去检查 URL、用户名、密码和 MySQL 服务是否在运行。

后端起来之后,先别急着启动前端,用浏览器或者 Postman 访问一下后端接口,确认能返回数据。比如商品列表接口一般是/api/goods/list或者/goods/list,具体路径看 Controller 里的@RequestMapping。返回 JSON 数据就说明后端没问题。

前端启动:

# 进入前端目录 cd frontend # 安装依赖,国内建议换淘宝镜像 npm install --registry=https://registry.npmmirror.com # 启动开发服务器 npm run serve

启动后控制台会输出本地访问地址,一般是http://localhost:8080或http://localhost:8081。打开浏览器访问,如果页面正常渲染但数据加载不出来,按 F12 看 Network 面板,大概率是接口地址配错了。前端调用后端的地址通常在src/utils/request.js或者.env.development文件里,确认它指向的是后端实际运行的端口。

3. 商城核心业务链路拆解:从商品列表到订单落库的代码走读

3.1 商品模块:分页查询与条件筛选的实现

商品列表是商城的入口,也是最能体现前后端分离套路的模块。前端发请求带分页参数和筛选条件,后端用 MyBatis 或 MyBatis-Plus 查数据库,返回分页结果。

先看后端的分页查询。如果用 MyBatis-Plus,分页需要先配置拦截器:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页拦截器,指定数据库类型为 MySQL interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

这段代码的作用是告诉 MyBatis-Plus 怎么拼分页 SQL。没有这个配置,Page对象传进去也不会生效,查出来永远是全量数据。DbType.MYSQL指定了数据库方言,不同数据库分页语法不一样,写错了会报 SQL 语法错误。

Service 层的分页查询:

public Page<Goods> getGoodsPage(int pageNum, int pageSize, String keyword, Long categoryId) { // 构建分页对象,pageNum 是当前页码,pageSize 是每页条数 Page<Goods> page = new Page<>(pageNum, pageSize); // 构建查询条件 LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); // 如果有关键词,按商品名模糊匹配 if (StringUtils.hasText(keyword)) { wrapper.like(Goods::getName, keyword); } // 如果有分类筛选,按分类 ID 精确匹配 if (categoryId != null) { wrapper.eq(Goods::getCategoryId, categoryId); } // 按创建时间倒序排列 wrapper.orderByDesc(Goods::getCreateTime); return goodsMapper.selectPage(page, wrapper); }

Page对象封装了页码和每页条数,LambdaQueryWrapper用来动态拼条件。like是模糊查询,对应 SQL 的LIKE '%keyword%';eq是等值查询,对应=。orderByDesc按创建时间倒序,保证新商品排在前面。这些条件都是可选的,前端传了就加,没传就跳过,这样一套代码能覆盖多种筛选场景。

前端这边,Vue3 用ref或reactive管理分页状态,用 axios 发请求:

import { ref, onMounted } from 'vue' import axios from 'axios' const goodsList = ref([]) const pageNum = ref(1) const pageSize = ref(10) const total = ref(0) const keyword = ref('') const fetchGoods = async () => { try { const res = await axios.get('/api/goods/page', { params: { pageNum: pageNum.value, pageSize: pageSize.value, keyword: keyword.value } }) // 后端返回结构一般是 { code, data: { records, total } } goodsList.value = res.data.data.records total.value = res.data.data.total } catch (error) { console.error('商品列表加载失败', error) } } onMounted(() => { fetchGoods() })

params里的字段名要和后端 Controller 接收的参数名一致,不一致就传不过去。res.data.data.records这个路径取决于后端返回的 JSON 结构,有的是res.data.data.list,有的是res.data.rows,对着后端实际返回改。onMounted保证组件加载完就拉一次数据。

3.2 购物车与订单:状态流转和库存扣减

购物车模块的核心是把商品加进去、改数量、删掉,然后结算生成订单。订单模块的核心是状态流转:待付款 → 已付款 → 已发货 → 已完成,中间还可能插入取消和退款。

购物车加商品的逻辑:

public void addToCart(Long userId, Long goodsId, Integer num) { // 先查购物车里有没有这条商品 Cart existing = cartMapper.selectOne( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getGoodsId, goodsId) ); if (existing != null) { // 已有则累加数量 existing.setNum(existing.getNum() + num); cartMapper.updateById(existing); } else { // 没有则新增一条 Cart cart = new Cart(); cart.setUserId(userId); cart.setGoodsId(goodsId); cart.setNum(num); cart.setCreateTime(new Date()); cartMapper.insert(cart); } }

这段逻辑看着简单,但有个坑:并发情况下,同一个用户同时点两次「加入购物车」,可能查出两条都不存在,然后插入两条重复记录。常见做法是给user_id + goods_id加唯一索引,插入冲突时改成更新。这个在毕业设计里不一定要求,但面试被问到就是加分项。

下单时扣库存是另一个关键点:

@Transactional(rollbackFor = Exception.class) public void createOrder(Long userId, List<CartItem> items) { for (CartItem item : items) { // 扣库存,用乐观锁或条件更新防止超卖 int affected = goodsMapper.reduceStock(item.getGoodsId(), item.getNum()); if (affected == 0) { throw new RuntimeException("库存不足:" + item.getGoodsName()); } } // 创建订单主表和明细表 Order order = new Order(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setStatus(0); // 0 表示待付款 orderMapper.insert(order); // ... 插入订单明细 }

@Transactional保证扣库存和创建订单在同一个事务里,任何一步失败都回滚。reduceStock对应的 SQL 应该是UPDATE goods SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num},AND stock >= #{num}这个条件很关键,它保证库存不够时更新影响行数为 0,从而触发异常回滚。如果写成先查库存再更新,并发下就会超卖。

订单状态流转一般用一个status字段表示,配合枚举类管理:

public enum OrderStatus { UNPAID(0, "待付款"), PAID(1, "已付款"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); private final int code; private final String desc; // 构造方法和 getter 省略 }

状态变更时要做校验,比如只有待付款的订单能取消,只有已付款的能发货。这个校验放在 Service 层,别放 Controller,不然业务逻辑散得到处都是。

3.3 用户认证:登录态与接口鉴权

商城项目一般用 JWT 做登录态。用户登录成功后,后端生成一个 token 返回给前端,前端存到 localStorage 或 sessionStorage,之后每次请求在 header 里带上这个 token。

后端生成和校验 token:

// 生成 token public String generateToken(Long userId, String username) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24小时过期 .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } // 校验 token public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); }

SECRET_KEY要足够复杂,别用123456这种,不然 token 能被伪造。setExpiration设置过期时间,毕业设计里设 24 小时够用了。前端在 axios 拦截器里统一加 token:

axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config })

后端用一个拦截器或过滤器统一校验,放行登录和注册接口,其他接口都要验 token。这样就不用每个 Controller 方法里都写一遍校验逻辑。

4. 避坑与排查:跑不起来、数据不对、页面空白怎么查

4.1 后端启动报数据库连接失败

现象:启动时控制台报Communications link failure或Access denied for user。

原因:三种可能——MySQL 服务没启动、连接 URL 或端口写错、用户名密码不对。

解决:先确认 MySQL 在运行,systemctl status mysql或 Windows 服务列表里看。然后检查application.yml里的 URL,端口默认 3306,如果你改过要对应改。最后确认用户名密码,可以在命令行用mysql -u root -p手动登录验证。如果密码忘了,MySQL 8.0 可以用ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';重置。

4.2 前端页面空白或接口 404

现象:浏览器打开前端地址,页面一片空白,或者控制台报 404。

原因:前端依赖没装全、后端没启动、接口地址配错、跨域被拦。

解决:先看浏览器控制台有没有 JS 报错,有的话大概率是依赖问题,删掉node_modules重新npm install。如果 Network 里接口请求返回 404,检查前端配置的接口前缀和后端实际路径是否一致。如果报 CORS 跨域错误,后端加一个全局跨域配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns("*")在 SpringBoot 2.4 以上版本用,低版本用allowedOrigins("*")。allowCredentials(true)允许携带 cookie,如果前端用 token 放 header 里,这个可以不设。

4.3 中文乱码

现象:商品名、用户名显示成问号或乱码。

原因:数据库字符集不是utf8mb4,或者连接 URL 没加characterEncoding=utf8。

解决:先查数据库字符集SHOW VARIABLES LIKE 'character%';,确认character_set_database是utf8mb4。不是的话改数据库默认字符集,或者建库时指定。然后确认连接 URL 里有characterEncoding=utf8。如果还不行,检查前端页面<meta charset="utf-8">有没有。

4.4 分页查询返回全量数据

现象:传了pageNum和pageSize,但返回的还是所有数据。

原因:MyBatis-Plus 分页拦截器没配,或者配了但没生效。

解决:确认MybatisPlusConfig类被 Spring 扫描到,加了@Configuration注解。确认拦截器里DbType.MYSQL写对了。如果用的是 PageHelper,确认PageHelper.startPage(pageNum, pageSize)在查询方法之前调用,且只对紧接着的那一条查询生效。

4.5 订单重复提交

现象:用户快速点两次「提交订单」,生成了两笔一样的订单。

原因:前端没做防重复点击,后端没做幂等校验。

解决:前端在提交按钮上加loading状态,请求没回来之前禁用按钮。后端可以用订单号做唯一索引,或者用 Redis 加锁。毕业设计里前端加个disabled就够用了,但要知道后端幂等才是根本方案。

5. 二次开发与进阶:换数据库、加接口、做部署的实操建议

把项目跑通只是第一步,真正让它变成你的东西,得动手改。我一般会从三个方向入手:换数据库、加业务接口、做打包部署。

换数据库这件事,很多人觉得改个 URL 就行,其实不止。如果你从 MySQL 换到 PostgreSQL,驱动类名要改成org.postgresql.Driver,URL 格式变成jdbc:postgresql://localhost:5432/mall_db,分页拦截器的DbType也要改成DbType.POSTGRE_SQL。另外,MySQL 的LIMIT语法和 PostgreSQL 的LIMIT ... OFFSET虽然像,但 MyBatis-Plus 会自动适配,不用手写。建表语句里的AUTO_INCREMENT要改成SERIAL或GENERATED BY DEFAULT AS IDENTITY。这些细节不改,启动就报错。

加接口的套路是固定的:Controller 定义路由和参数 → Service 写业务逻辑 → Mapper 写数据库操作。比如加一个「商品收藏」功能,先建favorite表,然后写FavoriteController、FavoriteService、FavoriteMapper。Controller 里用@RestController和@RequestMapping注解,方法参数用@RequestParam或@RequestBody接收。Service 里注入 Mapper,用 MyBatis-Plus 的insert、delete、selectList等方法操作数据。Mapper 接口继承BaseMapper<Favorite>就自动有了基础 CRUD 方法,不用手写 XML。

打包部署这块,后端用mvn package打成 jar,前端用npm run build打成静态文件。部署时后端java -jar运行,前端静态文件放到 Nginx 的html目录下,Nginx 配置里加一个反向代理,把/api开头的请求转发到后端端口。这样前端和后端就通过 Nginx 串起来了,不用在代码里写死 IP 和端口。

server { listen 80; server_name your_domain; location / { root /var/www/mall-frontend; 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; } }

try_files这一行是给 Vue Router 用的,防止刷新页面出现 404。proxy_pass把/api请求转发到后端,注意末尾的/要不要加,加了会去掉/api前缀,不加会保留,根据后端接口路径决定。

验证部署是否成功,最简单的办法是:本地浏览器访问服务器 IP,能打开页面;点几个商品,数据能加载;登录、加购物车、下单走一遍,不报错。如果页面能开但接口 404,检查 Nginx 的proxy_pass和后端实际路径是否匹配。如果接口通了但数据不对,检查数据库连接和 Nginx 转发的 header 有没有丢。

从那以后我每次拿到一个前后端分离项目,都强制自己先跑通「登录 → 查列表 → 加购物车 → 下单」这条最短链路,再去看其他模块。这条链路通了,说明环境、数据库、接口、跨域、鉴权这些基础环节都没问题,剩下的就是业务逻辑的堆叠。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表