JSP+Servlet+MySQL游戏购买网站实战:从课设到二次开发
简介:这是一套面向Java Web初学者与课程设计者的游戏购买网站完整项目源码,基于JSP+Servlet+DAO+MySQL实现,适合用于毕业设计、课程实训或Java Web入门练手。系统区分管理员与用户两种角色:管理员可完成登录、订单管理、客户管理、游戏管理及类目管理;用户则可浏览首页、登录注册、购买游戏、查看购物车、提交订单与查询订单,覆盖电商类网站的核心业务闭环。压缩包共246个文件、约14.85MB,包含55个Java源文件与55个class文件承载Servlet、Service、Dao等分层逻辑,20个JSP页面负责视图展示,另有SQL建库脚本、jar依赖、css与js静态资源及图片素材,结构完整、便于二次开发。项目已在Eclipse、MyEclipse、IDEA配合JDK1.8/1.9与Tomcat8.x/9.x环境下验证,并附带admin/admin与user1/123456测试账号,可直接导入运行。目前已有98人学习关注,适合希望理解MVC分层、掌握JSP与Servlet协作及数据库操作的读者参考借鉴。
1. 从一份“能跑起来的 JSP 课设”说起:游戏购买网站到底长什么样
前阵子帮一个学弟看他做的 Java Web 课设,需求写得很朴素:做一个游戏购买网站,用户能注册登录、浏览游戏、加购物车、下单,后台能管游戏和订单。他一开始想上 Spring Boot,结果环境配了两天没跑起来,最后退回 JSP + Servlet + MySQL 这套“老古董”,反而一个下午就把主流程跑通了。这件事挺能说明问题:这套技术栈不是拿来炫技的,它的价值在于结构透明、依赖少、调试路径短,特别适合想搞懂“一个请求从浏览器到数据库再回来”完整链路的开发者。这份基于 JSP + Servlet + MySQL 的游戏购买网站设计,核心就是围绕商品展示、用户会话、购物车、订单落库这几件事展开,适合课程设计、Java Web 入门实战,以及想拿一个干净骨架做二次开发的人。下面我按“它是什么、怎么跑、怎么改”的顺序,把这份源码拆开讲。
2. 技术栈与目录结构:一个请求在 JSP + Servlet + MySQL 里怎么走
2.1 为什么是这套组合,而不是上来就 Spring Boot
先把这个项目的技术选型说清楚。前端展示层用 JSP,页面里嵌 JSTL 标签和 EL 表达式;控制层用 Servlet,每个业务动作对应一个或多个 Servlet 类;数据层用 JDBC 直连 MySQL,配合 DAO 模式把 SQL 收拢到少数几个类里;会话和登录态靠 HttpSession 维持;构建工具常见做法是 Maven 打 war 包,丢进 Tomcat 跑。这套组合的优点是每一层都能看见:你在浏览器点一个“加入购物车”,请求打到哪个 Servlet、调了哪个 DAO、执行了哪条 SQL、最后转发到哪个 JSP,全在代码里摆着,没有框架帮你“自动装配”掉。
对比 Spring Boot,它少了自动配置和依赖注入的便利,但换来的是排错时不用猜。课设场景下,老师要看的是你对 Servlet 生命周期、请求转发与重定向的区别、Session 作用域、JDBC 资源释放这些基本功的理解,这套栈正好把这些点全暴露出来。我一般会建议新手先用这套把主流程写通,再去学框架,理解会扎实很多。
2.2 典型目录结构长什么样
这类项目常见做法是按 MVC 分包,目录大致如下(不同源码包命名会有出入,但骨架基本一致):
game-shop/ ├── pom.xml # Maven 依赖:servlet-api、jsp-api、jstl、mysql-connector-java ├── src/main/java/ │ ├── controller/ # Servlet 层 │ │ ├── UserServlet.java # 注册、登录、退出 │ │ ├── GameServlet.java # 游戏列表、详情、搜索 │ │ ├── CartServlet.java # 加购、改数量、删除 │ │ └── OrderServlet.java # 下单、订单列表 │ ├── dao/ # JDBC 数据访问 │ │ ├── UserDao.java │ │ ├── GameDao.java │ │ └── OrderDao.java │ ├── entity/ # 实体类,与表字段对应 │ │ ├── User.java │ │ ├── Game.java │ │ └── Order.java │ └── util/ │ └── DBUtil.java # 数据库连接工具 ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、欢迎页 │ │ └── lib/ # 手动放 jar 时用 │ ├── jsp/ # 页面:index、login、cart、order 等 │ ├── css/ js/ images/ │ └── index.jsp └── sql/ └── game_shop.sql # 建库建表 + 初始数据这个结构的关键在于controller只做参数接收和页面跳转,dao只做 SQL,entity只做数据载体,util管连接。分层清楚,后面改功能时知道该动哪一层,不会一改全乱。
2.3 一个“加入购物车”请求的完整链路
拿最典型的加购动作走一遍,能把这套栈的协作关系看明白。用户在游戏详情页点“加入购物车”,表单以 POST 提交到/cart?action=add,CartServlet 的 doPost 接住,从 Session 取当前用户,从请求取游戏 id,调 CartDao 写入或更新购物车表,最后重定向回购物车页。代码骨架大致是这样:
// CartServlet.java 片段 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); HttpSession session = req.getSession(); User user = (User) session.getAttribute("loginUser"); // 登录态从 Session 取 if (user == null) { resp.sendRedirect(req.getContextPath() + "/jsp/login.jsp"); return; } if ("add".equals(action)) { int gameId = Integer.parseInt(req.getParameter("gameId")); int num = Integer.parseInt(req.getParameter("num") == null ? "1" : req.getParameter("num")); cartDao.addOrUpdate(user.getId(), gameId, num); // 已存在则累加数量 resp.sendRedirect(req.getContextPath() + "/cart?action=list"); // 重定向防重复提交 } }这里几个点值得说:session.getAttribute("loginUser")是登录拦截的常见做法,没登录直接跳登录页;sendRedirect而不是forward,是为了避免刷新页面重复加购,这是新手最容易翻车的地方之一;action参数用同一个 Servlet 分发多个动作,是这类项目控制层常见的“轻量前端控制器”写法,比一个动作一个 Servlet 好维护。参数上gameId必须做空值和数字校验,否则用户手改 URL 就能让程序抛 NumberFormatException。
2.4 数据层:JDBC + DAO 的写法与资源释放
数据层是整个项目最容易出问题的地方。常见做法是写一个 DBUtil 统一拿连接,DAO 里用 try-with-resources 或 finally 关资源。下面是一个查询游戏列表的 DAO 片段:
// GameDao.java 片段 public List<Game> findByPage(int page, int size) { List<Game> list = new ArrayList<>(); String sql = "SELECT id, name, price, cover, stock FROM game ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); // 偏移量 ps.setInt(2, size); // 每页条数 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Game g = new Game(); g.setId(rs.getInt("id")); g.setName(rs.getString("name")); g.setPrice(rs.getBigDecimal("price")); g.setCover(rs.getString("cover")); g.setStock(rs.getInt("stock")); list.add(g); } } } catch (SQLException e) { throw new RuntimeException("查询游戏列表失败", e); } return list; }LIMIT ?, ?两个占位符分别是偏移量和条数,分页就靠它;用PreparedStatement而不是拼字符串,是为了防 SQL 注入,这点在登录和搜索里尤其重要;try-with-resources保证 Connection、Statement、ResultSet 按序关闭,漏关连接在 Tomcat 连接池里跑一会儿就会耗尽。价格字段用BigDecimal而不是 double,是金额计算的常规做法,避免浮点误差。
3. 把项目跑起来:环境、建库、部署与首屏验证
3.1 环境准备与依赖版本
跑这套项目需要 JDK(常见 8 或 11)、Tomcat(8.5 或 9)、MySQL(5.7 或 8.0)、Maven(可选,手动导 jar 也能跑)。版本上有个血泪经验:Tomcat 10 之后包名从javax.servlet改成了jakarta.servlet,如果你的源码是javax开头,硬丢进 Tomcat 10 会一片红,编译都过不了。所以先看源码 import 的是哪个包,再决定 Tomcat 版本,别上来就装最新版。
Maven 依赖大致是这几项:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> <!-- 容器已提供,不打进 war --> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>provided这个 scope 很关键,servlet-api 由 Tomcat 提供,打进 war 反而可能冲突;JSTL 必须打进去,否则页面标签不解析;MySQL 驱动版本要和数据库版本匹配,8.0 驱动连 5.7 库一般也能用,但 URL 参数要带时区和 SSL 设置。
3.2 建库建表与初始数据
源码里一般带一个sql/game_shop.sql,先建库再导表。核心表至少四张:用户表、游戏表、购物车表、订单表(订单明细可合并或单列)。建表语句骨架如下:
CREATE DATABASE IF NOT EXISTS game_shop DEFAULT CHARSET utf8mb4; USE game_shop; CREATE TABLE `user` ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 实际项目应存哈希,课设常见明文 nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE game ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, cover VARCHAR(255), stock INT DEFAULT 0, description TEXT ); CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, game_id INT NOT NULL, num INT DEFAULT 1, UNIQUE KEY uk_user_game (user_id, game_id) -- 同一用户同一游戏只留一行 );utf8mb4是为了支持中文和特殊字符;uk_user_game唯一索引让“加购已存在则累加”能用INSERT ... ON DUPLICATE KEY UPDATE一条 SQL 搞定;密码字段课设常存明文,但我在二次开发时会换成 BCrypt 哈希,这个后面讲改造时再说。
3.3 改配置、打 war、丢进 Tomcat
数据库连接信息一般在DBUtil.java或db.properties里,跑之前必须改成你本机的地址、库名、账号密码:
// DBUtil.java 片段 private static final String URL = "jdbc:mysql://localhost:3306/game_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws SQLException { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 8.0 驱动类名 } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动未加载", e); } return DriverManager.getConnection(URL, USER, PASSWORD); }serverTimezone不配在 8.0 驱动下常报时区错误;useSSL=false是本地开发的常见写法,省掉证书告警。改完执行mvn clean package得到 war,丢进 Tomcat 的webapps目录,启动后访问http://localhost:8080/项目名/。首屏能出游戏列表,说明 JSP 渲染、Servlet 转发、数据库查询这条链是通的;如果白屏或 500,先看 Tomcat 日志里的堆栈,八成是驱动没加载或 SQL 字段名对不上。
3.4 首屏验证清单
跑通不等于跑对,我一般按这个顺序验:首页游戏列表有数据、点详情能进、未登录点加购跳登录、登录后加购成功、购物车改数量金额跟着变、下单后订单表多一条、后台能查到这笔订单。每一步对应一个 Servlet 和一个 DAO,哪步断了就顺着那层的日志查,比漫无目的地翻代码快得多。
4. 核心模块拆解:登录会话、购物车与下单事务
4.1 登录与会话:Session 里到底该放什么
登录模块的 Servlet 做两件事:校验账号密码、把用户对象塞进 Session。校验走 UserDao 按用户名查记录,密码比对(课设明文,生产应哈希)。登录成功后session.setAttribute("loginUser", user),后续所有需要登录的 Servlet 都从 Session 取这个对象做拦截。这里有个细节:Session 里别放整个用户对象的所有字段,尤其别放密码,常见做法是放一个精简的登录用户对象或只放 id 和昵称,减少敏感信息驻留。
退出登录就是session.invalidate()或移除属性再跳首页。会话超时在web.xml里配<session-config><session-timeout>30</session-timeout></session-config>,单位是分钟。很多课设忘了配,默认 30 分钟其实也够,但显式写出来更清楚。
4.2 购物车:库存校验与数量边界
购物车模块的坑集中在两处:库存和数量边界。加购时要判断num > 0且不超过库存;改数量时同样要卡上限,否则用户手输 9999 也能提交。常见做法是在 CartServlet 里先查游戏库存再决定是否放行:
// CartServlet 改数量片段 int num = Integer.parseInt(req.getParameter("num")); Game game = gameDao.findById(gameId); if (num < 1) num = 1; if (num > game.getStock()) { req.setAttribute("msg", "数量超过库存,已调整为最大可购数"); num = game.getStock(); } cartDao.updateNum(user.getId(), gameId, num);库存判断放在服务端而不是只靠前端max属性,是因为前端校验能被绕过。购物车金额是实时算的,展示时用price * num在 JSP 里用 EL 算或后端算好传过去,别把总价存进购物车表,价格一变就对不上。
4.3 下单:一个必须用事务兜住的动作
下单是整个项目里唯一必须上事务的地方。它至少做三件事:写订单主表、写订单明细、扣减库存。任何一步失败都要回滚,否则会出现“订单有了库存没扣”或“库存扣了订单没生成”的脏数据。JDBC 手动事务的写法:
// OrderService 下单片段 Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 int orderId = orderDao.insertOrder(conn, user.getId(), totalAmount); for (CartItem item : cartItems) { orderDao.insertItem(conn, orderId, item.getGameId(), item.getNum(), item.getPrice()); int affected = gameDao.reduceStock(conn, item.getGameId(), item.getNum()); if (affected == 0) { // 库存不足,扣减失败 throw new RuntimeException("库存不足:" + item.getGameId()); } } cartDao.clear(conn, user.getId()); // 清空购物车 conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); // 任一步失败整体回滚 throw new RuntimeException("下单失败", e); } finally { if (conn != null) try { conn.close(); } catch (SQLException ignored) {} }关键在setAutoCommit(false)和rollback(),以及扣库存用UPDATE game SET stock = stock - ? WHERE id = ? AND stock >= ?这种带条件的更新,靠影响行数判断是否扣成功,比先查再改更抗并发。clear清购物车也放进同一事务,保证下单和清空要么都成要么都不成。
4.4 后台管理:游戏增删改查的最小闭环
后台一般单独一组 Servlet 或复用同一组加权限判断,核心是游戏的增删改查。新增游戏要处理封面上传(常见做法是存到 webapp 下某个目录,数据库只存相对路径),删除要考虑是否已被订单引用(有外键或逻辑删除更稳),修改要回填表单。这块没有太多玄学,重点是表单字段和实体字段一一对应,JSP 回填用${game.name}这类 EL 表达式,别在 JSP 里写 Java 脚本片段,那是老写法,维护起来很痛苦。
5. 避坑与排查:这套栈最容易翻车的五个地方
5.1 中文乱码:POST 和 GET 要分开治
现象是表单提交后数据库里存的是问号或乱码。原因通常是请求体编码没设,或者 GET 参数走了容器默认编码。解决:POST 在 Servlet 最前面加req.setCharacterEncoding("UTF-8");GET 在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8";数据库和表统一utf8mb4,连接 URL 带characterEncoding=utf8。三处都对齐才不会漏。
5.2 刷新重复下单:forward 和 redirect 用反了
现象是下单成功后按 F5 又生成一笔订单。原因是下单后用了forward,浏览器地址还停在下单请求上,刷新就重发。解决:所有“写操作成功后”的跳转一律用sendRedirect,让浏览器重新发一个 GET 请求,这就是常说的 PRG 模式。这个坑我见过太多次,属于必踩项。
5.3 连接耗尽:DAO 里漏关 ResultSet
现象是跑一会儿就报连接超时或 Too many connections。原因是某处rs或ps没关,连接被占着不还。解决:统一用 try-with-resources,或者 finally 里按 ResultSet、Statement、Connection 逆序关闭。排查时可以临时在 DBUtil 里打印当前连接数,或者看 MySQL 的show processlist,一堆 Sleep 状态的连接就是线索。
5.4 404 与 500:路径和包名两头查
404 多半是 Servlet 映射路径和表单 action 对不上,或者 war 包名和访问路径不一致;500 先看堆栈第一行,常见是驱动类名写错(8.0 是com.mysql.cj.jdbc.Driver)、SQL 字段名拼错、空指针(Session 里取出来是 null 没判)。排查顺序是先看 Tomcat 日志定位到具体类和行号,再对照代码,别靠猜。
5.5 Tomcat 10 的包名迁移
现象是换到 Tomcat 10 后所有 Servlet 编译报错。原因是javax.servlet.*全部改成了jakarta.servlet.*。解决:要么继续用 Tomcat 8.5/9,要么全局替换 import 并把依赖换成 jakarta 版本。课设交付前先确认目标环境版本,别在本地跑通、交上去跑不起来。
6. 二次开发怎么改:从明文密码到分页搜索的落地顺序
拿到这份源码,我建议的改造顺序是先安全、再体验、后扩展,别一上来就重构架构。第一步把密码明文换成哈希,引入一个轻量哈希工具,注册时存哈希、登录时比对哈希,改动集中在 UserDao 和登录 Servlet 两处,风险低收益高。第二步把首页游戏列表加分页和关键词搜索,DAO 里加一个带LIKE条件的 count 和分页查询,Servlet 接page和keyword参数,JSP 加翻页条,这是最能体现“会用 JDBC 动态条件”的改造点。第三步再考虑把购物车从数据库表迁到 Session(或反过来),以及给后台加权限拦截过滤器。
改造时有个验证方法我一直用:每改一个模块,先把对应的 SQL 在数据库客户端里手动跑一遍,确认结果对,再接到代码里;接口层面用浏览器或简单请求工具打一遍,看返回和跳转是否符合预期。这样出问题时能快速判断是 SQL 错还是代码错。下面这张表是我一般会排的改造优先级:
| 改造项 | 涉及文件 | 风险 | 建议顺序 |
|---|---|---|---|
| 密码哈希 | UserDao、登录 Servlet | 低 | 1 |
| 列表分页 + 搜索 | GameDao、GameServlet、列表 JSP | 中 | 2 |
| 下单事务补全 | OrderService、OrderDao | 中 | 3 |
| 后台权限过滤器 | 新增 Filter、web.xml | 中 | 4 |
| 购物车存储位置调整 | CartServlet、CartDao | 高 | 5 |
最后说个习惯。早些年我改这类课设项目,总想着先把架构“理顺”再动功能,结果改到一半发现登录都跑不通,回头排查花的时间比写功能还多。从那以后我每次动别人的 Java Web 源码,都强制先把原始版本完整跑通一遍、把主流程点一遍,再开始改,改完一个模块就回归一次主流程。这套笨办法救过我很多次。希望帮到你。
本文还有配套的精品资源,点击获取