简介:面向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> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <!-- JSTL 用于页面里做判断和循环 --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.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配置文件长这样:
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=50对应的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 ) ENGINE=InnoDB; CREATE TABLE `category` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL UNIQUE, `description` VARCHAR(255) ) ENGINE=InnoDB; 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`) ) ENGINE=InnoDB; 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`) ) ENGINE=InnoDB; 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`) ) ENGINE=InnoDB; 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`) ) ENGINE=InnoDB;表结构里的几个细节,答辩时很容易被追问。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, List<CartItem> 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?action=list"); } }删除图书时要注意外键约束:如果这本书已经被用户加进购物车或者下过单,直接 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而不是空的war,Application 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 contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>同时把 MySQL URL 里的characterEncoding=utf8固定住,连接池配置、页面编码、数据库编码三处全都统一成 UTF-8,乱码才会消失。只改 JSP 不改 MySQL 连接参数,数据库里存进去的中文依然会变成问号。
URL 参数乱码是 Tomcat 8.5 及以上版本默认编码不是 UTF-8 导致的。GET请求的中文参数会按 ISO-8859-1 解码,解决方式是在server.xml的 Connector 上加上URIEncoding="UTF-8",或者写一个字符编码过滤器统一处理。注意过滤器一定要放在其它过滤器前面,否则经过它处理前,参数已经被别的逻辑消费了。
5.3 现象:JSP 页面图片和 CSS 加载不出来
浏览器控制台报.css或.jpg全是 404,第一件事是把webapp根目录下的路径和 JSP 页面的引用路径做对比。JSP 页面里尽量写绝对路径,避免因为转发到不同层级目录导致相对路径失效。使用 EL 的pageContext.request.contextPath是标准解:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css"> <img src="${pageContext.request.contextPath}/images/logo.png" alt="logo">还有一个隐蔽坑:如果项目用的是 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 上增加两项:
allowPublicKeyRetrieval=true&useSSL=false完整的 MySQL 连接串长这样:
url=jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true另一个常见坑是 MySQL 5.7 和 8.0 的驱动类名不同。5.x 用com.mysql.jdbc.Driver,8.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 导出来,自己重新建一遍库,再按自己的包结构重写一个登录取购物车的闭环;这比直接改网上源码要费力,但答起辩来底气完全不同。希望这个方向上的踩坑记录能帮少走几步弯路,最终交出一份真正属于你自己的作品。
本文还有配套的精品资源,点击获取