
简介基于 JavaWeb 的超市管理系统毕业设计项目含完整数据库与源码主要面向计算机、通信、人工智能、自动化等专业学生可用于课程设计、课程大作业或毕业设计参考也适合 Java Web 初学者从零跟进项目结构。系统覆盖用户登录、会员管理、供应商管理、商品管理与结账等典型业务模块对应代码分层清晰具备二次开发空间。资源包为 zip 格式共 386 个文件约 10.36MB其中以 Java 源文件、class 编译文件、JSP 页面和 SQL 数据库脚本为主辅以 js/css 前端样式、jar 依赖与 png 图片基本涵盖运行、查看与部署所需内容。全部代码已调试测试可直接运行便于对照学习 DAO、Servlet、JSP 之间的关系也可作为毕业设计说明书撰写的项目依据。目前已有 156 人学习下载。1. 这个 javaweb 超市管理系统到底解决什么问题每年答辩季总有几个同学在演示“超市管理系统”时翻车IDEA 里点了启动浏览器转半天最后落在白屏 404 上或者控制台直接报 Communications link failure。这个被反复选作毕业设计题目的 javaweb 超市管理系统本质上是一套用 Java Web 技术栈做出来的后台管理项目配套 MySQL 数据库脚本和完整源码覆盖登录、商品、进货、销售、库存这些超市日常经营场景。它之所以是课设和毕设的常青树是因为功能边界清楚、工作量刚好够一个学生独立完成又能把 Servlet、JDBC、SQL、数据库设计这些必须掌握的东西全部串起来。这篇文章按我实际做过这类项目的顺序把系统模块、数据库表结构、本地启动配置和常见坑一次讲清楚让你拿到任何一份类似的 javaweb 超市管理系统源码都能在半天内跑起来并讲明白。2. 功能模块与角色权限先拆业务再打开 IDEA 写代码拿到项目源码的第一件事不是点运行而是把业务模块拆出来。超市管理系统这类题目答辩老师第一个问题永远是“你这个系统有哪些功能”如果你连模块边界都说不清楚后面代码写得再漂亮也拿不到分。从项目实施的角度看模块拆分决定了数据库表怎么建、Servlet 怎么分、页面之间怎么跳这个顺序反了后面每一步都在返工。2.1 五个核心模块一张表理清无论源码是 JSP Servlet 还是 SSM 结构超市管理系统的功能都跑不出下面这张表。把这一页记清楚你就知道项目在做什么模块核心功能涉及的数据表答辩时可以提的亮点商品管理商品增删改查、分类、按名称模糊搜索product、category商品价格和库存数据联动进货入库录入进货单、自动更新库存stock_in、stock_in_item批量插入与事务控制销售收银选商品、计算金额、生成销售单sale、sale_item一次销售同时写两张表并扣库存库存管理库存列表、低于预警线提示product用一条 SQL 统计预警商品统计报表月度销售统计、商品销量排行sale、sale_item 的聚合查询视图与聚合函数的使用这五个模块不是并列关系而是串联关系进货会改变库存销售也会改变库存报表模块读的是销售单数据。所以做项目时我会先问自己一个问题一条数据从录入到被统计出来中间经过几张表把这条链路想清楚再去看源码里的包结构就不会迷路。2.2 角色权限管理员和收银员为什么必须分开大多数超市管理系统源码里的权限设计不复杂就是在用户表里加一个 role 字段1 表示收银员2 表示管理员。收银员登录后只能看到销售收银、商品查询和自己的密码修改管理员才有商品管理、进货入库、统计报表这些入口。常见做法是用一个 LoginFilter 做拦截根据 session 里存的用户角色判断能否访问某个路径。不要一上来就设计 RBAC 五张表用户、角色、菜单、权限、用户角色关联表全套搬过来。带课设和带项目不一样把权限模型做重了代码量是上去了但真正演示时老师只关心你能不能讲清楚“收银员为什么不能删商品”一个 role 字段加一个过滤器就能说明白。你甚至可以主动说一句“这里没有用到 Spring Security是因为这个小系统用过滤器更直观”这反而是加分项。2.3 数据流从进货到收银库存怎么联动理解这个系统最好的方式是跟着一条商品走一遍全流程管理员在进货页面选择供应商录入商品和数量提交进货单系统把进货单主表插入一行同时把所有商品明细插入明细表并让商品表的库存数量增加收银员在收银页面录入商品编号勾选数量系统计算总价提交订单系统把销售单写入销售主表明细写入销售明细表同时扣减商品表对应商品的库存库存低于预警线时库存页面通过一条条件查询把商品标红。这个流程里最核心的技术点是“一次操作要同时改多张表”。进货要改 stock_in 和 product销售要改 sale、sale_item 和 product。如果有一个环节只成功了一半比如主表插入了、明细没插入那么库存对不上账这就是后面要重点做事务的原因。3. 数据库设计六张表撑起整个系统的增删改查源码里最值钱的部分往往不是 Java 代码而是那份 SQL 脚本。很多人把项目跑不起来归结为代码问题实际上十有八九是 SQL 脚本没导入对或者导入后表结构不匹配。超市管理系统的数据库设计有固定套路想清楚这六张表增删改查的代码就是照着表结构填空。3.1 六张核心表怎么设计不踩雷我一般建议把数据库拆成七张表其中用户和分类是基础表商品是核心主表进货和销售各自带着一张明细表。这种主表加明细表的拆法是进销存系统的通用结构因为一张订单会包含多个商品一个商品也会出现在多张订单里多对多关系必须靠明细表承接。表名用途关键字段注意点sys_user登录账号id、username、password、role密码不要明文存答辩前至少用 MD5 处理category商品分类id、name一般由管理员直接维护product商品主表id、name、category_id、price、stock、warning_line金额用 decimal库存用 intstock_in进货单主表id、supplier、create_time、total记录一次进货整体信息stock_in_item进货单明细id、stock_in_id、product_id、quantity、price一个主表对应多行明细sale销售单主表id、sale_time、total收银台上产生的每一笔订单sale_item销售单明细id、sale_id、product_id、quantity、price同样是一对多关系设计上容易踩的坑有两个。一个是价格字段用 float浮点运算会产生 0.1 0.2 不等于 0.3 的问题对账时很难解释所以金额统一用 decimal(10,2)。另一个是库存字段不留预警线等到演示统计报表时你没法告诉老师哪些商品需要补货。3.2 建表 SQL 关键片段用户表和商品表拿到源码后先看建表脚本里的字段名再对照 Java 里的实体类字段能对上基本就不会有大问题。下面这段 SQL 是这类系统最常见的写法create database supermarket default character set utf8mb4; use supermarket; create table sys_user ( id int primary key auto_increment, username varchar(50) not null unique, password varchar(64) not null, role tinyint not null default 1 comment 1-收银员 2-管理员 ); create table product ( id int primary key auto_increment, name varchar(100) not null, category_id int not null, price decimal(10,2) not null, stock int not null default 0, warning_line int not null default 10, index idx_category (category_id) );这段 SQL 有几个值得注意的参数设计。id 用自增主键好处是插入时不需要手动生成编号username 加 unique 约束避免两个收银员账号重名role 用 tinyint 而不是 varchar查询和比较都更高效。category_id 上加普通索引是因为商品列表页经常按分类过滤有索引能让数据库在数据量变大后不卡。关于外键很多毕设源码建表时会写 foreign key但我建议你在自己的项目里不要加满约束。加了外键后删除商品时如果该商品已经被销售明细引用数据库会拒绝删除报错信息对答辩演示很不友好。常见做法是程序里先做关联检查数据库层不设外键把一致性交给 Service 层事务控制这样演示时逻辑更可控。3.3 让统计报表模块变成加分项视图与聚合查询报表模块是答辩时最容易出彩的地方但也是很多学生最难写出来的地方。如果你在 Java 代码里写循环遍历所有销售单再逐个累加金额数据量一大页面就卡老师一眼就能看出来实现方式不对。正确做法是让数据库把活干完下面这个视图在 MySQL 里可以直接执行create view v_month_sale as select date_format(sale_time, %Y-%m) as month, sum(total) as amount from sale group by date_format(sale_time, %Y-%m);创建视图后Java 端只需要执行select * from v_month_sale再把结果渲染到页面上就够了。同理销量排行用一条关联查询就能完成select p.name, sum(si.quantity) as sale_count from sale_item si join product p on si.product_id p.id group by p.name order by sale_count desc limit 10;这条 SQL 用了 join 把商品名和销售明细关联起来再用 group by 按商品分组order by 排序后取前 10 名。答辩时老师如果问“你这个排行功能怎么做的”你直接说“用 join 加聚合查询”比解释半天 Java 循环要有说服力得多。3.4 表结构改坏了怎么办直接执行修改结构的 ALTER导入 SQL 脚本后经常发现表字段和源码里的实体类对不上这是最常见的翻车点。比如源码里商品表有 sale_count 字段而脚本里没有。不要重新建库直接执行修改结构的语句alter table product add column sale_count int not null default 0; alter table product modify column price decimal(10,2) not null;第一条给 product 表加一个销量字段第二条把 price 字段类型改成 decimal。执行完在 IDEA 右侧的 Database 面板里右键表名点击 Refresh就能看到新结构。记住改表结构之前一定先备份用create table product_bak as select * from product这种语句留个后路否则手滑 drop 了表整个项目就白搭了。4. 核心功能实现登录拦截、商品增删改查与销售事务的代码写法模块和表结构都定下来之后代码的核心就落在三件事上入口拦登录、商品做增删改查、销售单开事务。这一章直接给代码片段和参数说明你对照着手上的源码看就能快速定位每个功能写在哪里。4.1 登录拦截一个 Filter 挡掉所有未登录请求如果没有登录拦截用户直接在浏览器地址栏输入某个 Servlet 路径就能绕过登录页这是严重的功能漏洞。用 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(loginServlet) || uri.contains(/static/)) { chain.doFilter(req, resp); return; } // 未登录用户统一踢回登录页 Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(login.jsp); return; } chain.doFilter(req, resp); } }逻辑很简单白名单路径直接放行其他路径检查 session 里有没有 user 对象没有就重定向到登录页。注意WebFilter(/*)里的路径写法必须匹配所有请求很多初学者写成WebFilter(/)结果过滤器完全没生效。如果你手上源码是 SSM 结构这个 Filter 对应的是 Spring MVC 的拦截器拦截路径配置在 xml 里实现思路一样。4.2 商品管理增删改查PreparedStatement 的标准写法商品模块是整套增删改查的代表把这里的写法看懂进货模块、分类模块都是同一套套路。核心是用 PreparedStatement 做预编译避免 SQL 注入同时让代码更好读。public int addProduct(Product p) throws SQLException { String sql insert into product(name, category_id, price, stock, warning_line) values(?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getName()); ps.setInt(2, p.getCategoryId()); ps.setBigDecimal(3, p.getPrice()); ps.setInt(4, p.getStock()); ps.setInt(5, p.getWarningLine()); return ps.executeUpdate(); } }这段代码的重点在 try 后面的括号里Connection 和 PreparedStatement 都实现了 AutoCloseabletry 块结束后会自动关闭资源不需要手动写 finally 去 close。参数占位符从 1 开始计数必须和 SQL 里问号的顺序一一对应写错一个就报参数索引超出范围这个问题在商品编辑功能里最常出现。如果你拿到的是 SSM 版本这段代码会被替换成 Mapper 接口里的Insert注解或是 XML 里的 insert 语句但 SQL 本身的写法完全一样。4.3 销售收银为什么必须把主表明细表和库存放进同一个事务假设收银员提交一笔销售单系统要做三件事往 sale 表插一行、往 sale_item 表插多行、对 product 表做库存扣减。这三件事必须全部成功或者全部失败否则就会出现收了钱但库存没扣的情况。用一段伪代码说明事务边界public void createSale(SaleDTO dto) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 关闭自动提交开启事务 // 第一步插入销售主表获取生成的主键 saleId insertSale(conn, dto); // 第二步循环插入明细表 for (SaleItem item : dto.getItems()) { insertSaleItem(conn, item); } // 第三步扣减库存逐条执行 update updateStock(conn, dto.getItems()); conn.commit(); // 全部成功提交 } catch (SQLException e) { conn.rollback(); // 任何一步失败整体回滚 throw e; } finally { DBUtil.close(conn); } }注意这里所有方法都接收 conn 这个参数这是事务生效的关键。很多源码出错就错在各个 Dao 内部自己调用DBUtil.getConnection()拿新连接结果 setAutoCommit(false) 只控制了自己的连接另一个连接里的操作根本不在同一个事务里。我一般会让 Service 层统一获取连接再把这个连接传给 Dao或者用 ThreadLocal 绑定当前线程的连接这样事务边界才真正起作用。4.4 JSP 页面到 Servlet 的请求流转搞清楚前端怎么把数据送到后端调试时会少走很多弯路。以商品添加功能为例商品列表页有个“新增商品”按钮跳转到 product_add.jsp 页面表单提交地址写的是actionproduct?actionaddmethod 为 post。Servlet 里的 doPost 方法先通过request.getParameter(name)拿到表单字段再封装成 Product 对象调用 Service 层。保存成功后使用重定向response.sendRedirect(productServlet?actionlist);这里的参数 action 是 Servlet 内部用来区分增删改查操作的标记整个项目通常只有一个商品 Servlet里面用 if/else 或者 switch 判断 action 的值来分发逻辑。重定向而不是 forward 的原因是防止用户按 F5 时把同一条商品重复插入数据库这一点在答辩演示时尤其重要。5. 避坑与排查IDEA 运行 javaweb 项目的五类常见问题这一章直接列我在实际跑这类项目时遇到过的问题每一条都是“现象、原因、解决”三个步骤写全你照着排查速度最快。5.1 启动期Tomcat 启动成功但页面 404现象IDEA 控制台显示 Tomcat 启动成功浏览器访问http://localhost:8080/却 404或者只能访问 login.jsp其他页面一律白屏。原因有两个第一个是 Tomcat 的 Deployment 配置里 Application context 不是项目根路径默认是/supermarket_war_exploded导致你访问的路径不对。第二个是 Project Structure 里的 Artifacts 没有把项目依赖的第三方 jar 包打进 WEB-INF/libTomcat 运行时加载不到 Servlet 类。解决打开 Run 菜单的 Edit Configurations找到 Deployment 标签页把 Application context 改成/然后重新启动。同时打开 File - Project Structure - Artifacts选中 WEB-INF 下的 lib点击加号把所有依赖加入确保发布包里能看到 mysql-connector 的 jar 文件。5.2 连接期Communications link failure 和 Access denied现象Tomcat 启动不报错但点登录触发数据库操作时控制台出现Communications link failure或Access denied for user rootlocalhost。原因前者通常是 MySQL 服务和项目没在同一台机器或者端口不是默认的 3306也有可能是 MySQL 8 以上版本要求 URL 里带上 serverTimezone 参数。后者就是 db.properties 里的账号密码和数据库实际的不一样或者 MySQL 用户没有远程访问权限。解决先在命令行执行mysql -u root -p确认登录凭据正确。然后检查 db.properties 里的连接串MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver连接地址里加serverTimezoneAsia/ShanghaiuseSSLfalse少了时区参数一定会报错。5.3 驱动期ClassNotFoundException: com.mysql.jdbc.Driver现象启动后第一次访问数据库时报ClassNotFoundException: com.mysql.jdbc.Driver或者干脆抛 SQLException 说找不到合适的驱动。原因项目里放的 mysql-connector jar 版本和代码里写的驱动类名不匹配。MySQL 5.x 用的驱动类是com.mysql.jdbc.DriverMySQL 8 的 jar 包要求改成com.mysql.cj.jdbc.Driver。解决如果你手头源码里的 db.properties 写的是老驱动类名而你的 MySQL 是 8 版本把驱动类名改掉如果用的是 MySQL 5.7则可以继续用旧写法。同时在 Artifacts 里确认 mysql-connector-java 的 jar 确实打包进去了这是 ClassNotFoundException 最常见的来源。5.4 中文乱码三层编码必须同时是 UTF-8现象页面能打开但从数据库查出来的中文全是问号或者新增商品时输入中文名称保存后变成乱码。原因乱码永远不是单点问题是链路里某一层编码不一致。JSP 页面没有设置 pageEncoding数据库连接 URL 缺少 characterEncoding 参数MySQL 建表时字符集不是 utf8mb4这三层任何一层不一致都会出乱码。解决JSP 顶部加上% page contentTypetext/html;charsetUTF-8 %db.properties 的连接地址里加上useUnicodetruecharacterEncodingutf8建表时指定默认字符集。改完重启 Tomcat清掉浏览器缓存再看不要只改一处就急着下结论。5.5 数据期销售单保存了但库存没有扣现象销售收银页面提示保存成功但商品列表里的库存数字纹丝不动重新启动系统后数据不完整。原因SaleDao 里插入销售单用了自己的连接SaleItemDao 和 ProductDao 又各自拿新连接事务根本不在同一个 Connection 上执行。setAutoCommit(false) 只控制了一个连接其他连接的操作照常自动提交相当于事务没有生效。解决按 4.3 的方式重构Service 层统一获取连接把 conn 传给所有 Dao 方法不要在每个 Dao 内部自己调用 DBUtil.getConnection()。如果源码已经写成了每层各拿连接改成传参这种方式最直接、最好讲也最容易被答辩老师认可。6. 答辩演示前的三个加固技巧与验证方法6.1 一条命令重置整库给自己留后手演示前最怕的是自己把数据库改乱了商品价格改成负数、库存对不上账。提前准备一个 reset.sql内容就是从建库、建表到初始化测试数据的完整脚本演示前执行下面这条命令一键恢复mysql -u root -p reset.sql这条命令会从文件读取 SQL 并逐句执行提醒你注意两点reset.sql 开头要写drop table if exists否则第二次执行会报表已存在文件路径里的斜杠用正斜杠Windows 下 cmd 和 PowerShell 都认。6.2 批量生成演示数据让每张表都有内容答辩时最怕页面空荡荡老师看不到效果。用一条 INSERT 造出几十条商品数据比在页面里一个个点添加快得多insert into product(name, category_id, price, stock, warning_line) values (农夫山泉550ml, 1, 2.00, 200, 50), (可口可乐330ml, 1, 2.50, 150, 50), (乐事薯片原味, 2, 6.50, 80, 20);库存尽量设置成有大有小演示库存管理时能同时展示正常和预警两种状态。商品名称要贴近真实场景答辩老师看一眼就知道你有认真准备。6.3 用 EXPLAIN 验证报表 SQL回答“系统卡不卡”老师可能会问“数据量大时查询会慢吗”你不需要背一堆优化理论现场验证更有说服力。在 Navicat 或 IDEA 的 Database 控制台执行explain select p.name, sum(si.quantity) from sale_item si join product p on si.product_id p.id group by p.name;看输出的 type 字段和 rows 字段type 达到 index 或 ref 级别rows 数量小就说明这条 SQL 走的是有效索引而不是全表扫描。提前在 sale_item 表的 product_id 上建好普通索引create index idx_sale_item_product on sale_item(product_id);这样演示时你可以直接指着执行计划说“这里走了索引所以不会全表扫”。我吃过一次亏答辩前一晚把库存字段改错第二天打开系统数据全乱凌晨三点在找备份。后来养成了习惯每次动数据库结构前先导出一份当前脚本留底顺手把 reset.sql 也更新一遍。希望你不用经历这种教训提前把这份惯用后手备好讲的清楚演示不慌。希望帮到你。本文还有配套的精品资源点击获取