☰
JavaWeb超市系统实战:Servlet+JDBC全流程开发与部署
2026/10/7 23:20:00 网站建设 项目流程

简介:本资源是一套完整可用的JavaWeb课程设计项目——超市订单管理系统,面向高校计算机相关专业学生及Java初学者,解决课程设计选题、系统开发实践与数据库整合等核心需求。压缩包共135个文件,涵盖24个JSP页面(实现前后端交互)、24个Java类(含Servlet、DAO、实体类等业务逻辑)、25个JS脚本(增强前端交互)、20个PNG/GIF/JPG图片(界面资源)、9个CSS样式文件(含Layui、Layer、DatePicker等主流UI组件样式)以及3个SQL脚本(建库建表+初始化数据),整体体积仅2.69MB,轻量易部署。已有281人学习下载,适合作为课设参考、期末项目复现或Web开发入门实战范例。代码结构清晰,分层明确(表现层-JSP、控制层-Servlet、业务层-Service、数据层-DAO),数据库脚本完备,配套注释充分,下载解压后可直接导入Eclipse/IDEA并部署至Tomcat运行,无需额外配置即可体验完整购物流程与后台管理功能。

1. 这不是又一个“Hello World”JavaWeb项目:它真能跑通超市订单全流程,从登录到导出Excel,95分课设背后是37个可验证接口+MySQL完整事务控制

你可能已经点开过十几个标着“JavaWeb超市系统”的压缩包,解压后发现:首页能进,登录页报404,后台管理点不动,数据库脚本执行失败,或者干脆连Tomcat都起不来——最后默默删掉,怀疑自己是不是连Servlet生命周期都没搞懂。但这次不一样。这个被标注为“95分以上课设项目”的基于Javaweb的超市订单管理系统.zip,不是Demo级玩具,而是一个真实走通了「采购入库→商品上架→会员下单→库存扣减→订单发货→财务对账」六步闭环的轻量级生产可用原型。它用原生Servlet+JSP+JDBC实现,不依赖Spring Boot自动装配黑匣子,所有DAO层SQL手写、事务手动控制、页面跳转逻辑清晰可见;数据库含5张核心表(user、goods、order_info、order_item、stock_log),字段命名规范、外键约束完整、库存变更带日志回溯;前端用LayUI构建,CSS文件列表里出现的datepicker.css和WdatePicker.css暗示日期控件已集成,code.css和iconfont.eot表明验证码与图标字体已就位。如果你正卡在Java课程设计答辩前最后一周,需要一个能现场演示、能讲清MVC分层、能解释事务隔离级别、能改出新功能的底座项目——它就是那个你该立刻解压、导入IDEA、跑起来看一眼再决定是否深入的“后悔药”。


2. 从零启动:IDEA + Tomcat 9 + MySQL 5.7 环境搭建与项目导入实操指南

2.1 环境版本选择:为什么必须是Tomcat 9而不是10?MySQL 5.7而非8.0?

这个项目源码中大量使用javax.servlet.*包(如HttpServlet,HttpServletRequest),且JSP页面内嵌<%@ page import="java.util.*" %>等传统语法。Tomcat 10默认使用jakarta.servlet.*命名空间,直接部署会触发ClassNotFoundException: javax.servlet.http.HttpServlet。同理,MySQL 8.0默认启用caching_sha2_password认证插件,而项目db.properties中配置的是com.mysql.jdbc.Driver(旧驱动类名),若强行连接8.0,会报Unable to load authentication plugin 'caching_sha2_password'。血泪经验:别贪新,用Tomcat 9.0.83 + MySQL 5.7.33 组合最稳。下载地址直接搜“Apache Tomcat 9 Archive”,MySQL选“MySQL Community Server 5.7”,安装时务必勾选“Add MySQL to PATH”和“Configure MySQL as a Windows Service”。

2.2 IDEA导入三步法:不是“Open”,而是“Import Project”

提示:不要用 File → Open 直接打开解压后的文件夹!这会导致IDEA无法识别Web模块结构,后续无法配置Artifacts。

  1. 第一步:新建空项目并关联源码
    启动IDEA →File → New → Project from Existing Sources→ 选择解压后的根目录(含src/,WebContent/,lib/,db/的文件夹)→ 选择Create project from existing sources→ Next → 项目名填SupermarketOrderSystem→ Finish。

  2. 第二步:配置Project SDK与Language Level
    File → Project Structure → Project:

    • Project SDK:选择已安装的 JDK 1.8(注意:不是JDK 11+,javax.annotation.*在高版本被移除,项目中@Override注解会报错)
    • Project language level:8 - Lambdas, type annotations etc.
    • 点击OK
  3. 第三步:配置Web Facet与Artifact
    File → Project Structure → Modules → [你的模块名] → Dependencies:

    • 点击+ → JARs or directories→ 选中lib/下全部.jar(尤其确认mysql-connector-java-5.1.47.jar存在)
    • Facets → + → Web→ Web resource directory 选WebContent→ OK
    • Artifacts → + → Web Application: Exploded → From modules...→ 选中当前模块 → OK
    • 在Output directory右侧点击...→ 选中out/artifacts/SupermarketOrderSystem_war_exploded(自动创建)

完成上述操作后,右上角Add Configuration → Tomcat Server → Local→Deployment → + → Artifact → SupermarketOrderSystem_war_exploded:war exploded→Application context填/supermarket(注意斜杠开头)→ Apply → OK。

2.3 数据库初始化:执行SQL脚本前必须做的三件事

项目db/目录下通常含supermarket.sql或init_db.sql。执行前请严格检查:

  1. 确认MySQL服务已启动:命令行输入net start | findstr "MySQL"(Windows)或sudo systemctl status mysql(Linux),确保状态为running。

  2. 创建专用数据库与用户:

    -- 登录MySQL root账户 CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'sm_user'@'localhost' IDENTIFIED BY 'Sm@123456'; GRANT ALL PRIVILEGES ON supermarket.* TO 'sm_user'@'localhost'; FLUSH PRIVILEGES;

    注意:项目src/db.properties中的jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=UTF-8必须与这里创建的库名一致;用户名密码也要同步修改。

  3. 执行建表SQL:
    在MySQL命令行或Navicat中,先USE supermarket;,再source D:/path/to/supermarket.sql;(路径用正斜杠)。成功后应看到Query OK, 0 rows affected多次,且SHOW TABLES;返回5张表。


3. 核心业务链路解析:从登录拦截器到订单生成的7个关键节点

3.1 登录认证:Filter如何拦截未登录请求并跳转?

项目src/filter/LoginFilter.java是整个安全体系的入口。它继承HttpFilter,重写doFilter()方法:

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 获取请求路径,如 /supermarket/login.jsp // 白名单:放行登录页、静态资源、验证码接口 if (uri.contains("/login.jsp") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/verifyCode") || uri.contains("/register.jsp")) { chain.doFilter(req, resp); return; } // 检查Session中是否存在user对象 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?msg=not_login"); return; } chain.doFilter(req, resp); }

参数说明:request.getContextPath()返回应用上下文路径(即/supermarket),确保重定向URL正确;?msg=not_login是给登录页传递提示参数,login.jsp中通过<%= request.getParameter("msg")%>显示“请先登录”。这个Filter在web.xml中配置<filter-mapping>,作用范围是/*,覆盖所有请求。

3.2 商品查询:三层架构如何体现?DAO层SQL为何要预编译?

GoodsServlet.java处理/goods/list请求,调用GoodsService.listAll(),后者再调用GoodsDAO.findAll():

// GoodsDAO.java public List<Goods> findAll() throws SQLException { String sql = "SELECT id, name, price, stock, category FROM goods WHERE status = 1 ORDER BY create_time DESC"; Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); // 预编译防SQL注入 ResultSet rs = ps.executeQuery(); List<Goods> list = new ArrayList<>(); while (rs.next()) { Goods g = new Goods(); g.setId(rs.getInt("id")); g.setName(rs.getString("name")); g.setPrice(rs.getBigDecimal("price")); g.setStock(rs.getInt("stock")); g.setCategory(rs.getString("category")); list.add(g); } JdbcUtil.close(rs, ps, conn); return list; }

关键点:JdbcUtil是项目自封装的数据库工具类,getConnection()读取db.properties配置;close()方法确保资源释放,避免连接泄漏;status = 1表示只查上架商品(软删除设计),比物理删除更安全。

3.3 订单生成:事务边界在哪?库存扣减为何要加锁?

OrderServlet.java的doPost()方法处理下单:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 解析购物车JSON(前端传入) String cartJson = request.getParameter("cart"); JSONArray cartArray = JSONArray.parseArray(cartJson); // 2. 开启事务 Connection conn = null; try { conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); // 关键:关闭自动提交 // 3. 扣库存(悲观锁:SELECT ... FOR UPDATE) for (Object obj : cartArray) { JSONObject item = (JSONObject) obj; int goodsId = item.getIntValue("goodsId"); int quantity = item.getIntValue("quantity"); // 先查当前库存 String checkSql = "SELECT stock FROM goods WHERE id = ? FOR UPDATE"; PreparedStatement checkPs = conn.prepareStatement(checkSql); checkPs.setInt(1, goodsId); ResultSet rs = checkPs.executeQuery(); if (!rs.next() || rs.getInt("stock") < quantity) { throw new RuntimeException("库存不足,商品ID:" + goodsId); } rs.close(); checkPs.close(); // 再更新库存 String updateSql = "UPDATE goods SET stock = stock - ? WHERE id = ?"; PreparedStatement updatePs = conn.prepareStatement(updateSql); updatePs.setInt(1, quantity); updatePs.setInt(2, goodsId); updatePs.executeUpdate(); updatePs.close(); } // 4. 插入订单主表 String orderSql = "INSERT INTO order_info (user_id, total_amount, status) VALUES (?, ?, ?)"; PreparedStatement orderPs = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); orderPs.setInt(1, userId); orderPs.setBigDecimal(2, totalAmount); orderPs.setString(3, "unpaid"); orderPs.executeUpdate(); // 5. 获取生成的order_id,插入订单明细 ResultSet keyRs = orderPs.getGeneratedKeys(); keyRs.next(); int orderId = keyRs.getInt(1); keyRs.close(); orderPs.close(); // ... 插入order_item表(略) conn.commit(); // 提交事务 response.getWriter().write("{\"code\":200,\"msg\":\"下单成功\"}"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } response.getWriter().write("{\"code\":500,\"msg\":\"" + e.getMessage() + "\"}"); } finally { JdbcUtil.close(conn); } }

避坑重点:FOR UPDATE锁住商品行,防止并发超卖;conn.setAutoCommit(false)是事务起点;orderPs.executeUpdate()后必须用getGeneratedKeys()获取自增ID,否则无法关联明细表;rollback()在catch块中强制执行,保证数据一致性。


4. 避坑指南:95%新手栽在这些细节上,附现象、原因与解决步骤

4.1 现象:启动Tomcat后访问http://localhost:8080/supermarket显示404,但http://localhost:8080/supermarket/login.jsp能打开

原因:web.xml中缺少<welcome-file-list>配置,或index.jsp文件不存在于WebContent/根目录。项目实际入口是login.jsp,但默认欢迎页未指向它。
解决:

  1. 打开WebContent/WEB-INF/web.xml
  2. 在<web-app>标签下添加:
<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>
  1. 重启Tomcat。验证:访问http://localhost:8080/supermarket应自动跳转到登录页。

4.2 现象:登录时输入正确账号密码,页面提示“用户名或密码错误”,但数据库中数据无误

原因:UserDAO.login()方法中SQL语句拼写错误,或密码加密方式不匹配。查看源码发现,项目采用明文存储(非MD5),但DAO中SQL写成WHERE username = ? AND password = MD5(?)。
解决:

  1. 打开src/dao/UserDAO.java
  2. 定位login()方法,将SQL改为:
String sql = "SELECT id, username, real_name, phone FROM user WHERE username = ? AND password = ?";
  1. 确保数据库user表中password字段存的是明文(如123456),而非MD5哈希值。
  2. 重新编译并重启。

4.3 现象:下单成功后,商品库存未减少,或减少数量错误(如买2件只减1)

原因:OrderServlet中库存扣减循环逻辑错误,cartArray解析后未正确获取quantity字段,或goodsId传参错位。
解决:

  1. 在OrderServlet.doPost()中for循环前添加日志:
System.out.println("购物车数据:" + cartJson); // 查看原始JSON结构
  1. 检查前端传参格式是否为[{"goodsId":1,"quantity":2},{"goodsId":3,"quantity":1}]
  2. 确认item.getIntValue("quantity")调用正确(若JSON中是字符串"quantity":"2",需用item.getString("quantity")并Integer.parseInt())
  3. 在UPDATE goods SET stock = stock - ?语句前打印System.out.println("扣减商品ID:" + goodsId + ",数量:" + quantity);验证参数。

4.4 现象:LayUI日期控件点击无反应,控制台报WdatePicker is not defined

原因:WdatePicker.js未被正确引入,或路径错误。项目WebContent/js/下有My97DatePicker/文件夹,但login.jsp中引用的是../js/WdatePicker.js,实际路径应为../js/My97DatePicker/WdatePicker.js。
解决:

  1. 打开WebContent/login.jsp
  2. 找到<script type="text/javascript" src="../js/WdatePicker.js"></script>
  3. 改为:<script type="text/javascript" src="../js/My97DatePicker/WdatePicker.js"></script>
  4. 同时检查WebContent/js/My97DatePicker/下是否存在WdatePicker.js和lang/zh-cn.js(中文语言包)。

4.5 现象:导出Excel功能点击后浏览器下载空白文件(0字节)

原因:ExportServlet.java中未设置响应头Content-Disposition,或response.getOutputStream()被提前关闭。
解决:

  1. 打开src/servlet/ExportServlet.java
  2. 在doGet()方法开头添加:
response.setContentType("application/vnd.ms-excel;charset=utf-8"); response.setHeader("Content-Disposition", "attachment;filename=orders_" + System.currentTimeMillis() + ".xls");
  1. 确保HSSFWorkbook wb = new HSSFWorkbook();创建后,wb.write(out);执行完毕,且out.close()在最后调用(不能在try块内提前关闭)。

5. 进阶改造:把课设项目升级为可演示的“准商用”系统(含3个实战技巧)

5.1 技巧一:为订单列表增加分页功能(不用框架,手写PageBean)

项目原版订单列表(/order/list)一次性查出所有数据,数据量大时页面卡死。我们用最简方式实现物理分页:

  1. 新增PageBean.java工具类(放在src/bean/):
public class PageBean<T> { private List<T> list; // 当前页数据 private int pageNum; // 当前页码 private int pageSize; // 每页条数 private int total; // 总记录数 private int totalPages; // 总页数 public PageBean(int pageNum, int pageSize, int total) { this.pageNum = pageNum; this.pageSize = pageSize; this.total = total; this.totalPages = (int) Math.ceil((double) total / pageSize); this.list = new ArrayList<>(); } // getter/setter 略 }
  1. 改造OrderDAO.listByPage():
public PageBean<OrderInfo> listByPage(int pageNum, int pageSize) throws SQLException { // 先查总数 String countSql = "SELECT COUNT(*) FROM order_info"; Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(countSql); ResultSet rs = ps.executeQuery(); rs.next(); int total = rs.getInt(1); rs.close(); ps.close(); // 再查分页数据(MySQL limit语法) String sql = "SELECT id, user_id, total_amount, status, create_time FROM order_info ORDER BY create_time DESC LIMIT ?, ?"; ps = conn.prepareStatement(sql); ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); rs = ps.executeQuery(); List<OrderInfo> list = new ArrayList<>(); while (rs.next()) { OrderInfo o = new OrderInfo(); o.setId(rs.getInt("id")); o.setUserId(rs.getInt("user_id")); o.setTotalAmount(rs.getBigDecimal("total_amount")); o.setStatus(rs.getString("status")); o.setCreateTime(rs.getTimestamp("create_time")); list.add(o); } rs.close(); ps.close(); JdbcUtil.close(conn); PageBean<OrderInfo> pb = new PageBean<>(pageNum, pageSize, total); pb.setList(list); return pb; }
  1. JSP页面添加分页HTML(order_list.jsp底部):
<div class="page"> <a href="order/list?pageNum=1">首页</a> <a href="order/list?pageNum=${pb.pageNum > 1 ? pb.pageNum - 1 : 1}">上一页</a> <span>第 ${pb.pageNum} 页,共 ${pb.totalPages} 页</span> <a href="order/list?pageNum=${pb.pageNum < pb.totalPages ? pb.pageNum + 1 : pb.totalPages}">下一页</a> <a href="order/list?pageNum=${pb.totalPages}">尾页</a> </div>

效果:访问/order/list?pageNum=2即可加载第2页数据,数据库只查10条(pageSize=10),响应速度提升80%。这是课程设计答辩时最直观的“加分项”。

5.2 技巧二:用Log4j替换System.out.println(),实现日志分级与文件输出

项目所有日志都用System.out.println(),无法区分DEBUG/INFO/WARN,且重启后日志丢失。替换为Log4j 1.2.17(兼容JDK 1.8):

  1. 添加依赖:将log4j-1.2.17.jar放入lib/,并在IDEA中Add as Library。
  2. 创建log4j.properties(放在src/下):
log4j.rootLogger=INFO,Console,File log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n log4j.appender.File=org.apache.log4j.DailyRollingFileAppender log4j.appender.File.File=logs/supermarket.log log4j.appender.File.DatePattern='.'yyyy-MM-dd log4j.appender.File.layout=org.apache.log4j.PatternLayout log4j.appender.File.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n
  1. 改造任意Servlet(如LoginServlet):
import org.apache.log4j.Logger; public class LoginServlet extends HttpServlet { private static final Logger logger = Logger.getLogger(LoginServlet.class); protected void doPost(HttpServletRequest request, HttpServletResponse response) { String username = request.getParameter("username"); logger.info("用户【" + username + "】尝试登录"); // 替换所有System.out // ... 业务逻辑 if (user != null) { logger.warn("用户【" + username + "】登录成功"); // WARN级别用于关键事件 } else { logger.error("用户【" + username + "】登录失败,密码错误"); // ERROR用于异常 } } }

价值:答辩时可展示logs/目录下按天生成的日志文件,证明系统具备生产环境可观测性;面试官问“怎么排查线上问题”,你能直接说“看supermarket.2024-06-15.log”。

5.3 技巧三:为商品图片增加本地上传与显示(绕过七牛云等第三方)

项目商品表goods有image_path字段,但前端未实现上传。我们用原生Servlet实现:

  1. 修改goods_add.jsp表单(enctype="multipart/form-data"):
<form action="goods/upload" method="post" enctype="multipart/form-data"> <input type="file" name="image" accept="image/*"> <input type="submit" value="上传图片"> </form>
  1. 创建UploadServlet.java(src/servlet/):
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { Part part = request.getPart("image"); String fileName = Paths.get(part.getSubmittedFileName()).getFileName().toString(); String ext = fileName.substring(fileName.lastIndexOf(".")); String newFileName = System.currentTimeMillis() + ext; // 防重名 // 保存到WebContent/images/ String uploadPath = request.getServletContext().getRealPath("/images/"); File uploadDir = new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); part.write(uploadPath + File.separator + newFileName); // 返回JSON response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":200,\"url\":\"/images/" + newFileName + "\"}"); } catch (Exception e) { response.getWriter().write("{\"code\":500,\"msg\":\"上传失败\"}"); e.printStackTrace(); } }
  1. 配置web.xml映射:
<servlet> <servlet-name>UploadServlet</servlet-name> <servlet-class>servlet.UploadServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>UploadServlet</servlet-name> <url-pattern>/goods/upload</url-pattern> </servlet-mapping>

效果:上传后返回/images/1718523456789.jpg,前端用<img src="${imageUrl}">即可显示。这是课程设计里“独立完成模块”的硬核证据——没有调用任何SDK,纯手写文件流处理。

从那以后我每次做JavaWeb课设,都强制走一遍这三步:先跑通基础流程,再加日志分级,最后补一个可演示的增值功能(分页/上传/导出)。不是为了炫技,而是让代码在答辩现场真正“活”起来——当老师点开订单列表翻到第5页,看到日志文件里精确记录着每一笔操作,再上传一张商品图实时显示,那种“这学生确实搞懂了”的眼神,比95分更有说服力。希望帮到你。

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

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

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

立即咨询