
简介面向JSP课程设计与期末大作业场景基于JSP的MVC分层架构和MySQL数据库的图书购物网站源码包适合需要完成高质量项目的计算机专业学生使用。系统功能完善界面美观操作流畅代码注释详尽新手也能顺利理解并二次开发。压缩包共七十五个文件包含十四个JSP页面、十二个Java源文件、十二个编译后的class文件、十六张界面截图、SQL数据库脚本、项目配置文件、课程设计展示视频及说明文档整体约47.77MB目录结构清晰方便按模块查阅。已有三百四十八人学习下载。资源内附数据库脚本和部署说明下载后简单配置即可运行可直接作为课程设计或期末大作业提交也能帮助读者掌握MVC分层思想、图书检索、购物车、订单管理等典型功能实现。1. 为什么每个 JavaWeb 学习者都要做一次图书购物网站课程设计背后的真实门槛期末大作业选“图书购物网站”的人多半不是因为它新颖而是因为它刚好压在 JSP 课程设计的能力边界上登录注册、商品列表、搜索、购物车、下单、订单管理、后台维护这一整套流程几乎覆盖了 JavaWeb 入门阶段的所有必考点。你说它“简单”但真到答辩时老师最爱问的恰恰是这些功能背后的代码结构——Session 怎么管、购物车数据放哪、下单时库存怎么扣。如果只是把网上源码跑起来连“为什么订单表需要两个外键”都答不上来分数不会好看。这个项目真正的价值不在于“图书商城”这个题材而在于它逼着你把 MVC 模式、Servlet 生命周期、Filter 拦截、JDBC 事务这几样东西串成一条线。源码和数据库拿到手后能不能变成你自己的作品取决于你是否能把每一层代码拆开重装一遍。适合谁适合那些已经学完 JSP/Servlet 基础、但还没独立做过完整系统的同学。这篇文章就按“架构 → 建表 → 代码 → 避坑 → 答辩优化”的路径把这条线从头捋一遍。2. 基于 JSP Servlet MySQL 的 MVC 架构先分清谁在干活再动手写代码2.1 MVC 在 JSP 课程设计里的正确拆法Model、View、Controller 各放什么很多同学的“MVC 项目”最后写成了“JSP 大杂烩”数据库连接写在页面里业务判断写在%%里点击按钮直接调一个 JSP 文件。这种代码跑起来没问题但答辩时老师一看包结构就明白有没有用过心。标准的 MVC 拆法应该是Model 层JavaBean 实体类Book、User、CartItem、Order DAO 类BookDao、UserDao Service 层逻辑。View 层JSP 页面只负责展示数据和接收用户动作不直接写 SQL。Controller 层Servlet负责接收请求、调用 Service、转发或重定向到 JSP。以用户登录为例请求路径应该是login.jsp提交表单 →LoginServlet接收参数 →UserService调UserDao查库 → 成功则把 User 对象放进 Session重定向到首页失败则request.setAttribute(error, ...)转发回login.jsp。这里有个新手容易绕晕的点response.sendRedirect()和request.getRequestDispatcher().forward()到底用哪个。区分标准是——这次请求结束后浏览器地址栏需不需要变。登录成功后想让用户刷新不重复提交就用重定向登录失败要在同一个页面上显示错误提示就用转发。转发能把request里的属性带到 JSP重定向不行因为那是两次独立的请求。2.2 开发环境与项目骨架IDEA 里跑通 JavaWeb 项目的最小配置常见做法是用 IntelliJ IDEA Tomcat 8/9 JDK 1.8 组合。用 IDEA 运行 JavaWeb 项目配置的核心只有三步先把 Web 项目结构建对再把 Tomcat 加进来最后把依赖库关联好。如果项目用 Maven 管理pom.xml里至少要有这些依赖dependencies !-- JSP 和 Servlet 由 Tomcat 提供scope 设为 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency !-- JSTL 用于页面里做判断和循环 -- dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency /dependencies注意 Servlet API 和 JSP API 的 scope 必须设为provided否则打包时会和 Tomcat 自带的类冲突启动时出现java.lang.LinkageError。如果不用 Maven就在 Project Structure 里把 Tomcat 的servlet-api.jar和jsp-api.jar加到provided范围道理一样。项目骨架里的标准路径是src/main/java放 Servlet、Service、DAO、实体类src/main/webapp放 JSP、CSS、JS、图片和WEB-INF/web.xml。WEB-INF下的 JSP 不能被浏览器直接访问所以登录成功后的后台页面建议放这里强制走 Servlet 转发。2.3 数据库连接与 DAO 层别把 SQL 写在 JSP 里阅读源码时你会发现凡是被老师夸奖的课程设计DAO 层基本长一个样一个BaseDao负责拿连接各实体对应的 DAO 只写 SQL 和参数绑定。JDBC 最原始的写法是每次 new 一个DriverManager.getConnection但到了真实项目里这样写是灾难——每请求一次就建立一次数据库连接Tomcat 下并发稍微一高就报Too many connections。课程设计阶段用c3p0或Druid连接池比较稳妥。Druid 是阿里开源的监控页面在答辩时可以顺手展示一下子把项目档次拉起来。druid.properties配置文件长这样driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive50对应的BaseDao负责初始化连接池public class BaseDao { private static DruidDataSource dataSource; static { try { Properties props new Properties(); // 注意文件放在 src/main/resources 下 props.load(BaseDao.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource (DruidDataSource) DruidDataSourceFactory .createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(ResultSet rs, Statement stmt, Connection conn) { // 关闭顺序rs - stmt - conn用 try-with-resources 更优雅 if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }连接池的作用是“复用连接而不是新建连接”conn.close()在 Druid 里其实是把连接还给池子不是物理关闭。这段逻辑说明是答辩加分点建议记牢。3. 图书购物系统的数据库设计从 ER 图到建表 SQL 的完整落地3.1 核心表结构用户、图书、分类、购物车、订单、订单项打开项目的bookstore.sql你会发现表不会太多但每张表之间都有外键关联。最少需要六张表用户表user、图书表book、分类表category、购物车表cart、订单表orders、订单项表order_item。这里给出一套可以直接用的建表 SQL字符集统一utf8mb4排序规则utf8mb4_general_ci避免后面中文乱码CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8mb4; USE bookstore; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 建议存 MD5 或 SHA-256, phone VARCHAR(20), address VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255) ) ENGINEInnoDB; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 100, cover VARCHAR(255) COMMENT 封面图片路径, description TEXT, FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB; CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id), FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 下单时生成避免用自增ID当订单号, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, book_title VARCHAR(100) COMMENT 快照图书改名不影响历史订单, price DECIMAL(10,2) NOT NULL COMMENT 快照下单时的单价, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB;表结构里的几个细节答辩时很容易被追问。cart表用了UNIQUE KEY (user_id, book_id)意思是同一个用户对同一本书只能有一行记录数量变化用UPDATE而不是每次INSERT这样避免购物车越点越多。order_item表里故意冗余了book_title和price学名叫“数据快照”——用户下单之后即使管理员把书改名、调价订单历史记录仍然是当时的数据。3.2 外键、索引与事务MySQL 里这几个约束能省多少血泪有同学为了省事建表时把所有外键都去掉只靠代码层保证数据一致。课程设计阶段还是建议保留外键因为 MySQL 事务处理和外键约束能挡住大部分低级错误。举个最常见的翻车点用户删除自己账号时他名下有订单和购物车记录如果不做处理MySQL 会因为外键约束直接报错逼你想清楚“是禁止删除还是级联删除”。购物车表建议用ON DELETE CASCADE用户注销时购物车记录自动清空。订单表则不建议级联删因为订单涉及交易历史数据必须保留更合理的做法是给user表加一个status字段做软删除。建表时把外键更新规则写上FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADEMySQL 的事务默认是自动提交的每一条INSERT/UPDATE都在一个隐藏事务里。下单扣库存这种“先检查库存、再插入订单、再扣减库存”的多步操作必须手动开启事务否则极容易出现超卖——后面第 4 章会给出完整代码。3.3 初始化数据让项目第一次启动就有书可买拿到源码后数据库导入只是第一步你还需要保证里面有一批像样的测试数据。很多下载下来的 SQL 里只有表结构图书表空荡荡的登录进去首页什么都看不到自然觉得项目是坏的。建议自己补一批初始化数据分类和书对应好价格要有两位小数库存不要全默认成 100编一点有真实感的数字。INSERT INTO category (name, description) VALUES (Java, Java 编程入门与进阶), (Web前端, HTML/CSS/JavaScript 相关), (数据库, MySQL 等数据库技术), (计算机基础, 数据结构、操作系统、网络); INSERT INTO book (category_id, title, author, price, stock, cover, description) VALUES (1, Java核心技术 卷I, Cay S. Horstmann, 98.50, 46, images/java1.jpg, Java 入门经典), (1, Effective Java, Joshua Bloch, 108.00, 30, images/effective-java.jpg, Java 进阶必读), (2, JavaScript 高级程序设计, Nicholas C. Zakas, 129.80, 25, images/js.jpg, 红宝书), (3, MySQL 必知必会, Ben Forta, 49.90, 60, images/mysql.jpg, SQL 快速上手);初始化数据写得越贴近真实后面联调时越顺手。比如password如果代码里做了 MD5那么 INSERT 语句里就要存 MD5 后的值不能存明文否则登录功能怎么调都失败。这个问题排错特别隐蔽常见做法是注册页用DigestUtils.md5DigestAsHex加密初始化数据里就直接放加密后的字符串。4. 关键功能实现登录、商品列表、购物车与下单的代码拆解4.1 登录与会话管理Session 和过滤器 Filter 的配合登录是所有页面的前提也是整个项目里最容易写崩的一块。正确的流程是LoginServlet接收username和password调用 Service 层校验成功就session.setAttribute(user, user)失败就返回错误消息。登录成功后后续的购物车、下单接口都要从 Session 里拿用户 ID而不是每次让用户重新传——这是 MVC 里“会话状态”的核心。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password DigestUtils.md5DigestAsHex( req.getParameter(password).getBytes()); User user userService.findByUsernameAndPassword(username, password); if (user ! null) { req.getSession().setAttribute(user, user); resp.sendRedirect(req.getContextPath() /home); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }这里有一个细节密码一般不会在 Servlet 层自己写加密逻辑常见做法是放到 Service 层因为以后如果加“找回密码”功能Service 还要复用这套加密。另外req.getContextPath()一定要带上否则项目部署路径一旦不是根路径重定向就会跳错。只有登录接口加了判断还不够。购物车、订单、后台管理这些 Servlet如果用户没登录直接访问应该被拦截。课程设计里最简单的方案是写一个全局过滤器LoginFilter在web.xml或注解里配置要拦截的路径WebFilter(/admin/*) public class AdminAuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); Object admin session null ? null : session.getAttribute(admin); if (admin null) { request.setAttribute(error, 请先登录); request.getRequestDispatcher(/login.jsp).forward(request, resp); } else { chain.doFilter(req, resp); } } }过滤器是答辩高频考点要能说清楚它的执行时机请求到达 Servlet 之前过滤器先执行响应返回给浏览器之前过滤器又能再处理一次。项目里所有“必须在某个条件下才能访问”的需求都可以用它统一管起来。4.2 购物车操作从添加到更新数量的状态管理购物车设计有两条路。一条是把购物车数据全放 Session好处是不用碰数据库刷新页面不会丢但用户换个设备购物车就没了另一条像这个项目一样建cart表把数据持久化到 MySQL。课程设计建议用表方案因为涉及多表联查答辩时内容更丰富代码量也更够看。购物车模块至少三个接口addCart、updateCart、removeCart。添加商品的 Servlet 逻辑如下WebServlet(/cart/add) public class AddCartServlet extends HttpServlet { private CartDao cartDao new CartDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user (User) req.getSession().getAttribute(user); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } int bookId Integer.parseInt(req.getParameter(bookId)); int quantity Integer.parseInt(req.getParameter(quantity)); // 先查购物车是否已有这条记录 CartItem item cartDao.findByUserIdAndBookId(user.getId(), bookId); if (item null) { cartDao.insert(user.getId(), bookId, quantity); } else { // 已有则累加数量但要检查库存上限 int newQuantity item.getQuantity() quantity; cartDao.updateQuantity(user.getId(), bookId, newQuantity); } // 重定向回购物车页避免刷新时重复添加 resp.sendRedirect(req.getContextPath() /cart/list); } }注意“重定向回购物车页”这个动作它解决的是前后端交互里最经典的重复提交问题如果用转发用户在购物车页面按 F5浏览器会重新提交上一次的 POST 请求购物车数量翻倍改成重定向后刷新只是重复 GET 一次列表页副作用归零。购物车列表页的 JSP 里要展示总价常见的写法是用 EL 表达式加 JSTL 的c:forEach遍历在每行末尾累加小计金额。总价不仅在页面显示下单时后端还要重新计算一次千万不能直接相信前端传来的数字——把价格作为隐藏字段传回后端是很大的安全隐患答辩时主动说出来老师会觉得你有工程意识。4.3 下单与库存扣减事务处理让多步操作不再翻车整个项目里含金量最高的代码就是下单逻辑。它要完成三件事创建订单记录、创建订单项、扣减库存。这三步要么全部成功要么全部失败。用 MySQL 的事务处理把这三步包起来代码一旦中途抛异常就回滚数据还能回到下单前的状态。public void checkout(Order order, ListCartItem cartItems) { Connection conn null; try { conn BaseDao.getConnection(); conn.setAutoCommit(false); // 手动控制事务绝不能让每步都自动提交 OrderDao orderDao new OrderDao(); int orderId orderDao.insertOrder(conn, order); // 返回订单主键 BookDao bookDao new BookDao(); for (CartItem item : cartItems) { // 先查当前库存防止超卖 int stock bookDao.getStock(conn, item.getBookId()); if (stock item.getQuantity()) { throw new RuntimeException([ item.getBookTitle() ] 库存不足当前仅剩 stock 本); } orderDao.insertOrderItem(conn, orderId, item); int affectedRows bookDao.decreaseStock(conn, item.getBookId(), item.getQuantity()); if (affectedRows 0) { throw new RuntimeException( 并发提示这本书刚被别人买走请重试); } } conn.commit(); // 全部执行成功才提交 } catch (Exception e) { try { if (conn ! null) conn.rollback(); // 任何一步失败全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(下单失败 e.getMessage(), e); } finally { BaseDao.close(null, null, conn); } }逻辑说明setAutoCommit(false)之后的每一个 SQL 操作都不会立即生效直到conn.commit()。getStock和decreaseStock之间可能有并发但课程设计阶段用“先查再扣”已经够用更严谨的写法是用SELECT ... FOR UPDATE加行锁答辩时可以提一句说明你知道这个机制。加入affectedRows 0判断是个小技巧如果两个请求同时扣库存事务隔离级别下后执行的更新会因条件不满足而影响零行从而触发回滚避免卖超。4.4 后台管理图书增删改查和订单状态修改后台管理模块通常只对管理员开放功能至少包含图书列表、新增图书、编辑图书、删除图书、修改订单状态。这部分代码模式统一一个AdminBookServlet负责分发请求通过action参数区分是add、edit、delete还是list。很多源码里喜欢把这个 Servlet 写成巨长的一段if-else阅读和答辩效果都不好。推荐做法是用分层分发。每个业务动作对应一个私有方法doGet和doPost都先取出 action然后 switch 分发WebServlet(/admin/book) public class AdminBookServlet extends HttpServlet { private BookService bookService new BookService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (edit.equals(action)) { showEditPage(req, resp); } else if (delete.equals(action)) { deleteBook(req, resp); } else { listBooks(req, resp); } } private void deleteBook(HttpServletRequest req, HttpServletResponse resp) throws IOException { int id Integer.parseInt(req.getParameter(id)); bookService.deleteBookById(id); resp.sendRedirect(req.getContextPath() /admin/book?actionlist); } }删除图书时要注意外键约束如果这本书已经被用户加进购物车或者下过单直接 DELETE 会抛异常。源码里的解决方案通常有两种一种是order_item表保存了图书快照删除后订单依然显示书名所以可以物理删除但如果cart表里还有引用就需要先删购物车关联再删图书。写 SQL 时可以用多表关联一条语句完成也可以在 Service 层分两步做显式处理比赌外键不报错更可靠。5. 避坑指南JSP 课程设计最常见的 5 个翻车现场与排查思路5.1 现象IDEA 里运行报 404 或 ClassNotFoundException运行 JavaWeb 项目配置里最容易出问题的就是 Artifact 部署。明明代码没报错启动 Tomcat 后访问/login.jsp却 404常见原因是没把项目部署到 Tomcat 的 webapps。IDEA 中要检查 Run Configuration 里的 Deployment 标签确认 Artifact 是war exploded而不是空的warApplication context 最好设成/这样访问路径就不会带项目名前缀。另一个高频报错是java.lang.ClassNotFoundException: com.mysql.jdbc.Driver这是因为 MySQL 驱动 jar 没有打进 Tomcat 的运行时。非 Maven 项目要在 File → Project Structure → Artifacts → Output Layout 里右击把mysql-connector-java.jar加进去这一步光在项目的 Libraries 里添是不够的必须在 Artifacts 里也出现否则 Tomcat 启动时找不到。检查顺序先看WEB-INF/lib下有没有驱动 jar再看 Artifact 输出是否包含WEB-INF/lib。5.2 现象Tomcat 启动后中文乱码中文乱码分两种页面乱码和 URL 参数乱码原因和处理方式完全不一样。页面乱码检查 JSP 第一行有没有写全pageEncoding% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %同时把 MySQL URL 里的characterEncodingutf8固定住连接池配置、页面编码、数据库编码三处全都统一成 UTF-8乱码才会消失。只改 JSP 不改 MySQL 连接参数数据库里存进去的中文依然会变成问号。URL 参数乱码是 Tomcat 8.5 及以上版本默认编码不是 UTF-8 导致的。GET请求的中文参数会按 ISO-8859-1 解码解决方式是在server.xml的 Connector 上加上URIEncodingUTF-8或者写一个字符编码过滤器统一处理。注意过滤器一定要放在其它过滤器前面否则经过它处理前参数已经被别的逻辑消费了。5.3 现象JSP 页面图片和 CSS 加载不出来浏览器控制台报.css或.jpg全是 404第一件事是把webapp根目录下的路径和 JSP 页面的引用路径做对比。JSP 页面里尽量写绝对路径避免因为转发到不同层级目录导致相对路径失效。使用 EL 的pageContext.request.contextPath是标准解link relstylesheet href${pageContext.request.contextPath}/css/style.css img src${pageContext.request.contextPath}/images/logo.png altlogo还有一个隐蔽坑如果项目用的是 Servlet 3.0 注解配置而不是web.xml某些文件夹默认不会被 Tomcat 映射。确认css/images/js目录建在src/main/webapp下面不要建到WEB-INF目录里——WEB-INF下的静态资源浏览器无法访问刻意放进去的唯一场景是不想被直接访问的下载文件。5.4 现象MySQL 连接失败Public Key Retrieval is not allowed用 MySQL 8.0 及以上版本跑课程设计时经常报Public Key Retrieval is not allowed。原因是 8.0 默认使用了caching_sha2_password认证连接时客户端需要向服务器要公钥但 JDBC 连接参数没允许这个过程。常见解法是在连接 URL 上增加两项allowPublicKeyRetrievaltrueuseSSLfalse完整的 MySQL 连接串长这样urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue另一个常见坑是 MySQL 5.7 和 8.0 的驱动类名不同。5.x 用com.mysql.jdbc.Driver8.x 推荐com.mysql.cj.jdbc.Driver。如果下载的源码是基于 5.7 写的而你本地装了 8.0驱动类名不改就报ClassNotFoundException反过来也一样容易翻车。过期驱动还有一个表现是启动时警告SSL connection error按上面的参数去掉 SSL 即可。5.5 现象下单时库存超卖或订单数据丢失课程设计里的超卖场景比较假但因为代码逻辑混乱导致的下单后购物车不清空、订单项缺少书的标题却是真实存在的问题。下单成功后必须把已经转成订单的购物车条目删掉否则用户买完书购物车里的数量还在。正确流程是下单事务提交之后调用cartDao.deleteByUserId(userId)清空购物车。订单数据丢失通常是因为没有事务控制。前面那段checkout代码里insertOrder和insertOrderItem如果不在同一个事务内订单主表插入成功了订单项表插入失败用户会看到“下单失败”但订单却出现在列表里。排查时可以打印 SQL 日志观察每一条 DML 的执行顺序一旦发现插入订单项后没有 commit问题基本就定位了。6. 从课程设计到拿得出手的作品验证、优化与答辩要点6.1 功能验证清单照着这 12 条过一遍基本不会挂课程设计项目光“能跑”远远不够你得先按用户的视角走通一遍流程再按管理员的身份走一遍后台。我一般会把验证点列在纸上每过一项就打一个勾。用户端必须验证注册新账号成功、重复用户名被拦截、登录成功跳转首页、登录失败给出提示、退出登录后访问购物车被拦下来、商品列表分页正常、添加购物车后数量加一、重复添加同一本书只改数量、修改购物车数量后总价变化、删除购物车条目成功、下单后库存减少、下单后购物车清空。后台端必须验证管理员登录权限、新增图书后前台能看到、编辑图书后价格变化、删除有引用关系的图书时不会程序崩溃、订单状态能前进不能倒退。这些验证项看起来琐碎但恰恰是答辩时老师最关心的地方。每验证一项就顺手在源码里找一下对应的实现位置这样被提问时你脱口而出“这个逻辑在AddCartServlet里它先去查cart表有没有这条记录”比支支吾吾解释半天要有说服力得多。6.2 让老师眼前一亮的小优化分页、模糊搜索、MD5 加密老师看过的课程设计成百上千几个千篇一律的功能提升不了档次。但要是你主动做了分页和模糊搜索效果完全不同。分页别用前端 JS 假分页而是后端 SQL 的LIMIT offset, pageSize这样一次性把所有书查出来只显示前几条太容易被拆穿。模糊搜素用 SQL 的LIKE即可注意处理特殊字符转义更讲究一点的做法是把搜索关键词和排序字段拼到 SQL 之前用正则或参数化查询挡住 SQL 注入。密码方面如果源码直接明文存储答辩前建议改成 MD5 加盐。以下是一段简短的加盐加密逻辑public static String md5WithSalt(String rawPassword, String salt) { String base rawPassword { salt }; return DigestUtils.md5DigestAsHex(base.getBytes(StandardCharsets.UTF_8)); }注册时生成一个随机盐存到user.salt字段登录时用用户输入密码加数据库盐值再算 MD5比较结果。这样即使数据库泄露原始密码也不容易被反推出来。这段代码不算复杂但体现了完整性意识在课程设计里是加分项。6.3 代码质量自查命名、注释与 Git 管理最后过一遍自己改过的代码。类名用名词方法名用动宾结构像findUserByUsername、handleOrderCheckout这种老师扫一眼包结构就知道你用心了。不要满屏System.out.println要留下少量关键注释比如事务的commit和rollback位置以及连接池初始化的目的。另外一个很简单的习惯是尽早把项目初始化成 Git 仓库。每完成一个功能就git commit一次答辩前万一改崩了还能回滚。git log的历史记录比口头说“我做了很多”更有说服力。我从大学第一次做图书商城开始就被“代码能跑”和“逻辑经得起问”之间的差距狠狠教育过。后来带课程设计总是让同学先把原项目的 SQL 导出来自己重新建一遍库再按自己的包结构重写一个登录取购物车的闭环这比直接改网上源码要费力但答起辩来底气完全不同。希望这个方向上的踩坑记录能帮少走几步弯路最终交出一份真正属于你自己的作品。本文还有配套的精品资源点击获取