简介:这份资源是大三上学期JavaWeb期末项目的完整源码包,面向正在学习JavaWeb课程、需要完成课设或期末大作业的本科生。项目以甜品管理系统为主题,涵盖用户管理、商品管理、订单处理、购物车等典型电商业务模块,适合用来理解Servlet、JSP与DAO分层开发的整体流程。压缩包共197个文件,约6.05MB,其中46个java源文件与65个class编译文件构成核心业务逻辑,24个jsp页面负责前端展示,另有36个png及少量gif、jpg图片资源、6个jar依赖包,以及sql建表脚本、css样式、js脚本和xml配置等辅助文件,结构完整、层次分明。已有98人学习下载。读者可直接获得一套经过测试、可运行的课设参考方案,对照源码梳理Servlet跳转、数据库连接与前后端交互的实现思路,也可在此基础上修改主题或扩展功能,用于课程答辩与二次开发。
1. 大三上学期 JavaWeb 期末项目:甜品管理系统到底在考什么
很多同学拿到「甜品管理系统」这个题目,第一反应是去搜一套完整源码,改个名字交上去。但真正答辩时被问「你的库存扣减为什么放在 Service 层而不是 Controller」就答不上来。这个项目的本质,是用一个业务闭环(甜品上架 → 用户下单 → 库存扣减 → 订单状态流转)把 JavaWeb 的核心链路串起来:Servlet 或 Spring MVC 接请求、Service 写业务、DAO 落库、JSP 或前端页面渲染。它适合正在学 JavaWeb、需要交期末作业、又想顺便把 SSM 或 SpringBoot 跑通的人。下面我按「能跑起来 → 能讲清楚 → 能改得动」的顺序拆一遍,重点放在配置、参数和踩坑上,而不是贴一堆没注释的代码。
2. 环境搭建与项目骨架:从 IDEA 到能跑通第一个接口
2.1 JDK、Tomcat、Maven 的版本对齐
JavaWeb 项目翻车,八成死在版本上。我一般会先确认三件事:JDK 用 8 还是 11、Tomcat 用 8.5 还是 9、Maven 的编译插件版本。这三者不匹配,会出现「编译通过但启动 404」或者「class 文件版本不对」的玄学问题。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | 学校机房多为 8,SpringBoot 2.7 以下建议 8 |
| Tomcat | 8.5 / 9.0 | Tomcat 10 把 javax 换成 jakarta,老项目直接崩 |
| Maven | 3.6+ | 低于 3.5 拉依赖容易超时 |
| MySQL | 5.7 / 8.0 | 8.0 要改驱动类和时区参数 |
注意:如果你的项目用的是javax.servlet包,就绝对不要用 Tomcat 10,否则启动报ClassNotFoundException。这是最常见的「IDEA 运行 JavaWeb 项目配置」翻车点。
2.2 用 Maven 建 Web 骨架的最小 pom
不用 IDE 模板,手写一个最小pom.xml反而更清楚依赖从哪来。下面这段是我常用的骨架,打包方式必须是war,否则 Tomcat 不认。
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.dessert</groupId> <artifactId>dessert-mgr</artifactId> <version>1.0</version> <packaging>war</packaging> <!-- Web 项目必须 war --> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Servlet API,scope 必须是 provided,Tomcat 已自带 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- MySQL 驱动,8.0 用 com.mysql.cj.jdbc.Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <!-- JSP 支持 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> </dependencies> </project>逻辑说明:provided表示这个依赖由运行环境(Tomcat)提供,不打进 war 包,否则会和 Tomcat 自带的 Servlet 冲突,报NoSuchMethodError。参数上,maven.compiler.source/target要和你的 JDK 一致,用 JDK 11 编译却写 8,可能出现Unsupported class file major version。
2.3 目录结构与 web.xml 的取舍
标准 Maven Web 目录是src/main/webapp,里面放WEB-INF/web.xml和 JSP。Servlet 3.0 以后可以用@WebServlet注解代替 web.xml 配置,但期末项目里老师往往要求看到 web.xml,所以我一般两个都留:注解写映射,web.xml 写欢迎页和编码过滤器。
<!-- web.xml 里配置字符编码过滤器,解决中文乱码 --> <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")。这一步不做,甜品名称里的中文存进数据库就是问号,答辩演示时非常尴尬。
3. 数据库设计与甜品管理系统的核心表
3.1 四张表撑起整个业务闭环
甜品管理系统的表不用多,但关系要清楚。我一般设计四张:用户表、甜品表、订单表、订单明细表。订单和甜品是多对多,靠订单明细拆开。
CREATE TABLE dessert ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, category VARCHAR(20), status TINYINT DEFAULT 1 -- 1 上架 0 下架 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total DECIMAL(10,2), status TINYINT DEFAULT 0, -- 0 待支付 1 已支付 2 已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dessert_id INT NOT NULL, num INT NOT NULL, price DECIMAL(10,2) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:utf8mb4而不是utf8,因为甜品名可能有 emoji 或生僻字;DECIMAL(10,2)存金额,绝不能用FLOAT,否则 0.1+0.2 会出现精度误差,对账时对不上。stock字段是后面并发扣减的关键,先记住它。
3.2 JDBC 工具类的三个必调参数
不用框架的话,写一个DBUtil拿连接。URL 里三个参数最容易漏:useSSL、serverTimezone、characterEncoding。
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/dessert?useSSL=false" + "&serverTimezone=Asia/Shanghai" + // 8.0 驱动必须指定时区 "&characterEncoding=utf8" + // 防止中文乱码 "&allowPublicKeyRetrieval=true"; // 8.0 首次连接需要 private static final String USER = "root"; private static final String PWD = "123456"; public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }逻辑说明:serverTimezone不写,MySQL 8.0 会报The server time zone value is unrecognized;allowPublicKeyRetrieval=true是 8.0 默认加密方式导致的,不加会报Public Key Retrieval is not allowed。这两个报错在「JavaWeb 项目完整案例 MySQL」里出现频率极高。
3.3 DAO 层用 PreparedStatement 防注入
甜品搜索、登录这些带参数的查询,一律用PreparedStatement,不要用字符串拼接。
public Dessert findByName(String name) { String sql = "SELECT * FROM dessert WHERE name LIKE ?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + name + "%"); // 参数化,防 SQL 注入 ResultSet rs = ps.executeQuery(); if (rs.next()) { Dessert d = new Dessert(); d.setId(rs.getInt("id")); d.setName(rs.getString("name")); d.setPrice(rs.getBigDecimal("price")); return d; } } catch (SQLException e) { e.printStackTrace(); } return null; }参数说明:setString的索引从 1 开始,不是 0,这是新手最常见的下标越界来源。try-with-resources保证连接自动关闭,否则连接池耗尽后整个系统卡死。
4. 下单与库存扣减:业务层最容易出 bug 的地方
4.1 库存扣减必须放在事务里
下单流程是:插入订单 → 插入订单明细 → 扣减库存。这三步要么全成功,要么全回滚。如果扣库存失败但订单已插入,就会出现「有订单没货」的脏数据。
public boolean placeOrder(int userId, int dessertId, int num) { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 开启事务 // 1. 扣库存,带 stock >= num 条件,防止超卖 String sql1 = "UPDATE dessert SET stock = stock - ? " + "WHERE id = ? AND stock >= ?"; PreparedStatement ps1 = conn.prepareStatement(sql1); ps1.setInt(1, num); ps1.setInt(2, dessertId); ps1.setInt(3, num); if (ps1.executeUpdate() == 0) { conn.rollback(); // 库存不足,回滚 return false; } // 2. 插入订单 String sql2 = "INSERT INTO orders(user_id, total, status) VALUES(?,?,0)"; PreparedStatement ps2 = conn.prepareStatement(sql2, Statement.RETURN_GENERATED_KEYS); ps2.setInt(1, userId); ps2.setBigDecimal(2, BigDecimal.ZERO); ps2.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) {} return false; } finally { try { if (conn != null) conn.setAutoCommit(true); } catch (SQLException e) {} } }逻辑说明:WHERE id = ? AND stock >= ?这一句是防超卖的核心,把判断和扣减合成一条原子 SQL,避免「先查再扣」之间的并发窗口。参数上,setAutoCommit(false)开启事务后,任何一步失败都要rollback,成功才commit。RETURN_GENERATED_KEYS用来拿自增订单 ID,方便插入明细。
4.2 订单状态流转的边界
订单状态从 0(待支付)到 1(已支付)或 2(已取消),只能单向流转。取消订单时要把库存加回去,这一步很多人漏掉。
public boolean cancelOrder(int orderId) { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 只有待支付订单能取消 String sql = "UPDATE orders SET status = 2 WHERE id = ? AND status = 0"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, orderId); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 已支付或已取消,不能重复操作 } // 回补库存 String sql2 = "UPDATE dessert d JOIN order_item oi " + "ON d.id = oi.dessert_id SET d.stock = d.stock + oi.num " + "WHERE oi.order_id = ?"; conn.prepareStatement(sql2).executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) {} return false; } finally { try { if (conn != null) conn.setAutoCommit(true); } catch (SQLException e) {} } }参数说明:status = 0这个条件保证幂等,重复点击取消不会把库存加两次。JOIN 更新是 MySQL 特有的写法,Oracle 不支持,如果老师要求跨库,就拆成两条 SQL。
4.3 分页查询的 limit 参数
甜品列表要分页,LIMIT offset, size里 offset 是「跳过多少条」,不是页码。
public List<Dessert> page(int pageNo, int pageSize) { int offset = (pageNo - 1) * pageSize; // 页码从 1 开始 String sql = "SELECT * FROM dessert WHERE status = 1 " + "ORDER BY id DESC LIMIT ?, ?"; // ... setInt(1, offset); setInt(2, pageSize); }注意:pageNo从 1 开始,如果前端传 0,offset 变成负数,SQL 直接报错。我一般会在 Service 层做一次Math.max(1, pageNo)兜底。
5. 避坑与排查:答辩前一定要过的五道关
5.1 启动报 404,但控制台没报错
现象:Tomcat 启动成功,访问http://localhost:8080/dessert/list返回 404。 原因:多半是war包名和访问路径不一致,或者@WebServlet("/list")少了项目名。IDEA 里 Application context 默认是/,但 war 包名可能是dessert-mgr。 解决:在 IDEA 的 Run Configuration → Deployment 里把 Application context 设成/,或者访问时带上 war 包名。另外确认web.xml的metadata-complete不是true,否则注解不生效。
5.2 中文乱码,数据库里全是问号
现象:页面提交「芒果班戟」,数据库存成???。 原因:三个环节任一没设 UTF-8——请求编码、数据库连接编码、表字符集。 解决:Filter 里setCharacterEncoding("UTF-8");JDBC URL 加characterEncoding=utf8;建表用utf8mb4。三处都改完重启,缺一不可。
5.3 库存扣成负数
现象:并发测试时库存变成 -1。 原因:用了「先 SELECT 查库存,再 UPDATE 扣减」的写法,两个请求同时查到库存 1,都去扣。 解决:改成UPDATE ... WHERE stock >= num的原子写法,判断executeUpdate()返回值是否为 0。这是「JavaWeb 项目完整案例」里最值得写进答辩的细节。
5.4 连接池耗尽,系统越用越卡
现象:跑一段时间后所有请求卡住,报Too many connections。 原因:Connection 没关闭,或者异常路径下漏了close。 解决:统一用try-with-resources,或者在finally里关闭。如果用了 Druid 或 C3P0,检查maxActive配置,默认 8 太小,演示时并发一高就满。
5.5 时间字段差 8 小时
现象:订单创建时间比实际早 8 小时。 原因:JDBC URL 没写serverTimezone,或者写了UTC。 解决:改成serverTimezone=Asia/Shanghai。如果已经存了错数据,用UPDATE orders SET create_time = DATE_ADD(create_time, INTERVAL 8 HOUR)修回来。
6. 让答辩加分的两个进阶技巧
6.1 用拦截器做登录校验,而不是每个 Servlet 都写一遍
期末项目里,登录校验如果散落在每个 Servlet 里,代码又臭又长。我一般写一个LoginFilter,拦/admin/*路径,没登录就跳登录页。
@WebFilter("/admin/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; // 不放行 } chain.doFilter(req, resp); // 已登录,放行 } }逻辑说明:getSession().getAttribute("user")是登录成功后存进去的。参数上,request.getContextPath()拿到项目名,避免硬编码路径导致部署后跳转 404。这个技巧能让你的代码结构明显比同学干净,答辩时老师一问「你怎么做权限控制」就有话讲。
6.2 用 Postman 或浏览器控制台验证接口,而不是只点页面
演示前,我习惯用 Postman 直接打接口,确认返回 JSON 或跳转正确。比如下单接口,构造userId=1&dessertId=3&num=2,看返回和数据库是否一致。这样能提前发现「页面能点但接口报错」的问题。另外,浏览器 F12 的 Network 面板看请求参数和响应码,比猜快得多。
6.3 一个我踩过的坑:别在 JSP 里写业务逻辑
大三时我图省事,在 JSP 里直接写SELECT查库存,结果页面一多,改一个字段要翻十几个文件。后来全部挪到 Servlet + Service,JSP 只负责展示。血泪经验:JSP 里只放${}和 JSTL 标签,任何Class.forName或 SQL 都不该出现。这样后期加一个「甜品分类筛选」功能,只改 Service 和 DAO,页面几乎不动。
最后说个习惯:每次改完代码,先mvn clean package打一次 war,确认没有编译错误再部署。我见过太多人 IDEA 里跑得好好的,一打包就缺依赖。希望帮到你。
本文还有配套的精品资源,点击获取