简介基于Java的校园二手交易市场系统设计与实现完整交付包面向Java课程设计、毕业设计或需要快速掌握Java Web项目开发的学习者。整套资料已通过验收并可直接运行内含源代码压缩包、SQL数据库脚本及两段演示录像。源代码工程便于理解整体结构与业务逻辑数据库脚本提供初始表结构和测试数据演示视频则展示部署后的实际运行效果。压缩包共4个文件类型包括zip、sql、mp4总大小约73MB目录划分清楚下载后可对照视频快速完成部署。已有116人浏览学习适合正在完成二手交易类毕设或想提升Java后端开发能力的用户。1. 校园二手交易市场系统一个Java毕设项目的真实分量在Java毕设选题里“校园二手交易市场系统”是那种一眼看过去不惊艳、做起来才知道门道的题目。它本质是一套标准的Java Web增删改查系统用户注册登录、发布闲置、浏览下单、收藏留言管理端再做商品上下架和数据统计。业务模型不复杂却五脏俱全——有用户权限、有商品状态、有订单流转刚好能把Java基础里那些存货全用上。标题里的zip带着源代码、演示录像和数据库意味着你要的不是一份讲稿而是一个能导库、能启动、能照录像走通全流程的落地工程。适合两类人一是Java基础尚可、正在为毕设选型发愁的同学二是想拿完整案例练手的初级Web开发者。2. 技术选型与项目骨架为什么Spring BootMyBatis是二手交易毕设的稳答案标题只写了“基于Java”框架没锁死而这恰恰是第一个要拍板的地方。搜同样题目的项目跑得起来的绝大多数是两条路线SSMSpringSpringMVCMyBatis和Spring BootMyBatis。两条都能做但我的建议很直白拿到SSM包别嫌弃老SSM把Spring的容器装配、事务、AOP全暴露在XML里答辩被问“Bean生命周期”“事务怎么配”时你反而能接得住拿到Spring Boot包更好配置量大概是SSM的三分之一演示时内嵌Tomcat少一个部署环节。真正不建议的是在这个题目上做前后端分离——Vue加Spring Boot要同时起两个服务、处理跨域、维护Node依赖现场演示环境一乱就是灾难。毕设场景稳定大于炫技。对比项SSMSpring Boot MyBatis前后端分离上手成本中配置文件多低自动装配高两套工程答辩可问深度高手配容器中要懂starter原理高但难度陡演示稳定性中Tomcat部署易错高内嵌Tomcat低跨域加双服务适合人群想深挖原理效率优先、演示求稳已做过前端项目2.1 拿到压缩包先做三件事看依赖、看SQL脚本、看配置文件不管压缩包里是SSM还是Spring Boot我的习惯是先别急着开IDE。第一件打开pom.xml看依赖版本——JDK版本和Spring Boot版本对不对得上MySQL驱动是5.x还是8.x第二件找到sql目录或*.sql文件看有没有初始化数据很多包表结构有但数据是空的这意味着你注册完第一个账号就是全站第一个用户第三件打开application.yml或jdbc.properties看数据库用户名、密码、库名和你本机是否一致。这三步不做就点运行九成会在数据库连接上翻车。技术栈的具体组合我一般按JDK 8或11、Spring Boot 2.x、MySQL 8.0、Maven 3.6左右来配。MySQL 8.0是重点新机器装5.7反而费劲驱动类名也和5.x不一样后面避坑章会专门说。这套组合的好处是网上资料最密你遇到的每个报错几乎都有人踩过。2.2 项目骨架Web层、Service层、DAO层怎么切才经得住答辩分层的意义不是代码好看而是答辩必问为什么要分三层标准回答是职责单一、替换底层不影响上层、事务边界好控制。二手交易系统里最典型的例子是下单要查商品、改商品状态、插订单三个数据操作揉在controller里事务一加就失效拆到service层一个Transactional就能兜住。src/main/java ├── com/campus/market │ ├── MarketApplication.java │ ├── config # 拦截器、静态资源映射配置 │ ├── controller # 接收参数返回页面或JSON │ ├── service # 业务规则注册、发布、下单 │ │ └── impl │ ├── dao # MyBatis Mapper接口 │ ├── entity # 对应数据库表的实体类 │ └── common # 统一返回对象、自定义异常、工具类 src/main/resources ├── application.yml # 数据源、上传目录、端口 └── mapper # MyBatis XML写SQL这个骨架按controller → service → dao往下调反过来往上返回。controller只做参数接收和路由不写业务service里放校验和事务dao只管SQL。很多学生图省事把业务写进controller代码是少了几行但“又查又改又插”的操作揉在一起事务边界就糊了这也是答辩时最容易被追问的扣分点。2.3 最小启动配置数据源、连接池与上传目录怎么填Spring Boot的组合里一份application.yml就能让项目站起来。下面这份是常见的可抄配置库名按你的SQL脚本实际修改spring: datasource: url: jdbc:mysql://localhost:3306/campus_market?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 max-lifetime: 1800000 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true server: port: 8080 upload: dir: D:/campus-market-upload参数说明里有两个容易踩的driver-class-name在MySQL 8下必须是com.mysql.cj.jdbc.Driver老包的com.mysql.jdbc.Driver会直接报ClassNotFoundserverTimezoneAsia/Shanghai不写会差八小时插入时间全乱。连接池这一段用的HikariCP面试和答辩常被问两个值——maximum-pool-size控制最多同时拿几个连接minimum-idle是空闲时保底保留几个connection-timeout超过30秒拿不到连接就算失败。不少项目会把Hikari换成Druid那就多一个stat-view-servlet开SQL监控能看几百条慢SQL演示时打开监控页也是个加分动作。上传目录单独抽成upload.dir就是为了换机器只改一行不用翻代码。3. 数据库设计五张核心表和一对多关系的落地数据库是这种项目里最不该省的地方。二手交易系统去掉花哨功能核心实体就五个用户、商品、订单、收藏、留言。实体关系也很清爽用户一对多商品用户一对多订单用户和商品通过收藏表变成多对多商品一对多留言。能把这层关系在纸上画出来数据库的“知识点概念”类答辩题基本就拿下一半了。表名用途关键字段t_user注册登录、个人信息username、password、salt、credit_scoret_goods商品发布与状态seller_id、price、status、image_urlt_order订单流转goods_id、buyer_id、seller_id、statust_favorite用户收藏商品user_id、goods_id、联合唯一t_message买家咨询卖家回复goods_id、user_id、content、reply3.1 建表SQL五张表一次建完字段约束当场定死下面的SQL是直接能执行的版本。注意库名、表名都用小写order是MySQL保留字表名统一叫t_order这个坑我见过不止一次。CREATE DATABASE campus_market DEFAULT CHARACTER SET utf8mb4; USE campus_market; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL COMMENT 登录名唯一, password VARCHAR(64) NOT NULL COMMENT MD5加盐后的密文, salt VARCHAR(32) NOT NULL COMMENT 随机盐, nickname VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, head_image VARCHAR(255) DEFAULT NULL COMMENT 头像访问路径, credit_score INT DEFAULT 100 COMMENT 信用分, role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB; CREATE TABLE t_goods ( id INT AUTO_INCREMENT PRIMARY KEY, seller_id INT NOT NULL COMMENT 发布者对应t_user.id, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL COMMENT 成交价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价可选, image_url VARCHAR(255) DEFAULT NULL COMMENT 图片访问路径, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在售 1已售 2下架, is_deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_goods_status_create (status, create_time) ) ENGINEInnoDB; CREATE TABLE t_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 对外展示单号, goods_id INT NOT NULL, buyer_id INT NOT NULL, seller_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待发货 1已完成 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_order_buyer (buyer_id), KEY idx_order_seller (seller_id) ) ENGINEInnoDB; CREATE TABLE t_favorite ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, goods_id INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINEInnoDB; CREATE TABLE t_message ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, reply VARCHAR(500) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB;字段设计里三个决定要说清楚。price用DECIMAL(10,2)而不用float或double金额精度是答辩高频题二进制浮点会出0.1加0.2不等于0.3的问题DECIMAL是定点数不存在这个事。status用TINYINT不用VARCHAR存“在售/已售”数据库字典化之后查询排序都快程序里用常量或枚举映射避免到处if一串中文。两张明细表都建了普通KEY故意不建物理外键——物理外键在删除商品时会处处掣肘并发写也有锁开销这里用逻辑外键程序里维护关系更合适这句话答出来很多考官会点头。3.2 业务查询SQL分页、排序、联表一次到位增删改查是这个项目的主旋律但查得对不对全看where条件和联表。最核心的商品列表查询长这样SELECT g.id, g.title, g.price, g.image_url, u.nickname FROM t_goods g JOIN t_user u ON g.seller_id u.id WHERE g.status 1 -- 1按上面定义为已售,0为在售;按你的字典改 AND g.is_deleted 0 ORDER BY g.create_time DESC LIMIT #{offset}, #{pageSize};这段SQL的索引走的是idx_goods_status_create因为where里先卡status再用create_time排序联合索引刚好吃到。联表只取展示需要的昵称字段不SELECT *页面渲染会轻很多。LIMIT的offset用(pageNo-1)*pageSize算这是分页的老规矩。这里有一个常见的理解偏差增删改查不只是四个方法写完就算完查要有条件、有索引、有分页。答辩时被问“你数据库优化做了什么”答案不是“建了索引”四个字而是说清楚为哪条慢查询建了联合索引、执行计划里有没有走到。能说这一段的人和只会说“我建了索引”的人分数是两个档次。3.3 只建三张表的人后来都回去补课了压缩包里如果源项目只建了用户、商品、订单三张表收藏和留言多半被塞进了商品表或者用户表里。最典型的烂设计是在t_user表加一个favorite_goods字段里面逗号分隔一堆商品ID查收藏列表全靠FIND_IN_SET商品一多立刻卡顿。正确做法就是拆出t_favorite这种中间表user_id和goods_id联合唯一增删都是单行操作还能顺便统计商品被收藏次数。删除逻辑也要在这个阶段想清楚。商品可能被收藏、被留言、被下单物理删除一执行订单里关联的商品信息就变成悬空引用。我一般用is_deleted逻辑删除列表查询统一加and is_deleted0数据还在历史订单能追溯答辩问“为什么不用DELETE”也有话说。唯一要注意的是所有查询SQL都得记得带上这个条件漏一条就会把已删除数据查出来这种bug还不报错最磨人。4. 核心模块实现登录鉴权、商品上架与订单流转系统设计落到代码最见功力的三个点是登录鉴权、图片上传、下单事务。这三个点也是演示录像里必然出现的三个场景每段都值得写成像样。4.1 登录拦截器与Session状态管理二手交易系统的登录态用Session存最直接。登录成功后把userId塞进Session后续每个请求用拦截器拦一道public class LoginInterceptor implements HandlerInterceptor { // Controller方法执行前调用返回false则请求被拦截 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object userId request.getSession().getAttribute(userId); if (userId null) { // 未登录统一重定向到登录页AJAX请求可改为返回JSON response.sendRedirect(request.getContextPath() /login); return false; } return true; } }拦截器要注册进WebMvc配置并且明确放行哪些路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /goods/list, /goods/detail, /static/**, /error); } }参数说明里最容易被忽略的是/static/**不放过它CSS、JS、图片全被拦页面会变成纯HTML裸奔。放行规则是“游客能看的才放行”商品列表和详情是游客可见的下单、发布、收藏这些必须登录。管理员判断不放在拦截器里登录时查一下用户的role字段塞进Session到管理端Controller再校验这样拦截器职责单一答辩讲起来也顺。密码这一块如果下到的包是明文存储我建议花十分钟改成MD5加盐。注册时生成随机盐存盐和密文登录时取出盐重新算一遍比对绝不用明文比对数据库public static String md5WithSalt(String rawPassword, String salt) { String text salt rawPassword; return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8)); }盐的规则自己定但加盐位置要固定比如“盐密码”或“密码盐”。这里有个细节不要用固定盐每个用户注册时生成独立随机盐数据库t_user表里单独一列存。答辩追问“MD5为什么不安全、加盐解决了什么”答案集中在彩虹表固定盐的话所有用户同一密码密文相同随机盐让每个密文都不重复彩虹表直接失效。4.2 发布商品图片上传的路径才是真坑发布商品是演示录像里第一个亮眼的操作表单里一个标题、一段描述、一个价格、一张图。图片上传的代码逻辑不复杂但路径处理是重灾区PostMapping(/goods/add) public String addGoods(Goods goods, RequestParam(value file, required false) MultipartFile file, HttpSession session) { if (file ! null !file.isEmpty()) { String originalFilename file.getOriginalFilename(); // 原始文件名常含中文 String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; String saveDir uploadDir; // 来自配置 upload.dir try { file.transferTo(new File(saveDir, fileName)); goods.setImageUrl(/images/ fileName); // 数据库只存访问路径 } catch (IOException e) { throw new BusinessException(图片保存失败); } } goodsService.addGoods(goods); return redirect:/goods/my; }这里最关键的一行是goods.setImageUrl(/images/ fileName)数据库只存相对访问路径不存D:/campus-market-upload/xxx.jpg这种绝对地址。原因很实在换电脑、换系统盘符绝对路径必断而相对路径配合下面这段静态映射能稳定访问Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadDir /); }映射的意思是把URL里的/images/xxx翻译成磁盘上的D:/campus-market-upload/xxx用户看到的还是正常图片地址。文件名用UUID重造是为了避免两个用户上传同名文件互相覆盖也避免中文文件名在旧版Tomcat里编解码404。表单的multipart限制在配置里设了5MB单文件超出会抛MaxUploadSizeExceededException这个异常要单独捕获返回提示不然页面直接500。4.3 下单流程事务边界与数据一致性下单是整系统里事务最重的一个操作查商品状态、改商品状态、插订单三个动作必须要么全成功要么全失败。用Java保证数据一致性最稳的不是在代码里加锁而是数据库行锁加事务Service public class OrderServiceImpl implements OrderService { Override Transactional(rollbackFor Exception.class) public Integer createOrder(Long goodsId, Long buyerId) { // 1. 锁定商品并检查状态SELECT FOR UPDATE 会锁住这一行 Goods goods goodsDao.selectForUpdate(goodsId); if (goods null || goods.getStatus() ! GoodsStatus.ON_SALE) { throw new BusinessException(商品已下架或不存在); } // 2. 商品状态改为已售 goodsDao.updateStatus(goodsId, GoodsStatus.SOLD); // 3. 创建订单初始状态为待发货 Order order new Order(); order.setGoodsId(goodsId); order.setBuyerId(buyerId); order.setSellerId(goods.getSellerId()); order.setStatus(OrderStatus.WAIT_SEND); orderDao.insert(order); return order.getId(); } }这段代码要先理解selectForUpdate的代价它把商品行锁住第二个用户同时买同一件商品时会阻塞到前一个事务提交。这正是我们想要的——同一件商品只能被一个人买走后到的人拿到的是已被锁并且状态已变的行抛业务异常回滚。Transactional(rollbackFor Exception.class)这个写法要特意说明默认事务只回滚RuntimeException自定义的BusinessException如果不指定rollbackFor会出现订单没插进去但商品状态已改的脏数据。有一个失效坑值得背下来Transactional在同类内部方法调用时不生效比如OrderServiceImpl里另一个方法调this.createOrder事务会静默失效因为事务是基于AOP代理的自调用绕过了代理。解决方式是事务方法放到另一个Bean里调用或者自己注入自己。答辩时能主动讲出这个失效场景属于超纲加分。如果想讲得更深一点补充一个乐观锁写法update t_goods set status 1 where id #{id} and status 0这条SQL自带条件原子更新不需要select for update也能防超卖适合高并发场景。毕设项目单机演示行锁够用但如果考官问到“两个用户同时下单会怎样”你要说得清行锁和乐观锁两条路。5. 避坑指南从环境配置到状态错乱的五条血泪经验这些坑不是我一个人踩的是在帮人验收多个同类压缩包时见到的重灾区。诊断顺序也有讲究先看启动日志日志报的错最诚实再看配置文件八成是密码、时区、路径问题最后才怀疑代码。别一上来就改代码多半是冤枉了它。5.1 MySQL 8.0连接失败驱动、时区、密码加密三连坑现象项目启动报ClassNotFoundException: com.mysql.jdbc.Driver或Access denied for user root或Communications link failure。三个报错指向三个不同原因。原因驱动类名写的是5.x时代的com.mysql.jdbc.DriverMySQL 8必须用com.mysql.cj.jdbc.Driver密码加密规则是8.0默认的caching_sha2_password老客户端不认URL里没带时区参数驱动拒绝连接。解决驱动类名改成带cj的完整名URL加上serverTimezoneAsia/Shanghai如果还报密码认证问题URL追加上allowPublicKeyRetrievaltrueuseSSLfalse。这三件套一次改完基本不会再连不上。这事没有玄学就是版本换代留下的兼容缺口。5.2 图片上传成功但页面显示404现象商品发布提示成功列表页图片位置是破图直接访问图片URL返回404。原因访问路径和磁盘路径没对上。最常见的是数据库存了绝对路径D:/upload/xxx.jpg但页面请求的是相对路径或者资源映射配置没生效Spring Boot默认只映射/static、/public这些目录你自己造的/images必须显式声明。解决先打开浏览器F12看图片的完整URL确认请求路径是/images/xxx.jpg再去Config类确认addResourceHandlers已注册并且addResourceLocations结尾的斜杠不能丢——file:D:/upload和file:D:/upload/是两个效果少了斜杠映射会拼接出错。这类问题不是代码写错是映射细节差一点点。5.3 刚发布的商品在列表页搜不到详情页却能打开现象商品发布成功点详情能看回列表页死活不出现。原因列表SQL的where条件卡了状态字段但新增商品的status没被赋值。如果实体类里status默认是0而后端列表查询条件是status1或status2新商品就被挡在门外。解决先查数据库该行status实际是什么值再对着mapper里的查询条件看。发布时在Service里显式goods.setStatus(GoodsStatus.ON_SALE)别依赖数据库默认值。这个坑的特征是“详情通、列表不通”按这个特征排查五分钟定位。5.4 演示录像里的按钮你的代码里没有现象照着压缩包带的演示录像操作录像里有的功能页面上找不到或者页面布局对不上。原因录像和代码根本不是同一版本。二手压缩包经常是代码更新过、录像没重录或者代码是别人二次改过的录像还是初版录的。解决不要试图改代码去迎合录像正确做法是按你手上这份代码重新录一遍演示。重录不亏你顺手把每个功能都点了一遍既验证了代码可用又熟悉了操作路径答辩时不用临场找按钮。这一点算是我接手各种压缩包后最值的习惯——拿到包先自己录一版等于免费做了一遍全流程回归测试。5.5 家里能跑换到教室电脑就白屏现象在自己笔记本上一切正常换到答辩机器上登录页打得开一登录就报错或白屏。原因三个配置写死了——数据库密码写死成root/root但教室MySQL密码不一样图片路径写死成D:/upload但答辩机没有D盘端口8080被占用。解决把这三样全部外置到application.yml演示前改三处数据库密码改成现场实际的、上传目录改成本机存在的路径、端口改成18080这种冷门端口。还有一条后手提前拷一份MySQL初始化脚本现场万一连不上库一分钟重建。这种问题属于环境迁移不是代码bug但造成的翻车效果一样惨。6. 验证与答辩加分一条最短路自测清单和三个能问答辩的细节演示前给自己做一条最短路径回归比对着PPT念稿子有用得多。我习惯按这个顺序过一遍注册新账号、退出再登录、发布一件商品、用第二个账号浏览并下单、卖家账号确认完成、管理员账号上下架商品。注册和登录能通说明Session和数据库基本链路是好的发布能通则图片上传和路径映射没问题下单能通则事务和状态流转没崩。每一步的预期结果我都写在一张小纸片上实际输出和预期不一致时直接看对应环节的错误日志。自测过程中重点盯三个细节它们也是答辩加分点。第一个是下单防超卖同一件商品在两个浏览器同时点购买只有一个能成功靠的是selectForUpdate或者带条件的UPDATE这句话能答出来数据一致性问题就算过关。第二个是逻辑外键加逻辑删除商品被删后历史订单依然能查到商品标题因为数据没物理消失只是查列表时过滤掉了。第三个是MD5加盐能说清为什么同一密码不同用户密文不同为什么不能明文入库。验证不是走个过场。我给自己定的规矩是任何要拿去演示的项目先跑通最短路再谈优化录像从头录一遍当回归测试。这套动作在答辩当天换来的最大好处是——你心里清楚每个按钮背后跑的是什么SQL、什么状态流转而不是站在台上祈祷环境别出问题。希望帮到你。本文还有配套的精品资源点击获取