SpringBoot+Vue校园商铺管理系统:前后端分离实战全解析
最近两年一直在断断续续帮身边的人弄毕业设计和课程设计发现一个很有意思的现象大家想做的项目越来越“实景化”不再是单纯的学生管理系统、图书管理系统而是开始关注校园二手交易、校园商铺、校园跑腿这类能真实落地的场景。如果你也正在找一套前后端分离、技术栈主流、能跑通完整业务闭环的Java全栈项目那SpringBootVue校园商铺管理系统可以说是一个很不错的参考样本。它几乎覆盖了毕业设计答辩里会被问到的高频考点登录鉴权、角色权限、商品CRUD、订单状态机、文件上传、数据库多表关联、前后端跨域联调。这篇就基于我实际拆解这套系统源码时的思路把它的需求设计、后端实现、数据库表结构、前端调用方式以及我踩过的坑全部展开讲一下希望能帮想复制这套项目的人少走弯路。1. 校园商铺管理系统的需求与方案选型1.1 校园场景到底需要什么样的“商铺系统”校园里的商铺和校外电商平台有本质区别。用户群体非常集中基本就是本校学生和校园周边小商家商品类型也比较固定比如二手教材、闲置数码、零食饮料、打印资料、手工艺品交易流程不需要太复杂但一定要方便不需要对接物流更多的是校内自提或者当面交易。所以校园商铺管理系统的核心不是“大而全”而是“够用且能演示出技术点”。从实际需求角度出发这套系统至少要覆盖三类角色。学生买家浏览商品、搜索、加购物车、下单、查看自己的订单、评价商品。卖家/店主管理自己的店铺、发布商品、上下架、处理订单、查看简单的营收统计。系统管理员管理全部用户、审核店铺入驻、删除违规商品、查看全站数据。这三类角色对应到代码层面就是典型的RBAC权限模型同一套登录接口根据用户角色返回不同的菜单和操作权限。技术选型上SpringBootVue这一组合如今几乎是全栈项目的标准答案。后端SpringBoot负责暴露RESTful API前端Vue负责页面交互和接口调用数据层用MySQL存储业务数据。选这套组合有几方面的原因一是前后端分离结构清晰前端同学和后端同学可以并行开发答辩时也能分别讲清楚各自的职责二是SpringBoot简化了Spring的配置开箱即用省去了大量XML配置适合课设周期三是Vue的生态成熟配合Element UI或Ant Design Vue可以快速搭建出后台管理界面四是MySQL免费且通用几乎所有评测环境都能跑起来。和传统的JSPServlet或者SpringMVCThymeleaf模板渲染方案相比前后端分离的好处是更贴近企业现有开发模式。答辩老师如果想问“你如何同时维护两套代码”你可以解释前端只需要通过Axios调用后端接口后端只负责返回JSON数据两者通过接口文档约定字段格式。这种说法既体现出工程意识又不会让自己陷进复杂的页面渲染细节里。1.2 功能边界与模块拆分在动手写代码前一定要把功能边界划清楚否则开发过程中很容易陷入“无限加需求”的泥潭。校园商铺管理系统如果追求突出技术亮点建议按下面的模块来切分模块面向角色核心功能技术亮点用户认证全角色登录、注册、Token签发、密码加密JWT、拦截器、MD5/BCrypt店铺管理卖家、管理员店铺开设、审核、修改信息、店铺列表一对多关联、图片上传商品管理卖家、买家商品发布、上下架、图片上传、商品搜索CRUD、分页、模糊查询购物车买家加入购物车、修改数量、删除商品、批量结算中间表设计、批量插入订单管理买家、卖家下单、订单状态流转、订单列表事务控制、状态枚举评价管理买家、卖家评价商品、评价列表展示多表关联查询公告管理管理员发布公告、展示公告简单CMS逻辑数据统计管理员、卖家订单数量、销售额统计SQL聚合函数、图表展示这套模块划分不是随意堆出来的而是经过了一个筛选逻辑基础CRUD必须要有因为这是课设的基本盘权限验证必须要有因为这是区分“玩具项目”和“系统项目”的关键订单状态和库存扣减必须要有因为在答辩时最容易引发追问公告和统计是加分项实现成本低且能展示你理解事务和聚合查询。功能边界划定之后建议把接口统一设计成/api/user/**、/api/shop/**、/api/commodity/**、/api/order/**这样的前缀结构。这样做的好处很明显拦截器里可以按前缀分别设置白名单和黑名单比如登录注册接口放行其他接口一律校验Token前端也能根据API前缀快速定位到对应的页面逻辑。2. 后端SpringBoot核心设计与实现2.1 项目初始化和关键依赖配置新建一个SpringBoot项目时我建议直接通过Spring Initializr生成骨架注意SpringBoot版本选择。如果你是刚接触项目的同学这里有个易错点SpringBoot 2.x和3.x对Java版本要求完全不同3.x需要Java 17以上如果本地环境是JDK 8那创建项目时务必选择2.7.x版本否则编译阶段就会报错。我拆解的这套校园商铺系统源码采用SpringBoot 2.7.x配套JDK 8兼容性最好不少高校机房环境也还是JDK 8。pom.xml中的核心依赖大致如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里用MyBatis-Plus而不是原生MyBatis主要原因是快速开发。课设和毕设项目里的大部分接口都是单表CRUDMyBatis-Plus的BaseMapper直接提供了selectOne、insert、updateById、selectPage等方法省去了写大量XML的重复劳动。当然复杂多表查询还是要手写SQL这一点在后面的订单统计里会体现。application.yml核心配置需要注意几点数据库连接串必须加useSSLfalse和serverTimezoneAsia/Shanghai否则本地连接MySQL时会报时区错误警告MyBatis-Plus的逻辑删除配置global-config.db-config.logic-delete-field会自动拦截删除操作这样商品删除就走is_deleted字段标记而不是物理删除对数据统计友好。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case建议开启这样数据库里的user_id字段可以自动映射到实体类的userId属性不用写一堆TableField。2.2 基于JWT的用户认证与权限控制校园商铺系统的所有业务接口都需要登录后才可访问所以认证逻辑是整个后端的安全底座。这里采用的方案是JWT无状态认证。用户提交用户名密码成功后后端签发一个包含用户ID和角色的Token前端每次请求在Authorization请求头携带后端拦截器解析Token并放行或拒绝。Token的生成和解密我用了一个工具类封装关键代码逻辑如下public class JwtUtil { private static final String SECRET campus-shop-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 24 * 7; public static String createToken(Integer userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }这里有个安全细节密钥SECRET不能硬编码在代码里正式项目应该放到配置中心团队平台。课设里没关系但如果答辩时有老师问“Token被破解怎么办”你可以回答实际部署时会采用更复杂的密钥并加一层Redis黑名单来处理注销问题。这个回答能体现出你的工程意识。拦截器是权限控制的关键。我用HandlerInterceptor实现了一个AuthInterceptor核心逻辑是从请求头取Token解析失败就返回401解析成功就把用户信息放入ThreadLocal供后续Controller直接取用。具体写法如下public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); UserContext.set(claims.get(userId, Integer.class), claims.get(role, String.class)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }注册拦截器时需要同时配置排除路径否则登录接口也被拦截那就是“自己锁自己”。注册类里要放行/api/user/login、/api/user/register以及静态资源路径比如上传图片的/images/**。这里的UserContext是我自建的ThreadLocal工具类用来传递当前登录用户非常实用。除了拦截器角色权限我用Spring Security是不可能的因为那样会引入新的配置工作量。课设项目里更合适的做法是在业务层手动判断比如管理员接口在Controller层加上if (!ADMIN.equals(UserContext.getRole())) return R.error(无权限)。这种写法人人看得懂也方便在答辩时讲清楚权限判断流程。2.3 核心业务模块的事务与并发控制订单模块是整个系统的核心难点因为它涉及到多个表的写操作插入订单主表、插入订单明细表、扣减商品库存、清空购物车。任何一个环节失败都会导致数据不一致。所以这个方法必须加Transactional让所有操作处于同一事务中一旦异常就整体回滚。典型的下单核心方法如下Transactional(rollbackFor Exception.class) public R createOrder(OrderCreateDTO dto) { // 1. 遍历下单商品计算总金额同时检查库存 BigDecimal total new BigDecimal(0); ListCommodity commodityList new ArrayList(); for (OrderItemDTO item : dto.getItems()) { Commodity commodity commodityMapper.selectById(item.getCommodityId()); if (commodity null || commodity.getStatus() ! 1) { return R.error(商品不存在或已下架); } // 2. 扣减库存 int rows commodityMapper.deductStock(item.getCommodityId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足); } total total.add(commodity.getPrice().multiply(new BigDecimal(item.getQuantity()))); commodityList.add(commodity); } // 3. 创建订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(UserContext.getUserId()); order.setTotalAmount(total); order.setStatus(OrderStatusEnum.UNPAID.getCode()); orderMapper.insert(order); // 4. 插入订单明细表 for (int i 0; i commodityList.size(); i) { OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setCommodityId(commodityList.get(i).getId()); item.setPrice(commodityList.get(i).getPrice()); item.setQuantity(dto.getItems().get(i).getQuantity()); orderItemMapper.insert(item); } // 5. 清空购物车 cartMapper.deleteByUserIdAndCommodityIds(UserContext.getUserId(), dto.getItems()); return R.ok(下单成功, order.getId()); }这段代码里有个关键点扣减库存时一定要用SQL层的条件更新而不是先查库存再减。我见过很多初学者写if (stock quantity) { update stock stock - quantity }这在单线程下没问题但并发请求时会出现超卖。推荐的做法是在Mapper里写UPDATE commodity SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}返回受影响行数为0就说明库存不足。这个细节几乎是答辩必考点提前掌握了会非常加分。生成订单号的逻辑我用了简单的时间戳加随机数yyyyMMddHHmmss加上四位随机数避免重复。实际项目中可以用分布式ID方案但校园商铺单机部署完全够用。下单成功后前端可以模拟支付操作把订单状态从“待付款”置为“待发货”这只需要一个简单的更新接口。3. 数据库设计与MySQL落地细节3.1 核心数据表的设计思路数据库设计直接决定了接口开发的难易程度。校园商铺系统建议至少设计这些表用户表user、店铺表shop、商品表commodity、购物车表cart、订单主表orders、订单明细表order_item、评价表comment、公告表notice。这里我挑几个关键表展开。用户表字段不需要太复杂但角色字段必须有。角色用字符串保存区分STUDENT、SELLER、ADMIN即可。密码字段长度要注意使用BCrypt加密会得到60位字符串varchar(64)刚好够用如果用MD5加密就是32位varchar(32)也可以。表结构如下字段类型说明idbigint主键自增usernamevarchar(32)登录账号唯一passwordvarchar(64)加密后的密码rolevarchar(16)用户角色avatarvarchar(255)头像URLnicknamevarchar(32)昵称created_atdatetime注册时间商品表要关联店铺表一个店铺可以有多个商品一个商品只能属于一个店铺。商品表的字段最好包含main_image和sub_images主图用于列表展示子图用于商品详情。我用了一个字段存JSON数组比如[/upload/a.jpg,/upload/b.jpg]前端拿到后直接JSON.parse渲染多图。很多初学者会把图片分成多条记录存反而复杂了。购物车表比较特殊它本质是用户和商品的关联表额外带一个数量字段。联合索引务必设置在(user_id, commodity_id)上因为前端加入购物车时要先判断是否已存在同一商品存在则只更新数量不存在才插入新记录。订单相关的两张表是真正需要花心思的。订单主表orders保存一次下单的整体信息订单明细表order_item保存该订单下的每个商品快照。为什么需要快照因为商品表里的价格、名称可能会变但订单形成后这些信息就应该固定下来。所以在order_item中除了关联商品ID还冗余了商品名称、商品主图、商品单价。这是数据库设计里“冗余换稳定”的经典案例答辩时拿出来讲能加分。3.2 高频SQL的写法与性能提示正式建表时字符集统一使用utf8mb4因为MySQL默认的utf8实际只能存一部分字符用户手动输入一个表情符号就会导致数据写入失败非常尴尬。排序规则使用utf8mb4_general_ci即可。商品分页查询是使用频次最高的SQL没有之一。商品列表页的筛选条件通常是分类可选、关键字可选、价格排序可选分页用MyBatis-Plus的Page对象。这里有个容易被忽略的性能点如果商品表的数据量变大LIKE %关键词%是没办法走索引的但在课设场景下数据量只有几千条完全不用关心。我会在答辩时提前说一句“当前数据量下全表扫描成本可接受如果未来数据量增长会考虑引入Elasticsearch”这句话足以证明你思考过扩展性。店铺营业额统计的SQL建议准备好它是答辩中“加分项”的常客。我的写法是SELECT DATE_FORMAT(o.create_time, %Y-%m-%d) AS day, SUM(o.total_amount) AS amount, COUNT(o.id) AS order_count FROM orders o WHERE o.shop_id #{shopId} AND o.status 3 GROUP BY DAY(o.create_time) ORDER BY day DESC这里o.status 3限定了“已完成”状态避免把未支付或已取消的订单计入销售额。按天分组可以通过DATE_FORMAT完成前端拿到每日营业额数据后用ECharts渲染一个折线图整个系统的完整度立马上来了。另一个高频需求是“我的订单列表”。由于订单主表和订单明细表是一对多关系在列表页展示时一个用户有多个订单每个订单又包含多个商品如果用循环逐条查询明细会触发N1问题。更合理的做法是一次查出当前用户的所有订单再根据订单ID集合一次查出所有明细在Java内存中组装。MyBatis-Plus的selectBatchIds正好支持。我建议在课设中就用这种“两次查询”的方式而不是图省事在循环里查询。4. Vue前端页面实现与接口联调4.1 前端工程结构和路由规划前端部分我选择Vue 2 Vue Router 3 Element UI这套组合对课设来说最稳因为网上资料多遇到问题随便一搜就能解决。如果你使用Vue 3建议搭配Element Plus但注意Vue 3的生态和Vue 2不完全兼容很多组件库的用法略有差异。校园商铺系统拆解下来的前端部分我看到的页面主要包括登录注册页、首页商品列表、商品详情页、购物车页、结算下单页、个人中心、卖家店铺管理页商品发布/订单管理/数据统计、后台管理页用户管理/店铺审核/公告管理。路由规划是前端项目的骨架。理想的做法是把各个页面的路由按照模块拆分使用懒加载的方式引入组件比如const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/Home.vue), children: [ { path: mall, component: () import(/views/Mall.vue) }, { path: detail/:id, component: () import(/views/Detail.vue) }, { path: cart, component: () import(/views/Cart.vue) }, { path: orders, component: () import(/views/Orders.vue) } ]}, { path: /seller, component: () import(/layout/Seller.vue), children: [ { path: commodity, component: () import(/views/seller/CommodityManage.vue) }, { path: orders, component: () import(/views/seller/OrderManage.vue) } ]} ];路由守卫是必须的。前置路由守卫里检查本地是否有Token没有就跳转到/login有就放行。如果有页面需要特定角色才能访问则在路由meta里标记role守卫里判断当前用户角色是否匹配。这个机制的代码非常简单但对权限的体验提升很明显否则用户直接用URL访问管理页面就能看到后台。4.2 Axios请求封装与Token状态管理前端和后端联调最痛苦的事情就是每个请求都要手动携带Token并且捕获401统一跳转。解决办法就是封装一个统一的请求实例。我的习惯是在src/utils/request.js中使用axios.create创建实例然后在请求拦截器里注入Token在响应拦截器里统一处理错误码。代码长这样import axios from axios; import { Message } from element-ui; import router from /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( response { const res response.data; if (res.code ! 200) { if (res.code 401) { localStorage.removeItem(token); router.push(/login); } Message.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg)); } return res.data; }, error { Message.error(error.message || 网络异常); return Promise.reject(error); } ); export default service;这里有一个设计点后端统一返回体{ code, msg, data }所以拦截器在成功状态码时直接返回res.data业务代码里拿到的是剥离了一层包装后的数据用起来简洁。若后端返回401说明Token过期或者无效就该清除本地登录态重定向到登录页。这套封装逻辑几乎是所有前后端分离项目的标配值得反复练习。4.3 核心页面交互习惯与联调细节商品列表页是对接后端分页接口的典型场景。Element UI的el-pagination组件和高低端配合时需要注意currentPage和pageSize的绑定。每当页码或每页条数变化就触发加载列表的函数。后端分页返回的数据格式在这里要约定好MyBatis-Plus返回的IPage包含records、total、current、size字段前端拿records渲染表格拿total赋值给分页组件的total属性。加入购物车的交互比较常见前端只需要调用POST /api/cart/add并传入商品ID和数量。但要注意一个细节如果商品详情页和商品列表页都提供加购入口那么同一个操作要复用一个api/cart.js模块中的函数避免两个页面各写一套重复请求逻辑。维护性好也方便答辩展示代码规范。订单提交流程是前端交互中链路最长的场景购物车页面勾选要结算的商品一般是勾选状态记录在一个数组里点击“去结算”跳转确认订单页确认页展示商品明细和总金额点“提交订单”后端下单成功再跳转“支付模拟”页面点击“立即支付”调用支付接口更新订单状态。每一步的异步交互都要给用户反馈比如按钮loading、提交成功后Message提示。如果某个环节网络报错要能够停留当前页面不丢数据。还有一个前端普遍会踩的坑本地开发时跨域问题。前端运行在http://localhost:8081后端运行在http://localhost:8080如果不做任何配置浏览器会拦截跨域请求。最简单的解决办法是在vue.config.js中配置开发服务器代理devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/user/login时开发服务器会自动把请求转发到后端的http://localhost:8080/api/user/login前端眼里就没有跨域这回事。注意请求路径里的/api前缀不要省略它是代理匹配的关键。这一步如果不理解联调阶段会浪费大量时间。5. 常见问题与排查技巧实录5.1 环境搭建阶段的高频报错我在实际搭建这套校园商铺系统时最先遇到的就是JDK版本和SpringBoot版本不匹配。如果本地之前安装过多个JDK那么用java -version确认当前生效版本。SpringBoot 2.x要求Java 8或113.x要求17用2.7.x最保险。另一个高频报错是Maven依赖下载缓慢甚至卡死可以换国内镜像源在settings.xml里配置阿里云镜像即可。这个问题课设阶段几乎人人都会遇到不用慌。连接MySQL时报错Access denied for user rootlocalhost通常不是代码问题而是数据库用户名或密码不对。另外MySQL 8.0默认使用caching_sha2_password加密方式而某些版本的驱动连接会报错解决办法是在连接串里加上?useSSLfalse或升级MySQL驱动到8.0.33。如果看到Unknown database说明数据库还没创建执行CREATE DATABASE campus_shop DEFAULT CHARACTER SET utf8mb4;就能解决。还有一个小坑后端启动时提示端口被占用。我经常遇到因为上次进程没有关闭导致的这时在命令行执行netstat -ano | findstr 8080找到占用进程的PID再用taskkill /PID 进程号 /F干掉。或者更省事直接修改server.port换一个端口比如8081。5.2 前后端联调阶段的典型问题前端控制台报401 Unauthorized基本是Token没带或Token过期。先检查登录成功后是否把Token正确存储再看Axios请求拦截器是否从localStorage读到了Token最后检查后端的JwtUtil解析是否使用了同一个密钥。如果三个环节都一致就不会有问题。如果前端报404大概率是路径不一致比如后端接口是/api/user/login前端写成/api/user/Login检查大小写。跨域配置失效也经常发生。确认前端代理是否真的生效可以在Network面板看到请求URL仍然是/api/xxx而不是http://localhost:8080/xxx。如果代理没有生效需要检查vue.config.js里的target是不是写成了https而实际后端是http。另外开发服务器代理只对开发环境有效如果前端已经打包部署了则需要用Nginx做反向代理把同一个域下的/api路径转发到后端服务。我在联调时遇到过一次奇怪的问题后端接口直接用Postman访问一切正常前端访问却报500。后来一查发现是因为后端返回的日期格式是2025-01-01T12:00:00前端Jackson解析时因为时区偏移导致异常。解决办法是在后端全局配置时间格式化或者在实体类的日期字段上使用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。建议在后端配置类里统一设置省得一个个实体类加注解。5.3 库存并发问题的排查经验下单扣库存是并发问题最集中的地方。如果采用“先查询库存再更新”的方式高并发下会出现超卖。我建议用数据库原子操作UPDATE commodity SET stock stock - #{num} WHERE id #{id} AND stock #{num}。这样即使同一时间两个请求同时扣减同一商品的库存最终结果也不会变成负数。如果想让系统更加严谨可以在商品表加一个version字段在更新时校验版本号从而实现乐观锁。这两个方案都可以在答辩时演示老师听完通常会点头。还有一个隐藏问题事务方法内部使用UserContext获取当前登录用户但UserContext是ThreadLocal如果异步线程里取用户信息会拿到空值。校园商铺项目的场景都在单线程内所以不会触发这个问题。你要留意的是拦截器里的afterCompletion一定要调用UserContext.clear()否则线程池复用时会串号这是一个非常隐蔽的坑。5.4 答辩前必须完成的检查清单基于我拆解和复现这套系统的经验建议在正式演示前按清单走一遍数据初始化是否有默认管理员账号商品图片是否填的是可访问的本地路径而不是局域网IP后端接口是否能够冷启动后在30秒内全部就绪前端打包产物中/api是否通过Nginx正确转发演示用的浏览器是否已开启本地跨域允许手机热点或外网环境下图片是否还能正常加载。这里尤其推荐准备“一键重置数据库”的SQL脚本。演示过程中如果出现脏数据直接执行脚本重置到一个干净的初始状态比在页面上手动删除数据高效得多。脚本里可以带上几条演示商品数据包括一条价格有小数位的商品方便展示金额处理精度。个人经验是真正把课设做扎实不只是把代码跑通而是把接口设计、数据库关系、权限流程、事务边界都理顺。这套SpringBootVue校园商铺管理系统之所以适合拿来学习和复现是因为它规模适中、技术覆盖面广、业务场景贴近真实生活。你在完成它的过程中既练到了后端CRUD和事务又练到了前端组件和联调还能在答辩时拿出订单状态流程图和数据库ER图来讲解综合收益非常高。如果你在本地运行这套项目时遇到启动报错、依赖缺失或者数据表字段对不上的情况先对照MySQL版本和连接串排查再检查前后端路径是否一致大多数问题都能很快定位。最后分享一个小技巧演示前把数据库里的模拟数据做成“有人气”的样子比如给商品设置已被浏览数据、订单状态有不同阶段的记录这样展示系统时会让页面看起来更真实也方便你讲运营视角的故事。项目本身不难难的是把每个细节都吃透希望这篇东西能帮你省下几晚调试时间。