
简介这是一套基于JavaWeb技术栈开发的旅游管理系统完整源码面向Java初学者与Web开发入门者帮助掌握JSPServletMySQL三层架构的实战应用能力。系统覆盖旅游网站核心业务场景包含主界面展示、用户注册审核、景点信息检索、酒店在线预订及后台多模块管理含账号、人文内容、自驾路线、留言板等代码结构清晰具备典型MVC分层特征。资源包共738个文件以111个JSP页面实现前后端交互、415个GIF/69个JPG用于界面素材、40个CSS和23个JS支撑前端样式与交互辅以SQL数据库脚本、Java类及配置文件整体压缩后仅8.46MB轻量易部署。目前已有1111人学习下载读者可直接运行调试深入理解用户权限控制、表单提交处理、数据库增删改查及后台管理逻辑实现细节。1. JavaWeb旅游管理系统JSPServletMySQL一个能跑通、能改、能交作业的真实闭环项目你不是在学“JavaWeb”你是在搭一座桥——从写完System.out.println(Hello World)到真能把用户注册、登录、查线路、下订单、管理员后台全链路串起来的桥。这个标题里的“JavaWeb旅游管理系统JSPServletMySQL”不是教科书里的抽象概念而是一套可本地启动、带完整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. 搭建运行环境用IDEATomcatMySQL 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重复引入会类加载冲突。接着配置ArtifactsProject Structure → Artifacts → → Web Application: Archive → 给它起名如 tourism.war在左侧Output Layout中确保WEB-INF/classes指向你的src编译输出目录通常是out/production/your-project-nameWEB-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-8Servlet读取参数时若不显式设置中文必然乱码。别信网上“改my.cnf就完事”的玄学方案必须四步闭环安装时指定编码Windows MSI安装器勾选Use UTF8 as default charsetLinux用rpm安装后立即执行# 编辑 /etc/my.cnfCentOS或 C:\ProgramData\MySQL\MySQL Server 5.7\my.iniWin [client] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci init_connectSET NAMES utf8mb4 skip-character-set-client-handshake TRUE创建数据库时强制指定CREATE DATABASE tourism_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC连接URL加参数src/jdbc.propertiesjdbc.urljdbc:mysql://localhost:3306/tourism_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8allowPublicKeyRetrievaltrueuseSSLfalse注意useSSLfalse不是妥协而是MySQL 5.7.22默认要求SSL本地开发无证书时必须关掉serverTimezoneGMT%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_userid(PK),username(UNIQUE),password(VARCHAR(64)),realname,phone,email,role(ENUM(admin,user)),status(TINYINT)password用BCrypt加密存储role控制后台入口登录校验、权限分流、个人信息页展示t_routeid,rname,price,routeDesc,rimage(VARCHAR存路径),source(VARCHAR),is_hot(TINYINT),cid(FK)rimage存相对路径如/images/route1.jpg方便JSPimg src${route.rimage}首页轮播图、线路列表页、详情页图片定位t_categoryid,cname,description分类表如“国内游”“出境游”“周边游”左侧导航栏分类筛选t_orderid,uid(FK),rid(FK),orderTime,totalPrice,status(ENUM(wait,paid,closed))复合索引(uid, status)加速用户订单查询我的订单页、订单状态流转t_order_itemid,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 actionloginServlet就完事。真正的难点在于如何让“用户没登录时访问/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-nameLoginFilter/filter-name filter-classfilter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/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来如?currentPage2pageSize8% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle旅游线路/title/head body div classroute-list c:forEach items${page.list} varroute div classroute-item img src${page.contextPath}${route.rimage} alt${route.rname} stylewidth:200px;height:150px;object-fit:cover; h3${route.rname}/h3 p价格span classprice¥${route.price}/span/p a href${page.contextPath}/routeDetail.jsp?id${route.id}查看详情/a /div /c:forEach /div !-- 分页栏 -- div classpagination 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.currentPage1}pageSize${page.pageSize}下一页/a /c:if /div /body /html对应的IndexServletprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int currentPage Integer.parseInt(request.getParameter(currentPage)); int pageSize Integer.parseInt(request.getParameter(pageSize)); PageBeanRoute 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, ListOrderItem 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-namejsp/servlet-name添加初始化参数init-param param-namedevelopment/param-name param-valuetrue/param-value /init-param init-param param-namecheckInterval/param-name param-value1/param-value !-- 每秒检查一次 -- /init-param方案2每次修改JSP后在IDEA中右键项目 →Reload project强制重新编译JSP。4.2 现象Servlet中request.getParameter(xxx)始终为null原因表单method属性与Servlet处理方法不匹配如form用POSTServlet只写了doGet或表单input没有name属性只有id。解决检查HTML表单form methodpost actionLoginServlet→ Servlet必须重写doPost()检查inputinput typetext nameusernamename值必须与getParameter(username)一致在Servlet开头加日志System.out.println(Request method: request.getMethod());确认请求方法。4.3 现象MySQL连接成功但执行SELECT返回空结果集原因JDBC URL未加useUnicodetruecharacterEncodingutf8mb4导致中文条件查询失效如WHERE username张三查不到。解决严格按2.2节配置JDBC URL在MySQL命令行执行SET NAMES utf8mb4;后手动测试SQL确认数据能查出在DAO中打印完整SQLSystem.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 version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0若用Tomcat 7version应为3.0schemaLocation对应web-app_3_0.xsd。5. 进阶技巧让系统从“能跑”升级为“好维护、易扩展、抗并发”5.1 用BaseServlet统一请求分发告别N个Servlet文件的混乱每个功能写一个ServletLoginServlet、RouteListServlet、OrderServlet…会导致类爆炸。用反射泛型实现一个BaseServlet通过URL参数?methodadd自动调用对应方法// 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 { ListRoute 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中只需配置一个Servletservlet servlet-nameRouteServlet/servlet-name servlet-classservlet.RouteServlet/servlet-class /servlet servlet-mapping servlet-nameRouteServlet/servlet-name url-pattern/route/*/url-pattern /servlet-mapping访问/route?methodlist即调用list()方法。这直接解决“javaweb项目完整案例”中模块化不足的问题让代码结构像MVC一样清晰。5.2 DAO层封装通用CRUD用反射泛型消灭重复SQL每个实体类都写一套增删改查代码重复率高达80%。用BeanUtils和ResultSetMetaData实现通用DAO// src/dao/BaseDao.java public class BaseDaoT { private ClassT clazz; public BaseDao(ClassT clazz) { this.clazz clazz; } public ListT query(String sql, Object... params) { ListT 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 BaseDaoUser { public UserDao() { super(User.class); } public User findByUsername(String username) { String sql SELECT * FROM t_user WHERE username ?; ListUser users query(sql, username); return users.isEmpty() ? null : users.get(0); } }优势新增实体类时只需继承BaseDaoYourEntity无需重写CRUDtoCamelCase()自动处理下划线命名转驼峰适配MySQL字段名习惯。5.3 前端JSP页面优化用CSS Grid实现响应式线路卡片布局“饿了么element图标前端jsp”这类搜索反映的是新手对UI的焦虑。其实不用引入复杂框架纯CSS Grid就能搞定!-- index.jsp 中的线路列表 -- div classroute-grid styledisplay: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; padding: 20px; c:forEach items${page.list} varroute div classroute-card styleborder: 1px solid #eee; border-radius: 8px; overflow: hidden; img src${page.contextPath}${route.rimage} stylewidth: 100%; height: 150px; object-fit: cover; div classcard-body stylepadding: 12px; h3 stylemargin: 0 0 8px 0; font-size: 16px;${route.rname}/h3 p stylecolor: #666; margin: 0;${fn:substring(route.routeDesc, 0, 30)}.../p div styledisplay: flex; justify-content: space-between; align-items: center; margin-top: 10px; span stylecolor: #e63946; font-weight: bold;¥${route.price}/span a href${page.contextPath}/routeDetail.jsp?id${route.id} stylecolor: #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最终落库”的流程图当场说“你懂底层”。真正的技术深度从来不是堆砌新名词而是把JSPServletMySQL这条老路踩出自己的坑、填出自己的解、走出自己的节奏。希望帮到你。本文还有配套的精品资源点击获取