又是一个被毕业设计支配的季节。如果你正在springboot+vue这两个关键词里打转,或者已经决定了要做一套校园二手交易系统,但不知道从哪儿下手、数据库怎么设计、前端怎么跟后端联调、答辩时老师可能会从哪个角度问,那这篇文章就是按“从零到答辩通过”的完整链路来写的。
校园二手交易系统这个题目,在计算机毕业设计里属于典型的高性价比选择:业务边界清晰、用户角色不复杂、功能模块能讲清楚、演示起来也直观。但恰恰因为做的人多,答辩时老师的要求也会更具体。很多同学交上去的版本“能跑”,但一问到“订单状态是怎么流转的”“并发场景怎么处理”“JWT 过期了怎么办”就卡壳。这篇文章我会把这一整套系统的设计思路、表结构、核心代码逻辑、联调踩坑和答辩准备全部拆开,争取让你看完之后不光能复现,还能说出“为什么这样做”。
1. 毕设选题逻辑:为什么校园二手交易系统是“高性价比”项目
先聊点实际的。毕业设计选项目,本质上是在三个约束条件下找最优解:时间有限、代码能力参差、答辩要有东西可讲。很多人一上来就奔着“商城系统”去,但商城涉及商品 SKU、购物车、优惠券、支付回调、库存并发一堆问题,做完是能做,但代码量和调试成本都偏高。还有人选“博客系统”,简单是简单,可太常见了,答辩时没有任何可以展开讨论的点。
校园二手交易系统恰好卡在中间。它的核心业务是“用户发布闲置商品,其他用户浏览、联系、线下或平台内交易”,这个业务模型天然就包含了几块在答辩时非常能打的模块:用户注册登录(身份认证)、商品发布与上下架(资源管理)、商品分类浏览与搜索(数据检索)、收藏与留言(用户互动)、订单生成与状态流转(核心流程)。每一块拿出来都能讲出设计思路,组合在一起又不会让一个人在一个毕设周期内写完导致崩溃。
而且这个题目的“场景感”很强。二手交易跟买卖双方的真实行为相关,天然就能设计出“买家下单、卖家确认、交易完成”这样的流程,这比做一套纯粹的 CRUD 课程设计要高级得多。答辩时老师问“为什么订单要有状态”“为什么下架的商品不能被购买”,你都能用业务逻辑去回答,而不是只能说“我是照着接口文档写的”。
如果你基础一般,SpringBoot 的自动配置和 Vue 的组件化开发能把大量重复工作省掉;如果你基础不错,又可以在 JWT 鉴权、事务控制、分页性能、前端路由守卫这些点上往深了做,让项目在“能跑”的基础上多出很多可聊的技术细节。这就是这个题目的核心优势:下限低,上限足够高。
2. 技术栈选型背后的“不折腾原则”
选技术栈的唯一标准,不是“哪个最新”“哪个最火”,而是“哪个在毕设周期内最容易出成果,并且遇到问题时最容易找到参考”。所以后端选 SpringBoot、前端选 Vue、数据库选 MySQL,这不是跟风,而是这几个组件在社区里的资料密度太高了,随便碰到一个报错,搜索引擎都能给出答案。这一点在赶工的时候比什么都管用。
2.1 后端:SpringBoot 版本怎么定
SpringBoot 目前主流是 2.x 和 3.x 两个大版本。我的建议很直接:如果你 JDK 用的是 8 或 11,请用 SpringBoot 2.7.x;如果你 JDK 用的是 17 或更高,可以用 SpringBoot 3.x。不要一上来就追最新版本,因为 3.x 从javax包切到了jakarta包,很多老教程里的import javax.servlet在 3.x 里直接编译不过。你复现网上代码时,版本不匹配会浪费大量时间。
配套的持久层框架推荐 MyBatis-Plus 而不是原生 MyBatis。原因很实际:毕设里大量操作是单表 CRUD,MyBatis-Plus 提供的BaseMapper能让insert/selectById/updateById这类操作完全不用手写 XML,能省下几百行重复代码。你只需要在复杂多表查询的地方手写 SQL,比如“按分类查询商品并关联卖家昵称”,剩下的交给框架。
2.2 前端:Vue 2 还是 Vue 3
如果你之前只跟过 Vue 2 的教程,那就用 Vue 2 + Element UI,别纠结。如果你是从头学,直接 Vue 3 + Vite + Element Plus 也没问题。这个项目的前端页面不算复杂,两个版本都能撑起来。真正要关注的是配套关系:Element UI 不兼容 Vue 3,Element Plus 不兼容 Vue 2,装错包以后页面上什么都不显示,而且控制台报错信息还不太直观,这一点我在后面踩坑部分会单独说。
前端工程化的最小组合是:Vue 全家桶(Vue Router + Pinia 或 Vuex)+ Axios + Element UI/Plus。Vue Router 负责页面跳转和路由守卫,Axios 负责调用后端接口,Pinia 或 Vuex 负责存登录状态和用户信息。这一个组合就能覆盖整个系统的前端需求。
2.3 容易被追问的选型理由
答辩时老师很喜欢问“你为什么选这个技术”。回答的核心不是复述百度百科,而是讲清楚工具与业务的匹配关系。比如“SpringBoot 简化了项目搭建和依赖管理,内嵌 Tomcat,能直接打包运行,适合快速迭代”;“Vue 组件化开发适合把商品卡片、表单、导航栏这类复用 UI 抽成组件”;“MyBatis-Plus 减少单表 SQL 编写,让我集中精力处理订单和业务状态”。这些话听起来很简单,但比“SpringBoot 是主流框架”要扎实得多。
3. 数据库设计:先想清楚业务边界再写代码
很多人的项目做到一半发现要返工,根源都在数据库表设计出了问题。表结构一旦确定,后端的实体类、Mapper、Service、前端页面全都要跟着它走。改表的成本是链条式的,所以一开始就要把核心实体和它们之间的关系盘清楚。
校园二手交易系统最核心的表有这么几张:用户表、分类表、商品表、订单表、收藏表、留言表。接下来我逐张说一下设计的重点和理由。
3.1 用户表不是只有 username 和 password
用户表除了基本的账号密码,最好把nickname、avatar、phone、status都放进去。为什么?因为商品列表需要展示卖家昵称和头像,订单流程需要手机号联系,账号被封禁后需要status字段来控制登录状态。如果这些信息都去别的表关联,查询会变得非常啰嗦。
密码字段存储的是 BCrypt 加密后的密文,长度建议设 60 个字符以上。千万别用明文,也别用简单的 MD5。Spring Security 或者spring-security-crypto里的BCryptPasswordEncoder可以直接用,答辩时会是一个加分点。
3.2 商品表要冗余设计
商品表字段大致这样:id、user_id、category_id、title、description、price、original_price、images、status、view_count、create_time、update_time。
这里有两个容易忽略的设计点。第一,images字段我建议用字符串存多个图片地址,用逗号分隔;也可以单独建一张图片表,但对毕设来说,前者足够且查询简单。第二,status字段建议用0-在售、1-已售出、2-下架三态,而不是直接删记录。原因是:已售出的商品在订单完成后还要能展示交易记录,下架的商品还能重新上架,用状态字段能保留完整的数据轨迹。
索引方面,status和create_time要建立索引,因为首页和列表页最频繁的操作就是“按状态查商品并按时间排序”。
3.3 订单表的状态机是核心亮点
订单表字段至少要有:id、order_no、product_id、buyer_id、seller_id、price、status、create_time、pay_time、complete_time、cancel_time。
订单号建议用“时间戳 + 随机数”或者“日期 + 流水号”的方式生成,不要让数据库自增主键直接暴露给前端。status设计建议:0-待付款、1-待发货/待确认、2-已完成、3-已取消。这个状态机是答辩时很值得展开讲的部分,我后面专门用一节说代码实现。
3.4 分类表、收藏表、留言表
分类表非常简单:id、name、sort,预置几条数据即可,比如“数码产品”“生活用品”“教材书籍”“运动器材”“其他”。
收藏表是典型的中间表:id、user_id、product_id、create_time,保证user_id和product_id的组合唯一即可。留言表则包含id、product_id、user_id、content、create_time。这两个表的核心都是“谁对哪个商品做了什么”,索引字段集中在查询条件上。
4. 后端核心功能落地:从登录到订单状态流转
后端开发不要一上来就对着 Controller 敲代码。我的习惯是先把“核心流程”画出来,然后再按“用户 -> 必须登录才能操作 -> 查到对应数据 -> 更新数据 -> 返回结果”这个顺序去写。下面挑三个最核心的功能讲实现思路。
4.1 登录鉴权:为什么用 JWT 而不是 Session
毕设项目如果用 Session 做登录,也不是不行,但 JWT 的好处在于:后端不需要存会话状态,用户登录成功后拿到一个 token,后续每次请求在请求头里带上Authorization: Bearer <token>,后端校验通过就放行。这样拆开服务和压力分散都容易解释,也符合当前主流前后端分离项目的做法。
核心依赖是jjwt或java-jwt。流程分三块:
- 登录接口接收用户名密码,用
BCryptPasswordEncoder.matches校验密码。 - 校验通过后生成 JWT,里面放
userId和username,过期时间建议设 2 小时或 24 小时。 - 写一个拦截器或过滤器,校验请求头里的 token,把
userId解析出来放入请求上下文。
有一个细节要提前处理:拦截器要放行登录接口、注册接口、商品列表、商品详情这些“不需要登录就能访问”的接口,其余接口必须校验 token。前端拿到 token 后存在localStorage或 Pinia 里,路由守卫判断“有没有 token”决定能否进入个人中心。
4.2 图片上传:本地存储比对接 OSS 更适合毕设
图片上传模块,很多同学一上来就想接阿里云 OSS,结果配置了一堆 AccessKey、Bucket、域名,上传还是失败。说实话,毕设阶段用本地存储完全够用了。
在后端配置静态资源映射目录,比如上传到项目根目录下的uploads文件夹,启动类或配置类里写:
registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/uploads/");前端用 Element UI 的el-upload组件,action属性直接指向你后端的/api/upload,上传成功后会返回一个图片地址,你把它拼到商品表单的images字段里。这里需要注意:重启或重新部署后,上传的文件可能丢失,所以答辩演示时最好提前把商品图片传好,不要指望现场上传大文件。
4.3 订单状态流转:用状态机方式避免 if 满天飞
订单模块是整个系统里最值得展开讲的东西。状态流转只有四条合法路径:
- 待付款 -> 已支付(买家付款后触发)
- 已支付 -> 已完成(双方确认或系统自动确认后触发)
- 待付款 -> 已取消(买家主动取消或超时取消)
- 任意状态不能跳跃,比如待付款不能直接变成已完成。
代码实现时,最简单的方式是定义一个OrderStatusEnum,然后把“当前状态 + 目标状态”的合法性判断收敛到一个方法里:
public boolean canChange(Integer current, Integer target) { if (current == OrderStatusEnum.UNPAID.getCode()) { return target == OrderStatusEnum.PAID.getCode() || target == OrderStatusEnum.CANCELED.getCode(); } if (current == OrderStatusEnum.PAID.getCode()) { return target == OrderStatusEnum.COMPLETED.getCode(); } return false; }调用时先校验,再更新,并且把“更新订单状态”和“把商品状态改成已售出”放在同一个事务里。这里尤其要提醒:@Transactional只对同一个类内部通过this调用的方法不生效,必须把事务方法拆到另一个 Service 类里,或者自己注入自身代理,否则事务会静默失效。
另外一个容易被忽略的点是:买家发起购买请求时,要先查一次商品状态,确认是“在售”才允许创建订单。如果你不加这个判断,就会出现“同一件商品被两个人同时下单”的逻辑 bug。毕设现场演示时,如果老师问“两个人都点了购买怎么办”,你可以答“简单做法是创建订单前用UPDATE ... WHERE status = 0做乐观锁更新,影响行数为 1 才允许下单”,这就是一个很亮眼的回答。
5. 前端核心页面与交互:Vue 项目从 0 到 1 的关键节点
后端的本质是数据和接口,前端的本质是交互和展示。这套系统的前端页面主要有:登录注册页、首页(商品列表+分类筛选)、商品详情页、发布/编辑商品页、个人中心页(我的发布、我的订单、我的收藏)、留言区。
5.1 路由守卫和登录态统一处理
用Vue Router的beforeEach做全局前置守卫。判断逻辑很简单:访问需要登录的页面时,如果本地没有 token,就跳转到登录页,并带上redirect参数,登录成功后跳回目标页面。
每当前端需要调用后端接口,Axios 请求拦截器里统一加上Authorization头。响应拦截器里统一处理 401 状态码——token 过期或无效时,清理本地登录态并跳转登录页。这样能避免在每个页面手动判断登录失效,也能防止在答辩演示时突然弹出一堆红字错误。
5.2 首页商品列表:分页和筛选一起做
首页用el-card展示商品卡片,布局用栅格一行三列。请求参数带上pageNum、pageSize、categoryId和keyword,后端用 MyBatis-Plus 的Page对象接收,返回total和records列表。前端用el-pagination组件,页码改变时重新请求。
这里有一个很实务的细节:商品封面图可能有多张,列表页只需要取第一张。你在后端查询时可以直接用SUBSTRING_INDEX(images, ',', 1)取第一个图片地址,也可以在实体类里加一个@TableField(exist = false)的coverImage字段,业务层把第一张图塞进去。个人推荐后者,因为可读性好,而且能顺手处理“图片为空时给默认图”的逻辑。
搜索功能建议用keyword对title做LIKE查询,虽然性能不是最优,但完全符合毕设场景。如果老师追问“商品量大之后搜索性能怎么办”,你再接一句“后续可以改成 Elasticsearch 或 MySQL 全文索引”就够了。
5.3 发布与编辑商品:表单校验和图片上传联调
发布页用el-form的rules做校验:标题必填、价格必须大于 0、描述最少 10 个字、至少上传一张图片。图片上传用el-upload,上传成功拿到 URL 后塞进表单的images字段。提交时调POST /api/product,服务端拿到当前登录用户的userId作为sellerId。
编辑和发布的页面可以共用一个组件,用路由参数区分是新增还是编辑。编辑时回显数据注意日期格式——后端返回的createTime如果是LocalDateTime,JSON 序列化默认是一串数字或yyyy-MM-ddTHH:mm:ss,直接展示给用户很难看,需要配合@JsonFormat注解统一成yyyy-MM-dd HH:mm:ss。
6. 真实踩坑记录:这些错误不经历一遍很难发现
下面这些坑我几乎在每个相关的开发者身上都见过,而且它们的共同特点是:报错信息要么不明显,要么伪装成别的问题。如果你能提前避开,至少能省下 2 到 3 天的调试时间。
6.1 跨域:前端请求后端被浏览器拦截
前后端分离项目联调时,最容易遇到的就是跨域。现象是:在后端用 Postman 测接口一切正常,前端页面上请求却报错“CORS policy: No 'Access-Control-Allow-Origin' header”。原因是前端运行在http://localhost:5173(Vite 默认端口),后端运行在http://localhost:8080,端口不同就属于跨域。
解决方案很直接,后端写一个WebMvcConfigurer,放行指定路径并允许所有来源:
config.addCorsMappings(new CorsRegistry() { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); });需要注意的是,如果用了Spring Security,光有WebMvcConfigurer还不够,要在安全配置里也放行 OPTIONS 预检请求,否则跨域配置会被安全链拦截。
6.2 LocalDateTime 序列化后前端显示一串数字
这个坑非常阴间。后端实体类里用了LocalDateTime类型,默认情况下 Spring Boot 会把时间序列化成数组或时间戳,前端直接{{ item.createTime }}显示出来是一串数字。
解决办法是加依赖jackson-datatype-jsr310(SpringBoot 2.x 一般已经内置了),然后在application.yml里配置全局格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8如果局部字段特殊,再在实体类的字段上额外加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。这个配置要在开发早期就做好,不然后端接口改了格式,前端的展示逻辑也要跟着改,数据测试时很容易看错日期。
6.3 Vue 打包部署后刷新页面 404
如果你把前端项目npm run build之后扔到 Nginx 或后端静态目录里,点击页面内链接跳转是正常的,但一旦手动刷新某个子路由页面,就会报 404。原因是 Vue Router 用了history模式(路由地址是真实路径),刷新时服务器按照这个路径去找静态文件,找不到就 404。
解决办法有两种:一是把路由模式从createWebHistory()改成createWebHashHistory(),地址会多一个#,刷新不会 404;二是给 Nginx 配 try_files 重定向:
location / { try_files $uri $uri/ /index.html; }如果是直接放在后端 SpringBoot 里跑前端打包产物,也需要在资源处理上做转发。毕设阶段图省事的话,直接用 hash 模式最稳妥,演示时不会翻车。
6.4 上传的图片自己电脑能打开,别人访问不了
这个坑的典型现象是:开发机上浏览器访问http://localhost:8080/uploads/xxx.jpg正常,但是同一局域网里的其他设备访问同一个地址时报 403 或 404,或者用服务器部署后图片加载不出来。
原因基本都是静态资源映射的路径写的是绝对路径,比如file:D:/project/uploads/,或者没把uploads目录包含到打包后的运行路径中。更隐蔽的情况是,你启动项目时用的是java -jar,当前工作目录和 IDE 里运行时的目录不一样,导致System.getProperty("user.dir")指向错了地方。
解决思路是把上传根目录做成可配置项,在application.yml里配置一个自定义属性,比如upload.dir,然后在配置类里读取并映射到静态资源。这样换环境时只需要改配置文件,不用动代码。
6.5 事务不生效:同类的“陷阱”最容易踩
刚才在第 4.3 节提到过事务不生效的问题,这里再展开说。很多人第一次写订单创建逻辑时,会把createOrder方法放在OrderService里,内部调用同一个类的另一个方法updateProductAndCreateOrder,反手加上@Transactional,结果发现一个方法成功、另一个方法失败,数据不一致。
原因是 Spring 的事务默认基于 AOP 代理,同类内部的this调用不会经过代理对象,所以注解不生效。解决办法很朴素:把“创建订单 + 更新商品状态”的代码写在一个独立的 Service 方法里,事务加在公共 Service 方法上,或者注入ApplicationContext里拿代理对象再调用。
这个坑在答辩前一定要自查一遍,因为老师非常喜欢在演示时问“如果买家下单成功但商品状态没更新怎么办”。哪怕你代码里没有实际出现,能答出原理也会加分。
7. 从“能跑”到“能答辩”:文档整理和预期问题准备
最后这部分,探讨的是很多学生最不擅长的事:项目做完之后,怎么把它讲清楚。毕设评分里,系统的完成度只是一部分,论文和答辩表现同样占大头。“源码是复制来的”这种事在答辩现场其实很容易被看出来,但如果你能在看完本文之后,把整个系统的设计逻辑、核心流程、易错点都自己讲一遍,那就没人会质疑你。
7.1 论文和需求文档怎么写才不像“抄的”
不要按照网上那些模板从头抄到尾,而是按照你实际做的系统写。我建议按这个顺序整理:选题背景与意义 -> 需求分析(功能性需求、非功能性需求)-> 系统设计(总体架构、功能模块、数据库设计、接口设计)-> 核心功能实现 -> 系统测试。
需求分析不要只写“用户可以登录、发布商品”,要写成“用户输入用户名密码,系统校验成功后返回 Token,后续请求携带 Token 访问受保护资源”。这样既是需求描述,又是技术设计的铺垫,论文前后就能对得上。
数据库设计部分要附上完整的表结构说明和 E-R 图,字段要解释清楚“为什么存在”。比如商品表的status字段,说明它是为避免物理删除而设计的逻辑状态。这种表述在论文查重时也比较安全,因为它是你基于自己的系统写的,不是复制粘贴的通用描述。
7.2 高频答辩问题与应答思路
我收集了几个特别容易被问的问题,提前准备可以从容很多:
- “为什么不用 Session 而用 JWT?”回答要点:前后端分离场景下 Session 需要存储会话状态、存在跨域携带 Cookie 等问题;JWT 自包含、无状态、适合分布式部署。同时要承认 JWT 有“无法主动失效”的缺点,但毕设场景下通过设置过期时间可以接受。
- “商品列表分页是怎么实现的?”回答要点:MyBatis-Plus 分页插件,传入
pageNum和pageSize,返回total和records。如果有余力,可以补充一句“当数据量大时会考虑在status和create_time上建联合索引”。 - “两个买家同时买同一个商品怎么处理?”回答要点:创建订单前加状态校验,并通过条件更新
UPDATE product SET status = 1 WHERE id = ? AND status = 0,影响行数为 0 则说明商品已被买走。这就是乐观锁的思想,老师听到这种回答通常不会再深挖。 - “密码明文存储了吗,安全方面做了什么?”回答要点:后端用 BCrypt 加盐哈希存储密文,前端登录时通过 HTTPS 传输,拦截器统一校验登录态。这是很加分的点,因为很多毕设项目确实什么都不做。
7.3 演示顺序与演示数据的准备
答辩演示不要直接从登录页开始慢慢点。那样时间不够,而且容易在某个意外环节卡住。我建议的演示顺序是:
- 先展示首页,体现分类筛选和搜索功能,播放一段几秒的“正常浏览”过程;
- 展示商品详情和留言功能;
- 登录账号,发布一件新商品,上传一张图片,提交后回到首页能看到新商品;
- 切换另一个账号,浏览并下单,展示订单状态从“待付款”变成“待确认”,再触发完成操作;
- 演示个人中心里的“我发布的商品”“我的订单”“我的收藏”。
演示数据要提前造好:至少10件商品、5个分类、3个用户、若干收藏和留言。商品图片用固定的图或者网络公开图,不要用现场临时上传的大图,避免网络波动或文件存储路径出错。
我个人的建议是,在正式答辩前找一个不吃技术的朋友,让他按照这个顺序操作一遍,你站在旁边只看不提示。他要是能顺利走完,说明系统的容错性和交互逻辑没问题。他要是卡住了,那卡住的地方就是你答辩时的风险点,提前处理掉。
这个项目做完之后,后面往简历上写也很有空间:你可以说是“基于 SpringBoot + Vue 的前后端分离系统”,强调 JWT 身份认证、订单状态机、附件上传、分页查询这几个点。这些不是说大话,只要你真的按文章思路把代码写了一遍、把坑踩了一遍,每一个环节的细节你都能讲出真实依据。如果你在复现过程中遇到新的问题,欢迎带着具体报错来聊,我尽量帮你把问题定位到具体的代码或配置上。