
简介这是一套基于 JavaMySQL 实现校园二手交易市场的 Web 课程设计项目主要面向计算机专业学生适用于课程设计、毕业设计或 Java Web 综合实训。项目已完整实现用户注册登录、管理员对用户信息与买卖记录的管理、卖家发布下架修改商品、买家搜索排序分类加购下单以及个人中心查看买卖记录与购物车等核心功能并扩展了密码加密、邮箱通知和三段式交易确认流程业务链路完整模块划分清晰。资源包共 224 个文件包含 37 个 Java 源码文件、24 个 HTML 页面、21 个 XML 配置、1 个 SQL 脚本以及配套的 CSS/JS 前端样式、图片素材和 docx 设计文档整包约 9.89MB便于导入开发环境直接查看和运行。目前已有 120 人学习适合需要参考完整前后端实现、数据库表结构设计及购物车交易逻辑的学习者对照源码即可快速理解业务流程也可基于此做二次功能扩展。1. 校园二手交易市场这个Web项目为什么值得用JavaMySQL做毕业季的宿舍楼下最热闹书、台灯、小电扇堆在一起等人认领。事实上校园内部对二手商品流通的需求很大但想“建一个自己的平台”却大多卡在选择什么技术栈上。如果要把用户、商品、订单持久化下来MySQL是关系型数据库里最稳妥的选择要处理登录、发布、购买这些请求Java生态里有充足又成熟的Web库。基于JavaMySQL实现Web版校园二手交易市场本质上是在做一个B/S结构的C2C交易平台业务范围精确控制在校园内常见功能包括注册、登录、发布闲置、浏览商品、模拟下单和后台管理。这个项目往往出现在Java Web课程设计、毕业设计和Java初级岗位面试前的自测项目里。它不要求高并发和分布式却能完整覆盖Servlet生命周期、JDBC操作、事务处理、会话跟踪和分页查询这些基本功适合想用接近企业级套路做点实在东西的人。接下来要做的不是套用现成脚手架而是亲手把表结构、接口和状态变更串起来。2. 自下而上设计“校园二手交易市场”的MySQL表结构与Java对接先用数据库做题而不是先写代码。关键在于尽量贴近二手交易的真实形状让每张表都能回答一个业务问题。很多初学者把商品和订单揉在一张表里导致下单时大量更新冗余字段这就是设计阶段埋下的坑。这里把用户、商品、订单、关注四个对象分开并补上必要的外键关系Java代码后续会简单很多。2.1 从四个业务对象推出用户表、商品表、订单表和关注表的结构用户表存储在校生基本信息只保留必要的登录字段。password字段建议存SHA-256哈希值而不是明文user_role用于区分普通用户和管理员我一般用0表示普通学生1表示管理员2表示被禁用的用户。商品表主要字段是发布者、标题、描述、图片路径、价格、状态和浏览量。status是整个二手市场最核心的状态位0在架、1已拍下、2已卖出、3下架。订单表把买家、卖家和商品关联起来冗余了price_snapshot避免商品改价后历史订单金额跟着变。关注表是一个纯粹的中间表记录用户对商品的收藏关系不要用逗号分隔存入用户表。四张表的分工见下表表名负责的业务问题核心字段store_user谁在买卖username, password, student_no, user_roleproduct有什么闲置商品seller_id, title, price, status, view_countproduct_order一次交易如何流转product_id, buyer_id, seller_id, order_statusattention用户和商品的关注关系user_id, product_id下面是完整的建表SQL适用于MySQL 5.7及以上版本CREATE DATABASE IF NOT EXISTS campus_market DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE campus_market; CREATE TABLE store_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL COMMENT 登录名, password CHAR(64) NOT NULL COMMENT SHA256后的64位哈希, nickname VARCHAR(32) NOT NULL, student_no VARCHAR(20) NOT NULL COMMENT 学号, user_role TINYINT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB; CREATE TABLE product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, seller_id INT UNSIGNED NOT NULL, title VARCHAR(80) NOT NULL, description_text TEXT NOT NULL, image_path VARCHAR(200) DEFAULT NULL, price DECIMAL(10,2) NOT NULL COMMENT 精确到分, status TINYINT NOT NULL DEFAULT 0 COMMENT 0在架 1已拍下 2已卖出 3下架, view_count INT UNSIGNED NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_status (status), CONSTRAINT fk_product_seller FOREIGN KEY (seller_id) REFERENCES store_user (id) ) ENGINEInnoDB; CREATE TABLE product_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, product_id INT UNSIGNED NOT NULL, buyer_id INT UNSIGNED NOT NULL, seller_id INT UNSIGNED NOT NULL, price_snapshot DECIMAL(10,2) NOT NULL, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1已付款 2已完成 3已取消, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_product (product_id), KEY idx_buyer (buyer_id), KEY idx_seller_snap (seller_id), CONSTRAINT fk_order_product FOREIGN KEY (product_id) REFERENCES product (id), CONSTRAINT fk_order_buyer FOREIGN KEY (buyer_id) REFERENCES store_user (id), CONSTRAINT fk_order_seller FOREIGN KEY (seller_id) REFERENCES store_user (id) ) ENGINEInnoDB; CREATE TABLE attention ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id), CONSTRAINT fk_att_user FOREIGN KEY (user_id) REFERENCES store_user (id), CONSTRAINT fk_att_product FOREIGN KEY (product_id) REFERENCES product (id) ) ENGINEInnoDB;建表时有四个细节值得展开。价格用DECIMAL(10,2)不推荐float或double因为二进制浮点数处理0.29这种金额时会出现累加误差二手交易就是小额多次精确到分是最低要求。订单表里的price_snapshot是冗余字段看起来违反了单一数据源原则但它承担交易快照作用商品即使后来改价订单历史金额也不能变结算时靠这张快照。product_order.product_id加了唯一键限制同一商品只能有一条有效订单这是业务上对“一物一单”的强制约束否则同一买家重复下单会出现库存超卖。除此之外product表自身保存商品状态状态判定要交给Java逻辑数据库只提供约束。商品状态与订单状态必须同步变化具体怎么做第4章的状态机会专门讲。2.2 索引与外键如何取舍本项目为什么不设计级联删除上面的建表语句中已经加了普通索引和外键。外键在互联网高并发场景常被嫌弃但对课程设计和校园内部系统外键能保证数据一致优于在Java层手动检查。如果后续要拆微服务再把这几个外键用触发器或应用逻辑替换。我刻意没有写ON DELETE CASCADE。因为用户如果被删商品和订单会变成无主数据。常见的做法是把用户禁用user_role改成2保留所有历史记录商品下架也是把status改成3而不是物理删除。这样attention和product_order不会引用到不存在的主数据后台统计也始终有完整历史。很多答辩老师会问这个地方回答了其实就是加分项。索引数量上只给外键列和状态列建了普通索引。0C在百万级数据以内过多索引反而拖慢写入。UNIQUE KEY既是约束也是索引uk_user_product直接覆盖了用户关注查询的联合索引需求不需要再加冗余单列索引。2.3 Java端连MySQL驱动、连接参数和HikariCP初始配置做Java Web项目时不一定要立刻引入Spring Boot。用JDBC加一个轻量连接池就能把工程跑起来而且对理解底层有好处。首先在pom.xml中加入依赖dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version8.4.0/version /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.1.0/version /dependencyHikariCP是连接池不是ORM。用它主要是避免每次请求都新建物理连接否则并发数翻上去三次握手和连接释放的开销就会拖垮Tomcat。我习惯写一个很小的数据库工具类只暴露DataSourcepackage com.campus.market.util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; public class DbUtil { private static DataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/campus_market?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(20); config.setMinimumIdle(5); dataSource new HikariDataSource(config); } public static DataSource getDataSource() { return dataSource; } }JDBC URL中的serverTimezone不能省略。MySQL 8的驱动对时区识别很严格不写会报“The server time zone value ... is unrecognized”一类的错误。useSSLfalse是因为校园局域网内传数据不需要TLS加密allowPublicKeyRetrievaltrue解决MySQL 8使用caching_sha2_password时首次连接拿不到RSA公钥的问题。characterEncodingutf8负责通知驱动以UTF-8编码发送SQL和参数与表级的utf8mb4不冲突前者管传输后者管存储。连接池参数里minimumIdle5表示常驻连接保留5条maximumPoolSize20是上限。通常这两个值不需要调太大HikariCP的设计是宁可等待获得连接也不维护大量空闲连接。这样对Tomcat和MySQL的压力都比较友好。3. 跑通校园二手交易市场的最小Web工程从商品列表页开始接线有数据库还不够要让浏览器真正看到商品卡片才算“Web版”。这里先用一个最小但完整的Maven工程把商品列表跑起来。先不追求复杂业务把Controller、Service、DAO和JSP这条链路接通。3.1 分层分包在Java Web工程里把Servlet和JDBC分开我一般把工程拆成Controller、Service、DAO三层实体类和工具类单独建包。代码目录结构如下campus-web/ ├── pom.xml └── src/main/ ├── java/com/campus/market/ │ ├── controller/ # Servlet类 │ ├── service/ # 业务逻辑比如发布和下单 │ ├── dao/ # ProductDao, UserDao │ ├── entity/ # Product, User, Order │ ├── filter/ # 登录过滤器 │ └── util/ # DbUtil, StringUtil └── webapp/ ├── WEB-INF/ │ └── web.xml ├── jsp/ │ ├── product_list.jsp │ └── login.jsp └── static/ # css/js/images各层职责如下表层目录主要职责Controllercontroller接收HTTP请求转发参数跳转页面Serviceservice处理业务规则管理事务边界DAOdao执行SQL把ResultSet映射成实体Entityentity定义与表对应的POJOController不写SQLService不出现HTML标签DAO不处理业务判断。这个拆法不是给新手增加负担而是后续加功能时不至于在JSP里找Java代码。许多二手系统到后期改需求频繁如果代码都堆在一个Servlet里加一个字段就要翻好久。3.2 使用Servlet注解暴露商品列表接口Servlet 3.0以后web.xml可以非常精简甚至不写。用WebServlet注解把路径和类绑定起来。pom.xml需要引入Servlet API和JSTL依赖dependencies dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version5.0.0/version scopeprovided/scope /dependency dependency groupIdjakarta.servlet.jsp.jstl/groupId artifactIdjakarta.servlet.jsp.jstl-api/artifactId version2.0.0/version /dependency /dependencies这里用到的是Tomcat 10所以Servlet包名是jakarta.servlet。如果是Tomcat 9要把依赖换成javax.servlet-api 4.0.1。商品列表是项目首页最核心的Servlet如下WebServlet(name ProductListServlet, urlPatterns {/product/list}) public class ProductListServlet extends HttpServlet { private ProductService productService new ProductService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int page 1; int size 12; String pageParam request.getParameter(page); if (pageParam ! null) { page Integer.parseInt(pageParam); } String keyword request.getParameter(keyword); PageResultProduct result productService.queryOnSale(page, size, keyword); request.setAttribute(pageResult, result); request.getRequestDispatcher(/jsp/product_list.jsp).forward(request, response); } }这个Servlet把请求参数page和keyword转换成service入参不关心数据库如何查询。page默认值是1size固定12校园二手市场一个学院页面在移动端一屏放12张卡片已经够用。注意Integer.parseInt如果遇到非法参数会抛NumberFormatException我通常会加一个安全转换函数解析失败就回落默认值。这个小细节是区分“能跑的示例”和“能上线的工程”的地方。3.3 JSP用EL和JSTL渲染商品列表分页参数如何传product_list.jsp的职责是循环输出商品数据。下面的片段保留了核心结构省略了大部分CSS% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetUTF-8 title校园二手交易市场/title /head body h1正在出售/h1 div classproduct-grid c:forEach items${pageResult.records} varp div classcard img src${p.imagePath} alt${p.title} div classtitlea href/product/detail?id${p.id}${p.title}/a/div div classprice${p.price}/div div classseller${p.nickname}/div /div /c:forEach /div div classpagination c:if test${pageResult.currentPage 1} a href/product/list?page${pageResult.currentPage - 1}上一页/a /c:if span第 ${pageResult.currentPage} / ${pageResult.totalPage} 页/span c:if test${pageResult.currentPage pageResult.totalPage} a href/product/list?page${pageResult.currentPage 1}下一页/a /c:if /div /body /htmlPageResult里至少要有records、currentPage、totalPage、totalCount四个字段。Servlet把PageResult放进request作用域后JSP通过EL表达式读取属性。这里的循环用JSTL而不是%%是为了保持模板干净也避免脚本变量污染。商品图片image_path在表里存的是相对路径比如/upload/2024/10/01/xxx.jpg。直接把这个路径写给img src后Tomcat默认不会暴露磁盘任意目录所以需要把图片上传到webapp/upload目录或额外配置静态资源映射。若是独立磁盘目录要写一个Servlet或一个ResourceHandler把/upload/*指向绝对路径。完成这一步后访问http://localhost:8080/product/listTomcat会依据注解定位ProductListServletServlet查询数据库后forward到JSPJSP渲染成HTML响应到浏览器。这条链路就是整个Web项目的骨架。4. 校园二手交易市场核心功能登录校验、商品发布与订单状态机的细节实现只有列表页还不算交易市场。以下是这个项目真正的难点也是面试常问的点登录后的会话保持、发布商品时的脏数据控制、订单状态如何不发生逻辑错乱。4.1 登录后的会话保持使用HttpSession还是Cookie在Java Web里保存登录状态最常用的手段是HttpSession。登录成功时把用户对象放入sessionUser user userService.login(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /product/list); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/jsp/login.jsp).forward(request, response); }后续请求都能通过request.getSession(false)取到用户。session超时时间可以在web.xml配置session-config session-timeout30/session-timeout /session-config单位是分钟。校园项目不需要做SSO直接用HttpSession最简单。session-timeout不要设成30天因为二手交易涉及财物交接电脑忘在图书馆时别人可能继续操作你的卖家账号。我一般设成15到30分钟。代价是发布商品时如果用户写描述时间太长提交时可能session过期处理办法是表达复杂内容时前端定时向服务器发一个轻量心跳请求把session续期。只用Session还不够所有需要登录的路径都要做校验重复代码积少成多。写一个LoginFilter统一拦截/admin路径WebFilter(urlPatterns {/user/*, /admin/*}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); User user (session null) ? null : (User) session.getAttribute(currentUser); if (user null) { String currentUrl req.getRequestURL().toString(); resp.sendRedirect(req.getContextPath() /login?redirect URLEncoder.encode(currentUrl, UTF-8)); return; } chain.doFilter(request, response); } }这个过滤器把未登录用户重定向到登录页并携带redirect参数。登录成功后Servlet读取redirect参数再把请求送回原页面。这样每个Servlet内部就不用重复判断登录状态权限边界也很清晰。4.2 发布商品接口的防脏数据转义、字段校验和令牌防重发布商品是数据最脏的入口。用户可能从Word里复制内容带出各种特殊符号甚至包含