简介:这是一套基于JavaWeb技术栈开发的旅游管理系统完整源码,面向Java初学者与Web开发入门者,聚焦Servlet+JSP+MySQL三层架构实践,帮助学习者掌握用户注册审核、景点信息展示、酒店在线预订及后台多模块管理等典型业务场景。资源包共738个文件,含111个JSP页面(实现前后端交互)、415个GIF动图(用于界面效果展示)、69个JPG图片(景点与酒店素材)、40个CSS样式文件(统一前端视觉)、23个JS脚本(增强交互逻辑),以及SQL数据库脚本、Java类文件和Eclipse项目配置文件等,整体压缩后仅8.46MB,轻量易部署。已有1111人下载学习,资源结构清晰,包含完整MVC分层目录、可运行的MySQL数据库(含初始化数据)、带审核机制的用户权限体系,以及后台对塞北人文、景点、自驾游路线、留言板等特色模块的CRUD管理功能,是理解JavaWeb企业级项目流程的理想练手案例。
1. 为什么一个“JavaWeb旅游管理系统(JSP+Servlet+MySQL)”至今仍是校企衔接最稳的练手项目?
不是因为它多前沿——它用的是2003年就定型的JSP规范、2005年成熟的Servlet 2.5 API、以及MySQL 5.7这种连官方都已停止主流支持的版本;而是因为它像一把解剖刀,把Web开发最底层的三根主干:请求生命周期如何被Servlet接管、HTML如何被JSP动态拼装、关系数据如何通过JDBC落地到MySQL表结构里,全摊开在你眼皮底下。没有Spring Boot自动装配的黑匣子,没有MyBatis的XML映射抽象层,没有Vue的响应式劫持——你改一行request.getParameter("username"),就能立刻看到登录页提交后用户名是否真的传进来了;你删掉web.xml里一个<servlet-mapping>,整个功能路由就404;你把user表里phone字段设为NOT NULL但代码里没校验空值,插入时就会在控制台炸出SQLException: Column 'phone' cannot be null。它不教你怎么写高并发秒杀,但它逼你亲手把HTTP请求从TCP三次握手开始,一帧一帧拆解到ResultSet遍历结束。适合刚学完Java基础、还没碰过框架的本科生,也适合想回炉重造、看清MVC骨架的老手——因为所有“玄学报错”,最后都能归因到三个文件:.jsp、.java、.sql。
2. 搭建最小可运行环境:用IDEA跑通第一个Servlet,绕开90%新手卡点
2.1 选版本组合:为什么坚持用Tomcat 8.5 + MySQL 5.7 + JDK 8?
这不是守旧,是踩坑后的血泪经验。
- Tomcat 9+ 默认启用HTTP/2,而JSP 2.3规范对EL表达式解析有细微差异,会导致
<c:if test="${empty user}">这类写法在某些页面突然失效; - MySQL 8.0默认开启
caching_sha2_password认证插件,而老版mysql-connector-java 5.1.47(JSP项目标配驱动)根本不认识它,连接时直接抛Access denied for user——你查日志、改密码策略、重装驱动,折腾半天才发现是版本错配; - JDK 11+移除了
javax.xml.bind等模块,而部分JSP标签库(如JSTL 1.2)依赖它,编译期就报NoClassDefFoundError。
所以,稳定压倒一切:用apache-tomcat-8.5.99(实际取8.5.x最新稳定版)、mysql-5.7.44(官网归档版)、jdk-8u202(非最新但兼容性最强)。这三个版本在CSDN、掘金、B站实测视频中出现频率最高,意味着你搜报错时,90%的答案能直接复用。
2.2 IDEA配置Tomcat:三步完成部署,拒绝“Artifact is not configured”
提示:不要用IDEA自带的“Add Framework Support”向导——它会自动生成冗余的
web.xml和错误的输出路径,导致/WEB-INF/classes下找不到编译后的Servlet类。
手动配置才是正解:
- 下载并解压Tomcat:将
apache-tomcat-8.5.99.zip解压到无中文、无空格路径,例如D:\tomcat85; - 在IDEA中添加Tomcat Server:
File → Settings → Build, Execution, Deployment → Application Servers,点击+,选Tomcat Server,指向D:\tomcat85;Run → Edit Configurations → + → Tomcat Server → Local,在Deployment选项卡中点击+→Artifact→ 选中你的项目名(如travel-system:war exploded);
- 关键一步:修正Artifact输出路径:
File → Project Structure → Artifacts,选中你的war exploded项;- 在
Output Layout右侧,确认WEB-INF/classes目录下已包含所有*.class文件(即src/main/java编译结果); - 若为空,点击
+ → Directory Content → src/main/java,再点OK; - 最后点
Apply,重启IDEA。
此时运行http://localhost:8080/应看到Tomcat默认首页;访问http://localhost:8080/hello(假设你写了HelloServlet)则返回"Hello from Servlet!"——这才是真正的最小闭环。
2.3 写第一个Servlet:不用注解,手写web.xml,理解容器初始化逻辑
很多教程一上来就用@WebServlet("/hello"),但这样你会错过Servlet生命周期的关键钩子。先写XML方式,再迁移到注解:
<!-- web/WEB-INF/web.xml --> <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> <!-- 容器启动时加载 --> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>对应Java类:
// src/main/java/com/example/HelloServlet.java package com.example; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { @Override public void init() throws ServletException { System.out.println("【Servlet初始化】HelloServlet已加载"); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.println("<h1>Hello from Servlet!</h1>"); out.println("<p>当前时间:" + new java.util.Date() + "</p>"); out.close(); } @Override public void destroy() { System.out.println("【Servlet销毁】HelloServlet被卸载"); } }参数说明:
<load-on-startup>1</load-on-startup>:数字越小优先级越高,确保应用启动时就实例化,避免首次访问延迟;resp.setContentType("text/html;charset=UTF-8"):必须显式设置,否则中文会乱码(JSP页面同理);out.close():虽非强制,但显式关闭流能避免缓冲区残留,尤其在调试阶段更易定位输出问题。
3. 数据库建模与JDBC直连:用纯SQL定义旅游系统核心表,手写DAO不靠框架
3.1 旅游系统四张核心表设计:为什么order_item必须拆分,而user要加is_deleted软删除?
旅游管理系统不是博客或论坛,业务实体间存在强关联与状态流转。我们不照搬电商的复杂模型,只抓最痛的四个场景:用户注册登录、景点信息管理、订单生成、订单明细。对应的表结构必须满足:
- 用户能修改手机号但历史订单归属不变 →
user表需id主键+phone唯一索引; - 同一订单含多个景点门票 → 必须用
order_item中间表,而非在order表加item_ids逗号字符串(违反第一范式,无法索引查询); - 景点价格可能调整,但历史订单金额不能变 →
order_item必须冗余存储当时的price,而非只存spot_id; - 管理员误删景点需可恢复 →
spot表加is_deleted TINYINT(1) DEFAULT 0,查询时加WHERE is_deleted=0。
建表SQL(MySQL 5.7语法):
-- 用户表 CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL, -- 存BCrypt加密后字符串 `phone` VARCHAR(20) NOT NULL UNIQUE, `email` VARCHAR(100), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `is_deleted` TINYINT(1) DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 景点表 CREATE TABLE `spot` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `description` TEXT, `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `stock` INT NOT NULL DEFAULT 0, `is_deleted` TINYINT(1) DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 CREATE TABLE `order_master` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0待支付,1已支付,2已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME NULL, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 CREATE TABLE `order_item` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `order_id` BIGINT NOT NULL, `spot_id` BIGINT NOT NULL, `quantity` INT NOT NULL DEFAULT 1, `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 下单时快照价格 `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`order_id`) REFERENCES `order_master`(`id`), FOREIGN KEY (`spot_id`) REFERENCES `spot`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关键设计理由:
VARCHAR(100)而非TEXT存景点名:索引效率更高,且名称长度有限;DECIMAL(10,2)存价格:避免浮点数精度丢失(float在金额计算中会出0.01元误差);TINYINT(1)代替BOOLEAN:MySQL 5.7不原生支持布尔类型,TINYINT语义清晰且占空间最小;- 所有
DATETIME字段加DEFAULT CURRENT_TIMESTAMP:省去Java端手动赋值,数据库自动维护。
3.2 手写JDBC工具类:为什么DBUtil必须带连接池,而close()要判空?
直接new Connection()每次创建新连接,100并发就耗尽MySQL连接数(默认151)。必须用连接池,但又不能引入HikariCP等第三方——这是练手项目,要自己看懂池怎么工作。我们用最简commons-dbcp(Apache老牌轻量池),配合手动回收:
// src/main/java/com/example/util/DBUtil.java package com.example.util; import org.apache.commons.dbcp.BasicDataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static BasicDataSource dataSource; static { dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // 注意:MySQL 5.7用此驱动类 dataSource.setUrl("jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=utf8"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大活跃连接数 dataSource.setMaxIdle(10); // 最大空闲连接数 dataSource.setMinIdle(5); // 最小空闲连接数 dataSource.setRemoveAbandonedOnBorrow(true); dataSource.setRemoveAbandonedOnMaintenance(true); dataSource.setRemoveAbandonedTimeout(60); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn) { if (conn != null && !conn.isClosed()) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); // 生产环境应记日志,此处仅调试 } } } }参数说明:
setRemoveAbandonedOnBorrow(true):当借连接超时(removeAbandonedTimeout=60秒),自动回收被遗忘的连接,防止泄漏;setMinIdle(5):始终保持5个空闲连接,避免突发请求时创建连接的延迟;com.mysql.jdbc.Driver:MySQL 5.7必须用此驱动类名;MySQL 8.0才用com.mysql.cj.jdbc.Driver,混用必报ClassNotFoundException。
3.3 手写UserDAO:演示PreparedStatement防SQL注入,为何executeQuery()后必须rs.next()?
DAO层不追求复用,只求每行代码可知可控。以用户登录为例:
// src/main/java/com/example/dao/UserDAO.java package com.example.dao; import com.example.entity.User; import com.example.util.DBUtil; import java.sql.*; public class UserDAO { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, phone, email FROM user WHERE username = ? AND password = ? AND is_deleted = 0"; User user = null; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setString(1, username); // 防注入:参数化查询,不拼接字符串 ps.setString(2, password); rs = ps.executeQuery(); if (rs.next()) { // 关键!必须调用next()才能读取第一行,否则getXXX()报SQLException user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPhone(rs.getString("phone")); user.setEmail(rs.getString("email")); } } catch (SQLException e) { e.printStackTrace(); } finally { // 顺序不能错:rs → ps → conn if (rs != null) try { rs.close(); } catch (SQLException e) { } if (ps != null) try { ps.close(); } catch (SQLException e) { } if (conn != null) try { conn.close(); } catch (SQLException e) { } } return user; } }避坑点:
rs.next()必须放在if里判断,因为executeQuery()返回的是游标,未移动到首行前所有getXXX()都无效;finally中关闭资源顺序:ResultSet→PreparedStatement→Connection,反序可能导致Connection closed异常;ps.setString(1, username)中的1是参数位置,不是列索引,写错位置号(如ps.setString(2, username))会导致Parameter index out of range。
4. JSP页面开发:用原生JSTL+EL渲染景点列表,解决图片路径、中文乱码、刷新陷阱
4.1 JSP基础配置:为什么pageEncoding和contentType必须同时设,且值要一致?
很多新手只写<%@ page contentType="text/html;charset=UTF-8" %>,结果表单提交中文还是乱码。根本原因是:
contentType告诉浏览器用UTF-8解码HTML;pageEncoding告诉JSP引擎用UTF-8读取.jsp文件本身;- 若
.jsp文件保存为GBK(Windows记事本默认),而pageEncoding="UTF-8",引擎读取时就乱码,后续所有输出都错。
正确写法(每页顶部第一行):
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>旅游景点列表</title> </head> <body> <h2>热门景点</h2> <c:forEach items="${spotList}" var="spot"> <div> <h3>${spot.name}</h3> <p>价格:¥${spot.price} | 库存:${spot.stock}</p> <a href="spot_detail.jsp?id=${spot.id}">查看详情</a> </div> </c:forEach> </body> </html>验证方法:右键浏览器页面 → “查看页面源代码”,确认<meta charset="UTF-8">存在且生效;用curl -I http://localhost:8080/spot_list.jsp看响应头Content-Type: text/html;charset=UTF-8。
4.2 JSTL遍历与EL表达式:${spot.name}为何有时输出null,而<c:out value="${spot.name}"/>更安全?
EL表达式${spot.name}在spot为null时直接输出空字符串,但若spot对象本身为null,${spot.name}会抛NullPointerException导致页面500。而<c:out>默认做null安全处理:
<%-- 危险写法:spot可能为null --%> <p>景点名:${spot.name}</p> <%-- 推荐写法:自动转义HTML字符,且null时输出空字符串 --%> <p>景点名:<c:out value="${spot.name}" default="未知景点"/></p>参数说明:
default="未知景点":当value为null或空字符串时显示的备选文本;escapeXml="true"(默认):自动将<script>转成<script>,防XSS;若需输出原始HTML,设escapeXml="false",但必须确保内容可信。
4.3 图片路径与坐标定位:为什么<img src="images/spot1.jpg">总404,而CSS定位要用绝对路径?
JSP中src路径是相对于当前JSP文件的URL路径,不是相对于项目根目录。假设:
- JSP文件在
/web/spot_list.jsp; - 图片在
/web/images/spot1.jpg; - 则
<img src="images/spot1.jpg">正确(相对路径); - 但若JSP在
/web/admin/spot_manage.jsp,同样写src="images/spot1.jpg"就变成请求/admin/images/spot1.jpg,404。
解决方案:统一用应用上下文路径(Context Path):
<%-- 正确:无论JSP在哪层目录,都指向/web/images/ --%> <img src="${pageContext.request.contextPath}/images/spot1.jpg" alt="景点图"> <%-- CSS中定位图片坐标,必须用绝对路径(以/开头) --%> <style> .spot-img { background-image: url('<%=request.getContextPath()%>/images/spot_bg.jpg'); background-position: 10px 20px; /* X=10px, Y=20px */ } </style>request.getContextPath()返回应用部署路径(如/travel-system),拼接后生成/travel-system/images/spot1.jpg,浏览器能准确定位。
5. 避坑指南:JSP+Servlet+MySQL组合里最常翻车的5个现场,附诊断命令
5.1 现象:启动Tomcat后访问/login.jsp显示404,但/helloServlet能正常访问
原因:JSP文件未放在web目录下,或IDEA未将web设为Web资源根目录。
诊断:
- 查
Project Structure → Modules → [你的模块] → Web,确认Web resource directory指向src/main/webapp(或你实际的web目录); - 查
Artifacts → [你的war exploded] → Output Layout,确认web文件夹下有login.jsp; - 在Tomcat日志
logs/catalina.out中搜索deploying web application,看是否打印Deploying web application archive [...]及路径。
解决:右键web文件夹 →Mark Directory as → Resources Root(若用Maven标准结构,应为src/main/webapp)。
5.2 现象:登录成功后跳转/index.jsp,页面显示HTTP Status 404 – /index.jsp
原因:response.sendRedirect("/index.jsp")中的/是相对于服务器根路径(即http://localhost:8080/),而非应用上下文路径。
诊断:浏览器开发者工具Network标签,看跳转请求URL是http://localhost:8080/index.jsp(错)还是http://localhost:8080/travel-system/index.jsp(对)。
解决:
// 错误:绝对路径跳转 response.sendRedirect("/index.jsp"); // 正确:相对当前应用路径跳转 response.sendRedirect(request.getContextPath() + "/index.jsp"); // 或用RequestDispatcher(服务端转发,URL不变) request.getRequestDispatcher("/index.jsp").forward(request, response);5.3 现象:MySQL连接报错Communications link failure,但mysql -u root -p本地能登录
原因:MySQL未授权远程访问,或防火墙拦截3306端口。
诊断:
- 在服务器执行
netstat -an | findstr :3306(Windows)或ss -tuln | grep 3306(Linux),确认MySQL监听0.0.0.0:3306而非127.0.0.1:3306; - 登录MySQL执行:
SELECT host,user FROM mysql.user WHERE user='root';,若host为localhost,则只允许本地连接。
解决:
-- 授权root从任意IP连接(仅开发环境) GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456'; FLUSH PRIVILEGES;注意:生产环境严禁用
'root'@'%',应创建专用账号并限制IP段。
5.4 现象:JSP页面中文显示为??,但数据库里存的是正确汉字
原因:JDBC URL缺少useUnicode=true&characterEncoding=utf8参数,或MySQL服务端字符集非utf8mb4。
诊断:
- 连接MySQL执行
SHOW VARIABLES LIKE 'character_set%';,确认character_set_server和collation_server为utf8mb4; - 检查
DBUtil.java中URL是否含?useUnicode=true&characterEncoding=utf8。
解决: - 修改MySQL配置文件
my.ini(Windows)或my.cnf(Linux):[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci [client] default-character-set=utf8mb4 - 重启MySQL,重建数据库:
CREATE DATABASE travel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
5.5 现象:<c:forEach>遍历spotList为空,但Servlet中System.out.println(spotList.size())输出5
原因:Servlet未将List存入request.setAttribute("spotList", spotList),或JSP中EL表达式写错变量名。
诊断:
- 在Servlet中
request.setAttribute("spotList", spotList);后加System.out.println("Set attribute: " + spotList.size());; - 在JSP中加
<%= request.getAttribute("spotList") %>,看是否输出[com.example.entity.Spot@xxx, ...]。
解决: - 确保
request.setAttribute()在request.getRequestDispatcher().forward()之前调用; - EL变量名严格区分大小写:
spotList≠spotlist; - 若用
<c:forEach items="${spotList}",确保spotList是request作用域,而非session或application。
6. 进阶技巧:用Filter实现统一登录校验,以及JSP页面加载后自动刷新一次的务实方案
6.1 用Filter拦截未登录请求:比每个Servlet里写if(user==null)更干净
把登录校验从每个Servlet里抽出来,是迈向工程化的第一步。Filter不侵入业务逻辑,且能统一处理编码、日志、权限。
// src/main/java/com/example/filter/LoginFilter.java package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { // 不需要登录即可访问的路径 private static final String[] EXCLUDE_PATHS = {"/login.jsp", "/login", "/register.jsp", "/register"}; @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getServletPath(); // 检查是否为免登录路径 boolean exclude = false; for (String p : EXCLUDE_PATHS) { if (path.equals(p) || path.startsWith(p + "/")) { exclude = true; break; } } if (exclude) { chain.doFilter(req, resp); return; } // 检查Session中是否有user HttpSession session = request.getSession(false); Object user = (session != null) ? session.getAttribute("user") : null; if (user == null) { // 未登录,重定向到登录页,并保留原请求URL用于登录后跳转 String originalUrl = request.getRequestURL().toString(); if (request.getQueryString() != null) { originalUrl += "?" + request.getQueryString(); } session.setAttribute("originalUrl", originalUrl); response.sendRedirect(request.getContextPath() + "/login.jsp"); } else { chain.doFilter(req, resp); } } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }web.xml注册Filter(Servlet 3.0+可用注解,但XML更直观):
<filter> <filter-name>LoginFilter</filter-name> <filter-class>com.example.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 拦截所有请求 --> </filter-mapping>优势:
- 新增页面无需改Servlet代码,只要不在
EXCLUDE_PATHS里,自动受保护; originalUrl保存原请求地址,登录成功后可跳回(如从/order_list.jsp跳登录,登录后回/order_list.jsp);getSession(false)避免无谓创建Session,提升性能。
6.2 JSP页面加载完后刷新一次:解决缓存导致的数据陈旧问题(非F5硬刷)
有些场景需要页面首次加载后立即拉取最新数据,比如订单列表页刚支付完,跳转回来时需刷新状态。用JavaScript控制比服务端重定向更轻量:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% // 标记是否已刷新过,存于request属性 Boolean refreshed = (Boolean) request.getAttribute("refreshed"); if (refreshed == null || !refreshed) { // 第一次加载,设标记并输出JS刷新 request.setAttribute("refreshed", true); %> <script> // 页面加载完成后100ms刷新(避免DOM未就绪) window.addEventListener('load', function() { setTimeout(function() { location.reload(); }, 100); }); </script> <% } %> <h2>我的订单</h2> <!-- 此处渲染订单列表 -->为什么不用<meta http-equiv="refresh" content="0">?
- 该标签在HTML解析前就触发,可能导致页面白屏闪动;
location.reload()在load事件后执行,确保DOM、CSS、JS全部加载完毕,用户体验更平滑;setTimeout加100ms缓冲,避免极少数情况下load事件触发过早。
6.3 一个真实血泪经验:MySQL日期查询用DATE()函数,别信BETWEEN
旅游系统常要查“今天下单的订单”。新手常写:
-- ❌ 错误:BETWEEN '2024-05-20' AND '2024-05-20' 只匹配create_time='2024-05-20 00:00:00' SELECT * FROM order_master WHERE create_time BETWEEN '2024-05-20' AND '2024-05-20'; -- ✅ 正确:用DATE()提取日期部分 SELECT * FROM order_master WHERE DATE(create_time) = '2024-05-20';原理:BETWEEN a AND b等价于>= a AND <= b,而'2024-05-20'被MySQL隐式转为'2024-05-20 00:00:00',所以'2024-05-20 14:30:00'不满足<= '2024-05-20 00:00:00'。DATE(create_time)则剥离时间部分,只比日期。
我在某次课程设计答辩时,导师故意用下午3点下单测试,当场暴露这个Bug。从此养成了习惯:所有日期范围查询,先EXPLAIN看是否走索引——DATE(create_time)在MySQL 5.7中仍能利用create_time索引(前提是索引存在),而create_time >= '2024-05-20' AND create_time < '2024-05-21'是更优写法(推荐)。
希望帮到你。
本文还有配套的精品资源,点击获取