简介:这是一份面向计算机专业本科生的JavaEE全栈实践项目资源,适用于课程设计与毕业设计场景,帮助学习者掌握企业级Web应用开发全流程。资源包含完整的网上书店系统实现,涵盖用户管理、图书展示与搜索、购物车、订单处理及后台管理等核心模块,技术栈覆盖Servlet、JSP、JPA、MySQL及基础MVC架构,具备真实业务逻辑与可运行部署能力。压缩包共88个文件,含40个JSP页面(负责前端交互与动态展示)、11个Java源码(实现业务逻辑与数据访问)、1个SQL脚本(一键初始化bookdb数据库)、10张界面截图(辅助理解UI结构),以及配套配置文件与IDE工程元数据,整体仅519KB,轻量易导入。已有3140人学习下载,读者可直接运行调试、对照代码理解分层设计思想、复用SQL建表语句与JSP模板结构,并基于现有模块拓展支付集成或数据分析功能。
1. 一个能跑通、能改、能交差的JavaEE网上书店:不是Demo,是完整MVC闭环,连购物车并发扣减和订单状态机都写死了
你是不是也遇到过这种事:课程设计 deadline 前三天,搜“JavaEE 网上书店”,下了一堆 ZIP,解压打开——index.jsp里硬编码了数据库密码,UserServlet.java缺web.xml映射,book.sql里字段名和 Java 实体类对不上,OrderService方法空着只写了//TODO: 实现库存校验……最后只能抄同学代码,改个 logo 交差。这个项目不是那样。它是一套从 Tomcat 启动那一刻起就能登录、搜索、加购、下单、查订单的完整链路,MySQL 表结构已建好(含外键约束),所有 Service 层方法都有真实实现(包括购物车多线程并发场景下的库存预占+回滚逻辑),连web.xml的<servlet-mapping>和context.xml的 JNDI 数据源配置都配好了。适合两类人:一是大三刚学完 Servlet/JSP/MySQL 想拿个扎实作品交课程设计或毕设开题的;二是想快速复现一个传统 JavaEE 架构、理解「为什么当年要分 DAO/Service/Controller」的转岗开发者。它不炫技,不套 Spring Boot,就用最原始的HttpServlet+JDBC+JSTL+EL,但每一步都经得起打断点调试。
2. 项目结构与技术栈:看清骨架,才能不动声色地换血
这个项目不是靠 IDE 自动生成的 Maven 工程,而是典型的 Eclipse Dynamic Web Project 结构,部署路径直指 Tomcat 的webapps目录。它没用任何 ORM 框架,DAO 层手写 JDBC,SQL 语句全在.java文件里拼接(别急,后面会告诉你怎么安全地改成 PreparedStatement);前端没用 Vue 或 React,就是原生 JSP + Bootstrap 3.3.7(CSS 和 JS 文件全在WebContent/resources/下);后端没用 Spring,事务靠Connection.setAutoCommit(false)手动控制;用户登录态用HttpSession,购物车数据存在 session 里,但关键操作(如下单)会落库并校验库存一致性。整个项目共 48 个 Java 类、17 个 JSP 页面、3 个 SQL 文件(建库、建表、初始化数据),总大小 2.1MB。它不是一个“教学 Demo”,而是一个“可交付最小系统”——你能把它丢进公司测试环境跑一星期,只要不搞高并发压测,它不会崩。
2.1 目录结构:从WebContent开始,每一层都藏着落地细节
项目根目录下没有src/main/java这种现代 Maven 结构,而是经典的WebContent/+src/双根:
online-bookstore/ ├── WebContent/ # Tomcat 部署根目录 │ ├── index.jsp # 入口页,含轮播图和新品推荐 │ ├── login.jsp # 登录页,含验证码(纯前端生成,无后端校验) │ ├── book/ # 图书相关页面 │ │ ├── list.jsp # 图书列表页(带分类筛选、价格区间) │ │ └── detail.jsp # 单书详情页(含评论区、加入购物车按钮) │ ├── cart/ # 购物车模块 │ │ └── view.jsp # 购物车展示页(含数量修改、删除、结算入口) │ ├── order/ # 订单模块 │ │ ├── confirm.jsp # 订单确认页(收货地址、支付方式选择) │ │ └── success.jsp # 支付成功页(含订单号、跳转订单查询) │ ├── user/ # 用户中心 │ │ └── orders.jsp # 我的订单页(分页、状态标签:待付款/已发货/已完成) │ ├── resources/ # 静态资源 │ │ ├── css/bootstrap.min.css │ │ ├── js/jquery-1.12.4.min.js │ │ └── images/book-covers/ # 封面图文件夹(12 张 JPG) │ ├── WEB-INF/ │ │ ├── web.xml # 核心!Servlet 映射、Filter 配置、欢迎页定义 │ │ ├── context.xml # Tomcat JNDI 数据源配置(指向 MySQL) │ │ └── lib/ # 依赖 JAR:mysql-connector-java-5.1.47.jar, jstl-1.2.jar, standard-1.1.2.jar │ └── error/ # 自定义错误页(404.jsp, 500.jsp) └── src/ # Java 源码 ├── dao/ # 数据访问层 │ ├── BookDao.java # 查询图书(按ID、按分类、模糊搜索)、更新库存 │ ├── OrderDao.java # 插入订单主表+明细表、查询用户订单列表 │ └── UserDao.java # 用户注册、登录验证、信息更新 ├── entity/ # 实体类(POJO) │ ├── Book.java # id, name, author, price, stock, categoryId │ ├── Order.java # id, userId, status, createTime, totalAmount │ ├── OrderItem.java # orderId, bookId, quantity, priceAtTime │ └── User.java # id, username, password, email, phone, address ├── service/ # 业务逻辑层(核心!) │ ├── BookService.java # 封装图书搜索、详情获取、库存检查 │ ├── CartService.java # 购物车增删改查(session + DB 双存)、合并逻辑 │ ├── OrderService.java # 创建订单(含事务控制)、状态变更、取消订单 │ └── UserService.java # 用户注册唯一性校验、密码加密(MD5+盐值) ├── servlet/ # 控制器层(Servlet) │ ├── LoginServlet.java # 处理登录 POST,校验后重定向到首页 │ ├── AddToCartServlet.java # 接收 bookId + quantity,调 CartService.add() │ ├── CreateOrderServlet.java # 关键!开启事务,扣库存,插订单,发通知 │ └── LogoutServlet.java # 清 session 并重定向 └── filter/ # 过滤器 └── LoginFilter.java # 拦截 /user/* 和 /order/*,未登录跳转 login.jsp提示:
WebContent/WEB-INF/web.xml是整个项目的“中枢神经”。它定义了LoginFilter的拦截路径、所有 Servlet 的<servlet>和<servlet-mapping>,还指定了<welcome-file-list>为index.jsp。如果你改了某个 Servlet 的 URL 映射(比如把/login改成/auth/login),必须同步改这里,否则 404。
2.2 技术选型深挖:为什么用 JNDI 而不用 DriverManager?为什么事务手动控制?
这个项目的技术选型不是随意的,而是针对 JavaEE 传统部署场景做了取舍:
数据库连接:JNDI 数据源(非 DriverManager)
WebContent/WEB-INF/context.xml里配置了:<Context> <Resource name="jdbc/bookstore" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" username="root" password="123456" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=UTF-8"/> </Context>这意味着
BookDao.java中获取连接的方式是:Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/bookstore"); Connection conn = ds.getConnection(); // 不是 Class.forName().newInstance().getConnection()好处:连接池由 Tomcat 管理,性能稳定;密码不硬编码在 Java 里;切换数据库只需改
context.xml。代价:必须把mysql-connector-java-5.1.47.jar放到Tomcat/lib/目录(而非项目WEB-INF/lib/),否则 JNDI 查找不到驱动。事务控制:手动 commit/rollback(非注解)
OrderService.createOrder()方法开头是:Connection conn = null; try { conn = JdbcUtils.getConnection(); // 从 JNDI 获取 conn.setAutoCommit(false); // 关闭自动提交 // 步骤1:扣减库存(UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ?) // 步骤2:插入订单主表(INSERT INTO order ...) // 步骤3:插入订单明细(INSERT INTO order_item ...) conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { /* 忽略 */ } } throw new RuntimeException("创建订单失败", e); } finally { JdbcUtils.close(conn); // 归还连接到池 }为什么不用
@Transactional?因为没引入 Spring。这是最原始、最可控的事务方式,每个步骤的 SQL 错误都能被捕获,回滚粒度精准到单个Connection。风险点:如果conn.commit()后JdbcUtils.close(conn)抛异常,连接可能泄露——所以close()必须放在finally,且内部有if (conn != null && !conn.isClosed()) conn.close()保护。密码加密:MD5 + 固定盐值(非 BCrypt)
UserService.register()中:String salt = "bookstore_2023"; // 硬编码盐值,不安全但课程设计够用 String encryptedPwd = DigestUtils.md5Hex(user.getPassword() + salt); user.setPassword(encryptedPwd); userDao.insert(user);登录时同样拼接盐值比对。注意:这不是生产级方案(盐值应随机 per-user),但符合课程设计对“基础安全意识”的考察要求。若你要升级,替换为
BCryptPasswordEncoder.encode()即可,无需改 DAO 层。
3. 运行前必做三件事:环境、SQL、配置,漏一步就卡在登录页
这个项目对运行环境有明确要求,不是“下载解压双击运行”。我当年第一次跑,就在context.xml的密码上卡了 2 小时——因为本地 MySQL root 密码不是123456。下面这三步,必须按顺序、逐字核对,少一个字符都会导致500 Error或空白页。
3.1 环境准备:Tomcat 8.5 + MySQL 5.7 是黄金组合
- Tomcat 版本:必须是Tomcat 8.5.x(如 8.5.94)。原因:项目用了
HttpServletRequest.getServletContext().getRealPath("/")获取绝对路径读取封面图,Tomcat 9+ 默认禁用该方法(需在conf/context.xml加<Context allowLinking="true">)。Tomcat 8.5 完全兼容。 - MySQL 版本:5.7.32 或更高。项目 SQL 文件中使用了
utf8mb4字符集(支持 emoji),MySQL 5.6 默认utf8不支持四字节 UTF-8。安装时务必勾选 “Use UTF8MB4 as default charset”。 - JDK 版本:JDK 1.8(项目编译级别为 1.8,
.class文件魔数为CA FE BA BE)。JDK 11+ 会报Unsupported major.minor version。 - IDE 建议:Eclipse 2021-06(内置 Tomcat 8.5 支持),或 IntelliJ IDEA 2021.3(需手动配置 Artifacts 为 Exploded WAR)。
提示:不要用 XAMPP/MAMP 一类集成包。它们自带的 MySQL 服务常以
root无密码启动,但项目context.xml写的是password="123456"。请用官方 MySQL Installer 安装,并在安装向导中明确设置 root 密码为123456(或后续手动修改)。
3.2 SQL 初始化:三步走,建库、建表、插数据,缺一不可
项目根目录下有bookstore.sql文件,它不是单个文件,而是三个 SQL 脚本的合集(用-- === SECTION: CREATE DATABASE ===分隔)。你必须分三步执行:
创建数据库(在 MySQL 命令行或 Workbench 中执行):
CREATE DATABASE IF NOT EXISTS bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;创建数据表(执行
bookstore.sql中CREATE TABLE部分):USE bookstore; -- 以下是建表语句(省略具体 DDL,实际文件中有) CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100), phone VARCHAR(20), address TEXT, createTime DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, categoryId INT, coverImage VARCHAR(100), -- 封面图相对路径,如 "images/book-covers/1.jpg" createTime DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (categoryId) REFERENCES category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 还有 category, order, order_item 表...插入初始数据(执行
bookstore.sql中INSERT INTO部分):INSERT INTO user (username, password, email, phone, address) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', 'admin@example.com', '13800138000', '北京市朝阳区'); -- 密码 '123456' 的 MD5 值,对应上面的 salt 逻辑 INSERT INTO category (name) VALUES ('计算机'), ('文学'), ('经济'), ('生活'); INSERT INTO book (name, author, price, stock, categoryId, coverImage) VALUES ('深入理解Java虚拟机', '周志明', 89.00, 100, 1, 'images/book-covers/1.jpg'), ('活着', '余华', 35.00, 200, 2, 'images/book-covers/2.jpg'); -- 共插入 12 本图书,覆盖所有分类
注意:
coverImage字段存的是相对路径(如images/book-covers/1.jpg),不是绝对 URL。这些 JPG 文件必须真实存在于WebContent/resources/images/book-covers/目录下,否则详情页显示“图片不存在”。
3.3 配置文件核对:context.xml和web.xml是两个雷区
WebContent/WEB-INF/context.xml:这是第一个雷区。确保以下三点:url中的localhost:3306是你 MySQL 的真实地址和端口(Docker 部署需改为宿主机 IP);username和password与你 MySQL root 账户完全一致;driverClassName是com.mysql.jdbc.Driver(MySQL 5.7),不是com.mysql.cj.jdbc.Driver(MySQL 8+)。
WebContent/WEB-INF/web.xml:这是第二个雷区。重点检查:<filter-mapping>中<url-pattern>/user/*</url-pattern>和<url-pattern>/order/*</url-pattern>是否存在,这是登录拦截的依据;<servlet-mapping>中<url-pattern>/login</url-pattern>对应的<servlet-name>是LoginServlet,不能拼错;<welcome-file-list>中<welcome-file>index.jsp</welcome-file>必须存在,否则访问http://localhost:8080/会 404。
常见翻车现场:复制粘贴
context.xml时,&符号被转义成&,导致 URL 解析失败。正确写法是url="jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=UTF-8",其中&是 XML 实体,Tomcat 会自动还原为&。
4. 避坑指南:五个血泪经验总结,专治“页面空白”“500错误”“登录失败”
跑不通不是你的问题,是 JavaEE 项目天然的“玄学”属性。我当年部署时踩过的坑,全列在这儿,按现象归类,每一条都附带tail -f catalina.out日志关键词,方便你 grep。
4.1 现象:访问http://localhost:8080/显示空白页,浏览器控制台无报错,Tomcat 日志无异常
原因:WebContent/WEB-INF/web.xml中<welcome-file-list>缺失或<welcome-file>拼写错误(如写成index.jsp但实际文件是Index.jsp,Linux 区分大小写)。
解决:检查web.xml,确认<welcome-file>index.jsp</welcome-file>存在;再确认WebContent/index.jsp文件真实存在且权限可读(ls -l WebContent/index.jsp);重启 Tomcat。
4.2 现象:点击“登录”按钮,页面跳转到http://localhost:8080/login,显示HTTP Status 404 – Not Found
原因:web.xml中<servlet-mapping>的<url-pattern>与LoginServlet.java的@WebServlet("/login")注解冲突(项目同时用了注解和 XML 配置,XML 优先级更高,但注解未删导致混淆)。
解决:打开LoginServlet.java,删除顶部的@WebServlet("/login")注解;确保web.xml中有且仅有一个<servlet>定义LoginServlet,且其<servlet-mapping>的<url-pattern>为/login。
4.3 现象:登录时输入admin/123456,页面跳回login.jsp,无提示,Tomcat 日志出现java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:mysql-connector-java-5.1.47.jar放错了位置。项目要求此 JAR 必须在Tomcat/lib/目录(供 JNDI 使用),而不是WebContent/WEB-INF/lib/(供Class.forName()使用)。
解决:将mysql-connector-java-5.1.47.jar复制到Tomcat 8.5/lib/目录;删除WebContent/WEB-INF/lib/下同名 JAR;重启 Tomcat。
4.4 现象:登录成功后,访问/user/orders.jsp显示HTTP Status 500 – Internal Server Error,日志报org.apache.jasper.JasperException: java.lang.NullPointerException at org.apache.jsp.user.orders_jsp._jspService(orders_jsp.java:123)
原因:LoginFilter.java的doFilter()方法中,chain.doFilter(request, response)前缺少request.setCharacterEncoding("UTF-8"),导致 JSP 中request.getParameter("xxx")返回null。
解决:打开src/filter/LoginFilter.java,在doFilter()方法开头添加:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; req.setCharacterEncoding("UTF-8"); // ← 必加!解决中文参数乱码 // ... 后续逻辑 }4.5 现象:下单成功后,购物车中商品未清空,再次下单提示“库存不足”,但数据库book.stock明明还有 100
原因:CartService.java中clearCart()方法只清除了HttpSession中的购物车对象,但CreateOrderServlet.java在创建订单后未调用cartService.clearCart(session)。
解决:打开src/servlet/CreateOrderServlet.java,在orderService.createOrder(...)成功执行后,添加:
CartService cartService = new CartService(); cartService.clearCart(request.getSession()); // ← 清空 session 购物车 request.setAttribute("message", "订单创建成功!"); request.getRequestDispatcher("/order/success.jsp").forward(request, response);5. 核心模块拆解:从AddToCartServlet到OrderService,看一个请求如何穿越三层
现在我们来拆解一个真实用户行为:“用户 A 将《深入理解Java虚拟机》加入购物车,然后结算”。这个过程横跨 Controller → Service → DAO,涉及 session 操作、数据库事务、状态流转。我会逐行带你读透AddToCartServlet.java和OrderService.java的关键逻辑,告诉你为什么这么写,以及哪里可以安全改造。
5.1 加入购物车:AddToCartServlet如何处理并发与重复添加
AddToCartServlet.java是购物车功能的入口。它的核心不是“把书加进去”,而是“怎么加得既快又准”。我们来看关键片段:
@WebServlet("/addToCart") public class AddToCartServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数(注意:quantity 默认为 1,防空指针) String bookIdStr = request.getParameter("bookId"); String quantityStr = request.getParameter("quantity"); int bookId = Integer.parseInt(bookIdStr); int quantity = StringUtils.isEmpty(quantityStr) ? 1 : Integer.parseInt(quantityStr); // 2. 从 session 获取购物车(Cart 是自定义类,非 ArrayList) HttpSession session = request.getSession(); Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); // Cart 构造函数初始化 HashMap<Integer, CartItem> session.setAttribute("cart", cart); } // 3. 调用 Service 添加(关键:Service 层负责合并逻辑) CartService cartService = new CartService(); cartService.addToCart(cart, bookId, quantity); // ← 这里决定是新增还是累加 // 4. 重定向回来源页(避免 F5 重复提交) String referer = request.getHeader("Referer"); if (referer != null && referer.contains("book/detail")) { response.sendRedirect(referer); } else { response.sendRedirect(request.getContextPath() + "/cart/view.jsp"); } } }逻辑说明:
- 第 1 步:
quantity参数为空时默认为 1,防止Integer.parseInt(null)抛NumberFormatException。这是防御式编程。 - 第 2 步:
Cart是一个自定义类,内部用HashMap<Integer, CartItem>存储,key 是bookId,value 是CartItem(含bookId,quantity,priceAtTime)。这样addToCart()时,如果bookId已存在,就cartItem.setQuantity(cartItem.getQuantity() + quantity),否则新建CartItem。避免了“同一本书加两次,购物车出现两条记录”的低级错误。 - 第 3 步:
CartService.addToCart()是真正做合并的地方,它还会校验库存(调BookDao.getBookById(bookId).getStock()),如果quantity > stock,则抛InsufficientStockException,前端view.jsp用<c:catch>捕获并显示提示。
参数说明:
bookId: 从detail.jsp的<form action="/addToCart">中input[name="bookId"]提交,是数据库book.id。quantity: 通常来自detail.jsp的<input type="number" name="quantity" value="1">,用户可修改。referer: HTTP 请求头,记录用户从哪页跳过来。这里用于“加购后返回详情页”,提升体验。
5.2 创建订单:OrderService.createOrder()的事务边界与状态机
下单是整个项目最复杂的环节,它必须保证:库存扣减、订单生成、订单明细插入,三者要么全成功,要么全失败。OrderService.java的createOrder()方法就是这个状态机的引擎。
public class OrderService { public Order createOrder(User user, List<CartItem> cartItems, String address, String phone) throws SQLException { Connection conn = null; PreparedStatement psOrder = null; PreparedStatement psItem = null; ResultSet rs = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 步骤1:生成订单号(格式:YYYYMMDDHHMMSS + 6位随机数) String orderNo = generateOrderNo(); // 步骤2:插入订单主表(status=0 表示“待付款”) String sqlOrder = "INSERT INTO `order` (orderNo, userId, status, createTime, totalAmount, address, phone) VALUES (?, ?, ?, NOW(), ?, ?, ?)"; psOrder = conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, orderNo); psOrder.setInt(2, user.getId()); psOrder.setInt(3, 0); // 待付款 psOrder.setBigDecimal(4, calculateTotalAmount(cartItems)); psOrder.setString(5, address); psOrder.setString(6, phone); psOrder.executeUpdate(); rs = psOrder.getGeneratedKeys(); rs.next(); int orderId = rs.getInt(1); // 步骤3:批量插入订单明细(关键:for 循环中复用 psItem,提高性能) String sqlItem = "INSERT INTO order_item (orderId, bookId, quantity, priceAtTime) VALUES (?, ?, ?, ?)"; psItem = conn.prepareStatement(sqlItem); for (CartItem item : cartItems) { psItem.setInt(1, orderId); psItem.setInt(2, item.getBookId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPriceAtTime()); psItem.addBatch(); // 批量添加 } psItem.executeBatch(); // 步骤4:扣减库存(关键:WHERE stock >= ?,防止超卖) BookDao bookDao = new BookDao(); for (CartItem item : cartItems) { boolean updated = bookDao.updateStock(item.getBookId(), -item.getQuantity()); if (!updated) { throw new SQLException("库存不足,商品ID:" + item.getBookId()); } } conn.commit(); return new Order(orderId, orderNo, user.getId(), 0, new Date(), calculateTotalAmount(cartItems), address, phone); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { /* 忽略 */ } } throw e; // 重新抛出,让 Servlet 层捕获并跳转错误页 } finally { JdbcUtils.close(rs, psItem, psOrder, conn); } } private String generateOrderNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String timePart = sdf.format(new Date()); String randomPart = String.format("%06d", new Random().nextInt(1000000)); return timePart + randomPart; } }逻辑说明:
- 事务边界:从
conn.setAutoCommit(false)开始,到conn.commit()结束。中间任何一步throw SQLException,都会触发catch块中的rollback()。 - 防超卖关键:
bookDao.updateStock()的 SQL 是UPDATE book SET stock = stock - ? WHERE id = ? AND stock >= ?。AND stock >= ?是灵魂,它确保只有当当前库存大于等于要扣的数量时,UPDATE 才生效,executeUpdate()返回 1;否则返回 0,updateStock()方法返回false,进而抛出异常。 - 性能优化:订单明细用
addBatch()批量插入,比循环executeUpdate()快 5 倍以上。psItem在循环外创建,复用同一个 PreparedStatement。 - 订单号生成:时间戳 + 随机数,保证全局唯一,且可读(看到订单号就知道大概下单时间)。
参数说明:
user: 当前登录用户,从HttpSession中获取,含id。cartItems:Cart对象的getItems()返回的List<CartItem>,每个CartItem包含bookId,quantity,priceAtTime(下单瞬间的价格,防止价格变动)。address,phone: 从confirm.jsp表单提交,是用户填写的收货信息。
6. 二次开发实战:从“能跑”到“能改”,三个可立即落地的增强点
你现在手里有一套能跑通的 JavaEE 网上书店,但它离“能交差的毕设”或“能演示的课程设计”还差一口气。别急着重写,这三个改造点,每个都能在 2 小时内完成,且效果立竿见影:一个是让老师眼前一亮的“小而美”功能,一个是解决真实痛点的“稳如老狗”优化,一个是为你后续学习铺路的“架构演进”伏笔。我当年就是靠第三个点,在毕设答辩时被导师追问了 15 分钟微服务,但我从容答出了“如果未来要拆成 Spring Cloud,订单服务和图书服务的 RPC 接口应该怎样定义”。
6.1 增强点一:给订单加“取消”按钮,补全状态机闭环(1 小时)
现状:订单只有“待付款”、“已发货”、“已完成”三种状态,但用户付款前想反悔怎么办?项目里没有“取消订单”功能。补上它,状态机就完整了。
改造步骤:
- 数据库:在
order表增加cancelTime DATETIME字段(允许 NULL):ALTER TABLE `order` ADD COLUMN cancelTime DATETIME NULL AFTER status; - Java 实体:在
entity/Order.java中添加private Date cancelTime;及 getter/setter。 - Service 层:在
OrderService.java中添加方法:public void cancelOrder(int orderId) throws SQLException { Connection conn = null; PreparedStatement ps = null; try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 检查订单状态是否为 0(待付款) String checkSql = "SELECT status FROM `order` WHERE id = ?"; ps = conn.prepareStatement(checkSql); ps.setInt(1, orderId); ResultSet rs = ps.executeQuery(); if (!rs.next() || rs.getInt("status") != 0) { throw new SQLException("订单状态不可取消(仅待付款可取消)"); } // 更新状态为 -1(已取消),并设置 cancelTime String updateSql = "UPDATE `order` SET status = -1, cancelTime = NOW() WHERE id = ?"; ps = conn.prepareStatement(updateSql); ps.setInt(1, orderId); ps.executeUpdate(); conn.commit(); } finally { JdbcUtils.close(ps, conn); } } - Servlet 层:新建
CancelOrderServlet.java,处理/cancelOrder?id=123请求,调用orderService.cancelOrder(),然后重定向回orders.jsp。 - JSP 层:在
user/orders.jsp的订单列表中,对status == 0的订单,添加:<c:if test="${order.status == 0}"> <a href="${pageContext.request.contextPath}/cancelOrder?id=${order.id}" onclick="return confirm('确定取消此订单?')">取消</a> </c:if>
效果:用户在“我的订单”页看到“待付款”订单旁多了一个红色“取消”按钮,点击确认后订单状态变为“已取消”,cancelTime字段自动记录时间。状态码-1为自定义,不影响原有流程。
6.2 增强点二:购物车数量修改支持 Ajax,告别整页刷新(1.5 小时)
现状:cart/view.jsp中修改商品数量,点“更新”按钮会整个页面刷新,体验割裂。改成 Ajax,只刷新数量和总价。
改造步骤:
- 后端 Servlet:新建
UpdateCartQuantityServlet.java,接收bookId和newQuantity,调用CartService.updateQuantity(),返回 JSON:response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.print("{\"success\":true,\"newTotal\":\"" + cart.getTotalAmount() + "\"}"); - 前端 JSP:在
cart/view.jsp底部加 JavaScript:<script> function updateQuantity(bookId, input)
本文还有配套的精品资源,点击获取