ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JavaWeb甜品管理系统实战:从环境搭建到库存扣减的完整案例

JavaWeb甜品管理系统实战:从环境搭建到库存扣减的完整案例 简介这份资源是大三上学期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 文件版本不对」的玄学问题。组件推荐版本说明JDK8 或 11学校机房多为 8SpringBoot 2.7 以下建议 8Tomcat8.5 / 9.0Tomcat 10 把 javax 换成 jakarta老项目直接崩Maven3.6低于 3.5 拉依赖容易超时MySQL5.7 / 8.08.0 要改驱动类和时区参数注意如果你的项目用的是javax.servlet包就绝对不要用 Tomcat 10否则启动报ClassNotFoundException。这是最常见的「IDEA 运行 JavaWeb 项目配置」翻车点。2.2 用 Maven 建 Web 骨架的最小 pom不用 IDE 模板手写一个最小pom.xml反而更清楚依赖从哪来。下面这段是我常用的骨架打包方式必须是war否则 Tomcat 不认。project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIdcom.dessert/groupId artifactIddessert-mgr/artifactId version1.0/version packagingwar/packaging !-- Web 项目必须 war -- properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet APIscope 必须是 providedTomcat 已自带 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- MySQL 驱动8.0 用 com.mysql.cj.jdbc.Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- JSP 支持 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/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-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/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 下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明utf8mb4而不是utf8因为甜品名可能有 emoji 或生僻字DECIMAL(10,2)存金额绝不能用FLOAT否则 0.10.2 会出现精度误差对账时对不上。stock字段是后面并发扣减的关键先记住它。3.2 JDBC 工具类的三个必调参数不用框架的话写一个DBUtil拿连接。URL 里三个参数最容易漏useSSL、serverTimezone、characterEncoding。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/dessert?useSSLfalse serverTimezoneAsia/Shanghai // 8.0 驱动必须指定时区 characterEncodingutf8 // 防止中文乱码 allowPublicKeyRetrievaltrue; // 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 unrecognizedallowPublicKeyRetrievaltrue是 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 ListDessert 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 开始如果前端传 0offset 变成负数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 加characterEncodingutf8建表用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。 解决改成serverTimezoneAsia/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 或跳转正确。比如下单接口构造userId1dessertId3num2看返回和数据库是否一致。这样能提前发现「页面能点但接口报错」的问题。另外浏览器 F12 的 Network 面板看请求参数和响应码比猜快得多。6.3 一个我踩过的坑别在 JSP 里写业务逻辑大三时我图省事在 JSP 里直接写SELECT查库存结果页面一多改一个字段要翻十几个文件。后来全部挪到 Servlet ServiceJSP 只负责展示。血泪经验JSP 里只放${}和 JSTL 标签任何Class.forName或 SQL 都不该出现。这样后期加一个「甜品分类筛选」功能只改 Service 和 DAO页面几乎不动。最后说个习惯每次改完代码先mvn clean package打一次 war确认没有编译错误再部署。我见过太多人 IDEA 里跑得好好的一打包就缺依赖。希望帮到你。本文还有配套的精品资源点击获取
返回列表