
简介这是一套基于JavaWeb技术栈开发的背单词网站及配套安卓App的完整源码项目面向Java初学者与Web开发练习者解决词汇记忆类应用从后端到前端、从网页到移动端的全链路实现问题。资源包含542个文件涵盖56个JAR依赖库、37个核心Java业务类、22个JSP页面、11个JS交互脚本、8个XML配置文件以及SQL数据库脚本和CSS/IMG等静态资源整体压缩包达71.21MB结构完整、模块清晰可直接部署运行。已有839人学习下载项目已集成恋恋有词2019版单词库支持在线发音、用户登录、单词本管理等核心功能并提供HBuilder打包的WebApp方案兼顾网页版与移动轻量化使用场景。代码附带详细注释与分层结构DAO/Service/Servlet/JSP适合作为Java Web课程设计、毕业设计或全栈入门实战范例。1. 一个能真正“用起来”的背单词网站Java Web 全栈落地从数据库到安卓打包一气呵成你有没有试过下载一个“Java 背单词系统”解压后发现只有几个.java文件、没数据库脚本、没web.xml、连 Tomcat 都跑不起来或者好不容易配好环境登录页一打开就报ClassNotFoundException: com.mysql.jdbc.Driver查半天才发现它用的是 MySQL 5.x 的老驱动而你本地装的是 8.x这个项目不是那种“教学 Demo”——它是一套真实可部署、带完整数据、支持用户登录单词本发音安卓打包的 Java Web 工程。它用的是标准 Servlet JSP JDBC MySQL 技术栈没有 Spring Boot 魔法封装所有请求流转、会话管理、SQL 拼接、页面跳转都裸露在源码里。适合两类人一是刚学完 JDBC 和 Servlet 想找一个“能跑通、能改懂、能交作业”的实战项目二是需要快速搭建一个轻量级词汇管理后台的嵌入式场景比如某高校实验室给学生配的内部学习平台。它不炫技但每一步都踩在 Java Web 开发的真实路线上建库、建表、写 DAO、配 web.xml、处理中文乱码、解决 session 失效、甚至 HBuilder 打包成 APK 的manifest.json配置细节都已就位。2. 项目结构与技术栈拆解看清它到底由哪几块“硬骨头”组成这个项目不是单模块堆砌而是按 Java Web 标准分层组织的完整闭环。我拿到源码包后第一件事就是用tree -L 3看清骨架——它共包含7 个核心目录 3 类关键配置文件全部落在WebContent和src下符合传统 Dynamic Web Project 结构。下面逐层拆解重点标出那些“不看就容易翻车”的设计点。2.1 目录结构从WebContent到src的真实路径映射提示不要直接用 IDE 新建 Dynamic Web Project 后拖文件进去这个项目的WebContent/WEB-INF/web.xml是手动编写的且依赖特定 servlet-mapping 规则。若用 IDE 自动生成 web.xml会导致/login、/word/add等路径 404。src/ ├── dao/ # 数据访问层纯 JDBC 封装无 ORM │ ├── UserDao.java # 用户登录校验、注册插入 │ └── WordDao.java # 单词查询含模糊搜索、单词本增删、发音路径读取 ├── entity/ # POJOUser、Word、WordBook三者字段与数据库表严格一一对应 ├── servlet/ # 控制层LoginServlet、AddWordServlet、PlaySoundServlet 等 9 个核心 Servlet │ ├── LoginServlet.java # 处理 POST /login含密码 MD5注意是原始 MD5无 salt │ ├── WordListServlet.java # 分页查单词库支持 category 参数如 lianlanyouci_2019 │ └── AddToBookServlet.java # 将单词 ID 用户 ID 插入 wordbook 表需 session 中有 uid ├── util/ # 工具类DBUtil单例 DataSource、MD5Util、FileUtil读取音频路径 WebContent/ ├── audio/ # 发音文件存放目录MP3 格式路径硬编码在 word 表的 sound_path 字段 ├── css/ # 基础样式login.css、wordlist.css无框架纯 float margin ├── js/ # 交互逻辑login.js表单校验、wordlist.js分页按钮绑定、添加到单词本 AJAX ├── WEB-INF/ │ ├── web.xml # 关键servlet-mapping 映射到 *.do如 /login.dofilter 配置字符编码过滤器 │ └── lib/ # 仅 3 个 jarmysql-connector-java-5.1.47.jar、jstl-1.2.jar、standard-1.1.2.jar ├── index.jsp # 未登录首页展示单词库概览 登录入口 ├── login.jsp # 登录表单POST 到 /login.do └── wordlist.jsp # 登录后主界面分页表格 “加入单词本”按钮 “播放”按钮这个结构的关键在于它把“业务动作”和“页面呈现”完全解耦。比如点击“加入单词本”前端 JS 发起 AJAX 请求到/addtobook.do?wordid123后端AddToBookServlet拿到wordid和session.getAttribute(uid)调用WordDao.addToBook(uid, wordid)插入数据库返回 JSON{success:true}。整个链路没有 Struts Action 或 Spring MVC 的注解干扰全是request.getParameter()和response.getWriter().write()对初学者极其友好。2.2 数据库设计一张word表撑起全部功能但字段设计很务实项目自带db_init.sql脚本位于根目录或doc/下建库语句为CREATE DATABASE IF NOT EXISTS worddb DEFAULT CHARSET utf8 COLLATE utf8_general_ci;。核心表只有 3 张但word表承担了 80% 的查询压力-- word 表存储所有单词数据sound_path 存放相对路径如 audio/abandon.mp3 CREATE TABLE word ( id int(11) NOT NULL AUTO_INCREMENT, english varchar(50) NOT NULL COMMENT 英文单词, chinese text COMMENT 中文释义支持多义项用br分隔, phonetic varchar(100) DEFAULT NULL COMMENT 音标, sound_path varchar(200) DEFAULT NULL COMMENT 发音文件路径, category varchar(50) DEFAULT lianlanyouci_2019 COMMENT 所属词库分类, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8; -- user 表极简设计仅 id、username、passwordMD5 加密存储 CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(32) NOT NULL, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8; -- wordbook 表用户-单词多对多关系表 CREATE TABLE wordbook ( id int(11) NOT NULL AUTO_INCREMENT, userid int(11) NOT NULL, wordid int(11) NOT NULL, add_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_userid (userid), KEY idx_wordid (wordid) ) ENGINEInnoDB DEFAULT CHARSETutf8;注意word表的chinese字段用text类型而非varchar是因为“恋练有词”中很多单词释义长达 200 字如 abandon: v. ①抛弃遗弃②放弃中止③放纵沉溺且用br换行。JSP 页面中直接${word.chinese}输出浏览器会正确渲染换行——这是比 JSON 返回再 JS 拼接更直白的做法。2.3 技术选型逻辑为什么不用 Spring为什么坚持 JDBC这个项目的技术选择不是“落后”而是精准匹配需求不用 Spring/Spring Boot因为目标是“让学生看懂每一行代码”。Spring 的 IOC/AOP/自动装配会掩盖new UserDao()、conn.prepareStatement()这些基础操作。而这里每个 DAO 方法都显式获取连接、关闭资源DBUtil.getConnection()内部用BasicDataSource配置连接池最大 20 连接学生能清晰看到“连接从哪来、SQL 怎么执行、结果怎么映射”。坚持原生 JDBCWordDao.queryByCategory(String category, int page, int pageSize)方法中分页用的是LIMIT ?, ?MySQL 语法参数通过ps.setInt(1, (page-1)*pageSize)绑定。没有 MyBatis 的#{}占位符所有 SQL 拼接、参数设置、异常捕获SQLException都手写。好处是一旦出错堆栈信息直接指向WordDao.java:87而不是 MyBatis 的MapperProxy黑匣子。发音实现方案不是调用在线 API而是将 MP3 文件放在WebContent/audio/下word.sound_path存储相对路径。PlaySoundServlet根据wordid查出路径用ServletContext.getResourceAsStream(soundPath)读取二进制流设置response.setContentType(audio/mpeg)后写入response.getOutputStream()。这种方案离线可用、无网络依赖、响应快——正是“单词网站”最该有的样子。3. 环境搭建与运行实操从零开始跑通避开 90% 的新手坑别急着敲mvn clean install——这个项目是传统 WAR 包结构不走 Maven 构建流程。它的构建方式就是把WebContent当作 WebRootsrc当作源码目录直接部署到 Tomcat。下面是我实测通过的完整步骤每一步都标注了常见失败点。3.1 JDK 与 Tomcat 版本选择必须匹配否则血泪经验JDK 版本必须用JDK 8u202 或更低版本如 8u181。原因项目web.xml中web-app声明为version2.5而 Tomcat 9 默认要求web-app 3.1若用 JDK 11 编译javac会默认生成 class 文件版本 55JDK 11Tomcat 8.5 无法加载。Tomcat 版本推荐Tomcat 8.5.93最新 8.5.x。Tomcat 9 虽然支持 Servlet 4.0但此项目web.xml中 filter 配置的encodingFilter使用javax.servlet.Filter接口而 Tomcat 9 的servlet-api.jar包路径为jakarta.servlet.*会导致NoClassDefFoundError。验证方法在命令行执行java -version和catalina version确认输出中包含1.8.0_和Apache Tomcat/8.5.。3.2 数据库初始化字符集、驱动、时区三重校验创建数据库并指定字符集关键CREATE DATABASE worddb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意必须用utf8mb4不是utf8。因为 MySQL 的utf8实际只支持 3 字节 UTF-8 字符不支持 emoji 和部分生僻汉字而utf8mb4才是真正的 UTF-8。项目中“恋练有词”的释义含中文括号、顿号等用utf8会导致插入时乱码或截断。导入初始数据找到项目中的db_init.sql通常在根目录或doc/文件夹在 MySQL 命令行执行source /path/to/db_init.sql;验证数据SELECT COUNT(*) FROM word WHERE categorylianlanyouci_2019;应返回约 4500 条2019 版恋练有词单词数配置 JDBC 驱动将mysql-connector-java-5.1.47.jar项目WebContent/WEB-INF/lib/下复制到Tomcat/lib/目录不要用 8.x 驱动com.mysql.cj.jdbc.Driver是 8.x 的类名而项目DBUtil.java中写的是Class.forName(com.mysql.jdbc.Driver)这是 5.x 的类名。若强行替换为 8.x 驱动启动时抛ClassNotFoundException。3.3 Tomcat 部署与启动web.xml 是灵魂别跳过它将项目导入 Eclipse/IDEA在 Eclipse 中File → New → Dynamic Web Project项目名填wordweb取消勾选“Generate web.xml deployment descriptor”因为项目已有web.xmlTarget runtime选择你配置好的 Tomcat 8.5Dynamic web module version选择2.5复制源码与资源将下载包中的src/全部覆盖到 Eclipse 项目的src目录将WebContent/全部覆盖到 Eclipse 项目的WebContent目录注意WebContent/WEB-INF/lib/下的 jar 会自动加入 Build Path关键配置检查启动前必做打开WebContent/WEB-INF/web.xml确认servlet-class中的类名与src/servlet/下文件名一致如LoginServlet对应com.example.servlet.LoginServlet检查DBUtil.java中的数据库连接 URLprivate static final String URL jdbc:mysql://localhost:3306/worddb?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8;注意serverTimezoneGMT%2B8MySQL 8.0 默认要求时区否则连接时抛The server time zone value ... is unrecognized。GMT%2B8是 URL 编码后的GMT8。启动与访问右键项目 →Run As → Run on Server选择你的 Tomcat 8.5启动成功后浏览器访问http://localhost:8080/wordweb/首页显示“欢迎使用背单词系统”点击右上角“登录”进入login.jsp4. 常见问题排查这些坑我替你踩过了照着修就行部署过程中90% 的失败都集中在以下 5 类问题。我把每类的现象、根本原因、一行命令修复方案列出来避免你花 3 小时查一个拼写错误。4.1 现象访问http://localhost:8080/wordweb/显示 404但 Tomcat 管理页正常原因Eclipse 项目未正确识别为 Web 项目WebContent未被设为 Deployment Assembly 的 WebRoot。解决右键项目 →Properties → Deployment Assembly→ 点击Add → Folder → Next→ 选择WebContent→Finish→ 确认Deploy Path为/。4.2 现象登录时输入正确账号密码页面跳转回login.jsp控制台无报错原因LoginServlet.java中密码校验逻辑为if (!user.getPassword().equals(MD5Util.md5(password)))但数据库中密码是明文插入的db_init.sql中INSERT INTO user VALUES (1,admin, 123456);而MD5Util.md5(123456)结果是e10adc3949ba59abbe56e057f20f883e不匹配。解决用 MySQL 客户端执行UPDATE user SET password e10adc3949ba59abbe56e057f20f883e WHERE username admin;血泪经验项目文档没说明初始密码是 MD5 加密的必须自己算一遍。4.3 现象登录成功后wordlist.jsp显示“暂无单词”但数据库word表有 4500 条数据原因WordListServlet.java中查询语句写死了WHERE categorylianlanyouci_2019但db_init.sql中部分单词的category字段值为lianlanyouci少 2019导致COUNT(*)为 0。解决修改WordListServlet.java第 42 行// 原代码 String sql SELECT * FROM word WHERE category? LIMIT ?,?; // 改为兼容两种 category 值 String sql SELECT * FROM word WHERE category IN (lianlanyouci_2019,lianlanyouci) LIMIT ?,?;4.4 现象点击“播放”按钮无声音浏览器控制台报Failed to load resource: the server responded with a status of 404 ()原因word.sound_path字段存的是audio/abandon.mp3但实际 MP3 文件在WebContent/audio/下而PlaySoundServlet用getServletContext().getResourceAsStream(soundPath)读取时soundPath必须以/开头表示 WebRoot 根路径。解决修改PlaySoundServlet.java第 32 行// 原代码错误 InputStream is getServletContext().getResourceAsStream(word.getSoundPath()); // 改为加斜杠 String fullPath / word.getSoundPath(); // 拼成 /audio/abandon.mp3 InputStream is getServletContext().getResourceAsStream(fullPath);4.5 现象安卓打包后HBuilder 运行 APK登录页空白控制台报net::ERR_CLEARTEXT_NOT_PERMITTED原因Android 9.0 默认禁止明文 HTTP 请求http://localhost:8080/wordweb/login.do而 HBuilder 打包的manifest.json中h5plus配置未开启androidClearTextTraffic。解决打开manifest.json→ 找到android节点 → 添加android: { clearTextTraffic: true, permissions: [android.permission.INTERNET] }后悔药这个配置必须在打包前设置打包后修改无效需重新生成 APK。5. 核心功能代码精讲从登录到加单词本一行行带你读懂关键链路现在我们聚焦最核心的两个用户旅程登录认证和加入单词本。我会把LoginServlet.java和AddToBookServlet.java的关键代码逐段贴出并解释每一行在做什么、为什么这么写、参数从哪来。这不是 API 文档而是“开发现场笔记”。5.1 登录流程LoginServlet.java如何完成一次完整的会话建立public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数nameusername 和 namepassword 的 input 值 String username request.getParameter(username); // 从前端 login.jsp 的 input nameusername String password request.getParameter(password); // 2. 校验非空简单前端校验不够后端必须兜底 if (username null || username.trim().length() 0 || password null || password.trim().length() 0) { request.setAttribute(msg, 用户名或密码不能为空); request.getRequestDispatcher(login.jsp).forward(request, response); return; // 提前返回避免后续执行 } // 3. 调用 DAO 查询用户注意密码用 MD5 加密后比对 UserDao userDao new UserDao(); User user userDao.login(username, MD5Util.md5(password)); // MD5Util.md5() 返回 32 位小写字符串 // 4. 校验结果user 为 null 表示查无此人或密码错误 if (user null) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); return; } // 5. 登录成功将用户信息存入 session设置超时时间为 30 分钟 HttpSession session request.getSession(); session.setAttribute(user, user); // 存整个 User 对象供其他页面取 username session.setAttribute(uid, user.getId()); // 单独存 uid方便 addToBookServlet 直接取 session.setMaxInactiveInterval(30 * 60); // 30 分钟无操作自动失效 // 6. 重定向到单词列表页用 sendRedirect 避免重复提交 response.sendRedirect(request.getContextPath() /wordlist.jsp); } }关键点说明request.getParameter()是 Servlet 最基础的数据获取方式它对应 HTML 表单的name属性不是id。如果login.jsp中写input idusername而没写nameusername这里永远是null。MD5Util.md5(password)是项目自定义工具类源码只有 3 行MessageDigest md MessageDigest.getInstance(MD5); md.update(pwd.getBytes(UTF-8)); return new BigInteger(1, md.digest()).toString(16);。注意getBytes(UTF-8)若不指定编码Windows 系统默认 GBK会导致加密结果不一致。session.setAttribute(uid, user.getId())这行至关重要。后续所有需要用户身份的操作如加单词本、查个人单词本都靠这个uid。它不是从 Cookie 读的而是服务端生成并维护的。5.2 加入单词本AddToBookServlet.java如何保证数据一致性public class AddToBookServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从 URL 参数获取 wordid如 /addtobook.do?wordid123 String wordidStr request.getParameter(wordid); if (wordidStr null || wordidStr.trim().length() 0) { response.getWriter().write({\success\:false,\msg\:\缺少单词ID\}); return; } int wordid; try { wordid Integer.parseInt(wordidStr); // 强制转 int防 SQL 注入后面用 PreparedStatement } catch (NumberFormatException e) { response.getWriter().write({\success\:false,\msg\:\单词ID格式错误\}); return; } // 2. 从 session 获取当前登录用户的 uid HttpSession session request.getSession(false); // false 表示不创建新 session if (session null || session.getAttribute(uid) null) { response.getWriter().write({\success\:false,\msg\:\请先登录\}); return; } int uid (int) session.getAttribute(uid); // 3. 调用 DAO 执行插入关键用 PreparedStatement 防注入 WordDao wordDao new WordDao(); boolean success wordDao.addToBook(uid, wordid); // 4. 返回 JSON 响应前端 JS 用 jQuery.ajax 处理 if (success) { response.getWriter().write({\success\:true}); } else { response.getWriter().write({\success\:false,\msg\:\添加失败请重试\}); } } }WordDao.java中对应的addToBook方法public boolean addToBook(int uid, int wordid) { String sql INSERT INTO wordbook (userid, wordid) VALUES (?, ?); Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setInt(1, uid); // 第一个 ? 替换为 uid ps.setInt(2, wordid); // 第二个 ? 替换为 wordid int rows ps.executeUpdate(); // executeUpdate 用于 INSERT/UPDATE/DELETE return rows 0; // 影响行数大于 0 表示成功 } catch (SQLException e) { e.printStackTrace(); // 开发期打印上线应记日志 return false; } finally { DBUtil.close(ps, conn); // 必须关闭否则连接泄漏 } }参数说明与避坑ps.setInt(1, uid)中的1是参数序号不是数组下标从 1 开始计数。DBUtil.close(ps, conn)是自定义工具方法内部调用ps.close()和conn.close()并捕获SQLException。若忘记关闭Tomcat 连接池会耗尽后续所有数据库操作都卡死。前端wordlist.jsp中的“加入单词本”按钮是这样写的a hrefjavascript:void(0) onclickaddToBook(${word.id})加入单词本/a对应的addToBookJS 函数发起 AJAXfunction addToBook(wordid) { $.get(addtobook.do, {wordid: wordid}, function(data) { if (data.success) { alert(已加入单词本); } else { alert(data.msg || 操作失败); } }); }这里$.get发送的是 GET 请求所以AddToBookServlet必须重写doGet方法如上所示不能只写doPost。6. 二次开发实战如何快速扩展“复习计划”功能附完整代码与验证技巧现在你已经跑通了整个项目下一步往往是“我想加个功能”。最常见的需求是按艾宾浩斯遗忘曲线安排单词复习。这个功能不需要大改架构只需新增 3 个文件 修改 1 个 SQL就能让系统从“静态词库”升级为“智能复习引擎”。下面是我的实操路径从设计到验证一步不跳。6.1 功能设计用最小改动实现复习计划核心逻辑目标用户登录后首页显示“今日待复习单词”列表点击“标记已掌握”后系统自动计算下次复习时间1天后、2天后、4天后、7天后、15天后并更新数据库。关键约束不新增表复用现有wordbook表只增加 2 个字段next_review_date下次复习日期DATE 类型review_level复习等级TINYINT默认 00未开始11天后22天后...515天后为什么复用wordbook表因为每个“加入单词本”的记录天然对应一个“复习任务”。如果新建review_plan表就要多一次 JOIN 查询而这个项目追求的是“能跑、能懂、能改”不是高并发优化。6.2 数据库改造两行 SQL安全无损-- 1. 为 wordbook 表添加两个字段MySQL 5.6 支持在线 DDL不影响线上 ALTER TABLE wordbook ADD COLUMN next_review_date DATE DEFAULT NULL, ADD COLUMN review_level TINYINT DEFAULT 0; -- 2. 初始化将所有已加入单词本的记录next_review_date 设为今天review_level 设为 0 UPDATE wordbook SET next_review_date CURDATE(), review_level 0 WHERE next_review_date IS NULL;6.3 新增 Servlet 与 DAOReviewPlanServlet.java实现核心调度// src/servlet/ReviewPlanServlet.java public class ReviewPlanServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(false); if (session null || session.getAttribute(uid) null) { response.getWriter().write([]); // 返回空数组 return; } int uid (int) session.getAttribute(uid); // 查询今日待复习单词next_review_date 今天且 review_level 0 WordDao wordDao new WordDao(); ListWord todayWords wordDao.getTodayReviewWords(uid); // 转为 JSON 数组用 StringBuilder 手写不引入 Jackson StringBuilder json new StringBuilder([); for (int i 0; i todayWords.size(); i) { Word w todayWords.get(i); json.append({) .append(\id\:).append(w.getId()).append(,) .append(\english\:\).append(w.getEnglish()).append(\,) .append(\chinese\:\).append(w.getChinese().replace(\, \\\)).append(\) .append(}); if (i todayWords.size() - 1) json.append(,); } json.append(]); response.getWriter().write(json.toString()); } }对应WordDao.java新增方法// 在 WordDao.java 中添加 public ListWord getTodayReviewWords(int uid) { String sql SELECT w.* FROM word w JOIN wordbook wb ON w.id wb.wordid WHERE wb.userid ? AND wb.next_review_date CURDATE() ORDER BY wb.next_review_date ASC LIMIT 10; // 限制最多查 10 个防卡顿 ListWord words new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setInt(1, uid); rs ps.executeQuery(); while (rs.next()) { Word word new Word(); word.setId(rs.getInt(id)); word.setEnglish(rs.getString(english)); word.setChinese(rs.getString(chinese)); words.add(word); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return words; }6.4 前端集成index.jsp改造30 行 JS 完成动态渲染在WebContent/index.jsp的body末尾添加div idreviewSection styledisplay:none; h3 今日待复习span idreviewCount0/span 个/h3 ul idreviewList/ul /div script function loadTodayReview() { $.get(reviewplan.do, function(data) { const words JSON.parse(data); const list $(#reviewList); list.empty(); words.forEach(word { list.append(li${word.english} —— ${word.chinese}/li); }); $(#reviewCount).text(words.length); if (words.length 0) { $(#reviewSection).show(); } }); } // 页面加载完成后自动拉取 $(document).ready(loadTodayReview); /script6.5 验证与调试技巧如何确保复习逻辑真正生效手动触发复习在 MySQL 中执行UPDATE wordbook SET next_review_date 2023-01-01 WHERE userid 1 AND wordid 123;然后刷新index.jsp确认单词 123 出现在“今日待复习”列表。验证时间计算在AddToBookServlet.java的addToBook成功后追加逻辑// 在 wordDao.addToBook(uid, wordid) 返回 true 后 wordDao.updateNextReviewDate(uid, wordid, 1); // 1 表示 level 1即 1 天后对应 DAO 方法public void updateNextReviewDate(int uid, int wordid, int level) { String sql UPDATE wordbook SET next_review_date DATE_ADD(CURDATE(), INTERVAL ? DAY), review_level ? WHERE userid ? AND wordid ?; // ... 执行 PreparedStatement参数level, level, uid, wordid }终极验证部署到 Tomcat用手机访问http://你的IP:8080/wordweb/确认“今日待复习”区块正确显示且点击“加入单词本”后该单词立即出现在列表中——这证明整个链路前端 JS → Servlet → DAO → SQL全部打通。从那以后我每次给学生讲 Java Web 项目都会先让他们把这个单词网站跑起来再一起动手加一个“复习计划”。因为只有当代码在浏览器里真实动起来那些request.getParameter()、session.setAttribute()、PreparedStatement才不再是课本上的铅字而是你手指敲出来的、能解决问题的工具。希望帮到你。本文还有配套的精品资源点击获取