毕业设计季又到了,每年这个时候都有大量同学被同一个问题卡住:代码拿到了、文档也翻了一遍,但项目就是跑不起来;或者好不容易跑通了,老师一问细节就答不上来。这套基于Spring Boot + Vue的校园二手物品置换系统是我断断续续整理了一个多月才完全跑通的项目,前后端联调、数据库脚本、部署上线都踩过不少坑。这篇文章我把整个项目的拆解思路、核心模块设计、前后端对接细节、部署文档整理经验全部写出来,顺便把代码讲解时最值得展开的几个点也列明白,希望能帮你少走弯路,不管是课程设计、毕业设计还是想自己做一个完整的全栈项目,都有参考价值。
1. 功能模块设计:把需求边界划清楚,后面代码才好写
很多同学拿到项目第一步就急着开IDE看代码,这是最容易被绕进去的做法。二手物品置换系统这类项目,业务逻辑不复杂但涉及的实体关系比较多,动手之前先花一个小时把功能边界理清楚,后面写代码、写文档、答辩都会顺很多。
1.1 用户端和管理端到底各管哪些事
从实际使用场景出发,这个系统面向两类人:普通在校学生和管理员。普通学生需要账号注册登录、浏览商品、发布闲置、发起置换申请、管理自己的发布记录;管理员负责用户管理、商品审核、分类管理、数据统计这些后台操作。这看起来没什么好说的,但真正影响代码结构的是权限控制——普通用户只能操作自己创建的数据,管理员有超管权限,这两个角色的区分必须从一开始就贯穿到所有接口设计里。
置换流程这块很多人容易想复杂。其实核心就是一个状态机:用户A看中用户B的商品,发起置换申请,同时可以提交自己的一件闲置物品作为交换条件;B收到申请后可以选择同意或拒绝;同意后订单状态流转到待确认,双方线下见面完成交换后再确认收货。整个流程里涉及商品状态(在售中、已被申请、已置换成功)、申请记录状态(待处理、已同意、已拒绝、已完成),这两套状态要分开管理,不能混在一张表里。
1.2 商品信息的结构化拆分
商品模块看起来只是增删改查,但设计表结构时最常踩的坑是把所有字段塞进一张表。二手物品置换场景下,商品要有标题、描述、图片、原价、期望交换品类、几成新、交易地点、发布时间、发布人、浏览次数、状态这些信息。其中图片是多张,不是一张,这就牵扯出主表和子表的关系。分类信息也值得单独建表,因为首页和搜索页都需要按分类筛选,而且后续如果要做“猜你喜欢”这类推荐功能,分类表是基础。
我实际整理时把商品表字段定成了这样一份核心清单,供参考:
goods_id主键user_id发布者ID,关联用户表title标题description详细描述category_id分类IDprice参考价格(原价或心理价位)degree成色,用枚举值(1-全新、2-几乎全新、3-轻微使用痕迹、4-明显使用痕迹)expect_exchange期望换什么类型的物品status商品状态(0-在售、1-被申请锁定、2-置换成功、3-已下架)view_count浏览量create_time发布时间
图片单独一张表,按商品ID关联,存图片路径和排序值。这样做的好处是发布商品的表单可以灵活支持多图上传,不需要在商品表里预留一堆无意义的图片字段。
1.3 从用户故事推导核心用例
设计阶段有个特别好用的方法,就是把每个角色要做的事写成用户故事,然后看会触发哪些接口。比如“小王想用自己的旧耳机换一本考研数学的辅导书”,这个故事向前推导就是:登录系统、搜索或浏览商品列表、进入辅导书详情页、点击“申请置换”、填写说明并选择自己发布的旧耳机作为置换物、提交申请。向后推导是:辅导书发布者收到申请通知、查看申请详情和对方的商品信息、同意或拒绝、同意后双方看到联系方式、线下交换、双方各自确认完成。把这类用户故事列上四五个,系统的用例图、接口清单、测试用例就全有了,这也是一份很好的需求文档素材。
2. 数据库设计要点:表之间的关联关系决定业务逻辑怎么走
二手置换系统比普通的单边交易系统多一层复杂性,因为一笔置换涉及两件商品、两个用户,还有一条申请记录。数据库如果设计得不好,实现业务逻辑的时候就会到处拼接、频繁改表。
2.1 核心五张表的字段规划
我在整理这套项目时发现,除去管理员表,真正核心的业务表就是五张:用户表、商品表、商品图片表、置换申请表(同时也承担订单功能)、分类表。评论或留言可以先不做进第一版,因为对核心业务流程影响不大,但是答辩时容易被问到,所以可以在文档里说明这是一个可扩展模块。
置换申请表是业务核心,字段设计上要特别注意:
exchange_id主键goods_a_id发起方提供的商品IDgoods_b_id被申请的商品ID(即对方发布的商品)from_user_id发起方用户IDto_user_id接收方用户IDmessage申请留言status状态(0-待处理、1-已同意、2-已拒绝、3-已完成、4-已取消)create_time申请时间handle_time处理时间
为什么goods_a_id和goods_b_id要分开而不是设计一个“目标商品”字段?因为置换是双向的,发起方提交的旧物和接收方的商品在业务上是对等的。这样设计以后,展示“我发起的置换”和“我收到的置换申请”时就非常直观,一条SQL按从用户或目标用户筛选即可,不用做复杂的关联嵌套。
2.2 外键到底建不建?我的实践方案
很多教材都会讲必须建外键,但实际企业级项目里外键的使用是有争议的。这套项目规模不大,我的做法是:逻辑外键,不建物理外键。原因很简单,物理外键会带来几个实际问题——删除数据时要额外处理关联约束,批量导入测试数据时容易报错,而且项目后期如果做分库分表,物理外键就是个巨大的负担。在代码里通过MyBatis-Plus的关联查询或者业务层校验来保证数据一致性,对毕设项目完全够用。
需要注意的是,不建物理外键不代表不建索引。goods_a_id、goods_b_id、from_user_id、to_user_id这些高频查询字段全部要加普通索引,否则数据量一上来,连表查询会明显变慢。这个问题我在压测时真实遇到过,加了索引以后置换申请列表的查询时间从1.8秒降到了几十毫秒。
2.3 会话保持与数据权限方案
数据库层面还有一个关键点:用户身份怎么贯穿整个业务链路。这个系统用的是JWT方案,登录成功后后端签发一个Token,前端存储到浏览器本地,后续每次请求在请求头带上Authorization: Bearer xxx。后端通过拦截器解析Token,取出用户ID,放入ThreadLocal,业务层随时可以拿到当前操作人。所有涉及用户数据的操作(修改商品、处理置换申请),都要拿当前用户ID和数据的归属用户ID对比,不一致直接拒绝。
这里有个容易忽略的小细节:很多同学会把用户ID直接放在JWT的payload里,这个没问题,但要注意JWT一旦签发,在有效期内用户改了个头像或者昵称,单靠Token里存的ID是拿不到最新信息的。所以正确的做法是Token里只存user_id和过期时间,每次需要用户信息时再从数据库或者Redis里查。哪怕多一次查询,也比用户改了资料以后前端还显示旧信息要好得多。
3. 后端实现细节:Spring Boot怎么组织代码才不乱
后端代码的组织方式直接决定了你看代码、改代码、讲代码的效率。这套系统我按经典的分层架构来组织,即Controller层、Service层、Mapper层,严控层次间依赖方向,同时补充了统一响应体、统一异常处理、JWT拦截器这些横切能力。
3.1 统一的返回格式和异常处理为什么重要
前后端分离的项目,如果每个接口返回的数据格式都不一样,前端联调时会非常痛苦。我在项目里定义了一个Result类,包含code、message、data三个字段。成功的请求返回code: 200,业务失败返回对应的错误码,未登录返回401,无权限返回403。前端axios封装里统一判断code,不是200就弹错误提示,省掉了每个接口里重复写错误处理的代码。
统一异常处理这块千万不要省。实际开发中,参数校验、数据库唯一键冲突、空指针这类异常防不胜防。我在项目里写了@RestControllerAdvice全局异常处理器,捕获了MethodArgumentNotValidException、BusinessException、Exception三个层级。这意味着Service层抛出带业务错误码的异常时,前端能拿到友好的中文提示而不是一个满屏堆栈的500页面。
3.2 商品发布与图片上传的实现思路
文件上传是很多同学的盲区。这套项目用到的图片上传方案是本地磁盘存储,没有去接阿里云OSS或者七牛云。选择本地存储的理由有三点:项目规模不大、没有复杂的CDN需求、部署和讲解起来更直观。
具体实现是在配置里定义一个upload.path目录,前端上传时用MultipartFile接收文件,按时间戳重命名防止文件名重复,然后保存到本地磁盘。保存成功后,将存储的相对路径存数据库,接口返回给前端一个拼接了访问前缀的完整URL。这里最容易出的问题就是图片回显——保存了图片但浏览器访问不到。原因一般是Spring Boot的静态资源映射没有配置,需要在配置里把上传目录映射为/images/**的访问路径。另外还要注意服务器部署时上传目录的权限问题,我遇到过Linux下目录不可写导致上传失败的坑,原因是把上传目录放在了一个没有写权限的位置。
3.3 MyBatis-Plus的巧妙用法和坑点
持久层用的MyBatis-Plus,选它核心就两个原因:单表增删改查不需要手写SQL,分页插件成熟稳定。这套项目90%的数据库操作都是单表CRUD,MyBatis-Plus的BaseMapper直接覆盖,效率非常高。多表关联的查询(比如商品列表要关联出发布者的用户名和学校信息)就手写XML里的SQL,不强行用框架的能力去凑。
说几个实际使用中容易踩的坑。第一个是逻辑删除:如果给商品表设置了is_deleted逻辑删除字段,记得所有查询条件里自动带上deleted=0,MyBatis-Plus默认支持这个特性,但是如果用了自定义SQL,就必须自己在SQL里加条件。第二个是字段自动填充:创建时间、更新时间这类字段配置了自动填充之后,如果某次操作绕过了实体类的insert()方法直接执行自定义SQL,这些字段就不会自动填值,容易导致数据库里出现空值。第三个是分页插件的配置,新版MyBatis-Plus的分页插件需要显式创建一个MybatisPlusInterceptor的Bean,不配的话你会发现分页始终不起作用。
4. 前端Vue的实现逻辑:组件怎么划分,接口怎么对接
前端这部分用的Vue 2 + Element UI的组合。虽然Vue 3已经出来很久了,但毕设项目选Vue 2有它的现实考量:网上资料最多、Element UI组件库成熟、遇到问题随便一搜就有解决方案。如果你不是特别熟悉Vue 3的组合式API,选Vue 2踩坑成本确实低很多。
4.1 路由和权限拦截的组织方式
前端路由主要分成两块:游客能访问的(首页、商品列表、商品详情、登录注册)和需要登录才能访问的(个人中心、发布商品、我的置换)。Vue Router配置里通过meta字段标记哪些页面需要登录,然后在全局前置守卫里判断:如果目标路由需要登录且当前没有Token,就跳转到登录页。这个实现方式代码量很小,但是作用关键——用户刷新页面时后端会返回401,axios统一拦截后也跳转登录页,两套机制配合。
个人中心里我使用了嵌套路由,侧边栏固定,内容区根据子路由变化,分别对应“我发布的商品”“我收到的置换申请”“我发出的置换申请”“个人资料”这几个页面。嵌套路由的好处是数据加载逻辑更清晰,每个子页面各自拉数据,不需要互相干扰。
4.2 跨域问题和axios的二次封装
前后端分离开发时第一个响应的就是跨域错误。本地开发时前端跑在8080端口,后端跑在8081或者9090,浏览器直接拦截跨域请求。我的做法是后端写一个CorsConfig配置类,用addCorsMappings放开跨域限制。但要注意,上线部署时这个配置其实用不到了,因为你可能会把前端打包的dist目录直接放到Spring Boot的static目录下用同一个端口访问,或者使用Nginx反向代理,这两种方案都不存在跨域问题。所以跨域配置要区分环境,别开发完了忘了上线时关闭。
axios封装这块,我把baseURL、超时时间、请求头Token注入、响应拦截器集中在一起。响应拦截器里统一处理三种情况:code === 401跳转登录并清除本地Token、code !== 200直接Message.error弹出后端返回的错误提示、正常数据直接return res.data.data解包。这样做的好处是每个页面调用接口时只需要关心业务数据,不需要重复写错误处理。
4.3 商品发布表单的多图上传和联调细节
商品发布页是这个系统里交互最复杂的组件。多图上传我用的Element UI的el-upload组件,list-type设置为picture-card,上传成功后把返回的图片路径存进表单;预览和删除用组件自带的on-preview和on-remove钩子处理。这里有个细节,上传接口和表单提交接口是两个独立接口,上传接口在点击选择文件时就立即触发了,表单提交时只是把图片路径列表一起提交。这样做的好处是用户体验流畅,坏处是如果用户上传了图片但最终没提交表单,就会留下一堆垃圾图片。我的处理是在后端加了一个定时清理任务,每天凌晨删除超过24小时未被引用的图片,虽然毕设阶段一般用不上,但这个设计在答辩时是一个很好的亮点。
前端还有一个小坑:图片路径的拼接问题。后端返回的图片URL如果是相对路径(比如/images/xxx.jpg),前端渲染img标签时要在前面拼接上后端的访问前缀。最稳妥的做法是后端直接返回绝对URL,即http://ip:port加上路径,前端只管用。但如果前后端域名不一致或者端口变化,绝对URL又有问题。这个可以写一个环境变量来控制,打包时按部署环境切换。
5. 部署文档的整理思路:本地跑通和服务器发布是两套逻辑
部署文档是整个项目交付里被严重低估的一个部分。很多同学代码写得挺好,但部署文档只有一句“参考项目README”,结果老师或者同学一操作就报错。我整理这份部署文档时把环境分成两个场景分别写:本地开发环境启动和云服务器生产环境发布,两个场景常见的问题和操作方式差别很大。
5.1 三个最容易出问题的环境配置项
环境配置是部署环节里最大的坑源,集中在三个东西上。第一个是JDK版本,Spring Boot 2.x要求JDK 8以上,但如果你用的是Spring Boot 2.7加JDK 17,某些反射相关的功能可能会出问题。稳妥方案是JDK 1.8或JDK 11。第二个是Maven配置,国内网络环境下载依赖很慢,必须在settings.xml里配阿里云镜像,否则一次mvn clean package可能要等半小时。第三个是MySQL版本和编码,项目里数据库连接串要显式加上useSSL=false和characterEncoding=utf8,不然会有奇怪的报错。Node这边的版本也需要注意,Vue 2项目配合Node 16是稳定组合,Node 18以上有些老依赖会报OpenSSL错误,这是前端环境里最常遇到的问题。
还有一个经常被忽略的:后端配置里的数据库密码不要写死真实密码在配置文件里,至少分成application-dev.yml和application-prod.yml两套配置,用启动参数--spring.profiles.active=dev来切换。云服务器上部署时用prod配置,数据库密码从环境变量读取。这个做法不但安全,而且能避免本地连接测试库和线上库混淆的乌龙。
5.2 完整启动步骤清单
我整理项目文档时,把所有启动步骤分成了5个大阶段,每个阶段都有验证方式,方便快速定位问题出在哪一环:
- 初始化数据库:执行
init.sql脚本,创建数据库和各表。验证方式:show tables;能看到核心表。 - 启动后端:修改
application-dev.yml里的数据库账号密码,配置上传目录,然后执行mvn spring-boot:run或者用IDE启动。验证方式:控制台出现Started Application in xx seconds,浏览器访问http://localhost:8081/api/test返回JSON。 - 启动前端:在
vue-ui目录下先npm install,再npm run serve。验证方式:浏览器打开http://localhost:8080看到首页。 - 联调验证:注册一个新账号,发布一件商品,再切换到另一个账号发起置换申请,完整走一遍主流程。验证方式:数据库中
goods表和exchange表有对应记录。 - 生产环境构建:前端
npm run build产生dist目录,后端mvn clean package产生jar包,然后把dist目录复制到后端resources的static目录下,一起打包;或者用Nginx托管前端、Java进程跑后端。
5.3 生产部署的两种方案对比
生产部署真的到了云服务器上,有两种方案可以选择。方案一最简单:直接把前端构建出的dist目录放到Spring Boot项目的src/main/resources/static下重新打包,Java进程同时提供前端页面和后端API,只需开放一个8080端口。这种方式的优点是部署极其简单,不用额外装Nginx,两个环节变成了一个进程,管理成本低。缺点也有:前端和后端耦合在一起,更新前端页面也必须重新打后端jar包,而且Java进程对静态文件的并发处理能力一般,但用户量不大时完全没有问题。
方案二更规范:Nginx托管前端静态文件,后端Java进程单独跑在某个端口,Nginx配置反向代理把/api前缀的请求转发到后端端口。这种方式的优点是前后端解耦、静态文件性能好、扩展方便,但部署步骤多了一个Nginx配置环节。对于校园项目来说,我认为方案二可以作为加分项展示,实际部署用方案一就够稳定了。两种方案的Nginx配置和打包命令我都在部署文档里写清楚了,实际操作时可以按自己的服务器环境选择。
6. 代码讲解和答辩环节:哪些知识点最值得提前准备好
代码讲解是很多人的痛点,明明是自己做的项目,被老师一问就思路断裂。其实代码讲解是有迹可循的,抓住项目的几个核心设计点,把它的设计动机和实现原理讲透,整个讲解流程就会很顺畅。
6.1 置换流程的并发控制:数据库层面的行锁设计
这是一个非常容易被问到也很有价值的问题:如果两个人同时在发起置换申请,同一件商品会不会被重复锁定?这个系统的解决方案是商品表里加一个status字段,状态从在售变为被申请中。关键的实现细节是,状态更新不能先查再改(乐观锁思路),而是直接用一条带条件的UPDATE语句:UPDATE goods SET status = 1 WHERE goods_id = ? AND status = 0,根据影响行数判断是否更新成功。如果返回0说明商品已经被别人申请了,拒绝当前申请。这个方案本质上就是利用数据库的行锁机制,是乐观锁在业务中的典型应用。
讲这个设计的时候可以顺带提一下悲观锁方案的区别:用SELECT ... FOR UPDATE把商品行锁住再操作。悲观锁实现简单但并发性能差,乐观锁方案在大多数场景下更好。这个点如果你能主动讲出来,答辩老师会觉得你是真的理解业务背后的问题,而不是背代码。
6.2 JWT认证:怎么回答“Token过期了怎么办”
JWT认证几乎是必问的。你需要掌握几个基础知识点:JWT由Header、Payload、Signature三部分组成;服务端用密钥对签名做校验;JWT是无状态认证,服务端不存储会话信息。进阶问题常集中在Token过期处理上。这套项目的实现是:用户登录后签发Token,设置7天有效期(考虑校园项目使用频率,设置长一点减少重复登录的打扰)。用户操作时拦截器校验Token,如果过期就返回401,前端收到后清掉本地Token跳转登录页。
如果你想让项目更完整,可以提前设计一个双Token方案在文档里备注:一个短期Access Token和一个长期Refresh Token,Access Token过期后用Refresh Token换新的。这个方案Spring Boot配合Redis实现也不难,但作为扩展点写在文档里就足够展示了。注意,真正实现双Token会引入新的复杂度,而且可能引入Token刷新竞态问题,如果把握不好,计划里的设计尽量简单清晰,比信息过载要好。
6.3 数据库索引和SQL优化怎么讲出深度
商品列表页如果商品数量多了之后变慢,可以用这个点来展示优化思路。我在项目里给goods表的category_id、status、create_time建了联合索引,这样分类筛选、上下架状态过滤、按时间排序这三个高频操作都能走索引。还有一个典型的优化点是商品详情页的浏览量自增,用了UPDATE goods SET view_count = view_count + 1 WHERE goods_id = ?这条原子SQL,先查出来再更新会存在数据丢失问题。
讲解时注意把握“为什么”比“做了什么”更重要。直接说建了哪些索引不如说清楚通过EXPLAIN观察到没有走索引、才决定设计联合索引,这个发现问题—分析原因—解决问题的闭环过程是评分关键。
6.4 代码讲解的时间分配建议
我见过太多人讲代码时平均用力,每个文件都讲五分钟,结果讲到核心模块时时间不够了。建议这样分配时间:项目整体架构介绍10%,数据库设计15%,用户认证与权限控制15%,商品和置换核心业务流程30%,文件上传15%,部署方案10%,总结和展望5%。核心业务必须花最多时间讲,那是项目里代码量最大也最能体现业务理解的部分。
7. 整理这套项目的实操心得:文档工程化的一些经验总结
最后聊点实际的收获。整理这样一套包含源码、部署文档、讲解思路的项目,给我最大的体会就是一个词:闭环。代码能不能跑只是第一环,跑通了能不能说清设计思路是第二环,说清了能不能让别人按照文档复现是第三环。这三环每一环要单独验证,不能想当然。
几个具体建议:数据库脚本一定要在干净的MySQL环境里完整执行过,不要只在开发库上验证;前端打包前检查是否有未定义的全局变量或其他编译警告,npm run build报错时优先看是不是ES6语法或版本兼容问题;部署文档里的每个命令、每个路径都要原样实际执行一遍,不要凭经验写,因为你“感觉没问题”的地方可能别人操作时就是过不去。文档里最好有一个“常见问题排查”章节,把端口占用、数据库连接失败、跨域报错、静态资源404、依赖安装失败这五个高频问题的排查步骤和解决命令写清楚,这部分内容在实际使用中价值比正文还高。
额外提一个细节:项目文档和代码里的命名要统一,数据库表名用复数还是单数,前后端字段名用驼峰还是下划线,这些规范要在文档开头定义好。我在整理过程中就踩过字段命名前后不一致导致联调查了半天的问题,前端传userName,后端接收username,两边都没错但数据就是对不上,最后发现是命名规范不统一。这类问题提前约定好可以避免大量无效排查时间。
如果你准备拿这套项目去做课程设计或者毕业设计,记住一件事:完整跑通整个流程的价值远大于代码量本身。一个能稳定运行、逻辑清晰、可以说清设计决策的项目,哪怕功能不算多,也绝对能拿到不错的评价。