资讯详情

基于Servlet+JSP+MySQL的旅游景点管理系统设计与实现

📅 2026/10/11 19:10:49 | 华诺云谱 👁 阅读
基于Servlet+JSP+MySQL的旅游景点管理系统设计与实现
不做铺垫了直接说说这个项目。这段时间把之前做的一个“潍坊市旅游景点管理系统”源码整理了一遍资源编号95527用ServletJSPMySQL这套经典技术栈写完整的一套Web系统。在线路规划和景点信息管理这个方向上这类系统很有代表性——前台给游客浏览景点、查详情、发评论后台给管理员维护景点数据和公告。如果你正在学Java Web或者课程设计、毕业设计选题正好是“XX旅游景点管理系统”这篇文章直接照着改就能用。我会把遇到的问题和坑也一起写出来包括数据库设计、Servlet请求分发、分页查询、图片上传、中文乱码、Tomcat部署这些点都是新手最容易卡住的地方。1. 为什么用Servlet这套老技术1.1 课程设计里“点名”要用Servlet说实话现在企业开发已经很少直接写Servlet了但很多高校的Java Web课程设计、毕业设计题目里仍然明确要求用Servlet、JSP、MySQL、Tomcat不能用Spring Boot直接糊一个。这是有原因的——Servlet是Java Web最底层的请求处理模型能让人真正理解HTTP请求进来之后发生了什么。如果你一上来就用Spring Boot很多东西被自动配置和starter封装掉了你根本不知道一个URL请求是怎么从浏览器跑到服务端再被某个方法接住的。而用Servlet你就得自己写web.xml里配置Servlet映射或者用注解方式在doGet/doPost方法里手动接收参数自己创建数据库连接执行SQL再把结果集封装成对象最后通过request转发或重定向到JSP页面展示这一套走完你对Java Web的整个流转过程就通了。之后再学Spring MVC、Spring Boot你会恍然大悟——原来框架是把这一堆东西帮你干了。1.2 MVC模式在Servlet时代的经典写法这个景点管理系统我按MVC三层来组织代码Model层实体类景点、用户、管理员、评论、分类、公告 DAO层对数据库的增删改查View层JSP页面放在WEB-INF下禁止用户直接访问Controller层Servlet类负责接收请求、调用业务逻辑、跳转页面这套结构的好处是职责清晰。景点列表页出了问题先看是JSP渲染的问题还是Servlet传参的问题还是SQL查错了。不会像有些人写demo那样所有代码堆在一个JSP文件里出问题的时候无从下手。1.3 这个系统的功能范围一个完整的旅游景点管理系统我拆成了前台和后台两块前台游客用户用户注册、登录浏览景点列表带分页按分类筛选景点搜索景点名称/地区查看景点详情图片、介绍、门票价格、开放时间发表评论查看公告通知后台管理员管理员登录景点信息维护增删改查含图片上传景点分类管理公告发布与删除用户管理查看、禁用评论管理审核、删除这些功能看起来多其实都是“对数据库表的增删改查”加上“页面交互”。难点在于它们之间的关联关系——比如景点和分类是外键关系评论和景点是外键关系用户被禁用后登录状态如何处理。把这些关系理清楚系统骨架就立住了。2. 数据库设计——先想清楚表和关系2.1 核心表结构开发之前数据库设计一定不能偷懒。我见过太多人直接上来就写代码结果做到一半发现缺字段又要改表改代码非常痛苦。这个系统我设计了6张表它们的关系是一条很清晰的主线。CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL ); CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, realname VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), avatar VARCHAR(200), status INT DEFAULT 1 COMMENT 1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); CREATE TABLE t_scenic ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, location VARCHAR(200), introduction TEXT, ticket_price DECIMAL(10,2), open_time VARCHAR(100), image VARCHAR(200), category_id INT, view_count INT DEFAULT 0, is_hot INT DEFAULT 0 COMMENT 1-热门 0-普通, FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE TABLE t_comment ( id INT PRIMARY KEY AUTO_INCREMENT, scenic_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status INT DEFAULT 0 COMMENT 0-待审核 1-通过 2-拒绝, FOREIGN KEY (scenic_id) REFERENCES t_scenic(id), FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_notice ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100), content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );2.2 为什么这些字段要这么设计重点说几个容易被忽略的设计点。第一个是status字段。用户表和评论表都加了状态字段这不是多余。用户可能因为恶意评论被管理员禁用如果直接删除用户记录那么他之前发的评论也就没了对景点数据是一种破坏。用status0禁用保留数据只是不允许他再登录。评论的status字段是为了配合审核功能——用户发的评论默认待审核审核通过才在前台显示。第二个是外键。景点表里的category_id、评论表里的scenic_id和user_id都加了外键约束。在删除分类时如果分类下还有景点数据库会直接报错这其实是一件好事它迫使你在业务代码里先检查关联数据避免出现“孤儿数据”。第三个是view_count字段。这个其实就是景点的浏览量每次详情页被访问就加1。很多新手会想查浏览量的时候直接统计一下不就行了但作为列表排序的热度指标用单独字段累加的方式性能更好页面上也直接展示数字不用每次count一下。2.3 初始化数据系统里我预置了管理员账号admin/admin123以及少量景点测试数据。下载源码后直接导入SQL脚本就能跑起来。建议你自己插入测试数据时把景点图片路径放到项目的upload目录下路径保存相对路径如upload/network_map.png不要在数据库里写入完整绝对路径否则换一台机器就全是破图。3. 项目结构与环境搭建3.1 开发环境版本我用的是下面这套组合实测兼容性很稳JDK 8Apache Tomcat 8.5MySQL 5.7IDEAEclipse也可以但IDEA对Tomcat的集成更方便Maven 3.6用来管理依赖如果你不想用Maven也可以在项目lib目录下手动导入jar包效果一样。需要的jar包mysql-connector-java、commons-fileupload、commons-io、fastjson或者Gson。3.2 源码目录结构我的源码目录按功能分包每个包职责清晰src ├── com.travel.entity // 实体类Scenic、User、Admin、Comment、Category、Notice ├── com.travel.dao // 数据访问层ScenicDao、UserDao、CommentDao等 ├── com.travel.service // 业务层处理简单逻辑也可以用 ├── com.travel.servlet // Servlet控制器ScenicServlet、UserServlet、AdminServlet等 ├── com.travel.filter // Filter编码过滤器、登录验证过滤器 ├── com.travel.util // 工具类DBUtil、StringUtil └── db.properties // 数据库连接配置WebContent目录下WebContent ├── index.jsp // 前台首页转发到景点列表 ├── WEB-INF │ ├── web.xml │ └── views // 内部JSP页面 │ ├── front // 前台页面list.jsp、detail.jsp、login.jsp、register.jsp等 │ └── admin // 后台页面admin_index.jsp、scenic_list.jsp等 └── static // css、js、图片资源3.3 db.properties配置数据库连接信息放在db.properties里不硬编码在Java代码中。这样换数据库环境时只需要改配置文件不用重新编译。jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/travel_db?useSSLfalsecharacterEncodingutf-8 jdbc.usernameroot jdbc.password123456对应的JDBC工具类我简单封装了加载驱动和获取连接的方法public class DBUtil { private static String url; private static String user; private static String password; static { try { InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }这里有个非常关键的点JDBC驱动加载为什么要写在static块里因为静态代码块只在类加载时执行一次避免每次调用getConnection都重新注册驱动。如果写在方法里高并发场景下会频繁加载类既慢又容易出问题。4. 核心功能实现细节4.1 登录验证过滤器——一个Filter解决权限问题后台的所有操作都需要管理员登录前台的部分操作需要用户登录。如果每个Servlet里都写一遍“判断session里有没有user”代码会非常冗余。我写了一个拦截规则统一处理。public class LoginFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // 获取请求的路径 String uri req.getRequestURI(); String path uri.substring(uri.lastIndexOf(/) 1); // 放行登录、注册、静态资源 if (path.endsWith(.css) || path.endsWith(.js) || path.endsWith(.png) || path.endsWith(.jpg)) { chain.doFilter(request, response); return; } if (login.equals(path) || register.equals(path) || doLogin.equals(path) || doRegister.equals(path) || path.endsWith(index.jsp) || path.endsWith(logout)) { chain.doFilter(request, response); return; } // 判断session HttpSession session req.getSession(); Object admin session.getAttribute(admin); Object user session.getAttribute(user); if (admin ! null || user ! null) { chain.doFilter(request, response); } else { resp.sendRedirect( login.jsp); } } }登录成功之后在Servlet里把用户对象放进sessionrequest.getSession().setAttribute(user, user);这样一设计所有需要权限的页面和请求就被Filter统一拦住了。这个Filter控制的范围在web.xml里配置成拦截所有请求然后通过代码判断放行规则。比在web.xml里写一堆URL映射要灵活得多。这里有个细节静态资源如果不放行你会看到Tomcat控制台打出404页面样式也全部丢失。很多新手都在这一步卡半天。4.2 BaseServlet——减少Servlet数量的一个技巧如果每个功能都单独建Servlet这个项目光Servlet就有十多个。我写了一个BaseServlet父类用反射来分发方法。public abstract class BaseServlet extends HttpServlet { protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String methodName req.getParameter(method); if (methodName null || methodName.isEmpty()) { resp.sendRedirect(index.jsp); return; } try { Method method this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); method.invoke(this, req, resp); } catch (NoSuchMethodException e) { resp.sendRedirect(index.jsp); } catch (Exception e) { e.printStackTrace(); } } }这样每个业务模块只需要一个Servlet比如ScenicServlet对应景点模块里面可以有list()、detail()、add()、update()、delete()、search()等多个方法URL都写成/scenic?methodlist /scenic?methoddetailid1 /scenic?methoddeleteid3这一设计大大减少了类的数量代码结构也比“一个功能一个Servlet”更内聚。用到的反射机制在Java开发里非常普遍也值得作为一个技术亮点写进课程设计报告里。4.3 景点分页查询——列表页最核心的逻辑景点列表不能让所有数据一次性查出来因为数据多了页面会非常慢。分页是必备功能。我的前台列表实现了一个简单的分页查询。后端计算分页参数public void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; int pageSize 8; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { page Integer.parseInt(pageStr); } // 查询总数 int totalCount scenicDao.getCount(); int totalPages (int) Math.ceil(totalCount * 1.0 / pageSize); // 查询当前页数据 int offset (page - 1) * pageSize; ListScenic list scenicDao.findPage(offset, pageSize); req.setAttribute(scenicList, list); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/WEB-INF/views/front/list.jsp).forward(req, resp); }DAO层的分页SQL是MySQL的写法public ListScenic findPage(int offset, int pageSize) { String sql SELECT * FROM t_scenic LIMIT ?, ?; // 执行PreparedStatement设置参数 }LIMIT ?, ? 是MySQL的特有语法第一个询问偏移量第二个询问每页记录数。注意PreparedStatement中offset是int类型直接用setInt就行。JSP页面里我用c:forEach循环遍历并在底部生成上一页、下一页的链接。这里有个注意点上一页和下一页的跳转URL要带上当前的筛选和搜索条件。如果你在搜索状态下翻页URL还少了keyword参数就会翻到空页面。我在search方法里已经把keyword也传回了页面分页链接里拼上keyword参数这才保证搜索和分页不冲突。4.4 景点图片上传后台新增景点时管理员需要上传图片。用原生Servlet处理表单文件上传需要引入commons-fileupload依赖。// 创建核心解析类 DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(5 * 1024 * 1024); // 单个文件最大5MB if (ServletFileUpload.isMultipartContent(request)) { ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 普通表单字段比如name、location、ticket_price String name item.getFieldName(); String value item.getString(UTF-8); // 存入一个map } else { // 文件字段 String fileName new File(item.getName()).getName(); // 为了防止重名加时间戳 String newFileName System.currentTimeMillis() _ fileName; File uploadDir new File(upload); if (!uploadDir.exists()) { uploadDir.mkdir(); } File saveFile new File(uploadDir, newFileName); item.write(saveFile); // 保存相对路径到数据库 } } }这里有几个非常容易踩的坑我吃了不少亏第一文件名一定要重新生成不能用用户上传的原名。两个管理员上传一个同名的1.jpg后面会把前面覆盖掉。我加时间戳前缀来保证唯一。第二保存到数据库的路径建议存相对路径“upload/xxx.jpg”不要存“C:/apache-tomcat/webapps/upload/xxx.jpg”这样的绝对路径。因为部署到其他人的机器上路径完全对不上图片就404了。页面显示图片时用项目名相对路径拼出完整URL比如/TravelSystem/upload/xxx.jpg。第三FileItem.getString(UTF-8)这个细节很关键。如果直接调用getString()文件名里如果有中文就会乱码。文件的名字和表单字段的值都得指定UTF-8解码。4.5 评论功能与联表查询评论要求显示评论人的昵称和评论内容所以查询评论时不能只查t_comment还要联表查出用户表里的username。SELECT c.id, c.content, c.create_time, u.username FROM t_comment c LEFT JOIN t_user u ON c.user_id u.id WHERE c.scenic_id ? AND c.status 1 ORDER BY c.create_time DESC评论插入的时机也要想清楚。用户在景点详情页提交评论时不仅要往t_comment插入一条记录还应该同步把景点的view_count或者评论数做更新。不过我没有用独立字段存评论数而是在详情页统计时直接查询评论数量。如果将来数据量大了可以给t_scenic增加comment_count字段每次评论成功就1。4.6 后台管理页面的数据填充后台首页我放了几个数据统计卡片显示景点总数、用户总数、评论总数、待审核评论数。统计这些很简单DAO里写几个count方法就行。这里不追求复杂报表比拼的就是SQL基本功和JSP页面的展示。5. 部署与运行——从IDEA到独立Tomcat5.1 IDEA中直接启动在IDEA里配置好Tomcat后可以直接Run运行。最关键的一步是选择部署方式Artifact选择war exploded模式Application context设置为TravelSystem这样访问路径是http://localhost:8080/TravelSystem/如果第二行的Application context没配好或者配置成根路径“/”启动后页面会一直404因为你的URL里没带项目名。5.2 导出WAR包部署到服务器IDEA里执行Build - Build Artifacts - Build会生成war包。把这个war包放到Tomcat的webapps目录下启动Tomcat后会自动解压。访问路径是项目名。这里有一个容易忽略的地方如果部署到独立Tomcat数据库连接、上传图片目录等路径都要进行检查。尤其是图片上传在IDEA里运行时相对路径“upload”是相对Tomcat的bin目录生成的而不是相对项目的webapps目录。你在IDEA里测试上传功能时图片文件实际会出现在Tomcat安装目录的bin下而不是项目目录里。这个坑非常隐蔽我当时排查了很久。后来我在代码里没有用完全相对路径而是使用ServletContext获取真实路径String realPath getServletContext().getRealPath(/upload); File uploadDir new File(realPath);这样图片会真实写入到项目部署目录下的upload文件夹中不会跑到别的地方去。5.3 数据库导入源码里附带travel_db.sql脚本。先用命令行或Navicat创建数据库travel_db然后执行脚本mysql -uroot -p123456 travel_db.sql或者直接在Navicat里右键运行SQL文件。执行完成后能看到6张表。如果数据库名不一致需要修改db.properties里的jdbc.url尤其是databaseName后面的部分改成你实际创建的库名。如果忘了改启动时报错会提示“Unknown database”这个很好排查。6. 常见问题速查与避坑记录6.1 问题排查表按我实际调试过程中遇到的频率整理一个速查表问题现象排查思路解决方案JDBC连接失败Tomcat报Communications link failure检查MySQL是否启动启动MySQL服务检查端口3306是否被占用用户名密码错误Access denied for user检查db.properties配置改成正确账号密码中文乱码页面显示问号数据库存中文乱码请求、响应、JSP、数据库编码不一致页面统一UTF-8Servlet设置req.setCharacterEncoding连接URL加useUnicodetruecharacterEncodingutf-8建表指定UTF-8端口被占用Tomcat启动失败Port 8080 already in use查找占用进程改Tomcat端口或用netstat -ano查PID杀掉占用进程404错误输入路径找不到网页检查项目名、Servlet映射路径确认访问路径为http://localhost:8080/TravelSystem/405错误HTTP method POST is not supporteddoPost/doGet没实现在Servlet里实现doGet和doPost或重写service方法图片无法显示页面小图标破碎或图片区空白检查图片文件是否存在于部署目录使用ServletContext.getRealPath保存图片路径使用相对路径重新部署后测试评论不显示数据库有记录但页面无显示检查评论状态status值前台只查询status1的评论在后台审核通过后才会展示外键插入失败Cannot add or update a child row关联的主表记录不存在插入评论前确认scenic_id和user_id确实存在6.2 中文乱码问题的三重检查中文乱码是Web开发最经典的问题没有之一。这个系统里我做了三层防护第一层是过滤器。我写了一个CharacterEncodingFilter强制把请求和响应编码设置为UTF-8req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp);第二层是JSP页面每个JSP顶部都声明% page contentTypetext/html;charsetUTF-8 languagejava %。第三层是数据库连接URL。三层都配合好中文从浏览器到Servlet再到MySQL全链路不会乱码。6.3 从Servlet项目往Spring Boot迁移的思路如果你做完这个项目想继续提升把Servlet版本改造成Spring Boot版本是个很好的练习。改造思路也很清晰Servlet - ControllerController/RestController配置类代替web.xmlDAO层换成MyBatis或JPAFilter换成拦截器或Spring AOPJSP换成Thymeleaf或直接前后端分离你可以先保留Servlet版本作为课程设计提交然后自己私下用Spring Boot重写一遍对比两种方案收获会非常明显。代码量大概能少一半尤其是配置部分。这个项目做完一遍Servlet、JSP、JDBC、MySQL、会话管理、过滤器这些Java Web核心知识点基本都过了一遍。我在实际项目调试中最大的体会是不要做一个功能就去测试一个功能而是要把所有表结构先设计好然后把Filter和BaseServlet这些基础设施先搭好再做具体业务。基础不稳后面改起来全是牵一发动全身。如果你正准备做类似的管理系统不管是什么景区、图书馆、健身房这套代码结构都可以直接套用把表名和字段换一下就是一个全新项目。遇到问题多看Tomcat的日志最好的老师就是控制台里那几行红色报错。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑