☰
基于JSP的玩具销售系统毕业设计:从技术选型到并发防超卖的完整实现
2026/10/8 4:51:37 网站建设 项目流程

简介:这是一份面向高校计算机相关专业毕业设计的完整项目源码包,主题为基于JSP的网上玩具销售系统,适合正在准备毕设或需要Java Web实战案例的学生与开发者参考。项目采用JSP结合Servlet、JavaBeans实现服务端逻辑,配套SQL Server 2005数据库脚本,涵盖商品浏览、购物车、用户中心、订单管理等电商核心模块,并涉及HTML、CSS、JavaScript前端页面与session、cookie等安全处理思路。压缩包为rar格式,共1059个文件,约23.2MB,其中包含76个jsp页面、98个html、29个css、226个js脚本,以及gif、jpg、png等图片素材,另有java、class、jar、sql、xml等源码与配置文件和项目报告文档,结构完整便于二次开发与学习。目前已有390人学习下载,可作为毕设选题参考、课程设计模板或Java Web入门练手项目。

1. 从一份“能跑起来”的 JSP 玩具销售系统说起:它到底解决什么问题

很多计算机毕业设计的选题里,“基于 JSP 的网上玩具销售系统”出现的频率高得离谱,但真正能拿得出手、答辩时不被老师追问到卡壳的却不多。原因不是 JSP 过时,而是大部分人把它做成了一个“能点开、能下单、但经不起推敲”的壳子。这个标题背后真正要交付的,是一套完整的 B/S 架构电商雏形:用户能浏览玩具、加购物车、下单;后台能管商品、管订单、管库存;数据落在关系型数据库里,页面由 JSP 动态渲染。它适合软件工程、计算机科学与技术专业的本科毕业生,也适合想用最短路径理解“请求—业务—持久化”三层怎么串起来的新手。玩具这个品类选得其实很聪明——SKU 有分类(毛绒、积木、遥控)、有适龄段、有图片展示需求,天然逼着你处理分类树、图片上传和库存扣减,比“图书管理系统”更能撑起工作量。接下来我不讲空泛的“系统概述”,而是按我实际带过几届毕设的经验,把选型、建表、核心链路、避坑和进阶验证一层层拆开,让你照着能复现,答辩时能扛住追问。

2. 技术选型与工程骨架:为什么是 JSP + Servlet + JDBC 而不是一上来就 Spring

2.1 选型理由:毕设场景下 JSP 的“够用”和“边界”

先把话说透:如果这是企业项目,我不会推荐纯 JSP。但在毕业设计这个约束下——周期通常 8 到 12 周、要自己讲清每一层、答辩老师要看到你理解底层——JSP + Servlet + JDBC 的组合反而是性价比最高的。JSP 负责视图渲染,Servlet 负责请求分发和业务编排,JDBC 负责数据访问,三者边界清晰,你写的每一行都能在答辩时解释“为什么在这里”。常见做法是用 MVC 手动分层:entity放实体类,dao放数据库操作,service放业务逻辑,servlet放控制层,webapp下放 JSP 页面。这个结构不依赖任何重型框架,Tomcat 一挂就能跑,出问题也好定位。

需要提醒的是 JSP 的边界:它本质是 Servlet 的语法糖,第一次访问会被编译成.java再编译成.class。所以页面里写太多 Java 代码(脚本片段<% %>)会让后期维护变成灾难,我的习惯是 JSP 里只做循环和取值,业务判断全部推到 Servlet 或 Service。另外 JSP 的九大内置对象里,request、session、application三个作用域必须吃透,购物车用session、商品分类用application缓存、单次请求参数用request,用错作用域是新手最常见的翻车点。

2.2 环境搭建与最小可运行骨架

先确认工具链:JDK 8 或 11(别用太新的,Tomcat 8.5/9 对高版本 JDK 的模块化限制会给你添堵)、Tomcat 8.5 或 9、MySQL 5.7 或 8.0、IDE 用 IntelliJ IDEA 或 Eclipse 都行。下面是我一般会先跑通的最小骨架,一个能返回玩具列表的 Servlet。

// ToyListServlet.java —— 最小可运行的商品列表接口 @WebServlet("/toy/list") public class ToyListServlet extends HttpServlet { private ToyDao toyDao = new ToyDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 取分类参数,没有则查全部 String categoryId = req.getParameter("categoryId"); List<Toy> toys; if (categoryId == null || categoryId.isEmpty()) { toys = toyDao.findAll(); } else { toys = toyDao.findByCategory(Integer.parseInt(categoryId)); } // 2. 放入 request 作用域,转发给 JSP 渲染 req.setAttribute("toys", toys); req.getRequestDispatcher("/toy_list.jsp").forward(req, resp); } }

这段代码的逻辑说明:@WebServlet注解替代了 web.xml 配置,映射路径/toy/list;doGet里先判断分类参数,再调用 DAO 查询,最后用forward转发而不是redirect,因为要携带request里的数据。参数说明:categoryId为空时查全部,这是列表页的默认行为;Integer.parseInt这里没做异常捕获,生产环境必须包一层 try-catch,否则用户传个abc就直接 500,这是答辩时容易被问的点。

对应的 DAO 用 JDBC 写,注意一定要用PreparedStatement,别用字符串拼接 SQL,否则 SQL 注入演示环节老师随手一个' or '1'='1就能让你下不来台。

// ToyDao.java —— 参数化查询,杜绝 SQL 注入 public List<Toy> findByCategory(int categoryId) { List<Toy> list = new ArrayList<>(); String sql = "SELECT id, name, price, stock, img_url FROM toy WHERE category_id = ? AND status = 1"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, categoryId); // 参数占位,不拼接 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Toy t = new Toy(); t.setId(rs.getInt("id")); t.setName(rs.getString("name")); t.setPrice(rs.getBigDecimal("price")); t.setStock(rs.getInt("stock")); t.setImgUrl(rs.getString("img_url")); list.add(t); } } } catch (SQLException e) { e.printStackTrace(); // 毕设可接受,生产要上日志 } return list; }

DBUtil负责拿连接,毕设阶段用最简单的DriverManager即可,不必上连接池,但要在答辩时说明“生产环境会换成 Druid 或 HikariCP”,这体现你有工程意识。status = 1是软删除标记,商品下架不物理删除,保留订单关联,这个设计点讲出来是加分项。

3. 数据库设计与核心业务链路:从玩具表到下单扣库存

3.1 表结构:六张表撑起一个电商雏形

玩具销售系统的表不用多,但每张都要有存在的理由。我一般设计六张核心表:user(用户)、category(分类)、toy(商品)、cart(购物车)、orders(订单主表)、order_item(订单明细)。分类和商品是一对多,订单和明细是一对多,购物车关联用户和商品。下面这张表把关键字段和设计意图列清楚。

表名关键字段设计意图
userid, username, password, phone, rolerole 区分普通用户和管理员,0/1 即可
categoryid, name, parent_id, sortparent_id 支持二级分类,玩具分大类小类
toyid, name, price, stock, category_id, img_url, age_range, statusage_range 存“3-6岁”这类适龄段,玩具特色
cartid, user_id, toy_id, quantity同一用户同一商品只存一条,加购时更新数量
ordersid, order_no, user_id, total_amount, status, create_timeorder_no 用时间戳+随机数,避免自增暴露单量
order_itemid, order_id, toy_id, price, quantityprice 冗余存下单时价格,商品调价不影响历史订单

这里有个血泪经验:order_item里的price必须冗余存储,不能只存toy_id然后关联查当前价。否则商品一调价,历史订单金额全变,对账直接崩。同理orders的total_amount也是下单时算好落库,不要每次查询再累加。

3.2 下单链路:一个事务里做完扣库存和写订单

下单是整个系统最容易出问题的地方,核心就一句话:扣库存和写订单必须在同一个事务里,且库存判断要加行锁。下面是我常用的下单 Service 骨架。

// OrderService.java —— 下单核心:事务 + 行锁 + 库存校验 public String createOrder(int userId, List<CartItem> items) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 BigDecimal total = BigDecimal.ZERO; // 1. 逐项校验库存并锁定行(FOR UPDATE) for (CartItem item : items) { String lockSql = "SELECT stock, price FROM toy WHERE id = ? FOR UPDATE"; try (PreparedStatement ps = conn.prepareStatement(lockSql)) { ps.setInt(1, item.getToyId()); try (ResultSet rs = ps.executeQuery()) { if (!rs.next() || rs.getInt("stock") < item.getQuantity()) { throw new SQLException("库存不足: toyId=" + item.getToyId()); } total = total.add(rs.getBigDecimal("price") .multiply(new BigDecimal(item.getQuantity()))); } } } // 2. 写订单主表 String orderNo = System.currentTimeMillis() + "" + (int)(Math.random()*9000+1000); // ... insert orders,此处省略具体 SQL // 3. 写订单明细 + 扣减库存 for (CartItem item : items) { // insert order_item;update toy set stock = stock - ? where id = ? } conn.commit(); // 全部成功才提交 return orderNo; } catch (SQLException e) { if (conn != null) conn.rollback(); // 任一失败全回滚 throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

逻辑说明:setAutoCommit(false)开启事务,FOR UPDATE在查询时就锁住商品行,防止并发下单超卖。参数说明:total用BigDecimal累加,绝不能用double,金额精度问题在答辩时被问到会很难看。orderNo用时间戳加随机数,保证唯一且不暴露业务量。注意finally里要把autoCommit还原再关闭连接,否则连接池复用时状态会串。

购物车到订单的转换要注意:下单成功后要清空对应用户的购物车记录,但只清已下单的那些商品,别一把DELETE FROM cart WHERE user_id = ?全清了,用户可能还有没勾选的商品。这个细节我在评审时见过至少三个人翻车。

4. 页面渲染与交互细节:JSP 里那些容易翻车的地方

4.1 商品列表与分页:JSTL 替代脚本片段

JSP 里最忌讳满屏<% %>。用 JSTL 和 EL 表达式能让页面干净很多,也显得你懂规范。分页是列表页必备,SQL 层用LIMIT加偏移量,页面层用 JSTL 循环渲染。

<%-- toy_list.jsp —— JSTL 渲染商品列表,避免脚本片段 --%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <c:forEach items="${toys}" var="toy"> <tr> <td><img src="${pageContext.request.contextPath}/upload/${toy.imgUrl}" width="80"/></td> <td>${toy.name}</td> <td>¥${toy.price}</td> <td> <c:choose> <c:when test="${toy.stock > 0}">有货</c:when> <c:otherwise>缺货</c:otherwise> </c:choose> </td> <td><a href="cart/add?toyId=${toy.id}">加入购物车</a></td> </tr> </c:forEach> </table>

逻辑说明:c:forEach遍历request里的toys,c:choose根据库存显示状态。参数说明:${pageContext.request.contextPath}拿应用上下文路径,避免部署路径变化导致图片 404,这是很多人忽略的点。图片路径用相对上下文而非硬编码/upload/,部署到不同 context 下才不会挂。

4.2 图片上传与坐标定位:玩具图片展示的两个坑

玩具系统图片多,上传功能绕不开。用 Servlet 3.0 的@MultipartConfig就能处理,不必引第三方库。但有两个坑:一是上传目录不能放在项目源码目录里,Tomcat 重启会丢;二是文件名要重命名,否则同名覆盖。常见做法是存到服务器一个固定外部目录,数据库只存文件名。

关于热搜里提到的“jsp 图片如何对坐标定位”,这在玩具系统里其实有真实场景:比如商品详情页要在图片上标注“适合3岁以上”的角标,或者做图片热区点击。实现方式是用 CSS 的position: relative加绝对定位的div,坐标存数据库或写死在前端。如果要做可配置的热区,就在toy表加一个hotspot字段存 JSON 坐标串,页面渲染时解析。这个点如果做出来,答辩时是个不错的亮点,但别过度设计,玩具系统用不上复杂的图像标注。

// UploadServlet.java —— 图片上传,重命名防覆盖 @WebServlet("/upload") @MultipartConfig(maxFileSize = 5 * 1024 * 1024) // 单文件最大 5MB public class UploadServlet extends HttpServlet { private static final String UPLOAD_DIR = "D:/toy_upload/"; // 外部目录 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part part = req.getPart("img"); String original = part.getSubmittedFileName(); String ext = original.substring(original.lastIndexOf(".")); String newName = UUID.randomUUID().toString() + ext; // 重命名 part.write(UPLOAD_DIR + newName); // 写入外部目录 resp.getWriter().write(newName); // 返回文件名给前端 } }

参数说明:maxFileSize限制单文件大小,防止大图拖垮服务;UPLOAD_DIR用绝对路径指向项目外部,Tomcat 重启不丢文件。注意part.write的路径分隔符在 Windows 和 Linux 下不同,跨平台部署要用File.separator拼接。

5. 避坑与排查:答辩前必须自己先踩一遍的五个雷

5.1 中文乱码:现象是页面显示问号,原因是编码链没统一

现象:表单提交后数据库里存的是???,或者页面显示乱码。原因:JSP 页面、Servlet 请求、数据库连接三处编码不一致。解决:JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 里req.setCharacterEncoding("UTF-8")必须在取参数之前调用;数据库连接 URL 加?useUnicode=true&characterEncoding=utf8。三处缺一不可,我见过只改前两处、数据库连接漏了的,排查半天。

5.2 购物车数量不更新:现象是重复加购生成多条记录,原因是没做唯一约束

现象:同一商品加购两次,购物车出现两条记录。原因:cart表没加(user_id, toy_id)唯一索引,插入前也没查重。解决:加唯一索引,插入用INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + ?,一条 SQL 搞定更新或插入,比先查后插更稳。

5.3 订单金额对不上:现象是明细累加和主表金额差几分钱,原因是用了 double

现象:订单主表total_amount和明细累加差 0.01。原因:金额字段用了double或float,浮点运算精度丢失。解决:数据库用DECIMAL(10,2),Java 用BigDecimal,且BigDecimal比较用compareTo不用equals(equals会比较精度位数)。这个坑不踩一次很难记住。

5.4 图片上传后访问 404:现象是上传成功但页面裂图,原因是存储路径和访问路径不一致

现象:上传返回成功,但<img>标签加载 404。原因:文件存到了项目外部目录,但 Tomcat 没有配置该目录的静态资源映射。解决:在 Tomcat 的server.xml里配<Context docBase="D:/toy_upload" path="/upload"/>,或者用 Servlet 写一个读取文件的接口。前者简单,后者更可控,毕设用前者即可,但要在文档里说明部署时需要改配置。

5.5 会话失效后购物车丢失:现象是重启浏览器购物车空了,原因是只存了 session

现象:用户关浏览器再打开,购物车清空。原因:购物车只存在session里,没落库。解决:登录用户的购物车必须落cart表,session只做未登录时的临时存储,登录后合并。这个设计点讲清楚,能体现你理解“会话”和“持久化”的区别。

6. 进阶验证:怎么证明你的系统不是“只能演示”

6.1 用并发测试验证库存不超卖

答辩时老师最爱问“并发下单会不会超卖”。别嘴上说不会,直接跑个测试。用 JMeter 或写个简单的多线程测试,模拟 50 个线程同时抢 10 件库存的商品,看最终订单数和库存是否一致。

// StockConcurrencyTest.java —— 50 线程抢 10 件库存 public class StockConcurrencyTest { public static void main(String[] args) throws InterruptedException { int threads = 50; ExecutorService pool = Executors.newFixedThreadPool(threads); CountDownLatch latch = new CountDownLatch(threads); AtomicInteger success = new AtomicInteger(0); for (int i = 0; i < threads; i++) { final int userId = i + 1; pool.submit(() -> { try { new OrderService().createOrder(userId, Collections.singletonList(new CartItem(1, 1))); // 商品1买1件 success.incrementAndGet(); } catch (Exception e) { // 库存不足的会抛异常,不计入成功 } finally { latch.countDown(); } }); } latch.await(); System.out.println("成功下单数: " + success.get()); // 期望 <= 10 pool.shutdown(); } }

逻辑说明:50 个线程同时调用下单,FOR UPDATE行锁保证只有一个线程能扣到库存,其余抛“库存不足”。参数说明:商品初始库存设 10,期望成功数不超过 10。如果跑出来大于 10,说明你的锁没生效,大概率是FOR UPDATE写在了事务外,或者用了 MyISAM 引擎(不支持行锁,必须 InnoDB)。这个测试跑通,答辩时直接甩结果,比任何解释都有说服力。

6.2 一个我常用来收尾的检查习惯

每次交付前,我会把系统当成“陌生人第一次用”走一遍:注册一个新账号、浏览分类、加购三件不同商品、删掉一件、下单、查订单、退出、再登录看订单还在不在。这一条链路走通,基本功能就没大问题。然后我会故意输错参数——分类 ID 传字母、数量传负数、直接访问后台 URL——看系统是优雅报错还是白屏 500。毕设的分数往往就藏在这些边界处理里。做这个系统最大的教训是:别等到答辩前三天才联调,数据库字段类型、编码、路径这三样东西,越早统一越省心。希望帮到你。

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

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

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

立即咨询