
简介这份资源是大二小学期Java程序设计实践项目的完整源码包主题为校园二手交易市场面向正在完成Java课程设计、小学期实训或毕业设计初期练手的学生帮助解决从需求梳理到功能落地的全流程参考问题。压缩包共223个文件约9.97MB其中37个java文件构成后端核心业务逻辑24个html与9个js、8个css搭建前端页面与交互样式21个xml与2个properties负责项目配置另有67张jpg、43张png及5张jpeg图片作为商品与界面素材并附系统设计.docx、使用须知.docx等说明文档目录结构清晰便于按模块阅读与二次修改。资源描述强调全部源码均已严格测试、可直接运行适合作为课程答辩演示或功能扩展的起点。目前已有62人学习关注读者可从中获取完整的项目分层思路、数据库与页面联动方式以及常见功能实现模板快速理解校园二手交易场景下的商品发布、浏览与订单管理等模块设计。1. 校园二手交易市场大二小学期 Java 实践项目到底在练什么大二小学期撞上 Java 程序设计实践很多人第一反应是去搜「java 开源多商户跨境商城源码下载」想直接扒一套现成的交差。但校园二手交易市场这个题目恰恰不是让你堆功能而是逼你把 Java 基础、面向对象、集合、JDBC、Swing 或 Spring Boot 这套链路走通一遍。它解决的核心问题是学生之间教材、自行车、耳机、考研资料的流转需要一个比微信群刷屏更靠谱的发布、检索、下单、状态跟踪工具。适合谁适合刚学完 Java 基础、能写类和方法、但还没独立做过完整增删改查项目的人。这个项目的价值不在界面多炫而在你能不能把「商品发布 → 浏览搜索 → 下单 → 订单状态变更」这条业务闭环用代码讲清楚。我见过太多人卡在环境变量配置和数据库连接上最后功能没写几个时间全耗在 java 启动失败怎么解决。所以这篇不聊虚的直接按能跑通的路径拆。2. 先定技术栈再动手Swing 桌面版还是 Spring Boot Web 版2.1 两种路线的取舍与适用场景校园二手交易市场这个题目最常见的两种实现路线是 Java Swing 桌面应用和 Spring Boot MyBatis 的 Web 应用。选哪个不是看哪个高级而是看你的小学期时间、验收方式和自身基础。Swing 路线的好处是纯 Java SE不需要配 Tomcat、不需要懂前端一个 main 方法就能跑适合只学过 java 基础、面向对象编程 java、集合和 JDBC 的同学。缺点是界面丑、布局调起来烦、老师可能觉得技术栈太老。Spring Boot 路线的好处是分层清晰、能写 REST 接口、简历上好看缺点是你得同时搞定 Maven、MySQL、MyBatis 映射、跨域和前端页面任何一环出问题都可能卡两天。我一般建议如果小学期只有两到三周且你连 java 环境变量配置详细教程都还没完整走过一遍选 Swing。如果你已经能独立写 JDBC 增删改查并且想顺便练 Spring Boot MyBatis那就选 Web 版。两条路线的业务模型是一样的区别只在表现层和数据访问层的组织方式。对比项Swing 桌面版Spring Boot Web 版技术门槛低纯 Java SE中需懂 Maven/HTTP/JSON环境依赖JDK MySQLJDK Maven MySQL界面开发Swing 组件手写HTML/Thymeleaf 或前后端分离验收演示本地直接运行需启动服务、浏览器访问适合人群基础薄弱、时间紧有一定基础、想练工程化2.2 数据库表结构的最小设计不管选哪条路线数据库表都是地基。校园二手交易市场最少需要四张表用户表、商品表、订单表、分类表。不要一上来就设计十几张表先把核心闭环跑通。下面是我常用的建表 SQL字段够用且留了扩展余地。-- 用户表区分买家和卖家role 字段控制 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), role TINYINT DEFAULT 0, -- 0普通用户 1管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 商品表status 控制上下架和售出状态 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, category_id INT, seller_id INT NOT NULL, status TINYINT DEFAULT 1, -- 1在售 2已售 3下架 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (seller_id) REFERENCES user(id) ); -- 订单表记录交易关系 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, buyer_id INT NOT NULL, seller_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1, -- 1待确认 2已完成 3已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES product(id) );建表时注意三点金额用 DECIMAL 不要用 FLOAT避免精度问题status 用 TINYINT 加注释比字符串省空间且好判断外键约束在小学期项目里可以加能帮你发现插入顺序错误。如果你用的是 MySQL 8记得连接串里加serverTimezoneAsia/Shanghai否则 create_time 可能差 8 小时这是血泪经验。2.3 项目包结构的组织方式包结构决定了你后面找代码会不会翻车。我见过有人把所有类都塞在 default package 里最后自己都找不到。推荐按职责分层Swing 版和 Web 版都适用com.campus.market ├── entity // 实体类对应数据库表 │ ├── User.java │ ├── Product.java │ └── Order.java ├── dao // 数据访问层写 JDBC 或 MyBatis 接口 │ ├── UserDao.java │ ├── ProductDao.java │ └── OrderDao.java ├── service // 业务逻辑层处理下单、状态流转 │ ├── UserService.java │ ├── ProductService.java │ └── OrderService.java ├── util // 工具类数据库连接、字符串处理 │ └── DBUtil.java └── ui // 界面层Swing 窗体或 Controller ├── LoginFrame.java └── MainFrame.javaentity 里的类字段名和数据库列名对应dao 里只写 SQL 和结果集映射service 里做业务判断比如「商品已售出就不能再下单」ui 只负责展示和收集输入。这样分层的好处是老师问你某功能怎么实现你能直接指到对应层而不是在一坨代码里翻。3. 用 JDBC 打通增删改查从 DBUtil 到商品发布3.1 DBUtil 的写法与连接参数JDBC 是 Java 连数据库的基础也是小学期最容易被环境问题卡住的地方。先写一个 DBUtil把驱动加载、连接获取、资源关闭封装起来。注意MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是老版本的com.mysql.jdbc.Driver写错会报 ClassNotFoundException。package com.campus.market.util; import java.sql.*; public class DBUtil { // 数据库连接参数按自己环境改 private static final String URL jdbc:mysql://localhost:3306/campus_market?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; // 静态块加载驱动只执行一次 static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败检查pom或lib, e); } } // 获取连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭资源顺序不能反 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }参数说明URL 里的useSSLfalse关掉 SSL 警告serverTimezone解决时区characterEncodingutf8防止中文乱码。如果你用 Mavenmysql-connector-java 的依赖要写对版本8.x 对应 MySQL 85.x 对应 MySQL 5.7。连接失败时先看三件事MySQL 服务有没有启动、端口是不是 3306、用户名密码对不对。这三步能解决八成 java 启动失败怎么解决的问题。3.2 商品发布的 DAO 与 Service 实现商品发布是卖家最核心的操作涉及插入 product 表。DAO 层只负责 SQLService 层做参数校验。下面用 PreparedStatement 防止 SQL 注入这也是面试常问的点。// ProductDao.java public boolean insert(Product p) { String sql INSERT INTO product(title, description, price, category_id, seller_id, status) VALUES(?,?,?,?,?,1); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getTitle()); ps.setString(2, p.getDescription()); ps.setBigDecimal(3, p.getPrice()); ps.setInt(4, p.getCategoryId()); ps.setInt(5, p.getSellerId()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // ProductService.java public String publish(Product p) { if (p.getTitle() null || p.getTitle().trim().isEmpty()) { return 标题不能为空; } if (p.getPrice() null || p.getPrice().compareTo(BigDecimal.ZERO) 0) { return 价格必须大于0; } boolean ok productDao.insert(p); return ok ? 发布成功 : 发布失败请重试; }逻辑说明DAO 用 try-with-resources 自动关连接比手动 close 更安全。Service 返回字符串而不是布尔值是为了界面层能直接弹提示。参数上price 用 BigDecimal 接收避免 double 精度丢失。如果你在 Web 版里用 MyBatisinsert 语句写在 XML 里useGeneratedKeystrue keyPropertyid可以回填自增主键方便后续下单引用。3.3 商品列表查询与关键词搜索买家浏览商品需要列表查询加上关键词搜索能提升实用性。这里用 LIKE 做模糊匹配注意拼接%的位置。public ListProduct search(String keyword, int categoryId) { ListProduct list new ArrayList(); StringBuilder sql new StringBuilder( SELECT p.*, u.username AS sellerName FROM product p JOIN user u ON p.seller_id u.id WHERE p.status 1); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND p.title LIKE ?); } if (categoryId 0) { sql.append( AND p.category_id ?); } sql.append( ORDER BY p.create_time DESC); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { int idx 1; if (keyword ! null !keyword.trim().isEmpty()) { ps.setString(idx, % keyword.trim() %); } if (categoryId 0) { ps.setInt(idx, categoryId); } ResultSet rs ps.executeQuery(); while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setTitle(rs.getString(title)); p.setPrice(rs.getBigDecimal(price)); p.setSellerName(rs.getString(sellerName)); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }参数说明keyword 前后加%实现包含匹配categoryId 大于 0 才拼条件避免全表扫描时多一个无用过滤。JOIN user 表是为了列表直接显示卖家用户名省一次查询。注意 ResultSet 的列名要和 SELECT 别名一致sellerName对应u.username AS sellerName。如果搜索没结果先单独在 MySQL 客户端跑一遍 SQL确认数据存在再查 Java 代码。4. 订单状态流转把「下单」这件事写对有多难4.1 下单的业务校验与事务控制下单不是简单插一条 orders 记录它至少涉及三个动作检查商品是否在售、插入订单、把商品状态改为已售。这三个动作必须在一个事务里否则可能出现「订单插入了但商品还在售」的脏数据。小学期项目里很多人忽略事务答辩时被问「两个人同时买同一件商品怎么办」就答不上来。public String createOrder(int productId, int buyerId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查商品并加行锁防止并发下单 String checkSql SELECT seller_id, price, status FROM product WHERE id ? FOR UPDATE; PreparedStatement checkPs conn.prepareStatement(checkSql); checkPs.setInt(1, productId); ResultSet rs checkPs.executeQuery(); if (!rs.next()) { conn.rollback(); return 商品不存在; } if (rs.getInt(status) ! 1) { conn.rollback(); return 商品已售出或已下架; } int sellerId rs.getInt(seller_id); BigDecimal price rs.getBigDecimal(price); if (sellerId buyerId) { conn.rollback(); return 不能购买自己的商品; } // 2. 插入订单 String orderSql INSERT INTO orders(product_id, buyer_id, seller_id, amount, status) VALUES(?,?,?,?,1); PreparedStatement orderPs conn.prepareStatement(orderSql); orderPs.setInt(1, productId); orderPs.setInt(2, buyerId); orderPs.setInt(3, sellerId); orderPs.setBigDecimal(4, price); orderPs.executeUpdate(); // 3. 更新商品状态为已售 String updateSql UPDATE product SET status 2 WHERE id ? AND status 1; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, productId); int rows updatePs.executeUpdate(); if (rows 0) { conn.rollback(); return 下单失败商品状态已变; } conn.commit(); return 下单成功; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return 系统异常下单失败; } finally { try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明FOR UPDATE给商品行加排他锁防止两个买家同时查到 status1 然后都下单。UPDATE 语句里带AND status 1是双重保险即使锁没拦住更新影响行数为 0 也能触发回滚。参数上buyerId 从登录会话里取不要从前端传否则可以伪造。事务里任何一步失败都要 rollbackfinally 里恢复 autoCommit 并关连接。4.2 订单状态查询与卖家确认订单创建后是「待确认」状态卖家确认后才算完成。查询订单列表要区分买家视角和卖家视角用不同字段过滤。// 买家查自己的订单 public ListOrder listByBuyer(int buyerId) { String sql SELECT o.*, p.title AS productTitle FROM orders o JOIN product p ON o.product_id p.id WHERE o.buyer_id ? ORDER BY o.create_time DESC; // 省略结果集映射逻辑同上 } // 卖家确认订单 public boolean confirmOrder(int orderId, int sellerId) { String sql UPDATE orders SET status 2 WHERE id ? AND seller_id ? AND status 1; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, orderId); ps.setInt(2, sellerId); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }参数说明confirmOrder 里带 sellerId 条件防止 A 卖家确认 B 卖家的订单。status1 条件保证只能从待确认变已完成不能重复确认。如果更新返回 false先查订单当前 status 和 seller_id 是否匹配。这里不需要事务因为只操作一张表。4.3 用集合做内存缓存减少数据库压力小学期项目数据量小但如果你想让代码看起来有优化意识可以用 HashMap 做简单缓存。比如分类列表这种不常变的数据启动时加载一次后面直接读内存。public class CategoryCache { private static final MapInteger, String CACHE new HashMap(); static { // 类加载时从数据库读一次 String sql SELECT id, name FROM category; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { CACHE.put(rs.getInt(id), rs.getString(name)); } } catch (SQLException e) { e.printStackTrace(); } } public static String getName(int id) { return CACHE.getOrDefault(id, 未知分类); } }逻辑说明static 块在类第一次被加载时执行之后所有调用都走内存。参数上 getOrDefault 避免 null 导致界面显示空白。注意这种缓存不适合会变的数据比如商品列表就不能这么干否则新发布的商品看不到。集合选 HashMap 是因为按 id 查找是 O(1)比 List 遍历快。这也是 java 常用库函数里 Map 的典型用法。5. 避坑与排查小学期项目最容易翻车的五个地方5.1 中文乱码从数据库到界面全链路排查现象商品标题在数据库里正常但 Java 程序读出来是问号或乱码。原因通常是三处编码不一致数据库表字符集、JDBC 连接串、Java 源文件编码。解决建库时用utf8mb4连接串加characterEncodingutf8IDEA 里 File Encoding 设为 UTF-8。如果还乱码检查 MySQL 的character_set_server变量用SHOW VARIABLES LIKE character%看。5.2 空指针异常ResultSet 取值前先判空现象程序运行时报 NullPointerException定位到某行rs.getString(xxx)。原因SQL 查询结果为空或者列名写错导致取不到值。解决先if (rs.next())再取值列名用 SELECT 里的别名不要凭记忆写实体类字段用包装类型 Integer 而不是 int避免默认值 0 掩盖问题。5.3 事务未回滚异常被吞导致脏数据现象下单时插入订单成功但商品状态没改或者反过来。原因catch 块里只打印异常没 rollback或者 autoCommit 没关。解决事务代码用 try-catch-finallycatch 里必须 rollbackfinally 里恢复 autoCommit 并关连接。测试时故意在插入订单后抛异常看商品状态有没有回滚。5.4 驱动版本不匹配ClassNotFoundException 与时区报错现象启动时报ClassNotFoundException: com.mysql.jdbc.Driver或The server time zone value is unrecognized。原因MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver且必须指定 serverTimezone。解决改驱动类名连接串加serverTimezoneAsia/Shanghai。如果用 Maven确认 mysql-connector-java 版本和 MySQL 服务版本对应。5.5 界面卡死Swing 事件线程里做数据库操作现象Swing 程序点击按钮后界面无响应过几秒才恢复。原因数据库查询在 EDT事件调度线程里执行阻塞了界面刷新。解决耗时操作放到SwingWorker的 doInBackground 里完成后在 done 方法里更新界面。Web 版不存在这个问题但要注意接口响应时间慢查询加索引。6. 让项目多拿两分用导出功能和索引优化收尾项目能跑通之后想多拿两分可以加一个「我的发布」导出 CSV 功能顺便练一下 java 排序和文件流。下面这个方法把当前用户的商品按价格降序导出用 Comparator 做排序用 BufferedWriter 写文件。public void exportMyProducts(int sellerId, String filePath) { ListProduct list productDao.listBySeller(sellerId); // 按价格降序价格相同按发布时间降序 list.sort(Comparator.comparing(Product::getPrice).reversed() .thenComparing(Product::getCreateTime, Comparator.reverseOrder())); try (BufferedWriter writer new BufferedWriter( new OutputStreamWriter(new FileOutputStream(filePath), StandardCharsets.UTF_8))) { writer.write(标题,价格,状态,发布时间); writer.newLine(); for (Product p : list) { writer.write(p.getTitle() , p.getPrice() , (p.getStatus() 1 ? 在售 : 已售) , p.getCreateTime()); writer.newLine(); } } catch (IOException e) { e.printStackTrace(); } }逻辑说明Comparator.comparing 链式调用实现多级排序reversed 降序。OutputStreamWriter 指定 UTF-8否则 Excel 打开 CSV 中文乱码。参数上 filePath 由界面层用 JFileChooser 选不要写死。这个方法同时用到了集合排序、文件 IO、字符编码答辩时能讲出东西。另一个加分点是给 product 表的 title 和 category_id 加索引搜索时能快不少。小学期数据量小感觉不出来但你可以用EXPLAIN命令给老师演示索引生效前后的区别这比嘴上说「我做了优化」有说服力。-- 给搜索字段加索引 ALTER TABLE product ADD INDEX idx_title (title); ALTER TABLE product ADD INDEX idx_category_status (category_id, status); -- 用 EXPLAIN 看是否走索引 EXPLAIN SELECT * FROM product WHERE title LIKE Java% AND status 1;注意 LIKE 以%开头会导致索引失效所以搜索时如果用户输入的是前缀可以提示「输入开头几个字更快」。这是我在实际项目里踩过的坑明明加了索引查询还是慢最后发现是%keyword%的写法让索引白加了。最后说个习惯每写完一个功能先在 MySQL 客户端把 SQL 跑通再写 Java 代码每改一次数据库连接配置先写个 main 方法测试getConnection()能不能拿到连接。这样能把环境问题和代码问题分开省下大量翻车时间。希望帮到你。本文还有配套的精品资源点击获取