JSP玩具销售系统毕设:从架构设计到事务实现的完整指南
简介面向Java方向毕业生和Web开发初学者这套基于JSP的网上玩具销售系统毕业设计资料贴合典型电商业务场景涵盖商品展示、购物车、订单管理、用户中心等功能模块适合作为课程设计或毕业设计参考。压缩包共1059个文件约23.2MB以JSP页面、Java类和SQL脚本为核心同时包含大量HTML、JavaScript、CSS样式及GIF/JPG/PNG图片素材前台与后台资源齐全便于直接部署调试。项目在Eclipse环境中开发代码体现了JSPServletJavaBean的分层设计思想对会话跟踪、数据库连接、导航栏布局等关键实现均有实际样例附带项目报告与配置文档有助于理解从需求分析到编码实现的全过程。已有390人学习下载若需快速搭建同类型玩具销售或商城系统这套材料可提供直接的移植与二次开发基础。1. 网上玩具销售系统一个JSP毕设背后的完整答题思路每年毕业季都会有不少人拿着「基于jsp的网上玩具销售系统」这个题目来找我。乍一听它只是一个普通的CRUD管理系统做过的人都知道这类题目的难点从来不在“玩具”二字而在JSP、Servlet、Session、事务这些JavaWeb老伙计是否真的被你理解透了。答辩时老师不看你的玩具图片有多好看他看的是登录、购物车、订单这几条数据流有没有走通以及你能否讲清楚每一步为什么这么写。这篇笔记就按我自己带毕设的节奏来先讲清楚技术选型怎么向答辩老师交代再落到数据库、代码、配置和一组你大概率会撞上的坑。新手照做能拿一个能跑的完整系统熟手可以直接跳到第5章对照踩坑清单。我会把设计逻辑和可复现的代码放在一起尽量做到每一步都可以直接抄。2. 为什么毕业设计还在选JSP技术选型逻辑与Model2的边界2.1 别急着“升级”JSP在毕设场景里的合理性很多学生一拿到题目就问我“老师JSP是不是太老了我要不要换成Spring Boot加Vue”我的回答通常是先别换。毕设的核心目标是三个月内跑通一个完整业务闭环并让评委相信你掌握了Web开发的基础链路。JSP Servlet JDBC MySQL这套组合恰好把HTTP请求、会话管理、数据库访问、页面渲染全部摊在你面前没有框架替你隐藏细节这对“考察基本功”这件事反而是优势。换成Spring Boot加前后端分离难度会陡增而且题目里写的是“基于jsp”你交一个纯Vue项目本身就存在跑题风险。更常见的情况是老师默认你会一点Spring Boot集成JSP的目录技巧但你用不用都行。我一般建议把JSP作为视图层留下中间用Servlet当控制器数据访问单独写成DAO这样既符合题目字面要求又能在答辩时讲出清晰的层次感。2.2 JSP、Servlet、JavaBean的分工边界要讲清楚JSP在系统里的位置必须先说Model2这套东西。网上讨论很多核心只有一句话JSP只负责渲染页面请求处理交给Servlet业务数据和数据库操作交给JavaBean/DAO。用玩具销售系统举例用户点击“加入购物车”按钮后请求先到CartServletServlet调用CartService完成购物车数据更新最后把结果转发到cart.jsp去显示。这个过程里没有任何一句Java业务代码应该出现在JSP里。如果你打开一个JSP文件看到大段% %包着for循环和if判断那基本就是Model1写法答辩时会被老师追问到很难受。jsp modeled2思想是这道题绕不开的考察点建议把它写进系统设计说明里。按这个原则我把一个玩具销售系统的代码分成四层view目录放所有JSP页面controller放Servletservice放业务逻辑dao放数据库操作。JSP页面里只出现JSTL标签和EL表达式数据从request或session里取这样页面干净分工也明确。2.3 把“基于jsp的毕设选题”做成有区分度的三层架构既然题目是“网上玩具销售系统”那业务上至少要有用户、商品、购物车、订单四块。再加一个后台管理界面管理商品和订单就有了两个角色系统完整度立刻不一样。我见过太多交上去只有前台没有后台的毕设老师一问“商品数据从哪来”就只能说是手工INSERT的非常减分。三层架构的意义不在于代码行数多而在于每一层的责任单一。以“提交订单”为例控制器只做参数校验和页面跳转Service层负责把订单写入、扣减库存、清空购物车这几件事合并在一个事务里DAO层只执行SQL。答辩时老师大概率会问“订单提交过程中如果扣库存失败怎么办”这个问题你要是答不上来前面的功夫都白费。我会在4.4节把这段代码完整写出来。一个带后台管理、有分页、有事务控制的JSP玩具商城已经超过六成同题目的作品了。3. 先把数据库和工程搭起来建表、Maven目录、Tomcat与连接池3.1 玩具销售系统的表设计从用户到订单详情的8张表动手写代码之前一定要先把表设计好。这个系统的核心关系并不复杂但表与表之间的外键逻辑必须清楚。我一般建议设计8张表用户表、玩具分类表、玩具表、购物车表、订单表、订单明细表、收货地址表、管理员表。购物车表和订单明细表结构上有点像但语义完全不同购物车是临时的订单明细是下单那一刻商品的快照。用户表字段里要有username、password、phone、email、create_time密码字段要注意长度至少给64位方便存MD5或SHA256的结果。玩具表需要name、category_id、price、stock、image、description、status其中status表示上架还是下架这样后台可以“软删除”而不影响历史订单。订单表要有order_no、user_id、total_amount、status、create_time订单明细表要有order_id、product_id、product_name、price、quantity注意这里必须冗余一份product_name和price因为商品改价或删除后订单里的历史数据不能被影响。这是电商系统设计里的基本常识答辩时主动讲出来非常加分。SQL脚本我建议直接写到项目resources目录下命名schema.sql方便老师检查。建表语句要注意所有表都使用InnoDB引擎字符集统一utf8mb4订单金额字段用DECIMAL(10,2)不要用FLOAT或DOUBLE这是做电商相关系统必须养成的习惯。下面是一个核心建表示例-- 玩具表 CREATE TABLE product ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL COMMENT 分类id关联category表, name VARCHAR(100) NOT NULL COMMENT 玩具名称, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, image VARCHAR(255) DEFAULT NULL COMMENT 封面图路径, description TEXT COMMENT 商品描述, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_id (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;主键自增是标配不要用业务字段做主键后续改分类名或商品名时不用动主键。category_id加普通索引即可商品表日常按分类查询较多这行索引在数据量上来后能明显加快列表页速度。stock字段放在商品表里下单时直接对它做条件UPDATE天然避免超卖比先SELECT再UPDATE安全得多。image字段保存相对路径比如“/upload/xxx.jpg”不存完整URL这样换域名或换端口不用改数据库。3.2 创建Maven Web工程与标准目录结构我习惯用Maven来管理项目即使学校不要求Maven也能让依赖管理省心很多。创建一个maven-archetype-webapp骨架项目然后手动补齐java源码目录。目录结构长这样src/main/java └── com.toy.store ├── controller // Servlet ├── service // 业务逻辑接口和实现 ├── dao // 数据库访问 ├── entity // 实体类 └── util // 工具类 src/main/webapp ├── WEB-INF │ ├── web.xml │ └── jsp // 受保护的页面直接URL访问不到 ├── static // css、js、图片 └── upload // 商品图片上传目录JSP页面放到WEB-INF/jsp下面是一个值得养成的习惯。放在这个目录里的页面无法被浏览器直接通过路径访问必须由Servlet转发才能到达这能防止用户绕过登录检查直接打开订单页面也逼着你把页面跳转统一走控制器。有的同学把所有JSP堆在webapp根目录下虽然能跑但安全性很差答辩时会被问住。pom.xml里需要引入的依赖其实不多javax.servlet-api、javax.servlet.jsp-api、jstl、mysql-connector-java、commons-fileupload、commons-dbcp2或c3p0连接池。注意Servlet的依赖scope要设成provided因为Tomcat自己带了一份写错会导致运行时和编译版本冲突。3.3 配置Tomcat与数据库连接池环境这块我强烈建议直接用IntelliJ IDEA加Tomcat 8.5或9.0JDK用8或11MySQL用5.7或8.0。Tomcat 10以后的包名从javax改成jakarta和大量老教程对不上新手照抄代码会直接编译失败三年血泪经验告诉你别碰这个坑。数据库连接池我一般用DBCP2配置写在src/main/resources/db.properties里在Filter或监听器里初始化一次连接池之后所有DAO从这里拿Connection用完就还回去不用反复创建连接。数据库连接配置如下# db.properties drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/toy_store?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxTotal20 maxWaitMillis3000url里必须带characterEncodingutf8和serverTimezone这两个参数能解决90%的中文乱码和时区报错。serverTimezone记得写Asia/Shanghai不写的话MySQL 8经常抛CTS时区异常报了错又不知道去哪查。maxWaitMillis是获取连接的最大等待时间设3秒是为了让系统在数据库出问题时快速失败而不是一直卡死。连接池要用一个静态代码块来初始化只初始化一次我习惯写一个DbUtil工具类提供getConnection()和close()两个静态方法所有DAO统一调用。4. 玩具销售系统四块核心功能的实现注册登录、列表分页、购物车、订单4.1 注册与登录个人信息展示页面的Session管理注册登录是每个Web系统都躲不掉的一环也是答辩老师必看的模块。注册的逻辑很简单检查用户名是否存在不存在则插入用户表然后跳转到登录页。密码不能明文存我一般用MD5加盐或者SHA-256对毕设来说MD5加盐已经足够在答辩时讲清楚系统设计文档里也可以写一句“密码通过MD5加盐存储防止数据库泄露后明文暴露”。登录成功之后把用户对象放进Session然后跳转到首页。这里有个细节用户登录后首页右上角要显示“欢迎你xxx”并提供退出按钮这就是一个典型的jsp个人信息展示页面。它看起来不复杂但涉及EL表达式的取数逻辑Session里没有值怎么显示没有值应该显示“登录”和“注册”两个链接。用JSTL的c:choose标签就能轻松实现JSP页面里不用写任何Java判断。注册功能的校验必须写在Servlet里不能用前端表单验证代替。我见过很多同学只在JS里做了非空校验用Postman直接POST绕过页面提交空用户名和非法手机号照样进数据库。Servlet里至少要校验用户名非空、两次密码一致、用户名不重复。登录校验时从数据库查出密码再对比不要把数据库密码反解出来这是底线。4.2 玩具列表与分页JSTL渲染数据玩具列表页是整个商城最核心的展示页面关键点是分页查询。分页如果全查出来再在内存里切分数据库记录一多就会卡如果硬编码LIMIT 0,10则页码切换失效。标准做法是接收page参数和pageSize参数计算offset然后用“LIMIT offset, pageSize”查询当前页数据同时单独执行一条SELECT COUNT(*)获得总记录数来算总页数。分页查询Servlet里的一般做法是// PageServlet.java 片段 int page 1; int pageSize 12; String pageStr request.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { page Integer.parseInt(pageStr); } String categoryId request.getParameter(categoryId); ProductDao dao new ProductDao(); ListProduct list dao.findByPage(categoryId, page, pageSize); int total dao.countByCategory(categoryId); int totalPages (int) Math.ceil(total * 1.0 / pageSize); request.setAttribute(productList, list); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/WEB-INF/jsp/product_list.jsp).forward(request, response);page从请求参数读出来必须处理空值和非数字异常我习惯用一个try catch包住解析失败就默认第1页保证页面永不报500。dao.findByPage返回当前页数据dao.countByCategory返回总条数两次查询分开做数据量不大时这样最清晰。把计算结果放进request域用forward转发到JSP不要用sendRedirect否则request里的数据全丢了页面拿不到列表。totalPages的计算用Math.ceil向上取整12条数据一页有100条就是9页不会因为整除出了0页。JSP页面里用c:forEach循环展示产品卡片图片路径直接用${product.image}加价、上架状态这些字段也要显示出来。分页条我习惯用上一页、下一页加页码列表的方式页码用c:forEach从1循环到totalPages当前页高亮。这块代码写熟之后后台管理页的商品列表直接复用同一套逻辑能省下不少时间。4.3 购物车用Session实现临时存储购物车是玩具销售系统里最有区分度的模块因为它的数据不需要立刻入库。很多常见做法是把购物车数据存到一张表里但匿名用户购物车会很难处理而且毕设场景下没必要把简单问题复杂化。我一般直接用Session存一个List CartItem是一个实体类包含productId、name、price、quantity、小计金额。用户登录后把Session里的购物车塞进去退出登录时清掉整个系统里只有这一个地方在操作会话数据。加入购物车的核心逻辑在CartServlet里它拿到商品id和数量然后遍历当前Session中的购物车List如果已经有这个商品就把数量加上否则new一个CartItem放进去。这个过程要注意购物车List不能直接从session里get到就开搞第一次访问时session里是null必须先判断为null就new一个ArrayList。这个“先判空再操作”的防御性写法能避免大量空指针。我经常对学生说购物车模块最值得提前做的是计算总金额。在JSP页面用循环叠加每行都要算商品小计底部还要汇总整个购物车的总金额。这个功能本身不难但涉及EL表达式和JSTL的配合很多同学搞不清怎么在JSP里做累加最后又回头写% Java代码 %一下退回Model1。页面上的金额计算可以这样处理c:set vartotal value0 / c:forEach items${cart} varitem c:set varlineTotal value${item.price * item.quantity} / c:set vartotal value${total lineTotal} / tr td${item.name}/td td${item.price}/td tdinput typenumber namequantity value${item.quantity} //td td${lineTotal}元/td /tr /c:forEach trtd总计${total}元/td/trc:set可以在页面上定义局部变量total初始为0每循环一行就把行小计累加进去这是JSTL里做统计的标准写法。item.price是DECIMAL类型乘quantity会自动转成数值运算不用手动做类型转换。修改购物车数量时建议用一个表单把商品id和新的数量提交到CartServlet重新计算不要用JS改了页面数字不改后台数据。这里是高并发订单系统里“读改写”问题的简化版答辩时可以主动提一句“毕设场景下Session购物车足够如果追求平台化需要考虑Redis替代”展示你有扩展视野。4.4 订单提交与库存扣减事务里的一步都不能少订单提交是整篇代码里最需要谨慎对待的功能没有之一。用户点击“提交订单”后系统要干四件事往订单表插入一条记录、往订单明细表插入多条记录、扣减商品库存、清空购物车。这四件事要么全成功要么全失败。如果订单表写入了库存却没扣掉就会出现超卖如果库存扣了订单却没了用户会白付款。解决办法只有一个事务。在JDBC里的做法是关闭自动提交手动commit和rollback。以下是下单Service的核心逻辑// OrderService.java public boolean createOrder(Order order, ListCartItem items) { Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交事务开始 OrderDao orderDao new OrderDao(); int orderId orderDao.insert(conn, order); // 插入订单返回自增id for (CartItem item : items) { // 条件更新库存stock quantity 时才扣减避免超卖 int rows orderDao.reduceStock(conn, item.getProductId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足); } orderDao.insertOrderItem(conn, orderId, item); } conn.commit(); // 全部成功提交事务 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DbUtil.close(conn); } }setAutoCommit(false)之后这条连接上的所有SQL都不会真正落库直到你调用commit或rollback这是控制事务的关键开关。reduceStock方法里执行“UPDATE product SET stock stock - ? WHERE id ? AND stock ?”解决的并发超卖问题在后面5.3节细讲。订单表和订单明细表在同一个事务里但插入订单明细时要把商品名称、价格快照进去防止后续商品改价导致订单金额对不上。所有SQL操作都用同一个conn对象绝对不能在这段代码里再调用DbUtil.getConnection()去拿新连接否则事务就断了库存没扣但订单已提交这种玄学Bug就是这么来的。5. JSP玩具系统最容易翻车的5个位置现象、原因、解决5.1 注册成功跳登录页表单数据神秘丢失这是几乎所有学生在联调“注册”功能时遇到的第一个玄学问题。现象是注册页填好信息点提交Servlet里明明打印出用户名了数据库也插进去了但跳转登录页后刚才填的用户名消失了。原因通常在跳转那行代码上用了sendRedirect而不是forward。sendRedirect是浏览器重新发一次请求到目标URLrequest域全部重新创建你在Servlet里setAttribute的数据也随之消失。我一般让学生先想清楚“跨请求传数据必须用Session”这个原则。跳转方式的选择逻辑是如果只是页面转跳不带数据用sendRedirect没问题如果要带数据到下一个页面优先用forward或者把数据放到session里。解决方法是把注册成功的提示信息放进session跳转后在JSP页面取一次就remove掉这样还能实现“一次性提示”的效果比request域更稳。5.2 JSP页面里堆满Java代码答辩被老师追问去学生电脑上看代码时最常见的画面是JSP页面顶部写了一段 % int count (Integer) request.getAttribute(count); %中间再来几个 % for %循环看起来能跑但答辩时老师一翻源码就直接皱眉头。这不是运行问题是架构问题。在JSP里写Java逻辑意味着视图层和业务层耦合后期改一个展示逻辑要动页面源码改了页面又有可能碰坏业务代码。解决方向只有一个把JSP里的Java代码全部换成JSTL标签和EL表达式。EL表达式负责取数据 c:forEach负责循环 c:if负责条件判断原本10行Java代码压缩成3行标签。如果某个逻辑用JSTL写不出来那就说明这个逻辑不应该在页面里做应该放到Servlet里先算好把结果setAttribute。坚持这个原则JSP页面会干净到老师挑不出毛病。5.3 并发提交订单导致库存为负这是个典型的并发问题很多同学在测试时是一个人点的永远发现不了问题。直到答辩演示时老师要求用两个浏览器同时买最后一件玩具系统直接爆出库存“-1”非常尴尬。原因很简单代码里先查库存等于0就提示失败不等于0就扣减。但在高并发场景下两个请求同时执行到“查库存”这一步都看到库存为1然后同时扣减两个订单都成功库存变成-1。解决方法是把“判断库存和扣减库存”合并成一条SQL利用数据库行锁保证原子性。代码在4.4节已经出现过UPDATE product SET stock stock - ? WHERE id ? AND stock ?。这个方法受影响的记录条数就是判断依据返回0说明库存不足返回1说明扣减成功。加上事务配合同一件商品瞬间来100个请求也不会超卖。这一条值得写进系统设计文档答辩时老师对你的印象会完全不同。5.4 请求参数中文乱码中文乱码是JSP项目里的老熟人表现很直接登录注册后用户名变成问号或者在页面上看到一堆“䏿–‡”这样的乱字符。原因通常是POST请求参数编码和服务器解码不一致。Tomcat 8以上版本对GET请求默认使用UTF-8但POST请求如果不设置默认按ISO-8859-1解码中文必乱。解决办法是在web.xml里配置一个CharacterEncodingFilter拦截所有请求强制设置request和response的编码为UTF-8。注意字符集要和数据库的utf8mb4保持一致如果页面是UTF-8、数据库是GBK那怎么配都得乱。还有一个经常被忽略的是响应编码过滤器中要同时设置response.setContentType(text/html;charsetUTF-8)否则页面显示也会出问题。5.5 部署后图片相对路径404商品图片在本地开发时显示正常部署到服务器后图片全部404这个问题也很常见。原因通常是为了省事把图片路径写成了“/user/lisi/tomcat/webapps/upload/xxx.jpg”这种绝对磁盘路径。一旦换个环境部署路径直接失效。另一个常见原因是项目以ROOT之外的名字部署图片路径又用了相对路径访问的项目路径不同就找不到图片。解决方法是统一使用相对于项目根的路径比如“/upload/xxx.jpg”并且把upload目录放在webapp下面这样随项目一起部署不会出现路径漂移。如果要写文件用request.getServletContext().getRealPath(/upload)获取绝对路径来构造File对象千万别手写磁盘路径。图片上传功能建议用commons-fileupload组件它把表单字段和文件拆分处理比原生解析input流省心得多。6. 答辩前夜的三个检查点与一个演示小技巧整个系统跑通之后别急着去玩答辩前夜把下面这三件事做一遍胜算会高不少。第一检查所有Servlet里是否还有直接打印到控制台的调试代码如果没有日志框架至少用logger输出别让“console.log式”的System.out打满控制台显得很业余。第二准备一份预置演示数据两个分类、八个商品、一个有3件商品的购物车、一个已付款的历史订单。答辩现场争取2分钟内走完“注册到下单”全流程老师没有耐心等你在搜索框里慢慢打字。第三把数据库脚本和源码一并提交到Git仓库把提交记录留下来答辩时能证明所有代码是你一步步写出来的这一点比任何文档都管用。再分享一个我每次带学生都会强调的演示技巧演示登录时故意输错一次密码让页面提示“用户名或密码错误”然后再说“这是密码校验的提示说明底层拦截生效了”。这个小动作既能展示异常处理逻辑又不会让老师觉得你在背稿。等真正登录进去再从容地把订单流程走完节奏感就出来了。JSP项目的上限不在于页面多漂亮而在于数据流转是否严谨、异常分支是否有兜底这也是你在答辩中要传递的核心信息。希望这篇文章帮你把每一环都踩稳少走几段弯路。本文还有配套的精品资源点击获取