ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Servlet+JSP+MySQL手写三层商城实战

Servlet+JSP+MySQL手写三层商城实战

简介:本资源是一个基于JavaWeb技术实现的华为商城电子商务系统实战项目,面向Java初学者及Web开发入门者,帮助其系统掌握Servlet、JSP、MySQL数据库交互等核心Web开发技能。压缩包共2个文件,含1个RAR源码包(含完整Web应用结构:WEB-INF配置、Java类、JSP页面、lib依赖库)和1个SQL脚本文件(huawei.sql),用于一键初始化商品、用户、订单等数据库表结构与基础数据;整体包体58.24MB,轻量实用,便于本地快速部署运行。已有961人学习下载,说明其在实践教学与自学验证中具备良好口碑。读者可直接导入IDE运行项目,体验注册登录、商品浏览、购物车管理、订单提交等全流程功能,并通过源码深入理解MVC分层设计、JSP动态渲染、JSTL标签应用及MySQL CRUD操作的工程化落地方式。

1. 一个跑在 Tomcat 上的「真·商城」:不是 Spring Boot 脚手架,而是 Servlet+JSP+MySQL 手写三层的完整闭环

你可能刚学完 JavaWeb 三大组件(Servlet、Filter、Listener),也照着教程敲过用户登录、商品列表,但合上书就卡在「怎么才算一个能上线的商城?」——这个javaweb华为商城.zip就是答案:它不依赖任何现代框架,没用 Spring、没碰 MyBatis,纯靠HttpServlet接口、JDBC手写 DAO、JSP做视图、JSTL控制流程,外加一个带登录态校验、购物车持久化、订单生成与状态流转的完整业务流。它不是教学 Demo,而是一个可部署、可调试、可改价、可加商品、甚至能模拟用户下单失败重试的「最小可行商城」。适合两类人:一是刚学完 JDBC 和 MVC 分层、想验证自己是否真懂「请求怎么从浏览器进到数据库再吐回页面」的新手;二是需要快速搭一个 JavaWeb 底层教学案例的讲师或导师——所有代码都在 src 下,没有隐藏黑盒,连 MySQL 建表语句都塞在sql/目录里,连web.xml的 URL 映射都一行行写清楚。这不是玩具项目,它是用最原始但最透明的方式,把 Web 开发的「血肉」一节节摊开给你看。


2. 技术栈与工程结构:Servlet 3.0 + JSP 2.3 + MySQL 5.7 的经典组合

这个项目没玩花活,技术选型非常克制,全部落在 JavaWeb 教学与企业遗留系统维护的真实交界面上。它不追求新,但求稳、求透、求可调试。下面我带你一层层剥开它的骨架,重点不是“用了什么”,而是“为什么这么用”、“哪里藏着关键设计”。

2.1 核心依赖与版本锚点

项目基于标准 JavaEE Web 模块结构,WEB-INF/web.xml明确声明为 Servlet 3.0 规范(<web-app version="3.0">),这意味着它兼容 Tomcat 7+(实测 Tomcat 8.5 / 9.0 完全无压力)。JSP 使用的是 JSP 2.3(对应 Servlet 3.0),所以 EL 表达式(${user.name})和 JSTL 标签(<c:if>、<c:forEach>)可直接用,无需额外配置 TLD。数据库驱动用的是mysql-connector-java-5.1.47.jar,这是 MySQL 5.7 的黄金匹配版本——太新(如 8.x 驱动)会因默认时区、SSL 策略报错;太旧(如 5.0.x)则不支持useSSL=false参数,导致本地调试连不上。这个细节,我在第一次部署时就栽过坑,后面「避坑章节」会细说。

提示:项目未使用 Maven,所有.jar全部放在WEB-INF/lib/下,共 6 个:mysql-connector-java-5.1.47.jar、jstl-1.2.jar、standard-1.1.2.jar(JSTL 运行时依赖)、servlet-api-3.0.jar(仅编译用,Tomcat 运行时不加载)、commons-beanutils-1.9.4.jar(用于BeanUtils.populate()自动封装表单数据)、commons-logging-1.2.jar(日志门面)。没有一个多余。

2.2 工程目录结构:教科书级分层,但每层都有真实业务逻辑

javaweb华为商城/ ├── sql/ # 建库建表 SQL,含初始化数据(admin 用户、测试商品) ├── src/ │ ├── cn/huawei/web/ # Servlet 层:所有控制器,命名直白如 UserServlet、OrderServlet │ ├── cn/huawei/service/ # Service 层:接口 + 实现类,含事务控制(手动 commit/rollback) │ ├── cn/huawei/dao/ # DAO 层:JDBC 操作封装,Connection 由 BaseDao 统一管理 │ ├── cn/huawei/domain/ # Domain 层:纯 POJO,字段与数据库表一一对应(含 getter/setter) │ └── cn/huawei/util/ # 工具类:DBUtil(获取/关闭连接)、CookieUtil(读写购物车 Cookie) ├── WebContent/ # Web 根目录(Eclipse 时代叫法,等同于 modern 的 src/main/webapp) │ ├── WEB-INF/ │ │ ├── web.xml # 全量 URL 映射、Filter 配置、欢迎页 │ │ └── lib/ # 上述 6 个 jar │ ├── index.jsp # 首页,含轮播图、新品推荐(数据来自 request.setAttribute) │ ├── user/ # 用户相关页面:login.jsp、register.jsp、userinfo.jsp │ ├── product/ # 商品页:product_list.jsp、product_detail.jsp │ ├── cart/ # 购物车:cart.jsp(显示 Cookie 中商品)、cart_add.jsp(加购逻辑) │ └── order/ # 订单页:order_confirm.jsp(确认收货信息)、order_success.jsp

注意:WebContent是 Eclipse Dynamic Web Project 的默认 WebRoot,不是 Maven 结构。如果你用 IDEA,导入时需选「Import project from external model → Eclipse」,否则静态资源路径会错。这个结构看似老旧,但它的好处是——所有路径都是硬编码、可追踪、无魔法。比如cart_add.jsp里<form action="cart_add.jsp" method="post">,后端CartAddServlet的doPost()就处理这个请求,中间没有任何路由解析器、注解扫描器或自动映射。新手能一眼看清「点击按钮 → 提交到哪个文件 → 调哪个 Servlet → 走哪段 Java 代码」。

2.3 关键配置文件解读:web.xml是整个项目的总开关

web.xml不是摆设,它定义了这个商城的「运行契约」。我们挑三个最核心的配置来看:

<!-- 1. 全局初始化参数:数据库连接信息 --> <context-param> <param-name>driver</param-name> <param-value>com.mysql.jdbc.Driver</param-value> </context-param> <context-param> <param-name>url</param-name> <param-value>jdbc:mysql://localhost:3306/huawei_mall?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false</param-value> </context-param> <context-param> <param-name>username</param-name> <param-value>root</param-value> </context-param> <context-param> <param-name>password</param-name> <param-value>123456</param-value> </context-param>

这里useSSL=false是必须加的,否则 MySQL 5.7+ 默认要求 SSL 连接,本地开发环境没配证书就会报Communications link failure。&amp;是 XML 转义,实际拼接 URL 时会被解析为&。

<!-- 2. Filter:统一登录拦截 --> <filter> <filter-name>LoginFilter</filter-name> <filter-class>cn.huawei.web.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/user/*</url-pattern> <url-pattern>/cart/*</url-pattern> <url-pattern>/order/*</url-pattern> </filter-mapping>

这个 Filter 是商城安全边界的起点。它检查session.getAttribute("user")是否存在且不为 null,不存在则重定向到/user/login.jsp。注意:它不拦截/index.jsp或/product/,因为游客可以浏览商品——这是真实商城逻辑,不是所有页面都要登录。

<!-- 3. Servlet 映射:明确告诉容器「谁处理什么」 --> <servlet> <servlet-name>UserServlet</servlet-name> <servlet-class>cn.huawei.web.UserServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>UserServlet</servlet-name> <url-pattern>/user</url-pattern> </servlet-mapping>

UserServlet是一个典型的多方法分发 Servlet:通过request.getParameter("method")判断是login、logout还是register,然后调用对应私有方法。这种写法在 Servlet 3.0 之前很常见,虽不如@WebServlet注解简洁,但逻辑完全暴露,新手 debug 时打个断点就能看到method=login是怎么走到userLogin()里的。


3. 核心模块拆解:从用户登录到订单生成的完整链路

这个商城最值得细看的,不是某个炫酷功能,而是一个真实用户操作如何穿透整个分层架构。我们以「用户登录 → 加购商品 → 提交订单」为主线,逐层拆解代码,不跳步、不省略、不假设你已懂某一步。

3.1 用户登录:Servlet 接参 → Service 校验 → DAO 查库 → JSP 渲染

登录入口是user/login.jsp,表单提交到UserServlet(URL/user):

<!-- user/login.jsp 片段 --> <form action="/user" method="post"> <input type="hidden" name="method" value="login"/> <input type="text" name="username" placeholder="用户名"/> <input type="password" name="password" placeholder="密码"/> <button type="submit">登录</button> </form>

后端UserServlet.java的doPost()方法:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method = request.getParameter("method"); if ("login".equals(method)) { userLogin(request, response); // 调用私有方法 } else if ("logout".equals(method)) { userLogout(request, response); } }

userLogin()方法是关键:

private void userLogin(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数(原始方式,非 Spring 的 @RequestParam) String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 调用 Service 层,传入原始参数 UserService userService = new UserServiceImpl(); User user = userService.login(username, password); // 3. Service 返回 null?说明账号密码错 if (user == null) { request.setAttribute("msg", "用户名或密码错误!"); request.getRequestDispatcher("/user/login.jsp").forward(request, response); return; } // 4. 登录成功:存入 session,并重定向到首页 request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); }

这里没有@Autowired,没有 IOC 容器,new UserServiceImpl()是硬编码创建对象。好处是:你能清晰看到依赖关系——UserServlet依赖UserServiceImpl,UserServiceImpl又依赖UserDaoImpl。UserServiceImpl.login()方法:

public User login(String username, String password) { UserDao userDao = new UserDaoImpl(); // 注意:密码是明文比对(教学项目简化),真实场景必须 BCrypt 加密 return userDao.findByUsernameAndPassword(username, password); }

UserDaoImpl.findByUsernameAndPassword()最终执行 JDBC 查询:

public User findByUsernameAndPassword(String username, String password) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); // 从 web.xml 读取参数,创建 Connection String sql = "SELECT id, username, password, email, phone FROM users WHERE username = ? AND password = ?"; stmt = conn.prepareStatement(sql); stmt.setString(1, username); stmt.setString(2, password); rs = stmt.executeQuery(); if (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setEmail(rs.getString("email")); user.setPhone(rs.getString("phone")); return user; } } catch (SQLException e) { e.printStackTrace(); // 教学项目,直接打印堆栈 } finally { DBUtil.close(rs, stmt, conn); // 必须关闭,否则连接泄漏 } return null; }

整个链路:JSP 表单 → Servlet 获取参数 → Service 新建 DAO → DAO 执行 JDBC → 返回 User 对象 → Servlet 存入 Session → 重定向。每一行代码都可打断点、可修改、可替换。这就是它作为教学载体的价值——没有抽象掉“连接怎么来、结果怎么转对象、异常怎么处理”的过程。

3.2 购物车:Cookie 存储 + Servlet 同步 → 解决无 Session 场景

这个商城的购物车设计很务实:它不强制用户登录才能加购。游客也能把商品加入购物车,数据存在 Cookie 里;登录后,再把 Cookie 购物车合并到用户账户的数据库购物车中。这解决了教学中一个经典矛盾:既要演示 Session,又要演示 Cookie。

加购逻辑在CartAddServlet.java:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取商品 ID 和数量(来自 product_detail.jsp 表单) String pid = request.getParameter("pid"); String countStr = request.getParameter("count"); int count = 1; if (countStr != null && !countStr.trim().isEmpty()) { count = Integer.parseInt(countStr); } // 2. 从 Cookie 读取现有购物车(格式:pid1-count1|pid2-count2) Cookie[] cookies = request.getCookies(); String cartValue = ""; if (cookies != null) { for (Cookie cookie : cookies) { if ("cart".equals(cookie.getName())) { cartValue = cookie.getValue(); break; } } } // 3. 解析并更新购物车字符串(简单字符串拼接,非 JSON) StringBuilder newCart = new StringBuilder(); if (cartValue != null && !cartValue.isEmpty()) { String[] items = cartValue.split("\\|"); boolean found = false; for (String item : items) { if (item.startsWith(pid + "-")) { // 已存在,更新数量 int oldCount = Integer.parseInt(item.split("-")[1]); newCart.append(pid).append("-").append(oldCount + count).append("|"); found = true; } else { newCart.append(item).append("|"); } } if (!found) { newCart.append(pid).append("-").append(count).append("|"); } } else { // 购物车为空,直接添加 newCart.append(pid).append("-").append(count).append("|"); } // 4. 写回 Cookie(有效期 7 天) Cookie cartCookie = new Cookie("cart", newCart.toString()); cartCookie.setMaxAge(7 * 24 * 3600); cartCookie.setPath("/"); response.addCookie(cartCookie); // 5. 重定向回商品详情页,显示成功提示 request.setAttribute("msg", "已加入购物车!"); request.getRequestDispatcher("/product/product_detail.jsp").forward(request, response); }

注意:这里用|和-分隔,是典型教学项目做法——不引入 JSON 库,用最基础字符串操作。真实项目会用Jackson或Gson序列化Map<Long, Integer>,但新手看cartValue.split("\\|")比看jsonObject.getJSONArray("items")更容易理解数据结构。

3.3 订单生成:事务控制 + 状态机雏形

订单模块是整个项目复杂度最高的部分,它体现了「业务完整性」:扣库存、生成订单号、关联用户、写订单项、更新订单状态。OrderServlet.java的createOrder()方法展示了手动事务控制:

private void createOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取当前登录用户 User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/user/login.jsp"); return; } // 2. 从 Cookie 读购物车(游客模式),或从数据库查(登录用户) List<CartItem> cartItems = getCartItemsFromCookieOrDB(request); // 3. 开启事务:所有操作要么全成功,要么全回滚 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 步骤 A:扣减商品库存(for 循环,每个商品 update) ProductDao productDao = new ProductDaoImpl(); for (CartItem item : cartItems) { boolean updated = productDao.updateStock(item.getPid(), -item.getCount()); if (!updated) { throw new RuntimeException("商品库存不足:" + item.getPid()); } } // 步骤 B:生成订单主表(orders) Order order = new Order(); order.setOid(UUID.randomUUID().toString().replace("-", "")); // 32位订单号 order.setUid(user.getId()); order.setTotal(getTotalPrice(cartItems)); order.setStatus("1"); // 1=待付款,2=已付款,3=已发货... order.setOrdertime(new Date()); OrderDao orderDao = new OrderDaoImpl(); orderDao.addOrder(order, conn); // 传入 conn,保证同一事务 // 步骤 C:生成订单项(order_items) OrderItemDao orderItemDao = new OrderItemDaoImpl(); for (CartItem item : cartItems) { OrderItem orderItem = new OrderItem(); orderItem.setIid(UUID.randomUUID().toString().replace("-", "")); orderItem.setOid(order.getOid()); orderItem.setPid(item.getPid()); orderItem.setCount(item.getCount()); orderItem.setSubtotal(item.getSubtotal()); orderItemDao.addItem(orderItem, conn); } // 全部成功,提交事务 conn.commit(); // 清空购物车 Cookie Cookie cartCookie = new Cookie("cart", ""); cartCookie.setMaxAge(0); cartCookie.setPath("/"); response.addCookie(cartCookie); // 跳转成功页 request.setAttribute("order", order); request.getRequestDispatcher("/order/order_success.jsp").forward(request, response); } catch (Exception e) { // 出错,回滚 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute("msg", "订单创建失败:" + e.getMessage()); request.getRequestDispatcher("/cart/cart.jsp").forward(request, response); } finally { DBUtil.close(null, null, conn); } }

这个方法有三个关键点:

  • 手动事务:conn.setAutoCommit(false)+conn.commit()/conn.rollback(),比 Spring 的@Transactional更底层,让你看清事务边界在哪;
  • 状态码硬编码:order.setStatus("1"),后续扩展「已付款」「已发货」只需改这里和对应页面判断;
  • 订单号生成:用UUID去-,保证全局唯一,不依赖数据库自增 ID(避免并发问题)。

这就是一个「小而全」的订单状态机起点——它没做消息队列、没对接支付网关,但把「创建订单」这个原子操作的完整性,用最朴素的 JDBC 方式实现了。


4. 避坑指南:部署与调试中最常翻车的 4 个点

这个项目看着简单,但新手在本地跑通往往要折腾 1–2 小时。我当年第一次部署时,光解决乱码和连接失败就花了半天。下面这 4 条,全是血泪经验总结,按出现频率排序,每条都附带「现象 → 原因 → 解决」,照着做基本能一次过。

4.1 现象:中文乱码,登录页输入「张三」,后台收到「å¼ ä¸‰」

原因:Tomcat 默认使用 ISO-8859-1 解码 POST 请求体,而浏览器发送的是 UTF-8 编码的中文。request.getParameter()拿到的就是乱码字节数组。
解决:在UserServlet的doPost()开头,强制设置请求编码:

request.setCharacterEncoding("UTF-8");

注意:这行必须在getParameter()之前调用!否则无效。更彻底的做法是在web.xml中配置CharacterEncodingFilter,但本项目没配,所以每个 Servlet 的doPost()都得手动加这一行。别偷懒,我试过漏加一个,结果注册用户姓名全乱码,查数据库时一脸懵。

4.2 现象:java.sql.SQLException: Access denied for user 'root'@'localhost'或Communications link failure

原因:MySQL 5.7+ 默认开启skip-grant-tables或validate_password策略,或者web.xml里的url参数少了useSSL=false。
解决:

  1. 确保 MySQL 用户有远程访问权限(即使本地,也要host='%'):
    CREATE USER 'root'@'%' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'; FLUSH PRIVILEGES;
  2. web.xml中的url必须包含useSSL=false,且&要写成&amp;(XML 转义):
    <param-value>jdbc:mysql://localhost:3306/huawei_mall?useUnicode=true&amp;characterEncoding=UTF-8&amp;useSSL=false</param-value>
  3. 如果还连不上,用命令行验证:mysql -h 127.0.0.1 -P 3306 -u root -p,确保能登录。

4.3 现象:HTTP Status 404 – /user,访问/user报 404,但index.jsp能打开

原因:web.xml中servlet-mapping的url-pattern和浏览器地址栏输入的 URL 不匹配。常见错误:

  • 在web.xml里写了<url-pattern>/user</url-pattern>,但浏览器访问的是http://localhost:8080/user(缺项目上下文路径);
  • 或者项目名不是javaweb华为商城,而是被 IDE 自动改成javaweb%E5%8D%8E%E4%B8%BA%E5%95%86%E5%9F%8E(URL 编码),导致上下文路径错。
    解决:
  1. 查看 Tomcat 控制台启动日志,找这行:Deploying web application directory [...],后面跟着的实际路径就是上下文路径(Context Path);
  2. 浏览器访问时,必须带上上下文路径,例如:http://localhost:8080/javaweb华为商城/user;
  3. 更稳妥做法:在web.xml的<welcome-file-list>中指定index.jsp,然后所有跳转用相对路径,如<a href="${pageContext.request.contextPath}/user">登录</a>,这样无论项目名是什么都能适配。

4.4 现象:购物车加购后,cart.jsp显示为空,或数量总是 1

原因:CartAddServlet中解析 Cookie 的正则或分割逻辑出错,或者cart.jsp读取 Cookie 时没正确 split。
解决:

  1. 在CartAddServlet的doPost()中,加一行日志打印cartValue:
    System.out.println("原始购物车 Cookie: " + cartValue);
  2. 检查cart.jsp中读取逻辑:它应该用CookieUtil.getCartItems(request)(项目自带工具类),而不是自己手写getCookies();
  3. 关键:cartValue.split("\\|")中的\\|是转义,因为|是正则特殊字符。如果写成.split("|"),会按空字符切,得到一堆空字符串。这是 Java 字符串分割的经典玄学坑,我踩过三次才记住。

5. 二次开发实战:如何快速接入微信支付、替换前端 UI、增加商品搜索

现在你已经跑通了整个商城,下一步往往是「我想改点东西」。下面我给你三条最实用的改造路径,每条都给出具体改什么文件、改哪几行、预期效果,以及我踩过的坑。不讲大道理,只给可粘贴、可验证的代码。

5.1 接入微信支付:替换「订单成功页」为支付跳转

原项目订单成功页(order_success.jsp)只是静态展示,我们要让它变成「调起微信 JSAPI 支付」。微信支付需要后端统一下单接口返回prepay_id,再由前端调WeixinJSBridge.invoke()。改造分两步:

第一步:新增WxPayServlet.java(放在src/cn/huawei/web/)

@WebServlet("/wxpay") public class WxPayServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取订单号(来自 order_success.jsp 的 hidden input) String oid = request.getParameter("oid"); // 2. 查询订单金额(真实项目应查 DB,此处简化为固定值) double totalFee = 99.9; // 单位:分 → 9990 // 3. 调用微信统一下单接口(此处用伪代码,实际需引入 wxpay-sdk) // WxPayService wxPayService = new WxPayService(); // Map<String, String> result = wxPayService.unifiedOrder(oid, totalFee); // String prepayId = result.get("prepay_id"); // 4. 为演示,我们 mock 一个 prepay_id String prepayId = "wx" + System.currentTimeMillis() + "mock"; // 5. 构造 JSAPI 签名参数(关键!时间戳、随机串、签名必须一致) String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); String nonceStr = UUID.randomUUID().toString().replace("-", ""); String packageStr = "prepay_id=" + prepayId; String signType = "MD5"; // 签名原文:appId=wx...&nonceStr=...&package=prepay_id=...&signType=MD5&timeStamp=...&key=yourKey String paySign = generatePaySign(packageStr, timeStamp, nonceStr, signType); // 6. 返回 JSON 给前端 response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.print("{\"appId\":\"wx123456789\",\"timeStamp\":\"" + timeStamp + "\",\"nonceStr\":\"" + nonceStr + "\",\"package\":\"" + packageStr + "\",\"signType\":\"" + signType + "\",\"paySign\":\"" + paySign + "\"}"); out.flush(); } private String generatePaySign(String packageStr, String timeStamp, String nonceStr, String signType) { // 真实项目需用官方 SDK 的 WXPayUtil.sign(),此处简化为返回固定值 return "FAKE_PAY_SIGN_123456"; } }

第二步:修改order_success.jsp,加 JS 调起支付

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>订单成功</title> <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> </head> <body> <h2>订单提交成功!</h2> <p>订单号:<%= request.getAttribute("order").getOid() %></p> <p>金额:<%= request.getAttribute("order").getTotal() %> 元</p> <!-- 隐藏订单号,供 JS 使用 --> <input type="hidden" id="oid" value="<%= request.getAttribute("order").getOid() %>"> <button onclick="callWxPay()">立即支付</button> <script> function callWxPay() { const oid = document.getElementById('oid').value; fetch('/javaweb华为商城/wxpay', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'oid=' + oid }) .then(res => res.json()) .then(data => { // 调起微信支付 WeixinJSBridge.invoke('getBrandWCPayRequest', { "appId": data.appId, "timeStamp": data.timeStamp, "nonceStr": data.nonceStr, "package": data.package, "signType": data.signType, "paySign": data.paySign }, function(res) { if (res.err_msg == "get_brand_wcpay_request:ok") { alert("支付成功!"); window.location.href = "/index.jsp"; } else { alert("支付失败:" + res.err_msg); } }); }); } </script> </body> </html>

注意:微信 JSAPI 支付必须在微信内置浏览器中调用,且域名已配置 JSAPI 白名单。本地调试要用「微信开发者工具」或真机扫码。别在 Chrome 里点按钮,会报WeixinJSBridge is not defined。

5.2 替换前端 UI:用 Bootstrap 4 重写首页

原index.jsp是纯 HTML 表格布局,丑且难维护。我们用 Bootstrap 4 快速升级:

  1. 下载 Bootstrap 4 CSS/JS,放入WebContent/css/bootstrap.min.css和WebContent/js/bootstrap.min.js;
  2. 修改index.jsp头部:
    <link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css"> <script src="${pageContext.request.contextPath}/js/bootstrap.min.js"></script>
  3. 商品列表区域,把原来的<table>替换为 Bootstrap Card Grid:
    <div class="container mt-4"> <h2>热门商品</h2> <div class="row"> <c:forEach items="${products}" var="p"> <div class="col-md-3 mb-4"> <div class="card h-100"> <img src="${pageContext.request.contextPath}/images/${p.pimage}" class="card-img-top" alt="${p.pname}" style="height: 150px; object-fit: cover;"> <div class="card-body"> <h5 class="card-title">${p.pname}</h5> <p class="card-text text-danger">¥${p.shop_price}</p> <a href="${pageContext.request.contextPath}/product/product_detail.jsp?pid=${p.pid}" class="btn btn-primary btn-sm">查看详情</a> </div> </div> </div> </c:forEach> </div> </div>
  4. 重启 Tomcat,首页立刻变样。Bootstrap 的栅格系统让响应式布局一步到位,不用写一行 media query。

5.3 增加商品搜索:在首页加搜索框,后端用 LIKE 模糊查询

这是最常被问的需求。只需改两处:

第一处:index.jsp加搜索表单

<div class="container mt-4"> <form class="form-inline my-2 my-lg-0" action="${pageContext.request.contextPath}/product/search" method="get"> <input class="form-control mr-sm-2" type="search" name="q" placeholder="搜索商品..." aria-label="Search"> <button class="btn btn-outline-success my-2 my-sm-0" type="submit">搜索</button> </form> </div>

第二处:新增ProductSearchServlet.java

@WebServlet("/product/search") public class ProductSearchServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String q = request.getParameter("q"); if (q == null || q.trim().isEmpty()) { // 搜索空,重定向回首页 response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } ProductDao productDao = new ProductDaoImpl(); List<Product> products = productDao.searchByName(q.trim()); // 新增方法 request.setAttribute("products", products); request.setAttribute("searchQuery", q); request.getRequestDispatcher("/product/product_list.jsp").forward(request, response); } }

第三处:在ProductDaoImpl.java中新增searchByName()方法

public List<Product> searchByName(String keyword) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; List<Product> list = new ArrayList<>(); try { conn = DBUtil.getConnection(); // 使用 %?% 实现模糊搜索 String sql = "SELECT * FROM products WHERE pname LIKE ?"; stmt = conn.prepareStatement(sql); stmt.setString(1, "%" + keyword + "%"); rs = stmt.executeQuery(); while (rs.next()) { Product p = new Product(); p.setPid(rs.getLong("pid")); p.setPname(rs.getString("pname")); p.setShop_price(rs.getDouble("shop_price")); p.setPimage(rs.getString("pimage")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, stmt, conn); } return list; }

提示:LIKE搜索性能一般,数据量大时需加索引。在products(pname)上执行ALTER TABLE products ADD INDEX idx_pname(pname);即可。


从那以后我每次接手一个老 JavaWeb 项目,第一件事就是打开它的web.xml,看servlet-mapping和filter-mapping,再扫一眼WEB-INF/lib/有哪些

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进