news 2026/10/7 1:27:09

JSP+Servlet+JDBC构建洛阳旅游管理系统:从架构到部署的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet+JDBC构建洛阳旅游管理系统:从架构到部署的完整实践

简介:这份基于Java和JSP实现的洛阳旅游管理系统,是面向计算机专业毕业设计场景的完整项目,适合需要完成传统Web课题的学生参考。系统前端采用JSP、jQuery,后端由Servlet与JDBC支撑,集成网站首页、景点与资讯展示、景点资讯后台管理、留言中心、管理员维护、系统设置等模块,功能链完整,可辅助理解分层开发与数据库交互。资源包共349个文件,其中包含49个Java源码、30个JSP页面、31个JS脚本、29个样式表文件,并配有大量PNG/JPG图片素材和数据库脚本,既有可运行代码,也有直接可用的页面资源,压缩包约30.18MB,导入开发环境即可查看项目结构与配置。目前已有33人学习下载,适合作为毕业设计选题定纲、数据库表结构设计、前后台交互实现和界面布局的完整参考范本。

1. 洛阳旅游管理系统:一个能跑通的 JSP+Servlet 毕业设计完整样本

如果手里突然多了一个「基于 java+jsp 的洛阳旅游管理系统」源码包,第一反应往往是先看它是不是真能一键跑起来。我拆过不少同类项目,这个系统值得拆的地方在于:它没有把 Servlet 写成黑匣子,而是把 JSP 展示、Servlet 控制、JDBC 操作数据库这一条经典链路完整铺开了。前台有首页轮播、热门景点、天气展示、站内关键词搜索,后台有景点、资讯、留言、管理员、系统设置五大管理模块。对正在做毕业设计、或者刚接触 JSP 项目的初级工程师来说,这是很理想的骨架级参考——代码量不大,但 MVC 边界、文件上传、模糊查询、动态站点配置这些关键点一个不缺。资源包里自带了数据库脚本和全套页面,照着跑完再改,比自己从头搭要省太多时间。

2. 技术栈与架构边界:JSP+Servlet+JDBC 的分工到底在哪里

2.1 一个请求从 URL 到页面的完整链路

用户在前台点开"畅游"页面,看到的是渲染好的景点列表;管理员在后台"景点管理"里做增删改查,背后是同一条通路:表单或链接把请求发到对应的 Servlet,Servlet 里写业务判断,再调用 DAO 去查 MySQL 表,最后把结果塞进 request 转发给 JSP 用 JSTL 表达式循环输出。这是很典型的 JSP Model 2 写法,代码扁平,但每一层的角色都立得住。

以景点管理的列表请求为例,核心代码是这样的节奏:

// ScenesServlet.java 的 doGet 处理列表请求 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); SceneDao dao = new SceneDao(); if ("list".equals(action)) { List<Scene> list = dao.findAll(); request.setAttribute("sceneList", list); request.getRequestDispatcher("/admin/scene_list.jsp").forward(request, response); } }

这段代码说明了两个习惯。第一是action参数驱动同一个 Servlet 里的不同分支,增删改查共用一个类,路由逻辑全在action的值上,虽然不如 Spring MVC 的注解优雅,但对 JSP 项目来说是最容易排查的写法。第二是forward而不是redirect,列表查询这种读操作用 forward 直接带数据到 JSP,写操作(新增、编辑、删除)则统一用 redirect 回列表,避免刷新时重复提交。

再看 doPost 里的写法:

// ScenesServlet.java 的 doPost 处理新增和更新 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); Scene scene = new Scene(); scene.setName(request.getParameter("name")); scene.setDescription(request.getParameter("description")); scene.setPrice(new BigDecimal(request.getParameter("price"))); scene.setLocation(request.getParameter("location")); SceneDao dao = new SceneDao(); if ("add".equals(action)) { dao.insert(scene); } else if ("update".equals(action)) { scene.setId(Integer.parseInt(request.getParameter("id"))); dao.update(scene); } response.sendRedirect(request.getContextPath() + "/admin/ScenesServlet?action=list"); }

这里有一个血泪经验:setCharacterEncoding("UTF-8")必须放在读取任何参数之前,否则表单里凡是中文,进到数据库就全是问号。另外sendRedirect的路径用了request.getContextPath()拼前缀,而不是写死/luoyang_travel,这样后面部署时改 context-path 不用全局替换。

2.2 数据库表结构:核心表的字段与 sql 导入方式

数据库脚本是这个项目的命脉。系统跑起来需要几张核心表撑住:景点表放洛阳各个景点的名称、描述、价格和位置,资讯表放旅游攻略和公告,留言表承接前台访客的评论,管理员表管后台登录。具体字段设计见下表。

表名作用核心字段
tb_scene景点信息scene_id、scene_name、scene_img、scene_desc、scene_price、scene_location
tb_news资讯文章news_id、news_title、news_content、news_img、create_time
tb_message留言message_id、user_name、content、create_time
tb_admin后台管理员admin_id、admin_name、admin_pwd、role

导入的时候我习惯用 Navicat 的"运行 SQL 文件"功能,选中脚本直接执行,前提是先在本地建好一个同名数据库,字符集选 utf8mb4。命令行方式也很快:

mysql -u root -p luoyang_travel < luoyang.sql

注意luoyang.sql里的建表和 INSERT 语句带有DROP TABLE IF EXISTS前缀,重复执行也不会报错,适合反复折腾。跑完之后最好用SHOW TABLE STATUS;看一眼每张表的行数,确认数据真的导进去了。

2.3 环境搭配细节:为什么选 JDK 1.8 + Tomcat 9.0.73 + MySQL 5.7.26

这套组合是经典中的经典。JDK 1.8 是最后一个让 JSP 开发毫无心理负担的版本,Tomcat 9.0.73 对应 Servlet 4.0 规范,和项目里的javax.servlet包完全匹配。MySQL 5.7.26 是业界公认稳定版,和项目自带的 mysql-connector-java 5.x 驱动匹配,不用动版本。

如果本机装的是 MySQL 8.x,那就要注意了:驱动类名虽然还是com.mysql.jdbc.Driver(或改成com.mysql.cj.jdbc.Driver),但连接串要加时区参数,比如jdbc:mysql://localhost:3306/luoyang?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。项目里默认是 5.7 的写法,直接切 8.0 容易在启动时报Unable to load authentication plugin 'caching_sha2_password',这是 8.0 默认认证插件导致的,换一个 8.x 的驱动 jar 或者把 MySQL 用户认证方式改回 mysql_native_password 都能解。

数据库连接配置一般集中在 src 下的 jdbc.properties 或者 DBUtil 工具类里,改动点就三个:URL、用户名、密码。用 IDEA 2021.3 打开项目后,先确认 Project SDK 选到 1.8,再把 Tomcat 9.0.73 配进 Run Configuration,Artifact 选 war exploded,Application context 填/luoyang,启动后浏览器访问的就是http://localhost:8080/luoyang。

2.4 JSP 内置对象、EL 与 JSTL:把页面写薄的技巧

JSP 页面里最常用的内置对象是 request、session、pageContext。项目里${scene.sceneName}这种写法是 EL 表达式,它从 request 作用域按序取值。而<c:forEach items="${sceneList}" var="scene">是 JSTL 标签库,需要页面顶部<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。如果 IDEA 打开 JSP 报红,多半是 JSTL 依赖没有引入,Artifact 的 WEB-INF/lib 下要手动放 jstl.jar 和 standard.jar。

页面模板里还大量使用了 include 指令,比如<%@ include file="header.jsp" %>。这个静态 include 会在编译期把页面拼进去,好处是公共头部统一维护,坏处是如果两个页面都定义了同名变量,编译直接冲突。项目里 header.jsp 和 footer.jsp 尽量只放 HTML 结构,不放 Java 脚本片段。

<!-- admin/scene_list.jsp 中循环输出景点 --> <c:forEach items="${sceneList}" var="scene" varStatus="st"> <tr> <td>${st.count}</td> <td>${scene.sceneName}</td> <td>${scene.scenePrice}</td> <td><img src="${pageContext.request.contextPath}/${scene.sceneImg}" width="80" /></td> <td> <a href="${pageContext.request.contextPath}/admin/ScenesServlet?action=edit&id=${scene.sceneId}">编辑</a> <a href="${pageContext.request.contextPath}/admin/ScenesServlet?action=delete&id=${scene.sceneId}">删除</a> </td> </tr> </c:forEach>

这一小段 JSP 里包含了三个知识点:varStatus的 count 属性可以生成序号、EL 表达式负责取对象属性、所有链接都拼了 contextPath。答辩时被问"为什么删除是 get 而不是 post",能解释清楚就行:这不是规范做法,但 JSP 老项目的管理后台普遍这么写,生产系统要改成表单 post 提交。

3. 后台管理链路:景点、资讯、留言和系统配置的完整实现

3.1 景点管理:图片上传与模糊搜索的实现细节

景点管理是后台最重的模块,因为它同时包含了文件上传和模糊查询两个高频考点。图片上传走的是 commons-fileupload 组件,核心代码大致是这个样子:

// ScenesServlet.java 处理景点图片上传 if (ServletFileUpload.isMultipartContent(request)) { DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(5 * 1024 * 1024); // 单文件上限 5MB List<FileItem> items = upload.parseRequest(request); for (FileItem item : items) { if (!item.isFormField()) { String fileName = new File(item.getName()).getName(); String realPath = getServletContext().getRealPath("/upload/scene"); File dir = new File(realPath); if (!dir.exists()) { dir.mkdirs(); } item.write(new File(dir, fileName)); scene.setImg("upload/scene/" + fileName); } } }

这里有三处容易翻车的细节。第一,upload.setFileSizeMax如果不设,用户传一个大图会把 Tomcat 内存直接打满,项目里默认 5MB 是够用的。第二,new File(item.getName()).getName()这一步是为了把老版本浏览器传来的完整路径截成纯文件名,防止..或盘符混进保存路径。第三,图片存到的是部署目录下的/upload/scene,也就是 IDEA 的 target 目录,如果重启 Tomcat 后图片没了,多半是因为 IDEA 重新部署覆盖了目录,这不是代码问题,而是部署策略问题。

模糊搜索的 SQL 实现也不复杂。景点名称和描述都用 LIKE 匹配,但注意必须用 PreparedStatement 占位符拼,而不是字符串拼接:

// SceneDao.java 中的模糊查询 public List<Scene> search(String keyword) throws SQLException { String sql = "SELECT * FROM tb_scene WHERE scene_name LIKE ? OR scene_desc LIKE ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { List<Scene> list = new ArrayList<>(); while (rs.next()) { Scene scene = new Scene(); scene.setSceneId(rs.getInt("scene_id")); scene.setSceneName(rs.getString("scene_name")); // ... 省略其他字段封装 list.add(scene); } return list; } } }

用%keyword%的方式意味着只要景点名称或描述里包含关键词都能命中。答辩时被问"为什么不用 Statement",标准答案是 PreparedStatement 预编译避免 SQL 注入,同时参数有类型转换,比拼接'%' + keyword + '%'更安全。这条一定要能说清楚,因为这是 JSP 项目里数据库操作最常被追问的点。

3.2 资讯管理:新增、编辑与封面图

资讯管理在代码结构上是景点管理的简化版,区别主要在字段更少。数据库里 tb_news 表有 news_title、news_content、news_img、create_time,后台页面通常是一个表单加一个图片选择框。编辑操作的值回显是这里的关键——点击"编辑"时,Servlet 把这条资讯查出来放进 request,JSP 再用${news.newsTitle}回填到 input 的 value 里:

<input type="text" name="newsTitle" value="${news.newsTitle}" class="form-control" />

图片回显则要额外注意路径:

<c:if test="${not empty news.newsImg}"> <img src="${pageContext.request.contextPath}/${news.newsImg}" class="img-thumbnail" width="120" /> </c:if>

这里用<c:if>判断空值,避免后台编辑页打开时图片区域显示一个破碎的 img 标签。

3.3 留言中心:列表展示与删除

留言中心的功能很克制:前台访客提交 name+content 写入 tb_message 表,后台管理员只做两件事,查询列表和删除。这里比较值得借鉴的是前台提交时的 JS 校验——不通过就不发请求,减少无效数据:

function validateMessage() { var name = document.getElementById("userName").value.trim(); var content = document.getElementById("content").value.trim(); if (name === "" || content === "") { alert("昵称和留言内容都不能为空"); return false; } return true; }

后台删除用 action 参数拼到链接上,例如delete&id=${msg.messageId}。注意这种写法在真实生产环境里属于高危操作,但毕业设计的管理后台普遍这么做,展示阶段够用。只要答辩时能补一句"上线前会加权限校验和 CSRF 防护",面试官也不会揪着不放。

3.4 系统管理:动态改站点标题与底部的配置思路

系统管理模块负责网站名称、关键词、描述、底部信息的动态设置。常见做法是把这些配置存进独立的 tb_config 表,用 key-value 形式存:

// ConfigDAO 中按 key 读取 public String getConfig(String key) throws SQLException { String sql = "SELECT config_value FROM tb_config WHERE config_key = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, key); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getString("config_value"); } } } return ""; }

站点底部 footer 和首页 title 的 JSP 里,调用这个 getConfig 方法输出。好处很明显:改配置不用改代码,直接在后台的表单里填,下一次刷新页面就生效。这个模块在答辩时很加分,因为很多毕业设计都是把标题写死在 jsp 里,能做成配置化的反而是少数。

4. 前台页面与交互:轮播图、天气和站内搜索的实现方式

4.1 首页模块划分:导航、banner 和热门板块

首页 index.jsp 的结构是自上而下拆的:顶部导航栏(系统名称+菜单链接)、中部轮播图、右侧天气、下方热门景点卡片网格、再往下是热门资讯列表。这种布局用的是 Bootstrap 3 的栅格系统,css 引入的是 bootstrap.min.css,页面宽度控制在一个 container 里。前端资源里那串 CSS 文件——bootstrap-theme.css、font-awesome.css、map.css——分工很明确:前两个负责 UI 组件和图标,map.css 负责景点地图或详情页的样式。

4.2 天气模块:接口接入与容错

天气模块使用的是公共天气接口,接口返回 JSON,前端用 jQuery 的$.getJSON去取数据,然后拼到页面上。因为公共接口在本地环境下不一定通,所以项目里一般会做一层容错——接口超时就显示默认温度,保证页面外观不塌:

function loadWeather(cityCode) { var key = "city_" + cityCode; if (window.__weatherCache && window.__weatherCache[key]) { renderWeather(window.__weatherCache[key]); return; } $.getJSON("weatherApiUrl", { city: cityCode }) .done(function (data) { window.__weatherCache = window.__weatherCache || {}; window.__weatherCache[key] = data; renderWeather(data); }) .fail(function () { $("#weather-box").html("当前温度:22℃ 多云"); }); }

这里我一般会加一层内存缓存:同一个城市在短时间内重复访问,不重复请求接口,避免页面返回时重新拉取慢接口。fail分支里的兜底数据,保证接口挂了页面也不尴尬。

4.3 站内关键词搜索:GET 参数与防 XSS

首页的搜索框比较常规:form 用 GET 方法提交,action 指向 SearchServlet,参数是 keyword。GET 的好处是搜索条件能体现在 url 上,方便分享和刷新不弹窗:

<form action="${pageContext.request.contextPath}/search" method="get" class="navbar-form"> <input type="text" name="keyword" class="form-control" placeholder="搜索景点或资讯"> <button type="submit" class="btn btn-default">搜索</button> </form>

Servlet 端拿到 keyword 后调用两个 DAO——景点搜索和资讯搜索,把结果放进同一个 request,forward 到搜索结果页。如果搜索结果为空,页面给出"未找到相关内容"的提示,而不是白屏。还有一个容易被忽略的点:keyword 输出到页面前要做 HTML 转义,否则用户输入<script>会被浏览器执行。用 JSTL 的<c:out value="${keyword}"/>输出,天然带转义。

4.4 轮播图初始化与 JS 引用顺序

轮播图就是 Bootstrap 自带 carousel 组件,CSS 和 JS 都已经整合在 bootstrap 包里,只需要确保页面底部引入了 jquery.min.js 和 bootstrap.min.js 的加载顺序——先 jquery 后 bootstrap,否则插件组件不会初始化。很多 JSP 项目页面白屏或轮播不动,八成是这两个 script 标签顺序倒了。

<script src="${pageContext.request.contextPath}/js/jquery.min.js"></script> <script src="${pageContext.request.contextPath}/js/bootstrap.min.js"></script> <script> $(function () { $(".carousel").carousel({ interval: 4000 }); }); </script>

carousel初始化时可以传对象配置,interval: 4000表示每 4 秒切一张。Bootstrap 还支持在 data 属性里直接配置,比如<div id="carousel" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 1:25:58

医疗NLP诊断系统实战:从症状实体识别到疾病排序

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:25:51

KiCAD生产文件导出全流程:Gerber、钻孔、坐标与BOM指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:24:46

基于Web停车场管理系统:JAVA源码、数据库SQL与论文整合实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:24:16

RISC-V MCU子系统设计:ITCM、DTCM与Slave-AHB接口的工程实践与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:24:11

单片机控制板异常排查六步法:从电源到干扰全流程指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 1:24:11

FSDB波形生成与截取实战:vfast原理、fsdb_filter命令与跨工具链兼容

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华