
简介基于 JSP 和 Servlet 的 JavaWeb 蛋糕店售卖网站面向 JavaWeb 初学者及课程设计、毕业设计人群解决从零搭建蛋糕商城时前后台功能不完整、代码结构不清晰的问题。项目完整覆盖商城前台展示推荐、热销、新品、类型商品、商品详情、购物车增删改、登录注册、个人信息修改、购物车付款、订单查询与关键字搜索同时包含管理员后台的订单状态处理、用户管理、商品类目维护和商品信息维护操作技术栈为 JSP Servlet MySQL并整合 c3p0 连接池与 DButil 工具适合用来理解 MVC 分层与 JavaWeb 业务开发流程。压缩包内共 290 个文件以 JPG、JSP、Java、XML 为主附带 JS、CSS、GIF、PNG 以及 jar 包、SQL 脚本等图片与样式提供完整界面资源Java 和 JSP 实现动态逻辑与页面响应SQL 脚本便于初始化数据库整体大小约 19.11MB。已有 127 人学习下载。拿到后可获得一套可直接部署运行的商城源码包括前台展示模板、后台管理模块与数据库设计结构清晰、可供二次开发既可参照完成课程设计或毕业设计也可按需扩展商品分类、订单状态等新功能。1. 一个基于 JSP 和 Servlet 的蛋糕店售卖网站到底在做什么如果你正打算用“基于 jsp 和 servlet 的 javaweb 实现的蛋糕店售卖网站”作为课程设计或练手项目第一反应很可能是“先把页面做漂亮”。这个方向其实反了。这类题目的核心不是前端质感而是让你用 JavaWeb 最经典的技术栈——JSP 负责展示、Servlet 负责控制、JDBC 负责存取把一条从“浏览商品 → 加购 → 下单 → 后台维护”的业务闭环真正跑通。它检验的是你对请求响应模型、Session、状态流转和数据库设计的掌握而不是你用了什么炫酷框架。适合刚学完 JavaWeb 基础、想在 Tomcat 里跑通一个完整业务的人。读完这篇你能得到一个可以直接落地的工程结构以及所有我当年自己翻过车的细节。2. 技术选型与工程骨架为什么 Servlet JSP 这套老方案至今能打2.1 Servlet 管逻辑、JSP 管展示先想清楚分层再动手很多初学者会把 Java 代码直接写进 JSP页面里穿插大段% %虽然没有语法错误但后续加需求时会变成一场灾难。这里要建立第一个概念JSP 的本质是 Servlet它被容器翻译成.java再编译成.class运行所以在 JSP 里写业务不是不行而是职责错位。我一般会把项目按经典的 MVC 结构拆分Model 层JavaBean 封装数据DAO 类封装 JDBC 操作负责与 MySQL 交互View 层放在webapp下的 JSP 页面只做展示使用 EL 和 JSTL 取值Controller 层Servlet 接收请求、调用 DAO、决定跳转到哪个 JSP。一个请求的典型流转路径是浏览器发起GET/POST→web.xml或注解匹配到 Servlet → Servlet 调用 DAO 查询数据库 → 把结果放进request或session→forward到 JSP → 页面渲染后返回浏览器。这个链路全程没有 Spring没有 MyBatis但你能看清 HTTP 和 Java 之间的每一步关系这正是这类题目要考察的东西。2.2 建一个不踩坑的 JavaWeb 工程目录、依赖与 Tomcat 版本常见做法是用 IDEA 直接新建一个普通 Java 项目手工补 Web 目录结构而不是用 Maven 骨架因为很多课程环境不允许联网拉取依赖。标准目录结构应该是cake-shop/ ├── src/ │ └── main/ │ ├── java/ # Servlet、DAO、Bean │ └── webapp/ # JSP页面、静态资源 │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ # servlet-api.jar、jstl.jar、mysql-connector │ ├── css/ │ ├── images/ │ └── *.jsp这里最容易被忽视的一个细节是你的 Tomcat 版本决定你引入哪个 Servlet API。Tomcat 9 使用的是javax.servlet命名空间而 Tomcat 10 开始换成了jakarta.servlet如果你用 JDK 8 配 Tomcat 10代码里的import javax.servlet.http.HttpServlet会直接报“程序包不存在”。我建议直接使用 Tomcat 9 JDK 8然后从 Tomcat 安装目录的lib文件夹里拷贝servlet-api.jar到WEB-INF/lib这是最不容易踩坑的组合。web.xml是整个项目的入口描述文件如果你用的 Servlet 版本是 3.0 以上映射方式推荐用注解但web.xml仍然要保留负责配置欢迎页面和编码过滤器下面是一个基础模板web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list filter filter-nameencodingFilter/filter-name filter-classcom.cakeshop.filter.EncodingFilter/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 /web-app注意version3.1这一行它决定了容器按什么规范来解析你的 web.xml。很多学生项目启动后页面 404排查到最后是这里版本声明过低导致注解没有被扫描。UTF-8 编码过滤器放在最前面能避免后面写 Servlet 时每取一个参数都要手工转码。2.3 用最简请求链路先把项目跑起来为了确认整个环境没问题我会先写一个最小链路浏览器请求一个 ServletServlet 转发给 JSPJSP 显示一句话。这一步不通过后面所有功能都没必要写。先看 Servletpackage com.cakeshop.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; WebServlet(/hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute(shopName, 草莓奶油蛋糕店); request.getRequestDispatcher(/hello.jsp).forward(request, response); } }这里使用了WebServlet(/hello)注解等价于在 web.xml 里配置一个同名映射。setAttribute把数据放进 request 作用域forward转发给 JSP转发时浏览器地址栏不变化这是 JSP 页面里能通过${shopName}取到值的根本原因。再写webapp/hello.jsp% page contentTypetext/html;charsetUTF-8 languagejava % html head title连通性测试/title /head body h1${shopName} 欢迎你/h1 /body /html启动 Tomcat 后访问http://localhost:8080/hello能看到页面输出就说明 Servlet 编译、注解扫描、JSP 翻译三条链路全部正常。如果这时你看到 404 或者 500不要急着继续写功能先解决这个最小问题后面所有排查思路都会变得清晰。3. 数据库设计先行蛋糕、用户、购物车与订单的四张核心表3.1 四张表怎么设计字段、类型与主外键关系蛋糕店售卖网站的业务边界并不复杂但表结构如果设计得随手后面写购物车和订单 SQL 时会非常痛苦。一个正确的起点是把数据库拆成五张表用户表、蛋糕表、购物车表、订单表、订单明细表。为什么不把订单明细塞进订单表因为一张订单对应多个商品如果放在一行里后续统计销量时需要拆分字符串那是给自己找麻烦。先看建表 SQL我用的是 MySQL 5.7这也是很多学校机房统一安装的版本CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码建议存MD5或SHA-256, phone VARCHAR(20) DEFAULT NULL, address VARCHAR(200) DEFAULT NULL COMMENT 默认收货地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cake ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(30) DEFAULT 奶油蛋糕 COMMENT 分类奶油/慕斯/翻糖, taste VARCHAR(50) DEFAULT NULL COMMENT 口味标签草莓、巧克力, size VARCHAR(20) DEFAULT NULL COMMENT 尺寸6寸/8寸/10寸, price DECIMAL(10,2) NOT NULL COMMENT 价格用DECIMAL避免浮点误差, stock INT NOT NULL DEFAULT 0, image VARCHAR(255) DEFAULT NULL COMMENT 图片相对路径如 images/cake01.jpg, description TEXT, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意price用的是DECIMAL(10,2)而不是FLOAT或DOUBLE。FLOAT 在 Java 中读出后可能变成 19.999999和页面价格对不上。image字段只存相对路径不存完整 URL这样项目换服务器时不用改数据库。密码字段里保存的应该是经过摘要算法处理后的值我在下一章会说明登录时怎么比对。购物车表的设计有一个取舍点如果只要求“能演示”购物车可以完全放在 Session 里不用建表但要做到退出浏览器后依然保留就需要持久化。我这里提供一个折中方案表结构保留但购物车操作优先走 Session下单时才把 Session 中的商品写入订单表。这能避免事务复杂度。3.2 蛋糕特有字段建模口味、尺寸、甜度与图片定位蛋糕不同于普通商品它的属性是多维度的用户挑选时常常关心的是“几个人吃”和“什么口味”所以我设计了category、taste、size三个独立字段。有的人会把这些塞进一个 JSON 字段里这在展示层面确实灵活但在“后台按口味筛选蛋糕”的场景里会让 SQL 变成灾难还是拆开更务实。甜度、保质期这类细节我没有放进主表因为它们属于描述性信息直接写在description里就够了。除非你明确需要按甜度排序或筛选才值得单独建一段属性表。图片方面image存的是相对路径例如images/strawberry.jpgJSP 里要显示图片时用base标签拼接绝对路径这个我们放到第 4 章图片定位部分细说。有一条实践经验值得记住蛋糕图片的命名不要用中文更不要用带空格的数字文件名例如2024 新款蛋糕.jpg。Tomcat 对中文文件名的处理在不同系统上表现不一致Windows 下可能正常部署到 Linux 后图片直接 404这是非常典型的“本地好好的上服务器就挂”的翻车现场。3.3 购物车与订单的状态流转从加购到下单的数据变化购物车与订单最大的区别在于购物车是用户的临时草稿订单是正式业务数据。所以我给订单表设计了明确的状态流转字段CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号界面展示用, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2配送中 3已完成 4已取消, receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(200) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, cake_id INT NOT NULL, cake_name VARCHAR(100) NOT NULL COMMENT 下单时商品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时价格快照, quantity INT NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;下单时把一个 Session 购物车转化到这两张表需要执行至少两次插入一次插订单主表一次循环插明细表。这个操作必须放在同一个事务里不然会出现“订单主表有记录、明细丢失”的中间状态。我的处理方式是让 DAO 层接收一个 Connection 对象并在 Servlet 或 Service 层控制commit与rollback这一点会在第 5 章踩坑部分再展开。3.4 JDBC 工具类与连接参数连接、释放、事务的常见写法数据库连接代码在项目里会被反复调用我习惯写一个工具类统一管理。这里要注意如果把连接信息直接写死在每个 DAO 里后期换库或者改密码就要全局搜索替换。我一般使用静态代码块加载驱动并提供重载的获取连接方法package com.cakeshop.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/cake_shop?useUnicodetruecharacterEncodingUTF-8useSSLfalse; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查WEB-INF/lib下是否有jar包); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }useUnicodetruecharacterEncodingUTF-8这两个参数解决中文乱码问题useSSLfalse是为了避免 MySQL 5.7 在本地返回 SSL 警告刷屏。如果你用的是 MySQL 8.0驱动类名要换成com.mysql.cj.jdbc.DriverURL 还需要追加serverTimezoneAsia/Shanghai否则时间字段会差 8 个小时。这是我见过的最典型的“本地能跑、查询报错”的原因之一。4. 在 IDEA 里跑通完整流程Tomcat 配置、Servlet 注册与 JSP 页面渲染4.1 IDEA 运行 JavaWeb 项目配置Tomcat、Artifact 与热部署参数这是整个项目中我见过新手耗时最多的一关。IDEA 里运行 JavaWeb 项目核心不是写代码而是把项目打包成一个 Artifact 并挂到 Tomcat 上。步骤是Run → Edit Configurations → 左上角 → Tomcat Server → Local然后在Deployment标签页点击“”添加Artifact选择项目的war exploded格式。有几个参数必须认真对待。Application context决定访问路径前缀如果设置为/cake那么访问首页的地址就是http://localhost:8080/cake/我一般直接设置为/这样项目一启动就是根路径省去后面拼接前缀的麻烦。HTTP port默认是 8080如果被别的进程占用了会直接在控制台报Port 8080 is already in use这时可以改成 8081但要注意改了端口之后JSP 里所有的跳转链接都要同步更新。热部署选项里有一项On frame deactivation建议选择Update resources and classes这样你修改 JSP 或 CSS 后只要从 IDEA 切换到浏览器资源就会自动更新不必重启整个 Tomcat。但要注意如果你新增了 Java 方法或改变了类结构热部署有时会失效并抛出ClassCastException此时最稳妥的做法是重启 Tomcat。可能你还遇到过“JSP 页面让加载完后刷新一次”的需求比如后台改了蛋糕库存回到商品列表页希望看到最新数据但浏览器缓存了旧页面。这时候不要靠 Java 代码在处理最简单的方式是在 JSP 的head中加入一段刷新逻辑让页面完成渲染后自动重新拉取一次最新数据同时配合后端在返回响应时设置禁用缓存的响应头。meta http-equivCache-Control contentno-cache, no-store, must-revalidate /这段声明告诉浏览器这个页面不允许缓存每次重新访问都会向服务器发新请求。比 JSP 里用 JavaScript 定时刷新更干净而且不会出现“页面闪一下再更新”的视觉问题。记住一个判断原则需要强制刷新的是数据禁用缓存是在响应的源头动手。4.2 用 JDBC Servlet 把蛋糕列表搬到页面现在开始写第一个实际业务展示蛋糕列表。Servlet 负责查询数据这里的 DAO 用一个findAll()方法返回 ListWebServlet(/cake/list) public class CakeListServlet extends HttpServlet { private CakeDao cakeDao new CakeDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String category request.getParameter(category); ListCake cakeList cakeDao.findByCategory(category); request.setAttribute(cakeList, cakeList); request.getRequestDispatcher(/cake_list.jsp).forward(request, response); } }request.getParameter(category)接收的是 URL 中的查询参数例如cake/list?category慕斯因为 JSP 是转发的所以参数在 JSP 中也能通过${param.category}取回。cakeDao.findByCategory需要处理一个特殊情况当 category 为 null 时应查询所有蛋糕否则加条件这个在 SQL 里用动态拼接实现但要注意防止 SQL 注入参数要经过PreparedStatement的占位符传递。在cake_list.jsp中展示时我用 JSTL 标签循环列表并且把图片地址拼接成可访问的路径% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % base hrefhttp://${header[host]}${pageContext.request.contextPath}/ c:forEach items${cakeList} varcake div classcake-card img src${cake.image} alt${cake.name} / h3${cake.name}/h3 p口味${cake.taste}尺寸${cake.size}/p p classprice${cake.price}/p a hrefcart/add?id${cake.id}加入购物车/a /div /c:forEachbase标签是整个图片显示的关键。JSP 页面经过转发后浏览器地址栏 URL 并不会变但页面里所有相对路径的解析基准是当前 URL如果不加 baseimages/a.jpg可能会解析成http://localhost:8080/cake/images/a.jpg而加上 base 后解析为http://localhost:8080/项目名/images/a.jpg。这里的${pageContext.request.contextPath}会动态获取当前项目名所以项目部署时改名字图片也不会坏掉。这是解决“JSP 图片如何对坐标定位”最常见、最稳定的基础设施先让路径对再谈位置。4.3 购物车Session 到底存什么、怎么存购物车在 JavaWeb 里最常见的做法是放在 Session 中而不是每次操作都去读写数据库。原因有两层一是购物车是高频操作加购、改数量、删除都会频繁发生走数据库压力大二是用户没有登录也能加购此时数据库里连 user_id 都没有硬要落库反而不现实。我把购物车的结构设计成MapInteger, Integerkey 是蛋糕 idvalue 是数量然后把这个 Map 放进 Session 里。加购的 Servlet 逻辑如下WebServlet(/cart/add) public class CartAddServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int cakeId Integer.parseInt(request.getParameter(id)); HttpSession session request.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); } cart.put(cakeId, cart.getOrDefault(cakeId, 0) 1); session.setAttribute(cart, cart); response.sendRedirect(cart/view); } }这里有一个关键细节加购成功后我使用sendRedirect而不是forward。如果你用转转发的形式用户按一次 F5 刷新购物车里的数量就会 1这是“刷新导致重复下单”的经典翻车现场。sendRedirect会让浏览器重新发出一个 GET 请求地址栏变成cart/view此时再刷新也只是重新查看购物车不会触发加购。这也是协议设计中“POST 后重定向”模式在 Servlet 里的具体实现。cart/view这个 Servlet 根据 Map 中的蛋糕 id 查询出商品信息合成一个列表返回给 JSP页面循环展示即可。到了用户点击“结算”时我会强制要求用户先登录因为订单表必须知道 user_id这一步逻辑非常自然属于业务倒逼设计。4.4 个人信息展示页面与登录态Session 里的用户对象登录功能是所有后续操作的闸门。登录 Servlet 接收用户名和密码后从数据库查出用户比对通过后把整个 user 对象放进 SessionWebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { request.getSession().setAttribute(loginUser, user); response.sendRedirect(cake/list); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }密码比对前先做一次 MD5 摘要数据库里存的也是摘要值这样即使数据库被脱库也无法直接看到明文密码。不要用明文密码存库这是哪怕课程设计也应该坚持的习惯。登录成功后用户表里的昵称、电话、默认地址就可以在“jsp 个人信息展示页面”里拿出来展示了例如个人中心页面div classuser-info p用户名${loginUser.username}/p p手机号${loginUser.phone}/p p默认地址${loginUser.address}/p p注册时间${loginUser.createTime}/p /div注意${loginUser.phone}能直接取到值是因为 setAttribute 时存的是完整对象而不是散列的几个字符串。对象入 Session 是信息聚合的很好做法后续订单确认页也要用这里的地址作为默认收货地址。但要记住Session 里的数据不能无限膨胀如果蛋糕店项目后续增加了“浏览记录”“优惠券”这类功能尽量采用瘦身方案只把关键对象放 Session普通数据临时放 request。4.5 商品图片与页面美化定位方式与图标库引入到了这一步很多人的页面还是一堆文字上下排列看起来完全不像一个蛋糕店。其实 JSP 项目的页面美化完全可以用现成的前端方案补齐。图片的定位方式我一般分三种场景去处理商品列表里的蛋糕图片属于等尺寸卡片用 CSS Grid 或 Flexbox 布局即可不需要绝对定位详情页中大图配文字介绍小数点的错位用浮动布局如果是页面上的装饰性元素例如“新品上市”的角标才用position: absolute配合父容器position: relative进行绝对定位。在 JSP 页面里引入饿了么 Element 图标库是很常见的实操技巧因为 JSP 和普通 HTML 没有区别直接使用 CDN 就能让页面获得一套成熟的图标link relstylesheet hrefhttps://unpkg.com/element-ui/lib/theme-chalk/index.css然后你就可以在购物车按钮、删除按钮等位置直接使用i classel-icon-shopping-cart-2/i这类图标页面看起来会专业很多。不过这里有一个注意点Educational 环境不一定能访问外网 CDN如果页面引用了外部 CDN 但网络被限制图标会全部变成小方块。稳妥做法是把 CSS 文件下载后放到webapp/css目录下引用成本低且不依赖外网。5. 开发期最容易翻车的七个细节排查路径与解决记录5.1 Tomcat 启动后访问首页 404Artifact 没部署到位我见过最多的失败现场是代码写完了Tomcat 也启动了但浏览器访问http://localhost:8080/报 404。原因往往是 Deployment 标签页只添加了 Tomcat 默认的根 Artifact没有把自己项目的 war exploded 加进去。你会在 Tomcat 启动日志里看到项目名没有出现在/xxx路径下。解决方式是重新打开Edit Configurations → Deployment移除当前 Artifact重新添加项目模块再确认Application context是/然后重启 Tomcat。5.2 JSP 页面中文全部变成问号编码链没有对齐乱码问题的根源是字符在“浏览器 → Tomcat → Servlet → MySQL”这条链路里用了不同的编码规则。我处理乱码的顺序是先确认 JSP 文件本身是 UTF-8 编码存储再检查 JSP 头部的pageEncoding和contentType然后确认 web.xml 里配置了编码过滤器最后检查 MySQL 连接 URL 是否带上了characterEncodingUTF-8。四个地方只要有一处漏掉中文就可能变成问号。注意 IDEA 默认的工程编码如果设置成 GBK那你 JSP 里写再多 UTF-8 声明也没用必须把 IDE 的File Encodings全部改为 UTF-8。5.3 Servlet 请求路径匹配时映射重复注解和 web.xml 同时配置如果你既在 Servlet 类上写了WebServlet又在 web.xml 里写了一遍servlet-mappingTomcat 启动时会直接抛出生命周期异常提示两个映射指向同一路径。这不算大问题删掉其中一处就行。但我建议统一采用注解方式因为注解离代码更近修改路径时不易漏改。web.xml 只保留欢迎页、编码过滤器和 session 超时等全局配置。5.4 图片加载不出来或者位置乱了先查路径再查定位方式图片 404 时不要急着调 CSS先在浏览器按 F12 打开 Network 面板看 img 请求的 URL 是什么。如果 URL 里少了项目名说明缺少base标签回到 4.2 节的 base 方案。如果 URL 是正确的但返回 404检查图片是否真的存在以及文件名大小写是否一致。Linux 上文件名是区分大小写的Cake01.jpg和cake01.jpg是两个文件这属于只有部署到生产环境才会暴露的坑。定位错位的问题通常是因为图片没有固定宽高撑开了父容器解决方式是给.cake-card img设置统一的width: 100%; height: 200px; object-fit: cover;。这是最省事的图片定位方式。5.5 刷新页面后购物车数量翻倍没有遵循“POST 后重定向”这个问题我在 4.3 节里提过但值得单独列出因为它太常见了。现象是加购后点击一次按钮购物车里数量变成 2。原因是你加购的请求如果使用forward跳转到购物车页面浏览器中的当前地址仍然是cart/add刷新就等于再次提交加购。解决方式就是凡是会产生数据变更的操作加购、删除购物车项、提交订单处理成功后一律使用response.sendRedirect让浏览器跳到一个只读的查看页面。这样即使用户把 F5 按烂也只是重复查询。5.6 ClassNotFoundException 连不上 MySQLjar 包没有进最终部署产物代码里连接数据库报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver但你在 IDEA 的 Libraries 里明明添加了 jar。深层原因是 IDEA 的依赖管理有两个维度编译期依赖和部署期依赖。你添加的 jar 进入了编译 classpath但没有被复制到WEB-INF/lib里所以 Tomcat 运行时找不到。解决方式是打开Project Structure → Artifacts在 Output Layout 中确认mysql-connector.jar是否存在于WEB-INF/lib如果没有就手动添加。判断标准很简单直接去你项目的 out 目录下检查有没有这个 jar。5.7 下单后订单表和明细表数据不一致缺少事务控制最后一条是很容易被忽略但后果严重的创建订单时订单主表和明细表分别执行两次插入如果第二次插明细时出现异常主表数据已经写入你就得到了一条没有商品的空订单。解决方式是让这两个操作共享同一个数据库连接并把自动提交关闭。正确的调用方式是这样的Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); orderDao.insertOrder(conn, order); for (OrderItem item : items) { orderItemDao.insertItem(conn, item); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }这段代码把事务边界控制在 Service 层DAO 层的方法都接收 Connection 参数而不是各自去获取连接。这是事务控制的核心逻辑同一个事务必须使用同一个连接否则commit和rollback形同虚设。你甚至可以通过故意在第二个 insert 前抛一个异常来验证回滚是否生效这是验证事务最直接的办法。6. 从能跑到能上线会话安全、分页查询与部署前验证到这一步你的项目已经能在本地完整运行了但要想它经得起演示或者实际使用还需要做三个层面的收尾工作会话管理、列表分页、部署验证。第一项是设置 Session 超时时间。默认 Tomcat 的 session 超时是 30 分钟如果你希望用户在关掉浏览器后一段时间内继续保持登录可以在 web.xml 中显式配置session-config的session-timeout为 60 分钟。另外千万不要把密码存储在 Session 里前面第 4 章个人中心展示用户信息时应该把 password 字段置空或打码后再放入 Session这样即使 Session 被窃取也不至于泄露明文凭据。第二项是商品列表的分页。蛋糕店商品一般在几十个左右不分页也能跑但订单列表一定会越来越多。给蛋糕列表加上一个简单的分页查询只需要在前端接受两个参数page和pageSizeSQL 里使用LIMIT ?, ?占位符来查询再用一个count()查询总条数最后在 JSP 中渲染上一页和下一页的链接。注意页码越界的处理用户手动在 URL 里输入一个非常大的页码时要把页码强制重置为最后一页否则页面空白会让人误以为程序崩了。第三项是部署验证。把项目打成 war 包放到 Tomcat 的webapps目录下启动后你能看到 Tomcat 自动解压部署。这里要验证的是 base 标签是否还能正确工作因为 war 包部署后的项目名不再是 IDEA 里的那个 Artifact 名pageContext.request.contextPath会动态适配合集但如果你在某个 JSP 里硬编码了/cake/images/这种路径就一定会 404。所以上线前的最佳做法是全局搜索所有src和href凡是不以${pageContext.request.contextPath}开头或不是完整的绝对 URL都值得重新审视。我做这类项目最大的教训是永远不要高估环境的一致性。同样的代码在 Windows 的 IDEA 里没有任何问题部署到 Linux 的 Tomcat 上就可能在文件路径、字符集和驱动版本上全部爆发问题。与其到时候慌乱不如从第一天就把路径写对、把编码统一、把事务边界画清楚。这些基本功比任何花哨的前端效果都值钱。希望帮到你。本文还有配套的精品资源点击获取