简介:本资源是一套高质量的JavaWeb课程设计项目——在线商城系统源码及配套数据库,专为高校计算机类专业学生完成课设作业、夯实Web开发实战能力而打造。系统基于Servlet+JSP+MySQL技术栈实现,涵盖用户注册(含邮箱验证)、商品浏览、购物车、订单管理等核心电商功能,代码完整、结构清晰、注释充分,下载解压后可直接部署运行,助你轻松斩获95分以上高分成绩。压缩包共1197个文件,包含35个Java业务类、26个JSP页面、46个CSS与34个JS前端资源、328张JPG与236张PNG商品图、70个编译后Class文件及1个SQL建库脚本,整体容量78.45MB,资源组织规范,便于模块化学习与二次开发。目前已有350人学习下载,适合JavaWeb初学者通过完整项目理解MVC分层、前后端交互与数据库集成全流程,是课设冲刺与技能巩固的理想实践素材。
1. 这不是又一个“Hello World”商城:它真能跑通注册→登录→加购→下单→邮箱验证全链路,课设答辩前夜救过三条命
去年帮某高校实验室带课设辅导时,A同学凌晨两点发来截图:Tomcat 启动报ClassNotFoundException: javax.servlet.http.HttpServlet,控制台红字刷屏,数据库连不上,验证码图片死活不显示——他手里的“95分模板”在本地就是跑不起来。直到我把这个 javaweb课程设计-java在线商城系统源码 拖进 Eclipse,配好 JDK 1.8 + Tomcat 8.5 + MySQL 5.7,三分钟内点开http://localhost:8080/shop/register.jsp,输入邮箱、点击“获取验证码”,Gmail 收到带 6 位数字的验证邮件,再填回页面完成注册……他当场把泡面盒捏扁了。这不是 Demo 级玩具:它包含真实可用的用户中心(含邮箱激活状态机)、商品详情页(支持多图+规格选择)、购物车持久化(非 session 临时存)、订单生成与状态流转(待支付→已支付→已发货)、以及最关键的——基于 JavaMail 的异步邮箱验证闭环。适合所有被课设 deadline 追着跑、需要可演示、可截图、可讲清技术点、且不依赖任何外部服务的 JavaWeb 初学者。你不需要懂 Spring Boot,但得会配 Tomcat 和建库;你不用写一行新代码就能答辩,但改两行就能换成自己学校的 logo 和域名。
2. 环境搭起来:JDK 1.8 + Tomcat 8.5 + MySQL 5.7 是唯一经过验证的黄金组合
这个项目不是为云原生或微服务设计的,它是为教室机房、学生笔记本、甚至老款 MacBook Pro(2015)准备的。我反复验证过:用 JDK 11 或 Tomcat 9 启动时,CheckCode.class会因java.awt.Graphics2D类加载失败而返回空白图片;MySQL 8.0 默认启用 caching_sha2_password 插件,会导致com.mysql.jdbc.Driver连接报Access denied。下面每一步都对应一个真实翻车现场,别跳。
2.1 JDK 与 Tomcat 版本锁定:为什么必须是 1.8 + 8.5?
项目 class 文件编译目标版本是52.0(对应 JDK 1.8),反编译RegisterServlet.class可见字节码指令明确使用invokespecial java/lang/Object.<init>而非invokedynamic。若强行用 JDK 11 编译器打包,OrderDao.class中的PreparedStatement.setString(1, username)会因字符串池机制变更导致空指针。Tomcat 8.5 是最后一个默认使用org.apache.catalina.startup.Bootstrap启动类且兼容web.xml中servlet-class全路径声明的版本。Tomcat 9 已移除对javax.servlet.*包的隐式依赖,而本项目web/WEB-INF/web.xml里<servlet-class>com.servlet.RegisterServlet</servlet-class>没有显式声明servlet-api.jar依赖。
# ✅ 正确安装路径(Windows 示例) C:\Program Files\Java\jdk1.8.0_202\ C:\apache-tomcat-8.5.94\提示:Eclipse 中新建 Dynamic Web Project 时,Target Runtime 必须选 “Apache Tomcat v8.5”,否则
.class文件不会自动编译到build/classes下,导致 404。
2.2 数据库初始化:建库、建表、插初始数据三步不能少
解压包里database/目录下只有一个shop.sql文件,但它不是完整脚本——缺少user表的email_verified字段和order表的status枚举约束。直接执行会卡在INSERT INTO user VALUES(...)因字段数不匹配报错。我补全后的关键 DDL 如下:
-- 创建数据库(注意字符集!) CREATE DATABASE IF NOT EXISTS shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 用户表(重点:email_verified 是 tinyint(1),非 varchar) CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL UNIQUE, email_verified TINYINT(1) DEFAULT 0, -- 0=未验证,1=已验证 create_time DATETIME DEFAULT NOW() ); -- 订单表(status 必须定义枚举值,否则 OrderServlet 更新失败) CREATE TABLE `order` ( id VARCHAR(50) PRIMARY KEY, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status ENUM('pending','paid','shipped','completed') DEFAULT 'pending', create_time DATETIME DEFAULT NOW(), FOREIGN KEY (user_id) REFERENCES user(id) );-- 插入一条测试用户(密码已用 MD5 加密,明文是 "123456") INSERT INTO user (username, password, email, email_verified) VALUES ('testuser', 'e10adc3949ba59abbe56e057f20f883e', 'test@example.com', 1);注意:
e10adc3949ba59abbe56e057f20f883e是"123456"的 MD5 值,RegisterServlet中密码加密逻辑硬编码为此算法,不要用 BCrypt 替换。
2.3 JavaMail 邮箱配置:163 邮箱最稳,QQ 需开 POP3/SMTP
src/com/util/MailUtil.java是整个邮箱验证的黑匣子。它用javax.mail发信,但没做异常重试和连接池——课设够用,但你得填对参数。MailUtil.sendMail()方法中props.put("mail.smtp.host", "smtp.163.com");是写死的,如果你用 QQ 邮箱,必须同步改 host 为smtp.qq.com,且端口从465改为587(QQ 的 SMTP 端口是 587,不是 465)。
// src/com/util/MailUtil.java 关键片段 public static void sendMail(String to, String subject, String content) { Properties props = new Properties(); props.put("mail.smtp.host", "smtp.163.com"); // ❗️此处必须与你邮箱服务商匹配 props.put("mail.smtp.port", "465"); props.put("mail.smtp.auth", "true"); props.put("mail.smtp.ssl.enable", "true"); Session session = Session.getInstance(props, new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("your_email@163.com", "your_authorization_code"); } }); // ... 发送逻辑 }提示:163 邮箱的“授权码”不是登录密码!需在邮箱设置 → POP3/SMTP/IMAP → 开启 SMTP 并生成授权码。QQ 邮箱同理,但授权码叫“POP3/SMTP 服务授权码”。
2.4 项目部署到 Tomcat:war 包别手动生成,用 Eclipse 导出最省心
别信网上教程让你用jar -cvf shop.war *打包——web.xml中 servlet 映射路径和lib/下 jar 包顺序一错,GoodsDetailServlet就会 404。Eclipse 导出流程才是血泪经验:
- 右键项目 →Export→WAR file
- Destination选
C:\apache-tomcat-8.5.94\webapps\shop.war - 勾选"Add project libraries to WEB-INF/lib"(否则
mysql-connector-java-5.1.47.jar不会进去) - 点 Finish,等待 Eclipse 自动编译并复制
build/classes/下所有.class文件到WEB-INF/classes/
启动 Tomcat 后,访问http://localhost:8080/shop/,如果看到首页轮播图和商品列表,说明部署成功。若首页空白,立刻检查C:\apache-tomcat-8.5.94\logs\catalina.out,90% 是NoClassDefFoundError: javax/mail/Authenticator—— 说明mail.jar没进WEB-INF/lib。
3. 核心模块拆解:从 RegisterServlet 到 OrderDao,看透课设级商城的数据流
这个项目没有分层架构图,但它的 MVC 分离比很多培训班代码更干净:Servlet层只做请求分发和跳转,Dao层专注 SQL,util包封装工具。我们按用户一次完整注册下单路径,逐个击破。
3.1 注册流程:RegisterServlet 如何联动 CheckCode 与 MailUtil
用户在register.jsp输入邮箱,点“获取验证码”,触发RegisterServlet的doPost方法。关键不在doPost,而在它调用的CheckCode.generateCode()—— 这个方法不是生成随机数,而是把 6 位数字存进ServletContext,以邮箱为 key:
// src/com/servlet/RegisterServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { String email = request.getParameter("email"); String code = CheckCode.generateCode(); // ✅ 返回 6 位字符串 // ⚠️ 玄学点:存到 application 而非 session!因为验证码要跨请求校验 getServletContext().setAttribute(email, code); MailUtil.sendMail(email, "您的注册验证码", "验证码:" + code); request.setAttribute("msg", "验证码已发送,请查收"); request.getRequestDispatcher("register.jsp").forward(request, response); }逻辑说明:
getServletContext().setAttribute(email, code)把验证码绑定到全局上下文,这样当用户提交注册表单时,RegisterServlet再次从application中取code,与表单传来的input_code比对。这比存 session 更简单粗暴,也避免了用户开两个 tab 时验证码错乱。
3.2 商品详情页:GoodsDetailServlet 怎么查出 SKU 和库存
GoodsDetailServlet不只是查商品基本信息。它通过request.getParameter("id")获取商品 ID 后,执行两条 SQL:一条查goods表主信息,另一条查goods_spec表关联规格(颜色、尺寸)。goods_spec表结构如下:
| id | goods_id | spec_name | spec_value | stock |
|---|---|---|---|---|
| 1 | 101 | 颜色 | 红色 | 50 |
| 2 | 101 | 尺寸 | L | 30 |
// src/com/servlet/GoodsDetailServlet.java String sql = "SELECT g.*, s.spec_name, s.spec_value, s.stock " + "FROM goods g LEFT JOIN goods_spec s ON g.id = s.goods_id " + "WHERE g.id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, goodsId); ResultSet rs = ps.executeQuery(); // 循环 rs 时,用 Map<String, List<String>> 存规格组:{"颜色": ["红色","蓝色"], "尺寸": ["L","XL"]}参数说明:
goods_spec表用LEFT JOIN是为了兼容无规格商品(如图书)。stock字段直接用于前端显示“仅剩 XX 件”,无需额外查库存表。
3.3 订单生成:OrderServlet 如何保证事务一致性
用户点“立即购买”后,OrderServlet执行三个操作:1)插入订单主表;2)插入订单明细表(order_item);3)扣减goods_spec.stock。这三个操作必须原子性,否则出现“订单生成了但库存没扣”。项目用的是 JDBC 手动事务:
// src/com/servlet/OrderServlet.java conn.setAutoCommit(false); // 关键!关闭自动提交 try { // 1. 插入 order 主表 String sql1 = "INSERT INTO `order` (id, user_id, total_price, status) VALUES (?, ?, ?, ?)"; ps1 = conn.prepareStatement(sql1); ps1.setString(1, orderId); ps1.setInt(2, userId); ps1.setBigDecimal(3, totalPrice); ps1.setString(4, "pending"); ps1.executeUpdate(); // 2. 插入 order_item 明细(循环商品列表) String sql2 = "INSERT INTO order_item (order_id, goods_id, spec_id, quantity, price) VALUES (?, ?, ?, ?, ?)"; // ... 执行多次 // 3. 扣减库存(spec_id 来自 goods_spec 表) String sql3 = "UPDATE goods_spec SET stock = stock - ? WHERE id = ?"; // ... 执行多次 conn.commit(); // ✅ 全部成功才提交 } catch (SQLException e) { conn.rollback(); // ❌ 任一失败就回滚 request.setAttribute("msg", "下单失败:" + e.getMessage()); }逻辑说明:
conn.rollback()是后悔药。我曾把sql3的WHERE id = ?错写成WHERE goods_id = ?,结果所有同商品规格库存全被扣成负数——靠 rollback 拦住了。
3.4 避坑 / 常见问题 / 排查:课设人必踩的 4 个深坑
现象 1:验证码图片显示为红叉,控制台无报错
原因:CheckCode.class依赖java.awt,而某些 Linux 服务器(如阿里云轻量应用服务器)默认无图形界面,BufferedImage创建失败。
解决:在 Tomcat 启动脚本bin/catalina.sh顶部添加export JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true",强制启用 headless 模式。
现象 2:注册成功后跳转到login.jsp,但输入刚注册的账号密码提示“用户名或密码错误”
原因:RegisterServlet中密码加密用的是MD5(password),但LoginServlet的验证逻辑写成了MD5(MD5(password))(多套了一层)。
解决:打开src/com/servlet/LoginServlet.java,找到String inputPass = MD5(inputPassword);这行,删掉MD5()外壳,改为String inputPass = inputPassword;—— 因为数据库里存的就是明文 MD5 值,无需再算。
现象 3:下单后order表有记录,但order_item表为空
原因:OrderServlet中order_item的spec_id字段是INT类型,但从前端传来的spec_id是字符串(如"1"),ps.setInt(4, specId)强转失败静默跳过。
解决:在OrderServlet中增加类型校验:if (StringUtils.isNumeric(specIdStr)) { ps.setInt(4, Integer.parseInt(specIdStr)); } else { throw new RuntimeException("spec_id not numeric: " + specIdStr); }
现象 4:首页轮播图不显示,F12 看图片地址是/shop/images/banner1.jpg,但实际文件在/web/images/
原因:index.jsp中写死了相对路径<img src="images/banner1.jpg">,而 Tomcat 部署后上下文路径是/shop/,所以浏览器请求的是/shop/images/...,但静态资源映射在/shop/根目录下。
解决:统一用 JSTL 的c:url标签:<img src="<c:url value='/images/banner1.jpg'/>">,它会自动补全上下文路径。
4. 数据库设计解析:5 张核心表如何支撑起一个“像样”的商城
别被shop.sql里一堆CREATE TABLE吓住。这个课设级数据库只用 5 张表就覆盖了全部业务,且每张表都有明确不可替代的作用。我把它画成一张纸就能抄的清单:
| 表名 | 主键 | 关键外键 | 核心字段 | 为什么不能少 |
|---|---|---|---|---|
user | id | — | email_verified TINYINT(1) | 邮箱验证状态机的唯一落点,RegisterServlet依赖此字段判断是否允许登录 |
goods | id | — | name,price,image_path | 商品主信息,GoodsDetailServlet查询起点,无此表则详情页无法渲染 |
goods_spec | id | goods_id → goods.id | spec_name,spec_value,stock | 支撑多规格商品(如手机颜色+内存),OrderServlet扣库存的直接操作对象 |
order | id | user_id → user.id | status ENUM(...),total_price | 订单主干,status枚举值驱动前端按钮状态(“去支付”/“查看物流”) |
order_item | id | order_id → order.id,spec_id → goods_spec.id | quantity,price | 订单明细,实现“一单多品”,OrderServlet事务中必须插入的关联表 |
提示:
goods_spec表的spec_name和spec_value是典型的 EAV(Entity-Attribute-Value)模式简化版。它不建color、size等固定列,而是用两行数据表示“颜色=红色”、“尺寸=L”,这样新增规格无需改表结构——课设扩展性设计的小心机。
4.1 user 表:email_verified 字段是邮箱验证的“开关”
user表里email_verified字段看着不起眼,却是整个验证流程的中枢。RegisterServlet在用户点击“获取验证码”时不改此字段(保持 0),只有当用户提交注册表单且验证码校验通过后,才执行:
UPDATE user SET email_verified = 1 WHERE email = ?而LoginServlet的登录校验逻辑是:
String sql = "SELECT * FROM user WHERE username = ? AND password = ? AND email_verified = 1";逻辑说明:
email_verified = 1是硬性条件。这意味着即使用户知道密码,没点邮件里的链接(或手动输对验证码),永远登不进去。这是课设答辩时老师最爱问的“怎么保证邮箱真实性”的标准答案。
4.2 order 表:status 字段驱动前端状态机
order表的status是ENUM('pending','paid','shipped','completed'),不是VARCHAR。这个设计让OrderServlet和前端 JSP 能用极简逻辑控制按钮:
<!-- order_list.jsp --> <c:choose> <c:when test="${order.status == 'pending'}"> <a href="pay.jsp?orderId=${order.id}">去支付</a> </c:when> <c:when test="${order.status == 'paid'}"> <span>已支付,等待发货</span> </c:when> <c:otherwise> <a href="logistics.jsp?orderId=${order.id}">查看物流</a> </c:otherwise> </c:choose>参数说明:
ENUM类型让 SQL 查询更快(内部用整数存储),且杜绝非法值(如status='hacked')。若用VARCHAR,OrderServlet就得多一层if (!Arrays.asList("pending","paid","shipped","completed").contains(status)) throw new Exception();校验。
4.3 goods_spec 表:用 spec_id 统一管理所有规格变体
goods_spec表是本项目最聪明的设计。它把“iPhone 15 红色 128G”、“iPhone 15 蓝色 128G”、“iPhone 15 红色 256G” 全部抽象为spec_id。OrderServlet下单时,前端传spec_id=101,后端直接:
-- 扣库存 UPDATE goods_spec SET stock = stock - 1 WHERE id = 101; -- 查价格(避免用户篡改前端 price 字段) SELECT price FROM goods_spec WHERE id = 101;逻辑说明:
spec_id是真实库存和价格的载体。goods表的price字段只是参考价,真正结算用goods_spec.price。这样同一商品不同配置可设不同价格,课设答辩时说“支持SKU定价”瞬间加分。
5. 二次开发实战:3 个低侵入改造,让课设变成你的原创作品
别满足于“能跑就行”。课设高分的关键是让老师一眼看出“这学生真动手改过”。下面三个改造,每个都不超过 20 行代码,但效果立竿见影:第一个让登录页带学校 logo,第二个让订单号带日期前缀,第三个让邮箱验证加时效限制。我当年就是靠第三个,在答辩时被导师追问了 15 分钟实现细节,最后给了 98 分。
5.1 改造 1:首页加学校 logo,替换轮播图并绑定超链接
index.jsp里轮播图区域是硬编码的<div class="banner">,但web/images/下已有logo.png。我们不改 HTML,而是用 CSS 覆盖:
/* web/css/style.css 新增 */ .banner .logo-placeholder { position: absolute; top: 20px; left: 50px; width: 180px; height: 60px; background: url('../images/logo.png') no-repeat center; background-size: contain; z-index: 10; }然后在index.jsp的<div class="banner">内第一行插入:
<div class="logo-placeholder" onclick="window.open('https://www.yourschool.edu.cn', '_blank')"></div>效果:点击 logo 跳转学校官网,且适配所有屏幕尺寸。比直接
<img src="logo.png">更专业,因为用background-size: contain保证缩放不失真。
5.2 改造 2:订单号生成规则升级,从纯随机改为 “SCHOOL20240520123456”
OrderServlet中订单号生成逻辑在generateOrderId()方法里,原代码是UUID.randomUUID().toString().replace("-", "")。我们改成带学校缩写和时间戳的格式:
// src/com/servlet/OrderServlet.java private String generateOrderId() { String prefix = "SCHOOL"; // ✅ 改为你学校的英文缩写 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String timePart = sdf.format(new Date()); String randomPart = String.valueOf((int)(Math.random() * 100000)); return prefix + timePart + randomPart; // 例:SCHOOL2024052012345678901 }验证方法:下单后查
order表,id字段应为SCHOOL开头。这个改动让订单号可读性强,老师一看就知道“这学生理解了业务标识的含义”。
5.3 改造 3:邮箱验证码加 5 分钟有效期,防止暴力重发
原CheckCode.generateCode()只存验证码,不存时间。我们加一个code_time字段到ServletContext,校验时比对:
// src/com/servlet/RegisterServlet.java 修改校验逻辑 String storedCode = (String) getServletContext().getAttribute(email); Long codeTime = (Long) getServletContext().getAttribute(email + "_time"); if (storedCode == null || codeTime == null) { request.setAttribute("msg", "验证码已过期,请重新获取"); return; } if (System.currentTimeMillis() - codeTime > 5 * 60 * 1000) { // 5分钟 getServletContext().removeAttribute(email); getServletContext().removeAttribute(email + "_time"); request.setAttribute("msg", "验证码已过期,请重新获取"); return; } // 后续校验 storedCode 与 input_code并在generateCode()里同步存时间:
// src/com/util/CheckCode.java public static String generateCode() { String code = String.valueOf((int)(Math.random() * 900000 + 100000)); String email = getCurrentEmailFromRequest(); // ✅ 需从 request 获取当前邮箱 getServletContext().setAttribute(email, code); getServletContext().setAttribute(email + "_time", System.currentTimeMillis()); // ✅ 存时间戳 return code; }技巧:
getCurrentEmailFromRequest()需从RegisterServlet的request传入,不能在CheckCode里直接获取——这是 Servlet 生命周期的常识。从那以后我每次加时效逻辑,都强制走一遍System.currentTimeMillis()与new Date()的对比测试,确保时区一致。希望帮到你。
本文还有配套的精品资源,点击获取