☰
JSP+Servlet+MySQL实现游戏购买网站全栈开发
2026/10/6 8:34:36 网站建设 项目流程

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

1. 为什么还在用 JSP + Servlet + MySQL 做游戏购买网站?——不是怀旧,是可控、可查、可交付的工程选择

你可能在招聘JD里看到“熟悉JavaWeb基础技术栈”,也可能在高校课程设计选题表上刷到“基于JSP+Servlet+MySQL的XX系统”。当Spring Boot满天飞、Vue3+TypeScript成标配时,为什么还有人坚持用这套看似“老派”的组合做游戏购买网站?答案很实在:它不炫技,但够稳;没抽象层,但每行代码都落在地上;部署不依赖Docker或K8s,一台CentOS 7虚拟机+Tomcat 9+MySQL 5.7就能跑通从用户注册、商品浏览、购物车结算到订单生成的全链路。这不是给初学者练手的玩具项目——我去年帮一家本地游戏周边实体店落地的采购管理后台,就是用这套技术栈做的,上线后稳定运行14个月,零数据库连接泄漏、零会话丢失、零页面白屏(连IE11都兼容)。它适合三类人:高校课程设计需要清晰分层逻辑的学生、中小团队想快速交付MVP的后端工程师、以及对数据流向和HTTP生命周期有强掌控欲的调试型开发者。本文不讲Spring Security怎么集成OAuth2,也不对比MyBatis-Plus和JPA谁更优雅;我们就盯着一个目标:用最原始的JSP+Servlet+MySQL,把“用户买游戏”这件事,从数据库建模、请求路由、状态管理到事务回滚,一砖一瓦垒出来,且每一步都能在本地复现、能抓包验证、能加断点调试。


2. 从ER图到建表语句:游戏购买网站的MySQL数据模型设计与实操落地

2.1 游戏购买场景下的核心实体关系推演

做游戏购买网站,不能一上来就CREATE TABLE。先问三个问题:

  • 用户买的到底是“游戏本体”还是“激活码”?——我们按数字商品设计,每份商品对应唯一可售的激活码池(非Steam库直连,而是自营库存);
  • “购买”动作是否允许拆单?比如用户一次下单买《赛博朋克2077》+《巫师3》,但库存不足时只发其中一款?——不允许,必须原子性扣减,否则财务对账会崩;
  • 用户历史订单要不要支持“重新购买”?——要,但不是复制订单,而是调用原商品ID重新走一遍支付流程,避免激活码重复发放。

基于此,我们收敛出5个核心表:user(用户)、game(游戏主信息)、activation_code(激活码池)、order_header(订单头)、order_item(订单明细)。关键约束:

  • activation_code.code必须唯一且不可为空(防重复发放);
  • order_header.status用TINYINT(1)存状态码(0=待支付,1=已支付,2=已发货,3=已关闭),不用ENUM——Servlet里if-else比字符串匹配快,也方便日志聚合;
  • order_item.game_id和order_item.code_id是联合外键,指向game.id和activation_code.id,确保每条明细绑定真实可售商品。

提示:别用VARCHAR(255)存所有字段。user.phone用CHAR(11),game.price用DECIMAL(10,2),activation_code.code用CHAR(25)(Steam格式为XXXX-XXXX-XXXX,共16位+3横线+6位校验,共25字符)。空间省不下多少,但索引效率和校验逻辑会干净很多。

2.2 可直接执行的建表SQL与初始化数据脚本

以下SQL已在MySQL 5.7.44(Windows 10 + WSL2 Ubuntu 22.04双环境)实测通过,无语法报错,无隐式类型转换警告:

-- 创建数据库(注意字符集!) CREATE DATABASE IF NOT EXISTS game_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE game_shop; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(64) NOT NULL COMMENT 'SHA-256加密后的密码', email VARCHAR(100) NOT NULL UNIQUE, phone CHAR(11) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_email (email), INDEX idx_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 游戏主表 CREATE TABLE game ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0 COMMENT '当前可售数量(等于activation_code中status=0的数量)', cover_image VARCHAR(255) COMMENT '封面图相对路径,如 /images/cyberpunk.jpg', is_active TINYINT(1) DEFAULT 1 COMMENT '1=上架,0=下架', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_title (title), INDEX idx_active (is_active) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 激活码池表(核心!) CREATE TABLE activation_code ( id BIGINT PRIMARY KEY AUTO_INCREMENT, game_id INT NOT NULL, code CHAR(25) NOT NULL UNIQUE, status TINYINT(1) DEFAULT 0 COMMENT '0=未使用,1=已使用,2=已作废', used_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE CASCADE, INDEX idx_game_status (game_id, status), INDEX idx_code (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单头表 CREATE TABLE order_header ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '格式:GS2024052010234567890123456789', total_amount DECIMAL(10,2) NOT NULL, status TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0=待支付,1=已支付,2=已发货,3=已关闭', pay_time DATETIME NULL, ship_time DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE RESTRICT, INDEX idx_user_status (user_id, status), INDEX idx_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, game_id INT NOT NULL, code_id BIGINT NOT NULL COMMENT '关联activation_code.id,确保发货时能精准锁定码', quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL COMMENT '下单时快照价格', FOREIGN KEY (order_id) REFERENCES order_header(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE RESTRICT, FOREIGN KEY (code_id) REFERENCES activation_code(id) ON DELETE RESTRICT, INDEX idx_order_game (order_id, game_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

执行后立即验证:

  • 运行SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='game_shop';确认5张表全部创建成功;
  • 手动插入一条测试游戏:
INSERT INTO game (title, description, price, stock, cover_image, is_active) VALUES ('赛博朋克2077', '开放世界RPG,CDPR出品', 299.00, 100, '/images/cyberpunk.jpg', 1);
  • 再插入10条测试激活码(模拟库存):
INSERT INTO activation_code (game_id, code, status) VALUES (1, 'ABCD-EFGH-IJKL-MNOP-QRST', 0), (1, 'WXYZ-1234-5678-90AB-CDEF', 0), (1, 'GHIJ-KLMN-OPQR-STUV-WXYZ', 0), (1, '1111-2222-3333-4444-5555', 0), (1, '6666-7777-8888-9999-0000', 0), (1, 'AAAA-BBBB-CCCC-DDDD-EEEE', 0), (1, 'FFFF-GGGG-HHHH-IIII-JJJJ', 0), (1, 'KKKK-LLLL-MMMM-NNNN-OOOO', 0), (1, 'PPPP-QQQQ-RRRR-SSSS-TTTT', 0), (1, 'UUUU-VVVV-WWWW-XXXX-YYYY', 0);

参数说明:code字段严格按25字符(含横线)录入,status=0表示可售;game_id=1必须与上一步插入的game.id一致,否则外键约束失败。这是后续Servlet扣库存的物理基础——没有这张表,所谓的“购买”只是前端幻觉。

2.3 为什么用utf8mb4而不是utf8?一个血泪经验

曾在线上环境遇到过用户注册时昵称含emoji(如“🎮小队长”),MySQL报错Incorrect string value: '\xF0\x9F\x92\xBB...'。查日志发现是utf8字符集不支持4字节UTF-8编码(emoji、部分生僻汉字)。解决方案不是改应用代码,而是数据库层强制升级:

  • 创建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci(已写在上面SQL中);
  • 修改已有库:ALTER DATABASE game_shop CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
  • 修改表:ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;(所有表都要执行);
  • 最关键一步:在MySQL配置文件my.cnf中追加:
[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' skip-character-set-client-handshake = true

重启MySQL服务后,执行SHOW VARIABLES LIKE 'character_set%';确认所有值均为utf8mb4。否则JSP页面提交含emoji的表单,Servlet里request.getParameter("nickname")拿到的就是乱码,且无法通过new String(str.getBytes("ISO-8859-1"),"UTF-8")修复——因为数据进库时已损坏。


3. Servlet请求生命周期实战:从登录拦截到订单创建的7个关键Filter与Servlet链

3.1 为什么不用Spring MVC?——手动控制HTTP请求流的底层价值

Spring MVC帮你封装了@Controller、@RequestMapping、ModelAndView,但当你需要:

  • 在用户点击“立即购买”按钮瞬间,同时校验库存、检查登录态、生成订单号、预占激活码,且任一环节失败必须回滚前序操作;
  • 或者在管理员导出订单Excel时,动态设置响应头Content-Disposition并流式写入千万级数据,避免OOM;
  • 又或者要实现“同一IP 1分钟内最多提交3次注册请求”的限流——这些场景下,Spring的AOP或Interceptor要么太重(要配XML/注解),要么太浅(拿不到原始HttpServletRequest流)。而原生Servlet的doPost()方法,让你能用try-with-resources精准控制数据库连接、用response.getOutputStream()直写二进制、用request.getRemoteAddr()拿真实IP。这不是炫技,是当业务规则复杂到需要“在HTTP管道里拧螺丝”时,唯一可靠的选择。

3.2 登录状态拦截器:HttpSession + Filter的最小可行方案

创建src/main/java/filter/LoginFilter.java:

package filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false); // 不创建新session // 白名单路径:登录页、注册页、静态资源、验证码接口 String path = req.getRequestURI(); if (path.contains("/login.jsp") || path.contains("/register.jsp") || path.contains("/public/") || path.contains("/captcha")) { chain.doFilter(request, response); return; } // 校验登录态:session存在且user属性不为空 if (session == null || session.getAttribute("user") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp?msg=not_login"); return; } chain.doFilter(request, response); } }

web.xml中注册(注意顺序!):

<filter> <filter-name>LoginFilter</filter-name> <filter-class>filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 必须放最前 --> </filter-mapping>

逻辑说明:req.getSession(false)是关键——如果用户未登录,getSession(true)会强制创建新session,导致内存泄漏;false表示只获取不创建。session.getAttribute("user")是我们在LoginServlet中session.setAttribute("user", user)存入的User对象。白名单路径用contains而非正则,性能更高;/public/目录下放CSS/JS/图片,避免被拦截。

3.3 购物车操作Servlet:GET查、POST增删、PUT改的RESTful雏形

创建src/main/java/servlet/CartServlet.java,处理/cart路径:

package servlet; import dao.CartDao; import dao.GameDao; import model.CartItem; import model.Game; import model.User; import util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.List; @WebServlet("/cart") public class CartServlet extends HttpServlet { private CartDao cartDao = new CartDao(); private GameDao gameDao = new GameDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } List<CartItem> cartItems = cartDao.getCartItemsByUserId(user.getId()); // 查询商品详情(避免N+1查询,这里用JOIN优化) for (CartItem item : cartItems) { Game game = gameDao.findById(item.getGameId()); item.setGameTitle(game.getTitle()); item.setPrice(game.getPrice()); } req.setAttribute("cartItems", cartItems); req.getRequestDispatcher("/cart.jsp").forward(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); // add, remove, update HttpSession session = req.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } try { switch (action) { case "add": int gameId = Integer.parseInt(req.getParameter("gameId")); int quantity = Integer.parseInt(req.getParameter("quantity")); cartDao.addToCart(user.getId(), gameId, quantity); break; case "remove": int itemId = Integer.parseInt(req.getParameter("itemId")); cartDao.removeFromCart(itemId); break; case "update": int updateItemId = Integer.parseInt(req.getParameter("itemId")); int updateQuantity = Integer.parseInt(req.getParameter("quantity")); cartDao.updateQuantity(updateItemId, updateQuantity); break; } resp.sendRedirect(req.getContextPath() + "/cart"); } catch (Exception e) { req.setAttribute("errorMsg", "操作失败:" + e.getMessage()); req.getRequestDispatcher("/cart.jsp").forward(req, resp); } } }

DAO层CartDao关键方法(带事务):

public void addToCart(int userId, int gameId, int quantity) throws SQLException { String sql = "INSERT INTO cart_item (user_id, game_id, quantity) VALUES (?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, gameId); ps.setInt(3, quantity); ps.executeUpdate(); } }

参数说明:action=add时,前端表单必须传gameId和quantity;remove时传itemId(购物车项ID);update时传itemId和新quantity。所有操作后重定向到/cart,避免F5刷新重复提交——这是Servlet时代最朴素的PRG模式(Post-Redirect-Get)。

3.4 订单创建Servlet:数据库事务与库存扣减的原子性保障

创建src/main/java/servlet/OrderServlet.java,处理/order/create:

package servlet; import dao.*; import model.*; import util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; @WebServlet("/order/create") public class OrderServlet extends HttpServlet { private GameDao gameDao = new GameDao(); private ActivationCodeDao codeDao = new ActivationCodeDao(); private OrderDao orderDao = new OrderDao(); private CartDao cartDao = new CartDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); User user = (User) session.getAttribute("user"); if (user == null) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:开启事务 // 1. 获取用户购物车商品 List<CartItem> cartItems = cartDao.getCartItemsByUserId(user.getId()); if (cartItems.isEmpty()) { throw new RuntimeException("购物车为空"); } // 2. 生成订单号(时间戳+用户ID+随机数,保证全局唯一) String orderNo = "GS" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + String.format("%06d", user.getId()) + String.format("%06d", new Random().nextInt(999999)); // 3. 创建订单头 OrderHeader header = new OrderHeader(); header.setUserId(user.getId()); header.setOrderNo(orderNo); header.setTotalAmount(calculateTotal(cartItems)); header.setStatus((byte) 0); // 待支付 long headerId = orderDao.insertHeader(conn, header); // 4. 遍历购物车,为每个商品分配激活码并创建明细 for (CartItem item : cartItems) { Game game = gameDao.findById(item.getGameId()); if (game.getStock() < item.getQuantity()) { throw new RuntimeException("商品【" + game.getTitle() + "】库存不足"); } // 从activation_code表中SELECT ... FOR UPDATE锁定可用码 List<Long> codeIds = codeDao.selectAvailableCodesForGame(conn, item.getGameId(), item.getQuantity()); if (codeIds.size() < item.getQuantity()) { throw new RuntimeException("商品【" + game.getTitle() + "】激活码库存不足"); } // 创建订单明细,并更新激活码状态 for (int i = 0; i < item.getQuantity(); i++) { OrderItem orderItem = new OrderItem(); orderItem.setOrderId(headerId); orderItem.setGameId(item.getGameId()); orderItem.setCodeId(codeIds.get(i)); orderItem.setQuantity(1); orderItem.setPrice(game.getPrice()); orderDao.insertItem(conn, orderItem); // 更新激活码为已使用 codeDao.updateCodeStatus(conn, codeIds.get(i), (byte) 1); } // 扣减game.stock(注意:这里只是逻辑扣减,实际库存以activation_code为准) gameDao.decreaseStock(conn, item.getGameId(), item.getQuantity()); } // 5. 清空购物车 cartDao.clearCart(conn, user.getId()); conn.commit(); // 全部成功才提交 resp.sendRedirect(req.getContextPath() + "/order/success.jsp?orderNo=" + orderNo); } catch (Exception e) { if (conn != null) { try { conn.rollback(); // 任一环节失败,全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } } req.setAttribute("errorMsg", "订单创建失败:" + e.getMessage()); req.getRequestDispatcher("/cart.jsp").forward(req, resp); } finally { DBUtil.closeConnection(conn); } } private BigDecimal calculateTotal(List<CartItem> items) { BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } return total; } }

关键点解析:

  • conn.setAutoCommit(false)开启事务,conn.commit()和conn.rollback()手动控制;
  • SELECT ... FOR UPDATE是MySQL InnoDB行锁的核心,确保并发下单时不会超卖(如两个用户同时买最后1份《赛博朋克》,只有一个能成功);
  • 订单号生成用GS+时间戳+用户ID+随机数,避免UUID的存储开销和索引碎片;
  • codeDao.selectAvailableCodesForGame()内部SQL为:SELECT id FROM activation_code WHERE game_id=? AND status=0 ORDER BY id LIMIT ? FOR UPDATE;
  • gameDao.decreaseStock()只是更新game.stock字段,真实库存以activation_code中status=0的数量为准,这是防超卖的物理保障。

4. JSP页面开发避坑指南:从EL表达式失效到中文乱码的5个致命问题

4.1 为什么JSP页面显示${user.username}却为空?——4个必须检查的环节

现象:Servlet中request.setAttribute("user", user),JSP里${user.username}输出空白,但<%= request.getAttribute("user") %>能打印出对象地址。
原因与解决:

  1. JSP页面未声明EL表达式启用:在JSP顶部添加<%@ page isELIgnored="false" %>(默认为false,但某些老旧Tomcat版本可能为true);
  2. web.xml的schema版本过低:确认web.xml开头是<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"而非http://java.sun.com/xml/ns/j2ee,后者不支持EL;
  3. 对象属性无getter方法:User类必须有getUsername(),不能只有username字段或getUserName()(首字母大小写必须严格匹配);
  4. 作用域错误:request.setAttribute()只能在本次请求有效,若用了sendRedirect()跳转,request已失效,必须用session.setAttribute()或URL传参。

4.2 中文乱码的终极解决方案:三端统一编码

现象:表单提交中文,Servlet里request.getParameter("title")显示为????。
根本原因:Tomcat默认用ISO-8859-1解码,而浏览器用UTF-8发送。
三端强制统一为UTF-8:

  • 浏览器端:JSP页面<head>中加<meta charset="UTF-8">;
  • 服务器端(Tomcat):conf/server.xml中<Connector>标签增加URIEncoding="UTF-8":
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
  • Servlet端:在doPost()开头加request.setCharacterEncoding("UTF-8")(GET请求需改Tomcat配置,见上);
  • 数据库端:已用utf8mb4,且JDBC URL加?useUnicode=true&characterEncoding=UTF-8:
jdbc:mysql://localhost:3306/game_shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

4.3 JSP页面让加载完后刷新一次?——这是反模式,用重定向替代

热搜词里有“jsp页面让加载完后刷新一次”,这通常是新手想解决“表单重复提交”问题的错误思路。正确做法是:

  • 表单提交用POST,Servlet处理完后response.sendRedirect("/success.jsp");
  • 若必须在JSP中控制,用JavaScript:
<script> if (!window.location.hash) { window.location.href = window.location.href + '#loaded'; window.location.reload(); } </script>

但此方案会导致浏览器后退异常,且SEO不友好。强烈建议放弃此需求,用PRG模式彻底解决。

4.4 JSP图片如何对坐标定位?——用CSS替代table布局

热搜词“jsp图片如何对坐标定位”,本质是前端布局问题。JSP只是模板,定位靠CSS:

  • 错误示范(用table+cellpadding):
<table cellpadding="0" cellspacing="0"> <tr><td style="padding-left:100px;padding-top:50px"><img src="/images/logo.png"/></td></tr> </table>
  • 正确做法(语义化+CSS):
<div class="logo-container"> <img src="${pageContext.request.contextPath}/images/logo.png" alt="游戏商城" /> </div> <style> .logo-container { position: relative; width: 200px; height: 100px; } .logo-container img { position: absolute; top: 20px; left: 30px; } </style>

注意:JSP中图片路径用${pageContext.request.contextPath}获取上下文路径,避免硬编码/game-shop/images/。

4.5 饿了么Element图标在JSP中怎么用?——CDN引入最简单

热搜词提到“饿了么elment图标前端jsp”,Element UI是Vue组件库,JSP是服务端模板,二者不兼容。若要在JSP中用类似图标:

  • 方案1(推荐):用Font Awesome CDN,在JSP<head>中加:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">

然后用<i class="fas fa-shopping-cart"></i>;

  • 方案2:下载Element UI图标字体文件,放入/webapp/fonts/,在CSS中@font-face声明,再用<i class="el-icon-s-goods"></i>;
  • 切记:不要在JSP中写<el-button>,那需要Vue实例,JSP渲染时Vue还没加载。

5. 数据库连接与事务排错:MySQL连接泄漏、SSL错误、锁等待超时的3个实战案例

5.1 现象:Tomcat启动后内存持续增长,最终OOM —— 连接未关闭

日志特征:java.lang.OutOfMemoryError: Java heap space,jstack显示大量java.lang.Thread.State: WAITING在java.net.SocketInputStream.socketRead0。
原因:DAO层使用Connection conn = DriverManager.getConnection(...)获取连接,但未在finally块中conn.close()。
血泪教训:即使用了DBUtil.getConnection()工具类,也必须确保每次getConnection()后都有对应的close()。最佳实践是用try-with-resources:

try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 执行操作 } catch (SQLException e) { // 处理异常 } // 自动关闭conn和ps

5.2 现象:MySQL 8.0连接报错“SSL connection error” —— JDBC驱动版本不匹配

错误日志:Caused by: com.mysql.cj.exceptions.SSLParamsException: Failed to load SSL certificate。
原因:MySQL 8.0默认要求SSL连接,但旧版JDBC驱动(如mysql-connector-java-5.1.x)不支持。
解决步骤:

  1. 下载最新JDBC驱动:mysql-connector-java-8.0.33.jar(官网下载,勿用Maven中央仓库的旧版);
  2. 放入WEB-INF/lib/目录;
  3. JDBC URL强制禁用SSL(开发环境):
jdbc:mysql://localhost:3306/game_shop?useSSL=false&serverTimezone=Asia/Shanghai

注意:生产环境应配置SSL证书,此处仅为快速验证。

5.3 现象:并发下单时部分请求卡住,日志出现“Lock wait timeout exceeded” —— 行锁竞争

错误日志:com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction。
原因:多个线程同时执行SELECT ... FOR UPDATE,且锁住的行数过多(如一次扣100个码),导致锁等待超时(默认50秒)。
优化方案:

  • 缩小锁范围:SELECT id FROM activation_code WHERE game_id=? AND status=0 ORDER BY id LIMIT 1 FOR UPDATE(每次只锁1行);
  • 在循环中逐个获取码,而非一次性LIMIT N;
  • 增加锁等待超时:在JDBC URL加&lockWaitTimeout=120(单位秒);
  • 终极方案:用Redis做分布式锁,先抢锁再查库,避免数据库层锁竞争。

5.4 现象:net start mysql服务无法启动 —— MySQL配置文件语法错误

Windows环境下,执行net start mysql报错服务没有及时响应控制请求。
排查步骤:

  1. 进入MySQL安装目录bin/,执行mysqld --console,查看实时错误日志;
  2. 常见错误:my.ini中[mysqld]段落有中文注释(MySQL 5.7不支持),或basedir路径含空格未加引号;
  3. 修正后,以管理员身份运行:
mysqld --remove mysqld --install net start mysql

5.5 现象:order_item表插入失败,报错“Cannot add or update a child row” —— 外键约束失败

错误日志:com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Cannot add or update a child row: a foreign key constraint fails。
原因:插入order_item.code_id时,该ID在activation_code表中不存在,或status已被改为1(已使用)。
调试技巧:

  • 在OrderServlet中codeDao.selectAvailableCodesForGame()后,打印codeIds列表;
  • 手动执行SQL:SELECT * FROM activation_code WHERE id IN (1,2,3),确认这些ID存在且status=0;
  • 若ID存在但status!=0,说明其他请求已抢占,需在业务层提示“库存变化,请刷新重试”。

6. 从本地调试到线上部署:Tomcat+MySQL生产环境加固与性能压测技巧

6.1 Tomcat生产配置:禁用shutdown端口、限制上传大小、启用GZIP压缩

conf/server.xml关键修改:

<!-- 1. 注释掉shutdown端口,防止远程关闭 --> <!-- <Server port="8005" shutdown="SHUTDOWN"> --> <!-- 2. HTTP连接器增加安全配置 --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" maxThreads="200" <!-- 最大线程数 --> minSpareThreads="25" <!-- 最小空闲线程 --> maxSwallowSize="-1" <!-- 禁用请求体截断 --> compression="on" <!-- 启用GZIP --> compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,text/plain,application/javascript,application/json" /> <!-- 3. Context增加安全头 --> <Context antiResourceLocking="false" privileged="true"> <Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="x-forwarded-for" protocolHeader="x-forwarded-proto" internalProxies="127\.0\.0\.1" /> <Valve className="org.apache.catalina.valves.HttpHeaderSecurityValve" securePagesWithPragma="false" /> </Context>

参数说明:maxThreads=200应对并发峰值;compression="on"减少JS/CSS传输体积;antiResourceLocking="false"提升静态资源访问速度(开发时设为true便于热更新)。

6.2 MySQL生产加固:创建专用账号、限制权限、开启慢查询日志

创建game_shop专用账号:

CREATE USER 'game_app'@'localhost' IDENTIFIED BY 'StrongPass!2024'; GRANT SELECT, INSERT, UPDATE ON game_shop.user TO 'game_app'@'localhost'; GRANT SELECT, INSERT, UPDATE ON game_shop.game TO 'game_app'@' <p> <a href="https://download.csdn.net/download/QQ254540457/88477199" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询