
做校园商铺管理系统这个项目花时间最久的不是写代码而是想清楚到底怎么把各个模块串起来。SpringBoot、Vue、Java、MySQL、MyBatis这几个词拼在一起看起来就是一套标准的Java全栈组合但真正动手时才会发现从前端页面到后端接口从数据库表到联调部署每一环都有不少“看不见的坑”。这篇博文我打算把我自己从零实现这套基于SpringBootVue的校园商铺管理系统的全过程记录下来包括架构设计、数据库建模、后端实现、前端对接以及大量实测过的排查经验给正在做同类系统设计或者准备毕业设计的同学一份可以直接参考的完整路径。这套系统解决的是校园场景里多角色商铺管理的真实问题。学生用户可以在线浏览商品、下单购买商家能够管理自己的商品和订单管理员则负责审核商铺、管理用户和统计经营数据。整个项目采用前后端分离架构后端用SpringBoot提供RESTful API前端用Vue开发管理界面数据库使用MySQL存储业务数据持久层框架选择MyBatis来写灵活的SQL。无论你是想快速搭一个能跑通的全栈项目还是想深入理解这几个主流技术栈如何协同工作这篇文章都值得从头读完我尽量把每一步的原理和实操都写透。1. 项目整体设计与技术选型为什么是SpringBootVue1.1 前后端分离的架构思路刚开始规划这个项目时我其实纠结过要不要用传统的服务端渲染方案。毕竟校园商铺这种管理系统页面数量不算特别多用Thymeleaf之类的模板引擎也能做开发起来好像还更快。但仔细想清楚需求后我最终还是选择了前后端分离原因是这个系统有三个明显不同的使用端用户端需要像小程序一样流畅的浏览体验商家端表格操作密集管理端又有大量数据看板。如果全部塞在服务端渲染里光是页面状态同步和局部刷新就够让人头疼的。前后端分离的核心思路是把“数据”和“展示”彻底拆开。后端只负责提供接口不关心数据最终渲染成什么样子前端拿到JSON数据后自己做路由跳转、状态管理和交互渲染。这样做最直接的好处是前端开发和后端开发可以完全并行我不用等接口写完了再去做页面只要提前约定好数据结构两边各干各的就行。部署上也灵活前端打包成静态文件扔到Nginx里后端打成jar包独立运行哪个挂了单独排查互不拖累。对校园商铺这种权限层次分明的系统来说前后端分离还有一个隐藏优势权限控制可以在前端路由层先做一层拦截后端接口再做一次校验双重保障。校园场景里用户的角色相对固定管理员、商家、普通学生三类每个角色能看到的页面和能调用的接口都不一样这种“前端控制页面可见性、后端控制数据可访问性”的配合能减少很多越权操作的隐患。1.2 技术栈选型背后的考量先聊聊后端为什么选SpringBoot。说实话现在做Java后端项目SpringBoot几乎是默认选择但它的好处值得说清楚。SpringBoot最大的价值不是提供了多少新功能而是把Spring生态里那些繁琐的配置全部自动化了。以前用SSMSpringSpringMVCMyBatis的时候光是配XML文件就得写一大堆数据源、事务管理器、视图解析器、扫描路径每一样都要手动声明。SpringBoot通过自动配置机制引入一个starter依赖相关的Bean会自动注册到容器里默认配置就能跑起来只有在需要定制时才去覆盖开发效率高了一大截。持久层选择MyBatis而不是MyBatis-Plus或者Spring Data JPA我身边的人问过我好几次。这里说明一下MyBatis是半自动ORM框架SQL由开发者自己编写控制力最强遇到复杂的多表关联查询、动态条件拼接写起来非常顺手。MyBatis-Plus虽然开发效率更高封装了BaseMapper之后单表CRUD几乎不用写SQL但一旦遇到多表join还是要回到XML里手写SQL反而存在两套写法的割裂感。JPA则更适合领域驱动设计那种项目对SQL控制弱一些校园商铺这种需要精确掌握查询逻辑的场景我觉得MyBatis最合适。MySQL和Java的组合就更不用多说了MySQL 8.0支持窗口函数、公共表表达式这些现代SQL特性性能在中小型项目里绰绰有余。Java 8的Stream和Lambda让集合操作简洁很多配合Spring Boot的内嵌Tomcat打一个jar包就能运行。唯一要注意的是版本匹配我实践中比较稳的组合是JDK 1.8 SpringBoot 2.7.x MySQL 8.0这个组合经过大量项目验证网上资料也最多遇到问题随便一搜就能找到解决方案对新手特别友好。2. 核心模块设计与数据库建模2.1 用户角色与权限模型校园商铺系统的用户角色划分决定了整个系统的权限模型。我把用户分成三类学生、商家、管理员。学生用户对应前台购买者可以浏览商品、下单、支付模拟订单、查看个人订单商家用户管理自己的店铺和商品能够处理订单发货管理员负责全局管理包括用户审核、商铺审核、系统公告、数据统计。数据库设计上我没有搞复杂的RBAC表关系而是采用最简单直接的方式用户表加一个role字段区分角色。因为校园商铺系统角色固定不需要动态分配权限搞一套user_role、role_permission、permission多表关联反而增加维护成本。但是如果考虑到后期扩展role字段不利于扩展所以我做了一套折中方案用户表user存公共信息user_role表存用户角色关联角色表role固定三条数据这样既保留了扩展能力又不会过度设计。实际查询的时候通过用户ID关联出角色标识符即可接口里用拦截器判断角色访问权限。密码存储这里必须多说一句千万不能明文保存。项目中使用BCrypt加密Spring Security框架里提供的BCryptPasswordEncoder就能直接使用每次加密结果不同但校验可以通过避免了MD5加盐还要自己写逻辑的问题。用户登录成功后后端生成JWT令牌返回给前端前端把Token存到localStorage里每次请求在拦截器里带上Authorization请求头。后端再用一个HandlerInterceptor统一校验Token有效性顺便把当前登录用户信息放到ThreadLocal里供后续业务代码直接获取当前操作用户。2.2 商品、订单与库存设计商铺系统的核心业务数据基本上就是商品、订单、库存这三块。商品表products记录商品基本信息包括名称、描述、价格、库存数量、封面图、上下架状态、所属商铺ID还要加上创建时间和更新时间。商铺表shops则记录店铺信息包括店铺名称、简介、Logo、所属商家ID、审核状态因为校园商铺需要管理员审核才能营业这个状态字段不能少。订单设计是整个数据库建模里最关键的环节。我拆成了两张表订单主表orders和订单明细表order_items。订单主表存订单编号、用户ID、商铺ID、订单总金额、订单状态、收货信息、创建时间、支付时间、发货时间、完成时间。订单明细表存商品快照数据包括商品ID、商品名称、商品图片、购买价格、购买数量。为什么要做快照这是很多人容易忽略的细节用户下单后商家如果修改了商品价格历史订单里的金额和商品信息不应该变化所以必须把下单时的数据复制一份到订单明细表里而不是查询时实时关联商品表。这点如果没做后面查历史订单对不上账是必然的事。库存扣减我踩过坑一开始是直接UPDATE products SET stock stock - #{count} WHERE id #{id}然后判断受影响行数表面看没问题但高并发下会发生超卖。后来改成乐观锁方案在商品表加一个version字段更新时带上版本号判断UPDATE products SET stock stock - #{count}, version version 1 WHERE id #{id} AND stock #{count} AND version #{version}受影响行数为0就说明库存不足或者版本冲突需要提示用户重新下单。虽然校园商铺并发没那么夸张但把方案做对思路是通用的。订单状态我也设计了清晰的状态机待支付、已支付、已发货、已完成、已取消这几个状态之间的流转是固定的后端Service层在状态变更时做校验不允许随意跳转。例如已支付的订单不能直接变成已完成必须经过已发货这个状态这样能有效避免业务逻辑混乱。表名用途关键字段关联关系user用户信息id, username, password, role, phone与user_role一对多shop商铺信息id, user_id, name, status与user多对一product商品信息id, shop_id, name, price, stock, version与shop多对一orders订单主表id, order_no, user_id, shop_id, amount, status与user/shop多对一order_item订单明细id, order_id, product_id, product_name, price, quantity与orders多对一cart购物车id, user_id, product_id, quantity与user/product关联category商品分类id, name, sort与product一对多3. SpringBoot后端实现的关键环节3.1 项目搭建与MyBatis配置核心细节创建SpringBoot项目这一步我建议直接用Spring Initializr生成基础骨架避免手动创建目录结构的麻烦。依赖方面除了spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java这些必选项还需要引入jjwt用来生成和解析JWT Tokenlombok用来简化实体类的getter/setter方法hutool工具库处理一些通用的字符串和时间操作。MyBatis整合到SpringBoot里重点文件是application.yml配置。数据源用HikariCP这是SpringBoot默认的连接池性能好不需要额外引入依赖。关键配置包括驱动类、数据库地址、用户名密码还有MyBatis的mapper-locations指向XML文件位置type-aliases-package配置实体类包路径map-underscore-to-camel-case设为true让数据库的下划线字段自动映射成Java的驼峰属性。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.campus.shop.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl一个经常被忽略的配置是allowPublicKeyRetrievaltrueMySQL 8.0默认使用caching_sha2_password认证插件JDBC连接时如果不加这个参数偶尔会报Public Key Retrieval is not allowed错误很多人查了半天查不到原因其实就是一行配置的事。关于“表不存在自动建表”这个热词我要专门说明一下MyBatis本身只负责SQL操作不具备建表能力。SpringBoot里实现自动建表通常有三种做法。第一种是使用Spring的DataSourceInitializer在schema.sql里写好建表语句配置spring.sql.init.modealways应用启动时会自动执行。第二种是引入Flyway或者Liquibase这类数据库迁移工具管理建表和变更脚本。第三种是MyBatis-Plus的自动建表插件但它是框架层面的增强能力。我用的是第一种简单直接适合中小型项目。生产环境更推荐Flyway方式但校园商铺这种项目用schema.sql初始化就够了。3.2 Service层事务与并发处理实践经验Service层是整个后端业务的枢纽层事务控制必须在这里收敛。Spring的Transactional注解默认只在抛出RuntimeException时才回滚这一点很容易踩坑。比如某个Service方法里try-catch把异常吞掉了事务是不会回滚的数据就会处在一种半写入的尴尬状态。我的习惯是Service方法里不捕获异常让异常向上抛给Controller层由全局异常处理器统一处理这样既能保证事务边界干净又能返回统一的错误响应结构。针对订单创建这个核心方法我对事务传播级别的理解也更深入了一层。用户提交订单时需要同时往orders表和order_items表插入数据还要扣减库存这三个操作必须在一个事务里完成任何一步失败全部回滚。我用一个createOrder方法方法上加Transactional(rollbackFor Exception.class)传入订单相关参数按顺序执行插入主表、插入明细、扣减库存三个操作。rollbackFor这个属性非常重要指定了所有异常都回滚不局限于RuntimeException。并发这一块库存扣减使用乐观锁后还需要配合重试机制来提升用户体验。当第二个用户同时购买同一商品时乐观锁校验version可能失败我们不能直接让请求失败可以在Service层做一个最多三次的重试。第一次失败就重新查询最新version再次尝试失败次数到了再抛出库存不足异常这样在高并发下能最大化下单成功率。代码实现上用循环加version条件更新的方式每次失败重新查询版本号再更新实际测试下来效果很明显。Override Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { Product product productMapper.selectById(dto.getProductId()); if (product null) { throw new BizException(商品不存在); } // 创建订单主表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setShopId(product.getShopId()); order.setAmount(product.getPrice().multiply(new BigDecimal(dto.getQuantity()))); order.setStatus(OrderStatus.PENDING_PAYMENT); orderMapper.insert(order); // 创建订单明细 OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setProductImage(product.getImage()); item.setPrice(product.getPrice()); item.setQuantity(dto.getQuantity()); orderItemMapper.insert(item); // 乐观锁扣减库存 int rows productMapper.reduceStock(product.getId(), dto.getQuantity(), product.getVersion()); if (rows 0) { throw new BizException(库存不足或操作冲突请重试); } return OrderVO.from(order); }4. Vue前端实现与接口联调4.1 Vue工程结构与权限路由实现前端这部分我用的Vue版本是Vue 2.7配合Vue CLI虽然是Vue 3已经普及的时代但考虑到Element UI组件库的成熟度和大量历史资料Vue 2.7对做管理系统来说依然非常顺手。如果是从零开始的新项目也可以直接选择Vue 3Element Plus思路完全一致。工程目录我按views、router、store、api、utils、components这几个标准模块组织views下面再按角色分目录student、merchant、admin各自独立这样代码归属清晰不会互相污染。路由的权限控制是这个系统的亮点之一。我没有把全部路由写成静态路由而是采用了动态路由方案。用户登录后后端根据角色返回该用户可访问的路由菜单列表前端拿到后通过router.addRoutes动态添加路由再配合全局前置守卫做一个拦截判断。如果用户未登录直接访问任何页面都跳转到登录页如果已登录但访问了无权访问的路由跳转到403页面。这样做带来的直接好处是商家登录后根本拿不到管理端页面相关的路由配置即使手动在地址栏输入路径也会因为没有匹配路由而被拦截。Vuex状态管理里我只存了核心的登录用户信息和侧边栏折叠状态没有把大量业务数据往store里塞。因为校园商铺系统的大部分页面数据都是通过接口实时获取的放store里反而要处理数据过期问题增加复杂度。有一个经验是登录用户的角色字段必须持久化到localStorage里刷新页面后Vuex重新执行初始化从localStorage恢复用户信息才能保证刷新后仍然停留在当前页面而不是被路由守卫踢回登录页。4.2 Axios封装与接口调试实战Axios封装这块我直接说结论一定不要在每个组件里单独发起请求那样代码重复度高且错误处理没法统一。我把axios实例封装到了utils/request.js里通过axios.create配置baseURL和超时时间然后在请求拦截器里从localStorage取出Token添加到Authorization请求头。比较关键的是响应拦截器统一处理HTTP状态码和业务状态码正常返回直接剥离外层结构把data返回给调用方401状态码出现说明Token过期或者无效这时清除本地用户信息跳转到登录页500状态码则统一使用Element UI的Message组件弹出错误提示信息。前端联调时遇到跨域问题是最多的。开发环境下我用了Vue CLI的代理功能在vue.config.js里配置devServer.proxy把/api开头的请求代理到后端服务的http://localhost:8080这样浏览器视角是同源请求完全不涉及跨域。生产环境则在Nginx配置反向代理把前端静态资源和后端API请求都挂在同一个域名下从根本上规避跨域。这个方案比在后端加CrossOrigin注解或者配置CORS过滤器要更贴近实际部署场景推荐优先使用。调试方面我的常用组合是Chrome开发者工具的Network面板加Vue Devtools浏览器插件。Network面板重点看请求的Method、Status、耗时、请求头和响应体排查404、500这类问题非常高效。Vue Devtools则主要看组件的数据流和Vuex的状态变化。如果某个接口报500错误我通常先用Postman单独请求一次接口排除前端参数问题如果是后端异常再看后端控制台打印的异常堆栈这个排查链路能解决90%以上的联调问题。后端日志配置也很重要。我在开发环境用了MyBatis的StdOutImpl把SQL打印到控制台每一次查询都能看到实际执行的SQL语句还能直接看到参数值调试起来非常直观。生产环境则会关掉这个配置避免日志量过大影响性能。手动测试时我经常在SQL日志里看执行时长发现单条SQL超过200毫秒就会检查索引情况这一个习惯帮我提前发现了不少潜在的慢查询问题。5. 常见问题与避坑记录5.1 跨域、Token失效与请求拦截问题前端访问后端接口报CORS错误是最常见的第一道坎。现象就是浏览器控制台提示“Access-Control-Allow-Origin”相关错误。我这边的排查经验是开发环境优先检查vue.config.js里的代理配置是否生效看看Network面板的请求URL如果请求仍然发到了localhost:8080而不是通过代理转发说明proxy配置没生效需要重启前端开发服务器。如果是生产环境检查Nginx的location匹配规则确保/api前缀的请求被正确proxy_pass到后端服务地址。还有一个容易忽略的细节是Token失效后的全局处理。默认情况下多个接口同时请求时如果Token过期后端会同时返回多个401响应前端拦截器也就会触发多次跳转登录页的逻辑用户体验非常差。我的处理方式是引入一个状态标志isRefreshing401出现时先把请求放进一个队列里只发起一次刷新Token的请求刷新成功后再把队列里的请求重新发出去刷新失败才跳转登录页。虽然刷新Token逻辑在校园商铺系统里用得不多但处理好了能避免很多奇怪的跳转问题。后端的请求拦截器也踩过一个坑。HandlerInterceptor里校验Token通过后我一开始直接把userId塞进了request的Attribute里然后在Controller方法里通过RequestAttribute取出来。但后来发现如果Controller方法里有异步操作或者过滤器链调整Attribute可能取不到值。后来我改用ThreadLocal包装用户信息在拦截器里设置在请求结束后清除简单可靠。注意ThreadLocal一定要在afterCompletion里调用remove方法否则线程池复用线程时会留下脏数据导致一个用户请求拿到另一个用户的信息。5.2 中文乱码、时间格式与返回数据结构统一中文乱码问题前后端都有可能。后端方面数据库连接URL里必须加上characterEncodingutf8这个我前面提过。前端方面axios默认的Content-Type是application/json如果通过表单方式提交中文数据要先encodeURIComponent编码或者干脆统一用JSON格式提交避免编码不一致的问题。另外后端接口返回的中文如果出现这种字符检查一下数据库表字符集是否为utf8mb4MySQL 8.0默认就是utf8mb4但如果是拿旧库改造的表结构未必是utf8mb4需要ALTER TABLE转一下。时间格式的问题也困扰过我一阵子。Java 8里的LocalDateTime默认序列化成JSON时是一串数组格式不能被前端直接使用。我采取的方案是在application.yml里配置Jackson的日期格式同时给实体类的时间字段统一用JsonFormat注解指定pattern为yyyy-MM-dd HH:mm:ss。这样前端拿到的是格式化好的字符串直接展示就行不需要再做转换。入库时前端传yyyy-MM-dd HH:mm:ss格式的字符串Jackson也能自动反序列化成LocalDateTime不需要额外写转换器。统一返回结构这件事我强烈建议在项目一开始就做好。无论成功还是失败后端接口都返回Result对象包含code、message、data三个字段。code为200表示成功其他为业务错误码。全局异常处理器捕获所有异常转换成对应的Result返回给前端。前端响应拦截器里判断code如果不是200直接弹出message。这样做的好处是前端不需要在每一个请求回调里都写try-catch和错误提示逻辑代码清爽很多。一个完整的接口返回结构如下public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }5.3 MyBatis动态SQL与数据库性能优化记录MyBatis的动态SQL是指 product_list 这种多条件查询场景里的救命稻草。商品列表页需要根据商品名称、分类ID、价格区间、商铺ID进行组合筛选用动态SQL根据条件拼接WHERE子句代码可读性和维护性都非常好。我写动态SQL的经验是统一用 标签包住所有 条件它会在条件成立时自动生成WHERE关键字并且自动去除第一个条件前面的AND避免手动拼接SQL时出现语法错误。SELECT * FROM product AND name LIKE CONCAT(%, #{name}, %) AND category_id #{categoryId} AND shop_id #{shopId} AND price #{minPrice} AND price #{maxPrice} ORDER BY create_time DESC商品列表还有一个很容易踩的性能坑就是MyBatis的N1查询问题。一开始我查询商品时需要关联出商铺名称直觉写法是在循环里根据shopId逐条查商铺信息第一次查N条商品就要执行N1次SQL数据量一大接口直接变慢。后来我改用resultMap的association标签配置关联查询或者在业务层先批量查出所有涉及的商铺ID再用IN查询一次查出商铺Map在内存里做关联。我实际用的是后一种方案虽然代码多几行但SQL执行次数稳住了数据量到几千条时性能差距非常明显。数据库索引这块我的原则是根据实际查询的WHERE条件和排序字段设计索引。商品表里category_id和shop_id是高频查询条件我建了联合索引idx_category_shop。订单表里user_id和status字段一起查个人订单状态的频率很高建了联合索引idx_user_status。还有一个容易被忽略的点是外键字段一定要建索引即使数据库层面没有声明外键约束业务代码里按这个字段关联查询时也要走索引否则联合查询会全表扫描。使用EXPLAIN关键字查看SQL执行计划是我每次排查慢SQL的第一步看type是否为index/range/ref看possible_keys和key是否一致能快速定位索引有没有生效。5.4 部署上线阶段遇到的问题与细节补充项目全部写完后的部署也有一堆需要注意的地方。前端打包用npm run build产物在dist目录里里面是纯静态文件。后端打包用mvn package得到可执行的jar文件。生产环境我用Nginx托管前端静态文件并把/api请求反向代理到后端端口。这里最关键的一个问题是如果前后端分开部署在不同的服务器上后端需要配置CORS允许的前端域名否则浏览器跨域请求会被拦截。如果像我在同一台服务器上通过Nginx统一入口部署就不需要配置CORS因为所有请求同源。数据库初始化我用的是SQL脚本加Spring的schema.sql自动执行方案。开发环境和生产环境的数据库连接配置不一样我通过SpringBoot的profile机制解决application-dev.yml和application-prod.yml分别维护不同的环境参数。部署时用java -jar app.jar --spring.profiles.activeprod指定使用生产环境配置。密码这类敏感配置一定要配置在环境变量里不要写死在配置文件里提交到代码仓库这是我踩过一次教训后养成的习惯。最后还要提一个校园商铺系统特有的部署细节就是图片上传和访问路径。本地开发时我用了文件上传把图片存到本地磁盘路径上前端通过/upload/xxx.jpg访问。部署到服务器后这个路径就失效了因为服务器的磁盘结构和本地不一样。稳妥的做法是规划一个独立的静态资源目录比如/data/campus-shop/upload然后配置Nginx把/upload前缀映射到这个目录上。后端上传时动态获取配置的磁盘路径避免硬编码。更复杂的方案是接OSS对象存储但校园项目用本地磁盘加Nginx映射已经足够。6. 页面交互实现要点与用户体验优化6.1 商品浏览与购物车体验优化商品浏览页面是学生用户使用频率最高的模块列表加载速度和筛选体验直接影响用户的留存。我实现了搜索关键字防抖用户输入停止500毫秒后才发起查询请求避免每敲一个字就触发一次接口调用。商品卡片按网格排列展示图片、名称、价格和月销量几个核心信息点击卡片跳转商品详情。详情页展示完整信息包括商品介绍、库存状态、商铺信息和加入购物车按钮。库存不足时按钮置灰并提示缺货这种前置的交互状态判断能减少无效请求。购物车逻辑是在后端实现的数据库设计了cart表而不是简单存在前端localStorage里。因为用户在不同设备登录后购物车要保持一致存后端才是正确方案。购物车加购接口做了重复判断如果同一商品已存在于购物车里则数量累加而不是插入重复记录。购物车列表接口返回商品当前价格、库存状态、选中状态前端可以实时勾选多个商品合并下单。这个合并下单的逻辑是一个购物车多商品组成一个订单的功能后端通过循环插入订单明细实现。6.2 商家端订单处理与数据看板设计商家端最核心的页面是订单管理。我按订单状态做了标签页切换待付款、待发货、已发货、已完成、已取消商家最常用的是待发货页签看到新订单后点击发货按钮填写物流单号完成发货。订单列表表头固定内容区域滚动数据量大时操作不会迷失。订单详情页通过抽屉组件展示不跳转页面就能看到完整信息操作效率高不少。商家端的数据看板主要用于查看店铺经营概况。首页展示今日订单数、今日销售额、总商品数、待发货订单数这几个统计卡片下面用ECharts渲染最近7天的销售趋势折线图和商品销量排行柱状图。后端提供一个店铺统计接口一次返回所有统计数据前端按数据结构渲染。这类接口的SQL要注意效率日期聚合用DATE_FORMAT(create_time, %Y-%m-%d)分组配合订单表的索引刷一下就能出来。看板数据不用实时刷新我在前端做了10分钟的定时轮询成本和体验之间取一个平衡。6.3 管理端的审核流与全局配置管理端承担着系统治理的职责。商铺审核是重点功能商家提交店铺信息后管理员在待审核列表里查看详情确认资质无误后点击通过商铺状态变为营业中商家就能上架商品了。拒绝时填写原因商家端能看到拒绝理由并修改后重新提交。这个审核流程我用状态字段配合更新时间来实现审核记录存到一张审核日志表里后续有争议时可以追溯历史。全局配置这里我做了一个简单的参数表存放系统名称、公告内容、订单超时时间阈值等配置项。比如未支付订单超过30分钟后自动取消并释放库存这个时间阈值就放在配置表里管理员可以在页面直接修改不用改代码重新部署。定时任务用SpringBoot自带的Scheduled注解实现每5分钟扫描一次超过时间的待支付订单批量取消并恢复库存校园商铺这种规模的任务用单机定时任务就够了不需要引入XXL-JOB这类分布式调度框架。7. 从开发到交付的全流程复盘做完整个系统再回头看我最大的感触是技术选型本身不是项目成败的关键真正拉开差距的是对业务场景的理解深度和把细节做扎实的执行力。用户角色怎么划分、商品库存怎么扣、订单状态怎么流转、业务数据要不要做快照这些设计层面的东西决定了系统能不能经得起真实使用场景的考验。SpringBoot和Vue只是工具工具用熟了大家差不多但设计思想和工程素养不是一朝一夕能补上的。对正在准备毕业设计或者找工作项目展示的同学我有一条很实在的建议宁可项目功能少做两个也要把已做的功能做到逻辑完整、细节经得起问。比如我在面试时聊订单模块面试官会追问事务怎么控制、并发下库存怎么保证不超卖、订单状态怎么防止非法跳转这些才是体现技术含量的地方。如果你做了订单模块却答不上来这些项目含金量会打折反过来哪怕只有一个订单模块能把并发控制、事务边界、状态机设计讲透也足够证明你的后端基础很扎实。最后分享一个很小的经验技巧。校园商铺这类系统前后端联调阶段最容易出乱子的就是接口返回的数据结构和预期不一致。我后来养成了一个习惯后端每个接口写完先用Swagger文档固定下来前端同学按Swagger文档里的字段定义对接字段名和层级通过后再做页面渲染。这个小改动让联调效率提升非常明显不用再频繁跑过去问“这个字段叫什么”“这个字段的取值有哪些”减少了很多无意义的沟通成本。