简介:基于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_email |
| t_admin | 管理员 | admin_id, login_name, login_pw |
| t_jingdian | 旅游景点 | id, name, dizhi, menpiao, jieshao, fujian |
| t_line | 旅游线路 | id, name, price, start_date, contact, tel, publish_time |
| t_order | 线路预订关系 | order_id, user_id, line_id, order_time |
| t_gonggao | 系统公告 | id, title, content, publish_time |
| t_bbs | 论坛帖子 | id, title, content, user_id, publish_time |
SqlServer2012 建表脚本如下,重点看主键、外键和默认值三个位置。
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;databaseName=tour_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 加 URIEncoding="UTF-8";数据库那头依赖建表时的 NVARCHAR 字段,三个环节保持一致,基本不会再看到乱码。这个过滤器也解释了为什么项目里很多 JSP 页面顶部都有 pageEncoding="UTF-8",页面、请求、数据库三者对齐,问题才能根治。如果过滤器配了还是乱码,下一步检查 JSP 页面本身的物理编码是不是 UTF-8,MyEclipse8.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 List<FileItem> 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 status=1"; 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 的行锁提示,它在读取线路记录时直接加更新锁,防止两个请求同时读到 status=1 然后一起下单,这是并发场景下的常见做法,单机学习项目可以不写,但理解了不亏。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.SQLServerDriver | sqljdbc4.jar 没放入 WEB-INF/lib | 把 jar 放进去后重新部署并重启 Tomcat |
| The server selected protocol version TLS10 is not accepted | SqlServer 与 JDBC 驱动 TLS 版本不匹配 | 升级 sqljdbc 驱动,或连接 URL 加 encrypt=false;trustServerCertificate=true |
| HTTP Status 404 | 项目路径与访问路径不一致 | 确认访问 http://localhost:8080/tour/,项目名大小写要对 |
| 中文全部显示为问号 | 数据库字段是 VARCHAR 且页面不是 UTF-8 | 统一 NVARCHAR + 过滤器 + URIEncoding 三处对齐 |
| Port 8080 already in use | Tomcat 端口被占用 | 改 conf/server.xml 的 Connector 端口或结束占用进程 |
其中 TLS10 这个报错在较新 JDK 下连旧版 SqlServer 时经常出现,网上很多老教程没有提过,遇到时先不要怀疑代码,把驱动换成较新版本再试,一般能直接解决。
5.2 沉淀下来的三个安全基线
第一,所有 SQL 一律 PreparedStatement 参数绑定,上文登录、查询、分页都已经示范了这个写法,权限管理页面不能用 Statement 拼接。第二,密码不要明文存数据库,入门项目可以先 MD5 加固定盐做摘要,登录时对输入做同样摘要再比较,进阶再换 SHA-256 加随机盐。第三,上传目录 upload 要单独管理,部署时设置只写不执行的权限,并限制上传文件后缀白名单,防止通过图片上传拿到可执行脚本,这是 Tomcat 老版本最容易被人钻的空子。
把这三个改动落进代码里的成本很低,但能让这套旅游管理系统的安全基线从演示级别提到可交付级别。管理员权限校验、事务回滚、分页参数化这几处的写法,也是 java 面试八股文里反复出现的考察点,项目做完后值得回头再把这几个类单独读一遍。
本文还有配套的精品资源,点击获取