☰
SpringBoot 2.7 + Vue 2 商城实战:从环境搭建到订单闭环
2026/10/9 3:19:54 网站建设 项目流程

简介:这是一套面向Java全栈初学者与课程设计学生的完整网上购物商城系统实现,基于Spring Boot后端框架、Vue.js前端框架与MySQL关系型数据库构建,覆盖需求分析、模块设计、前后端联调到系统测试全流程,可直接用于毕业设计、实训项目或技术栈整合学习。资源包共774个文件,19.59MB,包含121个Java核心业务类(含Controller/Service/Entity)、46个Vue组件(如IndexHeader.vue、update-password.vue等)、153个JS交互逻辑脚本、44个CSS样式文件及配套SQL建表语句、运行批处理脚本(.bat)和静态资源(SVG/IMG),结构清晰、注释规范,便于理解MVC分层与前后端分离开发模式。已有84人学习下载,提供开箱即用的源码工程、配套论文文档及合理UI布局,助读者快速掌握电商系统核心功能(用户管理、商品展示、购物车、订单流程)与主流技术协同实践。

1. 这不是又一个“Hello World”商城:它能跑通从用户注册、商品浏览、下单支付到后台订单管理的完整闭环,且所有代码可直接导入 IDEA 和 VS Code 调试——适合毕业设计硬过稿、中小团队快速验证 MVP、或 SpringBoot + Vue 双栈新手建立真实项目肌肉记忆

你可能已经点开过十几个标着“SpringBoot+Vue商城”的 GitHub 仓库,结果发现:前端 npm run serve 启动报错failed to load tsconfig '@vue/tsconfig/tsconfig.web.json': tsconfig not found;后端启动卡在Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;数据库脚本执行失败,提示Unknown column 'create_time' in 'field list';更别提论文部分只有 Word 标题页和三行摘要。这份资源不是那种“概念演示型”工程——它基于 SpringBoot 2.7.18(非 3.x,避开了 Jakarta EE 9+ 的包名迁移雷区),Vue 2.6.14(非 Vue 3 Composition API,不依赖 Vite 或 TypeScript 强约束),MySQL 5.7 兼容脚本,且包含一份 128 页、查重率低于 18% 的完整本科毕业论文(含需求分析 UML 图、ER 图、系统测试用例表、部署截图)。它解决的不是“能不能跑”,而是“能不能交、能不能改、能不能上线前小范围试跑”。如果你正卡在毕设开题答辩前两周、外包客户催着看原型、或想用真实电商链路反向吃透 MyBatis 多表关联与 Vue 路由守卫的协同逻辑——这份源码就是你该立刻解压、建库、改配置、跑起来的那个。


2. 从零拉起服务:环境准备、数据库初始化与前后端联调的三步落地法

2.1 环境版本锚定:为什么必须用 SpringBoot 2.7.18 + Vue 2.6.14 + MySQL 5.7?

这不是保守,是血泪经验换来的兼容性锁死。SpringBoot 2.7.x 是最后一个默认使用javax.*包(而非jakarta.*)的稳定大版本,这意味着你的pom.xml中所有 starter(如spring-boot-starter-jdbc、mybatis-spring-boot-starter)无需修改包路径即可对接 MySQL 5.7 驱动(mysql-connector-java:8.0.33)。而 Vue 2.6.14 是 Vue CLI 4.5.15 生态的成熟终点——它不强制要求@vue/composition-api插件,vue-router 3.5.3与vuex 3.6.2的组合能稳定支撑购物车状态持久化与登录拦截。若强行升级到 SpringBoot 3.x,你会立刻撞上DataSourceProperties类被移至org.springframework.boot.jdbc包下,且spring-boot-starter-web默认启用 Jakarta Servlet API,导致web.xml风格的 Filter 配置全部失效;若用 Vue 3,<router-link>的to属性写法、this.$router.push()的参数结构、甚至v-model在表单元素上的绑定逻辑都会重构,而本项目论文中所有流程图、代码截图均基于 Vue 2 语义。所以,请严格按以下命令校验:

# 检查 Java 版本(必须为 JDK 8u291 或 JDK 11.0.16,SpringBoot 2.7 不支持 JDK 17+) java -version # 输出应为:openjdk version "11.0.16" 2022-07-19 # 检查 Node.js 版本(Vue CLI 4 要求 Node.js 10.13+,但 16.x 最稳) node -v # 输出应为:v16.20.2 # 检查 MySQL 版本(5.7.44 是本项目 SQL 脚本实测通过的最高小版本) mysql --version # 输出应为:mysql Ver 14.14 Distrib 5.7.44, for Linux (x86_64) using EditLine wrapper

提示:若你已装有 MySQL 8.0,请勿卸载重装。本项目提供mysql57_compat_mode.sql脚本,执行SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));即可临时兼容 5.7 语法,避免GROUP BY报错。这是比重装更安全的生产妥协方案。

2.2 数据库初始化:四张核心表(user、product、order、order_item)的手动建库与字符集纠偏

项目根目录下的sql/gouwu_db.sql并非一键执行就能成功。MySQL 5.7 默认sql_mode含STRICT_TRANS_TABLES,而脚本中user表的create_time字段定义为datetime NOT NULL DEFAULT '0000-00-00 00:00:00',会触发严格模式拒绝。正确流程是分三步:

  1. 创建数据库并显式指定字符集(关键!避免中文乱码):

    CREATE DATABASE gouwu_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE gouwu_db;
  2. 关闭严格模式(仅本次会话):

    SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
  3. 逐段执行建表语句(不要全选复制粘贴,防编码污染):

    -- 先建 user 表(含密码加密 salt 字段) CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'BCrypt 加密后的密码', `salt` varchar(32) NOT NULL COMMENT '随机 salt', `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1-启用,0-禁用', `create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 再建 product 表(注意 price 是 decimal(10,2),非 float) CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(200) NOT NULL COMMENT '商品名称', `description` text COMMENT '商品描述', `price` decimal(10,2) NOT NULL COMMENT '售价', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `category_id` bigint(20) NOT NULL COMMENT '分类ID', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1-上架,0-下架', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

参数说明:utf8mb4是必须项,它支持 emoji 和四字节 UTF-8 字符(如某些生僻汉字),而旧utf8在 MySQL 中实际只支持三字节;decimal(10,2)确保价格计算无浮点误差,CURRENT_TIMESTAMP作为默认值比'0000-00-00 00:00:00'更符合 5.7 语义。

2.3 后端启动:application.yml 配置项详解与三个必改字段

backend/src/main/resources/application.yml是整个 SpringBoot 工程的命门。以下三项不改,服务必然启动失败:

配置项原始值必须改为原因
spring.datasource.urljdbc:mysql://localhost:3306/gouwu_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaijdbc:mysql://127.0.0.1:3306/gouwu_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=falselocalhost在某些 Linux 环境下解析慢,127.0.0.1强制走 IPv4;allowPublicKeyRetrieval=true解决 MySQL 8.0+ 驱动连接 5.7 时的公钥获取异常;useSSL=false关闭 SSL(本地开发无需)
spring.redis.hostlocalhost127.0.0.1同上,避免 Redis 连接超时
file.upload-dirD:/gouwu/upload//home/yourname/gouwu/upload/(Linux)或C:/gouwu/upload/(Windows)上传图片的物理路径,必须存在且有读写权限,否则商品图片上传直接 500

启动命令(确保在backend目录下执行):

# 方式一:Maven 命令行启动(推荐,跳过 IDE 缓存干扰) mvn spring-boot:run -Dspring.profiles.active=dev # 方式二:IDEA 中右键运行 Application.java(需先设置 Run Configuration → Active profiles = dev)

启动成功标志:控制台末尾出现Tomcat started on port(s): 8080 (http)且无ERROR红字。此时访问http://localhost:8080/swagger-ui.html应能看到完整的 REST API 文档(含/api/user/login、/api/product/list等接口)。

2.4 前端启动:Vue CLI 4 的依赖安装与代理配置穿透

前端位于frontend目录。Vue 2 项目不依赖vite.config.ts,核心配置在vue.config.js中。重点看devServer.proxy:

// frontend/vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, // 修改请求头 origin 为 target pathRewrite: { '^/api': '/api' // 前端请求 /api/user/login,代理到 http://localhost:8080/api/user/login } } } } }

逻辑说明:Vue 开发服务器(8081)本身不处理/api请求,而是将所有以/api开头的请求,通过 HTTP 代理转发给后端(8080)。changeOrigin: true是关键,它让后端HttpServletRequest.getRemoteAddr()能正确拿到真实客户端 IP,而非代理服务器 IP;pathRewrite确保路径不被重复拼接(如不配置此项,请求会变成http://localhost:8080/api/api/user/login)。

安装依赖并启动:

cd frontend # 清理 node_modules(若之前安装失败过) rm -rf node_modules package-lock.json # 使用淘宝镜像加速(国内必备) npm config set registry https://registry.npmmirror.com npm install npm run serve

启动成功标志:终端输出App running at:后跟Local: http://localhost:8081/,浏览器打开该地址,应看到首页轮播图与商品列表——此时前端已通过代理调用后端接口,数据真实来自 MySQL。


3. 核心业务链路拆解:用户登录态管理、购物车持久化与订单生成的三层穿透

3.1 登录态设计:JWT Token + Redis 双存储的可靠性权衡

本项目未采用 Spring Security OAuth2 的重型方案,而是用jjwt库手写 JWT 生成,并将 Token 存入 Redis 做二次校验。这不是偷懒,而是为毕业设计可控性与答辩可解释性服务:

  • JWT Payload 设计(com.gouwu.util.JwtUtil.java):

    public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(username) // 主体:用户名 .claim("userId", userId) // 自定义声明:用户ID .setIssuedAt(new Date()) // 签发时间 .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 24小时过期 .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // HS256 签名 .compact(); }

    参数说明:SECRET_KEY定义在application-dev.yml中(如secret-key: gouwu-2024-springboot-vue),必须与前端解码密钥一致;setExpiration设为 24 小时,避免用户频繁登录,但又不会永久有效(安全底线)。

  • Redis 存储逻辑(com.gouwu.service.impl.UserServiceImpl.java):

    // 登录成功后,将 token 存入 Redis,key 为 "token:" + userId,value 为 token 字符串,过期时间 24 小时 redisTemplate.opsForValue().set("token:" + user.getId(), token, 24, TimeUnit.HOURS);

    为什么双存?JWT 本身可校验签名与过期,但无法主动作废(如用户点击“退出登录”)。Redis 存储提供了DEL token:123的即时吊销能力。前端退出时调用/api/user/logout接口,后端执行redisTemplate.delete("token:" + userId),下次该 Token 被携带过来时,后端先查 Redis,发现 key 不存在则拒绝访问——这就是“主动登出”的技术实现。

3.2 购物车:Session 还是 Redis?本项目选择后者并解决并发覆盖问题

购物车数据不存 Session(因前端是独立域名,跨域无法共享 Cookie),也不存前端 localStorage(易被清除且多端不同步),而是存 Redis,Key 设计为cart:${userId},Value 为 JSON 字符串:

{ "1001": {"productId": 1001, "quantity": 2, "selected": true}, "1002": {"productId": 1002, "quantity": 1, "selected": false} }

关键在于并发安全:当用户在两个标签页同时增加同一商品数量时,quantity不应被后写的请求覆盖。解决方案是RedisTemplate的opsForHash()原子操作:

// com.gouwu.service.impl.CartServiceImpl.java public void addCart(Long userId, Long productId, Integer quantity) { String cartKey = "cart:" + userId; // 1. 先查当前数量 Object existQty = redisTemplate.opsForHash().get(cartKey, productId.toString()); Integer newQty = (existQty == null) ? quantity : (Integer) existQty + quantity; // 2. 原子写入(HSET 是原子的) redisTemplate.opsForHash().put(cartKey, productId.toString(), JSON.toJSONString(new CartItem(productId, newQty, true))); // 3. 设置过期时间(30天,避免 Redis 内存泄漏) redisTemplate.expire(cartKey, 30, TimeUnit.DAYS); }

避坑点:opsForHash().put()是原子的,但get()+put()组合不是。本代码虽看似两步,但existQty查询的是旧值,newQty计算后直接put,没有中间状态竞争。真正高并发场景应改用 Lua 脚本,但本项目 QPS < 100,此方案足够。

3.3 订单生成:分布式事务的降级方案——本地消息表 + 定时补偿

订单创建涉及三个强一致性操作:1)扣减商品库存;2)清空购物车;3)插入订单主表与订单项表。SpringBoot 未引入 Seata 或 RocketMQ,而是用本地消息表 + 定时任务实现最终一致性:

  • 消息表结构(msg_log):

    CREATE TABLE `msg_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `msg_id` varchar(64) NOT NULL COMMENT '消息ID(UUID)', `content` text COMMENT '消息内容(JSON)', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-待发送,1-已发送,2-发送失败', `try_count` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数', `next_retry` datetime DEFAULT NULL COMMENT '下次重试时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_msg_id` (`msg_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表';
  • 订单创建主流程(OrderController.createOrder()):

    @Transactional // 保证本地 DB 操作原子性 public Result createOrder(@RequestBody OrderRequest request) { // 1. 扣库存(MyBatis XML 中用 <selectKey> 获取自增 ID) productMapper.reduceStock(request.getProductId(), request.getQuantity()); // 2. 插入订单(order表) orderMapper.insert(order); // 3. 插入订单项(order_item表) orderItemMapper.insertBatch(orderItems); // 4. 写入本地消息表(标记为待发送) msgLogMapper.insert(new MsgLog(UUID.randomUUID().toString(), JSON.toJSONString(request), 0, 0, new Date())); return Result.success("订单创建成功,支付链接已生成"); }
  • 定时补偿任务(MsgLogTask.java,每 5 分钟扫描):

    @Scheduled(cron = "0 0/5 * * * ?") public void checkAndSend() { List<MsgLog> pending = msgLogMapper.selectByStatus(0); for (MsgLog log : pending) { try { // 调用支付网关(模拟:发 MQ 或调第三方 API) payGateway.send(log.getContent()); log.setStatus(1); // 标记已发送 } catch (Exception e) { log.setTryCount(log.getTryCount() + 1); if (log.getTryCount() >= 3) { log.setStatus(2); // 标记失败,人工介入 } else { log.setNextRetry(DateUtil.offsetMinute(new Date(), 5)); } } msgLogMapper.updateById(log); } }

价值点:这比 “全部放一个 @Transactional 里等超时回滚” 更可靠。库存扣减失败,订单不创建;订单创建成功但消息发送失败,定时任务会重试,直到成功或人工介入。这是中小项目最务实的分布式事务解法。


4. 避坑指南:五个让开发者凌晨三点还在抓头发的真实问题与解法

4.1 现象:前端npm run serve报错failed to load tsconfig '@vue/tsconfig/tsconfig.web.json': tsconfig not found

原因:Vue CLI 4.5.15 默认不生成tsconfig.json,但某些插件(如@vue/eslint-config-typescript)错误地尝试加载它。本项目是纯 JavaScript 项目,根本不需要 TypeScript 配置。
解决:删除frontend目录下所有tsconfig*文件(包括tsconfig.json、tsconfig.web.json),并检查package.json中是否误引入了@vue/eslint-config-typescript。若存在,执行npm uninstall @vue/eslint-config-typescript --save-dev。然后npm run serve重试。

4.2 现象:后端启动时报Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties

原因:pom.xml中spring-boot-starter-jdbc版本与 SpringBoot 2.7.18 不匹配,或 Maven 本地仓库损坏。常见于从其他项目 copypom.xml时未更新<parent>的version。
解决:确认pom.xml顶部<parent>块为:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>

然后执行mvn clean compile -U(-U强制更新快照依赖),再mvn spring-boot:run。

4.3 现象:登录成功后,前端路由跳转到/user/profile,但页面空白,控制台报TypeError: Cannot read property 'username' of undefined

原因:Vuex store 中userstate 初始化为空对象{},但组件UserProfile.vue在mounted()中直接访问this.$store.state.user.username,此时异步的getUserInfo()action 尚未返回。
解决:在UserProfile.vue的computed中添加空值判断:

computed: { ...mapState(['user']), username() { return this.user && this.user.username ? this.user.username : '游客'; } }

并在mounted()中加 loading 状态:

mounted() { this.$store.dispatch('getUserInfo'); this.loading = true; }, watch: { 'user.id'(newVal) { if (newVal) this.loading = false; } }

4.4 现象:MySQL 执行gouwu_db.sql时卡在Creating table product...,无报错也无进度

原因:MySQL 5.7 默认innodb_lock_wait_timeout=50秒,而建表语句中ENGINE=InnoDB需要获取元数据锁,若此时有长事务(如未提交的UPDATE user SET status=0)持有表锁,则建表会被阻塞。
解决:登录 MySQL,执行SHOW PROCESSLIST;查看是否有State为Locked或Waiting for table metadata lock的线程,记下其Id,然后KILL Id;强制终止。再重新执行建表。

4.5 现象:支付成功后,订单状态仍是 “待支付”,payCallback接口从未被调用

原因:本项目模拟支付,PayController.callback()接口路径为/api/pay/callback,但前端OrderConfirm.vue中发起支付请求时,URL 写成了/api/pay/notify(笔误)。
解决:打开frontend/src/api/pay.js,找到payCallback方法,将url: '/api/pay/notify'改为url: '/api/pay/callback'。这是典型的前后端联调命名不一致问题,建议用 Swagger 文档 URL 作为唯一信源。


5. 论文写作与系统验证:如何把源码跑通的过程,直接转化为毕业论文的“系统测试”章节

5.1 测试用例设计:用 Postman 脚本生成可复现的测试证据链

毕业论文的“系统测试”章节常被批“只有截图,没有过程”。本项目提供postman_collection.json(位于docs/目录),内含 12 个可一键运行的测试场景,每个场景包含请求、断言、环境变量。例如“用户登录测试”:

步骤Postman 操作论文可直接引用的描述
1POST /api/user/login,Body:{"username":"admin","password":"123456"}发送标准登录请求,验证认证接口可用性
2在 Tests 标签页写断言:
pm.test("Status code is 200", function () { pm.response.to.have.status(200); });
pm.test("Response has token", function () { pm.expect(pm.response.json()).to.have.property('token'); });
断言响应状态码为 200 且返回 JSON 中包含token字段,证明 JWT 生成成功
3在 Tests 中提取 token:
const jsonData = pm.response.json();
pm.environment.set("auth_token", jsonData.token);
将返回的 token 存入 Postman 环境变量,供后续接口复用

论文写作技巧:在论文中不必贴全部 Postman 截图,而是用表格呈现。例如“表 4-3 用户模块核心接口测试用例”:

用例编号接口路径输入数据预期输出实际结果通过/失败
UC-01POST /api/user/loginusername=admin, password=123456status=200, token 非空✅通过
UC-02GET /api/user/profileHeader: Authorization: Bearer {token}status=200, 返回用户信息✅通过

这样,答辩老师扫一眼表格,就知道你真测了,且过程可追溯。

5.2 性能验证:用 Apache Bench 模拟百人并发抢购,定位瓶颈点

毕业设计常被问“系统能扛多少人?”。不用云服务器,用本机ab命令即可验证。以商品秒杀为例(POST /api/order/create):

# 模拟 100 个用户,每个用户请求 10 次,总 1000 次请求 ab -n 1000 -c 100 -p order_data.json -T "application/json" "http://localhost:8080/api/order/create"

order_data.json内容:

{"productId":1001,"quantity":1}

关键输出解读:

  • Requests per second: 若 ≥ 80 req/s,说明单机 Tomcat + MySQL 5.7 能支撑小型活动;
  • Time per request (mean): 若 < 200ms,用户体验流畅;
  • Failed requests: 若 > 0,检查 MySQL 连接池(application.yml中spring.datasource.hikari.maximum-pool-size: 20是否够用);
  • Percentage of the requests served within a certain time:90%行若 > 500ms,说明有慢查询,用EXPLAIN SELECT * FROM product WHERE id=1001;查索引。

论文写作技巧:在“性能测试”小节,直接贴ab命令和关键输出表格,结论写:“在 100 并发下,平均响应时间 142ms,失败率 0%,满足日均 1 万订单的业务需求”。数据比形容词有力得多。

5.3 论文图表生成:PlantUML 自动生成 UML 图,避免 Visio 手动画图翻车

本项目docs/uml/目录下所有.puml文件,可用 VS Code 插件 PlantUML Preview 实时渲染为 PNG。例如user_login_sequence.puml:

@startuml title 用户登录时序图 actor User participant "Frontend" as FE participant "Backend" as BE participant "MySQL" as DB User -> FE: 输入账号密码 FE -> BE: POST /api/user/login BE -> DB: SELECT * FROM user WHERE username=? DB --> BE: 返回用户数据 BE -> FE: 返回 JWT Token FE --> User: 跳转至首页 @enduml

玄学经验:答辩 PPT 中的 UML 图,务必用 PlantUML 生成,而非截图。因为老师可能现场问“如果改成短信登录,这个时序图怎么改?”,你打开.puml文件,删掉SELECT * FROM user行,加上BE -> SMS: 发送验证码,30 秒重新渲染,PPT 就更新了——这比手动画图改 10 分钟强太多。

从那以后我每次写毕业设计,都强制走一遍 Postman 全链路测试 + ab 压测 + PlantUML 图表生成,哪怕多花两天,也比答辩时被问住当场编瞎话强。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询