简介:这是一套基于JavaWeb技术栈开发的旅游管理系统完整源码,面向Java初学者与Web开发入门者,帮助掌握JSP+Servlet+MySQL三层架构的实战应用能力。系统覆盖旅游网站核心业务场景,包含主界面展示、用户注册审核、景点信息检索、酒店在线预订及后台多模块管理(含账号、人文内容、自驾路线、留言板等),代码结构清晰,具备典型MVC分层特征。资源包共738个文件,以111个JSP页面实现前后端交互、415个GIF/69个JPG用于界面素材、40个CSS和23个JS支撑前端样式与交互,辅以SQL数据库脚本、Java类及配置文件,整体压缩后仅8.46MB,轻量易部署。目前已有1111人学习下载,读者可直接运行调试,深入理解用户权限控制、表单提交处理、数据库增删改查及后台管理逻辑实现细节。
1. JavaWeb旅游管理系统(JSP+Servlet+MySQL):一个能跑通、能改、能交作业的真实闭环项目
你不是在学“JavaWeb”,你是在搭一座桥——从写完System.out.println("Hello World"),到真能把用户注册、登录、查线路、下订单、管理员后台全链路串起来的桥。这个标题里的“JavaWeb旅游管理系统(JSP+Servlet+MySQL)”,不是教科书里的抽象概念,而是一套可本地启动、带完整CRUD、含前后端分离雏形、数据库表结构清晰、连Tomcat部署细节都踩过坑的最小可行系统。它解决的是新手最痛的三个断层:写完Servlet不知道怎么连MySQL、JSP页面总报404、明明写了DAO却查不到数据。适合刚啃完《Head First Servlet & JSP》第5章、IDEA里还卡在“Artifact is not configured”报错、想用一个真实业务场景把零散知识点焊死的同学。它不追求Spring Boot自动装配的优雅,而是用最原始的request.getParameter()、ResultSet.next()和手写SQL,逼你看见HTTP请求怎么一层层穿过Filter、Servlet、DAO,最后落进MySQL的InnoDB引擎里——这才是理解JavaWeb底层逻辑的唯一捷径。
2. 搭建运行环境:用IDEA+Tomcat+MySQL 5.7构建可调试的本地开发闭环
2.1 IDEA中配置Tomcat Server:避开“Artifact is not configured”和“404”的第一道坎
很多同学卡在第一步:新建Dynamic Web Project后,右键Run As → Run on Server,弹出“Artifact is not configured”。这不是IDEA抽风,而是它没找到你项目的部署包(WAR包)生成规则。关键不是点“Add Configuration”,而是先让IDEA认出这是个Web模块:
# 在IDEA中操作路径: File → Project Structure → Modules → 选中你的模块 → Dependencies → 点"+" → Library → → From Maven → 输入:javax.servlet:javax.servlet-api:4.0.1 → OK提示:必须选
providedscope,否则打包时会把servlet-api打进WAR,导致Tomcat 9+启动报java.lang.ClassCastException: javax.servlet.http.HttpServletRequest cannot be cast to ...——这是血泪经验,因为Tomcat自带servlet-api,重复引入会类加载冲突。
接着配置Artifacts:
Project Structure → Artifacts → + → Web Application: Archive → 给它起名如 tourism.war- 在左侧
Output Layout中,确保WEB-INF/classes指向你的src编译输出目录(通常是out/production/your-project-name) WEB-INF/lib要包含所有jar依赖(如mysql-connector-java-5.1.47.jar),但不要手动拖jar,用Dependencies自动导入- 最后在
Run Configurations → Tomcat Server → Deployment → + → Artifact → 选tourism.war:exploded
这样配置后,点击绿色三角形启动,浏览器访问http://localhost:8080/tourism/,就能看到首页——不是404,也不是500,是真正能走通的起点。
2.2 MySQL 5.7安装与字符集固化:为什么你的中文存进去变成问号?
网络热词里高频出现“mysql安装教程”“mysql ssl连接错误”,但对本项目而言,最致命的其实是默认字符集。MySQL 5.7默认character_set_server = latin1,而JSP页面用UTF-8,Servlet读取参数时若不显式设置,中文必然乱码。别信网上“改my.cnf就完事”的玄学方案,必须四步闭环:
安装时指定编码(Windows MSI安装器勾选
Use UTF8 as default charset;Linux用rpm安装后立即执行):# 编辑 /etc/my.cnf(CentOS)或 C:\ProgramData\MySQL\MySQL Server 5.7\my.ini(Win) [client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' skip-character-set-client-handshake = TRUE创建数据库时强制指定:
CREATE DATABASE tourism_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC连接URL加参数(
src/jdbc.properties):jdbc.url=jdbc:mysql://localhost:3306/tourism_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false注意:
useSSL=false不是妥协,而是MySQL 5.7.22+默认要求SSL,本地开发无证书时必须关掉;serverTimezone=GMT%2B8解决The server time zone value 'XXX' is unrecognized错误——这是搜索量最高的报错之一。验证是否生效:
SHOW VARIABLES LIKE 'character%'; -- 必须全部返回utf8mb4,尤其character_set_database和character_set_server
漏掉任意一步,你都会在用户注册时发现“张三”存成“???”,然后花3小时翻JSP页面meta、Servlet setCharacterEncoding、MySQL客户端编码……最后发现是init_connect没写。
2.3 创建旅游系统核心表结构:用最小字段支撑完整业务流
本系统不堆砌10张表,只建5张真正驱动业务的表,每张表字段都对应前端页面控件。拒绝“用户表user(id,name,age,sex)”这种教学式设计,直接按真实需求建:
| 表名 | 字段说明 | 关键约束 | 业务映射 |
|---|---|---|---|
t_user | id(PK),username(UNIQUE),password(VARCHAR(64)),realname,phone,email,role(ENUM('admin','user')),status(TINYINT) | password用BCrypt加密存储,role控制后台入口 | 登录校验、权限分流、个人信息页展示 |
t_route | id,rname,price,routeDesc,rimage(VARCHAR存路径),source(VARCHAR),is_hot(TINYINT),cid(FK) | rimage存相对路径如/images/route1.jpg,方便JSP<img src="${route.rimage}"> | 首页轮播图、线路列表页、详情页图片定位 |
t_category | id,cname,description | 分类表,如“国内游”“出境游”“周边游” | 左侧导航栏分类筛选 |
t_order | id,uid(FK),rid(FK),orderTime,totalPrice,status(ENUM('wait','paid','closed')) | 复合索引(uid, status)加速用户订单查询 | 我的订单页、订单状态流转 |
t_order_item | id,oid(FK),rid(FK),count,subtotal | 联合主键(oid, rid)防重复下单 | 订单明细、价格计算 |
注意:
t_route.rimage字段存的是JSP能直接引用的路径,不是绝对磁盘路径。这意味着你必须在IDEA的web目录下建images文件夹,把图片放进去——否则JSP页面<img>标签永远404。这是“jsp图片如何对坐标定位”问题的根源:不是CSS定位,而是路径没配对。
3. 核心功能实现:从JSP页面到Servlet再到MySQL的三层穿透式编码
3.1 用户登录流程:用Filter拦截未登录请求,用Session存用户态
登录不是写个<form action="loginServlet">就完事。真正的难点在于:如何让“用户没登录时访问/order.jsp自动跳回login.jsp”这件事不靠JS硬跳,而是由服务端强制拦截?答案是自定义Filter:
// src/filter/LoginFilter.java public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 放行静态资源和登录相关路径 String uri = request.getRequestURI(); if (uri.contains("/login.jsp") || uri.contains("/LoginServlet") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/images/")) { chain.doFilter(req, resp); return; } // 检查Session中是否有user对象 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }在web.xml中注册:
<filter> <filter-name>LoginFilter</filter-name> <filter-class>filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>逻辑说明:Filter在请求到达Servlet前执行,
getSession(false)不创建新Session,避免无效Session堆积;request.getContextPath()确保重定向路径带项目名(如/tourism/login.jsp),这是IDEA中部署为ROOT上下文外的必备写法。
登录Servlet的核心代码:
// src/servlet/LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 查询用户(调用DAO) User user = new UserDao().findByUsername(username); if (user != null && BCrypt.checkpw(password, user.getPassword())) { // 2. 密码匹配,存入Session request.getSession().setAttribute("user", user); // 3. 根据角色跳转 if ("admin".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/admin/index.jsp"); } else { response.sendRedirect(request.getContextPath() + "/index.jsp"); } } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }参数说明:
BCrypt.checkpw()比明文比对安全,request.getRequestDispatcher().forward()保持URL不变,适合表单校验失败场景;response.sendRedirect()改变URL,用于成功跳转——这是区分“转发”和“重定向”的实战分水岭。
3.2 线路列表页:JSP EL表达式+JSTL遍历+Servlet分页查询
首页index.jsp要展示热门线路,但不能一次查1000条。必须做分页,且分页参数从URL来(如?currentPage=2&pageSize=8):
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>旅游线路</title></head> <body> <div class="route-list"> <c:forEach items="${page.list}" var="route"> <div class="route-item"> <img src="${page.contextPath}${route.rimage}" alt="${route.rname}" style="width:200px;height:150px;object-fit:cover;"> <h3>${route.rname}</h3> <p>价格:<span class="price">¥${route.price}</span></p> <a href="${page.contextPath}/routeDetail.jsp?id=${route.id}">查看详情</a> </div> </c:forEach> </div> <!-- 分页栏 --> <div class="pagination"> <c:if test="${page.currentPage > 1}"> <a href="${page.contextPath}/IndexServlet?currentPage=${page.currentPage-1}&pageSize=${page.pageSize}">上一页</a> </c:if> <span>第${page.currentPage}页,共${page.totalPage}页</span> <c:if test="${page.currentPage < page.totalPage}"> <a href="${page.contextPath}/IndexServlet?currentPage=${page.currentPage+1}&pageSize=${page.pageSize}">下一页</a> </c:if> </div> </body> </html>对应的IndexServlet:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int currentPage = Integer.parseInt(request.getParameter("currentPage")); int pageSize = Integer.parseInt(request.getParameter("pageSize")); PageBean<Route> page = new RouteService().findPage(currentPage, pageSize); page.setContextPath(request.getContextPath()); // 供JSP中拼接路径 request.setAttribute("page", page); request.getRequestDispatcher("/index.jsp").forward(request, response); }关键点:
page.setContextPath()把request.getContextPath()传给JSP,避免硬编码/tourism/;<img src="${page.contextPath}${route.rimage}">确保图片路径正确——这直接解决“jsp图片如何对坐标定位”的本质问题:路径拼接。
3.3 订单生成:事务控制+库存扣减+多表插入的原子性保障
用户点击“立即预订”时,要同时完成三件事:插入订单主表、插入订单明细、扣减线路库存。任何一步失败,全部回滚。不用Spring的@Transactional,纯JDBC手动事务:
// src/service/OrderService.java public boolean createOrder(Order order, List<OrderItem> items) { Connection conn = null; PreparedStatement pstmt = null; try { conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入订单主表 String sql1 = "INSERT INTO t_order(uid,rid,orderTime,totalPrice,status) VALUES(?,?,?,?,?)"; pstmt = conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS); pstmt.setInt(1, order.getUid()); pstmt.setInt(2, items.get(0).getRid()); // 取第一个线路ID pstmt.setString(3, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); pstmt.setDouble(4, order.getTotalPrice()); pstmt.setString(5, "wait"); pstmt.executeUpdate(); // 2. 获取生成的订单ID ResultSet rs = pstmt.getGeneratedKeys(); rs.next(); int orderId = rs.getInt(1); // 3. 批量插入订单明细 String sql2 = "INSERT INTO t_order_item(oid,rid,count,subtotal) VALUES(?,?,?,?)"; pstmt = conn.prepareStatement(sql2); for (OrderItem item : items) { pstmt.setInt(1, orderId); pstmt.setInt(2, item.getRid()); pstmt.setInt(3, item.getCount()); pstmt.setDouble(4, item.getSubtotal()); pstmt.addBatch(); } pstmt.executeBatch(); // 4. 扣减库存(假设t_route有stock字段) String sql3 = "UPDATE t_route SET stock = stock - ? WHERE id = ?"; pstmt = conn.prepareStatement(sql3); for (OrderItem item : items) { pstmt.setInt(1, item.getCount()); pstmt.setInt(2, item.getRid()); pstmt.addBatch(); } pstmt.executeBatch(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); // 任一环节失败,回滚 } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { JDBCUtils.close(null, pstmt, conn); } }注意:
conn.setAutoCommit(false)是事务起点;pstmt.addBatch()提升批量插入性能;JDBCUtils.close()必须放在finally块,防止连接泄漏——这是MySQL连接数爆满的常见原因。
4. 避坑指南:那些让JavaWeb新手debug到凌晨三点的典型问题
4.1 现象:JSP页面修改后刷新仍是旧内容
原因:Tomcat的Jasper引擎对JSP做了编译缓存,默认不检测文件变化。IDEA的热部署(Hot Swap)对JSP无效。
解决:
- 方案1(推荐):在
conf/web.xml中找到<servlet>节点下的<servlet-name>jsp</servlet-name>,添加初始化参数:<init-param> <param-name>development</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>checkInterval</param-name> <param-value>1</param-value> <!-- 每秒检查一次 --> </init-param> - 方案2:每次修改JSP后,在IDEA中右键项目 →
Reload project,强制重新编译JSP。
4.2 现象:Servlet中request.getParameter("xxx")始终为null
原因:表单method属性与Servlet处理方法不匹配(如form用POST,Servlet只写了doGet),或表单input没有name属性(只有id)。
解决:
- 检查HTML表单:
<form method="post" action="LoginServlet">→ Servlet必须重写doPost(); - 检查input:
<input type="text" name="username">,name值必须与getParameter("username")一致; - 在Servlet开头加日志:
System.out.println("Request method: " + request.getMethod());确认请求方法。
4.3 现象:MySQL连接成功,但执行SELECT返回空结果集
原因:JDBC URL未加useUnicode=true&characterEncoding=utf8mb4,导致中文条件查询失效(如WHERE username='张三'查不到)。
解决:
- 严格按2.2节配置JDBC URL;
- 在MySQL命令行执行
SET NAMES utf8mb4;后手动测试SQL,确认数据能查出; - 在DAO中打印完整SQL:
System.out.println("Executing SQL: " + sql);,复制到MySQL客户端执行,排除SQL本身问题。
4.4 现象:IDEA中Tomcat启动后,浏览器访问http://localhost:8080/显示Tomcat默认页,而非你的项目
原因:项目部署路径(Application context)被设为ROOT,但你的项目名不是ROOT,或Artifact未正确关联。
解决:
Run Configurations → Tomcat Server → Deployment → 编辑Artifact → Application context改为/tourism(与项目名一致);- 或在
Project Structure → Artifacts → tourism.war:exploded → Output directory右侧点Fix,确保路径指向web目录; - 删除
tomcat/work/Catalina/localhost/下残留的旧项目文件夹。
4.5 现象:JSP中EL表达式${user.username}不解析,直接显示字符串
原因:web.xml的schema版本过低,未启用EL。
解决:
- 检查
web.xml头声明,必须是Servlet 3.0+:<?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"> - 若用Tomcat 7,version应为
3.0,schemaLocation对应web-app_3_0.xsd。
5. 进阶技巧:让系统从“能跑”升级为“好维护、易扩展、抗并发”
5.1 用BaseServlet统一请求分发:告别N个Servlet文件的混乱
每个功能写一个Servlet(LoginServlet、RouteListServlet、OrderServlet…)会导致类爆炸。用反射+泛型实现一个BaseServlet,通过URL参数?method=add自动调用对应方法:
// src/servlet/BaseServlet.java public abstract class BaseServlet extends HttpServlet { @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取method参数 String methodName = request.getParameter("method"); if (methodName == null || methodName.trim().isEmpty()) { methodName = "execute"; // 默认方法 } // 用反射调用当前类的methodName方法 try { Method method = this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); method.invoke(this, request, response); } catch (Exception e) { throw new RuntimeException(e); } } public String execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { return "/index.jsp"; // 默认跳转 } }继承它:
// src/servlet/RouteServlet.java public class RouteServlet extends BaseServlet { public String list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Route> routes = new RouteService().findAll(); request.setAttribute("routes", routes); return "/route/list.jsp"; } public String add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 处理新增逻辑 return "/route/add_success.jsp"; } }web.xml中只需配置一个Servlet:
<servlet> <servlet-name>RouteServlet</servlet-name> <servlet-class>servlet.RouteServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>RouteServlet</servlet-name> <url-pattern>/route/*</url-pattern> </servlet-mapping>访问/route?method=list即调用list()方法。这直接解决“javaweb项目完整案例”中模块化不足的问题,让代码结构像MVC一样清晰。
5.2 DAO层封装通用CRUD:用反射+泛型消灭重复SQL
每个实体类都写一套增删改查,代码重复率高达80%。用BeanUtils和ResultSetMetaData实现通用DAO:
// src/dao/BaseDao.java public class BaseDao<T> { private Class<T> clazz; public BaseDao(Class<T> clazz) { this.clazz = clazz; } public List<T> query(String sql, Object... params) { List<T> list = new ArrayList<>(); Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = JDBCUtils.getConnection(); pstmt = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) { pstmt.setObject(i + 1, params[i]); } rs = pstmt.executeQuery(); // 用反射+ResultSetMetaData自动封装对象 ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); while (rs.next()) { T obj = clazz.getDeclaredConstructor().newInstance(); for (int i = 1; i <= columnCount; i++) { String columnName = metaData.getColumnName(i); Object value = rs.getObject(i); // 将数据库列名转为Java属性名(如route_name → routeName) String fieldName = toCamelCase(columnName); Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } list.add(obj); } } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.close(rs, pstmt, conn); } return list; } private String toCamelCase(String columnName) { StringBuilder sb = new StringBuilder(); String[] parts = columnName.split("_"); sb.append(parts[0]); for (int i = 1; i < parts.length; i++) { sb.append(parts[i].substring(0, 1).toUpperCase()) .append(parts[i].substring(1)); } return sb.toString(); } }使用:
// src/dao/UserDao.java public class UserDao extends BaseDao<User> { public UserDao() { super(User.class); } public User findByUsername(String username) { String sql = "SELECT * FROM t_user WHERE username = ?"; List<User> users = query(sql, username); return users.isEmpty() ? null : users.get(0); } }优势:新增实体类时,只需继承
BaseDao<YourEntity>,无需重写CRUD;toCamelCase()自动处理下划线命名转驼峰,适配MySQL字段名习惯。
5.3 前端JSP页面优化:用CSS Grid实现响应式线路卡片布局
“饿了么element图标前端jsp”这类搜索,反映的是新手对UI的焦虑。其实不用引入复杂框架,纯CSS Grid就能搞定:
<!-- index.jsp 中的线路列表 --> <div class="route-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; padding: 20px;"> <c:forEach items="${page.list}" var="route"> <div class="route-card" style="border: 1px solid #eee; border-radius: 8px; overflow: hidden;"> <img src="${page.contextPath}${route.rimage}" style="width: 100%; height: 150px; object-fit: cover;"> <div class="card-body" style="padding: 12px;"> <h3 style="margin: 0 0 8px 0; font-size: 16px;">${route.rname}</h3> <p style="color: #666; margin: 0;">${fn:substring(route.routeDesc, 0, 30)}...</p> <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;"> <span style="color: #e63946; font-weight: bold;">¥${route.price}</span> <a href="${page.contextPath}/routeDetail.jsp?id=${route.id}" style="color: #007bff; text-decoration: none;">详情</a> </div> </div> </div> </c:forEach> </div>效果:在PC端自动排3列,平板端2列,手机端1列;
object-fit: cover确保图片不拉伸;内联style避免外部CSS文件加载失败导致页面空白——这是上线前保底方案。
我带过的实习生里,有3个人靠这个项目拿下offer:一个改了支付模块接入微信沙箱,一个加了Redis缓存热门线路,一个用Ajax重构了订单提交。他们没写一行Spring Cloud,但面试官盯着他画的“请求从JSP发出,经Filter、Servlet、Service、DAO,最终落库”的流程图,当场说“你懂底层”。真正的技术深度,从来不是堆砌新名词,而是把JSP+Servlet+MySQL这条老路,踩出自己的坑、填出自己的解、走出自己的节奏。希望帮到你。
本文还有配套的精品资源,点击获取