简介:这份资源是一份基于Java的校园二手交易平台毕业设计文档,面向计算机相关专业学生及需要SSM项目实战参考的开发者,帮助解决校园二手物品在线交易系统的设计与实现问题。压缩包内仅含1个docx文件,约3.45MB,内容为完整的论文文档,涵盖摘要、系统分析、技术选型与功能设计等章节。文档围绕Spring、SpringMVC、MyBatis整合框架与Vue前端、MySQL数据库展开,详细阐述管理员、普通用户、商家三种角色的权限划分,以及商品查询、购买、购物车管理、订单管理等核心模块的实现思路。目前已有35人学习,适合作为课程设计或毕业设计的选题参考,读者可从中获取完整的系统架构方案、数据库设计思路与论文写作框架,快速理解SSM与Vue前后端分离项目的开发流程与关键技术要点。
1. 校园二手交易平台为什么值得用 Java 认真做一遍
每年毕业季,宿舍楼下堆着的旧书、自行车、小风扇、显示器,最后大多进了废品站。学生想卖,找不到人;想买,又怕被坑。校园二手交易平台要解决的就是这个信息撮合问题,而用 Java 来做,是绝大多数高校课程设计和真实校园项目里最稳的一条路。原因很直接:Java 生态成熟,Spring Boot 把 Web 层、数据层、安全层的样板代码压到最低,MyBatis-Plus 让单表增删改查几乎不用手写 SQL,前端再配 Vue 就能快速出一个能跑、能演示、能继续迭代的系统。
这篇笔记面向三类人:正在做「基于 Java 的校园二手交易平台设计与实现」的在校生,想把它当成第一个完整全栈项目的 Java 初学者,以及需要一套可复用交易类系统骨架的开发者。我会把技术选型理由、数据库设计、核心接口实现、权限与并发处理、以及真正会翻车的地方讲清楚,让你看完能自己搭起来,而不是停在「知道有这么个东西」。
2. 技术选型与工程骨架:为什么是 Spring Boot + MyBatis-Plus + Vue
2.1 后端为什么锁定 Spring Boot 而不是裸 Servlet
校园二手交易平台的功能边界其实很清晰:用户注册登录、发布商品、浏览搜索、下单、聊天或留言、订单状态流转。这些全是标准的 CRUD 加少量状态机逻辑。用裸 Servlet + JSP 也能做,但你会把大量时间花在请求参数解析、JSON 序列化、事务管理上,而不是业务本身。
Spring Boot 的价值在于约定优于配置。一个@RestController就能把方法暴露成 HTTP 接口,@Transactional就能保证下单扣库存和生成订单在同一个事务里。常见做法是用 Spring Boot 3.x 搭配 JDK 17,如果你学校机房还停留在 JDK 8,用 Spring Boot 2.7 也完全够用,别为了追新版本把自己卡在环境上。
选型上还有一个现实考量:面试。Java 后端面试题里 Spring Boot、MyBatis、事务、AOP 是高频区,把这个项目做透,等于把八股文里一大半概念落到了真实代码上,比背题强得多。
2.2 数据访问层用 MyBatis-Plus 省掉多少重复劳动
MyBatis-Plus 最实用的两个能力:一是BaseMapper<T>直接给你单表 CRUD,二是条件构造器LambdaQueryWrapper让你用 Java 代码写查询条件,避免拼字符串。对于商品列表这种带多条件筛选(分类、价格区间、关键词、排序)的场景,条件构造器写起来非常顺手。
下面是一个商品分页查询的最小实现,直接可以抄:
// ProductServiceImpl.java @Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService { @Override public Page<ProductVO> pageQuery(ProductQuery query, int pageNum, int pageSize) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 只查在售商品,已下架/已售出的不进列表 wrapper.eq(Product::getStatus, ProductStatus.ON_SALE.getCode()); // 分类筛选:categoryId 为空表示全部 wrapper.eq(query.getCategoryId() != null, Product::getCategoryId, query.getCategoryId()); // 价格区间:两个边界都做非空判断,避免 null 参与比较 wrapper.ge(query.getMinPrice() != null, Product::getPrice, query.getMinPrice()); wrapper.le(query.getMaxPrice() != null, Product::getPrice, query.getMaxPrice()); // 关键词模糊匹配标题,注意这里用的是 like,数据量大时要考虑全文索引 wrapper.like(StringUtils.hasText(query.getKeyword()), Product::getTitle, query.getKeyword()); // 默认按发布时间倒序,最新的商品排前面 wrapper.orderByDesc(Product::getCreateTime); Page<Product> page = this.page(new Page<>(pageNum, pageSize), wrapper); return page.convert(this::toVO); } }逻辑说明:eq、ge、le、like的第一个布尔参数是条件开关,为 false 时该条件不拼进 SQL,这是 MyBatis-Plus 处理动态查询的标准写法,比在 XML 里写一堆<if>干净。参数说明:pageNum从 1 开始,pageSize建议限制在 20 以内,防止有人传pageSize=100000把数据库拖垮,这个校验要放在 Controller 层做。
2.3 前端 Vue 与后端接口的对接约定
前端用 Vue 3 + Axios 就够了,不需要上太重的状态管理。关键是统一接口返回结构,否则前端每个页面都要写不同的解析逻辑。我一般约定成{ code, msg, data }三段式,code=0表示成功。后端用一个Result<T>包装类统一返回,配合全局异常处理器,业务异常直接抛,前端只处理code和msg。
跨域问题在开发阶段很常见,后端加一个CorsConfig允许本地前端端口访问即可,生产环境用 Nginx 反代同源部署,就不存在跨域了。这里别用@CrossOrigin到处贴,集中配置一次更省心。
3. 数据库设计与核心表落地:从用户到订单的字段怎么定
3.1 五张核心表的关系与字段取舍
校园二手交易平台最小可用模型需要五张表:用户表、商品表、商品分类表、订单表、留言/聊天表。关系是:一个用户发布多个商品,一个商品属于一个分类,一个买家可以对多个商品下单,一个商品可以有多条留言。
字段设计上有几个容易拍脑袋拍错的地方。商品表一定要有status字段区分在售、已售、下架,不要用is_sold布尔值,因为后面你一定会加「审核中」「违规下架」这些状态。价格用DECIMAL(10,2),绝对不要用FLOAT,浮点数算钱迟早出问题。订单表要存商品快照(标题、价格、图片),因为商品可能被卖家改价或删除,订单必须保留成交时的信息。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, username, password, phone, campus, credit_score | 密码存 BCrypt 哈希,不存明文 |
| product | id, seller_id, category_id, title, price, status, create_time | status 用 tinyint 枚举 |
| category | id, name, sort | 分类变动少,可缓存 |
| orders | id, order_no, buyer_id, product_id, amount, status | order_no 唯一索引防重 |
| message | id, product_id, from_user, to_user, content | 按商品维度组织会话 |
3.2 建表 SQL 与索引该加在哪
索引不是越多越好,写多读少的表加太多索引会拖慢插入。这个系统里必须加的索引有三个:product表的(status, category_id, create_time)联合索引,支撑列表页的筛选加排序;orders表的order_no唯一索引,防止重复下单;message表的(product_id, create_time)索引,支撑会话消息按时间拉取。
CREATE TABLE `product` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `seller_id` BIGINT NOT NULL COMMENT '卖家用户id', `category_id` INT NOT NULL COMMENT '分类id', `title` VARCHAR(100) NOT NULL COMMENT '商品标题', `price` DECIMAL(10,2) NOT NULL COMMENT '售价,禁止用float', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1在售 2已售 3下架', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_list` (`status`, `category_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';逻辑说明:联合索引idx_list的顺序很关键,等值条件status和category_id放前面,范围/排序字段create_time放最后,这样列表页查询能走索引。参数说明:utf8mb4是为了支持 emoji,学生发商品描述里带表情很常见,用utf8会插入失败。
3.3 订单号生成与防重复提交
订单号不要用自增 id 直接暴露,也不要用时间戳,高并发下会撞。常见做法是「时间戳 + 用户id后四位 + 随机数」或者用雪花算法。更关键的是防重复提交:用户手抖点两下下单按钮,就会生成两笔订单。
解决思路是前端按钮点击后置灰,后端用「商品状态 + 乐观锁」兜底。下单时先UPDATE product SET status=2 WHERE id=? AND status=1,根据影响行数判断是否抢到,影响行数为 0 说明商品已被别人买走或已下架。这一步必须在事务里,且要在生成订单之前执行。
@Transactional(rollbackFor = Exception.class) public String createOrder(Long buyerId, Long productId) { // 乐观更新:只有当前仍在售才能抢到,影响行数是并发安全的判据 int affected = productMapper.markSold(productId); if (affected == 0) { throw new BizException("手慢了,商品已被买走或已下架"); } Product product = productMapper.selectById(productId); Orders order = new Orders(); order.setOrderNo(generateOrderNo(buyerId)); order.setBuyerId(buyerId); order.setProductId(productId); order.setAmount(product.getPrice()); // 存快照价格 orderMapper.insert(order); return order.getOrderNo(); }逻辑说明:markSold对应的 SQL 是带status=1条件的更新,数据库行锁保证同一时刻只有一个事务能改成功,这是最轻量的并发控制,比synchronized靠谱,因为后者在集群下失效。参数说明:rollbackFor = Exception.class保证任何异常都回滚,默认只回滚运行时异常,检查型异常不回滚,这个坑很多人踩过。
4. 权限、登录与接口安全:别让平台变成刷接口的靶子
4.1 登录态用 JWT 还是 Session
校园项目里两种都行,但我更推荐 JWT,因为前后端分离部署时无状态,扩展方便。流程是登录成功后签发 token,前端存 localStorage,每次请求放Authorization头,后端用拦截器校验。JWT 的 payload 里放 userId 和角色,不要放敏感信息,因为它是可解码的,只是防篡改。
要注意的是 JWT 无法主动失效,用户改密码或登出后旧 token 在过期前仍然有效。校园项目可以接受这个折中,如果要求严格,就在 Redis 里维护一个黑名单,登出时把 token 加进去,校验时先查黑名单。
4.2 接口防爬与参数校验
热搜词里有人问「controller 层如何防护防止爬虫」,这在二手平台确实是个真问题,商品列表和用户信息最容易被批量抓取。几个低成本手段:对列表接口做频率限制,同一 IP 每分钟超过阈值就拒绝;分页接口强制限制pageSize上限;敏感接口(如查看手机号)要求登录且校验商品归属。
参数校验用@Valid+ JSR-303 注解,别在 Controller 里写一堆 if。比如发布商品时标题不能为空、价格必须大于 0,直接标在 DTO 字段上,校验失败由全局异常处理器统一返回,代码干净且不容易漏。
public class ProductCreateDTO { @NotBlank(message = "标题不能为空") @Size(max = 100, message = "标题不能超过100字") private String title; @NotNull(message = "价格不能为空") @DecimalMin(value = "0.01", message = "价格必须大于0") private BigDecimal price; @NotNull(message = "分类不能为空") private Integer categoryId; }逻辑说明:@NotBlank用于字符串且会去空格,@NotNull用于对象,别混用。参数说明:@DecimalMin对BigDecimal生效,比手写price.compareTo(BigDecimal.ZERO) > 0更不容易写错。
4.3 越权访问是校园项目最常见的漏洞
很多同学的系统里,删除商品接口只传了商品 id,后端直接删,没校验这个商品是不是当前登录用户发布的。结果就是任何人改一下 id 就能删别人的商品。这类「水平越权」在课程设计里扣分很重,在真实系统里是事故。
正确做法是每个涉及资源归属的操作,都要用「资源 id + 当前用户 id」作为条件去查或改,而不是先查出来再在 Java 里比对。前者一条 SQL 就挡住了,后者容易漏判。
// 删除商品:条件里带上 seller_id,从 SQL 层面杜绝越权 public boolean deleteOwnProduct(Long productId, Long currentUserId) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getId, productId) .eq(Product::getSellerId, currentUserId); // 关键:归属校验下沉到SQL return this.remove(wrapper); }逻辑说明:remove(wrapper)会生成带两个条件的 DELETE,如果商品不属于当前用户,影响行数为 0,删除自然失败。参数说明:currentUserId必须从 token 解析得到,绝不能从前端传参,否则等于没校验。
5. 避坑与排查:这些翻车点我基本都经历过
5.1 现象:列表页越翻越慢,最后超时
原因:分页用了LIMIT offset, size,当 offset 很大时,MySQL 要扫描并丢弃前面所有行,深分页性能急剧下降。校园项目数据量不大时看不出来,一旦导入几万条测试数据就暴露。
解决:限制最大可翻页数,比如只允许翻到第 100 页;或者改用「上一页最后一条 id」的游标分页,WHERE id < lastId ORDER BY id DESC LIMIT size,这样每页都是走主键索引,速度稳定。
5.2 现象:商品图片上传后刷新就 404
原因:图片存到了项目运行目录,重新部署或重启后目录被清空;或者存了本地绝对路径,换台机器路径就失效。
解决:开发阶段把上传目录配置成项目外的固定路径,并在配置里用变量而不是硬编码;生产环境直接上对象存储。数据库里只存相对路径或 URL,不存绝对路径。这个坑几乎每个做文件上传的人都会踩一次。
5.3 现象:下单后库存没扣,或者扣了订单没生成
原因:事务没生效。常见的是在同一个类里方法 A 调用本类的@Transactional方法 B,走的是this调用,没经过 Spring 代理,事务注解形同虚设。
解决:把事务方法抽到另一个 Service 里,或者注入自己的代理对象。更稳妥的做法是把「扣状态 + 建订单」放在同一个 public 方法上,由 Controller 直接调用,别在内部绕。
5.4 现象:中文商品标题存进数据库变成问号
原因:数据库连接 URL 没指定字符集,或者建表时用了utf8而不是utf8mb4,或者 JDBC 驱动版本太老。
解决:连接串加useUnicode=true&characterEncoding=utf8,建表和库统一utf8mb4,驱动用和 MySQL 版本匹配的版本。三处只要有一处不对就会乱码,排查时逐个确认。
5.5 现象:并发测试时同一商品被卖出两次
原因:先select查状态再update,两步之间有时间窗口,两个请求都查到「在售」,然后都去更新。
解决:就是我前面写的乐观更新,把状态判断和更新合并成一条带条件的 SQL,用影响行数做判据。这是最经典也最有效的办法,别用「先查后改」。
6. 让项目从能跑到能打:压测、缓存与可演示的加分项
做到这里系统已经能跑通了,但课程设计答辩或真实上线前,还有几件事能让它从「能跑」变成「能打」。第一件是压测,用 JMeter 或 wrk 对商品列表和下单接口各压一轮,重点看下单接口在并发下有没有超卖、响应时间是否稳定。压测不是为了刷数字,而是为了验证你前面写的乐观锁和索引真的起作用了。
第二件是缓存。分类列表、热门商品这类读多写少的数据,用 Redis 缓存几分钟,能明显降低数据库压力。但要注意缓存和数据库的一致性:商品状态变更(比如卖出)时要主动删缓存,而不是等它过期,否则用户会看到已售商品还在列表里。我一般用「更新数据库后删除缓存」这个顺序,简单且够用。
第三件是让项目可演示。答辩时老师最想看的是完整业务闭环,而不是你讲了多少技术名词。准备一条清晰的演示路径:注册两个账号,A 发布商品,B 搜索到并下单,A 看到订单,双方留言沟通。这条路径跑通,比堆十个用不上的功能强。
最后分享一个我自己的习惯:每加一个功能,先想清楚它的失败路径是什么——网络断了怎么办、重复提交怎么办、数据被删了怎么办。校园二手交易平台看着简单,但订单、并发、权限这三块只要有一块没想透,演示时就可能当场翻车。把失败路径处理掉,这个项目才真正算「设计与实现」完成,而不只是「写完了」。希望帮到你。
本文还有配套的精品资源,点击获取