ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java旅游信息管理系统开发实战:从JDBC到订单事务与并发控制

Java旅游信息管理系统开发实战:从JDBC到订单事务与并发控制 简介一份基于Java的旅游信息管理系统的毕业设计论文文档面向计算机相关专业学生和Web开发初学者适合用于毕业设计、课程设计或毕业答辩参考。全文按照标准论文结构撰写从项目背景与研究意义切入系统介绍了JavaScript、B/S架构和MySQL数据库等核心技术随后依次完成可行性分析、系统总体规划、模块设计、系统实体E-R图和数据库详细设计并围绕用户注册登录、旅游景点展示、民宿预订、旅游论坛以及后台管理等功能模块展开具体实现说明最后还给出了登录与功能测试内容覆盖了完整的软件工程流程。资源包为1个docx文件大小696KB内容清晰可直接阅读。目前已有6251人浏览学习获得了不错的学习反馈。借助这份文档既能快速掌握基于Java的Web旅游系统的设计思路与数据库建模方法也可学习如何规范撰写需求分析、概要设计、详细设计和测试等论文章节为独立完成类似项目提供有价值的参考。1. 基于Java的旅游信息管理系统绕不开的CRUD与预订事务课程设计清单里“基于Java的旅游信息管理系统”几乎是出现概率最高的题目之一。同一个题目在答辩现场能见到几十种做法有人用Swing拼桌面界面有人用JSP跑在Tomcat里还有人直接写成Spring Boot接口。真正的差异不在界面而在三个问题表结构怎么设计才能扛得住“用户—景点—线路—订单”这条业务链DAO层怎么组织才不把SQL散落得到处都是以及同一个热门线路被两个人同时下单时订单表会不会脏。这篇按一条常规落地路径拆解从技术选型、数据库表设计到订单事务和最后排错适合正在做Java课程设计、刚入门的后端开发者有五年经验的人也能在连接池参数、事务边界和Stream筛选这几个位置找到能直接抄走的写法。2. 技术选型与系统边界先定形式再定工具2.1 桌面端还是Web端两种交付形态的取舍旅游信息管理系统的标题里没有写“桌面”或“Web”所以第一步是自选交付形态。常见做法有两种Swing/JavaFX做桌面应用或者JSP/Servlet做传统Web应用。课程设计答辩时桌面端更容易演示——不用配容器、不用处理跨域双击就能跑Web端则更贴近“信息系统”这个词的日常语义也方便后续加登录会话和权限控制。选型上我给一张对照表按团队时间和部署环境决定维度Swing/JavaFX 桌面端JSP/Servlet Web端答辩演示双击运行依赖少需要Tomcat演示环境要配JDK和端口代码量界面代码多业务代码薄界面和业务能分层代码组织更清晰并发场景单用户为主事务好控制多用户并发订单预订更有说服力学习成本需要额外熟悉布局器需要理解HTTP、Session、请求周期我一般建议选Web端。旅游管理系统天然有多用户并发预订的场景桌面端很难讲清楚两个用户同时下单时你怎么保证票数不超卖而Web端配一个Servlet就能把线程池、数据库连接池和行锁串起来讲答辩证明显更扎实。2.2 数据访问层JDBC连接池为什么比MyBatis更稳数据访问层在这个系统里选什么直接决定你后面所有代码的写法。我的建议是JDBC 一个连接池不要上MyBatis。原因不是MyBatis不好而是课程设计型项目往往只需要四张表的增删改查用MyBatis要配Mapper、配XML、处理一二级缓存反而把“信息系统”的代码结构拉远了。JDBC裸写虽然啰嗦但每一步数据库交互都透明可控数据源换不换、事务提交回滚发生在哪里一眼能看穿。连接池用HikariCP目前最常见的Java连接池实现配置极少性能参数也稳定。核心参数只需要关注四个最大连接数、最小空闲连接数、连接超时时间和连接最大存活时间。下面这个JdbcUtils类就是整个系统的数据源基础import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; public class JdbcUtils { private static final HikariDataSource ds; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/tourism); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); // 同时最多10个连接 config.setMinimumIdle(2); // 空闲时至少保留2个 config.setConnectionTimeout(3000); // 3秒内拿不到连接直接失败 ds new HikariDataSource(config); } public static Connection getConnection() { return ds.getConnection(); } }这里有几个参数在Java课程设计里常被忽略maximumPoolSize不是越大越好10对这个小系统绰绰有余调大了反而让MySQL端连接数紧张connectionTimeout设为3秒的意义是当连接池被占满时新请求能快速失败并抛出SQLException而不是无限期等待。HikariCP内部通过动态代理封装了物理连接close()并不会真正关闭连接而是把连接归还到池里——理解了这一点后面写事务时就不会误关连接导致回滚失效。2.3 最小工程结构按包分层而不是按功能堆类定了JDBC路线之后工程结构我用下面这个经典分层。把实体类、DAO、Service、工具类分开后续无论是换连接池、加新查询还是接Web层都不需要动其他包。com.tourism ├── Main.java ├── dao/ # 数据访问层只写JDBC和SQL │ ├── ScenicSpotDao.java │ ├── RouteDao.java │ └── OrderDao.java ├── service/ # 业务层事务边界在这里 │ ├── QueryService.java │ └── BookingService.java ├── entity/ # 实体类字段与表一一对应 │ ├── User.java │ ├── ScenicSpot.java │ ├── Route.java │ └── Order.java └── util/ └── JdbcUtils.java实体类对应表结构DAO层只负责SQL执行和结果集转换Service层处理业务规则和事务Controller或界面代码只管接收参数和渲染。这样分完包之间依赖方向是单向的界面→Service→DAO→数据库。考试或答辩时被问到“你做了什么设计”能说出依赖方向这一条就比只会堆类的同行高一个身位。3. 数据库表设计与SQL落地四张表撑起整个系统3.1 表结构设计原则面向业务流程拆表而不是面向界面旅游信息管理系统的核心流程是用户注册登录→查看景点和线路→选择线路并预订→生成订单。按照这个流程最小表集合是四张用户表、景点表、线路表、订单表。很多课程设计会再加一张“线路-景点”关联表但考虑到一个线路最多包含三五个景点用字符串存景点ID列表就够用不必过度建模。设计时有一个常见误用把界面要展示的字段直接搬进表里。比如给线路表加一个“预订人数”列每次前端展示直接从这张表里读。问题在于预订人数和订单表里的记录是强关联数据两处更新一旦不同步报表和详情页数字就对不上。我的原则是能通过SQL聚合算出来的数据不落库必须保证单一数据源。3.2 建表SQL与字段说明下面是这套系统最核心的四张表建表语句我把容易解释清楚的字段全部加上备注CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 建议存SHA-256摘要, real_name VARCHAR(50) COMMENT 真实姓名选填, phone VARCHAR(20) COMMENT 手机号预订联系用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_scenic_spot ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 景点名称, city VARCHAR(50) NOT NULL COMMENT 所在城市查询高频字段, price DECIMAL(10,2) NOT NULL COMMENT 单人票价用DECIMAL不用FLOAT, description TEXT COMMENT 景点介绍允许为空 ); CREATE TABLE t_route ( id INT PRIMARY KEY AUTO_INCREMENT, route_name VARCHAR(100) NOT NULL COMMENT 线路名称, spot_ids VARCHAR(255) NOT NULL COMMENT 景点ID列表逗号分隔, total_price DECIMAL(10,2) COMMENT 汇总票价可程序计算, max_tickets INT NOT NULL DEFAULT 50 COMMENT 线路总票数上限 ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号时间戳随机数生成, user_id INT NOT NULL COMMENT 关联t_user.id, route_id INT NOT NULL COMMENT 关联t_route.id, ticket_count INT NOT NULL COMMENT 预订票数, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, total_amount DECIMAL(10,2) NOT NULL COMMENT 下单时的总金额, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_route (route_id), INDEX idx_user (user_id) );建表时有几个点需要单独说明。t_order的订单号用的是VARCHAR(32)而不是直接用自增ID因为答辩时如果展示页面订单号暴露自增ID很容易被看出数据量也容易被人直接遍历抓取订单。用“yyyyMMddHHmmss 四位随机数”拼一个不重复的订单号在课程设计里够用了。t_scenic_spot.price和t_route.total_price都用DECIMAL(10,2)这是Java的BigDecimal在数据库层的对应类型。用FLOAT或DOUBLE会在金额计算时产生精度丢失比如99.9元三条线路加总可能得到299.69999999999996这种结果用户界面直接显示会很尴尬。3.3 索引、外键与金额类型的设计取舍索引方面t_order上建了idx_route和idx_user两个普通索引对应高频查询按线路查订单总量、按用户查历史订单。t_scenic_spot.city也是典型查询条件但考虑到景点数据量小不加索引也没有问题如果景点上千条建议加上。外键方面我建议不建物理外键只保留逻辑关联。理由有两条一是课程设计项目里经常会出现“先删主表数据再清理子表”的操作物理外键会限制删除顺序业务代码里要额外处理异常二是四张表的数据量都很小数据库的完整性约束在这里带来的代码复杂度高于它解决问题的价值。订单表和用户表、线路表通过user_id、route_id保持逻辑关联删除数据时先删订单再删主表记录这一点在Service层控制。提示DECIMAL(10,2)的总精度是10位整数部分最多8位。旅游订单的金额在这个范围内完全够用但如果你以后扩展成酒店机票的大额订单记得把精度扩大。4. 核心代码实现DAO、筛选与订单事务4.1 DAO层把ResultSet转成安全的Java对象DAO层是整个系统里最容易写错的地方常见错误包括ResultSet取列时用数字索引、没有关闭资源、查询结果为空时直接返回null。下面这段ScenicSpotDao的写法是我在Java课程设计中反复验证过的标准写法public ListScenicSpot findByCity(String city) { String sql SELECT id, name, city, price, description FROM t_scenic_spot WHERE city ?; ListScenicSpot list new ArrayList(); try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, city); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { ScenicSpot spot new ScenicSpot(); spot.setId(rs.getInt(id)); // 按列名取值不按列号 spot.setName(rs.getString(name)); spot.setCity(rs.getString(city)); spot.setPrice(rs.getBigDecimal(price)); spot.setDescription(rs.getString(description)); list.add(spot); } } } catch (SQLException e) { throw new RuntimeException(按城市查询景点失败: city, e); } return list; }这段代码有几个细节值得展开。用rs.getInt(id)而不是rs.getInt(1)可以避免SQL里调整查询列顺序后导致Java代码取值错位这种错误在运行时表现为取出来的字段值全乱排查很费时间和Java数组越界异常一样属于低级但高频的失误。try-with-resources写法保证Connection、PreparedStatement、ResultSet三个资源无论正常还是异常都自动关闭不会把连接池里的连接泄漏掉。返回值用空集合而不是null调用方就不必每次都判空。4.2 Service层用集合容器和Stream完成筛选与统计查询聚合逻辑放在Service层Java的集合容器在这里价值最大。下面这段代码做两件事按价格区间筛选景点以及统计最受欢迎的3个城市。都用Stream实现整体比传统for循环更简洁public ListScenicSpot recommendByPriceRange( ListScenicSpot all, BigDecimal min, BigDecimal max) { return all.stream() .filter(s - s.getPrice().compareTo(min) 0) .filter(s - s.getPrice().compareTo(max) 0) .sorted(Comparator.comparing(ScenicSpot::getPrice)) .collect(Collectors.toList()); } public String[] topCities(ListScenicSpot all) { return all.stream() .collect(Collectors.groupingBy(ScenicSpot::getCity, Collectors.counting())) .entrySet().stream() .sorted(Map.Entry.String, LongcomparingByValue().reversed()) .limit(3) .map(Map.Entry::getKey) .toArray(String[]::new); }compareTo而不是或是因为BigDecimal之间的比较必须用compareToequals和关系运算符在这里都会有精度或对象复用问题。Collectors.groupingBy把List集合转成MapString, Longkey是城市value是景点数再经过entrySet().stream()二次排序取前3。最后的toArray(String[]::new)是Java 8以后List转数组的标准姿势比(String[]) list.toArray()安全不会抛ClassCastException。这段代码展示的“数据库查全量内存中筛选”方案在景点数据量小的系统里非常实用。如果直接写SQL来做价格区间和TOP N统计每次筛选都要拼字符串、改LIMIT参数而现在的实现里SQL只有最基础的全量查询所有动态条件都在Service层用集合操作完成可读性和可测性都更好。4.3 订单预订事务边界与并发控制订单预订是整个系统里技术含量最高的一段代码也是答辩时最值得展示的部分。核心问题有三个多个线程同时发起预订时怎么保证票数不超卖一个订单的多个数据库操作扣减票数、插入订单记录怎么保证原子性业务上出了问题怎么回滚。下面是一个带行锁和事务的预订方法关键逻辑我写在注释里public synchronized boolean createOrder(int userId, int routeId, int ticketCount) { String lockRouteSql SELECT max_tickets FROM t_route WHERE id ? FOR UPDATE; String sumTicketsSql SELECT COALESCE(SUM(ticket_count), 0) FROM t_order WHERE route_id ? AND pay_status IN (0,1); String insertOrderSql INSERT INTO t_order(order_no, user_id, route_id, ticket_count, pay_status, total_amount) VALUES(?,?,?,?,1,?); try (Connection conn JdbcUtils.getConnection()) { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); int maxTickets; int soldTickets; try (PreparedStatement lockPs conn.prepareStatement(lockRouteSql)) { lockPs.setInt(1, routeId); try (ResultSet rs lockPs.executeQuery()) { rs.next(); maxTickets rs.getInt(max_tickets); } } try (PreparedStatement sumPs conn.prepareStatement(sumTicketsSql)) { sumPs.setInt(1, routeId); try (ResultSet rs sumPs.executeQuery()) { rs.next(); soldTickets rs.getInt(1); } } if (maxTickets - soldTickets ticketCount) { conn.rollback(); return false; } try (PreparedStatement insertPs conn.prepareStatement(insertOrderSql)) { insertPs.setString(1, generateOrderNo()); insertPs.setInt(2, userId); insertPs.setInt(3, routeId); insertPs.setInt(4, ticketCount); insertPs.setBigDecimal(5, routePriceCache.get(routeId) .multiply(BigDecimal.valueOf(ticketCount))); insertPs.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { throw new RuntimeException(预订失败, e); } }逻辑说明分三层。第一层是SELECT ... FOR UPDATE这一步会锁住t_route中对应线路的行后面其他线程执行同样的查询会进入等待状态直到当前线程提交或回滚释放锁。这里的线程等待和synchronized关键字其实解决了不同层面的问题synchronized只能约束同一JVM内的并发而数据库行锁能约束多个应用实例之间的竞争。课程设计虽然只有一个JVM但把两层的差异讲清楚面试官会认为你理解了并发控制的全貌。第二层是setAutoCommit(false)和setTransactionIsolation(READ_COMMITTED)手动控制事务。这里设置成READ_COMMITTED是为了让SUM(ticket_count)在行锁保护下读到已提交的数据避免默认的REPEATABLE_READ下多个连接事务快照不一致导致统计数据偏移。注意getConnection()返回的是连接池的动态代理连接调用close()只会归还连接只有commit()或rollback()才真正结束事务所以后面的try-with-resources不会把事务截断出错。第三层是库存校验放在查询和插入之间。校验通过后才执行INSERT如果校验不通过直接rollback并返回false。maxTickets存在t_route表soldTickets通过订单表聚合得到两处数据都能对得上不会出现报表和实际票数不一致的情况。5. 编译参数、乱码与边界验证交付前最后一遍自测5.1 用一条命令链验证整个工程写完了代码先别启动先把编译关过了。Java课程设计里最常见的编译问题是编码默认控制台编码在Windows下是GBK代码文件如果是UTF-8javac直接编译会乱码甚至报错。确认整个工程是UTF-8编码后使用下面的命令编译全量代码mvn clean package -DskipTests -Dfile.encodingUTF-8 java -Dfile.encodingUTF-8 -jar target/tourism.jar如果不用Maven直接用javac也要带上编码参数javac -encoding UTF-8 -cp hikaricp.jar:mysql-connector-java.jar com/tourism/**/*.java-encoding UTF-8让编译器按UTF-8解析源码文件-Dfile.encodingUTF-8让JVM运行时的IO操作按UTF-8处理控制台输出和文件读写。这一步在Java基础面试里也常被问到答清楚能体现出对字符集的理解。5.2 两个高频报错源发行版17和中文乱码第一个报错是编译时提示“源发行版 17 需要目标发行版 17”这是JDK版本和Maven编译配置不匹配。IDE里默认用了一个新JDK跑Maven项目的pom.xml里没有指定maven.compiler.source和targetMaven就用了运行时的JDK版本。在pom.xml里显式锁定编译级别properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties第二个问题是控制台输出中文全是问号或乱码。先确认代码文件确实是UTF-8再确认启动命令带了-Dfile.encodingUTF-8最后检查Maven的pom.xml里有project.build.sourceEncoding。三个地方都设置一致乱码基本可以消除。5.3 边界验证把库存压到极限自测不要只跑“正常流程”要故意把票数买到超限。构造一个测试用例把某条线路max_tickets改成2连续发起3个线程各订2张票。预期结果是前两个线程有一个成功一个等待后失败第三个线程直接拿到库存不足的返回。用行锁实现时因为三个线程都先执行SELECT ... FOR UPDATE最终只有一个线程能拿到锁完成预订剩下两个在锁释放后重新读到的SUM已经变化从而准确返回false。还有一个容易被忽略的边界用户取消订单后pay_status变成2SUM(ticket_count)里使用IN (0,1)已经排除了已取消订单但报表统计总额时如果用同样的SQL已取消订单就不会出现在收入里。这是“业务状态和统计口径必须对齐”的典型场景Java课程设计答辩时能主动说“取消订单不计入已售票数但计入订单总量”这个细节会显著加分。最后留一个我常用的压测技巧把连接池的maximumPoolSize从10改成3再跑一遍并发预订测试如果代码里有连接泄漏几分钟内就会报连接超时此时优先检查try-with-resources是否把ResultSet也包了进去。同样的代码把连接池调回10一切正常问题就在资源释放而不在业务逻辑。本文还有配套的精品资源点击获取
返回列表