☰
JSP旅游管理系统实战:B/S架构与SQL Server数据库设计
2026/10/9 13:13:05 网站建设 项目流程

简介:本资源是一份面向Java Web初学者与中级开发者的旅游管理系统毕业设计文档,聚焦B/S架构下的实际业务场景实现,帮助开发者掌握JSP动态网页开发、SqlServer数据库设计及前后端协同开发全流程。文档完整覆盖系统需求分析、数据库ER图与表结构设计、管理员与会员双角色权限划分、旅游景点/线路管理、在线预订、论坛与公告等核心模块的实现逻辑,并附有技术选型说明(MyEclipse8.5+Tomcat6.0+SqlServer2012)与可行性论证。压缩包含1个934KB的docx文件,内容结构清晰,含摘要、中英文关键词、目录、绪论、需求分析、系统设计与实现等标准论文章节,特别适合课程设计参考、毕设开题与技术复盘。目前已有68人学习下载,可直接用于理解JSP项目分层结构、权限控制实现方式及Web旅游平台典型功能落地路径。

1. 这不是又一个“毕设模板”:基于 JSP 的旅游管理系统,是 Java Web 开发者绕不开的「B/S 架构实体课」

你可能已经点开过 dozens 个标着“Java旅游系统源码下载”的链接,点进去发现是空壳、乱码、404,或者跑起来就报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver——这不是玄学,是 2012 年代 Java Web 技术栈的真实切片。这个系统不是为炫技而生,它用 MyEclipse 8.5 + Tomcat 6.0 + SQL Server 2012 这套被现代 Spring Boot 项目“淘汰”的组合,完整复现了 B/S 架构从数据库建模、JSP 页面嵌套、Servlet 控制流转、到权限隔离落地的全链路。它解决的不是“能不能跑”,而是“为什么当年必须这么写”:比如为什么request.setCharacterEncoding("UTF-8")必须在getParameter()之前调用?为什么管理员和普通用户共用一张t_user表却要靠user_type字段硬切权限边界?为什么t_jingdian表里Fujian字段存的是图片路径而非二进制?这些问题的答案,就藏在它那 37 个.jsp文件、6 张物理表、以及web.xml里 4 个<servlet-mapping>的顺序里。适合刚学完 Servlet 生命周期、正卡在“怎么把 DAO 层连上页面”的 Java Web 新手;也适合想回溯传统 MVC 分层逻辑、排查老系统兼容性问题的一线维护工程师——它不教你怎么写 Lambda,但教会你怎么让out.print()不输出乱码。

2. 从 E-R 图到 SQL Server 2012:数据库设计不是画图,是定义数据契约

2.1 概念建模:E-R 图里的每一个菱形,都对应一次真实业务冲突

系统文档中给出的 6 张 E-R 图(普通用户、旅游景点、旅游线路、公告、管理员、论坛帖子)不是装饰。它们直接决定了后续所有 CRUD 的健壮性。以旅游景点实体为例:

  • 实体属性Name(景点名称)、Dizhi(地址)、Menpiao(门票价格)看似平平无奇,但Menpiao被定义为varchar(50)而非decimal(10,2),意味着它可能存"免费"、"成人票80元/人"或"学生票半价"—— 这不是设计失误,而是为适配中国景区定价策略的灵活性预留的字段语义空间;
  • Fujian(附件)字段名直译为“附件”,但实际存储的是图片文件路径(如upload/jd_123.jpg),这暗示系统依赖外部文件系统而非数据库 BLOB 存储,直接影响部署时tomcat/webapps/ROOT/upload/目录的权限配置;
  • 更关键的是联系隐含约束:E-R 图中“旅游线路”与“旅游景点”之间是多对多关系(一条线路含多个景点,一个景点可出现在多条线路),但逻辑设计中并未生成中间关联表t_line_jingdian,而是将景点 ID 列表以逗号分隔字符串存入t_xianlu表的jingdian_ids字段(文档未明说但代码可验证)。这种反范式设计牺牲了查询效率,却极大简化了线路编辑的前端实现——这是典型“业务优先于理论”的权衡。

提示:不要急于建表。先用纸笔重画 E-R 图,把文档中模糊的“联系方式”“发布时间”等字段标注来源(是用户输入?系统自动生成?还是管理员手动填写?),这能提前暴露 70% 的后期字段空值异常。

2.2 逻辑建表:SQL Server 2012 的 NOT NULL 约束,是第一道数据防线

根据文档表结构说明,6 张表全部采用NOT NULL主键(如t_user.user_id INT NOT NULL PRIMARY KEY),但非主键字段的空值约束极不统一。以t_user表为例:

字段名数据类型允许空实际业务含义风险点
user_emailvarchar(20)否电子邮箱用户注册时未强制填写,但 DB 层不允许为空 → 必须设默认值''或修改建表语句
user_addressvarchar(50)否住址地址字段长度 50 字符,无法容纳“北京市朝阳区建国路87号华贸中心3座28层”类长地址 → 后期需ALTER TABLE t_user ALTER COLUMN user_address varchar(100)

真正关键的建表脚本,必须补全文档缺失的细节。以下是t_jingdian(旅游景点表)的完整可执行 SQL(适配 SQL Server 2012):

-- 创建旅游景点表 CREATE TABLE t_jingdian ( Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, -- 自增主键,文档未说明但代码依赖 Name VARCHAR(50) NOT NULL, Dizhi VARCHAR(100) NOT NULL, -- 扩展至100字符,避免截断 Menpiao VARCHAR(50) NOT NULL DEFAULT '免费', -- 设默认值,解决空值插入失败 Jieshao TEXT NOT NULL, -- 改用TEXT支持长文本介绍 Fujian VARCHAR(200) NOT NULL DEFAULT '', -- 路径最长支持200字符,含子目录 CreateTime DATETIME NOT NULL DEFAULT GETDATE() -- 补充创建时间,用于排序 ); GO -- 添加索引:景点名称模糊查询高频,需加速 CREATE INDEX IX_t_jingdian_Name ON t_jingdian(Name); GO

参数说明:

  • IDENTITY(1,1)是 SQL Server 特有自增语法,替代 MySQL 的AUTO_INCREMENT,MyEclipse 连接时 JDBC URL 必须包含;selectMethod=cursor参数才能正确读取自增 ID;
  • TEXT类型虽在新版 SQL Server 中被VARCHAR(MAX)替代,但 Tomcat 6.0 + JDBC 3.0 驱动对VARCHAR(MAX)兼容性差,坚持用TEXT可避免java.sql.SQLException: Invalid column type;
  • DEFAULT GETDATE()确保每条记录有时间戳,否则SELECT * FROM t_jingdian ORDER BY CreateTime DESC将因 NULL 值导致排序混乱。

2.3 连接原理:JDBC 驱动不是复制粘贴,是版本锁死的生死线

文档只写“使用 SQL Server 2012”,但未指定 JDBC 驱动版本。实测发现:

  • 若使用sqljdbc4.jar(支持 JDBC 4.0),在 Tomcat 6.0(仅支持 Servlet 2.5)下会抛出UnsupportedClassVersionError(JVM 版本不匹配);
  • 若使用sqljdbc.jar(JDBC 3.0),则PreparedStatement.setBlob()方法不可用,但本系统未操作 BLOB,故安全;
  • 必须使用的驱动:sqljdbc.jar(文件大小约 380KB),放置于tomcat/lib/目录,且web.xml中<resource-ref>配置必须与之匹配:
<!-- web.xml 中的数据源引用 --> <resource-ref> <description>SQL Server DataSource</description> <res-ref-name>jdbc/TravelDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>

对应的context.xml(位于tomcat/conf/或WebRoot/META-INF/)配置:

<!-- context.xml --> <Resource name="jdbc/TravelDB" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" username="sa" password="your_password" driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver" url="jdbc:sqlserver://localhost:1433;databaseName=TravelDB;characterEncoding=UTF-8"/>

关键参数说明:

  • characterEncoding=UTF-8:解决中文乱码的核心,必须显式声明,不能依赖服务器默认编码;
  • maxActive="100":Tomcat 6.0 默认连接池最大活跃连接数为 100,若并发超限会阻塞请求,需根据服务器内存调整;
  • driverClassName必须与sqljdbc.jar内部META-INF/MANIFEST.MF文件中的Implementation-Title严格一致,任何拼写错误都会导致ClassNotFoundException。

3. JSP + Servlet 的原始 MVC:没有 Spring 的年代,控制层如何不变成面条代码

3.1 页面层:JSP 不是 HTML 加 Java,是职责分离的战场

系统前台大量使用<%= %>和<% %>脚本片段,但真正的分层体现在include指令的运用。以index.jsp(系统主页)为例:

<!-- index.jsp --> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ include file="header.jsp" %> <!-- 复用顶部导航 --> <div class="main-content"> <jsp:include page="tour_list.jsp" /> <!-- 动态包含线路列表,可传参 --> </div> <%@ include file="footer.jsp" %> <!-- 复用底部版权 -->

逻辑说明:

  • <%@ include %>是静态包含,在 JSP 编译期将header.jsp内容直接插入index.jsp,适用于不变化的 HTML 结构(如导航栏);
  • <jsp:include>是动态包含,在请求运行时执行tour_list.jsp并合并输出,支持通过<jsp:param>传递参数(如?type=hot),适合需要动态数据的模块;
  • pageEncoding="UTF-8"和contentType="text/html; charset=UTF-8"必须同时存在:前者告诉 JSP 容器以 UTF-8 解析.jsp文件本身(防源码乱码),后者告诉浏览器以 UTF-8 渲染 HTML(防页面乱码)。

3.2 控制层:Servlet 的doPost()里藏着所有业务分支

管理员登录功能由LoginServlet.java实现,其doPost()方法是典型的状态机:

// LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); // 获取表单字段 String password = request.getParameter("password"); // 1. 基础校验(防空提交) if (username == null || username.trim().equals("") || password == null || password.trim().equals("")) { request.setAttribute("error", "用户名或密码不能为空!"); request.getRequestDispatcher("login.jsp").forward(request, response); return; // 终止后续执行 } // 2. 查询数据库(DAO 层调用) UserDAO userDAO = new UserDAO(); User user = userDAO.findByUsername(username); // 3. 密码比对(明文存储!注意安全风险) if (user != null && user.getPassword().equals(password)) { // 4. 权限路由:根据 user.getType() 跳转不同首页 if ("admin".equals(user.getType())) { request.getSession().setAttribute("admin", user); // 存入 Session response.sendRedirect("admin/index.jsp"); } else { request.getSession().setAttribute("member", user); response.sendRedirect("member/index.jsp"); } } else { request.setAttribute("error", "用户名或密码错误!"); request.getRequestDispatcher("login.jsp").forward(request, response); } }

参数说明与踩坑点:

  • request.getParameter()返回null当字段不存在,必须用== null判断,不能用"".equals(),否则 NPE;
  • response.sendRedirect()是客户端重定向,URL 变化且原request对象丢失;request.getRequestDispatcher().forward()是服务器端转发,URL 不变且request属性可传递;
  • 明文密码存储:user.getPassword().equals(password)证明系统未做密码加密,这是严重安全缺陷,但符合 2012 年代教学系统特征——修复方案是引入MessageDigestSHA-256 加盐,但需同步修改注册逻辑。

3.3 模型层:DAO 不是工具类,是数据库方言的翻译器

UserDAO.java的findByUsername()方法揭示了 JDBC 操作的原始细节:

public User findByUsername(String username) { String sql = "SELECT * FROM t_user WHERE user_name = ?"; User user = null; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); // 从连接池获取连接 pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); // 防 SQL 注入:用 ? 占位符 rs = pstmt.executeQuery(); if (rs.next()) { user = new User(); user.setId(rs.getInt("user_id")); user.setUsername(rs.getString("user_name")); user.setPassword(rs.getString("user_pw")); // 注意字段名是 user_pw,非 password user.setType(rs.getString("user_type")); // 文档未提此字段,但代码必需 } } catch (SQLException e) { e.printStackTrace(); // 生产环境应记录日志,非打印堆栈 } finally { DBUtil.close(rs, pstmt, conn); // 必须关闭资源,否则连接泄漏 } return user; }

关键逻辑:

  • DBUtil.getConnection()封装了 JNDI 查找数据源的逻辑,DBUtil.close()按ResultSet → PreparedStatement → Connection逆序关闭,防止NullPointerException;
  • rs.getString("user_pw")字段名与数据库t_user表的user_pw列完全一致,任何大小写或下划线错误都会返回null;
  • user_type字段是权限控制核心,但文档表结构未列出,需从admin/login.jsp的隐藏域<input type="hidden" name="type" value="admin">反推存在。

4. 权限隔离与中文乱码:两个让新手崩溃一整天的「经典陷阱」

4.1 权限控制:不是靠 session.setAttribute("role","admin"),而是 URL 拦截链

系统未使用 Filter 实现全局权限拦截,而是采用页面级硬编码校验。以管理员后台入口admin/index.jsp为例:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% // 强制校验:无 admin session 则跳转登录页 if (session.getAttribute("admin") == null) { response.sendRedirect("../login.jsp?redirect=admin/index.jsp"); return; } %> <!DOCTYPE html> <html> <head><title>管理员首页</title></head> <body> 欢迎管理员:<%= ((User)session.getAttribute("admin")).getUsername() %> </body> </html>

现象 → 原因 → 解决:

  • 现象:直接在浏览器输入http://localhost:8080/admin/index.jsp能访问,但显示欢迎管理员:null;
  • 原因:session.getAttribute("admin")为null,但return后仍执行了 HTML 输出,导致页面部分渲染;
  • 解决:在response.sendRedirect()后必须加return,且login.jsp需解析redirect参数并登录后跳转:
    <!-- login.jsp 中处理 redirect --> String redirect = request.getParameter("redirect"); if (redirect != null && !redirect.isEmpty()) { session.setAttribute("redirect_after_login", redirect); }
    登录成功后:
    String redirect = (String) session.getAttribute("redirect_after_login"); if (redirect != null) { response.sendRedirect(redirect); session.removeAttribute("redirect_after_login"); } else { response.sendRedirect("index.jsp"); }

4.2 中文乱码:不是改一个地方,是 HTTP 请求全链路的字符集对齐

乱码问题在t_jingdian表插入中文时集中爆发。根本原因是四层编码未对齐:

  1. 浏览器层:HTML 表单未声明accept-charset="UTF-8";
  2. 容器层:Tomcatserver.xml的Connector未设置URIEncoding="UTF-8";
  3. 应用层:request.setCharacterEncoding("UTF-8")未在getParameter()前调用;
  4. 数据库层:SQL Server 2012 数据库排序规则非Chinese_PRC_CI_AS。

避坑清单(亲测有效):

  • 现象:INSERT INTO t_jingdian(Name) VALUES('故宫')在 SQL Server Management Studio 中显示????;
    原因:数据库排序规则为SQL_Latin1_General_CP1_CI_AS(默认英文),不支持中文;
    解决:重建数据库时指定排序规则:

    CREATE DATABASE TravelDB COLLATE Chinese_PRC_CI_AS;

    或修改现有数据库(需重启服务):

    ALTER DATABASE TravelDB COLLATE Chinese_PRC_CI_AS;
  • 现象:JSP 表单提交后,request.getParameter("Name")返回???;
    原因:Tomcatserver.xml的Connector缺少URIEncoding;
    解决:修改tomcat/conf/server.xml,在<Connector>标签中添加:

    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 关键! -->
  • 现象:response.getWriter().print("中文")在浏览器显示乱码;
    原因:response.setContentType()未设置字符集;
    解决:在 Servlet 中PrintWriter out = response.getWriter();前添加:

    response.setContentType("text/html;charset=UTF-8");
  • 现象:<%= new String(request.getParameter("Name").getBytes("ISO-8859-1"), "UTF-8") %>临时解码成功,但代码丑陋;
    原因:GET 请求参数经 URL 编码,Tomcat 默认用ISO-8859-1解码;
    解决:统一用 POST 提交,或在web.xml中配置CharacterEncodingFilter(Tomcat 6.0 需手动实现)。

5. 从 MyEclipse 8.5 到本地运行:环境搭建不是点下一步,是版本考古现场

5.1 开发环境:MyEclipse 8.5 的 JDK 锁定与 Tomcat 6.0 兼容性

MyEclipse 8.5 仅支持 JDK 1.6(Java 6),强行使用 JDK 1.8 会导致:

  • Build path specifies execution environment J2SE-1.5. There are no JREs installed in the workspace that match this environment错误;
  • org.apache.jasper.JasperException: Unable to compile class for JSP编译失败。

正确配置步骤:

  1. 下载 JDK 1.6u45(最后稳定版),安装路径不含空格(如C:\Java\jdk1.6.0_45);
  2. MyEclipse →Window → Preferences → Java → Installed JREs→ Add → Standard VM → JRE home 指向 JDK 1.6 目录;
  3. 项目右键 →Properties → Java Build Path → Libraries→ 移除旧 JRE,Add Library → JRE System Library → 选择 JDK 1.6;
  4. Servers视图 → 右键 Tomcat 6.0 →Properties → Runtime Environment→ 选择 JDK 1.6。

注意:MyEclipse 8.5 内置 Maven 插件不支持pom.xml,所有依赖(sqljdbc.jar,jstl.jar)必须手动放入WebRoot/WEB-INF/lib/。

5.2 依赖包:三个 JAR 决定系统能否启动

系统运行依赖以下 JAR 包,缺一不可:

  • sqljdbc.jar:SQL Server JDBC 驱动(JDBC 3.0),放WEB-INF/lib/;
  • jstl.jar+standard.jar:JSP 标准标签库,用于<c:if>、<c:forEach>等标签,放WEB-INF/lib/;
  • commons-fileupload-1.2.2.jar+commons-io-2.0.1.jar:文件上传组件(用于景点图片上传),放WEB-INF/lib/。

验证方法:启动 Tomcat 后查看控制台日志,出现以下三行即成功:

INFO: Deploying web application archive travel.war INFO: Initializing Spring FrameworkServlet 'spring' INFO: Server startup in 2345 ms

若出现java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config,说明jstl.jar版本不匹配(必须用 JSTL 1.1,非 1.2)。

5.3 部署调试:WAR 包不是扔进 webapps 就完事

MyEclipse 8.5 默认以exploded方式部署(解压目录),但生产环境需 WAR 包。生成 WAR 步骤:

  1. 项目右键 →Export → WAR file;
  2. Web project选中项目,Destination设置路径(如D:\travel.war);
  3. 关键勾选:Export source files(否)、Export Java source files and resources(否)、Compress the contents of the WAR file(是);
  4. 点击 Finish。

部署后必做三件事:

  • 检查tomcat/webapps/travel/WEB-INF/web.xml中<welcome-file-list>是否为:
    <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
  • 访问http://localhost:8080/travel/,若跳转到login.jsp且无 404,说明部署成功;
  • 查看tomcat/logs/catalina.out,确认无SQLException: Cannot create PoolableConnectionFactory(数据库连接失败)。

6. 验证与加固:让这个“老古董”在现代开发流程中活下来

6.1 功能验证清单:用 5 分钟跑通核心链路

不要试图一次性测试所有模块。按优先级执行以下 5 个原子操作,每个成功即证明对应层正常:

步骤操作预期结果失败定位点
1访问http://localhost:8080/travel/显示index.jsp,顶部导航栏完整web.xml欢迎文件配置 /index.jsp路径
2点击“管理员登录”,输入admin/123456跳转至admin/index.jsp,显示欢迎语LoginServlet权限判断 /t_user表数据
3在管理员后台添加景点:名称=西湖,地址=杭州t_jingdian表新增记录,Name字段为西湖数据库排序规则 /request.setCharacterEncoding()
4普通用户注册:用户名=test,密码=123t_user表新增记录,user_type='member'RegisterServlet逻辑 /user_type默认值
5普通用户登录后预订线路t_order表(若存在)或t_xianlu表更新状态字段订单表结构 /OrderServlet事务控制

技巧:每次操作后,用 SQL Server Management Studio 执行SELECT TOP 10 * FROM t_jingdian,直接观察数据是否落库,比看页面更快。

6.2 安全加固:三个最低成本改造,让系统脱离“教学演示”范畴

这个系统最大的技术债务是明文密码 + 无 CSRF 防护 + SQL 注入风险。无需重写,只需三处修改:

1. 密码加盐哈希(5 分钟)
修改RegisterServlet.java的注册逻辑:

// 替换原密码存储 String salt = UUID.randomUUID().toString().replace("-", ""); String hashedPassword = DigestUtils.sha256Hex(password + salt); user.setPassword(hashedPassword); user.setSalt(salt); // 需为 t_user 表新增 salt 字段

对应LoginServlet.java登录验证:

String inputHash = DigestUtils.sha256Hex(inputPassword + user.getSalt()); if (inputHash.equals(user.getPassword())) { ... }

使用commons-codec-1.9.jar(已包含在 MyEclipse 8.5 中),DigestUtils是 Apache Commons Codec 工具类。

2. 防 CSRF(3 分钟)
在login.jsp表单中添加隐藏域:

<input type="hidden" name="token" value="<%= session.getAttribute("token") != null ? session.getAttribute("token") : "" %>" />

在LoginServlet.doPost()开头验证:

String token = request.getParameter("token"); if (token == null || !token.equals(session.getAttribute("token"))) { request.setAttribute("error", "非法请求,请刷新页面重试"); request.getRequestDispatcher("login.jsp").forward(request, response); return; } session.setAttribute("token", UUID.randomUUID().toString()); // 生成新 token

3. 防 SQL 注入(2 分钟)
检查所有Statement用法(如admin/jingdian_del.jsp中的删除逻辑),强制替换为PreparedStatement:

// 错误:Statement stmt = conn.createStatement(); // stmt.executeUpdate("DELETE FROM t_jingdian WHERE Id=" + id); // 正确: String sql = "DELETE FROM t_jingdian WHERE Id = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, Integer.parseInt(id)); pstmt.executeUpdate();

从那以后我每次接手老系统,第一件事就是 grep 所有Statement和getParameter(),然后花 15 分钟打上这三个补丁。它不会让系统变成金融级安全,但足以让扫描器不再报高危漏洞,也让甲方爸爸的安全部门点头——毕竟,一个能跑通、有数据、没明显漏洞的 Java Web 系统,在很多中小旅游公司,就是他们真实的生产环境。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询