简介:基于Spring Boot与Vue的校园便利平台毕业设计源码,是一份经导师认可、评分98分的完整项目,整体采用前后端分离架构。它主要面向计算机、电子信息、数学等专业正在准备毕业设计或课程设计的学生,也适合刚入门前端或后端、想通过完整项目提升实战能力的开发者。资源包共610个文件,压缩包大小约14.76MB,文件类型以Java后端源码、Vue页面组件、SVG图标、XML及yml配置、JavaScript脚本、CSS样式和批处理命令为主,整体结构清晰,方便按业务模块阅读和二次开发。目前已有106人学习或下载。内容包含校园便利服务相关的前后端业务代码、关键页面组件的备份文件、一键构建与启动脚本,并保留部分配置文件副本便于调试比对;代码经过严格调试,可直接作为毕业设计演示、功能扩展或项目答辩的参考,也有助于理解Vue与Spring Boot之间的数据交互和项目部署流程。
1. 别把“基于Springboot和vue的 校园便利平台”当成一个 CRUD 项目来做
很多第一次拿到校园便利平台源码的开发者,会把它当作又一个“Spring Boot 加 Vue 的增删改查”来看。真正动手改的时候才发现,用户表和商品表只是热身,订单状态流转、登录态失效后该跳到哪里、部署上线后静态资源路径错乱,才是消耗时间的大头。这套毕业设计源码的业务边界很清晰:跑腿代办、闲置转让、校园公告可以共用同一套用户与订单模型;后端用 Spring Boot 提供 REST 接口,前端用 Vue 管理页面状态,两边靠 JSON 交互。另一个反直觉的结论是,不要一开始就去补全后台管理页面,而是先把“用户下单、接单、送达、评价”这条主干线跑通。下面的章节会按后端设计、前端实现、联调部署和答辩改进的顺序展开,只讲你改这份源码时最可能踩到的地方。
2. Spring Boot 源码改造第一步:数据模型和订单状态机
拿到源码先别急着启动,把后端目录结构读一遍再动手。校园便利平台这种规模的项目,常见做法是单 Maven 模块,而不是拆成多模块工程。单模块的好处在于,答辩时你能顺着 Controller 到 Service 再到 Mapper 走完一个请求,评审老师问“下单后数据库怎么变的”,你只需要翻开三层代码就能讲清楚。如果一开始就拆成一堆公共模块,反而会掩盖业务主线。
2.1 后端目录结构:按职责横向切,不按业务纵向切
一个适合毕业设计的目录结构通常长这样:
src/main/java/com/campus/platform ├── common # 统一返回体、全局异常 ├── config # 跨域、拦截器、Redis 配置 ├── controller # REST 接口入口 ├── entity # 数据库实体类 ├── mapper # MyBatis 映射接口 ├── service # 业务逻辑,接口与实现分离 └── utils # JWT、日期、随机数工具controller只做参数接收和结果包装,不写业务判断;service是重点,订单状态流转、库存扣减这类逻辑都应该在这里。common里的统一返回体Result需要包含code、message、data三个字段,这样前端拿到响应后能统一处理。为了避免每次返回都手写 Map,我会定义一个泛型类Result<T>,Controller 里返回Result.ok(data)或Result.error("xxx"),这也是 Spring Boot 项目里最常见的写法。
2.2 订单表是核心:状态字段用数字语义,不要用多列布尔值
校园便利平台的表一般包括用户、商品、订单、订单项、评论、公告。订单表是业务主干,设计时要注意快照字段,因为商品信息随时可能被卖家和平台修改。以订单表为例,核心字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(64) | 订单号,展示给用户看 |
| goods_id | bigint | 商品或代办事项标识 |
| buyer_id | bigint | 下单用户 |
| seller_id | bigint | 接单用户或卖家 |
| status | tinyint | 订单状态码,对应枚举 |
| amount | decimal(10,2) | 金额 |
| snapshot | varchar(255) | 下单时商品文字快照 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
snapshot字段是很容易被忽略的设计点。下单时把商品标题、价格、图片地址拼成一段文字或 JSON 存进去,之后即使商品被删除或改价,历史订单详情也不会出现空白。status字段用tinyint而不是字符串,是为了索引更快,也方便前端直接映射到状态标签。数据库里的状态码必须和代码里的枚举一一对应,否则就会出现“订单完成但前端显示待支付”这种低级问题。
2.3 用 Java 枚举做订单状态机,避免散落一地的 if 判断
很多新手会在 Service 里写if (status == 1) { ... } else if (status == 2) { ... },状态一多就乱。更常见的问题是允许了非法跳转,比如“待支付”直接变“已完成”。我一般会在entity包之外单独建一个枚举类OrderStatus,把允许的流转规则集中管理。
public enum OrderStatus { PENDING_PAYMENT(0, "待支付"), PAID(1, "已支付"), TAKEN(2, "已接单"), DELIVERING(3, "配送中"), COMPLETED(4, "已完成"), CANCELED(5, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public boolean canTransferTo(OrderStatus target) { switch (this) { case PENDING_PAYMENT: return target == PAID || target == CANCELED; case PAID: return target == TAKEN || target == CANCELED; case TAKEN: return target == DELIVERING; case DELIVERING: return target == COMPLETED; default: return false; } } public static OrderStatus fromCode(Integer code) { for (OrderStatus status : values()) { if (status.code == code) { return status; } } throw new IllegalArgumentException("未知订单状态"); } }调用方式是在 Service 里先查出当前订单,再通过currentStatus.canTransferTo(targetStatus)判断,不允许就直接抛业务异常。这里的关键参数是code,它在数据库里存整数,在 Java 里通过fromCode把查询结果转成枚举。这样写的好处有两点:所有合法状态转移一眼就能看完,答辩时被问到“取消订单后还能不能接单”,直接指向枚举即可;非法跳转在入口就被拦截,不会产生脏数据。如果想加“超时自动取消”,只需要在 Service 里判断创建时间超过 30 分钟后,再执行PENDING_PAYMENT到CANCELED的流转,不需要改动枚举。
2.4 JWT 拦截器与登录用户注入
后端接口不能每个方法都手动解析 token,常见做法是注册一个 Spring Boot 拦截器,在进入 Controller 前完成登录校验。
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); Long userId = JwtUtil.parseToken(token); if (userId == null) { response.setStatus(401); return false; } request.setAttribute("userId", userId); return true; } }JwtUtil.parseToken负责验证签名和过期时间,返回 null 表示 token 无效。把解析出的userId放入 request 属性后,Controller 里通过@RequestAttribute("userId") Long userId就能拿到当前登录用户。这样做的目的是避免每次业务操作都从 token 里再取一次用户 ID。角色校验可以放到拦截器里增加一个checkRole参数,也可以在进入 Controller 后用@RequireAdmin这类自定义注解解决。毕业设计做到拦截器这一层就足够清晰,不需要引入 Spring Security,因为后者的过滤器链会让项目复杂度上升不少。
3. Vue 前端工程化:路由权限、请求拦截和页面复用
前端部分如果还停留在“每个页面复制一份列表代码”的状态,后期改权限和状态展示会很痛苦。Vue 项目的重点不是页面多好看,而是路由守卫、请求封装和组件复用。接下来按我实际搭建这套前端源码的顺序,从环境安装到关键代码一块一块拆开。
3.1 Vue 项目初始化与依赖安装
这里使用 Vue 官方脚手架来初始化项目,Node 环境建议 18 或 20。执行下面的命令创建名为campus-front的工程:
npm create vue@latest campus-front cd campus-front npm install npm install axios element-plus pinia npm run dev创建过程中按需选择 Router、Pinia,不需要 TypeScript 时可以直接选否,让代码更简单。axios负责调用 Spring Boot 接口,element-plus提供表格、表单和弹窗组件,pinia管理登录状态。如果本地已经存在package-lock.json,安装依赖时把npm install换成npm ci能避免版本漂移问题。开发环境启动后默认端口是 5173,后端在 8080,两边端口不同但通过后端 CORS 解决访问限制。不要用任何浏览器插件绕过跨域,直接在 Spring Boot 侧配置允许来源,否则换一台电脑又得重新调试。
3.2 路由参数和登录守卫
后端接口需要登录才能访问,前端路由也需要相同的限制。Vue Router 的路由表里可以给页面标记meta.requiresAuth,然后在全局前置守卫里读取。
import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/goods/:id', component: () => import('@/views/GoodsDetail.vue'), meta: { requiresAuth: true } }, { path: '/orders', component: () => import('@/views/Orders.vue'), meta: { requiresAuth: true } } ] }) router.beforeEach((to) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.token) { return { path: '/login', query: { redirect: to.fullPath } } } return true })路由参数:id在对应页面里通过useRoute().params.id获取,适合商品详情、订单详情这种场景。这里的to.fullPath会包含 query 参数,登录成功后跳转回原页面时不会丢失列表筛选项。需要区分管理员页面时,可以在路由 meta 里再加一个role字段,守卫中判断userStore.role !== 'admin'就重定向到首页。Vue 页面的常见路由规划如下:
| 路由路径 | 页面组件 | 是否需要登录 | 说明 |
|---|---|---|---|
| / | Home.vue | 否 | 商品与服务列表 |
| /goods/:id | GoodsDetail.vue | 是 | 商品详情 |
| /orders | Orders.vue | 是 | 我的订单 |
| /admin/goods | AdminGoods.vue | 是 | 管理员商品管理 |
3.3 Axios 封装:注入 token 并处理 401
把 axios 实例单独抽成一个文件,所有页面都从这个实例发出请求。这样后端返回 401 时,前端能统一清除登录状态并跳转登录页。
import axios from 'axios' import { useUserStore } from '@/stores/user' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE || '/api', timeout: 10000 }) request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = userStore.token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.clear() window.location.href = '/login' } else { const message = error.response?.data?.message || '请求失败' ElMessage.error(message) } return Promise.reject(error) } )需要注意的是,response.data在拦截器里被直接返回完整数据,那么 Controller 里返回的Result结构就会直接透传到页面。页面里拿res.code === 0或res.success判断业务是否成功,具体字段取决于你自己的结果体设计。baseURL使用import.meta.env.VITE_API_BASE是为了在不同环境切换后端地址,开发环境可以直接在.env.development里写VITE_API_BASE=http://localhost:8080/api。
3.4 用状态标签组件统一订单展示
订单状态在列表、详情、个人中心都会出现,如果每个页面都写一遍if status === 0,文案和颜色很容易不一致。这里抽一个OrderStatus.vue,接收status数字,内部映射到 Element Plus 的 tag。
<template> <el-tag :type="typeByStatus[status]">{{ textByStatus[status] }}</el-tag> </template> <script setup> const props = defineProps({ status: { type: Number, required: true } }) const textByStatus = { 0: '待支付', 1: '已支付', 2: '已接单', 3: '配送中', 4: '已完成', 5: '已取消' } const typeByStatus = { 0: 'warning', 1: 'primary', 2: 'info', 3: 'info', 4: 'success', 5: 'danger' } </script>这个组件很小,但价值很高。订单状态文案只需要维护一处,后端新增状态时也只要改这个映射。对于校园便利平台,订单列表和接单大厅都能直接引入这个组件,避免复制粘贴导致的文案劣化。前端源码里类似的公共组件还应该有空状态显示和分页条,都是同样的复用思路。
4. 前后端联调:接口约定、跨域配置和 Long 型 ID 丢失
前后端分开开发时会遇到三类问题:接口路径对不上、跨域请求不通、JSON 数字精读丢失。这三类问题在基于 Spring Boot 和 Vue 的项目里几乎必现,越早处理越省时间。
4.1 接口路径按资源命名,前后端保持同一份表格
校园便利平台的接口不需要按微服务拆分,直接按资源命名即可。联调阶段最好用一张表固定路径,避免前端把/order/create写成/createOrder这类风格冲突。常用接口约定如下:
| 方法 | 路径 | 后端方法 | 说明 |
|---|---|---|---|
| GET | /api/goods/list | GoodsController.list | 分页获取商品 |
| GET | /api/goods/{id} | GoodsController.detail | 商品详情 |
| POST | /api/order | OrderController.create | 创建订单 |
| POST | /api/order/{id}/status | OrderController.updateStatus | 更新订单状态 |
| GET | /api/order/my | OrderController.myOrders | 我的订单 |
路径中的{id}对应 Vue 路由参数,比如订单详情页打开时请求/api/order/${route.params.id}。更新状态接口只有POST,后端入参通常是一个 DTO,包含目标状态和操作人备注。前端调用时只需要把目标状态码传过去,是否允许这条流转由后端枚举判断,前端不要做二次判断,否则容易出现“页面显示成功,接口返回错误”的分裂状态。
4.2 Spring Boot 跨域配置与前端配合
跨域的本质是浏览器阻止页面所在域名和接口所在域名不一致的请求。开发阶段前端在 5173 端口,后端在 8080 端口,最简单的方式是在 Spring Boot 里加一个全局 CORS 配置。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("http://localhost:*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里allowedOriginPatterns允许所有本地端口访问,适合开发期。生产环境要把http://localhost:*替换成你的实际域名或 IP。allowCredentials(true)表示允许携带 Cookie,如果你使用 token 而不是 Cookie,这个配置可以保留,因为 axios 默认不带 Cookie。注意,配置了 CORS 之后,前端依然需要把baseURL指向http://localhost:8080/api,否则请求还是发到 5173 端口去。
4.3 Long 型 ID 在前端丢精度的问题
Java 的 Long 类型最大可以到 19 位数字,JavaScript 的 Number 类型大约只能安全表示 53 位以内的整数。数据库主键超过9007199254740991后,前端拿到的 ID 会变成不精确的数字,点击“去详情页”时可能打开一个不存在的记录。解决方法是让 Spring Boot 把 Long 类型序列化成字符串。
@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer longToString() { return builder -> builder.serializerByType(Long.class, ToStringSerializer.instance); } }配置后,接口返回的id会变成字符串"123456...",前端路由参数:id也会拿到字符串,后端在处理路径参数时依然能自动转为 Long。这里要注意,普通long基本类型不会受此配置影响,所以实体类里尽量用包装类型Long,避免出现部分字段生效、部分字段失效的情况。这个问题在 Spring Boot 源码项目中很容易被忽略,但答辩时提出来反而是加分项。
4.4 用 @Valid 做参数校验,而不是在代码里手动判空
后端接口如果每个方法都写if (dto.getAmount() == null),代码会变得很长,还容易漏掉字段。Spring Boot 自带的 validation 依赖可以解决这个问题。
@PostMapping("/api/order") public Result createOrder(@RequestBody @Valid OrderCreateDTO dto) { return Result.ok(orderService.create(dto)); }OrderCreateDTO中字段上加@NotNull(message = "商品不能为空")、@DecimalMin(value = "0.01", message = "金额不合法")这类注解,校验失败时会抛出MethodArgumentNotValidException。全局异常处理器捕获后,把第一条错误信息返给前端,比默认的 400 错误结构友好得多。这里的重点是 DTO 和实体类分离,前端传什么、后端校验什么,都在 DTO 上体现,数据库实体类只负责持久化,不承担接口参数职责。
5. 本地跑通后,Spring Boot 与 Vue 的部署到生产环境
很多项目在本地一切正常,换到服务器就出现图片不显示、接口连不上数据库。部署经验也会成为答辩时谈资。这里给出一个最常见的部署顺序,先把 Spring Boot 打包成 jar,再把 Vue 构建成静态文件交给 Nginx,最后用 docker-compose 把 MySQL、后端、前端三个服务串起来。
5.1 Spring Boot 打包和配置分离
在后端根目录执行打包命令:
mvn clean package -DskipTests执行完成后,target目录下会生成一个可执行的 jar 包。生产环境不要直接修改application.yml,而是通过--spring.profiles.active=prod激活另一个配置文件。在application-prod.yml中,数据库地址从环境变量读取:
spring: datasource: url: jdbc:mysql://${MYSQL_HOST:localhost}:3306/campus_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: ${MYSQL_USER:root} password: ${MYSQL_PASSWORD:root123}MYSQL_HOST在本地运行时为空,默认落到localhost;在 docker-compose 环境里会被设置成mysql。这样做的好处是同一份 jar 可以同时用于本地测试和服务器部署,不需要维护两份打包产物。
5.2 Vue 构建出 Nginx 可托管的静态文件
前端执行构建:
npm run build构建结果在dist目录。Vue Router 使用 history 模式时,刷新/orders页面会导致 Nginx 返回 404,因为服务器上并没有真实的orders路径。Nginx 配置需要把所有请求落到index.html,让前端路由接管。
server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }在.env.production中把 API 地址指向真实后端地址,例如VITE_API_BASE=http://你的服务器IP:8080/api。前端页面和接口域名不一致时,依靠第 4 章的 CORS 配置放行。生产环境建议给这张 Nginx 配置加上 gzip 压缩和Cache-Control,但毕业设计做到能正常访问已经够用。
5.3 用 docker-compose 简化环境重建
服务器上可以使用 docker-compose 一次性启动三个容器,避免手动安装 MySQL 和 Java 环境。
version: "3.8" services: mysql: image: mysql:8.0 container_name: campus-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: campus_platform ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql backend: build: ./backend container_name: campus-backend environment: SPRING_PROFILES_ACTIVE: prod MYSQL_HOST: mysql MYSQL_USER: root MYSQL_PASSWORD: root123 ports: - "8080:8080" depends_on: - mysql frontend: build: ./frontend container_name: campus-frontend ports: - "80:80" depends_on: - backend volumes: mysql-data:各服务的对外端口和配置如下表格所示:
| 服务 | 容器名 | 对外端口 | 关键环境变量 |
|---|---|---|---|
| mysql | campus-mysql | 3306 | MYSQL_DATABASE |
| backend | campus-backend | 8080 | SPRING_PROFILES_ACTIVE |
| frontend | campus-frontend | 80 | 无 |
depends_on只保证 MySQL 容器先启动,不保证数据库已就绪。第一次启动时 Spring Boot 可能连接不上而报错,可以在后端容器里配置command: sh -c "wait-for mysql:3306 && java -jar app.jar",或者简单地在启动脚本里 sleep 十秒。图片上传功能如果还没接对象存储,需要把上传目录挂载到宿主机,不然容器重建后图片会丢失。
5.4 部署后的验证与检查
容器启动后,先确认后端进程健康,再打开浏览器测试页面。
curl http://localhost:8080/api/health docker compose logs backend | tail -n 50如果接口返回{ "code": 0, "message": "ok" },说明 Spring Boot 启动完成。同一个服务器上访问前端页面时,要确认浏览器开发者工具里的 API 请求是 200,而不是 CORS 报错。CORS 报错时回到CorsConfig检查allowedOriginPatterns是否包含当前访问地址。数据库密码修改后,必须同步修改 docker-compose 里的环境变量,否则后端连不上数据库会一直刷出Access denied日志。
6. 答辩前值得补的三个增强:Redis 缓存、WebSocket 推送和 Vue 播放 m3u8
如果时间还有富余,下面三个点都能做到“花半天加功能,答辩时多聊五分钟”。它们分别对应 Spring Boot 配置扩展、前后端实时通信、Vue 多媒体处理,正好覆盖毕业设计中最容易被提问的方向。
6.1 用 Redis 缓存热门商品列表
首页每次加载都全表扫描商品表,会被老师问到“并发访问高一点怎么办”。常见做法是引入spring-boot-starter-data-redis,在 Service 方法上做缓存。
@Cacheable(cacheNames = "hotGoods", key = "#page") public PageResult<GoodsVO> listGoods(int page, int size) { return goodsMapper.selectPage(page, size); }第一次访问后,结果会存进 Redis,后续请求直接走缓存。需要注意缓存更新策略,新增或下架商品时调用CacheEvict清掉对应 key,否则用户会一直看到旧数据。这个改动同时也能回应 Spring Boot 面试题里的缓存穿透和缓存击穿问题,答辩时把@Cacheable和@CacheEvict说清楚就够了。
6.2 用 WebSocket 把接单消息推给用户
当配送人员接单时,买家端不应该等到刷新列表才发现状态变化。后端可以增加一个 WebSocket 端点,在订单状态更新时推送消息到指定连接。
@ServerEndpoint("/ws/order/{orderId}") @Component public class OrderWebSocket { private static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("orderId") String orderId) { SESSIONS.put(orderId, session); } @OnClose public void onClose(@PathParam("orderId") String orderId) { SESSIONS.remove(orderId); } public static void send(String orderId, String message) { Session session = SESSIONS.get(orderId); if (session != null) { session.getAsyncRemote().sendText(message); } } }前端在订单详情页创建连接:
const ws = new WebSocket(`ws://${location.host}/api/ws/order/${orderId}`) ws.onmessage = (event) => { const data = JSON.parse(event.data) orderStore.updateStatus(data.status) }这里用ConcurrentHashMap保存会话,避免并发创建订单时出现线程问题。location.host需要和后端 WebSocket 地址一致,如果生产环境使用域名,记得把地址替换成完整域名。
6.3 在 Vue 里播放 m3u8 视频
校园便利平台如果增加“课程回放”或“校园公告视频”,最常见的播放格式是 HLS 的 m3u8。Vue 中播放这个格式可以使用 video.js,只需要安装对应的播放器和样式。
npm install video.js然后在组件中初始化播放器:
import videojs from 'video.js' import 'video.js/dist/video-js.css' const player = videojs('my-video', { sources: [{ src: 'https://example.com/course/lesson1.m3u8', type: 'application/x-mpegURL' }], controls: true })正常播放前,需要确认视频服务端返回的响应头包含Access-Control-Allow-Origin,浏览器加载 HLS 分片时会跨域请求.ts文件。可以在 Nginx 对视频路径添加add_header Access-Control-Allow-Origin *,生产环境再把这个*限制成自己的域名。用浏览器开发者工具看到网络请求返回 206 片段,说明播放链路已经走通。
本文还有配套的精品资源,点击获取