☰
JavaWeb校园二手交易系统:基于状态机的完整业务闭环实现
2026/10/18 9:18:39 网站建设 项目流程

简介:这是一套完整的基于JavaWeb技术栈开发的校园二手交易网站源码,面向计算机专业本科生、毕设开发者及JavaWeb初学者,旨在解决高校学生闲置物品流通难、交易缺乏可信平台的问题。资源包含786个文件,以210个JavaScript脚本(实现前端交互)、74个HTML页面(构成用户界面)、36个JSP动态页面(承载业务逻辑)、28个CSS样式文件(统一视觉风格)及73个JPG/PNG图片资源(含界面图标与示例图)为主干,辅以MySQL数据库支持,整体压缩包仅14.22MB,轻量易部署。已有71人学习下载,适合快速上手项目实践、理解MVC分层结构与前后端协同流程。读者可直接运行系统,完整体验学生注册登录、二手商品发布/搜索/下单、管理员后台分类管理、用户与商品审核、助手协同运维等核心功能,并通过预览中涉及的FCKeditor富文本编辑、ASP/CFM等多类型模板文件,了解早期Web系统兼容性设计思路。

1. 校园二手交易网不是“小商城”,而是学生真实行为闭环的最小可行系统

你见过毕设答辩现场被问倒最多的点是什么?不是登录校验怎么写,也不是 MySQL 索引怎么建——而是“你这个‘二手交易’,到底完成了几笔真实订单?”很多同学用 Spring Boot 搭个 CRUD 页面,商品列表一刷就出来,购物车一点就跳转,但后台没有订单状态流转、没有买家确认收货动作、没有卖家发货标记、甚至没有“已下架”和“已售罄”的状态隔离。结果导师一句:“这系统能跑通一笔从发布→浏览→下单→确认→完成的全流程吗?”当场卡壳。
这套基于 JavaWeb 的校园二手交易网源码,恰恰是为解决这个“伪闭环”问题而生:它不追求高并发或微服务架构,但强制落地了学生发布物品 → 同学搜索浏览 → 加入购物车 → 提交订单 → 卖家后台确认 → 订单完成六步不可跳过的业务链路;所有状态变更都写进数据库字段(如item_status、order_status),所有关键操作都有日志留痕(如user_id,operate_time,operate_type);连管理员回复留言都做了“已读/未读”标记。它适合计算机专业做毕设或课设的同学——不是拿来改个包名就交差,而是能带着完整流程图、状态机图、三张核心数据表结构、以及至少两笔模拟订单的截图,稳稳站在答辩台前说:“老师,我这个系统,真能跑通一次二手交易。”


2. 总体架构设计:B/S 分层结构与五大核心模块的职责边界

这套系统采用经典 JavaWeb 三层架构(表现层 JSP + 控制层 Servlet + 数据访问层 JDBC),不引入 Spring 或 MyBatis,降低学习门槛的同时,把每个环节的控制权牢牢握在开发者手里。整个系统划分为五个逻辑清晰、职责分明的模块:用户认证模块、物品管理模块、订单交易模块、消息互动模块、后台管理模块。每个模块对应一组独立的 Servlet 类、JSP 页面和数据库表,彼此通过标准 HTTP 请求参数和 session 属性通信,避免过度耦合。这种设计不是为了炫技,而是为了让答辩时你能指着代码说清楚:“登录验证走的是LoginServlet,它只管比对账号密码并设置 session;而发布物品由PublishItemServlet处理,它只负责接收表单、校验图片路径、插入item表——中间不掺和权限判断,也不处理订单逻辑。”

2.1 用户认证模块:Session 驱动的身份识别与角色分流

身份识别不靠 token,不靠 cookie 加密,就靠最朴素的HttpSession。学生登录成功后,LoginServlet将用户基本信息(user_id,username,role_type)存入 session,并重定向至首页。后续所有页面(如index.jsp、mycenter.jsp)顶部统一嵌入一段校验脚本:

<% Object userObj = session.getAttribute("user"); if (userObj == null) { response.sendRedirect("login.jsp"); return; } User user = (User) userObj; String role = user.getRole(); // "student" or "admin" %>

提示:role字段值来自数据库user表的role_type列,取值为字符串"student"或"admin",绝不使用数字 0/1。这是为后期扩展“助手角色”预留语义空间——比如未来加个assistant角色,只需改数据库值,不用动所有if(role == 1)的判断逻辑。

角色分流体现在导航栏动态渲染上。header.jsp中根据role值决定显示哪些菜单项:

<% if ("admin".equals(role)) { %> <a href="admin/item_list.jsp">商品管理</a> <a href="admin/user_list.jsp">用户管理</a> <a href="admin/message_list.jsp">留言管理</a> <% } else if ("student".equals(role)) { %> <a href="student/publish_item.jsp">发布物品</a> <a href="student/my_orders.jsp">我的订单</a> <a href="student/leave_message.jsp">给管理员留言</a> <% } %>

这种写法看似原始,但胜在可调试性强:你可以在任意 JSP 页面加<%= session.getAttribute("user") %>直接打印当前登录态,无需启动 Redis 或查 token 解析日志。对毕设场景而言,稳定压倒一切。

2.2 物品管理模块:带状态机的全生命周期控制

物品不是静态展示对象,而是有明确生命周期的状态实体。系统定义item表中status字段为 ENUM 类型,取值仅限:'on_sale'(在售)、'sold_out'(已售罄)、'offline'(已下架)、'deleted'(逻辑删除)。注意:没有'pending_review'或'audit_pass'这类审核状态——校园场景下,学生发布即可见,管理员仅做事后监管,符合“轻运营、重流通”的定位。

发布流程强制校验三项:

  • 图片路径必须以/upload/开头(防止路径穿越);
  • 价格必须为正数且保留两位小数(前端用input type="number"+ 后端BigDecimal解析);
  • 所属分类必须存在于category表(外键约束 + 下拉框select绑定)。

关键代码片段(PublishItemServlet.java):

// 获取分类ID并校验是否存在 String categoryIdStr = request.getParameter("category_id"); int categoryId = Integer.parseInt(categoryIdStr); CategoryDao categoryDao = new CategoryDao(); if (!categoryDao.existsById(categoryId)) { request.setAttribute("error", "所选分类不存在,请刷新页面重试"); request.getRequestDispatcher("publish_item.jsp").forward(request, response); return; } // 构建Item对象,status默认设为'on_sale' Item item = new Item(); item.setCategoryId(categoryId); item.setTitle(request.getParameter("title")); item.setDescription(request.getParameter("description")); item.setPrice(new BigDecimal(request.getParameter("price"))); item.setPicPath(request.getParameter("pic_path")); // 前端JS已拼好/upload/xxx.jpg item.setStatus("on_sale"); // 强制初始化为在售 item.setPublisherId(((User)session.getAttribute("user")).getId()); item.setCreateTime(new Date()); new ItemDao().insert(item); // 插入item表

注意:pic_path不由后端生成,而是前端 JS 在文件上传成功后,将返回的服务器路径(如/upload/20240512_abc123.jpg)填入隐藏域。这样既规避了 Servlet 文件上传的复杂解析,又确保路径绝对可控——你永远不需要在 Java 里调MultipartRequest或Apache Commons FileUpload。

2.3 订单交易模块:四状态驱动的真实履约链路

订单不是“提交即完成”,而是严格遵循created → confirmed_by_seller → delivered → completed四阶段状态机。order表中status字段定义为 VARCHAR(20),取值固定为上述四个字符串之一,不允许出现'paid'、'shipped'等电商术语——校园二手本质是线下交付,不存在支付网关和物流单号。

学生提交订单时(SubmitOrderServlet),系统仅做三件事:

  1. 校验所选物品status = 'on_sale';
  2. 扣减该物品库存(item.stock_count字段,初始为 1,因校园二手多为单件物品);
  3. 插入新订单,status = 'created',并记录buyer_id和item_id。

卖家(即物品发布者)登录后,在my_sold_items.jsp页面看到待处理订单列表,点击“确认交易”按钮,触发ConfirmOrderServlet:

// 根据order_id查询订单 Order order = new OrderDao().findById(orderId); Item item = new ItemDao().findById(order.getItemId()); // 仅当订单状态为'created'且物品状态为'on_sale'时才允许确认 if (!"created".equals(order.getStatus()) || !"on_sale".equals(item.getStatus())) { throw new RuntimeException("订单状态异常,无法确认"); } // 更新订单状态 order.setStatus("confirmed_by_seller"); new OrderDao().updateStatus(orderId, "confirmed_by_seller"); // 同步更新物品状态为'sold_out' item.setStatus("sold_out"); new ItemDao().updateStatus(item.getId(), "sold_out");

这个设计的关键在于:状态变更必须成对发生。确认订单的同时,物品必须下架——否则会出现“同一物品被两个买家同时下单”的竞态问题。虽然没用分布式锁,但通过数据库行级锁(InnoDB 默认)+ 显式事务包裹(Connection.setAutoCommit(false)),在单库场景下完全够用。


3. 数据库设计:三张核心表与外键约束的务实落地

系统共 7 张表,但真正承载业务主干的是user、item、order这三张。它们之间通过外键强关联,且所有外键均启用ON UPDATE CASCADE,杜绝因 ID 变更导致的数据断裂。设计原则就一条:宁可多查一次表,也不冗余存储字段。比如订单表不存商品名称、价格,只存item_id;用户表不存角色名称,只存role_type字符串。

3.1user表:角色字段语义化 + 注册时间自动填充

CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, -- 存BCrypt加密后的密文 `real_name` VARCHAR(30), `phone` VARCHAR(20), `email` VARCHAR(100), `role_type` ENUM('student', 'admin', 'assistant') DEFAULT 'student', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `last_login_time` DATETIME NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意:role_type使用ENUM而非TINYINT,是为了让 SQL 查询和代码逻辑一眼可读。例如SELECT * FROM user WHERE role_type = 'admin'比WHERE role_type = 1更不易出错。last_login_time允许为 NULL,首次登录后由LoginServlet更新,方便统计活跃用户。

3.2item表:状态字段驱动前端展示逻辑

CREATE TABLE `item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `category_id` INT NOT NULL, `publisher_id` INT NOT NULL, `title` VARCHAR(100) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL, `pic_path` VARCHAR(200) NOT NULL, `stock_count` INT DEFAULT 1, `status` ENUM('on_sale', 'sold_out', 'offline', 'deleted') DEFAULT 'on_sale', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE, FOREIGN KEY (`publisher_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键设计点:

  • stock_count默认为 1,符合校园二手“单件寄售”特性;若需支持多件,只需在发布页加数量输入框,后端校验非负整数即可;
  • status的ENUM值顺序按业务流转排列(on_sale→sold_out→offline),方便ORDER BY FIELD(status, ...)排序;
  • update_time自动更新,用于前台“最新上架”排序(ORDER BY update_time DESC LIMIT 10)。

3.3order表:状态机字段 + 关联信息懒加载

CREATE TABLE `order` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `item_id` INT NOT NULL, `buyer_id` INT NOT NULL, `seller_id` INT NOT NULL, `status` ENUM('created', 'confirmed_by_seller', 'delivered', 'completed') DEFAULT 'created', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `confirm_time` DATETIME NULL, `complete_time` DATETIME NULL, FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE CASCADE, FOREIGN KEY (`buyer_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE, FOREIGN KEY (`seller_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么不在订单表存商品名称和价格?因为:

  • 商品可能被修改(如降价),但历史订单价格必须锁定;
  • 若冗余存储,每次改商品名都要遍历所有相关订单更新,违背单一职责;
  • 实际查询时,JOIN item ON order.item_id = item.id一次搞定,MySQL 优化器对这种简单关联极其高效。

4. 关键功能实现:从发布物品到订单完成的四段真实代码链

这里不贴“Hello World”式伪代码,只呈现四段在真实运行环境中被反复验证过的核心逻辑。每段代码都来自项目源码中的.java文件,经过脱敏但保留全部业务语义。你可以直接复制进自己的 Eclipse 工程,稍作路径调整即可运行。

4.1 学生发布物品:前端表单 + 后端双重校验

前端publish_item.jsp中,图片上传区域使用标准<input type="file">,配合一个隐藏域pic_path:

<form action="servlet/PublishItemServlet" method="post"> <input type="text" name="title" required> <textarea name="description"></textarea> <input type="number" name="price" step="0.01" min="0.01" required> <select name="category_id" required> <!-- 由CategoryDao.findAll()动态生成option --> </select> <input type="file" id="picFile" accept="image/*"> <input type="hidden" name="pic_path" id="picPathInput"> <button type="submit">发布</button> </form> <script> document.getElementById('picFile').onchange = function(e) { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); fetch('servlet/UploadImageServlet', { method: 'POST', body: formData }).then(r => r.json()).then(data => { document.getElementById('picPathInput').value = data.path; // 如 /upload/20240512_xxx.jpg }); }; </script>

后端UploadImageServlet.java仅做安全校验和保存:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Part filePart = request.getPart("file"); String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); // 严格校验文件类型(仅允许jpg/jpeg/png) String contentType = filePart.getContentType(); if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType)) { response.getWriter().write("{\"error\":\"仅支持 JPG/PNG 格式\"}"); return; } // 生成唯一文件名:时间戳+随机数+原后缀 String ext = fileName.substring(fileName.lastIndexOf('.')); String newFileName = System.currentTimeMillis() + "_" + RandomStringUtils.randomAlphanumeric(6) + ext; String uploadPath = request.getServletContext().getRealPath("/upload"); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); filePart.write(uploadPath + File.separator + newFileName); // 返回JSON,前端用path字段填入隐藏域 response.setContentType("application/json"); response.getWriter().write(String.format("{\"path\":\"/upload/%s\"}", newFileName)); }

这段代码的价值在于:它把“文件上传”这个高频翻车点,压缩成一个可独立测试的单元。你可以在浏览器直接 POST 一个图片到/servlet/UploadImageServlet,看是否返回/upload/xxx.jpg—— 成功了,再联调发布逻辑;失败了,问题一定在路径、权限或 MIME 类型上,不会污染主业务流。

4.2 管理员回复留言:异步更新 + 已读标记

管理员在admin/message_list.jsp查看留言列表,每条记录旁有“回复”按钮和“已读”标签:

<c:forEach items="${messageList}" var="msg"> <tr> <td>${msg.id}</td> <td>${msg.content}</td> <td><c:if test="${msg.isRead == 1}">✓ 已读</c:if></td> <td> <a href="javascript:void(0)" onclick="showReplyForm(${msg.id})">回复</a> </td> </tr> </c:forEach>

点击“回复”弹出表单,提交至ReplyMessageServlet:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int messageId = Integer.parseInt(request.getParameter("message_id")); String replyContent = request.getParameter("reply_content"); // 1. 先标记为已读(即使还没回复) new MessageDao().markAsRead(messageId); // 2. 插入回复记录到 reply 表 Reply reply = new Reply(); reply.setMessageId(messageId); reply.setContent(replyContent); reply.setAdminId(((User)request.getSession().getAttribute("user")).getId()); reply.setCreateTime(new Date()); new ReplyDao().insert(reply); response.sendRedirect("message_list.jsp?success=1"); }

MessageDao.markAsRead()执行 SQL:UPDATE message SET is_read = 1 WHERE id = ?。这个“先标记后回复”的设计,解决了管理员打开列表却忘记标记的遗漏问题——只要点了回复,就代表他已看过这条留言。

4.3 学生查看我的订单:状态驱动的卡片式布局

student/my_orders.jsp不用复杂表格,而是用 Bootstrap 卡片按状态分组:

<div class="row"> <div class="col-md-4"> <h5>待卖家确认</h5> <c:forEach items="${createdOrders}" var="order"> <div class="card mb-3"> <div class="card-body"> <h6 class="card-title">${order.itemTitle}</h6> <p class="card-text">¥${order.price} | ${order.createTime}</p> <span class="badge bg-warning">等待确认</span> </div> </div> </c:forEach> </div> <div class="col-md-4"> <h5>已确认交易</h5> <c:forEach items="${confirmedOrders}" var="order"> <div class="card mb-3"> <div class="card-body"> <h6 class="card-title">${order.itemTitle}</h6> <p class="card-text">卖家:${order.sellerName}</p> <span class="badge bg-success">已确认</span> </div> </div> </c:forEach> </div> </div>

对应的MyOrdersServlet.java中,DAO 层按状态分页查询:

// 查询学生所有订单,并按status分组 List<Order> allOrders = new OrderDao().findByBuyerId(buyerId); Map<String, List<Order>> grouped = allOrders.stream() .collect(Collectors.groupingBy(Order::getStatus)); request.setAttribute("createdOrders", grouped.getOrDefault("created", Collections.emptyList())); request.setAttribute("confirmedOrders", grouped.getOrDefault("confirmed_by_seller", Collections.emptyList())); request.setAttribute("completedOrders", grouped.getOrDefault("completed", Collections.emptyList()));

这种写法让前端极度轻量,后端逻辑也一目了然:状态就是分组依据,无需额外字段。

4.4 后台商品管理:分类树形结构 + 状态筛选联动

admin/item_list.jsp顶部有两个筛选控件:分类下拉框和状态单选按钮。它们联动过滤商品列表:

<form id="filterForm"> <select name="category_id" onchange="this.form.submit()"> <option value="">全部分类</option> <c:forEach items="${categoryList}" var="cat"> <option value="${cat.id}" ${param.category_id == cat.id ? 'selected' : ''}> ${cat.name} </option> </c:forEach> </select> <select name="status" onchange="this.form.submit()"> <option value="">全部状态</option> <option value="on_sale" ${param.status == 'on_sale' ? 'selected' : ''}>在售</option> <option value="sold_out" ${param.status == 'sold_out' ? 'selected' : ''}>已售罄</option> <option value="offline" ${param.status == 'offline' ? 'selected' : ''}>已下架</option> </select> </form>

ItemDao.findByCondition()方法接收categoryId和status参数,构建动态 SQL:

public List<Item> findByCondition(Integer categoryId, String status) { StringBuilder sql = new StringBuilder("SELECT * FROM item WHERE 1=1"); List<Object> params = new ArrayList<>(); if (categoryId != null && categoryId > 0) { sql.append(" AND category_id = ?"); params.add(categoryId); } if (status != null && !status.trim().isEmpty()) { sql.append(" AND status = ?"); params.add(status); } sql.append(" ORDER BY update_time DESC"); return query(sql.toString(), params.toArray(), Item.class); }

这段 SQL 构建逻辑是毕设答辩高频考点。导师常问:“如果用户不选分类、只选状态,SQL 是什么?”——答案是SELECT * FROM item WHERE 1=1 AND status = ? ORDER BY ...,1=1保证后续AND语法合法。这种写法比拼接字符串更安全,也比 MyBatis 的<if>标签更贴近底层,适合教学演示。


5. 避坑指南:JavaWeb 二手交易网开发中最容易翻车的 4 个点

做毕设最怕什么?不是功能写不出来,而是写完了跑不起来,或者跑起来了但导师一问就露馅。下面这四点,是我带过十几届学生、看过上百份毕设代码后,总结出的最高频“翻车现场”。每一条都附带真实现象、根本原因和可立即执行的解决方案,照着做,能省下至少三天调试时间。

5.1 现象:登录后跳转首页,但导航栏不显示“我的中心”,F12 看 Network 发现 session 为空

原因:web.xml中未配置<session-config>,导致 Tomcat 默认 session 超时时间为 30 分钟,而开发调试时经常隔几分钟才点一下页面,session 已失效;更隐蔽的是,某些浏览器(如 Safari)在跨域或 iframe 场景下会拒绝发送 Cookie,导致JSESSIONID无法回传。
解决:

  1. 在web.xml顶部显式声明 session 超时为 60 分钟:
<session-config> <session-timeout>60</session-timeout> </session-config>
  1. 所有跳转链接统一用response.sendRedirect(request.getContextPath() + "/xxx.jsp"),绝不用相对路径(如response.sendRedirect("index.jsp")),避免因部署路径(ROOT 或 /campus)不同导致重定向失败;
  2. 在LoginServlet登录成功后,手动调用session.setMaxInactiveInterval(3600)(单位秒),双重保险。

5.2 现象:上传图片后页面显示 404,检查发现/upload/xxx.jpg路径下文件存在,但浏览器打不开

原因:Tomcat 默认不提供upload目录的静态资源服务。/upload/是 Web 应用目录下的子文件夹,但 Tomcat 只认webapp下的images/、css/等标准路径,对自定义文件夹默认 404。
解决:
在web.xml中添加 servlet-mapping,将/upload/*请求交给DefaultServlet处理:

<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/upload/*</url-pattern> </servlet-mapping>

这是 JavaWeb 项目的“后悔药”。没有这行配置,你永远无法在浏览器直接访问上传的图片;加上它,http://localhost:8080/campus/upload/20240512_xxx.jpg就能正常显示。别信网上那些“改 Tomcat conf/server.xml”的方案,太重,且毕业答辩环境未必允许你动服务器配置。

5.3 现象:管理员修改商品价格后,学生端订单详情页仍显示旧价格

原因:订单详情页(order_detail.jsp)直接从order表查数据,而order表里根本没有price字段,它只存item_id;但 DAO 层OrderDao.findById()方法错误地写了SELECT o.*, i.price FROM order o JOIN item i ON o.item_id = i.id,导致查出来的 price 是当前 item 表的最新值,而非下单时的快照。
解决:

  1. 立刻删掉所有JOIN item的订单查询;
  2. 订单详情页改为两次查询:先查order表获取item_id,再用item_id单独查item表——这样你就能在 DAO 层ItemDao.findById()方法里,加一行日志:“查询商品ID=123的价格”,方便调试;
  3. 如果真要存快照,就在order表加snapshot_price DECIMAL(10,2)字段,在SubmitOrderServlet插入订单时,从item表查一次价格并写死进去,而不是依赖 JOIN。

5.4 现象:MySQL 中文乱码,数据库显示????,但 Navicat 里能看到正确汉字

原因:不是数据库编码问题,而是 JDBC 连接 URL 缺少useUnicode=true&characterEncoding=UTF-8参数。MySQL 服务端编码(utf8mb4)和客户端连接编码(默认 latin1)不一致,导致 Java 程序写入时被转码丢失。
解决:
检查db.properties或JDBCUtil.java中的连接字符串,必须包含:

jdbc.url=jdbc:mysql://localhost:3306/campus?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

注意:serverTimezone=Asia/Shanghai也必须加上,否则new Date()写入时间可能比实际晚 8 小时。这个参数和字符集一样,是 JavaWeb 项目上线前必检项,漏掉一个,整个系统中文和时间全乱。


6. 答辩验收 checklist:用这 7 个动作证明你的系统真的能跑通

答辩不是讲 PPT,是现场演示。导师最想看到的,是你能用鼠标点出一套完整的业务闭环。我带过的某高校毕设答辩要求:学生必须在 5 分钟内,完成从注册、发布、搜索、下单到后台确认的全流程演示。做不到?直接扣分。所以,别等答辩当天才练,现在就按这份 checklist 过一遍。每一项都对应一个可截图、可复述、可解释的技术点,做完你就知道哪里还缺火候。

6.1 动作一:登录两个账号,分别扮演学生和管理员

打开两个浏览器窗口(Chrome 和 Firefox,避免 session 冲突):

  • 窗口 A:用student1/password1登录,进入学生首页;
  • 窗口 B:用admin/admin123登录,进入后台首页。

验证点:窗口 A 的导航栏应显示“发布物品”“我的订单”,窗口 B 应显示“商品管理”“用户管理”。如果窗口 A 出现“商品管理”菜单,说明role_type字段值写错了(比如存成了1而非"student"),立刻查user表。

6.2 动作二:学生发布一件物品,并在首页搜索到它

在窗口 A,进入“发布物品”页:

  • 填写标题“二手《数据结构》教材(第三版)”;
  • 价格填25.00;
  • 选择分类“教材”(确保category表里有这条记录);
  • 上传一张本地图片(建议用 100x100 像素的测试图,上传快);
  • 点击发布,跳转回首页。
    然后在首页搜索框输入“数据结构”,回车。

验证点:搜索结果页必须出现刚发布的那本书,且图片能正常显示(检查浏览器地址栏 URL 是否为/upload/xxx.jpg)。如果图片 404,立刻回去检查web.xml的<servlet-mapping>是否生效。

6.3 动作三:学生下单,后台立即看到新订单

在窗口 A,找到刚发布的书,点击“立即购买” → “提交订单”。
切换到窗口 B(管理员),点击左侧菜单“我的出售”或“订单管理”。

验证点:订单列表中必须出现一条新记录,status为created,buyer_name显示学生用户名。如果列表为空,立刻去数据库查order表——有没有新记录?如果没有,说明SubmitOrderServlet的 insert 语句没执行;如果有但 status 不对,说明ItemDao.updateStatus()把物品状态改错了。

6.4 动作四:管理员确认订单,学生端订单状态实时更新

在窗口 B,找到那条created状态的订单,点击“确认交易”。
立刻切回窗口 A,刷新“我的订单”页。

验证点:“待卖家确认”分组里那本书应该消失,同时“已确认交易”分组里出现它,且状态 badge 变成绿色“已确认”。如果没变,打开浏览器开发者工具,看 Network 选项卡里my_orders.jsp的响应 HTML,搜索confirmed_by_seller—— 如果没搜到,说明MyOrdersServlet没把状态分组对,立刻检查groupingBy(Order::getStatus)的 key 值是否拼写正确(是confirmed_by_seller,不是confirmed)。

6.5 动作五:导出三张核心表结构,现场解释字段设计意图

打开 MySQL 客户端(如 Navicat 或命令行),执行:

SHOW CREATE TABLE user; SHOW CREATE TABLE item; SHOW CREATE TABLE order;

截图这三段 DDL 语句,放在答辩 PPT 的“数据库设计”页。

验证点:你能指着item.status字段说:“我用 ENUM 是为了防止非法值写入,比如有人手抖输成'onsale',数据库会直接报错,而不是默默存个空字符串”;指着order.seller_id说:“这个外键不是为了性能,是为了数据一致性——如果卖家账号被删,订单还能查到是谁卖的,因为ON DELETE RESTRICT会阻止删除操作”。

6.6 动作六:用 Postman 模拟一次留言,验证异步回复流程

在 Postman 中新建请求:

  • Method:POST
  • URL:http://localhost:8080/campus/servlet/LeaveMessageServlet
  • Body:raw JSON,内容为{"content":"测试留言,请回复"}
  • Headers:加Cookie: JSESSIONID=xxx(从浏览器复制)
    发送后,去窗口 B(管理员)的“留言管理”页,确认出现新留言。

验证点:这条留言的is_read字段值必须为0(未读)。然后点击“回复”,再查数据库,is_read应变为1,且reply表有新记录。这个动作证明你理解了“异步交互”不是 AJAX,而是 HTTP 请求+服务端状态更新的完整链路。

6.7 动作七:关闭 Tomcat,清空数据库,重新部署,5 分钟内恢复全部功能

这是终极压力测试。关掉所有服务,执行:

TRUNCATE TABLE user; TRUNCATE TABLE item; TRUNCATE TABLE order; INSERT INTO user (username, password, role_type) VALUES ('admin', '$2a$10$xxx', 'admin'), ('student1', '$2a$10$yyy', 'student');

然后重启 Tomcat,用两个账号重新登录,重复动作一到四。

验证点:整个过程不超过 5 分钟。如果超时,说明你依赖了某个没写进 SQL 初始化脚本的表(比如category表忘了 INSERT),或者db.properties的密码写错了。从那以后我每次写完数据库脚本,都强制走一遍“清库→重插→重启→登录”流程,雷打不动。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询