基于JSP的旅游管理系统:B/S架构、事务与权限实战解析

发布时间:2026/9/18 1:58:27
基于JSP的旅游管理系统:B/S架构、事务与权限实战解析 简介基于Java的旅游管理系统设计与实现文档是一份面向具备Java Web开发基础者的毕业设计参考材料。系统以B/S架构为基础使用JSP构建前端、SqlServer2012管理后台数据开发环境为MyEclipse8.5与Tomcat6.0核心功能覆盖旅游景点管理、旅游线路管理、在线预订、网站论坛及公告管理等并针对管理员和会员用户提供区分化操作平台。文档完整阐述了课题背景、需求可行性分析技术、经济、操作、法律四个维度、数据库设计与功能模块实现过程并配有系统用户用例图注重用户体验与数据安全通过权限管理与模块化设计保障系统的稳定高效。资源包内仅包含1个docx文档压缩后大小约934KB便于随时阅读与二次修改。目前已有67人学习浏览对需要完成Java Web方向课程设计、毕业设计或了解旅游信息系统设计流程的开发者均具有实用参考价值。1. 基于 JSP 的旅游管理系统B/S 架构为什么还在被选大多数课程设计和毕设选题里旅游管理系统都是常客。我拆过的这套基于 JSP 的旅游管理系统技术栈看起来不新——JSP Servlet SqlServer2012 Tomcat6.0但它把 B/S 架构的请求-响应模型、JDBC 持久层、会话与权限控制、订单事务完整地串在了一条线上。系统里有两类用户管理员维护景点、线路、公告、会员普通用户在线浏览、预订线路、论坛发帖。对刚接触 Java Web 的人来说它是理解「前端页面-后端处理-数据库存储」三层落地的低成本样本对有经验的开发者它也是一个复习 Servlet 生命周期和 SqlServer 事务边界的现成案例尤其是 SqlServer2012 引入的 OFFSET FETCH 分页语法和 MySQL 的习惯差异值得单独记一笔。2. 持久层设计SqlServer2012 表结构与 JDBC 连接封装2.1 从 E-R 图落成六张业务表原文已经给出了普通用户和景点两张表的结构剩下的线路、预订、公告、帖子需要自己补。先明确实体关系用户与线路之间是多对多的预订关系所以不能把线路直接挂在用户表下而是拆出一张订单表公告和帖子相对独立各自一张表管理员单独建表不和会员混合存储。这样设计遵守了关系表的基本范式也方便权限控制时按表查询。表名职责关键字段t_user会员用户user_id, user_name, user_pw, user_tel, user_emailt_admin管理员admin_id, login_name, login_pwt_jingdian旅游景点id, name, dizhi, menpiao, jieshao, fujiant_line旅游线路id, name, price, start_date, contact, tel, publish_timet_order线路预订关系order_id, user_id, line_id, order_timet_gonggao系统公告id, title, content, publish_timet_bbs论坛帖子id, title, content, user_id, publish_timeSqlServer2012 建表脚本如下重点看主键、外键和默认值三个位置。CREATE TABLE t_user ( user_id INT IDENTITY(1,1) PRIMARY KEY, user_name NVARCHAR(50) NOT NULL UNIQUE, user_pw NVARCHAR(50) NOT NULL, user_realname NVARCHAR(50), user_sex NVARCHAR(2) DEFAULT N男, user_tel NVARCHAR(20), user_email NVARCHAR(50), user_address NVARCHAR(100) ); CREATE TABLE t_line ( id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, start_date DATETIME, contact NVARCHAR(20), tel NVARCHAR(20), publish_time DATETIME DEFAULT GETDATE(), status INT DEFAULT 1 ); CREATE TABLE t_order ( order_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL REFERENCES t_user(user_id), line_id INT NOT NULL REFERENCES t_line(id), order_time DATETIME DEFAULT GETDATE() );这段建表脚本把关键约束都放在数据库层处理UNIQUE 约束保证用户名不重复外键 REFERENCES 让订单表不会指向不存在的用户或线路GETDATE() 默认值避免 Java 端手工拼当前时间。实际操作中我一般顺手给 t_line 增加 status 字段0 表示下架、1 表示可预订替代直接删除线路记录这样历史订单在关联查询时不会因为线路被删而出现空引用。price 用 DECIMAL(10,2) 而不是 FLOAT到结算时不会出现小数点误差。字段全部用 NVARCHAR 而不是 VARCHAR这也是配合 SqlServer 中文存储的常见做法能减少乱码问题出现的概率。2.2 JDBC 驱动连接的关键参数这个项目使用 SqlServer2012连接层常见有两条路微软官方驱动 sqljdbc4.jar驱动类是 com.microsoft.sqlserver.jdbc.SQLServerDriver或者开源社区的 jtds。我的经验是 Tomcat6.0 SqlServer2012 优先用官方驱动对 2012 的版本兼容性更完整jtds 的优势在字符集处理上更宽松但如果数据库已经是 NVARCHAR 存储这个优势并不重要。公用连接类可以封装成下面这样。package com.tour.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 微软官方驱动类名不能写错 private static final String DRIVER com.microsoft.sqlserver.jdbc.SQLServerDriver; // 1433 为 SqlServer 默认端口tour_db 是实际数据库名 private static final String URL jdbc:sqlserver://localhost:1433;databaseNametour_db; private static final String USER sa; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError( 缺少 sqljdbc4.jar请放入 WEB-INF/lib 后重启); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明静态块里 Class.forName 负责把驱动类加载进 JVM这一步失败通常不是驱动没放对位置就是 JDK 与驱动版本不匹配getConnection 每次返回一个物理连接在并发不高的管理后台够用若要上线到高并发环境需要替换成连接池。参数说明URL 里的 databaseName 指定实例内的库名localhost 换成远程 IP 即可部署到独立数据库服务器USER 和 PASSWORD 建议单独创建一个专用账号只授予 tour_db 的读写权限不要直接用 sa万一代码被反编译泄露的也是低权限账号。驱动 jar 必须放在 WEB-INF/lib 下Tomcat 启动时才会加载到应用级类加载器里放在 JDK 的 lib 目录反而容易出现版本冲突。2.3 中文乱码一个过滤器解决 80% 的问题原文专门列了一节讲中文乱码这在 SqlServer JSP 的老技术栈里确实是高频事故。乱码通常来自三个位置请求参数在 Tomcat 解析时用了 ISO-8859-1数据库字段用了 VARCHAR 而页面输入的是中文以及响应输出没有声明 UTF-8。三者各管一段最省事的方式是用过滤器统一设置。package com.tour.filter; import javax.servlet.*; import java.io.IOException; public class EncodingFilter implements Filter { private String charset UTF-8; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(charset); response.setCharacterEncoding(charset); response.setContentType(text/html;charset charset); chain.doFilter(request, response); } }在 web.xml 里把过滤器映射到/*让它拦截所有请求。说明request.setCharacterEncoding 只对 POST 请求体生效GET 的 queryString 编码需要在 Tomcat 的 server.xml 里给 Connector 加 URIEncodingUTF-8数据库那头依赖建表时的 NVARCHAR 字段三个环节保持一致基本不会再看到乱码。这个过滤器也解释了为什么项目里很多 JSP 页面顶部都有 pageEncodingUTF-8页面、请求、数据库三者对齐问题才能根治。如果过滤器配了还是乱码下一步检查 JSP 页面本身的物理编码是不是 UTF-8MyEclipse8.5 默认新建页面的编码需要手动改掉这个坑比过滤器更容易被忽略。3. 双角色权限与景点、线路管理的实现路径3.1 登录会话里的角色识别系统把用户分成管理员和普通会员权限边界非常清楚管理员维护景点、线路、公告、会员普通用户只有浏览、查询、预订和论坛发帖。落实到代码上第一步就是登录后的角色识别。原文给出了普通用户和管理员两张用例图实现时常见做法是两个登录入口共用一个 LoginServlet前端通过 role 参数区分查哪张表。用户登录成功后Session 里至少放三样东西用户 ID、用户名、角色。后面的 JSP 页面用角色决定菜单渲染服务端用角色决定是否放行管理操作。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String role request.getParameter(role); String username request.getParameter(username); String password request.getParameter(password); String sql member.equals(role) ? SELECT user_id, user_name FROM t_user WHERE user_name? AND user_pw? : SELECT admin_id, login_name FROM t_admin WHERE login_name? AND login_pw?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { HttpSession session request.getSession(); session.setAttribute(userId, rs.getInt(1)); session.setAttribute(userName, rs.getString(2)); session.setAttribute(role, role); response.sendRedirect(admin.equals(role) ? admin/index.jsp : index.jsp); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } catch (SQLException e) { throw new ServletException(登录查询失败, e); } }说明这里用 PreparedStatement 而不是 Statement 拼字符串是登录接口的底线参数化查询能直接避免 SQL 注入。权限矩阵后续维护时要注意前台只做展示管理员模块的所有 JSP 都放在 /admin 目录下并且每个管理员 Servlet 开头都要校验 session 里的 role 是不是 admin只靠页面隐藏入口并不可靠。常见做法是抽一个公共方法public static boolean isAdmin(HttpSession session) { return session ! null admin.equals(session.getAttribute(role)); }这个方法放在 BaseServlet 或者工具类里管理员模块的 doGet 和 doPost 第一行就判断不通过就重定向到登录页。别小看这一步课程设计里最常见的扣分点就是普通用户直接访问 /admin/line_list.jsp 也能看到管理页面权限只做了客户端隐藏没做服务端校验。普通用户登录后也要做同样的角色判断防止管理员 Session 被误用。3.2 景点管理Tomcat6 下的图片上传与保存路径景点实体包含名称、地址、门票价格、介绍和图片 fujian。管理端对景点做增删改查图片上传是里面最绕的一环。Tomcat6.0 是 Servlet 2.5 规范没有后来 Servlet 3.0 的 Part API所以常见方案是引入 commons-fileupload 处理 multipart 表单同时需要 commons-io 依赖。DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(2 * 1024 * 1024); // 单文件不超过 2MB ListFileItem items upload.parseRequest(request); String uploadDir getServletContext().getRealPath(/upload); File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); // 首次上传时自动创建目录 } for (FileItem item : items) { if (item.isFormField()) { // 普通表单字段如景点名称、地址、门票 } else { String fileName System.currentTimeMillis() _ new File(item.getName()).getName(); item.write(new File(dir, fileName)); // 数据库存 /upload/xxx.jpg页面访问时拼接项目根路径 } }说明文件名用毫秒时间戳加原始名拼能避免两个用户上传同名文件互相覆盖getRealPath(/upload) 拿到的是部署后 webapp 下的物理路径所以文件会落在 webapps/tour/upload 下以后再部署前要记得把 upload 目录备份出来否则 Tomcat 重装后图片会丢。数据库里存相对路径展示时用 request.getContextPath() 补全这样部署到带域名前缀的服务器也不用改数据。景点列表的模糊查询是管理端常用功能。按名称或地址模糊匹配SQL 仍然参数化String keyword request.getParameter(keyword); String sql SELECT * FROM t_jingdian WHERE name LIKE ? OR dizhi LIKE ? ORDER BY id DESC; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ps.setString(2, % keyword %);说明LIKE 的通配符是拼在参数值里的SQL 结构保持固定PreparedStatement 仍然有效如果直接把通配符拼进 Statement等于把注入漏洞放回去了。这个知识点是 java 基础里最常被面试官追问的点写项目时记住一个原则凡是带用户输入的条件永远只走参数绑定。3.3 普通用户的景点查询与详情展示前台页面按板块组织首页放推荐景点和最新公告景点列表显示门票与简介详情页再展示完整介绍和图片。普通用户的查询逻辑与后台共用同一套 SQL只是不提供删除和修改入口。这里提示一点分页最好放在查询 SQL 里而不是先查出全表再在 Java 里截取 List后者在景点数据过千后内存和响应时间都会劣化。这也是把查询封装成 DAO 方法而不是到处写裸 JDBC 的理由至少要把连接获取、预编译、结果集遍历这几段固定代码收敛到一个公共类里。4. 线路预订与论坛模块事务边界和分页查询4.1 预订动作为什么要放在一个事务里业务上用户预订一条线路包含两个动作在 t_order 里插入一条订单记录同时更新 t_line 的预订状态或预订人数。如果插入订单成功而更新线路失败用户会看到自己已经预订成功管理员却看不到对应的人次变化更麻烦的是下次并发预订时余位判断会出错。两个 SQL 必须作为一个原子操作提交这就是事务边界的由来。public boolean bookLine(int userId, int lineId) { String checkSql SELECT id FROM t_line WITH (UPDLOCK) WHERE id? AND status1; String insertSql INSERT INTO t_order(user_id, line_id) VALUES(?,?); String updateSql UPDATE t_line SET order_count order_count 1 WHERE id?; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement check conn.prepareStatement(checkSql); PreparedStatement insert conn.prepareStatement(insertSql); PreparedStatement update conn.prepareStatement(updateSql)) { check.setInt(1, lineId); ResultSet rs check.executeQuery(); if (!rs.next()) { conn.rollback(); return false; // 线路不存在或已下架 } insert.setInt(1, userId); insert.setInt(2, lineId); insert.executeUpdate(); update.setInt(1, lineId); update.executeUpdate(); conn.commit(); return true; } catch (Exception e) { conn.rollback(); return false; } } catch (SQLException e) { return false; } }逻辑说明conn.setAutoCommit(false) 以后commit 之前的所有 executeUpdate 都在同一个数据库事务里rollback 会撤销 insert 和 update 两边的改动。这里有两个细节容易忽略checkSql 里的 WITH (UPDLOCK) 是 SqlServer 的行锁提示它在读取线路记录时直接加更新锁防止两个请求同时读到 status1 然后一起下单这是并发场景下的常见做法单机学习项目可以不写但理解了不亏。try-with-resources 的写法在处理 JDBC 资源时比手动 finally close 少写很多样板代码前提是 JDK7 以上MyEclipse8.5 默认配置下如果用 JDK1.6 则需要退回传统 finally 写法。4.2 订单表扩展与预订列表预订功能上线后通常会遇到两个新需求用户查看“我的订单”管理员查看“全部订单”。前者要根据 userId 过滤后者要连表查出用户名和线路名。连接查询时如果线路被物理删除订单会变成孤儿记录所以前面建议线路用 status 下架而不是 delete这条设计在联表查询时就体现出价值了。SELECT o.order_id, u.user_name, l.name AS line_name, l.price, o.order_time FROM t_order o JOIN t_user u ON o.user_id u.user_id JOIN t_line l ON o.line_id l.id WHERE o.user_id 2 ORDER BY o.order_time DESC;说明这个查询把用户表和线路表 join 进来一次性拿到订单列表需要的所有展示字段避免在 Java 里再循环补查用户名和线路名。WHERE 条件按当前登录用户的 userId 传入管理员版本去掉 WHERE 或按线路名过滤即可。price 字段直接从线路表取注意不要在订单表里冗余一份价格快照——如果线路价格调整历史订单显示的价格也会跟着变业务上是否需要价格快照要提前和需求方确认。4.3 论坛分页SqlServer2012 的 OFFSET FETCH 写法论坛帖子是典型的持续增长数据前端列表必须分页。SqlServer2012 之前的分页要写 ROW_NUMBER()2012 版本引入了 OFFSET FETCH和 MySQL 的 LIMIT 在思路上非常接近也更容易被 Java 端理解。SELECT * FROM t_bbs ORDER BY publish_time DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY;Java 端传参时两个问号分别对应 (currentPage - 1) * pageSize 和 pageSize。说明OFFSET 是跳过的行数FETCH NEXT 是取多少行ORDER BY 必须写因为分页依赖稳定的排序如果帖子发布时间相同建议在 ORDER BY 里加一个 id DESC 作为次级排序避免同一页内顺序抖动。总数查询用 SELECT COUNT(*)拿到 totalCount 后按 pageSize 向上取整计算总页数前端页码循环输出分页才算闭环limit 参数、总页数计算、首页上一页下一页链接三个部分要同步写对。论坛发帖本身绕不开用户关联插入帖子时要把 session 里的 userId 一起写入而不是让前端传 user_name这样能防止用户伪造身份发帖。显示帖子时再 join 一次用户表取昵称。公告模块更简单只要一条按时间倒序的查询可以做成前台首页的右侧栏这个模块的价值更多体现在首页信息聚合而不是技术实现。5. 部署验证Tomcat6.0 下的排错顺序与安全基线课程设计或内部项目里很多问题出在部署环节而不是代码本身。从 MyEclipse8.5 导出的 Web 项目要部署到独立 Tomcat6.0正确顺序是项目右键 Export 成 WAR 文件放到 Tomcat 的 webapps 目录启动 bin/startup.bat等 webapps 下自动出现同名目录再访问项目路径。手动复制项目目录也可以但 WAR 打包能确保 classes 和 lib 按原结构归档。5.1 运行期典型报错与处理报错信息原因处理方式ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriversqljdbc4.jar 没放入 WEB-INF/lib把 jar 放进去后重新部署并重启 TomcatThe server selected protocol version TLS10 is not acceptedSqlServer 与 JDBC 驱动 TLS 版本不匹配升级 sqljdbc 驱动或连接 URL 加 encryptfalse;trustServerCertificatetrueHTTP Status 404项目路径与访问路径不一致确认访问 http://localhost:8080/tour/项目名大小写要对中文全部显示为问号数据库字段是 VARCHAR 且页面不是 UTF-8统一 NVARCHAR 过滤器 URIEncoding 三处对齐Port 8080 already in useTomcat 端口被占用改 conf/server.xml 的 Connector 端口或结束占用进程其中 TLS10 这个报错在较新 JDK 下连旧版 SqlServer 时经常出现网上很多老教程没有提过遇到时先不要怀疑代码把驱动换成较新版本再试一般能直接解决。5.2 沉淀下来的三个安全基线第一所有 SQL 一律 PreparedStatement 参数绑定上文登录、查询、分页都已经示范了这个写法权限管理页面不能用 Statement 拼接。第二密码不要明文存数据库入门项目可以先 MD5 加固定盐做摘要登录时对输入做同样摘要再比较进阶再换 SHA-256 加随机盐。第三上传目录 upload 要单独管理部署时设置只写不执行的权限并限制上传文件后缀白名单防止通过图片上传拿到可执行脚本这是 Tomcat 老版本最容易被人钻的空子。把这三个改动落进代码里的成本很低但能让这套旅游管理系统的安全基线从演示级别提到可交付级别。管理员权限校验、事务回滚、分页参数化这几处的写法也是 java 面试八股文里反复出现的考察点项目做完后值得回头再把这几个类单独读一遍。本文还有配套的精品资源点击获取