简介:面向Web程序设计课程学习者的网上商城期末大作业完整项目,提供可直接运行的完整商城站点,适合课程设计、毕业设计或需要Java Web项目参考的初、中级开发者;是Java Web全栈综合练习,能将课堂知识串联成完整项目。共207个文件,压缩包约4.7MB;其中75个Java文件负责后台业务逻辑,27个JSP页面处理动态输出,21个HTML、18个CSS和18个JavaScript文件搭建前端展示与交互,另配有图片素材和XML/Properties配置,整体结构清晰,便于按模块学习与二次开发。目前已有145人学习下载。资源覆盖网上商城从数据层、业务层到展示层的完整链路,可帮助理解请求转发、会话管理、数据库读写等Web开发关键环节;前端还基于Bootstrap等样式库优化了用户界面,文件分类较清晰,便于快速定位后端逻辑、前端页面与样式素材,适合作为期末作品或毕业设计参考,也便于答辩演示、功能扩展或代码复用。
1. 这个网上商城课程作业 zip,解压不是重点,跑通才是
先给一个反直觉结论:WEB 程序设计课程的期末大作业,挂科的人往往不是因为功能没做完,而是把项目做成了“能打开首页的黑匣子”,答辩时一被问就翻车。标题里这个网上商城项目 zip,核心是一套完整的电商教学闭环:用户注册登录、商品列表、购物车、下单、订单管理。它要解决的是“用课堂上学过的 Servlet、JSP、MySQL 把这些页面串成一个能演示的完整流程”这个具体任务。适合期末赶进度的学生、想拿高分答辩的开发者,以及拿到别人项目想改成自己的初学者。所以这个 zip 的价值不在解压,在于你能顺着下面这条线走一遍:看懂骨架、跑通功能、避开坑、答辩有底气。按章节往下做,一台装了 MySQL 和 Tomcat 的机器就能完整复现。
2. 网上商城的技术骨架:先看懂表设计和三层架构,再决定怎么改
动手改代码之前,先花半小时把项目的骨架看清。网上商城这类课程作业的套路非常固定:前端是 JSP 加少量 CSS 和 JavaScript,后端是 Servlet 做控制器,业务逻辑写在 service 包,数据库访问写在 dao 包,数据库用 MySQL,容器用 Tomcat。搞清楚这一层,后面所有调试才有方向。
2.1 课程作业的通用选型:Servlet+JSP 为什么仍是主流
一个网上商城作业包打开,后端是 Servlet+JSP 的概率非常高。原因很简单:课程考核点落在“HTTP 请求怎么进 Servlet、Session 怎么保持登录、SQL 怎么写”这层基础功上,而不是框架的自动装配。Spring Boot 这类框架把连接池、事务、参数绑定都藏起来了,表面开发快,但答辩时被追问一句“你的事务注解底层做了什么”,很多人直接卡住。
所以拿到 zip 后第一件事不是急着改代码,先确认技术栈。常见的形态是:JSP 做页面,Servlet 做控制器,业务逻辑在 service 包,数据库操作在 dao 包,数据库 MySQL,部署容器 Tomcat。这种分层看起来土,但它是一条能从头讲到尾的链路。如果包里的代码把 SQL 全部写在 JSP 里,那你拿到的是“能跑但不好答辩”的版本,后面会单独说怎么改。
| 方案 | 课程适配度 | 答辩追问难度 | 改造空间 |
|---|---|---|---|
| Servlet + JSP + MySQL | 高 | 中 | 大 |
| Spring Boot + Thymeleaf | 低 | 高 | 中 |
| PHP + MySQL | 中 | 中 | 小 |
2.2 数据库四张表:用户、商品、订单、订单明细
网上商城的表设计,四张表是下限:user、goods、orders、order_item。很多作业只建三张表,把商品名称和单价直接冗余在 orders 表里,理由是下订单查询方便。问题在于后台一旦改商品价格,历史订单跟着变,答辩老师一条 SQL 就把你问住了。正确做法是 orders 只记录订单号和用户,商品维度全部落在 order_item 里,明细表保存下单那一刻的名称和价格快照。
CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARSET utf8mb4; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, phone VARCHAR(11), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, cover VARCHAR(255), status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架' ); CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, goods_id INT NOT NULL, goods_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL );几个参数说明:price 用 DECIMAL(10,2) 而不是 FLOAT,是为了避免浮点累加时的精度误差,计算订单总金额时能少很多玄学问题。password 字段长度给 64,给 MD5 或 SHA-256 加盐后的结果留够空间,不要用 VARCHAR(20) 存明文。status 用 TINYINT 而不是字符串,后面扩展状态机时只改注释不动数据结构。索引方面,orders.user_id 和 order_item.order_id 建议各加普通索引,不加也能跑,但订单列表的 JOIN 查询在数据量稍大时会明显变慢,答辩现场容易被追问。
2.3 从 zip 到本地服务器的部署路径
拿到 zip 解压后,先确认工程目录是不是标准 Web 应用结构。常见结构包含 src 目录的 Java 源码、webapp 或 WebContent 目录的页面和静态资源、WEB-INF/web.xml 的 Servlet 映射与过滤器注册、lib 的第三方 jar。如果连 lib 都没有,说明驱动用的是 IDE 的 Library 方式引入,换机器部署时容易踩驱动找不到的坑。
shop-project ├── src/com/demo/shop │ ├── servlet/IndexServlet.java │ ├── servlet/LoginServlet.java │ ├── servlet/CartAddServlet.java │ ├── service/OrderService.java │ ├── dao/UserDao.java │ ├── dao/GoodsDao.java │ └── util/DBUtil.java ├── webapp │ ├── WEB-INF/web.xml │ ├── lib/mysql-connector-java.jar │ ├── jsp/login.jsp │ ├── jsp/register.jsp │ ├── jsp/goods_list.jsp │ └── jsp/cart.jsp └── sql/shop.sql部署我用的是 IDE 内嵌 Tomcat 的方式:导入工程为 Web 项目,配置本机 Tomcat 安装路径,把 artifact 加到 Deployment 里,点 Run 就能跑。这里有两个必调参数:Tomcat 默认端口 8080,被占用时改 conf/server.xml 里 Connector 的 port;MySQL 连接 URL 里的 serverTimezone,MySQL 8.x 必须带 serverTimezone=Asia/Shanghai,否则连接直接报错。这两个参数属于“换台电脑就翻车”的高频点,记住位置后面避坑章还会展开。
注意:如果工程里同时存在 web.xml 和 @WebServlet 注解,且 web.xml 的 metadata-complete 设为 true,注解会全部失效。遇到 Servlet 映射 404 时先查这一项。
3. 从注册登录到商品列表:把第一条业务链路跑通
这一章的目标是让用户注册、登录、看商品列表这三件事串起来。串起来的关键有两个:参数从页面到 Servlet 的传递,登录状态从登录页到其他页面的传递。代码能跑是底线,更重要的是知道每一行在做什么。
3.1 注册和登录:密码存储与 Session 登录态
注册页收集 username、password、confirmPassword 三个参数,前端用 JS 校验一次,Servlet 里必须再校验一次。规则和参数说明见代码:
package com.demo.shop.servlet; @WebServlet("/register") public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 编码设置必须放在读取参数之前 req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String confirm = req.getParameter("confirmPassword"); if (username == null || username.length() < 3 || password == null || password.length() < 6 || !password.equals(confirm)) { resp.sendRedirect("register.jsp?err=1"); return; } // 课程作业建议至少用 MD5,实际生产必须加盐 UserDao dao = new UserDao(); if (dao.exists(username)) { resp.sendRedirect("register.jsp?err=2"); return; } dao.save(username, MD5Util.md5(password)); // 注册成功直接跳登录页,不自动登录,流程更清晰 resp.sendRedirect("login.jsp?ok=1"); } }两个关键点:doPost 处理表单提交,GET 请求访问这个 URL 时应重定向到注册页,不然浏览器地址栏直接输入 /register 会得到 405。编码设置放在读取参数之前是硬性要求,放在之后中文参数必乱码。MD5Util 核心就一行:MessageDigest.getInstance("MD5") 后按十六进制输出,用 Spring 的 DigestUtils 也可以。真实项目必须加盐,课程作业做到 MD5 已经能挡住“密码明文存数据库”这个答辩雷点。
登录逻辑类似,但多了一个 Session 写入:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = new UserDao().findByUsernameAndPassword( username, MD5Util.md5(password)); if (user == null) { resp.sendRedirect("login.jsp?err=1"); return; } // 登录态放 Session 而不放 Cookie,后端依赖的会话数据不进浏览器 HttpSession session = req.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 单位秒,半小时超时 resp.sendRedirect("index"); } }Session 默认超时 30 分钟,web.xml 里可以全局覆盖。我习惯把 user 对象整个放进去而不是只放 userId,页面用 EL 表达式直接拿用户名,省一次查库。前提是 User 对象的 password 属性在 setAttribute 前置空,否则调试日志会带出密码,这是自己在代码里埋的雷。
3.2 商品列表页:分页参数与 JSP 渲染
商品列表页是访问 /index,Servlet 查当前页的商品列表,转发给 JSP 渲染。带分页的实现如下:
@WebServlet("/index") public class IndexServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pageParam = req.getParameter("page"); int page = 1; if (pageParam != null && pageParam.matches("\\d+")) { page = Integer.parseInt(pageParam); } int pageSize = 12; GoodsDao dao = new GoodsDao(); List<Goods> list = dao.findByPage((page - 1) * pageSize, pageSize); int total = dao.count(); int totalPage = (total + pageSize - 1) / pageSize; req.setAttribute("list", list); req.setAttribute("totalPage", totalPage); req.setAttribute("currentPage", page); req.getRequestDispatcher("jsp/goods_list.jsp").forward(req, resp); } }page 参数做了正则过滤,防止前端传入非数字导致 NumberFormatException。totalPage 用 (total + pageSize - 1) / pageSize 计算,避免浮点运算。对应的 JSP 片段:
<%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <base href="${pageContext.request.contextPath}/"> </head> <body> <c:forEach var="g" items="${list}"> <div class="goods-card"> <p class="name">${g.name}</p> <p class="price">¥${g.price}</p> <a href="cart/add?goodsId=${g.id}">加入购物车</a> </div> </c:forEach> <c:forEach var="i" begin="1" end="${totalPage}"> <a href="index?page=${i}">${i}</a> </c:forEach> </body> </html>两个细节要注意:head 里的 解决了 CSS、图片和链接的相对路径问题,没有它,订单列表页里的链接大概率按 /order/ 前缀解析,然后集体 404。c:forEach 依赖 JSTL 核心库,如果 webapp/WEB-INF/lib 下没有 jstl.jar 和 standard.jar,JSP 会直接报 500。很多作业包解压后首页就打不开,十次有八次是缺这两个 jar。
3.3 数据库连接:抽一个工具类,别在每个 Servlet 里连库
课程作业最常见的坏味道是每个 Servlet 都写一遍 Class.forName、DriverManager.getConnection,结果换数据库密码要改七八个文件。抽一个 DBUtil 是正确起步:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { // MySQL 8.x 驱动类名带 cj,老驱动类名已弃用 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }URL 里三个参数是踩坑重灾区:useUnicode=true 和 characterEncoding=UTF-8 保证中文写入不乱码;serverTimezone=Asia/Shanghai 是 MySQL 8.x 的强校验,不加就报 The server time zone value 异常。数据库连接池(比如 Druid)课程作业不是必须,但答辩时主动提出“这里可以换连接池,避免每次请求建立物理连接”,是明确加分表达。驱动类名 5.1.x 和 8.x 不一样,这也是后面避坑章 5.5 的引子。
4. 购物车与下单流程:Session 购物车、库存扣减与事务边界
购物车是商城作业里最容易做成“能用但挨骂”的部分。有些实现把购物车放 Cookie,优点是刷新不丢,缺点是数据在浏览器端,服务端无法校验数量和价格,答辩被问“用户改了 Cookie 里的价格怎么办”就沉默。课程作业更推荐购物车放 Session,服务端完全掌控。
4.1 购物车的数据结构:Map 而不是 List
Session 里购物车字段类型用 Map<Integer, CartItem>,key 是商品 ID,value 是购物车条目。为什么不用 List?用户反复添加同一商品时,Map 只需要 get 出来改数量,不用遍历 List 找同一行。CartItem 建议这样设计:
public class CartItem { private int goodsId; private String goodsName; private BigDecimal price; // 加入购物车那一刻的商品单价 private int quantity; // 购买数量 private boolean checked; // 是否勾选,结算时只算勾选项 public BigDecimal getSubtotal() { return price.multiply(BigDecimal.valueOf(quantity)); } }加入购物车的 Servlet 把“购物车不存在就新建、商品已有就累加、没有就 new 一个”三个分支写清楚:
@WebServlet("/cart/add") public class CartAddServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int goodsId = Integer.parseInt(req.getParameter("goodsId")); int quantity = 1; if (req.getParameter("quantity") != null) { quantity = Integer.parseInt(req.getParameter("quantity")); } HttpSession session = req.getSession(); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); } CartItem item = cart.get(goodsId); if (item != null) { item.setQuantity(item.getQuantity() + quantity); } else { Goods goods = new GoodsDao().findById(goodsId); item = new CartItem(); item.setGoodsId(goodsId); item.setGoodsName(goods.getName()); item.setPrice(goods.getPrice()); item.setQuantity(quantity); cart.put(goodsId, item); } session.setAttribute("cart", cart); // 重定向而不是转发,防止刷新页面时购物车数量不断累加 resp.sendRedirect("jsp/cart.jsp"); } }这里用 resp.sendRedirect 而不是 forward,能让浏览器地址栏变成 cart.jsp,刷新页面时触发 GET 而不是重新执行 addServlet,挡掉很大一部分重复加购的翻车现场。price 取的是加入购物车那一刻的商品表价格,而不是结算时再查表,保证购物车展示和最终结算口径一致。这个“价格快照”的思路,和订单明细表里的 goods_name 快照是同一条设计原则。
4.2 创建订单与扣减库存:一个事务里完成
订单创建牵扯三步:生成订单号、写 orders 表、遍历购物车扣库存并写 order_item 明细。中间任何一步失败,前面做的要全部撤销,所以必须放在一个数据库事务里:
public boolean createOrder(int userId, Map<Integer, CartItem> cart) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 生成订单号并插入订单主表 String orderNo = "S" + System.currentTimeMillis() + userId; BigDecimal total = BigDecimal.ZERO; for (CartItem item : cart.values()) { total = total.add(item.getSubtotal()); } new OrderDao(conn).insertOrder(orderNo, userId, total); // 2. 遍历购物车,验证库存并扣减 PreparedStatement ps = conn.prepareStatement( "UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?"); for (CartItem item : cart.values()) { ps.setInt(1, item.getQuantity()); ps.setInt(2, item.getGoodsId()); ps.setInt(3, item.getQuantity()); if (ps.executeUpdate() == 0) { throw new SQLException("库存不足,商品ID=" + item.getGoodsId()); } // 3. 写订单明细 new OrderDao(conn).insertItem(item); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } return false; } finally { DBUtil.close(conn); } }扣库存的 UPDATE 带了 AND stock >= ?,一条 SQL 同时完成“检查库存并扣减”,不需要先 SELECT 再 UPDATE,既能减少一次查询,也规避了并发下的超卖风险。事务能否回滚的前提是 OrderDao 的全部 SQL 都使用同一个 conn,绝不能在 service 层再调 DBUtil.getConnection() 去拿第二个连接,这是很多人写了事务代码却不生效的根本原因。订单号用时间戳拼 userId,课程作业够用,但并发下有可能重复,答辩时可以主动说“真实环境建议用 UUID 或雪花算法”,这属于能接住追问的加分表达。
4.3 订单列表与状态字段:用 TINYINT 定义状态机
orders.status 建议定义成 TINYINT 并在代码里写常量或枚举,不要直接比较字符串。状态流转一般是待支付(0) → 已支付(1) → 已发货(2) → 已完成(3)。课程作业的“模拟支付”就是点一下支付按钮把状态从 0 改成 1,真实支付回调不在期末范围内。
订单列表页要展示“这个订单包含哪些商品”,不要用 N+1 查询,也就是别在页面循环里逐条查 order_item,用一条 GROUP_CONCAT 聚合查询更干净:
SELECT o.id, o.order_no, o.total_amount, o.status, u.username, GROUP_CONCAT(CONCAT(i.goods_name, 'x', i.quantity) SEPARATOR '、') AS items_text FROM orders o JOIN user u ON o.user_id = u.id JOIN order_item i ON o.id = i.order_id GROUP BY o.id ORDER BY o.create_time DESC;GROUP_CONCAT 默认长度上限 1024,商品特别多时可能被截断,课程作业的量级不会触发,但心里有数。order_item 里的 goods_name 是下单快照,后台改商品名也不会影响历史订单明细,这正是第 2 章建表时强调快照字段的原因。状态字段配合 Java 枚举或者常量类使用,页面展示时再用 switch 映射成中文,比在 SQL 里写 CASE WHEN 更清晰,也更好维护。
5. 网上商城作业避坑:5 个最容易让页面白屏的细节
课程作业的坑往往不在复杂逻辑,而在几个偷懒的细节。这一章全是血泪经验,每一条都按“现象 → 原因 → 解决”写,照着排查能省一晚。
5.1 中文乱码回马枪:编码设置的位置和顺序
现象:注册提交后,数据库里 username 变成问号,或者 JSP 商品名显示乱码。原因:设置编码的时机太晚,比如先读了 getParameter 再 setCharacterEncoding,那一次读取已经按 ISO-8859-1 解码了;或者只处理了请求,没处理响应输出。MySQL 连接 URL 缺 characterEncoding=UTF-8 也会存乱码。解决:编码在所有入口统一处理,最稳的做法是写一个 CharsetFilter,在 doFilter 最靠前的位置设置:
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request = (HttpServletRequest) req; // 必须放在读取任何参数之前 request.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); }同时确认 web.xml 里这个 filter 的 url-pattern 是 /* ,并且注册顺序在业务 Filter 之前。自己搭工程时把这两行放进 filter,比在几十个 Servlet 里各自 set 一遍可靠得多。
5.2 Session 重启就丢:购物车被清空不算 bug,但演示会翻车
现象:把项目 redeploy 或重启 Tomcat 后,重新打开页面发现购物车空空的,登录态也没了。原因:Session 默认存在 JVM 内存里,进程重启就没了。这是正常机制,不是项目 bug。解决:知道它为什么会丢,演示时避开这个时刻即可。常见的做法是把演示顺序设计成“先启动服务器 → 登录 → 逛商品 → 加购 → 下单”,中间不去碰 redeploy。如果作业要求“刷新页面购物车还在”,这归 Session 管;要求“重启后购物车还在”,那需要 Cookie 或数据库保存购物车,课程作业一般不要求到这层。
5.3 刷新页面重复下单:PRG 模式解决的经典问题
现象:下单成功后顺手按 F5,订单表里多了两条一模一样的订单。原因:下单接口是 POST,Servlet 处理完没有重定向,响应结束后浏览器刷新会把上一次 POST 再发一遍。解决:用 POST-Redirect-GET 模式:下单成功后不要 forward 或直接输出内容,而是 response.sendRedirect("order/list"),让浏览器跳到 GET 请求上。这个习惯适用于所有写操作:注册、加购、下单,写完之后一律重定向。它能同时解决“返回上一页再刷新”的重复提交问题,是成本最低的幂等手段。
5.4 图片样式加载 404:JSP 相对路径的经典误导
现象:首页正常,点进订单列表页后 CSS 全丢,图片裂开,控制台一堆 404。原因:JSP 里的 href="css/style.css" 是相对路径,浏览器访问 /order/list 时会把基准当成 /order/,于是解析成 /order/css/style.css,自然找不到。解决:在每个 JSP 的 head 里加一行 base 标签: ,页面里所有相对路径都基于项目根目录解析,不会因为 URL 层级变化集体失效。这也是第 3 章 JSP 示例特意放这一行的原因。
5.5 换电脑就连不上数据库:驱动、时区、密码三个顺序
现象:别人电脑上能跑,拿回来访问首页报 500,控制台第一行是 Communications link failure 或 Access denied。原因:常见三个:MySQL 8.x 驱动类名变了、URL 没带 serverTimezone、数据库密码和本地不一致。解决:按顺序排查,先看 lib 里 mysql-connector-java 的 jar 版本,5.1.x 用老驱动类名,8.0.11 起必须写 com.mysql.cj.jdbc.Driver;再检查 URL 有没有 serverTimezone=Asia/Shanghai;最后用命令行 mysql -u root -p 验证账号密码能连上。三步做完,十台机器能解决九台。剩下那一台,多半是 MySQL 端口不是默认 3306,检查 URL 里的端口号就行。
6. 让答辩更有底气的三个小技巧:日志、数据核对、下单入口
6.1 用一个 Filter 打印请求日志,把“我感觉能跑”变成“我测过”
答辩被问系统性能时,空口说“应该还行”最吃亏。我习惯在所有项目里加一个 AccessLogFilter,打印每个请求的方法、URL 和耗时:
@WebFilter("/*") public class AccessLogFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; long start = System.currentTimeMillis(); chain.doFilter(req, resp); long cost = System.currentTimeMillis() - start; System.out.printf("%s %s 耗时 %dms%n", request.getMethod(), request.getRequestURI(), cost); } }耗时是第一次进入 filter 到响应结束的时间,反映一次 HTTP 请求全链路耗时。答辩时打开控制台现场点几个页面,把耗时数据亮出来,比任何话术都有说服力。耗时稳定在几十毫秒,就大方说“列表页平均 XX 毫秒”;如果上千毫秒,提前自查是不是 SQL 没走索引。
6.2 备一条 SQL 自证数据一致性
老师问数据一致性,不要只解释事务。备一条能现场执行的核对 SQL:
-- 找出总金额与明细累加不一致的订单,正常应返回空结果 SELECT o.order_no, o.total_amount, SUM(i.price * i.quantity) AS real_total FROM orders o JOIN order_item i ON o.id = i.order_id GROUP BY o.id, o.order_no, o.total_amount HAVING o.total_amount != real_total;返回空结果,配合下单时开启事务的代码,就能证明“金额一致性有代码逻辑和 SQL 双层保障”。如果查出数据,正好是答辩的改进素材:说明你发现了问题并知道怎么修。这条 SQL 比堆概念扎实得多。
6.3 调整演示入口,把最顺的流程放在第一屏
答辩演示顺序和代码质量同样重要。默认首页如果是注册页,评委要找半天入口;如果登录后落地页是商品列表,整个演示能压缩到十次点击以内。我交课程作业前会写一个一分钟演示脚本,从启动服务器到提交订单必须一遍过,中间不允许出现“等我找一下这个功能在哪”的空档。另一个习惯是提前主动触发一次“库存不足”,确认提示文案。与其让评委问库存为 0 会怎样而现查代码,不如演示时就展示边界处理。这个习惯帮我免掉过很多次临时救场。希望帮到你。
本文还有配套的精品资源,点击获取