ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于JSP的网上玩具销售系统:毕业设计从零搭建到避坑指南

2026/10/8 4:31:39 拓冰建站 浏览量
基于JSP的网上玩具销售系统:毕业设计从零搭建到避坑指南 简介基于JSP的网上玩具销售系统是一份面向Java Web开发学习者的完整项目资源适合毕业设计参考或电商系统入门实践。系统围绕玩具商城场景实现商品浏览、分类导航、购物车与用户中心等核心模块前端包含HTML、CSS、JavaScript与JSP动态页面后端配合Servlet、JavaBeans及SQL数据库完成业务逻辑与数据管理可帮助读者理解典型B2C站点的分层开发流程。压缩包共1059个文件约23.2MB以JSP、HTML、CSS、JavaScript及图片资源为主另有Java源码、class字节码、数据库脚本及项目配置文件基本覆盖从页面展示到服务器交互的完整代码链路。包内还包含Eclipse工程相关文件与若干文档便于直接导入IDE查看运行。资源目前已有390人学习下载适合正在做Java Web课程设计或需要快速搭建玩具销售系统原型的开发者参考从中获取目录结构规划、数据库设计思路与前后端整合方式。1. 基于jsp的网上玩具销售系统毕业设计为什么还选这套技术栈临近开题做Java方向的本科生绕不开一个问题选Spring Boot还是选JSP。我的建议很直接——如果目标是顺利做完、能演示、能答辩基于JSP的网上玩具销售系统是性价比很高的选择。它把登录、商品管理、购物车、订单、库存扣减这一条业务闭环完整走一遍Servlet控制流程、JSP渲染页面、MySQL存数据正好覆盖毕业设计要展示的核心能力。这套方案按可复现的最小路径展开环境版本怎么配、建表SQL长什么样、Dao和Servlet怎么分层、下单事务怎么保证以及验收前后容易翻车的地方。适合还没定技术栈或者想快速跑通一个完整系统的读者。2. 搭建jsp项目骨架从空目录到Tomcat能跑通的最小工程2.1 技术选型与版本组合JSPServletMySQL不用全家桶为什么不用Spring BootSpring Boot本身不复杂但它带出一整套Maven依赖、自动配置和starter约定对没有接触过的学生来说报错时根本分不清是自己的代码问题还是依赖冲突。答辩现场演示的核心是业务逻辑不是框架深度JSP这套越简单越好。版本组合是陷入兼容性泥潭的第一道坎我的固定组合是组件版本理由JDK1.8Tomcat 9的默认支持版本编译行为稳定Tomcat9.0兼容javax.*命名空间避开Tomcat 10的jakarta迁移坑MySQL5.7 或 8.05.7兼容性最好8.0记得用mysql-connector-java 8.xServlet API3.1Tomcat自带对应JSP 2.3功能足够JSTL1.2页面端遍历和格式化必用别用老1.1版数据库连接池C3P0 0.9.5配置文件驱动适合教学场景这个组合的兼容性验证成本最低。用IDEA社区版新建普通Java Web项目把Tomcat配进去不引入Maven——把jar直接扔到WEB-INF/lib下拷贝即用的方式对毕设最省心。注意不要用Spring Initializr创建那会引入一堆用不到的依赖。2.2 目录结构一个可运行的jsp工程要放哪些文件很多同学项目能跑但结构混乱答辩老师翻目录找不到代码。一个标准Java Web工程的骨架长这样toy-shop/ ├── src/ │ └── com/example/toy/ │ ├── bean/ # 实体类Product, User, Order, OrderItem │ ├── dao/ # 数据访问层BaseDao 各业务Dao │ ├── servlet/ # 控制器LoginServlet, CartServlet, OrderServlet │ ├── filter/ # LoginFilter, EncodingFilter │ └── util/ # 连接池工具、分页工具 ├── web/ # 部署根目录 │ ├── static/ │ │ ├── css/ # 全站样式 │ │ ├── images/ # 商品图片和用户头像 │ ├── admin/ # 后台管理页面 │ ├── user/ # 前台页面index.jsp, cart.jsp, order.jsp │ └── WEB-INF/ │ ├── lib/ # 所有第三方jar │ └── web.xml # 部署描述文件src下只放Java代码web放页面和静态资源。WEB-INF/lib放jar的好处是部署时不用额外设置classpath拷到Tomcat的webapps下就能跑。这里最容易翻车的是目录名IDEA新建项目默认叫webEclipse的Dynamic Web Project默认叫WebContent两个IDE模板不同照着网上的教程配Artifacts时会完全对不上。我一般建议把部署目录统一改成web。web.xml里除了Servlet映射还要把首页指到商品列表页welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list不配欢迎页的话访问站点根路径会直接列目录或者404答辩演示第一步就扣分。始终记得根路径要直通首页。2.3 数据库连接池配置别在每个JSP页面里new Connection最直观但最糟糕的写法是每次请求都DriverManager.getConnection。玩具系统演示时看不出问题但答辩老师问一句“数据库连接怎么管理的”就答不上来。用连接池既能回答问题也能让系统在并发请求时不再频繁创建销毁连接。C3P0配置简单不需要监控界面适合这个场景。在src下放一个c3p0-config.xmlc3p0-config default-config property namedriverClasscom.mysql.jdbc.Driver/property property namejdbcUrljdbc:mysql://localhost:3306/toy_shop?useUnicodetrueamp;characterEncodingutf8/property property nameuserroot/property property namepassword123456/property property nameinitialPoolSize5/property property namemaxPoolSize20/property property nameminPoolSize5/property property nameacquireIncrement5/property property namecheckoutTimeout3000/property property nameidleConnectionTestPeriod60/property /default-config /c3p0-config连接池的三个参数最值得关心initialPoolSize是启动时预创建的连接数太小的话第一次请求会慢maxPoolSize是上限超过这个数的请求会排队而不是新建连接checkoutTimeout是排队等待的最长时间单位毫秒设成3000意味着等3秒还拿不到连接就抛异常。这样当数据库挂了或连接泄漏时前端错误提示会明确很多。再写一个工具类获取连接public class DbUtil { private static ComboPooledDataSource ds new ComboPooledDataSource(); public static Connection getConnection() throws SQLException { return ds.getConnection(); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) {} } if (ps ! null) { try { ps.close(); } catch (SQLException e) {} } if (conn ! null) { try { conn.close(); } catch (SQLException e) {} } } }DbUtil里close连接时实际是归还给连接池不是真正断开所以每次请求结束时必须把三个资源都关干净否则连接会被占满表现为系统跑一阵后所有页面都卡住。这个工具类在第3章的BaseDao里直接复用。3. 数据库设计与Dao层把玩具系统的表结构和数据访问一次定死3.1 核心表设计商品、分类、用户、订单、订单明细玩具销售系统是标准的“商品-购物车-订单”模型五张表就够。特别要注意玩具不是普通商品它的核心信息是适用年龄和材质这两个字段必须单独建列否则后面的筛选和详情页都得从描述里截取字符串既丑又慢。CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ); CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2), stock INT NOT NULL DEFAULT 0, age_range VARCHAR(20) COMMENT 适用年龄如3-6岁, material VARCHAR(50) COMMENT 材质如ABS塑料, image_url VARCHAR(255), description TEXT, sales_count INT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), avatar VARCHAR(255), create_time DATETIME ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成, create_time DATETIME ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL );订单表里存product_name和price快照这是很多新手想不到的坑下单后商品改名或改价历史订单不能跟着变快照字段就是为这个准备的。orders表用order_no做人工可读的单号推荐“yyyyMMddHHmmss用户ID随机数”格式比自增ID更适合在电商页面展示。status字段务必带上订单查询和后台列表都靠它过滤写“我的订单”页面时只需要一个WHERE条件。3.2 Dao层封装写一个BaseDao项目里的查询都走它JSP项目的Dao层很容易写成每个方法重复几十行样板代码。既然答辩会问“你用了什么设计模式”那就用一个BaseDao把获取连接、释放资源、参数绑定统一收敛起来具体业务Dao只关注SQL本身。public class BaseDao { protected Connection conn; protected PreparedStatement ps; protected ResultSet rs; // 执行insert/update/deleteparams为SQL中的?占位参数按顺序传入 protected int update(String sql, Object... params) throws SQLException { conn DbUtil.getConnection(); ps conn.prepareStatement(sql); if (params ! null) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } } return ps.executeUpdate(); } // 执行查询返回ResultSet由调用方封装成Bean protected ResultSet query(String sql, Object... params) throws SQLException { conn DbUtil.getConnection(); ps conn.prepareStatement(sql); if (params ! null) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } } rs ps.executeQuery(); return rs; } }写Dao时坚持用prepareStatement而不是Statement拼接字符串这是避免SQL注入的底线。以登录查询为例public class UserDao extends BaseDao { public User findByUsernameAndPassword(String username, String password) { String sql SELECT * FROM user WHERE username? AND password?; try { ResultSet rs query(sql, username, password); if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setNickname(rs.getString(nickname)); return u; } } catch (SQLException e) { e.printStackTrace(); } finally { DbUtil.close(conn, ps, rs); } return null; } }注意query()里已经拿到了ps和rsfinally里把三个资源一起关掉这是唯一合法的位置。不要在catch里只写System.out.println了事最少也要e.printStackTrace()否则Service层看到的SQLException来源完全不可追溯。密码存数据库前建议做一次MD5或SHA-256哈希答辩时能多讲一个安全设计。3.3 分页与列表查询JSP页面做分页的两种常用方式商品列表是系统门面玩具系统SKU数量不多但分页几乎是答辩必问的功能点。最常见做法是SQL的LIMIT配合COUNT查询这是最容易向老师讲清楚的第一种方式public class PageBean { private int currentPage 1; // 当前页默认第一页 private int pageSize 8; // 每页条数玩具商品推荐8或12 private int totalCount; // 总记录数 private int totalPage; // 总页数由totalCount和pageSize计算 private ListProduct list; // 当前页数据 // getter/setter略计算totalPage时用 (totalCount pageSize - 1) / pageSize }ProductDao里的分页方法public ListProduct findPage(int currentPage, int pageSize) { String sql SELECT * FROM product WHERE status1 ORDER BY create_time DESC LIMIT ?, ?; int offset (currentPage - 1) * pageSize; // 页码从1开始数据库偏移从0开始 ResultSet rs query(sql, offset, pageSize); // 循环rs.next()封装成Product对象放入List返回 }LIMIT的两个参数分别是偏移量和条数。currentPage1时偏移0currentPage2时偏移8。还要配一个计数查询public int countAll() { String sql SELECT COUNT(*) FROM product WHERE status1; // 执行后返回 rs.getInt(1) }两个查询一配合PageBean就完整了。第二种做法是前端点击页码时把currentPage传给商品列表ServletServlet把PageBean放进request再转发到index.jspJSP页面上用JSTL的c:forEach遍历list用c:if渲染“上一页/下一页”。注意Servlet转发必须用请求转发request.getRequestDispatcher().forward()不能用重定向否则request里的PageBean会丢。页码越界也要处理当前页小于1强制归1大于totalPage就归到totalPage否则空列表页面很难看。4. 核心业务链路登录拦截、购物车Session与订单事务4.1 登录状态管理用Filter统一拦截而不是在JSP页面里写if网上销售系统必须有“登录后才能下单”的闭环。很多新手在每个JSP页面顶部用session.getAttribute(user)判空二十几个页面复制同一堆判断代码。用Filter统一拦截是更工程化的做法一个类解决全部页面WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、登录接口和静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/static/) || uri.endsWith(index.jsp)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }用WebFilter注解省去web.xml里的配置。拦的是/*但必须放行三类东西登录相关的页面和Servlet、static目录下的CSS和图片、还有首页——没登录也允许游客浏览商品这是电商系统的正常逻辑。最容易翻车的是图片全部挂在static下Filter没放行静态资源登录前页面样式全丢看起来像系统坏了。4.2 购物车选Session还是数据库表给毕业设计的建议购物车有两种主流实现存Session和存数据库表。对毕业设计而言Session方案前置条件少、演示直观是一个合理的选择。存数据库表更接近真实电商但需要额外维护购物车表和登录状态的联动代码量明显增加。对比项Session购物车数据库购物车实现成本低一个List就能存高需要额外表结构和CRUD清除时机关闭浏览器即消失持久保留登录就能恢复并发压力缓存在应用内存每次都要查数据库答辩观感说清楚内存态即可更“像真实系统”Session方案的核心是一个CartItem对象public class CartItem { private Product product; private int quantity; public double getSubTotal() { return product.getPrice() * quantity; } // getter/setter略 }加入购物车的Servlet逻辑是一个固定模式HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayList(); session.setAttribute(cart, cart); } int productId Integer.parseInt(request.getParameter(productId)); for (CartItem item : cart) { if (item.getProduct().getId() productId) { item.setQuantity(item.getQuantity() 1); // 已存在则数量加1 response.sendRedirect(cart.jsp); return; } } // 不存在则新加一项Product从数据库查出后set进CartItem CartItem newItem new CartItem(); newItem.setProduct(productDao.findById(productId)); newItem.setQuantity(1); cart.add(newItem);这里必须处理的边界是同一商品重复加入时不能生成两个CartItem否则购物车列表出现两行一模一样的东西。遍历查找、命中就quantity加1这是卡住不少人的小细节。Session的默认超时时间是30分钟购物车数据会随会话过期消失这一点答辩时被问到要能讲清楚。4.3 提交订单与扣库存事务必须放在Service层而不是JSP里提交订单是系统里唯一需要跨多张表写数据的地方写入orders、写入order_item、扣减product.stock、清空购物车。这四步任何一步失败都不能留下半截数据。事务的ACID特性在教科书上人人会背但落到代码里关键是让这四步共用同一个Connection。public boolean submitOrder(User user, ListCartItem cart) { Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交手动控制事务 // 1. 生成订单号并插入orders表拿到自增订单ID String orderNo TOY System.currentTimeMillis() user.getId(); String sqlOrder INSERT INTO orders(order_no, user_id, total_amount, status, create_time) VALUES(?,?,?,0,NOW()); // 2. 遍历购物车逐条插入order_item // 3. 扣库存UPDATE product SET stockstock-? WHERE id? AND stock? // 4. 提交事务 conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { DbUtil.close(conn, null, null); } }扣库存的UPDATE是最关键的一行SQL里带上AND stock?的意思是“库存不够就不更新”。并发情况下两个用户同时买同一件商品时这个条件能避免超卖——虽然毕设演示并发量不大但答辩老师就爱问这个点。事务一旦开启commit和rollback必须成对出现漏掉rollback的后果是异常时已插入的订单和明细全部残留下次测试时订单表里会莫名多出几条垃圾数据。清空购物车这一步可以放在事务里也可以放在事务外我习惯放在事务提交后执行因为session里的数据不影响数据库一致性。5. 避坑jsp玩具系统从开发到答辩的6个常见问题这一章直接按血泪经验写每一条都是真实项目里见过或踩过的按“现象→原因→解决”讲清楚答辩前最好逐条对一遍。5.1 中文乱码与编码不一致现象JSP页面显示正常但存到数据库里的中文变成问号或者从数据库读出来乱码。原因JSP的pageEncoding、浏览器提交时的编码、MySQL连接的characterEncoding三处只要有一处不一致就会出问题。最常见的是请求参数没设置UTF-8Servlet里request.getParameter拿到的是ISO-8859-1解码的乱码写进数据库就废了。解决在web.xml里注册一个编码过滤器所有请求统一转UTF-8filter filter-nameencodingFilter/filter-name filter-classcom.example.toy.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时保证三处一致JSP第一行pageEncodingUTF-8、MySQL连接串带characterEncodingutf8、数据库表默认字符集utf8mb4。初始化参数encoding在EncodingFilter里用getInitParameter读取后再setCharacterEncoding。按这个顺序排查中文问题基本十分钟内解决。5.2 Tomcat 10启动后项目直接报ClassNotFoundException现象按教程用Tomcat 10部署一启动就是Servlet相关的类找不到或ClassNotFound。原因Tomcat 10把javax.servlet迁移成了jakarta.servlet教程里的代码和jar都是按javax写的命名空间对不上。这个坑对新手几乎是毁灭性的因为报错信息又长又像自己的代码写错了。解决换Tomcat 9.0.x不要用Tomcat 10或11。如果机器上已经装了Tomcat 10在IDEA的Run Configuration里把Application Server换掉即可。毕业设计选型永远把“稳定”放在“新”前面能跑通就是最大的胜利。5.3 商品图片上传后页面不显示现象图片文件上传到了服务器目录但浏览器里img标签显示404刷新也没用。原因图传上去了但路径不对。很多同学把图片存到了项目部署目录的深层路径页面引用的是相对路径浏览器解析URL时结合了当前页面路径拼接出的地址和实际文件位置对不上。解决图片这种被页面反复引用的资源统一存到固定静态目录比如web/static/images/product/页面里一律用绝对路径img src${pageContext.request.contextPath}/static/images/product/${product.imageUrl} width180 height180${pageContext.request.contextPath}会自动带上应用上下文路径不管页面在哪个层级图片都能找到。这个写法同样适用于CSS和JS的引用。上传功能如果用的是Servlet 3.0的Part接口注意获取文件名时要手动解析header不同浏览器格式有差异。5.4 JSP页面加载完后想自动刷新一次现象后台改完数据回到商品列表页却还是旧数据得手动刷新才更新。原因浏览器缓存了旧页面或者Servlet转发时数据没重新查。解决在JSP头部用meta标签控制加载后刷新meta http-equivrefresh content1content的值是秒数1表示页面加载完成后1秒自动刷新一次。更推荐在Servlet响应头里写response.setHeader(Refresh, 1);两种方式二选一。但要注意如果每个页面都强制刷新会给数据库造成不必要的压力。正确做法是只在数据更新后的目标页加这个头比如下单成功后的订单详情页。这样既解决“看到旧数据”的问题又不影响其他页面的性能。5.5 Filter拦截导致静态资源和登录页循环跳转现象配置LoginFilter后CSS全丢、登录页进不去甚至出现“重定向次数过多”的报错。原因Filter的放行条件太严格。比如只放行了login.jsp但请求的是loginServlet的路径或者CSS在static目录下没放行Filter把静态资源也当成需要登录的页面重定向了形成死循环。解决放行条件写成“包含路径片段”而不是“精确等于”if (uri.contains(login) || uri.contains(/static/)) { chain.doFilter(req, resp); return; }contains(login)会把login.jsp和loginServlet一起放行。调试Filter时先在放行条件里打印URISystem.out.println(Filter拦截: uri);看完日志就能立刻定位是哪个路径没放行。这条经验在配置任何Filter时都通用。5.6 500错误只显示“服务器内部错误”看不到具体堆栈现象页面变成500但浏览器只显示白底红字的错误页完全不知道代码哪里出错。原因JSP页面编译异常时Tomcat默认只展示简化的错误页或者异常被Servlet吞掉了。解决开发阶段不配置自定义错误页让Tomcat展示原始堆栈。验收前再打开自定义页面把不友好的错误信息挡掉error-page error-code500/error-code location/error.jsp/location /error-pageerror.jsp里写“系统繁忙请稍后重试”同时记录日志。开发时建议把Console日志和catalina.out日志留着Service层异常不要只printStackTrace用日志框架输出到文件出问题时能按时间线串起来。这一步不起眼却是整个学期省时间最多的习惯。6. 答辩前的验证清单与两个加分项6.1 用户中心与个人信息展示页面最值得加的模块答辩时演示“登录后能看到自己的信息”远比在代码里讲一百句设计模式有感染力。在用户中心放一个信息展示页面把Session里的用户信息渲染出来顺手统计订单数量div classuser-card img src${pageContext.request.contextPath}/static/images/avatar/${user.avatar} classavatar h3${user.nickname}/h3 p用户名${user.username}/p p手机号${user.phone}/p /div这个页面的意义不只是展示它连带验证了登录Filter是否放行对了路径、Session里的用户对象是否完整、图片相对路径是否正确。把个人信息展示页面做成独立JSP复用价值比想象的高后面的“我的订单”页面直接复制这个布局就行。6.2 商品图片定位与展示用CSS控制而不是JS硬调很多同学为了图片对齐在JSP里写一堆JavaScript答辩时解释不清楚还容易出兼容问题。推荐用CSS的object-fit控制图片在固定尺寸容器里的显示区域.product-thumb { width: 180px; height: 180px; overflow: hidden; } .product-thumb img { width: 100%; height: 100%; object-fit: cover; /* 按比例裁剪填满整个容器 */ }object-fit:cover让图片不变形地填满容器超出部分裁掉比手动算坐标稳定得多。如果确实要对某张图做精确定位用position:relative的父容器配合position:absolute的图片相对坐标用百分比而不是像素响应式下不容易跑位。这一招在商品详情页和推荐位都用得上。6.3 验收前把这条清单过一遍检查项验证场景注意点注册登录新用户名注册→登录→退出密码脱敏别在页面回显商品浏览游客直接访问首页→能看商品Filter别误拦index.jsp加入购物车同一商品重复加→数量累加购物车列表不能出现两条相同商品提交订单下单→订单生成→商品库存减1连开两个窗口抢最后一件库存订单列表登录后查看我的订单status状态流转能演示管理端后台添加商品→前台立刻可见新增后图片路径正确、状态为1异常路径未登录直接访问订单页能跳转登录页而非报错我最早做这个题目的时候最怕的是答辩前一天发现数据库字段少了一个改表结构牵一发动全身Service层、JSP页面全要跟着动。后来养成的习惯是先把表结构定死一行代码都不写就开始建库建完表对着字段清单过一遍需求确认能覆盖所有页面再回头写代码。这套习惯让我后期几乎没动过数据库设计所有的修改都集中在业务逻辑和页面样式上。希望帮到你。本文还有配套的精品资源点击获取