ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0校园店铺系统设计与实战解析

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0校园店铺系统设计与实战解析

很多同学拿到一套"Java Web 校园网上店铺设计与实现系统源码-SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0【含文档】"这种标题的项目,第一反应是导入IDE、跑起来、看页面。但真正自己动手从头搭一遍,或者在原有代码基础上改需求时,才发现里面的门道比想象中多不少。这篇文章我不打算复述一遍项目说明书,而是把这套技术栈组合背后"为什么要这么选"、数据库设计里那些容易忽略的决策点、前后端对接时的隐性约定、以及本地跑通和后续二次开发的实操经验,一次性讲透。

这篇文章适合谁看?两类人。一类是准备拿类似选题做课程项目或实训作业的同学,你需要的不只是能运行,而是能说清楚每个模块为什么这么设计;另一类是刚接触SpringBoot2+Vue3这套前后端分离方案的开发者,想借一个完整业务场景把主流工具串起来。无论哪类,下面这些内容都是我从实际搭建和改代码的过程中总结出来的,比单纯看源码里的注释要实用得多。

1. 为什么是这套技术组合:选型思路与版本匹配

1.1 从"跑得起来"倒推版本组合

任何项目第一步不是写代码,而是确定版本。很多拿到源码跑不起来的案例,八成不是代码问题,是版本不匹配。这套项目标得很清楚:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0。

先说SpringBoot2。现在SpringBoot3已经出了,但校园项目、教学项目、以及大量生产环境的老系统,主力依然是SpringBoot2。原因很实际:SpringBoot3基于Jakarta EE,很多老版本的第三方库没有跟进适配,尤其是一些生成验证码、导出Excel、权限框架的老版本starter,在SpringBoot3下会直接报ClassNotFound。SpringBoot2.7.x是2.x系列的最后一个大版本,稳定性好,资料最全,遇到问题几乎都能搜到答案,作为课程项目和中小型系统的最佳版本没有之一。

再说Vue3。现在新项目基本不会再开Vue2的坑了,Composition API加上Vite的构建速度,开发体验完全不一样。但要注意,Vue3搭配的UI库、路由、状态管理工具链,与Vue2时代有重大差异。如果源码里用的是Vue3 + Element Plus + Vue Router 4 + Pinia,这套组合已经是当前前端生态里最成熟的一套。

MySQL 8.0的选择更容易理解。MySQL 5.7虽然还很普及,但8.0的窗口函数、JSON能力、以及默认utf8mb4字符集,对这类含商品描述、订单备注的系统更合适。特别是8.0的caching_sha2_password认证方式,经常成为"本地连不上数据库"的头号原因,这个问题后面会细说。

1.2 MyBatis-Plus这个"顺手工具"选得聪明

很多同学一开始纠结用不用MyBatis-Plus。用MyBatis-Plus意味着CRUD基本不用写XML,但这套项目里依然保留了XML的mapper文件——这正是实际项目中真实的使用方式:单表操作用Plus自带的BaseMapper和Wrapper,多表关联、复杂统计才写自定义SQL。这个思路非常符合企业里"合理偷懒"的原则。

相比JPA,MyBatis-Plus的学习成本和可控性对课程项目更友好。JPA抽象度高,但一旦方法命名规则写错,生成的SQL和你预期不一致,排查成本很高;MyBatis-Plus则让你清楚看到SQL在做什么,出了问题好定位。相比原生MyBatis,Plus解决了大量的样板代码问题,分页插件也做得足够成熟,不需要自己封装拦截器。

版本对应关系是很多人忽视的坑:SpringBoot2.7.x时代建议用MyBatis-Plus 3.5.x;如果你拿到的是SpringBoot1.x的旧项目,就得用Plus的3.0以下版本。这套源码用SpringBoot2,如果自己新建项目引入Plus时版本选错,启动阶段就会出现各种莫名奇妙的Bean注册失败。

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>

1.3 前后端分离的通信约定

前后端分离项目,最难的不是某一个技术点,而是约定。这套系统无论是买家端、卖家端还是后台管理端,基本都是通过JSON交互,状态码、分页结构、返回格式这些约定,比框架本身更重要。

一个通用的返回结构一般长这样:

{ "code": 200, "message": "操作成功", "data": { "records": [], "total": 100, "current": 1, "size": 10 } }

没有统一返回结构的项目,前后端联调时每个人各写各的,前端拿数据要到处判断字段是否存在,是混乱的根源。这套源码里如果统一封装了Result对象,那所有Controller的返回值都走同一套标准,前端axios拦截器里只要处理一个code字段就够了。

2. 数据库先行:一个校园店铺需要哪些表和字段

2.1 表结构拆解:从用户到订单的完整链路

拿到项目第一件事,先打开数据库设计文档,看清楚拆了几张表。一个典型的校园网上店铺,核心表逃不过下面这几类:

  • 用户表(管理员、商家、普通用户三类角色如何区分)
  • 商品分类表(一级分类、二级分类)
  • 商品表(标题、描述、价格、库存、销量、上下架状态、图片)
  • 购物车表(哪个用户、哪个商品、数量)
  • 订单表(订单编号、总金额、状态、收货信息)
  • 订单明细表(订单关联了哪些商品、下单时的快照价格和名称)
  • 收货地址表
  • 轮播图表、公告表、评论表等辅助业务表

关键点来了:订单明细表必须存"下单那一刻"的商品名称和价格快照,而不是通过商品ID去关联查询。为什么?因为商品后期可能改名、改价、甚至下架删除,如果订单明细通过关联查询实时取名称价格,历史订单显示的内容就会"变"。这是很多新手设计表结构时最容易犯的错误。源码里只要做了快照字段,就说明设计者是想清楚了业务闭环的。

另一个容易忽略的是删除策略。商品表如果用户删除了,购物车里关联的商品怎么办?订单明细里的商品怎么办?实际项目里大多数不会做物理删除,而是用一个deleted、status之类的字段做逻辑删除,很多新手无法理解这一步是为了什么。MyBatis-Plus的@TableLogic注解就是为这个场景准备的。

@TableLogic private Integer deleted;

有了这个注解,所有Plus自动生成的查询都会自动追加AND deleted = 0的过滤条件,开发层面的删除操作变成了更新操作。这套源码如果在设计上有这个字段,说明表结构层就考虑了数据完整性和可追溯性。

2.2 金额和库存的两个"敏感区"

金额字段用decimal,不要用double或float。这个账很多人要算清楚:浮点数在计算机里存储的是近似值,0.1+0.2算出来不是0.3,这在商品价格这种需要精确到分的场景下非常致命。decimal(10,2)能存到千万级别的金额,对这个项目已经绰绰有余。

库存字段的设计思路也值得一提。下单时要判断库存是否充足,支付成功后扣减库存,取消订单要回补库存,这些操作不是一个简单的UPDATE goods SET stock = stock - 1就完事的,还需要加上WHERE stock >= 1这样的条件,防止超卖。哪怕校园项目的并发量不高,这样写也代表了一种严谨的设计习惯,面试或答辩时能讲得出这一层逻辑,比单纯说"我用了事务"更能体现水准。

下单扣库存的完整SQL写法一般长这样:

UPDATE goods SET stock = stock - #{quantity} WHERE id = #{goodsId} AND stock >= #{quantity};

如果返回的影响行数为0,说明库存不足,事务回滚。这个模式放在校园店铺里略显"杀鸡用牛刀",但这种防御性写法值得借鉴。

3. 后端落地:SpringBoot2工程结构与核心逻辑

3.1 为什么分层结构比"能跑"更重要

SpringBoot工程的标准分层是Controller、Service、Mapper,加上entity(或domain)、dto、vo、config、common、utils这些辅助包。初看觉得包多类多,实际写起来却发现每个类职责异常清晰:

  • Controller层:只做参数接收和结果返回,不写业务逻辑
  • Service层:事务的控制、业务规则的编排都在这里
  • Mapper层:数据库交互,配合MyBatis-Plus的BaseMapper
  • entity:对应数据库表
  • dto:接收前端传来的参数,可以按需组合多个实体的字段
  • vo:返回给前端的视图对象,隐藏不必要的字段

很多没经验的同学图省事,直接在Controller里写业务逻辑,甚至把SQL拼在Controller里。小项目跑起来的时候感觉不出差别,一旦需求变更,比如原来只查商品列表,现在要加一个"是否已收藏"字段,就会发现所有代码都耦合在一起,无从下手。这个项目的源码分包如果规整,阅读起来会非常舒适。

我当时看这套项目的代码,Controller统一返回Result,Service里处理了事务,Mapper的XML文件集中在resources下,是标准的企业项目风格。这种风格对于要从"能跑"进阶到"能讲清楚"的同学来说,是最好的学习样本。

3.2 登录鉴权:Session、Token与权限控制

这套项目要做到不同角色的用户看到不同界面、执行不同操作,身份认证是首先要解决的问题。常见的方案两种:基于Session的会话保持,和基于Token的无状态认证。

SpringBoot2时代最流行的无状态方案是JWT(JSON Web Token)。一个典型的JWT访问流程是这样的:

  1. 用户提交账号密码,后端校验成功后生成一个token
  2. token返回给前端,前端存储在localStorage或Pinia里
  3. 后续每次请求,前端在请求头带上Authorization: Bearer <token>
  4. 后端通过拦截器解析token,拿到用户ID和角色,判断是否有权限

好处是后端无需存储会话状态,水平扩展方便,对前后端分离项目特别友好。需要留意的是JWT本身有个特点:签发后无法主动失效,所以如果要支持"修改密码后强制下线"这类功能,就得配合token黑名单或Redis存储方案。大部分校园项目不会做这么深,能实现"登录后才能访问、管理员才能操作"就够用了。

实现层面,一般会写一个JwtInterceptor或HandlerInterceptor的实现类,注册到SpringMVC的拦截器链里,同时配置白名单,比如登录接口、注册接口、商品列表接口这些不需要登录就能访问的路径。

registry.addInterceptor(jwtInterceptor) .addPathPatterns("/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/goods/**");

一个值得注意的细节是跨域。前后端分离部署,前端运行在5173端口,后端运行在8080端口,浏览器会拦截跨域请求。SpringBoot里一般用CorsFilter统一处理,或者用@CrossOrigin注解。如果前端用Vite开发服务器,也可以配置proxy代理,把/api开头的请求转发到后端,开发环境下这样能规避大部分跨域问题。

3.3 下单流程:事务与状态机的"隐藏细节"

订单模块是这套系统里最值得花时间吃透的部分。一个标准的下单流程包含:校验商品状态和库存、生成订单主表记录、生成订单明细、扣减库存、清除购物车中对应商品。这几个步骤必须在一个事务里完成,任何一步失败都要整体回滚。

Spring里用@Transactional就能搞定。但有几个细节容易被忽视:

  • @Transactional默认只对RuntimeException回滚,如果代码里捕获了异常但没有重新抛出,事务是不会回滚的
  • 事务方法必须通过代理对象调用,同类中this.method()调用不会走代理,即@Transactional不生效
  • 事务的隔离级别和传播行为,在复杂嵌套场景下需要显式指定

订单状态一般是一个小型状态机:待支付、已支付、待发货、已发货、已完成、已取消。用数字或字符串常量化存到数据库里,前后端共同维护一套约定。比如0是待支付,1是已支付,2是已发货,3是已完成,4是已取消,前后端字典保持一致。后续做订单超时自动取消、支付回调等扩展时,只要顺着状态流转添加逻辑就行。

订单编号不要用数据库自增ID。在实际开发中,订单号通常用时间戳+随机数、雪花算法等方式生成,因为订单号会暴露在用户面前、也可能作为支付流水关联凭证,需要保证唯一性和一定的不可预测性。有些项目里会出现"订单编号由前端传入"的做法,这是很有问题的,工具都用错了地方。

4. 前端Vue3工程:页面、状态与接口对接

4.1 两套子系统的路由与权限边界

这套项目前端通常会被设计成两个逻辑端:面向普通用户的商城端,和面向管理员的后台管理端。这两端不仅页面不同,路由结构、布局组件、权限控制都有差异。

商城端一般有首页、商品列表页、商品详情页、购物车页、订单确认页、个人中心。后台管理端则通常包含:商品管理(增删改查、上下架)、分类管理、订单管理、用户管理、轮播图管理。整个工程如果使用Vue Router的createWebHistory模式,路径上会有/admin前缀来区分两套界面。

权限控制是前端最需要注意的地方。Vue Router提供了beforeEach全局前置守卫,每次路由跳转前判断用户是否登录、是否有访问目标路由的权限。代码一般长这样:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path.startsWith('/admin')) { if (!token) { next('/login') } else if (userInfo.role !== 'ADMIN') { next('/') } else { next() } } else if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })

有一个思路要讲清楚:前端守卫只是体验优化,真正的权限控制必须后端做。前端跳过了某个按钮或路由,不代表请求发不出去;只要后端接口做了权限校验,没有权限的用户即使自己拼URL也无法拿到数据。这层思维在答辩和技术面试时经常被问到,提前建立这个认知很重要。

4.2 axios封装与页面状态管理的分工

一个可维护的前端工程,绝不会在每个页面里直接调用axios然后散落一地重复代码。通常做法是封装一个request.js,统一做超时时间、请求头token注入、响应状态码处理、错误信息弹出。

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( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

页面状态用Pinia来管理,比较典型的场景是:登录后保存用户信息,购物车数量在多个页面之间需要共享。如果用localStorage硬传或者props逐层传递,很快就会乱成一团。用Pinia的defineStore定义store,在任何组件里都能读取和修改同一份状态,逻辑清晰很多。

购物车数量的经典例子:用户把商品加入购物车,顶部导航栏的小红点要立刻更新。这个数据被多个组件共享,放组件内state根本不同步,放Pinia就是顺手的事。此外,Router的beforeEach、axios拦截器都可以不依赖组件访问Pinia中的store,全局逻辑能串为一个整体。

4.3 商品列表页的搜索、分页与筛选交互

商城首页往往包含搜索框、分类筛选、价格排序、分页加载几个功能。前端把筛选条件组织成一个对象,作为查询参数传给后端,后端通过MyBatis-Plus的Wrapper做条件拼装。我见过很多项目把筛选逻辑全部写在SQL的where 1=1和<if>标签里,可维护性其实并不差,只是代码不够优雅。在MyBatis-Plus里可以这样处理:

LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(categoryId), Goods::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Goods::getName, keyword) .orderByDesc("销量".equals(sort) ? Goods::getSales : Goods::getCreateTime);

注意.eq(condition, column, value)里的condition参数非常实用,条件为false时MyBatis-Plus会自动忽略该条件拼接,避免大量if嵌套。商品列表的分页查询配合Page<Goods>对象和Plus的分页插件,前端传current和size,后端返回records和total,一个标准的列表接口半小时内就能搞定。

5. 从零跑通项目:环境准备与高频踩坑实录

5.1 第一次启动的完整步骤

拿到这套源码后,很多人卡在编译和启动阶段。这里说一个从零开始的稳妥流程,你按顺序走,能避开大部分问题。

第一步:安装JDK(1.8或11)、Node.js(16或18)、MySQL 8.0、Maven。这些都是基础环境。Maven建议用3.6以上,镜像源改到国内镜像会快很多。

第二步:创建数据库并导入SQL脚本。打开MySQL命令行或工具,执行项目下db目录里的init.sql(具体文件名以实际项目为准)。注意确认MySQL账号密码与后端application.yml中的配置一致。

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

这里有两个高频坑:serverTimezone没设置会报时区异常,MySQL8.0的驱动类名必须是com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver。

第三步:启动后端。在IDEA里导入项目,等待Maven下载依赖后,找到主启动类运行。启动成功后控制台会出现SpringBoot的Banner,并显示Tomcat started on port(s): 8080。

第四步:启动前端。在终端进入frontend目录:

npm install npm run dev

Vite默认启动在5173端口,浏览器访问http://localhost:5173就能看到页面。如果后端启动在不同的端口,需要检查前端配置的代理地址是否匹配。

5.2 跑不起来的高频原因及排查思路

遇上"跑不起来",先别慌,大多数问题可以归为几类,一步步排查。

连接不上MySQL。报错信息里出现Access denied,说明账号密码不对;出现Communications link failure,通常是URL中主机或端口写错,或MySQL服务没启动;出现Public Key Retrieval is not allowed,这是MySQL8.0的caching_sha2_password加密方式导致的,在URL后面加allowPublicKeyRetrieval=true即可,或者把用户改成mysql_native_password加密。最后那种解法是:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';

前端页面能打开,但所有接口都报404。最常见的两个原因:一是后端没启动;二是前端代理没配好。Vite开发环境下,在vite.config.js里配代理:

server: { host: 'localhost', port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

请求能发出,但CORS报错。如果不想用代理,那后端一定要开启跨域。SpringBoot里的做法:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*"); } }

Mapper提示找不到Statement。报错Invalid bound statement (not found)。出现这个问题的原因通常是:MyBatis-Plus的mapper接口扫描范围不对,或者XML文件没有编译到target目录。检查@MapperScan注解包路径是否正确,以及application.yml里是否有类似配置:

mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml

没有这个配置的话,XML文件不会自动加载。

5.3 这类项目常见的一个"伪bug":数据库时区

我不知道其他人什么感受,反正在长连接数据库、保存创建时间时,时区问题算是最隐形的坑。如果数据库连接URL里没有指定serverTimezone=Asia/Shanghai,MySQL8.0的默认时区是UTC,中国的应用写入的时间会比北京时间少8小时。很多人在页面看到时间不对,第一反应是代码逻辑错了,到处找时间处理工具类,实际上改URL的参数就能解决。

另一个相关的问题是数据库时区和系统时区不一致时,Java的LocalDateTime和MySQL的datetime类型交互经常出问题。统一设置连接时区是省心方案。

6. 五张表和一个状态字段怎么彻底吃透业务

6.1 从商品到订单的状态流转

如果把整个校园店铺的业务串起来,核心就是商品和订单的状态流转。

商品的状态相对简单:上架、下架。这个状态直接影响前端展示逻辑,下架商品在用户端列表里不显示,但通过链接直达详情页时,可以提示"商品已下架"。很多源码在这里处理得很粗糙,直接在前端调接口时报404,体验很差。

订单状态则要复杂得多。一张订单在生命周期里要经过多个状态,每个状态对应不同的用户操作和不同的后端行为:

状态含义可执行操作操作后的状态
0待支付用户支付1
1已支付商家发货2
2已发货用户确认收货3
3已完成无无
4已取消无无

这个表看起来简单,却意味着数据库里多一个订单状态字段就能驱动整个交易闭环。后台管理端的订单列表,本质上就是按这个状态字段做分类查询和数据统计。源码里如果能对各个状态的处理逻辑做出清晰注释,对读者是最友好的。

6.2 秒杀、优惠券等扩展需求的预留接口

一套可以交作业的项目,通常不会只满足基本功能。很多同学在拿到源码后希望加一些功能,最常加的就是秒杀和优惠券。改造前先想清楚:现有表结构是否支持?

秒杀的本质是"短时间内大量订单请求",需要更严格的库存扣减逻辑,可能需要引入Redis做预扣减,或者至少加上乐观锁。优惠券的本质是"订单金额计算规则",需要在订单表中增加"优惠金额""券ID"字段,在订单生成时插入计算逻辑。这两类扩展都不需要推翻现有业务表结构,只需要在原有表上增加少量字段、在订单流程中增加处理步骤。这是标准化表设计带来的好处。

所以拿到源码后,我建议你先花半天时间认真梳理表关系,而不是急着改代码。表结构清楚了,业务扩展点自然就浮出来了。

7. 从"跑通源码"到"讲清楚项目"的临门一脚

7.1 动手改造前,先确认这几个关键问题

无论是为了实际应用还是课程答辩,拿到源码后先尝试回答这几个问题,答得上来,说明你真正理解了项目;答不上来,建议对照源码和文档逐一看懂:

  1. 后端如何识别登录用户身份?JWT token存在哪里?过期了怎么办?
  2. 订单支付成功后,是同步扣库存还是异步扣库存?
  3. 商品分类的层级最多有几级?前端如何渲染分类树?
  4. 首页轮播图管理是独立的表吗?后台的上传图片是本地存储还是对象存储?
  5. 如果访问量突然变大,哪些地方会成为瓶颈?

这里面每一个问题都对应一段源码中的设计。带着问题去读代码,比漫无目的地翻来翻去高效得多。

7.2 二次开发最值得动的三个方向

如果学有余力,或者想在原项目上做出差异化,我最推荐从这三个方向入手。

第一个方向是数据可视化。在后台管理端增加一个数据统计页面,用ECharts展示每日订单量、销售额、热门商品Top10。这个功能不需要改动原有业务表,只需新增几个聚合查询接口,前端的图表库也是现成的,改动量小,演示效果极好。

第二个方向是消息通知。用户下单后给商家发通知、商家发货后给用户发通知。实现上,可以在订单状态变更的操作后增加站内信逻辑,或集成短信服务。这部分能体现你对业务流程的理解深度。

第三个方向是接口缓存。首页商品列表、分类导航这些访问频繁但变化不频繁的数据,用Redis缓存起来,减少数据库压力。SpringBoot2时代最标准的做法是@Cacheable注解配合Redis,代码改动不大,但能显著提升响应速度,这也是生产环境中的真实需求。

7.3 写文档时的几个"加分项"

这类项目通常配有文档,很多同学直接复制源码里的说明文档交差,这其实浪费了一个很好的展示机会。真正用心写的文档,应该包含:系统架构图、数据库ER图、核心接口说明、关键功能截图、项目部署步骤。这些内容组起来就是一份能让老师或面试官快速理解项目的说明书。

另外,把项目过程中的"排错经历"写进文档,比写"本项目运行稳定、功能完善"这类空话有价值得多。比如"解决了MySQL8.0连接认证方式导致的应用无法连接问题",短短一句,就已经在证明你有实际动手能力,而不只是会复制粘贴。

实际操作中的一个提醒

最后说点最容易被忽略的:数据初始化。这个项目的SQL脚本里一般会带一些测试数据和初始化账号,比如管理员账号、测试商品、测试订单等。跑通项目后,花时间把测试数据梳理一遍,删掉看着不真实的内容,再按校园场景重新造一些规范数据。很多人在演示时被问"为什么测试订单的收货人叫某某",只是因为懒得改数据,这个问题在展示时容易扣分。

在本地把每个模块亲手操作一遍、把每个状态流转走通一遍,会发现代码里很多设计"当时看不懂,用了才明白为什么要这样"。学这种全栈项目,最忌只看不动。跑起来,改一处,再跑一遍,印象比读十遍源码都深刻。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进