
简介这是一份面向高校计算机相关专业毕业设计的完整项目源码包主题为基于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 而不是一上来就 Spring2.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); ListToy 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/listdoGet里先判断分类参数再调用 DAO 查询最后用forward转发而不是redirect因为要携带request里的数据。参数说明categoryId为空时查全部这是列表页的默认行为Integer.parseInt这里没做异常捕获生产环境必须包一层 try-catch否则用户传个abc就直接 500这是答辩时容易被问的点。对应的 DAO 用 JDBC 写注意一定要用PreparedStatement别用字符串拼接 SQL否则 SQL 注入演示环节老师随手一个 or 11就能让你下不来台。// ToyDao.java —— 参数化查询杜绝 SQL 注入 public ListToy findByCategory(int categoryId) { ListToy 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, ListCartItem 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()*90001000); // ... insert orders此处省略具体 SQL // 3. 写订单明细 扣减库存 for (CartItem item : items) { // insert order_itemupdate 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 prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${toys} vartoy tr tdimg src${pageContext.request.contextPath}/upload/${toy.imgUrl} width80//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 tda hrefcart/add?toyId${toy.id}加入购物车/a/td /tr /c:forEach /table逻辑说明c:forEach遍历request里的toysc: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 contentTypetext/html;charsetUTF-8 %Servlet 里req.setCharacterEncoding(UTF-8)必须在取参数之前调用数据库连接 URL 加?useUnicodetruecharacterEncodingutf8。三处缺一不可我见过只改前两处、数据库连接漏了的排查半天。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不用equalsequals会比较精度位数。这个坑不踩一次很难记住。5.4 图片上传后访问 404现象是上传成功但页面裂图原因是存储路径和访问路径不一致现象上传返回成功但img标签加载 404。原因文件存到了项目外部目录但 Tomcat 没有配置该目录的静态资源映射。解决在 Tomcat 的server.xml里配Context docBaseD:/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。毕设的分数往往就藏在这些边界处理里。做这个系统最大的教训是别等到答辩前三天才联调数据库字段类型、编码、路径这三样东西越早统一越省心。希望帮到你。本文还有配套的精品资源点击获取