1. 项目定位与整体设计思路
1.1 这个系统到底要解决什么问题
先说说我为什么会对这个项目感兴趣。每年毕业季,大量还能用的教材、台灯、小风扇、自行车、甚至九成新的专业课笔记都被直接扔进垃圾桶。而低年级学生正愁买新教材贵、买二手找不到靠谱渠道。这种供需错配,正是"校园二手物品置换系统"存在的根本逻辑。
说白了,这个系统不是做一个"简单的商品列表",它要解决几个真正麻烦的问题:第一,学生之间的信任问题,买卖双方都是校内用户,需要有校园身份认证的机制;第二,交易撮合问题,用户得能快速找到自己需要的东西,所以搜索、分类、筛选这些基础能力一个不能少;第三,置换而非纯买卖,很多场景下学生想要的不是钱,而是"用我的《高等数学》换你的《线性代数》"这种物品互换,这就比普通电商系统多了一层业务逻辑。
从实际落地的角度说,这类项目也特别适合拿来练手。原因很简单:业务复杂度适中,既有CRUD,又有文件上传、权限控制、状态流转这类稍微需动点脑筋的东西,还有前端联调和部署,覆盖了一个完整项目的全部环节。而SpringBoot作为当前Java后端开发的事实标准,正好能把从空项目到可用接口的全过程压缩到极短的时间。
1.2 技术选型背后的考量
很多人一上来就纠结:要不要上微服务?要不要用Redis?要不要搞个消息队列?我的建议是——不要。这不是生产环境的秒杀系统,而是一个校园场景的信息撮合平台。技术选型最忌讳的就是为了用技术而用技术。
我当时选型是这套组合:SpringBoot做后端核心框架,MyBatis做持久层,MySQL存业务数据,Vue 2 + Element UI做管理后台和用户端页面,文件上传走本地存储(如果想要更规范一点,也可以上MinIO,后面我会细说)。整个架构是前后端分离,但最终打成传统WAR/JAR包部署,而不是拆成独立的Nginx静态服务加后端服务。
这么选有几个很实际的考量:
一是SpringBoot的自动装配极大减少了配置量。我不用再像以前SSM时代那样写一堆XML配置文件,只需要在application.yml里声明数据源、端口、上传大小限制即可。对毕设团队或者个人开发者来说,省下来的时间足够把一个功能模块打磨得更完善。
二是MyBatis够灵活。虽然JPA上手更快,但在处理多表联查、动态SQL这种场景时,MyBatis的SQL可控性优势非常明显。特别是二手物品这种带多条件筛选(分类、价格区间、成色、校区、是否可置换)的场景,动态SQL写起来几乎零成本。
三是前后端分离但合并部署。开发阶段Vue跑在8080端口代理请求到后端8081,上线时直接把dist目录拷到SpringBoot的static目录下,一个JAR包搞定。对校园服务器资源紧张的情况来说,这种部署方式最省事。
提示:如果后续想升级,把存储从本地磁盘迁移到MinIO的改动很小,只需要封装一层OSS接口,业务代码不用动。这也是封装"上传服务"这个模块的意义所在。
2. 环境准备与数据库设计
2.1 开发环境与工具链
这个项目对环境的要求其实不高,我建议的版本组合如下:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | JDK 8最稳,JDK 11也行,别一上来就上17/21 |
| Maven | 3.6及以上 | 依赖管理和打包 |
| MySQL | 5.7或8.0 | 8.0需要注意驱动和时区配置 |
| SpringBoot | 2.7.x | 别选3.x,原因后面讲 |
| MyBatis | mybatis-spring-boot-starter 2.x | 配合SpringBoot 2.x |
| Vue | 2.x | 生态成熟,坑少 |
| Node | 14/16 | 打包Vue项目用 |
我踩过最大的坑就是SpringBoot版本。标题里很多人写的是"springboot版本太高"这类热搜词,确实是个高频问题。SpringBoot 3.x基于Jakarta EE,把javax.*全部换成了jakarta.*,很多老教程直接失效,一些中间件驱动还没跟上。如果你照着网上教程写import javax.servlet.http.HttpServletRequest,在3.x里直接编译不过。所以做这类毕设项目,老老实实用2.7.x。
2.2 数据库表结构设计思路
数据库设计是这个项目真正的核心,建议动手写代码前先花一晚上把表设计理清。我最终设计出的核心表包括这些:
user表:用户信息。字段包括
id、student_no(学号)、password、nickname、phone、avatar、role(用户/管理员)、status、create_time。学号要做唯一索引,因为这是校园场景下身份识别的关键。category表:物品分类。两级结构,
parent_id指向父分类,比如"学习用品"下挂"教材""文具","电子产品"下挂"手机""电脑"。item表:物品信息。这是最核心的表,字段包括
id、user_id(发布者)、category_id、title、description、price、original_price、condition_level(成色,用1-5表示)、trade_type(1表示出售,2表示置换,3表示置换+补差价)、wanted_item(想置换什么,文本描述)、campus(校区)、status(0在售、1已下单、2已完成、3下架)、view_count、create_time。item_image表:物品图片。为什么单独建表而不是直接放个
images字段用逗号拼接?因为后续要做主图覆盖、多图标展示,单独建表在SQL查询时更清晰,也方便后续做图片懒加载。trade_order表:订单表。字段包括
id、item_id、buyer_id、seller_id、order_type(购买/置换)、amount、status(0待确认、1已完成、2已取消)、create_time。favorite表:收藏表。
user_id+item_id唯一索引,避免重复收藏。message表:站内私信。这个功能很多人会忽略,但实际上是二手交易里用户沟通频率最高的场景。
设计时特别注意一点:不要把业务状态和流程状态混在一个字段里。比如物品状态status已经包含0-3四种状态了,有些同学还会加一个is_deleted来标识删除,这两个字段很容易互相干扰。我的做法是:下架和删除都通过status表达(下架即从列表消失),物理删除只用于管理员处理违规物品。
3. 核心功能实现拆解
3.1 用户注册登录与JWT鉴权
用户模块是第一个要实现的,它撑起了整个系统的访问控制。我用的是JWT方案,流程是:登录成功后后端生成一个token返回给前端,前端存在localStorage里,每次请求在Header里带上Authorization: Bearer token,后端通过拦截器校验。
核心实现思路是这样的:
- 密码不能明文存,用BCrypt加密。SpringSecurity自带的
BCryptPasswordEncoder可以直接用,不要自己写MD5加盐,安全性差。 - 注册校验:学号唯一、密码强度、手机号格式。这里有个业务细节,校园场景建议增加"学号+姓名"匹配校验,但考虑到毕设的演示属性,可以通过一个管理端的审核机制来代替。
- JWT的过期时间设计:我设了24小时,移动端请求频繁,过期太短会导致体验差。配合拦截器在token过期时返回401,前端再引导用户重新登录。
JWT那块最简单的方式是用jjwt库。网上很多教程让你写一大串工具类,其实核心逻辑就三点:生成token、解析token、判断过期。需要注意的是签名密钥不要太简单,我习惯放在配置文件中,用@Value注入。
3.2 商品发布与图片上传
商品发布是用户最常用的功能,也是后端代码最容易出问题的地方。
前端表单字段很多:标题、描述、分类、价格、成色、交易方式、期望置换物品、校区、图片九宫格。提交时用FormData提交,兼容性最好。后端接收时注意,字段用@RequestParam逐个接收,或者定义一个DTO对象用@ModelAttribute接收,不要用@RequestBody— 因为@RequestBody只能接收JSON,收不了文件。
图片上传这块有个经典问题:本地磁盘存储的路径和访问映射。我的做法是:
upload: dir: /data/upload url: /api/file/**代码里实现一个WebMvcConfigurer,把本地目录映射成URL访问路径:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String dir = uploadDir.endsWith("/") ? uploadDir : uploadDir + "/"; registry.addResourceHandler("/api/file/**") .addResourceResolver(new FileSystemResourceResolver(dir)); }这样用户上传的图片访问地址就是/api/file/20240101/uuid.jpg,前端直接拼这个URL就能展示。文件命名一定要用UUID重命名,避免中文名和重名问题。
注意:如果上传的图片无法预览,99%是资源映射路径写错了,排查时先看浏览器Network里图片请求返回的是200还是404。
3.3 商品搜索与分类筛选
商品列表页是整个系统的门面。你写的是"校园二手物品置换系统",用户进来第一眼看到的就是商品流,所以列表接口必须好使。
我的设计是统一走一个/api/item/list接口,接受这些参数:keyword、categoryId、campus、tradeType、minPrice、maxPrice、sort、page、pageSize。
放在MyBatis里用动态SQL拼条件:
<select id="selectItemList" resultType="com.example.vo.ItemVO"> SELECT i.*, c.category_name, u.nickname, u.avatar FROM item i LEFT JOIN category c ON i.category_id = c.id LEFT JOIN user u ON i.user_id = u.id <where> i.status != 3 <if test="keyword != null and keyword != ''"> AND (i.title LIKE CONCAT('%', #{keyword}, '%') OR i.description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND (i.category_id = #{categoryId} OR i.category_id IN (SELECT id FROM category WHERE parent_id = #{categoryId})) </if> <if test="campus != null and campus != ''"> AND i.campus = #{campus} </if> <if test="tradeType != null"> AND i.trade_type = #{tradeType} </if> <if test="minPrice != null"> AND i.price >= #{minPrice} </if> <if test="maxPrice != null"> AND i.price <= #{maxPrice} </if> </where> ORDER BY <choose> <when test="sort == 'price_asc'">i.price ASC</when> <when test="sort == 'price_desc'">i.price DESC</when> <when test="sort == 'newest'">i.create_time DESC</when> <otherwise>i.create_time DESC</otherwise> </choose> </select>关键词搜索用LIKE '%关键词%'对毕设来说完全够用。如果你图省事想上Elasticsearch或者HanLP分词,纯属过度设计。数据量就几万条,MySQL全表扫描带索引也毫秒级返回。
3.4 置换/交易流程设计
二手置换和普通电商最大的区别,在于它的订单状态流转不是"付款-发货-收货"这么简单,而是"两者互换"。
我的做法是在trade_order表里加一个related_order_id字段,用于关联"换入物"和"换出物"两个订单。流程设计如下:
- A用户对B用户的物品发起置换申请,B用户收到一条站内消息。
- B用户同意后,系统自动生成两条订单:一条是B物品给A(记录卖/换出方向),一条是A的置换物品给B(记录换入方向)。
- 双方在线下约好见面交易后,在线点击"确认完成",两条订单同时状态改为已完成。
这个流程看似简单,实际写代码时需要注意一个并发问题:同一件物品可能同时被好几个人申请置换。我用的方案是给item表加一列transaction_lock,发起置换申请时用UPDATE item SET transaction_lock = 1 WHERE id = ? AND transaction_lock = 0这种原子操作抢占,影响行数为1才说明抢到了,否则提示"该物品正在处理中"。
这个"乐观锁思路"是面试时最爱问的一个点,也是实打实能跑通业务的方案。
4. 前后端对接与项目部署
4.1 接口设计与统一响应结构
前后端分离的项目,最怕的就是接口约定不一致。我强烈建议从第一个接口开始就统一返回值结构,不要这里返回JSON、那里返回字符串。
我封装了一个ApiResult<T>:
public class ApiResult<T> { private Integer code; // 200成功,401未登录,500异常 private String message; private T data; public static <T> ApiResult<T> success(T data) { ... } public static <T> ApiResult<T> error(Integer code, String message) { ... } }前端拿到响应先判断code,再取data。这大大减少了联调时"这个接口怎么返回格式不一样"的痛苦。
另外,全局异常处理一定要做。用@RestControllerAdvice捕获业务异常和系统异常,返回统一格式。否则MyBatis一抛SQL异常,用户看到一堆堆栈信息,既不安全也不好看。
4.2 Vue打包与SpringBoot集成
前端开发完以后,执行npm run build,会生成dist目录,里面是index.html和一堆带hash的静态资源。把dist目录里的内容复制到SpringBoot项目的src/main/resources/static下,重新打包,一个JAR就包含全部前端页面。
如果暂时不想复制,也可以直接在application.yml里配置:
spring: web: resources: static-locations: file:D:/workspace/dist/,classpath:/static/开发阶段改前端代码直接刷新就能看到效果,上线时再复制进static目录。这个小技巧能让开发和部署的切换效率提高不少。
还有一个细节容易被忽略:打包进JAR的静态资源,跟后端接口用的是同一端口。如果Vue路由用的是history模式,直接刷新前端页面会导致404。解决办法是后端加一个转发规则,把非/api/开头的路径都转发到index.html:
@Controller public class ForwardController { @RequestMapping(value = {"/", "/index.html", "/item/**", "/profile/**", "/admin/**"}) public String forward() { return "forward:/index.html"; } }4.3 项目打包部署要点
打包部署我用的是Maven,命令很简单:
mvn clean package -DskipTests打包后JAR在target目录下,直接java -jar就能跑。但有几个细节值得说:
- 上传文件目录必须放在JAR包外。我放在
/data/upload下,这个目录不属于classpath,打包进去重打也没了。 - 生产环境数据库连接、上传目录等配置要用外部配置文件覆盖,启动时指定
--spring.config.location=/data/conf/application-prod.yml。 - 如果部署在校园内网的Windows服务器上,记得关掉机器防火墙的相应端口,或者直接在局域网测试环境改端口。
这里说一句肺腑之言:别在自己电脑上觉得能跑就万事大吉。把项目拿到一台干净的服务器上,重新配置、重新打包、重新运行,这一整个过程走一遍,你才会真正理解什么叫"环境不一致"。
5. 常见问题与排查经验
5.1 SpringBoot版本引发的连锁问题
这个锅基本上每个写SpringBoot的人都背过。版本太高最直接的后果就是:网上教程全是2.x的写法,你的3.x项目里javax变jakarta,一堆第三方配置类失效,然后你开始怀疑人生。
如果你确实手滑新建了3.x项目,解决办法是调回2.7.x。直接在pom.xml里改:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent>如果改了以后Maven下载依赖报错,记得在IDEA的Maven设置里把"Always update snapshots"勾上,或者清一遍本地仓库里的lastUpdated文件。这也算是Maven项目构建方法里最常遇到的一个坑。
5.2 上传文件大小限制
这个几乎是必踩的坑。SpringBoot默认单文件上传上限是1MB,一旦前端传多张图片,总会莫名其妙地报FileSizeLimitExceededException。
在application.yml里配置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB建议把单文件上限设成5MB或10MB,一次请求最多10张图,总量控制在50MB内。另外在Controller方法上最好也做一层业务限制,前端也要压缩图片,不然手机拍的照片轻松就是5MB以上,服务端存储压力也大。
5.3 时间字段的时区问题
MySQL 8.0的默认时区是UTC,如果你连接的字符串没带serverTimezone参数,查出来的时候所有时间会比中国时间少8小时。
连接串里明确加上:
url: jdbc:mysql://localhost:3306/campus_market?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4同时MySQL启动时也可以加--default-time-zone='+08:00'。这个问题很隐蔽,前端展示"刚刚"、后端返回"昨天",用户一脸懵,排查了半天才发现是时区问题。
5.4 静态资源404和拦截器放行
如果登录拦截器写得太粗暴,把所有请求都拦了,那登录页面自己的CSS、JS、图片也全挂了。
我的放行规则是:
/api/user/login和/api/user/register放行/api/item/list、/api/item/detail放行/api/file/**放行/error放行
其余全部走JWT校验。
经验之谈:开发阶段可以用HandlerInterceptor加一个开关,配置里写auth.enabled=false能快速关闭校验,方便前端单独联调接口。上线再打开。
6. 一些项目落地的小建议
如果这个项目是你用来做毕设的,有个建议给到:把功能分层做,先把核心链路(注册、登录、发布、列表、详情、下单、确认完成)跑通,再补辅助功能(收藏、评论、私信、管理后台)。我见过太多同学卡在图片上传上搞了两三天,结果课设答辩时连商品列表都没做出来。
管理后台这个模块也别真做"摆设"。最简单的后台只需要三个页面:用户管理(禁用/解禁)、物品管理(下架违规物品)、分类管理,后端对应三个Controller就够。这样做完以后,整个系统"管理员"、"卖家"、"买家"三种角色都有,功能完整度和答辩说服力会提高一个档次。
另外,别忘了项目里的README和数据库初始化脚本。我每次拿到一个项目,第一件事就是看数据库脚本能不能一步导入。用PRIMARY KEY AUTO_INCREMENT、统一的utf8mb4字符集,外键约束合理设置,这些看起来不起眼的小事,恰恰是评审老师最关注的基本功。
按我这个路线把一个校园二手物品置换系统做完,你对SpringBoot的理解绝对不止于"会用",而是真正建立起了"从需求到上线"的完整链路:需求分析、数据库设计、接口设计、权限控制、文件处理、打包部署、问题排查。这些能力,比任何一个单独的技术点都要值钱。