ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSP蛋糕商城系统:JavaWeb全流程实战与并发库存控制

JSP蛋糕商城系统:JavaWeb全流程实战与并发库存控制 简介这是一套面向JavaWeb初学者与课程设计者的完整电商实战项目资源聚焦蛋糕店线上销售场景提供从界面交互到后台管理的全流程解决方案。资源包含408个文件总大小21.93MB涵盖55个核心Java业务类、58个JSP页面、110个编译后Class文件、20个XML配置及1个SQL数据库脚本分别支撑MVC分层架构、动态网页渲染、数据持久化与系统初始化其中GoodsDao、OrderDao等DAO类与AdminGoodsAddServlet等Servlet文件体现了典型JavaWeb开发范式。已有3238人学习下载读者可直接导入IDE运行调试获得可部署的蛋糕商城系统源码、结构清晰的数据库表设计含用户、商品、订单三类主表、管理员后台全功能模块商品/分类/订单管理及配套静态资源82张商品图、14个JS交互脚本是理解ServletJSPMySQL协同开发的优质教学案例。1. 这不是“又一个JSP练手项目”它是一套能直接上架蛋糕店、跑通从首页到发货全流程的JavaWeb销售系统你可能已经删过三版“用户登录商品列表”的JSP Demo——页面能刷出来但数据库连不上、图片路径404、订单状态死在“待支付”不动。而这个“基于JSP的蛋糕商城系统”我拆包实测过它不是教学玩具是能立刻部署到真实小蛋糕店用的最小可行系统MVP。它自带完整SQL建表脚本含goods、category、orders、user四张核心表所有DAO层方法都已实现增删改查事务控制Servlet层严格遵循MVC分层AdminGoodsAddServlet.class里连文件上传路径、缩略图生成逻辑都写死了本地相对路径就连前端JSP里的img src都用request.getContextPath()动态拼接避免硬编码导致迁移翻车。适合两类人一是刚学完Servlet生命周期、正卡在“怎么把数据库数据塞进表格里”的Java新手跟着它能把JDBC连接池、request.getParameter()取值、session存购物车这些黑匣子全摸透二是需要快速交付线下门店线上化的小团队拿过去改改logo、换换配色、填进真实商品图3小时内就能让老板在后台新增一款“6寸草莓慕斯”并实时看到订单弹窗。别被标题里“源码SQL”骗了——它真正值钱的是那套经过真实下单压测的库存扣减逻辑OrderDao.class里用SELECT FOR UPDATE锁行而不是简单UPDATE stockstock-1这步避开了并发下单超卖的血泪坑。2. 从零部署用最简环境跑通蛋糕商城的完整链路Tomcat 8 MySQL 5.7 JDK 82.1 环境准备为什么必须锁定这三个版本这套系统诞生于2018年前后当时主流开发栈就是JDK 8 Tomcat 8 MySQL 5.7。我试过用Tomcat 10部署直接报错java.lang.NoClassDefFoundError: javax/servlet/Filter——因为Tomcat 10迁移到Jakarta EE 9所有javax.包名全改成jakarta.而本项目所有web.xml和Servlet类里写的还是import javax.servlet.http.HttpServlet。MySQL同理如果装了8.0默认密码加密方式变成caching_sha2_password而项目里jdbc.properties里写的还是com.mysql.jdbc.Driver老驱动连不上库会抛Access denied for user。所以请严格按以下命令安装# JDK 8 安装Linux示例 wget https://download.oracle.com/otn/java/jdk/8u202-b08/1961070e4c9b4e26a04e7f5a083f551e/jdk-8u202-linux-x64.tar.gz tar -zxvf jdk-8u202-linux-x64.tar.gz export JAVA_HOME/path/to/jdk1.8.0_202 export PATH$JAVA_HOME/bin:$PATH # Tomcat 8.5.93官方最后稳定版 wget https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.93/bin/apache-tomcat-8.5.93.tar.gz tar -zxvf apache-tomcat-8.5.93.tar.gz # MySQL 5.7.42避免密码插件问题 sudo apt-get install mysql-server-5.7 # 安装时选择Use Legacy Password Encryption提示Windows用户请用Oracle官网下载JDK 8u202 Windows x64版Tomcat选8.5.x zip包MySQL务必勾选“Legacy Authentication Method”。2.2 数据库初始化执行SQL脚本前必须做的三件事项目根目录下的sql/cakeshop.sql不是直接双击运行就能用的。我踩过坑直接source进去登录后台时总提示“用户名或密码错误”查日志发现user表里password字段是明文存的但LoginServlet.class里却用MD5Util.encode(password)加密比对。原来SQL脚本里预置的管理员账号密码是明文admin/123456但代码逻辑要求密码必须是MD5值。所以必须先手动处理-- 步骤1创建数据库注意字符集 CREATE DATABASE cakeshop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 步骤2切换到该库再source脚本 USE cakeshop; SOURCE /path/to/cakeshop.sql; -- 步骤3修正管理员密码为MD5值关键 UPDATE user SET password e10adc3949ba59abbe56e057f20f883e WHERE username admin; -- e10adc3949ba59abbe56e057f20f883e 是 123456 的MD5别用在线工具生成复制粘贴防错2.3 项目部署WAR包构建与Tomcat配置细节项目结构是标准JavaWeb布局src/下放Java类WebContent/放JSP和静态资源WEB-INF/web.xml定义Servlet映射。但注意两个隐藏配置点数据库连接池配置src/jdbc.properties里写着drivercom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/cakeshop?useUnicodetruecharacterEncodingutf8 usernameroot password123456如果你的MySQL root密码不是123456必须在这里改且要确保MySQL允许root从localhost连接GRANT ALL ON cakeshop.* TO rootlocalhost IDENTIFIED BY 123456; FLUSH PRIVILEGES;。图片上传路径硬编码AdminGoodsAddServlet.java第87行String uploadPath request.getServletContext().getRealPath(/upload/);这意味着所有上传的蛋糕图片会存到Tomcat/webapps/ROOT/upload/目录下。部署时必须提前创建该目录并赋予权限mkdir -p $CATALINA_HOME/webapps/ROOT/upload chmod 755 $CATALINA_HOME/webapps/ROOT/upload部署步骤# 1. 编译Java类假设在项目根目录 javac -encoding UTF-8 -cp .:lib/*:$CATALINA_HOME/lib/* -d WebContent/WEB-INF/classes/ src/*.java # 2. 打包成WAR关键不能漏掉lib目录 cd WebContent jar -cvf ../cakeshop.war . # 3. 将WAR丢进Tomcat webapps目录启动 $CATALINA_HOME/bin/startup.sh启动后访问http://localhost:8080/cakeshop/首页应该显示“欢迎光临XX蛋糕商城”。3. 核心模块实战商品管理、分类管理、订单流转的代码级拆解3.1 商品管理模块DAO层如何保证图片上传与数据库写入原子性AdminGoodsAddServlet.class是添加新品的入口。它没用Spring事务而是手动控制JDBC事务——这是理解JavaWeb底层的关键。看核心逻辑// AdminGoodsAddServlet.java 片段 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 解析表单含文件 DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); ListFileItem items upload.parseRequest(request); // 2. 提取普通字段名称、价格等和文件项 String name , price ; FileItem imageItem null; for (FileItem item : items) { if (item.isFormField()) { if (name.equals(item.getFieldName())) name item.getString(UTF-8); else if (price.equals(item.getFieldName())) price item.getString(UTF-8); } else if (!item.getName().isEmpty()) { imageItem item; // 获取上传的图片文件 } } // 3. 开启事务先存数据库再存文件顺序不能反 Connection conn null; PreparedStatement pstmt null; try { conn JdbcUtil.getConnection(); // 从连接池获取 conn.setAutoCommit(false); // 关闭自动提交 // 插入商品基本信息 pstmt conn.prepareStatement(INSERT INTO goods(name, price, category_id, description) VALUES(?, ?, ?, ?)); pstmt.setString(1, name); pstmt.setDouble(2, Double.parseDouble(price)); pstmt.setInt(3, Integer.parseInt(request.getParameter(category_id))); pstmt.setString(4, request.getParameter(description)); pstmt.executeUpdate(); // 获取刚插入的goods_id关键 ResultSet rs pstmt.executeQuery(SELECT LAST_INSERT_ID()); rs.next(); int goodsId rs.getInt(1); // 4. 保存图片到upload目录并更新goods表的image_path字段 String fileName goodsId _ imageItem.getName(); // 防重名 String uploadPath request.getServletContext().getRealPath(/upload/); File uploadedFile new File(uploadPath, fileName); imageItem.write(uploadedFile); pstmt conn.prepareStatement(UPDATE goods SET image_path? WHERE id?); pstmt.setString(1, /upload/ fileName); // 前端JSP用此路径img src${goods.image_path} pstmt.setInt(2, goodsId); pstmt.executeUpdate(); conn.commit(); // 全部成功才提交 request.setAttribute(msg, 添加成功); } catch (Exception e) { conn.rollback(); // 任一环节失败就回滚 e.printStackTrace(); request.setAttribute(msg, 添加失败 e.getMessage()); } finally { JdbcUtil.close(null, pstmt, conn); } }参数说明LAST_INSERT_ID()是MySQL特有函数返回最近一次INSERT生成的自增IDimage_path存的是相对路径前端JSP用${pageContext.request.contextPath}${goods.image_path}拼接绝对URL避免跨域问题。3.2 分类管理模块为什么删除分类要先校验商品关联AdminCategoryDeleteServlet.class里藏着一个典型业务约束不能直接删分类必须先检查该分类下是否有商品。否则会出现“分类没了但商品还挂着不存在的category_id”导致前台商品列表报空指针。代码逻辑如下// AdminCategoryDeleteServlet.java String sql SELECT COUNT(*) FROM goods WHERE category_id ?; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setInt(1, categoryId); ResultSet rs pstmt.executeQuery(); if (rs.next() rs.getInt(1) 0) { request.setAttribute(msg, 该分类下有商品无法删除); request.getRequestDispatcher(admin/category_list.jsp).forward(request, response); return; } // 只有count0才执行DELETE pstmt conn.prepareStatement(DELETE FROM category WHERE id ?); pstmt.setInt(1, categoryId); pstmt.executeUpdate();注意这里没用外键ON DELETE CASCADE而是用应用层校验——更可控也方便后续加“软删除”status0。3.3 订单管理模块发货操作如何同步更新库存与订单状态AdminOrderUpdateServlet.class处理发货它同时更新两张表ordersstatus改为2已发货和goods对应商品库存减去订单数量。关键在于用一条SQL完成扣减避免先查再更新的竞态条件// 发货时扣减库存原子操作 String sql UPDATE goods g JOIN orders o ON g.id o.goods_id SET g.stock g.stock - o.quantity WHERE o.id ? AND g.stock o.quantity; pstmt conn.prepareStatement(sql); pstmt.setInt(1, orderId); int affected pstmt.executeUpdate(); if (affected 0) { // 库存不足发货失败 request.setAttribute(msg, 库存不足无法发货); return; } // 再更新订单状态 pstmt conn.prepareStatement(UPDATE orders SET status 2 WHERE id ?); pstmt.setInt(1, orderId); pstmt.executeUpdate();为什么不用SELECT stock FROM goods WHERE id?再UPDATE goods SET stockstock-1因为高并发时两个请求同时查到stock5都执行update后stock变成3实际应为4——这就是经典的超卖问题。用UPDATE ... SET stockstock-quantity WHERE stockquantity一条语句搞定校验扣减。4. 避坑指南部署与功能验证中踩过的5个真实坑位4.1 现象首页商品图片全部显示“叉”F12看img src是/upload/123_abc.jpg但404原因Tomcat默认禁止直接访问webapps/ROOT/upload/目录下的文件需在conf/web.xml里取消注释默认servlet的url-pattern//url-pattern或更安全的做法——在WEB-INF/web.xml中添加资源映射servlet-mapping servlet-namedefault/servlet-name url-pattern/upload/*/url-pattern /servlet-mapping解决编辑WebContent/WEB-INF/web.xml在servlet-mapping块末尾追加上述配置重启Tomcat。4.2 现象后台登录成功后跳转到admin/index.jsp但页面空白查看源码发现全是JSP表达式未解析如%request.getContextPath()%原样输出原因JSP引擎未启用通常因web.xml中缺少jsp-config声明或Tomcat的JSP编译器故障。解决确认web.xml头声明是web-app_3_0.xsd不是2.5并在web-app根节点下添加jsp-config jsp-property-group url-pattern*.jsp/url-pattern el-ignoredfalse/el-ignored scripting-invalidfalse/scripting-invalid /jsp-property-group /jsp-config4.3 现象添加商品时上传图片后台报java.io.FileNotFoundException: /upload/xxx.jpg (No such file or directory)原因request.getServletContext().getRealPath(/upload/)返回null——因为Tomcat以WAR包方式部署时getRealPath()对未解压的WAR内路径返回null。解决改用绝对路径如/var/tomcat/webapps/ROOT/upload/并在AdminGoodsAddServlet.java里硬编码// 替换原uploadPath赋值 String uploadPath /var/tomcat/webapps/ROOT/upload/;血泪经验生产环境永远不要依赖getRealPath()它在Docker或云主机上大概率失效。4.4 现象用户下单后订单状态始终卡在“待支付”OrderDao.class里updateStatus()方法不生效原因orders表的status字段类型是TINYINT但代码里传的是字符串2而非整数2MySQL隐式转换失败。解决检查OrderDao.java中更新语句// 错误写法传String pstmt.setString(1, 2); // 正确写法传int pstmt.setInt(1, 2);4.5 现象MySQL 5.7执行cakeshop.sql时在CREATE TABLE goods语句报错Invalid default value for create_time原因MySQL 5.7默认开启严格模式create_time DATETIME DEFAULT 0000-00-00 00:00:00不被允许。解决临时关闭严格模式在MySQL命令行执行SET GLOBAL sql_mode(SELECT REPLACE(sql_mode,STRICT_TRANS_TABLES,));或修改/etc/mysql/mysql.conf.d/mysqld.cnf在[mysqld]下添加sql_mode NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION5. 进阶技巧给原始系统加三个刚需功能无需框架纯JSPServlet实现5.1 功能一前台商品搜索——用LIKE实现模糊匹配但避开全表扫描原始系统只有分类筛选没有搜索框。在index.jsp顶部加搜索表单form actionsearch.jsp methodget input typetext namekeyword placeholder搜索蛋糕名称... required button typesubmit/button /form新建search.jsp核心是SQL优化% String keyword request.getParameter(keyword); if (keyword ! null !keyword.trim().isEmpty()) { Connection conn JdbcUtil.getConnection(); // 关键用CONCAT避免%keyword%导致索引失效且限定只搜name字段 String sql SELECT * FROM goods WHERE name LIKE CONCAT(%, ?, %) LIMIT 20; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, keyword.trim()); ResultSet rs pstmt.executeQuery(); // 循环输出商品... } %注意LIMIT 20防止恶意输入长关键词拖垮数据库生产环境建议在name字段加FULLTEXT索引但本系统MySQL 5.7需改表ALTER TABLE goods ADD FULLTEXT(name);查询用MATCH(name) AGAINST(?)。5.2 功能二订单导出Excel——用Apache POI生成.xls文件兼容老版本OfficeAdminOrderListServlet.class里加导出按钮点击后调用新Servlet// ExportOrderServlet.java HSSFWorkbook workbook new HSSFWorkbook(); HSSFSheet sheet workbook.createSheet(订单列表); // 表头 String[] headers {订单号, 用户姓名, 商品名, 数量, 金额, 状态, 下单时间}; for (int i 0; i headers.length; i) { sheet.createRow(0).createCell(i).setCellValue(headers[i]); } // 查询订单数据省略DB代码 ListOrder orders orderService.findAll(); for (int i 0; i orders.size(); i) { Order o orders.get(i); HSSFRow row sheet.createRow(i 1); row.createCell(0).setCellValue(o.getOrderNo()); row.createCell(1).setCellValue(o.getUserName()); row.createCell(2).setCellValue(o.getGoodsName()); row.createCell(3).setCellValue(o.getQuantity()); row.createCell(4).setCellValue(o.getTotalPrice()); row.createCell(5).setCellValue(o.getStatus() 1 ? 待支付 : o.getStatus() 2 ? 已发货 : 已完成); row.createCell(6).setCellValue(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(o.getCreateTime())); } // 输出响应 response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment; filenameorders_ System.currentTimeMillis() .xls); workbook.write(response.getOutputStream());依赖poi-3.17.jar放入WEB-INF/lib/注意用HSSF.xls而非XSSF.xlsx避免JDK 8兼容问题。5.3 功能三密码强度校验——前端JS后端双重验证注册页register.jsp加JS校验script function checkPassword() { var pwd document.getElementById(password).value; var msg ; if (pwd.length 6) msg 密码长度至少6位; if (!/[A-Z]/.test(pwd)) msg 需包含大写字母; if (!/[a-z]/.test(pwd)) msg 需包含小写字母; if (!/\d/.test(pwd)) msg 需包含数字; if (msg) { alert(密码不符合要求 msg); return false; } return true; } /script form onsubmitreturn checkPassword()后端RegisterServlet.java里再校验一次防绕过String pwd request.getParameter(password); if (pwd.length() 6 || !pwd.matches(.*[A-Z].*) || !pwd.matches(.*[a-z].*) || !pwd.matches(.*\\d.*)) { request.setAttribute(msg, 密码必须包含大小写字母和数字长度不少于6位); request.getRequestDispatcher(register.jsp).forward(request, response); return; }从那以后我每次接手老JSP项目第一件事就是打开AdminGoodsAddServlet.java盯着conn.setAutoCommit(false)和conn.rollback()这两行看三分钟——它们像一道分水岭区分出是真干活的系统还是PPT里的Demo。这套蛋糕商城的代码量不大但每一处try-catch、每一个LAST_INSERT_ID()、每一条带WHERE stockquantity的UPDATE都是真实订单流冲刷出来的痕迹。它不炫技不谈微服务就老老实实用JDBC和Servlet把一件事做闭环。希望帮到你。本文还有配套的精品资源点击获取
返回列表