
简介本资源是一套面向计算机专业本科生的JavaEE课程设计项目——影视创作论坛系统适用于Web开发实践、毕业设计参考及JavaEE技术栈综合训练。系统围绕影视爱好者交流场景构建涵盖影视资讯发布、幕后创作分享、观影体验讨论等核心功能完整呈现了B/S架构下用户管理、内容发布、评论互动等典型模块。压缩包共142个文件含55个XML配置与映射文件支撑SpringMyBatis框架集成、46个rels关系文件保障Office文档兼容性、18个PNG与17个JPEG格式界面截图及原型图直观展示前后端交互效果整体容量54.95MB结构清晰、模块可拆解。目前已有487人学习下载资源提供可运行源码、配套MySQL数据库脚本及完整课程设计论文覆盖需求分析、系统设计、编码实现到测试部署全流程特别适合初学者理解MVC分层结构与企业级Web开发规范。1. 一个能跑通、能改、能交作业的JavaEE影视论坛不是Demo是完整可部署的课程设计闭环某高校计算机专业大三学生A同学在课程设计截止前72小时对着IDEA里报红的web.xml和Tomcat控制台满屏的ClassNotFoundException发呆——他下载了三个号称“基于JavaEE的影视论坛”源码包两个连登录页都打不开一个数据库脚本缺字段第三个倒是能注册但发帖后刷新就404。这不是个例。真正能从git clone一路走到mvn clean package再部署到本地Tomcat、用户能注册→发帖→回帖→上传封面图、管理员能审核帖子的JavaEE论坛项目少之又少。这个.rar包就是那个“少之又少”里的一个它用JSPServletMySQLBootstrap构建不依赖Spring Boot自动装配黑匣子所有请求链路清晰可见它包含完整的数据库建表语句、初始化数据含5类影视标签、10个模拟用户、前后端分离式静态资源组织更重要的是它把课程设计最常被扣分的三个点——权限校验逻辑、文件上传安全处理、帖子内容XSS过滤——全写在源码里且每处都有中文注释。适合需要快速交付、理解底层流程、或在此基础上加功能比如接入Redis缓存热帖的开发者。2. 环境准备与项目结构解析看清骨架再动手2.1 运行环境最低要求与验证步骤这个项目是典型的传统JavaEE Web应用不追求新潮框架但对基础环境版本有明确约束。我建议你按以下顺序逐项验证跳过任何一步都可能在后续编译或部署时报出难以定位的错误JDK 8u202 或更高版本必须Java 11 会导致部分JSP EL表达式解析失败如${user.nickname}在某些Tomcat版本下报错而JDK 7已淘汰。验证命令java -version # 正确输出示例java version 1.8.0_291提示如果系统默认是Java 11请临时切换。Windows下可在IDEA的Project Structure → Project Settings → Project → Project SDK中手动指定Linux/macOS可通过export JAVA_HOME/path/to/jdk1.8设置。Apache Tomcat 8.5.x推荐8.5.94Tomcat 9 对Servlet 3.1规范支持更严格而本项目web.xml中声明的是web-app version3.0直接部署到Tomcat 9会触发web.xml版本兼容警告并可能忽略部分配置。验证方式解压Tomcat后运行bin/startup.batWin或bin/startup.shMac/Linux访问http://localhost:8080看到Tomcat欢迎页即成功。MySQL 5.7.x非8.0项目SQL脚本使用utf8字符集非utf8mb4MySQL 8.0默认utf8mb4且sql_mode更严格执行建表语句时会报Unknown character set: utf8。解决方案安装MySQL 5.7或修改SQL脚本中所有CHARSETutf8为CHARSETutf8mb4并在MySQL配置文件my.cnf中添加[client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci2.2 项目目录结构每个文件夹都承担明确职责解压.rar后你会看到一个标准的Java Web项目结构。不要被src/下的多层包名吓到核心模块其实就五个我按调用链路从外到内梳理film-forum/ ├── src/ # Java源码根目录 │ ├── main/ # 主要业务代码 │ │ ├── java/ # Servlet、DAO、Service层 │ │ │ └── com.example.filmforum/ │ │ │ ├── controller/ # 所有Servlet入口LoginServlet、PostServlet等 │ │ │ ├── dao/ # JDBC操作封装UserDao、PostDao │ │ │ ├── model/ # 实体类User、Post、Comment │ │ │ └── service/ # 业务逻辑UserService、PostService │ │ └── webapp/ # Web资源根目录JSP、CSS、JS、图片 │ │ ├── WEB-INF/ │ │ │ ├── web.xml # 核心配置Servlet映射、过滤器、监听器 │ │ │ └── lib/ # 依赖JAR包mysql-connector-java-5.1.47.jar等 │ │ ├── static/ # 静态资源Bootstrap CSS/JS、自定义样式 │ │ └── views/ # JSP页面login.jsp、index.jsp、post_detail.jsp │ └── test/ # 单元测试JUnit可选运行 ├── sql/ # 数据库脚本 │ ├── film_forum.sql # 完整建库建表初始化数据含管理员账号admin/123456 │ └── update_v1.1.sql # 后续升级脚本如新增点赞字段 └── pom.xml # Maven配置若你选择用Maven重构此文件已预置关键点在于webapp/WEB-INF/web.xml是整个项目的中枢神经。它定义了所有URL路径如何路由到对应Servlet也声明了CharacterEncodingFilter解决中文乱码、LoginFilter未登录跳转登录页等关键过滤器。打开它你会看到类似这样的映射servlet servlet-nameLoginServlet/servlet-name servlet-classcom.example.filmforum.controller.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping这意味着当浏览器访问http://localhost:8080/film-forum/login时Tomcat会实例化LoginServlet并调用其doPost()方法。这是理解整个项目请求流的第一把钥匙。2.3 依赖JAR包清单与作用说明webapp/WEB-INF/lib/目录下共7个JAR包全部为项目必需缺一不可。它们不是随意堆砌而是精准匹配JavaEE 6规范需求JAR包名版本作用不替换理由mysql-connector-java-5.1.47.jar5.1.47MySQL JDBC驱动5.1.x系列对JDK 8兼容性最佳8.0驱动需改Class.forName(com.mysql.cj.jdbc.Driver)且连接URL加serverTimezoneUTCjavax.servlet-api-3.0.1.jar3.0.1Servlet API接口定义Tomcat 8.5自带但开发时需引入编译版本必须与web.xml中web-app version3.0一致jstl-1.2.jar1.2JSP标准标签库c:if、c:forEach页面遍历帖子列表、条件显示管理按钮全靠它1.2是JSP 2.0黄金版本standard-1.1.2.jar1.1.2JSTL配套实现与jstl-1.2.jar成对出现缺一不可否则JSP页面报org.apache.taglibs.standard.tag.common.core.IfTag类找不到commons-fileupload-1.3.3.jar1.3.3文件上传解析帖子封面图项目中PostServlet用它解析multipart/form-data请求1.3.x是最后一个支持Servlet 3.0的稳定版commons-io-2.6.jar2.6IO工具配合FileUpload保存文件FileUpload依赖它做流操作2.6与JDK 8无兼容问题bootstrap-3.3.7-dist.zip3.3.7前端UI框架已解压在static/bootstrap/项目CSS/JS直接引用此版本新版Bootstrap 5移除了jQuery依赖会导致现有JSP中$(#modal).modal(show)失效注意如果你用IDEA打开项目它可能提示javax.servlet-api重复因Tomcat也提供。请在Project Structure → Modules → Dependencies中将该JAR的Scope设为Provided避免打包时重复包含。3. 核心模块拆解从登录到发帖的完整链路3.1 用户认证模块Session Filter的朴素但可靠方案项目没有用Shiro或Spring Security而是用最基础的HttpSession存储用户信息并通过Filter统一拦截未登录请求。这种做法在课程设计中反而更易理解、更易调试。关键代码在src/main/java/com/example/filmforum/filter/LoginFilter.javapublic class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // 1. 放行静态资源和登录相关路径 String uri req.getRequestURI(); if (uri.contains(/static/) || uri.endsWith(/login) || uri.endsWith(/register)) { chain.doFilter(request, response); return; } // 2. 检查Session中是否存在user对象 HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { // 3. 未登录重定向到登录页并携带原请求路径作为参数 String originalUrl req.getRequestURL().toString(); resp.sendRedirect(req.getContextPath() /login?redirect URLEncoder.encode(originalUrl, UTF-8)); return; } // 4. 已登录放行 chain.doFilter(request, response); } }逻辑说明与参数说明req.getSession(false)false表示不创建新Session只获取已有Session。这是安全习惯避免未登录用户也生成无用Session占用内存。redirect参数当用户访问/post/create但未登录时Filter会重定向到/login?redirect%2Ffilm-forum%2Fpost%2Fcreate。登录成功后LoginServlet会读取此参数resp.sendRedirect(redirectUrl)跳转回原页面用户体验无缝。为什么不用Cookie存Token因为课程设计场景下Session由Tomcat容器管理服务端可控无需自己实现Token签发/校验/过期逻辑降低复杂度。配套的LoginServlet核心逻辑src/main/java/com/example/filmforum/controller/LoginServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 1. 调用Service层验证实际是查DB比对密码明文——课程设计简化版生产环境应加盐哈希 UserService userService new UserService(); User user userService.login(username, password); if (user ! null) { // 2. 登录成功存入Session HttpSession session request.getSession(true); // true表示必要时创建新Session session.setAttribute(user, user); session.setMaxInactiveInterval(30 * 60); // Session超时30分钟 // 3. 获取redirect参数跳转 String redirect request.getParameter(redirect); if (redirect ! null !redirect.isEmpty()) { response.sendRedirect(redirect); } else { response.sendRedirect(request.getContextPath() /index.jsp); } } else { // 4. 登录失败设置错误信息转发回登录页 request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/views/login.jsp).forward(request, response); } }血泪经验初学者常犯的错误是request.getSession(true)写成request.getSession()后者等价于true看似没区别但getSession()在某些Tomcat版本下可能返回null。显式写true或false是防御性编程习惯。3.2 影视帖子发布模块文件上传与内容安全双保险发帖功能是项目亮点它同时解决了两个课程设计高频扣分点图片上传和XSS防护。文件上传流程PostServlet.java前端表单enctypemultipart/form-data包含文本域标题、内容和文件域封面图。PostServlet用DiskFileItemFactory和ServletFileUpload解析请求// 创建工厂设置临时文件目录和内存阈值 DiskFileItemFactory factory new DiskFileItemFactory(); factory.setRepository(new File(request.getServletContext().getRealPath(/temp))); // 临时目录 factory.setSizeThreshold(1024 * 1024); // 1MB以内存处理超则写临时文件 ServletFileUpload upload new ServletFileUpload(factory); upload.setHeaderEncoding(UTF-8); // 解决中文文件名乱码 ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 处理普通表单字段title, content String fieldName item.getFieldName(); String value item.getString(UTF-8); if (title.equals(fieldName)) title value; else if (content.equals(fieldName)) content value; } else { // 处理文件字段coverImage String fileName new File(item.getName()).getName(); // 获取原始文件名 if (!fileName.isEmpty()) { // 生成唯一文件名防止覆盖和路径穿越 String uniqueFileName System.currentTimeMillis() _ fileName; String uploadPath request.getServletContext().getRealPath(/uploads/); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); File uploadedFile new File(uploadDir, uniqueFileName); item.write(uploadedFile); // 保存文件 coverImagePath /uploads/ uniqueFileName; // 存入数据库的相对路径 } } }XSS过滤逻辑PostService.java用户输入的帖子内容可能含恶意JS如scriptalert(xss)/script。项目在保存前用正则粗筛public String filterXSS(String input) { if (input null) return null; // 移除script、iframe、onerror等危险标签和事件 String filtered input.replaceAll((?i)script[^]*?.*?/script, ); filtered filtered.replaceAll((?i)iframe[^]*?.*?/iframe, ); filtered filtered.replaceAll((?i)on\\w\\s*\\s*\[^\]*\, ); filtered filtered.replaceAll((?i)javascript:, ); return filtered; }注意这是课程设计级防护非生产级。真实项目应使用OWASP Java Encoder或Jsoup进行HTML清理。但此代码清晰展示了“输入即污染”的安全意识且能防住大部分基础XSS尝试。3.3 数据库交互手写JDBC DAO的透明可控性项目采用最原始的JDBC不使用Hibernate或MyBatis好处是每一行SQL、每一次ResultSet遍历都暴露在你眼前调试时System.out.println(sql)就能看到执行语句。以PostDao.java的查询热门帖子为例public ListPost findHotPosts(int limit) { ListPost posts new ArrayList(); String sql SELECT p.id, p.title, p.content, p.cover_image, p.view_count, p.create_time, u.nickname as author_name FROM post p JOIN user u ON p.author_id u.id WHERE p.status 1 // status1表示已审核通过 ORDER BY p.view_count DESC LIMIT ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, limit); try (ResultSet rs pstmt.executeQuery()) { while (rs.next()) { Post post new Post(); post.setId(rs.getLong(id)); post.setTitle(rs.getString(title)); post.setContent(rs.getString(content)); // 此处content已过XSS过滤 post.setCoverImage(rs.getString(cover_image)); post.setViewCount(rs.getInt(view_count)); post.setCreateTime(rs.getTimestamp(create_time)); post.setAuthorName(rs.getString(author_name)); posts.add(post); } } } catch (SQLException e) { e.printStackTrace(); // 课程设计允许生产环境应记录日志 } return posts; }关键细节DBUtil.getConnection()封装了DriverManager.getConnection()复用数据库连接配置jdbc:mysql://localhost:3306/film_forum。try-with-resources自动关闭Connection、PreparedStatement、ResultSet避免连接泄漏。这是JDK 7的必备写法。status 1帖子有状态机0-草稿1-已发布2-已删除管理员后台可审核体现基本业务流程。4. 部署与运行从零到首页的实操步骤4.1 数据库初始化三步走不漏一行项目sql/film_forum.sql是完整可执行脚本但直接source可能失败。按此顺序操作创建数据库并指定字符集CREATE DATABASE film_forum CHARACTER SET utf8 COLLATE utf8_general_ci; USE film_forum;执行建表语句复制film_forum.sql中CREATE TABLE部分注意脚本中所有ENGINEInnoDB DEFAULT CHARSETutf8必须保留utf8是MySQL 5.7的合法写法非utf8mb4。插入初始化数据复制INSERT INTO部分其中管理员账号为INSERT INTO user (username, password, nickname, email, role, status) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, adminfilm.com, ADMIN, 1);密码123456的MD5值是e10adc3949ba59abbe56e057f20f883e可直接登录后台。排查如果执行后SELECT * FROM user看不到数据检查是否忘了USE film_forum;或MySQL客户端默认字符集不是utf8执行SET NAMES utf8;后再source。4.2 IDEA中配置Tomcat并部署添加Tomcat ServerRun→Edit Configurations→→Tomcat Server→Local→Configure...→ 选择你解压的Tomcat 8.5目录。配置Deployment在刚创建的Tomcat配置中点击Deployment选项卡 →→Artifact→ 选择film-forum:war exploded注意是exploded非war。Application context填/film-forum与项目名一致访问路径即http://localhost:8080/film-forum。配置Server选项On Update action选Update classes and resources热更新On frame deactivation选Update resources切出IDEA窗口时自动更新静态文件。启动点击绿色三角形。首次启动会自动部署控制台输出INFO: Server startup in [xxx] ms即成功。访问http://localhost:8080/film-forum看到首页轮播图和帖子列表说明部署成功。4.3 常见问题排查这5个坑我替你踩过了现象1首页CSS样式完全丢失页面纯文字堆叠原因webapp/static/下的Bootstrap CSS/JS文件路径与JSP中引用路径不匹配。检查index.jsp中link relstylesheet href${pageContext.request.contextPath}/static/bootstrap/css/bootstrap.min.css解决确保webapp/static/bootstrap/css/目录下存在bootstrap.min.css。如果文件在webapp/static/css/则引用路径改为/static/css/bootstrap.min.css。路径错误是前端资源加载失败的头号原因。现象2登录后点击“我的帖子”报404URL变成http://localhost:8080/post/my原因web.xml中url-pattern配置错误。正确应为/post/*而非/post。解决打开webapp/WEB-INF/web.xml找到PostServlet的servlet-mapping确认url-pattern是/post/*。*表示匹配所有以/post/开头的路径如/post/my、/post/create。现象3上传封面图后帖子详情页图片不显示控制台报404原因PostServlet中保存文件的路径与JSP中引用的路径不一致。代码中/uploads/ uniqueFileName但JSP中写成img srcuploads/xxx.jpg少了开头的/。解决JSP中图片路径必须以/开头表示从Web应用根目录开始img src${pageContext.request.contextPath}/uploads/${post.coverImage}。现象4Tomcat启动报错java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因mysql-connector-java-5.1.47.jar未正确放入WEB-INF/lib/或IDEA未识别该JAR。解决手动检查webapp/WEB-INF/lib/目录下是否有该JAR在IDEA中右键项目 →Add Framework Support→Web Application→ 确认lib目录被标记为Library若仍报错在web.xml中添加listener声明ContextLoaderListener本项目已内置检查是否被误删。现象5发帖后内容中的中文显示为??数据库里也是乱码原因MySQL连接URL未指定字符集或数据库/表字符集非utf8。解决修改DBUtil.java中连接字符串private static final String URL jdbc:mysql://localhost:3306/film_forum?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8;确认数据库和表字符集SHOW CREATE DATABASE film_forum; -- 应显示 DEFAULT CHARSETutf8 SHOW CREATE TABLE post; -- 应显示 ENGINEInnoDB DEFAULT CHARSETutf85. 二次开发实战给论坛加“点赞”功能的全流程现在你已经能跑通整个项目下一步是让它成为你的作品。我以“为帖子添加点赞功能”为例演示如何从零开始扩展一个新特性。这不是照着教程抄而是还原一个真实开发者的思考链路需求分析 → 数据库变更 → 后端逻辑 → 前端交互 → 测试验证。5.1 需求分析与数据库设计点赞功能看似简单但需考虑三个核心问题去重同一用户对同一帖子只能点一次赞计数首页需显示每个帖子的点赞数状态用户进入帖子详情页时需知道“我是否已点过赞”以便显示“已赞”或“点赞”按钮。因此不能只在post表加一个like_count字段无法去重。必须新建一张关联表post_likeCREATE TABLE post_like ( id BIGINT PRIMARY KEY AUTO_INCREMENT, post_id BIGINT NOT NULL, user_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_post_user (post_id, user_id), -- 关键联合唯一索引保证一帖一赞 KEY idx_post (post_id), KEY idx_user (user_id) );提示UNIQUE KEY uk_post_user (post_id, user_id)是技术关键。它让数据库强制校验避免应用层重复插入比Java代码里先SELECT再INSERT更可靠。5.2 后端逻辑实现Controller Service DAO三层联动第一步DAO层新增方法PostLikeDao.java// 检查用户是否已点赞该帖子 public boolean hasLiked(long postId, long userId) { String sql SELECT COUNT(*) FROM post_like WHERE post_id ? AND user_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setLong(1, postId); pstmt.setLong(2, userId); try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { return rs.getInt(1) 0; } } } catch (SQLException e) { e.printStackTrace(); } return false; } // 新增点赞记录 public int addLike(long postId, long userId) { String sql INSERT INTO post_like (post_id, user_id) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { pstmt.setLong(1, postId); pstmt.setLong(2, userId); return pstmt.executeUpdate(); // 返回影响行数1表示成功 } catch (SQLException e) { if (e.getSQLState().equals(23000)) { // MySQL唯一键冲突错误码 return 0; // 已存在不重复插入 } e.printStackTrace(); return -1; } } // 获取帖子点赞数 public int getLikeCount(long postId) { String sql SELECT COUNT(*) FROM post_like WHERE post_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setLong(1, postId); try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } } catch (SQLException e) { e.printStackTrace(); } return 0; }第二步Service层封装业务PostService.javapublic class PostService { private PostLikeDao likeDao new PostLikeDao(); private PostDao postDao new PostDao(); // 点赞主逻辑先检查再插入最后更新计数可选此处用实时查询 public Result likePost(long postId, long userId) { // 1. 检查是否已点赞 if (likeDao.hasLiked(postId, userId)) { return Result.fail(您已赞过此帖); } // 2. 插入点赞记录 int result likeDao.addLike(postId, userId); if (result 1) { return Result.success(点赞成功); } else if (result 0) { return Result.fail(点赞失败重复操作); } else { return Result.fail(点赞失败系统异常); } } // 获取帖子详情时一并查询点赞状态和数量 public Post getPostWithLikeStatus(long postId, long userId) { Post post postDao.findById(postId); if (post ! null) { post.setLikeCount(likeDao.getLikeCount(postId)); post.setHasLiked(likeDao.hasLiked(postId, userId)); } return post; } }第三步Controller暴露APILikeServlet.javaWebServlet(/like) public class LikeServlet extends HttpServlet { private PostService postService new PostService(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取Session中的用户 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 未登录); return; } User user (User) session.getAttribute(user); // 2. 获取参数 String postIdStr request.getParameter(postId); if (postIdStr null || postIdStr.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, 缺少postId参数); return; } long postId Long.parseLong(postIdStr); // 3. 调用Service Result result postService.likePost(postId, user.getId()); // 4. 返回JSON简化版实际可用Jackson response.setContentType(application/json;charsetUTF-8); PrintWriter out response.getWriter(); out.print({\code\: (result.isSuccess() ? 200 : 400) ,\msg\:\ result.getMsg() \}); out.flush(); } }注意web.xml中需添加该Servlet映射servlet servlet-nameLikeServlet/servlet-name servlet-classcom.example.filmforum.controller.LikeServlet/servlet-class /servlet servlet-mapping servlet-nameLikeServlet/servlet-name url-pattern/like/url-pattern /servlet-mapping5.3 前端交互JSP jQuery实现无刷新点赞在post_detail.jsp中找到帖子标题下方插入点赞按钮和计数显示!-- 点赞按钮区域 -- div classlike-section button typebutton classbtn btn-sm btn-outline-secondary idlikeBtn onclicktoggleLike(${post.id}) span idlikeText${post.hasLiked ? 已赞 : 点赞}/span i classglyphicon glyphicon-heart idlikeIcon stylecolor: ${post.hasLiked ? red : gray};/i /button span classbadge bg-secondary idlikeCount${post.likeCount}/span /div !-- jQuery脚本 -- script src${pageContext.request.contextPath}/static/jquery-3.6.0.min.js/script script function toggleLike(postId) { const $btn $(#likeBtn); const $text $(#likeText); const $icon $(#likeIcon); const $count $(#likeCount); $.post(${pageContext.request.contextPath}/like, {postId: postId}, function(data) { const result JSON.parse(data); if (result.code 200) { // 切换状态 const isLiked $text.text() 已赞; $text.text(isLiked ? 点赞 : 已赞); $icon.css(color, isLiked ? gray : red); // 更新计数1或-1此处简化重新查总数 const currentCount parseInt($count.text()); $count.text(isLiked ? (currentCount - 1) : (currentCount 1)); } else { alert(操作失败 result.msg); } }).fail(function() { alert(网络错误请重试); }); } /script5.4 验证与收尾确保功能闭环功能验证用用户A登录进入帖子详情页点击“点赞”按钮变“已赞”计数1刷新页面按钮仍为“已赞”计数不变证明hasLiked查询生效用用户B登录同一帖子点击“点赞”计数再1用户A再次点击提示“您已赞过此帖”。数据验证SELECT * FROM post_like WHERE post_id 1; -- 应看到两条记录user_id不同 SELECT COUNT(*) FROM post_like; -- 总点赞数应与前端显示一致边界验证未登录用户点击按钮应跳转到登录页LoginFilter已拦截直接访问/like?postId1GET方式应返回405 Method Not AllowedLikeServlet只处理POST。从那以后我每次给课程设计加功能都强制走一遍“数据库设计 → DAO → Service → Controller → JSP”五步链路哪怕只是加个字段。因为漏掉任何一环都会在某个深夜的调试中让你对着控制台发呆半小时。希望帮到你。本文还有配套的精品资源点击获取