ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于JavaWeb的超市管理系统:从分层架构到事务控制的完整实践

基于JavaWeb的超市管理系统:从分层架构到事务控制的完整实践 简介一份基于JavaWeb的超市管理系统毕业设计项目覆盖前台收银、商品管理、会员与供应商管理等核心模块适合计算机、软件工程等专业学生用于课程设计、大作业或毕业设计参考。项目为个人毕设答辩评分达98分代码均经过调试测试可直接运行完整程度较高。资源共386个文件压缩包约10.36MB。其中包含49个Java源文件与56个JSP页面对应业务逻辑与动态页面另有98个class编译文件、9个XML配置及SQL/MySQL数据库脚本可直接导入建表前端部分含CSS、SCSS、JavaScript及图标字体等资源jar包则用于项目依赖。整体目录结构清晰便于按模块阅读与二次开发。目前已有156人学习下载。通过该项目可掌握JavaWeb分层开发、Servlet/JSP交互、数据库设计等关键环节也可在现有功能上修改扩展适合小白入门与进阶提升。1. 基于javaweb的超市管理系统一个答辩98分项目能复用的边界毕设还剩半个月想找一套能直接运行的基于javaweb的超市管理系统源码又怕答辩现场讲不清数据库和调用关系这种情况每年都有。这套资源恰好踩在这个需求上传统 JavaWeb 技术栈JSP 做页面、Servlet 接请求、DAO 访问 MySQL业务覆盖商品、会员、供应商、收银结账和用户登录五个模块代码量不大却把分层、数据库设计、增删改查、会话控制这些得分点全占了。它适合课程设计和毕业设计时间紧的学生照着跑通也适合想快速拿一套源码二次开发的从业者。一句话这是一个能跑、能讲、能改的 javaweb 项目完整案例不是那种只能贴在论文里的死代码。2. 分层结构与数据库设计五个核心类对应的表和调用关系2.1 从包内类反推项目骨架下载包里的编译产物清单里出现频率最高的是这五个类ProductDaoImpl、VipDaoImpl、ProviderDaoImpl、UserDaoImpl以及 CheckoutServlet。注意项目正文里这些类名重复出现比如 ProductDaoImpl.class ProductDaoImpl.class 连着两次这是打包时编译输出被重复拷贝导致的属于打包遗留问题对源码运行没有任何影响最多算个“强迫症警告”。从命名就能看出分层习惯每个业务模块独立成 DAO控制器单独抽出来JSP 只负责展示。以这个资源最常见的组织方式来看代码结构会是这样UserDaoImpl 对应管理员登录和用户信息查询典型方法有 findByUsernameAndPassword、updatePassword登录页和密码修改页都走它ProductDaoImpl 对应商品表listProduct、addProduct、updateProduct、deleteProduct、findProductById 是一整套数据库增删改查VipDaoImpl 对应会员表addVip、findVipByPhone、updatePoints 是核心收银台要用会员手机号查折扣ProviderDaoImpl 对应供应商表listProvider、addProvider、updateProvider负责后台供应商维护CheckoutServlet 不是 DAO而是一个控制器它接收结账页传来的商品编号和数量调用上面几个 DAO 算价格、扣库存再把结果写回页面。把这些类串成一条线就是“页面提交请求 → Servlet 创建 DAO 实现类实例并调用方法 → 方法内部通过 JDBC 执行 SQL → 数据返回 JSP 渲染”。这样做的好处是每个类只干一件事答辩的时候讲“高内聚低耦合”非常有依据。如果你自己动手改成 Spring 版发现业务逻辑基本不用动改的只是对象的创建方式这也从侧面说明原来的分层没画歪。2.2 数据库四张核心表的结构根据上面五个类反推数据库至少要有四张表用户表、商品表、会员表、供应商表。建表 SQL 常见写法如下CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4; USE supermarket; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(50) NOT NULL COMMENT 密码, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ); CREATE TABLE t_provider ( id INT PRIMARY KEY AUTO_INCREMENT, provider_name VARCHAR(100) NOT NULL, contact VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, address VARCHAR(200) DEFAULT NULL ); CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, product_no VARCHAR(30) NOT NULL UNIQUE, product_name VARCHAR(100) NOT NULL, spec VARCHAR(50) DEFAULT NULL, unit VARCHAR(10) DEFAULT NULL, price DECIMAL(10,2) DEFAULT 0.00, stock INT DEFAULT 0, provider_id INT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_vip ( id INT PRIMARY KEY AUTO_INCREMENT, vip_no VARCHAR(30) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(20) DEFAULT NULL, points INT DEFAULT 0, discount DECIMAL(3,2) DEFAULT 1.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );参数选择上有几个点要在论文里写清楚。价格用 DECIMAL(10,2) 而不是 float/double浮点结算容易出现 0.1 乘 3 变成 0.30000000000000004 这类幺蛾子答辩时被问“为什么不用 double”答不上来会很难看库存字段 stock 用 INT最低值要配合事务判断库存是否充足discount 用 DECIMAL(3,2)1.00 表示原价0.90 表示九折这样收银台算钱时直接用乘法不用 switch 判断折扣等级。这里还有两个容易被忽略的细节。第一product 表的 provider_id 指向 t_provider.id但建议只做逻辑关联不强制建物理外键否则删除供应商或者改商品时很容易触碰外键约束报错演示环节翻车率高。第二表名不建议直接用 user 和 order这两个词在 MySQL 里和保留字有冲突风险写成 t_user、t_order 这类带前缀的名字最稳。如果你下载的包里面表名是其他名字不影响思路但连接配置要认真对一遍。2.3 从登录到结账的一次完整调用链看代码之前先把调用链理清后面排错会快很多。登录场景长这样login.jsp 表单提交到 LoginServletdoPost 方法里取出用户名和密码调用 userDaoImpl.findByUsernameAndPassword(username, password)返回非空就往 session 里放 user再重定向到 index.jsp返回空则回显“用户名或密码错误”。这里有个可以考虑的点密码比对放在 SQL 里做简单直观但登录日志里会带明文在 Java 里先查出用户再比对就更稳一点毕设用前者居多够用了。结账场景稍微复杂。CheckoutServlet 拿到商品 id、数量以及选填的会员手机号先查商品价格再查会员折扣计算出折后总价然后把库存扣掉最后把商品名、单价、数量、总价封装返回给 JSP 展示。扣库存的 SQL 常规写法是UPDATE t_product SET stock stock - ? WHERE id ? AND stock ?;stock ? 这个条件很关键它让数据库在库存不足时返回 0 行影响数而不是先扣成负数再想办法回滚。Servlet 通过更新行数判断是否成功等于 0 就直接提示“库存不足请修改数量”。这种方式不需要锁表在并发不高的超市场景下完全够用也比先 SELECT 再 UPDATE 少一次查询。数据库连接一般集中在工具类里比如 DBUtil 或 DBHelper静态块注册驱动getConnection() 从 db.properties 读取 url、username、password。这种写法的好处是页面和 DAO 都不直接碰连接串将来换数据库环境只改一个文件其他代码一行不用动。3. 从0到1跑起来环境选择、IDEA导入和数据库初始化的完整路径3.1 环境版本怎么选传统 JavaWeb 项目对环境版本很敏感不是越新越好。我建议按“JDK 1.8 Tomcat 8.5/9.0 MySQL 5.7 IDEA 2021 以上”的组合来配这是大量 javaweb 项目完整案例 mysql 组合验证下来最稳的一套。如果你的电脑只有 JDK 17也能运行但 Tomcat 要跟着换新版本并注意 Tomcat 10 的 Jakarta 命名空间问题。环境推荐版本说明JDK1.8兼容性最好网上绝大部分毕设源码按 8 编写Tomcat8.5 或 9.0必须避开 Tomcat 10原因见避坑章MySQL5.78.0 也可以但驱动要换 8.0.xIDEA2021.3 及以上社区版就能跑 Web 项目JDBC驱动mysql-connector-java 5.1.49配 MySQL 5.7 最稳妥先记住一句话凡是在代码里看见 import javax.servlet就直接用 Tomcat 9 以下别硬上 Tomcat 10后面会解释为什么。3.2 IDEA 导入项目并挂上 Tomcat拿到源码包后先解压确认里面有 src 目录、web/WEB-INF 目录、.sql 脚本和 lib 下的 jar 包。在 IDEA 里选择 File → New → Project from Existing Sources定位到项目根目录如果弹窗问 Import project from external model选 Eclipse 或直接选 Create project from existing sourcesIDEA 会尝试识别 Web 结构。如果识别成了普通 Java 项目右键模块名 → Add Framework Support → 勾选 Web Application让 IDEA 生成 web 目录关联。接着把 WEB-INF/lib 里的 jar 全部选中右键 Add as Library。这一步漏掉的后果是启动时报 NoClassDefFoundError而且日志位置很靠后不太好找。接下来是 IDEA 运行 javaweb 项目配置里最容易出错的一步。Run → Edit Configurations → 点加号 → Tomcat Server → Local。Server 标签页选到本机 Tomcat 安装目录Deployment 标签页点加号 → Artifact → 选 war explodedApplication context 习惯写 /supermarket浏览器访问路径就是这个。启动前双击 Artifact 名称在 Output Layout 里确认 WEB-INF/lib 下面能看到 mysql 驱动和 jstl 的 jar看不到就手动补进去。每个 Tomcat 配置只能对应一个 artifact如果电脑上之前跑过别的项目把 Deployment 里的多余项删掉只留当前项目否则一启动会同时部署两个应用端口和路径互相干扰。3.3 数据库初始化与连接配置打开 MySQL 命令行执行mysql -uroot -p123456 supermarket.sql如果提示 mysql 命令找不到先 cd 到 MySQL 安装目录的 bin 下再执行用 Navicat 或 DataGrip 手动建库导入也行。执行完用 SHOW TABLES; 确认四张表都在再随便 SELECT 一下 t_user看看初始管理员账号在不在。很多项目初始账号是 admin/admin123如果表里是空的那就手动 INSERT 一条。然后改数据库连接配置。项目里通常会有一个 db.properties 或 DBUtil.java 里的常量内容形如jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456把你本机 MySQL 的用户名密码填进去。url 里的三个参数建议原样保留characterEncodingutf8 管中文不乱码serverTimezone 不写MySQL 8 会报时间时区异常useSSLfalse 在驱动 5.1.49 下能避免那一大串 SSL 告警。如果本机 MySQL 是 8.0驱动类要换成 com.mysql.cj.jdbc.Driverjar 也要换成 8.0.x 版本。3.4 启动后的验证清单跑通之后不要急着交差按下面顺序点一遍每个环节都对应一个答辩提问点。打开 http://localhost:8080/supermarket/能看到登录页说明 web.xml 映射和欢迎页配置正常。用初始账号登录能进首页说明 session 写入和重定向没问题。商品管理页新增一件商品回列表页能看到说明 INSERT 和 SELECT 通了。会员页面添加一条会员记录手机号记下来收银台要用。到收银页面选商品、填数量、结账回商品列表确认库存已减少。这五步走完整个系统的数据库增删改查闭环就算跑通了。每一步失败都不要急着改代码先去 Tomcat 控制台看异常第一行优先级是 SQL 语法问题大于连接串问题大于编码问题。控制台日志是这个阶段唯一的黑匣子学会看堆栈比会背代码重要得多。4. 核心业务流程拆解登录校验、商品CRUD、会员折扣和收银库存的代码实现4.1 用户登录与权限控制UserDaoImpl 里最核心的方法一般长这样public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, real_name FROM t_user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(real_name)); } } catch (SQLException e) { e.printStackTrace(); } return null; }这里有一个典型的 JavaWeb 设计细节查询字段没有把 password 放进返回结果User 对象里不持有密码JSP 里就不会不小心把密码打印出来。PreparedStatement 全部用占位符不拼字符串是 JDBC 防注入最简单的写法也是论文里“安全设计”一节能写的内容。权限控制方面常见做法是写一个 Filter配置拦 /*对 login.jsp 放行其他请求判断 session 里有没有 user没有就重定向回登录页。这个小 Filter 非常推荐自己补上因为很多毕设源码为了演示方便直接省略了权限拦截答辩老师一眼就能看出来。4.2 商品增删改查与分页商品模块是纯正的数据库增删改查ProductDaoImpl 里五个方法对应列表页下方的按钮。分页是最值得讲的部分传统 JavaWeb 分页不是前端组件自动做而是 SQL 里 LIMIT offset, rows再配合一个 COUNT 查询计算总页数。public ListProduct listProductByPage(int pageNum, int pageSize) { String sql SELECT p.*, pr.provider_name FROM t_product p LEFT JOIN t_provider pr ON p.provider_id pr.id ORDER BY p.id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); ResultSet rs ps.executeQuery(); ListProduct list new ArrayList(); while (rs.next()) { list.add(mapProduct(rs)); } return list; } catch (SQLException e) { e.printStackTrace(); } return Collections.emptyList(); }手写分页的要点是 offset 从 0 开始所以页码减一ORDER BY id DESC 让新商品出现在第一页演示时观感好。另一个常见坑是 LEFT JOIN 左侧一定是主表 t_product写反了会导致供应商信息为空的行整个消失。列表页的编辑、删除按钮通常通过路径传参比如 delete?id12Servlet 里再调 ProductDaoImpl 的 deleteById。4.3 会员管理和折扣怎么参与收银VipDaoImpl 里的逻辑聚焦在三件事新增会员、按手机号查会员、更新积分。结账时的折扣计算集中在 CheckoutServlet常见写法是Vip vip vipDao.findByPhone(phone); BigDecimal total BigDecimal.ZERO; for (CartItem item : cartItems) { Product product productDao.findById(item.getProductId()); if (product.getStock() item.getQuantity()) { // 库存不足直接中断本次结账 return; } BigDecimal itemPrice product.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())); if (vip ! null) { itemPrice itemPrice.multiply(vip.getDiscount()); } total total.add(itemPrice); }用 BigDecimal 做金额运算是底线。如果下载的源码里是用 double 计算总价强烈建议全部改成 BigDecimal否则浮点误差会在账单明细里露馅。会员没匹配到时折扣默认 1.00价格不变这个分支要在注释里写清楚答辩老师几乎必问“普通用户怎么不享受折扣”。4.4 供应商模块ProviderDaoImpl 和商品模块结构类似区别在于供应商没有库存和价格核心字段是联系人、电话、地址。小型项目里常见的做法是商品列表页直接 JOIN 供应商名而不是再发一次查询数据量小、单机跑的时候完全没问题反而让新手更容易看懂。一个值得注意的边界是供应商被删除后历史商品列表里 provider_name 变成 null页面会显示空用 COALESCE 兜底SELECT p.*, COALESCE(pr.provider_name, 未知供应商) AS provider_name FROM t_product p LEFT JOIN t_provider pr ON p.provider_id pr.id4.5 收银结算与库存联动CheckoutServlet 是整个项目技术含量最高的类它要做四件事接收商品 id 和数量、计算应收金额、扣减库存、把结果传给 JSP。实用性上我会把它拆成 service 层来处理但毕设代码直接写在 Servlet 里也能讲清楚。关键问题在于扣库存和写销售记录两条 SQL 不能半成功半失败如果发现库存扣了但页面没跳出小票优先怀疑两条 SQL 没在同一个 Connection 里执行或者 catch 块里只打印堆栈没做回滚。这个问题的完整解法放在后一章。Servlet 参数来源用途productId收银台页面定位商品取价格和库存quantity用户输入扣库存和计算小计金额phone用户输入可为空查询会员折扣totalPrice服务端计算回显给 JSP 的结账结果这一章的内容对应论文里的“模块设计图”“数据库 E-R 图”“系统截图”三个部分。答辩 PPT 里贴这些截图时重点讲清一次请求的完整路径剩下的交给代码去解释。5. 避坑指南五个启动异常从现象到原因再到解决5.1 Tomcat 10 一启动就 ClassNotFoundException现象Tomcat 启动后 Web 应用部署失败控制台报 java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet或 java.lang.ClassNotFoundException: javax.servlet.Filter。原因Tomcat 10 起把原来的 javax.servlet 命名空间迁移到了 jakarta.servlet老项目的编译产物和 web.xml 头部还是 javax硬跑必挂。这不是代码问题是 Tomcat 和项目时代错位。解决直接换 Tomcat 8.5 或 9.0下载 zip 版解压后在 IDEA 的 Tomcat 配置里重新指定 home 目录重启即可一行代码都不用动。如果非要用 Tomcat 10需要把所有 import javax.servlet 改成 jakarta.servlet工作量不大但容易漏毕设阶段不建议折腾。5.2 明明密码对的却报 Access denied for user现象登录页一提交就 500控制台提示 java.sql.SQLException: Access denied for user rootlocalhost。原因db.properties 里的密码与本机 MySQL 不一致或者改完 db.properties 后没有重启 Tomcat。properties 文件通常在类加载时只读一次代码里没有热加载逻辑改了文件不重启不生效。解决先在命令行执行 mysql -uroot -p 验证本机密码再对比 db.properties 里的 jdbc.password确认后重启 Tomcat。如果安装 MySQL 时禁用了 root 的 localhost 登录连接串主机改成 127.0.0.1 通常更稳。5.3 页面上商品名全是问号现象登录正常英文显示正常中文文本显示为“????”或菱形乱码。原因三个环节至少断了一个——JSP 页面编码不是 UTF-8、数据库连接串没带 characterEncoding、Tomcat 接收参数时解码不对。解决按顺序检查。JSP 文件头写 % page contentTypetext/html;charsetUTF-8 %连接串带 characterEncodingutf8建库时用 utf8mb4。如果三者都在仍然乱码加一个字符集 FilterWebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }注意 Tomcat 8.5 之后 GET 请求编码默认已经是 UTF-8这个 Filter 主要防的是 POST 提交参数乱码要放在 web.xml 过滤器链的最前面。5.4 结账后商品库存没变或变成负数现象页面提示结算成功回到商品列表stock 数字没减少或者变成了负数。原因库存没减少通常是 CheckoutServlet 里执行了 UPDATE但连接设置了 setAutoCommit(false) 又没有调用 commit事务没提交数据在连接关闭时回滚了。变成负数则说明 UPDATE 语句里少了 stock ? 条件两个窗口同时结账把库存扣穿。解决先在数据库客户端直接查 t_product 的真实库存别只看页面缓存。确认没扣就是事务未提交确认扣成负数就加库存校验条件。更彻底的做法是把结账流程包进同一个 Connection示例代码在下一章。这个问题的排查经验和课本上“事务 ACID”的定义对上了答辩时可以顺手引用。5.5 Tomcat 端口占用现象启动时报 Address already in use: JVM_Bind :8080Tomcat 实例起不来。原因之前跑过其他项目或者本机其他服务占用了 8080 端口。解决Windows 下执行 netstat -ano | findstr :8080 找到占用进程的 PID在任务管理器里结束它或者直接在 IDEA 的 Tomcat 配置里把 HTTP port 改成 8081访问地址同步改。改端口比杀进程省事但如果端口是 80 等特殊端口要注意系统权限。这一章最后补一句遇到异常先读第一行读不懂再搜错误码不要凭感觉改代码。JavaWeb 排错百分之八十靠这条习惯。6. 进阶改造给 CheckoutServlet 换一个带事务的结账写法拿到这套 javaweb 的超市管理系统源码后大多数人会直接交作业但如果你想在答辩里多拿几分最值得改的就是 CheckoutServlet。原因很现实源码能跑通只说明程序没崩而“事务”“回滚”“并发”这三个词一出现答辩老师的兴趣立刻上来。改造目标是把查价格、扣库存、写记录推进同一个 Connection 里哪个环节失败都全部撤销。public class CheckoutService { public boolean checkout(ListCartItem items, String vipPhone) { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); VipDaoImpl vipDao new VipDaoImpl(conn); ProductDaoImpl productDao new ProductDaoImpl(conn); BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { Product p productDao.findByIdForUpdate(item.getProductId()); if (p null || p.getStock() item.getQuantity()) { return false; } total total.add(p.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); productDao.decreaseStock(item.getProductId(), item.getQuantity()); } if (vipPhone ! null !vipPhone.isEmpty()) { Vip vip vipDao.findByPhone(vipPhone); if (vip ! null) total total.multiply(vip.getDiscount()); } conn.commit(); return true; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } } }这里有两个关键习惯要说明。一个是 findByIdForUpdate 而不是 findById它会执行 SELECT ... FOR UPDATE把商品行临时锁住避免两个人同时结账时库存扣穿单机超市系统虽然遇不到高并发但写进论文是实打实的加分点。另一个是 DAO 构造函数里传入同一个 Connection而不是在每个 DAO 内部各拿新连接否则事务根本管不到另一个连接上的 SQL。改造完以后原先 Servlet 里几十行逻辑可以缩到两行CheckoutService service new CheckoutService(); boolean ok service.checkout(cart, phone);从那以后我每次拿到这种“能跑但没事务”的 JavaWeb 项目第一个动作永远是把结账、下单这类多步写操作拎出来单独加事务哪怕只是把两条 SQL 塞进同一个 Connection。项目可以简单但事务边界不能含糊这是我在一次库存对不上账的深夜排查里换来的血泪经验希望帮到你。本文还有配套的精品资源点击获取
返回列表