简介:这是一套面向计算机相关专业在校学生的JavaWeb课程设计与期末大作业完整项目,主题为在线旅游网站,适合作为课设、毕设、作业或项目初期立项演示的参考方案,也适合具备一定基础的小白学习进阶。压缩包共1870个文件,约70.29MB,其中以1753个jpg图片资源为主,用于页面素材与景点展示;另有39个java源文件承载后端业务逻辑,配合12个html页面、10个css样式与7个js脚本构建前端交互,并包含sql脚本、xml配置及properties文件,整体结构完整、层次清晰。目前已有756人学习下载。项目代码均经过测试运行成功后才上传,答辩评审平均分达96分,读者可据此快速理解在线旅游网站的功能模块划分与前后端协作方式,并在此基础上修改扩展,实现个性化功能需求。
1. 在线旅游网站课设:从建表到跑通,一套能交差的 JavaWeb 完整方案
每年期末,总有一批人卡在 JavaWeb 课程设计上。选题选了在线旅游网站,听起来业务清晰、页面好画,真动手才发现:用户、景点、订单、支付状态四张表就够绕晕,JSP 里嵌 Java 代码写着写着就变成一锅粥,最后文档说明也不知道该写什么才算“完整”。这篇笔记就围绕“JavaWeb 课程设计/期末大作业-在线旅游网站+源代码+文档说明”这个典型题目,把一套能跑、能讲、能交差的方案从头拆一遍。适合正在赶课设的在校生,也适合想拿一个完整案例重新熟悉 Servlet、JSP、JDBC 这套经典组合的开发者。核心目标只有一个:让你在本地把项目跑起来,并且清楚每一层为什么这么分、参数为什么这么设、答辩时被问到不至于翻车。
2. 在线旅游网站的技术选型:为什么这套老组合反而最适合课设
2.1 分层结构先定死,别一上来就写 JSP
很多人做课设的习惯是:新建一个 Web 项目,先写 index.jsp,再写 login.jsp,页面越堆越多,最后业务逻辑全塞在<% %>里。这种写法在只有两三个页面时看不出问题,一旦加到景点列表、详情、下单、订单查询,JSP 就会变成黑匣子——改一个字段要在五个文件里搜。
常见做法是采用 MVC 三层结构,具体到 JavaWeb 课设,落地成这样:
| 层次 | 职责 | 典型文件 |
|---|---|---|
| 表现层 | 接收请求、展示数据 | JSP、Servlet |
| 业务层 | 处理下单、库存、状态流转 | Service 类 |
| 数据层 | 增删改查、连接管理 | DAO 类、JDBC 工具类 |
| 实体层 | 承载数据 | User、Scenic、Order 等 JavaBean |
Servlet 负责收参数、调 Service、转发 JSP;Service 写业务规则,比如“下单前先查余票”;DAO 只跟数据库打交道。这样分的好处是答辩时老师问“你的业务逻辑写在哪”,你能直接指到 Service 层,而不是在一堆 JSP 里翻。
2.2 数据库选 MySQL,连接池别用 DriverManager 硬扛
课设数据量不大,MySQL 5.7 或 8.0 都行,建库语句用 utf8mb4 字符集,避免景点中文名乱码。连接方式上,新手最容易犯的错是每次查询都DriverManager.getConnection(),一个页面刷三次就开三次连接,压测直接崩。课设里推荐用 Druid 或 C3P0 连接池,配置放在druid.properties:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username=root password=你的密码 initialSize=5 maxActive=20 maxWait=3000initialSize是启动时建几个连接,maxActive是上限,maxWait是拿不到连接等多久(毫秒)。课设演示时 5 到 20 完全够用。注意serverTimezone一定要写,否则 MySQL 8 会报时区错误,这是血泪经验。
2.3 前端不追求花哨,Bootstrap 加 JSP 足够交差
课设评分看的是功能完整度和代码结构,不是 UI 多炫。用 Bootstrap 5 的 CDN 引入,表格、按钮、表单直接套 class,省下大量调 CSS 的时间。JSP 里用 JSTL 的<c:forEach>遍历景点列表,比脚本片段干净得多。如果老师要求“界面美观”,加一个轮播图组件放在首页即可,不要在这上面耗三天。
3. 从建表到跑通第一个页面:可复现的最小闭环
3.1 四张核心表的建表语句与字段说明
在线旅游网站最小可用模型需要四张表:用户、景点、订单、订单明细。先建库再建表:
CREATE DATABASE travel_db DEFAULT CHARACTER SET utf8mb4; USE travel_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_scenic ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, description TEXT, image_url VARCHAR(255) ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2), status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, scenic_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) );t_order.status用 0 表示待支付、1 表示已支付、2 表示已取消,这个约定要写进文档说明里,答辩时老师常问状态流转。order_no用时间戳加随机数生成,保证唯一。t_scenic.stock是余票,下单时减、取消时加,这是业务层要保证的。
3.2 JDBC 工具类与 DAO 的写法
先写一个JDBCUtils,用 Druid 拿连接:
public class JDBCUtils { private static DataSource ds; static { try { Properties p = new Properties(); p.load(JDBCUtils.class.getClassLoader() .getResourceAsStream("druid.properties")); ds = DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { throw new RuntimeException("连接池初始化失败", e); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } public static void close(Connection c, Statement s, ResultSet r) { // 依次关闭,注意判空 } }DAO 里用PreparedStatement防注入,比如查景点列表:
public List<Scenic> findAll() { List<Scenic> list = new ArrayList<>(); String sql = "SELECT id,name,price,stock,description,image_url FROM t_scenic"; try (Connection c = JDBCUtils.getConnection(); PreparedStatement ps = c.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Scenic s = new Scenic(); s.setId(rs.getInt("id")); s.setName(rs.getString("name")); s.setPrice(rs.getBigDecimal("price")); s.setStock(rs.getInt("stock")); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; }try-with-resources 自动关连接,比手动 finally 干净。注意getBigDecimal对应 DECIMAL 字段,别用getDouble,金额计算会丢精度。
3.3 用 Servlet 跑通景点列表页
写一个ScenicListServlet,映射到/scenic/list:
@WebServlet("/scenic/list") public class ScenicListServlet extends HttpServlet { private ScenicService service = new ScenicServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Scenic> list = service.findAll(); req.setAttribute("scenics", list); req.getRequestDispatcher("/scenic_list.jsp").forward(req, resp); } }JSP 里用 JSTL 渲染:
<c:forEach items="${scenics}" var="s"> <div class="card"> <h5>${s.name}</h5> <p>价格:${s.price} 元</p> <p>余票:${s.stock}</p> <a href="${pageContext.request.contextPath}/scenic/detail?id=${s.id}">查看详情</a> </div> </c:forEach>启动 Tomcat,访问http://localhost:8080/项目名/scenic/list,能看到景点卡片就算闭环跑通。这一步跑不通,后面下单、支付全是空中楼阁。
4. 下单与订单状态流转:业务层最容易写乱的地方
4.1 下单接口的参数校验与库存扣减
下单是课设里最容易被追问的环节。参数至少要有scenicId、quantity,从 session 里取userId。业务层顺序是:查用户是否登录、查景点是否存在、查库存是否足够、扣库存、生成订单、写订单明细。任何一步失败都要回滚,所以必须用事务:
public String createOrder(int userId, int scenicId, int quantity) { Connection c = null; try { c = JDBCUtils.getConnection(); c.setAutoCommit(false); ScenicDao scenicDao = new ScenicDaoImpl(c); Scenic s = scenicDao.findById(scenicId); if (s == null || s.getStock() < quantity) { throw new RuntimeException("库存不足"); } scenicDao.reduceStock(scenicId, quantity); OrderDao orderDao = new OrderDaoImpl(c); String orderNo = System.currentTimeMillis() + "" + (int)(Math.random()*9000+1000); orderDao.insert(userId, orderNo, s.getPrice().multiply(new BigDecimal(quantity))); c.commit(); return orderNo; } catch (Exception e) { try { if (c != null) c.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException("下单失败:" + e.getMessage()); } finally { try { if (c != null) { c.setAutoCommit(true); c.close(); } } catch (SQLException e) { e.printStackTrace(); } } }关键点:DAO 要接收外部传入的 Connection,不能自己从连接池拿,否则事务管不到。setAutoCommit(false)之后所有操作在同一个连接上,commit 才生效。这是课设里区分“会写”和“抄的”的分水岭。
4.2 订单状态用枚举还是常量
状态值建议用常量类而不是散落的 0、1、2:
public class OrderStatus { public static final int UNPAID = 0; public static final int PAID = 1; public static final int CANCELLED = 2; }文档说明里画一张状态流转表:待支付 → 已支付(支付成功)、待支付 → 已取消(用户取消或超时)。取消时要把库存加回去,同样在事务里做。老师如果问“超时未支付怎么处理”,可以答“课设里用定时任务扫描,实际项目用延迟队列”,这样既诚实又显得有了解。
4.3 订单列表与详情页的数据组装
订单列表要显示订单号、金额、状态、下单时间,详情页还要显示买了哪些景点。常见做法是订单表关联订单明细表,再关联景点表:
SELECT o.order_no, o.total_amount, o.status, o.create_time, i.quantity, s.name, s.price FROM t_order o JOIN t_order_item i ON o.id = i.order_id JOIN t_scenic s ON i.scenic_id = s.id WHERE o.user_id = ? ORDER BY o.create_time DESC在 Java 里用一个OrderVO承载多表字段,避免前端拿到一堆零散属性。这一步做干净,文档说明里的“数据库设计”章节就有东西可写。
5. 避坑与排查:课设里翻车最多的五个地方
5.1 中文乱码:现象是页面显示问号,原因是编码链没统一
现象:景点名在数据库里正常,JSP 页面显示???。原因通常是三处编码不一致:数据库字符集、JDBC URL 的characterEncoding、JSP 的pageEncoding。解决:建库用 utf8mb4,URL 加useUnicode=true&characterEncoding=utf8,JSP 顶部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 里resp.setContentType("text/html;charset=UTF-8")。四处对齐,乱码消失。
5.2 404:现象是访问 Servlet 报找不到,原因是路径映射对不上
现象:浏览器访问/scenic/list返回 404。原因可能是@WebServlet的值和访问路径不一致,或者项目部署路径(context path)没算进去。解决:JSP 里所有链接用${pageContext.request.contextPath}拼前缀,Servlet 注解写/scenic/list,访问时带上项目名。另外检查web.xml的metadata-complete是否为 true,如果是,注解会被忽略。
5.3 连接池报错:现象是启动就抛异常,原因是配置文件没放对位置
现象:Tomcat 启动时报Connection refused或Access denied。原因通常是druid.properties放在了src下但没被编译到WEB-INF/classes,或者数据库密码写错。解决:在 IDEA 里确认druid.properties位于src/main/resources,编译后检查target/classes下有没有。密码里有特殊字符要转义,serverTimezone不能省。
5.4 事务不生效:现象是扣了库存但订单没生成,原因是 DAO 自己拿了连接
现象:下单时库存减了,订单表却没记录。原因就是前面说的,DAO 内部用JDBCUtils.getConnection()自己拿连接,和 Service 的连接不是同一个,事务管不到。解决:DAO 方法签名改成接收Connection参数,Service 统一管理连接和事务。这个坑几乎每届都有人踩。
5.5 文档说明写成流水账:现象是老师翻两页就放下,原因是没写设计决策
现象:文档说明里只有“第一步建表、第二步写 Servlet”,没有为什么。原因是没有把选型理由和边界写进去。解决:文档里至少包含需求分析、数据库 E-R 说明、分层结构图、核心接口说明、状态流转表、测试用例。每张表写清楚字段含义和约束,每个接口写清楚入参出参。这样文档说明才配得上“完整”两个字。
6. 让课设多拿几分:把订单超时取消做成可演示的亮点
大部分课设做到下单、支付、查询就停了,答辩时老师容易觉得“功能太基础”。有一个成本不高但演示效果很好的进阶点:订单超时自动取消。实现方式用ScheduledExecutorService起一个定时任务,每 30 秒扫一次待支付且创建时间超过 15 分钟的订单,批量取消并回补库存。
@WebListener public class OrderTimeoutTask implements ServletContextListener { private ScheduledExecutorService scheduler; @Override public void contextInitialized(ServletContextEvent sce) { scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { try { orderService.cancelTimeoutOrders(15); } catch (Exception e) { e.printStackTrace(); } }, 0, 30, TimeUnit.SECONDS); } @Override public void contextDestroyed(ServletContextEvent sce) { if (scheduler != null) scheduler.shutdown(); } }cancelTimeoutOrders里查status=0 AND create_time < NOW() - INTERVAL 15 MINUTE,逐条取消并回补库存,同样走事务。演示时把超时时间临时改成 1 分钟,下单后不支付,刷新订单列表就能看到状态变成“已取消”,库存也回来了。这个点写进文档说明的“扩展功能”章节,比堆十个静态页面有用。
验证方法也简单:开两个浏览器,一个下单不支付,另一个观察景点余票;等定时任务跑完,余票恢复、订单状态变更,说明事务和定时任务都正常。如果没恢复,先查定时任务有没有启动(看控制台日志),再查事务有没有提交。
我自己的习惯是:课设代码写完先不急着写文档,把整个流程手动走三遍——注册、登录、浏览、下单、取消、再下单,每遍记录哪里卡住。卡住的地方就是文档里要重点说明的地方,也是答辩时最可能被问的地方。这套方案不追求技术多新,追求的是每一层都说得清、每一步都能复现。希望帮到你。
本文还有配套的精品资源,点击获取