
前阵子接了个校园便利平台的项目前后端分离后端用的 SpringBoot前端用的 Vue。本来我以为是又一个常规的管理系统真正做下来才发现校园场景和普通电商完全是两套逻辑。这个项目的价值不在于代码本身多高深而在于把“校园需求”翻译成一套可运行、可交付、可维护的工程配套的源码、数据库脚本和文档也都整理出来了。这篇文章我打算把整个项目的设计思路、数据库建模、核心代码实现、打包部署、常见坑位全部复盘一遍不管是拿来交课程设计、毕业设计还是想自己搭一个校园服务类小平台应该都能少走几条弯路。1. 校园便利平台的需求拆解与功能定位1.1 校园场景和普通电商的本质差异校园便利平台听起来很像一个小型电商但真正调研之后会发现校园里的“便利需求”远比交易行为更丰富。学生之间最痛点的几件事一是二手物品交易教材、台灯、电动车、宿舍小冰箱毕业季更是一波大换血二是代取快递和跑腿校区大、快递点分散上课时段根本走不开三是失物招领校园卡、耳机、钥匙丢失频率极高四是校内信息公告比如实验室招人、社团活动、临时拼车。这些需求平时散落在 QQ 群、微信群信息被刷掉就没了而且没有评价机制可靠性全凭运气。校园便利平台要做的就是把散落的信息“结构化”让买卖、跑腿、失物、公告各自有各自的状态流转和闭环。这也是商品模块、订单模块之外我额外设计了失物招领、公告、评论评分的原因。如果只照着外卖和电商平台抄做出来就是一个脱离校园语境的系统验收时也容易被认为是“套了个壳”。1.2 角色权限模型设计整个平台的角色我压缩成三类学生、管理员、配送员。学生是默认角色可以在平台上发布闲置物品、下单、接单跑腿、发布失物信息配送员本质上还是学生但经过管理员审核后可以多一个接跑腿单的权限我把他做成用户表里的一个角色字段而不是单独建表这样登录鉴权更简单也不容易把边界搞复杂。管理员承担的是内容治理角色审核商品、处理举报、删除违规失物/公告、封禁用户。这里要注意一个细节权限模型不要一开始就上 Spring Security 的完整框架直接基于 JWT 拦截器判断角色就能把项目控制得很清晰。后期需要精细权限再升级也不迟。角色通过 user 表的 role 字段区分接口用自定义注解或者拦截器校验角色编码代码量少阅读理解成本低。1.3 功能模块边界划分我把系统拆成八个核心模块用户认证、商品中心、订单中心、跑腿任务、失物招领、公告中心、评论系统、管理后台。商品中心和订单中心是体量最大的部分也是数据库设计的重心。跑腿任务和失物招领是校园特色板块跑腿任务需要存储起点、终点、期望完成时间和佣金金额失物招领需要区分“寻物”和“招领”两种类型并处理认领流程。管理后台用同一套 Vue 前端做不过引入路由级别的权限控制管理员和学生登陆后看到的菜单不一样。这个设计对最终演示很友好别人一看就知道系统不是只做学生端后台数据管理能力也完整。2. 技术选型背后为什么 SpringBoot Vue 是最舒服的组合2.1 后端框架版本与配套组件选择的理由后端我选的是 SpringBoot 2.7.x这个版本特别稳既兼容了 Java 8又能方便地整合 Redis、MinIO 这些常用组件。SpringBoot 3.x 虽然已经出了好几年但对很多老环境和服务器镜像不友好尤其你这套系统要交作业或者部署到学校机房JDK 版本可能只有 8选 2.7.x 能省掉一堆环境问题。项目官方文档里也建议用户用 2.7.x 起步我实际踩下来确实比 3.x 省心。数据访问层我用了 MyBatis-Plus没有选 Spring Data JPA。原因很直接校园系统有大量多表关联查询、状态统计、模糊搜索MyBatis-Plus 既能写 XML 管复杂 SQL又有内置的 CRUD 方法可以少写大量重复代码。分页直接使用 MyBatis-Plus 的分页插件配合前端 Table 组件一个分页查询的接口 10 分钟就能写完。数据库连接池必须是 Druid 或者 HikariCPSpringBoot 2.7 默认用 HikariCP实测高并发下性能很稳定不需要额外换。2.2 前端用 Vue 3 Vite Element-Plus 的体验前端选择了 Vue 3 Vite Element-Plus没用 Vue CLI。Vite 的冷启动速度和热更新比 Webpack 时代舒服太多开发阶段改一行代码页面秒级刷新体验上的优势肉眼可见。Vue 3 的组合式 API 配合 setup 语法糖写业务弹窗、列表页面比 Options API 更紧凑。状态管理用了 Pinia没有用 Vuex。Pinia 的模块结构更直观而且对 TypeScript 的支持天然很好。这里建议项目尽量上 TypeScript不用全部强制类型但至少给接口请求体和响应体定义 interface后期维护时能少踩一半的雷。路由直接使用 Vue Router 4配合动态菜单渲染管理员和学生登录后看到的导航从后端接口获取而不是写死在前端。2.3 文件存储为什么引入 MinIO 而不是本地路径项目中商品图片、失物照片、用户头像都是文件存储的刚需。一开始很多同学喜欢直接存到本地磁盘的“upload”目录写起来确实简单但后面打包部署、数据备份、服务器迁移时会很痛苦文件散落在服务器上一旦数据要迁移图片和数据库对不上就麻烦了。我的做法是引入 MinIO开箱即用的对象存储服务支持私有化部署不用对接云厂商的 OSS对学校和课程设计场景非常合适。MinIO 通过 SpringBoot 的配置类注入 client提供桶创建、上传、预签名 URL 下载这几套接口。生产中可以替换成 Aliyun OSS 或者腾讯云 COS只需要改实现类业务层不用动。MinIO 的配置包括 endpoint、accessKey、secretKey、bucketName这些全部写在 application.yml 中正式交付源码时只需要用户自己改配置就能跑起来。3. 数据库设计把这些表拆出来系统才算立得住3.1 核心表清单与字段规划表设计是整个系统最值得花时间的部分定型之后代码基本就是按表写 CRUD。我最终定了 12 张表用户表、商品分类表、商品表、商品图片表、订单表、订单项表、跑腿任务表、失物招领表、公告表、评论表、用户举报表、积分记录表。这里我挑几个关键表来说。用户表必须有 id、username、passwordBCrypt 加密后存储、nickname、avatar、phone、role0 学生 / 1 配送员 / 2 管理员、status、create_time。商品表包含 id、user_id、category_id、title、description、price、original_price、quality几成新、status0 上架 / 1 下架 / 2 已卖出、view_count、create_time。图片表单独拆出来是为了方便一个商品对应多张轮播图同时也为后期做瀑布流缩略图留好余地。订单表比较关键需要同时满足商品购买和跑腿交易的场景我是统一用 order_type 字段区分1 代表二手交易2 代表跑腿任务。订单表里包括 order_no、buyer_id、seller_id、total_amount、status、create_time、pay_time、finish_time。跑腿任务表则是独立业务有 task_no、publisher_id、receiver_id、start_location、end_location、reward、status、expect_time。3.2 状态字段用枚举约束而不是随便填数据库里最容易烂的就是状态字段很多人用 int 存 0、1、2代码里到处写魔法数字过两周自己都记不住。干活时我强烈建议在 Java 代码里用枚举定义状态数据库字段加注释说明数字含义前端再生成对应的字典。三段式约束下来后期接需求或者报表统计都会非常清晰。订单状态我定义为0 待付款、1 已付款待接单/待发货、2 进行中、3 已完成、4 已取消、5 退款中、6 已退款。商品状态和订单状态解耦商品被下单完成后立即置为 2 已卖出防止别人再下单。跑腿任务状态独立一套0 待接单、1 已接单、2 已完成、3 已取消。3.3 索引与连表查询优化数据库规模虽然不大但还是要避免全表扫描。user 表的 username 加唯一索引订单表的 buyer_id、seller_id、status 加复合索引商品表的 category_id 和 status 加联合索引评论表的 goods_id 加普通索引。分页查询商品列表时where 条件注意用状态字段过滤掉下架和已卖出的数据。连接池参数也顺手调一下initialSize5、minIdle5、maxActive20、maxWait60000超时时间和空闲检测时间按默认经验值设置。配置在 application.yml 中配合 Druid 监控页面能看到慢 SQL 和并发情况演示的时候开一下监控特别加分。SQL 脚本我拆分成了 schema.sql建表语句和 data.sql初始化数据初始化数据包括一个管理员账号、几个测试商品和分类保证源码导入后不需要额外折腾就能直接看到页面效果。4. 核心模块代码实现从 Controller 到 Service 的一整套写法4.1 用户登录注册与 JWT 鉴权认证这块我没有引入复杂的框架直接用 JWT 拦截器解决。用户注册时密码通过 BCryptPasswordEncoder 加密再入库登录成功后生成 JWT tokentoken 里放入 userId 和 role过期时间设置为 7 天。前端把它存在 localStorage 中每次请求在 Axios 拦截器里带上 Authorization 头。后端写一个 LoginInterceptor 实现 HandlerInterceptor在 preHandle 里解析 token、校验过期时间、把 userId 放入 ThreadLocal 中供本次请求使用。排除登录、注册、首页商品列表、失物招领、公告这些匿名可访问的接口。管理员接口再通过一个自定义注解或者简单角色判断校验 role 是否等于 2。这种写法代码量少而且学生理解起来很容易。需要注意刷新 token 的机制。虽然本项目 7 天过期够用但如果是实际部署建议在登录时同时返回一个 refreshToken过期后无感刷新能避免用户用着用着突然弹出重新登录。我这次为了演示简单暂时没做刷新但源码中留了对应注释。4.2 商品发布与服务端校验商品发布是一个典型的前后端配合功能。前端表单提交到后端时后端必须重新校验用户是否存在、商品分类是否存在、价格是否大于 0、标题长度是否合法。千万不能只依赖前端校验防止有人通过接口直接绕过前端。Service 层大致流程先通过当前登录用户 ID 查出用户信息然后组装 Goods 实体status 默认 0view_count 默认 0然后保存商品和图片列表。保存图片列表时要与主表事务保持一致否则会出现商品有了图片不见了。图片上传部分走 MinIO 预签名 URL 的方式前端先请求后端获取上传凭证然后直传 MinIO最后只把可公开访问的 URL 保存到数据库。这里要特别提醒一下商品列表接口不要一次性把 description 这种大文本字段全查出来列表页只查必要的字段等用户点击详情再查全文。分页查询可以用 MyBatis-Plus 的 LambdaQueryWrapper也可以自己写 XML 完成多表连接业务不复杂的情况下 LambdaQueryWrapper 完全够用。4.3 下单与防超卖设计下单是整个系统最见细节的地方。用户购买商品时先根据商品 ID 查询商品状态必须处于上架状态才能下单。然后开启一个事务首先尝试更新商品状态为“已卖出”如果 update 影响行数为 0说明已经被别人抢先下单了直接抛出库存不足/商品已下架的异常。这个做法本质上是乐观锁的思路把状态更新本身作为原子操作比先查再更新更安全。订单生成之后要生成一个唯一的订单号我采用“日期格式 用户ID后四位 随机数”的方式尽力保证分布式环境下不重复。订单创建后状态为待付款因为演示环境不接真实支付所以提供一个模拟支付的接口把订单状态从待付款变成待发货/待接单。整个流程的前端状态变化依赖订单状态字段因此后端每个状态变更都要写清楚操作人和操作时间方便用户回溯。4.4 Vue 前端请求封装与路由守卫Vue 前端我把 Axios 做了一层封装统一配置 baseURL、超时时间、请求拦截和响应拦截。响应拦截器中如果返回 401直接清空 token 并跳回登录页如果业务码非 200通过 Element-Plus 的 ElMessage 弹出错误信息。这样业务代码里不用每个接口都写错误处理看代码非常干净。路由守卫分为全局前置守卫和动态路由两个部分。全局前置守卫判断 localStorage 里有没有 token没有且目标路由需要登录则跳转登录页。动态路由在用户登录后拉取后端菜单权限然后通过 router.addRoute 动态挂载路由。Vue 3 里这一步要注意路由实例必须 export 出来守卫文件与路由实例不要循环引用否则很容易出现“No match”类型的问题。5. 源码、数据库和文档一份能交付的完整项目应该长什么样5.1 源码目录结构与模块划分前后端分离的源码目录结构一定要清爽。后端我按 com.campus.platform 作为包根底下再分 controller、service、mapper、entity、common、config、utils。前端 src 下按 api、assets、components、router、stores、utils、views 划分。views 里按 role 文件夹分学生端和管理端页面组件命名尽量用“页面 功能”例如 GoodsList.vue、GoodsDetail.vue、OrderList.vue团队成员一眼就能找到对应文件。前后端各自带着 pom.xml 和 package.json后端用 Maven 构建前端用 npm 或 pnpm 构建。源码交付出去之前我在每个模块的入口处都写了注释说明模块职责、核心类和关键流程。当然了源代码里不能把真实数据库密码、MinIO 密码写成明文写死我会提供 application-example.yml把敏感信息用占位符替换然后在文档里教你复制一份改成正式配置。这样源码传到网上、发给别人也不用担心泄露。5.2 数据库脚本与初始化数据数据库脚本是能跑起来的前提但很多人经常忽略数据脚本的重要性。我的 schema.sql 里除了建表语句还写了每个字段的注释、默认值、索引定义。data.sql 里初始化了管理员账号密码用 BCrypt 的哈希值、12 个商品分类、3 条公告、2 个测试商品和 2 条失物招领记录。这样导入数据库后登录页面直接能看到数据而不是一张空表对于课程设计验收场景来说体验好很多。如果你使用的 MySQL 版本是 8.0注意脚本中要显式设置字符集为 utf8mb4否则中文会乱码。数据库连接地址、账号密码需要根据自己环境改成实际值。脚本末尾可以加一段 clear 业务数据的 SQL 注释方便二次开发。5.3 项目文档应该包含哪些层面交付的“文档”不只是 README 一句话我建议一份像样的项目文档至少包含五个部分项目介绍与功能清单、技术栈说明、快速启动指南、数据库设计说明含 ER 图和表结构、API 接口说明。如果对象是课程设计/毕业设计还需要额外写一份系统操作手册和测试报告。启动指南必须写到傻瓜级JDK 版本、Maven 版本、Node 版本、MySQL 版本、Redis 是否必须、MinIO 怎么启动、后端参数怎么改、前端怎么启动。API 文档不需要用特别重的 swagger 插件我直接在 doc 目录下放一个 Markdown 文件每个接口写清楚请求方法、请求参数、响应示例。这样打开文档的人不需要把项目启动起来也能理解接口逻辑。文档结构化解析的意思是把需求文档、设计文档、部署文档分开命名规范加上日期避免以后更新后找不到最新版。6. 本地启动与打包部署从 Vite 到 SpringBoot 的完整过程6.1 本地开发环境的快速启动流程拿到源码后启动分四步。第一步导入数据库新建一个 campus_platform 数据库执行 schema.sql 和 data.sql。第二步准备中间件安装并启动 Redis 和 MinIOMinIO 需要创建一个 public 桶并配置好访问密钥。第三步启动后端用 IDEA 打开 backend 目录修改 application.yml 里的数据库账号密码、Redis 地址、MinIO 配置然后启动 SpringBoot 项目。第四步启动前端进入 frontend 目录执行 npm install然后 npm run dev浏览器访问 Vite 给的本地地址即可。如果你要用 pnpm注意版本要跟 package.json 里的 packageManager 保持一致否则安装依赖时可能出现各种奇怪的问题。整个流程我踩过很多次坑最常发生在 MinIO 桶权限和跨域配置上后面我专门讲。6.2 Vue 打包放进 SpringBoot 的单体部署方案部署方案有两种一种是传统的分开部署前端打包成静态资源扔到 Nginx后端单独跑 8080 端口另一种是前后端合并部署把 Vue 打包后的 dist 内容放到 SpringBoot 的 src/main/resources/static 目录下这样整个系统只有一个 8080 端口对外直接用 IP 访问开发和演示都很方便。合并部署要特别注意 Vue Router 的 history 模式。如果前端路由使用了 history 模式而直接把静态资源丢进 SpringBoot刷新页面就会出现 404。解决办法有两种在 SpringBoot 里写一个路由兜底 Controller让非 /api 开头的路径全部转发到 index.html或者干脆使用 hash 模式。hash 模式虽然 URL 上多一个 #看起来没那么漂亮但对于课程设计和内部演示反而最省事不用处理服务端资源映射也免去 Nginx 的 try_files 配置。我推荐在交付文档里把两种模式都讲清楚默认代码里使用 hash 模式部署零成本。6.3 Docker Compose 一键部署方案可选加分项如果不想被各种环境变量折磨可以在项目中提供一份 docker-compose.yml把 MySQL、Redis、MinIO、后端、前端分别编排起来。后端镜像用 openjdk:8-jre前端用 nginx:alpine挂载目录把 dist 和 nginx 配置映射进去。一个 docker compose up -d 就能把整套环境拉起来。不过要注意Docker 部署时数据库数据持久化要用 volume不要放在容器内部否则 docker compose down 之后数据全部丢失。MinIO 的端口如果和宿主机冲突需要改成别的端口。这个方案我放在“扩展资料”里没有作为主路径因为有些学校机房没法运行 Docker但会写清楚怎么用。7. 常见问题排查与踩坑记录7.1 启动阶段的高频问题下面我列一个问题速查表基本覆盖了大家最容易遇到的情况每一条都是真实踩过坑之后验证过的。现象可能原因解决方案后端启动报数据库连接失败MySQL 未启动 / 账号密码错误 / 驱动版本不匹配检查 MySQL 服务核对 application.yml改用 mysql-connector-j 8.x后端启动报端口被占用8080 被其他进程占用临时改成 server.port8081 或杀掉占用进程前端 npm install 失败网络环境 / node 版本过高使用镜像源或安装 Node 16 LTSVite 能启动但接口 404前端代理配置错误配置 vite.config.js 的 server.proxy / 检查后端 context-path图片上传失败MinIO 未启动 / 桶权限私有 / 跨域未开启动 MinIO创建公共读策略设置 CORS 允许来源登录后刷新页面 404前端使用 history 模式改用 hash 模式或后端兜底转发到 index.html7.2 开发过程中最值得注意的 5 个细节第一跨域问题。前后端分离开发时前端访问后端一定会有跨域我在后端写了一个全局 CORS 配置类允许本地开发地址和部署地址的请求。如果你换了一个端口记得把 origins 改一下否则前端会报 CORS error。第二ThreadLocal 存用户信息时要记得过滤器里 finally 清除否则线程池复用时可能会把上一个请求的用户信息串到下一个请求中这个 bug 非常隐蔽排查成本很高。第三Element-Plus 表单校验要配置正确的 trigger 属性比如 blur / change。我在商品表单里因为 trigger 配错了导致二次校验不触发用户填写完了点提交还能被判定为空。第四分页插件不要和 ThreadLocal 乱用。MyBatis-Plus 分页插件需要配置 PaginationInnerInterceptor并且体检分页参数要从 Page 对象中获取如果前端传的当前页字段名对不上查出来的数据永远是第一页。第五JWT 密钥不要用默认值。源码里我用的是随机字符串如果你从模板复制一定要改掉因为密钥泄露意味着任何人都可以伪造 token这是一个严重的安全隐患。7.3 总结一点经验这类校园平台项目最大的难点往往不在新技术而在“流程闭环”。商品从发布到下架订单从创建到完成跑腿任务从悬赏到交付每一步都不能断。数据库设计时把状态流转想清楚代码实现时把事务和状态边界控制好项目就成功了一大半。我做完这个项目最大的体会是业务先跑通再做技术美化。把表格字段和接口先定下来后面 Vue 页面和 SpringBoot 代码无非就是按图施工。