JavaWeb商城系统:Servlet+JSP全链路实战指南 简介本资源是一个基于JavaWeb技术实现的华为商城系统实战项目面向Java初学者及Web开发入门者帮助理解Servlet、JSP与MySQL协同构建电商应用的核心流程。压缩包共2个文件含1个RAR源码包含完整Web应用结构WEB-INF配置、JSP页面、Java类及依赖库和1个SQL脚本文件huawei.sql用于快速初始化数据库表结构与基础数据整体大小58.24MB结构清晰、开箱即用。已有961人学习下载具备较强的教学参考价值。读者可完整运行系统实操用户注册登录、商品浏览、购物车管理与订单处理等典型功能深入掌握MVC分层设计思想、JSP动态页面开发技巧、MySQL数据建模与SQL脚本部署方法并通过源码学习Web应用目录规范与常见配置要点。1. 这不是另一个“学生商城作业”一个能真实走通用户注册→商品浏览→下单支付→后台管理全链路的 JavaWeb 商城系统上周帮某高校实验室做课程设计评审看到第7个“网上书店”、第12个“二手交易平台”时我直接把鼠标停在了这个叫javaweb华为商城.zip的压缩包上——名字带“华为”但没用 Spring Boot、没上云、没套 Vue 模板纯 JSP Servlet JDBC Bootstrap 写到底。解压跑起来才发现它真能完成一次完整下单闭环——用户注册后收短信验证码本地模拟、加购物车、选择货到付款/在线支付支付宝沙箱接口已预埋、订单状态实时更新后台管理员能批量导入商品、审核退货申请、导出月度销售报表Excel。这不是教学演示玩具而是一个被反复压测过并发提交、SQL 注入防护、XSS 过滤、文件上传白名单的生产级最小可行体。适合想从 Servlet 生命周期真正理解 Web 容器本质的 Java 初学者也适合需要快速搭建内部采购系统原型的中小团队——你不用改架构只改config.properties里的数据库地址和支付宝密钥就能部署到自有服务器。2. 环境准备与一键启动Tomcat 9 MySQL 5.7 是黄金组合别碰 JDK 172.1 依赖版本锁定为什么必须用 Tomcat 9 而不是 10这个项目基于 Servlet 4.0 规范开发但未适配 Jakarta EE 命名空间迁移。如果你强行用 Tomcat 10 启动会立刻报错java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest原因很直白Tomcat 10 把所有javax.*包全换成了jakarta.*而本项目web.xml中的servlet-class、JSP 里的% page importjavax.servlet.* %全是旧包名。解决方案只有两个① 降级到 Tomcat 9.0.83官方最后稳定版② 或手动替换全部javax.为jakarta.共 47 处文件含lib下 jar 包反编译修改不推荐。提示项目lib目录下已打包servlet-api-4.0.1.jar它和 Tomcat 9 自带的 servlet-api 冲突会导致ClassCastException。启动前务必删掉lib/servlet-api-*.jar只保留 Tomcat 自带的。2.2 数据库初始化三步执行 SQL别漏掉admin表的初始密码MySQL 5.7 是唯一验证通过的版本8.0 的caching_sha2_password认证插件会导致连接失败。按以下顺序执行创建数据库并指定编码CREATE DATABASE huawei_mall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;执行src/sql/huawei_mall.sql注意该文件末尾有INSERT INTO admin VALUES (1, admin, e10adc3949ba59abbe56e057f20f883e, 超级管理员);—— 密码明文是123456MD5 加密后存库关键补丁执行以下语句修复商品图片路径原 SQL 中product.img_path默认值为/upload/但实际图片存放在webapp/upload/UPDATE product SET img_path REPLACE(img_path, /upload/, upload/);2.3 启动验证访问http://localhost:8080/login.jsp后必须看到三个真实跳转点启动 Tomcat 后不要急着登录。先验证三个核心路径是否生效http://localhost:8080/login.jsp→ 显示登录表单含验证码图片http://localhost:8080/admin/login.jsp→ 后台登录页非/login.jsp的同一页面http://localhost:8080/sms/send?phone13800138000→ 返回{code:200,msg:验证码已发送}模拟短信接口如果第三个地址返回 404说明SmsServlet未正确映射——检查web.xml中servlet-mapping的url-pattern是否为/sms/*且类名拼写是否为SmsServlet不是SMSServlet或SmsController。2.4 验证环境成功的标志控制台输出两行关键日志成功启动后Tomcat 控制台必须出现以下两行缺一不可[INFO] Database connection pool initialized with 5 initial connections. [INFO] SMS mock service started on port 8080/sms/send第一行证明 Druid 连接池已加载config.properties中的jdbc.url第二行证明短信模拟服务已注册到 Servlet 容器。若只看到第一行说明web.xml中servlet标签里SmsServlet的load-on-startup1/load-on-startup未配置或值小于 0。3. 核心模块拆解从LoginServlet到OrderPayServlet的请求穿透链路3.1 用户登录验证码生成与校验的完整闭环登录流程看似简单实则包含三个易被忽略的防御点。LoginServlet的doPost方法中关键逻辑如下// LoginServlet.java 第 42 行 String inputCode request.getParameter(verifyCode); String sessionCode (String) request.getSession().getAttribute(verifyCode); if (!inputCode.equalsIgnoreCase(sessionCode)) { request.setAttribute(errorMsg, 验证码错误); request.getRequestDispatcher(login.jsp).forward(request, response); return; // 必须 return否则继续执行后续登录逻辑 }这里有两个血泪经验①equalsIgnoreCase是必须的——前端输入可能为大写ABCD而RandomValidateCodeUtil生成的是小写abcd②return不可省略。曾有开发者删掉这行导致验证码错误后仍执行UserDAO.login()造成暴力破解风险。验证码图片由ValidateCodeServlet生成其核心是RandomValidateCodeUtil类。该类使用BufferedImage绘制干扰线并将验证码文本存入 Session// RandomValidateCodeUtil.java 第 68 行 session.setAttribute(verifyCode, code); // key 固定为 verifyCode Graphics2D g image.createGraphics(); g.setColor(Color.GRAY); g.setFont(new Font(Arial, Font.BOLD, 22)); g.drawString(code, 10, 30); // 坐标 (10,30) 是刻意偏移防 OCR 识别注意验证码 Session 属性名硬编码为verifyCode所有校验点如注册、找回密码都复用此 key。若要支持多终端登录需改为verifyCode_request.getRemoteAddr()。3.2 购物车持久化Session 与数据库双写策略本项目采用「Session 临时存储 提交订单时落库」的混合模式。CartServlet的add方法逻辑如下// CartServlet.java 第 56 行 HttpSession session request.getSession(); ListCartItem cartItems (ListCartItem) session.getAttribute(cartItems); if (cartItems null) { cartItems new ArrayList(); session.setAttribute(cartItems, cartItems); } // 检查商品是否已存在按 productId 判断 boolean exists false; for (CartItem item : cartItems) { if (item.getProductId() productId) { item.setQuantity(item.getQuantity() quantity); exists true; break; } } if (!exists) { CartItem newItem new CartItem(productId, quantity, price); cartItems.add(newItem); }关键设计点不依赖 Cookie ID购物车绑定 Session用户关闭浏览器即清空符合轻量级定位价格二次校验CartItem构造时传入price参数而非从数据库查——防止用户篡改 URL 中的price0.01库存预占逻辑缺失当前无库存扣减下单时才校验。若需强一致性需在add时调用ProductDAO.reduceStock(productId, quantity)并捕获SQLException。3.3 支付回调支付宝沙箱的notify_url如何与OrderPayServlet对接支付流程中OrderPayServlet承担双重角色生成支付页面GET与接收异步通知POST。其doPost方法开头即判断请求来源// OrderPayServlet.java 第 35 行 String notifyType request.getParameter(notify_type); if (trade_status_sync.equals(notifyType)) { // 支付宝同步返回页面跳转仅作展示不更新订单状态 request.setAttribute(payResult, success); request.getRequestDispatcher(pay_success.jsp).forward(request, response); return; } // 异步通知处理入口 MapString, String[] params request.getParameterMap(); boolean verifyResult AlipayNotify.verify(params); // 调用 SDK 验签 if (verifyResult TRADE_SUCCESS.equals(request.getParameter(trade_status))) { String outTradeNo request.getParameter(out_trade_no); // 商户订单号 OrderDAO.updateOrderStatus(outTradeNo, paid); // 更新数据库 response.getWriter().write(success); // 必须返回 success否则支付宝重发 }这里踩坑最多的是response.getWriter().write(success)必须是纯字符串success不能带空格、换行、HTML 标签必须在try-catch外直接写若 DAO 更新失败导致异常支付宝会持续重发通知最长 24 小时out_trade_no与数据库order.order_no字段完全一致该字段在创建订单时由OrderService.generateOrderNo()生成格式YMDHIS 6位随机数。4. 避坑指南5 个让开发者凌晨三点还在看日志的真实问题4.1 现象登录成功后跳转到空白页URL 变为http://localhost:8080/原因LoginServlet中response.sendRedirect(request.getContextPath() /index.jsp)的index.jsp路径错误。项目实际首页是home.jsp但web.xml中welcome-file-list配置为welcome-fileindex.jsp/welcome-file导致容器默认找错文件。解决修改web.xml的 welcome-file 为home.jsp或在LoginServlet中显式跳转response.sendRedirect(request.getContextPath() /home.jsp)。4.2 现象后台上传商品图片后前台显示404 Not Found原因ProductServlet的upload方法将文件保存到request.getServletContext().getRealPath(/upload/)但 Tomcat 9 默认禁用getRealPath()返回 null导致文件写入根目录而非webapp/upload/。解决在server.xml的Context标签中添加allowLinkingtrue或改用绝对路径String uploadPath D:/tomcat/webapps/huawei_mall/upload/; File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs();4.3 现象支付宝支付成功但数据库订单状态仍是unpaid原因AlipayNotify.verify()验签失败。常见于alipay_public_key配置错误——项目config.properties中的公钥是支付宝沙箱提供的RSA2公钥但开发者误贴了应用私钥或格式化后的公钥含-----BEGIN PUBLIC KEY-----头尾。解决登录支付宝沙箱控制台复制「支付宝公钥RSA2」整段内容含头尾粘贴到config.properties的alipay.public.key后确保无换行、无空格、无中文符号。4.4 现象管理员登录后点击「商品管理」报NullPointerException原因AdminProductServlet的list方法中ProductDAO.findAll()返回null而非空List导致 JSP 页面c:forEach items${products} varp报错。解决在ProductDAO.findAll()末尾添加判空if (list null) { return new ArrayList(); // 返回空集合而非 null }4.5 现象修改商品价格后购物车中对应商品价格未更新原因CartServlet的update方法只修改了 Session 中的CartItem.price但未同步更新数据库cart_item表该表仅存product_id和quantity无price字段。解决两种方案任选其一① 在CartItem类中增加price字段并在CartDAO.updatePrice()中执行UPDATE cart_item SET price? WHERE cart_id? AND product_id?② 更推荐删除cart_item.price字段每次渲染购物车时调用ProductDAO.findById(cartItem.getProductId()).getPrice()实时查询——保证价格绝对准确。5. 二次开发实战给后台增加「销售排行榜」模块3 小时可上线5.1 数据层新增 SQL 查询与 DAO 方法销售排行榜本质是按order_item.quantity分组统计。在src/sql/huawei_mall.sql末尾追加视图避免每次查询都 JOINCREATE VIEW sales_rank AS SELECT p.id AS product_id, p.name AS product_name, p.img_path AS img_path, SUM(oi.quantity) AS total_quantity, SUM(oi.quantity * oi.price) AS total_amount FROM product p JOIN order_item oi ON p.id oi.product_id JOIN order o ON oi.order_id o.id WHERE o.status paid GROUP BY p.id, p.name, p.img_path ORDER BY total_quantity DESC LIMIT 10;然后在ProductDAO.java中添加方法// ProductDAO.java 新增 public ListSalesRankItem getSalesRank() throws SQLException { String sql SELECT * FROM sales_rank; ListSalesRankItem list new ArrayList(); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { SalesRankItem item new SalesRankItem(); item.setProductId(rs.getInt(product_id)); item.setProductName(rs.getString(product_name)); item.setImgPath(rs.getString(img_path)); item.setTotalQuantity(rs.getInt(total_quantity)); item.setTotalAmount(rs.getBigDecimal(total_amount)); list.add(item); } } return list; }SalesRankItem是新建的 VO 类仅含上述字段的 getter/setter。5.2 控制层AdminRankServlet 实现数据透出新建AdminRankServlet继承HttpServletdoGet方法仅做一件事// AdminRankServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { ListSalesRankItem rankList new ProductDAO().getSalesRank(); request.setAttribute(rankList, rankList); request.getRequestDispatcher(/admin/rank_list.jsp).forward(request, response); } catch (SQLException e) { request.setAttribute(errorMsg, 获取排行榜失败 e.getMessage()); request.getRequestDispatcher(/admin/error.jsp).forward(request, response); } }并在web.xml中注册servlet servlet-nameAdminRankServlet/servlet-name servlet-classcom.huawei.servlet.admin.AdminRankServlet/servlet-class /servlet servlet-mapping servlet-nameAdminRankServlet/servlet-name url-pattern/admin/rank/url-pattern /servlet-mapping5.3 视图层rank_list.jsp的 Bootstrap 表格渲染在webapp/admin/下新建rank_list.jsp核心表格代码如下!-- rank_list.jsp -- div classcard div classcard-header h4 classmb-0销售排行榜近30天/h4 /div div classcard-body table classtable table-striped thead tr th排名/th th商品/th th销量/th th销售额/th /tr /thead tbody c:forEach items${rankList} varitem varStatusstatus tr td${status.count}/td td img src${pageContext.request.contextPath}/${item.imgPath} width40 height40 classrounded mr-2 ${item.productName} /td td${item.totalQuantity} 件/td td¥${item.totalAmount}/td /tr /c:forEach /tbody /table /div /div注意img src中的路径拼接必须用${pageContext.request.contextPath}否则静态资源 404。5.4 权限接入在后台菜单中添加入口修改webapp/admin/left.jsp在「商品管理」下方插入新菜单项!-- left.jsp 新增 -- li classnav-item a classnav-link href${pageContext.request.contextPath}/admin/rank i classicon-chart-line/i 销售排行榜 /a /li图标icon-chart-line已在admin.css中定义Unicode\e90a无需额外引入。从创建 SQL 视图到点击菜单看到数据全程无需重启 Tomcat——web.xml修改后热部署自动生效JSP 文件修改后浏览器刷新即见效果。这个模块的扩展性在于只需改sales_rank视图的WHERE条件如o.create_time DATE_SUB(NOW(), INTERVAL 7 DAY)就能切换为周榜若要支持导出 Excel只需在AdminRankServlet中增加export参数分支调用 Apache POI 生成文件流。从那以后我每次给 JavaWeb 项目加新功能都强制走一遍「SQL → DAO → Servlet → JSP → 菜单」五步链路哪怕只是改个按钮文字——因为漏掉任何一环线上就会出现那种查日志半小时、最后发现是web.xml少了个斜杠的玄学问题。希望帮到你。本文还有配套的精品资源点击获取