news 2026/10/6 3:17:16

JSP+Servlet+MySQL游戏商城实战:从零搭建可控Web系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet+MySQL游戏商城实战:从零搭建可控Web系统

简介:本资源是一个基于Java Web技术栈实现的游戏在线购买系统,面向Java初学者与Web开发入门学习者,帮助其掌握MVC分层架构下的电商类项目开发全流程。系统完整实现管理员与用户双角色功能:管理员可进行游戏、类目、订单及客户管理;用户可浏览商品、加入购物车、下单支付并查看订单,涵盖从登录认证到数据持久化的典型业务闭环。资源包共246个文件,含55个Java源码(DAO/Service/Servlet)、20个JSP页面、55个编译后Class文件、62张界面截图及配套CSS/JS/SQL等资源,总大小14.85MB,结构清晰,便于按模块理解前后端交互逻辑。已有96人学习下载,附带完整数据库脚本、默认账号密码及主流IDE兼容配置说明,开箱即用,适合课程设计、毕业设计或Java Web技能实战训练。

1. 为什么一个“游戏购买网站”还值得用 JSP+Servlet+MySQL 重做一遍?

不是所有老技术都该被淘汰——当你需要在两周内交付一个能跑在 Tomcat 7/8 上、支持学生课程设计答辩、对接校内实训机房环境、且不依赖 Spring Boot 自动装配和前端构建工具的 Web 系统时,JSP + Servlet + MySQL 这套组合反而成了最稳的“压舱石”。它不炫技,但每一步都可追踪:表单提交走哪个 Servlet,数据库连接在哪初始化,JSP 页面里${user.nickname}是怎么从 request.setAttribute() 里捞出来的,全在眼皮底下。我带过 12 届 JavaWeb 实训,发现学生翻车最多的地方,不是不会写 MyBatis XML,而是搞不清request.getRequestDispatcher().forward()和response.sendRedirect()的本质区别——前者是服务端跳转、URL 不变、request 域有效;后者是客户端重定向、URL 变、request 域清空。这个项目标题背后,其实是一套被低估的“可控性训练体系”:用最朴素的三层结构(表现层 JSP、控制层 Servlet、数据层 JDBC),把 HTTP 请求生命周期、会话管理、事务边界、SQL 注入防御这些硬核概念,全部钉死在代码行里。适合刚学完 JDBC 和 HTML 表单、还没碰过 MVC 框架的学生,也适合需要快速验证业务流程、拒绝黑匣子部署的中小团队原型验证。


2. 从零搭起骨架:Tomcat + MySQL + 项目结构三件套落地实操

2.1 Tomcat 8.5.99(兼容 JDK 8)本地部署与验证

别急着写代码,先确认容器能扛住请求。下载 Apache Tomcat 8.5.x(必须选 8.5.99 或 8.5.94,避开 8.5.97 因 CVE-2023-46589 导致的 WAR 包解压路径遍历漏洞),解压后进入bin目录:

# Windows 下执行 startup.bat # Linux/macOS 下执行 chmod +x startup.sh ./startup.sh

启动后访问http://localhost:8080,看到 Apache Tomcat Welcome Page 即成功。关键验证点:打开conf/server.xml,确认<Connector port="8080" protocol="HTTP/1.1"下没有URIEncoding="UTF-8"—— 这个属性必须手动加上,否则中文参数(如?gameName=原神)会乱码。加完重启,再用 curl 测试:

curl "http://localhost:8080/?q=测试中文" | iconv -f ISO-8859-1 -t UTF-8 2>/dev/null | grep -q "测试中文" && echo "✅ URI 编码生效" || echo "❌ 需检查 server.xml"

提示:若看到SEVERE: Failed to initialize connector [Connector[HTTP/1.1-8080]],八成是端口被占用。用netstat -ano | findstr :8080(Windows)或lsof -i :8080(macOS/Linux)查 PID 后 kill 掉。

2.2 MySQL 5.7.44 安装与游戏库初始化(避坑版)

热词里反复出现mysql 5.7.44,不是偶然——这是 5.7 系列最后一个稳定 LTS 版本,对 JDBC 4.2 驱动兼容性极佳,且sql_mode默认不含STRICT_TRANS_TABLES,避免学生因插入 NULL 到非空字段直接报错崩溃。安装时严禁用 MySQL Installer 全家桶,它会偷偷装 Service、注册 Windows 服务、改 PATH,导致后续手动启停混乱。正确做法:

  1. 下载mysql-5.7.44-winx64.zip(官网归档页,非最新下载页)
  2. 解压到D:\mysql-5.7.44
  3. 创建my.ini放入根目录,内容精简到只保留必要项:
[mysqld] port=3306 basedir=D:\\mysql-5.7.44 datadir=D:\\mysql-5.7.44\\data max_connections=100 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci sql_mode=NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION log-error=D:\\mysql-5.7.44\\logs\\error.log [client] default-character-set=utf8mb4
  1. 初始化并启动:
# 进入 bin 目录 cd D:\mysql-5.7.44\bin # 初始化 data 目录(首次运行) mysqld --initialize-insecure --user=mysql # 安装为 Windows 服务(可选,推荐手动启停更可控) mysqld --install MySQL57 --defaults-file="D:\\mysql-5.7.44\\my.ini" # 手动启动(推荐) mysqld --defaults-file="D:\\mysql-5.7.44\\my.ini"

启动后用mysql -u root -p登录(初始密码为空),执行建库脚本:

CREATE DATABASE IF NOT EXISTS game_store CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE game_store; -- 游戏主表(含价格、库存、状态) CREATE TABLE games ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, status ENUM('on_sale','out_of_stock','discontinued') DEFAULT 'on_sale', cover_url VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 用户表(简化版,仅需登录验证) CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, -- 存 BCrypt 加密后字符串 email VARCHAR(100), balance DECIMAL(10,2) DEFAULT 0.00, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 订单表(核心业务链路起点) CREATE TABLE orders ( id VARCHAR(32) PRIMARY KEY, -- UUID 字符串,避免暴露订单量 user_id INT NOT NULL, game_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, total_price DECIMAL(10,2) NOT NULL, status ENUM('pending','paid','shipped','cancelled') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (game_id) REFERENCES games(id) );

注意:utf8mb4是必须项,否则 emoji 表情(如游戏名里的 🎮)存不进数据库;ENUM类型比VARCHAR更安全,避免状态值写错;orders.id用 UUID 而非自增,防止竞拍类场景被刷单。

2.3 Eclipse + Dynamic Web Project 项目结构规范

用 Eclipse EE 版(2023-09 或更新),新建Dynamic Web Project,务必勾选 “Generate web.xml deployment descriptor”—— 这是 Servlet 3.0 以下版本路由的核心。项目结构必须严格遵循:

game-store/ ├── WebContent/ │ ├── index.jsp ← 首页(游戏列表) │ ├── login.jsp ← 登录页 │ ├── cart.jsp ← 购物车页 │ ├── order_confirm.jsp ← 订单确认页 │ └── WEB-INF/ │ ├── web.xml ← Servlet 映射中枢 │ └── lib/ │ └── mysql-connector-java-5.1.49.jar ← 必须用 5.1.x,兼容 JDK 8 ├── src/ │ ├── servlet/ │ │ ├── LoginServlet.java │ │ ├── GameListServlet.java │ │ ├── AddToCartServlet.java │ │ └── CheckoutServlet.java │ └── dao/ │ ├── GameDAO.java │ ├── UserDAO.java │ └── OrderDAO.java └── build.properties

web.xml中定义 Servlet 映射(关键!不能全靠注解):

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

提示:mysql-connector-java-5.1.49.jar是黄金版本——5.1.x 系列对com.mysql.jdbc.Driver类支持最稳,而 8.0.x 驱动要求com.mysql.cj.jdbc.Driver且需显式配置serverTimezone=GMT%2B8,学生极易漏配导致java.sql.SQLException: The server time zone value '...' is unrecognized。用 5.1.49 可省去所有时区烦恼。


3. 核心业务链路:从用户登录到订单生成的 Servlet 控制流拆解

3.1 LoginServlet:表单验证 + Session 绑定 + 防暴力破解

登录不是简单查密码。LoginServlet.doPost()必须完成三件事:
①参数清洗:request.getParameter("username").trim()去首尾空格,防admin这类空格绕过;
②凭证校验:用BCrypt.checkpw(inputPassword, dbHash)替代明文比对;
③会话绑定:HttpSession session = request.getSession(true); session.setAttribute("user", user);

完整代码节选:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username").trim(); String password = request.getParameter("password"); // 1. 基础校验 if (username == null || username.length() == 0 || password == null) { request.setAttribute("error", "用户名或密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 2. DAO 查询(含密码校验) UserDAO userDAO = new UserDAO(); User user = userDAO.findByUsername(username); if (user == null || !BCrypt.checkpw(password, user.getPassword())) { // 3. 记录失败次数(防爆破) int failCount = (int) request.getSession().getAttribute("loginFailCount"); request.getSession().setAttribute("loginFailCount", failCount + 1); request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 4. 登录成功:重置失败计数、绑定用户、跳转首页 request.getSession().removeAttribute("loginFailCount"); request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); }

参数说明:request.getSession(true)的true表示“如果 session 不存在则创建”,这是登录场景必需;session.setAttribute("user", user)后,所有 JSP 页面可通过${sessionScope.user.username}直接取值,无需重复查询。

3.2 GameListServlet:分页查询 + 状态过滤 + 图片路径拼接

游戏列表页要解决三个现实问题:

  • 数据量大时不能SELECT * FROM games全查(哪怕只有 200 条也会卡);
  • “已下架”游戏不该展示给普通用户;
  • 封面图 URL 需统一前缀(如/images/covers/xxx.jpg)。

Servlet 中分页逻辑:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int page = Integer.parseInt(request.getParameter("page") != null ? request.getParameter("page") : "1"); int pageSize = 12; // 每页 12 个游戏 int offset = (page - 1) * pageSize; GameDAO gameDAO = new GameDAO(); // 关键:WHERE status='on_sale' 过滤,LIMIT + OFFSET 分页 List<Game> games = gameDAO.findOnSaleGames(offset, pageSize); int total = gameDAO.countOnSaleGames(); // 单独 COUNT 语句 int totalPages = (int) Math.ceil((double) total / pageSize); request.setAttribute("games", games); request.setAttribute("currentPage", page); request.setAttribute("totalPages", totalPages); request.setAttribute("baseUrl", request.getContextPath() + "/list?page="); request.getRequestDispatcher("/index.jsp").forward(request, response); }

对应 JSP 中图片渲染(解决热词jsp图片如何对坐标定位的底层需求):

<c:forEach items="${games}" var="game"> <div class="game-card" style="position: relative;"> <img src="${pageContext.request.contextPath}/images/covers/${game.coverUrl}" alt="${game.name}" style="width: 100%; height: 200px; object-fit: cover;"> <!-- 绝对定位价格标签 --> <div style="position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; padding: 4px 8px;"> ¥${game.price} </div> </div> </c:forEach>

注意:<img>的src必须用${pageContext.request.contextPath}开头,否则静态资源路径在 WAR 包部署后会 404;object-fit: cover保证图片等比缩放填满容器,解决“图片变形”痛点。

3.3 CheckoutServlet:数据库事务 + 库存扣减 + 订单号生成

支付环节是唯一必须用事务的地方。CheckoutServlet伪代码逻辑:

// 1. 开启事务(Connection.setAutoCommit(false)) // 2. 查询游戏库存(SELECT stock FROM games WHERE id=? FOR UPDATE) // 3. 若库存 < quantity → rollback,返回错误 // 4. 更新库存(UPDATE games SET stock = stock - ? WHERE id=?) // 5. 插入订单(INSERT INTO orders (...) VALUES (...)) // 6. 生成 UUID 订单号(String orderId = UUID.randomUUID().toString().replace("-", "")) // 7. commit() // 8. 跳转 success.jsp

DAO 层关键代码(GameDAO.updateStockAndCreateOrder()):

public boolean updateStockAndCreateOrder(Connection conn, int gameId, int quantity, int userId, BigDecimal totalPrice) throws SQLException { // 步骤2:SELECT ... FOR UPDATE 锁定该行 String selectSql = "SELECT stock FROM games WHERE id = ? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(selectSql)) { ps.setInt(1, gameId); ResultSet rs = ps.executeQuery(); if (!rs.next() || rs.getInt("stock") < quantity) { return false; // 库存不足 } } // 步骤4:扣减库存 String updateSql = "UPDATE games SET stock = stock - ? WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setInt(1, quantity); ps.setInt(2, gameId); ps.executeUpdate(); } // 步骤5:插入订单(注意:order_id 是 UUID 字符串) String insertSql = "INSERT INTO orders (id, user_id, game_id, quantity, total_price) VALUES (?, ?, ?, ?, ?)"; String orderId = UUID.randomUUID().toString().replace("-", ""); try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setString(1, orderId); ps.setInt(2, userId); ps.setInt(3, gameId); ps.setInt(4, quantity); ps.setBigDecimal(5, totalPrice); ps.executeUpdate(); } return true; }

提示:FOR UPDATE是 InnoDB 行锁,防止并发下单超卖;UUID.randomUUID().toString().replace("-", "")生成 32 位无横线字符串,比System.currentTimeMillis()更难被预测和刷单。


4. 避坑指南:JSP+Servlet+MySQL 项目中 5 个血泪级常见问题

4.1 现象:JSP 页面中文显示为??,但数据库里存的是正确的汉字

原因:JSP 文件本身编码不是 UTF-8,或页面未声明 charset。
解决:
① 在 Eclipse 中右键 JSP 文件 → Properties → Resource → Text file encoding → 选UTF-8;
② JSP 顶部添加<%@ page contentType="text/html;charset=UTF-8" %>;
③ HTML head 内写<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">;
④ Tomcatserver.xmlConnector 中确认URIEncoding="UTF-8"已设置。

4.2 现象:response.sendRedirect("/success.jsp")后页面 404

原因:sendRedirect()是客户端跳转,路径相对于域名根目录,而非当前应用上下文。
解决:

  • 错误写法:response.sendRedirect("/success.jsp")→ 访问http://localhost:8080/success.jsp(缺上下文);
  • 正确写法:response.sendRedirect(request.getContextPath() + "/success.jsp")→http://localhost:8080/game-store/success.jsp。

4.3 现象:MySQL 插入时间字段自动设为0000-00-00 00:00:00

原因:MySQL 5.7 默认sql_mode含NO_ZERO_DATE,但 JDBC 驱动未传zeroDateTimeBehavior=convertToNull。
解决:
JDBC URL 改为:
jdbc:mysql://localhost:3306/game_store?useUnicode=true&characterEncoding=utf8mb4&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8
(注意:serverTimezone=GMT%2B8是 URL 编码后的GMT+8)

4.4 现象:购物车 addItem 后刷新页面,商品消失

原因:购物车存在request域(request.setAttribute()),而刷新是新请求,request域已销毁。
解决:
购物车必须存在session域:

// 正确:存入 session HttpSession session = request.getSession(); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } cart.add(new CartItem(gameId, quantity));

4.5 现象:<c:forEach>标签报org.apache.jasper.JasperException: /index.jsp (line: 12, column: 0) According to TLD or attribute directive in tag file, attribute items does not accept any expressions

原因:JSTL 标签库未正确引入,或web.xml中web-app版本低于 2.4。
解决:
①web.xml头部必须为:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">

②WEB-INF/lib/下放入jstl-1.2.jar和standard-1.1.2.jar(JSTL 1.2 配 standard 1.1.2 最稳);
③ JSP 顶部加:<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。


5. 进阶实战:让 JSP 页面加载完自动刷新一次的三种可靠方案

热词里高频出现jsp页面让加载完后刷新一次,这不是玄学需求——它真实存在于两类场景:
①登录后跳转首页,需刷新以显示欢迎语和余额(因session刚绑定,JSP 第一次渲染时 EL 表达式可能未生效);
②支付成功页,需刷新触发订单状态轮询或防止重复提交。

下面给出三种生产环境验证过的方案,按可靠性排序:

5.1 方案一:<meta http-equiv="refresh">(最轻量,适用于跳转后首次渲染)

在success.jsp或index.jsp的<head>内加入:

<!-- 仅当 URL 含 ?refresh=1 时执行刷新 --> <c:if test="${not empty param.refresh}"> <meta http-equiv="refresh" content="0;url=${pageContext.request.requestURL}?refreshed=1"/> </c:if>

然后在CheckoutServlet重定向时带上参数:

response.sendRedirect(request.getContextPath() + "/success.jsp?refresh=1");

优势:纯前端,无 JS 依赖;劣势:URL 会多出?refresh=1,需配合后端判断避免无限循环。

5.2 方案二:JavaScriptlocation.reload()+ Session 标记(推荐用于登录后首页)

在index.jsp底部添加:

<c:if test="${empty sessionScope.userRefreshed}"> <script> // 设置 Session 标记(通过 AJAX) fetch('${pageContext.request.contextPath}/mark-refreshed', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'action=mark' }).then(() => location.reload()); </script> </c:if>

配套MarkRefreshedServlet:

protected void doPost(HttpServletRequest request, HttpServletResponse response) { request.getSession().setAttribute("userRefreshed", "true"); }

优势:URL 干净,刷新后sessionScope.userRefreshed为 true,下次不再触发;劣势:需额外 Servlet。

5.3 方案三:HTTP Header 强制缓存失效(最彻底,适用于支付成功页)

在SuccessServlet中设置响应头:

response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1 response.setHeader("Pragma", "no-cache"); // HTTP 1.0 response.setDateHeader("Expires", 0); // Proxies request.getRequestDispatcher("/success.jsp").forward(request, response);

同时在success.jsp加防重复提交:

<c:if test="${not empty param.paid}"> <script> // 支付成功后禁用按钮,防止刷新重复提交 document.addEventListener('DOMContentLoaded', function() { const btn = document.getElementById('payBtn'); if (btn) btn.disabled = true; }); </script> </c:if>

优势:从协议层杜绝浏览器缓存,确保每次都是新请求;劣势:需配合前端按钮禁用,否则用户仍可 F5 刷新。

我一般会在登录成功后用方案二(Session 标记),支付成功页用方案三(Header 控制),因为前者要保障用户体验,后者要保障业务原子性。这三招用下来,再没遇到过学生抱怨“登录后名字不显示”或“付款页刷新就重复扣款”的问题。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:16:51

古诗自动生成与情感分析:LSTM、情感分类与韵律约束的工程实践

简介&#xff1a;一套基于机器学习与自然语言处理的古诗自动生成与情感分析系统项目资料包&#xff0c;面向自然语言处理学习者、诗歌生成研究者及对中文文本分析感兴趣的开发者。资源覆盖语料爬取、数据清洗与标注、词频与情感分析、规则作诗及神经网络写诗等完整流程&#xf…

作者头像 李华
网站建设 2026/10/6 3:16:41

Git全流程操作手册:从安装配置到分支合并与SSH认证实战

写这篇 Git 操作全流程手册&#xff0c;是因为我在各种团队和项目里见过太多因为 Git 使用不当而浪费时间的事故。提交信息乱写、分支合并一团糟、SSH 认证失败后什么都连不上&#xff0c;这些坑几乎每个人都会踩一遍。Git 是分布式版本控制系统&#xff0c;它真正解决的核心问…

作者头像 李华
网站建设 2026/10/6 3:16:23

从吼英语到流利口语:高阻力训练与肌肉记忆的实操指南

先说一个可能没人信的事实&#xff1a;把我从大山里带出来的那张车票&#xff0c;不是火车票&#xff0c;是一口在楼顶喊出来的英语。我爸至今不明白&#xff0c;为什么我每天傍晚要爬上自家平房顶&#xff0c;对着对面的山坡吼半个钟头。他只知道后来我考上了外语专业&#xf…

作者头像 李华
网站建设 2026/10/6 3:16:22

前后端实时通信选型:短轮询、长轮询、SSE与WebSocket对比

前阵子做内部工单系统&#xff0c;需求是后端任务状态一变&#xff0c;前端要立刻弹红点提示&#xff0c;顺便把消息卡片推出来。同事第一反应是“直接上WebSocket”&#xff0c;我翻了翻需求&#xff0c;最后选的却是SSE。这类选型分歧在前后端实时通信里太常见了——很多人把…

作者头像 李华
网站建设 2026/10/6 3:15:42

无模型自适应控制(MFAC)原理与Matlab/Simulink仿真实现

刚开始接触无模型自适应控制&#xff08;MFAC&#xff09;的时候&#xff0c;大部分人第一反应是“不依赖模型&#xff0c;那参数到底从哪来”&#xff1f;这个问题我当年也问过自己。MFAC的核心逻辑&#xff0c;是对被控系统在相邻采样点之间的动态变化做紧格式动态线性化&…

作者头像 李华
网站建设 2026/10/6 3:15:10

数据结构绪论全解析:逻辑结构、存储结构与复杂度分析

我当年考研复习数据结构&#xff0c;翻开王道的书&#xff0c;第一页就是绪论。说实话&#xff0c;第一遍我基本没看懂&#xff1a;这一章不就是在介绍“什么是数据结构”吗&#xff1f;后面写代码的时候不就知道了吗&#xff1f;后来我才发现&#xff0c;这一章是整个数据结构…

作者头像 李华