简介:这是一套基于JSP+Servlet+MySQL实现的游戏购买网站源码,面向学习Java Web课程设计、毕业设计或小型电商项目实战的学生与开发者,可用于理解传统MVC分层、数据库访问与购物流程落地。管理员端覆盖登录、订单管理、客户管理、游戏管理与类目管理;用户端包含首页浏览、用户登录、购买游戏、查看购物车、提交订单及查看订单,并配有admin/admin与user1/123456测试账号,便于快速跑通业务闭环。压缩包共246个文件、约14.85MB,以55个Java源文件、55个class、20个JSP页面为核心,辅以62张jpg与7张png界面素材、9个jar依赖、8个js与6个css样式脚本,以及SQL脚本和XML配置,能较直观地还原项目结构与页面资源组织。已有99人学习下载,适合按DAO、Servlet、JSP三层对照阅读,也可作为课程设计模板二次改造,重点借鉴订单与购物车状态流转、管理员权限入口划分及Tomcat8/9下的部署排错思路。
1. 从一份课程设计选题说起:游戏购买网站为什么值得做
选题会上有人问:一个游戏购买网站,和普通电商有什么区别,值得单独做一套 JSP + Servlet + MySQL 的课程设计吗?差别在“商品形态”。游戏是虚拟商品,下单即发码或绑账号,没有物流、没有库存损耗,但有分区定价、版本差异(标准版/豪华版)、折扣期、退款窗口,还有“已购不可重复购买”这类校验。把这些约束跑通,业务闭环比卖实物更紧凑,也更适合在有限周期内做完整。这套方案面向需要交 JSP/Servlet 课程设计或毕设的学生,也适合想快速复刻一套 Java Web 三层架构交易系统的开发者——用最朴素的 Servlet 容器,把游戏购买这条链路从浏览、加购、下单到订单查询一次打通,不依赖任何重型框架,答辩时每一行都能讲清来路。
2. 需求与选型:四个角色、五条业务链定下技术底座
2.1 先把角色和业务链路切清楚
做这类系统最容易翻车的地方,是一上来就建表。表建完了,才发现“游客能不能加购”“管理员和运营是不是同一个人”“退款要不要走审批”这些没定,改表比写代码还累。我一般的做法是先画一张角色-动作矩阵,把三类使用者和他们能触发的动作列全,再反推表结构。
角色就三个:游客(未登录,可浏览、搜索、看详情)、注册用户(可加购、下单、支付、查订单、评价)、后台管理员(管游戏、管分类、管订单状态、管用户)。业务链有五条:
- 浏览链:首页推荐 → 分类筛选 → 搜索 → 游戏详情页
- 交易链:加入购物车 → 结算 → 生成订单 → 模拟支付 → 订单状态流转
- 用户链:注册 → 登录 → 个人中心 → 我的订单 → 修改资料
- 内容链:游戏评价、评分展示、销量排序
- 后台链:游戏上下架、分类维护、订单查看与发货(发码)、用户封禁
这五条链决定了系统至少要有 8 张核心表。角色矩阵还有个隐性好处:答辩时老师问“这个功能谁能用”,你能立刻答上来,而不是现场想。
2.2 技术选型:为什么不用框架,反而更好交付
技术栈按标题来:JSP + Servlet + MySQL,JDBC 直连,前端用 JSP 内嵌 JSTL 和少量原生 JS,服务器用 Tomcat 跑 Servlet。很多人第一反应是“怎么不用 Spring Boot”,但这个场景恰恰应该不用。原因有三条,都是实打实的工程理由:
第一,课程设计和毕设考的是“你懂不懂 HTTP 请求怎么进来、Servlet 怎么分发、会话怎么维持”,框架把这一层封成黑匣子,你写得越快,答得越虚。第二,Servlet + JSP 的调试路径极短,报错能直接落到某一行的doGet,排查成本低。第三,不引依赖意味着环境只要 JDK + Tomcat + MySQL 三件套,换台机器十分钟就能跑起来,验收时不会因为 Maven 拉包失败卡住。
选型定下来之后,工程结构也要跟着定。常规做法是按 MVC 分层:
game-mall/ ├── src/main/java/com/demo/gamemall/ │ ├── controller/ # Servlet:登录、游戏、购物车、订单 │ ├── service/ # 业务逻辑:下单校验、状态流转 │ ├── dao/ # JDBC 数据访问 │ ├── entity/ # 实体类:User / Game / Order / OrderItem │ └── util/ # DBUtil、MD5、分页工具 ├── src/main/webapp/ │ ├── WEB-INF/views/ # JSP 页面,禁止外部直接访问 │ ├── static/ # css / js / 游戏封面图 │ └── WEB-INF/web.xml # Servlet 映射与过滤器配置 └── sql/init.sql # 建库建表 + 初始化数据views目录放在WEB-INF下面是个关键细节:JSP 放外面会被用户直接敲 URL 访问到,绕过 Servlet 里做的权限判断,这是很多同学交上去被扣分的地方。用WEB-INF包住,只能由 Servlet 内部forward过去,访问路径就受控了。
数据库选 MySQL 5.7 及以上即可,字符集统一utf8mb4。连接方式用 JDBC,不引连接池也行,但更稳妥的做法是手写一个极简的连接管理工具类,或者引druid单包——课程设计里引不引池都能过,关键是你要能解释“为什么每次请求都 new 一个 Connection 会有问题”。
2.3 核心数据表设计:八张表撑起整条交易链
表设计是整个方案里最该花时间的地方。下面给出建表语句的核心部分,字段做了精简但约束齐全,可以直接用。
-- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5加盐后的密码', nickname VARCHAR(32), email VARCHAR(64), role TINYINT NOT NULL DEFAULT 0 COMMENT '0普通用户 1管理员', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0封禁', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 游戏商品表 CREATE TABLE t_game ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL COMMENT '游戏名称', category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT '原价', discount DECIMAL(3,2) DEFAULT 1.00 COMMENT '折扣系数 0.80=八折', cover_url VARCHAR(255), description TEXT, stock INT NOT NULL DEFAULT 0 COMMENT '可售激活码数量', sales INT NOT NULL DEFAULT 0 COMMENT '销量,用于排序', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_status_sales (status, sales DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号', user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', pay_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id, create_time DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;另外五张表是t_category(分类)、t_cart(购物车,用户与游戏唯一索引)、t_order_item(订单明细,记录下单时的价格快照)、t_comment(评价)、t_code(激活码,属于某游戏,被某订单占用)。其中两个设计点值得单独说:
一是t_game里的discount存系数而不是直接改price。原价和折扣价分开存,结算时能用“原价 × 折扣”复算,运营调价时历史订单不受影响;如果直接把 price 改成折后价,退回原价时就丢了基准。二是t_order_item必须冗余存下单时的price,不能只存 game_id 去关联查价格——商品价格随时会变,订单是法律意义上的凭证,快照不能省。这两点在答辩里被问到,能答上来就是加分项。
3. 总体设计:三层架构下的请求流转与模块划分
3.1 一次请求的完整路径
把架构讲清楚,比堆功能更重要。这个系统采用典型的 MVC 三层结构,一次“加入购物车”的请求大致走这样一条路:
浏览器 POST 到/cart/add→CartServlet.doPost接收,从HttpSession取当前登录用户 → 调CartService.add(userId, gameId, quantity)做业务校验(是否已购买、库存是否充足、数量是否合法)→CartDao执行 JDBC 的INSERT ... ON DUPLICATE KEY UPDATE→ 返回结果 → Servlet 要么redirect回列表页,要么forward到错误页。
这条路径上有三件事必须定死:第一,所有请求先进 Filter,做编码设置和登录拦截,避免每个 Servlet 重复写request.setCharacterEncoding("UTF-8");第二,Service 层只做业务判断,不碰HttpServletRequest,也不写 SQL;第三,DAO 层只做 CRUD,不做业务判断。分层的意义在于“换个界面不用重写业务”,答辩时老师很爱问“你的业务逻辑写在哪”,答在 Service 层且能指出具体文件,比含含糊糊强得多。
3.2 过滤器与登录拦截
编码和权限拦截用一个 Filter 就能覆盖全部请求,这是性价比最高的写法。
// EncodingFilter:统一编码 + 登录拦截 public class AuthFilter implements Filter { // 白名单:无需登录即可访问的路径 private static final Set<String> WHITE = Set.of( "/login", "/register", "/game/list", "/game/detail", "/index" ); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding("UTF-8"); // 必须在取参数之前设置 response.setContentType("text/html;charset=UTF-8"); String path = request.getServletPath(); HttpSession session = request.getSession(false); boolean logged = session != null && session.getAttribute("loginUser") != null; // 后台路径额外校验角色 if (path.startsWith("/admin") && !isAdmin(session)) { response.sendRedirect(request.getContextPath() + "/login"); return; } if (!logged && !WHITE.contains(path)) { response.sendRedirect(request.getContextPath() + "/login?from=" + path); return; } chain.doFilter(req, resp); } }这里有两个容易忽略的点。一是setCharacterEncoding必须写在getParameter之前,否则在旧版 Tomcat 上 POST 中文照样乱码,属于遇到才懂的玄学问题。二是getSession(false)而不是getSession(),后者在用户没会话时会新建一个空 Session,配合某些监控工具会产生一堆垃圾会话。另外白名单用Set.of要求 JDK 9+,如果环境还是 JDK 8,改成Arrays.asList().contains()即可。
web.xml里注册时要注意顺序,编码过滤器放第一个:
<filter> <filter-name>authFilter</filter-name> <filter-class>com.demo.gamemall.util.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>authFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>URL 映射用/*全拦,再在代码里用白名单放行,比配一堆*.jsp更可控。注意静态资源(css/js/图片)也会被拦,如果白名单没放行,页面会光秃秃的没有样式,这个问题我第一次做的时候卡了半小时。
3.3 模块职责与页面清单
模块划分按业务链来,每个模块一个 Servlet 类,对应一组 JSP:
| 模块 | Servlet | 核心方法 | 对应页面 |
|---|---|---|---|
| 用户 | UserServlet | register / login / logout / profile | login.jsp / register.jsp / profile.jsp |
| 游戏 | GameServlet | list / detail / search | index.jsp / list.jsp / detail.jsp |
| 购物车 | CartServlet | add / update / remove / list | cart.jsp |
| 订单 | OrderServlet | create / pay / list / detail | order_list.jsp / order_detail.jsp |
| 后台 | AdminServlet | gameCrud / orderList / ship | admin/ 下的管理页 |
一个 Servlet 用action参数分发具体方法,比给每个操作建一个 Servlet 类更好维护。比如/game?action=detail&id=3,在doGet里 switch 到对应方法。这种写法在 Servlet 3.0 之后也可以用@WebServlet("/game")注解替代 web.xml 配置,二者选一即可,别混着用——混用会导致映射冲突,启动就报错,是个高频踩坑点。
4. 关键模块实现:下单链路与会话维持
4.1 购物车与下单:事务是绕不过去的坎
下单是整个系统最容易出问题的地方。一个订单要同时做三件事:写t_order主表、批量写t_order_item明细、扣减/占用激活码。这三步必须在一个事务里,任何一步失败都得回滚,否则就会出现“有订单没明细”或者“钱扣了没发码”的脏数据。
// OrderService.createOrder:下单主流程 public String createOrder(int userId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 List<CartItem> items = cartDao.listByUser(conn, userId); if (items.isEmpty()) throw new BizException("购物车为空"); String orderNo = OrderNoGen.next(); // 时间戳 + 随机数 BigDecimal total = BigDecimal.ZERO; for (CartItem it : items) { // 校验:库存不足或已购买则整单失败 if (it.getStock() < it.getQuantity()) throw new BizException("商品库存不足:" + it.getTitle()); // 价格用 BigDecimal,避免 double 精度丢失 total = total.add(it.getPrice().multiply(it.getDiscount()) .multiply(BigDecimal.valueOf(it.getQuantity()))); } orderDao.insert(conn, orderNo, userId, total); // 1. 主表 orderItemDao.batchInsert(conn, orderNo, items); // 2. 明细 conn.commit(); // 全部成功才提交 cartDao.clear(conn, userId); // 清空购物车 return orderNo; } catch (Exception e) { if (conn != null) conn.rollback(); // 任一失败回滚 throw e; } finally { DBUtil.close(conn); // 归还/关闭连接 } }几个参数和写法要能解释清楚:setAutoCommit(false)后所有 DML 都进同一事务,commit前数据对外不可见;金额一律用BigDecimal,double在 0.1+0.2 这种场景会算出 0.30000000000000004,电商系统用 double 属于硬伤;订单号用“yyyyMMddHHmmss + 4 位随机”生成,比自增 ID 对外暴露更安全;库存扣减放在支付成功之后再真正减,下单阶段只做校验,避免用户下单不付款把库存锁死。
注意:事务要用同一个 Connection,Service 内部不能一会儿用工具类 new 一个连接、一会儿又用 DAO 自己开的连接,否则事务边界就断了。这是手写 JDBC 时最隐蔽的坑。
4.2 会话与登录态:Session 怎么用才不出事
登录成功后把User对象(去掉密码字段)放进HttpSession,key 统一叫loginUser,全站取用这一个名字,别这个 Servlet 放user、那个放currentUser,取不到就 NPE,排查半天才发现 key 不一致。
// LoginServlet 核心片段 User user = userService.login(username, md5(password + SALT)); if (user == null) { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response); } else { user.setPassword(null); // 敏感字段不入会话 request.getSession().setAttribute("loginUser", user); request.getSession().setMaxInactiveInterval(30 * 60); // 30 分钟超时 response.sendRedirect(request.getContextPath() + "/game?action=list"); }密码加盐 MD5 虽然强度不高,但比明文强,答辩时能说出“为什么不存明文”就行。更好的做法是 BCrypt,但引第三方库需评估环境。setMaxInactiveInterval设置会话超时,默认 30 分钟,验收时如果老师让你演示“长时间未操作后退回登录页”,这项就是考点。登录成功后用sendRedirect而不是forward,是为了避免用户刷新页面重复提交表单——这个细节面试和答辩都常问。
4.3 游戏列表的分页与搜索
列表页要处理三件事:分页、分类筛选、关键词搜索。SQL 层面用LIMIT offset, size配合COUNT(*)算总页数。
// GameDao 分页查询,参数化防注入 public List<Game> page(Connection conn, Integer categoryId, String keyword, int pageNo, int pageSize) throws SQLException { StringBuilder sql = new StringBuilder( "SELECT * FROM t_game WHERE status = 1"); List<Object> params = new ArrayList<>(); if (categoryId != null) { sql.append(" AND category_id = ?"); params.add(categoryId); } if (keyword != null && !keyword.isBlank()) { sql.append(" AND title LIKE ?"); // 用占位符,绝不拼字符串 params.add("%" + keyword.trim() + "%"); } sql.append(" ORDER BY sales DESC LIMIT ?, ?"); params.add((pageNo - 1) * pageSize); params.add(pageSize); return query(conn, sql.toString(), params); }两个必须讲清的设计点:第一,LIKE一律用PreparedStatement占位符,拼字符串就是 SQL 注入的入口,搜索框输入' OR '1'='1就能把全表拖出来,这是安全红线;第二,排序按sales DESC而不是按 id,游戏商城首页默认展示热销,符合真实产品逻辑,也让你在答辩时能解释“推荐位怎么来的”。LIMIT的 offset 在数据量大时会变慢,但课程设计数据量小,不必上游标分页,够用即可,别过度设计。
5. 避坑清单:五个最容易翻车的点
5.1 中文乱码三处齐发
现象:表单提交的中文入库变成问号,JSP 页面显示乱码,GET 请求参数乱码。原因通常不是一处,而是三层都没统一:JSP 页头缺<%@ page contentType="text/html;charset=UTF-8" %>、Filter 里编码没设或设在取参之后、数据库连接串没带characterEncoding=utf8。解决方式是三处一起查,JDBC URL 写成jdbc:mysql://localhost:3306/gamemall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,Tomcat 8 以上 GET 默认 UTF-8 一般不用改server.xml,但老版本要留意。
5.2 事务不回滚,脏数据留在库里
现象:下单时明细插入失败抛异常,但主订单记录还在,后台能看到一笔“空订单”。原因多为异常被 Service 内部 catch 后没重新抛出,或者回滚用的 Connection 和提交用的不是同一个。解决:Service 里捕获后必须throw或显式rollback,并且整个流程共用一个 Connection,DAO 方法一律接收Connection参数而不自己开。
5.3 表单重复提交,一笔订单扣两次
现象:用户点“提交订单”手快点了两下,生成两个订单号。原因是没有幂等控制。解决有三条路:前端按钮点击后disabled、后端生成订单前用 Session 存一个 token 校验一次、或者给订单表加“用户 + 购物车快照”唯一约束。最简单有效的是 token 方案,在结算页渲染时生成 UUID 放 Session 和隐藏域,提交时比对并立即移除。
5.4 后台越权访问,普通用户能进管理页
现象:普通登录用户手动敲/admin/game?action=list能进后台。原因只在登录 Filter 里判断了“是否登录”,没判断角色。解决是在 Filter 里对/admin/**单独校验user.getRole() == 1,不满足直接 403 或跳登录页。更稳的做法是后台 Servlet 里也做一次校验,双重保险,别只依赖一层。
5.5 连接泄漏,演示到一半数据库连不上
现象:系统跑十几分钟后所有请求卡住,日志报Too many connections。原因是 DAO 里 Connection、Statement、ResultSet 没在 finally 关,或者关了 Connection 没关 ResultSet。解决是严格按“后开先关”的顺序在 finally 里逐层关闭,或者改用 try-with-resources。手写 JDBC 最容易忘的就是 ResultSet,它不关会一直占着游标。
6. 验收与答辩:把功能演示变成一次可复现的走查
验收这件事,很多人栽在“系统能跑但讲不清”。我一般会在答辩前一天做一次完整走查,按固定顺序走,走完一遍心里就有底了。走查清单大致是这样:
| 顺序 | 操作 | 要展示的点 | 常见追问 |
|---|---|---|---|
| 1 | 游客浏览首页、切换分类、搜索关键词 | 分页、排序、搜索防注入 | 排序字段怎么定的 |
| 2 | 未登录点“加入购物车” | 被拦截跳登录页 | Filter 怎么判断的 |
| 3 | 注册 → 登录 → 回跳原页面 | Session 维持、密码加盐 | 密码怎么存的 |
| 4 | 加购两件 → 结算 → 提交订单 | 事务、金额计算 | 金额为什么用 BigDecimal |
| 5 | 模拟支付 → 查看订单详情 | 状态流转 | 状态机怎么设计的 |
| 6 | 切换管理员登录 → 后台上下架 → 发货 | 角色校验 | 越权怎么防的 |
| 7 | 普通用户访问后台 URL | 403 拦截 | 拦截写在哪层 |
走查时有个技巧:每一步都提前准备好“如果被问就打开这个文件”的位置。比如被问事务,直接打开OrderService指setAutoCommit(false)和commit两行;被问防注入,打开GameDao指占位符。这比背稿子有用得多,也显得你真的写过。
数据库这块建议提前导出一份init.sql,包含建表语句和几条测试数据,答辩机上现场执行一遍,既证明可复现,也避免被人质疑“数据是手填的”。另外准备一句能说清项目边界的话:这是个教学演示系统,支付是模拟的、激活码是预置的、没有接真实第三方服务,把边界说在前面,比被追问时露怯好。老师真正想确认的不是“你有没有做个淘宝”,而是“你知不知道一个交易系统最少要管哪些事”——订单快照、事务边界、权限校验、幂等,这四件事你答得上,分数就稳了。
从那以后我每次交项目前,都会强制自己按这张清单从头走一遍,不看代码只按用户视角点,点到哪一步卡住就是哪一步的 bug。这套习惯帮我挡掉过不少低级错误。希望这份方案能帮到你,把课程设计做成一份讲得出、跑得动、经得起问的东西。
本文还有配套的精品资源,点击获取