ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

校园二手图书交易系统:Javaweb课程设计完整实现与避坑指南

校园二手图书交易系统:Javaweb课程设计完整实现与避坑指南 简介这份资源是面向高校计算机相关专业学生的Java Web课程设计完整项目包以校园二手图书交易系统为主题适合正在准备课设、大作业或需要Java Web实战练手的学习者。项目源码经过本地编译与严格调试可直接运行评审得分在95分以上难度适中内容经助教老师审定能满足学习与参考需求。压缩包共510个文件约28.32MB涵盖118个Java源文件、136个XML配置、9个SQL脚本、18个Vue组件及91张JPG图片等分别对应后端业务逻辑、框架与依赖配置、数据库建表数据、前端页面组件和界面素材结构完整。目前已有119人学习关注。通过该资源可掌握Spring Boot与Vue前后端分离的典型开发流程理解用户、图书、订单等核心模块的Controller、Service与Model分层设计并借助SQL脚本快速还原数据库环境是课程设计答辩与Java Web入门进阶的实用参考。1. 校园二手图书交易系统从课程设计到能跑起来的 Javaweb 项目每年学期末图书馆门口、宿舍楼下都会冒出一堆摆摊卖旧书的人而另一边低年级学生正为几十块一本的专业教材发愁。校园二手图书交易系统要解决的就是这个信息不对称问题——让卖方能发布书籍信息买方能按书名、分类、成色检索并下单。这个标题对应的是一套完整的 Javaweb 课程设计包含源码和数据库适合正在做 Java 课设、需要交大作业的学生也适合想拿一个完整 CRUD 项目练手的 Java 初学者。它涉及的技术栈是典型的 Javaweb 组合Servlet/JSP 或 Spring Boot、MySQL 数据库、前端页面核心功能围绕用户管理、图书发布、订单交易展开。下面我会按「技术选型 → 数据库设计 → 核心功能实现 → 避坑 → 进阶」的顺序把这类项目从零跑通的路径讲清楚。2. 技术选型与项目骨架Servlet/JSP 还是 Spring Boot2.1 两种主流技术路线的取舍校园二手图书交易系统在 Javaweb 课程设计里通常有两种实现路线。第一种是传统 Servlet JSP JDBC第二种是 Spring Boot MyBatis Thymeleaf。选哪条路取决于你的课设要求和时间预算。传统 Servlet JSP 路线的优势在于「透明」。你写的每一个请求都对应一个 Servlet 类web.xml或注解里配置映射JSP 负责渲染。老师检查代码时能清楚看到 HTTP 请求怎么进来、业务逻辑怎么处理、数据怎么落库。缺点是代码量大一个图书列表功能可能要写 Servlet、DAO、JSP 三个文件而且 JDBC 连接管理容易出问题。如果你的课设明确要求「不能用框架」或者你想彻底搞懂 Javaweb 的底层流程选这条路。Spring Boot 路线的优势在于「快」。依赖注入、自动配置、内嵌 Tomcat一个RestController就能暴露接口MyBatis 或 JPA 处理数据库映射。代码量能压缩到传统路线的三分之一左右。缺点是如果老师追问「Spring 的自动配置原理是什么」你得能答上来否则容易被质疑是抄的。另外Spring Boot 项目在 IDEA 里的配置项比较多新手容易在application.yml和依赖版本上翻车。我一般建议如果课设评分看重「功能完整度」和「代码规范」选 Spring Boot如果看重「对 Javaweb 原理的理解」选 Servlet JSP。两条路都能拿到 95 分以上关键是把功能做完整、把异常处理写好。2.2 用 Maven 搭出可运行的项目骨架不管选哪条路项目结构都要清晰。下面是一个传统 Servlet JSP 项目的 Mavenpom.xml核心依赖配置dependencies !-- Servlet APIscope 必须是 provided因为 Tomcat 自带 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP 支持 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驱动版本要和你的 MySQL 服务端匹配 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- JSTL 标签库JSP 页面里用 c:forEach 需要它 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- 数据库连接池别直接用 DriverManager -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies这段配置里有两个参数容易设错。第一javax.servlet-api的scope必须是provided因为 Tomcat 容器本身提供了 Servlet 实现如果你打成compile部署时会报ClassCastException或类冲突。第二MySQL 驱动版本要和数据库服务端匹配MySQL 5.7 用5.1.x驱动MySQL 8.0 用8.0.x驱动版本不匹配会报Unknown system variable query_cache_size之类的错。目录结构按 Maven 标准来src/main/java放 Java 代码src/main/webapp放 JSP 和静态资源src/main/resources放db.properties等配置文件。包名建议按com.campus.book开头下面分controller、service、dao、entity、util五个包。这个分层不是形式主义——controller只负责接收参数和返回视图service写业务逻辑dao只做数据库操作entity对应数据库表。分层清晰的项目老师一眼就能看出你懂 MVC。2.3 IDEA 里配置 Tomcat 并跑通第一个页面项目骨架搭好后在 IDEA 里配置 Tomcat 运行环境。步骤是Run → Edit Configurations → 点左上角→ 选 Tomcat Server → Local → 在 Server 标签页指定 Tomcat 安装目录 → 在 Deployment 标签页点→ 选 Artifact → 选xxx:war exploded。注意选war exploded而不是war前者支持热部署改 JSP 不用重启。配置完成后写一个最简单的IndexServlet测试环境WebServlet(/index) public class IndexServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 从数据库查最新上架的 8 本书传给 JSP 渲染 BookService bookService new BookService(); ListBook latestBooks bookService.getLatestBooks(8); req.setAttribute(latestBooks, latestBooks); // 转发到首页 JSP注意路径以 / 开头相对于 webapp 根目录 req.getRequestDispatcher(/WEB-INF/views/index.jsp).forward(req, resp); } }这段代码里WebServlet(/index)是注解式映射不需要再配web.xml。getLatestBooks(8)里的 8 是首页展示数量你可以改成 10 或 12但别设太大否则首页加载慢。forward是服务器内部转发浏览器地址栏不变如果你用sendRedirect地址栏会变成 JSP 路径而且req.setAttribute的数据会丢失。JSP 放在WEB-INF/views下是为了防止用户直接通过 URL 访问 JSP 页面必须经过 Servlet 转发才能看到这是 Javaweb 项目的基本安全习惯。3. 数据库设计五张核心表与增删改查落地3.1 用户表、图书表、订单表的关系建模校园二手图书交易系统的数据库不需要太复杂五张表就能覆盖核心业务user用户、book图书、category分类、order订单、order_item订单明细。表之间的关系是一个用户可以发布多本图书一个用户可以下多个订单一个订单包含多个订单明细每本图书属于一个分类。先看user表的设计CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码存MD5值, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, school VARCHAR(100) DEFAULT NULL COMMENT 学校, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;password字段长度设 64 是因为 MD5 值是 32 位十六进制字符串留一倍余量方便以后换加密算法。username加了UNIQUE约束注册时如果插入重复用户名数据库会直接报错你在 Service 层捕获SQLIntegrityConstraintViolationException就能提示「用户名已存在」不用先查再插避免并发问题。create_time用DEFAULT CURRENT_TIMESTAMP插入时不用手动传时间。book表是核心字段设计直接影响检索功能CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) DEFAULT NULL COMMENT 作者, isbn VARCHAR(20) DEFAULT NULL COMMENT ISBN号, category_id INT NOT NULL COMMENT 分类ID, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价, condition_level TINYINT DEFAULT 3 COMMENT 成色1-55为全新, description TEXT COMMENT 描述, cover_image VARCHAR(255) DEFAULT NULL COMMENT 封面图路径, seller_id INT NOT NULL COMMENT 卖家ID, status TINYINT DEFAULT 1 COMMENT 1在售 2已售 3下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id), FOREIGN KEY (seller_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表;price用DECIMAL(10,2)而不是FLOAT因为浮点数做金额计算会出现0.1 0.2 0.30000000000000004的问题DECIMAL是精确小数。condition_level用TINYINT存 1 到 5前端渲染成「几成新」。status字段控制图书状态下架的书不显示在列表里但数据保留方便卖家重新上架。外键约束建议加上虽然有些互联网公司禁止用外键但课设项目数据量小外键能保证数据一致性而且老师看到外键会认为你数据库学得扎实。3.2 用 JDBC 工具类封装数据库连接不管选 Servlet 还是 Spring Boot数据库连接管理都是绕不开的。直接在每个 DAO 里写DriverManager.getConnection会导致连接无法复用性能差且容易泄漏。用 Druid 连接池 工具类封装是标准做法。先写db.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/campus_book?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot passwordyour_password initialSize5 maxActive20url里的serverTimezoneAsia/Shanghai必须加否则 MySQL 8.0 驱动会报时区错误。useSSLfalse在本地开发时加上避免 SSL 握手警告。maxActive20是最大连接数课设项目 20 足够设太大反而浪费资源。工具类JdbcUtilspublic class JdbcUtils { private static DataSource dataSource; static { try { Properties props new Properties(); // 从类路径加载配置文件 props.load(JdbcUtils.class.getClassLoader() .getResourceAsStream(db.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序ResultSet → Statement → Connection反了会报错 try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }静态代码块在类加载时执行一次连接池只初始化一次。close方法的关闭顺序必须是ResultSet → Statement → Connection反过来关会抛异常。注意这里的conn.close()不是真正关闭物理连接而是把连接归还给连接池Druid 内部会处理。3.3 图书检索的 SQL 与分页实现二手图书交易系统最核心的功能是检索。用户可能按书名模糊查、按分类筛选、按价格排序这三个条件要能组合。下面是一个带分页的检索 SQLSELECT b.*, c.name AS category_name, u.nickname AS seller_name FROM book b LEFT JOIN category c ON b.category_id c.id LEFT JOIN user u ON b.seller_id u.id WHERE b.status 1 AND (b.title LIKE CONCAT(%, #{keyword}, %) OR #{keyword} IS NULL) AND (b.category_id #{categoryId} OR #{categoryId} IS NULL) ORDER BY b.create_time DESC LIMIT #{offset}, #{pageSize}这个 SQL 用了LEFT JOIN关联分类和用户表这样列表页能直接显示分类名和卖家昵称不用再查一次。#{keyword} IS NULL这个条件让同一个 SQL 支持「有搜索词」和「无搜索词」两种情况不用在 Java 里拼字符串。LIMIT #{offset}, #{pageSize}是 MySQL 的分页语法offset (pageNum - 1) * pageSize。对应的 DAO 方法public ListBook searchBooks(String keyword, Integer categoryId, int pageNum, int pageSize) { String sql SELECT b.*, c.name AS category_name, u.nickname AS seller_name FROM book b LEFT JOIN category c ON b.category_id c.id LEFT JOIN user u ON b.seller_id u.id WHERE b.status 1 AND (b.title LIKE ? OR ? IS NULL) AND (b.category_id ? OR ? IS NULL) ORDER BY b.create_time DESC LIMIT ?, ?; Connection conn null; PreparedStatement ps null; ResultSet rs null; ListBook list new ArrayList(); try { conn JdbcUtils.getConnection(); ps conn.prepareStatement(sql); // 注意参数顺序keyword 出现两次categoryId 出现两次 ps.setString(1, keyword null ? null : % keyword %); ps.setString(2, keyword); if (categoryId null) { ps.setNull(3, Types.INTEGER); ps.setNull(4, Types.INTEGER); } else { ps.setInt(3, categoryId); ps.setInt(4, categoryId); } ps.setInt(5, (pageNum - 1) * pageSize); ps.setInt(6, pageSize); rs ps.executeQuery(); while (rs.next()) { list.add(mapRowToBook(rs)); } } catch (SQLException e) { e.printStackTrace(); } finally { JdbcUtils.close(conn, ps, rs); } return list; }参数设置是这段代码最容易翻车的地方。keyword在 SQL 里出现了两次第一次要加%通配符第二次要传原始值用于IS NULL判断。categoryId同理。如果参数顺序设错查询结果会完全不对。ps.setNull(3, Types.INTEGER)不能写成ps.setObject(3, null)后者在某些驱动版本下会报错。4. 核心功能实现登录、发布、下单的完整链路4.1 用户登录与 Session 管理登录功能看起来简单但涉及密码加密、Session 管理和拦截器三个点。密码存 MD5 值登录时把用户输入的密码也转成 MD5 再比对public User login(String username, String rawPassword) { // 先对明文密码做 MD5再和数据库里的密文比对 String md5Password DigestUtils.md5Hex(rawPassword); String sql SELECT * FROM user WHERE username ? AND password ?; // ... 执行查询返回 User 对象或 null }DigestUtils来自 Apache Commons Codec比手写MessageDigest简洁。注意 MD5 已经不安全了但课设项目用 MD5 足够生产环境应该用 BCrypt。登录成功后把用户信息存入 SessionWebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); UserService userService new UserService(); User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/WEB-INF/views/login.jsp).forward(req, resp); return; } // 登录成功把 user 存入 session req.getSession().setAttribute(currentUser, user); resp.sendRedirect(req.getContextPath() /index); } }req.getSession()会自动创建 Session 并生成 JSESSIONID通过 Cookie 发给浏览器。setAttribute(currentUser, user)把用户对象存进去后续页面用${sessionScope.currentUser.nickname}就能取到昵称。sendRedirect后面必须return否则代码会继续执行到forward导致IllegalStateException。4.2 图书发布文件上传与表单验证发布图书涉及文件上传封面图和表单验证。文件上传用 Servlet 3.0 的MultipartConfig注解WebServlet(/book/publish) MultipartConfig( maxFileSize 5 * 1024 * 1024, // 单文件最大 5MB maxRequestSize 10 * 1024 * 1024, // 整个请求最大 10MB fileSizeThreshold 1024 * 1024 // 超过 1MB 写入临时文件 ) public class PublishBookServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 检查登录状态 User currentUser (User) req.getSession().getAttribute(currentUser); if (currentUser null) { resp.sendRedirect(req.getContextPath() /login); return; } // 获取普通表单字段 String title req.getParameter(title); String author req.getParameter(author); String priceStr req.getParameter(price); // 服务端验证书名不能为空价格必须是正数 if (title null || title.trim().isEmpty()) { req.setAttribute(error, 书名不能为空); req.getRequestDispatcher(/WEB-INF/views/publish.jsp).forward(req, resp); return; } BigDecimal price; try { price new BigDecimal(priceStr); if (price.compareTo(BigDecimal.ZERO) 0) { throw new NumberFormatException(); } } catch (NumberFormatException e) { req.setAttribute(error, 价格必须是正数); req.getRequestDispatcher(/WEB-INF/views/publish.jsp).forward(req, resp); return; } // 处理文件上传 Part filePart req.getPart(coverImage); String imagePath null; if (filePart ! null filePart.getSize() 0) { String fileName UUID.randomUUID().toString() getExtension(filePart.getSubmittedFileName()); // 保存到 webapp/uploads 目录 String uploadDir getServletContext().getRealPath(/uploads); File dir new File(uploadDir); if (!dir.exists()) dir.mkdirs(); filePart.write(uploadDir File.separator fileName); imagePath uploads/ fileName; } // 组装 Book 对象并入库 Book book new Book(); book.setTitle(title); book.setAuthor(author); book.setPrice(price); book.setCoverImage(imagePath); book.setSellerId(currentUser.getId()); book.setStatus(1); new BookService().publish(book); resp.sendRedirect(req.getContextPath() /book/my); } }MultipartConfig的三个参数要理解maxFileSize是单个文件上限maxRequestSize是整个请求上限包含所有文件和表单字段fileSizeThreshold是内存缓冲区大小超过这个值会写入临时文件。如果上传大文件报FileSizeLimitExceededException就是这两个 size 设小了。文件名用UUID重命名是为了防止用户上传同名文件覆盖也防止文件名里的特殊字符导致路径问题。getServletContext().getRealPath(/uploads)获取的是 webapp 下的真实路径但注意如果项目打成 war 包部署这个路径在容器重启后可能被清空生产环境应该存到外部目录。课设项目用getRealPath没问题。4.3 下单流程与事务控制下单是最能体现业务逻辑复杂度的功能。用户点击「购买」后系统要做三件事创建订单、创建订单明细、把图书状态改为已售。这三步必须在一个事务里完成否则可能出现「订单创建了但图书还在售」的数据不一致。public boolean createOrder(int buyerId, int bookId) { Connection conn null; try { conn JdbcUtils.getConnection(); // 关闭自动提交开启事务 conn.setAutoCommit(false); // 1. 查询图书信息同时加行锁防止并发下单 String bookSql SELECT * FROM book WHERE id ? AND status 1 FOR UPDATE; PreparedStatement bookPs conn.prepareStatement(bookSql); bookPs.setInt(1, bookId); ResultSet rs bookPs.executeQuery(); if (!rs.next()) { conn.rollback(); return false; // 图书不存在或已售 } BigDecimal price rs.getBigDecimal(price); int sellerId rs.getInt(seller_id); if (sellerId buyerId) { conn.rollback(); return false; // 不能买自己的书 } // 2. 创建订单 String orderSql INSERT INTO order (buyer_id, seller_id, total_amount, status, create_time) VALUES (?, ?, ?, 1, NOW()); PreparedStatement orderPs conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); orderPs.setInt(1, buyerId); orderPs.setInt(2, sellerId); orderPs.setBigDecimal(3, price); orderPs.executeUpdate(); ResultSet keys orderPs.getGeneratedKeys(); keys.next(); int orderId keys.getInt(1); // 3. 创建订单明细 String itemSql INSERT INTO order_item (order_id, book_id, price) VALUES (?, ?, ?); PreparedStatement itemPs conn.prepareStatement(itemSql); itemPs.setInt(1, orderId); itemPs.setInt(2, bookId); itemPs.setBigDecimal(3, price); itemPs.executeUpdate(); // 4. 更新图书状态为已售 String updateSql UPDATE book SET status 2 WHERE id ?; PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setInt(1, bookId); updatePs.executeUpdate(); // 全部成功提交事务 conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } JdbcUtils.close(conn, null, null); } }这段代码有几个关键点。第一conn.setAutoCommit(false)开启事务所有操作在commit()之前都不会真正写入数据库。第二SELECT ... FOR UPDATE加了行锁防止两个用户同时买同一本书——第一个事务锁住这行第二个事务会等待等第一个提交后第二个查到status 2就会返回失败。第三Statement.RETURN_GENERATED_KEYS让 JDBC 返回自增主键这样不用再查一次订单 ID。第四finally里把autoCommit恢复为true再归还连接否则连接池里的这个连接下次被复用时还是手动提交模式会导致其他操作不生效。5. 避坑与排查课设项目最容易翻车的五个地方5.1 中文乱码从请求到响应全链路排查现象表单提交的中文书名在数据库里变成???或者 JSP 页面显示乱码。原因乱码可能出现在三个环节——请求编码、数据库编码、响应编码。任何一个环节没设对都会乱码。解决请求端用request.setCharacterEncoding(UTF-8)但注意这行必须在getParameter之前调用。如果用 Tomcat 8 以上GET 请求默认是 UTF-8但 POST 请求需要手动设。数据库连接 URL 加useUnicodetruecharacterEncodingutf8。数据库和表的字符集用utf8mb4。响应端设response.setContentType(text/html;charsetUTF-8)。JSP 页面头部加% page contentTypetext/html;charsetUTF-8 languagejava %。四个地方都设对乱码基本不会出现。5.2 连接池报错连接泄漏与超时配置现象项目运行一段时间后报GetConnectionTimeoutException或者 Tomcat 日志里出现Connection leak detected。原因DAO 方法里Connection没有在finally里关闭或者close顺序写错导致异常连接没归还给连接池。另一个原因是maxActive设太小并发一高就不够用。解决每个 DAO 方法都用try-finally确保JdbcUtils.close被调用。Druid 可以开启removeAbandonedtrue和removeAbandonedTimeout300自动回收超过 5 分钟没关闭的连接但这只是兜底根本还是要写对close。maxActive根据并发量调整课设项目 20 够用如果报超时可以先调到 50 试试。5.3 文件上传失败路径与权限问题现象上传封面图时报FileNotFoundException或AccessDeniedException。原因getRealPath(/uploads)返回的路径在 IDEA 里指向target目录下的临时部署路径这个目录可能不存在或者没有写权限。另外如果MultipartConfig没加req.getPart会返回 null。解决先new File(uploadDir).mkdirs()确保目录存在。检查MultipartConfig注解是否加在 Servlet 类上。如果还是失败把上传目录改到项目外的绝对路径比如D:/upload/用FileOutputStream手动写入。IDEA 里运行 Tomcat 时getRealPath的路径可以在启动日志里看到打印出来确认一下。5.4 事务不生效自动提交没关或异常没回滚现象下单时订单创建了但图书状态没改或者图书状态改了但订单没创建。原因conn.setAutoCommit(false)没调用或者调用了但异常发生时没有rollback。另一个常见原因是把Connection存在成员变量里多个请求共用同一个连接事务互相干扰。解决Connection必须是方法内的局部变量不能是 Servlet 的成员变量。setAutoCommit(false)后所有executeUpdate都在同一个事务里任何一步抛异常都要rollback。finally里恢复autoCommit为true。如果用了 Spring 的Transactional注意方法必须是public且同类内部调用不会触发事务。5.5 JSP 页面报 404路径与转发问题现象访问/book/list报 404或者 JSP 页面里的 CSS/JS 加载不出来。原因WebServlet的路径和访问 URL 不匹配或者forward的 JSP 路径写错。静态资源 404 通常是因为用了相对路径而页面 URL 有多层路径。解决WebServlet(/book/list)对应的访问地址是http://localhost:8080/项目名/book/list注意项目名不能漏。forward的路径以/开头相对于 webapp 根目录比如/WEB-INF/views/book-list.jsp。静态资源用${pageContext.request.contextPath}/css/style.css这种绝对路径不要用../css/style.css。如果 JSP 放在WEB-INF下必须通过 Servlet 转发访问直接输 URL 会 404这是正常的。6. 从能跑到能拿高分三个进阶技巧6.1 用过滤器统一处理编码和登录校验每个 Servlet 里都写setCharacterEncoding和登录检查太啰嗦用 Filter 统一处理WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 统一设置请求和响应编码 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 放行登录、注册、静态资源 if (uri.contains(/login) || uri.contains(/register) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/uploads/)) { chain.doFilter(req, resp); return; } // 其他请求检查登录状态 if (request.getSession().getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login); return; } chain.doFilter(req, resp); } }WebFilter(/*)拦截所有请求。放行列表里加上静态资源路径否则 CSS 和图片也会被拦截导致页面样式丢失。这个 Filter 同时解决了编码和登录校验两个问题代码量比在每个 Servlet 里写少得多。6.2 用 AJAX 做无刷新检索和加入购物车传统表单提交会刷新整个页面用户体验差。用 AJAX 只更新图书列表区域function searchBooks() { const keyword document.getElementById(keyword).value; const categoryId document.getElementById(category).value; // 用 fetch 发 GET 请求后端返回 JSON fetch(${contextPath}/book/search?keyword${encodeURIComponent(keyword)}categoryId${categoryId}) .then(response response.json()) .then(data { const listDiv document.getElementById(bookList); listDiv.innerHTML ; // 清空旧数据 data.forEach(book { // 动态创建图书卡片 const card document.createElement(div); card.className book-card; card.innerHTML img src${contextPath}/${book.coverImage} alt封面 h3${book.title}/h3 p${book.author}/p p classprice¥${book.price}/p button onclickbuyBook(${book.id})购买/button ; listDiv.appendChild(card); }); }) .catch(error console.error(检索失败:, error)); }后端对应的 Servlet 返回 JSON 而不是 JSPWebServlet(/book/search) public class BookSearchServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword req.getParameter(keyword); String categoryIdStr req.getParameter(categoryId); Integer categoryId (categoryIdStr null || categoryIdStr.isEmpty()) ? null : Integer.parseInt(categoryIdStr); ListBook books new BookService().searchBooks(keyword, categoryId, 1, 20); // 用 Jackson 或 Gson 转 JSON resp.setContentType(application/json;charsetUTF-8); ObjectMapper mapper new ObjectMapper(); mapper.writeValue(resp.getWriter(), books); } }encodeURIComponent对关键词编码防止特殊字符导致 URL 解析错误。后端返回 JSON 时contentType必须是application/json否则前端response.json()会解析失败。用 Jackson 的ObjectMapper序列化ListBook注意Book类要有 getter 方法否则序列化出来是空对象。6.3 用 Git 管理代码并写一份能加分的 README课设交作业时老师不只看代码能不能跑还看你的工程素养。用 Git 管理代码提交记录清晰README 写清楚项目介绍、技术栈、数据库配置步骤、运行截图这些都能加分。README 至少包含这几块项目简介一句话说清做什么、技术栈Java 8 Servlet 4.0 MySQL 8.0 Tomcat 9、功能列表用户注册登录、图书发布、检索、下单、订单管理、数据库配置导入campus_book.sql修改db.properties里的密码、运行步骤IDEA 导入 Maven 项目配置 Tomcat启动后访问/index。截图放首页、图书详情页、订单页三张就够。Git 提交信息写清楚每次改了什么比如feat: 添加图书检索分页、fix: 修复下单事务未回滚问题。不要一次性提交所有代码分多次提交能体现开发过程。如果老师检查提交记录看到你按功能模块逐步提交印象分会高很多。我自己的习惯是每完成一个功能就提交一次提交前先git status确认没有多余文件target目录和.idea目录加到.gitignore里。数据库脚本单独放一个sql目录README 里写清楚导入顺序。这些细节不费多少时间但能让你的课设从「能跑」变成「规范」95 分以上的项目通常赢在这些地方。希望帮到你。本文还有配套的精品资源点击获取
返回列表