☰
JavaWeb电子商城毕业设计实战:从数据库建模到答辩避坑指南
2026/9/28 3:07:18 网站建设 项目流程

简介:这是一份面向计算机相关专业毕业设计的JavaWeb电子商城系统源码与数据库备份,适合正在完成课程设计或毕业设计、希望以完整项目为参考的高校学生及初级开发者。项目基于JSP+Servlet+JSTL+EL+MySQL8+JDBC技术栈实现,涵盖用户、商品、购物车、分类、订单等典型商城模块,可在Eclipse+Tomcat7+JDK10环境下配置运行。压缩包共含417个文件,大小约9.1MB,其中42个Java文件承载Servlet及Dao层业务逻辑,20个JSP页面覆盖前台浏览与后台管理界面,另含28个HTML页面、12个CSS样式、9个JS脚本、216张商品展示图片,以及可直接导入MySQL的SQL数据库脚本。目前已有103人学习下载,适合通过完整项目梳理JavaWeb请求响应流程、JDBC数据访问、EL与JSTL视图渲染等关键环节,也可作为答辩演示与功能说明的参考资料。读者拿到后可对照源码理解分层结构并迁移到自己的毕设项目中,大幅缩短起步时间。

1. 毕业设计选型:JavaWeb电子商城系统为什么是好上手又难被问倒的方向

如果你在资源站里搜过毕设题目,大概率见过这个组合:基于JavaWeb的电子商城系统源码加数据库。名字听起来平平无奇,但在我陪跑过好几届本科毕业设计的经验里,这恰恰是完成率最高、答辩容错率最大的选题类型。它不是最亮眼的题目,却是踩坑最少的路:业务链路完整,覆盖Servlet、JSP、JDBC、Session、Filter这些JavaWeb核心考点,又不依赖支付网关、消息队列这些外部服务,整个系统就一个Tomcat加一个MySQL,全链路你一个人能控制住。下面这套落地路径是我实际带项目时整理的,从数据库建模到IDEA配置,从核心代码到避坑清单,照着做就能把一套能讲清楚原理的商城系统跑在自己的机器上。

2. 先把数据库定下来:六张表、字段边界与一份能直接跑的建表SQL

拿到任何一份电子商城源码,我都建议先别急着往IDEA里导,第一步永远是打开SQL脚本,把表结构读懂再启动项目。数据库是这个系统真正的地基,后面所有Servlet、Service、DAO都在为这张关系网服务,字段少一个,写代码时就要绕一大圈。

2.1 功能边界:毕设商城不需要做成京东

做电子商城最容易犯的错,是把目标定得太大。支付接口、优惠券、秒杀、商品推荐、物流查询,这些功能看起来是商城标配,实际上每一个都是坑。支付需要商户号和签名机制,没有真实商户号就只能模拟;秒杀真正难的不是秒杀本身,而是并发控制和防止超卖;推荐系统更是另一个领域的内容,放进来只会让答辩变成大型翻车现场。

我在给学生定需求时通常会划一条线,商城只做“最小闭环”:

  • 用户注册与登录,密码不能明文存储
  • 商品按分类展示,支持分页浏览
  • 商品详情展示
  • 购物车增删改
  • 提交订单,模拟支付并更新订单状态
  • 用户查看自己的订单列表
  • 管理员登录后台,处理商品上下架和订单状态

这条闭环能走通,JavaWeb课程里最核心的知识点已经覆盖大半,而且每个功能你都能在答辩时讲清楚来龙去脉。管理员后台不必做重,能维护商品状态就行。关于角色权限,我习惯在用户表里加一个role字段,0是普通用户,1是管理员,不单独建admin表,这样少一张表,权限判断逻辑只集中在一个地方。

2.2 六张表的关系:从用户到订单明细怎么串起来

按上面的边界,核心表可以压到六张:t_user、t_category、t_product、t_cart、t_order、t_order_item,它们之间的关系很直观:

  • t_product属于一个t_category
  • t_cart通过user_id和product_id分别关联用户与商品
  • t_order通过user_id关联用户
  • t_order_item通过order_id关联订单,又通过product_id弱关联商品

先看用户表。字段包括id、username、password、nickname、phone、role、create_time。password列我建议定成64位char,存的是MD5加盐之后的值,不是明文。role存放用户角色,0是普通用户,1是管理员,后台登录和前台登录共用一张表。

分类表字段很简单:id、name、description、sort。sort是排序权重,数字小的排前面。分类不要设计成无限级分类,一方面那是另一个复杂度级别,另一方面毕设答辩里一级分类足够,还可以直接在下拉框中展示。

商品表字段有id、category_id、name、subtitle、main_image、price、stock、status、create_time。这里有个新手容易漏掉的字段——status,用来表示上架和下架。商城一定不能让下架商品出现在用户端,后台商品管理中会反复用到这个字段。price用DECIMAL(10,2),不要用float,二进制浮点数处理金额会出现精度问题,答辩时这是一个可讲的细节。

购物车表有四个字段:id、user_id、product_id、quantity、create_time。购物车有两种设计,一种是临时存Session,一种是持久化存数据库。毕设里建议用数据库表,这样用户关闭浏览器再回来购物车还在,答辩时还能讲一句持久化购物车和Session购物车的区别,属于加分项。

订单表是整个设计的核心,字段为id、order_no、user_id、total_price、status、receiver_name、receiver_phone、receiver_address、create_time、pay_time。status是订单状态机:0代表待付款,1代表待发货,2代表待收货,3代表已完成,4代表已取消。这个状态字段会贯穿用户端和管理员后台,后面下单代码里也靠它驱动流程。

最后是订单明细表t_order_item,字段为id、order_id、product_id、product_name、product_image、price、quantity、total_price。注意我故意把product_name、product_image、price冗余进来了。订单生成后,商品可能改价、改名甚至下架,订单里的快照不能变,这就是冗余字段的价值。答辩时能主动说清楚这一点,评委会觉得你是真的在考虑业务而不是在背表结构。

2.3 建表SQL与初始数据:执行一遍就能进入开发状态

下面这份建表脚本以MySQL 5.7或8.0为例,字符集统一用utf8mb4。utf8mb4比utf8多支持了emoji等四字节字符,虽然商城未必用得到,但作为规范可以直接养成习惯。脚本里我顺手带了一个管理员账号、三个商品分类和几条演示商品,导入后不用面对空荡荡的后台。

CREATE DATABASE IF NOT EXISTS mall DEFAULT CHARACTER SET utf8mb4; USE mall; DROP TABLE IF EXISTS t_order_item; DROP TABLE IF EXISTS t_order; DROP TABLE IF EXISTS t_cart; DROP TABLE IF EXISTS t_product; DROP TABLE IF EXISTS t_category; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 'MD5加盐后的密文', nickname VARCHAR(30) DEFAULT '', phone VARCHAR(11) DEFAULT '', role TINYINT DEFAULT 0 COMMENT '0普通用户 1管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE = InnoDB COMMENT = '用户表'; CREATE TABLE t_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, description VARCHAR(255) DEFAULT '', sort INT DEFAULT 0 ) ENGINE = InnoDB COMMENT = '商品分类表'; CREATE TABLE t_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, subtitle VARCHAR(255) DEFAULT '', main_image VARCHAR(255) DEFAULT '', price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category_id (category_id), KEY idx_status (status) ) ENGINE = InnoDB COMMENT = '商品表'; CREATE TABLE t_cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINE = InnoDB COMMENT = '购物车表'; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待付款 1待发货 2待收货 3已完成 4已取消', receiver_name VARCHAR(20) NOT NULL, receiver_phone VARCHAR(11) NOT NULL, receiver_address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINE = InnoDB COMMENT = '订单表'; CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name VARCHAR(100) NOT NULL, product_image VARCHAR(255) DEFAULT '', price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, KEY idx_order_id (order_id) ) ENGINE = InnoDB COMMENT = '订单明细表'; -- 初始数据:admin / admin123 的MD5值 INSERT INTO t_user (username, password, nickname, role) VALUES ('admin', '0192023a7bbd73250516f069df18b500', '管理员', 1); INSERT INTO t_category (name, description, sort) VALUES ('手机数码', '手机、耳机、充电器等', 1), ('电脑办公', '笔记本、键鼠、显示器', 2), ('服饰鞋包', '男装、女装、箱包', 3); INSERT INTO t_product (category_id, name, subtitle, main_image, price, stock, status) VALUES (1, '智能手机 X', '6.5英寸 128GB', '/static/images/p1.jpg', 1999.00, 100, 1), (1, '蓝牙耳机 Pro', '主动降噪 长续航', '/static/images/p2.jpg', 399.00, 200, 1), (2, '轻薄笔记本', '16GB内存 512GB固态', '/static/images/p3.jpg', 4999.00, 50, 1), (2, '无线键鼠套装', '2.4G连接 静音按键', '/static/images/p4.jpg', 129.00, 300, 1), (3, '休闲双肩包', '防泼水 大容量', '/static/images/p5.jpg', 159.00, 150, 1);

执行时注意表顺序,先建用户和分类,再建商品,最后建订单相关表,避免外键逻辑上的错乱。存储引擎统一用InnoDB,原因很直接:订单流程需要事务支持,MyISAM不支持事务,下单时一旦中途报错,数据会处于半完成状态。这一步不用额外配置什么,MySQL默认引擎就是InnoDB。

3. 让源码在本地跑起来:项目结构、Maven依赖与IDEA部署配置

拿到源码最让人烦躁的不是业务代码,而是环境起不来。这一章我按从零到能打开首页的顺序,把项目结构、依赖配置和IDEA里Tomcat的运行设置写清楚,照着操作,二十分钟内应该能看到登录页面。

3.1 项目结构:三层架构的包怎么划

凡是叫“JavaWeb电子商城”的毕设项目,最稳的结构还是经典三层架构。Controller层放Servlet,负责接收请求参数和转发视图;Service层放业务逻辑,事务边界在这里控制;Dao层放JDBC数据访问。实体类放entity,工具类放util,过滤器放filter。

mall ├── pom.xml └── src/main ├── java/com/example/mall │ ├── controller # Servlet,接收参数和跳转 │ ├── service # 业务接口,含订单事务 │ ├── dao # JDBC数据访问 │ ├── entity # User/Product/Order等实体类 │ ├── filter # 登录拦截、编码过滤器 │ └── util # DBUtil、MD5Util等 ├── resources │ └── db.properties # 数据库连接配置 └── webapp ├── WEB-INF/web.xml ├── static # css/js/images ├── index.jsp ├── login.jsp ├── register.jsp └── user ├── cart.jsp ├── order_list.jsp └── order_confirm.jsp

我见过不少源码把JDBC直接写在Servlet里,前几个页面的功能看着很简单,做到购物车和订单就开始失控。因为十几个Servlet都要查数据库,复制粘贴会让你改一处漏三处,最后连自己都找不到数据是从哪里查出来的。所以就算你只是拿源码来改,也建议保持三层结构,不要为了方便把SQL塞进Servlet。

3.2 Maven依赖与连接池参数:这里最容易翻车

pom.xml 是整个JavaWeb项目依赖的入口,常见的坑集中在Servlet版本和MySQL驱动版本上。我用的配置如下:

<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>mall</artifactId> <version>1.0</version> <packaging>war</packaging> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.9</version> </dependency> </dependencies> <build> <finalName>mall</finalName> </build> </project>

servlet-api和jsp-api的scope必须是provided,因为Tomcat本身就带这两套jar,不再包装进去,否则运行时会遇到jar包冲突。jstl是JSP页面里用<c:forEach>遍历商品列表必需的。MySQL驱动版本要跟本机MySQL对应,5.7用5.1.49,8.x用8.0.33,混用最常见的报错是Unable to load authentication plugin 'caching_sha2_password'。

数据库连接池我用Druid,比自己在代码里手动Class.forName再DriverManager.getConnection要稳得多,尤其在并发访问时会明显看出来差别。连接配置放在resources/db.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 jdbc.initialSize=5 jdbc.maxActive=20

注意driver的值,MySQL 8.x 必须写成com.mysql.cj.jdbc.Driver,5.x 才是com.mysql.jdbc.Driver,写错会在项目启动时直接报ClassNotFoundException。URL里的characterEncoding=utf8和serverTimezone=Asia/Shanghai对中文乱码和日期时间显示都有直接影响,少一个后面都会莫名其妙的出问题。之后写一个工具类读取这个配置:

package com.example.mall.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static DataSource dataSource; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

这段代码在整个项目里只有一个静态代码块,应用启动时加载一次连接池,后面所有DAO都从DBUtil.getConnection()拿连接。这样做的好处是连接复用,不会出现每次请求都新建数据库连接的资源浪费。

3.3 IDEA运行JavaWeb项目的完整配置:从Artifact到Tomcat

在IDEA里运行JavaWeb项目和运行普通Java程序不一样,必须把项目打包成Web Artifact再部署到Tomcat。完整步骤我按顺序列一遍:

  1. 用IDEA打开项目根目录下的pom.xml,选择Open as Project,等Maven把依赖下载完。
  2. 打开File → Project Structure → Project,把SDK设为1.8。
  3. 打开Project Structure → Modules,确认Web模块存在,src/main/webapp被标记为Web资源目录。
  4. 打开Project Structure → Artifacts,点加号新建一个Web Application: Exploded,名字会自动识别成mall:war exploded。
  5. 打开Run/Debug Configurations,添加Tomcat Server → Local,在Application Server里选择本机Tomcat安装目录。
  6. 切到Deployment标签页,点加号把mall:war exploded加进去,Application Context填/mall。
  7. 启动前确认MySQL服务已经运行,并且第2节的SQL脚本已经执行过。
  8. 点击运行,浏览器访问http://localhost:8080/mall/。

这里最大的区别是选择war exploded而不是war。war是把项目打包成压缩包再部署,每次改代码都要重新打包;exploded是解压目录模式,IDEA可以直接把编译后的class和资源增量同步到Tomcat,调试体验好得多。

4. 核心业务实现:登录鉴权、商品分页、购物车与下单事务

环境跑通之后,下一步就是把业务链路上最关键的几个点写扎实。注册、商品查询这类简单逻辑网上一搜很多,这里我挑三个最有讲头也最容易被答辩追问的部分:登录拦截、分页查询、订单事务。

4.1 登录鉴权:用Filter拦截未登录访问

如果没有登录拦截,用户可以在浏览器地址栏直接输入http://localhost:8080/mall/user/cart.jsp进入购物车页面。短小的JSP项目里最常见的防法是每个页面开头写一段判断Session的Java代码,复制粘贴十几遍,总有一天会漏一页。我的做法是写一个Filter统一拦截。

package com.example.mall.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/*") public class LoginFilter implements Filter { @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); String ctx = req.getContextPath(); // 直接放行登录、注册、静态资源和首页 if (uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".jpg") || uri.endsWith(".png") || uri.contains("/login") || uri.contains("/register") || uri.equals(ctx + "/index.jsp") || uri.equals(ctx + "/")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); Object user = session == null ? null : session.getAttribute("user"); if (user == null) { resp.sendRedirect(ctx + "/login.jsp"); } else { chain.doFilter(request, response); } } @Override public void destroy() { } }

uri.endsWith(".css")这串判断是必须的,否则登录页面的样式表会被拦下来,页面渲染出来是纯HTML裸样式,这是新手最容易疑惑的现象。req.getSession(false)带false参数表示如果当前没有Session就直接返回null,不会顺手创建一个新Session,这个细节值得记一下。放行规则这个集合里,登录、注册、首页、静态资源都不需要登录,其余路径统一校验。

4.2 商品分页:JDBC查询加上limit偏移量计算

商品列表的分页在JavaWeb里是个高频考点,核心SQL只有一个:LIMIT 偏移量, 每页条数。偏移量不能直接拿页码来算,而是(pageNum - 1) * pageSize,这是很多人翻到第二页就数据错乱的原因。

package com.example.mall.dao; import com.example.mall.entity.Product; import com.example.mall.util.DBUtil; import java.math.BigDecimal; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ProductDao { public List<Product> findPage(int pageNum, int pageSize) throws SQLException { String sql = "SELECT id, category_id, name, subtitle, main_image, price, stock, status " + "FROM t_product WHERE status = 1 " + "ORDER BY create_time DESC LIMIT ?, ?"; List<Product> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getLong("id")); p.setName(rs.getString("name")); p.setSubtitle(rs.getString("subtitle")); p.setMainImage(rs.getString("main_image")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); list.add(p); } } } return list; } public int countOnSale() throws SQLException { String sql = "SELECT COUNT(*) FROM t_product WHERE status = 1"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { rs.next(); return rs.getInt(1); } } }

在Servlet里取参数时,必须做类型转换和边界判断。用户手动把URL改成?pageNum=-1或者?pageNum=999是很常见的操作,后端不拦住就会出现空白页。

int pageNum = 1; int pageSize = 8; String p = req.getParameter("pageNum"); if (p != null && !p.isBlank()) { pageNum = Integer.parseInt(p); } int total = productDao.countOnSale(); int totalPage = Math.max(1, (int) Math.ceil(total * 1.0 / pageSize)); if (pageNum < 1) pageNum = 1; if (pageNum > totalPage) pageNum = totalPage;

total * 1.0 / pageSize这一步是为了把整数除法转成浮点除法再向上取整,否则5条数据每页8条时,5 / 8在Java整数运算里直接等于0,分页控件会显示空白。

4.3 下单事务:先扣库存再插明细,回滚时机不能错

订单是商城系统的核心事务场景,我说的“事务”不是Spring里的@Transactional,而是JDBC原生事务,这更符合JavaWeb课程设计的技术栈。下单至少涉及四步操作:插入订单、插入订单明细、扣减商品库存、清空购物车,这四步必须在一个数据库连接里完成,要么全部成功,要么全部回滚。

public boolean createOrder(Long userId, String receiverName, String receiverPhone, String receiverAddress, List<CartItem> items) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 计算总额 BigDecimal total = new BigDecimal("0"); for (CartItem item : items) { total = total.add(item.getPrice().multiply(new BigDecimal(item.getQuantity()))); } // 2. 插入订单,订单状态默认0待付款 String orderNo = "NO" + System.currentTimeMillis(); String insertOrder = "INSERT INTO t_order(order_no, user_id, total_price, status, " + "receiver_name, receiver_phone, receiver_address) VALUES(?,?,?,0,?,?,?)"; long orderId; try (PreparedStatement ps = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, orderNo); ps.setLong(2, userId); ps.setBigDecimal(3, total); ps.setString(4, receiverName); ps.setString(5, receiverPhone); ps.setString(6, receiverAddress); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { rs.next(); orderId = rs.getLong(1); } } // 3. 插入订单明细,同时扣库存 String insertItem = "INSERT INTO t_order_item(order_id, product_id, product_name, " + "product_image, price, quantity, total_price) VALUES(?,?,?,?,?,?,?)"; String deductStock = "UPDATE t_product SET stock = stock - ? WHERE id = ? AND stock >= ?"; for (CartItem item : items) { try (PreparedStatement ps = conn.prepareStatement(insertItem)) { ps.setLong(1, orderId); ps.setLong(2, item.getProductId()); ps.setString(3, item.getProductName()); ps.setString(4, item.getProductImage()); ps.setBigDecimal(5, item.getPrice()); ps.setInt(6, item.getQuantity()); ps.setBigDecimal(7, item.getPrice().multiply(new BigDecimal(item.getQuantity()))); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement(deductStock)) { ps.setInt(1, item.getQuantity()); ps.setLong(2, item.getProductId()); ps.setInt(3, item.getQuantity()); int rows = ps.executeUpdate(); if (rows == 0) { conn.rollback(); // 库存不足,回滚 return false; } } } // 4. 清空购物车 String clearCart = "DELETE FROM t_cart WHERE user_id = ?"; try (PreparedStatement ps = conn.prepareStatement(clearCart)) { ps.setLong(1, userId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

这段代码里最值得讲的是库存扣减的SQL写法:UPDATE t_product SET stock = stock - ? WHERE id = ? AND stock >= ?。在同一个事务里,插入明细和扣减库存的顺序不会影响最终一致性,但把库存校验放进UPDATE语句的条件里,可以让数据库帮我们判断“库存够不够”,同时避免高并发下的超卖问题。conn.rollback()之后的return false是给上层Servlet一个提示,告诉用户“库存不足”,而不是直接抛500。

事务完成后finally块里要把setAutoCommit(true)恢复,然后把连接归还给Druid连接池。连接池里的连接是不关闭的,如果漏掉重置自动提交,下一次这个连接被借出时事务状态可能是错的,这种问题不出现则已,一出现就是查半天都找不到原因的玄学故障。

5. JavaWeb电子商城实战避坑:乱码、连接池假死、404与分页错乱

这一章是我自己在这类项目上翻过车之后沉淀下来的清单。每一条都是真实场景,写出来是希望你能在答辩前把这些雷扫干净。

5.1 中文乱码:页面、请求、数据库三层各管各的编码

现象:页面上的商品名称显示成???,或者注册用户名存进数据库后变成乱码,有些页面第一次打开正常,提交表单后跳出来的全是问号。

原因:乱码是三层编码不一致共同造成的。第一层是JSP页面的pageEncoding,第二层是HTTP请求和响应的字符集,第三层是数据库连接URL里的字符集参数。只改其中一处,问题不会彻底解决。

解决:JSP文件头部统一写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;在web.xml里配置一个全局过滤器,把请求和响应的编码都设为UTF-8:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

如果项目没用Spring,就自己写一个实现Filter的类,在doFilter里调用request.setCharacterEncoding("UTF-8")。同时确认MySQL连接URL里带了characterEncoding=utf8。三层全对了,乱码基本不会出现。

5.2 数据库连接不释放:系统运行五分钟就开始假死

现象:项目刚启动时一切正常,登录、商品列表都能用,过了几分钟,点击任何页面都转圈,控制台开始刷出wait millis 10000 active 20 max 20之类的报错。

原因:这不是Tomcat的问题,是连接池被耗尽。代码里conn获取了但没有关闭,或者关闭动作写在非finally的位置,一旦中间有异常抛出,连接就永久不归还连接池。Druid连接池默认最大20个连接,满20个后新的请求只能等待,等待超时就把页面卡死。

解决:所有使用Connection的地方,统一使用JDK 7以上的try-with-resources写法,让连接、Statement、ResultSet都自动关闭。如果是指定的参考资料里写了手写的finally块,不要把conn.close()放在业务代码行后面,要放在finally块里。这条是血泪经验,排查起来很费时间,但从写第一步就接入连接池的自动关闭,能帮你少熬一个通宵。

5.3 部署后一直404:Artifact没打全,控制台不报错

现象:Tomcat正常启动,日志没有任何异常,访问http://localhost:8080/mall/却始终404。

原因:IDEA的Artifact配置不完整。最常见的情况是WEB-INF/lib下没有包含Maven依赖,或者src/main/webapp没有被标记为Web资源目录,项目部署到Tomcat下面后是一个空壳。

解决:回到Project Structure → Artifacts,检查mall:war exploded的输出结构里是否包含WEB-INF/lib、WEB-INF/classes、static、JSP文件。如果lib为空,把Available Elements里所有jar包右键选Put into WEB-INF/lib。改完以后删除之前的Artifact重新建一次,再启动。访问路径也要确认上下文是/mall,在Deployment标签里能看到红色路径提示。

5.4 分页翻到后面数据错乱:limit边界和类型转换

现象:商品列表第一页正常,第二页也能显示,点第三页或最后一页的时候,页面空白或者数据重复。

原因:分页SQL里的偏移量算错,这是最常见的。偏移量应该等于(pageNum - 1) * pageSize,如果直接把pageNum传给LIMIT,第一页没问题,第二页就会从第2条数据开始取,第三页从第3条开始,每页只有8条的话,越到后面错得越明显。另一个隐蔽原因是前端把页码作为字符串拼接,pageNum做了字符串相加,比如2 + 1变成了"21"。

解决:Servlet里接收参数后,先把字符串Integer.parseInt转成int,再做(pageNum - 1) * pageSize运算。同时加上边界判断:pageNum < 1时置为1,pageNum > totalPage时置为当前最后一页。这样即使用户手动改URL参数,也不会看到空白页。

5.5 改完代码不生效:Tomcat跑的还是旧逻辑

现象:修改了Servlet代码,重新运行,页面行为跟改之前一样,甚至删除了一整个JSP文件,刷新页面还能打开。

原因:这个遇到过的人很多。IDEA里Tomcat默认配置是On Update action: Restart Server,你改了代码但没有触碰Tomcat自身,它不会主动重新编译部署。更隐蔽的是IDEA的自动编译没有开启,改完Java文件直接运行,用的还是旧的class文件。

解决:在Tomcat的Run Configuration里,把On Update action和On frame deactivation都设为Update classes and resources,这样改动后只要IDE失去焦点就会自动编译部署。每次改完代码按Ctrl+F9强制重新编译,然后看Tomcat控制台有没有重新部署的日志。如果改了JSP页面不生效,还要检查Tomcat的JSP缓存路径,必要时直接Redeploy。

6. 答辩前最后一步:把演示流程和数据准备到万无一失

功能做完了,数据库跑通了,离拿高分还差最后一个动作:把演示当作一场小型的上线演练来做。我见过太多功能完好但演示翻车的场景,结论是数据准备和流程排练比写代码更影响最终结果。

6.1 十到十五分钟的演示脚本

不要到了现场才临场发挥。我习惯把演示流程固定成一条不用思考的链路:先用管理员账号登录后台,确认能看到商品列表和订单管理入口;退出登录,注册一个新用户,走一遍注册流程;用新用户登录,浏览商品分类,翻两页分页列表;把两三个商品加入购物车,进购物车修改数量;提交订单,在模拟支付页确认金额;回到后台,把订单状态从待付款改成待发货;切回用户端查看订单列表,确认状态同步。

这条链路走下来正好覆盖注册、搜索、分页、购物车、订单、权限几个核心模块。答辩现场最怕的不是功能出错,而是你花了两分钟在页面上找入口,评委的注意力一分散,后面想拉回来就难了。

6.2 数据准备与细节加分项

演示用的数据库不要拿初始数据硬撑。建议手动在t_product里插入二三十条商品数据,把分类覆盖全,每类至少五六条。图片要真实存在,main_image字段指向static/images/下的本地文件,不要引用外链图片,答辩教室可能没网,外链图一加载失败整个页面就变难看了。

几个加分的小细节成本不高但效果很好:密码加密工具类里不要只做一次MD5,加个固定盐,答辩时可以讲“即使数据库泄露,明文密码也无法直接还原”;商品列表按create_time倒序展示,这是一个默认用户习惯;在订单相关的表上主动建立索引,并且在答辩时说出idx_user_id、idx_order_id这两条索引的作用,很多学生写完了都不知道数据库有索引这回事,你能说出来就是差异。

我自己的习惯是答辩前三天,把数据库整个删掉重新执行一遍初始化脚本,然后按演示链路完整走三遍。第一遍验证功能,第二遍练手速,第三遍看着表故意把操作做错一次,比如库存不足直接下单,确认系统的提示正常。这个习惯帮我在答辩现场躲过一次数据残留导致的商品列表为空,也让我在评理委员追问事务时能从容地说出回滚时机。

项目本身不难,难的是把每个环节的细节都掌握在自己手里。希望这篇笔记能帮你少踩几个坑,顺利把毕业设计这一关过了。

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

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

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

立即咨询