
简介面向需要完成课程设计或毕业设计的计算机专业学生这份网上零食销售系统项目给出了基于JavaJavaBeanJSP的完整实现方案并以MYSQL作为后台数据库。系统围绕零食电商场景展开涵盖商品展示、用户注册登录、购物车管理、订单提交与后台信息管理等典型模块源码说明经过测试校正后可以成功运行配套文档也有助于梳理系统架构、业务流程和数据库表设计。资源包体约633MB主要包含项目源码、JSP页面、JavaBean组件、数据库SQL脚本以及完整说明文档不仅适合直接参考部署也为二次开发或答辩讲解提供了便利。已有656人学习下载内容组织清晰适合作为相关项目设计与论文撰写的参照可在此基础上继续扩展会员分级、促销优惠、销售统计等进阶功能。1. 网上零食销售系统JavaBeanjsp的课设源码包先跑通再谈原理很多同学拿到这套基于javaJavaBeanjsp的网上零食销售系统源码包第一反应是双击 index.jsp 看效果结果浏览器甩出一屏 500 报错。这不是代码不行而是 jsp 项目的运行链路比想象的更长Tomcat 要配、MYSQL 要建、JDBC 连接串要对任何一环没对齐页面就白屏。这套资源的价值在于业务代码和数据库脚本是完整的前台购物、后台管理都有对应实现只是没人告诉你先装什么、先跑哪一步、报错看哪里。这篇文章就把这些补上从环境核对到表结构、核心代码、踩坑记录一路拆到底适合正在做课程设计或准备毕业设计的 JavaWeb 方向学生。2. 先看清这套 jspJavaBean 的底子三层结构与传统 MVC 的现实分工2.1 Model 1 还是 Model 2JavaBean 在 jsp 项目里的真实位置拿到源码先别急着导 IDE翻一下目录看 jsp 页面里有没有jsp:useBean、jsp:setProperty标签有没有% ... %脚本片段直接写 Java 代码。这套系统是典型的 JavaBeanJSP 组合介于 Model 1 和 Model 2 之间页面负责展示JavaBean 负责封装业务逻辑和数据库访问Servlet 用得很少或不用。这种设计的好处是上手门槛低课程设计阶段能把对象封装、数据库增删改查、页面参数传递讲清楚。缺点也明显业务逻辑和页面展示耦合在 jsp 里项目规模一大就乱。所以你看源码的时候要把注意力放在 JavaBean 类的职责划分上。通读代码你会发现这套资源里的 JavaBean 大体分三类Bean 类型代表类职责实体BeanUser、Goods、Order封装字段对应数据库表字段提供 getter/setter业务Bean/DAOUserDao、GoodsDao、OrderDao写 JDBC 操作查询、插入、更新返回实体或列表工具BeanDBUtil、PageBean管理连接、分页参数、公共方法判断一个 Bean 写得好不好就看它是不是单一职责。比如 GoodsDao 只应该处理商品表的读写订单相关逻辑不该出现在里面。如果代码里把下单和扣库存写在同一个方法里那就要留意事务边界后面第四章会专门讲这个坑。2.2 MYSQL 端表结构设计零食库存、订单用户三张核心表数据库脚本一般在源码包的sql或database目录下文件命名通常是snack.sql或db_snack.sql。用文本编辑器打开看一眼这套系统的表至少包含三张核心表设计思路是标准的电商雏形。用户表负责登录注册字段一般有 uid主键自增、username、password有的版本还会带上 phone 和 address地址字段直接影响后面订单里的收货信息没有的话下单就得另外填。商品表 goods 是关键gid 主键、goodsName、price、stock、pic 图片路径这套系统里图片通常用相对路径存比如upload/xxx.jpg部署时这个目录没建页面图片就会全部裂开。订单表 orders 和订单明细表分开设计主表存下单时间、总金额、用户 id明细表存商品 id、数量、单价这个设计比把所有字段塞一张表要规范得多。导入脚本之前有个关键动作确认 MYSQL 版本。如果你是 MYSQL 8.0 以上直接用 source 命令导就行如果是 MYSQL 5.7 或更低检查一下脚本里有没有utf8mb4字符集声明没有的话手动指定字符集再导不然中文零食名称入库直接变乱码。导入方式有两种命令行和图形化工具二选一。命令行方式进入 mysql 客户端后执行mysql -u root -p Enter password: ****** source /path/to/snack.sql;图形化工具更直观用 Navicat 新建数据库右键选择运行 SQL 文件。需要注意连接数据库时字符集选utf8mb4不然脚本里有中文注释会出现 ERROR 1300 之类报错。导入完成后用use snack_db; show tables;验证一下至少能看到 user、goods、orders、order_detail 这几张表。2.3 目录结构与部署前环境核对解压源码包后标准的 JavaWeb 项目目录结构是src源码目录加WebRoot或web目录。在导入 IDE 之前花两分钟核对一下环境版本这一步能帮你避开后面 80% 的翻车现场。组件推荐版本备注JDK1.8 或 11大多数课设代码基于 JDK8 语法Tomcat8.5 或 9.0支持 Servlet 3.1/4.0jsp 2.3MYSQL5.7 或 8.0驱动版本不同连接写法不同IDEEclipse / IDEA导入 Web 项目或 Maven 项目注意如果代码里没有 pom.xml 也没 .classpath说明它只是一个传统的 Dynamic Web Project用 Eclipse 导入时选 Existing Projects into Workspace用 IDEA 则要配置 Web 模块和 Artifact。这一步错了后面所有编译报错都看不懂因为项目压根没被识别成 Web 工程。版本核对这块还有个常见误区看到javax.servlet报红色下划线就以为缺依赖。这不是下载一个 jar 塞进 lib 那么简单jsp 和 servlet API 由 Tomcat 提供在编译期要引入servlet-api.jar和jsp-api.jar在运行期由容器加载不能把这两个 jar 打包进 WEB-INF/lib否则 Tomcat 启动会出现 jar 冲突的诡异报错。这一点很多新手容易搞混记好了项目里如果自带这两个 jar通常建议排除掉。3. 从数据库脚本到第一行可运行代码搭建与初始化3.1 SQL 脚本导入连接参数同步改数据库导入完成后打开 Java 源码里的DBUtil.java或db.properties检查连接串。这套课设项目绝大多数会封装一个 DBUtil 类静态代码块加载驱动read 配置文件或者直接硬编码连接参数。package com.snack.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { // 8.x驱动com.mysql.cj.jdbc.Driver // 5.x驱动com.mysql.jdbc.Driver private static final String DRIVER com.mysql.cj.jdbc.Driver; // serverTimezone 必须和本地时区一致否则日期字段报错 private static final String URL jdbc:mysql://localhost:3306/snack_db?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { 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) {} } } }参数说明useUnicodetrue和characterEncodingutf-8这两个参数保证中文数据从数据库到页面不出现乱码必须成对出现。useSSLfalse是本地开发阶段关闭 SSL 握手避免 MYSQL 8.x 在连接时因为证书校验浪费时间。serverTimezoneAsia/Shanghai是 MYSQL 8.x 的强制要求5.x 驱动可以不加。修改密码时注意PASSWORD字段改成你自己 MYSQL 的 root 密码不要动 URL 里的数据库名。如果数据库名不是 snack_db需要和脚本里的 CREATE DATABASE 对齐。改完之后把这段代码放到一个测试类里跑一下能拿到 Connection 对象再往下走这一步过不了后面全是白忙。3.2 JDBC 工具类连接、关闭、查询封装成 JavaBeanDBUtil 已经有了连接和关闭但业务代码里不能每次写一堆 try-catch-finally要在这基础上封装一个 BaseDao 或者直接在 GoodsDao 里写通用的查询方法。常见的做法是建立一个 BaseDao 类提供通用的增删改查方法子类继承后复用。package com.snack.dao; import com.snack.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BaseDao { // 通用查询返回ListString[] // SQL里如果有参数通过args传入按顺序匹配? protected ListString[] query(String sql, Object... args) { ListString[] list new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); if (args ! null) { for (int i 0; i args.length; i) { ps.setObject(i 1, args[i]); } } rs ps.executeQuery(); ResultSetMetaData meta rs.getMetaData(); int cols meta.getColumnCount(); while (rs.next()) { String[] row new String[cols]; for (int i 1; i cols; i) { row[i - 1] rs.getString(i); } list.add(row); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; } }这块代码里最容易翻车的是ps.setObject(i 1, args[i])如果你用setString去设置int类型的参数某些版本驱动会隐式转换报错。通用方法用setObject反而省事让驱动自己判断类型。其次ResultSetMetaData是拿查询列的元数据动态获取列数避免硬编码列数。这套写法在课设里足够用不用上框架。3.3 JSP 页面接收请求useBean 与 setProperty 的参数绑定逻辑数据库通了接下来看 jsp 页面怎么和 JavaBean 交互。这套源码里商品列表页大概率长这样页面前半部分是 jsp:useBean 声明后半部分通过 getProperty 或直接调用方法输出。% page contentTypetext/html;charsetUTF-8 languagejava % jsp:useBean idgoodsDao classcom.snack.dao.GoodsDao scopepage/ % java.util.ListString[] list goodsDao.findGoodsList(); % table border1 tr td零食名称/td td单价/td td库存/td /tr % for (String[] row : list) { % tr td% row[1] %/td td% row[2] %/td td% row[3] %/td /tr % } % /tablejsp:useBean实例化 GoodsDao 对象id 属性是引用名class 是全限定类名scope 决定存活范围。page 表示仅当前页面有效session 表示整个会话期间同一个对象购物车场景就用 session 作用域。页面里% row[1] %是表达式输出等价于 out.print()这里 row 下标从 0 开始字段顺序取决于 SQL 里 SELECT 的列顺序改 SQL 时一定要同步更新页面下标。如果页面用了jsp:setProperty namegoodsDao property*意思是把请求参数自动绑定到 Bean 的同名属性上。比如注册页面表单里有 username、password 两个输入框Bean 里也有 username、password 字段这个标签会自动做赋值。它的前提是 Bean 必须有无参构造方法并且字段要有 setter 方法缺一个都会在运行期抛java.lang.InstantiationException。4. 核心功能实现前台购物、后台管理两条主线4.1 零食列表与分页查询 JavaBean 的分页参数到底怎么算商品列表页是这套系统的主页面零食种类一多就必须分页。源码里常见的做法是 PageBean 封装分页参数DAO 层的查询方法接收 currentPage 和 pageSize拼上 LIMIT 关键字查数据库。package com.snack.bean; public class PageBean { private int currentPage; // 当前页从页面传入 private int pageSize; // 每页条数通常固定10可配置 private int totalCount; // 总记录数通过SELECT COUNT(*)获得 private int totalPage; // 总页数由totalCount和pageSize计算得出 public PageBean(int currentPage, int pageSize) { this.currentPage currentPage; this.pageSize pageSize; } // 计算总页数总条数/每页条数有余数要加1 public int getTotalPage() { if (totalCount 0) { return 0; } return (totalCount pageSize - 1) / pageSize; } public int getStartIndex() { // LIMIT的起始位置 (当前页-1) * 每页条数 return (currentPage - 1) * pageSize; } }这个方法配合 DAO 层使用getTotalPage里那个(totalCount pageSize - 1)是整除上取整的标准写法比先除法再判断余数要简洁。分页查询 SQL 写成SELECT * FROM goods LIMIT ?, ?第一个问号传起始位置第二个传 pageSize两个参数在 PreparedStatement 中都要用setInt设置不能省。页面上翻页链接的做法一般是把 currentPage 作为请求参数拼在 URL 后面list.jsp?currentPage2在 JSP 接收参数时要处理边界情况。用户手动改 URL 把 currentPage 传成 -1 或者 999分页就会查出负数偏移量。最多网上常见的防护手段是判断 currentPage 小于 1 就强制设为 1大于 totalPage 就设为 totalPage。这个校验逻辑写在 PageBean 构造函数里最合适一劳永逸。4.2 购物车Session 作用域下的数量加减与总价计算购物车是这套系统里最有代表性的功能它不用单独建表而是用 Session 存一个 MapKEY 是商品 idVALUE 是购买数量。点击加入购物车时先判断 Session 里有没有这个 Map没有就新建有就数量加一。// 从Session获取购物车Map键为商品gid值为购买数量 // SuppressWarnings 抑制类型转换时IDE的黄色警告 SuppressWarnings(unchecked) public static void addCart(HttpServletRequest request, int gid) { HttpSession session request.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMapInteger, Integer(); session.setAttribute(cart, cart); } if (cart.containsKey(gid)) { cart.put(gid, cart.get(gid) 1); // 已存在则数量1 } else { cart.put(gid, 1); // 新商品默认数量1 } }这代码里最关键的一行是session.getAttribute(cart)的强制类型转换因为 Session 存的是 Object取出来要想用 Map 的方法就必须强转。强转之前一定要判空否则第一次访问时 Session 里没有 cart 属性取出来是 null直接 .get 方法就空指针。很多同学在这里翻车报错日志里全是NullPointerException却找不到原因就是因为没有判空直接调了方法。购物车总价的计算一般是在 JSP 页面里遍历这个 Map每取一个 gid 就查一次商品表拿单价乘上数量累加。这里有个性能小建议不要在循环里反复调用 DAO 查数据库可以先用SELECT * FROM goods WHERE gid IN (?,?,?)一次查出多个商品。课设项目数据量小可以不优化但这个思路说出来答辩时多少能加点印象分。4.3 订单提交与库存联动事务在哪一层控制购物车清空前的最后一步是提交订单。这个动作牵涉两张表的写操作插入订单主表和订单明细表同时扣减商品库存。任何一步失败数据库中都会出现只有订单没有明细的脏数据所以这里必须启用事务。public boolean createOrder(Order order, ListOrderDetail details) { Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 ps1 conn.prepareStatement(INSERT INTO orders(uid,total_price,create_time) VALUES(?,?,?)); ps1.setInt(1, order.getUid()); ps1.setDouble(2, order.getTotalPrice()); ps1.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps1.executeUpdate(); for (OrderDetail detail : details) { ps2 conn.prepareStatement(INSERT INTO order_detail(gid,goods_name,price,count) VALUES(?,?,?,?)); ps2.setInt(1, detail.getGid()); ps2.setString(2, detail.getGoodsName()); ps2.setDouble(3, detail.getPrice()); ps2.setInt(4, detail.getCount()); ps2.executeUpdate(); PreparedStatement ps3 conn.prepareStatement(UPDATE goods SET stock stock - ? WHERE gid ? AND stock ?); ps3.setInt(1, detail.getCount()); ps3.setInt(2, detail.getGid()); ps3.setInt(3, detail.getCount()); int rows ps3.executeUpdate(); if (rows 0) { throw new SQLException(库存不足或商品不存在); } } conn.commit(); // 全部成功提交事务 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtil.close(null, ps2, conn); } }这段代码里有两个细节值得注意。第一个是conn.setAutoCommit(false)在 JDBC 默认模式下每次 executeUpdate 都是一次独立提交第一个 insert 成功后第二个 insert 失败数据就残缺了。关闭自动提交后所有操作要么一起成功要么一起回滚这是事务的原子性。第二个细节是库存扣减 SQL 里带了AND stock ?条件这个条件保证库存不足时更新行数为 0通过rows 0让事务回滚。这样比先查再判断再更新的方式更安全高并发场景下不会超卖。这个功能做完这套课设的核心业务就算打通了用户登录可以下单管理员后台可以管理商品和订单。到这一步你再看源码包里的文档基本就能对照着理解每一段代码的意义。5. 部署避坑与常见问题跑起来之前先看这几条血泪经验5.1 Tomcat 启动后访问 404context path 与项目名不一致现象Tomcat 启动无报错浏览器输入http://localhost:8080/能进 Tomcat 欢迎页但访问http://localhost:8080/零食系统/就 404。原因Tomcat 默认把 webapps 目录下的文件夹名作为 context path 来访问。你部署的项目文件夹叫snack_web却按snack访问肯定找不到。IDE 里部署时 Artifact 名称又可能和原始目录名不一致进一步加剧这个问题。解决先确认部署后的实际目录名。Tomcat 方式下直接看 webapps 下文件夹名IDEA 方式下打开 Run/Debug Configurations看 Deployment 标签页里的 Application context 值把它改成与 Artifact 名一致或者统一改成你习惯的路径。改完重启 Tomcat再访问新路径就行。5.2 页面中文乱码request 与 response 两边编码要同时锁死现象登录注册后跳转到欢迎页页面上显示浣犲ソ这类乱码数据库里中文却是正常的或者数据库中文是乱码页面显示正常。原因编码在三个环节传递。JSP 页面本身的pageEncoding决定文件保存编码HTTP 响应时contentType里的 charset 决定浏览器解析编码JDBC 连接串里的characterEncoding决定读写数据库编码。任何一端不统一乱码就从那个环节开始出现。解决把三处统一成 UTF-8。JSP 头部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %JDBC 连接串保持characterEncodingutf-8数据库建表时用DEFAULT CHARSETutf8mb4。另外凡是接收中文参数的 JSP像搜索框、收货人姓名一定要在request.getParameter()之前调用request.setCharacterEncoding(UTF-8)放最后一行是不生效的因为参数已经被容器预先解析了。5.3 JDBC 驱动版本与 MYSQL 8.x 认证插件对不上现象启动项目后执行任意 DAO 方法报错Unable to load authentication plugin caching_sha2_password或者ClassNotFoundException: com.mysql.jdbc.Driver。原因MYSQL 8.0 默认认证插件是 caching_sha2_password老版本 mysql-connector-java 5.x 不认识这个插件反过来驱动类名也从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver代码没同步改就找不到类。解决两种做法任选。一种是把驱动 jar 换成 8.x 版本并修改 DBUtil 里的 DRIVER 变量为com.mysql.cj.jdbc.Driver连接串加serverTimezoneAsia/Shanghai。另一种是保持 5.x 驱动不变在 MYSQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;把认证插件降级。第一种方案更推荐毕竟 MYSQL 8.x 是主流。5.4 Tomcat 启动端口占用8080 被其他进程抢走现象控制台报Port 8080 required by Tomcat v9.0 Server at localhost is already in use或者 IDE 里启动 Tomcat 几秒后自动失败。原因上一个 Tomcat 实例没完全关闭或者本地其他程序占用了 8080 端口。常见的是之前部署过另一个项目没停掉直接断电或强制结束进程导致端口未释放。解决命令行执行netstat -ano | findstr 8080找到占用端口的 PID然后去任务管理器结束该进程。注意不要乱删系统进程先用tasklist确认这个 PID 对应的程序名。如果你需要经常切换项目可以在 Tomcat 的 server.xml 里把Connector port8080改成 8081 或 8090一劳永逸。5.5 部署后页面图片破损相对路径和虚拟目录映射问题现象零食列表页文字正常但所有商品图片显示为破图图标浏览器 F12 看到图片地址返回 404。原因这套系统里图片的上传路径和访问路径往往不是同一个目录。上传代码把文件存到了项目根目录下的upload文件夹但通过 URL 访问时Tomcat 默认并不把项目目录的 upload 直接映射为静态资源可访问路径。还有一种情况是代码里写了某台电脑的绝对路径比如D:/workspace/snack/upload换机器后路径不存在。解决先在项目的 WebRoot 下建一个 upload 目录多数情况图片放这里就能直接访问。如果上传路径是绝对的找到上传代码里保存文件的根路径常量改成相对路径或从配置文件读取。最省事的方式是给 Tomcat 加虚拟路径映射在 server.xml 的 Host 标签下配置Context path/upload docBaseD:/实际路径/upload /把物理路径暴露到 URL 上这样代码不用改。6. 验证与进阶给这套课设项目加一个订单统计的小功能项目跑通只是第一步答辩或交接时被问一句这套系统怎么验证功能正常很多人会愣住。验证不是打开页面点两下就说完成要有明确的数据依据。我一般会先用 SQL 直接查数据库验证业务逻辑产生的数据是否符合预期再针对性加一个小功能展示源码的可扩展性。先做数据验证。打开 MYSQL 命令行执行这几条查询# 验证用户表和订单的关联完整性 SELECT u.username, o.total_price, o.create_time FROM user u JOIN orders o ON u.uid o.uid ORDER BY o.create_time DESC LIMIT 10; # 验证库存扣减是否正确下单前后对比 SELECT g.goods_name, g.stock, IFNULL(SUM(od.count), 0) AS sold_count FROM goods g LEFT JOIN order_detail od ON g.gid od.gid GROUP BY g.gid;第一条语句如果返回空说明下单功能在写入订单时没有正确关联用户要检查下单代码里是否从登录 Session 中获取了 uid。第二条语句用 LEFT JOIN 关联明细表能直观看出哪些商品卖了多少、库存还剩多少。如果返回的 sold_count 和实际页面显示不一致说明事务没生效部分订单在扣库存中途失败了。验证通过后我习惯在这个项目上加一个简单的热销零食排行榜实现成本非常低但能体现你对这套 JSPJavaBean 结构的理解。核心思路是新增一个方法用 SQL 聚合查出销量前五的商品再在后台管理页面加一个展示表格。// 在OrderDetailDao中新增topSales方法 // 返回销量前5的商品名称和总销量 public ListString[] findTop5() { String sql SELECT od.goods_name, SUM(od.count) AS total_sold FROM order_detail od GROUP BY od.goods_name ORDER BY total_sold DESC LIMIT 5; return query(sql); }然后在后台 index.jsp 页面加一段 Java 代码调用这个 DAO 方法并循环输出表格。整个功能不需要写 Servlet不需要改数据库表结构只加一个方法加一段页面输出就行。做完后通过 SQL 语句再次查库存对比验证如果前后一致说明订单链路从页面到 DAO 到数据库全部健康。这套资源本身是一个完整的课设项目但它真正的价值是让你能读懂 JSP 项目的运行链路理解 JavaBean 如何组织业务逻辑以及 JDBC 在事务和并发场景下的边界在哪里。如果只想拿它交作业按照第二章和第三章的步骤走一遍就能跑如果想要在答辩时讲清楚为什么这么设计建议把第四章的事务代码自己手写一遍体会一下setAutoCommit前后的区别。从那以后我每次拿到 jspJavaBean 课设源码不管代码多简单都强制走一遍建库导数据、核对驱动、独立测事务、查库存关联这套流程确认每一步有数据返回再往页面看。希望这篇拆解能让你少走两步弯路把时间省下来真正理解这套老技术栈的底层逻辑。本文还有配套的精品资源点击获取