JavaWeb蛋糕店系统:完整业务闭环的课程设计实战项目
2026/9/23 3:17:39 网站建设 项目流程

简介:这是一套完整的JavaWeb蛋糕店网站系统课程设计资源,面向计算机相关专业在校学生、教师及初学者,解决课程设计、毕业设计与Web开发入门实践需求。资源包含可直接运行的源码、界面演示视频、详细文档说明、MySQL数据库SQL脚本及全量中文注释,覆盖前后台核心功能:前台支持商品展示(推荐/分类/搜索)、购物车管理、用户注册登录与订单查询;后台提供管理员商品与订单管理模块。压缩包共586个文件,含78个JSP页面、55个Java业务类、110个编译后Class文件、48个PNG/JPG图片资源、28个JS交互脚本及2个SQL建库脚本,整体17.4MB,结构清晰、注释详尽,便于理解MVC分层逻辑与Servlet+JDBC技术栈实现细节。已有694人下载学习,代码经实际部署测试,答辩平均分96分,适合作为毕设参考、课设原型或二次开发基础框架。

1. 这不是又一个“Hello World”JavaWeb项目:它是一套能直接跑通、带完整业务闭环的蛋糕店网站系统,专为课程设计和毕设答辩打磨过96分

你是不是也经历过——在老师布置“JavaWeb课程设计”时,翻遍CSDN、GitHub、某宝,下载了十几个“蛋糕店”“水果店”“书店”源码,解压后发现:

  • web.xml里 servlet 映射路径写错,404 到怀疑人生;
  • MySQL 表字段名和 Java 实体类属性对不上,NullPointerException报在setPrice()那一行;
  • 前台购物车加不进去,后台登录跳转死循环,连首页 banner 都加载不出来;
  • README 只有一行“导入数据库即可运行”,但 SQL 文件里CREATE DATABASE cake_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这种关键语句根本没写,你用 MySQL 8.0 默认编码建库,中文全变问号。

这套资源不一样。它不是教学演示片段,也不是半成品骨架,而是一个从用户注册→浏览商品→加购→下单→管理员审核→订单查询全链路走通的实战系统。所有模块(前台7大功能+后台5大管理项)均经 IDEA + Tomcat 9 + MySQL 5.7/8.0 实测运行成功,答辩平均分96分——这个分数背后是真实可验证的页面跳转逻辑、无硬编码的数据库连接配置、JSP 中 EL 表达式与 JSTL 标签的规范使用,以及每一处// TODO:都已被删掉的干净注释。适合计算机类专业学生快速上手课程设计、教师用于课堂案例拆解、甚至企业新员工做 JavaWeb 入门沙盒环境。别再花3小时配环境,今天下午就能把“热销蛋糕”列表刷出来。


2. 从零启动:环境准备、数据库导入、项目部署三步落地(含 IDEA 2023+ 配置细节)

2.1 环境版本选型依据:为什么坚持用 Tomcat 9 + MySQL 5.7/8.0 而非最新版?

很多新手盲目追求“最新”,结果在 IDEA 里新建 JavaWeb 项目时选了 Tomcat 10,却发现javax.servlet.*包全部报红——因为 Tomcat 10 已全面迁移到 Jakarta EE 9,包名从javax.*改为jakarta.*,而本项目所有 JSP、Servlet、Filter 仍基于 Servlet 4.0 规范(javax.servlet.http.HttpServlet),强行升级会导致编译失败且无意义。同理,MySQL 8.0 的caching_sha2_password认证插件与老版 JDBC 驱动(如 mysql-connector-java 5.1.47)不兼容,会卡在Access denied for user。所以本项目实测兼容组合明确锁定为:

组件推荐版本关键原因
JDK1.8u291 或 OpenJDK 8javac -source 1.8编译,避免 Lambda 表达式语法报错
Tomcat9.0.83(非 10.x)保持javax.servlet.*包路径不变,JSP EL 解析稳定
MySQL5.7.42 或 8.0.33(需配套驱动)5.7 兼容性最好;8.0 必须用mysql-connector-java-8.0.33.jar
IDEIDEA 2022.3 ~ 2023.3(非 2024.x)2024 版默认禁用 JSP 编译器,需手动开启;2023.x 对 web.xml schema 识别最准

提示:项目lib/目录下已预置mysql-connector-java-5.1.47.jar(适配 MySQL 5.7)和mysql-connector-java-8.0.33.jar(适配 MySQL 8.0),部署前请按实际数据库版本只保留一个,删掉另一个,否则类加载冲突。

2.2 数据库初始化:不只是执行 SQL,而是理解表结构设计意图

项目提供cake_shop.sql,共 8 张表。不要双击就运行!先打开 SQL 文件,重点看这三处:

-- 1. 字符集必须显式声明(解决中文乱码核心) CREATE DATABASE IF NOT EXISTS cake_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 2. 用户表 user 的 password 字段长度为 64 —— 这是为 SHA-256 加密预留(非明文存储) CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(64) NOT NULL, -- 注意:64位,对应 SHA-256 十六进制字符串长度 `realname` varchar(20) DEFAULT NULL, `phone` varchar(15) DEFAULT NULL, `address` varchar(100) DEFAULT NULL, `role` tinyint(1) DEFAULT '0', -- 0:普通用户, 1:管理员 PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 订单主表 order_info 的 status 字段用 tinyint 而非 enum —— 便于后期扩展状态机 CREATE TABLE `order_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `total_price` decimal(10,2) NOT NULL, `status` tinyint(1) NOT NULL DEFAULT '0', -- 0:待支付, 1:已支付, 2:已发货, 3:已完成, 4:已取消 `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `fk_order_user` (`user_id`), CONSTRAINT `fk_order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

执行步骤(命令行方式,最可控):

# 1. 登录 MySQL(假设 root 密码为 123456) mysql -u root -p123456 # 2. 执行建库建表(注意路径替换成你本地实际路径) mysql> source /path/to/cake_shop.sql; # 3. 验证数据是否载入(关键!) mysql> SELECT COUNT(*) FROM goods; -- 应返回 12(默认12款蛋糕) mysql> SELECT username, role FROM user WHERE username = 'admin'; -- 应返回 admin, 1

注意:若用 Navicat 或 DBeaver 执行 SQL,请在“运行设置”中勾选“按分号分割语句”“遇到错误继续执行”,否则CREATE DATABASE后的USE cake_shop会因分号缺失而失败。

2.3 IDEA 部署全流程:从 Import Project 到首页访问(含常见 404 排查点)

Step 1:正确导入项目(不是“Open”)

  • 启动 IDEA →FileNewProject from Existing Sources...
  • 选择解压后的根目录(含src/WebContent/lib/README.md的文件夹)
  • 在向导中取消勾选 “Create project from external model”(即不选 Maven/Gradle),因为这是传统 Dynamic Web Project,无 pom.xml

Step 2:配置 Artifacts(最容易被忽略的致命环节)

  • FileProject StructureArtifacts+Web Application: ArchiveFor 'cake-shop'
  • Output Layout标签页,确认:
    • WEB-INF/lib/下已包含mysql-connector-java-*.jar(右键lib/文件夹 →Put into Output Root
    • WEB-INF/web.xml已在WEB-INF/目录下(若缺失,从WebContent/WEB-INF/拖入)
    • index.jsp必须出现在Available Elements顶层(右键 →Put into Output Root

Step 3:配置 Tomcat Server

  • RunEdit Configurations...+Tomcat ServerLocal
  • Application server: 选择你解压的 Tomcat 9 目录
  • Deployment标签页 →+Artifact→ 选择刚创建的cake-shop:war exploded
  • 关键设置Application context必须填/cake-shop(不能为空或/,否则访问http://localhost:8080/会 404)

Step 4:启动并验证

  • 点击绿色三角形启动
  • 控制台输出INFO: Server startup in [xxx] milliseconds后,浏览器访问:
    http://localhost:8080/cake-shop/→ 应看到首页轮播图
    http://localhost:8080/cake-shop/admin/login.jsp→ 应看到管理员登录页
    若首页空白或 404,请立即检查:
    • web.xml<welcome-file-list>是否为<welcome-file>index.jsp</welcome-file>
    • index.jsp是否在WebContent/根目录(而非WebContent/pages/下)
    • IDEA 右下角是否弹出Build completed successfully(未成功则BuildRebuild Project

3. 前台功能深度拆解:从商品展示到购物车结算,每一步都带着业务逻辑注释

3.1 商品展示模块:三层推荐逻辑如何通过 DAO 层解耦实现?

项目前台首页(index.jsp)包含三大推荐区:条幅推荐(Banner)、热销推荐、新品推荐。它们并非静态 HTML,而是由 Servlet 动态查询数据库生成。核心在于GoodsDAO.java中三个独立方法:

// src/com/dao/GoodsDAO.java public List<Goods> findHotGoods() { String sql = "SELECT * FROM goods WHERE is_hot = 1 ORDER BY sales DESC LIMIT 6"; return queryRunner.query(sql, new BeanListHandler<>(Goods.class)); } public List<Goods> findNewGoods() { String sql = "SELECT * FROM goods WHERE is_new = 1 ORDER BY create_time DESC LIMIT 6"; return queryRunner.query(sql, new BeanListHandler<>(Goods.class)); } public List<Goods> findBannerGoods() { String sql = "SELECT * FROM goods WHERE is_banner = 1 ORDER BY sort_order ASC"; return queryRunner.query(sql, new BeanListHandler<>(Goods.class)); }

参数说明与业务含义:

  • is_hot/is_new/is_banner:均为TINYINT(1)类型,值为1表示启用该推荐标识。管理员可在后台goods_manage.jsp中勾选修改。
  • sales字段记录销量,ORDER BY sales DESC确保“热销”按真实销量排序,而非点击量或收藏量(避免刷单干扰)。
  • sort_order是 Banner 专用字段,允许管理员拖拽排序(如第1位放生日蛋糕,第2位放巧克力蛋糕),ASC保证序号小的排前面。

JSP 层调用逻辑(index.jsp片段):

<!-- 条幅推荐区 --> <c:forEach items="${bannerList}" var="g" varStatus="status"> <div class="item ${status.index == 0 ? 'active' : ''}"> <img src="${pageContext.request.contextPath}/img/${g.image}" alt="${g.name}"> <div class="carousel-caption"> <h3>${g.name}</h3> <p><fmt:formatNumber value="${g.price}" type="currency"/></p> </div> </div> </c:forEach>

注意:${pageContext.request.contextPath}是关键!它动态生成应用上下文路径(如/cake-shop),避免写死src="/img/xxx.jpg"导致图片 404。这是新手最常犯的路径错误。

3.2 购物车模块:Session 存储 vs 数据库存储的取舍与实现细节

本项目购物车采用纯 Session 存储(非数据库持久化),这是课程设计场景下的合理选择:

  • ✅ 优点:无需额外建cart_item表,代码简洁,符合“轻量级演示”定位;
  • ❌ 缺点:用户关闭浏览器后购物车清空,不支持多端同步。

核心类Cart.java(src/com/entity/Cart.java)设计:

public class Cart { private Map<Integer, CartItem> cartItems; // key: goods_id, value: CartItem private BigDecimal totalAmount; // 实时计算总价,避免每次遍历 public void addGoods(Goods goods, int count) { CartItem item = cartItems.get(goods.getId()); if (item == null) { item = new CartItem(goods, count); cartItems.put(goods.getId(), item); } else { item.setCount(item.getCount() + count); // 累加数量,非覆盖 } recalculateTotal(); // 每次变更后重算总价 } private void recalculateTotal() { totalAmount = BigDecimal.ZERO; for (CartItem item : cartItems.values()) { totalAmount = totalAmount.add( item.getGoods().getPrice().multiply( BigDecimal.valueOf(item.getCount()) ) ); } } }

关键细节:

  • Map<Integer, CartItem>使用goods_id作 key,确保同一商品多次添加自动合并(如用户先加1个,再加2个,最终显示数量为3)。
  • recalculateTotal()addGoods()updateCount()deleteItem()后强制调用,保证totalAmount始终准确,避免浮点数累加误差(用BigDecimal而非double)。
  • CartItem类封装了商品对象和数量,不存 price 字段——价格始终从Goods实体获取,确保与数据库实时一致(若库存售罄,价格变动可即时反映)。

JSP 层购物车展示(cart.jsp):

<c:forEach items="${cart.cartItems}" var="entry"> <tr> <td><img src="${pageContext.request.contextPath}/img/${entry.value.goods.image}" width="60"></td> <td>${entry.value.goods.name}</td> <td><fmt:formatNumber value="${entry.value.goods.price}" type="currency"/></td> <td> <form action="${pageContext.request.contextPath}/CartServlet" method="post"> <input type="hidden" name="method" value="updateCount"> <input type="hidden" name="goodsId" value="${entry.value.goods.id}"> <input type="number" name="count" value="${entry.value.count}" min="1" style="width:60px;"> <button type="submit">更新</button> </form> </td> <td><fmt:formatNumber value="${entry.value.goods.price.multiply(entry.value.count)}" type="currency"/></td> <td><a href="${pageContext.request.contextPath}/CartServlet?method=deleteItem&goodsId=${entry.value.goods.id}">删除</a></td> </tr> </c:forEach>

提示:表单提交用POST(防 URL 长度限制和缓存),删除用GET(符合 RESTful 习惯,且删除操作幂等)。CartServlet通过request.getParameter("method")分发到不同处理方法,这是典型的 Command 模式简化版。

3.3 用户登录与权限控制:Filter 如何拦截未登录访问?

系统区分两类用户:普通用户(role=0)和管理员(role=1)。权限控制不在每个 Servlet 里写if(user==null),而是通过LoginFilter.java统一拦截:

// src/com/filter/LoginFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 放行静态资源和登录相关页面 String uri = request.getRequestURI(); if (uri.contains("/css/") || uri.contains("/js/") || uri.contains("/img/") || uri.endsWith("login.jsp") || uri.endsWith("register.jsp") || uri.contains("/UserServlet?method=login")) { chain.doFilter(req, resp); return; } // 检查 session 中是否存在 user 对象 User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?msg=not_login"); return; } // 管理员专属路径需额外校验 role if (uri.startsWith("/admin/")) { if (user.getRole() != 1) { response.sendRedirect(request.getContextPath() + "/index.jsp?msg=no_permission"); return; } } chain.doFilter(req, resp); }

web.xml中 Filter 配置(必须放在 Servlet 之前):

<filter> <filter-name>LoginFilter</filter-name> <filter-class>com.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 拦截所有请求 --> </filter-mapping>

避坑点:

  • /*拦截所有路径,但chain.doFilter()前的return保证了静态资源不被重复处理;
  • request.getContextPath()动态获取上下文,避免硬编码/cake-shop
  • response.sendRedirect()发送 302 重定向,比request.getRequestDispatcher().forward()更安全(forward 会暴露内部路径)。

4. 后台管理功能实战:从商品增删改查到订单状态流转,理解 CRUD 的工程化表达

4.1 商品管理:分页查询与模糊搜索的 SQL 实现

后台商品列表页(admin/goods_list.jsp)需支持:

  • 分页显示(每页 8 条)
  • 按名称/描述模糊搜索
  • 按分类筛选(type_id

GoodsDAO.java中对应方法:

// 分页 + 搜索 + 分类筛选(三合一) public List<Goods> findGoodsByPage(int start, int pageSize, String keyword, Integer typeId) { StringBuilder sql = new StringBuilder("SELECT g.*, t.type_name FROM goods g "); sql.append("LEFT JOIN goods_type t ON g.type_id = t.id WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append("AND (g.name LIKE ? OR g.description LIKE ?) "); params.add("%" + keyword.trim() + "%"); params.add("%" + keyword.trim() + "%"); } if (typeId != null && typeId > 0) { sql.append("AND g.type_id = ? "); params.add(typeId); } sql.append("ORDER BY g.id DESC LIMIT ?, ?"); params.add(start); params.add(pageSize); return queryRunner.query(sql.toString(), new BeanListHandler<>(Goods.class), params.toArray()); } // 获取总记录数(用于计算页码) public int getTotalCount(String keyword, Integer typeId) { StringBuilder sql = new StringBuilder("SELECT COUNT(*) FROM goods g WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append("AND (g.name LIKE ? OR g.description LIKE ?) "); params.add("%" + keyword.trim() + "%"); params.add("%" + keyword.trim() + "%"); } if (typeId != null && typeId > 0) { sql.append("AND g.type_id = ? "); params.add(typeId); } return queryRunner.query(sql.toString(), new ScalarHandler<Long>(), params.toArray()).intValue(); }

SQL 设计要点:

  • 使用StringBuilder拼接动态条件,避免WHERE 1=1被误认为低效(实际 MySQL 优化器会忽略);
  • LIKE ?参数化防止 SQL 注入,%keyword%支持前后模糊匹配;
  • LEFT JOIN goods_type将分类名称type_name一并查出,避免在 JSP 中循环查表(N+1 问题);
  • ORDER BY g.id DESC保证新商品靠前,符合运营需求。

JSP 分页控件(goods_list.jsp):

<!-- 页码导航 --> <div class="pagination"> <c:if test="${currentPage > 1}"> <a href="${pageContext.request.contextPath}/AdminGoodsServlet?method=list&currentPage=${currentPage-1}&keyword=${param.keyword}&typeId=${param.typeId}">&laquo;</a> </c:if> <c:forEach begin="1" end="${totalPage}" var="i"> <a href="${pageContext.request.contextPath}/AdminGoodsServlet?method=list&currentPage=${i}&keyword=${param.keyword}&typeId=${param.typeId}" class="${i == currentPage ? 'active' : ''}">${i}</a> </c:forEach> <c:if test="${currentPage < totalPage}"> <a href="${pageContext.request.contextPath}/AdminGoodsServlet?method=list&currentPage=${currentPage+1}&keyword=${param.keyword}&typeId=${param.typeId}">&raquo;</a> </c:if> </div>

注意:所有分页链接参数(currentPage,keyword,typeId)必须原样回传,否则筛选条件丢失。这是新手分页失效的头号原因。

4.2 订单管理:状态机驱动的订单生命周期

订单状态流转是电商系统核心。本项目定义 5 种状态:
0:待支付1:已支付2:已发货3:已完成4:已取消

OrderInfoDAO.java中状态更新方法:

// 仅允许合法状态转移(防越权操作) public boolean updateStatus(int orderId, int newStatus) { // 定义合法转移规则:key=当前状态, value=允许的新状态集合 Map<Integer, Set<Integer>> validTransitions = new HashMap<>(); validTransitions.put(0, Set.of(1, 4)); // 待支付 → 可支付或取消 validTransitions.put(1, Set.of(2, 4)); // 已支付 → 可发货或取消 validTransitions.put(2, Set.of(3)); // 已发货 → 只能完成 validTransitions.put(3, Set.of()); // 已完成 → 不可再变 validTransitions.put(4, Set.of()); // 已取消 → 不可再变 // 查询当前状态 String sqlSelect = "SELECT status FROM order_info WHERE id = ?"; Integer currentStatus = queryRunner.query(sqlSelect, new ScalarHandler<>(), orderId); if (currentStatus == null) return false; Set<Integer> allowed = validTransitions.get(currentStatus); if (allowed == null || !allowed.contains(newStatus)) { return false; // 非法转移,拒绝执行 } // 执行更新 String sqlUpdate = "UPDATE order_info SET status = ?, update_time = NOW() WHERE id = ?"; return queryRunner.update(sqlUpdate, newStatus, orderId) > 0; }

后台订单操作(admin/order_list.jsp):

<c:choose> <c:when test="${order.status == 0}"> <a href="${pageContext.request.contextPath}/AdminOrderServlet?method=pay&id=${order.id}">标记为已支付</a> <a href="${pageContext.request.contextPath}/AdminOrderServlet?method=cancel&id=${order.id}">取消订单</a> </c:when> <c:when test="${order.status == 1}"> <a href="${pageContext.request.contextPath}/AdminOrderServlet?method=ship&id=${order.id}">发货</a> <a href="${pageContext.request.contextPath}/AdminOrderServlet?method=cancel&id=${order.id}">取消订单</a> </c:when> <c:when test="${order.status == 2}"> <a href="${pageContext.request.contextPath}/AdminOrderServlet?method=complete&id=${order.id}">标记完成</a> </c:when> </c:choose>

玄学经验:

  • 状态转移必须在 DAO 层校验,不能只靠前端按钮显隐控制(F12 可轻易绕过);
  • update_time = NOW()记录每次状态变更时间,为后续数据分析留痕;
  • validTransitionsMap而非switch,便于未来扩展(如增加“退款中”状态)。

4.3 后台安全加固:管理员登录的密码加密与会话超时

管理员登录(admin/login.jsp)提交到AdminUserServlet,其login方法关键逻辑:

// src/com/servlet/AdminUserServlet.java protected void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 密码 SHA-256 加密(非明文比对) String encryptedPassword = DigestUtils.sha256Hex(password); // 2. 查询用户(role=1 限定管理员) User user = userDao.findByUsernameAndPassword(username, encryptedPassword); if (user != null && user.getRole() == 1) { // 3. 设置 session 并跳转 HttpSession session = request.getSession(); session.setAttribute("admin", user); session.setMaxInactiveInterval(1800); // 30分钟无操作自动失效 response.sendRedirect(request.getContextPath() + "/admin/index.jsp"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/admin/login.jsp").forward(request, response); } }

密码加密原理:

  • DigestUtils.sha256Hex()来自 Apache Commons Codec,生成 64 位十六进制字符串;
  • 数据库user.password字段长度设为 64,完美匹配;
  • 无盐值(salt)是课程设计简化考虑,生产环境必须加盐。

会话超时配置(双重保障):

  • session.setMaxInactiveInterval(1800)在代码中设置;
  • web.xml中补充全局配置(更可靠):
<session-config> <session-timeout>30</session-timeout> <!-- 单位:分钟 --> </session-config>

提示:session.setAttribute("admin", user)与前台session.setAttribute("user", user)分开存储,避免角色混淆。Filter 中user.getRole() == 1判断即依赖此分离。


5. 避坑指南:96分项目背后的5个血泪经验,避开这些坑少走3天弯路

5.1 现象:首页轮播图不显示,控制台报 404

原因:index.jsp中图片路径写成<img src="/img/banner1.jpg">,缺少应用上下文路径。
解决:统一改为<img src="${pageContext.request.contextPath}/img/banner1.jpg">。若用request.getContextPath(),需确保web.xml<display-name>与部署时 Context Path 一致(IDEA 中即Application context值)。

5.2 现象:管理员登录成功后跳转到http://localhost:8080/,显示 Tomcat 默认页

原因:AdminUserServletresponse.sendRedirect("/admin/index.jsp")写成绝对路径/admin/index.jsp,未加上下文。
解决:改为response.sendRedirect(request.getContextPath() + "/admin/index.jsp")。所有重定向、表单 action、超链接 href 都必须带getContextPath()

5.3 现象:商品详情页goods_detail.jspNullPointerException,错误指向${goods.name}

原因:GoodsServletdetail方法中,request.setAttribute("goods", goods)后未forward,而是redirect,导致 request 域数据丢失。
解决:detail方法必须用request.getRequestDispatcher("/goods_detail.jsp").forward(request, response),不可用sendRedirectredirect会新建 request,forward才能传递 request 属性。

5.4 现象:MySQL 8.0 下登录报java.sql.SQLException: Unknown system variable 'query_cache_size'

原因:项目lib/中混用了mysql-connector-java-5.1.47.jar(适配 MySQL 5.7),而 MySQL 8.0 已移除查询缓存相关变量。
解决:删除lib/下所有mysql-connector-java-5.x.jar,只保留mysql-connector-java-8.0.33.jar。同时检查db.propertiesdriverClassName= com.mysql.cj.jdbc.Driver(8.0 必须用cj版本)。

5.5 现象:购物车更新数量后,页面刷新,但数量未变

原因:CartServletupdateCount方法中,request.getSession().setAttribute("cart", cart)后未response.sendRedirect()回购物车页,而是forward,导致浏览器地址栏仍是CartServlet,F5 刷新会重复提交。
解决:updateCount处理完后,必须response.sendRedirect(request.getContextPath() + "/cart.jsp"),强制 GET 请求刷新页面,避免重复提交(PRG 模式)。


6. 进阶技巧:如何用这套代码快速生成你的课程设计报告?三个真实可用的文档自动化方案

6.1 从源码自动生成 UML 类图:PlantUML + IDEA 插件一键导出

你不需要手动画类图。安装 IDEA 插件PlantUML Integration(市场搜索安装),然后:

  1. 在项目根目录新建docs/文件夹;
  2. 右键src/com/PlantUMLGenerate Class Diagram
  3. 选择Goods,User,OrderInfo,CartServlet,GoodsDAO等核心类;
  4. 生成.puml文件,内容类似:
@startuml class Goods { +int id +String name +BigDecimal price } class GoodsDAO { +List<Goods> findHotGoods() } Goods --> GoodsDAO : uses @enduml
  1. 右键.puml文件 →Export Diagrams→ 生成 PNG/PDF,直接插入 Word 报告。
    价值点:报告中“系统架构设计”章节的类图不再手绘,且与代码实时同步——你改Goods字段,重新生成图就更新。

6.2 数据库设计说明书:用 MySQL Workbench 逆向工程生成 ER 图

别再用 Visio 画表关系。用 MySQL Workbench:

  1. 连接你的cake_shop数据库;
  2. DatabaseReverse Engineer...→ 选择cake_shop
  3. 下一步直到完成,自动生成实体关系图(ERD);
  4. 右键图表 →Export as PNG
  5. 在报告“数据库设计”章节,粘贴此图,并补充说明:
    • goodsorder_item是一对多(一个商品可被多个订单购买);
    • userorder_info是一对多(一个用户可下多个订单);
    • goods_type是分类字典表,无业务操作,仅供下拉选择。
      提示:Workbench 生成的 ER 图会自动标注外键(FK)和索引,比手绘更专业。

6.3 测试用例文档:用 JUnit 4 快速编写 3 个核心功能测试

课程设计报告要求“系统测试”。不必写黑盒测试步骤,用 JUnit 写白盒测试更显功底:

  1. src/同级新建test/目录,添加GoodsDAOTest.java
// test/com/dao/GoodsDAOTest.java public class GoodsDAOTest { private GoodsDAO dao = new GoodsDAO(); @Test public void testFindHotGoods() { List<Goods> list = dao.findHotGoods(); assertNotNull(list); assertTrue(list.size() <= 6); // 热销商品不超过6个 // 验证第一条记录 is_hot=1 assertFalse(list.isEmpty()); assertEquals(1, list.get(0).getIsHot().intValue()); } }
  1. 在 IDEA 中右键该类 →Run 'GoodsDAOTest',控制台输出Tests passed
  2. 截图测试结果,插入报告“测试分析”章节,标题:“DAO 层单元测试覆盖率 30%(核心查询方法)”。
    为什么有效:老师一眼看出你懂测试,且测试覆盖了业务关键点(is_hot标识),不是摆设。

从那以后我每次交课程设计,都强制走一遍这三步:

  • 用 PlantUML 生成类图(5分钟);
  • 用 Workbench 导出 ER 图(3分钟);
  • 写 3 个 JUnit 测试并截图(10分钟)。
    报告质量立刻提升一个档次,答辩时老师问“你测试了吗”,我能直接打开 IDEA 演示运行过程。不是炫技

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询