
简介基于JSP的网上玩具销售系统是一份适合毕业设计参考的Java Web电商项目面向计算机相关专业学生及Java Web入门开发者覆盖商品展示、购物车、订单提交等典型业务场景。整个项目以玩具商城为业务背景模块划分清晰从用户注册登录到商品查询下单均有对应实现。压缩包共收录1059个文件包含JSP动态页面、Java类、前端HTML/CSS/JavaScript脚本、图片素材以及数据库SQL脚本和项目配置文件整体约23.2MB压缩包内目录结构有条理便于按模块阅读与二次开发。已有390人学习。资料中不仅提供可运行的源码和数据库设计还含有环境部署所需的关键文件与结构说明可帮助读者梳理JSPServletJavaBeans的开发流程理解前端表单与后端逻辑的交互方式也可作为课程设计或毕业设计的直接素材。1. JSP玩具商城能干什么别急着写代码先看清这套系统的四块硬骨头每年毕业设计选题季“基于JSP的网上玩具销售系统”都会出现在一大批任务书里。很多同学的第一反应是“JSP太老了”可真动手之后才发现商品分类、搜索分页、购物车、下单扣库存、个人中心、订单管理这套链路放到今天的任何电商项目里依然是核心业务。用JSP做一遍的最大优势是每一层代码都摊在你面前没有Spring Boot帮你把细节藏起来答辩论证和跑通全流程反而更扎实。这套方案适合两类人一类是需要快速交付Java Web课程设计或毕业设计的在校生另一类是写了太久框架、想回头把Servlet和会话机制补一补基础的在职开发。下面按落地顺序拆解从技术骨架到踩坑现场争取让新手能照着复现让熟手看到边界。2. 技术选型与项目骨架为什么JSPServletJavaBean这套老组合依然适合毕业设计2.1 把职责拆开JSP管展示、Servlet管控制、JavaBean管数据JSP常被说“过时”但它的设计初衷并不过时甚至和现在的前后端分离是同一个思路视图、控制、模型各干各的。JSP里只放HTML片段和EL表达式接收请求参数、控制页面跳转的活交给Servlet数据库连接和业务计算的活放进JavaBean或DAO类。这样拆完以后答辩时被问到“某个功能在哪一层改”能立刻指出来。最常见也最危险的写法是直接在JSP页面里拼JDBC查询。这种代码第一次跑起来很快商品列表、详情、下单全在一个文件里解决但后面越改越乱最典型的就是商品表改名后到处搜SQL片段漏改一个页面整个功能挂掉。我一般要求项目里只有两个地方能出现SQLDAO类的方法里或者在MyBatis/JPA这类持久层框架的映射文件里。JSP里如果出现“% ”开头的Java代码块基本离翻车不远了。项目的分包结构也提前定好后面写功能会顺手很多。简单够用的分法是entity放数据库表对应的JavaBeandao放数据访问service放业务逻辑servlet放控制器util放工具类webapp/pages放JSP文件。这套结构和现在Spring Boot项目的Controller-Service-Mapper结构一一对应以后想往框架迁移也不用推倒重来。2.2 搭Maven骨架pom.xml依赖与webapp目录的参考写法建项目时我一般不再手动往WEB-INF/lib里拷jar包而是用Maven管理依赖pom.xml里只需要三个核心依赖javax.servlet-api、jstl、mysql-connector-java。需要注意javax.servlet-api的依赖范围要写成provided因为Tomcat本身就带Servlet容器实现打包时如果把这个jar包带进war会和容器里的类冲突启动时可能直接报NoSuchMethodError。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies编译版本锁在1.8是个保守选择对应Tomcat 8.5到9.0都非常稳定。如果用了Tomcat 10依赖的包名要从javax.*换成jakarta.*javax.servlet-api直接失效这是很多项目一部署就白屏的常见原因。打包时用mvn clean package生成war包文件名可以简短一点比如toy-store.war后面部署时少打几个字母也少输错一次。2.3 数据透出到页面EL表达式与JSTL的固定套路JSP页面里尽量别写% %更别写% %这种Scriptlet。EL表达式负责把request、session里的数据打印出来JSTL的c:forEach负责循环c:if负责判断这套组合足够覆盖商品列表、分类导航、订单状态展示这些高频场景。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classproduct-grid c:forEach items${productList} varp div classcard img src${p.imgUrl} width120 height120 alt${p.name} / p classname${p.name}/p p classpricefmt:formatNumber value${p.price} pattern0.00 //p a hrefproduct?actiondetailid${p.id}查看详情/a /div /c:forEach /divc:forEach的items接收Servlet放进request里的集合名var是循环变量名这个变量直接能用的原因是通过EL表达式找JavaBean的getter。fmt:formatNumber处理价格很实用pattern0.00保证9.9也显示成9.90不会因为小数位数被答辩老师挑毛病。如果页面上出现${p.name}原样输出说明EL没生效先查web.xml里的Servlet版本声明再查JSTL的jar包是否真的打包进了WEB-INF/lib这两个问题出现的频率非常高。3. 核心链路落地建表、分页、购物车与下单事务的完整实现3.1 数据库设计玩具商品、订单和用户最少拆几张表网上玩具销售系统最核心的业务对象是商品、用户、订单数据库至少要拆出用户表、分类表、商品表、订单表和订单明细表这五张。购物车我基本不建表理由放在购物车那一节讲。先看建表脚本字段设计里藏着不少答辩容易问到的点。CREATE DATABASE toy_store DEFAULT CHARACTER SET utf8mb4; CREATE TABLE toy_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0 ); CREATE TABLE toy_product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(120) NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0, img_url VARCHAR(255), description TEXT, status TINYINT DEFAULT 1, KEY idx_category_id (category_id) ); CREATE TABLE toy_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255) ); CREATE TABLE toy_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_amount DECIMAL(10, 2), receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), pay_status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE toy_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(120), price DECIMAL(10, 2), quantity INT, KEY idx_order_id (order_id) );价格用DECIMAL而不是FLOATFLOAT是近似值算总价时0.1加0.2可能变成0.30000000000000004答辩演示时出现这种结果非常尴尬。订单明细里冗余了product_name和price字段这是刻意设计的——商品改价或改名后历史订单不能被影响展示订单快照时直接从明细表读。status TINYINT DEFAULT 1用来表示商品上下架删除商品用逻辑删除比物理删除安全省得关联数据全被删掉。3.2 商品列表分页不引分页插件的Servlet写法与JSP页码条商品数量过百以后一次全查出来会让首页响应时间明显变慢。这个项目不建议为了分页去引入PageHelper之类的插件手写分页也就多写一个LIMIT和一个算页数的表达式而且答辩被问到分页原理时能讲得头头是道。// ProductDao.java public ListProduct findPage(int pageNo, int pageSize) { String sql SELECT id, category_id, name, price, stock, img_url, description FROM toy_product WHERE status 1 ORDER BY id DESC LIMIT ?, ?; int offset (pageNo - 1) * pageSize; // 使用PreparedStatement防止分页参数拼接导致的SQL注入 } public int count() { // SELECT COUNT(*) FROM toy_product WHERE status 1 // 只返回一个int不要查出整个列表再取size }LIMIT ?, ?的第一个参数是偏移量第二个是每页条数计算偏移量时pageNo要从前端传过来并做边界校验小于1就当成1。totalPage的计算表达式写成(int) Math.ceil(totalCount / (double) pageSize)这里如果不转double整数除法在总记录数不是页大小的整数倍时会少算一页。JSP页码条用c:forEach配合begin和end来渲染当前页数的选中态通过c:if test${pageNo i}来控制核心逻辑就是给链接加上pageNo参数。3.3 购物车放Session还是Cookie选型对比与加购代码购物车是这项目里一个典型的选型决策点。我这里用一张对比表说清楚答辩时直接照着讲就行。维度Session方案Cookie方案存储位置服务器内存浏览器本地容量限制受JVM内存影响远大于4KB单个Cookie约4KB生命周期随会话结束清空可设置过期时间持久保存跨设备同步依赖服务端会话浏览器间不通实现复杂度低直接操作Session对象需要序列化、编码、拼接字符串选Session的核心原因有两个。一是购物车是临时数据用户关掉浏览器清空是正常预期没必须落到数据库二是购物车里可能涉及促销规则、库存预占这些操作放服务端才能安全地计算和处理。Cookie能做的只是把商品ID拼成一个字符串存下来严格来说很多电商的“未登录购物车”确实用Cookie但毕业设计里Session的实现更直观也好演示。// AddCartServlet.java 核心代码 HttpSession session request.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new ConcurrentHashMap(); } int productId Integer.parseInt(request.getParameter(productId)); cart.put(productId, cart.getOrDefault(productId, 0) 1); session.setAttribute(cart, cart); response.sendRedirect(cart?actionlist);购物车Map的key是商品IDvalue是数量getOrDefault避免了首次加购时要先判断是否存在的代码分支。这里用ConcurrentHashMap而不是HashMap因为同一个会话里商品列表页和详情页可能同时在发加购请求并发修改普通HashMap在极端情况会丢数据或者死循环。存完立即sendRedirect而不是forward这是防止刷新页面重复加购的第一步后面避坑章节还会再提。3.4 下单事务库存扣减与订单生成的原子性保证下单是整条业务链里最需要谨慎的地方。扣减库存、写订单主表、写订单明细这三步必须保证要么全部成功要么全部回滚否则就会出现“订单生成了但库存没扣”或者“库存扣了但订单没建”这种后患。MySQL的InnoDB支持事务代码里的关键是Connection的提交时机。Connection conn dataSource.getConnection(); conn.setAutoCommit(false); try { // 1. 校验并扣减库存SQL中带 stock ? 条件 String deductSql UPDATE toy_product SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement ps conn.prepareStatement(deductSql); ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); int rows ps.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足); } // 2. 插入订单主表获得 orderId // 3. 批量插入订单明细把商品快照和数量写入 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }UPDATE toy_product SET stock stock - ? WHERE id ? AND stock ?这条SQL是防超卖最直接的手段。stock ?让数据库在库存不够时返回影响行数0不需要先查一次库存再判断减少了一次查询窗口里的并发风险。别指望用Java的synchronized锁扣库存那个锁的范围只在一个Tomcat实例内有效部署多节点时照样超卖而且把整个下单流程锁住等于把并发能力锁死了。事务里所有操作共用一个Connection否则各开各的连接事务就是各管各的。4. 图片上传、个人信息展示与模拟支付把玩具商城补成闭环4.1 商品图片上传Servlet 3.0的Part接口替代SmartUpload老教程里上传图片经常是引入SmartUpload或Commons FileUpload现在Servlet 3.0规范已经把multipart解析做成标准能力了Tomcat 8以上直接支持少引一个第三方库代码量也少一半。上传处理类上要加MultipartConfig这是很多新手漏掉的注解不加的话request.getPart()直接抛异常。WebServlet(/admin/upload) MultipartConfig(maxFileSize 5 * 1024 * 1024, maxRequestSize 10 * 1024 * 1024) public class UploadServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part part req.getPart(file); // 表单中 file 控件的 name String originalName part.getSubmittedFileName(); String ext originalName.substring(originalName.lastIndexOf(.)); String fileName UUID.randomUUID().toString() ext; // 重命名避免中文和路径问题 String saveDir req.getServletContext().getRealPath(/upload); File dir new File(saveDir); if (!dir.exists()) { dir.mkdirs(); } part.write(saveDir File.separator fileName); resp.getWriter().write(/upload/ fileName); } }maxFileSize限制单张图片大小超过会抛IllegalStateExceptionServlet里要捕获并提示“图片超过5MB”。getServletContext().getRealPath(/upload)在开发阶段会指向IDEA的target目录这个路径的坑放到避坑章节专门讲。文件名用UUID重命名是为了避免上传两个同名图片互相覆盖也避免文件名里带中文或空格导致URL拼接出错。保存完成后把相对路径/upload/xxx.jpg写进数据库页面上只用相对路径渲染将来把上传目录迁移到静态资源服务器时不用改代码。4.2 jsp个人信息展示页面EL取数、图片坐标定位与文件上传联动个人信息展示页面是这个项目里最容易做得“看起来很完整”的地方也是热搜词“jsp个人信息展示页面”“jsp图片如何对坐标定位”最贴合的落点。用户头像的上传原图比例经常是歪的直接用img展示会被拉伸变形。我一般用CSS做坐标定位和裁切固定宽高、object-fit: cover让图片居中裁剪再用border-radius切成圆形头像一套代码把尺寸、位置、造型全部解决。div classavatar-box img src${sessionScope.user.avatar} width96 height96 styleobject-fit: cover; border-radius: 50%; alt头像 / /div c:choose c:when test${not empty sessionScope.user.nickname} p昵称${sessionScope.user.nickname}/p /c:when c:otherwise p昵称未设置去a hrefpages/profile.jsp编辑资料/a/p /c:otherwise /c:choosesessionScope.user.avatar这里必须写sessionScope而不是.user的直接取原因是用户登录后基本信息存储在Session里Servlet往Session塞数据时用的key是userEL表达式写${sessionScope.user.avatar}语义最明确。c:choose相当于if-else处理“昵称未设置”的兜底展示比在Java代码里拼HTML字符串干净得多。换头像之后还有一个小坑浏览器可能缓存了旧头像页面刷新还是旧图。我给头像拼接时间戳参数的方案是src${sessionScope.user.avatar}?t${currentTimeMillis}这个技巧和热搜里“jsp页面让加载完后刷新一次”的诉求对应但要注意它只适合解决缓存失效绝不能拿去防表单重复提交。4.3 模拟支付回调没有真实支付接口时的订单状态演示毕业设计搭真实支付需要企业资质和审核没有这个概念过不了。常见做法是做一个模拟收银台页面用户点击“确认支付”后后端把对应订单的pay_status改成1再跳转到支付成功页。这套流程能演示完整的订单状态流转又不会被第三方支付平台的审核卡住。// SimulatePayServlet.java WebServlet(/pay/simulate) public class SimulatePayServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(user); int orderId Integer.parseInt(req.getParameter(orderId)); boolean owner orderDao.isOrderOwner(orderId, user.getId()); if (!owner) { resp.sendError(403, 无权操作该订单); return; } orderDao.updatePayStatus(orderId, 1); resp.sendRedirect(pages/pay_success.jsp?orderId orderId); } }isOrderOwner这一步不能省否则任意登录用户拿着订单号就能把别人的订单改成已支付这在答辩演示时一旦被发现是一个非常致命的业务漏洞。模拟支付成功后跳转用redirect而不是forward同样是防止刷新页面重复提交。订单列表里渲染状态用c:if test${order.payStatus 1}已支付/c:if一个标签判断就够。如果项目想再进一步可以接支付宝沙箱环境但考虑到毕设评审一般只看流程完整性模拟支付的投入产出比最高。5. 避坑指南JSP项目里最容易翻车的5个真实现场5.1 中文乱码Tomcat、JSP、MySQL三处编码链路排查现象商品名称、收货人名字在页面上全部变成“???”或者一堆乱码字符。原因这条链路里只要有一处编码不一致就乱——JSP页面本身的编码、请求参数进来时的编码、JDBC连接串里的编码、数据库表里的编码四处全部要用UTF-8才能保住中文。解决第一JSP头统一写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %第二写一个过滤器见下方代码第三JDBC连接串后面必须加useUnicodetruecharacterEncodingutf8第四建表时用utf8mb4比utf8多支持表情符号和特殊字符。// EncodingFilter.java public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { req.setCharacterEncoding(UTF-8); res.setCharacterEncoding(UTF-8); chain.doFilter(req, res); } }过滤器在web.xml里配置时要放在所有Servlet映射之前url-pattern写成/*让所有POST请求的body在进入业务代码前就被解码。Tomcat 8以上的URI部分默认UTF-8理论上GET请求的中文参数问题比老版本少很多但如果遇到GET乱码检查server.xml里的URIEncoding是否被改过。这套排查顺序能解决九成以上的乱码问题。5.2 图片404clean一次全消失上传目录到底该放哪现象本地开发时上传的图片能正常显示执行一次mvn clean或者重启IDE之后图片路径全部404。原因getRealPath(/upload)在IDEA的Tomcat配置下指向target/项目名/uploadclean会把这个目录整个删掉辛辛苦苦传的测试图全没了。解决思路分为开发和部署两套。开发阶段在图省事的话把保存路径改成项目源码目录下的webapp/upload但注意IDEA的部署配置里要把这个目录设置为“不随clean清空”更稳妥的做法是统一上传到服务器固定目录。# 部署环境示例上传目录放项目外 # /usr/local/toystore/upload # Tomcat的server.xml中添加虚拟映射 # Context docBase/usr/local/toystore/upload path/upload /配置虚拟映射后访问/upload/xxx.jpg请求直接打到项目外部的磁盘路径部署包更新、重启、clean都不影响图片文件。数据库里存的仍然是/upload/xxx.jpg相对路径所以业务代码完全不用感知文件物理位置的变化。答辩前一定要把上传目录从target里挪出来再截图不然评委第二天复审的时候打开系统发现商品图全挂了。5.3 JSTL标签不生效版本与WEB-INF/lib的隐藏问题现象页面上${p.name}被原样输出c:forEach整个标签字面量显示在页面上完全没有被解析。原因要么是web.xml里的Servlet版本声明太老要么是JSTL的jar包没被Maven打包进WEB-INF/lib要么是Tomcat 10配了javax版本的JSTL导致包名对不上。解决先把web.xml头部的版本声明确认是3.0或以上再检查.class目录下的WEB-INF/lib里有没有jstl-1.2.jar最后确认Tomcat版本和依赖的命名空间匹配。Tomcat 8.5/9配javax.servlet.jsp.jstlTomcat 10必须用jakarta.servlet.jsp.jstl这两类包名完全不兼容。排查时最容易忽略的是IDEA的Artifacts配置。有时候Maven依赖里明明有jstl但war包构建时没有把依赖塞进WEB-INF/lib打开IDEA的Project Structure找到Artifacts检查Output Layout里有没有jstl相关的jar包。这个坑藏得比较深因为它不在代码报错里只在运行时表现出标签不解析。5.4 表单重复提交刷新一下订单却多了一条现象下单页点提交按钮后浏览器地址栏还停留在/order/add此时按F5刷新浏览器会弹出“确认重新发送表单”点击确认后后台又生成了一条一模一样的订单库存也被多扣了一次。原因Servlet处理完订单后如果是forward到成功页地址栏URL没变刷新操作会重放上一次的POST请求。解决处理订单成功后用response.sendRedirect(pages/pay.jsp?orderId orderId)也就是业界说的Post/Redirect/Get模式。第一次提交是POST处理完立刻让浏览器跳转成GET请求之后的刷新友好得多也不会重放提交动作。// 前端二次保险提交按钮点击后立即禁用 document.getElementById(submitBtn).addEventListener(click, function () { this.disabled true; document.getElementById(orderForm).submit(); });按钮置灰是防手抖的辅助手段不能作为唯一的防线因为键盘回车提交或开发者工具重放请求能绕过。真正兜底在后端要么用PRG模式要么在Session里存一个一次性令牌token每次打开下单页生成一个随机值放在Session和表单里提交时校验并清除校验不过就拒绝。另外注意网上常说的“jsp页面让加载完后刷新一次”这种技巧绝对不能用在订单提交场景它会把POST再刷新一遍正好踩中重复提交的雷区。5.5 本地能跑服务器挂JDK、Tomcat与部署包的版本组合现象Windows的IntelliJ里一切正常把war包发到Linux服务器Tomcat启动不报错一访问页面就500。看到catalina.out里的java.lang.UnsupportedClassVersionError时才知道编译和运行的JDK版本不一致。原因本机用JDK 17编译的class文件服务器上的Tomcat跑在JDK 8上字节码版本太高就拒绝加载。解决pom.xml里把maven.compiler.source和target都锁成1.8同时检查全局的JAVA_HOME环境变量。部署前的版本组合我一般这样搭JDK 8 Tomcat 8.5/9.0 MySQL 5.7/8.0这套组合的兼容性最好相关报错资料也最容易搜到。服务器上排查时先看logs/catalina.out不要打开浏览器看页面就猜。大多数明显错误在这份日志里都有直接堆栈比如ClassNotFoundException、UnsupportedClassVersionError、NoClassDefFoundError按堆栈去修比盲试快得多。部署war包时确认Tomcat的webapps目录权限是当前启动用户可写否则自动解压war包会失败表现为访问时目录为空或404。6. 验证与进阶从玩具商城到前后端分离的演进路径6.1 用JMeter和curl给JSP商城做一次体检答辩前一定要做一次基础压测不需要多专业但至少要知道系统在50个并发下会不会挂。JMeter里建一个线程组50个线程循环10次分别打登录、商品列表、加购、模拟支付四个接口重点看两个指标错误率和90%响应时间。JSP项目最常见的问题不在JSP本身而在DAO层写出N1查询——列表页先查商品再在循环里对每个商品查一次分类名50并发直接能把数据库连接池打满。# 粗略测接口响应时间 time curl -s -o /dev/null http://localhost:8080/toy-store/pages/home.jspcurl的time命令输出里total如果稳定超过300ms就有优化空间。优先看EXPLAIN SELECT是否走了索引商品列表的category_id和订单明细的order_id都建了索引的话大部分查询应该都在10ms级别。JSP编译和渲染本身很轻瓶颈基本都在数据库访问次数上。6.2 把JSP渲染层换成前端框架一套架构的两种活法如果答辩后想继续发展这个项目或者想在简历上写成“前后端分离项目”改动路径其实很清晰保留dao、service、entity这些业务层不动把Servlet改成返回JSON的接口JSP换成Vue或普通HTMLfetch。购物车Session方案不用改因为前后端分离后依然可以通过Session ID关联用户订单事务代码不用改数据库表也不用改要动的只是Controller层和渲染层。我自己带这类项目时习惯一个教训先让JSP版本完整跑通并截图再逐步替换渲染层每替换一个页面就回归一次下单链路。这样出了问题能立刻判断是新页面的接口对接问题还是老代码的业务问题不用把整个项目当黑匣子猜。JSP这套技术就算以后生产环境用得少了它在职责划分、会话管理、事务边界上的基本功换个框架一样用得上。希望这个项目能帮你把Java Web的地基打扎实答辩顺利。本文还有配套的精品资源点击获取