JSP+Servlet蛋糕店项目实战:从购物车到订单管理的JavaWeb全流程解析

发布时间:2026/10/4 12:10:41
JSP+Servlet蛋糕店项目实战:从购物车到订单管理的JavaWeb全流程解析
简介基于jsp和servlet的JavaWeb蛋糕店售卖网站项目完整覆盖前台购物与后台管理两大模块适合JavaWeb初学者、毕业设计及课程实训场景。前台包含商品分类展示、推荐位、购物车、登录注册、订单查询与关键词搜索后台支持订单状态流转、用户管理、商品类目与商品信息维护。项目采用IDEA开发Tomcat 8.5.35MySQL 5.7使用c3p0连接池与DButil工具提供SQL数据库脚本和分层实现思路。压缩包共290个文件包含82个jpg图片、58个jsp页面、55个java源码、20个xml配置、14个js、12个css以及jar包和数据库脚本整体19.11MB目录结构规整便于按前台、后台模块对照学习。目前已有127人学习下载适合用来理解JavaWeb经典三层架构、购物车逻辑及后台CRUD流程也可作为功能较完整的课程设计项目参考。1. 这个 JSPServlet 蛋糕店项目不用框架也能讲透电商闭环的完整案例这个基于 JSP 和 Servlet 的 JavaWeb 蛋糕店售卖网站是一套没有 Spring Boot、完全靠 Tomcat 8.5 MySQL 5.7 把页面和数据库串起来的完整项目源码。前台有推荐商品、分类展示、商品详情、购物车、登录注册、个人信息修改、下单付款、订单查询、关键字搜索后台有订单状态管理、用户增删改查、商品类目维护、商品信息编辑删除。对正在学 Servlet/JSP 的人来说它比直接上手 Spring Boot 更能理解一次 HTTP 请求从 JSP 页面到 Servlet、再到 DAO、最后回到页面的完整链路对要交课程设计的人来说它的功能点足够撑起一篇完整的毕业设计和答辩演示不用再东拼西凑。2. 环境与项目骨架IDEA 2017、Tomcat 8.5、MySQL 5.7 怎么配合才不翻车2.1 版本匹配关系为什么这套组合是课程设计的标准答案项目正文里明确写了开发环境是 IDEA 2017.3.5、Tomcat 8.5.35、MySQL 5.7开发插件用到 c3p0 和 DButil。这套组合在 2017 到 2019 年期间是 JavaWeb 课程设计的“顶配”到现在依然有大量教学案例沿用因为它有一个很难替代的优势版本之间互相咬合得严丝合缝出问题时排查路径非常短。Tomcat 8.5 对应的是 Servlet 3.1 和 JSP 2.3 规范支持注解配置和异步处理但它又不像 Tomcat 9/10 那样强制要求 Jakarta 命名空间所以网上绝大多数的 JavaWeb 教程代码都可以直接跑。IDEA 2017 自带的标准 Tomcat Server 运行配置就是围绕 8.x 设计的部署时不需要额外插件的兼容层。MySQL 5.7 是目前 JDBC 连接串兼容性最好的版本老项目常用的jdbc:mysql://localhost:3306/xxx写法不需要改驱动类名也不存在 MySQL 8.0 那种cj驱动迁移问题。前端部分项目用的是 Bootstrap 的 CSS 框架和 layer 弹窗组件。Bootstrap 负责栅格布局和按钮、表单、导航栏的样式layer 负责购物车确认、订单操作提示这类轻交互弹层。这种选型在当年非常主流如今看也一点不落后——JSP 页面本身就是服务端渲染配合 Bootstrap 的响应式栅格直接在 PC 浏览器里调试即可不用额外配置 Node 环境。技术栈对应关系我整理了一张表模块技术选型作用服务端容器Tomcat 8.5.35运行 Servlet 和 JSP处理 HTTP 请求后端语言Java 8项目源码基于 JDK 8 编写页面渲染JSP JSTL服务端动态输出 HTML控制层Servlet接收请求、调用业务逻辑、转发页面数据库MySQL 5.7存储用户、商品、订单等数据数据库连接池c3p0管理数据库连接复用JDBC 封装DButil封装连接获取、关闭和事务操作前端框架Bootstrap layer页面布局、样式、弹窗交互提示如果你本机装的是 MySQL 8.0这个项目也能跑但连接串里的驱动要换成com.mysql.cj.jdbc.Driver并加上serverTimezoneAsia/Shanghai后面避坑章节会专门讲。2.2 从解压到出页面四步把项目在本地跑起来拿到压缩包之后第一步不是急着打开 IDEA而是先看目录结构。典型的分层结构像这样cake_shop/ ├── src/ │ ├── com/xxx/entity/ # 实体类User、Product、Category、Order等 │ ├── com/xxx/dao/ # 数据访问层操作数据库表 │ ├── com/xxx/servlet/ # 控制器处理请求和转发 │ ├── com/xxx/util/ # 工具类DButil、MD5Util等 │ └── c3p0-config.xml # 连接池配置文件 ├── web/ │ ├── index.jsp # 前台首页 │ ├── cart.jsp # 购物车页面 │ ├── user/ # 登录、注册、个人信息页面 │ ├── product/ # 商品列表、商品详情页面 │ ├── order/ # 订单列表页面 │ ├── admin/ # 后台管理页面 │ ├── static/ # css、js、images 静态资源 │ └── WEB-INF/ │ ├── web.xml # 项目部署描述符 │ └── lib/ # 项目依赖的 jar 包 └── sql/ └── cake_shop.sql # 数据库初始化脚本这种结构是 Maven 出现之前的经典 Web 项目布局src放 Java 源码web是 Web 根目录编译输出后 IDEA 会自动把 class 文件和web下的资源打包成 war。没有 Maven 的依赖管理所有 jar 包都手动放在WEB-INF/lib下好处是脱离中央仓库也能离线编译坏处是依赖版本得自己留意。第二步是初始化数据库。用 MySQL 命令行或 Navicat 执行sql目录下的脚本CREATE DATABASE IF NOT EXISTS cake_shop DEFAULT CHARACTER SET utf8mb4; USE cake_shop; SOURCE D:/cake_shop/sql/cake_shop.sql;执行完建议立刻检查几张核心表的数量比如商品表、用户表、订单表确认脚本完整导入了初始数据。如果表是空的后续前台页面会一片空白容易误判成项目跑不起来。第三步是改连接池配置。打开src/c3p0-config.xml把数据库用户名和密码改成你本机的property nameuser valueroot/ property namepassword value123456/ property namedriverClass valuecom.mysql.jdbc.Driver/ property namejdbcUrl valuejdbc:mysql://localhost:3306/cake_shop?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalse/value /property这段配置里driverClass是 MySQL 5.7 对应的 JDBC 驱动全类名jdbcUrl中必须带上useUnicodetruecharacterEncodingutf8否则页面中文正常但数据库里写入的数据会出现乱码。useSSLfalse是为了避免本地连接时 MySQL 报 SSL 告警。如果你用 MySQL 8.0驱动类名要改成com.mysql.cj.jdbc.Driver并且jdbcUrl最后追加serverTimezoneAsia/Shanghai。第四步是在 IDEA 里配置 Tomcat。菜单路径是 Run → Edit Configurations点左上角加号选择 Tomcat Server → Local然后在 Server 面板里选择本地 Tomcat 8.5 的安装目录Deployment 面板点加号把当前项目的 war 包加进去Application context 建议设为/cake_shop。启动后浏览器访问http://localhost:8080/cake_shop/能看到首页就算部署成功。注意IDEA 2017 的 Tomcat 集成和现在的版本略有不同。新版 IDEA 要求先在 Settings → Build Tools → Maven 里指定 Maven老版本直接把 Tomcat 路径配进 Run Configuration 就能用。配好之前先确认 Tomcat 没有手动启动过端口不冲突。2.3 数据库表怎么分用户、商品、订单三组表的协作蛋糕店项目的数据模型不复杂核心表按业务域分三组用户域是tb_user商品域是tb_category和tb_product订单域是tb_order和tb_order_item再加上后台需要的管理员标识一般就这几张表。表名关键字段业务说明tb_userid, username, password, phone, address, role前台用户注册信息和后台管理员共用tb_categoryid, name, description商品类目如蛋糕、慕斯、面包tb_productid, category_id, name, price, image, intro, is_hot, is_new, status商品基本信息与推荐位标记tb_orderid, user_id, order_no, total_amount, status, create_time订单主表状态未付款/已付款/已发货/已完成tb_order_itemid, order_id, product_id, product_name, price, quantity订单明细快照商品信息注意tb_order_item里重复保存了product_name和price这是电商系统里常见的“快照”设计——订单生成后商品改名或调价不影响用户订单里记录的信息。理解这张表的设计思路比记住字段本身更有价值。如果不小心删掉了这里的某张表或者tb_product和tb_category之间没有外键约束功能上其实还能用但后台删除类目时会连带产生孤儿数据前台商品列表会出现“分类不存在”的展示问题。我在后面避坑章节会再展开讲这一点。3. 前台功能拆解从首页推荐位到搜索框的完整调用路径3.1 推荐商品和分类商品一个 Servlet 加三次查询铺满首页前台首页的推荐位分成三块条幅推荐、热销推荐、新品推荐。对应到数据库层面的实现最直接的做法是给tb_product设计两个布尔字段并在查询时配合排序条件。常见做法是给tb_product加两个字段is_hot标记热销is_new标记新品。条幅推荐则用is_hot 1 AND is_new 1的前两条记录或者单独用一个banner字段。项目里的 DAO 层查询逻辑大体会是这样// ProductDao.java public ListProduct findHotProducts(int limit) { String sql SELECT id, category_id, name, price, image, intro, is_hot, is_new FROM tb_product WHERE is_hot 1 AND status 1 ORDER BY id DESC LIMIT ?; return query(sql, limit); } public ListProduct findNewProducts(int limit) { String sql SELECT id, category_id, name, price, image, intro, is_hot, is_new FROM tb_product WHERE is_new 1 AND status 1 ORDER BY create_time DESC LIMIT ?; return query(sql, limit); }这里有两个参数要注意。LIMIT ?占位符在 MySQL 里可以直接用 JDBC 传参但如果你换成了 Oracle 或者 SQL Server分页写法要改成ROWNUM或OFFSET FETCH。status 1是商品上下架状态位后台把商品下架后前台推荐位就不会再显示它这比物理删除数据更安全。首页对应的 Servlet 拿到这些列表之后通常是这样转发到 JSP 的// IndexServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) { ProductDao dao new ProductDao(); request.setAttribute(hotList, dao.findHotProducts(4)); request.setAttribute(newList, dao.findNewProducts(4)); request.setAttribute(categoryList, dao.findAllCategories()); request.getRequestDispatcher(/index.jsp).forward(request, response); }request.setAttribute把查询结果放进请求域JSP 页面里用 JSTL 的c:forEach遍历输出这样页面代码里不会有 Java 脚本片段。项目如果用了脚本片段% %效果也一样但维护性要差一些。你在 JSP 里看到的c:forEach标签本质上是 JSTL 标签库帮你循环输出 HTML背后的集合对象就是 Servlet 塞进来的List。分类商品展示页的实现逻辑类似区别是 Servlet 接收categoryId参数然后调用findByCategoryId(categoryId)查询对应类别的商品列表。这里的坑在于分类参数校验——如果请求里没带categoryId需要给一个默认值或者跳回首页否则 SQL 拼接时会拿到null。3.2 购物车逻辑Session 里的 Map 能不能直接当购物车用购物车是这个项目里最值得研究的一块。代码量不大但涉及 Session 存储、对象引用、数量变更和结算联动。常见的实现方案是在用户点击“加入购物车”时把商品对象和数量存进 Session// CartServlet.java 核心逻辑 HttpSession session request.getSession(); MapProduct, Integer cart (MapProduct, Integer) session.getAttribute(cart); if (cart null) { cart new HashMapProduct, Integer(); } int productId Integer.parseInt(request.getParameter(productId)); int quantity 1; Product product productDao.findById(productId); if (cart.containsKey(product)) { cart.put(product, cart.get(product) quantity); } else { cart.put(product, quantity); } session.setAttribute(cart, cart);这段代码用HashMapProduct, Integer作为购物车结构Product对象作为键。这里有个隐蔽的坑Product必须正确重写hashCode()和equals()否则同一个商品每次从数据库查出来的对象都是不同的引用containsKey判断永远不相等购物车同一个商品会出现多行。如果项目里的Product是简单实体没有重写这两个方法我一般会建议改成用productId作为 Map 的键把Product作为 value 存进一个HashMapInteger, IntegerID 对应数量购物车页面再根据 ID 反查商品。这样不仅绕开了 equals 问题后续做购物车数量回显也更简单。购物车修改数量、删除商品这些操作本质上是拿到 Session 里的 map 对象做一次put或remove然后重新setAttribute。注意每次操作都要重新写回 Session不是因为 Session 里的对象是引用类型而是为了语义清晰避免某些 Servlet 容器对 Session 序列化时出现脏数据。关于购物车里商品的小计金额和总金额不要在前端 JSP 里用 JavaScript 计算正确做法是在 JSP 渲染时用 EL 表达式${item.price * item.quantity}直接算出小计再在c:forEach外面用自定义标签或脚本片段汇总。这个汇总逻辑也可以放到 Servlet 里提前算好存成totalAmount塞进 requestJSP 只负责显示。3.3 注册、登录和个人信息Session 生命周期与三处状态同步用户模块的流程绝大多数 JavaWeb 课程设计都一样注册页提交用户名和密码Servlet 校验用户名是否已被占用通过后写入tb_user然后重定向到登录页。登录成功后在 Session 里存一个user对象后续页面通过判断session.getAttribute(user)是否为空来决定展示“欢迎你”还是“登录”按钮。注册逻辑里有一个值得注意的细节密码存储。很多课程设计直接明文存数据库这在功能演示上没问题但如果你要把这个项目写进简历或交给导师看建议至少做一个 MD5 加盐。项目里如果有MD5Util工具类在注册和登录两个入口都要调用而且要保证加盐规则一致// 注册时 String md5Password MD5Util.md5(password cake_salt); user.setPassword(md5Password); userDao.insert(user); // 登录时 String md5Password MD5Util.md5(password cake_salt); User loginUser userDao.findByUsernameAndPassword(username, md5Password);加盐的意义是防止两个不同用户密码相同导致密文相同。这里强调一下MD5 本身不可逆但不加盐的 MD5 可以被彩虹表直接反查出原密码。课程设计加一层盐技术上就说得过去了也不用上升到 SHA-256 的复杂度。个人信息修改包含两块密码修改和收获信息修改。密码修改需要用户输入旧密码Servlet 先比对旧密码是否匹配匹配后再更新新密码。收获信息就是tb_user里的phone和address字段这两个字段在生成订单时要回填到订单表所以修改后应该立刻更新 Session 里的user对象否则用户在同一个会话里下单订单里的收货地址还是旧的。Session 的另一个作用是保存登录状态。项目里一般用request.getSession().setAttribute(user, user)标记登录退出时调用session.invalidate()销毁会话。如果项目没做过滤器未登录用户可以直接访问订单查询页面这时候页面取session.getAttribute(user)得到的是null页面里用${user.name}会报错或显示空白。正确的做法是加一个判断未登录用户重定向到login.jsp或者在后端做一个全局登录拦截过滤器。3.4 下单、付款和订单查询状态字段怎么流转才算闭环下单是整个前台模块里最重的操作因为它要同时写订单主表和订单明细表。代码流程上一般是提交购物车后Servlet 拉取 Session 里的购物车数据计算总金额生成订单号然后逐条插入明细。// OrderServlet.java 下单核心流程 User user (User) session.getAttribute(user); MapProduct, Integer cart (MapProduct, Integer) session.getAttribute(cart); String orderNo CK System.currentTimeMillis(); double totalAmount 0; ListOrderItem items new ArrayList(); for (Map.EntryProduct, Integer entry : cart.entrySet()) { Product p entry.getKey(); int quantity entry.getValue(); totalAmount p.getPrice() * quantity; OrderItem item new OrderItem(); item.setOrderNo(orderNo); item.setProductId(p.getId()); item.setProductName(p.getName()); item.setPrice(p.getPrice()); item.setQuantity(quantity); items.add(item); } order.setOrderNo(orderNo); order.setUserId(user.getId()); order.setTotalAmount(totalAmount); order.setStatus(0); // 0:未付款, 1:已付款, 2:已发货, 3:已完成订单状态是整个项目里最重要的状态机。0 是未付款用户在前台点击“去付款”后状态变为 1后台管理员发货后变为 2用户确认收货或管理员标记完成后变为 3。删除订单不是物理删除而是把状态改成 4 或者标记is_deleted字段这样订单总金额统计里仍然有据可查。订单查询分两类用户查自己的订单、管理员查所有订单。用户的查询入口在个人中心SQL 基本是SELECT * FROM tb_order WHERE user_id ? ORDER BY create_time DESC。如果项目里做了分页功能DAO 层要考虑LIMIT的偏移量计算否则超过一页的数据翻页会重复。关键字搜索的逻辑可以跟在商品展示模块后面讲。搜索框提交keyword给搜索 ServletServlet 调用 DAO 的模糊查询public ListProduct searchProducts(String keyword) { String sql SELECT id, category_id, name, price, image, intro FROM tb_product WHERE name LIKE ? AND status 1; return query(sql, % keyword %); }注意LIKE %?%这种写法在 JDBC 里是不成立的必须把%拼进参数值里即% keyword %。如果项目里已经写成LIKE ?且传参时少了百分号搜索结果永远为空这是新手最容易踩的搜索类 bug。4. 后台管理功能五个模块就是一组带状态的 CRUD4.1 后台入口和权限识别管理员身份放在 Session 里还是单独表后台入口的触发条件项目描述里写得很明确当管理员用户登录后会显示后台管理按钮。这就意味着用户表里一定有一个字段标识身份常见的是role字段普通用户为 0管理员为 1。登录成功后只要判断user.getRole() 1就在首页渲染后台管理入口。问题来了这个判定放在 JSP 里还是放在 Servlet 里如果只在 JSP 里用c:if判断显隐那用户通过地址栏直接访问/admin/xxx.jsp仍然能进后台因为 JSP 本身不拦截请求。所以严谨的后台权限识别应该做成过滤器对所有/admin/*路径做 Session 检查// AdminFilter.java public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; User user (User) request.getSession().getAttribute(user); if (user null || user.getRole() ! 1) { response.sendRedirect(request.getContextPath() /login.jsp); } else { chain.doFilter(req, res); } }先在项目里查一下web.xml是否配置了类似的过滤器。很多课程设计没有这一步后台管理页面裸奔在地址栏里虽然不影响演示但在答辩时一旦被问到“权限是怎么控制的”容易答不上来。把过滤器补上成本很低但技术含量立刻不一样。4.2 订单管理按状态查询和发货、完成、删除的状态流转后台订单管理页面对应的是管理员对所有用户订单的集合操作。列表页通常会提供状态筛选栏管理员选择“待发货”后页面带上status1的参数请求后台订单 ServletDAO 用WHERE status ?过滤。订单操作的核心是状态流转有三个按钮对应三种操作按钮状态变化SQL 操作发货1已付款→ 2已发货UPDATE tb_order SET status 2 WHERE id ?完成2已发货→ 3已完成UPDATE tb_order SET status 3 WHERE id ?删除任意状态 → 4逻辑删除UPDATE tb_order SET status 4 WHERE id ?这里要注意两个边界。第一删除操作如果做成物理删除会连带把订单明细也删掉导致后台统计功能丧失数据源第二直接用等号更新状态没问题但严谨一点的写法是加状态条件比如发货操作写UPDATE tb_order SET status 2 WHERE id ? AND status 1可以防止重复点击或状态不一致时产生脏更新。后台执行状态变更后页面应该刷新一次让最新状态显示出来。常见做法是用 layer 弹窗确认后提交给一个专门的状态更新 Servlet完成后用 JavaScript 跳回订单列表页并带上原筛选参数。这就是所谓“操作后页面重载”的交互——改动展示分离弹窗层只负责提醒和确认真正的状态变更在 Servlet 里完成。4.3 用户管理后台怎么查、增、改、删用户后台用户管理模块包含四个基本功能查询所有用户、新增用户、修改用户密码、修改用户信息、删除用户。这套 CRUD 没有复杂逻辑唯一要注意的是删除用户的关联数据问题。用户tb_user与订单tb_order之间存在外键关系。后台删除一个用户时如果该用户已经下过订单直接删除用户会让订单表里的user_id变成悬空值。处理方式有两种一是先删除该用户的所有订单及订单明细再删除用户二是把用户状态改成禁用而非物理删除。课程设计项目里大多数选第一种操作路径是 UserDeleteServlet 里先调 OrderDao 按 user_id 删除订单再调 UserDao 删除用户。新增用户和修改用户密码的页面结构几乎一样区别在于新增走 insert修改走 update。密码修改有一个常见的做事习惯在后台列表里直接修改用户密码时会先弹出一个 layer prompt 让管理员输入新密码而不是跳转到新页面。这是因为 layer 弹窗层在当前页面操作更轻量也符合后台管理的操作习惯。实现方式是在列表页用 layer.prompt 收集新值然后通过 AJAX 提交到对应的 Servlet。后台列表里修改用户手机号和收货地址同样可以走 layer 弹窗。这种方式比整行编辑更省事缺点是每个字段要单独写一个 AJAX 接口。项目里如果做成了整页编辑表单维护起来也 OK看个人习惯。用户列表的查询一般配合分页和关键字搜索。如果项目只在后台写了一个findAll我建议补一个按用户名模糊查询的方法后台搜索用户时直接WHERE username LIKE ?这不算过度设计属于后台管理的基本功能预期。4.4 商品和类目管理图片上传和删除时的外键约束商品管理是后台模块里字段最多的一个功能因为tb_product本身字段多名称、价格、图片、简介、分类、上下架状态、热销标签、新品标签。新增商品的表单里最麻烦的是图片上传。JSP 表单需要设置enctypemultipart/form-dataServlet 端用 commons-fileupload 组件解析上传的图片文件并把文件保存到 Web 应用根目录的 upload 文件夹。保存路径写入数据库时要注意存相对路径还是绝对路径// 保存相对路径到数据库 String relativePath /static/upload/ fileName; product.setImage(relativePath); // JSP 页面直接 img src${product.image} 就能访问存相对路径的好处是项目换机器部署时图片路径不用改。如果存了绝对路径比如C:/apache-tomcat/webapps/...迁到 Linux 服务器后所有图片都会 404。这是老 JavaWeb 项目里非常经典的路径坑。类目管理的核心是分类的增删改。删除类目时要处理该类目下已关联商品的问题。如果tb_product表里category_id有外键约束直接删除类目会报外键约束错误即使没有外键删除后商品列表会没法展示分类名称。正确做法是先判断该类目下是否有商品有商品则提示“请先把该类目下商品移走或删除”否则才执行删除。对应到 SQL就是在删除前先执行一次SELECT COUNT(*) FROM tb_product WHERE category_id ?。商品编辑页回显时注意下拉框的选中状态。JSP 里做分类下拉框要用c:forEach遍历 categoryList并且在每个option上判断当前商品的categoryId是否等于循环项相等时输出selected属性。这个逻辑不复杂但很多人第一次写时漏了判断编辑商品时分类总会被重置成第一项。5. 避坑指南跑通这套 JavaWeb 项目最容易踩的六个问题5.1 Tomcat 启动报 ClassNotFound 或端口占用现象启动项目时 IDEA 控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者提示Port 8080 is already in use。原因ClassNotFound 通常是项目 Artifacts 里没有把WEB-INF/lib下的 jar 包打包进去IDEA 只是编译了源码没有把依赖放进部署目录端口占用则是有另一个 Tomcat 实例或者别的程序占了 8080。解决打开 File → Project Structure → Artifacts在 Output Layout 里看WEB-INF/lib下有没有 mysql-connector、c3p0、jstl 这些 jar确认项目是 war exploded 打包方式。端口冲突时在 Run Configuration 里把 Tomcat 端口改成 8081或找到占用进程结束掉。5.2 页面中文乱码数据库、连接串、JSP 三层编码不一致现象前台页面中文正常但后台录入商品后页面上显示????或者浏览器里全是乱码。原因MySQL 数据库表用了latin1默认字符集或者jdbcUrl没加characterEncodingutf8又或者 JSP 页面本身没写pageEncodingUTF-8和meta charsetutf-8。解决登录 MySQL 检查表字符集SHOW CREATE TABLE tb_product如果不是 utf8mb4 就执行ALTER TABLE tb_product CONVERT TO CHARACTER SET utf8mb4再把 c3p0 连接串补上useUnicodetruecharacterEncodingutf8最后统一 JSP 声明里 charsetutf-8。三层都统一后刷新页面即可。5.3 MySQL 连不上Access denied 或 Connection refused现象运行项目后提示Access denied for user rootlocalhost (using password: YES)或Connection refused。原因c3p0-config.xml 里的密码和 MySQL 实际密码不一致或者 MySQL 服务没启动又或者 MySQL 8.0 的 caching_sha2_password 认证插件不被旧驱动支持。解决先确认 MySQL 服务运行中用命令行mysql -uroot -p测一下密码是否正确。如果本机是 MySQL 8.0 就按 2.2 小节的方法换驱动和连接串如果是 MySQL 5.7 却提示认证插件问题检查是否之前改过 root 的 plugin。5.4 修改了 Servlet 或 JSP刷新页面却还是旧效果现象改了ProductDao里的 SQL重启 Tomcat 后页面报数没有变化甚至改了 JSP 重启后还是上一版样式。原因IDEA 的 Tomcat 运行配置使用了默认的 deployment 方式没有触发on frame deactivation热部署或者 JSP 编译缓存没有清掉。解决在 Run Configuration 的 On Update Action 里选择Update classes and resourcesOn frame deactivation 选Update classes and resources这样改 JSP 和静态资源时无需重启即可生效。改 Java 源码后还是要重新构建如果多次构建仍不生效尝试删掉tomcat/work目录下的缓存再启动。5.5 后台删除商品或类目报外键约束错误现象后台执行商品删除或类目删除时报Cannot delete or update a parent row: a foreign key constraint fails。原因tb_order_item里还有商品 ID 的引用tb_product里还有类目 ID 的引用。数据库外键阻止了直接删除。解决按依赖关系逆序删除——先删订单明细中对应商品 ID 的记录再删商品先确认类目下没有商品再删类目。不建议通过数据库去外键约束来绕过因为这会掩盖数据一致性问题。5.6 Session 里的用户对象或者购物车一直不生效现象登录成功跳转首页后页面上不显示用户名加入购物车后购物车页面仍然是空的。原因登录 Servlet 往 Session 里写入了user但首页 JSP 里判断的是session.getAttribute(user)名字不一致或者购物车 Servlet 写入了cart但购物车 JSP 读的是session.getAttribute(cart)两个字符串对不上。解决统一 Session 的 key 字符串建议在一个常量类里定义public static final String SESSION_USER user;和SESSION_CART cart;所有 Servlet 和 JSP 都用这个常量避免手写字符串拼错。如果项目里已经出现这种 bug全局搜索getAttribute(里的键值把所有不一致的名字逐一修正。6. 进阶改造给订单模块补上库存校验和事务回滚这个项目功能闭环已经完整但如果你要拿它参加答辩或者写进简历我建议做一个成本不高却很有说服力的改造给下单流程加库存扣减和事务控制。先在tb_product表里加一个库存字段简单执行ALTER TABLE tb_product ADD COLUMN stock INT DEFAULT 100;。然后在商品详情页的“加入购物车”按钮旁边显示剩余库存下单时进行校验。下订单的 Servlet 里要把插订单主表、插订单明细、扣库存这三个动作放进同一个事务。项目里 DButil 如果已经封装了事务方法一般长这样public void placeOrder(Order order, ListOrderItem items) throws SQLException { Connection conn DButil.getConnection(); try { conn.setAutoCommit(false); for (OrderItem item : items) { int rows orderDao.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足 item.getProductName()); } } orderDao.insertOrder(order); for (OrderItem item : items) { orderDao.insertOrderItem(item); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { DButil.close(conn); } }这段代码的核心是deductStock方法SQL 写成UPDATE tb_product SET stock stock - ? WHERE id ? AND stock ?。这样写的好处是利用数据库原子操作避免超卖——如果库存不够受影响行数是 0直接抛异常回滚。新手容易踩的坑是先SELECT stock再UPDATE两个步骤之间可能被其他请求打断。把这个更新操作合成一条 SQL就不用纠结并发问题了。事务最烦人的地方在于连接管理。setAutoCommit(false)之前必须拿到的是同一个 Connection如果订单表插入和明细表插入分别调用了DButil.getConnection()那它们不在同一个事务里前半段成功后半段失败就会出现脏订单。我一般习惯检查 DButil 里是不是用了 ThreadLocal 来绑定当前线程的连接没有的话就用事务 Service 方法把 Connection 作为参数传下去最不济就在一个方法内集中处理不要在多个方法里各自拿连接。做完这个改造后可以从验证效果的角度自查把某商品库存改成 1同时开两个浏览器页面下单该商品观察是否只有一个订单能成功扣掉库存人为修改明细表插入 SQL 使其报错看订单主表和库存是否都回滚到原始状态。这两条验证路径能证明事务和库存扣减是真实生效的不是嘴上说说。从那以后我每次上手一个 JavaWeb 课程设计都会先看一眼下单流程有没有事务包裹、购物车有没有走 Session、后台管理有没有做权限拦截这三个点检查完项目能不能拿去答辩我心里基本有数了。希望这个拆解过程也能帮你在自己的项目里快速定位到值得改的地方。本文还有配套的精品资源点击获取