恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Java与Spring Boot的校园二手交易系统设计:从数据库到前后端部署
首页
资讯中心
/
基于Java与Spring Boot的校园二手交易系统设计:从数据库到前后端部署
基于Java与Spring Boot的校园二手交易系统设计:从数据库到前后端部署
发布时间:2026/9/16 9:27:31
简介一套完整的校园二手交易市场系统Java毕业设计资源包面向计算机相关专业学生及需要快速搭建类似平台的学习者适合具备Java Web基础、希望参考完整项目实现的人群可满足毕业设计、课程设计或项目练手需求。压缩包共4个文件内含项目源代码压缩包、SQL数据库脚本以及两段MP4操作演示录像01、02部分整体大小约73.27MB。源代码涵盖完整业务逻辑与前后端交互SQL脚本可直接导入数据库完成表结构及初始化数据演示录像则逐步展示从环境配置到功能使用的全过程。整套资源已通过验收且可正常运行能有效降低环境搭建与代码阅读门槛便于掌握用户注册、商品发布、订单管理等核心模块同时可用于答辩或功能演示。目前已有116人学习下载对希望借鉴完整项目方案并快速上手Java Web开发的读者颇具参考价值。1. 基于 Java 的校园二手交易市场这套毕设题到底在考什么很多人拿到「基于 Java 的校园二手交易市场系统」这个题目第一反应是先找源码但验收的重点不在页面好看而在交易链路能不能说清楚商品从发布到被下单再到订单状态流转数据一致性靠什么保证。这个题目逼你同时面对三类问题——用户与商品的关系建模、订单状态的并发控制、图片文件的上传落地这正是 Java Web 开发最常见的组合考点。一套 MySQL 脚本加一个 Spring Boot 后端就能把系统跑起来。下面按「数据库 → 后端 → 前端 → 部署 → 进阶」展开给出可直接抄的建表 SQL、核心接口代码和打包命令。适合正在做同课题的毕业生也适合想快速搭一套小型交易系统的开发者。2. 数据库设计从 ER 模型到建表 SQL数据库设计是交易系统最容易返工的地方。很多源码包里的 ER 图画得很完整但实际建表时字段对不上、状态没默认值结果联调阶段疯狂改代码。这个系统的核心实体共五张表用户user、商品goods、分类category、订单orders、收藏favorite再加一张可选的评论表comment就够。别把评论和回复拆成两张表单表加 parent_id 足够支撑演示。2.1 核心实体与字段取舍用户表不要存明文密码这是底线。密码字段最少要用 BCrypt 加密后入库。学号student_no可以作为业务唯一键但用户主键建议用自增 id因为商品表、订单表里引用主键做关联是最省事的做法。商品表是整张库的枢纽字段设计决定后面查询好不好写。价格字段一定用 DECIMAL(10,2) 而不是 FLOAT避免浮点误差在订单计算时露馅。status 字段用 TINYINT0 表示上架、1 表示交易中已被锁定、2 表示已售出、3 表示下架这样订单在下单时对商品行加锁就有明确依据。订单表不要由业务代码手动拼接订单号常见做法是插入时用时间戳加随机数生成或者用 MyBatis-Plus 的 ID 生成策略再拼接前缀。另一个容易漏的点是加 version 字段或用状态机字段防止重复点击下单这在后面讲事务时会回到。ER 关系可以这样画user 与 goods 是一对多category 与 goods 是一对多goods 与 orders 是一对一一笔订单对应一件商品user 与 goods 通过 favorite 表构成多对多收藏关系。这四条线画清楚数据库课程设计部分的答辩基本就稳了。表名关键字段类型约束与说明useridBIGINT自增主键userstudent_noVARCHAR(20)唯一索引userpasswordVARCHAR(100)BCrypt 密文goodspriceDECIMAL(10,2)商品售价goodsstatusTINYINT0上架 1锁定 2已售 3下架ordersorder_noVARCHAR(32)唯一业务单号ordersstatusTINYINT0待付款 1待发货 2待收货 3完成 4取消2.2 建表 SQL 与关键索引我一般会写一套含初始分类数据的 SQL因为演示时不想手动去后台补分类。以下 SQL 在 MySQL 8.0 下编写JDBC 驱动要使用com.mysql.cj.jdbc.Driver。CREATE DATABASE campus_trade DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE campus_trade; CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) DEFAULT 二手淘客, avatar VARCHAR(255) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no), KEY idx_username (username) ) ENGINEInnoDB; CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0 ) ENGINEInnoDB; CREATE TABLE goods ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, category_id INT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2) DEFAULT NULL, images VARCHAR(1024) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0, view_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_category (category_id), KEY idx_status (status), KEY idx_create_time (create_time), CONSTRAINT fk_goods_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_goods_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB; CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, goods_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, finish_time DATETIME DEFAULT NULL, UNIQUE KEY uk_order_no (order_no), UNIQUE KEY uk_goods_order (goods_id), KEY idx_buyer (buyer_id), KEY idx_seller (seller_id), CONSTRAINT fk_orders_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ) ENGINEInnoDB;这段 SQL 有两个细节答辩时容易被追问。orders 表对 goods_id 建了唯一索引uk_goods_order含义是一件商品同时只能存在一个有效订单即使代码层忘了判重数据库层也会在重复下单时抛 Duplicate Entry 错误。第二个细节是所有外键都显式命名迁移或排错时更容易定位约束冲突。Alibaba Java 开发手册推荐逻辑外键而非物理外键实际生产里确实更常见。但作为课程设计和毕业设计保留物理外键更有说服力这两张表并发量很低外键性能影响可以忽略。2.3 订单状态与并发控制最容易丢分的点是订单状态在并发下怎么保持一致。典型场景是商品详情页两个人同时点「立即购买」系统只能允许一个成功。常见做法是悲观锁在 Service 层对商品行执行SELECT ... FOR UPDATE。SELECT * FROM goods WHERE id #{goodsId} AND status 0 FOR UPDATE;执行效果是事务 A 拿到该商品行锁后事务 B 执行同一条语句会阻塞到 A 提交或回滚。拿到锁的线程再判断 status 是否仍为 0然后执行UPDATE goods SET status 1 WHERE id ...最后插入订单。整个过程必须在同一个事务方法里完成锁才能跨 SQL 生效。Transactional不能写错位置。常见错误是 Controller 里先查询商品再调用下单方法查询发生在事务外并发判断就失效。正确做法是把「查并锁行 改状态 建订单」封装到 OrderService 的一个方法里。Override Transactional(rollbackFor Exception.class) public Long createOrder(Long goodsId, Long buyerId) { // selectByIdForUpdate 对应 SELECT ... FOR UPDATE Goods goods goodsMapper.selectByIdForUpdate(goodsId); if (goods null || goods.getStatus() ! 0) { throw new BizException(商品不存在或已下架); } if (goods.getUserId().equals(buyerId)) { throw new BizException(不能购买自己发布的商品); } goods.setStatus(1); goodsMapper.updateById(goods); Order order new Order(); order.setOrderNo(OrderNoGenerator.generate()); order.setGoodsId(goodsId); order.setBuyerId(buyerId); order.setSellerId(goods.getUserId()); order.setPrice(goods.getPrice()); order.setStatus(0); orderMapper.insert(order); return order.getId(); }selectByIdForUpdate是写在 GoodsMapper 里的自定义方法对应上面的 FOR UPDATE 语句。先 SELECT 再 UPDATE 是为了拿到锁后重新检查商品状态。如果跳过 SELECT 直接UPDATE ... WHERE status0虽然能拦截部分并发但拿不到商品价格和卖家 id订单行就没法正确组装。status 流转要写在代码里而不是靠前端控制。比较规范的做法是做一个订单状态枚举统一管理「只有交易中才能取消」「只有待收货才能确认」这类规则演示时手动改数据库这种操作就不会发生。超时未支付自动取消真实系统用定时任务或延迟队列毕设阶段在查询订单时判断创建时间超过 30 分钟并自动改为取消是性价比很高的方案。3. Spring Boot 后端接口分层与业务实现拿到源码包后最先要跑通的是登录、发布商品和下单三个接口它们串联用户、商品、订单三张核心表。常见的项目结构是 controller / service / mapper 三层加一个 common 包放统一返回体和异常处理。统一返回体建议用{ code, message, data }三段式前后端联调比直接返回对象更容易定位问题。3.1 Maven 依赖与项目目录划分pom.xml 里最容易被遗漏的是spring-boot-starter-validation和spring-boot-starter-aop。前者支持在 Controller 参数上直接写NotBlank完成校验后者是事务注解生效的前提之一。图片上传用spring-boot-starter-web自带的 MultipartFile 即可如果用 MyBatis-Plus还要引mybatis-plus-boot-starter版本与 Spring Boot 主版本对应关系需要看一下兼容矩阵。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies目录划分按职责组织答辩画架构图时能直接照着讲不会出现「业务写在 Controller」这种尴尬com.campus.trade ├── controller # 接收 HTTP 请求 ├── service # 业务逻辑 事务边界 ├── mapper # MyBatis-Plus 接口与 XML ├── entity # 数据库实体 ├── common # 统一返回体、异常、工具类 └── config # WebMvc/静态资源/拦截器配置3.2 发布商品与图片上传链路发布商品最麻烦的不是 insert而是图片。常见做法是把 MultipartFile 写到本地磁盘 upload 目录返回可访问的 URL 存入 goods.images 字段。images 字段用逗号分隔多图路径前端轮播图直接 split 成数组。PostMapping(/api/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty() || file.getSize() 5 * 1024 * 1024) { return Result.error(文件为空或超过5MB); } String originalFilename file.getOriginalFilename(); // 只取扩展名防止路径注入 String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) ext; File dest new File(uploadDir File.separator fileName); try { file.transferTo(dest); return Result.success(/upload/ fileName); } catch (IOException e) { log.error(upload fail, e); return Result.error(上传失败); } }关键在于 uploadDir 从配置读而不是硬编码否则打成 jar 后路径失效。我在 application.yml 里配app.upload-dir: ./upload通过Value注入。静态资源映射要在 WebMvcConfig 里手动加Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(uploadDir File.separator); }上传接口返回的 URL 拼进商品表单。商品新增接口用Validated校验 title、price 等必填项然后在 GoodsService 里把 status 赋默认值 0。价格用 BigDecimal 接收前端传字符串即可避免浮点精度丢失。不做这一步演示时可能出现 19.99 变成 19.989999。3.3 登录鉴权与个人中心校园二手交易系统最常见的鉴权是 JWT。登录成功后后端返回 token前端放入请求头 Authorization后端拦截器解析并放行。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } Long userId JwtUtil.getUserId(token); // 后续 Controller 通过 request 取 userId request.setAttribute(userId, userId); return true; } }拦截器注册时注意放行白名单/api/auth/login、/api/auth/register、/upload/**和商品列表、详情等公开接口其余走 JWT 校验。验证码可以不加但连续五次登录失败锁定账号这个功能性价比很高计数器存 Redis 或 ConcurrentHashMap 都行答辩时能多讲两点。个人中心接口基本是三个我发布的、我购买的、我收藏的。都能用 MyBatis-Plus 的 LambdaQueryWrapper 写但分页必须配置PaginationInnerInterceptor否则 Page 对象只会返回全量数据而 total 永远为 1这是新手必踩的坑。方法路径说明鉴权POST/api/auth/login登录返回 token否GET/api/goods/list分页商品列表支持关键词否POST/api/goods/save发布商品是POST/api/upload图片上传单张是POST/api/order/create提交订单是4. 前端页面与接口联调从 JSP 到 Vue 的取舍源码包里前端可能有两种形态JSP 工程或 Vue 工程。从近几年毕业设计源码的分布看Vue2 Element-UI 仍然最常见Vue3 Element-Plus 在上升。JSP 的整页刷新在演示时观感不如 Vue 局部刷新如果从零搭建我会选 Vue。4.1 Vue 工程的最小依赖与 axios 封装Vue 方案下「前台商城」和「后台管理」通常在一个工程里。路由用 vue-routertoken 用 localStorage 存不需要 Vuex/Pinia。axios 封装拦截器请求前加 Authorization。// src/utils/request.js import axios from axios; const request axios.create({ baseURL: http://localhost:8080/api, // 后端地址上线改 nginx 同源 timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } return Promise.reject(error); } ); export default request;response 拦截器直接返回response.data业务代码拿到的就是{ code, message, data }不用每处写.data.data。后端如果返回 code500还可以在这里统一弹错误提示。演示时最怕接口报错但不提示用户以为按钮坏了。4.2 联调中 3 个高频坑与规避手段第一个坑是跨域。后端没配 CORS、前端端口与后端不一致时浏览器直接报 CORS error。推荐后端统一处理Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }关键点allowCredentials(true)时addAllowedOrigin(*)无效必须用addAllowedOriginPattern(*)。这在 Spring Boot 2.4 里会卡住很多人。第二个坑是 LocalDateTime 序列化后变成数组或带 T 的 ISO 格式。统一在 yaml 里配格式和时区spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个坑是图片上传后刷新变 404。排查时先直接访问/upload/文件名看是否落在 uploadDir 下再确认静态资源映射里方法名是 addResourceHandler 而不是 addResourceHandlers。4.3 演示录像里必须录进去的操作链路演示录像是验收材料的一部分。录制时把窗口布置成三层左侧浏览器、右侧 IDEA 控制台和数据库客户端。先录 SQL 脚本执行再录后端启动最后进页面操作。五步操作要完整注册新用户并登录发布一件带图片的二手商品在商品列表搜索到刚发布的商品用第二个账号购买模拟卖家发货、买家收货返回个人中心展示订单状态变化录制时可以有意制造一次「服务未启动导致接口报错重启后恢复」的对比比一路顺畅更能体现对系统的理解。但核心流程不能翻车数据库连不上这种环境问题会显得不够专业。5. 从源代码到交付打包部署与坑点排查交付时接收方拿到的是「源代码 演示录像 数据库脚本」这个结构。最后一步要确认代码能在干净机器上跑起来。很多项目在自己电脑上正常换台机器就起不来问题往往出在环境依赖和路径配置。5.1 干净环境下的最小启动条件Vue 前端需要 Node.jsnpm install后npm run serve启动后端需要 JDK 8 或 11、Maven 3.6。先执行项目里的 .sql 脚本建库MySQL 8.0 下特别注意脚本开头指定 utf8mb4避免中文乱码。数据库连接串尽量写127.0.0.1而不是 localhost。某些 Linux 环境的 JDBC 驱动解析 localhost 会走到 IPv6 ::1导致连不上 MySQL这种错误最容易在换机器后出现。5.2 Maven 打包命令与启动参数cd backend mvn clean package -DskipTests java -jar target/campus-trade-0.0.1.jar --server.port8080-DskipTests在交付前打包是必要的否则测试类连不上数据库会失败。如果打包出来是 plain jar 而不是可执行 jar检查 pom 里是否有spring-boot-maven-plugin。启动后不要立刻点页面先看控制台有没有Tomcat started on port 8080和数据库连接池初始化完成两行关键日志。5.3 按现象定位问题的排错表现象首要排查点常用命令/操作端口被占用8080 被其他进程占用netstat -ano | findstr 8080数据库连不上驱动、时区、密码改用 127.0.0.1 再试页面能开但接口 404打包漏了静态资源mvn clean package 重新打图片不显示静态资源映射未生效直接访问 /upload/xx.jpg控制台中文乱码IDEA/终端编码-Dfile.encodingUTF-8图片不显示是交付时最容易出的问题。常见原因upload 目录在项目根目录而 jar 运行时的工作目录是命令行所在目录两者不一致。解决办法是把 uploadDir 配成绝对路径或在启动脚本里 cd 到固定目录再执行 java -jar。启动脚本建议写成 start.sh / start.bat里面包含「切目录 → 设置 JAVA_OPTS → 执行 java -jar」。演示录像里执行脚本比手工敲一长串命令更有工程感。6. 系统通过验收之后的进阶配置缓存、安全与监控如果被追问「真实部署还要做哪些事」有三组配置值得提前准备好。第一组是缓存把访问量大且写操作少的接口加缓存比如首页商品列表和分类导航。直接用 Spring Cache 注解key 按「分类 ID 页码」生成缓存时间设 5 分钟。商品详情不能缓存太久否则卖家下架后用户还能访问到残留数据。management: endpoints: web: exposure: include: health,info第二组是安全。密码入库用 BCryptSpring Security 里的 BCryptPasswordEncoder 可以直接拿来用。数据库连接串追加useSSLfalseallowPublicKeyRetrievaltrue避免 MySQL 8.0 连接握手报错。所有 SQL 走预编译不拼接字符串这是防 SQL 注入的基本功。统一异常处理里把业务异常和系统异常分开系统异常打完整堆栈业务异常只回传 message避免把异常堆栈暴露到前端。第三组是验证服务存活。引入 Actuator 后暴露 health 端点curl http://localhost:8080/actuator/health返回 UP 就说明进程正常。配合启动脚本做端口检查比只看进程是否存在更可靠。最后给一个可落地的技巧商品列表接口的 SQL 加 LIMIT 分页即使前端不传页码后端也在 Mapper 里绑定默认值。这样即使用脚本遍历接口数据库也不会全表扫描。这个习惯比加 Redis 更基础也是交易类系统压测时最常被点名的改进点。本文还有配套的精品资源点击获取